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(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
119 pub vmf_cfg: Option<VmfPhaseCfg>,
121 pub gdn_cfg: Option<GdnCfg>,
123 pub logit_multiplier: Option<f32>,
125 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
130 graph_failed: std::sync::atomic::AtomicBool,
135 pub kv_history: Vec<u32>,
140 pub kv_history_device: bool,
143 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
145 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
148 pub dsv4: Option<
152 Box<(
153 crate::dsv4::Dsv4Globals,
154 Vec<crate::dsv4::Dsv4Layer>,
155 crate::dsv4::Dsv4Cfg,
156 crate::dsv4::Dsv4State,
157 )>,
158 >,
159 pub dsv41: Option<
163 Box<(
164 crate::dsv41::Dsv41Globals,
165 Vec<crate::dsv41::Dsv41Layer>,
166 crate::dsv41::Dsv41Cfg,
167 crate::dsv41::Dsv41State,
168 )>,
169 >,
170 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
172 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
174 pub qwen4_exp: Option<
177 Box<(
178 crate::qwen4_exp::Globals,
179 Vec<crate::qwen4_exp::Layer>,
180 crate::qwen4_exp::Cfg,
181 crate::qwen4_exp::State,
182 )>,
183 >,
184 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
188 pub dspark: Option<crate::dsv4::DsparkState>,
190 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
193 pub dspark_hist: Vec<usize>,
195 pub dspark_real: Vec<u32>,
199 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
203 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
205 pub dspark_draft_ns: u128,
209 pub short_conv_cfg: Option<ShortConvCfg>,
212 pub mtp: Option<MtpModule>,
214 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
218 verify_exact_moe: bool,
221 pub speculative: bool,
223 pub ignore_eos: bool,
229 pub draft_full_streak: u32,
235 pub spec_k_adapt: Option<usize>,
243 pub spec_acc_ewma: f32,
245 rng: SplitMix64,
246 sampler_scratch: SamplerScratch,
247 spec_forced: Option<u32>,
253 spec_q: Vec<Vec<f32>>,
254 spec_p: Vec<f32>,
255 spec_res: Vec<f32>,
256 spec_qs: Vec<sampler::Sparse>,
258 spec_ps: sampler::Sparse,
259 spec_ress: sampler::Sparse,
260 mtp_graph_mode: Option<bool>,
267 #[cfg(target_os = "macos")]
270 metal_verify: Option<MetalVerifyPending>,
271 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
275 ws: ForwardScratch,
279 pool: Option<std::sync::Arc<Pool>>,
281 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
285 pub(crate) dyn_force_f32: bool,
287 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
292 pub(crate) dyn_active: Option<usize>,
298 pub(crate) dyn_blend_loaded: bool,
302 pub(crate) dyn_phi_layer: Option<usize>,
305 dyn_phi_ema: Vec<f32>,
307 dyn_phi_seen: usize,
308 pub dyn_router: Option<crate::swarm::DynRouter>,
311 o1_cfg: Option<crate::nystrom::O1Cfg>,
314 o1_epoch: u64,
317 o1_flags: Vec<bool>,
319 trace: bool,
322 calib_temp: f32,
325 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
327 graph_kv_id: u64,
328 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
331 graph_want_logits: bool,
332 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
336 graph_head_required: bool,
337 graph_logits: Option<Vec<f32>>,
340 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
343 graph_refused: std::sync::atomic::AtomicBool,
349 pub embed_multiplier: f32,
351 pub attn_scale: f32,
354 pub swa: Option<(usize, usize)>,
357 pub swa_trim: Option<(usize, usize)>,
364 pub sliding_layers: Option<Vec<bool>>,
367 pub no_rope_layers: Option<Vec<bool>>,
372 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
378 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
381 pub kv_prefix: KvPrefix,
385 pub last_prefill_tokens: usize,
388 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
391 pub rotary_dim_local: Option<usize>,
392 pub rope_scale: f32,
393 pub rope_scale_local: f32,
394 pub global_attn: Option<(usize, usize)>,
397 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
400 pub attn_v_norm: bool,
402 pub qk_norm_after_rope: bool,
404 pub proj_gate_sigmoid: bool,
407 pub final_softcap: Option<f32>,
409 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
413 pub attn_softcap: f32,
415 confidence_on: bool,
419 #[cfg(test)]
422 nll_test_fail_at: Option<usize>,
423 #[cfg(test)]
426 nll_test_force_serial: bool,
427}
428
429#[cfg(target_os = "macos")]
430impl Drop for Pipeline {
431 fn drop(&mut self) {
432 let _ = crate::gpu_metal::wait_replay();
434 crate::gpu::kv_mirror_drop(self.graph_kv_id);
435 }
436}
437
438#[cfg(not(target_os = "macos"))]
439impl Drop for Pipeline {
440 fn drop(&mut self) {
441 crate::gpu::graph_kv_reset(self.graph_kv_id);
445 }
446}
447
448pub struct PipelineWeights {
453 pub embed_tokens: QTensor,
455 pub layers: Vec<LayerWeights>,
457 pub lm_head: QTensor,
459 pub final_norm: Vec<f32>,
461}
462
463pub struct LayerWeights {
465 pub input_norm: Vec<f32>,
466 pub post_norm: Vec<f32>,
469 pub attn_out_norm: Option<Vec<f32>>,
472 pub layer_scale: Option<f32>,
474 pub ffn_out_norm: Option<Vec<f32>>,
477 pub ffn: FfnKind,
478 pub attn: AttnKind,
479}
480
481#[derive(Clone, Copy, PartialEq, Debug, Default)]
484pub enum Act {
485 #[default]
486 Silu,
487 GeluTanh,
488 Gelu,
490 Situ {
493 beta: f32,
494 linear_beta: f32,
495 },
496}
497
498impl Act {
499 pub fn from_arch(name: &str) -> Self {
500 if name == "gelu_tanh" {
501 Self::GeluTanh
502 } else if name == "gelu" {
503 Self::Gelu
504 } else {
505 Self::Silu
506 }
507 }
508
509 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
511 match arch.hidden_act.as_str() {
512 "situ" => Self::Situ {
513 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
514 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
515 },
516 other => Self::from_arch(other),
517 }
518 }
519
520 #[inline]
521 pub fn apply(self, x: f32) -> f32 {
522 match self {
523 Self::Silu => inference::silu(x),
524 Self::GeluTanh => inference::gelu_tanh(x),
525 Self::Gelu => inference::gelu_erf(x),
526 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
527 }
528 }
529
530 #[inline]
533 pub fn combine(self, g: f32, u: f32) -> f32 {
534 match self {
535 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
536 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
537 }
538 _ => self.apply(g) * u,
539 }
540 }
541
542 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
546 match self {
547 Self::Silu => Some(crate::gpu::GraphAct::Silu),
548 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
549 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
550 Self::Situ { .. } => None,
551 }
552 }
553}
554
555pub struct DenseFfn {
557 pub gate_proj: QTensor,
558 pub up_proj: QTensor,
559 pub down_proj: QTensor,
560 pub act: Act,
562 pub down_t: Option<QTensor>,
568 pub segs: Vec<FfnSeg>,
575}
576
577pub struct FfnSeg {
582 pub gate: QTensor,
583 pub up: QTensor,
584 pub down: QTensor,
585 pub start: usize,
586 pub width: usize,
587}
588
589pub enum FfnKind {
592 Dense(DenseFfn),
593 Moe(MoeFfn),
597 DenseMoe(Box<DenseMoeFfn>),
604}
605
606pub struct DenseMoeFfn {
608 pub dense: DenseFfn,
609 pub moe: MoeFfn,
610 pub post_norm_1: Vec<f32>,
612 pub pre_norm_2: Vec<f32>,
615 pub post_norm_2: Vec<f32>,
617}
618
619pub struct MoeFfn {
620 pub router: QTensor,
622 pub experts: Vec<DenseFfn>,
623 pub top_k: usize,
624 pub norm_topk_prob: bool,
625 pub router_sigmoid: bool,
628 pub sigmoid_denom_eps: f32,
633 pub expert_bias: Option<Vec<f32>>,
637 pub routed_scaling: f32,
640 pub route_tau: Option<f32>,
646 pub shared: Option<(DenseFfn, Option<QTensor>)>,
649 pub stats: std::cell::RefCell<Vec<u64>>,
653 pub act_sq: std::cell::RefCell<Vec<f64>>,
660 pub act_rows: std::cell::RefCell<Vec<f32>>,
666 pub mask: Option<Vec<bool>>,
671 pub per_expert_scale: Option<Vec<f32>>,
674 pub router_input_norm: bool,
678 pub resonance: Option<Resonance>,
682 pub grown: Vec<GrownExpert>,
688}
689
690#[derive(Debug, Clone, PartialEq, Eq)]
692pub struct GrownExpert {
693 pub record: String,
695 pub record_index: usize,
697 pub layer: usize,
698 pub expert: usize,
702}
703
704pub struct Resonance {
706 pub mu: Vec<f32>,
708 pub u: Vec<f32>,
710 pub k: usize,
711 pub bias: Vec<f32>,
713 pub shell: Vec<f32>,
719}
720
721static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
724
725pub fn growth_shell_enabled() -> bool {
730 use std::sync::atomic::Ordering;
731 match GROWTH_SHELL.load(Ordering::Relaxed) {
732 1 => true,
733 2 => false,
734 _ => {
735 let off = std::env::var("CMF_GROWTH_SHELL")
736 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
737 .unwrap_or(false);
738 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
739 !off
740 }
741 }
742}
743
744pub fn set_growth_shell(on: Option<bool>) {
749 GROWTH_SHELL.store(
750 match on {
751 Some(true) => 1,
752 Some(false) => 2,
753 None => 0,
754 },
755 std::sync::atomic::Ordering::Relaxed,
756 );
757}
758
759impl Resonance {
760 pub fn has_shell(&self) -> bool {
762 self.shell.iter().any(|s| s.is_finite())
763 }
764
765 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
768 let mut out = vec![f32::INFINITY; ne];
769 if growth_shell_enabled() {
770 for (o, s) in out.iter_mut().zip(&self.shell) {
771 *o = *s;
772 }
773 }
774 out
775 }
776
777 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
782 let h = x.len();
783 let ne = out.len();
784 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
785 for e in 0..ne {
786 let mu = &self.mu[e * h..(e + 1) * h];
787 let mut d2 = 0.0f32;
788 for j in 0..h {
789 let d = x[j] - mu[j];
790 d2 += d * d;
791 }
792 let mut proj = 0.0f32;
793 for i in 0..self.k {
794 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
795 let mut p = 0.0f32;
796 for j in 0..h {
797 p += (x[j] - mu[j]) * u[j];
798 }
799 proj += p * p;
800 }
801 let err = d2 - proj;
802 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
803 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
804 out[e] = f32::NEG_INFINITY;
805 }
806 }
807 }
808}
809
810pub enum AttnKind {
813 Full {
815 wq: QTensor,
816 wk: QTensor,
817 wv: QTensor,
818 wo: QTensor,
819 q_norm: Option<Vec<f32>>,
820 k_norm: Option<Vec<f32>>,
821 output_gate: bool,
822 softplus_gate: Option<(QTensor, bool)>,
826 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
828 },
829 Linear(VmfPhaseWeights),
831 LinearGdn(GdnWeights),
833 ShortConv(ShortConvWeights),
836 Mla(Box<MlaWeights>),
844 Kda(Box<crate::linear_core::KdaWeights>),
848 Bounded(Box<crate::bounded::BoundedWeights>),
853}
854
855pub struct MlaWeights {
857 pub q_proj: QTensor,
861 pub q_a: Option<QTensor>,
864 pub q_a_norm: Option<Vec<f32>>,
865 pub kv_a: QTensor,
867 pub kv_a_norm: Vec<f32>,
869 pub kv_b: QTensor,
871 pub o_proj: QTensor,
873 pub nh: usize,
874 pub qk_rope: usize,
875 pub qk_nope: usize,
876 pub v_dim: usize,
877 pub lora: usize,
878 pub scale: f32,
880 pub nope: bool,
882}
883
884pub struct MtpModule {
889 pub enorm: Vec<f32>,
890 pub hnorm: Vec<f32>,
891 pub eh_proj: QTensor,
893 pub layer: LayerWeights,
894 pub final_norm: Vec<f32>,
895 pub kv: crate::kv_cache::LayerKvCache,
896}
897
898#[cfg(target_os = "macos")]
905enum MetalRowsItem<'a> {
906 Gdn {
907 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
908 first: usize,
909 },
910 Attn {
911 l: crate::gpu_metal::AttnGpuLayer<'a>,
912 li: usize,
913 q_norm: Option<&'a [f32]>,
914 k_norm: Option<&'a [f32]>,
915 output_gate: bool,
916 },
917}
918
919#[cfg(target_os = "macos")]
920struct MetalVerifyPending {
921 graph: crate::gpu_metal::VerifyGraph,
922 gdn_layers: Vec<usize>,
923 attn_layers: Vec<(usize, usize)>,
924}
925
926#[cfg(target_os = "macos")]
930struct MetalWarmPending {
931 graph: crate::gpu_metal::VerifyGraph,
932 cpu_stored: usize,
933 b: usize,
934}
935
936#[cfg(target_os = "macos")]
937enum MetalRowsRun {
938 Declined,
940 Failed,
943 Completed(MetalVerifyPending),
944}
945
946#[cfg(target_os = "macos")]
947enum MetalPrefillOutcome {
948 Declined,
949 Failed,
950 Completed(Vec<f32>),
951}
952
953#[cfg(target_os = "macos")]
954enum MetalBatchNllOutcome {
955 Declined,
956 Failed(String),
957 Completed(f64, usize),
958}
959
960#[derive(Clone, Copy)]
964enum SpecTrial {
965 Spec {
966 t0: std::time::Instant,
967 gen0: usize,
968 rounds: usize,
969 },
970 Plain {
971 t0: std::time::Instant,
972 gen0: usize,
973 },
974 Decided {
975 spec: bool,
976 recheck_at: usize,
977 },
978}
979
980pub(crate) fn spec_time_level() -> u8 {
984 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
985 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
986 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
987 Err(_) => 0,
988 })
989}
990
991struct SpecStampLog {
997 t_last: std::time::Instant,
998 items: Vec<(&'static str, f32)>,
999}
1000
1001static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1002
1003pub(crate) fn spec_stamp(name: &'static str) {
1004 if spec_time_level() == 0 {
1005 return;
1006 }
1007 if let Ok(mut g) = SPEC_STAMPS.lock() {
1008 if let Some(log) = g.as_mut() {
1009 let now = std::time::Instant::now();
1010 log.items
1011 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1012 log.t_last = now;
1013 }
1014 }
1015}
1016
1017fn spec_stamps_begin() {
1018 if spec_time_level() == 0 {
1019 return;
1020 }
1021 if let Ok(mut g) = SPEC_STAMPS.lock() {
1022 *g = Some(SpecStampLog {
1023 t_last: std::time::Instant::now(),
1024 items: Vec::with_capacity(64),
1025 });
1026 }
1027}
1028
1029fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1030 SPEC_STAMPS
1031 .lock()
1032 .ok()
1033 .and_then(|mut g| g.take())
1034 .map(|l| l.items)
1035 .unwrap_or_default()
1036}
1037
1038fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1041 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1042 for &(n, ms) in items {
1043 match agg.iter_mut().find(|e| e.0 == n) {
1044 Some(e) => {
1045 e.1 += ms;
1046 e.2 += 1;
1047 }
1048 None => agg.push((n, ms, 1)),
1049 }
1050 }
1051 let mut s = String::with_capacity(agg.len() * 16);
1052 for (n, ms, k) in agg {
1053 if k > 1 {
1054 s.push_str(&format!("{n} {ms:.1}/{k} "));
1055 } else {
1056 s.push_str(&format!("{n} {ms:.1} "));
1057 }
1058 }
1059 s
1060}
1061
1062#[derive(Default, Clone, Copy)]
1084struct SpecMon {
1085 round_ms: f64,
1086 tokens: f64,
1087 plain_ms: f64,
1088 n: u32,
1089 fails: u32,
1090 metal: bool,
1091}
1092
1093const SPEC_PROXY_TOKENS: f64 = 3.5;
1096const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1099
1100impl SpecMon {
1101 fn round(&mut self, dt_ms: f64, produced: usize) {
1102 self.n += 1;
1103 if self.n == 1 {
1104 return; }
1106 let a = if self.n == 2 { 1.0 } else { 0.3 };
1107 self.round_ms += a * (dt_ms - self.round_ms);
1108 self.tokens += a * (produced as f64 - self.tokens);
1109 }
1110 fn pays(&self) -> bool {
1111 if self.plain_ms > 0.0 {
1112 self.tokens * self.plain_ms > self.round_ms * 1.03
1113 } else {
1114 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1115 }
1116 }
1117 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1119 let n = generated.saturating_sub(gen0);
1120 if n >= 8 {
1121 return true;
1122 }
1123 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1124 }
1125}
1126
1127pub const KV_PREFIX_TAIL: usize = 128;
1130
1131#[derive(Debug, Clone, Default)]
1137pub struct KvPrefix {
1138 len: usize,
1139 hash: u64,
1140 tail: Vec<u32>,
1141 device: bool,
1146}
1147
1148impl KvPrefix {
1149 #[inline]
1150 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1151 for &id in ids {
1152 h ^= id as u64;
1153 h = h.wrapping_mul(0x100000001b3);
1154 h ^= h >> 29;
1155 }
1156 h
1157 }
1158
1159 pub fn clear(&mut self) {
1160 self.len = 0;
1161 self.hash = 0xcbf29ce484222325;
1162 self.tail.clear();
1163 self.device = false;
1164 }
1165
1166 pub fn on_device(&self) -> bool {
1168 self.device
1169 }
1170
1171 pub fn set_on_device(&mut self, device: bool) {
1173 self.device = device;
1174 }
1175
1176 pub fn len(&self) -> usize {
1178 self.len
1179 }
1180
1181 pub fn is_empty(&self) -> bool {
1182 self.len == 0
1183 }
1184
1185 pub fn tail_len(&self) -> usize {
1187 self.tail.len()
1188 }
1189
1190 pub fn set(&mut self, ids: &[u32]) {
1192 self.clear();
1193 self.extend(ids);
1194 }
1195
1196 pub fn extend(&mut self, more: &[u32]) {
1198 if self.len == 0 && self.hash == 0 {
1199 self.hash = 0xcbf29ce484222325;
1200 }
1201 self.hash = Self::fold(self.hash, more);
1202 self.len += more.len();
1203 if more.len() >= KV_PREFIX_TAIL {
1204 self.tail.clear();
1205 self.tail
1206 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1207 } else {
1208 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1209 self.tail.drain(..drop);
1210 self.tail.extend_from_slice(more);
1211 }
1212 }
1213
1214 pub fn extension(&self, ids: &[u32]) -> usize {
1218 if self.len == 0 || ids.len() <= self.len {
1219 return 0;
1220 }
1221 let t = self.tail.len();
1222 if ids[self.len - t..self.len] != self.tail[..] {
1223 return 0;
1224 }
1225 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1226 return 0;
1227 }
1228 self.len
1229 }
1230}
1231
1232pub struct GenerateResult {
1234 pub text: String,
1235 pub token_ids: Vec<u32>,
1236 pub prompt_tokens: usize,
1237 pub tokens_generated: usize,
1238 pub finish_reason: String,
1239 pub mtp_drafted: usize,
1241 pub mtp_accepted: usize,
1242 pub token_confidence: Vec<f32>,
1247 pub traces: Vec<TokenTrace>,
1250}
1251
1252#[derive(Clone, Debug)]
1257pub struct TokenTrace {
1258 pub t: usize,
1260 pub token_id: u32,
1262 pub confidence: f32,
1264 pub active_skill: Option<String>,
1266 pub recon: Option<f32>,
1270 pub switched: bool,
1273}
1274
1275#[cfg_attr(not(test), allow(dead_code))]
1280fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1281 let t = if temp > 1e-3 { temp } else { 1.0 };
1282 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1283 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1284 if sum > 0.0 {
1285 (((logits[id as usize] - max) / t).exp()) / sum
1286 } else {
1287 0.0
1288 }
1289}
1290
1291fn prefill_batched() -> bool {
1294 std::env::var("CMF_PREFILL")
1295 .map(|v| v != "seq")
1296 .unwrap_or(true)
1297}
1298
1299#[inline]
1303fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1304 let graph_quality = unmasked && prefer_graph;
1305 let fused_head_quality = graph_quality && native_metal;
1306 (graph_quality, fused_head_quality)
1307}
1308
1309#[derive(Clone, Copy)]
1313enum PrefillIn<'a> {
1314 Ids(&'a [u32]),
1315 Hidden(&'a [f32]),
1316}
1317
1318impl Pipeline {
1325 fn can_prefill_batched(&self) -> bool {
1326 #[cfg(test)]
1327 let force_serial = self.nll_test_force_serial;
1328 #[cfg(not(test))]
1329 let force_serial = false;
1330 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1331 }
1332
1333 fn automatic_gpu_prefix(&self) -> Option<usize> {
1336 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1337 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1338 }
1339
1340 pub fn prefill_chunk(&self) -> usize {
1344 let env = env_prefill_chunk();
1345 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1346 return prefill_chunk_rule(env, ChunkHost::here(), false);
1347 }
1348 prefill_chunk_rule(
1349 None,
1350 ChunkHost::Other,
1351 self.chunk_stack_facts().dense_on_discrete(),
1352 )
1353 }
1354
1355 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1356 let plain_dense = !self.weights.layers.is_empty()
1357 && self.g3n.is_none()
1358 && self.dsv4.is_none()
1359 && self.dsv41.is_none()
1360 && self.qwen4_exp.is_none()
1361 && self.weights.layers.iter().all(|lw| {
1362 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1363 });
1364 let gpu_on = crate::gpu::enabled();
1365 ChunkStackFacts {
1366 plain_dense,
1367 discrete: gpu_on && crate::gpu::discrete(),
1368 gpu_on,
1369 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1372 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1373 multi_gpu: self.gpu_plan.is_some(),
1374 o1: self.o1_active(),
1375 }
1376 }
1377}
1378
1379pub fn prefill_chunk() -> usize {
1388 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1389}
1390
1391fn env_prefill_chunk() -> Option<usize> {
1392 std::env::var("CMF_PREFILL_CHUNK")
1393 .ok()
1394 .and_then(|v| v.parse::<usize>().ok())
1395}
1396
1397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1399enum ChunkHost {
1400 Macos,
1401 Aarch64,
1403 Other,
1405}
1406
1407impl ChunkHost {
1408 fn here() -> Self {
1409 if cfg!(target_os = "macos") {
1410 ChunkHost::Macos
1411 } else if cfg!(target_arch = "aarch64") {
1412 ChunkHost::Aarch64
1413 } else {
1414 ChunkHost::Other
1415 }
1416 }
1417}
1418
1419const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1426
1427fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1433 if let Some(n) = env {
1434 return n.max(1);
1435 }
1436 match host {
1437 ChunkHost::Macos => 512,
1438 ChunkHost::Aarch64 => 256,
1441 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1442 ChunkHost::Other => 48,
1443 }
1444}
1445
1446#[derive(Clone, Copy, Debug, Default)]
1448struct ChunkStackFacts {
1449 plain_dense: bool,
1452 discrete: bool,
1454 gpu_on: bool,
1456 capacity_split: bool,
1458 multi_gpu: bool,
1460 o1: bool,
1462}
1463
1464impl ChunkStackFacts {
1465 fn dense_on_discrete(self) -> bool {
1466 self.plain_dense
1467 && self.discrete
1468 && self.gpu_on
1469 && !self.capacity_split
1470 && !self.multi_gpu
1471 && !self.o1
1472 }
1473}
1474
1475#[inline]
1481fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1482 if end <= start || start >= input_len {
1483 return 0;
1484 }
1485 let rows = (end.min(input_len) - start).min(input_len - start);
1486 if end < input_len {
1487 rows
1488 } else {
1489 rows.saturating_sub(1)
1490 }
1491}
1492
1493pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1498pub(crate) struct ReuseLayer {
1499 pub full: bool,
1502 pub host_rows: usize,
1505 pub device_rows: Option<usize>,
1507 pub device_state: bool,
1509}
1510
1511#[derive(Debug, Clone, PartialEq, Eq)]
1513pub(crate) enum ReusePlan {
1514 Ready,
1516 Pull(Vec<(usize, usize, usize)>),
1519 Fresh,
1521}
1522
1523pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1533 let mut pulls = Vec::new();
1534 for (li, l) in layers.iter().enumerate() {
1535 if !l.full {
1536 if l.device_state {
1537 return ReusePlan::Fresh;
1538 }
1539 continue;
1540 }
1541 if l.host_rows == reuse_from {
1542 continue;
1543 }
1544 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1545 pulls.push((li, l.host_rows, reuse_from));
1546 continue;
1547 }
1548 return ReusePlan::Fresh;
1549 }
1550 if pulls.is_empty() {
1551 ReusePlan::Ready
1552 } else {
1553 ReusePlan::Pull(pulls)
1554 }
1555}
1556
1557impl Pipeline {
1558 fn clear_sequence_state(&mut self) {
1566 #[cfg(target_os = "macos")]
1569 let _ = crate::gpu_metal::wait_replay();
1570 self.kv_cache.clear();
1571 self.clear_history();
1574 self.graph_logits = None;
1575 if let Some(b) = &mut self.dsv41 {
1576 b.3.clear();
1577 }
1578 crate::gpu::graph_kv_reset(self.graph_kv_id);
1579 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1584 }
1585
1586 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1593 if self.graph_prefill_preferred() {
1594 return true;
1595 }
1596 let kv_id = self.graph_kv_id;
1597 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1598 .map(|li| {
1599 let full = matches!(
1600 self.weights.layers[self.phys_layer(li)].attn,
1601 AttnKind::Full { .. }
1602 );
1603 ReuseLayer {
1604 full,
1605 host_rows: self.kv_cache.layers[li].pos_len(),
1608 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1609 device_state: crate::gpu::graph_state_resident(kv_id, li),
1610 }
1611 })
1612 .collect();
1613 if layers
1617 .iter()
1618 .all(|l| l.device_rows.is_none() && !l.device_state)
1619 {
1620 return true;
1621 }
1622 let plan = kv_reuse_plan(reuse_from, &layers);
1623 let (what, rows, n) = match &plan {
1624 ReusePlan::Ready => ("host ready", 0, 0),
1625 ReusePlan::Fresh => ("fresh", 0, 0),
1626 ReusePlan::Pull(p) => (
1627 "pull",
1628 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1629 p.len(),
1630 ),
1631 };
1632 let t0 = std::time::Instant::now();
1633 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1634 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1635 eprintln!(
1636 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1637 if ok { "" } else { " (failed → fresh)" },
1638 t0.elapsed().as_secs_f64() * 1e3
1639 );
1640 }
1641 ok
1642 }
1643
1644 fn apply_kv_reuse_plan(
1645 &mut self,
1646 reuse_from: usize,
1647 plan: ReusePlan,
1648 layers: &[ReuseLayer],
1649 ) -> bool {
1650 let kv_id = self.graph_kv_id;
1651 match plan {
1652 ReusePlan::Fresh => return false,
1653 ReusePlan::Ready => {}
1654 ReusePlan::Pull(pulls) => {
1655 let (nkv, hd) = {
1660 let c = &self.kv_cache.layers[pulls[0].0];
1661 (c.num_kv_heads, c.head_dim)
1662 };
1663 let uniform = pulls.iter().all(|&(li, _, _)| {
1664 let c = &self.kv_cache.layers[li];
1665 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1666 });
1667 let batched = if uniform {
1668 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1669 } else {
1670 None
1671 };
1672 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1673 Some(rows) => rows,
1674 None => {
1675 let mut rows = Vec::with_capacity(pulls.len());
1676 for &(li, from, to) in &pulls {
1677 let (lnkv, lhd) = {
1678 let c = &self.kv_cache.layers[li];
1679 (c.num_kv_heads, c.head_dim)
1680 };
1681 let Some((k, v, first_valid)) =
1682 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1683 else {
1684 return false;
1685 };
1686 let need_from = match self.layer_window(li) {
1690 Some(w) => from.max((to + 1).saturating_sub(w)),
1691 None => from,
1692 };
1693 if first_valid > need_from {
1694 return false;
1695 }
1696 rows.push((k, v));
1697 }
1698 rows
1699 }
1700 };
1701 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1702 let cache = &mut self.kv_cache.layers[li];
1703 let row = cache.num_kv_heads * cache.head_dim;
1704 for p in 0..to - from {
1705 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1706 }
1707 if cache.pos_len() != to {
1708 return false;
1709 }
1710 }
1711 }
1712 }
1713 for (li, l) in layers.iter().enumerate() {
1717 if l.full
1718 && l.device_rows.is_some_and(|d| d > reuse_from)
1719 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1720 {
1721 return false;
1722 }
1723 }
1724 true
1725 }
1726
1727 fn finish_generation(
1733 &mut self,
1734 mtp: &mut Option<MtpModule>,
1735 router: &mut Option<crate::swarm::DynRouter>,
1736 clear_sequence: bool,
1737 ) {
1738 if router.is_some() {
1742 let _ = self.set_active_skill(None);
1743 }
1744 #[cfg(target_os = "macos")]
1751 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1752 if clear_sequence {
1753 self.clear_sequence_state();
1754 if let Some(m) = mtp.as_mut() {
1755 m.kv.clear();
1761 }
1762 if let Some(m) = self.mtp.as_mut() {
1763 m.kv.clear();
1767 }
1768 }
1769 self.graph_want_logits = false;
1770 self.graph_head_required = false;
1771 self.graph_logits = None;
1772 self.graph_failed
1773 .store(false, std::sync::atomic::Ordering::Relaxed);
1774 self.cancel
1775 .store(false, std::sync::atomic::Ordering::Relaxed);
1776 self.dyn_router = router.take().or(self.dyn_router.take());
1777 self.mtp = mtp.take().or(self.mtp.take());
1778 self.mtp_graph_mode = None;
1779 self.spec_forced = None;
1780 }
1781
1782 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1786 if self
1787 .graph_failed
1788 .swap(false, std::sync::atomic::Ordering::Relaxed)
1789 {
1790 self.cancel
1791 .store(false, std::sync::atomic::Ordering::Relaxed);
1792 self.clear_sequence_state();
1793 self.graph_logits = None;
1794 self.graph_want_logits = false;
1795 self.graph_head_required = false;
1796 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1797 }
1798 Ok(())
1799 }
1800
1801 #[cfg(target_os = "macos")]
1802 fn fail_metal_graph(&mut self, reason: &str) {
1803 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1804 self.clear_sequence_state();
1805 self.graph_logits = None;
1806 self.graph_failed
1807 .store(true, std::sync::atomic::Ordering::Relaxed);
1808 self.cancel
1809 .store(true, std::sync::atomic::Ordering::Relaxed);
1810 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1811 }
1812
1813 fn nll_begin(&mut self) -> Result<(), String> {
1818 if self
1819 .graph_failed
1820 .swap(false, std::sync::atomic::Ordering::Relaxed)
1821 {
1822 self.cancel
1823 .store(false, std::sync::atomic::Ordering::Relaxed);
1824 self.clear_sequence_state();
1825 self.graph_logits = None;
1826 self.graph_want_logits = false;
1827 self.graph_head_required = false;
1828 return Err("GPU graph failed before NLL scoring".to_string());
1829 }
1830 self.clear_sequence_state();
1831 self.graph_logits = None;
1832 self.graph_want_logits = false;
1833 self.graph_head_required = false;
1834 Ok(())
1835 }
1836
1837 fn nll_end(&mut self) {
1841 self.clear_sequence_state();
1842 self.graph_logits = None;
1843 self.graph_want_logits = false;
1844 self.graph_head_required = false;
1845 self.graph_failed
1846 .store(false, std::sync::atomic::Ordering::Relaxed);
1847 }
1848
1849 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1852 #[cfg(test)]
1853 if self.nll_test_fail_at == Some(pos) {
1854 self.nll_test_fail_at = None;
1855 self.graph_failed
1856 .store(true, std::sync::atomic::Ordering::Relaxed);
1857 self.cancel
1858 .store(true, std::sync::atomic::Ordering::Relaxed);
1859 }
1860 if self
1861 .graph_failed
1862 .swap(false, std::sync::atomic::Ordering::Relaxed)
1863 {
1864 self.cancel
1865 .store(false, std::sync::atomic::Ordering::Relaxed);
1866 self.clear_sequence_state();
1867 self.graph_logits = None;
1868 self.graph_want_logits = false;
1869 return Err(format!(
1870 "GPU graph failed during NLL {phase} at position {pos}"
1871 ));
1872 }
1873 Ok(())
1874 }
1875
1876 #[inline]
1880 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1881 virtual_idx % self.physical_layers
1882 }
1883
1884 #[inline]
1887 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1888 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1889 }
1890
1891 #[allow(clippy::too_many_arguments)]
1893
1894 #[cfg(target_os = "macos")]
1913 fn graph_prefill_preferred(&self) -> bool {
1914 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1915 if !crate::gpu::enabled_here()
1916 || !graph_force
1917 || std::env::var("CMF_GPU_BLOCK")
1918 .map(|v| v == "0")
1919 .unwrap_or(false)
1920 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1923 {
1924 return false;
1925 }
1926 self.weights
1927 .layers
1928 .iter()
1929 .any(|lw| {
1930 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1931 })
1932 }
1933
1934 #[cfg(not(target_os = "macos"))]
1941 fn batch_prefix_prefill(&self) -> bool {
1942 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1943 Ok("0") => return false,
1944 Ok("1") => true,
1945 _ => false,
1946 };
1947 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1948 && crate::gpu::enabled_here()
1949 && !self.graph_refused()
1950 && (forced || self.graph_attn_decline_reason().is_some())
1951 && self.wgpu_graph_attn_decline().is_none()
1952 && self.attn_softcap == 0.0
1953 && self
1954 .weights
1955 .layers
1956 .iter()
1957 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1958 && self.automatic_gpu_prefix().is_some()
1959 }
1960
1961 fn g4_batch_prefill(&self) -> bool {
1967 #[cfg(target_os = "macos")]
1968 {
1969 false
1970 }
1971 #[cfg(not(target_os = "macos"))]
1972 {
1973 (self.global_attn.is_some() || self.attn_v_norm)
1974 && crate::gpu::g4_batch_on()
1975 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1976 && crate::gpu::enabled_here()
1977 && crate::gpu::discrete()
1978 && !self.graph_refused()
1979 && !self.o1_active()
1980 && self.attn_softcap == 0.0
1981 && self.wgpu_graph_attn_decline().is_none()
1982 && self.weights.layers.iter().all(|lw| {
1983 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1984 })
1985 && self.automatic_gpu_prefix().is_none()
1986 }
1987 }
1988
1989 #[cfg(not(target_os = "macos"))]
1996 fn g4_batch_ingest(
1997 &mut self,
1998 ids: &[u32],
1999 pos0: usize,
2000 keep_all: bool,
2001 ) -> Result<(usize, Vec<f32>), String> {
2002 let hs = self.hidden_size;
2003 let chunk = self.generation_batch_k().max(1);
2004 let end_all = pos0 + ids.len();
2005 let mut pos = pos0;
2006 let mut out = Vec::new();
2007 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2008 let end = (pos + chunk).min(end_all);
2009 let bk = end - pos;
2010 let mut hiddens = vec![0f32; bk * hs];
2011 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2012 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2013 }
2014 let positions: Vec<usize> = (pos..end).collect();
2015 let t_chunk = std::time::Instant::now();
2016 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2017 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2018 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2019 eprintln!(
2020 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2021 bk as f64 / (ms / 1000.0)
2022 );
2023 }
2024 match outcome {
2025 crate::gpu::BatchGraphOutcome::Completed => {
2026 if keep_all {
2027 out.extend_from_slice(&hiddens);
2028 } else {
2029 out = hiddens[(bk - 1) * hs..].to_vec();
2030 }
2031 pos = end;
2032 }
2033 crate::gpu::BatchGraphOutcome::Failed => {
2034 return Err("Gemma-4 batched prefill failed after admission".into());
2035 }
2036 crate::gpu::BatchGraphOutcome::Declined => {
2037 #[cfg(feature = "gpu")]
2038 if pos > pos0 {
2039 self.pull_lagging_host_kv(0, self.num_layers, pos);
2040 }
2041 break;
2042 }
2043 }
2044 }
2045 Ok((pos - pos0, out))
2046 }
2047
2048 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2053 #[cfg(not(target_os = "macos"))]
2054 {
2055 if ids.is_empty() || !self.g4_batch_prefill() {
2056 return None;
2057 }
2058 self.clear_sequence_state();
2059 match self.g4_batch_ingest(ids, 0, true) {
2060 Ok((n, rows)) if n == ids.len() => Some(rows),
2061 _ => {
2062 self.clear_sequence_state();
2063 None
2064 }
2065 }
2066 }
2067 #[cfg(target_os = "macos")]
2068 {
2069 let _ = ids;
2070 None
2071 }
2072 }
2073
2074 #[cfg(not(target_os = "macos"))]
2075 fn graph_prefill_preferred(&self) -> bool {
2076 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2084 if !graph_on || !crate::gpu::enabled_here() {
2085 return false;
2086 }
2087 if self.embryo_resident_eligible() {
2091 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2096 }
2097 if self.o1_active() {
2111 return false;
2112 }
2113 if self.wgpu_graph_attn_decline().is_some() {
2117 return false;
2118 }
2119 if self
2120 .weights
2121 .layers
2122 .iter()
2123 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2124 {
2125 return true;
2126 }
2127 self.weights
2138 .layers
2139 .iter()
2140 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2141 && self.automatic_gpu_prefix().is_none()
2142 }
2143
2144 #[cfg(target_os = "macos")]
2145 fn q1_graph_gpu(
2146 &mut self,
2147 start: usize,
2148 upto: Option<usize>,
2149 position: usize,
2150 h: &mut [f32],
2151 ) -> usize {
2152 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2154 const MOE_WORKING_SET_REASON: &str =
2155 "model weights exceed Metal working set for native MoE graph";
2156 if self
2160 .graph_declines
2161 .borrow()
2162 .iter()
2163 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2164 {
2165 return start;
2166 }
2167 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2168 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2170 || !crate::gpu::enabled_here()
2171 || !graph_force
2172 || std::env::var("CMF_GPU_BLOCK")
2173 .map(|v| v == "0")
2174 .unwrap_or(false)
2175 {
2176 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2177 eprintln!(
2178 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2179 self.attn_softcap > 0.0,
2180 crate::gpu::enabled_here(),
2181 graph_force,
2182 );
2183 }
2184 if self.graph_head_required {
2185 self.fail_metal_graph("native graph front gate refused");
2186 }
2187 return start;
2188 }
2189 let gemma4 = self.metal_gemma4();
2199 let swa_graph = self.metal_graph_swa() || gemma4;
2200 let graph_attn_reason = self.graph_attn_decline_reason();
2207 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2208 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2209 if (self.swa.is_some() && !swa_graph)
2210 || (self.global_attn.is_some() && !gemma4)
2211 || self.attention_heads_per_layer.is_some()
2212 || (self.attn_v_norm && !gemma4)
2213 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2216 || self.weights.layers.iter().any(|lw| {
2217 ((lw.attn_out_norm.is_some()
2218 || lw.ffn_out_norm.is_some()
2219 || lw.layer_scale.is_some())
2220 && !gemma4)
2221 || matches!(&lw.ffn, FfnKind::Dense(d)
2222 if !(matches!(d.act, Act::Silu | Act::Gelu)
2223 || (gemma4 && d.act == Act::GeluTanh)))
2224 })
2225 {
2226 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2229 self.note_graph_decline("metal block graph", reason);
2230 }
2231 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2232 eprintln!(
2233 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2234 self.swa.is_some(),
2235 self.global_attn.is_some(),
2236 self.attention_heads_per_layer.is_some(),
2237 self.attn_v_norm,
2238 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2239 );
2240 }
2241 if self.graph_head_required {
2242 self.fail_metal_graph("native graph architecture gate refused");
2243 }
2244 return start;
2245 }
2246 let limit = upto
2249 .map(|u| u + 1)
2250 .unwrap_or(self.num_layers)
2251 .min(self.num_layers);
2252
2253 enum Item<'a> {
2254 Gdn {
2255 run: Vec<GdnGpuLayer<'a>>,
2256 first: usize,
2257 },
2258 Attn {
2259 l: AttnGpuLayer<'a>,
2260 li: usize,
2261 q_norm: Option<&'a [f32]>,
2262 k_norm: Option<&'a [f32]>,
2263 output_gate: bool,
2264 proj_gate: Option<(&'a QTensor, bool)>,
2268 head_gate_w: Option<&'a [f32]>,
2271 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2272 full_gpu: bool,
2275 },
2276 }
2277
2278 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2285 let attend_contract = attend_mode != "0"
2286 && attend_mode != "off"
2287 && self.head_dim % 4 == 0
2288 && self.head_dim <= 256
2289 && (self.rotary_dim == 0
2293 || (self.rotary_dim >= 2
2294 && self.rotary_dim <= self.head_dim
2295 && (self.rotary_dim / 2) % 32 == 0))
2296 && self.num_kv_heads > 0
2297 && self.num_heads % self.num_kv_heads == 0;
2298
2299 let mut plan: Vec<Item> = Vec::new();
2300 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2301 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2303 let mut scan = start;
2304 while scan < limit {
2305 let lw = &self.weights.layers[self.phys_layer(scan)];
2306 let ffn = match &lw.ffn {
2307 FfnKind::Dense(d) if d.segs.is_empty() => {
2308 let (Some(g), Some(u), Some(dn)) = (
2309 d.gate_proj.metal_graph_parts(),
2310 d.up_proj.metal_graph_parts(),
2311 d.down_proj.metal_graph_parts(),
2312 ) else {
2313 if block_diag {
2314 eprintln!(
2315 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2316 );
2317 }
2318 break;
2319 };
2320 MetalFfn::Dense {
2321 gate: g,
2322 up: u,
2323 down: dn,
2324 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2327 }
2328 }
2329 FfnKind::Moe(m) => {
2330 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2336 {
2337 MetalFfn::FlintMoe(flint)
2338 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2339 MetalFfn::Moe(moe)
2340 } else {
2341 if block_diag {
2342 eprintln!(
2343 "block-graph: L{scan} MoE outside the graph contract — run ends"
2344 );
2345 }
2346 break;
2347 };
2348 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2349 model_ref.get_or_insert_with(|| model.clone());
2350 }
2351 ffn
2352 }
2353 _ => {
2354 if block_diag {
2355 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2356 }
2357 break;
2358 }
2359 };
2360 match &lw.attn {
2361 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2362 let parts = (
2363 w.in_proj_qkv.metal_graph_parts(),
2364 w.in_proj_z.metal_graph_parts(),
2365 w.in_proj_a.f32_parts(),
2366 w.in_proj_b.f32_parts(),
2367 w.out_proj.metal_graph_parts(),
2368 );
2369 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2370 if block_diag {
2371 eprintln!(
2372 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2373 w.in_proj_qkv.metal_graph_parts().is_some(),
2374 w.in_proj_z.metal_graph_parts().is_some(),
2375 w.in_proj_a.f32_parts().is_some(),
2376 w.in_proj_b.f32_parts().is_some(),
2377 w.out_proj.metal_graph_parts().is_some(),
2378 );
2379 }
2380 break;
2381 };
2382 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2383 model_ref.get_or_insert_with(|| model.clone());
2384 }
2385 let gl = GdnGpuLayer {
2386 attn_norm: &lw.input_norm,
2387 post_norm: &lw.post_norm,
2388 qkv,
2389 z,
2390 a,
2391 b,
2392 out,
2393 ffn,
2394 conv1d: &w.conv1d,
2395 a_log: &w.a_log,
2396 dt_bias: &w.dt_bias,
2397 gnorm: &w.norm,
2398 };
2399 match plan.last_mut() {
2400 Some(Item::Gdn { run, .. }) => run.push(gl),
2401 _ => plan.push(Item::Gdn {
2402 run: vec![gl],
2403 first: scan,
2404 }),
2405 }
2406 }
2407 AttnKind::Full {
2408 wq,
2409 wk,
2410 wv,
2411 wo,
2412 q_norm,
2413 k_norm,
2414 output_gate,
2415 softplus_gate,
2416 bias,
2417 } if (!self.kv_cache.layers[scan].o1_sealed()
2418 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2423 && softplus_gate
2427 .as_ref()
2428 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2429 {
2430 let parts = (
2431 wq.metal_graph_parts(),
2432 wk.metal_graph_parts(),
2433 wv.metal_graph_parts(),
2434 wo.metal_graph_parts(),
2435 );
2436 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2437 break;
2438 };
2439 if let QTensor::Mapped { model, .. } = wq {
2440 model_ref.get_or_insert_with(|| model.clone());
2441 }
2442 let cache = &self.kv_cache.layers[scan];
2443 let o1_metal = cache.o1.is_some()
2447 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2448 && cache.o1_views().is_some();
2449 let head_gate_w = softplus_gate
2453 .as_ref()
2454 .and_then(|(g, _)| g.f32_parts())
2455 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2456 .map(|(d, _, _)| d);
2457 let (nkv_g, hd_g) = if gemma4 {
2460 let (a, b, _) = self.layer_geom(scan);
2461 (a, b)
2462 } else {
2463 (self.num_kv_heads, self.head_dim)
2464 };
2465 let full_gpu = attend_contract
2466 && softplus_gate.is_none() == head_gate_w.is_none()
2467 && cache.mode == crate::kv_cache::KvMode::F32
2468 && (cache.o1.is_none() || o1_metal)
2469 && bias.is_none()
2470 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2471 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2472 && pk.1 == nkv_g * hd_g
2473 && pv.1 == nkv_g * hd_g
2474 && po.2 == self.num_heads * hd_g;
2475 plan.push(Item::Attn {
2476 l: AttnGpuLayer {
2477 attn_norm: &lw.input_norm,
2478 post_norm: &lw.post_norm,
2479 wq: pq,
2480 wk: pk,
2481 wv: pv,
2482 wo: po,
2483 ffn,
2484 attn_out_norm: lw.attn_out_norm.as_deref(),
2485 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2486 layer_scale: lw.layer_scale,
2487 },
2488 li: scan,
2489 q_norm: q_norm.as_deref(),
2490 k_norm: k_norm.as_deref(),
2491 output_gate: *output_gate,
2492 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2493 head_gate_w,
2494 bias: bias
2495 .as_ref()
2496 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2497 full_gpu,
2498 });
2499 }
2500 _ => break,
2501 }
2502 scan += 1;
2503 }
2504 let Some(model) = model_ref else {
2505 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2506 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2507 }
2508 if self.graph_head_required {
2509 self.fail_metal_graph("native graph has no mapped model reference");
2510 }
2511 return start;
2512 };
2513 if plan.is_empty() {
2514 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2515 eprintln!("q1-graph: empty plan at layer {start}");
2516 }
2517 if self.graph_head_required {
2518 self.fail_metal_graph("native graph plan is empty");
2519 }
2520 return start;
2521 }
2522 let has_moe = plan.iter().any(|it| match it {
2523 Item::Gdn { run, .. } => run
2524 .iter()
2525 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2526 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2527 });
2528 let has_flint_moe = plan.iter().any(|it| match it {
2529 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2530 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2531 });
2532 let metal_working_set = crate::gpu_metal::working_set_bytes();
2533 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2534 let model_bytes = model.primary_bytes().len() as u64;
2535 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2541 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2542 let pager_cap = if !has_moe {
2551 None
2552 } else if has_flint_moe {
2553 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2557 } else {
2558 crate::gpu_metal::moe_pager_cap_bytes()
2559 };
2560 let pager_eligible =
2561 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2562 let mut pager_span: Option<(usize, usize)> = None;
2563 if pager_eligible {
2564 let cap = pager_cap.expect("checked above");
2565 let tail_span = (self.graph_want_logits
2569 && upto.is_none()
2570 && scan == self.num_layers
2571 && std::env::var("CMF_GPU_LMHEAD")
2572 .map(|v| v != "0")
2573 .unwrap_or(true))
2574 .then(|| self.weights.lm_head.metal_graph_parts())
2575 .flatten()
2576 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2577 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2578 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2579 Some(match span {
2580 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2581 None => (lo, hi),
2582 })
2583 };
2584 let page_span = |span: (usize, usize), include_tail: bool| {
2585 let (mut lo, mut hi) = span;
2586 if include_tail {
2587 if let Some((tail_lo, tail_hi)) = tail_span {
2588 lo = lo.min(tail_lo);
2589 hi = hi.max(tail_hi);
2590 }
2591 }
2592 crate::gpu_metal::page_round_span(lo, hi)
2593 };
2594 let mut selected = 0usize;
2595 let mut selected_span: Option<(usize, usize)> = None;
2596 let mut gdn_prefix: Option<(usize, usize)> = None;
2604 'items: for (i, item) in plan.iter().enumerate() {
2605 match item {
2606 Item::Attn { li, .. } => {
2607 let Some(span) = add_layer_span(selected_span, *li) else {
2608 break;
2609 };
2610 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2611 break;
2612 };
2613 if page_hi - page_lo > cap {
2614 break;
2615 }
2616 selected = i + 1;
2617 selected_span = Some((page_lo, page_hi));
2618 }
2619 Item::Gdn { first, run } => {
2620 let mut whole = None;
2627 for take in 1..=run.len() {
2628 whole = add_layer_span(whole, *first + take - 1);
2629 if whole.is_none() {
2630 break;
2631 }
2632 }
2633 let whole_fits_empty_page = whole
2634 .and_then(|span| page_span(span, false))
2635 .is_some_and(|(lo, hi)| hi - lo <= cap);
2636 if whole_fits_empty_page {
2637 let mut span = selected_span;
2638 for take in 1..=run.len() {
2639 let Some(next) = add_layer_span(span, *first + take - 1) else {
2640 break 'items;
2641 };
2642 span = Some(next);
2643 }
2644 let Some(span) = span else {
2645 break;
2646 };
2647 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2648 else {
2649 break;
2650 };
2651 if page_hi - page_lo > cap {
2652 break;
2653 }
2654 selected = i + 1;
2655 selected_span = Some((page_lo, page_hi));
2656 continue;
2657 }
2658 if selected != 0 {
2659 break;
2660 }
2661 let mut span = None;
2662 let mut best: Option<(usize, (usize, usize))> = None;
2663 for take in 1..=run.len() {
2664 let Some(next) = add_layer_span(span, *first + take - 1) else {
2665 break;
2666 };
2667 let Some((page_lo, page_hi)) = page_span(next, false) else {
2670 break;
2671 };
2672 if page_hi - page_lo > cap {
2673 break;
2674 }
2675 span = Some((page_lo, page_hi));
2676 best = Some((take, (page_lo, page_hi)));
2677 }
2678 let Some((take, page)) = best else {
2679 break;
2680 };
2681 selected = i + 1;
2682 selected_span = Some(page);
2683 if take != run.len() {
2684 gdn_prefix = Some((i, take));
2685 break 'items;
2686 }
2687 }
2688 }
2689 }
2690 if selected != 0 {
2691 if let Some((item, take)) = gdn_prefix {
2692 match &mut plan[item] {
2693 Item::Gdn { run, .. } => run.truncate(take),
2694 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2695 }
2696 }
2697 let (page_lo, page_hi) = selected_span.expect("selected page span");
2698 tracing::info!(
2699 page_start = page_lo,
2700 page_end = page_hi,
2701 page_mb = (page_hi - page_lo) / (1024 * 1024),
2702 cap_mb = cap / (1024 * 1024),
2703 graph_items = selected,
2704 "native Metal MoE graph uses transient bounded weight page"
2705 );
2706 plan.truncate(selected);
2707 pager_span = Some((page_lo, page_hi));
2708 } else {
2709 tracing::warn!(
2710 cap_mb = cap / (1024 * 1024),
2711 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2712 );
2713 }
2714 }
2715 if has_moe && needs_bounded_pager && pager_span.is_none() {
2724 tracing::info!(
2725 model_bytes,
2726 metal_working_set,
2727 "native Metal MoE graph declined before allocation"
2728 );
2729 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2730 if self.graph_head_required {
2731 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2732 }
2733 return start;
2734 }
2735 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2736 let dev_attend = attend_contract
2737 && (self.head_dim <= 128
2738 || has_moe
2739 || (self.head_dim <= 256 && has_gdn)
2745 || (self.head_dim <= 256 && swa_graph)
2749 || attend_mode == "force"
2750 || attend_mode == "256");
2751 if !dev_attend {
2752 for it in &mut plan {
2753 if let Item::Attn { li, full_gpu, .. } = it {
2754 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2757 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2758 if !keep_o1 {
2759 *full_gpu = false;
2760 }
2761 }
2762 }
2763 }
2764 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2765 use std::sync::atomic::{AtomicBool, Ordering};
2766 static SAID: AtomicBool = AtomicBool::new(false);
2767 if !SAID.swap(true, Ordering::Relaxed) {
2768 let fg = plan
2769 .iter()
2770 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2771 .count();
2772 let att = plan
2773 .iter()
2774 .filter(|it| matches!(it, Item::Attn { .. }))
2775 .count();
2776 eprintln!(
2777 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2778 plan.len(),
2779 self.head_dim,
2780 self.rotary_dim,
2781 self.num_kv_heads,
2782 self.num_heads,
2783 );
2784 }
2785 }
2786 let dims = GraphDims {
2787 hidden: self.hidden_size,
2788 eps: self.rms_eps as f32,
2789 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2790 };
2791 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2795 crate::gpu_metal::MoePagerPageScope::new(
2796 _mt0,
2797 position,
2798 start,
2799 page_lo,
2800 page_hi,
2801 pager_cap.unwrap_or(0),
2805 )
2806 });
2807 let graph_new = match pager_span {
2808 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2809 None => TokenGraph::new(&model, dims, h),
2810 };
2811 let Some(mut graph) = graph_new else {
2812 if self.graph_head_required {
2813 self.fail_metal_graph("native TokenGraph allocation refused");
2814 }
2815 return start;
2816 };
2817 if swa_graph && self.head_dim > 128 {
2818 graph.set_attend_blk_from(64);
2824 }
2825 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2826 nv: cfg.num_v_heads,
2827 nk: cfg.num_k_heads,
2828 dk: cfg.key_head_dim,
2829 dv: cfg.value_head_dim,
2830 kk: cfg.conv_kernel,
2831 hidden: self.hidden_size,
2832 inter: self.intermediate_size,
2833 c_dim: cfg.conv_dim(),
2834 eps: cfg.rms_eps as f32,
2835 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2836 });
2837 let mut valid = 0usize;
2841 let mut end = start;
2842 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2844 static ONCE: std::sync::Once = std::sync::Once::new();
2845 ONCE.call_once(|| {
2846 for it in &plan {
2847 match it {
2848 Item::Gdn { first, run } => {
2849 eprintln!("plan: Gdn first={first} len={}", run.len())
2850 }
2851 Item::Attn { li, full_gpu, .. } => {
2852 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2853 }
2854 }
2855 }
2856 });
2857 }
2858 for item in &plan {
2859 let ok = match item {
2860 Item::Gdn { run, .. } => gcfg
2861 .as_ref()
2862 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2863 .unwrap_or(false),
2864 Item::Attn { l, .. } => graph.attn_ok(l),
2865 };
2866 if !ok {
2867 if block_diag {
2868 eprintln!(
2869 "block-graph: plan item {} ({}) failed graph preflight",
2870 valid,
2871 match item {
2872 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2873 Item::Attn { li, .. } => format!("Attn L{li}"),
2874 }
2875 );
2876 }
2877 break;
2878 }
2879 valid += 1;
2880 end += match item {
2881 Item::Gdn { run, .. } => run.len(),
2882 Item::Attn { .. } => 1,
2883 };
2884 }
2885 plan.truncate(valid);
2886 if plan.is_empty() {
2887 if self.graph_head_required {
2888 self.fail_metal_graph("native graph preflight produced no valid items");
2889 }
2890 return start;
2891 }
2892
2893 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2894 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2895 return start;
2896 }
2897
2898 let one_pass = |t: (usize, usize, usize)| {
2907 use cortiq_core::TensorDtype as D;
2908 matches!(
2909 model.tensors[t.0].dtype,
2910 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
2911 )
2912 };
2913 let dense_fast = plan.iter().all(|it| match it {
2914 Item::Attn {
2915 l, li, full_gpu, ..
2916 } => {
2917 *full_gpu
2918 && self.kv_cache.layers[*li].o1.is_none()
2919 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2920 && match l.ffn {
2921 MetalFfn::Dense { gate, up, down, .. } => {
2922 one_pass(gate) && one_pass(up) && one_pass(down)
2923 }
2924 _ => false,
2925 }
2926 }
2927 Item::Gdn { .. } => false,
2928 });
2929 let moe_plan = !dense_fast
2933 && plan.iter().all(|it| match it {
2934 Item::Attn {
2935 l, li, full_gpu, ..
2936 } => {
2937 *full_gpu
2938 && self.kv_cache.layers[*li].o1.is_none()
2939 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
2940 }
2941 Item::Gdn { .. } => false,
2942 });
2943 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
2944 if block_diag {
2945 use std::sync::atomic::{AtomicBool, Ordering};
2946 static SAID_AB: AtomicBool = AtomicBool::new(false);
2947 if !SAID_AB.swap(true, Ordering::Relaxed) {
2948 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
2949 }
2950 }
2951 let _mv_fast = match ab {
2952 Some((bits, _)) => {
2953 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2954 crate::gpu_metal::MvFastGuard::set_raw(bits)
2955 }
2956 None if moe_plan => {
2957 graph.set_dense_concurrent(
2958 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
2959 );
2960 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
2961 }
2962 None => {
2963 graph.set_dense_concurrent(dense_fast);
2964 let q8r4 = if swa_graph {
2967 crate::gpu_metal::DENSE_Q8R4
2968 } else {
2969 0
2970 };
2971 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2972 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2973 } else {
2974 0
2975 })
2976 }
2977 };
2978
2979 let pool = self.pool.clone();
2982 let (nh, nkv, hd, hs, eps) = (
2983 self.num_heads,
2984 self.num_kv_heads,
2985 self.head_dim,
2986 self.hidden_size,
2987 self.rms_eps,
2988 );
2989 let norm_style = self.norm_style;
2990 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2991 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2992 let kv_id = self.graph_kv_id;
2993 let mut pending: Vec<(usize, usize)> = Vec::new();
2996 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
2999 for item in &plan {
3000 let _xt0 = std::time::Instant::now();
3001 let _xkind: u32 = match item {
3002 Item::Gdn { .. } => 2,
3003 Item::Attn { .. } => 3,
3004 };
3005 if self.loop_final_norm {
3007 let item_start = match item {
3008 Item::Gdn { first, .. } => *first,
3009 Item::Attn { li, .. } => *li,
3010 };
3011 if item_start > start && self.is_loop_end(item_start - 1) {
3012 graph.encode_loop_norm(&self.weights.final_norm);
3013 }
3014 }
3015 match item {
3016 Item::Gdn { run, first } => {
3017 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3018 if l.linear_state.len() != want {
3019 l.linear_state = vec![0f32; want];
3020 }
3021 }
3022 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3023 .iter()
3024 .map(|l| l.linear_state.as_slice())
3025 .collect();
3026 let _ig = std::time::Instant::now();
3027 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3028 tracing::error!("q1 graph: GDN run refused after validation");
3030 return start;
3031 }
3032 graph.commit_kind = 2;
3035 graph.commit();
3036 crate::gpu::stageprof(0, _ig.elapsed());
3037 pending.push((*first, run.len()));
3038 }
3039 Item::Attn {
3040 l,
3041 li,
3042 q_norm,
3043 k_norm,
3044 output_gate,
3045 proj_gate,
3046 head_gate_w,
3047 bias,
3048 full_gpu,
3049 } => {
3050 let _ia = std::time::Instant::now();
3051 let inv_freq_l = self.layer_inv_freq(*li);
3057 let rd_l = self.layer_geom(*li).2;
3058 let window_l = self.layer_window(*li);
3059 let head_gate_w = *head_gate_w;
3060 let (nkv, hd) = if gemma4 {
3063 let (a, b, _) = self.layer_geom(*li);
3064 (a, b)
3065 } else {
3066 (nkv, hd)
3067 };
3068 if *full_gpu {
3070 let cache = &self.kv_cache.layers[*li];
3071 let o1p = if cache.o1.is_some() {
3072 match cache.o1_views() {
3073 Some(views) => Some(crate::gpu::O1AttnParams {
3074 views,
3075 epoch: self.o1_epoch,
3076 }),
3077 None => None,
3079 }
3080 } else {
3081 None
3082 };
3083 let o1_layer = cache.o1.is_some();
3084 if o1_layer && o1p.is_none() {
3085 }
3087 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3088 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3089 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3090 debug_assert!(
3093 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3094 );
3095 let p = crate::gpu::AttnDeviceParams {
3096 kv_id,
3097 layer: *li,
3098 nh,
3099 nkv,
3100 hd,
3101 rd: rd_l,
3102 position,
3103 scale: self.attn_scale,
3104 eps: eps as f32,
3105 gemma,
3106 late_qk_norm: self.qk_norm_after_rope,
3107 output_gate: *output_gate,
3108 q_norm: *q_norm,
3109 k_norm: *k_norm,
3110 inv_freq: &inv_freq_l,
3111 rope_scale: self.layer_rope_scale(*li),
3112 cpu_k,
3113 cpu_v,
3114 cpu_stored,
3115 cpu_gen: cache.generation(),
3116 o1: o1p,
3117 window: window_l,
3118 softmax_sink: cache.sinks.as_deref(),
3119 head_gate: head_gate_w,
3120 v_norm: self.attn_v_norm,
3121 };
3122 let o1_bad = o1_layer && p.o1.is_none();
3123 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3124 {
3125 if p.o1.is_none() {
3127 dev_attn.push((*li, nkv, hd));
3128 }
3129 graph.commit_kind = 3;
3130 graph.commit();
3131 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3135 continue;
3136 }
3137 }
3139 graph.encode_attn_prefix(l);
3140 if let Err(err) = graph.sync_checked() {
3141 self.fail_metal_graph(&err);
3142 return start;
3143 }
3144 if !pending.is_empty() {
3145 let idxs: Vec<usize> =
3146 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3147 let mut outs: Vec<&mut [f32]> = self
3148 .kv_cache
3149 .layers
3150 .iter_mut()
3151 .enumerate()
3152 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3153 .map(|(_, s)| s.linear_state.as_mut_slice())
3154 .collect();
3155 graph.read_states(&mut outs);
3156 }
3157 let mut q_raw = attention::take_buf(l.wq.1);
3158 let mut k = attention::take_buf(l.wk.1);
3159 let mut v = attention::take_buf(l.wv.1);
3160 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3161 let mut gate_raw = proj_gate.map(|(gp, _)| {
3164 let mut normed = attention::take_buf(hs);
3165 graph.read_normed(&mut normed);
3166 let mut raw = attention::take_buf(gp.rows());
3167 gp.matvec(&normed, &mut raw, pool.as_deref());
3168 attention::recycle_buf(&mut normed);
3169 raw
3170 });
3171 let cfg = QwenAttnCfg {
3172 num_heads: nh,
3173 num_kv_heads: nkv,
3174 head_dim: hd,
3175 hidden_size: hs,
3176 position,
3177 inv_freq: &inv_freq_l,
3178 rotary_dim: rd_l,
3179 scale: self.attn_scale,
3180 softcap: self.attn_softcap,
3181 window: window_l,
3182 v_norm: self.attn_v_norm,
3185 qk_norm_after_rope: self.qk_norm_after_rope,
3186 gate_sigmoid: self.proj_gate_sigmoid,
3187 q_norm: *q_norm,
3188 k_norm: *k_norm,
3189 output_gate: *output_gate,
3190 softplus_gate: None,
3191 rope_scale: self.layer_rope_scale(*li),
3192 bias: *bias,
3193 rms_eps: eps,
3194 norm_style,
3195 pool: pool.as_deref(),
3196 v_head_dim: hd,
3197 };
3198 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3201 || std::env::var("CMF_ATTN_DUMP").is_ok();
3202 let _ = full_gpu;
3203 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3204 let mut ao = attention::qwen_attention_core(
3205 q_raw,
3206 k,
3207 v,
3208 &mut self.kv_cache.layers[*li],
3209 &cfg,
3210 );
3211 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3215 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3216 let (cq, _cg, _ck, _cv) =
3217 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3218 let cache = &self.kv_cache.layers[*li];
3219 let n = cache.head_keys(0).len() / hd;
3220 let mut bytes: Vec<u8> = Vec::new();
3221 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3222 bytes.extend_from_slice(&v.to_le_bytes());
3223 }
3224 for v in &cq {
3225 bytes.extend_from_slice(&v.to_le_bytes());
3226 }
3227 for g in 0..nkv {
3228 for v in cache.head_keys(g) {
3229 bytes.extend_from_slice(&v.to_le_bytes());
3230 }
3231 }
3232 for g in 0..nkv {
3233 for v in cache.head_values(g) {
3234 bytes.extend_from_slice(&v.to_le_bytes());
3235 }
3236 }
3237 let _ =
3238 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3239 }
3240 }
3241 if let Some((qr0, k0, v0)) =
3242 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3243 {
3244 let (cq, _cg, ck, cv) =
3245 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3246 let mut h_now = vec![0f32; hs];
3247 graph.read_h(&mut h_now);
3248 let cache = &self.kv_cache.layers[*li];
3249 let n_after = cache.head_keys(0).len() / hd;
3250 let stored = n_after.saturating_sub(1);
3254 let cpu_k: Vec<&[f32]> = (0..nkv)
3255 .map(|g| &cache.head_keys(g)[..stored * hd])
3256 .collect();
3257 let cpu_v: Vec<&[f32]> = (0..nkv)
3258 .map(|g| &cache.head_values(g)[..stored * hd])
3259 .collect();
3260 let p = crate::gpu::AttnDeviceParams {
3261 kv_id,
3262 layer: *li,
3263 nh,
3264 nkv,
3265 hd,
3266 rd: rd_l,
3270 position,
3271 scale: self.attn_scale,
3272 eps: eps as f32,
3273 gemma,
3274 late_qk_norm: self.qk_norm_after_rope,
3275 output_gate: *output_gate,
3276 q_norm: *q_norm,
3277 k_norm: *k_norm,
3278 inv_freq: &inv_freq_l,
3279 rope_scale: self.layer_rope_scale(*li),
3280 cpu_k,
3281 cpu_v,
3282 cpu_stored: stored,
3283 cpu_gen: cache.generation(),
3284 o1: None,
3285 window: window_l,
3286 softmax_sink: cache.sinks.as_deref(),
3287 head_gate: None,
3288 v_norm: self.attn_v_norm,
3289 };
3290 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3291 let md = |a: &[f32], b: &[f32]| {
3292 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3293 };
3294 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3295 eprintln!(
3296 "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}",
3297 nn(&cq),
3298 md(&cq, &dq),
3299 nn(&ck),
3300 md(&ck, &dk),
3301 nn(&cv),
3302 md(&cv, &dv),
3303 nn(&ao),
3304 md(&ao, &dao)
3305 );
3306 } else {
3307 eprintln!("attn-oracle L{li}: device probe declined");
3308 }
3309 }
3310 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3311 attention::apply_projected_gate(
3314 &mut ao,
3315 raw,
3316 per_head,
3317 hd,
3318 self.proj_gate_sigmoid,
3319 );
3320 }
3321 if let Some(mut raw) = gate_raw.take() {
3322 attention::recycle_buf(&mut raw);
3323 }
3324 graph.encode_attn_suffix(l, &ao);
3325 graph.commit();
3328 attention::recycle_buf(&mut ao);
3329 }
3330 }
3331
3332 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3333 }
3334 let mut lm_rows = None;
3339 if self.graph_want_logits
3340 && upto.is_none()
3341 && end == self.num_layers
3342 && std::env::var("CMF_GPU_LMHEAD")
3343 .map(|v| v != "0")
3344 .unwrap_or(true)
3345 {
3346 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3347 if graph.lm_head_ok(lm) {
3348 graph.encode_lm_head(&self.weights.final_norm, lm);
3349 lm_rows = Some(lm.1);
3350 }
3351 }
3352 }
3353 if self.graph_head_required && lm_rows.is_none() {
3354 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3355 self.fail_metal_graph("fused graph head was requested but not encodable");
3356 return start;
3357 }
3358 let _sy0 = std::time::Instant::now();
3359 if let Err(err) = graph.sync_checked() {
3360 if let Some(profile) = pager_profile_page.as_mut() {
3361 profile.finish(start, graph.pager_page_stats(), "sync-error");
3362 }
3363 self.fail_metal_graph(&err);
3364 return start;
3365 }
3366 let _rs0 = std::time::Instant::now();
3367 if !pending.is_empty() {
3368 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3369 let mut outs: Vec<&mut [f32]> = self
3370 .kv_cache
3371 .layers
3372 .iter_mut()
3373 .enumerate()
3374 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3375 .map(|(_, s)| s.linear_state.as_mut_slice())
3376 .collect();
3377 graph.read_states(&mut outs);
3378 }
3379 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3380 use std::sync::atomic::{AtomicU64, Ordering};
3381 static SY: AtomicU64 = AtomicU64::new(0);
3382 static RS: AtomicU64 = AtomicU64::new(0);
3383 static N: AtomicU64 = AtomicU64::new(0);
3384 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3385 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3386 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3387 if n % 100 == 0 {
3388 eprintln!(
3389 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3390 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3391 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3392 );
3393 }
3394 }
3395 if let Some(rows) = lm_rows {
3396 crate::gpu::hostprof_encode_done(_mt0);
3397 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3398 graph.read_logits(&mut lg);
3399 crate::gpu::hostprof_total(_mt0);
3400 lg.resize(self.vocab_size, 0.0);
3401 if let Some(c) = self.final_softcap {
3402 softcap_in_place(&mut lg, c, self.pool.as_deref());
3403 }
3404 self.graph_logits = Some(lg);
3405 }
3406 graph.read_h(h);
3407 if self.graph_head_required && self.graph_logits.is_none() {
3408 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3409 self.fail_metal_graph("fused graph head completed without logits readback");
3410 return start;
3411 }
3412 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3413 METAL_GRAPH_LAYERS.fetch_add(
3414 end.saturating_sub(start) as u64,
3415 std::sync::atomic::Ordering::Relaxed,
3416 );
3417 if self.graph_head_required {
3418 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3419 }
3420 for (li, nkv, hd) in dev_attn {
3424 let mut krow = attention::take_buf(nkv * hd);
3425 let mut vrow = attention::take_buf(nkv * hd);
3426 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3427 let cache = &mut self.kv_cache.layers[li];
3428 cache.append(&krow, &vrow, &[]);
3429 let n = cache.seq_len;
3430 let mut imp = attention::take_buf(n);
3431 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3432 cache.accumulate_imp(&imp);
3433 attention::recycle_buf(&mut imp);
3434 }
3435 attention::recycle_buf(&mut krow);
3436 attention::recycle_buf(&mut vrow);
3437 }
3438 if let Some(profile) = pager_profile_page.as_mut() {
3439 profile.finish(end, graph.pager_page_stats(), "page-complete");
3440 }
3441 if let Some((_, arm)) = ab {
3442 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3443 }
3444 end
3445 }
3446
3447 pub fn new(
3448 tokenizer: Tokenizer,
3449 weights: PipelineWeights,
3450 hidden_size: usize,
3451 intermediate_size: usize,
3452 num_heads: usize,
3453 num_kv_heads: usize,
3454 head_dim: usize,
3455 num_layers: usize,
3456 physical_layers: usize,
3457 loop_final_norm: bool,
3458 vocab_size: usize,
3459 rms_eps: f64,
3460 rope_base: f32,
3461 norm_style: NormStyle,
3462 max_seq_len: usize,
3463 sampler_config: SamplerConfig,
3464 ) -> Self {
3465 let rng = match sampler_config.seed {
3466 Some(s) => SplitMix64::new(s),
3467 None => SplitMix64::from_entropy(),
3468 };
3469 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3470 let pool = Pool::from_env();
3471 if let Some(p) = &pool {
3472 tracing::info!("worker pool: {} threads", p.n_workers());
3473 if let Some(model) = weights
3475 .lm_head
3476 .model_arc()
3477 .or_else(|| weights.embed_tokens.model_arc())
3478 {
3479 let regions: Vec<&[u8]> =
3480 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3481 p.bind_numa(®ions);
3482 }
3483 }
3484 Self {
3485 gpu_plan: None,
3486 tokenizer: std::sync::Arc::new(tokenizer),
3487 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3488 sampler_config,
3489 weights,
3490 hidden_size,
3491 intermediate_size,
3492 num_heads,
3493 num_kv_heads,
3494 head_dim,
3495 num_layers,
3496 physical_layers,
3497 loop_final_norm,
3498 vocab_size,
3499 rms_eps,
3500 rope_base,
3501 norm_style,
3502 rotary_dim: head_dim,
3503 attention_heads_per_layer: None,
3504 kv_heads_per_layer: None,
3505 v_head_dim: None,
3506 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3507 .filter(|v| !v.is_empty())
3508 .map(std::path::PathBuf::from),
3509 graph_declines: std::cell::RefCell::new(Vec::new()),
3510 mimo_moe: Default::default(),
3511 minicpm_tail: Default::default(),
3512 vmf_cfg: None,
3513 gdn_cfg: None,
3514 kda_cfg: None,
3515 g3n: None,
3516 dsv4: None,
3517 dsv41: None,
3518 dsv41_vision: None,
3519 dsv41_prefill: None,
3520 qwen4_exp: None,
3521 dsv4_mtp: Vec::new(),
3522 dspark: None,
3523 dspark_pending: Vec::new(),
3524 dspark_hist: Vec::new(),
3525 dspark_real: Vec::new(),
3526 dspark_trunk_picks: Vec::new(),
3527 dspark_exp: Vec::new(),
3528 dspark_draft_ns: 0,
3529 logit_multiplier: None,
3530 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3531 graph_failed: std::sync::atomic::AtomicBool::new(false),
3532 kv_history: Vec::new(),
3533 kv_history_device: false,
3534 short_conv_cfg: None,
3535 mtp: None,
3536 mimo_mtp: None,
3537 verify_exact_moe: false,
3538 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3539 ignore_eos: false,
3540 draft_full_streak: 0,
3541 spec_k_adapt: None,
3542 spec_acc_ewma: 0.7,
3543 rng,
3544 sampler_scratch: SamplerScratch::default(),
3545 spec_forced: None,
3546 spec_q: Vec::new(),
3547 spec_p: Vec::new(),
3548 spec_res: Vec::new(),
3549 spec_qs: Vec::new(),
3550 spec_ps: Vec::new(),
3551 spec_ress: Vec::new(),
3552 mtp_graph_mode: None,
3553 #[cfg(target_os = "macos")]
3554 metal_verify: None,
3555 inv_freq,
3556 ws: ForwardScratch::new(hidden_size),
3557 pool,
3558 model: None,
3559 dyn_force_f32: false,
3560 dyn_skill_layers: Vec::new(),
3561 dyn_active: None,
3562 dyn_blend_loaded: false,
3563 dyn_phi_layer: None,
3564 dyn_phi_ema: Vec::new(),
3565 dyn_phi_seen: 0,
3566 dyn_router: None,
3567 o1_cfg: None,
3568 o1_epoch: 0,
3569 o1_flags: Vec::new(),
3570 trace: false,
3571 calib_temp: 1.0,
3572 confidence_on: true,
3573 embed_multiplier: 1.0,
3574 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3575 swa: None,
3576 swa_trim: None,
3577 sliding_layers: None,
3578 no_rope_layers: None,
3579 anchor_core: None,
3580 bounded_rope: None,
3581 kv_prefix: KvPrefix::default(),
3582 last_prefill_tokens: 0,
3583 inv_freq_local: None,
3584 rotary_dim_local: None,
3585 rope_scale: 1.0,
3586 rope_scale_local: 1.0,
3587 global_attn: None,
3588 inv_freq_global: None,
3589 attn_v_norm: false,
3590 qk_norm_after_rope: false,
3591 proj_gate_sigmoid: false,
3592 final_softcap: None,
3593 head_clusters: None,
3594 attn_softcap: 0.0,
3595 graph_want_logits: false,
3596 graph_head_required: false,
3597 graph_logits: None,
3598 embryo_graph: None,
3599 graph_refused: std::sync::atomic::AtomicBool::new(false),
3600 graph_kv_id: {
3601 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3602 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3603 },
3604 #[cfg(test)]
3605 nll_test_fail_at: None,
3606 #[cfg(test)]
3607 nll_test_force_serial: false,
3608 }
3609 }
3610
3611 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3619 if let Err(e) = self.try_set_o1(cfg) {
3620 tracing::error!("{e}");
3621 }
3622 }
3623
3624 pub fn bounded_native(&self) -> bool {
3628 self.anchor_core.is_some()
3629 }
3630
3631 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3634 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3635 }
3636
3637 pub fn o1_refusal(&self) -> Option<String> {
3639 self.anchor_core.as_ref().map(|ac| {
3640 format!(
3641 "--o1 / CMF_O1 refused: the anchor is native bounded \
3642 (anchor_core kind={} window={} sink={}); the file's operator \
3643 is executed as-is and no post-hoc Nyström overlay applies",
3644 ac.kind, ac.window, ac.sink
3645 )
3646 })
3647 }
3648
3649 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3651 if let Some(c) = &cfg {
3652 if let Some(why) = self.o1_refusal() {
3653 self.o1_flags = Vec::new();
3654 self.o1_cfg = None;
3655 return Err(why);
3656 }
3657 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3658 self.o1_flags.clear();
3659 self.o1_cfg = None;
3660 return Err(format!(
3661 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3662 c.w, c.sink
3663 ));
3664 }
3665 }
3666 self.o1_flags = match &cfg {
3667 Some(c) => {
3668 let mut flags = c.layer_flags(self.num_layers);
3669 for (li, f) in flags.iter_mut().enumerate() {
3670 if *f
3676 && (!matches!(
3677 self.weights.layers[self.phys_layer(li)].attn,
3678 AttnKind::Full { .. }
3679 ) || self.layer_window(li).is_some()
3680 || self.kv_cache.layers[li].sinks.is_some()
3681 || self.layer_v_dim(li) != self.layer_geom(li).1)
3682 {
3683 *f = false;
3684 }
3685 }
3686 flags
3687 }
3688 None => Vec::new(),
3689 };
3690 if let Some(c) = &cfg {
3691 let n = self.o1_flags.iter().filter(|&&f| f).count();
3692 tracing::info!(
3693 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3694 self.num_layers,
3695 c.m,
3696 c.w,
3697 c.sink,
3698 c.rect
3699 );
3700 }
3701 self.o1_cfg = cfg;
3702 Ok(())
3703 }
3704
3705 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3711 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3712 return Err(format!(
3713 "anchor_core kind '{}' is not executable by this runtime",
3714 cfg.kind
3715 ));
3716 }
3717 if cfg.window == 0 {
3718 return Err("anchor_core.window must be >= 1".into());
3719 }
3720 let mut n = 0usize;
3721 for li in 0..self.num_layers {
3722 let pl = self.phys_layer(li);
3723 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3724 if w.window != cfg.window || w.sink != cfg.sink {
3725 return Err(format!(
3726 "layer {li}: bounded weights (window {} sink {}) disagree with \
3727 anchor_core (window {} sink {})",
3728 w.window, w.sink, cfg.window, cfg.sink
3729 ));
3730 }
3731 self.kv_cache.layers[li].install_bounded(cfg.window);
3732 n += 1;
3733 }
3734 }
3735 if n == 0 {
3736 return Err("anchor_core is present but no layer executes it".into());
3737 }
3738 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3739 self.bounded_rope = Some(std::sync::Arc::new(rope));
3740 self.anchor_core = Some(cfg.clone());
3741 self.embryo_graph = None;
3742 tracing::info!(
3743 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3744 cfg.kind,
3745 cfg.window,
3746 cfg.sink,
3747 self.kv_cache
3748 .layers
3749 .iter()
3750 .map(|l| l.bounded_state_bytes())
3751 .max()
3752 .unwrap_or(0)
3753 );
3754 Ok(())
3755 }
3756
3757 pub fn install_wire_identity(&mut self, identity: u64) {
3761 for li in 0..self.kv_cache.layers.len() {
3762 let pl = self.phys_layer(li);
3763 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3764 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3765 Some(AttnKind::Linear(_))
3766 | Some(AttnKind::LinearGdn(_))
3767 | Some(AttnKind::ShortConv(_))
3768 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3769 _ => crate::kv_cache::WireKind::Full,
3770 };
3771 let l = &mut self.kv_cache.layers[li];
3772 l.wire_kind = kind;
3773 l.wire_identity = identity;
3774 l.wire_layer = li as u32;
3777 }
3778 }
3779
3780 pub fn clear_history(&mut self) {
3782 self.kv_history.clear();
3783 self.kv_history_device = false;
3784 self.kv_prefix.clear();
3785 }
3786
3787 pub fn graph_refused(&self) -> bool {
3791 self.graph_refused
3792 .load(std::sync::atomic::Ordering::Relaxed)
3793 }
3794
3795 pub fn mark_graph_refused(&self) {
3797 if !self
3798 .graph_refused
3799 .swap(true, std::sync::atomic::Ordering::Relaxed)
3800 {
3801 tracing::info!(
3802 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3803 self.graph_kv_id
3804 );
3805 }
3806 }
3807
3808 pub fn device_sequence_position(&self) -> Option<usize> {
3812 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3813 }
3814
3815 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3820 let dev = self.device_sequence_position();
3821 if recorded_on_device {
3822 dev == Some(n) && self.embryo_resident_wanted()
3823 } else {
3824 dev.is_none()
3825 }
3826 }
3827
3828 pub(crate) fn invalidate_for_weight_change(&mut self) {
3836 self.clear_sequence_state();
3837 self.embryo_graph = None;
3838 }
3839
3840 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3846 let (n, on_device) = if self.bounded_native() {
3847 (
3848 self.kv_prefix.extension(input_ids),
3849 self.kv_prefix.on_device(),
3850 )
3851 } else {
3852 let h = &self.kv_history;
3853 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3854 (h.len(), self.kv_history_device)
3855 } else {
3856 (0, false)
3857 }
3858 };
3859 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3862 tracing::warn!(
3863 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3864 the device now holds {:?} — re-prefilling from zero",
3865 if on_device { "resident device" } else { "host" },
3866 self.device_sequence_position()
3867 );
3868 return 0;
3869 }
3870 n
3871 }
3872
3873 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3876 self.cached_prefix_len(input_ids)
3877 }
3878
3879 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3885 let on_device = self.device_sequence_position().is_some();
3886 if self.bounded_native() {
3887 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3888 let prev_device = self.kv_prefix.on_device();
3889 self.kv_history.clear();
3890 self.kv_history_device = false;
3891 if keep && prev_device == on_device {
3892 self.kv_prefix.extend(&consumed[reused..]);
3893 } else {
3894 self.kv_prefix.set(consumed);
3895 }
3896 self.kv_prefix.set_on_device(on_device);
3897 } else {
3898 self.kv_history = consumed.to_vec();
3899 self.kv_history_device = on_device;
3900 }
3901 }
3902
3903 pub fn o1_active(&self) -> bool {
3905 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3906 }
3907
3908 pub fn generation_batch_k(&self) -> usize {
3926 if let Some(k) = std::env::var("CMF_BATCH_K")
3927 .ok()
3928 .and_then(|v| v.parse::<usize>().ok())
3929 {
3930 return k;
3931 }
3932 if self.g4_batch_prefill() {
3935 return 128;
3936 }
3937 #[cfg(not(target_os = "macos"))]
3938 if self.graph_prefill_preferred() && !self.o1_active() {
3939 let moe = self
3942 .weights
3943 .layers
3944 .iter()
3945 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
3946 let q82 = crate::gpu::moe_q82_graph_on()
3952 && self.weights.layers.iter().any(|lw| {
3953 matches!(&lw.ffn, FfnKind::Moe(m)
3954 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
3955 });
3956 return if q82 {
3957 128
3958 } else if moe {
3959 32
3960 } else {
3961 128
3962 };
3963 }
3964 0
3965 }
3966
3967 pub fn generation_graph_prefill(&self) -> bool {
3968 let graph = self.graph_prefill_preferred();
3969 #[cfg(not(target_os = "macos"))]
3980 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
3981 return false;
3982 }
3983 graph
3984 }
3985
3986 pub fn o1_device_stats(&self) -> (usize, u64) {
3991 crate::gpu::o1_device_stats(self.graph_kv_id)
3992 }
3993
3994 pub fn o1_begin(&mut self) {
3999 self.o1_begin_with_prefix(None);
4000 }
4001
4002 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4006 if let Some(c) = &self.o1_cfg {
4007 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4008 let boundary = requested_prefix.map(|p| {
4009 p.max(
4010 crate::nystrom::o1_deferred_boundary(w, sink)
4011 .expect("o1 config boundary validated in set_o1"),
4012 )
4013 });
4014 for (li, &f) in self.o1_flags.iter().enumerate() {
4015 if f {
4016 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4017 }
4018 }
4019 }
4020 }
4021
4022 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4024 self.o1_cfg.as_ref().and_then(|c| {
4025 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4026 .map(|floor| requested_prefix.max(floor))
4027 })
4028 }
4029
4030 fn o1_note_transition(&mut self) {
4031 let mut transitioned = false;
4035 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4036 if flagged {
4037 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4038 }
4039 }
4040 if transitioned {
4041 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4042 }
4043 }
4044
4045 fn o1_pending(&self) -> bool {
4046 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4047 f && self.kv_cache.layers[li].seq_len > 0
4048 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4049 })
4050 }
4051
4052 fn o1_fail(&mut self, err: String) {
4053 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4054 self.clear_sequence_state();
4055 self.graph_failed
4056 .store(true, std::sync::atomic::Ordering::Relaxed);
4057 self.cancel
4058 .store(true, std::sync::atomic::Ordering::Relaxed);
4059 }
4060
4061 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4066 if self.o1_cfg.is_none() {
4067 return Ok(false);
4068 }
4069 let mut participating = false;
4070 for li in 0..self.num_layers {
4071 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4072 continue;
4073 }
4074 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4075 return Err(err);
4076 }
4077 if self.kv_cache.layers[li].seq_len == 0 {
4078 continue;
4079 }
4080 participating = true;
4081 let num_heads = self.layer_num_heads(li);
4082 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4083 }
4084 self.o1_note_transition();
4085 for li in 0..self.num_layers {
4086 if self.o1_flags.get(li).copied().unwrap_or(false) {
4087 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4088 return Err(err);
4089 }
4090 }
4091 }
4092 Ok(participating
4093 && (0..self.num_layers).all(|li| {
4094 !self.o1_flags.get(li).copied().unwrap_or(false)
4095 || self.kv_cache.layers[li].seq_len == 0
4096 || self.kv_cache.layers[li].o1_sealed()
4097 }))
4098 }
4099
4100 fn o1_progress(&mut self) {
4103 if !self.o1_active() {
4104 return;
4105 }
4106 for li in 0..self.num_layers {
4107 if self.o1_flags.get(li).copied().unwrap_or(false) {
4108 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4109 self.o1_fail(err);
4110 return;
4111 }
4112 }
4113 }
4114 self.o1_note_transition();
4118 if !self.o1_pending() {
4119 return;
4120 }
4121 if let Err(err) = self.o1_seal_checked() {
4122 self.o1_fail(err);
4123 }
4124 }
4125
4126 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4131 if self
4132 .graph_failed
4133 .swap(false, std::sync::atomic::Ordering::Relaxed)
4134 {
4135 self.cancel
4136 .store(false, std::sync::atomic::Ordering::Relaxed);
4137 self.clear_sequence_state();
4138 return Err(format!("{phase}: deferred O(1) transition failed"));
4139 }
4140 Ok(())
4141 }
4142
4143 pub fn o1_seal(&mut self) {
4147 if let Err(err) = self.o1_seal_checked() {
4148 self.o1_fail(err);
4149 }
4150 }
4151
4152 pub fn set_trace(&mut self, on: bool) {
4154 self.trace = on;
4155 }
4156
4157 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4160 self.rng = match config.seed {
4161 Some(seed) => SplitMix64::new(seed),
4162 None => SplitMix64::from_entropy(),
4163 };
4164 self.sampler_config = config;
4165 }
4166
4167 pub fn set_confidence(&mut self, on: bool) {
4172 self.confidence_on = on;
4173 }
4174
4175 pub fn set_calib_temp(&mut self, t: f32) {
4178 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4179 }
4180
4181 pub fn calib_temp(&self) -> f32 {
4183 self.calib_temp
4184 }
4185
4186 pub fn rope_inv_freq(&self) -> &[f32] {
4190 self.inv_freq.as_slice()
4191 }
4192
4193 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4196 self.rotary_dim = rotary_dim.min(self.head_dim);
4197 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4198 self.embryo_graph = None;
4202 }
4203
4204 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4205 QwenAttnCfg {
4206 num_heads: self.num_heads,
4207 num_kv_heads: self.num_kv_heads,
4208 head_dim: self.head_dim,
4209 hidden_size: self.hidden_size,
4210 position,
4211 inv_freq: &self.inv_freq,
4212 rotary_dim: self.rotary_dim,
4213 scale: self.attn_scale,
4214 softcap: self.attn_softcap,
4215 window: None,
4216 v_norm: false,
4217 qk_norm_after_rope: self.qk_norm_after_rope,
4218 gate_sigmoid: self.proj_gate_sigmoid,
4219 q_norm: None,
4220 k_norm: None,
4221 output_gate: false,
4222 softplus_gate: None,
4223 rope_scale: self.rope_scale,
4224 bias: None,
4225 rms_eps: self.rms_eps,
4226 norm_style: self.norm_style,
4227 pool: self.pool.as_deref(),
4228 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4229 }
4230 }
4231
4232 pub fn generate(
4234 &mut self,
4235 prompt: &str,
4236 max_tokens: usize,
4237 task_mask: Option<&TaskMask>,
4238 on_token: Option<TokenCallback>,
4239 ) -> Result<GenerateResult, String> {
4240 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4241 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4242 }
4243
4244 pub fn generate_from_vl(
4247 &mut self,
4248 input: &crate::dsv41_vision::PreparedVlInputs,
4249 max_tokens: usize,
4250 task_mask: Option<&TaskMask>,
4251 on_token: Option<TokenCallback>,
4252 ) -> Result<GenerateResult, String> {
4253 let Some(dsv41) = &self.dsv41 else {
4254 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4255 };
4256 if input.token_ids.is_empty() {
4257 return Err("empty V4.1 multimodal prompt".into());
4258 }
4259 if input.token_types.len() != input.token_ids.len() {
4260 return Err(format!(
4261 "V4.1 token type count {} != token count {}",
4262 input.token_types.len(),
4263 input.token_ids.len()
4264 ));
4265 }
4266 let dim = dsv41.2.dim;
4267 let mut embeddings = vec![None; input.token_ids.len()];
4268 let mut participates = vec![true; input.token_ids.len()];
4269 if !input.images.is_empty() {
4270 let vision = self
4271 .dsv41_vision
4272 .as_ref()
4273 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4274 for image in &input.images {
4275 let end = image.start.saturating_add(image.types.len());
4276 if end > input.token_ids.len() {
4277 return Err(format!(
4278 "V4.1 image span {}..{} exceeds prompt length {}",
4279 image.start,
4280 end,
4281 input.token_ids.len()
4282 ));
4283 }
4284 let mut span = vec![0.0f32; image.types.len() * dim];
4285 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4286 for (offset, &kind) in image.types.iter().enumerate() {
4287 let pos = image.start + offset;
4288 if input.token_types[pos] != kind {
4289 return Err(format!(
4290 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4291 input.token_types[pos]
4292 ));
4293 }
4294 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4295 participates[pos] = false;
4296 }
4297 }
4298 }
4299 for (pos, &kind) in input.token_types.iter().enumerate() {
4300 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4301 return Err(format!("V4.1 text position {pos} has an image embedding"));
4302 }
4303 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4304 return Err(format!("V4.1 image position {pos} has no image embedding"));
4305 }
4306 }
4307 self.dsv41_prefill = Some((embeddings, participates));
4308 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4309 self.dsv41_prefill = None;
4310 result
4311 }
4312
4313 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4315 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4316 }
4317
4318 pub fn generate_from_ids(
4326 &mut self,
4327 input_ids: &[u32],
4328 max_tokens: usize,
4329 task_mask: Option<&TaskMask>,
4330 on_token: Option<TokenCallback>,
4331 ) -> Result<GenerateResult, String> {
4332 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4333 }
4334
4335 pub fn generate_from_embeds(
4341 &mut self,
4342 input_ids: &[u32],
4343 prompt_rows: &[f32],
4344 max_tokens: usize,
4345 task_mask: Option<&TaskMask>,
4346 on_token: Option<TokenCallback>,
4347 ) -> Result<GenerateResult, String> {
4348 if input_ids.is_empty()
4349 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4350 {
4351 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4352 }
4353 if prompt_rows.iter().any(|x| !x.is_finite()) {
4354 return Err("embedded prompt contains non-finite values".into());
4355 }
4356 if !self.can_prefill_batched()
4357 || self.dyn_router.is_some()
4358 || self.o1_active()
4359 || self.mtp.is_some()
4360 || self.gpu_plan.is_some()
4361 {
4362 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4363 }
4364 self.generate_with_prompt_rows(
4365 input_ids,
4366 Some(prompt_rows),
4367 max_tokens,
4368 task_mask,
4369 on_token,
4370 )
4371 }
4372
4373 fn generate_with_prompt_rows(
4374 &mut self,
4375 input_ids: &[u32],
4376 prompt_rows: Option<&[f32]>,
4377 max_tokens: usize,
4378 task_mask: Option<&TaskMask>,
4379 mut on_token: Option<TokenCallback>,
4380 ) -> Result<GenerateResult, String> {
4381 #[cfg(target_os = "macos")]
4382 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4383 if std::env::var("CMF_TRACE_H").is_ok() {
4384 eprintln!("input_ids: {input_ids:?}");
4385 }
4386 if input_ids.is_empty() {
4387 return Err("empty prompt: nothing to generate from".to_string());
4388 }
4389 self.graph_failed
4393 .store(false, std::sync::atomic::Ordering::Relaxed);
4394 let task_mask = self.drop_open_mask(task_mask);
4399
4400 let mut reuse_from = {
4408 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4409 if on
4410 && prompt_rows.is_none()
4411 && task_mask.is_none()
4412 && self.mtp.is_none()
4413 && !(self.mimo_mtp.is_some() && self.speculative)
4414 && self.o1_cfg.is_none()
4415 && self.dsv41.is_none()
4416 {
4417 self.cached_prefix_len(input_ids)
4418 } else {
4419 0
4420 }
4421 };
4422 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4425 reuse_from = 0;
4426 }
4427 self.last_prefill_tokens = input_ids.len() - reuse_from;
4428 let bounded_native = self.bounded_native();
4429 if reuse_from == 0 {
4430 self.clear_sequence_state();
4432 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4433 eprintln!(
4434 "kv-reuse: {} of {} prompt positions already cached",
4435 reuse_from,
4436 input_ids.len()
4437 );
4438 }
4439 crate::gpu::graph_race_begin_generation();
4440 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4444 std::env::var("CMF_O1_PREFILL")
4445 .ok()
4446 .and_then(|v| v.parse::<usize>().ok())
4447 .filter(|&p| p > 0)
4448 } else {
4449 None
4450 };
4451 if task_mask.is_none() {
4452 self.o1_begin_with_prefix(o1_prefill);
4453 }
4454
4455 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4461 #[cfg(target_os = "macos")]
4500 let metal_graph = crate::gpu::q1_force()
4501 && crate::gpu::enabled_here()
4502 && std::env::var("CMF_GPU_BLOCK")
4503 .map(|v| v != "0")
4504 .unwrap_or(true);
4505 #[cfg(not(target_os = "macos"))]
4506 let metal_graph = false;
4507 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4508 let spec_cheap_round =
4512 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4513 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4514 || match spec_sample_env.as_deref() {
4515 Some("1") => true,
4516 Some(_) => false,
4517 None => metal_graph && spec_cheap_round,
4518 };
4519 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4535 for lw in &self.weights.layers {
4536 if let FfnKind::Dense(d) = &lw.ffn {
4537 dense_n += 1;
4538 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4539 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4540 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4541 {
4542 dense_q4tp += 1;
4543 }
4544 }
4545 }
4546 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4547 let penalized = !metal_graph
4559 && (self.sampler_config.repetition_penalty != 1.0
4560 || self.sampler_config.presence_penalty != 0.0
4561 || !self.sampler_config.suppress_tokens.is_empty());
4562 #[cfg(feature = "gpu")]
4567 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4568 #[cfg(not(feature = "gpu"))]
4569 let metal_wgpu = false;
4570 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4571 let spec_wanted = match spec_env.as_deref() {
4572 Some("0") => false,
4573 Some(_) => {
4574 if metal_wgpu {
4575 tracing::warn!(
4576 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4577 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4578 );
4579 }
4580 true
4581 }
4582 None => spec_default_ok && !penalized && !metal_wgpu,
4583 };
4584 let graph_spec = self.speculative
4588 && (graph_on || metal_graph)
4589 && self.mtp.is_some()
4590 && task_mask.is_none()
4591 && !self.o1_active()
4592 && spec_sampling_ok
4593 && spec_wanted;
4594 #[cfg(target_os = "macos")]
4598 if metal_graph {
4599 static SAID: std::sync::Once = std::sync::Once::new();
4600 SAID.call_once(|| {
4601 let spec = if graph_spec {
4602 let k = std::env::var("CMF_GRAPH_SPEC_K")
4603 .ok()
4604 .and_then(|v| v.parse::<usize>().ok())
4605 .filter(|&v| (1..=8).contains(&v))
4606 .unwrap_or(7);
4607 let arm = if self.sampler_config.temperature < 1e-6 {
4608 "greedy"
4609 } else {
4610 "sampling"
4611 };
4612 format!(
4613 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4614 Self::draft_vocab_rows(usize::MAX)
4615 )
4616 } else if !self.speculative {
4617 "spec off (CMF_MTP=0)".to_string()
4618 } else if self.mtp.is_none() {
4619 "spec off (no MTP head)".to_string()
4620 } else if !spec_sampling_ok {
4621 if spec_cheap_round {
4622 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4623 } else {
4624 "spec off (sampling without a top-k: the dense chain \
4625 costs more than it saves)"
4626 .to_string()
4627 }
4628 } else if !spec_wanted {
4629 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4630 } else if task_mask.is_some() {
4631 "spec off (task mask)".to_string()
4632 } else {
4633 "spec off (O(1) attention)".to_string()
4634 };
4635 let on = |var: &str| {
4636 if std::env::var(var).as_deref() == Ok("0") {
4637 "off"
4638 } else {
4639 "on"
4640 }
4641 };
4642 tracing::info!(
4643 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4644 MTP graph {}, attend {}, probe {}",
4645 if crate::gpu_metal::state4_on() {
4646 "on"
4647 } else {
4648 "off"
4649 },
4650 if crate::gpu_metal::async_replay_on() {
4651 "on"
4652 } else {
4653 "off"
4654 },
4655 on("CMF_METAL_PREFILL"),
4656 on("CMF_MTP_GRAPH"),
4657 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4658 if crate::gpu::probe_enabled() {
4659 "bypassed (q1 force)"
4660 } else {
4661 "off"
4662 },
4663 );
4664 });
4665 }
4666 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4673 let spec_active = self.speculative
4674 && self.mtp.is_some()
4675 && task_mask.is_none()
4676 && !self.o1_active()
4677 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4678 let mut mtp = if spec_active { self.mtp.take() } else { None };
4681 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4682 eprintln!(
4683 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4684 mtp.is_some(),
4685 self.speculative,
4686 self.sampler_config.temperature < 1e-6,
4687 );
4688 }
4689 if let Some(m) = &mut mtp {
4690 m.kv.clear();
4691 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4693 self.mtp_graph_mode = None;
4694 }
4695 let mimo_spec = self.speculative
4699 && self.mimo_mtp.is_some()
4700 && task_mask.is_none()
4701 && !self.o1_active()
4702 && self.dyn_router.is_none()
4703 && self.sampler_config.temperature < 1e-6
4704 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4705 if let Some(st) = self.mimo_mtp.as_mut() {
4706 st.reset();
4707 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4708 Self::mimo_mtp_hist_cap(st, input_ids.len());
4709 }
4710 }
4711 let mut router = if mtp.is_none() {
4715 self.dyn_router.take()
4716 } else {
4717 None
4718 };
4719 let mut reuse_from = reuse_from;
4720 if let Some(r) = &mut router {
4721 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4724 let _ = self.set_active_skill(None);
4727 reuse_from = 0;
4728 self.last_prefill_tokens = input_ids.len();
4729 }
4730 }
4731
4732 let mut all_ids = input_ids.to_vec();
4733 let mut generated = 0usize;
4734 let mut finish_reason = "max_tokens".to_string();
4735 let mut drafted = 0usize;
4736 let mut accepted = 0usize;
4737 let mut dsv4_spec_bad = 0usize;
4744 let mut dsv4_spec_retry_at = 0usize;
4745 let mut confidence: Vec<f32> = Vec::new();
4746 let trace_on = self.trace;
4747 let calib_temp = self.calib_temp;
4748 let mut traces: Vec<TokenTrace> = Vec::new();
4749
4750 let mut hidden = vec![0.0f32; self.hidden_size];
4756 let mut pos = reuse_from;
4757 let fuse_lm = mtp.is_none()
4766 && router.is_none()
4767 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4768 self.graph_logits = None;
4769 self.graph_want_logits = false;
4770 let _tpf = std::time::Instant::now();
4771 let batch_k = self.generation_batch_k();
4772 if let Some(rows) = prompt_rows {
4773 let hs = self.hidden_size;
4774 let chunk = self.prefill_chunk().max(1);
4775 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4776 let end = (pos + chunk).min(input_ids.len());
4777 let hb = match self.prefill_input_rows(
4778 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
4779 pos,
4780 task_mask,
4781 ) {
4782 Ok(hb) => hb,
4783 Err(err) => {
4784 self.finish_generation(&mut mtp, &mut router, true);
4785 return Err(err);
4786 }
4787 };
4788 if mimo_spec {
4789 self.mimo_note_rows(&hb, pos);
4790 }
4791 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4792 pos = end;
4793 }
4794 }
4795 while self.qwen4_exp.is_some()
4806 && mtp.is_none()
4807 && pos < input_ids.len()
4808 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4809 {
4810 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4813 let want_logits = end == input_ids.len();
4814 let mut lg = Vec::new();
4815 if let Some(b) = &mut self.qwen4_exp {
4816 crate::qwen4_exp::forward_tokens(
4817 &b.0,
4818 &b.1,
4819 &b.2,
4820 &mut b.3,
4821 &input_ids[pos..end],
4822 pos,
4823 &self.inv_freq,
4824 self.pool.as_deref(),
4825 &mut lg,
4826 want_logits,
4827 );
4828 }
4829 if want_logits {
4830 self.graph_logits = Some(lg);
4831 }
4832 pos = end;
4833 hidden.fill(0.0);
4834 }
4835 while self.dsv4.is_some()
4836 && mtp.is_none()
4837 && pos < input_ids.len()
4838 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4839 {
4840 let end = (pos + prefill_chunk()).min(input_ids.len());
4841 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4842 let mut lg = Vec::new();
4843 if let Some(b) = &mut self.dsv4 {
4844 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4845 crate::dsv4::forward_chunk(
4846 g,
4847 layers,
4848 &cfg,
4849 st,
4850 &ids,
4851 pos,
4852 &self.inv_freq,
4853 self.pool.as_deref(),
4854 &mut lg,
4855 end == input_ids.len(),
4856 );
4857 }
4858 if end == input_ids.len() {
4859 self.graph_logits = Some(lg);
4860 }
4861 pos = end;
4862 hidden = vec![0.0; self.hidden_size];
4863 }
4864 let dsv41_prefill = self.dsv41_prefill.take();
4865 while self.dsv41.is_some()
4866 && mtp.is_none()
4867 && pos < input_ids.len()
4868 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4869 {
4870 let end = (pos + prefill_chunk()).min(input_ids.len());
4871 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4872 let mut lg = Vec::new();
4873 if let Some(b) = &mut self.dsv41 {
4874 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4875 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4876 crate::dsv41::forward_chunk_masked_with_embeddings(
4877 g,
4878 layers,
4879 cfg,
4880 st,
4881 &ids,
4882 pos,
4883 &embeddings[pos..end],
4884 &participates[pos..end],
4885 self.pool.as_deref(),
4886 &mut lg,
4887 );
4888 } else {
4889 crate::dsv41::forward_chunk(
4890 g,
4891 layers,
4892 cfg,
4893 st,
4894 &ids,
4895 pos,
4896 self.pool.as_deref(),
4897 &mut lg,
4898 );
4899 }
4900 }
4901 if end == input_ids.len() {
4902 self.graph_logits = Some(lg);
4903 }
4904 pos = end;
4905 hidden = vec![0.0; self.hidden_size];
4906 }
4907 let dyn_prefill = router.is_some();
4912 let o1_prefill_limit = o1_prefill
4920 .and_then(|requested| self.o1_effective_boundary(requested))
4921 .map(|boundary| boundary.min(input_ids.len()));
4922 let mut o1_sealed = false;
4923 if let Some(limit) = o1_prefill_limit {
4924 if self.can_prefill_batched() && limit > 2 {
4927 let chunk = self.prefill_chunk();
4928 let hs = self.hidden_size;
4929 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4930 let end = (pos + chunk).min(limit);
4931 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4932 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4933 pos = end;
4934 }
4935 } else {
4936 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4937 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4938 pos += 1;
4939 }
4940 }
4941 if pos >= limit {
4942 o1_sealed = match self.o1_seal_checked() {
4943 Ok(sealed) => sealed,
4944 Err(err) => {
4945 self.finish_generation(&mut mtp, &mut router, true);
4946 return Err(err);
4947 }
4948 };
4949 tracing::info!(
4950 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4951 o1_prefill.unwrap_or(0),
4952 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4953 .unwrap_or(limit),
4954 limit,
4955 input_ids.len()
4956 );
4957 }
4958 }
4959 let graph_prefill = self.graph_prefill_preferred();
4965 #[cfg(target_os = "macos")]
4973 if task_mask.is_none()
4974 && !dyn_prefill
4975 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4976 && crate::gpu::enabled_here()
4977 && self.gdn_cfg.is_some()
4978 && self.g3n.is_none()
4979 && input_ids.len() > 8
4980 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4981 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4982 {
4983 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4984 .ok()
4985 .and_then(|v| v.parse().ok())
4986 .filter(|&v| (16..=512).contains(&v))
4987 .unwrap_or(256);
4988 let hs = self.hidden_size;
4989 let _tp = std::time::Instant::now();
4990 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4991 let end = (pos + chunk).min(input_ids.len());
4992 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4993 MetalPrefillOutcome::Completed(hb) => hb,
4994 MetalPrefillOutcome::Declined => break,
4995 MetalPrefillOutcome::Failed => {
4996 self.finish_generation(&mut mtp, &mut router, true);
4997 return Err("ordinary Metal prefill failed after admission".into());
4998 }
4999 };
5000 if let Some(m) = &mut mtp {
5001 let n_pairs = if end < input_ids.len() {
5002 end - pos
5003 } else {
5004 end - pos - 1
5005 };
5006 if n_pairs > 0 {
5007 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5008 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5009 .collect();
5010 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5011 for (j, (h, t)) in pairs.iter().enumerate() {
5012 let h = h.to_vec();
5013 let _ = self.mtp_step(m, &h, *t, pos + j);
5014 }
5015 }
5016 }
5017 }
5018 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5019 pos = end;
5020 }
5021 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5022 eprintln!(
5023 "metal-prefill: {} of {} tokens in {:.1} ms",
5024 pos,
5025 input_ids.len(),
5026 _tp.elapsed().as_secs_f64() * 1e3
5027 );
5028 }
5029 }
5030 #[cfg(not(target_os = "macos"))]
5035 if task_mask.is_none()
5036 && !dyn_prefill
5037 && mtp.is_none()
5038 && o1_prefill.is_none()
5039 && batch_k > 0
5040 && pos + 1 < input_ids.len()
5041 && self.g4_batch_prefill()
5042 {
5043 let t_g4 = std::time::Instant::now();
5044 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5045 Ok((n, last)) => {
5046 if n > 0 {
5047 hidden.copy_from_slice(&last);
5048 }
5049 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5050 eprintln!(
5051 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5052 input_ids.len() - pos,
5053 t_g4.elapsed().as_secs_f64() * 1e3
5054 );
5055 }
5056 pos += n;
5057 }
5058 Err(e) => {
5059 self.finish_generation(&mut mtp, &mut router, true);
5060 return Err(e);
5061 }
5062 }
5063 }
5064 self.mimo_moe_prepare();
5065 #[cfg(not(target_os = "macos"))]
5071 if task_mask.is_none()
5072 && !dyn_prefill
5073 && !graph_prefill
5074 && mtp.is_none()
5075 && o1_prefill.is_none()
5076 && !self.o1_active()
5077 && input_ids.len() > 2
5078 && self.batch_prefix_prefill()
5079 {
5080 let chunk = self.prefill_chunk().max(1);
5081 let hs = self.hidden_size;
5082 let t_bp = std::time::Instant::now();
5083 let pos0 = pos;
5084 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5085 let end = (pos + chunk).min(input_ids.len());
5086 let bk = end - pos;
5087 let mut hiddens = vec![0f32; bk * hs];
5088 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5089 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5090 }
5091 let positions: Vec<usize> = (pos..end).collect();
5092 let mut run = 0usize;
5093 let outcome = self.try_batch_graph_wgpu_prefix(
5094 &mut hiddens,
5095 &positions,
5096 bk,
5097 None,
5098 Some(&mut run),
5099 );
5100 match outcome {
5101 crate::gpu::BatchGraphOutcome::Completed => {
5102 let hb = if run < self.num_layers {
5103 self.prefill_batch_span(
5104 PrefillIn::Hidden(&hiddens),
5105 pos,
5106 None,
5107 run,
5108 self.num_layers,
5109 )
5110 } else {
5111 hiddens
5112 };
5113 if mimo_spec {
5114 self.mimo_note_rows(&hb, pos);
5115 }
5116 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5117 pos = end;
5118 }
5119 crate::gpu::BatchGraphOutcome::Failed => {
5120 self.finish_generation(&mut mtp, &mut router, true);
5121 return Err("batched prefix prefill failed after admission".into());
5122 }
5123 crate::gpu::BatchGraphOutcome::Declined => {
5124 #[cfg(feature = "gpu")]
5127 if pos > pos0 {
5128 self.pull_lagging_host_kv(0, self.num_layers, pos);
5129 }
5130 break;
5131 }
5132 }
5133 }
5134 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5135 eprintln!(
5136 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5137 pos - pos0,
5138 input_ids.len(),
5139 t_bp.elapsed().as_secs_f64() * 1e3
5140 );
5141 }
5142 }
5143 if task_mask.is_none()
5144 && !dyn_prefill
5145 && !graph_prefill
5146 && self.can_prefill_batched()
5147 && self.g3n.is_none()
5148 && o1_prefill.is_none()
5149 && input_ids.len() > 2
5150 {
5151 let chunk = self.prefill_chunk();
5157 let hs = self.hidden_size;
5158 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5159 let end = (pos + chunk).min(input_ids.len());
5160 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5161 if mimo_spec {
5162 self.mimo_note_rows(&hb, pos);
5163 }
5164 if let Some(m) = &mut mtp {
5165 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5166 .ok()
5167 .and_then(|v| v.parse().ok())
5168 .unwrap_or(0);
5169 for p in pos..end {
5170 if p + 1 < input_ids.len() {
5171 if probe >= 1 && p + 2 < input_ids.len() {
5172 let (d1, mut hx) = self.mtp_step_h(
5176 m,
5177 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5178 input_ids[p + 1],
5179 p,
5180 );
5181 let mut ok = d1 == input_ids[p + 2];
5182 Self::chain_probe_note(0, ok);
5183 let mut d_prev = d1;
5184 let mut extra = 0usize;
5185 for j in 1..probe {
5186 if p + 2 + j >= input_ids.len() {
5187 break;
5188 }
5189 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5190 extra += 1;
5191 ok = ok && dj == input_ids[p + 2 + j];
5192 Self::chain_probe_note(j, ok);
5193 d_prev = dj;
5194 hx = hj;
5195 }
5196 m.kv.truncate_last(extra);
5197 } else {
5198 let _ = self.mtp_step(
5199 m,
5200 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5201 input_ids[p + 1],
5202 p,
5203 );
5204 }
5205 }
5206 }
5207 }
5208 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5209 pos = end;
5210 }
5211 }
5212 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5213 if task_mask.is_none()
5214 && !dyn_prefill
5215 && !graph_prefill
5216 && !pair_off
5217 && self.pair_supported()
5218 && o1_prefill.is_none()
5219 {
5220 while pos + 1 < input_ids.len()
5221 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5222 {
5223 let e1 = self.embed_single(input_ids[pos]);
5224 let e2 = self.embed_single(input_ids[pos + 1]);
5225 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5226 if mimo_spec {
5227 self.mimo_note_rows(&h1, pos);
5228 self.mimo_note_rows(&h2, pos + 1);
5229 }
5230 self.commit_linear_scratch();
5232 if let Some(m) = &mut mtp {
5233 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5234 if pos + 2 < input_ids.len() {
5235 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5236 .ok()
5237 .and_then(|v| v.parse().ok())
5238 .unwrap_or(0);
5239 if probe >= 1 && pos + 3 < input_ids.len() {
5240 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5244 let mut ok = d1 == input_ids[pos + 3];
5245 Self::chain_probe_note(0, ok);
5246 let mut d_prev = d1;
5247 let mut extra = 0usize;
5248 for j in 1..probe {
5249 if pos + 3 + j >= input_ids.len() {
5250 break;
5251 }
5252 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5253 extra += 1;
5254 ok = ok && dj == input_ids[pos + 3 + j];
5255 Self::chain_probe_note(j, ok);
5256 d_prev = dj;
5257 hx = hj;
5258 }
5259 m.kv.truncate_last(extra);
5260 } else {
5261 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5262 }
5263 }
5264 }
5265 hidden = h2;
5266 pos += 2;
5267 }
5268 }
5269 let o1_batch_ready = o1_sealed
5282 && o1_prefill.is_some()
5283 && mtp.is_none()
5284 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5285 && (0..self.num_layers).all(|li| {
5286 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5287 cache.o1.is_none() || cache.o1_views().is_some()
5288 });
5289 let mtp_batch_prefill = mtp.is_some()
5294 && graph_prefill
5295 && task_mask.is_none()
5296 && !dyn_prefill
5297 && !self.o1_active()
5298 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5299 if batch_k > 0
5300 && (graph_prefill || o1_batch_ready)
5301 && task_mask.is_none()
5302 && (!self.o1_active() || o1_batch_ready)
5303 && (mtp.is_none() || mtp_batch_prefill)
5304 && !dyn_prefill
5305 && pos + 1 < input_ids.len()
5306 {
5307 let hs = self.hidden_size;
5308 let chunk = batch_k;
5309 while pos < input_ids.len() {
5310 let end = (pos + chunk).min(input_ids.len());
5311 let bk = end - pos;
5312 let mut hiddens = vec![0f32; bk * hs];
5313 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5314 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5315 }
5316 let positions: Vec<usize> = (pos..end).collect();
5317 let t_chunk = std::time::Instant::now();
5318 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5319 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5320 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5321 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5322 eprintln!(
5323 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5324 if o1_batch_ready {
5325 "o1"
5326 } else if mtp_batch_prefill {
5327 "ordinary_mtp"
5328 } else {
5329 "ordinary"
5330 },
5331 bk as f64 / (ms / 1000.0)
5332 );
5333 }
5334 {
5335 use std::sync::atomic::{AtomicBool, Ordering};
5336 static SAID: AtomicBool = AtomicBool::new(false);
5337 if !SAID.swap(true, Ordering::Relaxed) {
5338 if ok_b {
5339 tracing::info!(
5340 "batched prefill: ACTIVE mode={} (k={bk})",
5341 if o1_batch_ready {
5342 "o1"
5343 } else if mtp_batch_prefill {
5344 "ordinary_mtp"
5345 } else {
5346 "ordinary"
5347 }
5348 );
5349 } else {
5350 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5351 }
5352 }
5353 }
5354 if ok_b {
5355 if mimo_spec {
5356 self.mimo_note_rows(&hiddens, pos);
5357 }
5358 if mtp_batch_prefill {
5359 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5360 if n_pairs > 0 {
5361 let rows: Vec<Vec<f32>> = (0..n_pairs)
5367 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5368 .collect();
5369 let pairs: Vec<(&[f32], u32)> = rows
5370 .iter()
5371 .enumerate()
5372 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5373 .collect();
5374 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5375 eprintln!(
5376 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5377 pos,
5378 n_pairs,
5379 pos + n_pairs - 1,
5380 );
5381 }
5382 let warm_error = if let Some(m) = mtp.as_mut() {
5383 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5384 } else {
5385 None
5386 };
5387 if let Some(err) = warm_error {
5388 self.finish_generation(&mut mtp, &mut router, true);
5393 return Err(err.to_string());
5394 }
5395 }
5396 }
5397 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5398 pos = end;
5399 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5400 self.finish_generation(&mut mtp, &mut router, true);
5405 return Err(if o1_batch_ready {
5406 "sealed O(1) batch graph failed after admission".to_string()
5407 } else {
5408 "ordinary recurrent batch graph failed after admission".to_string()
5409 });
5410 } else {
5411 break; }
5413 }
5414 }
5415 if graph_prefill
5419 && task_mask.is_none()
5420 && mtp.is_none()
5421 && !dyn_prefill
5422 && pos == 0
5423 && input_ids.len() > 1
5424 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5425 {
5426 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5427 self.graph_logits = Some(lg);
5428 hidden = vec![0.0; self.hidden_size];
5429 pos = input_ids.len();
5430 }
5431 }
5432 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5433 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5434 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5435 if mimo_spec {
5436 self.mimo_note_rows(&hidden, pos);
5437 }
5438 if let Some(m) = &mut mtp {
5439 if pos + 1 < input_ids.len() {
5440 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5446 .ok()
5447 .and_then(|v| v.parse().ok())
5448 .unwrap_or(0);
5449 if probe >= 1 && pos + 2 < input_ids.len() {
5450 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5451 let mut ok = d1 == input_ids[pos + 2];
5452 Self::chain_probe_note(0, ok);
5453 let mut d_prev = d1;
5454 let mut extra = 0usize;
5455 for j in 1..probe {
5456 if pos + 2 + j >= input_ids.len() {
5457 break;
5458 }
5459 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5460 extra += 1;
5461 ok = ok && dj == input_ids[pos + 2 + j];
5462 Self::chain_probe_note(j, ok);
5463 d_prev = dj;
5464 hx = hj;
5465 }
5466 m.kv.truncate_last(extra);
5469 } else {
5470 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5471 }
5472 }
5473 }
5474 pos += 1;
5475 }
5476 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5477 eprintln!(
5478 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5479 input_ids.len(),
5480 _tpf.elapsed().as_secs_f64() * 1000.0
5481 );
5482 }
5483 if self
5484 .graph_failed
5485 .swap(false, std::sync::atomic::Ordering::Relaxed)
5486 {
5487 self.finish_generation(&mut mtp, &mut router, true);
5492 return Err("GPU token graph failed during prefill".to_string());
5493 }
5494 if self
5497 .cancel
5498 .swap(false, std::sync::atomic::Ordering::Relaxed)
5499 {
5500 self.finish_generation(&mut mtp, &mut router, true);
5504 return Ok(GenerateResult {
5505 text: String::new(),
5506 token_ids: Vec::new(),
5507 prompt_tokens: input_ids.len(),
5508 tokens_generated: 0,
5509 finish_reason: "cancelled".to_string(),
5510 mtp_drafted: 0,
5511 mtp_accepted: 0,
5512 token_confidence: Vec::new(),
5513 traces: Vec::new(),
5514 });
5515 }
5516
5517 if !o1_sealed {
5520 match self.o1_seal_checked() {
5521 Ok(_) => {}
5522 Err(err) => {
5523 self.finish_generation(&mut mtp, &mut router, true);
5524 return Err(err);
5525 }
5526 }
5527 }
5528
5529 macro_rules! commit {
5531 ($id:expr) => {{
5532 all_ids.push($id);
5533 generated += 1;
5534 self.note_draft_id($id);
5535 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5536 finish_reason = "stop".to_string();
5537 false
5538 } else {
5539 let token_text = self.tokenizer.decode_token($id);
5540 let mut go = true;
5541 if let Some(ref mut cb) = on_token {
5542 if !cb(&token_text) {
5543 finish_reason = "cancelled".to_string();
5544 go = false;
5545 }
5546 }
5547 go
5548 }
5549 }};
5550 }
5551
5552 let mut spec_trial = SpecTrial::Spec {
5563 t0: std::time::Instant::now(),
5564 gen0: generated,
5565 rounds: 0,
5566 };
5567 let mut spec_mon = SpecMon {
5573 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5574 ..SpecMon::default()
5575 };
5576 let mut spec_watchdog_off = false;
5577 let mut spec_walls: Vec<f32> = Vec::new();
5580 let mut spec_round_end: Option<std::time::Instant> = None;
5583 if mimo_spec {
5584 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5585 if let Some(mut st) = self.mimo_mtp.take() {
5586 self.mimo_mtp_probe(&mut st, input_ids, &path);
5587 self.mimo_mtp = Some(st);
5588 }
5589 }
5590 }
5591 let mut next_pos = input_ids.len();
5593 'decode: while generated < max_tokens {
5594 if self
5595 .graph_failed
5596 .swap(false, std::sync::atomic::Ordering::Relaxed)
5597 {
5598 self.finish_generation(&mut mtp, &mut router, true);
5603 return Err("GPU token graph failed during decode".to_string());
5604 }
5605 if self
5606 .cancel
5607 .swap(false, std::sync::atomic::Ordering::Relaxed)
5608 {
5609 finish_reason = "cancelled".to_string();
5610 break 'decode;
5611 }
5612 if mimo_spec && next_pos > 0 {
5617 self.mimo_note_rows(&hidden, next_pos - 1);
5620 }
5621 let forced = self.spec_forced.take();
5622 let mut logits = match (forced, self.graph_logits.take()) {
5623 (Some(_), _) => Vec::new(),
5624 (None, Some(lg)) => lg,
5625 (None, None) => {
5626 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5627 inference::rms_norm_into(
5628 &hidden,
5629 &self.weights.final_norm,
5630 self.rms_eps,
5631 self.norm_style,
5632 &mut self.ws.n1,
5633 );
5634 self.lm_head_forward(&self.ws.n1)
5635 }
5636 };
5637 if generated
5640 == std::env::var("CMF_LOGIT_DUMP_STEP")
5641 .ok()
5642 .and_then(|v| v.parse().ok())
5643 .unwrap_or(0)
5644 {
5645 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5646 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5647 for v in hidden.iter().chain(logits.iter()) {
5648 bytes.extend_from_slice(&v.to_le_bytes());
5649 }
5650 if let Err(e) = std::fs::write(&path, &bytes) {
5651 eprintln!("logit dump: failed to write {path}: {e}");
5652 self.finish_generation(&mut mtp, &mut router, true);
5653 return Err(format!("logit dump write failed: {e}"));
5654 }
5655 }
5656 }
5657 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5661 if !logits.is_empty() {
5662 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5663 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5664 if let Err(e) =
5665 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5666 {
5667 eprintln!("logit dump: failed to write {}: {e}", path.display());
5668 }
5669 }
5670 }
5671 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5672 Some(c) => c,
5673 None => {
5674 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5675 sampler::sample_with_scratch_pool(
5676 &logits,
5677 &self.sampler_config,
5678 self.sampler_config.penalty_past(&all_ids, bounded_native),
5679 &mut self.rng,
5680 &mut self.sampler_scratch,
5681 self.pool.as_deref(),
5682 )
5683 }
5684 };
5685 if self.confidence_on {
5686 confidence.push(if logits.is_empty() {
5687 0.0
5688 } else {
5689 sampler::top1_prob_pool(
5690 self.pool.as_deref(),
5691 &mut self.sampler_scratch,
5692 &logits,
5693 t_next,
5694 calib_temp,
5695 )
5696 });
5697 }
5698 if !logits.is_empty() {
5699 attention::recycle_buf(&mut logits);
5700 }
5701 if trace_on {
5702 let skill = router.as_ref().and_then(|r| r.active_id());
5706 traces.push(TokenTrace {
5707 t: generated,
5708 token_id: t_next,
5709 confidence: confidence.last().copied().unwrap_or(0.0),
5710 active_skill: skill,
5711 recon: None,
5712 switched: false,
5713 });
5714 }
5715 if !commit!(t_next) {
5716 break 'decode;
5717 }
5718 if generated >= max_tokens {
5719 break 'decode;
5720 }
5721
5722 self.swa_trim_tails();
5725 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5726 static SAID: std::sync::Once = std::sync::Once::new();
5732 SAID.call_once(|| {
5733 tracing::warn!(
5734 "KV cache full at {} positions — evicting half; quality \
5735 will degrade. Raise CMF_MAX_SEQ.",
5736 self.kv_cache.max_seq_len,
5737 );
5738 });
5739 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5740 self.kv_cache.evict(keep);
5741 }
5742
5743 if graph_spec {
5746 match spec_trial {
5747 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5748 spec_mon.plain_ms =
5749 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5750 let keep = spec_mon.pays();
5751 tracing::info!(
5752 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5753 spec_mon.tokens,
5754 spec_mon.round_ms,
5755 spec_mon.plain_ms,
5756 if keep { "speculating" } else { "plain" }
5757 );
5758 spec_mon.fails = 0;
5759 spec_trial = SpecTrial::Decided {
5760 spec: keep,
5761 recheck_at: if keep { usize::MAX } else { generated + 128 },
5762 };
5763 }
5764 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5765 spec_mon.n = 0;
5766 spec_trial = SpecTrial::Spec {
5767 t0: std::time::Instant::now(),
5768 gen0: generated,
5769 rounds: 0,
5770 };
5771 }
5772 _ => {}
5773 }
5774 spec_watchdog_off = matches!(
5775 spec_trial,
5776 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5777 );
5778 }
5779 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5781 let budget = max_tokens - generated - 1;
5782 if let Some(mut st) = self.mimo_mtp.take() {
5783 let k = st.depth.min(budget);
5784 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5785 self.mimo_mtp = Some(st);
5786 let r = match r {
5787 Ok(r) => r,
5788 Err(err) => {
5789 self.finish_generation(&mut mtp, &mut router, true);
5790 return Err(err);
5791 }
5792 };
5793 if let Some(r) = r {
5794 drafted += r.drafted;
5795 accepted += r.accepted.len();
5796 let mut stopped = false;
5797 for &id in &r.accepted {
5798 if self.confidence_on {
5799 confidence.push(0.0);
5800 }
5801 if !commit!(id) {
5802 stopped = true;
5803 break;
5804 }
5805 }
5806 if stopped {
5807 break 'decode;
5808 }
5809 next_pos += r.accepted.len() + 1;
5810 hidden = r.hidden;
5811 self.graph_logits = Some(r.logits);
5814 continue 'decode;
5815 }
5816 }
5817 }
5818 #[cfg(feature = "gpu")]
5821 if self.speculative
5822 && self.qwen4_exp.is_some()
5823 && task_mask.is_none()
5824 && self.sampler_config.temperature < 1e-6
5825 && generated + 1 < max_tokens
5826 && next_pos > 0
5827 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5828 {
5829 let r = match &mut self.qwen4_exp {
5830 Some(b) => crate::qwen4_exp::spec_round(
5831 &b.0,
5832 &b.1,
5833 &b.2,
5834 &mut b.3,
5835 next_pos,
5836 &all_ids,
5837 &self.inv_freq,
5838 self.pool.as_deref(),
5839 ),
5840 None => None,
5841 };
5842 if let Some(r) = r {
5843 drafted += r.drafted;
5844 accepted += r.accepted.len();
5845 let mut stopped = false;
5846 for &id in &r.accepted {
5847 if self.confidence_on {
5848 confidence.push(0.0);
5849 }
5850 if !commit!(id) {
5851 stopped = true;
5852 break;
5853 }
5854 }
5855 if stopped {
5856 break 'decode;
5857 }
5858 next_pos += r.accepted.len() + 1;
5859 hidden.fill(0.0);
5860 self.graph_logits = Some(r.logits);
5861 continue 'decode;
5862 }
5863 }
5864 match &mut mtp {
5865 #[cfg(feature = "gpu")]
5867 Some(m)
5868 if graph_spec
5869 && !spec_watchdog_off
5870 && generated + 1 < max_tokens
5871 && next_pos > 0 =>
5872 {
5873 let t_round = std::time::Instant::now();
5874 if spec_time_level() >= 2 {
5875 if let Some(t) = spec_round_end.take() {
5876 eprintln!(
5877 "spec-gap {:.2} ms (host between rounds)",
5878 t.elapsed().as_secs_f64() * 1e3
5879 );
5880 }
5881 }
5882 spec_stamps_begin();
5883 #[cfg(target_os = "macos")]
5888 let allocs0 =
5889 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
5890 #[cfg(not(target_os = "macos"))]
5891 let allocs0 = 0u64;
5892 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5893 m,
5894 &hidden,
5895 t_next,
5896 next_pos,
5897 &mut drafted,
5898 &mut accepted,
5899 &mut all_ids,
5900 max_tokens - generated,
5901 ) {
5902 next_pos = n_pos;
5903 hidden = new_h;
5904 let level = spec_time_level();
5905 if level > 0 {
5906 let wall = t_round.elapsed().as_secs_f32() * 1e3;
5907 let stamps = spec_stamps_take();
5908 let median = if spec_walls.len() >= 3 {
5911 let mut s = spec_walls.clone();
5912 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5913 Some(s[s.len() / 2])
5914 } else {
5915 None
5916 };
5917 let outlier = median.is_some_and(|m| wall > 1.4 * m);
5918 #[cfg(target_os = "macos")]
5919 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5920 .load(std::sync::atomic::Ordering::Relaxed)
5921 - allocs0;
5922 #[cfg(not(target_os = "macos"))]
5923 let allocs = allocs0;
5924 eprintln!(
5925 "spec-round wall {wall:.1} ms → {} tokens{}{}",
5926 extra.len() + 1,
5927 if allocs > 0 {
5928 format!(" [{allocs} new device buffers]")
5929 } else {
5930 String::new()
5931 },
5932 match (outlier, median) {
5933 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5934 _ => String::new(),
5935 }
5936 );
5937 if level >= 2 || outlier {
5938 let sum: f32 = stamps.iter().map(|s| s.1).sum();
5939 eprintln!(
5940 "spec-stamps: {}| untracked {:.1}",
5941 spec_stamps_format(&stamps),
5942 wall - sum
5943 );
5944 }
5945 if spec_mon.n >= 1 {
5946 spec_walls.push(wall);
5947 }
5948 }
5949 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5953 spec_trial = Self::spec_trial_round(
5956 spec_trial,
5957 &mut spec_mon,
5958 generated + extra.len() + 1,
5959 );
5960 let mut stopped = false;
5961 for &id in &extra {
5962 if self.confidence_on {
5963 confidence.push(0.0);
5964 }
5965 if !commit!(id) {
5966 stopped = true;
5967 break;
5968 }
5969 }
5970 if stopped {
5971 break 'decode;
5972 }
5973 if spec_time_level() >= 2 {
5974 spec_round_end = Some(std::time::Instant::now());
5975 }
5976 continue 'decode;
5977 }
5978 if self
5979 .graph_failed
5980 .swap(false, std::sync::atomic::Ordering::Relaxed)
5981 {
5982 self.finish_generation(&mut mtp, &mut router, true);
5988 return Err("GPU MTP graph failed during speculative decode".to_string());
5989 }
5990 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6001 spec_mon.tokens = 0.0;
6002 spec_mon.fails = 3;
6003 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6004 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6005 next_pos += 1;
6006 continue 'decode;
6007 }
6008 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6010 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6011 drafted += 1;
6012 let emb1 = self.embed_single(t_next);
6013 let emb2 = self.embed_single(draft);
6014 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6015
6016 inference::rms_norm_into(
6017 &h1,
6018 &self.weights.final_norm,
6019 self.rms_eps,
6020 self.norm_style,
6021 &mut self.ws.n1,
6022 );
6023 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6024 let t_after = sampler::sample_with_scratch_pool(
6025 &logits1,
6026 &self.sampler_config,
6027 self.sampler_config.penalty_past(&all_ids, bounded_native),
6028 &mut self.rng,
6029 &mut self.sampler_scratch,
6030 self.pool.as_deref(),
6031 );
6032 if self.confidence_on {
6033 confidence.push(sampler::top1_prob_pool(
6034 self.pool.as_deref(),
6035 &mut self.sampler_scratch,
6036 &logits1,
6037 t_after,
6038 calib_temp,
6039 ));
6040 }
6041 attention::recycle_buf(&mut logits1);
6042 if trace_on {
6043 traces.push(TokenTrace {
6046 t: generated,
6047 token_id: t_after,
6048 confidence: confidence.last().copied().unwrap_or(0.0),
6049 active_skill: None,
6050 recon: None,
6051 switched: false,
6052 });
6053 }
6054 let stop = !commit!(t_after);
6055
6056 if t_after == draft {
6057 accepted += 1;
6058 self.commit_linear_scratch();
6059 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6060 hidden = h2;
6061 next_pos += 2;
6062 } else {
6063 for layer in &mut self.kv_cache.layers {
6065 layer.truncate_last(1);
6066 }
6067 if !stop {
6068 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6069 hidden = self.forward_layers(
6070 &self.embed_single(t_after),
6071 next_pos + 1,
6072 None,
6073 );
6074 }
6075 next_pos += 2;
6076 }
6077 if stop {
6078 break 'decode;
6079 }
6080 }
6081 _ => {
6083 #[cfg(feature = "gpu")]
6088 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6089 static SAID: std::sync::Once = std::sync::Once::new();
6090 SAID.call_once(|| {
6091 eprintln!(
6092 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6093 !self.dsv4_mtp.is_empty(),
6094 task_mask.is_none(),
6095 router.is_none(),
6096 !trace_on,
6097 self.sampler_config.temperature < 1e-6,
6098 self.sampler_config.repetition_penalty == 1.0,
6099 );
6100 });
6101 }
6102 #[cfg(feature = "gpu")]
6103 if Self::dsv4_spec_on()
6104 && self.dsv4.is_some()
6105 && !self.dsv4_mtp.is_empty()
6106 && task_mask.is_none()
6107 && router.is_none()
6108 && !trace_on
6109 && self.sampler_config.temperature < 1e-6
6110 && self.sampler_config.repetition_penalty == 1.0
6111 && generated + 1 < max_tokens
6112 && all_ids.len() >= 2
6113 && generated >= dsv4_spec_retry_at
6114 {
6115 let tip_token = all_ids[all_ids.len() - 2];
6116 let drafted0 = drafted;
6117 let round = self.dsv4_spec_step(
6118 tip_token,
6119 t_next,
6120 next_pos,
6121 max_tokens.saturating_sub(generated),
6122 &mut drafted,
6123 &mut accepted,
6124 );
6125 if drafted > drafted0 {
6126 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6127 if useful {
6128 dsv4_spec_bad = 0;
6129 } else {
6130 dsv4_spec_bad += 1;
6131 if dsv4_spec_bad >= 2 {
6132 dsv4_spec_bad = 0;
6133 dsv4_spec_retry_at = generated.saturating_add(32);
6134 tracing::info!(
6135 "dsv4: draft не окупился дважды — точный walk на 32 токена"
6136 );
6137 }
6138 }
6139 }
6140 if let Some((extra, n_pos)) = round {
6141 next_pos = n_pos;
6142 let mut stopped = false;
6143 for &id in &extra {
6144 if self.confidence_on {
6145 confidence.push(0.0);
6146 }
6147 if !commit!(id) {
6148 stopped = true;
6149 break;
6150 }
6151 }
6152 if stopped {
6153 break 'decode;
6154 }
6155 continue 'decode;
6156 }
6157 }
6158 self.graph_want_logits = fuse_lm;
6159 let mut t_fwd = t_next;
6165 let pure_greedy = self.sampler_config.temperature < 1e-6
6166 && self.sampler_config.repetition_penalty == 1.0
6167 && self.sampler_config.presence_penalty == 0.0
6168 && self.sampler_config.suppress_tokens.is_empty();
6169 let burst_k = std::env::var("CMF_MULTISTEP")
6174 .ok()
6175 .and_then(|v| v.parse::<usize>().ok())
6176 .unwrap_or(0);
6177 if pure_greedy
6178 && burst_k >= 1
6179 && fuse_lm
6180 && task_mask.is_none()
6181 && router.is_none()
6182 && !trace_on
6183 && !self.confidence_on
6184 && std::env::var_os("CMF_FORCE_IDS").is_none()
6189 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6190 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6191 {
6192 let mut stopped = false;
6193 loop {
6194 if self
6199 .cancel
6200 .swap(false, std::sync::atomic::Ordering::Relaxed)
6201 {
6202 finish_reason = "cancelled".to_string();
6203 stopped = true;
6204 break;
6205 }
6206 let room = max_tokens.saturating_sub(generated);
6207 if room <= 2 {
6208 break;
6209 }
6210 let k = burst_k.min(room - 1);
6211 if k < 1 {
6212 break;
6213 }
6214 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6215 if self
6216 .graph_failed
6217 .swap(false, std::sync::atomic::Ordering::Relaxed)
6218 {
6219 self.finish_generation(&mut mtp, &mut router, true);
6220 return Err(
6221 "GPU token graph failed during greedy burst".to_string()
6222 );
6223 }
6224 break;
6225 };
6226 next_pos += k;
6227 for &id in &ids {
6228 if !commit!(id) {
6229 stopped = true;
6230 break;
6231 }
6232 }
6233 if stopped {
6234 break;
6235 }
6236 t_fwd = *ids.last().unwrap();
6237 }
6238 if stopped {
6239 break 'decode;
6240 }
6241 }
6242 #[cfg(target_os = "macos")]
6252 if graph_spec
6253 && spec_watchdog_off
6254 && next_pos > 0
6255 && self.mtp_graph_mode == Some(true)
6256 && crate::gpu::q1_force()
6257 {
6258 if let Some(m) = mtp.as_mut() {
6259 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6260 }
6261 }
6262 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6263 next_pos += 1;
6264 if let Some(r) = &mut router {
6267 let phi = self.dyn_phi_ema.clone();
6268 let decision = r.step(&phi, generated);
6269 if let Some(new_active) = decision {
6270 let _ = self.set_active_skill(new_active);
6271 }
6272 if trace_on {
6275 if let Some(last) = traces.last_mut() {
6276 let e = r.last_best_e();
6277 last.recon = e.is_finite().then_some(e);
6278 last.switched = decision.is_some();
6279 }
6280 }
6281 }
6282 }
6283 }
6284 }
6285
6286 let cancelled = finish_reason == "cancelled";
6287 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6291 if mimo_spec {
6292 if let Some(st) = self.mimo_mtp.as_ref() {
6293 let line = st.stats.line();
6294 tracing::info!("{line}");
6295 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6296 eprintln!("{line}");
6297 }
6298 }
6299 }
6300 self.finish_generation(&mut mtp, &mut router, cancelled);
6301
6302 let output_ids = &all_ids[input_ids.len()..];
6303 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6307 let consumed = std::mem::take(&mut all_ids);
6312 if dyn_switched {
6313 self.clear_sequence_state();
6314 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6315 self.clear_history();
6316 } else {
6317 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6318 }
6319 all_ids = consumed;
6320 let output_ids = &all_ids[input_ids.len()..];
6321 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6323 Ok(GenerateResult {
6324 text: self.tokenizer.decode(output_ids),
6325 token_ids: output_ids.to_vec(),
6326 prompt_tokens: input_ids.len(),
6327 tokens_generated: generated,
6328 finish_reason,
6329 mtp_drafted: drafted,
6330 mtp_accepted: accepted,
6331 token_confidence: confidence,
6332 traces,
6333 })
6334 }
6335
6336 fn mtp_step(
6340 &mut self,
6341 m: &mut MtpModule,
6342 hidden: &[f32],
6343 next_token: u32,
6344 position: usize,
6345 ) -> u32 {
6346 self.mtp_step_h(m, hidden, next_token, position).0
6347 }
6348
6349 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6353 use std::sync::Mutex;
6354 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6355 let mut t = T.lock().unwrap();
6356 if t.len() <= depth {
6357 t.resize(depth + 1, (0, 0));
6358 }
6359 t[depth].0 += 1;
6360 t[depth].1 += prefix_ok as u64;
6361 if depth == 0 && t[0].0 % 128 == 0 {
6362 let line: Vec<String> = t
6363 .iter()
6364 .enumerate()
6365 .map(|(d, (n, k))| {
6366 format!(
6367 "d{}={:.0}%({n})",
6368 d + 1,
6369 100.0 * *k as f64 / (*n).max(1) as f64
6370 )
6371 })
6372 .collect();
6373 eprintln!("mtp-chain: {}", line.join(" "));
6374 }
6375 }
6376
6377 fn mtp_step_hl(
6385 &mut self,
6386 m: &mut MtpModule,
6387 hidden: &[f32],
6388 next_token: u32,
6389 position: usize,
6390 ) -> (Vec<f32>, Vec<f32>) {
6391 #[cfg(target_os = "macos")]
6396 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6397 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6398 self.mtp_graph_mode = Some(true);
6399 return r;
6400 }
6401 if self.mtp_graph_mode == Some(true) {
6402 tracing::error!("mtp Metal graph failed after admission");
6403 self.clear_sequence_state();
6404 self.graph_failed
6405 .store(true, std::sync::atomic::Ordering::Relaxed);
6406 self.cancel
6407 .store(true, std::sync::atomic::Ordering::Relaxed);
6408 return (Vec::new(), Vec::new());
6409 }
6410 self.mtp_graph_mode = Some(false);
6411 }
6412 #[cfg(feature = "gpu")]
6413 if self.mtp_graph_mode != Some(false) {
6414 if !self.mtp_graph_ok(m) {
6415 if self.mtp_graph_mode == Some(true) {
6416 tracing::error!("mtp graph became unavailable after admission");
6421 self.clear_sequence_state();
6422 self.graph_failed
6423 .store(true, std::sync::atomic::Ordering::Relaxed);
6424 self.cancel
6425 .store(true, std::sync::atomic::Ordering::Relaxed);
6426 return (Vec::new(), Vec::new());
6427 }
6428 self.mtp_graph_mode = Some(false);
6429 } else {
6430 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6431 self.mtp_graph_mode = Some(true);
6432 return r;
6433 }
6434 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6435 return (Vec::new(), Vec::new());
6442 }
6443 tracing::error!("mtp graph failed or declined after admission");
6447 self.clear_sequence_state();
6448 self.graph_failed
6449 .store(true, std::sync::atomic::Ordering::Relaxed);
6450 self.cancel
6451 .store(true, std::sync::atomic::Ordering::Relaxed);
6452 return (Vec::new(), Vec::new());
6453 }
6454 }
6455 let e = self.embed_single(next_token);
6459 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6460 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6461 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6462 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6463 let mut x = vec![0.0f32; self.hidden_size];
6464 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6465
6466 let lw = &m.layer;
6468 inference::rms_norm_into(
6469 &x,
6470 &lw.input_norm,
6471 self.rms_eps,
6472 self.norm_style,
6473 &mut self.ws.n1,
6474 );
6475 let attn = match &lw.attn {
6476 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6478 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6479 AttnKind::Full {
6480 wq,
6481 wk,
6482 wv,
6483 wo,
6484 q_norm,
6485 k_norm,
6486 output_gate,
6487 softplus_gate,
6488 bias,
6489 } => {
6490 let mut cfg = self.attn_cfg(position);
6491 cfg.q_norm = q_norm.as_deref();
6492 cfg.k_norm = k_norm.as_deref();
6493 cfg.output_gate = *output_gate;
6494 cfg.softplus_gate = softplus_gate
6495 .as_ref()
6496 .map(|(gate, per_head)| (gate, *per_head));
6497 cfg.bias = bias
6498 .as_ref()
6499 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6500 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6501 }
6502 AttnKind::Linear(_)
6503 | AttnKind::LinearGdn(_)
6504 | AttnKind::ShortConv(_)
6505 | AttnKind::Bounded(_) => {
6506 unreachable!("MTP block is full attention")
6507 }
6508 };
6509 for (i, &a) in attn.iter().enumerate() {
6510 x[i] += a;
6511 }
6512 inference::rms_norm_into(
6513 &x,
6514 &lw.post_norm,
6515 self.rms_eps,
6516 self.norm_style,
6517 &mut self.ws.p1,
6518 );
6519 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6520 for (i, &f) in ffn.iter().enumerate() {
6521 x[i] += f;
6522 }
6523
6524 inference::rms_norm_into(
6525 &x,
6526 &m.final_norm,
6527 self.rms_eps,
6528 self.norm_style,
6529 &mut self.ws.n1,
6530 );
6531 let lg = self.lm_head_forward(&self.ws.n1);
6532 (lg, x)
6533 }
6534
6535 fn mtp_step_h(
6537 &mut self,
6538 m: &mut MtpModule,
6539 hidden: &[f32],
6540 next_token: u32,
6541 position: usize,
6542 ) -> (u32, Vec<f32>) {
6543 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6544 let draft = sampler::argmax(&lg);
6545 attention::recycle_buf(&mut lg);
6546 (draft, x)
6547 }
6548
6549 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6555 match trial {
6556 SpecTrial::Spec { t0, gen0, rounds } => {
6557 let rounds = rounds + 1;
6558 if rounds >= 5 {
6559 if mon.plain_ms > 0.0 {
6560 let keep = mon.pays();
6561 mon.fails = 0;
6562 tracing::info!(
6563 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6564 mon.tokens,
6565 mon.round_ms,
6566 mon.plain_ms,
6567 if keep { "speculating" } else { "plain" }
6568 );
6569 SpecTrial::Decided {
6570 spec: keep,
6571 recheck_at: if keep { usize::MAX } else { generated + 128 },
6572 }
6573 } else if mon.pays() {
6574 mon.fails = 0;
6579 tracing::info!(
6580 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6581 mon.tokens,
6582 mon.round_ms,
6583 );
6584 SpecTrial::Decided {
6585 spec: true,
6586 recheck_at: usize::MAX,
6587 }
6588 } else {
6589 SpecTrial::Plain {
6590 t0: std::time::Instant::now(),
6591 gen0: generated,
6592 }
6593 }
6594 } else {
6595 SpecTrial::Spec { t0, gen0, rounds }
6596 }
6597 }
6598 SpecTrial::Decided { spec: true, .. } => {
6599 if mon.pays() {
6600 mon.fails = 0;
6601 trial
6602 } else {
6603 mon.fails += 1;
6604 if mon.fails >= 4 {
6605 if mon.plain_ms <= 0.0 {
6606 tracing::info!(
6610 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6611 mon.tokens,
6612 mon.round_ms,
6613 );
6614 return SpecTrial::Plain {
6615 t0: std::time::Instant::now(),
6616 gen0: generated,
6617 };
6618 }
6619 tracing::info!(
6620 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6621 mon.tokens,
6622 mon.round_ms,
6623 mon.plain_ms
6624 );
6625 SpecTrial::Decided {
6626 spec: false,
6627 recheck_at: generated + 128,
6628 }
6629 } else {
6630 trial
6631 }
6632 }
6633 }
6634 other => other,
6635 }
6636 }
6637
6638 fn mtp_kv_id(&self) -> u64 {
6641 self.graph_kv_id | (1u64 << 40)
6642 }
6643
6644 const MTP_LAYER_BASE: usize = 0;
6649
6650 #[cfg(feature = "gpu")]
6657 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6658 self.mtp_graph_mode != Some(true)
6659 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6660 }
6661
6662 #[cfg(feature = "gpu")]
6668 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6669 let mut ok = true;
6670 let mut expected = false;
6671 for li in 0..self.num_layers {
6672 if matches!(
6673 self.weights.layers[self.phys_layer(li)].attn,
6674 AttnKind::Full { .. }
6675 ) {
6676 expected = true;
6677 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6678 }
6679 }
6680 !expected || ok
6681 }
6682
6683 fn graph_gdn_layer_count(&self) -> usize {
6687 (0..self.num_layers)
6688 .filter(|&li| {
6689 matches!(
6690 &self.weights.layers[self.phys_layer(li)].attn,
6691 AttnKind::LinearGdn(_)
6692 )
6693 })
6694 .count()
6695 }
6696
6697 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6700 let e = self.embed_single(next_token);
6701 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6702 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6703 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6704 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6705 let mut x = vec![0.0f32; self.hidden_size];
6706 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6707 x
6708 }
6709
6710 #[cfg(feature = "gpu")]
6713 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6714 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6715 return false;
6716 }
6717 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6718 || !crate::gpu::enabled_here()
6719 || self.attn_softcap > 0.0
6720 || self.attention_heads_per_layer.is_some()
6721 || self.v_head_dim.is_some()
6724 {
6725 return false;
6726 }
6727 matches!(
6728 &m.layer.attn,
6729 AttnKind::Full {
6730 softplus_gate: None,
6731 ..
6732 }
6733 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6734 }
6735
6736 #[cfg(feature = "gpu")]
6740 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6741 if !self.mtp_block_graph_ok(m) {
6742 return false;
6743 }
6744 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6745 return false;
6746 };
6747 let FfnKind::Dense(d) = &m.layer.ffn else {
6748 return false;
6749 };
6750 d.segs.is_empty()
6751 && wq.graph_weight().is_some()
6752 && wk.graph_weight().is_some()
6753 && wv.graph_weight().is_some()
6754 && wo.graph_weight().is_some()
6755 && d.gate_proj.graph_weight().is_some()
6756 && d.up_proj.graph_weight().is_some()
6757 && d.down_proj.graph_weight().is_some()
6758 && self.weights.lm_head.graph_weight().is_some()
6759 }
6760
6761 #[cfg(feature = "gpu")]
6767 fn mtp_step_graph(
6768 &mut self,
6769 m: &mut MtpModule,
6770 hidden: &[f32],
6771 next_token: u32,
6772 position: usize,
6773 ) -> Option<(Vec<f32>, Vec<f32>)> {
6774 if !self.mtp_graph_ok(m) {
6775 return None;
6776 }
6777 let lw = &m.layer;
6778 let AttnKind::Full {
6779 wq,
6780 wk,
6781 wv,
6782 wo,
6783 q_norm,
6784 k_norm,
6785 output_gate,
6786 softplus_gate,
6787 bias,
6788 } = &lw.attn
6789 else {
6790 return None;
6791 };
6792 if softplus_gate.is_some() {
6793 return None;
6794 }
6795 let FfnKind::Dense(d) = &lw.ffn else {
6796 return None;
6797 };
6798 if !d.segs.is_empty() {
6799 return None; }
6801 let mut x = self.mtp_block_input(m, hidden, next_token);
6804 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6805 let (_, i, kind, rs) = t.graph_weight()?;
6806 Some(crate::gpu::GraphW {
6807 idx: i,
6808 kind,
6809 row_scale: rs,
6810 data: &[],
6811 prism: crate::gpu::GraphPrismOp::None,
6812 affine: false,
6813 })
6814 }
6815 let (model, _, _, _) = wq.graph_weight()?;
6816 let model = model.clone();
6817 let (lm_gw, lm_rows) = {
6818 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6819 let rows = if kind == 6 {
6823 self.draft_head_rows(self.weights.lm_head.rows())
6824 } else {
6825 self.weights.lm_head.rows()
6826 };
6827 (
6828 crate::gpu::GraphW {
6829 idx: i,
6830 kind,
6831 row_scale: rs,
6832 data: &[],
6833 prism: crate::gpu::GraphPrismOp::None,
6834 affine: false,
6835 },
6836 rows,
6837 )
6838 };
6839 let layer = crate::gpu::GraphLayer {
6840 input_norm: &lw.input_norm,
6841 attn: crate::gpu::GraphAttn::Full {
6842 wq: gw(wq)?,
6843 wk: gw(wk)?,
6844 wv: gw(wv)?,
6845 wo: gw(wo)?,
6846 q_norm: q_norm.as_deref(),
6847 k_norm: k_norm.as_deref(),
6848 late_qk_norm: self.qk_norm_after_rope,
6849 bias: bias
6850 .as_ref()
6851 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6852 output_gate: *output_gate,
6853 cpu_k: m.kv.k_heads(),
6854 cpu_v: m.kv.v_heads(),
6855 cpu_base: m.kv.base(),
6856 geom: None,
6857 head_gate: None,
6858 },
6859 post_norm: &lw.post_norm,
6860 attn_out_norm: None,
6861 ffn_out_norm: None,
6862 layer_scale: None,
6863 ffn: crate::gpu::GraphFfn::Dense {
6864 gate: gw(&d.gate_proj)?,
6865 up: gw(&d.up_proj)?,
6866 down: gw(&d.down_proj)?,
6867 act: d.act.graph_act()?,
6868 },
6869 };
6870 let nh = self.num_heads;
6871 let (nkv, hd, rd) = self.layer_geom(0);
6872 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6873 let mut logits = Vec::new();
6874 let ok = crate::gpu::forward_token_graph(
6875 &model,
6876 self.mtp_kv_id(),
6877 std::slice::from_ref(&layer),
6878 &[None],
6879 self.o1_epoch,
6880 &self.inv_freq,
6881 &mut x,
6882 nh,
6883 nkv,
6884 hd,
6885 self.attn_scale,
6886 rd,
6887 self.hidden_size,
6888 self.intermediate_size,
6889 position,
6890 self.kv_cache.max_seq_len,
6891 gemma,
6892 self.rms_eps as f32,
6893 Some((&lm_gw, lm_rows)),
6894 &m.final_norm,
6895 &mut logits,
6896 &[],
6897 1,
6898 None,
6899 None,
6900 None,
6901 Self::MTP_LAYER_BASE,
6902 true,
6903 );
6904 match ok {
6905 crate::gpu::TokenGraphOutcome::Completed => {}
6906 crate::gpu::TokenGraphOutcome::Declined => return None,
6907 crate::gpu::TokenGraphOutcome::Failed => {
6908 self.clear_sequence_state();
6912 self.graph_failed
6913 .store(true, std::sync::atomic::Ordering::Relaxed);
6914 self.cancel
6915 .store(true, std::sync::atomic::Ordering::Relaxed);
6916 return None;
6917 }
6918 }
6919 logits.resize(self.vocab_size, 0.0);
6920 Some((logits, x))
6921 }
6922
6923 #[cfg(feature = "gpu")]
6931 fn mtp_warm_graph(
6932 &mut self,
6933 m: &mut MtpModule,
6934 pairs: &[(&[f32], u32)],
6935 first_pos: usize,
6936 ) -> crate::gpu::BatchGraphOutcome {
6937 if pairs.is_empty() {
6938 return crate::gpu::BatchGraphOutcome::Completed;
6939 }
6940 if !self.mtp_block_graph_ok(m) {
6941 return crate::gpu::BatchGraphOutcome::Declined;
6942 }
6943 let hs = self.hidden_size;
6944 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6947 for (h, t) in pairs {
6948 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6949 }
6950 let lw = &m.layer;
6951 let AttnKind::Full {
6952 wq,
6953 wk,
6954 wv,
6955 wo,
6956 q_norm,
6957 k_norm,
6958 output_gate,
6959 bias,
6960 ..
6961 } = &lw.attn
6962 else {
6963 return crate::gpu::BatchGraphOutcome::Declined;
6964 };
6965 let FfnKind::Dense(d) = &lw.ffn else {
6966 return crate::gpu::BatchGraphOutcome::Declined;
6967 };
6968 if !d.segs.is_empty() {
6969 return crate::gpu::BatchGraphOutcome::Declined; }
6971 let (
6974 AttnKind::Full {
6975 softplus_gate: None,
6976 ..
6977 },
6978 Some(gact),
6979 ) = (&lw.attn, d.act.graph_act())
6980 else {
6981 return crate::gpu::BatchGraphOutcome::Declined;
6982 };
6983 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6984 let (_, i, kind, rs) = t.graph_weight()?;
6985 Some(crate::gpu::GraphW {
6986 idx: i,
6987 kind,
6988 row_scale: rs,
6989 data: &[],
6990 prism: crate::gpu::GraphPrismOp::None,
6991 affine: false,
6992 })
6993 }
6994 let Some((model, _, _, _)) = wq.graph_weight() else {
6995 return crate::gpu::BatchGraphOutcome::Declined;
6996 };
6997 let model = model.clone();
6998 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6999 gw(wq),
7000 gw(wk),
7001 gw(wv),
7002 gw(wo),
7003 gw(&d.gate_proj),
7004 gw(&d.up_proj),
7005 gw(&d.down_proj),
7006 ) else {
7007 return crate::gpu::BatchGraphOutcome::Declined;
7008 };
7009 let layer = crate::gpu::GraphLayer {
7010 input_norm: &lw.input_norm,
7011 attn: crate::gpu::GraphAttn::Full {
7012 wq: gwq,
7013 wk: gwk,
7014 wv: gwv,
7015 wo: gwo,
7016 q_norm: q_norm.as_deref(),
7017 k_norm: k_norm.as_deref(),
7018 late_qk_norm: self.qk_norm_after_rope,
7019 bias: bias
7020 .as_ref()
7021 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7022 output_gate: *output_gate,
7023 cpu_k: m.kv.k_heads(),
7024 cpu_v: m.kv.v_heads(),
7025 cpu_base: m.kv.base(),
7026 geom: None,
7027 head_gate: None,
7028 },
7029 post_norm: &lw.post_norm,
7030 attn_out_norm: None,
7031 ffn_out_norm: None,
7032 layer_scale: None,
7033 ffn: crate::gpu::GraphFfn::Dense {
7034 gate: gg,
7035 up: gu,
7036 down: gd,
7037 act: gact,
7038 },
7039 };
7040 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7041 let nh = self.num_heads;
7042 let (nkv, hd, rd) = self.layer_geom(0);
7043 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7044 crate::gpu::forward_batch_graph(
7045 &model,
7046 self.mtp_kv_id(),
7047 std::slice::from_ref(&layer),
7048 &self.inv_freq,
7049 &mut hiddens,
7050 nh,
7051 nkv,
7052 hd,
7053 rd,
7054 hs,
7055 self.intermediate_size,
7056 &positions,
7057 self.kv_cache.max_seq_len,
7058 gemma,
7059 self.rms_eps as f32,
7060 self.attn_scale,
7061 pairs.len(),
7062 &[],
7063 0,
7064 None,
7065 None,
7066 )
7067 }
7068
7069 #[cfg(feature = "gpu")]
7076 fn mtp_warm_graph_fallback(
7077 &mut self,
7078 m: &mut MtpModule,
7079 pairs: &[(&[f32], u32)],
7080 first_pos: usize,
7081 ) -> bool {
7082 if pairs.is_empty() {
7083 return true;
7084 }
7085 let graphable = self.mtp_block_graph_ok(m);
7086 if !graphable {
7087 if self.mtp_graph_mode == Some(true) {
7091 return false;
7092 }
7093 self.mtp_graph_mode = Some(false);
7094 for (j, (h, t)) in pairs.iter().enumerate() {
7095 self.mtp_warm(m, h, *t, first_pos + j);
7096 }
7097 return true;
7098 }
7099
7100 for (j, (h, t)) in pairs.iter().enumerate() {
7105 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7106 return false;
7107 }
7108 }
7109 self.mtp_graph_mode = Some(true);
7110 true
7111 }
7112
7113 #[cfg(feature = "gpu")]
7118 fn mtp_warm_prefill_pairs(
7119 &mut self,
7120 m: &mut MtpModule,
7121 pairs: &[(&[f32], u32)],
7122 first_pos: usize,
7123 ) -> Result<(), &'static str> {
7124 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7129 if self.mtp_graph_mode == Some(true) {
7130 return Err("MTP token graph became unavailable after admission");
7131 }
7132 self.mtp_graph_mode = Some(false);
7133 for (j, (h, t)) in pairs.iter().enumerate() {
7134 self.mtp_warm(m, h, *t, first_pos + j);
7135 }
7136 return Ok(());
7137 }
7138 match self.mtp_warm_graph(m, pairs, first_pos) {
7139 crate::gpu::BatchGraphOutcome::Completed => {
7140 if !pairs.is_empty() {
7141 self.mtp_graph_mode = Some(true);
7142 }
7143 Ok(())
7144 }
7145 crate::gpu::BatchGraphOutcome::Declined => {
7146 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7147 Ok(())
7148 } else {
7149 Err("MTP warm-up fallback failed after device admission")
7150 }
7151 }
7152 crate::gpu::BatchGraphOutcome::Failed => {
7153 Err("MTP warm batch graph failed after admission")
7154 }
7155 }
7156 }
7157
7158 #[cfg(not(feature = "gpu"))]
7159 fn mtp_warm_prefill_pairs(
7160 &mut self,
7161 m: &mut MtpModule,
7162 pairs: &[(&[f32], u32)],
7163 first_pos: usize,
7164 ) -> Result<(), &'static str> {
7165 for (j, (h, t)) in pairs.iter().enumerate() {
7166 self.mtp_warm(m, h, *t, first_pos + j);
7167 }
7168 Ok(())
7169 }
7170
7171 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7175 let e = self.embed_single(next_token);
7176 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7177 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7178 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7179 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7180 let mut x = vec![0.0f32; self.hidden_size];
7181 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7182 inference::rms_norm_into(
7183 &x,
7184 &m.layer.input_norm,
7185 self.rms_eps,
7186 self.norm_style,
7187 &mut self.ws.n1,
7188 );
7189 let attn = match &m.layer.attn {
7190 AttnKind::Full {
7191 wq,
7192 wk,
7193 wv,
7194 wo,
7195 q_norm,
7196 k_norm,
7197 output_gate,
7198 softplus_gate,
7199 bias,
7200 } => {
7201 let mut cfg = self.attn_cfg(position);
7202 cfg.q_norm = q_norm.as_deref();
7203 cfg.k_norm = k_norm.as_deref();
7204 cfg.output_gate = *output_gate;
7205 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7206 cfg.bias = bias
7207 .as_ref()
7208 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7209 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7210 }
7211 _ => return,
7212 };
7213 let _ = attn;
7214 }
7215
7216 #[cfg(feature = "gpu")]
7223 #[allow(clippy::too_many_arguments)]
7224 fn graph_spec_step(
7225 &mut self,
7226 m: &mut MtpModule,
7227 hidden: &[f32],
7228 t_next: u32,
7229 next_pos: usize,
7230 drafted: &mut usize,
7231 accepted: &mut usize,
7232 all_ids: &mut Vec<u32>,
7236 room: usize,
7241 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7242 #[cfg(target_os = "macos")]
7253 let metal_native = crate::gpu::q1_force();
7254 #[cfg(not(target_os = "macos"))]
7255 let metal_native = false;
7256 #[cfg(feature = "gpu")]
7257 let k_default = if metal_native {
7258 7
7261 } else if crate::gpu_wgpu::verify_i8_on() {
7262 5
7263 } else {
7264 4
7265 };
7266 #[cfg(not(feature = "gpu"))]
7267 let k_default = 4;
7268 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7269 .ok()
7270 .and_then(|v| v.parse().ok())
7271 .filter(|&v| (1..=8).contains(&v));
7272 let (k_start, k_max) = if metal_native {
7277 (7, 7)
7278 } else {
7279 (3, k_default.max(5))
7280 };
7281 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7282 let k_spec = k_full.min(room).max(1);
7283 let k_capped = k_spec < k_full;
7286 if next_pos == 0 {
7287 return None;
7288 }
7289 let t_round = std::time::Instant::now();
7290 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7306 let sub0 = subs();
7307 let cfg = self.sampler_config.clone();
7312 let penalized = !(cfg.repetition_penalty == 1.0
7313 && cfg.presence_penalty == 0.0
7314 && cfg.suppress_tokens.is_empty());
7315 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7320 let sampling = cfg.temperature >= 1e-6;
7321 let sparse = sampling && sampler::sparse_ok(&cfg);
7327 let base_len = all_ids.len();
7328 if sampling && !sparse && self.spec_q.len() < k_spec {
7329 self.spec_q.resize_with(k_spec, Vec::new);
7330 }
7331 if sparse && self.spec_qs.len() < k_spec {
7332 self.spec_qs.resize_with(k_spec, Vec::new);
7333 }
7334 let mut drafts = Vec::with_capacity(k_spec);
7339 let mut hx = hidden.to_vec();
7340 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7343 spec_stamp("pro");
7344 #[cfg(target_os = "macos")]
7350 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7351 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7352 Ok(ids) => {
7353 self.mtp_graph_mode = Some(true);
7354 drafts = ids;
7355 }
7356 Err(true) => {
7357 tracing::error!("mtp Metal draft chain failed after commit");
7358 self.clear_sequence_state();
7359 self.graph_failed
7360 .store(true, std::sync::atomic::Ordering::Relaxed);
7361 self.cancel
7362 .store(true, std::sync::atomic::Ordering::Relaxed);
7363 return None;
7364 }
7365 Err(false) => {}
7366 }
7367 }
7368 for j in drafts.len()..k_spec {
7369 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7370 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7371 if spec_dbg {
7372 let saved = self.mtp_graph_mode;
7373 self.mtp_graph_mode = Some(false);
7374 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7375 self.mtp_graph_mode = saved;
7376 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7377 return None;
7378 }
7379 m.kv.truncate_last(1);
7380 dbg_ref = Some(r);
7381 }
7382 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7383 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7384 return None;
7385 }
7386 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7387 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7388 let dl = lg
7389 .iter()
7390 .zip(&lg_cpu)
7391 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7392 let dh = hj
7393 .iter()
7394 .zip(&h_cpu)
7395 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7396 eprintln!(
7397 "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 {}",
7398 next_pos - 1 + j,
7399 sampler::argmax(&lg_cpu),
7400 sampler::argmax(&lg),
7401 n(&h_cpu),
7402 n(&hj),
7403 m.kv.seq_len
7404 );
7405 }
7406 let dj = if sparse {
7407 let mut q = std::mem::take(&mut self.spec_qs[j]);
7408 let ok = sampler::sparse_distribution_into(
7409 &lg,
7410 &cfg,
7411 all_ids,
7412 &mut self.sampler_scratch,
7413 self.pool.as_deref(),
7414 &mut q,
7415 );
7416 let d = if ok {
7417 sampler::draw_sparse(&q, &mut self.rng)
7418 } else {
7419 let t = sampler::argmax(&lg);
7421 q.clear();
7422 q.push((t, 1.0));
7423 t
7424 };
7425 self.spec_qs[j] = q;
7426 all_ids.push(d);
7427 d
7428 } else if sampling {
7429 let mut q = std::mem::take(&mut self.spec_q[j]);
7430 sampler::distribution_into(
7431 &lg,
7432 &cfg,
7433 all_ids,
7434 &mut self.sampler_scratch,
7435 self.pool.as_deref(),
7436 &mut q,
7437 );
7438 let d = sampler::draw(&q, &mut self.rng);
7439 self.spec_q[j] = q;
7440 all_ids.push(d); d
7442 } else if greedy_pen {
7443 let d = sampler::argmax_penalized(
7444 &lg,
7445 &cfg,
7446 all_ids,
7447 &mut self.sampler_scratch,
7448 self.pool.as_deref(),
7449 );
7450 all_ids.push(d);
7451 d
7452 } else {
7453 sampler::argmax(&lg)
7454 };
7455 attention::recycle_buf(&mut lg);
7456 drafts.push(dj);
7457 hx = hj;
7458 spec_stamp("d.pick");
7459 }
7460 all_ids.truncate(base_len);
7461 *drafted += k_spec;
7462 let t_draft = t_round.elapsed();
7463 let sub_draft = subs();
7464 let b = k_spec + 1;
7467 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7468 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7469 let e = self.embed_single(t);
7470 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7471 }
7472 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7473 spec_stamp("v.emb");
7474 let (lm_gw, lm_rows) = {
7475 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7476 (
7477 crate::gpu::GraphW {
7478 idx: i,
7479 kind,
7480 row_scale: rs,
7481 data: &[],
7482 prism: crate::gpu::GraphPrismOp::None,
7483 affine: false,
7484 },
7485 self.weights.lm_head.rows(),
7486 )
7487 };
7488 let mut logits = Vec::new();
7489 let final_norm = self.weights.final_norm.clone();
7490 #[cfg(target_os = "macos")]
7499 let greedy_dev = metal_native
7500 && !sampling
7501 && !greedy_pen
7502 && !self.confidence_on
7503 && self.final_softcap.is_none()
7504 && self.vocab_size == lm_rows
7510 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7511 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7512 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7513 #[cfg(not(target_os = "macos"))]
7514 let greedy_dev = false;
7515 let mut dev_ids: Vec<u32> = Vec::new();
7516 #[cfg(target_os = "macos")]
7517 let verify_outcome = if metal_native {
7518 let lm = self.weights.lm_head.q1_parts()?;
7519 let n_score = self.vocab_size.min(lm_rows);
7520 self.try_batch_graph_metal(
7521 &mut hiddens,
7522 &positions,
7523 b,
7524 Some((lm, &final_norm, &mut logits)),
7525 if greedy_dev {
7526 Some((n_score, &mut dev_ids))
7527 } else {
7528 None
7529 },
7530 )
7531 } else {
7532 self.try_batch_graph_wgpu(
7533 &mut hiddens,
7534 &positions,
7535 b,
7536 Some(crate::gpu::SpecTail {
7537 lm: lm_gw,
7538 lm_rows,
7539 final_norm: &final_norm,
7540 logits_out: &mut logits,
7541 }),
7542 )
7543 };
7544 #[cfg(not(target_os = "macos"))]
7545 let verify_outcome = self.try_batch_graph_wgpu(
7546 &mut hiddens,
7547 &positions,
7548 b,
7549 Some(crate::gpu::SpecTail {
7550 lm: lm_gw,
7551 lm_rows,
7552 final_norm: &final_norm,
7553 logits_out: &mut logits,
7554 }),
7555 );
7556 match verify_outcome {
7557 crate::gpu::BatchGraphOutcome::Completed => {}
7558 crate::gpu::BatchGraphOutcome::Declined => {
7559 m.kv.truncate_last(k_spec);
7563 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7564 self.clear_sequence_state();
7565 self.graph_failed
7566 .store(true, std::sync::atomic::Ordering::Relaxed);
7567 self.cancel
7568 .store(true, std::sync::atomic::Ordering::Relaxed);
7569 tracing::error!("MTP graph mirror rewind failed after verify decline");
7570 }
7571 return None;
7572 }
7573 crate::gpu::BatchGraphOutcome::Failed => {
7574 self.clear_sequence_state();
7578 self.graph_failed
7579 .store(true, std::sync::atomic::Ordering::Relaxed);
7580 self.cancel
7581 .store(true, std::sync::atomic::Ordering::Relaxed);
7582 tracing::error!("MTP verify batch graph failed after admission");
7583 return None;
7584 }
7585 }
7586 #[cfg(target_os = "macos")]
7592 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7593 let snap: Vec<Vec<f32>> = self
7594 .kv_cache
7595 .layers
7596 .iter()
7597 .map(|l| l.linear_state.clone())
7598 .collect();
7599 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7600 let toks: Vec<u32> = std::iter::once(t_next)
7601 .chain(drafts.iter().copied())
7602 .collect();
7603 let want_save = self.graph_want_logits;
7604 self.graph_want_logits = false;
7605 for (i, &t) in toks.iter().enumerate() {
7606 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7607 let _ = self.graph_logits.take();
7608 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7612 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7613 }
7614 let ref_lg = self.logits_from_hidden(&hi);
7615 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7616 let ra = sampler::argmax(&ref_lg);
7617 let va = sampler::argmax(row);
7618 let mut md = 0f32;
7619 let mut rms = 0f64;
7620 for j in 0..lm_rows.min(ref_lg.len()) {
7621 let d = (ref_lg[j] - row[j]).abs();
7622 md = md.max(d);
7623 rms += (d as f64) * (d as f64);
7624 }
7625 let mut hd = 0f32;
7626 for j in 0..self.hidden_size {
7627 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7628 }
7629 eprintln!(
7630 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7631 next_pos + i,
7632 if ra == va { "OK" } else { "MISMATCH" },
7633 (rms / lm_rows as f64).sqrt()
7634 );
7635 }
7636 self.graph_want_logits = want_save;
7637 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7640 if l.linear_state.len() == st.len() {
7641 l.linear_state.copy_from_slice(&st);
7642 } else {
7643 l.linear_state = st;
7644 }
7645 }
7646 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7647 let extra = l.seq_len.saturating_sub(n0);
7648 if extra > 0 {
7649 l.truncate_last(extra);
7650 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7651 }
7652 }
7653 }
7654 let t_verify = t_round.elapsed();
7655 let sub_verify = subs();
7656 let mut a = 0usize;
7661 let mut forced: Option<u32> = None;
7662 let ids: Vec<u32> = if sparse {
7663 let mut p = std::mem::take(&mut self.spec_ps);
7664 let mut res = std::mem::take(&mut self.spec_ress);
7665 while a < k_spec {
7666 let ok = sampler::sparse_distribution_into(
7667 &logits[a * lm_rows..(a + 1) * lm_rows],
7668 &cfg,
7669 all_ids,
7670 &mut self.sampler_scratch,
7671 self.pool.as_deref(),
7672 &mut p,
7673 );
7674 if !ok {
7675 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7676 p.clear();
7677 p.push((t, 1.0));
7678 }
7679 match sampler::spec_accept_or_correct_sparse(
7680 &p,
7681 &self.spec_qs[a],
7682 drafts[a],
7683 &mut self.rng,
7684 &mut res,
7685 ) {
7686 None => {
7687 all_ids.push(drafts[a]);
7688 a += 1;
7689 }
7690 Some(c) => {
7691 forced = Some(c);
7692 break;
7693 }
7694 }
7695 }
7696 all_ids.truncate(base_len);
7697 self.spec_ps = p;
7698 self.spec_ress = res;
7699 drafts.clone()
7700 } else if sampling {
7701 let mut p = std::mem::take(&mut self.spec_p);
7702 let mut res = std::mem::take(&mut self.spec_res);
7703 while a < k_spec {
7704 sampler::distribution_into(
7705 &logits[a * lm_rows..(a + 1) * lm_rows],
7706 &cfg,
7707 all_ids,
7708 &mut self.sampler_scratch,
7709 self.pool.as_deref(),
7710 &mut p,
7711 );
7712 match sampler::spec_accept_or_correct(
7713 &p,
7714 &self.spec_q[a],
7715 drafts[a],
7716 &mut self.rng,
7717 &mut res,
7718 self.pool.as_deref(),
7719 ) {
7720 None => {
7721 all_ids.push(drafts[a]);
7722 a += 1;
7723 }
7724 Some(c) => {
7725 forced = Some(c);
7726 break;
7727 }
7728 }
7729 }
7730 all_ids.truncate(base_len);
7731 self.spec_p = p;
7732 self.spec_res = res;
7733 drafts.clone()
7735 } else if greedy_pen {
7736 let mut ids: Vec<u32> = Vec::with_capacity(b);
7740 for i in 0..b {
7741 let t = sampler::argmax_penalized(
7742 &logits[i * lm_rows..(i + 1) * lm_rows],
7743 &cfg,
7744 all_ids,
7745 &mut self.sampler_scratch,
7746 self.pool.as_deref(),
7747 );
7748 ids.push(t);
7749 if i < k_spec && t == drafts[i] {
7750 all_ids.push(t);
7751 } else {
7752 break;
7753 }
7754 }
7755 all_ids.truncate(base_len);
7756 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7757 a += 1;
7758 }
7759 ids
7762 } else if greedy_dev && dev_ids.len() == b {
7763 let ids = std::mem::take(&mut dev_ids);
7764 while a < k_spec && ids[a] == drafts[a] {
7765 a += 1;
7766 }
7767 ids
7768 } else {
7769 if logits.len() < b * lm_rows {
7770 self.clear_sequence_state();
7773 self.graph_failed
7774 .store(true, std::sync::atomic::Ordering::Relaxed);
7775 self.cancel
7776 .store(true, std::sync::atomic::Ordering::Relaxed);
7777 tracing::error!("Metal verify returned neither logits nor argmax ids");
7778 return None;
7779 }
7780 let ids: Vec<u32> = (0..b)
7781 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7782 .collect();
7783 while a < k_spec && ids[a] == drafts[a] {
7784 a += 1;
7785 }
7786 ids
7787 };
7788 spec_stamp("acc");
7789 if spec_dbg {
7790 eprintln!(
7791 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7792 drafts, ids
7793 );
7794 }
7795 #[cfg(target_os = "macos")]
7799 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7800 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7801 {
7802 let snap: Vec<Vec<f32>> = self
7803 .kv_cache
7804 .layers
7805 .iter()
7806 .map(|l| l.linear_state.clone())
7807 .collect();
7808 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7809 let toks: Vec<u32> = std::iter::once(t_next)
7810 .chain(drafts.iter().copied())
7811 .collect();
7812 let want_save = self.graph_want_logits;
7813 self.graph_want_logits = false;
7814 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7815 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7816 let _ = self.graph_logits.take();
7817 }
7818 self.graph_want_logits = want_save;
7819 let plain_states: Vec<Vec<f32>> = self
7820 .kv_cache
7821 .layers
7822 .iter()
7823 .map(|l| l.linear_state.clone())
7824 .collect();
7825 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7826 let mut rows = Vec::new();
7827 for (li, (l, n0)) in self
7828 .kv_cache
7829 .layers
7830 .iter_mut()
7831 .zip(attn_lens.iter())
7832 .enumerate()
7833 {
7834 let extra = l.seq_len.saturating_sub(*n0);
7835 if extra > 0 {
7836 let mut kk = Vec::new();
7837 let mut vv = Vec::new();
7838 for g in 0..nkv {
7839 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7840 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7841 }
7842 rows.push((li, kk, vv));
7843 l.truncate_last(extra);
7844 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7845 }
7846 }
7847 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7848 if l.linear_state.len() == st.len() {
7849 l.linear_state.copy_from_slice(&st);
7850 } else {
7851 l.linear_state = st;
7852 }
7853 }
7854 Some((plain_states, rows))
7855 } else {
7856 None
7857 };
7858 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7859 #[cfg(target_os = "macos")]
7868 let mut warm_pending: Option<MetalWarmPending> = None;
7869 #[cfg(target_os = "macos")]
7870 if metal_native {
7871 m.kv.truncate_last(k_spec.saturating_sub(1));
7872 if self.mtp_graph_mode == Some(true) {
7873 crate::gpu_metal::kv_mirror_set_stored(
7876 self.mtp_kv_id(),
7877 Self::MTP_LAYER_BASE,
7878 m.kv.seq_len,
7879 );
7880 if !warm_off && a > 0 {
7881 let pairs: Vec<(&[f32], u32)> = (0..a)
7882 .map(|j| {
7883 (
7884 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7885 ids[j],
7886 )
7887 })
7888 .collect();
7889 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7890 }
7891 }
7892 spec_stamp("c.wsub");
7893 }
7894 #[cfg(target_os = "macos")]
7896 if metal_native {
7897 if !self.metal_verify_commit(a) {
7900 self.clear_sequence_state();
7901 self.graph_failed
7902 .store(true, std::sync::atomic::Ordering::Relaxed);
7903 self.cancel
7904 .store(true, std::sync::atomic::Ordering::Relaxed);
7905 tracing::error!("Metal verify state/KV handoff failed after admission");
7906 return None;
7907 }
7908 if let Some((plain_states, rows)) = commit_ref {
7909 crate::gpu_metal::queue_fence();
7910 let _ = crate::gpu_metal::wait_replay();
7913 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7914 let mut worst_s = 0f32;
7915 let mut worst_li = 0usize;
7916 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7917 if l.linear_state.len() != ps.len() || ps.is_empty() {
7918 continue;
7919 }
7920 let d = l
7921 .linear_state
7922 .iter()
7923 .zip(ps)
7924 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7925 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7926 let rel = d / n.max(1e-6);
7927 if rel > worst_s {
7928 worst_s = rel;
7929 worst_li = li;
7930 }
7931 }
7932 let mut worst_k = 0f32;
7933 for (li, kk, vv) in &rows {
7934 let l = &self.kv_cache.layers[*li];
7935 let n0 = l.seq_len - (kk.len() / (nkv * hd));
7936 let mut ck = Vec::new();
7937 let mut cv = Vec::new();
7938 for g in 0..nkv {
7939 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7940 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7941 }
7942 if ck.len() == kk.len() {
7943 let dk = ck
7944 .iter()
7945 .zip(kk)
7946 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7947 let dv = cv
7948 .iter()
7949 .zip(vv)
7950 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7951 worst_k = worst_k.max(dk).max(dv);
7952 } else {
7953 eprintln!(
7954 "commit-check L{li}: kv row count mismatch {} vs {}",
7955 ck.len(),
7956 kk.len()
7957 );
7958 }
7959 }
7960 eprintln!(
7961 "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}"
7962 );
7963 }
7964 }
7965 if !metal_native && a + 1 < b {
7966 let expected_gdn_layers = self.graph_gdn_layer_count();
7967 if expected_gdn_layers > 0
7968 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7969 {
7970 self.clear_sequence_state();
7971 self.graph_failed
7972 .store(true, std::sync::atomic::Ordering::Relaxed);
7973 self.cancel
7974 .store(true, std::sync::atomic::Ordering::Relaxed);
7975 tracing::error!("GDN speculative restore failed after verify");
7976 return None;
7977 }
7978 }
7979 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7980 self.clear_sequence_state();
7985 self.graph_failed
7986 .store(true, std::sync::atomic::Ordering::Relaxed);
7987 self.cancel
7988 .store(true, std::sync::atomic::Ordering::Relaxed);
7989 tracing::error!("trunk graph KV rewind failed after speculative verify");
7990 return None;
7991 }
7992 *accepted += a;
7993 if !metal_native {
8004 m.kv.truncate_last(k_spec.saturating_sub(1));
8006 }
8007 spec_stamp("c.trunc");
8008 if !metal_native
8009 && self.mtp_graph_mode == Some(true)
8010 && !self.rewind_mtp_graph_mirror(next_pos)
8011 {
8012 self.clear_sequence_state();
8016 self.graph_failed
8017 .store(true, std::sync::atomic::Ordering::Relaxed);
8018 self.cancel
8019 .store(true, std::sync::atomic::Ordering::Relaxed);
8020 tracing::error!("MTP graph mirror rewind failed after verify commit");
8021 return None;
8022 }
8023 if !warm_off && a > 0 {
8024 let mut warmed = false;
8027 #[cfg(target_os = "macos")]
8028 if metal_native && self.mtp_graph_mode == Some(true) {
8029 warmed = match warm_pending.take() {
8033 Some(p) => self.mtp_warm_batch_finish(m, p),
8034 None => false,
8035 };
8036 if !warmed {
8037 warmed = true;
8038 for j in 0..a {
8039 let row =
8040 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8041 if self
8042 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8043 .is_none()
8044 {
8045 warmed = false;
8046 break;
8047 }
8048 }
8049 }
8050 }
8051 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8052 let rows: Vec<Vec<f32>> = (0..a)
8053 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8054 .collect();
8055 let pairs: Vec<(&[f32], u32)> = rows
8056 .iter()
8057 .zip(ids.iter())
8058 .map(|(r, &t)| (r.as_slice(), t))
8059 .collect();
8060 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8061 Ok(()) => warmed = true,
8062 Err(err) => {
8063 tracing::error!("{err}");
8069 self.clear_sequence_state();
8070 self.graph_failed
8071 .store(true, std::sync::atomic::Ordering::Relaxed);
8072 self.cancel
8073 .store(true, std::sync::atomic::Ordering::Relaxed);
8074 return None;
8075 }
8076 }
8077 }
8078 if !warmed {
8079 for j in 0..a {
8080 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8081 let row = row.to_vec();
8082 self.mtp_warm(m, &row, ids[j], next_pos + j);
8083 }
8084 }
8085 }
8086 spec_stamp("c.warm");
8090 if let Some(c) = forced {
8091 self.spec_forced = Some(c);
8092 self.graph_logits = None;
8093 } else if greedy_dev && logits.is_empty() {
8094 self.spec_forced = Some(ids[a]);
8097 self.graph_logits = None;
8098 } else {
8099 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8100 row.resize(self.vocab_size, 0.0);
8101 if let Some(c) = self.final_softcap {
8102 for l in row.iter_mut() {
8103 *l = c * (*l / c).tanh();
8104 }
8105 }
8106 self.graph_logits = Some(row);
8107 }
8108 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8109 spec_stamp("c.row");
8110 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8116 let end = subs();
8117 eprintln!(
8118 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8119 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8120 t_draft.as_secs_f64() * 1e3,
8121 sub_draft - sub0,
8122 (t_verify - t_draft).as_secs_f64() * 1e3,
8123 sub_verify - sub_draft,
8124 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8125 end - sub_verify,
8126 self.draft_full_streak,
8127 );
8128 }
8129 if k_env.is_none() && !metal_native && !k_capped {
8134 let f = a as f32 / k_spec.max(1) as f32;
8138 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8139 let mut k_next = k_spec;
8140 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8141 k_next = k_spec + 1;
8142 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8143 k_next = k_spec - 1;
8144 }
8145 if k_next != k_spec {
8146 self.spec_acc_ewma = 0.6;
8147 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8148 eprintln!("spec-k: {k_spec} → {k_next}");
8149 }
8150 }
8151 self.spec_k_adapt = Some(k_next);
8152 }
8153 spec_stamp("end");
8154 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8155 }
8156
8157 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8166 if !self.pair_supported() {
8167 return (0.0, 0.0);
8168 }
8169 if self.gdn_cfg.is_some()
8177 && self.graph_prefill_preferred()
8178 && self.automatic_gpu_prefix().is_some()
8179 {
8180 tracing::info!(
8181 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8182 );
8183 return (0.0, 0.0);
8184 }
8185 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8192 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8193 let emb1 = self.embed_single(1);
8194 let emb2 = self.embed_single(2);
8195 let pos = self.kv_cache.seq_len();
8196
8197 let t0 = std::time::Instant::now();
8198 for _ in 0..iters {
8199 let _ = self.forward_layers(&emb1, pos, None);
8200 let _ = self.forward_layers(&emb2, pos + 1, None);
8201 for l in &mut self.kv_cache.layers {
8202 l.truncate_last(2);
8203 }
8204 }
8205 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8206
8207 let t1 = std::time::Instant::now();
8208 for _ in 0..iters {
8209 let _ = self.forward_pair(&emb1, &emb2, pos);
8210 for l in &mut self.kv_cache.layers {
8211 l.truncate_last(2);
8212 }
8213 }
8214 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8215 match graph_env {
8216 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8217 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8218 }
8219 (singles_ms, pair_ms)
8220 }
8221
8222 fn pair_supported(&self) -> bool {
8230 !self.weights.layers.is_empty()
8237 && self.g3n.is_none()
8238 && !self
8239 .weights
8240 .layers
8241 .iter()
8242 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8243 }
8244
8245 fn forward_pair(
8246 &mut self,
8247 emb1: &[f32],
8248 emb2: &[f32],
8249 position: usize,
8250 ) -> (Vec<f32>, Vec<f32>) {
8251 self.mimo_moe_prepare();
8254 let mut h1 = emb1.to_vec();
8255 let mut h2 = emb2.to_vec();
8256 let (_nkv, _hd, hs, _rd, eps) = (
8257 self.num_kv_heads,
8258 self.head_dim,
8259 self.hidden_size,
8260 self.rotary_dim,
8261 self.rms_eps,
8262 );
8263 let pool = self.pool.clone();
8264 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8271
8272 for li in 0..self.num_layers {
8273 let _capacity_tail = automatic_gpu_prefix
8274 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8275 .map(|_| crate::gpu::enter_cpu_scope());
8276 crate::gpu::set_layer(li as i64);
8279 let lw = &self.weights.layers[self.phys_layer(li)];
8280 inference::rms_norm_into(
8283 &h1,
8284 &lw.input_norm,
8285 self.rms_eps,
8286 self.norm_style,
8287 &mut self.ws.n1,
8288 );
8289 inference::rms_norm_into(
8290 &h2,
8291 &lw.input_norm,
8292 self.rms_eps,
8293 self.norm_style,
8294 &mut self.ws.n2,
8295 );
8296
8297 let (a1, a2) = match &lw.attn {
8298 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8299 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8300 AttnKind::Bounded(w) => {
8301 let rope = self
8304 .bounded_rope
8305 .clone()
8306 .expect("bounded layer without an installed rotation table");
8307 let cfg = crate::bounded::BoundedAttnCfg {
8308 num_heads: self.num_heads,
8309 num_kv_heads: self.num_kv_heads,
8310 head_dim: self.head_dim,
8311 hidden_size: hs,
8312 scale: self.attn_scale,
8313 rope: &rope,
8314 pool: pool.as_deref(),
8315 };
8316 let a1 = crate::bounded::bounded_attention(
8317 &self.ws.n1,
8318 w,
8319 &mut self.kv_cache.layers[li],
8320 &cfg,
8321 );
8322 let a2 = crate::bounded::bounded_attention(
8323 &self.ws.n2,
8324 w,
8325 &mut self.kv_cache.layers[li],
8326 &cfg,
8327 );
8328 (a1, a2)
8329 }
8330 AttnKind::Linear(w) => {
8331 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8332 let layer = &mut self.kv_cache.layers[li];
8333 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8334 vmf_phase_pair(
8335 &self.ws.n1,
8336 &self.ws.n2,
8337 w,
8338 &cfg,
8339 state,
8340 scratch,
8341 self.pool.as_deref(),
8342 )
8343 }
8344 AttnKind::LinearGdn(w) => {
8345 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8346 let layer = &mut self.kv_cache.layers[li];
8347 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8348 gdn_pair(
8349 &self.ws.n1,
8350 &self.ws.n2,
8351 w,
8352 &cfg,
8353 state,
8354 scratch,
8355 self.pool.as_deref(),
8356 )
8357 }
8358 AttnKind::ShortConv(w) => {
8359 let cfg = self
8360 .short_conv_cfg
8361 .expect("short-conv layer without short_conv_cfg");
8362 let layer = &mut self.kv_cache.layers[li];
8363 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8364 short_conv_pair(
8365 &self.ws.n1,
8366 &self.ws.n2,
8367 w,
8368 &cfg,
8369 state,
8370 scratch,
8371 self.pool.as_deref(),
8372 )
8373 }
8374 AttnKind::Full {
8375 wq,
8376 wk,
8377 wv,
8378 wo,
8379 q_norm,
8380 k_norm,
8381 output_gate,
8382 softplus_gate,
8383 bias,
8384 } => {
8385 let inv_freq_l = self.layer_inv_freq(li);
8386 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8387 let cfg = QwenAttnCfg {
8388 num_heads: self.layer_num_heads(li),
8389 num_kv_heads: nkv_l,
8390 head_dim: hd_l,
8391 hidden_size: hs,
8392 position,
8393 inv_freq: &inv_freq_l,
8394 rotary_dim: rd_l,
8395 scale: self.attn_scale,
8396 softcap: self.attn_softcap,
8397 window: self.layer_window(li),
8398 v_norm: self.attn_v_norm,
8399 qk_norm_after_rope: self.qk_norm_after_rope,
8400 gate_sigmoid: self.proj_gate_sigmoid,
8401 q_norm: q_norm.as_deref(),
8402 k_norm: k_norm.as_deref(),
8403 output_gate: *output_gate,
8404 softplus_gate: softplus_gate
8405 .as_ref()
8406 .map(|(gate, per_head)| (gate, *per_head)),
8407 rope_scale: self.layer_rope_scale(li),
8408 bias: bias
8409 .as_ref()
8410 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8411 rms_eps: eps,
8412 norm_style: self.norm_style,
8413 pool: pool.as_deref(),
8414 v_head_dim: self.layer_v_dim(li),
8415 };
8416 attention::qwen_attention_pair(
8417 &self.ws.n1,
8418 &self.ws.n2,
8419 wq,
8420 wk,
8421 wv,
8422 wo,
8423 &mut self.kv_cache.layers[li],
8424 &cfg,
8425 )
8426 }
8427 };
8428 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8429 Some(w) => (
8430 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8431 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8432 ),
8433 None => (a1, a2),
8434 };
8435 for i in 0..self.hidden_size {
8436 h1[i] += a1[i];
8437 h2[i] += a2[i];
8438 }
8439 let (mut a1, mut a2) = (a1, a2);
8440 attention::recycle_buf(&mut a1);
8441 attention::recycle_buf(&mut a2);
8442
8443 let lw = &self.weights.layers[self.phys_layer(li)];
8444 inference::rms_norm_into(
8445 &h1,
8446 &lw.post_norm,
8447 self.rms_eps,
8448 self.norm_style,
8449 &mut self.ws.p1,
8450 );
8451 inference::rms_norm_into(
8452 &h2,
8453 &lw.post_norm,
8454 self.rms_eps,
8455 self.norm_style,
8456 &mut self.ws.p2,
8457 );
8458 let (f1, f2) = match &lw.ffn {
8459 FfnKind::DenseMoe(dm) => (
8462 dense_moe_ffn(
8463 dm,
8464 &self.ws.p1,
8465 &h1,
8466 self.rms_eps,
8467 self.norm_style,
8468 self.pool.as_deref(),
8469 ),
8470 dense_moe_ffn(
8471 dm,
8472 &self.ws.p2,
8473 &h2,
8474 self.rms_eps,
8475 self.norm_style,
8476 self.pool.as_deref(),
8477 ),
8478 ),
8479 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8480 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8481 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8482 ),
8483 _ => ffn_forward_pair(
8484 &lw.ffn,
8485 &self.ws.p1,
8486 &self.ws.p2,
8487 self.pool.as_deref(),
8488 None,
8489 ),
8490 };
8491 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8492 Some(w) => (
8493 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8494 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8495 ),
8496 None => (f1, f2),
8497 };
8498 for i in 0..self.hidden_size {
8499 h1[i] += f1[i];
8500 h2[i] += f2[i];
8501 }
8502 let (mut f1, mut f2) = (f1, f2);
8503 attention::recycle_buf(&mut f1);
8504 attention::recycle_buf(&mut f2);
8505 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8506 for i in 0..self.hidden_size {
8507 h1[i] *= sc;
8508 h2[i] *= sc;
8509 }
8510 }
8511 if self.is_loop_end(li) && li + 1 < self.num_layers {
8513 h1 = inference::rms_norm(
8514 &h1,
8515 &self.weights.final_norm,
8516 self.rms_eps,
8517 self.norm_style,
8518 );
8519 h2 = inference::rms_norm(
8520 &h2,
8521 &self.weights.final_norm,
8522 self.rms_eps,
8523 self.norm_style,
8524 );
8525 }
8526 }
8527 crate::gpu::set_layer(-1); if self.o1_active() {
8534 self.commit_linear_scratch();
8535 }
8536 self.o1_progress();
8537 self.swa_trim_tails();
8538 (h1, h2)
8539 }
8540
8541 fn commit_linear_scratch(&mut self) {
8543 for layer in &mut self.kv_cache.layers {
8544 if !layer.linear_scratch.is_empty() {
8545 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8546 layer.linear_scratch.clear();
8547 }
8548 }
8549 }
8550
8551 pub fn forward_ids(
8554 &mut self,
8555 ids: &[u32],
8556 task_mask: Option<&TaskMask>,
8557 ) -> Result<Vec<f32>, String> {
8558 #[cfg(target_os = "macos")]
8559 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8560 if ids.is_empty() {
8561 return Err("empty id sequence".to_string());
8562 }
8563 self.clear_sequence_state();
8564 self.check_forward_graph("forward_ids setup", 0)?;
8565 if task_mask.is_none() {
8566 self.o1_begin();
8567 }
8568 let mut hidden = vec![0.0f32; self.hidden_size];
8569 let mut pos = 0usize;
8570 if let Some(b) = &mut self.dsv41 {
8571 let pool = self.pool.clone();
8572 let mut logits = Vec::new();
8573 crate::dsv41::forward_chunk(
8574 &b.0,
8575 &b.1,
8576 &b.2,
8577 &mut b.3,
8578 ids,
8579 0,
8580 pool.as_deref(),
8581 &mut logits,
8582 );
8583 if let Err(err) = self.o1_seal_checked() {
8584 self.clear_sequence_state();
8585 return Err(err);
8586 }
8587 return Ok(logits);
8588 }
8589 #[cfg(not(target_os = "macos"))]
8600 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8601 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8602 Ok(r) => r,
8603 Err(e) => {
8604 self.clear_sequence_state();
8605 return Err(e);
8606 }
8607 };
8608 if n > 0 {
8609 hidden.copy_from_slice(&last);
8610 pos = n;
8611 }
8612 }
8613 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8614 let chunk = self.prefill_chunk();
8618 let hs = self.hidden_size;
8619 while pos < ids.len() {
8620 let end = (pos + chunk).min(ids.len());
8621 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8622 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8623 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8624 pos = end;
8625 }
8626 }
8627 if task_mask.is_none()
8636 && !self.graph_prefill_preferred()
8637 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8638 && self.pair_supported()
8639 {
8640 while pos + 1 < ids.len() {
8641 let e1 = self.embed_single(ids[pos]);
8642 let e2 = self.embed_single(ids[pos + 1]);
8643 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8644 self.check_forward_graph("forward_ids pair", pos + 1)?;
8645 self.commit_linear_scratch();
8646 hidden = h2;
8647 pos += 2;
8648 }
8649 }
8650 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8653 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8654 self.graph_logits = Some(lg);
8655 hidden = vec![0.0; self.hidden_size];
8656 pos = ids.len();
8657 }
8658 }
8659 while pos < ids.len() {
8660 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8661 self.check_forward_graph("forward_ids", pos)?;
8662 pos += 1;
8663 }
8664 if let Some(logits) = self.graph_logits.take() {
8665 if let Err(err) = self.o1_seal_checked() {
8669 self.clear_sequence_state();
8670 return Err(err);
8671 }
8672 return Ok(logits);
8673 }
8674 if let Err(err) = self.o1_seal_checked() {
8678 self.clear_sequence_state();
8679 return Err(err);
8680 }
8681 let normed = inference::rms_norm(
8682 &hidden,
8683 &self.weights.final_norm,
8684 self.rms_eps,
8685 self.norm_style,
8686 );
8687 Ok(self.lm_head_forward(&normed))
8688 }
8689
8690 #[doc(hidden)]
8694 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8695 #[cfg(target_os = "macos")]
8696 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8697 if ids.is_empty() {
8698 return Err("empty id sequence".to_string());
8699 }
8700 self.clear_sequence_state();
8701 self.dsv41
8702 .as_ref()
8703 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8704 self.o1_begin();
8705 let rows = {
8706 let pool = self.pool.clone();
8707 let b = self
8708 .dsv41
8709 .as_mut()
8710 .expect("dsv41 checked above; state cannot change during forward");
8711 let mut rows = Vec::with_capacity(ids.len());
8712 for (position, &id) in ids.iter().enumerate() {
8713 let mut logits = Vec::new();
8714 crate::dsv41::forward_token(
8715 &b.0,
8716 &b.1,
8717 &b.2,
8718 &mut b.3,
8719 id,
8720 position,
8721 pool.as_deref(),
8722 &mut logits,
8723 );
8724 rows.push(logits);
8725 }
8726 rows
8727 };
8728 self.o1_seal();
8729 Ok(rows)
8730 }
8731
8732 pub fn dsv41_moe_capture(
8736 &mut self,
8737 ids: &[u32],
8738 lo: usize,
8739 hi: usize,
8740 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
8741 #[cfg(target_os = "macos")]
8742 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8743 if ids.is_empty() {
8744 return Err("empty id sequence".to_string());
8745 }
8746 self.dsv41
8747 .as_ref()
8748 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
8749 self.clear_sequence_state();
8750 let res = {
8751 let pool = self.pool.clone();
8752 let b = self.dsv41.as_mut().expect("dsv41 checked above");
8753 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
8754 };
8755 self.clear_sequence_state();
8756 res
8757 }
8758
8759 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8766 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8767 Ok((nll / cnt.max(1) as f64).exp())
8768 }
8769
8770 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
8777 if !self.can_prefill_batched() {
8778 return self.nll_ids_from(ids, 0).map(|_| ());
8779 }
8780 self.nll_begin()?;
8781 let result: Result<(), String> = (|| {
8782 let chunk = chunk.max(1);
8783 let mut pos = 0usize;
8784 while pos < ids.len() {
8785 let end = (pos + chunk).min(ids.len());
8786 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
8787 self.nll_check_graph("calibration prefill", pos)?;
8788 pos = end;
8789 }
8790 Ok(())
8791 })();
8792 self.nll_end();
8793 result
8794 }
8795
8796 pub fn calib_window_span(
8805 &mut self,
8806 ids: &[u32],
8807 input: Option<&[f32]>,
8808 from: usize,
8809 mid: usize,
8810 upto: usize,
8811 ) -> Result<Vec<f32>, String> {
8812 if !self.can_prefill_batched() {
8813 return Err("calib_window_span: this architecture has no batched prefill".into());
8814 }
8815 self.nll_begin()?;
8816 let result: Result<Vec<f32>, String> = (|| {
8817 let h = if mid > from {
8818 let inp = match input {
8819 Some(h) => PrefillIn::Hidden(h),
8820 None => PrefillIn::Ids(ids),
8821 };
8822 let h = self.prefill_batch_span(inp, 0, None, from, mid);
8823 self.nll_check_graph("calibration span", 0)?;
8824 h
8825 } else {
8826 input.map(<[f32]>::to_vec).unwrap_or_default()
8827 };
8828 if upto > mid {
8829 let inp = if mid > from || input.is_some() {
8830 PrefillIn::Hidden(&h)
8831 } else {
8832 PrefillIn::Ids(ids)
8833 };
8834 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
8835 self.nll_check_graph("calibration span", 0)?;
8836 }
8837 Ok(h)
8838 })();
8839 self.nll_end();
8840 result
8841 }
8842
8843 #[cfg(feature = "gpu")]
8847 pub fn qwen4_capture(
8848 &mut self,
8849 ids: &[u32],
8850 lo: usize,
8851 hi: usize,
8852 ) -> Result<Vec<Vec<f32>>, String> {
8853 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
8854 crate::qwen4_exp::capture_window(
8855 &b.0,
8856 &b.1,
8857 &b.2,
8858 &mut b.3,
8859 ids,
8860 lo,
8861 hi,
8862 &self.inv_freq,
8863 self.pool.as_deref(),
8864 )
8865 }
8866
8867 pub fn qwen4_expert_dirs(
8869 &self,
8870 li: usize,
8871 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
8872 let b = self.qwen4_exp.as_ref()?;
8873 crate::qwen4_exp::expert_dirs(&b.1, li)
8874 }
8875
8876 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8881 self.clear_sequence_state();
8882 FFN_PROBE.with(|p| {
8883 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8884 });
8885 crate::gpu::cpu_scope(|| {
8886 for (pos, &id) in ids.iter().enumerate() {
8887 let emb = self.embed_single(id);
8888 let _ = self.forward_layers(&emb, pos, None);
8889 }
8890 });
8891 self.clear_sequence_state();
8892 FFN_PROBE
8893 .with(|p| p.borrow_mut().take())
8894 .unwrap_or_default()
8895 }
8896
8897 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8901 if let Err(err) = self.nll_begin() {
8902 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8906 self.nll_end();
8907 return Err(err);
8908 }
8909 FFN_PROBE.with(|p| {
8910 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8911 });
8912 let result: Result<(), String> = (|| {
8913 for chunk in ids.chunks(256) {
8914 if chunk.len() < 2 {
8915 continue;
8916 }
8917 self.nll_ids_masked(chunk, 0, None)?;
8918 }
8919 Ok(())
8920 })();
8921 self.nll_end();
8922 let probe = FFN_PROBE
8923 .with(|p| p.borrow_mut().take())
8924 .unwrap_or_default();
8925 match result {
8926 Ok(()) => Ok(probe),
8927 Err(err) => {
8928 drop(probe);
8929 Err(err)
8930 }
8931 }
8932 }
8933
8934 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8938 self.nll_begin()?;
8939 let result: Result<f64, String> = (|| {
8940 let mut nll = 0f64;
8941 let mut cnt = 0usize;
8942 let mut hidden = vec![0f32; self.hidden_size];
8943 for (pos, &id) in ids.iter().enumerate() {
8944 if pos > 0 {
8945 inference::rms_norm_into(
8946 &hidden,
8947 &self.weights.final_norm,
8948 self.rms_eps,
8949 self.norm_style,
8950 &mut self.ws.n1,
8951 );
8952 let mut logits = self.lm_head_forward(&self.ws.n1);
8953 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8954 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8955 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8956 nll -= p.max(1e-300).ln();
8957 cnt += 1;
8958 attention::recycle_buf(&mut logits);
8959 }
8960 let emb = self.embed_single(id);
8961 hidden = self.forward_layers(&emb, pos, Some(mask));
8962 self.nll_check_graph("masked serial forward", pos)?;
8963 let _ = self.graph_logits.take();
8967 }
8968 Ok((nll / cnt.max(1) as f64).exp())
8969 })();
8970 self.nll_end();
8971 result
8972 }
8973
8974 pub fn nll_ids_masked(
8993 &mut self,
8994 ids: &[u32],
8995 start: usize,
8996 task_mask: Option<&TaskMask>,
8997 ) -> Result<(f64, usize), String> {
8998 let task_mask = self.drop_open_mask(task_mask);
8999 self.nll_ids_inner(ids, start, task_mask)
9000 }
9001
9002 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9003 self.nll_ids_inner(ids, start, None)
9004 }
9005
9006 fn nll_ids_inner(
9007 &mut self,
9008 ids: &[u32],
9009 start: usize,
9010 task_mask: Option<&TaskMask>,
9011 ) -> Result<(f64, usize), String> {
9012 self.nll_begin()?;
9013 let result: Result<(f64, usize), String> = (|| {
9014 let mut nll = 0f64;
9015 let mut cnt = 0usize;
9016 let (graph_quality, fused_head_quality) = nll_graph_policy(
9029 task_mask.is_none(),
9030 self.graph_prefill_preferred(),
9031 crate::gpu::q1_force(),
9032 );
9033 self.graph_head_required = fused_head_quality;
9034 self.graph_want_logits = fused_head_quality;
9035 #[cfg(target_os = "macos")]
9036 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9037 match self.nll_batch_metal(ids, start) {
9038 MetalBatchNllOutcome::Completed(nll, count) => {
9039 return Ok((nll, count));
9040 }
9041 MetalBatchNllOutcome::Declined => {}
9042 MetalBatchNllOutcome::Failed(err) => return Err(err),
9043 }
9044 }
9045 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9050 if self.can_prefill_batched() && !graph_quality && !force_serial {
9051 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9060 .ok()
9061 .and_then(|v| v.parse().ok())
9062 .filter(|&c: &usize| c > 0)
9063 .unwrap_or(128);
9064 const LM_SUB: usize = 32;
9065 let n = ids.len().saturating_sub(1);
9066 let hs = self.hidden_size;
9067 let rows = self.weights.lm_head.rows();
9068 let mut pos = 0usize;
9069 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9070 while pos < n {
9071 let end = (pos + chunk_len).min(n);
9072 let bsz = end - pos;
9073 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9074 self.nll_check_graph("batched prefill", pos)?;
9075 if state_trace && end % 256 == 0 {
9076 self.trace_recurrent_state(end);
9077 }
9078 let mut k0 = 0usize;
9079 while k0 < bsz {
9080 let k1 = (k0 + LM_SUB).min(bsz);
9081 let sb = k1 - k0;
9082 if pos + k1 <= start {
9085 k0 = k1;
9086 continue;
9087 }
9088 let mut normed = vec![0.0f32; sb * hs];
9089 for k in 0..sb {
9090 let r = inference::rms_norm(
9091 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9092 &self.weights.final_norm,
9093 self.rms_eps,
9094 self.norm_style,
9095 );
9096 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9097 }
9098 let mut logits = vec![0.0f32; sb * rows];
9099 self.weights
9100 .lm_head
9101 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9102 for k in 0..sb {
9103 if pos + k0 + k < start {
9104 continue;
9105 }
9106 self.nll_check_graph("batched score row", pos + k0 + k)?;
9107 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9108 if let Some(mu) = self.logit_multiplier {
9109 for v in lg.iter_mut() {
9110 *v *= mu;
9111 }
9112 }
9113 if let Some(c) = self.final_softcap {
9117 softcap_in_place(lg, c, self.pool.as_deref());
9118 }
9119 if let Some(cm) = self.head_clusters.clone() {
9122 self.hierarchical_head_logprobs(
9123 &normed[k * hs..(k + 1) * hs],
9124 &cm,
9125 lg,
9126 );
9127 }
9128 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9129 let target = ids[pos + k0 + k + 1] as usize;
9130 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9131 let lse: f64 = lg
9132 .iter()
9133 .map(|&v| ((v - max) as f64).exp())
9134 .sum::<f64>()
9135 .ln()
9136 + max as f64;
9137 nll += lse - lg[target] as f64;
9138 cnt += 1;
9139 if std::env::var("CMF_PPL_TRACE").is_ok() {
9140 let top = lg
9141 .iter()
9142 .enumerate()
9143 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9144 .map(|(i, _)| i)
9145 .unwrap_or(0);
9146 eprintln!(
9147 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9148 pos + k0 + k,
9149 target,
9150 lse - lg[target] as f64,
9151 top,
9152 lg[target],
9153 lg[top]
9154 );
9155 }
9156 }
9157 k0 = k1;
9158 }
9159 pos = end;
9160 }
9161 return Ok((nll, cnt));
9162 }
9163 for pos in 0..ids.len().saturating_sub(1) {
9164 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9165 self.nll_check_graph("serial forward", pos)?;
9166 let out_of_band = self.graph_logits.take();
9174 if self.graph_head_required && out_of_band.is_none() {
9175 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9176 return Err(format!(
9177 "fused Metal graph head did not complete at NLL position {pos}"
9178 ));
9179 }
9180 if pos < start {
9181 continue;
9182 }
9183 let logits = match out_of_band {
9184 Some(lg) => lg,
9185 None => {
9186 let normed = inference::rms_norm(
9187 &hidden,
9188 &self.weights.final_norm,
9189 self.rms_eps,
9190 self.norm_style,
9191 );
9192 self.lm_head_forward(&normed)
9196 }
9197 };
9198 let target = ids[pos + 1] as usize;
9199 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9200 let lse: f64 = logits
9201 .iter()
9202 .map(|&v| ((v - max) as f64).exp())
9203 .sum::<f64>()
9204 .ln()
9205 + max as f64;
9206 let tok_nll = lse - logits[target] as f64;
9207 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9208 let top = logits
9209 .iter()
9210 .enumerate()
9211 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9212 .map(|(i, _)| i)
9213 .unwrap_or(0);
9214 eprintln!(
9215 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9216 logits[target], logits[top]
9217 );
9218 }
9219 nll += tok_nll;
9220 cnt += 1;
9221 }
9222 Ok((nll, cnt))
9223 })();
9224 self.nll_end();
9225 result
9226 }
9227
9228 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9233 let normed = inference::rms_norm(
9234 hidden,
9235 &self.weights.final_norm,
9236 self.rms_eps,
9237 self.norm_style,
9238 );
9239 let mut logits = self.lm_head_forward(&normed);
9242 let target = target as usize;
9243 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9244 let lse: f64 = logits
9245 .iter()
9246 .map(|&v| ((v - max) as f64).exp())
9247 .sum::<f64>()
9248 .ln()
9249 + max as f64;
9250 let tok_nll = lse - logits[target] as f64;
9251 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9252 let top = logits
9253 .iter()
9254 .enumerate()
9255 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9256 .map(|(i, _)| i)
9257 .unwrap_or(0);
9258 eprintln!(
9259 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9260 logits[target], logits[top]
9261 );
9262 }
9263 attention::recycle_buf(&mut logits);
9264 tok_nll
9265 }
9266
9267 fn trace_recurrent_state(&self, pos: usize) {
9275 let stats = |v: &[f32]| -> (f64, f64) {
9276 if v.is_empty() {
9277 return (0.0, 0.0);
9278 }
9279 let (mut ss, mut mx) = (0f64, 0f64);
9280 for &x in v {
9281 ss += (x as f64) * (x as f64);
9282 mx = mx.max((x as f64).abs());
9283 }
9284 ((ss / v.len() as f64).sqrt(), mx)
9285 };
9286 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9287 let lw = &self.weights.layers[self.phys_layer(li)];
9288 let (kind, s_len) = match &lw.attn {
9289 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9290 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9291 AttnKind::Bounded(_) => ("bounded", 0),
9292 AttnKind::Full { .. } => ("full", 0),
9293 _ => ("other", 0),
9294 };
9295 let (rms, max) = stats(&l.linear_state);
9296 let s_part = if kind == "vmf" {
9297 &l.linear_state[..s_len.min(l.linear_state.len())]
9298 } else if kind == "gdn" {
9299 let ring = l
9300 .linear_state
9301 .len()
9302 .saturating_sub(s_len)
9303 .min(l.linear_state.len());
9304 &l.linear_state[ring..]
9305 } else {
9306 &l.linear_state[..0]
9307 };
9308 let (s_rms, s_max) = stats(s_part);
9309 let (ring_rms, ring_len) = match &l.bounded {
9310 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9311 None => (0.0, 0),
9312 };
9313 eprintln!(
9314 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9315 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9316 l.linear_state.len(),
9317 l.seq_len
9318 );
9319 }
9320 }
9321
9322 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9340 self.nll_begin()?;
9345 let requested_prefix = (prefill > 0).then_some(prefill);
9346 self.o1_begin_with_prefix(requested_prefix);
9347 let n = ids.len().saturating_sub(1);
9348 let requested_start = prefill.min(n);
9349 let exact_end = if self.o1_active() {
9354 match requested_prefix {
9355 Some(requested) => self.o1_effective_boundary(requested),
9356 None => self
9357 .o1_cfg
9358 .as_ref()
9359 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9360 }
9361 .unwrap_or(requested_start)
9362 .min(n)
9363 } else {
9364 requested_start
9365 };
9366 let mut nll = 0f64;
9367 let mut cnt = 0usize;
9368
9369 let mut pos = 0usize;
9373 if self.can_prefill_batched() {
9374 const CHUNK: usize = 128;
9375 while pos < exact_end {
9376 let end = (pos + CHUNK).min(exact_end);
9377 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9378 if self
9379 .graph_failed
9380 .swap(false, std::sync::atomic::Ordering::Relaxed)
9381 {
9382 self.cancel
9383 .store(false, std::sync::atomic::Ordering::Relaxed);
9384 self.nll_end();
9385 return Err("GPU graph failed during O(1) NLL prefix".into());
9386 }
9387 for row in 0..end - pos {
9388 let score_pos = pos + row;
9389 if score_pos >= requested_start && score_pos < n {
9390 nll += self.nll_from_hidden(
9391 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9392 ids[score_pos + 1],
9393 score_pos,
9394 );
9395 cnt += 1;
9396 }
9397 }
9398 pos = end;
9399 }
9400 } else {
9401 while pos < exact_end {
9402 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9403 if self
9404 .graph_failed
9405 .swap(false, std::sync::atomic::Ordering::Relaxed)
9406 {
9407 self.cancel
9408 .store(false, std::sync::atomic::Ordering::Relaxed);
9409 self.nll_end();
9410 return Err("GPU graph failed during O(1) NLL prefix".into());
9411 }
9412 if pos >= requested_start && pos < n {
9413 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9414 cnt += 1;
9415 }
9416 pos += 1;
9417 }
9418 }
9419 self.o1_seal_checked().map_err(|err| {
9420 self.nll_end();
9421 err
9422 })?;
9423
9424 let batch_k = std::env::var("CMF_BATCH_K")
9433 .ok()
9434 .and_then(|v| v.parse::<usize>().ok())
9435 .unwrap_or(0);
9436 let batch_admitted = batch_k > 0
9437 && self.can_prefill_batched()
9438 && self.o1_active()
9439 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9440 && (0..self.num_layers).all(|li| {
9441 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9442 cache.o1.is_none() || cache.o1_views().is_some()
9443 });
9444 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9445 eprintln!(
9446 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9447 batch_admitted,
9448 batch_k,
9449 n.saturating_sub(exact_end),
9450 );
9451 }
9452 let mut batch_completed = false;
9453 if batch_admitted && exact_end < n {
9454 let hs = self.hidden_size;
9455 let mut batch_pos = exact_end;
9456 while batch_pos < n {
9457 let end = (batch_pos + batch_k).min(n);
9458 let bk = end - batch_pos;
9459 let mut hiddens = vec![0.0f32; bk * hs];
9460 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9461 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9462 }
9463 let positions: Vec<usize> = (batch_pos..end).collect();
9464 let t_batch = std::time::Instant::now();
9465 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9466 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9467 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9468 eprintln!(
9469 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9470 batch_pos,
9471 end.saturating_sub(1),
9472 bk as f64 / (ms / 1000.0),
9473 );
9474 }
9475 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9476 self.nll_end();
9477 return Err(err);
9478 }
9479 match outcome {
9480 crate::gpu::BatchGraphOutcome::Completed => {
9481 batch_completed = true;
9482 for row in 0..bk {
9483 nll += self.nll_from_hidden(
9484 &hiddens[row * hs..(row + 1) * hs],
9485 ids[batch_pos + row + 1],
9486 batch_pos + row,
9487 );
9488 cnt += 1;
9489 }
9490 batch_pos = end;
9491 }
9492 crate::gpu::BatchGraphOutcome::Declined => {
9493 if batch_completed {
9494 self.nll_end();
9495 return Err(format!(
9496 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9497 ));
9498 }
9499 break;
9500 }
9501 crate::gpu::BatchGraphOutcome::Failed => {
9502 self.nll_end();
9503 return Err(format!(
9504 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9505 ));
9506 }
9507 }
9508 }
9509 if batch_completed && cnt == n.saturating_sub(requested_start) {
9510 self.nll_end();
9511 return Ok((nll, cnt));
9512 }
9513 }
9514
9515 for pos in exact_end..n {
9520 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9521 if self
9522 .graph_failed
9523 .swap(false, std::sync::atomic::Ordering::Relaxed)
9524 {
9525 self.cancel
9526 .store(false, std::sync::atomic::Ordering::Relaxed);
9527 self.nll_end();
9528 return Err(format!(
9529 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9530 ));
9531 }
9532 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9533 cnt += 1;
9534 }
9535 self.nll_end();
9536 Ok((nll, cnt))
9537 }
9538
9539 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9547 self.clear_sequence_state();
9548 let n = ids.len().saturating_sub(1);
9549 let mut correct = Vec::with_capacity(n);
9550 let mut pmax = Vec::with_capacity(n);
9551 for pos in 0..n {
9552 let emb = self.embed_single(ids[pos]);
9553 let hidden = self.forward_layers(&emb, pos, None);
9554 let logits = if let Some(logits) = self.graph_logits.take() {
9555 logits
9556 } else {
9557 let normed = inference::rms_norm(
9558 &hidden,
9559 &self.weights.final_norm,
9560 self.rms_eps,
9561 self.norm_style,
9562 );
9563 self.lm_head_forward(&normed)
9567 };
9568 let target = ids[pos + 1] as usize;
9569 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9570 for (i, &v) in logits.iter().enumerate() {
9571 if v > mval {
9572 mval = v;
9573 amax = i;
9574 }
9575 }
9576 correct.push(amax == target);
9577 let row: Vec<f32> = temps
9578 .iter()
9579 .map(|&t| {
9580 let tt = t.max(1e-3);
9581 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9582 1.0 / s.max(1e-12) })
9584 .collect();
9585 pmax.push(row);
9586 }
9587 self.clear_sequence_state();
9588 (correct, pmax)
9589 }
9590
9591 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9598 if self.dyn_router.is_none() {
9599 return Ok((self.ppl_ids(ids)?, 0));
9600 }
9601 self.nll_begin()?;
9602 let saved_active = self.dyn_active;
9603 let mut router = self
9604 .dyn_router
9605 .take()
9606 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9607 router.reset();
9608 self.dyn_phi_seen = 0;
9609 let _ = self.set_active_skill(None);
9610
9611 let result: Result<(f64, usize), String> = (|| {
9612 let mut nll = 0f64;
9613 let mut cnt = 0usize;
9614 for pos in 0..ids.len().saturating_sub(1) {
9615 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9616 self.nll_check_graph("dynamic serial forward", pos)?;
9617 let out_of_band = self.graph_logits.take();
9618 let mut logits = match out_of_band {
9619 Some(lg) => lg,
9620 None => {
9621 let normed = inference::rms_norm(
9622 &hidden,
9623 &self.weights.final_norm,
9624 self.rms_eps,
9625 self.norm_style,
9626 );
9627 self.lm_head_forward(&normed)
9631 }
9632 };
9633 let target = ids[pos + 1] as usize;
9634 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9635 let lse: f64 = logits
9636 .iter()
9637 .map(|&v| ((v - max) as f64).exp())
9638 .sum::<f64>()
9639 .ln()
9640 + max as f64;
9641 let tok_nll = lse - logits[target] as f64;
9642 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9643 let top = logits
9644 .iter()
9645 .enumerate()
9646 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9647 .map(|(i, _)| i)
9648 .unwrap_or(0);
9649 eprintln!(
9650 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9651 logits[target], logits[top]
9652 );
9653 }
9654 nll += tok_nll;
9655 cnt += 1;
9656 attention::recycle_buf(&mut logits);
9657 let phi = self.dyn_phi_ema.clone();
9659 if let Some(new_active) = router.step(&phi, pos) {
9660 let _ = self.set_active_skill(new_active);
9661 }
9662 }
9663 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9664 })();
9665
9666 let _ = self.set_active_skill(saved_active);
9669 self.dyn_router = Some(router);
9670 self.nll_end();
9671 result
9672 }
9673
9674 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9676 self.clear_sequence_state();
9677 let mut acc = vec![0f32; self.hidden_size];
9678 for (pos, &id) in ids.iter().enumerate() {
9679 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9680 for (a, v) in acc.iter_mut().zip(&h) {
9681 *a += v;
9682 }
9683 }
9684 let n = ids.len().max(1) as f32;
9685 for a in acc.iter_mut() {
9686 *a /= n;
9687 }
9688 self.clear_sequence_state();
9689 acc
9690 }
9691
9692 pub fn probe_phi_span(
9706 &mut self,
9707 ids: &[u32],
9708 layer: usize,
9709 span: std::ops::Range<usize>,
9710 ) -> Vec<f32> {
9711 #[cfg(target_os = "macos")]
9712 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9713 let end = span.end.min(ids.len());
9714 let start = span.start.min(end);
9715 let reset = |p: &mut Self| p.clear_sequence_state();
9716 reset(self);
9717 let mut acc = vec![0f32; self.hidden_size];
9718 for (pos, &id) in ids[..end].iter().enumerate() {
9719 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9720 if pos >= start {
9721 for (a, v) in acc.iter_mut().zip(&h) {
9722 *a += v;
9723 }
9724 }
9725 }
9726 let n = end - start;
9727 if n > 0 {
9728 let n = n as f32;
9729 for a in acc.iter_mut() {
9730 *a /= n;
9731 }
9732 }
9733 reset(self);
9734 acc
9735 }
9736
9737 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9744 #[cfg(target_os = "macos")]
9745 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9746 self.graph_logits = None;
9747 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9748 if let Some(logits) = self.graph_logits.take() {
9749 return logits;
9750 }
9751 inference::rms_norm_into(
9752 &hidden,
9753 &self.weights.final_norm,
9754 self.rms_eps,
9755 self.norm_style,
9756 &mut self.ws.n1,
9757 );
9758 self.lm_head_forward(&self.ws.n1)
9759 }
9760
9761 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9767 self.prefill_batch_masked(ids, start_pos, None)
9768 }
9769
9770 fn prefill_batch_masked(
9776 &mut self,
9777 ids: &[u32],
9778 start_pos: usize,
9779 task_mask: Option<&TaskMask>,
9780 ) -> Vec<f32> {
9781 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9782 }
9783
9784 fn prefill_rows(
9792 &mut self,
9793 ids: &[u32],
9794 pos: usize,
9795 task_mask: Option<&TaskMask>,
9796 ) -> Result<Vec<f32>, String> {
9797 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9798 }
9799
9800 fn prefill_input_rows(
9801 &mut self,
9802 input: PrefillIn<'_>,
9803 pos: usize,
9804 task_mask: Option<&TaskMask>,
9805 ) -> Result<Vec<f32>, String> {
9806 self.mimo_moe_prepare();
9807 let hs = self.hidden_size;
9808 let bk = match input {
9809 PrefillIn::Ids(ids) => ids.len(),
9810 PrefillIn::Hidden(rows) => rows.len() / hs,
9811 };
9812 #[cfg(not(target_os = "macos"))]
9813 if task_mask.is_none()
9814 && !self.o1_active()
9815 && bk > 1
9816 && (self.batch_prefix_prefill()
9817 || (self.verify_exact_moe
9818 && crate::gpu::enabled_here()
9819 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9820 {
9821 let mut hiddens = match input {
9822 PrefillIn::Hidden(rows) => rows.to_vec(),
9823 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9824 };
9825 let positions: Vec<usize> = (pos..pos + bk).collect();
9826 let mut run = 0usize;
9827 match self.try_batch_graph_wgpu_prefix(
9828 &mut hiddens,
9829 &positions,
9830 bk,
9831 None,
9832 Some(&mut run),
9833 ) {
9834 crate::gpu::BatchGraphOutcome::Completed => {
9835 let out = if run < self.num_layers {
9836 self.prefill_batch_span(
9837 PrefillIn::Hidden(&hiddens),
9838 pos,
9839 None,
9840 run,
9841 self.num_layers,
9842 )
9843 } else {
9844 hiddens
9845 };
9846 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9847 Err("MiMo attention graph failed after admission".into())
9848 } else {
9849 Ok(out)
9850 };
9851 }
9852 crate::gpu::BatchGraphOutcome::Failed => {
9853 return Err("batched prefix prefill failed after admission".into());
9854 }
9855 crate::gpu::BatchGraphOutcome::Declined => {
9856 #[cfg(feature = "gpu")]
9858 self.pull_lagging_host_kv(0, self.num_layers, pos);
9859 }
9860 }
9861 }
9862 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9863 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9864 Err("batch tail graph failed after admission".into())
9865 } else {
9866 Ok(out)
9867 }
9868 }
9869
9870 fn prefill_batch_span(
9876 &mut self,
9877 input: PrefillIn<'_>,
9878 start_pos: usize,
9879 task_mask: Option<&TaskMask>,
9880 from: usize,
9881 upto_excl: usize,
9882 ) -> Vec<f32> {
9883 let hs = self.hidden_size;
9884 let b = match input {
9885 PrefillIn::Ids(ids) => ids.len(),
9886 PrefillIn::Hidden(hb) => hb.len() / hs,
9887 };
9888 let upto_excl = upto_excl.min(self.num_layers);
9889 let mut h: Vec<f32>;
9893 let mut h_ready;
9894 match input {
9895 PrefillIn::Ids(_) => {
9896 h = vec![0.0; b * hs];
9897 h_ready = false;
9898 }
9899 PrefillIn::Hidden(hb) => {
9900 h = hb.to_vec();
9901 h_ready = true;
9902 }
9903 }
9904 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9905 if let PrefillIn::Ids(ids) = input {
9906 for (bi, &id) in ids.iter().enumerate() {
9907 let e = me.embed_single(id);
9908 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9909 }
9910 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9911 if let Ok(t) = tp.parse::<usize>() {
9912 if t >= start_pos && t < start_pos + ids.len() {
9913 let bi = t - start_pos;
9914 let row = &h[bi * hs..(bi + 1) * hs];
9915 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9916 eprintln!(
9917 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9918 ids[bi],
9919 row[0],
9920 row[1],
9921 ids.len(),
9922 &ids[..ids.len().min(8)]
9923 );
9924 }
9925 }
9926 }
9927 }
9928 };
9929 let (_nkv, _hd, _rd, eps) = (
9930 self.num_kv_heads,
9931 self.head_dim,
9932 self.rotary_dim,
9933 self.rms_eps,
9934 );
9935 let pool = self.pool.clone();
9936 let norm_style = self.norm_style;
9937 self.mimo_moe_prepare();
9938 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9939
9940 #[cfg(target_os = "macos")]
9941 let mut chunk_skip_until = 0usize;
9942 for li in from..upto_excl {
9943 let _capacity_tail = automatic_gpu_prefix
9944 .filter(|&prefix| {
9945 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9946 })
9947 .map(|_| crate::gpu::enter_cpu_scope());
9948 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9953 let _fast_gemm = self
9954 .proj_gate_sigmoid
9955 .then(crate::gpu::enter_prefill_fast_gemm);
9956 #[cfg(target_os = "macos")]
9962 if task_mask.is_none() {
9963 if li < chunk_skip_until {
9964 continue;
9965 }
9966 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9972 fill_h(&mut h, self);
9973 h_ready = true;
9974 }
9975 let ids_for_embed = match input {
9976 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9977 PrefillIn::Hidden(_) => None,
9978 };
9979 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9980 if end > li {
9981 h_ready = true;
9982 chunk_skip_until = end;
9983 if self.is_loop_end(end - 1) && end < self.num_layers {
9986 for bi in 0..b {
9987 let normed = inference::rms_norm(
9988 &h[bi * hs..(bi + 1) * hs],
9989 &self.weights.final_norm,
9990 eps,
9991 norm_style,
9992 );
9993 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9994 }
9995 }
9996 continue;
9997 }
9998 }
9999 if !h_ready {
10000 fill_h(&mut h, self);
10001 h_ready = true;
10002 }
10003 if task_mask.is_none() && self.verify_exact_moe {
10004 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10005 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10006 crate::gpu::BatchGraphOutcome::Completed => continue,
10007 crate::gpu::BatchGraphOutcome::Failed => return h,
10008 crate::gpu::BatchGraphOutcome::Declined => {}
10009 }
10010 }
10011 #[cfg(feature = "gpu")]
10012 self.pull_lagging_host_kv(li, li + 1, start_pos);
10013 let lw = &self.weights.layers[self.phys_layer(li)];
10014 let t_attn = std::time::Instant::now();
10015 match &lw.attn {
10017 AttnKind::Kda(w) => {
10018 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10020 let mut normed = vec![0.0f32; b * hs];
10021 for bi in 0..b {
10022 inference::rms_norm_into(
10023 &h[bi * hs..(bi + 1) * hs],
10024 &lw.input_norm,
10025 eps,
10026 norm_style,
10027 &mut normed[bi * hs..(bi + 1) * hs],
10028 );
10029 }
10030 let attn = crate::linear_core::kda_forward_batch(
10031 &normed,
10032 b,
10033 w,
10034 &cfg,
10035 &mut self.kv_cache.layers[li].linear_state,
10036 pool.as_deref(),
10037 );
10038 for (dst, &a) in h.iter_mut().zip(&attn) {
10039 *dst += a;
10040 }
10041 }
10042 AttnKind::LinearGdn(w) => {
10043 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10045 let mut normed = vec![0.0f32; b * hs];
10046 for bi in 0..b {
10047 let r = inference::rms_norm(
10048 &h[bi * hs..(bi + 1) * hs],
10049 &lw.input_norm,
10050 eps,
10051 norm_style,
10052 );
10053 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10054 }
10055 let attn = crate::linear_core::gdn_forward_batch(
10056 &normed,
10057 b,
10058 w,
10059 &cfg,
10060 &mut self.kv_cache.layers[li].linear_state,
10061 pool.as_deref(),
10062 );
10063 for (dst, &a) in h.iter_mut().zip(&attn) {
10064 *dst += a;
10065 }
10066 }
10067 AttnKind::ShortConv(w) => {
10068 let cfg = self
10071 .short_conv_cfg
10072 .expect("short-conv layer without short_conv_cfg");
10073 let mut normed = vec![0.0f32; b * hs];
10074 for bi in 0..b {
10075 inference::rms_norm_into(
10076 &h[bi * hs..(bi + 1) * hs],
10077 &lw.input_norm,
10078 eps,
10079 norm_style,
10080 &mut normed[bi * hs..(bi + 1) * hs],
10081 );
10082 }
10083 let attn = short_conv_forward_batch(
10084 &normed,
10085 b,
10086 w,
10087 &cfg,
10088 &mut self.kv_cache.layers[li].linear_state,
10089 pool.as_deref(),
10090 );
10091 for (dst, &a) in h.iter_mut().zip(&attn) {
10092 *dst += a;
10093 }
10094 }
10095 AttnKind::Mla(w) => {
10096 let inv_freq_l = self.layer_inv_freq(li);
10099 let rs = self.layer_rope_scale(li);
10100 let mut normed = vec![0.0f32; hs];
10101 for bi in 0..b {
10102 inference::rms_norm_into(
10103 &h[bi * hs..(bi + 1) * hs],
10104 &lw.input_norm,
10105 eps,
10106 norm_style,
10107 &mut normed,
10108 );
10109 let ao = mla_attention(
10110 w,
10111 &normed,
10112 &mut self.kv_cache.layers[li],
10113 start_pos + bi,
10114 &inv_freq_l,
10115 rs,
10116 eps,
10117 pool.as_deref(),
10118 );
10119 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10120 *dst += a;
10121 }
10122 }
10123 }
10124 AttnKind::Full {
10125 wq,
10126 wk,
10127 wv,
10128 wo,
10129 q_norm,
10130 k_norm,
10131 output_gate,
10132 softplus_gate,
10133 bias,
10134 } => {
10135 let mut normed = vec![0.0f32; b * hs];
10139 for bi in 0..b {
10140 inference::rms_norm_into(
10141 &h[bi * hs..(bi + 1) * hs],
10142 &lw.input_norm,
10143 eps,
10144 norm_style,
10145 &mut normed[bi * hs..(bi + 1) * hs],
10146 );
10147 }
10148 let inv_freq_l = self.layer_inv_freq(li);
10149 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10150 let cfg = QwenAttnCfg {
10151 num_heads: self.layer_num_heads(li),
10152 num_kv_heads: nkv_l,
10153 head_dim: hd_l,
10154 hidden_size: hs,
10155 position: start_pos,
10156 inv_freq: &inv_freq_l,
10157 rotary_dim: rd_l,
10158 scale: self.attn_scale,
10159 softcap: self.attn_softcap,
10160 window: self.layer_window(li),
10161 v_norm: self.attn_v_norm,
10162 qk_norm_after_rope: self.qk_norm_after_rope,
10163 gate_sigmoid: self.proj_gate_sigmoid,
10164 q_norm: q_norm.as_deref(),
10165 k_norm: k_norm.as_deref(),
10166 output_gate: *output_gate,
10167 softplus_gate: softplus_gate
10168 .as_ref()
10169 .map(|(gate, per_head)| (gate, *per_head)),
10170 rope_scale: self.layer_rope_scale(li),
10171 bias: bias
10172 .as_ref()
10173 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10174 rms_eps: eps,
10175 norm_style,
10176 pool: pool.as_deref(),
10177 v_head_dim: self.layer_v_dim(li),
10178 };
10179 #[cfg(feature = "gpu")]
10180 let _mirror = self
10181 .prefill_mirror_target(li)
10182 .map(crate::gpu::enter_prefill_mirror);
10183 let mut attn = attention::qwen_attention_batch(
10184 &normed,
10185 b,
10186 wq,
10187 wk,
10188 wv,
10189 wo,
10190 &mut self.kv_cache.layers[li],
10191 &cfg,
10192 );
10193 if let Some(w) = &lw.attn_out_norm {
10194 for bi in 0..b {
10195 inference::rms_norm_into(
10196 &attn[bi * hs..(bi + 1) * hs],
10197 w,
10198 eps,
10199 norm_style,
10200 &mut normed[bi * hs..(bi + 1) * hs],
10201 );
10202 }
10203 attn.copy_from_slice(&normed);
10204 }
10205 for (dst, &a) in h.iter_mut().zip(&attn) {
10206 *dst += a;
10207 }
10208 }
10209 AttnKind::Bounded(w) => {
10210 let mut normed = vec![0.0f32; b * hs];
10213 for bi in 0..b {
10214 inference::rms_norm_into(
10215 &h[bi * hs..(bi + 1) * hs],
10216 &lw.input_norm,
10217 eps,
10218 norm_style,
10219 &mut normed[bi * hs..(bi + 1) * hs],
10220 );
10221 }
10222 let rope = self
10223 .bounded_rope
10224 .clone()
10225 .expect("bounded layer without an installed rotation table");
10226 let cfg = crate::bounded::BoundedAttnCfg {
10227 num_heads: self.num_heads,
10228 num_kv_heads: self.num_kv_heads,
10229 head_dim: self.head_dim,
10230 hidden_size: hs,
10231 scale: self.attn_scale,
10232 rope: &rope,
10233 pool: pool.as_deref(),
10234 };
10235 let mut attn = crate::bounded::bounded_attention_batch(
10236 &normed,
10237 b,
10238 w,
10239 &mut self.kv_cache.layers[li],
10240 &cfg,
10241 );
10242 if let Some(wn) = &lw.attn_out_norm {
10243 for bi in 0..b {
10244 inference::rms_norm_into(
10245 &attn[bi * hs..(bi + 1) * hs],
10246 wn,
10247 eps,
10248 norm_style,
10249 &mut normed[bi * hs..(bi + 1) * hs],
10250 );
10251 }
10252 attn.copy_from_slice(&normed);
10253 }
10254 for (dst, &a) in h.iter_mut().zip(&attn) {
10255 *dst += a;
10256 }
10257 attention::recycle_buf(&mut attn);
10258 }
10259 AttnKind::Linear(w) => {
10260 for bi in 0..b {
10261 let normed = inference::rms_norm(
10262 &h[bi * hs..(bi + 1) * hs],
10263 &lw.input_norm,
10264 eps,
10265 norm_style,
10266 );
10267 vmf_phase_forward(
10268 &normed,
10269 w,
10270 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10271 &mut self.kv_cache.layers[li].linear_state,
10272 pool.as_deref(),
10273 )
10274 .iter()
10275 .enumerate()
10276 .for_each(|(i, &a)| h[bi * hs + i] += a);
10277 }
10278 }
10279 }
10280
10281 let lw = &self.weights.layers[self.phys_layer(li)];
10283 let mut post = vec![0.0f32; b * hs];
10284 for bi in 0..b {
10285 let r =
10286 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10287 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10288 }
10289 let mask_row = task_mask
10292 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10293 .and_then(|m| m.ffn_masks.get(li))
10294 .map(|v| v.as_slice());
10295 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10296 let t_ffn = std::time::Instant::now();
10297 let mut ffn = match &lw.ffn {
10298 FfnKind::Dense(d) if !d.segs.is_empty() => {
10299 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10300 }
10301 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10302 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10303 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10304 }
10305 FfnKind::Moe(m) if self.verify_exact_moe => {
10306 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10307 }
10308 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10311 let before = m.stats.borrow().clone();
10312 let out = crate::gpu::cpu_scope(|| {
10313 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10314 });
10315 self.mimo_moe.prime(li, m, &before);
10316 out
10317 }
10318 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10319 FfnKind::DenseMoe(dm) => {
10322 let mut out = vec![0.0f32; b * hs];
10323 for bi in 0..b {
10324 let r = dense_moe_ffn(
10325 dm,
10326 &post[bi * hs..(bi + 1) * hs],
10327 &h[bi * hs..(bi + 1) * hs],
10328 eps,
10329 norm_style,
10330 pool.as_deref(),
10331 );
10332 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10333 }
10334 out
10335 }
10336 };
10337 if prefill_prof_on() {
10338 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10339 PREFILL_SPLIT[1].fetch_add(
10340 t_ffn.elapsed().as_nanos() as u64,
10341 std::sync::atomic::Ordering::Relaxed,
10342 );
10343 }
10344 if let Some(w) = &lw.ffn_out_norm {
10345 for bi in 0..b {
10346 inference::rms_norm_into(
10347 &ffn[bi * hs..(bi + 1) * hs],
10348 w,
10349 eps,
10350 norm_style,
10351 &mut post[bi * hs..(bi + 1) * hs],
10352 );
10353 }
10354 ffn.copy_from_slice(&post);
10355 }
10356 for (dst, &f) in h.iter_mut().zip(&ffn) {
10357 *dst += f;
10358 }
10359 if let Some(sc) = lw.layer_scale {
10360 for v in h.iter_mut() {
10361 *v *= sc;
10362 }
10363 }
10364 if self.layer_dump.is_some() {
10366 for bi in 0..b {
10367 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10368 }
10369 }
10370 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10371 if let Ok(t) = tp.parse::<usize>() {
10372 if t >= start_pos && t < start_pos + b {
10373 let bi = t - start_pos;
10374 let row = &h[bi * hs..(bi + 1) * hs];
10375 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10376 eprintln!(
10377 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10378 row[0], row[1]
10379 );
10380 }
10381 }
10382 }
10383 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10387 let row = &h[(b - 1) * hs..b * hs];
10388 let rms =
10389 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10390 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10391 eprintln!(
10392 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10393 match &self.weights.layers[self.phys_layer(li)].attn {
10394 AttnKind::LinearGdn(_) => "gdn",
10395 AttnKind::Linear(_) => "vmf",
10396 AttnKind::ShortConv(_) => "conv",
10397 _ => "attn",
10398 },
10399 match &lw.ffn {
10400 FfnKind::Moe(_) => "moe",
10401 FfnKind::Dense(_) => "dense",
10402 FfnKind::DenseMoe(_) => "dense+moe",
10403 },
10404 );
10405 }
10406 if self.is_loop_end(li) && li + 1 < self.num_layers {
10408 for bi in 0..b {
10409 let normed = inference::rms_norm(
10410 &h[bi * hs..(bi + 1) * hs],
10411 &self.weights.final_norm,
10412 eps,
10413 norm_style,
10414 );
10415 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10416 }
10417 }
10418 if std::env::var("CMF_TRACE_H").is_ok() {
10419 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10420 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10421 eprintln!(
10422 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10423 lw.layer_scale
10424 );
10425 }
10426 }
10427 crate::gpu::set_layer(-1); if prefill_prof_on() {
10429 let ms = |a: &std::sync::atomic::AtomicU64| {
10430 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10431 };
10432 let sp = &attention::ATTN_SPLIT;
10433 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10434 eprintln!(
10435 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10436 host gate {:.1}], host loop {:.1}, attend {:.1} \
10437 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10438 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10439 ms(&PREFILL_SPLIT[0]),
10440 ms(&sp[0]),
10441 qkv_enq,
10442 qkv_wait,
10443 ms(&sp[6]),
10444 ms(&sp[1]),
10445 ms(&sp[2]),
10446 ms(&sp[4]),
10447 ms(&sp[5]),
10448 ca_wait,
10449 ms(&sp[3]),
10450 ms(&PREFILL_SPLIT[1]),
10451 );
10452 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10453 eprintln!(
10454 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10455 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10456 fallbacks (cumulative)",
10457 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10458 );
10459 }
10460 self.o1_progress();
10465 self.swa_trim_tails();
10467 h
10468 }
10469
10470 fn embed_single(&self, id: u32) -> Vec<f32> {
10472 let mut out = vec![0.0f32; self.hidden_size];
10473 if (id as usize) < self.weights.embed_tokens.rows() {
10474 self.weights.embed_tokens.row_f32(id as usize, &mut out);
10475 }
10476 if self.embed_multiplier != 1.0 {
10477 for v in out.iter_mut() {
10478 *v *= self.embed_multiplier;
10479 }
10480 }
10481 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
10485 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10486 v[0] = id as f32;
10487 return v;
10488 }
10489 if let Some(b) = &self.g3n {
10492 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10493 }
10494 out
10495 }
10496
10497 #[cfg(target_os = "macos")]
10503 fn chunk_run_gpu(
10504 &mut self,
10505 li0: usize,
10506 h: &mut [f32],
10507 b: usize,
10508 pos0: usize,
10509 embed_ids: Option<&[u32]>,
10510 cap: usize,
10511 ) -> usize {
10512 let gemma4 =
10519 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10520 if !crate::gpu::enabled_here()
10521 || std::env::var("CMF_GPU_CHUNK")
10522 .map(|v| v == "0")
10523 .unwrap_or(false)
10524 || b < 32
10525 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10528 || (self.global_attn.is_some() && !gemma4)
10529 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10533 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10536 || self.o1_active()
10539 || (self.attn_v_norm && !gemma4)
10540 || !self.metal_rope_scale_ok()
10541 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10542 {
10543 return li0;
10544 }
10545 let Some(model) = self.model.clone() else {
10546 return li0;
10547 };
10548 let (nh, nkv, hd, hs) = (
10549 self.num_heads,
10550 self.num_kv_heads,
10551 self.head_dim,
10552 self.hidden_size,
10553 );
10554 let loop_end = if self.loop_final_norm {
10558 ((li0 / self.physical_layers) + 1) * self.physical_layers
10559 } else {
10560 self.num_layers
10561 };
10562 let run_end = self.num_layers.min(loop_end).min(cap);
10563 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10568 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10569 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10570 let mut stored_at: Vec<usize> = Vec::new();
10571 for li in li0..run_end {
10572 let lw = &self.weights.layers[self.phys_layer(li)];
10573 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10574 && !gemma4
10575 {
10576 break;
10577 }
10578 let AttnKind::Full {
10579 wq,
10580 wk,
10581 wv,
10582 wo,
10583 q_norm,
10584 k_norm,
10585 output_gate: false,
10586 softplus_gate,
10587 bias,
10588 } = &lw.attn
10589 else {
10590 break;
10591 };
10592 let head_gate = match softplus_gate {
10594 None => None,
10595 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10596 Some((d, r, c)) if r == nh && c == hs => Some(d),
10597 _ => break,
10598 },
10599 Some(_) => break,
10600 };
10601 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10605 FfnKind::Dense(d)
10606 if (matches!(d.act, Act::Silu | Act::Gelu)
10607 || (gemma4 && d.act == Act::GeluTanh))
10608 && d.segs.is_empty() =>
10609 {
10610 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10611 }
10612 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10613 Some((router, experts, q8)) => {
10614 let e0 = &m.experts[0];
10615 let route = Box::new(move |lg: &[f32]| {
10616 let (idx, p, wsum) = moe_route(lg, m, None);
10618 let mut st = m.stats.borrow_mut();
10619 if st.len() < m.experts.len() {
10620 st.resize(m.experts.len(), 0);
10621 }
10622 for &e in &idx {
10623 st[e] += 1;
10624 }
10625 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10626 (idx, w)
10627 });
10628 (
10629 &e0.gate_proj,
10630 &e0.up_proj,
10631 &e0.down_proj,
10632 Act::Silu,
10633 Some(crate::gpu_metal::ChunkMoe {
10634 router,
10635 experts,
10636 q8,
10637 route,
10638 }),
10639 )
10640 }
10641 None => break,
10642 },
10643 _ => break,
10644 };
10645 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10651 t.q8_row_parts()
10652 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10653 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10654 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10655 }
10656 fn cf(t: &QTensor) -> &[f32] {
10657 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10658 }
10659 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10660 if moe.is_none()
10669 && !gemma4
10670 && (col.iter().any(|c| !c.is_empty())
10671 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10672 {
10673 break;
10674 }
10675 let parts = (
10676 cw(wq),
10677 cw(wk),
10678 cw(wv),
10679 cw(wo),
10680 cw(gate_t),
10681 cw(up_t),
10682 cw(down_t),
10683 );
10684 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10685 else {
10686 break;
10687 };
10688 let layer = &self.kv_cache.layers[li];
10689 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10690 break;
10691 }
10692 debug_assert!(
10694 layer.base() == 0
10695 || self
10696 .layer_window(li)
10697 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10698 );
10699 stored_at.push(layer.head_len(0));
10700 let (nkv, hd) = if gemma4 {
10703 let (a, bb, _) = self.layer_geom(li);
10704 (a, bb)
10705 } else {
10706 (nkv, hd)
10707 };
10708 layers.push(crate::gpu_metal::ChunkLayer {
10709 model: &model,
10710 kv_id: self.graph_kv_id,
10711 layer: li,
10712 wq: pq,
10713 wk: pk,
10714 wv: pv,
10715 wo: po,
10716 gate: pg,
10717 up: pu,
10718 down: pd,
10719 input_norm: &lw.input_norm,
10720 post_norm: &lw.post_norm,
10721 bias: bias
10722 .as_ref()
10723 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10724 q_norm: q_norm.as_deref(),
10725 k_norm: k_norm.as_deref(),
10726 inv_freq: &tables[li - li0],
10727 rope_scale: self.layer_rope_scale(li),
10728 rd: self.layer_geom(li).2,
10729 nh,
10730 nkv,
10731 hd,
10732 hs,
10733 inter: gate_t.rows(),
10734 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10735 late_qk_norm: self.qk_norm_after_rope,
10736 eps: self.rms_eps as f32,
10737 window: self.layer_window(li),
10738 head_gate,
10739 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10740 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10741 v_norm: self.attn_v_norm,
10742 scale: self.attn_scale,
10743 attn_out_norm: lw.attn_out_norm.as_deref(),
10744 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10745 layer_scale: lw.layer_scale,
10746 }),
10747 col,
10748 moe,
10749 });
10750 }
10751 if layers.is_empty() {
10752 return li0;
10753 }
10754 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10756 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10757 .iter()
10758 .zip(&rows_kv)
10759 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10760 .collect();
10761 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10762 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10763 let li = layers[i].layer;
10764 let layer = &self.kv_cache.layers[li];
10765 let nkv = rows_kv[i].0;
10766 io.push(crate::gpu_metal::ChunkIo {
10767 cpu_stored: stored_at[i],
10768 cpu_gen: layer.generation(),
10769 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10770 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10771 out_k: ok,
10772 out_v: ov,
10773 imp: oi,
10774 });
10775 }
10776 let n_run = layers.len();
10777 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10778 let ep = embed_ids.and_then(|ids| {
10781 self.weights
10782 .embed_tokens
10783 .q8_row_parts()
10784 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10785 idx,
10786 rows,
10787 row_scale: rs,
10788 ids,
10789 mult: self.embed_multiplier,
10790 })
10791 });
10792 if embed_ids.is_some() && ep.is_none() {
10793 return li0;
10794 }
10795 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10796 return li0;
10797 }
10798 drop(io);
10799 drop(layers);
10800 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10803 let li = li0 + i;
10804 let row = rows_kv[i].1;
10805 let layer = &mut self.kv_cache.layers[li];
10806 for bi in 0..b {
10807 layer.append(
10808 &ok[bi * row..(bi + 1) * row],
10809 &ov[bi * row..(bi + 1) * row],
10810 &[],
10811 );
10812 }
10813 layer.accumulate_imp(oi);
10814 }
10815 last
10816 }
10817
10818 fn layer_is_local(&self, li: usize) -> bool {
10821 if let Some(layers) = &self.sliding_layers {
10822 return layers.get(li).copied().unwrap_or(false);
10823 }
10824 match self.swa {
10825 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10826 None => false,
10827 }
10828 }
10829
10830 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10833 if self.layer_is_local(li) {
10834 if let Some(f) = &self.inv_freq_local {
10835 return f.clone();
10836 }
10837 } else if let Some(f) = &self.inv_freq_global {
10838 return f.clone();
10839 }
10840 self.inv_freq.clone()
10841 }
10842
10843 fn layer_window(&self, li: usize) -> Option<usize> {
10845 self.swa
10846 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10847 }
10848
10849 fn swa_trim_tails(&mut self) {
10858 let Some((slack, align)) = self.swa_trim else {
10859 return;
10860 };
10861 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10862 return;
10863 }
10864 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10865 if let Some(w) = self.layer_window(li) {
10866 self.kv_cache.layers[li].trim_window(w, slack, align);
10867 }
10868 }
10869 }
10870
10871 fn layer_num_heads(&self, li: usize) -> usize {
10872 self.attention_heads_per_layer
10873 .as_ref()
10874 .and_then(|v| v.get(li).copied())
10875 .unwrap_or(self.num_heads)
10876 }
10877
10878 fn layer_rope_scale(&self, li: usize) -> f32 {
10879 if self.layer_is_local(li) {
10880 self.rope_scale_local
10881 } else {
10882 self.rope_scale
10883 }
10884 }
10885
10886 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10889 if !self.layer_is_local(li) {
10890 if let Some((ghd, gkv)) = self.global_attn {
10891 return (gkv, ghd, ghd);
10892 }
10893 }
10894 let rd = if self.layer_is_local(li) {
10895 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10896 } else {
10897 self.rotary_dim
10898 };
10899 let rd = if self
10901 .no_rope_layers
10902 .as_ref()
10903 .and_then(|v| v.get(li))
10904 .copied()
10905 == Some(true)
10906 {
10907 0
10908 } else {
10909 rd
10910 };
10911 (self.layer_num_kv_heads(li), self.head_dim, rd)
10912 }
10913
10914 fn layer_num_kv_heads(&self, li: usize) -> usize {
10917 self.kv_heads_per_layer
10918 .as_ref()
10919 .and_then(|v| v.get(self.phys_layer(li)).copied())
10920 .unwrap_or(self.num_kv_heads)
10921 }
10922
10923 fn layer_v_dim(&self, li: usize) -> usize {
10925 let (_, hd, _) = self.layer_geom(li);
10926 self.v_head_dim.unwrap_or(hd).min(hd)
10927 }
10928
10929 pub fn set_attn_geometry(
10938 &mut self,
10939 kv_heads_per_layer: Option<Vec<usize>>,
10940 v_head_dim: Option<usize>,
10941 ) -> Result<(), String> {
10942 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10943 if self.global_attn.is_some() {
10944 return Err(
10945 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10946 attention geometry"
10947 .into(),
10948 );
10949 }
10950 if self
10951 .weights
10952 .layers
10953 .iter()
10954 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10955 {
10956 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10957 }
10958 }
10959 if let Some(vd) = v_head_dim {
10960 if vd == 0 || vd > self.head_dim {
10961 return Err(format!(
10962 "v_head_dim {vd} must be in 1..={} (head_dim)",
10963 self.head_dim
10964 ));
10965 }
10966 }
10967 if let Some(v) = &kv_heads_per_layer {
10968 if v.len() != self.physical_layers {
10969 return Err(format!(
10970 "kv_heads_per_layer has {} entries, expected {} layers",
10971 v.len(),
10972 self.physical_layers
10973 ));
10974 }
10975 for (li, &nkv) in v.iter().enumerate() {
10976 let is_attn = matches!(
10977 self.weights.layers.get(li).map(|lw| &lw.attn),
10978 Some(AttnKind::Full { .. }) | None
10979 );
10980 if !is_attn {
10981 continue;
10982 }
10983 let nh = self
10984 .attention_heads_per_layer
10985 .as_ref()
10986 .and_then(|h| h.get(li).copied())
10987 .unwrap_or(self.num_heads);
10988 if nkv == 0 || nh % nkv != 0 {
10989 return Err(format!(
10990 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10991 ));
10992 }
10993 }
10994 }
10995 self.kv_heads_per_layer = kv_heads_per_layer;
10996 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10997 if self.kv_heads_per_layer.is_some() {
10998 for li in 0..self.kv_cache.layers.len() {
10999 let full = matches!(
11000 self.weights
11001 .layers
11002 .get(self.phys_layer(li))
11003 .map(|lw| &lw.attn),
11004 Some(AttnKind::Full { .. })
11005 );
11006 let nkv = self.layer_num_kv_heads(li);
11007 let cache = &self.kv_cache.layers[li];
11008 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11009 let sinks = cache.sinks.clone();
11010 self.kv_cache.layers[li] =
11011 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11012 self.kv_cache.layers[li].sinks = sinks;
11013 }
11014 }
11015 }
11016 Ok(())
11017 }
11018
11019 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11023 let Some(lw) = self.weights.layers.get(phys) else {
11024 return Err(format!("sinks for layer {phys}: no such layer"));
11025 };
11026 if !matches!(lw.attn, AttnKind::Full { .. }) {
11027 return Err(format!(
11028 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11029 ));
11030 }
11031 let nh = self
11032 .attention_heads_per_layer
11033 .as_ref()
11034 .and_then(|h| h.get(phys).copied())
11035 .unwrap_or(self.num_heads);
11036 if sinks.len() != nh {
11037 return Err(format!(
11038 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11039 sinks.len()
11040 ));
11041 }
11042 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11043 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11044 }
11045 for li in 0..self.kv_cache.layers.len() {
11046 if self.phys_layer(li) == phys {
11047 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11048 }
11049 }
11050 Ok(())
11051 }
11052
11053 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11063 if self.kv_heads_per_layer.is_some() {
11064 return Some("per-layer KV head counts");
11065 }
11066 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11067 return Some("V heads narrower than Q/K heads");
11068 }
11069 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11070 return Some("learned attention sinks");
11071 }
11072 if self.swa.is_some() || self.sliding_layers.is_some() {
11073 return Some("sliding-window layers");
11074 }
11075 None
11076 }
11077
11078 #[cfg(target_os = "macos")]
11086 fn metal_rope_scale_ok(&self) -> bool {
11087 !(self.qk_norm_after_rope
11088 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11089 }
11090
11091 #[cfg(target_os = "macos")]
11108 fn metal_graph_swa(&self) -> bool {
11109 (self.swa.is_some() || self.sliding_layers.is_some())
11110 && self.attention_heads_per_layer.is_none()
11111 && !self.attn_v_norm
11112 && self.attn_softcap == 0.0
11113 && self.kv_heads_per_layer.is_none()
11114 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11115 && self.global_attn.is_none()
11116 && self.inv_freq_global.is_none()
11117 && self.metal_rope_scale_ok()
11118 && !(0..self.num_layers).any(|li| {
11119 self.layer_is_local(li)
11120 && self.inv_freq_local.is_none()
11121 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11122 })
11123 }
11124
11125 #[cfg(target_os = "macos")]
11133 fn metal_gemma4(&self) -> bool {
11134 crate::gpu_metal::gemma4::enabled()
11135 && (self.global_attn.is_some() || self.attn_v_norm)
11136 && self.attention_heads_per_layer.is_none()
11137 && self.kv_heads_per_layer.is_none()
11138 && self.v_head_dim.is_none()
11139 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11140 && self.attn_softcap == 0.0
11141 && !self.qk_norm_after_rope
11142 && self.metal_rope_scale_ok()
11143 && self
11144 .weights
11145 .layers
11146 .iter()
11147 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11148 && !(0..self.num_layers).any(|li| {
11149 self.layer_is_local(li)
11150 && self.inv_freq_local.is_none()
11151 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11152 })
11153 }
11154
11155 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11162 let geometry_reason = self.graph_attn_decline_reason();
11166 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11167 if has_scaled_rope && geometry_reason.is_none() {
11168 return Some("scaled RoPE without per-layer geometry");
11169 }
11170 if has_scaled_rope && self.qk_norm_after_rope {
11175 return Some("scaled RoPE with post-RoPE Q/K norm");
11176 }
11177 geometry_reason?;
11178 if self.attention_heads_per_layer.is_some() {
11184 return Some("per-layer Q head counts with per-layer geometry");
11185 }
11186 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11190 if !gemma4
11191 && self.weights.layers.iter().any(|lw| {
11192 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11193 })
11194 {
11195 return Some("sandwich norms / layer scale with per-layer geometry");
11196 }
11197 if self.weights.layers.iter().any(|lw| {
11201 matches!(
11202 &lw.attn,
11203 AttnKind::Full {
11204 output_gate: true,
11205 ..
11206 }
11207 )
11208 }) && self.v_head_dim.is_some()
11209 {
11210 return Some("gated attention with V narrower than K");
11211 }
11212 if (0..self.num_layers).any(|li| {
11213 self.layer_is_local(li)
11214 && self.inv_freq_local.is_none()
11215 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11216 }) {
11217 return Some("local rotary width without a local RoPE table");
11218 }
11219 None
11220 }
11221
11222 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11226 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11229 return None;
11230 }
11231 if self.global_attn.is_some() {
11232 return Some("per-layer head width (Gemma-4 global layers)");
11233 }
11234 if self.attn_v_norm {
11235 return Some("V norm");
11236 }
11237 if self.weights.layers.iter().any(|lw| {
11238 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11239 }) {
11240 return Some("sandwich norms / layer scale");
11241 }
11242 None
11243 }
11244
11245 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11250 self.graph_attn_decline_reason()?;
11251 let (nkv, hd, rd) = self.layer_geom(li);
11252 let invf: &[f32] = if self.layer_is_local(li) {
11253 match &self.inv_freq_local {
11254 Some(f) => f.as_slice(),
11255 None => self.inv_freq.as_slice(),
11256 }
11257 } else {
11258 match &self.inv_freq_global {
11259 Some(f) => f.as_slice(),
11260 None => self.inv_freq.as_slice(),
11261 }
11262 };
11263 Some(crate::gpu::GraphAttnGeom {
11264 nkv,
11265 hd,
11266 dv: self.layer_v_dim(li),
11267 rd,
11268 invf,
11269 rope_scale: self.layer_rope_scale(li),
11270 window: self.layer_window(li),
11271 sink: self.kv_cache.layers[li].sinks.as_deref(),
11272 v_norm: self.attn_v_norm,
11273 })
11274 }
11275
11276 #[cfg(feature = "gpu")]
11288 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11289 if !self.proj_gate_sigmoid
11290 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11291 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11292 || self.graph_refused()
11293 || self.o1_active()
11294 || self.attn_softcap > 0.0
11295 || self.wgpu_graph_attn_decline().is_some()
11296 {
11297 return None;
11298 }
11299 let g = self.graph_attn_geom(li)?;
11300 let (nkv, hd, _) = self.layer_geom(li);
11301 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11303 return None;
11304 }
11305 Some(crate::gpu::PrefillMirror {
11306 kv_id: self.graph_kv_id,
11307 layer: li,
11308 limit: self.kv_cache.max_seq_len,
11309 })
11310 }
11311
11312 #[cfg(feature = "gpu")]
11321 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11322 let kv_id = self.graph_kv_id;
11323 for li in from..upto.min(self.num_layers) {
11324 if !matches!(
11325 self.weights.layers[self.phys_layer(li)].attn,
11326 AttnKind::Full { .. }
11327 ) {
11328 continue;
11329 }
11330 let host = self.kv_cache.layers[li].pos_len();
11332 if host >= position {
11333 continue;
11334 }
11335 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11336 continue;
11337 };
11338 let to = dev.min(position);
11339 if to <= host {
11340 continue;
11341 }
11342 let (nkv, hd) = {
11343 let c = &self.kv_cache.layers[li];
11344 (c.num_kv_heads, c.head_dim)
11345 };
11346 let Some((k, v, first_valid)) =
11347 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11348 else {
11349 continue;
11350 };
11351 let need_from = match self.layer_window(li) {
11354 Some(w) => host.max((position + 1).saturating_sub(w)),
11355 None => host,
11356 };
11357 if first_valid > need_from {
11358 tracing::warn!(
11359 "layer {li}: device KV rows {host}..{to} no longer resident \
11360 (from {first_valid}); host attention will miss them"
11361 );
11362 }
11363 let row = nkv * hd;
11364 let cache = &mut self.kv_cache.layers[li];
11365 for p in 0..to - host {
11366 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11367 }
11368 }
11369 }
11370
11371 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11374 let mut seen = self.graph_declines.borrow_mut();
11375 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11376 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11377 seen.push((site, reason));
11378 }
11379 }
11380
11381 pub fn graph_declines(&self) -> Vec<String> {
11384 self.graph_declines
11385 .borrow()
11386 .iter()
11387 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11388 .collect()
11389 }
11390
11391 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11400 let Some(dir) = &self.layer_dump else {
11401 return;
11402 };
11403 let mut bytes = Vec::with_capacity(row.len() * 4);
11404 for v in row {
11405 bytes.extend_from_slice(&v.to_le_bytes());
11406 }
11407 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11408 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11409 use std::sync::atomic::{AtomicBool, Ordering};
11410 static SAID: AtomicBool = AtomicBool::new(false);
11411 if !SAID.swap(true, Ordering::Relaxed) {
11412 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11413 }
11414 }
11415 }
11416
11417 fn mimo_moe_prepare(&mut self) {
11420 if !self.mimo_moe.is_undecided() {
11421 return;
11422 }
11423 let slot = {
11424 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11425 .filter_map(
11426 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11427 FfnKind::Moe(m) => Some((li, m)),
11428 _ => None,
11429 },
11430 )
11431 .collect();
11432 if layers.is_empty()
11435 || self.physical_layers != self.num_layers
11436 || self.gpu_plan.is_some()
11437 {
11438 crate::mimo_moe::Slot::Off
11439 } else {
11440 let graph_prefix =
11444 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11445 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11446 }
11447 };
11448 self.mimo_moe = slot;
11449 }
11450
11451 #[cfg(test)]
11452 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11453 self.graph_kv_id
11454 }
11455
11456 pub(crate) fn mimo_graph_layer_rows(
11460 &mut self,
11461 li: usize,
11462 h: &mut [f32],
11463 positions: &[usize],
11464 ) -> crate::gpu::BatchGraphOutcome {
11465 use crate::gpu::BatchGraphOutcome as Out;
11466 let b = positions.len();
11467 if !(1..=4).contains(&b)
11468 || h.len() != b * self.hidden_size
11469 || !self.mimo_moe.is_dynamic(li, true)
11470 || !crate::gpu::enabled_here()
11471 || !crate::gpu::wgpu_active()
11472 || self.o1_active()
11473 || self.physical_layers != self.num_layers
11474 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11478 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11479 || self.wgpu_graph_attn_decline().is_some()
11480 {
11481 return Out::Declined;
11482 }
11483 let attn_started = std::time::Instant::now();
11484 let outcome = {
11485 let lw = &self.weights.layers[li];
11486 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11487 return Out::Declined;
11488 }
11489 let FfnKind::Moe(m) = &lw.ffn else {
11490 return Out::Declined;
11491 };
11492 let AttnKind::Full {
11493 wq,
11494 wk,
11495 wv,
11496 wo,
11497 q_norm,
11498 k_norm,
11499 output_gate,
11500 softplus_gate,
11501 bias,
11502 } = &lw.attn
11503 else {
11504 return Out::Declined;
11505 };
11506 if *output_gate || softplus_gate.is_some() {
11507 return Out::Declined;
11508 }
11509 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11510 m.experts
11511 .first()?
11512 .gate_proj
11513 .mapped_q4tp()
11514 .map(|(m, _)| m.clone())
11515 }) else {
11516 return Out::Declined;
11517 };
11518 fn gw<'a>(
11519 t: &'a QTensor,
11520 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11521 ) -> Option<crate::gpu::GraphW<'a>> {
11522 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11523 if m.uid() != owner.uid() || t.has_prism_contract() {
11524 return None;
11525 }
11526 return Some(crate::gpu::GraphW {
11527 idx,
11528 kind,
11529 row_scale: rs,
11530 data: &[],
11531 prism: crate::gpu::GraphPrismOp::None,
11532 affine: false,
11533 });
11534 }
11535 t.as_f32().map(|data| crate::gpu::GraphW {
11536 idx: 0,
11537 kind: 4,
11538 row_scale: &[],
11539 data,
11540 prism: crate::gpu::GraphPrismOp::None,
11541 affine: false,
11542 })
11543 }
11544 let (Some(q), Some(k), Some(v), Some(o)) = (
11545 gw(wq, &model),
11546 gw(wk, &model),
11547 gw(wv, &model),
11548 gw(wo, &model),
11549 ) else {
11550 return Out::Declined;
11551 };
11552 let layer = crate::gpu::GraphLayer {
11553 input_norm: &lw.input_norm,
11554 post_norm: &lw.post_norm,
11555 attn_out_norm: None,
11556 ffn_out_norm: None,
11557 layer_scale: None,
11558 ffn: crate::gpu::GraphFfn::AttentionOnly,
11559 attn: crate::gpu::GraphAttn::Full {
11560 wq: q,
11561 wk: k,
11562 wv: v,
11563 wo: o,
11564 q_norm: q_norm.as_deref(),
11565 k_norm: k_norm.as_deref(),
11566 late_qk_norm: self.qk_norm_after_rope,
11567 bias: bias
11568 .as_ref()
11569 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11570 output_gate: false,
11571 cpu_k: self.kv_cache.layers[li].k_heads(),
11572 cpu_v: self.kv_cache.layers[li].v_heads(),
11573 cpu_base: self.kv_cache.layers[li].base(),
11574 geom: self.graph_attn_geom(li),
11575 head_gate: None,
11576 },
11577 };
11578 let (nkv, hd, rd) = self.layer_geom(li);
11579 crate::gpu::forward_batch_graph_at(
11580 &model,
11581 self.graph_kv_id,
11582 li,
11583 &[layer],
11584 &self.inv_freq,
11585 h,
11586 self.layer_num_heads(li),
11587 nkv,
11588 hd,
11589 rd,
11590 self.hidden_size,
11591 1,
11592 positions,
11593 self.kv_cache.max_seq_len,
11594 self.norm_style == cortiq_core::NormStyle::Gemma,
11595 self.rms_eps as f32,
11596 self.attn_scale,
11597 b,
11598 &[],
11599 self.o1_epoch,
11600 None,
11601 None,
11602 )
11603 };
11604 match outcome {
11605 Out::Completed => {}
11606 Out::Declined => return Out::Declined,
11607 Out::Failed => {
11608 self.graph_failed
11609 .store(true, std::sync::atomic::Ordering::Relaxed);
11610 return Out::Failed;
11611 }
11612 }
11613 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11614 let hs = self.hidden_size;
11615 let lw = &self.weights.layers[li];
11616 let FfnKind::Moe(m) = &lw.ffn else {
11617 unreachable!()
11618 };
11619 let mut post = vec![0.0; h.len()];
11620 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11621 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11622 }
11623 let mut ffn = if b == 1 {
11624 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11625 } else {
11626 moe_ffn_banked_rows(
11627 &mut self.mimo_moe,
11628 li,
11629 m,
11630 &post,
11631 b,
11632 hs,
11633 self.pool.as_deref(),
11634 )
11635 };
11636 for (x, &f) in h.iter_mut().zip(&ffn) {
11637 *x += f;
11638 }
11639 attention::recycle_buf(&mut ffn);
11640 if self.layer_dump.is_some() {
11641 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11642 self.dump_layer_row(pos, li, row);
11643 }
11644 }
11645 crate::mimo_moe::note_attention_graph(b, attn_ns);
11646 Out::Completed
11647 }
11648
11649 fn layer_attn_plain(&self, li: usize) -> bool {
11650 self.kv_heads_per_layer.is_none()
11651 && self.v_head_dim.is_none()
11652 && self.global_attn.is_none()
11653 && self.layer_window(li).is_none()
11654 && self.kv_cache.layers[li].sinks.is_none()
11655 }
11656
11657 fn forward_layers(
11659 &mut self,
11660 hidden: &[f32],
11661 position: usize,
11662 task_mask: Option<&TaskMask>,
11663 ) -> Vec<f32> {
11664 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11665 self.o1_progress();
11666 self.swa_trim_tails();
11667 out
11668 }
11669
11670 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11678 self.embed_single(id)
11679 }
11680
11681 pub fn split_supported(&self) -> Result<(), String> {
11685 if self.dsv4.is_some() {
11686 return Err(
11687 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11688 );
11689 }
11690 if self.dsv41.is_some() {
11691 return Err(
11692 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11693 .into(),
11694 );
11695 }
11696 if self.qwen4_exp.is_some() {
11697 return Err(
11698 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11699 );
11700 }
11701 if self.g3n.is_some() {
11702 return Err(
11703 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11704 );
11705 }
11706 Ok(())
11707 }
11708
11709 pub fn forward_span(
11714 &mut self,
11715 hidden: &[f32],
11716 position: usize,
11717 from: usize,
11718 upto: usize,
11719 task_mask: Option<&TaskMask>,
11720 ) -> Result<Vec<f32>, String> {
11721 self.split_supported()?;
11722 if from > upto || upto >= self.num_layers {
11723 return Err(format!(
11724 "forward_span: layer range {from}..={upto} outside 0..{}",
11725 self.num_layers
11726 ));
11727 }
11728 if hidden.len() != self.hidden_size {
11729 return Err(format!(
11730 "forward_span: hidden len {} ≠ hidden_size {}",
11731 hidden.len(),
11732 self.hidden_size
11733 ));
11734 }
11735 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11736 self.o1_progress();
11737 self.swa_trim_tails();
11738 if self
11739 .graph_failed
11740 .swap(false, std::sync::atomic::Ordering::Relaxed)
11741 {
11742 self.cancel
11743 .store(false, std::sync::atomic::Ordering::Relaxed);
11744 self.clear_sequence_state();
11745 return Err("forward_span: deferred O(1) transition failed".into());
11746 }
11747 Ok(out)
11748 }
11749
11750 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11753 let normed = inference::rms_norm(
11754 hidden,
11755 &self.weights.final_norm,
11756 self.rms_eps,
11757 self.norm_style,
11758 );
11759 self.lm_head_forward(&normed)
11760 }
11761
11762 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11764 sampler::sample_with_scratch(
11765 logits,
11766 &self.sampler_config,
11767 past_tokens,
11768 &mut self.rng,
11769 &mut self.sampler_scratch,
11770 )
11771 }
11772
11773 pub fn reset_session(&mut self) {
11775 self.clear_sequence_state();
11776 }
11777
11778 pub fn prefill_span_ids(
11784 &mut self,
11785 ids: &[u32],
11786 start_pos: usize,
11787 upto: usize,
11788 task_mask: Option<&TaskMask>,
11789 ) -> Result<Vec<f32>, String> {
11790 self.split_supported()?;
11791 if upto >= self.num_layers {
11792 return Err(format!(
11793 "prefill_span_ids: upto {upto} outside 0..{}",
11794 self.num_layers
11795 ));
11796 }
11797 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11801 let out =
11802 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11803 self.check_o1_progress_failure("prefill_span_ids")?;
11804 Ok(out)
11805 } else {
11806 let hs = self.hidden_size;
11807 let mut out = Vec::with_capacity(ids.len() * hs);
11808 for (i, &id) in ids.iter().enumerate() {
11809 let emb = self.embed_id(id);
11810 out.extend_from_slice(&self.forward_span(
11811 &emb,
11812 start_pos + i,
11813 0,
11814 upto,
11815 task_mask,
11816 )?);
11817 }
11818 Ok(out)
11819 }
11820 }
11821
11822 pub fn prefill_span_hidden(
11825 &mut self,
11826 hidden: &[f32],
11827 start_pos: usize,
11828 from: usize,
11829 upto: usize,
11830 task_mask: Option<&TaskMask>,
11831 ) -> Result<Vec<f32>, String> {
11832 self.split_supported()?;
11833 let hs = self.hidden_size;
11834 if hidden.is_empty() || hidden.len() % hs != 0 {
11835 return Err(format!(
11836 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11837 hidden.len()
11838 ));
11839 }
11840 if from > upto || upto >= self.num_layers {
11841 return Err(format!(
11842 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11843 self.num_layers
11844 ));
11845 }
11846 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11847 let out = self.prefill_batch_span(
11848 PrefillIn::Hidden(hidden),
11849 start_pos,
11850 task_mask,
11851 from,
11852 upto + 1,
11853 );
11854 self.check_o1_progress_failure("prefill_span_hidden")?;
11855 Ok(out)
11856 } else {
11857 let b = hidden.len() / hs;
11858 let mut out = Vec::with_capacity(hidden.len());
11859 for i in 0..b {
11860 let h = self.forward_span(
11861 &hidden[i * hs..(i + 1) * hs],
11862 start_pos + i,
11863 from,
11864 upto,
11865 task_mask,
11866 )?;
11867 out.extend_from_slice(&h);
11868 }
11869 Ok(out)
11870 }
11871 }
11872
11873 fn try_token_graph_wgpu(
11877 &self,
11878 hidden: &[f32],
11879 position: usize,
11880 logits_out: &mut Vec<f32>,
11881 layers_run: &mut usize,
11882 ) -> Option<Result<Vec<f32>, ()>> {
11883 self.try_token_graph_wgpu_steps(
11884 hidden,
11885 position,
11886 logits_out,
11887 1,
11888 None,
11889 Some(layers_run),
11890 0,
11891 self.num_layers,
11892 )
11893 }
11894
11895 fn try_token_graph_wgpu_span(
11899 &self,
11900 hidden: &[f32],
11901 position: usize,
11902 logits_out: &mut Vec<f32>,
11903 from: usize,
11904 upto_excl: usize,
11905 layers_run: &mut usize,
11906 ) -> Option<Result<Vec<f32>, ()>> {
11907 self.try_token_graph_wgpu_steps(
11908 hidden,
11909 position,
11910 logits_out,
11911 1,
11912 None,
11913 Some(layers_run),
11914 from,
11915 upto_excl,
11916 )
11917 }
11918
11919 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11923 if self.o1_active() || self.attn_softcap > 0.0 {
11924 return None;
11925 }
11926 if let Some(reason) = self.wgpu_graph_attn_decline() {
11929 self.note_graph_decline("wgpu multi-burst", reason);
11930 return None;
11931 }
11932 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11933 if !graph_on || self.graph_refused() {
11934 return None;
11941 }
11942 let emb = self.embed_single(t_next);
11943 let mut lg = Vec::new();
11944 let mut ids = Vec::new();
11945 match self.try_token_graph_wgpu_steps(
11946 &emb,
11947 position,
11948 &mut lg,
11949 k,
11950 Some(&mut ids),
11951 None,
11952 0,
11953 self.num_layers,
11954 ) {
11955 Some(Ok(_)) => {}
11956 Some(Err(())) => {
11957 self.graph_failed
11962 .store(true, std::sync::atomic::Ordering::Relaxed);
11963 return None;
11964 }
11965 None => return None,
11966 }
11967 (ids.len() == k).then_some(ids)
11968 }
11969
11970 fn try_token_graph_wgpu_steps(
11974 &self,
11975 hidden: &[f32],
11976 position: usize,
11977 logits_out: &mut Vec<f32>,
11978 steps: usize,
11979 ids_out: Option<&mut Vec<u32>>,
11980 layers_run: Option<&mut usize>,
11981 from: usize,
11982 upto_excl: usize,
11983 ) -> Option<Result<Vec<f32>, ()>> {
11984 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11987 Some(end) if end < upto_excl => {
11988 if steps != 1 || layers_run.is_none() || from >= end {
11989 return None;
11990 }
11991 end
11992 }
11993 _ => upto_excl,
11994 };
11995 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11998 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11999 return None;
12003 }
12004 if let Some(reason) = self.wgpu_graph_attn_decline() {
12011 self.note_graph_decline("wgpu token graph", reason);
12012 return None;
12013 }
12014 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12019 .map(|li| {
12020 if !o1_gpu {
12021 return None;
12022 }
12023 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12024 })
12025 .collect();
12026 if self.o1_active() && o1_gpu {
12027 let want: usize = (from..upto_excl)
12030 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12031 .count();
12032 let have = o1_views.iter().filter(|v| v.is_some()).count();
12033 if want == 0 || have != want {
12034 use std::sync::atomic::{AtomicUsize, Ordering};
12044 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
12045 let code = have * 1000 + want;
12046 if LAST.swap(code, Ordering::Relaxed) != code {
12047 tracing::warn!(
12048 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
12049 );
12050 }
12051 return None;
12052 }
12053 }
12054 let nh = self.num_heads;
12055 let (nkv, hd, rd) = self.layer_geom(0);
12056 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12057 let mut layers = Vec::with_capacity(upto_excl - from);
12058 let mut model = None;
12059 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
12060 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
12061 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
12063 let name = &m.tensors[i].name;
12064 let prism = if crate::prism::is_inverse_embedding(m, name) {
12065 crate::gpu::GraphPrismOp::InverseEmbedding
12066 } else if crate::prism::is_forward_weight(m, name) {
12067 crate::gpu::GraphPrismOp::Forward
12068 } else {
12069 crate::gpu::GraphPrismOp::None
12070 };
12071 return Some(crate::gpu::GraphW {
12072 idx: i,
12073 kind,
12074 row_scale: rs,
12075 data: &[],
12076 prism,
12077 affine: crate::prism::is_affine_target(m, name),
12078 });
12079 }
12080 match t.as_f32() {
12082 Some(d) => Some(crate::gpu::GraphW {
12083 idx: 0,
12084 kind: 4,
12085 row_scale: &[],
12086 data: d,
12087 prism: crate::gpu::GraphPrismOp::None,
12088 affine: false,
12089 }),
12090 None => {
12091 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
12092 eprintln!("batch graph: weight has no graph/f32 representation");
12093 }
12094 None
12095 }
12096 }
12097 }
12098 for li in from..upto_excl {
12099 let lw = &self.weights.layers[self.phys_layer(li)];
12100 if dbg {
12101 let ak = match &lw.attn {
12102 AttnKind::Mla(_) => "Mla".into(),
12103 AttnKind::Full {
12104 output_gate, bias, ..
12105 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
12106 AttnKind::LinearGdn(_) => "LinearGdn".into(),
12107 AttnKind::Kda(_) => "Kda".into(),
12108 AttnKind::Linear(_) => "Linear".into(),
12109 AttnKind::ShortConv(_) => "ShortConv".into(),
12110 AttnKind::Bounded(_) => "Bounded".into(),
12111 };
12112 let fk = match &lw.ffn {
12113 FfnKind::Dense(_) => "Dense",
12114 FfnKind::Moe(_) => "Moe",
12115 FfnKind::DenseMoe(_) => "DenseMoe",
12116 };
12117 eprintln!("graph L{li}: attn={ak} ffn={fk}");
12118 }
12119 let gffn = match &lw.ffn {
12120 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
12124 FfnKind::Dense(d) => {
12125 let Some(act) = d.act.graph_act() else {
12129 self.note_graph_decline(
12130 "wgpu token graph",
12131 "dense FFN activation without a graph kernel",
12132 );
12133 return None;
12134 };
12135 crate::gpu::GraphFfn::Dense {
12136 gate: gw(&d.gate_proj)?,
12137 up: gw(&d.up_proj)?,
12138 down: gw(&d.down_proj)?,
12139 act,
12140 }
12141 }
12142 FfnKind::Moe(m) => {
12143 if m.route_tau.is_some() || m.mask.is_some() {
12151 return None;
12152 }
12153 let shared = m.shared.as_ref();
12154 let inter = m.experts.first()?.gate_proj.rows();
12160 let detached_shared = match shared {
12168 Some((se, None))
12169 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12170 {
12171 if !matches!(se.act, Act::Silu)
12172 || se.gate_proj.mapped_q4tp().is_none()
12173 || se.up_proj.mapped_q4tp().is_none()
12174 || se.down_proj.mapped_q4tp().is_none()
12175 {
12176 return None;
12177 }
12178 Some(crate::gpu::GraphMoeDetachedShared {
12179 gate: gw(&se.gate_proj)?,
12180 up: gw(&se.up_proj)?,
12181 down: gw(&se.down_proj)?,
12182 width: se.gate_proj.rows(),
12183 })
12184 }
12185 Some((se, Some(_)))
12189 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12190 {
12191 return None;
12192 }
12193 _ => None,
12194 };
12195 let has_shared = shared.is_some() && detached_shared.is_none();
12196 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
12197 let sgate = match shared {
12198 Some((_, Some(sg))) if has_shared => gw(sg)?,
12199 _ => gw(&m.router)?,
12203 };
12204 let router = gw(&m.router)?;
12205 if router.prism != crate::gpu::GraphPrismOp::None
12211 || sgate.prism != crate::gpu::GraphPrismOp::None
12212 || router.affine
12213 || sgate.affine
12214 {
12215 tracing::warn!(
12216 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
12217 );
12218 return None;
12219 }
12220 let mut experts = Vec::with_capacity(m.experts.len() + 1);
12221 let mut q4tp: Option<bool> = None;
12224 let mut gu_q2: Option<bool> = None;
12227 let mut q82: Option<bool> = None;
12229 for e in m
12230 .experts
12231 .iter()
12232 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
12233 {
12234 if !matches!(e.act, Act::Silu)
12235 || e.gate_proj.rows() != inter
12236 || e.up_proj.rows() != inter
12237 {
12238 return None;
12239 }
12240 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12245 let Some((em, ei, _, _)) = expert_weight
12246 .graph_weight()
12247 .or_else(|| expert_weight.graph_weight_descriptor())
12248 else {
12249 return None;
12250 };
12251 let name = &em.tensors[ei].name;
12252 if crate::prism::is_forward_weight(em, name)
12253 || crate::prism::is_inverse_embedding(em, name)
12254 || crate::prism::is_affine_target(em, name)
12255 {
12256 tracing::warn!(
12257 "resident MoE declined: expert Prism/affine transform is not implemented"
12258 );
12259 return None;
12260 }
12261 }
12262 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12263 Some((mm, gi)) => (
12264 mm,
12265 gi,
12266 e.up_proj.mapped_q4t()?.1,
12267 e.down_proj.mapped_q4t()?.1,
12268 false,
12269 false,
12270 false,
12271 ),
12272 None => match e.gate_proj.mapped_q2tp() {
12273 Some((mm, gi)) => (
12274 mm,
12275 gi,
12276 e.up_proj.mapped_q2tp()?.1,
12277 e.down_proj.mapped_q4tp()?.1,
12278 true,
12279 true,
12280 false,
12281 ),
12282 None => match e.gate_proj.mapped_q4tp() {
12283 Some((mm, gi)) => (
12284 mm,
12285 gi,
12286 e.up_proj.mapped_q4tp()?.1,
12287 e.down_proj.mapped_q4tp()?.1,
12288 true,
12289 false,
12290 false,
12291 ),
12292 None => {
12295 if !crate::gpu::moe_q82_graph_on() {
12296 return None;
12297 }
12298 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12299 (
12300 mm,
12301 gi,
12302 e.up_proj.mapped_q8_2f()?.1,
12303 e.down_proj.mapped_q8_2f()?.1,
12304 false,
12305 false,
12306 true,
12307 )
12308 }
12309 },
12310 },
12311 };
12312 if *q4tp.get_or_insert(is_p) != is_p
12313 || *gu_q2.get_or_insert(is_q2) != is_q2
12314 || *q82.get_or_insert(is_8) != is_8
12315 {
12316 tracing::warn!(
12322 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}, q8_2f={is_8}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
12323 );
12324 return None;
12325 }
12326 model.get_or_insert_with(|| mm.clone());
12327 experts.push((gi, ui, di));
12328 }
12329 crate::gpu::GraphFfn::Moe {
12330 router,
12331 shared_gate: sgate,
12332 experts,
12333 n_exp: m.experts.len(),
12334 top_k: std::env::var("CMF_TOPK_PROBE")
12340 .ok()
12341 .and_then(|v| v.parse::<usize>().ok())
12342 .filter(|k| *k > 0 && *k <= m.top_k)
12343 .unwrap_or(m.top_k),
12344 inter,
12345 norm_topk: m.norm_topk_prob,
12346 q4tp: q4tp?,
12347 gu_q2: gu_q2.unwrap_or(false),
12348 q82: q82.unwrap_or(false),
12349 sigmoid: m.router_sigmoid,
12350 sigmoid_denom_eps: m.sigmoid_denom_eps,
12351 bias: m.expert_bias.as_deref(),
12352 has_shared,
12353 detached_shared,
12354 shared_gated,
12355 route_scale: m.routed_scaling,
12356 }
12357 }
12358 };
12359 let attn = match &lw.attn {
12360 AttnKind::Full {
12361 wq,
12362 wk,
12363 wv,
12364 wo,
12365 q_norm,
12366 k_norm,
12367 output_gate,
12368 softplus_gate,
12369 bias,
12370 } => {
12371 if self.attention_heads_per_layer.is_some() {
12372 return None;
12373 }
12374 let head_gate = match softplus_gate {
12378 None => None,
12379 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
12380 Some(_) => {
12381 self.note_graph_decline(
12382 "wgpu token graph",
12383 "projected softplus / per-element output gate",
12384 );
12385 return None;
12386 }
12387 };
12388 let (m, _, _, _) = wq.graph_weight_or_blr()?;
12389 model = Some(m.clone());
12390 crate::gpu::GraphAttn::Full {
12391 wq: gw(wq)?,
12392 wk: gw(wk)?,
12393 wv: gw(wv)?,
12394 wo: gw(wo)?,
12395 q_norm: q_norm.as_deref(),
12396 k_norm: k_norm.as_deref(),
12397 late_qk_norm: self.qk_norm_after_rope,
12398 bias: bias
12399 .as_ref()
12400 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12401 output_gate: *output_gate,
12402 cpu_k: self.kv_cache.layers[li].k_heads(),
12403 cpu_v: self.kv_cache.layers[li].v_heads(),
12404 cpu_base: self.kv_cache.layers[li].base(),
12405 geom: self.graph_attn_geom(li),
12406 head_gate,
12407 }
12408 }
12409 AttnKind::LinearGdn(w) => {
12410 let cfg = self.gdn_cfg?;
12411 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
12412 model = Some(m.clone());
12413 crate::gpu::GraphAttn::Gdn {
12414 qkv: gw(&w.in_proj_qkv)?,
12415 z: gw(&w.in_proj_z)?,
12416 a: gw(&w.in_proj_a)?,
12417 b: gw(&w.in_proj_b)?,
12418 out: gw(&w.out_proj)?,
12419 conv1d: &w.conv1d,
12420 a_log: &w.a_log,
12421 dt_bias: &w.dt_bias,
12422 norm: &w.norm,
12423 nv: cfg.num_v_heads,
12424 nk: cfg.num_k_heads,
12425 dk: cfg.key_head_dim,
12426 dv: cfg.value_head_dim,
12427 kk: cfg.conv_kernel,
12428 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12429 }
12430 }
12431 AttnKind::ShortConv(w) => {
12432 let cfg = self.short_conv_cfg?;
12433 let (m, _, _, _) = w
12434 .in_proj
12435 .graph_weight()
12436 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12437 model = Some(m.clone());
12438 crate::gpu::GraphAttn::ShortConv {
12439 inp: gw(&w.in_proj)?,
12440 out: gw(&w.out_proj)?,
12441 taps: &w.conv,
12442 kernel: cfg.kernel,
12443 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12444 }
12445 }
12446 _ => return None,
12447 };
12448 layers.push(crate::gpu::GraphLayer {
12449 input_norm: &lw.input_norm,
12450 attn,
12451 post_norm: &lw.post_norm,
12452 attn_out_norm: lw.attn_out_norm.as_deref(),
12453 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12454 layer_scale: lw.layer_scale,
12455 ffn: gffn,
12456 });
12457 }
12458 let model = model?;
12459 let lm_gw = if upto_excl == self.num_layers
12465 && self.graph_want_logits
12466 && std::env::var("CMF_GPU_LMHEAD")
12467 .map(|v| v != "0")
12468 .unwrap_or(true)
12469 {
12470 self.weights
12471 .lm_head
12472 .graph_weight()
12473 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12474 .map(|(m, i, kind, rs)| {
12475 let name = &m.tensors[i].name;
12476 let prism = if crate::prism::is_inverse_embedding(m, name) {
12477 crate::gpu::GraphPrismOp::InverseEmbedding
12478 } else if crate::prism::is_forward_weight(m, name) {
12479 crate::gpu::GraphPrismOp::Forward
12480 } else {
12481 crate::gpu::GraphPrismOp::None
12482 };
12483 (
12484 crate::gpu::GraphW {
12485 idx: i,
12486 kind,
12487 row_scale: rs,
12488 data: &[],
12489 prism,
12490 affine: crate::prism::is_affine_target(m, name),
12491 },
12492 self.weights.lm_head.rows(),
12493 )
12494 })
12495 } else {
12496 None
12497 };
12498 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12499 let emb_gw = if steps > 1 {
12501 self.weights
12502 .embed_tokens
12503 .graph_weight()
12504 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12505 .map(|(m, i, kind, rs)| {
12506 let name = &m.tensors[i].name;
12507 let prism = if crate::prism::is_inverse_embedding(m, name) {
12508 crate::gpu::GraphPrismOp::InverseEmbedding
12509 } else if crate::prism::is_forward_weight(m, name) {
12510 crate::gpu::GraphPrismOp::Forward
12511 } else {
12512 crate::gpu::GraphPrismOp::None
12513 };
12514 (
12515 crate::gpu::GraphW {
12516 idx: i,
12517 kind,
12518 row_scale: rs,
12519 data: &[],
12520 prism,
12521 affine: crate::prism::is_affine_target(m, name),
12522 },
12523 self.weights.embed_tokens.rows(),
12524 self.embed_multiplier,
12525 )
12526 })
12527 } else {
12528 None
12529 };
12530
12531 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12537 (from..upto_excl.min(self.num_layers - 1))
12538 .filter(|&li| (li + 1) % self.physical_layers == 0)
12539 .map(|li| li - from)
12540 .collect()
12541 } else {
12542 Vec::new()
12543 };
12544 let mut h = hidden.to_vec();
12545 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12551 let outcome = crate::gpu::forward_token_graph(
12552 &model,
12553 self.graph_kv_id,
12554 &layers,
12555 &o1_views,
12556 self.o1_epoch,
12557 &self.inv_freq,
12558 &mut h,
12559 nh,
12560 nkv,
12561 hd,
12562 self.attn_scale,
12563 rd,
12564 self.hidden_size,
12565 self.intermediate_size,
12566 position,
12567 self.kv_cache.max_seq_len,
12568 gemma,
12569 self.rms_eps as f32,
12570 lm,
12571 &self.weights.final_norm,
12572 logits_out,
12573 &loop_norm_at,
12574 steps,
12575 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12576 ids_out,
12577 layers_run,
12578 from,
12579 dump_hidden,
12580 );
12581 match outcome {
12582 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12583 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12584 crate::gpu::TokenGraphOutcome::Declined => None,
12585 }
12586 }
12587
12588 #[cfg(target_os = "macos")]
12597 #[allow(clippy::type_complexity)]
12598 fn metal_rows_plan(
12599 &self,
12600 ) -> Option<(
12601 Vec<MetalRowsItem<'_>>,
12602 std::sync::Arc<cortiq_core::CmfModel>,
12603 Option<crate::gpu_metal::GdnGpuCfg>,
12604 )> {
12605 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12606 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12607 if !graph_force
12608 || !crate::gpu::enabled_here()
12609 || std::env::var("CMF_GPU_BLOCK")
12610 .map(|v| v == "0")
12611 .unwrap_or(false)
12612 || self.attn_softcap > 0.0
12613 || self.o1_active()
12614 || self.swa.is_some()
12615 || self.global_attn.is_some()
12616 || self.attention_heads_per_layer.is_some()
12617 || self.graph_attn_decline_reason().is_some()
12619 || self.attn_v_norm
12620 || self.loop_final_norm
12621 || !self.metal_rope_scale_ok()
12622 {
12623 return None;
12624 }
12625 let attend_contract = self.head_dim % 4 == 0
12626 && self.head_dim <= 256
12627 && self.rotary_dim >= 2
12628 && self.rotary_dim <= self.head_dim
12629 && (self.rotary_dim / 2) % 32 == 0
12630 && self.num_kv_heads > 0
12631 && self.num_heads % self.num_kv_heads == 0;
12632 if !attend_contract {
12633 return None;
12634 }
12635 let mut plan: Vec<MetalRowsItem> = Vec::new();
12636 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12637 for li in 0..self.num_layers {
12638 let lw = &self.weights.layers[self.phys_layer(li)];
12639 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12640 return None;
12641 }
12642 let ffn = match &lw.ffn {
12643 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12644 let (Some(g), Some(u), Some(dn)) = (
12645 d.gate_proj.metal_graph_parts(),
12646 d.up_proj.metal_graph_parts(),
12647 d.down_proj.metal_graph_parts(),
12648 ) else {
12649 return None;
12650 };
12651 MetalFfn::Dense {
12652 gate: g,
12653 up: u,
12654 down: dn,
12655 act: 0, }
12657 }
12658 _ => return None,
12659 };
12660 match &lw.attn {
12661 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12662 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12663 w.in_proj_qkv.metal_graph_parts(),
12664 w.in_proj_z.metal_graph_parts(),
12665 w.in_proj_a.f32_parts(),
12666 w.in_proj_b.f32_parts(),
12667 w.out_proj.metal_graph_parts(),
12668 ) else {
12669 return None;
12670 };
12671 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12672 model_ref.get_or_insert_with(|| model.clone());
12673 }
12674 let gl = GdnGpuLayer {
12675 attn_norm: &lw.input_norm,
12676 post_norm: &lw.post_norm,
12677 qkv,
12678 z,
12679 a,
12680 b: bb,
12681 out,
12682 ffn,
12683 conv1d: &w.conv1d,
12684 a_log: &w.a_log,
12685 dt_bias: &w.dt_bias,
12686 gnorm: &w.norm,
12687 };
12688 match plan.last_mut() {
12689 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12690 _ => plan.push(MetalRowsItem::Gdn {
12691 run: vec![gl],
12692 first: li,
12693 }),
12694 }
12695 }
12696 AttnKind::Full {
12697 wq,
12698 wk,
12699 wv,
12700 wo,
12701 q_norm,
12702 k_norm,
12703 output_gate,
12704 softplus_gate: None,
12705 bias: None,
12706 } => {
12707 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
12708 wq.metal_graph_parts(),
12709 wk.metal_graph_parts(),
12710 wv.metal_graph_parts(),
12711 wo.metal_graph_parts(),
12712 ) else {
12713 return None;
12714 };
12715 if let QTensor::Mapped { model, .. } = wq {
12716 model_ref.get_or_insert_with(|| model.clone());
12717 }
12718 let cache = &self.kv_cache.layers[li];
12719 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12720 return None;
12721 }
12722 plan.push(MetalRowsItem::Attn {
12723 l: AttnGpuLayer {
12724 attn_norm: &lw.input_norm,
12725 post_norm: &lw.post_norm,
12726 wq: pq,
12727 wk: pk,
12728 wv: pv,
12729 wo: po,
12730 ffn,
12731 attn_out_norm: None,
12732 ffn_out_norm: None,
12733 layer_scale: None,
12734 },
12735 li,
12736 q_norm: q_norm.as_deref(),
12737 k_norm: k_norm.as_deref(),
12738 output_gate: *output_gate,
12739 });
12740 }
12741 _ => return None,
12742 }
12743 }
12744 let model = model_ref?;
12745 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12746 nv: cfg.num_v_heads,
12747 nk: cfg.num_k_heads,
12748 dk: cfg.key_head_dim,
12749 dv: cfg.value_head_dim,
12750 kk: cfg.conv_kernel,
12751 hidden: self.hidden_size,
12752 inter: self.intermediate_size,
12753 c_dim: cfg.conv_dim(),
12754 eps: cfg.rms_eps as f32,
12755 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12756 });
12757 Some((plan, model, gcfg))
12758 }
12759
12760 #[cfg(target_os = "macos")]
12762 #[allow(clippy::too_many_arguments)]
12763 fn metal_attn_params<'a>(
12764 li: usize,
12765 cache: &'a crate::kv_cache::LayerKvCache,
12766 q_norm: Option<&'a [f32]>,
12767 k_norm: Option<&'a [f32]>,
12768 output_gate: bool,
12769 inv_freq: &'a [f32],
12770 rope_scale: f32,
12771 geom: (usize, usize, usize, usize),
12772 pos0: usize,
12773 kv_id: u64,
12774 scale: f32,
12775 eps: f32,
12776 gemma: bool,
12777 late_qk_norm: bool,
12778 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12779 let (nh, nkv, hd, rd) = geom;
12780 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12781 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12782 let cpu_stored = cpu_k[0].len() / hd;
12783 (
12784 crate::gpu_metal::AttnDeviceParams {
12785 kv_id,
12786 layer: li,
12787 nh,
12788 nkv,
12789 hd,
12790 rd,
12791 position: pos0,
12792 scale,
12793 eps,
12794 gemma,
12795 late_qk_norm,
12796 output_gate,
12797 q_norm,
12798 k_norm,
12799 inv_freq,
12800 rope_scale,
12801 cpu_k,
12802 cpu_v,
12803 cpu_stored,
12804 cpu_gen: cache.generation(),
12805 o1: None,
12806 window: None,
12807 softmax_sink: cache.sinks.as_deref(),
12808 head_gate: None,
12809 v_norm: false,
12810 },
12811 cpu_stored,
12812 )
12813 }
12814
12815 #[cfg(target_os = "macos")]
12820 #[allow(clippy::type_complexity)]
12821 fn metal_rows_run(
12822 &mut self,
12823 hiddens: &mut [f32],
12824 pos0: usize,
12825 b: usize,
12826 prefill: bool,
12827 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12828 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12832 ) -> MetalRowsRun {
12833 use crate::gpu_metal::{GraphDims, VerifyGraph};
12834 if !crate::gpu_metal::wait_replay() {
12840 tracing::error!("Metal rows graph: the pending async replay failed");
12841 return MetalRowsRun::Failed;
12842 }
12843 spec_stamp("v.wait");
12844 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12850 if want > 0 {
12851 let phys = self.physical_layers.max(1);
12852 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12853 let is_gdn = self
12854 .weights
12855 .layers
12856 .get(li % phys)
12857 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12858 if is_gdn && l.linear_state.len() != want {
12859 l.linear_state = vec![0f32; want];
12860 }
12861 }
12862 }
12863 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12864 return MetalRowsRun::Declined;
12865 };
12866 spec_stamp("v.plan");
12867 let dims = GraphDims {
12868 hidden: self.hidden_size,
12869 eps: self.rms_eps as f32,
12870 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12871 };
12872 let Some(mut graph) = (if prefill {
12873 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12874 } else {
12875 VerifyGraph::new(&model, dims, hiddens, b)
12876 }) else {
12877 return MetalRowsRun::Declined;
12878 };
12879 let geom = (
12880 self.num_heads,
12881 self.num_kv_heads,
12882 self.head_dim,
12883 self.rotary_dim,
12884 );
12885 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12886 let eps = self.rms_eps as f32;
12887 let kv_id = self.graph_kv_id;
12888 let inv_freq = self.inv_freq.clone();
12889 for item in &plan {
12890 let ok = match item {
12891 MetalRowsItem::Gdn { run, .. } => gcfg
12892 .as_ref()
12893 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12894 .unwrap_or(false),
12895 MetalRowsItem::Attn {
12896 l,
12897 li,
12898 q_norm,
12899 k_norm,
12900 output_gate,
12901 } => {
12902 let (p, _) = Self::metal_attn_params(
12903 *li,
12904 &self.kv_cache.layers[*li],
12905 *q_norm,
12906 *k_norm,
12907 *output_gate,
12908 &inv_freq,
12909 self.layer_rope_scale(*li),
12910 geom,
12911 pos0,
12912 kv_id,
12913 self.attn_scale,
12914 eps,
12915 gemma,
12916 self.qk_norm_after_rope,
12917 );
12918 graph.attn_ok(l, &p)
12919 }
12920 };
12921 if !ok {
12922 use std::sync::atomic::{AtomicBool, Ordering};
12923 static SAID: AtomicBool = AtomicBool::new(false);
12924 if !SAID.swap(true, Ordering::Relaxed) {
12925 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12926 }
12927 return MetalRowsRun::Declined;
12928 }
12929 }
12930 let lm = match &spec {
12931 Some((lm, _, _)) => {
12932 if !graph.lm_head_ok(*lm) {
12933 return MetalRowsRun::Declined;
12934 }
12935 Some(*lm)
12936 }
12937 None => None,
12938 };
12939 let mut gdn_layers = Vec::new();
12940 let mut attn_layers = Vec::new();
12941 for item in &plan {
12942 match item {
12943 MetalRowsItem::Gdn { run, first } => {
12944 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12945 .iter()
12946 .map(|l| l.linear_state.as_slice())
12947 .collect();
12948 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12949 return MetalRowsRun::Declined;
12950 }
12951 gdn_layers.extend(*first..*first + run.len());
12952 }
12953 MetalRowsItem::Attn {
12954 l,
12955 li,
12956 q_norm,
12957 k_norm,
12958 output_gate,
12959 } => {
12960 let (p, cpu_stored) = Self::metal_attn_params(
12961 *li,
12962 &self.kv_cache.layers[*li],
12963 *q_norm,
12964 *k_norm,
12965 *output_gate,
12966 &inv_freq,
12967 self.layer_rope_scale(*li),
12968 geom,
12969 pos0,
12970 kv_id,
12971 self.attn_scale,
12972 eps,
12973 gemma,
12974 self.qk_norm_after_rope,
12975 );
12976 if !graph.encode_attn_b(l, &p) {
12977 return MetalRowsRun::Declined;
12978 }
12979 attn_layers.push((*li, cpu_stored));
12980 }
12981 }
12982 }
12983 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12984 if !graph.encode_lm_head_b(final_norm, lm) {
12985 return MetalRowsRun::Declined;
12986 }
12987 if let Some((n, _)) = argmax_out.as_ref() {
12992 if !graph.encode_argmax_b(*n) {
12993 argmax_out = None;
12994 }
12995 }
12996 }
12997 spec_stamp("v.enc");
12998 if !graph.sync() {
12999 return MetalRowsRun::Failed;
13000 }
13001 spec_stamp("v.gpu");
13002 match (spec, argmax_out) {
13003 (Some(_), Some((_, ids))) => {
13004 ids.resize(b, 0);
13005 if !graph.read_argmax(ids) {
13006 return MetalRowsRun::Failed;
13007 }
13008 spec_stamp("v.am");
13009 }
13010 (Some((lm, _, logits)), None) => {
13011 logits.resize(b * lm.1, 0.0);
13012 if !graph.read_logits(logits) {
13013 return MetalRowsRun::Failed;
13014 }
13015 spec_stamp("v.lg");
13016 }
13017 (None, _) => {}
13018 }
13019 if !graph.read_hidden(hiddens) {
13020 return MetalRowsRun::Failed;
13021 }
13022 spec_stamp("v.hid");
13023 MetalRowsRun::Completed(MetalVerifyPending {
13024 graph,
13025 gdn_layers,
13026 attn_layers,
13027 })
13028 }
13029
13030 #[cfg(target_os = "macos")]
13036 fn try_batch_graph_metal(
13037 &mut self,
13038 hiddens: &mut [f32],
13039 positions: &[usize],
13040 b: usize,
13041 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13042 argmax_out: Option<(usize, &mut Vec<u32>)>,
13043 ) -> crate::gpu::BatchGraphOutcome {
13044 let _t0 = std::time::Instant::now();
13045 if positions.len() != b
13046 || positions.windows(2).any(|w| w[1] != w[0] + 1)
13047 || hiddens.len() != b * self.hidden_size
13048 {
13049 return crate::gpu::BatchGraphOutcome::Declined;
13050 }
13051 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
13052 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
13053 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
13054 MetalRowsRun::Completed(pending) => pending,
13055 };
13056 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13057 eprintln!(
13058 "metal-verify: {:.1} ms | b={b}",
13059 _t0.elapsed().as_secs_f64() * 1e3
13060 );
13061 }
13062 self.metal_verify = Some(pending);
13063 crate::gpu::BatchGraphOutcome::Completed
13064 }
13065
13066 #[cfg(target_os = "macos")]
13071 fn prefill_rows_metal(
13072 &mut self,
13073 ids: &[u32],
13074 start_pos: usize,
13075 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13076 ) -> MetalPrefillOutcome {
13077 let b = ids.len();
13078 if b == 0 || b > 512 {
13079 return MetalPrefillOutcome::Declined;
13080 }
13081 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13082 let with_head = spec.is_some();
13083 let hs = self.hidden_size;
13084 let mut hiddens = vec![0f32; b * hs];
13085 for (j, &id) in ids.iter().enumerate() {
13086 let e = self.embed_single(id);
13087 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
13088 }
13089 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
13090 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
13091 MetalRowsRun::Failed => {
13092 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13093 return MetalPrefillOutcome::Failed;
13094 }
13095 MetalRowsRun::Completed(pending) => pending,
13096 };
13097 let idxs = pending.gdn_layers.clone();
13099 let mut outs: Vec<&mut [f32]> = self
13100 .kv_cache
13101 .layers
13102 .iter_mut()
13103 .enumerate()
13104 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13105 .map(|(_, l)| l.linear_state.as_mut_slice())
13106 .collect();
13107 if !pending.graph.finish_states(&mut outs) {
13108 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13109 return MetalPrefillOutcome::Failed;
13110 }
13111 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13112 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13116 for (li, cpu_stored) in &pending.attn_layers {
13117 let mut kbuf = vec![0f32; b * nkv * hd];
13118 let mut vbuf = vec![0f32; b * nkv * hd];
13119 if !crate::gpu_metal::kv_mirror_read_rows(
13120 self.graph_kv_id,
13121 *li,
13122 nkv,
13123 hd,
13124 *cpu_stored,
13125 b,
13126 &mut kbuf,
13127 &mut vbuf,
13128 ) {
13129 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13130 return MetalPrefillOutcome::Failed;
13131 }
13132 rows.push((*li, *cpu_stored, kbuf, vbuf));
13133 }
13134 for (li, cpu_stored, kbuf, vbuf) in rows {
13135 let cache = &mut self.kv_cache.layers[li];
13136 for r in 0..b {
13137 cache.append(
13138 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13139 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13140 &[],
13141 );
13142 }
13143 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
13144 }
13145 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13146 if with_head {
13147 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13148 }
13149 MetalPrefillOutcome::Completed(hiddens)
13150 }
13151
13152 #[cfg(target_os = "macos")]
13153 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
13154 self.prefill_rows_metal(ids, start_pos, None)
13155 }
13156
13157 #[cfg(target_os = "macos")]
13162 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
13163 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
13164 return MetalBatchNllOutcome::Declined;
13165 }
13166 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
13167 return MetalBatchNllOutcome::Declined;
13168 };
13169 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
13170 .ok()
13171 .and_then(|v| v.parse::<usize>().ok())
13172 .filter(|&v| (1..=512).contains(&v))
13173 .unwrap_or(32);
13174 let final_norm = self.weights.final_norm.clone();
13175 let mut nll = 0.0f64;
13176 let mut count = 0usize;
13177 let mut pos = 0usize;
13178 let mut completed = 0usize;
13179 while pos < ids.len() {
13180 let end = (pos + chunk).min(ids.len());
13181 let mut logits = Vec::new();
13182 let outcome =
13183 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
13184 match outcome {
13185 MetalPrefillOutcome::Declined => {
13186 return if completed == 0 {
13187 MetalBatchNllOutcome::Declined
13188 } else {
13189 MetalBatchNllOutcome::Failed(format!(
13190 "ordinary Metal NLL batch declined after {completed} chunks"
13191 ))
13192 };
13193 }
13194 MetalPrefillOutcome::Failed => {
13195 return MetalBatchNllOutcome::Failed(
13196 "ordinary Metal NLL batch failed after admission".to_string(),
13197 );
13198 }
13199 MetalPrefillOutcome::Completed(_) => {}
13200 }
13201 completed += 1;
13202 let vocab = self.vocab_size.min(lm.1);
13203 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
13204 return MetalBatchNllOutcome::Failed(
13205 "ordinary Metal NLL head returned an invalid shape".to_string(),
13206 );
13207 }
13208 for row in 0..(end - pos) {
13209 let absolute = pos + row;
13210 if absolute < start || absolute + 1 >= ids.len() {
13211 continue;
13212 }
13213 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
13214 if let Some(mu) = self.logit_multiplier {
13215 for v in lg.iter_mut() {
13216 *v *= mu;
13217 }
13218 }
13219 if let Some(c) = self.final_softcap {
13220 softcap_in_place(lg, c, self.pool.as_deref());
13221 }
13222 let target = ids[absolute + 1] as usize;
13223 if target >= vocab {
13224 return MetalBatchNllOutcome::Failed(format!(
13225 "target token {target} exceeds Metal head rows {vocab}"
13226 ));
13227 }
13228 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13229 let lse: f64 = lg
13230 .iter()
13231 .map(|&v| ((v - max) as f64).exp())
13232 .sum::<f64>()
13233 .ln()
13234 + max as f64;
13235 nll += lse - lg[target] as f64;
13236 count += 1;
13237 }
13238 pos = end;
13239 }
13240 MetalBatchNllOutcome::Completed(nll, count)
13241 }
13242
13243 #[cfg(target_os = "macos")]
13247 fn metal_verify_commit(&mut self, a: usize) -> bool {
13248 let Some(mut pending) = self.metal_verify.take() else {
13249 return false;
13250 };
13251 let n = a + 1;
13252 let idxs = pending.gdn_layers.clone();
13254 let mut outs: Vec<&mut [f32]> = self
13255 .kv_cache
13256 .layers
13257 .iter_mut()
13258 .enumerate()
13259 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13260 .map(|(_, l)| l.linear_state.as_mut_slice())
13261 .collect();
13262 if !pending.graph.commit(n, &mut outs) {
13263 return false;
13264 }
13265 spec_stamp("c.replay");
13266 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13267 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13271 for (li, cpu_stored) in &pending.attn_layers {
13272 let mut kbuf = vec![0f32; n * nkv * hd];
13273 let mut vbuf = vec![0f32; n * nkv * hd];
13274 if !crate::gpu_metal::kv_mirror_read_rows(
13275 self.graph_kv_id,
13276 *li,
13277 nkv,
13278 hd,
13279 *cpu_stored,
13280 n,
13281 &mut kbuf,
13282 &mut vbuf,
13283 ) {
13284 return false;
13285 }
13286 rows.push((*li, *cpu_stored, kbuf, vbuf));
13287 }
13288 for (li, cpu_stored, kbuf, vbuf) in rows {
13289 let cache = &mut self.kv_cache.layers[li];
13290 for r in 0..n {
13291 cache.append(
13292 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13293 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13294 &[],
13295 );
13296 }
13297 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13298 }
13299 spec_stamp("c.kv");
13300 true
13301 }
13302
13303 #[cfg(target_os = "macos")]
13310 fn mtp_warm_batch_submit(
13311 &mut self,
13312 m: &mut MtpModule,
13313 pairs: &[(&[f32], u32)],
13314 first_pos: usize,
13315 ) -> Option<MetalWarmPending> {
13316 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13317 let b = pairs.len();
13318 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13319 return None;
13320 }
13321 let AttnKind::Full {
13322 wq,
13323 wk,
13324 wv,
13325 wo,
13326 q_norm,
13327 k_norm,
13328 output_gate,
13329 softplus_gate: None,
13330 bias: None,
13331 } = &m.layer.attn
13332 else {
13333 return None;
13334 };
13335 let FfnKind::Dense(d) = &m.layer.ffn else {
13336 return None;
13337 };
13338 if !d.segs.is_empty() {
13339 return None;
13340 }
13341 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13342 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13343 else {
13344 return None;
13345 };
13346 let (Some(g), Some(u), Some(dn)) = (
13347 d.gate_proj.q1_parts(),
13348 d.up_proj.q1_parts(),
13349 d.down_proj.q1_parts(),
13350 ) else {
13351 return None;
13352 };
13353 let Some(eh) = m.eh_proj.q1_parts() else {
13354 return None;
13355 };
13356 let QTensor::Mapped { model, .. } = wq else {
13357 return None;
13358 };
13359 let model = model.clone();
13360 let hs = self.hidden_size;
13361 let mut cat = vec![0f32; b * 2 * hs];
13363 for (j, (h, tok)) in pairs.iter().enumerate() {
13364 let e = self.embed_single(*tok);
13365 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13366 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13367 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13368 }
13369 let dims = GraphDims {
13370 hidden: hs,
13371 eps: self.rms_eps as f32,
13372 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13373 };
13374 spec_stamp("w.cat");
13375 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13376 return None;
13377 };
13378 spec_stamp("w.new");
13379 let l = AttnGpuLayer {
13380 attn_norm: &m.layer.input_norm,
13381 post_norm: &m.layer.post_norm,
13382 wq: pq,
13383 wk: pk,
13384 wv: pv,
13385 wo: po,
13386 ffn: MetalFfn::Dense {
13387 gate: g,
13388 up: u,
13389 down: dn,
13390 act: (d.act == Act::Gelu) as u8,
13391 },
13392 attn_out_norm: None,
13393 ffn_out_norm: None,
13394 layer_scale: None,
13395 };
13396 let (nh, nkv, hd, rd) = (
13397 self.num_heads,
13398 self.num_kv_heads,
13399 self.head_dim,
13400 self.rotary_dim,
13401 );
13402 let inv_freq = self.inv_freq.clone();
13403 let cpu_stored;
13404 {
13405 let cache = &m.kv;
13406 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13407 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13408 cpu_stored = cpu_k[0].len() / hd;
13409 if cpu_stored > first_pos {
13414 spec_stamp("w.decl");
13415 return None;
13416 }
13417 let p = AttnDeviceParams {
13418 kv_id: self.mtp_kv_id(),
13419 layer: Self::MTP_LAYER_BASE,
13420 nh,
13421 nkv,
13422 hd,
13423 rd,
13424 position: first_pos,
13425 scale: self.attn_scale,
13426 eps: self.rms_eps as f32,
13427 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13428 late_qk_norm: self.qk_norm_after_rope,
13429 output_gate: *output_gate,
13430 q_norm: q_norm.as_deref(),
13431 k_norm: k_norm.as_deref(),
13432 inv_freq: &inv_freq,
13433 rope_scale: self.rope_scale,
13434 cpu_k,
13435 cpu_v,
13436 cpu_stored,
13437 cpu_gen: cache.generation(),
13438 o1: None,
13439 window: None,
13440 softmax_sink: None,
13441 head_gate: None,
13442 v_norm: false,
13443 };
13444 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13445 return None;
13446 }
13447 }
13448 spec_stamp("w.enc");
13449 if !graph.submit() {
13450 return None;
13451 }
13452 spec_stamp("w.sub");
13453 Some(MetalWarmPending {
13454 graph,
13455 cpu_stored,
13456 b,
13457 })
13458 }
13459
13460 #[cfg(target_os = "macos")]
13463 fn mtp_warm_batch_metal(
13464 &mut self,
13465 m: &mut MtpModule,
13466 pairs: &[(&[f32], u32)],
13467 first_pos: usize,
13468 ) -> bool {
13469 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13470 Some(p) => self.mtp_warm_batch_finish(m, p),
13471 None => false,
13472 }
13473 }
13474
13475 #[cfg(target_os = "macos")]
13480 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13481 let MetalWarmPending {
13482 mut graph,
13483 cpu_stored,
13484 b,
13485 } = pending;
13486 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13487 if !graph.sync() {
13488 return false;
13489 }
13490 spec_stamp("w.gpu");
13491 let mut kbuf = vec![0f32; b * nkv * hd];
13492 let mut vbuf = vec![0f32; b * nkv * hd];
13493 if !crate::gpu_metal::kv_mirror_read_rows(
13494 self.mtp_kv_id(),
13495 Self::MTP_LAYER_BASE,
13496 nkv,
13497 hd,
13498 cpu_stored,
13499 b,
13500 &mut kbuf,
13501 &mut vbuf,
13502 ) {
13503 return false;
13504 }
13505 for r in 0..b {
13506 m.kv.append(
13507 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13508 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13509 &[],
13510 );
13511 }
13512 crate::gpu_metal::kv_mirror_set_stored(
13513 self.mtp_kv_id(),
13514 Self::MTP_LAYER_BASE,
13515 cpu_stored + b,
13516 );
13517 spec_stamp("w.kv");
13518 true
13519 }
13520
13521 pub(crate) fn note_draft_id(&mut self, id: u32) {
13528 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13529 if (id as usize) >= cut {
13530 self.draft_full_streak = 16;
13531 } else {
13532 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13533 }
13534 }
13535
13536 fn draft_head_rows(&self, head_rows: usize) -> usize {
13539 if self.draft_full_streak > 0 {
13540 head_rows
13541 } else {
13542 Self::draft_vocab_rows(head_rows)
13543 }
13544 }
13545
13546 fn draft_vocab_rows(head_rows: usize) -> usize {
13549 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13550 let n = *N.get_or_init(|| {
13551 std::env::var("CMF_DRAFT_VOCAB")
13552 .ok()
13553 .and_then(|v| v.parse().ok())
13554 .unwrap_or(65536)
13555 });
13556 if n == 0 { head_rows } else { n.min(head_rows) }
13557 }
13558
13559 #[cfg(target_os = "macos")]
13564 fn mtp_step_metal(
13565 &mut self,
13566 m: &mut MtpModule,
13567 hidden: &[f32],
13568 next_token: u32,
13569 position: usize,
13570 want_logits: bool,
13571 ) -> Option<(Vec<f32>, Vec<f32>)> {
13572 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13573 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13574 || !crate::gpu::q1_force()
13575 || !crate::gpu::enabled_here()
13576 || self.attn_softcap > 0.0
13577 || self.attention_heads_per_layer.is_some()
13578 || m.kv.mode != crate::kv_cache::KvMode::F32
13579 || m.kv.o1.is_some()
13580 {
13581 return None;
13582 }
13583 let AttnKind::Full {
13584 wq,
13585 wk,
13586 wv,
13587 wo,
13588 q_norm,
13589 k_norm,
13590 output_gate,
13591 softplus_gate: None,
13592 bias: None,
13593 } = &m.layer.attn
13594 else {
13595 return None;
13596 };
13597 let FfnKind::Dense(d) = &m.layer.ffn else {
13598 return None;
13599 };
13600 if d.act != Act::Silu || !d.segs.is_empty() {
13601 return None;
13602 }
13603 let (pq, pk, pv, po) = (
13604 wq.q1_parts()?,
13605 wk.q1_parts()?,
13606 wv.q1_parts()?,
13607 wo.q1_parts()?,
13608 );
13609 let (g, u, dn) = (
13610 d.gate_proj.q1_parts()?,
13611 d.up_proj.q1_parts()?,
13612 d.down_proj.q1_parts()?,
13613 );
13614 let QTensor::Mapped { model, .. } = wq else {
13615 return None;
13616 };
13617 let model = model.clone();
13618 let lm = if want_logits {
13619 Some(self.weights.lm_head.q1_parts()?)
13620 } else {
13621 None
13622 };
13623 let dims = GraphDims {
13624 hidden: self.hidden_size,
13625 eps: self.rms_eps as f32,
13626 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13627 };
13628 let hs = self.hidden_size;
13631 let mut x = vec![0f32; hs];
13632 let mut graph = TokenGraph::new(&model, dims, &x)?;
13633 let mut folded = false;
13634 if let Some(eh) = m.eh_proj.q1_parts() {
13635 let e = self.embed_single(next_token);
13636 let mut cat = vec![0.0f32; 2 * hs];
13637 let (cat_e, cat_h) = cat.split_at_mut(hs);
13638 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13639 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13640 folded = graph.encode_input_proj(eh, &cat);
13641 }
13642 if !folded {
13643 x = self.mtp_block_input(m, hidden, next_token);
13644 graph = TokenGraph::new(&model, dims, &x)?;
13645 }
13646 spec_stamp("d.in");
13647 let l = AttnGpuLayer {
13648 attn_norm: &m.layer.input_norm,
13649 post_norm: &m.layer.post_norm,
13650 wq: pq,
13651 wk: pk,
13652 wv: pv,
13653 wo: po,
13654 ffn: MetalFfn::Dense {
13655 gate: g,
13656 up: u,
13657 down: dn,
13658 act: (d.act == Act::Gelu) as u8,
13659 },
13660 attn_out_norm: None,
13661 ffn_out_norm: None,
13662 layer_scale: None,
13663 };
13664 let (nh, nkv, hd, rd) = (
13665 self.num_heads,
13666 self.num_kv_heads,
13667 self.head_dim,
13668 self.rotary_dim,
13669 );
13670 let inv_freq = self.inv_freq.clone();
13671 {
13672 let cache = &m.kv;
13673 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13674 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13675 let cpu_stored = cpu_k[0].len() / hd;
13676 let p = AttnDeviceParams {
13677 kv_id: self.mtp_kv_id(),
13678 layer: Self::MTP_LAYER_BASE,
13679 nh,
13680 nkv,
13681 hd,
13682 rd,
13683 position,
13684 scale: self.attn_scale,
13685 eps: self.rms_eps as f32,
13686 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13687 late_qk_norm: self.qk_norm_after_rope,
13688 output_gate: *output_gate,
13689 q_norm: q_norm.as_deref(),
13690 k_norm: k_norm.as_deref(),
13691 inv_freq: &inv_freq,
13692 rope_scale: self.rope_scale,
13693 cpu_k,
13694 cpu_v,
13695 cpu_stored,
13696 cpu_gen: cache.generation(),
13697 o1: None,
13698 window: None,
13699 softmax_sink: None,
13700 head_gate: None,
13701 v_norm: false,
13702 };
13703 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13704 return None;
13705 }
13706 }
13707 let draft_rows = if let Some(lm) = lm {
13713 self.draft_head_rows(lm.1)
13714 } else {
13715 0
13716 };
13717 if let Some(lm) = lm {
13718 if !graph.lm_head_ok(lm) {
13719 return None;
13720 }
13721 if draft_rows < lm.1 {
13722 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13723 return None;
13724 }
13725 } else {
13726 graph.encode_lm_head(&m.final_norm, lm);
13727 }
13728 }
13729 spec_stamp("d.enc");
13730 if graph.sync_checked().is_err() {
13731 return None;
13732 }
13733 spec_stamp("d.gpu");
13734 let mut logits = Vec::new();
13735 if let Some(lm) = lm {
13736 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13737 logits = attention::take_buf(n_read);
13738 graph.read_logits(&mut logits);
13739 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13741 }
13742 graph.finish(&mut x);
13743 let mut krow = attention::take_buf(nkv * hd);
13744 let mut vrow = attention::take_buf(nkv * hd);
13745 if crate::gpu_metal::kv_mirror_read_last(
13746 self.mtp_kv_id(),
13747 Self::MTP_LAYER_BASE,
13748 nkv,
13749 hd,
13750 &mut krow,
13751 &mut vrow,
13752 ) {
13753 m.kv.append(&krow, &vrow, &[]);
13754 }
13755 attention::recycle_buf(&mut krow);
13756 attention::recycle_buf(&mut vrow);
13757 spec_stamp("d.rd");
13758 Some((logits, x))
13759 }
13760
13761 fn mtp_chain_on() -> bool {
13774 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13775 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13776 }
13777
13778 #[cfg(target_os = "macos")]
13790 fn mtp_draft_chain_metal(
13791 &mut self,
13792 m: &mut MtpModule,
13793 hidden: &[f32],
13794 t_next: u32,
13795 position: usize,
13796 k: usize,
13797 ) -> Result<Vec<u32>, bool> {
13798 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13799 if k == 0
13800 || k > 64
13801 || !Self::mtp_chain_on()
13802 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13803 || !crate::gpu::q1_force()
13804 || !crate::gpu::enabled_here()
13805 || self.attn_softcap > 0.0
13806 || self.attention_heads_per_layer.is_some()
13807 || m.kv.mode != crate::kv_cache::KvMode::F32
13808 || m.kv.o1.is_some()
13809 || self.dsv4.is_some()
13811 || self.dsv41.is_some()
13812 || self.qwen4_exp.is_some()
13813 || self.g3n.is_some()
13814 {
13815 return Err(false);
13816 }
13817 let AttnKind::Full {
13818 wq,
13819 wk,
13820 wv,
13821 wo,
13822 q_norm,
13823 k_norm,
13824 output_gate,
13825 softplus_gate: None,
13826 bias: None,
13827 } = &m.layer.attn
13828 else {
13829 return Err(false);
13830 };
13831 let FfnKind::Dense(d) = &m.layer.ffn else {
13832 return Err(false);
13833 };
13834 if d.act != Act::Silu || !d.segs.is_empty() {
13835 return Err(false);
13836 }
13837 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13838 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13839 else {
13840 return Err(false);
13841 };
13842 let (Some(g), Some(u), Some(dn)) = (
13843 d.gate_proj.q1_parts(),
13844 d.up_proj.q1_parts(),
13845 d.down_proj.q1_parts(),
13846 ) else {
13847 return Err(false);
13848 };
13849 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13850 return Err(false);
13851 };
13852 let QTensor::Mapped { model, .. } = wq else {
13853 return Err(false);
13854 };
13855 let model = model.clone();
13856 let QTensor::Mapped {
13859 model: em,
13860 idx: eidx,
13861 dtype: cortiq_core::TensorDtype::Q4TiledP,
13862 ..
13863 } = &self.weights.embed_tokens
13864 else {
13865 return Err(false);
13866 };
13867 if !std::sync::Arc::ptr_eq(em, &model)
13868 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13869 {
13870 return Err(false);
13871 }
13872 let embed = (
13873 *eidx,
13874 self.weights.embed_tokens.rows(),
13875 self.weights.embed_tokens.cols(),
13876 );
13877 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13878 return Err(false);
13879 }
13880 let dims = GraphDims {
13881 hidden: self.hidden_size,
13882 eps: self.rms_eps as f32,
13883 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13884 };
13885 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13886 return Err(false);
13887 };
13888 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13889 return Err(false);
13890 }
13891 let l = AttnGpuLayer {
13892 attn_norm: &m.layer.input_norm,
13893 post_norm: &m.layer.post_norm,
13894 wq: pq,
13895 wk: pk,
13896 wv: pv,
13897 wo: po,
13898 ffn: MetalFfn::Dense {
13899 gate: g,
13900 up: u,
13901 down: dn,
13902 act: (d.act == Act::Gelu) as u8,
13903 },
13904 attn_out_norm: None,
13905 ffn_out_norm: None,
13906 layer_scale: None,
13907 };
13908 let (nh, nkv, hd, rd) = (
13909 self.num_heads,
13910 self.num_kv_heads,
13911 self.head_dim,
13912 self.rotary_dim,
13913 );
13914 let inv_freq = self.inv_freq.clone();
13915 let draft_rows = self.draft_head_rows(lm.1);
13916 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13917 if n_arg == 0 {
13918 return Err(false);
13919 }
13920 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13927 let t_chain = std::time::Instant::now();
13928 graph.chain_ids_init(t_next, k);
13929 let cpu_stored;
13930 {
13931 let cache = &m.kv;
13932 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13933 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13934 cpu_stored = cpu_k[0].len() / hd;
13935 for j in 0..k {
13936 if !graph.encode_chain_input(
13937 embed,
13938 j as u32,
13939 &m.enorm,
13940 &m.hnorm,
13941 self.embed_multiplier,
13942 eh,
13943 ) {
13944 return Err(false);
13945 }
13946 let p = AttnDeviceParams {
13950 kv_id: self.mtp_kv_id(),
13951 layer: Self::MTP_LAYER_BASE,
13952 nh,
13953 nkv,
13954 hd,
13955 rd,
13956 position: position + j,
13957 scale: self.attn_scale,
13958 eps: self.rms_eps as f32,
13959 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13960 late_qk_norm: self.qk_norm_after_rope,
13961 output_gate: *output_gate,
13962 q_norm: q_norm.as_deref(),
13963 k_norm: k_norm.as_deref(),
13964 inv_freq: &inv_freq,
13965 rope_scale: self.rope_scale,
13966 cpu_k: cpu_k.clone(),
13967 cpu_v: cpu_v.clone(),
13968 cpu_stored: cpu_stored + j,
13969 cpu_gen: cache.generation(),
13970 o1: None,
13971 window: None,
13972 softmax_sink: None,
13973 head_gate: None,
13974 v_norm: false,
13975 };
13976 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13977 return Err(false);
13978 }
13979 if draft_rows < lm.1 {
13980 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13981 return Err(false);
13982 }
13983 } else {
13984 graph.encode_lm_head(&m.final_norm, lm);
13985 }
13986 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13987 return Err(false);
13988 }
13989 if split {
13990 graph.commit();
13993 }
13994 }
13995 }
13996 let t_enc = t_chain.elapsed();
13997 if graph.sync_checked().is_err() {
13998 return Err(true);
13999 }
14000 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14001 eprintln!(
14002 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14003 t_enc.as_secs_f64() * 1e3,
14004 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14005 if split { ", split" } else { "" }
14006 );
14007 }
14008 let mut ids = vec![0u32; k];
14009 if !graph.chain_ids_read(&mut ids) {
14010 return Err(true);
14011 }
14012 let mut kbuf = vec![0f32; k * nkv * hd];
14013 let mut vbuf = vec![0f32; k * nkv * hd];
14014 if !crate::gpu_metal::kv_mirror_read_rows(
14015 self.mtp_kv_id(),
14016 Self::MTP_LAYER_BASE,
14017 nkv,
14018 hd,
14019 cpu_stored,
14020 k,
14021 &mut kbuf,
14022 &mut vbuf,
14023 ) {
14024 return Err(true);
14025 }
14026 for r in 0..k {
14027 m.kv.append(
14028 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14029 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14030 &[],
14031 );
14032 }
14033 Ok(ids)
14034 }
14035
14036 fn try_batch_graph_wgpu(
14037 &self,
14038 hiddens: &mut [f32],
14039 positions: &[usize],
14040 k: usize,
14041 spec: Option<crate::gpu::SpecTail<'_>>,
14042 ) -> crate::gpu::BatchGraphOutcome {
14043 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
14044 }
14045
14046 fn try_batch_graph_wgpu_prefix(
14051 &self,
14052 hiddens: &mut [f32],
14053 positions: &[usize],
14054 k: usize,
14055 spec: Option<crate::gpu::SpecTail<'_>>,
14056 layers_run: Option<&mut usize>,
14057 ) -> crate::gpu::BatchGraphOutcome {
14058 let graph_end = match self.mimo_moe.graph_prefix_end() {
14059 Some(end) if end < self.num_layers => {
14060 if layers_run.is_none() || spec.is_some() || end == 0 {
14061 return crate::gpu::BatchGraphOutcome::Declined;
14062 }
14063 end
14064 }
14065 _ => self.num_layers,
14066 };
14067 let _tb = std::time::Instant::now();
14068 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
14069 if self.attn_softcap > 0.0 {
14070 return crate::gpu::BatchGraphOutcome::Declined; }
14072 if let Some(reason) = self
14075 .wgpu_graph_attn_decline()
14076 .or_else(|| self.batch_graph_gemma_decline())
14077 {
14078 self.note_graph_decline("wgpu batch graph", reason);
14079 return crate::gpu::BatchGraphOutcome::Declined;
14080 }
14081 let nh = self.num_heads;
14082 let (nkv, hd, rd) = self.layer_geom(0);
14083 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14084 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14085 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
14086 {
14087 let name = &m.tensors[i].name;
14088 let prism = if crate::prism::is_inverse_embedding(m, name) {
14089 crate::gpu::GraphPrismOp::InverseEmbedding
14090 } else if crate::prism::is_forward_weight(m, name) {
14091 crate::gpu::GraphPrismOp::Forward
14092 } else {
14093 crate::gpu::GraphPrismOp::None
14094 };
14095 return Some(crate::gpu::GraphW {
14096 idx: i,
14097 kind,
14098 row_scale: rs,
14099 data: &[],
14100 prism,
14101 affine: crate::prism::is_affine_target(m, name),
14102 });
14103 }
14104 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14105 eprintln!(
14106 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
14107 t.rows(),
14108 t.cols()
14109 );
14110 }
14111 t.as_f32().map(|d| crate::gpu::GraphW {
14112 idx: 0,
14113 kind: 4,
14114 row_scale: &[],
14115 data: d,
14116 prism: crate::gpu::GraphPrismOp::None,
14117 affine: false,
14118 })
14119 }
14120 let built: Option<(
14121 Vec<crate::gpu::GraphLayer<'_>>,
14122 std::sync::Arc<cortiq_core::CmfModel>,
14123 )> = (|| {
14124 let mut layers = Vec::with_capacity(graph_end);
14125 let mut model = None;
14126 for li in 0..graph_end {
14127 let lw = &self.weights.layers[self.phys_layer(li)];
14128 let gffn = match &lw.ffn {
14135 FfnKind::Dense(d) if !d.segs.is_empty() => {
14136 if batch_debug {
14137 eprintln!("batch graph: dense segmented FFN at layer {li}");
14138 }
14139 return None;
14140 }
14141 FfnKind::Dense(d) => {
14142 let Some(act) = d.act.graph_act() else {
14143 if batch_debug {
14144 eprintln!(
14145 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
14146 d.act
14147 );
14148 }
14149 return None;
14150 };
14151 crate::gpu::GraphFfn::Dense {
14152 gate: gw(&d.gate_proj)?,
14153 up: gw(&d.up_proj)?,
14154 down: gw(&d.down_proj)?,
14155 act,
14156 }
14157 }
14158 FfnKind::Moe(m) => {
14159 if m.route_tau.is_some() || m.mask.is_some() {
14166 return None;
14167 }
14168 let shared = m.shared.as_ref();
14172 let has_shared = shared.is_some();
14173 let shared_gated = matches!(shared, Some((_, Some(_))));
14174 let sgate = match shared {
14175 Some((_, Some(sg))) => gw(sg)?,
14176 _ => gw(&m.router)?,
14180 };
14181 let router = gw(&m.router)?;
14182 if router.prism != crate::gpu::GraphPrismOp::None
14188 || router.affine
14189 || sgate.prism != crate::gpu::GraphPrismOp::None
14190 || sgate.affine
14191 {
14192 return None;
14193 }
14194 let inter = m.experts.first()?.gate_proj.rows();
14195 let mut experts = Vec::with_capacity(m.experts.len() + 1);
14196 let mut q4tp: Option<bool> = None;
14197 let mut gu_q2: Option<bool> = None;
14198 let mut q82: Option<bool> = None;
14199 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
14200 if !matches!(e.act, Act::Silu)
14201 || e.gate_proj.rows() != inter
14202 || e.up_proj.rows() != inter
14203 {
14204 return None;
14205 }
14206 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
14210 {
14211 Some((mm, gi)) => (
14212 mm,
14213 gi,
14214 e.up_proj.mapped_q4t()?.1,
14215 e.down_proj.mapped_q4t()?.1,
14216 false,
14217 false,
14218 false,
14219 ),
14220 None => match e.gate_proj.mapped_q2tp() {
14221 Some((mm, gi)) => (
14222 mm,
14223 gi,
14224 e.up_proj.mapped_q2tp()?.1,
14225 e.down_proj.mapped_q4tp()?.1,
14226 true,
14227 true,
14228 false,
14229 ),
14230 None => match e.gate_proj.mapped_q4tp() {
14231 Some((mm, gi)) => (
14232 mm,
14233 gi,
14234 e.up_proj.mapped_q4tp()?.1,
14235 e.down_proj.mapped_q4tp()?.1,
14236 true,
14237 false,
14238 false,
14239 ),
14240 None => {
14241 if !crate::gpu::moe_q82_graph_on() {
14242 return None;
14243 }
14244 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14245 (
14246 mm,
14247 gi,
14248 e.up_proj.mapped_q8_2f()?.1,
14249 e.down_proj.mapped_q8_2f()?.1,
14250 false,
14251 false,
14252 true,
14253 )
14254 }
14255 },
14256 },
14257 };
14258 if *q4tp.get_or_insert(is_p) != is_p
14259 || *gu_q2.get_or_insert(is_q2) != is_q2
14260 || *q82.get_or_insert(is_8) != is_8
14261 {
14262 return None;
14263 }
14264 if [gi, ui, di].into_iter().any(|idx| {
14265 mm.tensors.get(idx).is_some_and(|t| {
14266 crate::prism::is_forward_weight(mm, &t.name)
14267 || crate::prism::is_affine_target(mm, &t.name)
14268 })
14269 }) {
14270 return None;
14271 }
14272 model.get_or_insert_with(|| mm.clone());
14273 experts.push((gi, ui, di));
14274 }
14275 crate::gpu::GraphFfn::Moe {
14276 router,
14277 shared_gate: sgate,
14278 experts,
14279 n_exp: m.experts.len(),
14280 top_k: m.top_k,
14281 inter,
14282 norm_topk: m.norm_topk_prob,
14283 q4tp: q4tp?,
14284 gu_q2: gu_q2.unwrap_or(false),
14285 q82: q82.unwrap_or(false),
14286 sigmoid: m.router_sigmoid,
14287 sigmoid_denom_eps: m.sigmoid_denom_eps,
14288 bias: m.expert_bias.as_deref(),
14289 has_shared,
14290 detached_shared: None,
14291 shared_gated,
14292 route_scale: m.routed_scaling,
14293 }
14294 }
14295 _ => return None,
14296 };
14297 let attn = match &lw.attn {
14298 AttnKind::Full {
14299 wq,
14300 wk,
14301 wv,
14302 wo,
14303 q_norm,
14304 k_norm,
14305 output_gate,
14306 softplus_gate,
14307 bias,
14308 } => {
14309 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14310 if batch_debug {
14311 eprintln!(
14312 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14313 softplus_gate.is_some(),
14314 self.attention_heads_per_layer.is_some()
14315 );
14316 }
14317 return None;
14318 }
14319 let (m, _, _, _) =
14320 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
14321 model = Some(m.clone());
14322 crate::gpu::GraphAttn::Full {
14323 wq: gw(wq)?,
14324 wk: gw(wk)?,
14325 wv: gw(wv)?,
14326 wo: gw(wo)?,
14327 q_norm: q_norm.as_deref(),
14328 k_norm: k_norm.as_deref(),
14329 late_qk_norm: self.qk_norm_after_rope,
14330 bias: bias
14331 .as_ref()
14332 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14333 output_gate: *output_gate,
14334 cpu_k: self.kv_cache.layers[li].k_heads(),
14335 cpu_v: self.kv_cache.layers[li].v_heads(),
14336 cpu_base: self.kv_cache.layers[li].base(),
14337 geom: self.graph_attn_geom(li),
14338 head_gate: None,
14341 }
14342 }
14343 AttnKind::LinearGdn(w) => {
14344 let Some(cfg) = self.gdn_cfg else {
14345 if batch_debug {
14346 eprintln!("batch graph: no GDN config at layer {li}");
14347 }
14348 return None;
14349 };
14350 let (m, _, _, _) = w
14351 .in_proj_qkv
14352 .graph_weight()
14353 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14354 model = Some(m.clone());
14355 crate::gpu::GraphAttn::Gdn {
14356 qkv: gw(&w.in_proj_qkv)?,
14357 z: gw(&w.in_proj_z)?,
14358 a: gw(&w.in_proj_a)?,
14359 b: gw(&w.in_proj_b)?,
14360 out: gw(&w.out_proj)?,
14361 conv1d: &w.conv1d,
14362 a_log: &w.a_log,
14363 dt_bias: &w.dt_bias,
14364 norm: &w.norm,
14365 nv: cfg.num_v_heads,
14366 nk: cfg.num_k_heads,
14367 dk: cfg.key_head_dim,
14368 dv: cfg.value_head_dim,
14369 kk: cfg.conv_kernel,
14370 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14371 }
14372 }
14373 _ => return None,
14374 };
14375 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14378 layers.push(crate::gpu::GraphLayer {
14379 input_norm: &lw.input_norm,
14380 attn,
14381 post_norm: &lw.post_norm,
14382 attn_out_norm: if g4 {
14383 lw.attn_out_norm.as_deref()
14384 } else {
14385 None
14386 },
14387 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14388 layer_scale: if g4 { lw.layer_scale } else { None },
14389 ffn: gffn,
14390 });
14391 }
14392 Some((layers, model?))
14393 })();
14394 let Some((layers, model)) = built else {
14395 {
14396 use std::sync::atomic::{AtomicBool, Ordering};
14397 static SAID: AtomicBool = AtomicBool::new(false);
14398 if !SAID.swap(true, Ordering::Relaxed) {
14399 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14400 }
14401 }
14402 return crate::gpu::BatchGraphOutcome::Declined;
14403 };
14404 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14405 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14406 }
14407 crate::gpu::forward_batch_graph(
14408 &model,
14409 self.graph_kv_id,
14410 &layers,
14411 &self.inv_freq,
14412 hiddens,
14413 nh,
14414 nkv,
14415 hd,
14416 rd,
14417 self.hidden_size,
14418 self.intermediate_size,
14419 positions,
14420 self.kv_cache.max_seq_len,
14421 gemma,
14422 self.rms_eps as f32,
14423 self.attn_scale,
14424 k,
14425 &(0..graph_end)
14426 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14427 .collect::<Vec<_>>(),
14428 self.o1_epoch,
14429 spec,
14430 layers_run,
14431 )
14432 }
14433
14434 fn draft_probe() -> bool {
14438 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14439 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14440 }
14441
14442 #[cfg(feature = "gpu")]
14454 fn dsv4_spec_on() -> bool {
14455 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14456 *ON.get_or_init(|| {
14457 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14461 return v != "0";
14462 }
14463 std::env::var("CMF_DSV4_SPEC")
14470 .map(|v| v != "0")
14471 .unwrap_or_else(|_| {
14472 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14473 })
14474 })
14475 }
14476
14477 #[cfg(feature = "gpu")]
14484 fn dsv4_spec_step(
14485 &mut self,
14486 tip_token: u32,
14487 t_next: u32,
14488 next_pos: usize,
14489 max_extra: usize,
14490 drafted: &mut usize,
14491 accepted_ctr: &mut usize,
14492 ) -> Option<(Vec<u32>, usize)> {
14493 let t_all = std::time::Instant::now();
14494 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14495 thread_local! {
14496 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14497 const { std::cell::Cell::new(None) };
14498 }
14499 LAST.with(|l| {
14500 if let Some(prev) = l.get() {
14501 eprintln!(
14502 "между раундами {:.1} мс",
14503 prev.elapsed().as_secs_f64() * 1e3
14504 );
14505 }
14506 l.set(Some(std::time::Instant::now()));
14507 });
14508 }
14509 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14510 eprintln!("spec_step: вход pos={next_pos}");
14511 }
14512 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14513 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14514 if self.dspark.is_none() {
14516 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14517 if t.is_empty() {
14518 return None;
14519 }
14520 crate::dsv4::dspark_arm(&t, cfg.dim);
14521 self.dspark = Some(crate::dsv4::DsparkState::new(
14522 self.dsv4_mtp.len(),
14523 &cfg,
14524 t.len(),
14525 ));
14526 }
14527 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14528 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
14529 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14530 eprintln!("spec_step: пак не построился (targets {targets:?})");
14531 }
14532 let pack = pack?;
14533 let block = crate::dsv4::dspark_block();
14534 let b_box = self.dsv4.as_mut()?;
14535 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
14536 let ds = self.dspark.as_mut()?;
14537 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14540 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14541 if dbg {
14542 eprintln!("spec_step: нет захвата");
14543 }
14544 return None;
14545 }
14546 ds.have_hidden = true;
14547 let tip_pos = next_pos.checked_sub(1)?;
14548 let draft_started = std::time::Instant::now();
14549 let mut conf = Vec::new();
14550 let props = crate::dsv4::dspark_draft_gpu(
14551 g,
14552 &self.dsv4_mtp,
14553 &cfg,
14554 ds,
14555 pack,
14556 st.kv_id,
14557 tip_token,
14558 tip_pos,
14559 self.pool.as_deref(),
14560 &mut conf,
14561 );
14562 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14563 *drafted += block;
14564 if props.is_empty() || props[0] != t_next {
14565 if dbg {
14566 eprintln!(
14567 "spec_step: черновик {} (props0={:?} t_next={t_next})",
14568 if props.is_empty() {
14569 "пуст"
14570 } else {
14571 "мимо"
14572 },
14573 props.first()
14574 );
14575 }
14576 return None;
14577 }
14578 let mut k_verify = crate::dsv4::dspark_verify_k()
14585 .min(props.len())
14586 .min(max_extra.saturating_add(1));
14587 let conf_min = {
14593 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14594 *M.get_or_init(|| {
14595 std::env::var("CMF_DSPARK_CONF_MIN")
14596 .ok()
14597 .and_then(|v| v.parse().ok())
14598 .unwrap_or(0.0)
14599 })
14600 };
14601 if conf_min > 0.0 && conf.len() >= props.len() {
14602 let mut keep = 1usize;
14603 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14604 keep += 1;
14605 }
14606 k_verify = k_verify.min(keep.max(2));
14607 }
14608 if k_verify < 2 {
14609 return None;
14610 }
14611 let mut fed = Vec::with_capacity(k_verify);
14612 fed.push(t_next);
14613 fed.extend_from_slice(&props[1..k_verify]);
14614 let mut argmax = Vec::new();
14615 let mut logits_all = Vec::new();
14616 let mut walked = Vec::new();
14617 let txn = crate::dsv4::dsv4_verify_chunk(
14618 g,
14619 layers,
14620 &cfg,
14621 st,
14622 &fed,
14623 next_pos,
14624 &self.inv_freq,
14625 self.pool.as_deref(),
14626 &targets,
14627 &mut argmax,
14628 &mut logits_all,
14629 &mut walked,
14630 );
14631 if txn.is_none() && dbg {
14632 eprintln!("spec_step: verify отказал");
14633 }
14634 let txn = txn?;
14635 let spec_gpu_end = txn.gpu_end;
14636 let b = fed.len();
14637 let mut accepted = 1usize;
14638 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14639 accepted += 1;
14640 }
14641 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14646 accepted = 1;
14647 }
14648 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14649 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14650 }
14651 let t_fin = std::time::Instant::now();
14652 if !crate::dsv4::dsv4_spec_finish(
14653 g,
14654 layers,
14655 &cfg,
14656 st,
14657 txn,
14658 accepted,
14659 &fed,
14660 &self.inv_freq,
14661 self.pool.as_deref(),
14662 ) {
14663 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14664 return None;
14665 }
14666 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14667 eprintln!(
14668 "finish(k={accepted}): {:.1} мс",
14669 t_fin.elapsed().as_secs_f64() * 1e3
14670 );
14671 }
14672 *accepted_ctr += accepted - 1;
14673 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14678 let dev_caps: Vec<usize> = targets
14683 .iter()
14684 .copied()
14685 .filter(|&t| t < spec_gpu_end)
14686 .collect();
14687 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14688 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14689 return None;
14690 }
14691 for t in 0..accepted {
14692 let tip = t + 1 == accepted;
14693 for (slot, &tl) in targets.iter().enumerate() {
14694 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14695 let lo = (di * b + t) * hc * dim;
14696 crate::dsv4::dspark_capture(
14697 &caps_all[lo..lo + hc * dim],
14698 &cfg,
14699 slot,
14700 &mut ds.main_hidden,
14701 );
14702 } else if tip
14703 && crate::dsv4::dspark_peek_slot(slot, dim, {
14704 let lo = slot * dim;
14705 &mut ds.main_hidden[lo..lo + dim]
14706 })
14707 {
14708 } else {
14713 crate::dsv4::dspark_capture(
14717 &walked[t * hc * dim..(t + 1) * hc * dim],
14718 &cfg,
14719 slot,
14720 &mut ds.main_hidden,
14721 );
14722 }
14723 }
14724 crate::dsv4::dspark_ring_append(
14725 g,
14726 &self.dsv4_mtp,
14727 &cfg,
14728 ds,
14729 next_pos + t,
14730 self.pool.as_deref(),
14731 );
14732 }
14733 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14734 self.graph_logits = Some(row);
14735 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14740 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14741 crate::dsv4::pick_tally_arm();
14742 }
14743 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14744 eprintln!(
14745 "spec_step total {:.1} мс (k={accepted})",
14746 t_all.elapsed().as_secs_f64() * 1e3
14747 );
14748 }
14749 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14750 }
14751
14752 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14753 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14754 return;
14755 }
14756 let trunk_now = crate::dsv4::pick_tally_take();
14758 crate::dsv4::trunk_freq_note(&trunk_now);
14759 if !trunk_now.is_empty() {
14760 self.dspark_trunk_picks.push(trunk_now);
14761 let keep = crate::dsv4::dspark_block();
14762 if self.dspark_trunk_picks.len() > keep {
14763 self.dspark_trunk_picks.remove(0);
14764 }
14765 }
14766 for p in std::mem::take(&mut self.dspark_pending) {
14769 let Some(i) = position.checked_sub(p.0 + 1) else {
14770 continue;
14771 };
14772 let mut p = p;
14773 if i < p.1.len() {
14774 if p.2 && p.1[i] == token_id {
14775 p.3 = i + 1;
14776 } else {
14777 p.2 = false;
14778 }
14779 if i + 1 < p.1.len() {
14780 self.dspark_pending.push(p);
14781 continue;
14782 }
14783 }
14784 self.dspark_hist.push(p.3);
14785 self.dspark_real.push(token_id);
14786 }
14787 let Some(b) = &mut self.dsv4 else { return };
14788 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14789 let n_layers = layers.len();
14790 if self.dspark.is_none() {
14791 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14792 if t.is_empty() {
14793 return;
14794 }
14795 eprintln!(
14796 "DSpark: захват со слоёв {t:?}, блок {}",
14797 crate::dsv4::dspark_block()
14798 );
14799 crate::dsv4::dspark_arm(&t, cfg.dim);
14800 self.dspark = Some(crate::dsv4::DsparkState::new(
14801 self.dsv4_mtp.len(),
14802 &cfg,
14803 t.len(),
14804 ));
14805 }
14806 let ds = self.dspark.as_mut().unwrap();
14807 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14808 return; }
14810 let mut conf = Vec::new();
14811 crate::dsv4::pick_tally_arm();
14812 let draft_started = std::time::Instant::now();
14817 #[cfg(feature = "gpu")]
14818 let gpu_draft = crate::dsv4::dspark_gpu_on();
14819 #[cfg(not(feature = "gpu"))]
14820 let gpu_draft = false;
14821 let props = if gpu_draft {
14822 #[cfg(feature = "gpu")]
14823 {
14824 let kv_id = b.3.kv_id;
14825 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14826 Some(pk) => crate::dsv4::dspark_draft_gpu(
14827 g,
14828 &self.dsv4_mtp,
14829 &cfg,
14830 ds,
14831 pk,
14832 kv_id,
14833 token_id,
14834 position,
14835 self.pool.as_deref(),
14836 &mut conf,
14837 ),
14838 None => Vec::new(),
14839 }
14840 }
14841 #[cfg(not(feature = "gpu"))]
14842 Vec::new()
14843 } else {
14844 crate::gpu::cpu_scope(|| {
14845 crate::dsv4::dspark_draft(
14846 g,
14847 &self.dsv4_mtp,
14848 &cfg,
14849 ds,
14850 token_id,
14851 position,
14852 self.pool.as_deref(),
14853 &mut conf,
14854 )
14855 })
14856 };
14857 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14858 let draft_picks = crate::dsv4::pick_tally_take();
14859 crate::dsv4::dspark_freq_note(&draft_picks);
14860 crate::dsv4::pick_tally_arm();
14863 if !props.is_empty() {
14864 let (tu, tt) = {
14868 let flat: Vec<(usize, Vec<usize>)> = self
14869 .dspark_trunk_picks
14870 .iter()
14871 .flat_map(|v| v.iter().cloned())
14872 .collect();
14873 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14875 std::collections::HashMap::new();
14876 for (li, picks) in flat {
14877 per.entry(li).or_default().extend(picks);
14878 }
14879 let n = per.len().max(1);
14880 let mut u = 0usize;
14881 let mut t = 0usize;
14882 for (_, v) in per {
14883 t += v.len();
14884 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14885 }
14886 (u / n, t / n)
14887 };
14888 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14889 self.dspark_exp.push((tu, tt, du, dt));
14890 self.dspark_pending.push((position, props, true, 0));
14891 }
14892 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14893 let n = self.dspark_hist.len() as f32;
14894 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14895 let block = crate::dsv4::dspark_block();
14896 let mut at = vec![0usize; block + 1];
14897 for &k in &self.dspark_hist {
14898 at[k] += 1;
14899 }
14900 let mut surv = Vec::with_capacity(block);
14902 for i in 1..=block {
14903 let k = at[i..].iter().sum::<usize>() as f32 / n;
14904 surv.push(format!("{k:.2}"));
14905 }
14906 let distinct = self
14907 .dspark_real
14908 .iter()
14909 .collect::<std::collections::HashSet<_>>()
14910 .len();
14911 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14912 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14913 });
14914 let m = self.dspark_exp.len().max(1);
14915 eprintln!(
14916 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14917 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14918 self.dspark_hist.len(),
14919 mean + 1.0,
14920 surv.join(" ")
14921 );
14922 eprintln!(
14923 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14924 эксперты ствол {}/{} на слой за {block} токенов, \
14925 черновик {}/{} за блок, draft {:.2} мс/блок",
14926 self.dspark_real.len(),
14927 tu / m,
14928 tt / m,
14929 du / m,
14930 dt / m,
14931 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14932 );
14933 }
14934 }
14935
14936 fn forward_layers_upto(
14937 &mut self,
14938 hidden: &[f32],
14939 position: usize,
14940 task_mask: Option<&TaskMask>,
14941 upto: Option<usize>,
14942 ) -> Vec<f32> {
14943 if let Some(plan) = self.gpu_plan.clone() {
14949 if upto.is_none() && plan.len() > 1 {
14950 let mut h = hidden.to_vec();
14951 for &(dev, from, upto_incl) in plan.iter() {
14952 h = crate::gpu::with_device(dev, || {
14953 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14954 });
14955 }
14956 return h;
14957 }
14958 }
14959 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14960 }
14961
14962 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14967 self.set_gpu_plan_at(devices, None)
14968 }
14969
14970 pub fn set_gpu_plan_at(
14974 &mut self,
14975 devices: Option<&[usize]>,
14976 at: Option<usize>,
14977 ) -> Result<(), String> {
14978 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14979 self.gpu_plan = None;
14980 return Ok(());
14981 };
14982 self.split_supported()?;
14983 let n = self.num_layers;
14984 if devs.len() > n {
14985 return Err(format!("{} devices for {n} layers", devs.len()));
14986 }
14987 if let Some(k) = at {
14988 if k == 0 || k >= n {
14989 return Err(format!("split at {k}: the model has {n} layers"));
14990 }
14991 if devs.len() == 2 {
14992 self.gpu_plan = Some(std::sync::Arc::new(vec![
14993 (devs[0], 0, k - 1),
14994 (devs[1], k, n - 1),
14995 ]));
14996 return Ok(());
14997 }
14998 return Err(format!(
14999 "an explicit split point takes exactly 2 devices, got {}",
15000 devs.len()
15001 ));
15002 }
15003 let per = n.div_ceil(devs.len());
15004 let mut plan = Vec::with_capacity(devs.len());
15005 let mut from = 0usize;
15006 for &d in devs {
15007 if from >= n {
15008 break;
15009 }
15010 let upto = (from + per - 1).min(n - 1);
15011 plan.push((d, from, upto));
15012 from = upto + 1;
15013 }
15014 self.gpu_plan = Some(std::sync::Arc::new(plan));
15015 Ok(())
15016 }
15017
15018 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
15020 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
15021 }
15022
15023 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
15029 if let Some(x) = t.as_f32() {
15030 return x.to_vec();
15031 }
15032 let mut out = vec![0.0; t.rows() * t.cols()];
15033 for r in 0..t.rows() {
15034 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
15035 }
15036 out
15037 }
15038
15039 fn embryo_resident_eligible(&self) -> bool {
15040 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
15043 || self.num_layers != self.physical_layers
15044 || self.loop_final_norm
15045 || self.weights.layers.len() != self.num_layers
15046 || self.head_clusters.is_none()
15047 || self.final_softcap.is_some()
15048 || self.logit_multiplier.is_some()
15049 || self.attn_softcap != 0.0
15050 || self.mtp.is_some()
15051 || self.g3n.is_some()
15052 || self.dsv4.is_some()
15053 || self.dsv41.is_some()
15054 || self.qwen4_exp.is_some()
15055 || self.dyn_router.is_some()
15062 || self.dyn_phi_layer.is_some()
15063 || self.dyn_blend_loaded
15064 || self.o1_cfg.is_some()
15065 || self.swa.is_some()
15066 || self.sliding_layers.is_some()
15067 || self.global_attn.is_some()
15068 || self.attention_heads_per_layer.is_some()
15069 || self.attn_v_norm
15070 || self
15071 .kv_cache
15072 .layers
15073 .iter()
15074 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
15075 || self.rope_scale != 1.0
15076 || self.rope_scale_local != 1.0
15077 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
15078 || self.hidden_size == 0
15079 || self.hidden_size > 1024
15080 || self.intermediate_size > 1024
15081 || self.num_heads == 0
15082 || self.num_kv_heads == 0
15083 || self.num_heads % self.num_kv_heads != 0
15084 || self.num_heads.saturating_mul(self.head_dim) > 1024
15085 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
15086 || self.vocab_size == 0
15087 || self.kv_cache.max_seq_len == 0
15088 || self.rotary_dim == 0
15089 || self.rotary_dim > self.head_dim
15090 || self.rotary_dim % 2 != 0
15091 || self.inv_freq.len() < self.rotary_dim / 2
15092 {
15093 return false;
15094 }
15095 if self.weights.lm_head.as_f32().is_none()
15106 || self.weights.embed_tokens.as_f32().is_none()
15107 || self.weights.lm_head.rows() < self.vocab_size
15108 || self.weights.lm_head.cols() != self.hidden_size
15109 || self.weights.embed_tokens.rows() < self.vocab_size
15110 || self.weights.embed_tokens.cols() != self.hidden_size
15111 || self.weights.final_norm.len() != self.hidden_size
15112 {
15113 return false;
15114 }
15115 if let Some(cfg) = self.vmf_cfg {
15116 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
15117 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
15118 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
15119 || cfg.state_len() == 0
15120 {
15121 return false;
15122 }
15123 }
15124 if let Some(g) = self.gdn_cfg {
15125 if g.num_v_heads == 0
15130 || g.num_k_heads == 0
15131 || g.num_v_heads % g.num_k_heads != 0
15132 || g.key_head_dim == 0
15133 || g.key_head_dim > 128
15134 || g.value_head_dim == 0
15135 || g.value_head_dim > 256
15136 || g.value_head_dim % 4 != 0
15137 || g.conv_kernel == 0
15138 || g.num_v_heads > 512
15139 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
15140 || g.conv_dim() > 2048
15141 || g.conv_dim() % 4 != 0
15142 || g.hidden_size != self.hidden_size
15143 || g.output_gate_sigmoid
15144 || g.rms_eps != self.rms_eps
15145 || g.state_len() == 0
15146 {
15147 return false;
15148 }
15149 }
15150 let mut full_seen = false;
15151 for lw in &self.weights.layers {
15152 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
15153 return false;
15154 }
15155 match &lw.attn {
15156 AttnKind::LinearGdn(w) => {
15157 let Some(g) = self.gdn_cfg else {
15158 return false;
15159 };
15160 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
15161 if w.in_proj_qkv.rows() != g.conv_dim()
15162 || w.in_proj_qkv.cols() != self.hidden_size
15163 || w.in_proj_qkv.as_f32().is_none()
15164 || w.in_proj_z.rows() != nv * dv
15165 || w.in_proj_z.cols() != self.hidden_size
15166 || w.in_proj_z.as_f32().is_none()
15167 || w.in_proj_a.rows() != nv
15168 || w.in_proj_a.cols() != self.hidden_size
15169 || w.in_proj_a.as_f32().is_none()
15170 || w.in_proj_b.rows() != nv
15171 || w.in_proj_b.cols() != self.hidden_size
15172 || w.in_proj_b.as_f32().is_none()
15173 || w.conv1d.len() != g.conv_dim() * kk
15174 || w.a_log.len() != nv
15175 || w.dt_bias.len() != nv
15176 || w.norm.len() != dv
15177 || w.out_proj.rows() != self.hidden_size
15178 || w.out_proj.cols() != nv * dv
15179 || w.out_proj.as_f32().is_none()
15180 {
15181 return false;
15182 }
15183 }
15184 AttnKind::Linear(w) => {
15185 let Some(cfg) = self.vmf_cfg else {
15186 return false;
15187 };
15188 if w.thq.rows() != cfg.num_heads * cfg.nphase
15189 || w.thq.cols() != self.hidden_size
15190 || w.thq.as_f32().is_none()
15191 || w.thk.rows() != cfg.num_heads * cfg.nphase
15192 || w.thk.cols() != self.hidden_size
15193 || w.thk.as_f32().is_none()
15194 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
15195 || w.v_proj.cols() != self.hidden_size
15196 || w.v_proj.as_f32().is_none()
15197 || w.out_proj.rows() != self.hidden_size
15198 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
15199 || w.out_proj.as_f32().is_none()
15200 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
15201 {
15202 return false;
15203 }
15204 if let Some((kg, kb)) = &w.k_gate {
15205 if kg.rows() != cfg.num_heads
15206 || kg.cols() != self.hidden_size
15207 || kg.as_f32().is_none()
15208 || kb.len() != cfg.num_heads
15209 {
15210 return false;
15211 }
15212 }
15213 if let Some(conv) = &w.conv {
15214 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
15215 return false;
15216 }
15217 }
15218 }
15219 AttnKind::Full {
15220 wq,
15221 wk,
15222 wv,
15223 wo,
15224 q_norm,
15225 k_norm,
15226 output_gate,
15227 softplus_gate,
15228 bias,
15229 } => {
15230 if full_seen
15231 || q_norm.is_some()
15232 || k_norm.is_some()
15233 || *output_gate
15234 || softplus_gate.is_some()
15235 || bias.is_some()
15236 || wq.as_f32().is_none()
15237 || wk.as_f32().is_none()
15238 || wv.as_f32().is_none()
15239 || wo.as_f32().is_none()
15240 || wq.rows() != self.num_heads * self.head_dim
15241 || wk.rows() != self.num_kv_heads * self.head_dim
15242 || wv.rows() != self.num_kv_heads * self.head_dim
15243 || wq.cols() != self.hidden_size
15244 || wk.cols() != self.hidden_size
15245 || wv.cols() != self.hidden_size
15246 || wo.rows() != self.hidden_size
15247 || wo.cols() != self.num_heads * self.head_dim
15248 {
15249 return false;
15250 }
15251 full_seen = true;
15252 }
15253 AttnKind::Bounded(w) => {
15254 let Some(ac) = self.anchor_core.as_ref() else {
15257 return false;
15258 };
15259 if self.bounded_rope.is_none()
15260 || w.window != ac.window
15261 || w.sink != ac.sink
15262 || w.window == 0
15263 || w.window + w.sink > 256
15264 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15265 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15266 || w.wq.as_f32().is_none()
15267 || w.wk.as_f32().is_none()
15268 || w.wv.as_f32().is_none()
15269 || w.wo.as_f32().is_none()
15270 || w.wq.rows() != self.num_heads * self.head_dim
15271 || w.wk.rows() != self.num_kv_heads * self.head_dim
15272 || w.wv.rows() != self.num_kv_heads * self.head_dim
15273 || w.wq.cols() != self.hidden_size
15274 || w.wk.cols() != self.hidden_size
15275 || w.wv.cols() != self.hidden_size
15276 || w.wo.rows() != self.hidden_size
15277 || w.wo.cols() != self.num_heads * self.head_dim
15278 {
15279 return false;
15280 }
15281 }
15282 _ => return false,
15283 }
15284 match &lw.ffn {
15285 FfnKind::Dense(d) => {
15286 if d.act != Act::Silu
15287 || !d.segs.is_empty()
15288 || d.gate_proj.as_f32().is_none()
15289 || d.up_proj.as_f32().is_none()
15290 || d.down_proj.as_f32().is_none()
15291 || d.gate_proj.rows() != self.intermediate_size
15292 || d.gate_proj.cols() != self.hidden_size
15293 || d.up_proj.rows() != self.intermediate_size
15294 || d.up_proj.cols() != self.hidden_size
15295 || d.down_proj.rows() != self.hidden_size
15296 || d.down_proj.cols() != self.intermediate_size
15297 {
15298 return false;
15299 }
15300 }
15301 FfnKind::Moe(m) => {
15302 if m.resonance.is_none()
15303 || m.top_k != 1
15304 || m.router_sigmoid
15305 || !m.norm_topk_prob
15306 || m.expert_bias.is_some()
15307 || m.routed_scaling != 1.0
15308 || m.route_tau.is_some()
15309 || m.shared.is_none()
15310 || m.mask.is_some()
15311 || m.per_expert_scale.is_some()
15312 || m.router_input_norm
15313 || m.experts.is_empty()
15314 || m.experts.len() > 8
15315 {
15316 return false;
15317 }
15318 let r = m.resonance.as_ref().unwrap();
15319 if r.mu.len() != m.experts.len() * self.hidden_size
15320 || r.bias.len() != m.experts.len()
15321 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15322 || r.k > 128
15323 {
15324 return false;
15325 }
15326 let Some((shared, gate)) = &m.shared else {
15327 return false;
15328 };
15329 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15330 return false;
15331 }
15332 if shared.gate_proj.as_f32().is_none()
15333 || shared.up_proj.as_f32().is_none()
15334 || shared.down_proj.as_f32().is_none()
15335 || shared.gate_proj.rows() != self.intermediate_size
15336 || shared.gate_proj.cols() != self.hidden_size
15337 || shared.up_proj.rows() != self.intermediate_size
15338 || shared.up_proj.cols() != self.hidden_size
15339 || shared.down_proj.rows() != self.hidden_size
15340 || shared.down_proj.cols() != self.intermediate_size
15341 {
15342 return false;
15343 }
15344 for e in &m.experts {
15345 if e.act != Act::Silu
15346 || !e.segs.is_empty()
15347 || e.gate_proj.as_f32().is_none()
15348 || e.up_proj.as_f32().is_none()
15349 || e.down_proj.as_f32().is_none()
15350 || e.gate_proj.rows() != self.intermediate_size
15351 || e.gate_proj.cols() != self.hidden_size
15352 || e.up_proj.rows() != self.intermediate_size
15353 || e.up_proj.cols() != self.hidden_size
15354 || e.down_proj.rows() != self.hidden_size
15355 || e.down_proj.cols() != self.intermediate_size
15356 {
15357 return false;
15358 }
15359 }
15360 }
15361 FfnKind::DenseMoe(_) => return false,
15362 }
15363 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15364 return false;
15365 }
15366 }
15367 if full_seen && self.anchor_core.is_some() {
15368 return false;
15369 }
15370 full_seen || self.num_layers > 0
15371 }
15372
15373 fn embryo_resident_wanted(&self) -> bool {
15378 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15379 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15380 && matches!(
15381 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15382 Ok("1") | Ok("parallel")
15383 )
15384 && crate::gpu::enabled_here()
15385 && !self.graph_refused()
15386 && self.embryo_resident_eligible()
15387 }
15388
15389 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15397 if ids.len() < 2
15398 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15399 || !self.embryo_resident_wanted()
15400 {
15401 return None;
15402 }
15403 let model = self.ensure_embryo_graph()?;
15404 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15405 .ok()
15406 .and_then(|v| v.parse::<usize>().ok())
15407 .filter(|&v| v >= 1)
15408 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15409 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15410 let hs = self.hidden_size;
15411 let n = ids.len();
15412 let mut pos = start;
15413 let mut last = None;
15414 let mut rows = Vec::with_capacity(cmax * hs);
15415 while pos < n {
15416 let end = (pos + cmax).min(n);
15417 rows.clear();
15418 for &id in &ids[pos..end] {
15419 rows.extend_from_slice(&self.embed_single(id));
15420 }
15421 let mut lg = Vec::new();
15422 if !crate::gpu::forward_embryo_graph_chunk(
15423 &model,
15424 self.graph_kv_id,
15425 &rows,
15426 pos,
15427 end - pos,
15428 &mut lg,
15429 ) {
15430 if pos == start {
15431 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15432 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15433 }
15434 return None;
15435 }
15436 self.kv_cache.clear();
15440 self.clear_history();
15441 crate::gpu::graph_kv_reset(self.graph_kv_id);
15442 panic!(
15443 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15444 );
15445 }
15446 last = Some(lg);
15447 pos = end;
15448 }
15449 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15450 eprintln!(
15451 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15452 n - start,
15453 (n - start).div_ceil(cmax)
15454 );
15455 }
15456 last
15457 }
15458
15459 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15460 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15461 const UMAX: u32 = u32::MAX;
15462 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15463 const REC: usize = 64;
15464 struct Pack {
15465 data: Vec<f32>,
15466 }
15467 impl Pack {
15468 fn put(&mut self, x: &[f32]) -> u32 {
15469 if x.is_empty() {
15470 return u32::MAX;
15471 }
15472 let off = self.data.len();
15473 self.data.extend_from_slice(x);
15474 off as u32
15475 }
15476 }
15477 let vmf = self.vmf_cfg;
15481 let gdn = self.gdn_cfg;
15482 let mut pack = Pack { data: Vec::new() };
15483 let mut meta = vec![0u32; HEADER];
15484 meta[0] = self.hidden_size as u32;
15485 meta[1] = self.intermediate_size as u32;
15486 meta[2] = self.vocab_size as u32;
15487 meta[3] = self.num_layers as u32;
15488 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15489 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15490 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15491 if let Some(g) = gdn {
15492 meta[24] = g.num_v_heads as u32;
15493 meta[25] = g.num_k_heads as u32;
15494 meta[26] = g.key_head_dim as u32;
15495 meta[27] = g.value_head_dim as u32;
15496 meta[28] = g.conv_kernel as u32;
15497 meta[29] = g.conv_dim() as u32;
15498 }
15499 meta[7] = self.num_heads as u32;
15500 meta[8] = self.num_kv_heads as u32;
15501 meta[9] = self.head_dim as u32;
15502 meta[10] = self.kv_cache.max_seq_len as u32;
15503 let clusters = self.head_clusters.as_ref().unwrap();
15504 let cluster_count = clusters.len() / self.hidden_size;
15505 if clusters.len() % self.hidden_size != 0
15506 || cluster_count == 0
15507 || cluster_count > 1024
15508 || self.vocab_size % cluster_count != 0
15509 || self.weights.lm_head.rows() < self.vocab_size
15510 || self.weights.final_norm.len() != self.hidden_size
15511 {
15512 return None;
15513 }
15514 meta[11] = cluster_count as u32;
15515 meta[12] = (self.vocab_size / cluster_count) as u32;
15516 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15517 meta[16] = self.rotary_dim as u32;
15518 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15519 meta[19] = (self.rms_eps as f32).to_bits();
15520 let max_conv = self
15521 .weights
15522 .layers
15523 .iter()
15524 .filter_map(|lw| match &lw.attn {
15525 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
15526 _ => None,
15527 })
15528 .max()
15529 .unwrap_or(1);
15530 let phase_stride = vmf
15536 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15537 .unwrap_or(0);
15538 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15539 let state_stride = phase_stride.max(gdn_stride);
15540 let bounded = self.anchor_core.clone();
15544 let (anchor_window, anchor_sink) = bounded
15545 .as_ref()
15546 .map(|ac| (ac.window, ac.sink))
15547 .unwrap_or((0, 0));
15548 let kv_stride = if bounded.is_some() {
15549 2usize
15550 .saturating_mul(self.num_kv_heads)
15551 .saturating_mul(anchor_window)
15552 .saturating_mul(self.head_dim)
15553 } else {
15554 2usize
15555 .saturating_mul(self.num_kv_heads)
15556 .saturating_mul(self.kv_cache.max_seq_len)
15557 .saturating_mul(self.head_dim)
15558 };
15559 meta[14] = state_stride as u32;
15560 meta[15] = kv_stride as u32;
15561 meta[18] = anchor_window as u32;
15562 meta[20] = anchor_sink as u32;
15563 meta[21] = match &self.bounded_rope {
15564 Some(rope) => {
15565 let off = pack.put(&rope.cos);
15567 let _ = pack.put(&rope.sin);
15568 off
15569 }
15570 None => UMAX,
15571 };
15572 let mut full_seen = false;
15573 let mut bounded_seen = 0usize;
15574 let mut phase_seen = 0usize;
15578 let mut gdn_seen = 0usize;
15579 for (li, lw) in self.weights.layers.iter().enumerate() {
15580 let base = meta.len();
15581 meta.resize(base + REC, UMAX);
15582 meta[base] = match &lw.attn {
15583 AttnKind::Linear(w) if w.phase_delta => 1,
15584 AttnKind::Linear(_) => 0,
15585 AttnKind::Full { .. } => 2,
15586 AttnKind::Bounded(_) => 3,
15587 AttnKind::LinearGdn(_) => 4,
15588 _ => UMAX,
15589 };
15590 meta[base + 1] = pack.put(&lw.input_norm);
15591 meta[base + 2] = pack.put(&lw.post_norm);
15592 meta[base + 25] = match &lw.attn {
15593 AttnKind::Linear(_) => {
15594 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15595 phase_seen += 1;
15596 off
15597 }
15598 AttnKind::LinearGdn(_) => {
15599 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15600 gdn_seen += 1;
15601 off
15602 }
15603 _ => UMAX,
15604 };
15605 match &lw.attn {
15606 AttnKind::LinearGdn(w) => {
15607 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15610 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15611 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15612 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15613 meta[base + 60] = pack.put(&w.conv1d);
15614 meta[base + 61] = pack.put(&w.a_log);
15615 meta[base + 62] = pack.put(&w.dt_bias);
15616 meta[base + 63] = pack.put(&w.norm);
15617 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15618 meta[base + 24] = 0;
15619 }
15620 AttnKind::Linear(w) => {
15621 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15622 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15623 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15624 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15625 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15626 meta[base + 7] = pack.put(&decay);
15627 if let Some((kg, kb)) = &w.k_gate {
15628 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15629 meta[base + 9] = pack.put(kb);
15630 }
15631 if let Some(conv) = &w.conv {
15632 meta[base + 10] = pack.put(conv);
15633 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15634 } else {
15635 meta[base + 24] = 0;
15636 }
15637 }
15638 AttnKind::Full { wq, wk, wv, wo, .. } => {
15639 full_seen = true;
15640 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15641 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15642 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15643 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15644 meta[base + 26] = (li * kv_stride) as u32;
15645 }
15646 AttnKind::Bounded(w) => {
15647 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15648 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15649 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15650 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15651 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15653 meta[base + 27] = pack.put(&w.sink_k);
15654 meta[base + 28] = pack.put(&w.sink_v);
15655 bounded_seen += 1;
15656 }
15657 _ => return None,
15658 }
15659 match &lw.ffn {
15660 FfnKind::Dense(d) => {
15661 meta[base + 15] = 0;
15662 meta[base + 16] = 0;
15663 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15664 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15665 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15666 }
15667 FfnKind::Moe(m) => {
15668 let r = m.resonance.as_ref().unwrap();
15669 let (shared, _) = m.shared.as_ref().unwrap();
15670 meta[base + 15] = 1;
15671 meta[base + 16] = m.experts.len() as u32;
15672 meta[base + 17] = pack.put(&r.mu);
15673 meta[base + 18] = pack.put(&r.u);
15674 meta[base + 19] = pack.put(&r.bias);
15675 meta[base + 20] = r.k as u32;
15676 let mut shell = r.effective_shell(m.experts.len());
15685 shell.push(f32::NEG_INFINITY);
15686 meta[base + 30] = pack.put(&shell);
15687 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15688 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15689 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15690 for (e, ex) in m.experts.iter().enumerate() {
15691 meta[base + 32 + e * 3] =
15692 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15693 meta[base + 33 + e * 3] =
15694 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15695 meta[base + 34 + e * 3] =
15696 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15697 }
15698 }
15699 FfnKind::DenseMoe(_) => return None,
15700 }
15701 }
15702 if !full_seen && self.num_layers == 0 {
15703 return None;
15704 }
15705 let id = {
15706 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15707 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15708 };
15709 let model = crate::gpu::EmbryoGraphModel {
15710 id,
15711 hidden: self.hidden_size,
15712 intermediate: self.intermediate_size,
15713 vocab: self.vocab_size,
15714 layers: self.num_layers,
15715 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15716 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15717 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15718 anchor_q_heads: self.num_heads,
15719 anchor_kv_heads: self.num_kv_heads,
15720 anchor_head_dim: self.head_dim,
15721 rotary_dim: self.rotary_dim,
15722 max_seq: self.kv_cache.max_seq_len,
15723 cluster_count,
15724 cluster_size: self.vocab_size / cluster_count,
15725 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15726 state_stride,
15727 kv_stride,
15728 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15729 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15730 weights: pack.data,
15731 meta,
15732 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15733 clusters: clusters.as_ref().clone(),
15734 final_norm: self.weights.final_norm.clone(),
15735 inv_freq: self.inv_freq.as_ref().clone(),
15736 bounded: bounded.is_some(),
15737 kv_layers: if bounded.is_some() {
15738 bounded_seen
15739 } else {
15740 self.num_layers
15741 },
15742 state_layers: phase_seen + gdn_seen,
15743 anchor_window,
15744 anchor_sink,
15745 phase_layers: phase_seen,
15746 gdn_layers: gdn_seen,
15747 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15748 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15749 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15750 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15751 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15752 };
15753 self.embryo_graph = Some(std::sync::Arc::new(model));
15754 }
15755 self.embryo_graph.clone()
15756 }
15757
15758 fn forward_layers_span(
15759 &mut self,
15760 hidden: &[f32],
15761 position: usize,
15762 task_mask: Option<&TaskMask>,
15763 from: usize,
15764 upto: Option<usize>,
15765 ) -> Vec<f32> {
15766 #[cfg(target_os = "macos")]
15767 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
15768 position,
15769 from,
15770 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
15771 );
15772 debug_assert!(
15773 from == 0
15774 || (self.dsv4.is_none()
15775 && self.dsv41.is_none()
15776 && self.qwen4_exp.is_none()
15777 && self.g3n.is_none())
15778 );
15779 #[cfg(target_os = "macos")]
15785 if !crate::gpu_metal::wait_replay() {
15786 self.fail_metal_graph("the pending async replay failed before a plain forward");
15787 return vec![0.0; self.hidden_size];
15788 }
15789 if let Some(b) = &mut self.qwen4_exp {
15790 let _ = (task_mask, upto);
15791 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15792 let mut logits = Vec::new();
15793 crate::qwen4_exp::forward_token(
15794 &b.0,
15795 &b.1,
15796 &b.2,
15797 &mut b.3,
15798 token_id,
15799 position,
15800 &self.inv_freq,
15801 self.pool.as_deref(),
15802 &mut logits,
15803 true,
15804 );
15805 self.graph_logits = Some(logits);
15806 return vec![0.0; self.hidden_size];
15807 }
15808 if let Some(b) = &mut self.dsv4 {
15814 let _ = (task_mask, upto);
15815 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15816 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15817 st.pos = position;
15818 let mut logits = Vec::new();
15819 crate::dsv4::forward_token(
15820 g,
15821 layers,
15822 &cfg,
15823 st,
15824 token_id,
15825 &self.inv_freq,
15826 self.pool.as_deref(),
15827 &mut logits,
15828 );
15829 self.graph_logits = Some(logits);
15830 self.dspark_probe(position, token_id);
15831 return vec![0.0; self.hidden_size];
15834 }
15835 if let Some(b) = &mut self.dsv41 {
15837 let _ = (task_mask, upto);
15838 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15839 let mut logits = Vec::new();
15840 crate::dsv41::forward_token(
15841 &b.0,
15842 &b.1,
15843 &b.2,
15844 &mut b.3,
15845 token_id,
15846 position,
15847 self.pool.as_deref(),
15848 &mut logits,
15849 );
15850 self.graph_logits = Some(logits);
15851 return vec![0.0; self.hidden_size];
15852 }
15853 if let Some(b) = &self.g3n {
15856 let _ = (task_mask, upto);
15857 return crate::g3n::g3n_forward(
15858 &b.0,
15859 &b.1,
15860 hidden,
15861 position,
15862 &mut self.kv_cache.layers,
15863 self.num_heads,
15864 self.num_kv_heads,
15865 self.head_dim,
15866 self.pool.as_deref(),
15867 );
15868 }
15869 if from == 0
15876 && upto.is_none()
15877 && task_mask.is_none()
15878 && self.anchor_core.is_some()
15879 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15880 {
15881 static ONCE: std::sync::Once = std::sync::Once::new();
15882 ONCE.call_once(|| {
15883 eprintln!(
15884 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15885 unsupported={} eligible={}",
15886 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15887 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15888 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15889 crate::gpu::enabled_here(),
15890 self.graph_refused(),
15891 self.embryo_resident_eligible(),
15892 );
15893 });
15894 }
15895 if from == 0
15896 && upto.is_none()
15897 && task_mask.is_none()
15898 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15902 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15903 && matches!(
15907 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15908 Ok("1") | Ok("parallel")
15909 )
15910 && crate::gpu::enabled_here()
15911 && !self.graph_refused()
15912 && (position == 0 || self.device_sequence_position().is_some())
15918 && self.embryo_resident_eligible()
15919 && let Some(model) = self.ensure_embryo_graph()
15920 {
15921 let mut lg = Vec::new();
15922 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15923 {
15924 self.graph_logits = Some(lg);
15925 return vec![0.0; self.hidden_size];
15926 }
15927 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15928 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15929 }
15930 self.mark_graph_refused();
15936 if position != 0 {
15937 self.kv_cache.clear();
15941 self.clear_history();
15942 crate::gpu::graph_kv_reset(self.graph_kv_id);
15943 panic!(
15944 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15945 );
15946 }
15947 }
15948 let mut h = hidden.to_vec();
15949 self.mimo_moe_prepare();
15952 let _mimo_q8 = self
15953 .mimo_moe
15954 .is_on()
15955 .then(crate::qtensor::enter_full_gpu_q8_scope);
15956 let (nh, _nkv, _hd, hs, _rd, eps) = (
15959 self.num_heads,
15960 self.num_kv_heads,
15961 self.head_dim,
15962 self.hidden_size,
15963 self.rotary_dim,
15964 self.rms_eps,
15965 );
15966 let pool = self.pool.clone();
15967 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15979 let graph_on = match graph_env.as_deref() {
15980 Some("0") => false,
15981 Some("prefill") => false, Some(_) => true,
15983 None => crate::gpu::wgpu_graph_default(),
15989 };
15990 let graph_trusted =
15991 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15992 let race_eligible =
15993 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
15994 let mut tail_start = 0usize;
15995 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15996 let t_graph = std::time::Instant::now();
15997 let mut lg = Vec::new();
15998 let mut gl = 0usize;
15999 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
16000 let declined = built.is_none();
16001 let built = match built {
16002 Some(Ok(hh)) => Some(hh),
16003 Some(Err(())) => {
16004 self.clear_sequence_state();
16008 self.graph_failed
16009 .store(true, std::sync::atomic::Ordering::Relaxed);
16010 self.cancel
16011 .store(true, std::sync::atomic::Ordering::Relaxed);
16012 tracing::error!("token graph failed after admission; sequence state cleared");
16013 return vec![0.0; self.hidden_size];
16014 }
16015 None => None,
16016 };
16017 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
16022 self.mark_graph_refused();
16023 }
16024 graph_note(built.is_some(), gl, self.num_layers);
16025 if let Some(hh) = built {
16026 let dur = t_graph.elapsed();
16027 if std::env::var("CMF_GRAPH_PROF").is_ok() {
16028 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
16029 }
16030 if gl > 0 && gl < self.num_layers {
16031 h = hh;
16037 tail_start = gl;
16038 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
16039 if !graph_trusted {
16040 crate::gpu::graph_race_record(true, dur);
16041 }
16042 if !lg.is_empty() {
16043 lg.resize(self.vocab_size, 0.0);
16046 if let Some(c) = self.final_softcap {
16047 softcap_in_place(&mut lg, c, self.pool.as_deref());
16048 }
16049 self.graph_logits = Some(lg);
16050 }
16051 return hh;
16052 }
16053 }
16059 }
16060 let span = from > 0 || upto.is_some();
16084 if span && graph_on && task_mask.is_none() && graph_trusted {
16085 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
16086 let mut lg = Vec::new();
16087 let mut gl = 0usize;
16088 let span_res =
16089 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
16090 let span_res = match span_res {
16091 Some(Ok(hh)) => Some(hh),
16092 Some(Err(())) => {
16093 self.clear_sequence_state();
16094 self.graph_failed
16095 .store(true, std::sync::atomic::Ordering::Relaxed);
16096 self.cancel
16097 .store(true, std::sync::atomic::Ordering::Relaxed);
16098 tracing::error!(
16099 "span token graph failed after admission; sequence state cleared"
16100 );
16101 return vec![0.0; self.hidden_size];
16102 }
16103 None => None,
16104 };
16105 graph_note(span_res.is_some(), gl, upto_excl - from);
16106 if std::env::var("CMF_GPU_DEBUG").is_ok() {
16107 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
16111 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
16112 eprintln!(
16113 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
16114 upto_excl - from,
16115 span_res.is_some()
16116 );
16117 }
16118 }
16119 if let Some(hh) = span_res {
16120 if gl == upto_excl - from {
16121 if !lg.is_empty() {
16122 lg.resize(self.vocab_size, 0.0);
16123 if let Some(c) = self.final_softcap {
16124 softcap_in_place(&mut lg, c, self.pool.as_deref());
16125 }
16126 self.graph_logits = Some(lg);
16127 }
16128 crate::gpu::set_layer(-1);
16129 return hh;
16130 }
16131 h = hh;
16133 tail_start = from + gl;
16134 }
16135 }
16136 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
16141
16142 let host_tail = tail_start > from;
16152 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
16153 let automatic_gpu_prefix = self.automatic_gpu_prefix();
16154
16155 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
16156 #[cfg(target_os = "macos")]
16157 let mut gpu_skip_until = 0usize;
16158 for li in tail_start.max(from)..self.num_layers {
16159 let _capacity_tail = automatic_gpu_prefix
16160 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
16161 .map(|_| crate::gpu::enter_cpu_scope());
16162 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
16164 if li > u {
16165 break;
16166 }
16167 }
16168 if let Some(mask) = task_mask {
16169 if !mask.layer_alive(li) {
16170 continue; }
16172 }
16173 #[cfg(target_os = "macos")]
16177 {
16178 if li < gpu_skip_until {
16179 continue;
16180 }
16181 if task_mask.is_none() {
16182 let end = self.q1_graph_gpu(li, upto, position, &mut h);
16183 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
16184 return vec![0.0; self.hidden_size];
16188 }
16189 if end > li {
16190 gpu_skip_until = end;
16191 if self.is_loop_end(end - 1) && end < self.num_layers {
16194 h = inference::rms_norm(
16195 &h,
16196 &self.weights.final_norm,
16197 self.rms_eps,
16198 self.norm_style,
16199 );
16200 }
16201 continue;
16202 }
16203 }
16204 }
16205
16206 if task_mask.is_none() {
16207 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
16208 crate::gpu::BatchGraphOutcome::Completed => continue,
16209 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
16210 crate::gpu::BatchGraphOutcome::Declined => {}
16211 }
16212 }
16213 #[cfg(feature = "gpu")]
16214 self.pull_lagging_host_kv(li, li + 1, position);
16215 let lw = &self.weights.layers[self.phys_layer(li)];
16216 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
16217 if tp.parse::<usize>().ok() == Some(position) {
16218 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16219 eprintln!(
16220 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16221 h[0], h[1]
16222 );
16223 }
16224 }
16225 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16228 inference::rms_norm_into(
16229 &h,
16230 &lw.input_norm,
16231 self.rms_eps,
16232 self.norm_style,
16233 &mut self.ws.n1,
16234 );
16235 drop(prof);
16236
16237 let attn_out = match &lw.attn {
16238 AttnKind::Mla(w) => {
16239 let inv_freq_l = self.layer_inv_freq(li);
16240 let rs = self.layer_rope_scale(li);
16241 let eps = self.rms_eps;
16242 let pool = self.pool.clone();
16243 mla_attention(
16244 w,
16245 &self.ws.n1,
16246 &mut self.kv_cache.layers[li],
16247 position,
16248 &inv_freq_l,
16249 rs,
16250 eps,
16251 pool.as_deref(),
16252 )
16253 }
16254 AttnKind::Linear(w) => {
16255 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16256 vmf_phase_forward(
16257 &self.ws.n1,
16258 w,
16259 &cfg,
16260 &mut self.kv_cache.layers[li].linear_state,
16261 self.pool.as_deref(),
16262 )
16263 }
16264 AttnKind::Kda(w) => {
16265 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16266 crate::linear_core::kda_forward(
16267 &self.ws.n1,
16268 w,
16269 &cfg,
16270 &mut self.kv_cache.layers[li].linear_state,
16271 self.pool.as_deref(),
16272 )
16273 }
16274 AttnKind::LinearGdn(w) => {
16275 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16276 gdn_forward(
16277 &self.ws.n1,
16278 w,
16279 &cfg,
16280 &mut self.kv_cache.layers[li].linear_state,
16281 self.pool.as_deref(),
16282 )
16283 }
16284 AttnKind::ShortConv(w) => {
16285 let cfg = self
16286 .short_conv_cfg
16287 .expect("short-conv layer without short_conv_cfg");
16288 short_conv_forward(
16289 &self.ws.n1,
16290 w,
16291 &cfg,
16292 &mut self.kv_cache.layers[li].linear_state,
16293 self.pool.as_deref(),
16294 )
16295 }
16296 AttnKind::Bounded(w) => {
16297 let rope = self
16300 .bounded_rope
16301 .clone()
16302 .expect("bounded layer without an installed rotation table");
16303 let cfg = crate::bounded::BoundedAttnCfg {
16304 num_heads: self.num_heads,
16305 num_kv_heads: self.num_kv_heads,
16306 head_dim: self.head_dim,
16307 hidden_size: hs,
16308 scale: self.attn_scale,
16309 rope: &rope,
16310 pool: pool.as_deref(),
16311 };
16312 crate::bounded::bounded_attention(
16313 &self.ws.n1,
16314 w,
16315 &mut self.kv_cache.layers[li],
16316 &cfg,
16317 )
16318 }
16319 AttnKind::Full {
16320 wq,
16321 wk,
16322 wv,
16323 wo,
16324 q_norm,
16325 k_norm,
16326 output_gate,
16327 softplus_gate,
16328 bias,
16329 } if self.kv_cache.layers[li].o1_sealed() => {
16330 let inv_freq_l = self.layer_inv_freq(li);
16333 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16334 let cfg = QwenAttnCfg {
16335 num_heads: self.layer_num_heads(li),
16336 num_kv_heads: nkv_l,
16337 head_dim: hd_l,
16338 hidden_size: hs,
16339 position,
16340 inv_freq: &inv_freq_l,
16341 rotary_dim: rd_l,
16342 scale: self.attn_scale,
16343 softcap: self.attn_softcap,
16344 window: None,
16345 v_norm: self.attn_v_norm,
16346 qk_norm_after_rope: self.qk_norm_after_rope,
16347 gate_sigmoid: self.proj_gate_sigmoid,
16348 q_norm: q_norm.as_deref(),
16349 k_norm: k_norm.as_deref(),
16350 output_gate: *output_gate,
16351 softplus_gate: softplus_gate
16352 .as_ref()
16353 .map(|(gate, per_head)| (gate, *per_head)),
16354 rope_scale: self.layer_rope_scale(li),
16355 bias: bias
16356 .as_ref()
16357 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16358 rms_eps: eps,
16359 norm_style: self.norm_style,
16360 pool: pool.as_deref(),
16361 v_head_dim: self.layer_v_dim(li),
16362 };
16363 attention::qwen_attention_nystrom(
16364 &self.ws.n1,
16365 wq,
16366 wk,
16367 wv,
16368 wo,
16369 &mut self.kv_cache.layers[li],
16370 &cfg,
16371 )
16372 }
16373 AttnKind::Full {
16374 wq,
16375 wk,
16376 wv,
16377 wo,
16378 q_norm,
16379 k_norm,
16380 output_gate,
16381 softplus_gate,
16382 bias,
16383 } => 'attn: {
16384 let dropin_reason =
16389 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16390 if let Some(reason) = dropin_reason {
16391 self.note_graph_decline("wgpu attn dropin", reason);
16392 }
16393 if graph_on
16394 && dropin_reason.is_none()
16395 && !*output_gate
16396 && softplus_gate.is_none()
16397 && self.attention_heads_per_layer.is_none()
16398 && bias.is_none()
16399 && task_mask.is_none()
16400 {
16401 let inv_freq_l = self.layer_inv_freq(li);
16402 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16403 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16404 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16405 wq.mapped_q1(),
16406 wk.mapped_q1(),
16407 wv.mapped_q1(),
16408 wo.mapped_q1(),
16409 ) {
16410 let gm = gm.clone();
16411 let mut out = vec![0f32; hs];
16412 let cache = &self.kv_cache.layers[li];
16413 if crate::gpu::attn_dropin(
16414 &gm,
16415 self.graph_kv_id,
16416 li,
16417 &self.ws.n1,
16418 qi,
16419 ki,
16420 vi,
16421 oi,
16422 q_norm.as_deref(),
16423 k_norm.as_deref(),
16424 self.qk_norm_after_rope,
16425 &inv_freq_l,
16426 nh,
16427 nkv_l,
16428 hd_l,
16429 rd_l,
16430 hs,
16431 position,
16432 self.kv_cache.max_seq_len,
16433 gemma,
16434 eps as f32,
16435 cache.k_heads(),
16436 cache.v_heads(),
16437 &mut out,
16438 ) {
16439 break 'attn out;
16440 }
16441 }
16442 }
16443 let masked = task_mask
16444 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16445 .unwrap_or(false);
16446 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16447 let plain = self.layer_attn_plain(li);
16450 match (masked, f32_view) {
16451 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16454 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16455 attention::multi_head_attention(
16456 &self.ws.n1,
16457 q,
16458 k,
16459 v,
16460 o,
16461 &mut self.kv_cache.layers[li],
16462 self.num_heads,
16463 self.num_kv_heads,
16464 self.head_dim,
16465 self.hidden_size,
16466 position,
16467 &active_heads,
16468 &self.inv_freq,
16469 )
16470 }
16471 (masked, _) => {
16472 if masked {
16473 tracing::warn!(
16474 "layer {li}: head mask on quantized weights or on a \
16475 window/sink/per-layer-geometry layer not supported \
16476 yet — executing dense"
16477 );
16478 }
16479 let inv_freq_l = self.layer_inv_freq(li);
16480 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16481 let cfg = QwenAttnCfg {
16482 num_heads: self.layer_num_heads(li),
16483 num_kv_heads: nkv_l,
16484 head_dim: hd_l,
16485 hidden_size: hs,
16486 position,
16487 inv_freq: &inv_freq_l,
16488 rotary_dim: rd_l,
16489 scale: self.attn_scale,
16490 softcap: self.attn_softcap,
16491 window: self.layer_window(li),
16492 v_norm: self.attn_v_norm,
16493 qk_norm_after_rope: self.qk_norm_after_rope,
16494 gate_sigmoid: self.proj_gate_sigmoid,
16495 q_norm: q_norm.as_deref(),
16496 k_norm: k_norm.as_deref(),
16497 output_gate: *output_gate,
16498 softplus_gate: softplus_gate
16499 .as_ref()
16500 .map(|(gate, per_head)| (gate, *per_head)),
16501 rope_scale: self.layer_rope_scale(li),
16502 bias: bias
16503 .as_ref()
16504 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16505 rms_eps: eps,
16506 norm_style: self.norm_style,
16507 pool: pool.as_deref(),
16508 v_head_dim: self.layer_v_dim(li),
16509 };
16510 attention::qwen_attention(
16511 &self.ws.n1,
16512 wq,
16513 wk,
16514 wv,
16515 wo,
16516 &mut self.kv_cache.layers[li],
16517 &cfg,
16518 )
16519 }
16520 }
16521 }
16522 };
16523 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
16526 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
16527 None => attn_out,
16528 };
16529 let lw = &self.weights.layers[self.phys_layer(li)];
16530 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16531 inference::add_rmsnorm_fused_into(
16532 &mut h,
16533 &attn_out,
16534 &lw.post_norm,
16535 self.rms_eps,
16536 self.norm_style,
16537 &mut self.ws.p1,
16538 );
16539 drop(prof);
16540 let mut attn_out = attn_out;
16541 attention::recycle_buf(&mut attn_out);
16542 let post_normed = &self.ws.p1;
16543
16544 let ffn_masked = task_mask
16545 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16546 .unwrap_or(false);
16547 let ffn_out = match (ffn_masked, &lw.ffn) {
16559 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16563 let row = task_mask
16564 .and_then(|tm| tm.ffn_masks.get(li))
16565 .map(|v| v.as_slice());
16566 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16567 }
16568 (true, FfnKind::Dense(d)) => {
16569 let tm = task_mask.unwrap();
16570 let alive = tm.ffn_active_count(li);
16571 let deep = alive * 2 <= self.intermediate_size;
16572 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16573 let active = tm.ffn_active_indices(li);
16574 sparse_ffn_quant(
16575 d,
16576 post_normed,
16577 &active,
16578 self.hidden_size,
16579 self.pool.as_deref(),
16580 )
16581 } else if deep
16582 && let (Some(g), Some(u), Some(dn)) = (
16583 d.gate_proj.as_f32(),
16584 d.up_proj.as_f32(),
16585 d.down_proj.as_f32(),
16586 )
16587 {
16588 let active = tm.ffn_active_indices(li);
16589 inference::sparse_ffn_forward(
16590 post_normed,
16591 g,
16592 u,
16593 dn,
16594 self.hidden_size,
16595 self.intermediate_size,
16596 &active,
16597 self.pool.as_deref(),
16598 )
16599 } else {
16600 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16601 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16602 }
16603 }
16604 (true, FfnKind::Moe(m)) => {
16605 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16609 ffn_forward(
16610 &lw.ffn,
16611 post_normed,
16612 self.pool.as_deref(),
16613 allowed.as_deref(),
16614 )
16615 }
16616 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16617 dm,
16618 post_normed,
16619 &h,
16620 self.rms_eps,
16621 self.norm_style,
16622 self.pool.as_deref(),
16623 ),
16624 (false, _) => match &lw.ffn {
16625 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16626 dm,
16627 post_normed,
16628 &h,
16629 self.rms_eps,
16630 self.norm_style,
16631 self.pool.as_deref(),
16632 ),
16633 FfnKind::Moe(m)
16634 if task_mask.is_none()
16635 && host_tail
16636 && li >= tail_start
16637 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
16638 {
16639 moe_ffn_minicpm_tail(
16640 &mut self.minicpm_tail,
16641 li,
16642 self.num_layers,
16643 m,
16644 post_normed,
16645 self.pool.as_deref(),
16646 )
16647 }
16648 FfnKind::Moe(m)
16649 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16650 {
16651 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16652 }
16653 _ => {
16654 let allowed = match (&lw.ffn, task_mask) {
16655 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16656 _ => None,
16657 };
16658 ffn_forward(
16659 &lw.ffn,
16660 post_normed,
16661 self.pool.as_deref(),
16662 allowed.as_deref(),
16663 )
16664 }
16665 },
16666 };
16667 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16668 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16669 None => ffn_out,
16670 };
16671 for (i, &f) in ffn_out.iter().enumerate() {
16672 h[i] += f;
16673 }
16674 let mut ffn_out = ffn_out;
16675 attention::recycle_buf(&mut ffn_out);
16676
16677 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16679 for v in h.iter_mut() {
16680 *v *= sc;
16681 }
16682 }
16683 if self.layer_dump.is_some() {
16685 self.dump_layer_row(position, li, &h);
16686 }
16687
16688 if self.is_loop_end(li) && li + 1 < self.num_layers {
16691 h = inference::rms_norm(
16692 &h,
16693 &self.weights.final_norm,
16694 self.rms_eps,
16695 self.norm_style,
16696 );
16697 }
16698
16699 if self.dyn_phi_layer == Some(li) {
16703 self.update_dyn_phi(&h);
16704 }
16705 }
16706 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16708 crate::gpu::graph_race_record(false, t.elapsed());
16709 }
16710
16711 h
16712 }
16713
16714 fn update_dyn_phi(&mut self, h: &[f32]) {
16717 const A: f32 = 0.2;
16718 if self.dyn_phi_ema.len() != h.len() {
16719 self.dyn_phi_ema = vec![0.0; h.len()];
16720 self.dyn_phi_seen = 0;
16721 }
16722 if self.dyn_phi_seen == 0 {
16723 self.dyn_phi_ema.copy_from_slice(h);
16724 } else {
16725 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16726 *e = (1.0 - A) * *e + A * v;
16727 }
16728 }
16729 self.dyn_phi_seen += 1;
16730 }
16731
16732 pub fn dyn_phi(&self) -> &[f32] {
16734 &self.dyn_phi_ema
16735 }
16736
16737 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16739 self.dyn_phi_layer = layer;
16740 self.dyn_phi_ema.clear();
16741 self.dyn_phi_seen = 0;
16742 }
16743
16744 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16746 let Some(model) = &self.model else {
16747 return Vec::new();
16748 };
16749 model
16750 .header
16751 .skills
16752 .iter()
16753 .enumerate()
16754 .filter_map(|(i, sk)| {
16755 if sk.is_v2() {
16759 return None;
16760 }
16761 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16762 let sel = sk.selection.as_ref()?;
16763 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16764 })
16765 .collect()
16766 }
16767
16768 pub fn active_skill(&self) -> Option<usize> {
16770 self.dyn_active
16771 }
16772
16773 pub fn enable_dynamic_routing(&mut self) -> usize {
16778 use crate::swarm::{DynRouter, RoutableSkill};
16779 let Some(model) = self.model.clone() else {
16780 return 0;
16781 };
16782 if let Some(r) = &model.header.router {
16789 tracing::warn!(
16790 "dynamic routing disabled: this file declares router policy '{}' with \
16791 granularity \"{}\" — the request-level decision applies instead",
16792 r.policy,
16793 r.granularity
16794 );
16795 return 0;
16796 }
16797 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16803 || model.header.skills.iter().any(|s| s.is_v2())
16804 {
16805 tracing::warn!(
16806 "dynamic routing disabled: this file carries format-v2 skill records \
16807 (SKILLS_V2) — they route per request through a router policy only"
16808 );
16809 return 0;
16810 }
16811 if self.dyn_blend_loaded {
16814 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16815 return 0;
16816 }
16817 if let Some(a) = self.dyn_active {
16821 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16822 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16823 return 0;
16824 }
16825 }
16826 let hidden = self.hidden_size;
16827 let mut skills = Vec::new();
16828 for (idx, id, _phi) in self.dynamic_skills() {
16829 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16830 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16831 skills.push(rs);
16832 }
16833 }
16834 }
16835 if skills.is_empty() {
16836 return 0;
16837 }
16838 let phi = skills[0].phi_layer;
16840 if skills.iter().any(|s| s.phi_layer != phi) {
16841 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16842 }
16843 let n = skills.len();
16844 self.set_dyn_phi_layer(Some(phi));
16845 self.dyn_router = Some(DynRouter::new(skills));
16846 n
16847 }
16848
16849 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16851 self.dyn_router
16852 .as_ref()
16853 .map(|r| r.switches.clone())
16854 .unwrap_or_default()
16855 }
16856
16857 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16860 let _mimo_q8 = self
16861 .mimo_moe
16862 .is_on()
16863 .then(crate::qtensor::enter_full_gpu_q8_scope);
16864 let rows = self.weights.lm_head.rows();
16865 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16866 let served = self.mimo_moe.is_on()
16870 && crate::gpu::mimo_q8_short_enabled()
16871 && rows == self.vocab_size
16872 && !self.weights.lm_head.has_prism_contract()
16873 && self
16874 .weights
16875 .lm_head
16876 .graph_weight()
16877 .is_some_and(|(model, idx, kind, _)| {
16878 kind == 7
16879 && crate::gpu::q82_short_rows(
16880 model,
16881 idx,
16882 hidden,
16883 1,
16884 rows,
16885 self.hidden_size,
16886 &mut logits,
16887 )
16888 });
16889 if !served {
16890 self.weights
16891 .lm_head
16892 .matvec(hidden, &mut logits, self.pool.as_deref());
16893 }
16894 logits.resize(self.vocab_size, 0.0);
16895 if let Some(m) = self.logit_multiplier {
16896 for l in logits.iter_mut() {
16897 *l *= m;
16898 }
16899 }
16900 if let Some(c) = self.final_softcap {
16901 softcap_in_place(&mut logits, c, self.pool.as_deref());
16902 }
16903 if let Some(cm) = self.head_clusters.as_ref() {
16904 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16905 }
16906 logits
16907 }
16908
16909 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16912 let h = hidden.len();
16913 let ncl = cm.len() / h.max(1);
16914 if ncl == 0 || logits.len() % ncl != 0 {
16915 return;
16916 }
16917 let cs = logits.len() / ncl;
16918 let mut lc = vec![0.0f32; ncl];
16920 for c in 0..ncl {
16921 let row = &cm[c * h..(c + 1) * h];
16922 let mut s = 0.0f32;
16923 for j in 0..h {
16924 s += row[j] * hidden[j];
16925 }
16926 lc[c] = s;
16927 }
16928 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16929 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16930 for c in 0..ncl {
16931 let blk = &mut logits[c * cs..(c + 1) * cs];
16932 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16933 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16934 let add = lc[c] - lse - bl;
16935 for v in blk.iter_mut() {
16936 *v += add;
16937 }
16938 }
16939 }
16940
16941 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16946 #[cfg(target_os = "macos")]
16947 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16948 self.clear_sequence_state();
16949 crate::gpu::graph_race_begin_generation();
16953 if task_mask.is_none() {
16954 self.o1_begin();
16955 }
16956 let mut hidden = vec![0.0f32; self.hidden_size];
16957 for (pos, &id) in ids.iter().enumerate() {
16958 let emb = self.embed_single(id);
16959 hidden = self.forward_layers(&emb, pos, task_mask);
16960 }
16961 if let Err(err) = self.o1_seal_checked() {
16962 self.o1_fail(err);
16963 }
16964 if let Some(logits) = self.graph_logits.take() {
16967 return logits;
16968 }
16969 inference::rms_norm_into(
16970 &hidden,
16971 &self.weights.final_norm,
16972 self.rms_eps,
16973 self.norm_style,
16974 &mut self.ws.n1,
16975 );
16976 self.lm_head_forward(&self.ws.n1)
16977 }
16978}
16979
16980pub fn create_test_pipeline(
16982 hidden_size: usize,
16983 intermediate_size: usize,
16984 num_heads: usize,
16985 num_kv_heads: usize,
16986 head_dim: usize,
16987 num_layers: usize,
16988 vocab_size: usize,
16989) -> Pipeline {
16990 let synth = |n: usize, salt: usize| -> Vec<f32> {
16993 (0..n)
16994 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16995 .collect()
16996 };
16997 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16998 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16999 };
17000 let layer_weights: Vec<LayerWeights> = (0..num_layers)
17001 .map(|li| LayerWeights {
17002 input_norm: vec![1.0; hidden_size],
17003 post_norm: vec![1.0; hidden_size],
17004 attn_out_norm: None,
17005 ffn_out_norm: None,
17006 layer_scale: None,
17007 ffn: FfnKind::Dense(DenseFfn {
17008 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
17009 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
17010 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
17011 act: Act::Silu,
17012 down_t: None,
17013 segs: Vec::new(),
17014 }),
17015 attn: AttnKind::Full {
17016 bias: None,
17017 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
17018 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
17019 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
17020 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
17021 q_norm: None,
17022 k_norm: None,
17023 output_gate: false,
17024 softplus_gate: None,
17025 },
17026 })
17027 .collect();
17028
17029 Pipeline::new(
17030 Tokenizer::byte_level(),
17031 PipelineWeights {
17032 embed_tokens: qt(vocab_size, hidden_size, 100),
17033 layers: layer_weights,
17034 lm_head: qt(vocab_size, hidden_size, 200),
17035 final_norm: vec![1.0; hidden_size],
17036 },
17037 hidden_size,
17038 intermediate_size,
17039 num_heads,
17040 num_kv_heads,
17041 head_dim,
17042 num_layers,
17043 num_layers, false, vocab_size,
17046 1e-6,
17047 10_000.0,
17048 NormStyle::Qwen,
17049 4096,
17050 SamplerConfig {
17051 seed: Some(42),
17052 ..Default::default()
17053 },
17054 )
17055}
17056
17057#[inline]
17062fn mask_bit(row: &[u8], j: usize) -> bool {
17063 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
17064}
17065
17066fn mask_gain() -> f32 {
17077 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17078 *G.get_or_init(|| {
17079 std::env::var("CMF_FFN_MASK_GAIN")
17080 .ok()
17081 .and_then(|v| v.parse().ok())
17082 .unwrap_or(1.0)
17083 })
17084}
17085
17086fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
17087 let fill = meanfill().and_then(|(i, v)| {
17090 let li = crate::gpu::cur_layer();
17091 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
17092 });
17093 for r in 0..rows {
17094 let base = r * inter;
17095 for (bi, &byte) in row.iter().enumerate() {
17096 if byte == 0xFF {
17097 continue;
17098 }
17099 let j0 = bi * 8;
17100 for bit in 0..8 {
17101 let j = j0 + bit;
17102 if j < inter && byte & (1 << bit) == 0 {
17103 g[base + j] = fill.map_or(0.0, |f| f[j]);
17104 }
17105 }
17106 }
17107 }
17108 let gain = mask_gain();
17109 if gain != 1.0 {
17110 for v in g[..rows * inter].iter_mut() {
17111 *v *= gain;
17112 }
17113 }
17114}
17115
17116#[inline]
17118fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
17119 row.is_none_or(|r| mask_bit(r, i))
17120}
17121
17122fn all_bits_on(row: &[u8], n: usize) -> bool {
17125 (0..n).all(|i| mask_bit(row, i))
17126}
17127
17128fn tube_topk() -> usize {
17136 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17137 *K.get_or_init(|| {
17138 std::env::var("CMF_TUBE_TOPK")
17139 .ok()
17140 .and_then(|v| v.parse().ok())
17141 .unwrap_or(0)
17142 })
17143}
17144
17145fn tube_score_oracle() -> bool {
17146 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17147 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
17148}
17149
17150fn tube_ffn_routed(
17157 d: &DenseFfn,
17158 xs: &[f32],
17159 b: usize,
17160 pool: Option<&Pool>,
17161 mask_row: Option<&[u8]>,
17162 k: usize,
17163) -> Vec<f32> {
17164 let hidden = d.down_proj.rows();
17165 let core = d.gate_proj.rows();
17166 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17167 let mut out = match (b, core_full, mask_row) {
17168 (1, true, _) => dense_ffn(d, xs, pool),
17169 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17170 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17171 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17172 };
17173 let cand: Vec<usize> = (0..d.segs.len())
17174 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
17175 .collect();
17176 if cand.is_empty() {
17177 return out;
17178 }
17179 let oracle = tube_score_oracle();
17183 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
17184 let mut scores = vec![0f32; b * cand.len()];
17185 for (ci, &i) in cand.iter().enumerate() {
17186 let seg = &d.segs[i];
17187 let w = seg.width;
17188 let mut g = vec![0.0f32; b * w];
17189 if b == 1 {
17190 seg.gate.matvec(xs, &mut g, pool);
17191 } else {
17192 seg.gate.matmat(xs, b, &mut g, pool);
17193 }
17194 for v in g.iter_mut() {
17195 *v = Act::Silu.combine(*v, 1.0);
17196 }
17197 if !oracle {
17198 for t in 0..b {
17199 scores[t * cand.len() + ci] =
17200 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17201 }
17202 }
17203 if oracle || b > 1 {
17204 let mut u = vec![0.0f32; b * w];
17205 if b == 1 {
17206 seg.up.matvec(xs, &mut u, pool);
17207 } else {
17208 seg.up.matmat(xs, b, &mut u, pool);
17209 }
17210 for (a, &v) in g.iter_mut().zip(u.iter()) {
17211 *a *= v;
17212 }
17213 if oracle {
17214 for t in 0..b {
17215 scores[t * cand.len() + ci] =
17216 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17217 }
17218 }
17219 }
17220 acts.push(g);
17221 }
17222 let keep = k.min(cand.len());
17224 let mut scratch: Vec<f32> = Vec::new();
17225 for t in 0..b {
17226 let mut sc: Vec<(f32, usize)> = (0..cand.len())
17227 .map(|ci| (scores[t * cand.len() + ci], ci))
17228 .collect();
17229 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
17230 let mut alive = vec![false; cand.len()];
17231 for &(_, ci) in sc.iter().take(keep) {
17232 alive[ci] = true;
17233 }
17234 if b > 1 {
17235 for (ci, a) in acts.iter_mut().enumerate() {
17236 if !alive[ci] {
17237 let w = d.segs[cand[ci]].width;
17238 a[t * w..(t + 1) * w].fill(0.0);
17239 }
17240 }
17241 } else {
17242 for (ci, &i) in cand.iter().enumerate() {
17246 if !alive[ci] {
17247 continue;
17248 }
17249 let seg = &d.segs[i];
17250 let w = seg.width;
17251 let g = &mut acts[ci];
17252 if !tube_score_oracle() {
17253 scratch.clear();
17254 scratch.resize(w, 0.0);
17255 seg.up.matvec(xs, &mut scratch, pool);
17256 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17257 *a *= v;
17258 }
17259 }
17260 let mut acc = vec![0.0f32; hidden];
17261 seg.down.matvec(g, &mut acc, pool);
17262 for (o, a) in out.iter_mut().zip(&acc) {
17263 *o += *a;
17264 }
17265 }
17266 }
17267 }
17268 if b > 1 {
17269 for (ci, &i) in cand.iter().enumerate() {
17270 let seg = &d.segs[i];
17271 let mut acc = vec![0.0f32; b * hidden];
17272 seg.down.matmat(&acts[ci], b, &mut acc, pool);
17273 for (o, a) in out.iter_mut().zip(&acc) {
17274 *o += *a;
17275 }
17276 }
17277 }
17278 out
17279}
17280
17281fn tube_ffn(
17287 d: &DenseFfn,
17288 xs: &[f32],
17289 b: usize,
17290 pool: Option<&Pool>,
17291 mask_row: Option<&[u8]>,
17292) -> Vec<f32> {
17293 if tube_topk() > 0 {
17294 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17295 }
17296 let hidden = d.down_proj.rows();
17297 let core = d.gate_proj.rows();
17298 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17299 let mut out = match (b, core_full, mask_row) {
17300 (1, true, _) => dense_ffn(d, xs, pool),
17301 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17302 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17303 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17304 };
17305 TUBE_SCRATCH.with(|sc| {
17306 let mut sc = sc.borrow_mut();
17307 let [g, u, acc] = &mut *sc;
17308 for seg in &d.segs {
17309 if !tube_bit(mask_row, seg.start) {
17310 continue;
17311 }
17312 let w = seg.width;
17313 g.resize(b * w, 0.0);
17314 if b == 1
17315 && d.act == Act::Silu
17316 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17317 {
17318 } else {
17320 u.resize(b * w, 0.0);
17321 if b == 1 {
17322 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17323 } else {
17324 seg.gate.matmat(xs, b, g, pool);
17325 seg.up.matmat(xs, b, u, pool);
17326 }
17327 for i in 0..b * w {
17328 g[i] = d.act.combine(g[i], u[i]);
17329 }
17330 }
17331 acc.resize(b * hidden, 0.0);
17332 acc.fill(0.0);
17333 if b == 1 {
17334 seg.down.matvec(g, acc, pool);
17335 } else {
17336 seg.down.matmat(g, b, acc, pool);
17337 }
17338 for (o, a) in out.iter_mut().zip(acc.iter()) {
17339 *o += *a;
17340 }
17341 }
17342 out
17343 })
17344}
17345
17346thread_local! {
17347 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17351 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17352}
17353
17354static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
17357 std::sync::atomic::AtomicU64::new(0),
17358 std::sync::atomic::AtomicU64::new(0),
17359];
17360
17361fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17365 #[cfg(feature = "gpu")]
17366 {
17367 use std::sync::atomic::Ordering::Relaxed;
17368 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17369 (
17370 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17371 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17372 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17373 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17374 )
17375 }
17376 #[cfg(not(feature = "gpu"))]
17377 (0.0, 0.0, 0.0, 0)
17378}
17379
17380fn wgpu_mirror_counters() -> [u64; 6] {
17383 #[cfg(feature = "gpu")]
17384 {
17385 crate::gpu_wgpu::MIRROR_EVENTS
17386 .each_ref()
17387 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17388 }
17389 #[cfg(not(feature = "gpu"))]
17390 [0; 6]
17391}
17392
17393fn prefill_prof_on() -> bool {
17394 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17395 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17396}
17397
17398fn dense_ffn_batch(
17399 d: &DenseFfn,
17400 xs: &[f32],
17401 b: usize,
17402 pool: Option<&Pool>,
17403 mask_row: Option<&[u8]>,
17404) -> Vec<f32> {
17405 let inter = d.gate_proj.rows();
17406 let hidden = d.down_proj.rows();
17407 let fused_act = d.act.graph_act();
17417 if mask_row.is_none()
17418 && fused_act.is_some()
17419 && b >= 32
17420 && crate::gpu::enabled_here()
17421 && !crate::gpu::mm_killed()
17422 && refit_dir().is_none()
17427 && !ffn_probe_active()
17432 && !crate::gptq_capture::capturing()
17436 {
17437 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17438 d.gate_proj.mapped_q4t(),
17439 d.up_proj.mapped_q4t(),
17440 d.down_proj.mapped_q4t(),
17441 ) {
17442 let mut out = vec![0.0f32; b * hidden];
17443 let act = fused_act.expect("checked above");
17444 if crate::gpu::q4_ffn_act(
17445 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
17446 ) {
17447 return out;
17448 }
17449 }
17450 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17455 d.gate_proj.mapped_q4tp(),
17456 d.up_proj.mapped_q4tp(),
17457 d.down_proj.mapped_q4tp(),
17458 ) {
17459 let mut out = vec![0.0f32; b * hidden];
17460 let act = fused_act.expect("checked above");
17461 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17462 {
17463 return out;
17464 }
17465 }
17466 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17472 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17473 d.gate_proj.mapped_device_gemm(),
17474 d.up_proj.mapped_device_gemm(),
17475 d.down_proj.mapped_device_gemm(),
17476 ) {
17477 let mut out = vec![0.0f32; b * hidden];
17478 let act = fused_act.expect("checked above");
17479 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17480 return out;
17481 }
17482 }
17483 }
17484 let mut g = vec![0.0f32; b * inter];
17485 d.gate_proj.matmat(xs, b, &mut g, pool);
17486 let mut u = vec![0.0f32; b * inter];
17487 d.up_proj.matmat(xs, b, &mut u, pool);
17488 if gate_topk() > 0 && d.act == Act::Silu {
17489 for t in 0..b {
17490 let row = &mut g[t * inter..(t + 1) * inter];
17491 for v in row.iter_mut() {
17492 *v = Act::Silu.combine(*v, 1.0);
17493 }
17494 keep_top_k(row, gate_topk());
17495 }
17496 for i in 0..b * inter {
17497 g[i] *= u[i];
17498 }
17499 } else {
17500 for i in 0..b * inter {
17501 g[i] = d.act.combine(g[i], u[i]);
17502 }
17503 }
17504 if let Some(row) = mask_row {
17505 zero_masked_cols(&mut g, b, inter, row);
17506 }
17507 if oracle_topk() > 0 {
17508 for t in 0..b {
17509 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
17510 }
17511 }
17512 let mut out = vec![0.0f32; b * hidden];
17513 d.down_proj.matmat(&g, b, &mut out, pool);
17514 if refit_dir().is_some() {
17515 let li = crate::gpu::cur_layer();
17516 if li >= 0 {
17517 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
17518 }
17519 }
17520 FFN_PROBE.with(|pr| {
17524 if let Some(acc) = pr.borrow_mut().as_mut() {
17525 let li = crate::gpu::cur_layer();
17526 if li < 0 {
17527 return;
17528 }
17529 let Some(row) = acc.get_mut(li as usize) else {
17530 return;
17531 };
17532 let sq = probe_sq();
17533 for t in 0..b {
17534 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
17535 *a += if sq {
17536 (v as f64) * (v as f64)
17537 } else {
17538 (v as f64).abs()
17539 };
17540 }
17541 }
17542 }
17543 });
17544 out
17545}
17546
17547fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
17552 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17553 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17554 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
17555 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
17556 if (!on && !dump) || b == 0 {
17557 return;
17558 }
17559 let hidden = xs.len() / b;
17560 if on {
17561 let mut acc = m.act_sq.borrow_mut();
17562 if acc.len() < hidden {
17563 acc.resize(hidden, 0.0);
17564 }
17565 for t in 0..b {
17566 let row = &xs[t * hidden..(t + 1) * hidden];
17567 for (a, &v) in acc.iter_mut().zip(row) {
17568 *a += (v as f64) * (v as f64);
17569 }
17570 }
17571 }
17572 if dump {
17573 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
17576 .ok()
17577 .and_then(|v| v.parse().ok())
17578 .unwrap_or(4096);
17579 let mut rows = m.act_rows.borrow_mut();
17580 if rows.len() < cap * hidden {
17581 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17582 rows.extend_from_slice(&xs[..take * hidden]);
17583 }
17584 }
17585}
17586
17587#[derive(Clone, Copy)]
17590struct SendVecs(*mut Vec<f32>);
17591unsafe impl Send for SendVecs {}
17592unsafe impl Sync for SendVecs {}
17593impl SendVecs {
17594 #[inline]
17595 fn at(self, i: usize) -> *mut Vec<f32> {
17596 unsafe { self.0.add(i) }
17597 }
17598}
17599
17600fn moe_ffn_batch(
17601 m: &MoeFfn,
17602 xs: &[f32],
17603 b: usize,
17604 hidden: usize,
17605 pool: Option<&Pool>,
17606 allowed: Option<&[bool]>,
17607) -> Vec<f32> {
17608 accumulate_act(m, xs, b);
17609 let ne = m.experts.len();
17610 let mut logits = vec![0.0f32; b * ne];
17611 match &m.resonance {
17612 Some(r) => {
17613 let hdim = xs.len() / b.max(1);
17614 for bi in 0..b {
17615 r.scores(
17616 &xs[bi * hdim..(bi + 1) * hdim],
17617 &mut logits[bi * ne..(bi + 1) * ne],
17618 );
17619 }
17620 }
17621 None => m.router.matmat(xs, b, &mut logits, pool),
17622 }
17623
17624 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17627 {
17628 let mut st = m.stats.borrow_mut();
17629 if st.len() < ne {
17630 st.resize(ne, 0);
17631 }
17632 for bi in 0..b {
17633 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17634 for &e in &idx {
17635 st[e] += 1;
17636 assign[e].push((bi, p[e] / wsum));
17637 }
17638 }
17639 }
17640
17641 let mut out = vec![0.0f32; b * hidden];
17642 let cols = m.experts[0].gate_proj.cols();
17643 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17644 let sb = list.len();
17645 let mut sub = vec![0.0f32; sb * cols];
17646 for (k, &(bi, _)) in list.iter().enumerate() {
17647 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17648 }
17649 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17650 for (k, &(bi, w)) in list.iter().enumerate() {
17651 for i in 0..hidden {
17652 out[bi * hidden + i] += w * eo[k * hidden + i];
17653 }
17654 }
17655 };
17656 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17662 if pool.is_some() && active.len() >= 8 {
17663 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17664 {
17665 let panel_ptr = SendVecs(panels.as_mut_ptr());
17666 let experts = &m.experts;
17669 let (active_r, assign_r) = (&active, &assign);
17670 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17671 let run = |start: usize, end: usize| {
17672 let _cpu_scope = inherit_cpu();
17673 #[cfg(target_os = "macos")]
17681 let _metal_cpu = crate::gpu::enter_cpu_scope();
17682 for ai in start..end {
17683 let e = active_r[ai];
17684 let list = &assign_r[e];
17685 let sb = list.len();
17686 let mut sub = vec![0.0f32; sb * cols];
17687 for (k, &(bi, _)) in list.iter().enumerate() {
17688 sub[k * cols..(k + 1) * cols]
17689 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17690 }
17691 unsafe {
17693 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17694 }
17695 }
17696 };
17697 match pool {
17698 Some(p) => p.run_rows(active.len(), &run),
17699 None => run(0, active.len()),
17700 }
17701 }
17702 for (ai, &e) in active.iter().enumerate() {
17703 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17704 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17705 for i in 0..hidden {
17706 out[bi * hidden + i] += w * eo[i];
17707 }
17708 }
17709 }
17710 } else {
17711 for &e in &active {
17712 run_expert(&m.experts[e], &assign[e], &mut out);
17713 }
17714 }
17715 if let Some((se, gate)) = &m.shared {
17716 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17717 let mut gl = vec![0.0f32; b];
17718 gate.matmat(xs, b, &mut gl, pool);
17719 (0..b)
17720 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17721 .collect()
17722 } else {
17723 (0..b).map(|bi| (bi, 1.0)).collect()
17724 };
17725 run_expert(se, &all, &mut out);
17726 }
17727 out
17728}
17729
17730fn moe_ffn_rows_exact(
17742 m: &MoeFfn,
17743 xs: &[f32],
17744 b: usize,
17745 hidden: usize,
17746 pool: Option<&Pool>,
17747) -> Vec<f32> {
17748 let mut out = vec![0.0f32; b * hidden];
17749 let per_row = |out: &mut [f32]| {
17750 for r in 0..b {
17751 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17752 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17753 }
17754 };
17755 let covered = !crate::gpu::enabled_here()
17756 && moe_batch_enabled()
17757 && m.shared.is_none()
17758 && m.resonance.is_none()
17759 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17760 && m.experts.iter().all(|d| d.act == Act::Silu);
17761 if !covered {
17762 per_row(&mut out);
17763 return out;
17764 }
17765 let ne = m.experts.len();
17766 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17768 for r in 0..b {
17769 let x = &xs[r * hidden..(r + 1) * hidden];
17770 accumulate_act(m, x, 1);
17771 let mut logits = vec![0.0f32; ne];
17772 m.router.matvec(x, &mut logits, pool);
17773 let (idx, p, wsum) = moe_route(&logits, m, None);
17774 {
17775 let mut st = m.stats.borrow_mut();
17776 if st.len() < ne {
17777 st.resize(ne, 0);
17778 }
17779 for &e in &idx {
17780 st[e] += 1;
17781 }
17782 }
17783 let w: Vec<f32> = idx
17784 .iter()
17785 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17786 .collect();
17787 routes.push((idx, w));
17788 }
17789 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17790 per_row(&mut out);
17791 return out;
17792 }
17793 let mut experts: Vec<usize> = Vec::new();
17795 let mut groups: Vec<Vec<usize>> = Vec::new();
17796 for (r, (idx, _)) in routes.iter().enumerate() {
17797 for &e in idx {
17798 match experts.iter().position(|&x| x == e) {
17799 Some(g) => groups[g].push(r),
17800 None => {
17801 experts.push(e);
17802 groups.push(vec![r]);
17803 }
17804 }
17805 }
17806 }
17807 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17808 let inter = m.experts[experts[0]].gate_proj.rows();
17809 let pairs: Vec<(&QTensor, &QTensor)> = experts
17810 .iter()
17811 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17812 .collect();
17813 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17814 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17815 per_row(&mut out);
17816 return out;
17817 }
17818 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17819 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17820 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17821 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17822 per_row(&mut out);
17823 return out;
17824 }
17825 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17827 let mut p = 0usize;
17828 for (g, &e) in experts.iter().enumerate() {
17829 for &r in &groups[g] {
17830 slot.insert((r, e), p);
17831 p += 1;
17832 }
17833 }
17834 for (r, (idx, w)) in routes.iter().enumerate() {
17835 let terms: Vec<(&[f32], f32)> = idx
17836 .iter()
17837 .zip(w)
17838 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17839 .collect();
17840 let row = &mut out[r * hidden..(r + 1) * hidden];
17841 for (i, dst) in row.iter_mut().enumerate() {
17842 let mut acc = 0f32;
17844 for (d, we) in &terms {
17845 acc += we * d[i];
17846 }
17847 *dst = acc;
17848 }
17849 }
17850 out
17851}
17852
17853pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17857 match pool {
17858 Some(p) if v.len() >= 1 << 15 => {
17859 let base = v.as_mut_ptr() as usize;
17860 p.run_rows(v.len(), &|s, e| {
17861 let sl =
17864 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17865 for x in sl {
17866 *x = c * (*x / c).tanh();
17867 }
17868 });
17869 }
17870 _ => {
17871 for x in v.iter_mut() {
17872 *x = c * (*x / c).tanh();
17873 }
17874 }
17875 }
17876}
17877
17878thread_local! {
17879 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17883 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17884}
17885
17886fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17888 if gate_topk() > 0
17891 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17892 {
17893 return out;
17894 }
17895 let prism_body = d.gate_proj.has_prism_contract()
17911 || d.up_proj.has_prism_contract()
17912 || d.down_proj.has_prism_contract();
17913 if !prism_body
17914 && crate::gpu::enabled_here()
17915 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17916 {
17917 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17918 crate::gpu::ProbeArm::Gpu
17919 } else {
17920 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17921 };
17922 match arm {
17923 crate::gpu::ProbeArm::Gpu => {
17924 let t0 = std::time::Instant::now();
17925 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17926 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17927 return out;
17928 }
17929 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17933 }
17934 crate::gpu::ProbeArm::CpuTimed => {
17935 let t0 = std::time::Instant::now();
17936 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17937 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17938 return out;
17939 }
17940 crate::gpu::ProbeArm::Cpu => {
17941 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17942 }
17943 }
17944 }
17945 dense_ffn_cpu(d, x, pool)
17946}
17947
17948fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17950 let inter = d.gate_proj.rows();
17951 FFN_SCRATCH.with(|s| {
17952 let mut s = s.borrow_mut();
17953 let [g, u, ..] = &mut *s;
17954 g.resize(inter, 0.0);
17955 if gate_topk() > 0 {
17958 u.resize(inter, 0.0);
17962 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17963 for i in 0..inter {
17964 g[i] = Act::Silu.combine(g[i], 1.0);
17965 }
17966 keep_top_k(g, gate_topk());
17967 for i in 0..inter {
17968 g[i] *= u[i];
17969 }
17970 } else if d.act == Act::Silu && {
17971 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17972 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17973 } {
17974 } else {
17976 u.resize(inter, 0.0);
17977 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17979 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17980 for i in 0..inter {
17981 g[i] = d.act.combine(g[i], u[i]);
17982 }
17983 }
17984 FFN_PROBE.with(|pr| {
17992 if let Some(acc) = pr.borrow_mut().as_mut() {
17993 let li = crate::gpu::cur_layer();
17994 if li >= 0 {
17995 if let Some(row) = acc.get_mut(li as usize) {
17996 match probe_topk() {
17997 0 if probe_sq() => {
17998 for (a, &v) in row.iter_mut().zip(g.iter()) {
17999 *a += (v as f64) * (v as f64);
18000 }
18001 }
18002 0 if probe_signed() => {
18003 for (a, &v) in row.iter_mut().zip(g.iter()) {
18004 *a += v as f64;
18005 }
18006 }
18007 0 => {
18008 for (a, &v) in row.iter_mut().zip(g.iter()) {
18009 *a += (v as f64).abs();
18010 }
18011 }
18012 k => {
18013 let n = g.len();
18014 let k = k.min(n);
18015 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18016 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18017 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18018 });
18019 let thr = *kth;
18020 for (a, &v) in row.iter_mut().zip(g.iter()) {
18021 if v.abs() >= thr {
18022 *a += 1.0;
18023 }
18024 }
18025 }
18026 }
18027 }
18028 }
18029 }
18030 });
18031 if oracle_topk() > 0 {
18032 keep_top_k(g, oracle_topk());
18033 }
18034 {
18035 let li = crate::gpu::cur_layer();
18036 if li >= 0 {
18037 adump_row(li as usize, g);
18038 }
18039 }
18040 let mut out = attention::take_buf(d.down_proj.rows());
18041 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
18042 d.down_proj.matvec(g, &mut out, pool);
18043 out
18044 })
18045}
18046
18047pub struct RefitAcc {
18060 pub support: Vec<u32>,
18061 pub gss: Vec<f32>,
18062 pub ya: Vec<f32>,
18063 pub hidden: usize,
18064 pub tokens: u64,
18065 pub buf_g: Vec<f32>,
18071 pub buf_o: Vec<f32>,
18072 pub buf_t: usize,
18073}
18074
18075type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
18079
18080static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
18081 std::sync::OnceLock::new();
18082
18083fn ffn_probe_active() -> bool {
18086 FFN_PROBE.with(|p| p.borrow().is_some())
18087}
18088
18089fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
18090 REFIT
18091 .get_or_init(|| {
18092 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
18093 (
18094 d,
18095 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
18096 )
18097 })
18098 })
18099 .as_ref()
18100}
18101
18102fn refit_accumulate(
18104 li: usize,
18105 g: &[f32],
18106 b: usize,
18107 inter: usize,
18108 out: &[f32],
18109 hidden: usize,
18110 pool: Option<&Pool>,
18111) {
18112 let Some((dir, map)) = refit_dir() else {
18113 return;
18114 };
18115 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18116 let (from, to) = *SPAN.get_or_init(|| {
18117 let g = |k: &str, d: usize| {
18118 std::env::var(k)
18119 .ok()
18120 .and_then(|v| v.parse().ok())
18121 .unwrap_or(d)
18122 };
18123 (
18124 g("CMF_FFN_REFIT_FROM", 0),
18125 g("CMF_FFN_REFIT_TO", usize::MAX),
18126 )
18127 });
18128 if li < from || li > to {
18129 return;
18130 }
18131 let mut guard = map.lock().unwrap();
18132 let (map, shared) = &mut *guard;
18133 let acc = match map.entry(li) {
18134 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
18135 std::collections::hash_map::Entry::Vacant(e) => {
18136 let path = format!("{dir}/support.{li}.u32");
18137 let Ok(bytes) = std::fs::read(&path) else {
18138 eprintln!("refit: no {path} — layer {li} skipped");
18139 return;
18140 };
18141 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
18142 let support: Vec<u32> = bytes[4..4 + n * 4]
18143 .chunks_exact(4)
18144 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18145 .collect();
18146 eprintln!(
18147 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
18148 (n * n + hidden * n) as f64 * 4.0 / 1e6
18149 );
18150 e.insert(RefitAcc {
18151 gss: vec![0.0; n * n],
18152 ya: vec![0.0; hidden * n],
18153 buf_g: Vec::new(),
18154 buf_o: Vec::new(),
18155 buf_t: 0,
18156 support,
18157 hidden,
18158 tokens: 0,
18159 })
18160 }
18161 };
18162 let ns = acc.support.len();
18163 let cap = refit_batch();
18165 if acc.buf_g.is_empty() {
18166 acc.buf_g = vec![0.0; ns * cap];
18167 acc.buf_o = vec![0.0; hidden * cap];
18168 }
18169 let take = b.min(cap - acc.buf_t);
18170 for t in 0..take {
18171 let col = acc.buf_t + t;
18172 for (j, &n) in acc.support.iter().enumerate() {
18173 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
18174 }
18175 for h in 0..hidden {
18176 acc.buf_o[h * cap + col] = out[t * hidden + h];
18177 }
18178 }
18179 acc.buf_t += take;
18180 acc.tokens += take as u64;
18181 if acc.buf_t < cap {
18182 return;
18183 }
18184 let bt = acc.buf_t;
18185 acc.buf_t = 0;
18186 let RefitAcc {
18196 gss,
18197 ya,
18198 buf_g,
18199 buf_o,
18200 ..
18201 } = acc;
18202 let need = (ns * ns).max(hidden * ns);
18203 if shared.len() < need {
18204 shared.resize(need, 0.0);
18205 }
18206 let scratch = &mut shared[..];
18207 let _ = bt;
18208 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
18209 add_into(gss, &scratch[..ns * ns], pool);
18210 if crate::gpu::gemm_nt_f32_transient(
18211 buf_o,
18212 buf_g,
18213 &mut scratch[..hidden * ns],
18214 hidden,
18215 cap,
18216 ns,
18217 ) {
18218 add_into(ya, &scratch[..hidden * ns], pool);
18219 } else {
18220 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18221 }
18222 } else {
18223 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
18224 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18225 }
18226 }
18230
18231fn refit_batch() -> usize {
18233 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18234 *B.get_or_init(|| {
18235 std::env::var("CMF_FFN_REFIT_BATCH")
18236 .ok()
18237 .and_then(|v| v.parse().ok())
18238 .unwrap_or(4096)
18239 })
18240}
18241
18242fn accum_outer_t(
18245 c: &mut [f32],
18246 m: usize,
18247 n: usize,
18248 b: usize,
18249 left: &[f32],
18250 right: &[f32],
18251 pool: Option<&Pool>,
18252) {
18253 let ptr = SendMut(c.as_mut_ptr());
18254 let body = |i: usize| {
18255 let ptr = &ptr;
18256 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18257 for t in 0..b {
18258 let a = left[i * b + t];
18259 if a == 0.0 {
18260 continue;
18261 }
18262 for (j, o) in row.iter_mut().enumerate() {
18263 *o += a * right[j * b + t];
18264 }
18265 }
18266 };
18267 match pool {
18268 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18269 for i in s..e {
18270 body(i);
18271 }
18272 }),
18273 _ => {
18274 for i in 0..m {
18275 body(i);
18276 }
18277 }
18278 }
18279}
18280
18281fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18284 let n = dst.len().min(src.len());
18285 match pool {
18286 Some(p) if n >= 1 << 16 => {
18287 let ptr = SendMut(dst.as_mut_ptr());
18288 let f = |s: usize, e: usize| {
18289 let ptr = &ptr;
18290 for blk in s..e {
18291 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18292 for i in a..b {
18293 unsafe { *ptr.0.add(i) += src[i] };
18294 }
18295 }
18296 };
18297 p.run_rows(n.div_ceil(4096), &f);
18298 }
18299 _ => {
18300 for (d, v) in dst.iter_mut().zip(&src[..n]) {
18301 *d += *v;
18302 }
18303 }
18304 }
18305}
18306
18307fn accum_outer(
18312 c: &mut [f32],
18313 m: usize,
18314 n: usize,
18315 b: usize,
18316 left: &[f32],
18317 right: &[f32],
18318 pool: Option<&Pool>,
18319) {
18320 const TILE: usize = 32;
18321 let tiles = m.div_ceil(TILE);
18322 let cp = SendMut(c.as_mut_ptr());
18323 let body = |ti: usize| {
18324 let cp = &cp;
18325 let i0 = ti * TILE;
18326 let i1 = (i0 + TILE).min(m);
18327 for t in 0..b {
18328 let r = &right[t * n..t * n + n];
18329 for i in i0..i1 {
18330 let a = left[i * b + t];
18331 if a == 0.0 {
18332 continue;
18333 }
18334 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18336 for (o, v) in row.iter_mut().zip(r) {
18337 *o += a * *v;
18338 }
18339 }
18340 }
18341 };
18342 match pool {
18343 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18344 for ti in s..e {
18345 body(ti);
18346 }
18347 }),
18348 _ => {
18349 for ti in 0..tiles {
18350 body(ti);
18351 }
18352 }
18353 }
18354}
18355
18356pub fn refit_flush() -> usize {
18358 let Some((dir, map)) = refit_dir() else {
18359 return 0;
18360 };
18361 let guard = map.lock().unwrap();
18362 let mut n = 0;
18363 for (li, acc) in guard.0.iter() {
18364 let w = |name: &str, v: &[f32]| {
18367 let path = format!("{dir}/{name}.{li}.f32");
18368 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18369 match std::fs::write(&path, &bytes) {
18370 Ok(()) => {}
18371 Err(e) => eprintln!(
18372 "refit: FAILED to write {path} ({} MB): {e}",
18373 bytes.len() / 1_000_000
18374 ),
18375 }
18376 };
18377 w("gss", &acc.gss);
18378 w("ya", &acc.ya);
18379 println!(
18380 "refit L{li}: {} support, {} tokens, hidden {}",
18381 acc.support.len(),
18382 acc.tokens,
18383 acc.hidden
18384 );
18385 n += 1;
18386 }
18387 n
18388}
18389
18390fn adump_row(li: usize, g: &[f32]) {
18395 use std::io::Write as _;
18396 static FILES: std::sync::OnceLock<
18397 Option<(
18398 String,
18399 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18400 )>,
18401 > = std::sync::OnceLock::new();
18402 let Some((prefix, map)) = FILES
18403 .get_or_init(|| {
18404 std::env::var("CMF_FFN_ADUMP")
18405 .ok()
18406 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18407 })
18408 .as_ref()
18409 else {
18410 return;
18411 };
18412 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18415 let (from, to) = *SPAN.get_or_init(|| {
18416 let g = |k: &str, d: usize| {
18417 std::env::var(k)
18418 .ok()
18419 .and_then(|v| v.parse().ok())
18420 .unwrap_or(d)
18421 };
18422 (
18423 g("CMF_FFN_ADUMP_FROM", 0),
18424 g("CMF_FFN_ADUMP_TO", usize::MAX),
18425 )
18426 });
18427 if li < from || li > to {
18428 return;
18429 }
18430 let mut map = map.lock().unwrap();
18431 let f = map.entry(li).or_insert_with(|| {
18432 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18433 });
18434 let mut bytes = Vec::with_capacity(g.len() * 2);
18435 for v in g {
18436 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18437 }
18438 let _ = f.write_all(&bytes);
18439}
18440
18441fn oracle_topk() -> usize {
18447 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18448 *K.get_or_init(|| {
18449 std::env::var("CMF_FFN_ORACLE_TOPK")
18450 .ok()
18451 .and_then(|v| v.parse().ok())
18452 .unwrap_or(0)
18453 })
18454}
18455
18456fn gate_topk() -> usize {
18462 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18463 *K.get_or_init(|| {
18464 std::env::var("CMF_FFN_GATE_TOPK")
18465 .ok()
18466 .and_then(|v| v.parse().ok())
18467 .unwrap_or(0)
18468 })
18469}
18470
18471fn gate_block() -> usize {
18478 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18479 *B.get_or_init(|| {
18480 std::env::var("CMF_FFN_GATE_BLOCK")
18481 .ok()
18482 .and_then(|v| v.parse().ok())
18483 .unwrap_or(1)
18484 })
18485}
18486
18487fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18489 let n = g.len();
18490 let nb = n.div_ceil(block);
18491 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18492 if kb >= nb {
18493 return;
18494 }
18495 let mut score: Vec<f32> = (0..nb)
18496 .map(|b| {
18497 g[b * block..((b + 1) * block).min(n)]
18498 .iter()
18499 .map(|v| v * v)
18500 .sum::<f32>()
18501 })
18502 .collect();
18503 let mut ord = score.clone();
18504 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
18505 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18506 });
18507 let thr = *kth;
18508 for b in 0..nb {
18509 if score[b] < thr {
18510 g[b * block..((b + 1) * block).min(n)].fill(0.0);
18511 }
18512 }
18513 score.clear();
18514}
18515
18516fn keep_top_k(g: &mut [f32], k: usize) {
18518 if gate_block() > 1 {
18519 return keep_top_blocks(g, k, gate_block());
18520 }
18521 let n = g.len();
18522 if k == 0 || k >= n {
18523 return;
18524 }
18525 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18526 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18527 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18528 });
18529 let thr = *kth;
18530 for v in g.iter_mut() {
18531 if v.abs() < thr {
18532 *v = 0.0;
18533 }
18534 }
18535}
18536
18537fn probe_sq() -> bool {
18541 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18542 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
18543}
18544
18545fn probe_signed() -> bool {
18549 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18550 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
18551}
18552
18553fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
18561 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
18562 M.get_or_init(|| {
18563 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
18564 let b = std::fs::read(&p).ok()?;
18565 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
18566 let vals: Vec<f32> = b[8..]
18567 .chunks_exact(4)
18568 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18569 .collect();
18570 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
18571 Some((inter, vals))
18572 })
18573 .as_ref()
18574}
18575
18576fn probe_topk() -> usize {
18579 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18580 *K.get_or_init(|| {
18581 std::env::var("CMF_FFN_PROBE_TOPK")
18582 .ok()
18583 .and_then(|v| v.parse().ok())
18584 .unwrap_or(0)
18585 })
18586}
18587
18588thread_local! {
18589 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18592 const { std::cell::RefCell::new(None) };
18593}
18594
18595fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18608 if d.gate_proj.has_prism_contract()
18613 || d.up_proj.has_prism_contract()
18614 || d.down_proj.has_prism_contract()
18615 {
18616 return None;
18617 }
18618 let dt = d.down_t.as_ref()?;
18619 let inter = d.gate_proj.rows();
18620 let hidden = dt.cols();
18621 if k == 0 || k >= inter || d.act != Act::Silu {
18622 return None;
18623 }
18624 DYN_SCRATCH.with(|sc| {
18625 let mut sc = sc.borrow_mut();
18626 let DynScratch {
18627 g,
18628 mag,
18629 live,
18630 parts,
18631 } = &mut *sc;
18632 g.resize(inter, 0.0);
18633 d.gate_proj.matvec(x, g, pool);
18634 for v in g.iter_mut() {
18635 *v = inference::silu(*v);
18636 }
18637 mag.clear();
18640 mag.extend(g.iter().map(|v| v.abs()));
18641 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18642 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18643 });
18644 let thr = *kth;
18645 live.clear();
18646 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18647 let mut out = vec![0.0f32; hidden];
18648 match pool {
18649 Some(p) if live.len() >= 64 => {
18650 let nw = p.n_workers() + 1;
18651 parts.clear();
18652 parts.resize(nw * hidden, 0.0);
18653 let ptr = SendMut(parts.as_mut_ptr());
18654 let n = live.len();
18655 let live_ref: &[u32] = live;
18656 let g_ref: &[f32] = g;
18657 p.run(&|w, workers| {
18658 let chunk = n.div_ceil(workers);
18659 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18660 if s >= e {
18661 return;
18662 }
18663 WORKER_SCRATCH.with(|ws| {
18664 let mut ws = ws.borrow_mut();
18665 let [scratch, acc] = &mut *ws;
18666 scratch.resize(hidden.max(x.len()), 0.0);
18667 acc.clear();
18668 acc.resize(hidden, 0.0);
18669 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18670 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18673 d.up_proj.prefetch_row(nx as usize);
18674 dt.prefetch_row(nx as usize);
18675 }
18676 let idx = nrm as usize;
18677 let up = d.up_proj.row_dot(idx, x, scratch);
18678 let a = g_ref[idx] * up;
18679 if a != 0.0 {
18680 dt.add_row_scaled(idx, a, acc, scratch);
18681 }
18682 }
18683 for (j, v) in acc.iter().enumerate() {
18684 unsafe { *ptr.at(w * hidden + j) = *v };
18685 }
18686 });
18687 });
18688 for w in 0..nw {
18689 for (j, o) in out.iter_mut().enumerate() {
18690 *o += parts[w * hidden + j];
18691 }
18692 }
18693 }
18694 _ => {
18695 WORKER_SCRATCH.with(|ws| {
18696 let mut ws = ws.borrow_mut();
18697 let [scratch, _acc] = &mut *ws;
18698 scratch.resize(hidden.max(x.len()), 0.0);
18699 for &nrm in live.iter() {
18700 let idx = nrm as usize;
18701 let up = d.up_proj.row_dot(idx, x, scratch);
18702 let a = g[idx] * up;
18703 if a != 0.0 {
18704 dt.add_row_scaled(idx, a, &mut out, scratch);
18705 }
18706 }
18707 });
18708 }
18709 }
18710 Some(out)
18711 })
18712}
18713
18714struct DynScratch {
18717 g: Vec<f32>,
18718 mag: Vec<f32>,
18719 live: Vec<u32>,
18720 parts: Vec<f32>,
18721}
18722
18723thread_local! {
18724 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18725 std::cell::RefCell::new(DynScratch {
18726 g: Vec::new(),
18727 mag: Vec::new(),
18728 live: Vec::new(),
18729 parts: Vec::new(),
18730 })
18731 };
18732 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18734 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18735}
18736
18737fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18742 let inter = d.gate_proj.rows();
18743 FFN_SCRATCH.with(|s| {
18744 let mut s = s.borrow_mut();
18745 let [g, u, ..] = &mut *s;
18746 g.resize(inter, 0.0);
18747 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18748 } else {
18750 u.resize(inter, 0.0);
18751 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18752 for i in 0..inter {
18753 g[i] = d.act.combine(g[i], u[i]);
18754 }
18755 }
18756 zero_masked_cols(g, 1, inter, mask_row);
18757 let mut out = attention::take_buf(d.down_proj.rows());
18758 d.down_proj.matvec(g, &mut out, pool);
18759 out
18760 })
18761}
18762
18763fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18769 if d.gate_proj.has_prism_contract()
18770 || d.up_proj.has_prism_contract()
18771 || d.down_proj.has_prism_contract()
18772 {
18773 return None;
18774 }
18775 d.act.graph_act()?;
18778 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18781 return None;
18782 }
18783 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18784 let mut model_ref = None;
18785 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18786 let model = model_ref?;
18787 let hidden = jobs[0].down.1;
18788 let mut out = attention::take_buf(hidden);
18789 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18790 Some(out)
18791 } else {
18792 let mut out = out;
18793 attention::recycle_buf(&mut out);
18794 None
18795 }
18796}
18797
18798#[allow(clippy::type_complexity)]
18803#[allow(clippy::type_complexity)]
18804pub(crate) fn moe_parts(
18805 t: &QTensor,
18806) -> Option<(
18807 &std::sync::Arc<cortiq_core::CmfModel>,
18808 usize,
18809 usize,
18810 usize,
18811 &[f32],
18812 &[f32],
18813 bool,
18814 bool,
18815 bool,
18816)> {
18817 match t {
18818 QTensor::Mapped {
18819 model,
18820 idx,
18821 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18822 rows,
18823 cols,
18824 row_scale,
18825 col_field,
18826 ..
18827 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18828 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18829 )),
18830 QTensor::Mapped {
18832 model,
18833 idx,
18834 dtype: cortiq_core::TensorDtype::Q1,
18835 rows,
18836 cols,
18837 ..
18838 } => Some((
18839 model,
18840 *idx,
18841 *rows,
18842 *cols,
18843 &[][..],
18844 &[][..],
18845 true,
18846 false,
18847 false,
18848 )),
18849 QTensor::Mapped {
18851 model,
18852 idx,
18853 dtype: cortiq_core::TensorDtype::Q4Tiled,
18854 rows,
18855 cols,
18856 ..
18857 } => Some((
18858 model,
18859 *idx,
18860 *rows,
18861 *cols,
18862 &[][..],
18863 &[][..],
18864 false,
18865 true,
18866 false,
18867 )),
18868 QTensor::Mapped {
18870 model,
18871 idx,
18872 dtype: cortiq_core::TensorDtype::Q4TiledP,
18873 rows,
18874 cols,
18875 ..
18876 } => Some((
18877 model,
18878 *idx,
18879 *rows,
18880 *cols,
18881 &[][..],
18882 &[][..],
18883 false,
18884 true,
18885 false,
18886 )),
18887 QTensor::Mapped {
18891 model,
18892 idx,
18893 dtype: cortiq_core::TensorDtype::Q2TiledP,
18894 rows,
18895 cols,
18896 ..
18897 } => Some((
18898 model,
18899 *idx,
18900 *rows,
18901 *cols,
18902 &[][..],
18903 &[][..],
18904 false,
18905 true,
18906 true,
18907 )),
18908 _ => None,
18909 }
18910}
18911
18912#[cfg(target_os = "macos")]
18919#[allow(clippy::type_complexity)]
18920fn chunk_moe_parts(
18921 m: &MoeFfn,
18922 hidden: usize,
18923) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18924 if m.shared.is_some()
18925 || m.per_expert_scale.is_some()
18926 || m.resonance.is_some()
18927 || m.route_tau.is_some()
18928 || m.router_input_norm
18929 || m.experts.is_empty()
18930 || m.top_k == 0
18931 || std::env::var("CMF_RMS_TRACE").is_ok()
18932 || std::env::var("CMF_ACT_DUMP").is_ok()
18933 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18934 {
18935 return None;
18936 }
18937 let (rf, rr, rc) = m.router.f32_parts()?;
18938 if rr != m.experts.len() || rc != hidden {
18939 return None;
18940 }
18941 let inter = m.experts[0].gate_proj.rows();
18942 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
18945 let experts = m
18946 .experts
18947 .iter()
18948 .map(|e| {
18949 if e.act != Act::Silu
18950 || !e.segs.is_empty()
18951 || e.gate_proj.rows() != inter
18952 || e.up_proj.rows() != inter
18953 || e.down_proj.cols() != inter
18954 || e.gate_proj.cols() != hidden
18955 || e.down_proj.rows() != hidden
18956 {
18957 return None;
18958 }
18959 if q8 {
18960 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18961 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18962 }
18963 Some((
18964 e.gate_proj.mapped_q4tp()?.1,
18965 e.up_proj.mapped_q4tp()?.1,
18966 e.down_proj.mapped_q4tp()?.1,
18967 ))
18968 })
18969 .collect::<Option<Vec<_>>>()?;
18970 Some((rf, experts, q8))
18971}
18972
18973#[cfg(target_os = "macos")]
18986fn metal_flint_moe_graph_parts(
18987 m: &MoeFfn,
18988 hidden: usize,
18989 scratch_key: usize,
18990) -> Option<crate::gpu::GpuFlintMoe<'_>> {
18991 use std::sync::Arc;
18992
18993 const HIDDEN: usize = 2_048;
18994 const ROUTED_INTER: usize = 512;
18995 const SHARED_INTER: usize = 1_024;
18996 const EXPERTS: usize = 256;
18997 const TOP_K: usize = 6;
18998
18999 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
19000 return None;
19001 }
19002 let routed0 = m.experts.first()?;
19003 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
19004 let (router, router_rows, router_cols) = m.router.f32_parts()?;
19005 if router_rows != EXPERTS || router_cols != HIDDEN {
19006 return None;
19007 }
19008 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
19009 return None;
19010 }
19011 let same_q2 = |t: &QTensor| -> Option<usize> {
19012 let (owner, idx) = t.mapped_q2tp()?;
19013 Arc::ptr_eq(&owner, &model).then_some(idx)
19014 };
19015 let same_q4 = |t: &QTensor| -> Option<usize> {
19016 let (owner, idx) = t.mapped_q4tp()?;
19017 Arc::ptr_eq(&owner, &model).then_some(idx)
19018 };
19019 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19020 if e.act != Act::Silu
19021 || !e.segs.is_empty()
19022 || e.gate_proj.rows() != ROUTED_INTER
19023 || e.gate_proj.cols() != HIDDEN
19024 || e.up_proj.rows() != ROUTED_INTER
19025 || e.up_proj.cols() != HIDDEN
19026 || e.down_proj.rows() != HIDDEN
19027 || e.down_proj.cols() != ROUTED_INTER
19028 {
19029 return None;
19030 }
19031 Some((
19032 same_q2(&e.gate_proj)?,
19033 same_q2(&e.up_proj)?,
19034 same_q4(&e.down_proj)?,
19035 ))
19036 };
19037 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19038 if e.act != Act::Silu
19039 || !e.segs.is_empty()
19040 || e.gate_proj.rows() != SHARED_INTER
19041 || e.gate_proj.cols() != HIDDEN
19042 || e.up_proj.rows() != SHARED_INTER
19043 || e.up_proj.cols() != HIDDEN
19044 || e.down_proj.rows() != HIDDEN
19045 || e.down_proj.cols() != SHARED_INTER
19046 {
19047 return None;
19048 }
19049 Some((
19050 same_q4(&e.gate_proj)?,
19051 same_q4(&e.up_proj)?,
19052 same_q4(&e.down_proj)?,
19053 ))
19054 };
19055 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
19056 if experts.len() != EXPERTS {
19057 return None;
19058 }
19059 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
19060 if shared_gate.is_some() {
19061 return None;
19062 }
19063 Some(crate::gpu::GpuFlintMoe {
19064 router,
19065 bias: m.expert_bias.as_deref(),
19066 experts,
19067 scratch_key,
19068 shared: shared(shared_ffn)?,
19069 top_k: m.top_k,
19070 norm_topk: m.norm_topk_prob,
19071 route_scale: m.routed_scaling,
19072 sigmoid: m.router_sigmoid,
19073 sigmoid_denom_eps: m.sigmoid_denom_eps,
19074 })
19075}
19076
19077#[cfg(target_os = "macos")]
19078fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
19079 if m.router_input_norm
19080 || m.route_tau.is_some()
19081 || m.mask.is_some()
19082 || m.per_expert_scale.is_some()
19083 || m.experts.is_empty()
19084 || m.top_k == 0
19085 || m.resonance.is_some()
19086 {
19087 return None;
19088 }
19089 let (sh, sg) = match &m.shared {
19094 Some((sh, sg)) => (Some(sh), sg.as_ref()),
19095 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
19096 None => return None,
19097 };
19098 let (rf, rr, rc) = m.router.f32_parts()?;
19099 if rr != m.experts.len() || rc != hidden {
19100 return None;
19101 }
19102 let shared_gated = sg.is_some();
19103 let sf = match sg {
19104 Some(sg) => {
19105 let (sf, sr, sc) = sg.f32_parts()?;
19106 if sr * sc != hidden {
19107 return None;
19108 }
19109 sf
19110 }
19111 None => &rf[..hidden],
19114 };
19115 if let Some(b) = &m.expert_bias {
19116 if b.len() != m.experts.len() {
19117 return None;
19118 }
19119 }
19120 let inter = m.experts[0].gate_proj.rows();
19121 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
19124 let q8 = crate::gpu_metal::moe_q8::enabled()
19128 && m.shared.is_none()
19129 && m.experts[0].gate_proj.q8_2f_parts().is_some();
19130 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19131 if e.act != Act::Silu
19132 || e.gate_proj.rows() != inter
19133 || e.gate_proj.cols() != hidden
19134 || e.up_proj.rows() != inter
19135 || e.up_proj.cols() != hidden
19136 || e.down_proj.rows() != hidden
19137 || e.down_proj.cols() != inter
19138 {
19139 return None;
19140 }
19141 if q8 {
19142 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19143 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19144 }
19145 let pick = |t: &QTensor| -> Option<usize> {
19146 if gu_q2 {
19147 t.mapped_q2tp().map(|(_, i)| i)
19148 } else {
19149 t.mapped_q4tp().map(|(_, i)| i)
19150 }
19151 };
19152 Some((
19153 pick(&e.gate_proj)?,
19154 pick(&e.up_proj)?,
19155 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
19156 ))
19157 };
19158 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
19159 let shared = match sh {
19160 Some(sh) => Some(trio(sh)?),
19161 None => None,
19162 };
19163 Some(crate::gpu::GpuMoe {
19164 router: rf,
19165 sgate: sf,
19166 experts,
19167 shared,
19168 n_exp: m.experts.len(),
19169 top_k: m.top_k,
19170 inter,
19171 norm_topk: m.norm_topk_prob,
19172 route_scale: m.routed_scaling,
19173 gu_q2,
19174 q8,
19175 sigmoid: m.router_sigmoid,
19176 sigmoid_denom_eps: m.sigmoid_denom_eps,
19177 bias: m.expert_bias.as_deref(),
19178 shared_gated,
19179 })
19180}
19181
19182fn forced_ids_at(step: usize) -> Option<u32> {
19188 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
19189 IDS.get_or_init(|| {
19190 std::env::var("CMF_FORCE_IDS")
19191 .ok()
19192 .and_then(|p| std::fs::read_to_string(p).ok())
19193 .map(|s| {
19194 s.split_whitespace()
19195 .filter_map(|t| t.parse().ok())
19196 .collect()
19197 })
19198 .unwrap_or_default()
19199 })
19200 .get(step)
19201 .copied()
19202}
19203
19204pub(crate) fn moe_push_job_parts<'a>(
19208 gate: &'a QTensor,
19209 up: &'a QTensor,
19210 down: &'a QTensor,
19211 x: &[f32],
19212 w: f32,
19213 swiglu_limit: f32,
19214 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19215 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19216) -> Option<()> {
19217 use crate::qtensor::prescale;
19218 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
19219 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
19220 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
19221 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19222 return None; }
19224 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
19227 return None;
19228 }
19229 if !gq2 && dq2 {
19230 return None;
19231 }
19232 model_ref.get_or_insert_with(|| gm.clone());
19233 let dt = |cf: &[f32]| {
19234 if cf.is_empty() {
19235 cortiq_core::TensorDtype::Q8Row
19236 } else {
19237 cortiq_core::TensorDtype::Q8_2f
19238 }
19239 };
19240 jobs.push(crate::gpu::MoeJob {
19241 gate: (gi, gr, gc, grs),
19242 up: (ui, ur, uc, urs),
19243 down: (di, dr, dc, drs),
19244 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
19245 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
19246 down_col: dcf,
19247 w,
19248 q1: gq1,
19249 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
19250 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
19251 gu_q2: gq2,
19252 swiglu_limit,
19253 act: 0,
19254 });
19255 Some(())
19256}
19257
19258fn moe_push_job<'a>(
19260 d: &'a DenseFfn,
19261 x: &[f32],
19262 w: f32,
19263 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19264 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19265) -> Option<()> {
19266 use crate::qtensor::prescale;
19267 let act = d.act.graph_act()?.code();
19270 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
19271 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
19272 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
19273 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19274 return None; }
19276 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
19277 return None;
19278 }
19279 if !gq2 && dq2 {
19280 return None;
19281 }
19282 model_ref.get_or_insert_with(|| gm.clone());
19283 let gdt = if gcf.is_empty() {
19284 cortiq_core::TensorDtype::Q8Row
19285 } else {
19286 cortiq_core::TensorDtype::Q8_2f
19287 };
19288 let udt = if ucf.is_empty() {
19289 cortiq_core::TensorDtype::Q8Row
19290 } else {
19291 cortiq_core::TensorDtype::Q8_2f
19292 };
19293 jobs.push(crate::gpu::MoeJob {
19294 gate: (gi, gr, gc, grs),
19295 up: (ui, ur, uc, urs),
19296 down: (di, dr, dc, drs),
19297 xs_gate: prescale(x, gcf, gdt).into_owned(),
19298 xs_up: prescale(x, ucf, udt).into_owned(),
19299 down_col: dcf,
19300 w,
19301 q1: gq1,
19302 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
19303 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
19304 gu_q2: gq2,
19305 swiglu_limit: 0.0,
19306 act,
19307 });
19308 Some(())
19309}
19310
19311fn sparse_ffn_quant(
19318 d: &DenseFfn,
19319 x: &[f32],
19320 active: &[u16],
19321 hidden: usize,
19322 pool: Option<&Pool>,
19323) -> Vec<f32> {
19324 let n = active.len();
19325 let inter = d.gate_proj.rows();
19326 let mut act = vec![0.0f32; n];
19327 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
19330 let compute = |ai: usize| -> f32 {
19331 let idx = active[ai] as usize;
19332 if idx >= inter {
19333 return 0.0; }
19335 let mut s = if need_scratch {
19336 vec![0.0f32; hidden]
19337 } else {
19338 Vec::new()
19339 };
19340 let gate = d.gate_proj.row_dot(idx, x, &mut s);
19341 let up = d.up_proj.row_dot(idx, x, &mut s);
19342 d.act.combine(gate, up)
19343 };
19344 match pool {
19345 Some(p) if n >= 256 => {
19346 let ptr = SendMut(act.as_mut_ptr());
19347 p.run(&|widx, nw| {
19348 let chunk = n.div_ceil(nw);
19349 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19350 for ai in s..e {
19351 unsafe { *ptr.at(ai) = compute(ai) };
19352 }
19353 });
19354 }
19355 _ => {
19356 for (ai, a) in act.iter_mut().enumerate() {
19357 *a = compute(ai);
19358 }
19359 }
19360 }
19361 let mut out = vec![0.0f32; hidden];
19363 for (ai, &idx) in active.iter().enumerate() {
19364 let w = act[ai];
19365 if w.abs() >= 1e-12 && (idx as usize) < inter {
19366 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19367 }
19368 }
19369 out
19370}
19371
19372#[doc(hidden)]
19374pub fn sparse_ffn_quant_for_test(
19375 d: &DenseFfn,
19376 x: &[f32],
19377 active: &[u16],
19378 hidden: usize,
19379) -> Vec<f32> {
19380 sparse_ffn_quant(d, x, active, hidden, None)
19381}
19382
19383fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19387 let deq = |t: &QTensor| -> Vec<f32> {
19388 let (rows, cols) = (t.rows(), t.cols());
19389 let mut out = vec![0.0f32; rows * cols];
19390 for r in 0..rows {
19391 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19392 }
19393 out
19394 };
19395 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19396}
19397
19398struct SendMut(*mut f32);
19400unsafe impl Send for SendMut {}
19401unsafe impl Sync for SendMut {}
19402impl SendMut {
19403 #[inline]
19404 #[allow(clippy::mut_from_ref)]
19407 unsafe fn at(&self, i: usize) -> &mut f32 {
19408 unsafe { &mut *self.0.add(i) }
19409 }
19410}
19411
19412pub(crate) fn moe_route(
19423 logits: &[f32],
19424 m: &MoeFfn,
19425 allowed: Option<&[bool]>,
19426) -> (Vec<usize>, Vec<f32>, f32) {
19427 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
19428}
19429
19430pub(crate) fn moe_route_with_eps(
19439 logits: &[f32],
19440 m: &MoeFfn,
19441 allowed: Option<&[bool]>,
19442 sigmoid_denom_eps: f32,
19443) -> (Vec<usize>, Vec<f32>, f32) {
19444 let ne = logits.len();
19445 let admit = |e: usize| {
19451 m.mask.as_ref().is_none_or(|mk| mk[e])
19452 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19453 };
19454 if m.resonance.is_some() && m.top_k == 1 {
19467 let mut best: Option<usize> = None;
19468 for e in (0..ne).filter(|&e| admit(e)) {
19469 let l = logits[e];
19470 if l == f32::NEG_INFINITY || l.is_nan() {
19471 continue;
19472 }
19473 if best.is_none_or(|b| l > logits[b]) {
19474 best = Some(e);
19475 }
19476 }
19477 if let Some(b) = best {
19478 let mut p = vec![0.0f32; ne];
19479 p[b] = 1.0;
19480 return (vec![b], p, 1.0 / m.routed_scaling);
19481 }
19482 }
19483 let p: Vec<f32> = if m.router_sigmoid {
19489 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19490 } else {
19491 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19492 if mx == f32::NEG_INFINITY {
19493 vec![1.0 / ne.max(1) as f32; ne]
19494 } else {
19495 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
19496 let s: f32 = e.iter().sum();
19497 for v in &mut e {
19498 *v /= s;
19499 }
19500 e
19501 }
19502 };
19503 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
19504 match &m.expert_bias {
19506 Some(b) => idx.sort_unstable_by(|&x, &y| {
19507 (p[y] + b[y])
19508 .partial_cmp(&(p[x] + b[x]))
19509 .unwrap()
19510 .then(x.cmp(&y))
19511 }),
19512 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
19513 }
19514 idx.truncate(m.top_k);
19515 if let Some(tau) = m.route_tau {
19519 let total: f32 = idx.iter().map(|&e| p[e]).sum();
19520 if total > 0.0 {
19521 let mut acc = 0.0f32;
19522 let mut keep = idx.len();
19523 for (i, &e) in idx.iter().enumerate() {
19524 acc += p[e];
19525 if acc >= tau * total {
19526 keep = i + 1;
19527 break;
19528 }
19529 }
19530 idx.truncate(keep);
19531 }
19532 }
19533 let wsum: f32 = if m.norm_topk_prob {
19534 let s: f32 = idx.iter().map(|&e| p[e]).sum();
19535 (if m.router_sigmoid {
19539 s + sigmoid_denom_eps
19540 } else {
19541 s
19542 }) / m.routed_scaling
19543 } else {
19544 1.0 / m.routed_scaling
19545 };
19546 (idx, p, wsum)
19547}
19548
19549fn moe_trace(idx: &[usize]) {
19551 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
19552}
19553
19554pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
19557 use std::io::Write;
19558 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
19559 std::sync::OnceLock::new();
19560 let Some(f) = F.get_or_init(|| {
19561 let p = std::env::var("CMF_MOE_TRACE").ok()?;
19562 Some(std::sync::Mutex::new(
19563 std::fs::OpenOptions::new()
19564 .create(true)
19565 .append(true)
19566 .open(p)
19567 .ok()?,
19568 ))
19569 }) else {
19570 return;
19571 };
19572 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
19573 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
19574}
19575
19576pub(crate) fn moe_ffn(
19579 m: &MoeFfn,
19580 x: &[f32],
19581 pool: Option<&Pool>,
19582 allowed: Option<&[bool]>,
19583) -> Vec<f32> {
19584 let r = moe_ffn_route(m, x, pool, allowed);
19585 moe_ffn_experts(m, x, &r, pool)
19586}
19587
19588pub(crate) struct MoeRoute {
19592 pub idx: Vec<usize>,
19593 pub p: Vec<f32>,
19594 pub wsum: f32,
19595 pub logits: Vec<f32>,
19596}
19597
19598pub(crate) fn moe_ffn_route(
19604 m: &MoeFfn,
19605 x: &[f32],
19606 pool: Option<&Pool>,
19607 allowed: Option<&[bool]>,
19608) -> MoeRoute {
19609 accumulate_act(m, x, 1);
19610 let ne = m.experts.len();
19611 let mut logits = vec![0.0f32; ne];
19612 match &m.resonance {
19613 Some(r) => r.scores(x, &mut logits),
19614 None => m.router.matvec(x, &mut logits, pool),
19615 }
19616 let (idx, p, wsum) = moe_route(&logits, m, allowed);
19617 {
19618 let mut st = m.stats.borrow_mut();
19619 if st.len() < ne {
19620 st.resize(ne, 0);
19621 }
19622 for &e in &idx {
19623 st[e] += 1;
19624 }
19625 }
19626 moe_trace(&idx);
19632 MoeRoute {
19633 idx,
19634 p,
19635 wsum,
19636 logits,
19637 }
19638}
19639
19640#[cfg(target_os = "macos")]
19649fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
19650 const HIDDEN: usize = 2_048;
19651 const ROUTED_INTER: usize = 512;
19652 const SHARED_INTER: usize = 1_024;
19653 const EXPERTS: usize = 256;
19654 const TOP_K: usize = 6;
19655
19656 let Some(routed) = m.experts.first() else {
19657 return false;
19658 };
19659 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
19660 return false;
19661 };
19662 let Some((shared, shared_gate)) = &m.shared else {
19663 return false;
19664 };
19665 if shared_gate.is_some()
19666 || model.header.arch.arch_name != "flint"
19667 || model.header.arch.prism_hadamard.is_some()
19668 || model.header.arch.hidden_size != HIDDEN
19669 || m.experts.len() != EXPERTS
19670 || m.top_k != TOP_K
19671 || !m.router_sigmoid
19672 || !m.norm_topk_prob
19673 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
19674 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
19675 || m.route_tau.is_some()
19676 || m.mask.is_some()
19677 || m.per_expert_scale.is_some()
19678 || m.router_input_norm
19679 || m.resonance.is_some()
19680 || !m.grown.is_empty()
19681 {
19682 return false;
19683 }
19684
19685 let routed_q2 = routed.act == Act::Silu
19686 && routed.segs.is_empty()
19687 && routed.gate_proj.rows() == ROUTED_INTER
19688 && routed.gate_proj.cols() == HIDDEN
19689 && routed.up_proj.rows() == ROUTED_INTER
19690 && routed.up_proj.cols() == HIDDEN
19691 && routed.down_proj.rows() == HIDDEN
19692 && routed.down_proj.cols() == ROUTED_INTER
19693 && routed.up_proj.mapped_q2tp().is_some()
19694 && routed.down_proj.mapped_q4tp().is_some();
19695 let shared_q4 = shared.act == Act::Silu
19696 && shared.segs.is_empty()
19697 && shared.gate_proj.rows() == SHARED_INTER
19698 && shared.gate_proj.cols() == HIDDEN
19699 && shared.up_proj.rows() == SHARED_INTER
19700 && shared.up_proj.cols() == HIDDEN
19701 && shared.down_proj.rows() == HIDDEN
19702 && shared.down_proj.cols() == SHARED_INTER
19703 && shared.gate_proj.mapped_q4tp().is_some()
19704 && shared.up_proj.mapped_q4tp().is_some()
19705 && shared.down_proj.mapped_q4tp().is_some();
19706 routed_q2 && shared_q4
19707}
19708
19709pub(crate) fn moe_ffn_experts(
19712 m: &MoeFfn,
19713 x: &[f32],
19714 r: &MoeRoute,
19715 pool: Option<&Pool>,
19716) -> Vec<f32> {
19717 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
19718 if crate::gpu::enabled_here() {
19723 #[cfg(target_os = "macos")]
19724 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
19725 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
19731 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
19732 });
19733 }
19734 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
19735 crate::gpu::ProbeArm::Gpu => {
19736 let t0 = std::time::Instant::now();
19737 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
19738 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
19739 return out;
19740 }
19741 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
19742 }
19743 crate::gpu::ProbeArm::CpuTimed => {
19744 let t0 = std::time::Instant::now();
19745 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19746 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
19747 return out;
19748 }
19749 crate::gpu::ProbeArm::Cpu => {
19750 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19751 }
19752 }
19753 }
19754 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
19755}
19756
19757fn moe_ffn_minicpm_tail(
19761 slot: &mut crate::minicpm_tail::MiniCpmTail,
19762 li: usize,
19763 total_layers: usize,
19764 m: &MoeFfn,
19765 x: &[f32],
19766 pool: Option<&Pool>,
19767) -> Vec<f32> {
19768 let route = moe_ffn_route(m, x, pool, None);
19769 match slot.forward(li, m, x, &route, pool, total_layers) {
19770 Some(out) => out,
19771 None => moe_ffn_experts(m, x, &route, pool),
19776 }
19777}
19778
19779fn moe_ffn_banked(
19783 slot: &mut crate::mimo_moe::Slot,
19784 li: usize,
19785 m: &MoeFfn,
19786 x: &[f32],
19787 pool: Option<&Pool>,
19788) -> Vec<f32> {
19789 let t0 = std::time::Instant::now();
19790 let r = moe_ffn_route(m, x, pool, None);
19791 slot.note_route(t0.elapsed().as_nanos() as u64);
19792 match slot.forward(li, m, x, &r, pool) {
19793 Some(out) => out,
19794 None => crate::qtensor::float_activations_scope(|| {
19795 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19796 }),
19797 }
19798}
19799
19800fn moe_ffn_banked_rows(
19802 slot: &mut crate::mimo_moe::Slot,
19803 li: usize,
19804 m: &MoeFfn,
19805 xs: &[f32],
19806 b: usize,
19807 hidden: usize,
19808 pool: Option<&Pool>,
19809) -> Vec<f32> {
19810 let t0 = std::time::Instant::now();
19811 let routes: Vec<_> = xs
19812 .chunks_exact(hidden)
19813 .map(|x| moe_ffn_route(m, x, pool, None))
19814 .collect();
19815 slot.note_route(t0.elapsed().as_nanos() as u64);
19816 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19817 return out;
19818 }
19819 let mut out = Vec::with_capacity(b * hidden);
19820 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19821 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19822 crate::qtensor::float_activations_scope(|| {
19824 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19825 })
19826 });
19827 out.extend(row);
19828 }
19829 out
19830}
19831
19832fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19837 use std::sync::atomic::{AtomicBool, Ordering};
19838 if built {
19839 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19840 if total_layers > 0 && layers_run < total_layers {
19841 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19842 } else {
19843 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19844 }
19845 } else {
19846 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19847 }
19848 static SAID: AtomicBool = AtomicBool::new(false);
19849 if !SAID.swap(true, Ordering::Relaxed) {
19850 if built {
19851 tracing::info!("wgpu whole-token graph: ACTIVE");
19852 } else {
19853 tracing::warn!("wgpu whole-token graph refused — per-op path");
19854 }
19855 }
19856}
19857
19858pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19862pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19863pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19867pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19869
19870pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19874pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19875pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19876 std::sync::atomic::AtomicU64::new(0);
19877pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19878pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19879pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19883 std::sync::atomic::AtomicU64::new(0);
19884pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19885pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19886 std::sync::atomic::AtomicU64::new(0);
19887pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19888 std::sync::atomic::AtomicU64::new(0);
19889
19890fn moe_batch_enabled() -> bool {
19893 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19894 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19895}
19896
19897fn moe_ffn_cpu_batched(
19903 m: &MoeFfn,
19904 x: &[f32],
19905 idx: &[usize],
19906 p: &[f32],
19907 wsum: f32,
19908 pool: Option<&Pool>,
19909) -> Option<Vec<f32>> {
19910 if idx.is_empty() || !moe_batch_enabled() {
19911 return None;
19912 }
19913 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19917 return None;
19918 }
19919 let n = idx.len() + usize::from(m.shared.is_some());
19920 let mut pairs = Vec::with_capacity(n);
19921 let mut downs = Vec::with_capacity(n);
19922 let mut ws = Vec::with_capacity(n);
19923 for &e in idx {
19924 let d = &m.experts[e];
19925 if d.act != Act::Silu {
19926 return None;
19927 }
19928 pairs.push((&d.gate_proj, &d.up_proj));
19929 downs.push(&d.down_proj);
19930 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19931 }
19932 if let Some((se, gate)) = &m.shared {
19935 if se.act != Act::Silu {
19936 return None;
19937 }
19938 let g = gate.as_ref().map_or(1.0, |gate| {
19939 let mut gl = [0.0f32; 1];
19940 gate.matvec(x, &mut gl, pool);
19941 1.0 / (1.0 + (-gl[0]).exp())
19942 });
19943 pairs.push((&se.gate_proj, &se.up_proj));
19944 downs.push(&se.down_proj);
19945 ws.push(g);
19946 }
19947 let inter = pairs[0].0.rows();
19948 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19949 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19950 return None;
19951 }
19952 let mut out = attention::take_buf(x.len());
19953 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19954 attention::recycle_buf(&mut out);
19955 return None;
19956 }
19957 Some(out)
19958}
19959
19960pub(crate) fn moe_cold_experts_cpu(
19966 experts: &[(&DenseFfn, f32)],
19967 x: &[f32],
19968 pool: Option<&Pool>,
19969) -> Vec<f32> {
19970 let mut out = attention::take_buf(x.len());
19971 if experts.is_empty() {
19972 return out;
19973 }
19974 let pairs: Vec<_> = experts
19975 .iter()
19976 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19977 .collect();
19978 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19979 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19980 let inter = experts[0].0.gate_proj.rows();
19981 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19982 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19983 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19984 {
19985 return out;
19986 }
19987 out.fill(0.0);
19988 for &(expert, weight) in experts {
19989 let mut one = dense_ffn(expert, x, pool);
19990 for (o, v) in out.iter_mut().zip(&one) {
19991 *o += weight * v;
19992 }
19993 attention::recycle_buf(&mut one);
19994 }
19995 out
19996}
19997
19998pub(crate) fn moe_cold_experts_rows_cpu(
20002 jobs: &[Vec<(&DenseFfn, f32)>],
20003 xs: &[f32],
20004 hidden: usize,
20005 pool: Option<&Pool>,
20006) -> Vec<f32> {
20007 let mut out = vec![0.0; xs.len()];
20008 let mut experts: Vec<&DenseFfn> = Vec::new();
20009 let mut groups: Vec<Vec<usize>> = Vec::new();
20010 let mut terms = vec![Vec::new(); jobs.len()];
20011 for (r, row) in jobs.iter().enumerate() {
20012 for &(e, w) in row {
20013 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
20014 Some(g) => g,
20015 None => {
20016 experts.push(e);
20017 groups.push(Vec::new());
20018 groups.len() - 1
20019 }
20020 };
20021 terms[r].push((g, groups[g].len(), w));
20022 groups[g].push(r);
20023 }
20024 }
20025 if experts.is_empty() {
20026 return out;
20027 }
20028 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
20029 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
20030 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
20031 let count: usize = lens.iter().sum();
20032 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
20033 let mut ds = vec![vec![0.0; hidden]; count];
20034 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
20035 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
20036 {
20037 let mut offset = 0;
20038 let offsets: Vec<_> = lens
20039 .iter()
20040 .map(|&n| {
20041 let start = offset;
20042 offset += n;
20043 start
20044 })
20045 .collect();
20046 for (r, terms) in terms.iter().enumerate() {
20047 for &(g, slot, w) in terms {
20048 for (o, &v) in out[r * hidden..(r + 1) * hidden]
20049 .iter_mut()
20050 .zip(&ds[offsets[g] + slot])
20051 {
20052 *o += w * v;
20053 }
20054 }
20055 }
20056 } else {
20057 for (r, jobs) in jobs.iter().enumerate() {
20058 if !jobs.is_empty() {
20059 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
20060 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
20061 attention::recycle_buf(&mut row);
20062 }
20063 }
20064 }
20065 out
20066}
20067
20068fn moe_ffn_cpu(
20070 m: &MoeFfn,
20071 x: &[f32],
20072 idx: &[usize],
20073 p: &[f32],
20074 wsum: f32,
20075 pool: Option<&Pool>,
20076) -> Vec<f32> {
20077 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
20078 return out;
20079 }
20080 let mut out = attention::take_buf(x.len());
20081 for &e in idx {
20082 let mut eo = dense_ffn(&m.experts[e], x, pool);
20083 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
20084 for i in 0..out.len() {
20085 out[i] += w * eo[i];
20086 }
20087 attention::recycle_buf(&mut eo);
20088 }
20089 if let Some((se, gate)) = &m.shared {
20090 let mut so = dense_ffn(se, x, pool);
20091 let g = gate.as_ref().map_or(1.0, |gate| {
20092 let mut gl = [0.0f32; 1];
20093 gate.matvec(x, &mut gl, pool);
20094 1.0 / (1.0 + (-gl[0]).exp())
20095 });
20096 for i in 0..out.len() {
20097 out[i] += g * so[i];
20098 }
20099 attention::recycle_buf(&mut so);
20100 }
20101 out
20102}
20103
20104#[allow(clippy::too_many_arguments)]
20112pub(crate) fn mla_attention(
20113 w: &MlaWeights,
20114 normed: &[f32],
20115 cache: &mut crate::kv_cache::LayerKvCache,
20116 position: usize,
20117 inv_freq: &[f32],
20118 rope_scale: f32,
20119 eps: f64,
20120 pool: Option<&Pool>,
20121) -> Vec<f32> {
20122 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
20123 let hd = dr + dn;
20124 let mut q = vec![0.0f32; nh * hd];
20125 match (&w.q_a, &w.q_a_norm) {
20126 (Some(qa), Some(qn)) => {
20127 let mut t = vec![0.0f32; qa.rows()];
20128 qa.matvec(normed, &mut t, pool);
20129 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
20130 w.q_proj.matvec(&tn, &mut q, pool);
20131 }
20132 _ => w.q_proj.matvec(normed, &mut q, pool),
20133 }
20134 let mut ca = vec![0.0f32; lora + dr];
20135 w.kv_a.matvec(normed, &mut ca, pool);
20136 let (c_lat, k_rope) = ca.split_at_mut(lora);
20137 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
20138 let mut kvb = vec![0.0f32; nh * (dn + dv)];
20139 w.kv_b.matvec(&latn, &mut kvb, pool);
20140 if !w.nope {
20141 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
20142 }
20143 for h in 0..nh {
20144 if !w.nope {
20145 attention::rope_rotate_scaled(
20146 &mut q[h * hd..h * hd + dr],
20147 position,
20148 inv_freq,
20149 rope_scale,
20150 );
20151 }
20152 }
20153 let mut k = vec![0.0f32; nh * hd];
20154 let mut v = vec![0.0f32; nh * hd];
20155 for h in 0..nh {
20156 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
20157 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
20158 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
20159 }
20160 cache.append(&k, &v, &vec![true; nh]);
20161 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
20162 attention::recycle_buf(&mut imp);
20163 let mut ov = vec![0.0f32; nh * dv];
20164 for h in 0..nh {
20165 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
20166 }
20167 let mut out = vec![0.0f32; w.o_proj.rows()];
20168 w.o_proj.matvec(&ov, &mut out, pool);
20169 out
20170}
20171
20172fn dense_moe_ffn(
20179 dm: &DenseMoeFfn,
20180 x_normed: &[f32],
20181 h_raw: &[f32],
20182 eps: f64,
20183 norm_style: NormStyle,
20184 pool: Option<&Pool>,
20185) -> Vec<f32> {
20186 let mut d = dense_ffn(&dm.dense, x_normed, pool);
20187 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
20188 let m = &dm.moe;
20189 let ne = m.experts.len();
20190 let mut logits = vec![0.0f32; ne];
20191 if m.router_input_norm {
20192 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
20193 let inv = 1.0 / (ss + eps as f32).sqrt();
20194 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
20195 m.router.matvec(&xr, &mut logits, pool);
20196 } else {
20197 m.router.matvec(h_raw, &mut logits, pool);
20198 }
20199 let (idx, p, wsum) = moe_route(&logits, m, None);
20200 {
20201 let mut st = m.stats.borrow_mut();
20202 if st.len() < ne {
20203 st.resize(ne, 0);
20204 }
20205 for &e in &idx {
20206 st[e] += 1;
20207 }
20208 }
20209 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
20210 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
20211 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
20212 for (di, mi) in d.iter_mut().zip(&mo) {
20213 *di += mi;
20214 }
20215 d
20216}
20217
20218fn moe_gpu_refused(why: &'static str) {
20225 use std::sync::atomic::{AtomicBool, Ordering};
20226 static SAID: AtomicBool = AtomicBool::new(false);
20227 if !SAID.swap(true, Ordering::Relaxed) {
20228 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
20229 }
20230}
20231
20232fn moe_ffn_gpu(
20233 m: &MoeFfn,
20234 x: &[f32],
20235 idx: &[usize],
20236 p: &[f32],
20237 wsum: f32,
20238 pool: Option<&Pool>,
20239) -> Option<Vec<f32>> {
20240 use crate::gpu::MoeJob;
20241
20242 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
20243 let mut model_ref = None;
20244 for &e in idx {
20245 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
20246 moe_gpu_refused("push_job(expert)");
20247 return None;
20248 }
20249 }
20250 if let Some((se, gate)) = &m.shared {
20251 let g = gate.as_ref().map_or(1.0, |gate| {
20252 let mut gl = [0.0f32; 1];
20253 gate.matvec(x, &mut gl, pool);
20254 1.0 / (1.0 + (-gl[0]).exp())
20255 });
20256 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
20257 moe_gpu_refused("push_job(shared)");
20258 return None;
20259 }
20260 }
20261 let Some(model) = model_ref else {
20262 moe_gpu_refused("no model_ref");
20263 return None;
20264 };
20265 let hidden = jobs[0].down.1;
20266 let mut out = vec![0.0f32; hidden];
20267 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20268 Some(out)
20269 } else {
20270 moe_gpu_refused("gpu::moe_block");
20271 None
20272 }
20273}
20274
20275fn ffn_forward(
20277 ffn: &FfnKind,
20278 x: &[f32],
20279 pool: Option<&Pool>,
20280 experts_allowed: Option<&[bool]>,
20281) -> Vec<f32> {
20282 match ffn {
20283 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
20284 FfnKind::Dense(d) => dense_ffn(d, x, pool),
20285 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
20286 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20290 }
20291}
20292
20293fn ffn_forward_pair(
20297 ffn: &FfnKind,
20298 x1: &[f32],
20299 x2: &[f32],
20300 pool: Option<&Pool>,
20301 experts_allowed: Option<&[bool]>,
20302) -> (Vec<f32>, Vec<f32>) {
20303 let d = match ffn {
20304 FfnKind::Dense(d) if !d.segs.is_empty() => {
20307 return (
20308 tube_ffn(d, x1, 1, pool, None),
20309 tube_ffn(d, x2, 1, pool, None),
20310 );
20311 }
20312 FfnKind::Dense(d) => d,
20313 FfnKind::Moe(m) => {
20314 return (
20315 moe_ffn(m, x1, pool, experts_allowed),
20316 moe_ffn(m, x2, pool, experts_allowed),
20317 );
20318 }
20319 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20320 };
20321 let inter = d.gate_proj.rows();
20322 FFN_SCRATCH.with(|s| {
20323 let mut s = s.borrow_mut();
20324 let [g1, g2, u1, u2] = &mut *s;
20325 g1.resize(inter, 0.0);
20326 g2.resize(inter, 0.0);
20327 u1.resize(inter, 0.0);
20328 u2.resize(inter, 0.0);
20329 QTensor::matvec2_many(
20332 [&d.gate_proj, &d.up_proj],
20333 x1,
20334 x2,
20335 [g1.as_mut_slice(), u1.as_mut_slice()],
20336 [g2.as_mut_slice(), u2.as_mut_slice()],
20337 pool,
20338 );
20339 for i in 0..inter {
20340 g1[i] = d.act.combine(g1[i], u1[i]);
20341 g2[i] = d.act.combine(g2[i], u2[i]);
20342 }
20343 let mut o1 = attention::take_buf(d.down_proj.rows());
20344 let mut o2 = attention::take_buf(d.down_proj.rows());
20345 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
20346 (o1, o2)
20347 })
20348}
20349
20350#[cfg(test)]
20351mod tests {
20352
20353 #[test]
20358 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
20359 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
20360 let dense_card = ChunkStackFacts {
20361 plain_dense: true,
20362 discrete: true,
20363 gpu_on: true,
20364 ..Default::default()
20365 };
20366 assert!(dense_card.dense_on_discrete());
20367 assert_eq!(
20369 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
20370 DISCRETE_DENSE_PREFILL_CHUNK
20371 );
20372 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
20373 for (label, facts) in [
20374 (
20375 "GDN hybrid / MoE / DeepSeek stack",
20376 ChunkStackFacts {
20377 plain_dense: false,
20378 ..dense_card
20379 },
20380 ),
20381 (
20382 "integrated GPU",
20383 ChunkStackFacts {
20384 discrete: false,
20385 ..dense_card
20386 },
20387 ),
20388 (
20389 "CPU only",
20390 ChunkStackFacts {
20391 gpu_on: false,
20392 discrete: false,
20393 ..dense_card
20394 },
20395 ),
20396 (
20397 "capacity split",
20398 ChunkStackFacts {
20399 capacity_split: true,
20400 ..dense_card
20401 },
20402 ),
20403 (
20404 "multi-GPU plan",
20405 ChunkStackFacts {
20406 multi_gpu: true,
20407 ..dense_card
20408 },
20409 ),
20410 (
20411 "O(1) layers",
20412 ChunkStackFacts {
20413 o1: true,
20414 ..dense_card
20415 },
20416 ),
20417 ] {
20418 assert!(!facts.dense_on_discrete(), "{label}");
20419 assert_eq!(
20420 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
20421 48,
20422 "{label} keeps the historical x86 chunk"
20423 );
20424 }
20425 for dense in [false, true] {
20427 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
20428 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
20429 }
20430 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
20432 for dense in [false, true] {
20433 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
20434 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
20435 }
20436 }
20437 }
20438
20439 #[test]
20440 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
20441 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
20442 let full = |host_rows, device_rows| ReuseLayer {
20443 full: true,
20444 host_rows,
20445 device_rows,
20446 device_state: false,
20447 };
20448 assert_eq!(
20451 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
20452 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
20453 );
20454 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
20456 assert_eq!(
20458 kv_reuse_plan(339, &[full(339, Some(345))]),
20459 ReusePlan::Ready
20460 );
20461 assert_eq!(
20463 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
20464 ReusePlan::Pull(vec![(0, 300, 339)])
20465 );
20466 assert_eq!(
20468 kv_reuse_plan(339, &[full(300, Some(320))]),
20469 ReusePlan::Fresh
20470 );
20471 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
20472 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
20473 let conv = |device_state| ReuseLayer {
20476 full: false,
20477 host_rows: 0,
20478 device_rows: None,
20479 device_state,
20480 };
20481 assert_eq!(
20482 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
20483 ReusePlan::Fresh
20484 );
20485 assert_eq!(
20486 kv_reuse_plan(339, &[conv(false), full(339, None)]),
20487 ReusePlan::Ready
20488 );
20489 }
20490
20491 #[test]
20492 fn nll_graph_policy_scopes_only_the_fused_head() {
20493 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
20494 ("vulkan graph", true, true, false, true, false),
20496 ("native Metal graph", true, true, true, true, true),
20498 ("masked", false, true, false, false, false),
20500 ("graph disabled", true, false, true, false, false),
20501 ] {
20502 let (graph_quality, graph_head_required) =
20503 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
20504 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
20505 assert_eq!(graph_head_required, want_head, "{label}: fused head");
20506 }
20507 }
20508
20509 #[test]
20510 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
20511 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
20512 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
20513 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
20514 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
20515 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
20516 }
20517
20518 #[test]
20519 fn cancel_flag_stops_generation() {
20520 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
20521 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20524 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
20525 assert_eq!(r.finish_reason, "cancelled");
20526 assert!(
20527 r.token_ids.is_empty(),
20528 "no tokens after cancel: {:?}",
20529 r.token_ids
20530 );
20531 assert_eq!(p.kv_cache.seq_len(), 0);
20532 assert!(p.kv_history.is_empty());
20533 assert!(!p.graph_want_logits);
20534 assert!(p.graph_logits.is_none());
20535 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
20537 assert_ne!(r2.finish_reason, "cancelled");
20538 }
20539 use super::*;
20540
20541 #[test]
20542 fn explicit_sliding_schedule_selects_local_rope_profile() {
20543 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
20548 p.swa = Some((1024, usize::MAX));
20549 p.sliding_layers = Some(vec![true, true, true, false]);
20550 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
20551 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
20552 p.rope_scale = 1.277_258_9;
20553 p.rope_scale_local = 1.0;
20554
20555 for li in 0..3 {
20556 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
20557 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
20558 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
20559 }
20560 assert_eq!(p.layer_window(3), None);
20561 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
20562 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
20563 }
20564
20565 #[test]
20573 fn dynamic_ffn_equals_the_zeroing_arm() {
20574 let (hidden, inter) = (8usize, 32usize);
20575 let synth = |n: usize, salt: usize| -> Vec<f32> {
20576 (0..n)
20577 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
20578 .collect()
20579 };
20580 let down = synth(hidden * inter, 3);
20581 let mut down_t = vec![0.0f32; inter * hidden];
20582 for r in 0..hidden {
20583 for c in 0..inter {
20584 down_t[c * hidden + r] = down[r * inter + c];
20585 }
20586 }
20587 let d = DenseFfn {
20588 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20589 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20590 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
20591 act: Act::Silu,
20592 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
20593 segs: Vec::new(),
20594 };
20595 let x = synth(hidden, 11);
20596 let k = 12usize;
20597 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
20598 let mut g = vec![0.0f32; inter];
20600 d.gate_proj.matvec(&x, &mut g, None);
20601 let mut u = vec![0.0f32; inter];
20602 d.up_proj.matvec(&x, &mut u, None);
20603 for v in g.iter_mut() {
20604 *v = inference::silu(*v);
20605 }
20606 keep_top_k(&mut g, k);
20607 for i in 0..inter {
20608 g[i] *= u[i];
20609 }
20610 let mut want = vec![0.0f32; hidden];
20611 d.down_proj.matvec(&g, &mut want, None);
20612 for (a, b) in want.iter().zip(&got) {
20613 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
20614 }
20615 }
20616
20617 #[test]
20623 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
20624 let (hidden, core, tube) = (8usize, 12usize, 8usize);
20625 let inter = core + tube;
20626 let synth = |n: usize, salt: usize| -> Vec<f32> {
20627 (0..n)
20628 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
20629 .collect()
20630 };
20631 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
20632 let d_all = synth(hidden * inter, 3);
20633 let dense = DenseFfn {
20635 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
20636 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
20637 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
20638 act: Act::Silu,
20639 down_t: None,
20640 segs: Vec::new(),
20641 };
20642 let rows =
20643 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
20644 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
20645 let mut o = Vec::with_capacity(hidden * (b - a));
20646 for r in 0..hidden {
20647 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
20648 }
20649 o
20650 };
20651 let tubed = DenseFfn {
20652 down_t: None,
20653 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
20654 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
20655 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
20656 act: Act::Silu,
20657 segs: vec![FfnSeg {
20658 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
20659 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
20660 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
20661 start: core,
20662 width: tube,
20663 }],
20664 };
20665 let x = synth(hidden, 7);
20666 let want = dense_ffn(&dense, &x, None);
20667 let got = tube_ffn(&tubed, &x, 1, None, None);
20668 for (a, b) in want.iter().zip(&got) {
20669 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
20670 }
20671 let mut bits = vec![0u8; inter.div_ceil(8)];
20673 for n in 0..core {
20674 bits[n / 8] |= 1 << (n % 8);
20675 }
20676 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20677 let masked = dense_ffn_masked(&dense, &x, None, &bits);
20678 for (a, b) in masked.iter().zip(&closed) {
20679 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
20680 }
20681 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20683 for (a, b) in closed.iter().zip(&batch) {
20684 assert_eq!(a, b, "batch arm disagrees with decode arm");
20685 }
20686 }
20687
20688 #[test]
20690 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
20691 let (hidden, inter) = (16usize, 40usize);
20692 let synth = |n: usize, salt: usize| -> Vec<f32> {
20693 (0..n)
20694 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
20695 .collect()
20696 };
20697 let d = DenseFfn {
20698 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20699 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20700 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
20701 act: Act::Silu,
20702 down_t: None,
20703 segs: Vec::new(),
20704 };
20705 let x = synth(hidden, 9);
20706 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
20708
20709 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
20710
20711 let mut g = vec![0.0f32; inter];
20713 d.gate_proj.matvec(&x, &mut g, None);
20714 let mut u = vec![0.0f32; inter];
20715 d.up_proj.matvec(&x, &mut u, None);
20716 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
20717 for i in 0..inter {
20718 g[i] = if act_set.contains(&(i as u16)) {
20719 inference::silu(g[i]) * u[i]
20720 } else {
20721 0.0
20722 };
20723 }
20724 let mut reference = vec![0.0f32; hidden];
20725 d.down_proj.matvec(&g, &mut reference, None);
20726
20727 let max_d = sparse
20728 .iter()
20729 .zip(&reference)
20730 .map(|(a, b)| (a - b).abs())
20731 .fold(0.0f32, f32::max);
20732 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
20733 }
20734
20735 fn attach_test_mtp(p: &mut Pipeline) {
20737 let (h, inter, heads, kv, hd) = (
20738 p.hidden_size,
20739 p.intermediate_size,
20740 p.num_heads,
20741 p.num_kv_heads,
20742 p.head_dim,
20743 );
20744 let synth = |n: usize, salt: usize| -> Vec<f32> {
20745 (0..n)
20746 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
20747 .collect()
20748 };
20749 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
20750 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20751 };
20752 p.mtp = Some(MtpModule {
20753 enorm: vec![1.0; h],
20754 hnorm: vec![1.0; h],
20755 eh_proj: qt(h, 2 * h, 301),
20756 layer: LayerWeights {
20757 input_norm: vec![1.0; h],
20758 post_norm: vec![1.0; h],
20759 attn_out_norm: None,
20760 ffn_out_norm: None,
20761 layer_scale: None,
20762 ffn: FfnKind::Dense(DenseFfn {
20763 gate_proj: qt(inter, h, 315),
20764 up_proj: qt(inter, h, 316),
20765 down_proj: qt(h, inter, 317),
20766 act: Act::Silu,
20767 down_t: None,
20768 segs: Vec::new(),
20769 }),
20770 attn: AttnKind::Full {
20771 bias: None,
20772 wq: qt(heads * hd, h, 311),
20773 wk: qt(kv * hd, h, 312),
20774 wv: qt(kv * hd, h, 313),
20775 wo: qt(h, heads * hd, 314),
20776 q_norm: None,
20777 k_norm: None,
20778 output_gate: false,
20779 softplus_gate: None,
20780 },
20781 },
20782 final_norm: vec![1.0; h],
20783 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
20784 });
20785 }
20786
20787 #[test]
20788 fn speculative_equals_vanilla_greedy() {
20789 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20793 let run = |spec: bool| {
20794 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20795 p.sampler_config.temperature = 0.0;
20796 attach_test_mtp(&mut p);
20797 p.speculative = spec;
20798 let r = p.generate("abcdef", 12, None, None).unwrap();
20799 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
20800 };
20801 let (vanilla, d0, _) = run(false);
20802 let (spec, d1, a1) = run(true);
20803 assert_eq!(d0, 0, "vanilla path must not draft");
20804 assert!(d1 > 0, "speculative path must draft");
20805 assert_eq!(
20806 vanilla, spec,
20807 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
20808 );
20809 }
20810
20811 #[test]
20812 fn speculative_accepts_constant_oracle() {
20813 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20815 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20816 p.sampler_config.temperature = 0.0;
20817 p.sampler_config.repetition_penalty = 1.0;
20818 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
20821 attach_test_mtp(&mut p);
20822 p.speculative = true;
20823 let r = p.generate("abcd", 10, None, None).unwrap();
20824 assert!(r.mtp_drafted > 0);
20825 assert_eq!(
20826 r.mtp_accepted, r.mtp_drafted,
20827 "constant logits → every draft accepted"
20828 );
20829 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20832 }
20833
20834 #[test]
20835 fn empty_prompt_is_an_error_not_a_panic() {
20836 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20837 let r = p.generate("", 4, None, None);
20838 assert!(r.is_err(), "empty prompt must be a clean error");
20839 }
20840
20841 #[test]
20842 fn every_token_enters_kv_exactly_once() {
20843 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20844 p.sampler_config.temperature = 0.0;
20846 let r = p.generate("abc", 2, None, None).unwrap();
20847 assert_eq!(r.prompt_tokens, 3);
20848 assert_eq!(
20852 p.kv_cache.seq_len(),
20853 3 + r.tokens_generated - 1,
20854 "each token must be cached exactly once (v1 cached the last prompt token twice)"
20855 );
20856 }
20857
20858 #[test]
20859 fn generation_is_reproducible_with_seed() {
20860 let run = || {
20861 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20862 p.generate("hello", 8, None, None).unwrap().token_ids
20863 };
20864 assert_eq!(run(), run());
20865 }
20866
20867 #[test]
20868 fn resetting_sampler_restarts_the_seeded_stream() {
20869 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20870 let config = SamplerConfig {
20871 seed: Some(1234),
20872 ..SamplerConfig::default()
20873 };
20874 p.set_sampler_config(config.clone());
20875 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20876 p.set_sampler_config(config);
20877 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20878 assert_eq!(first, second);
20879 }
20880
20881 #[test]
20882 fn eviction_bounds_the_cache() {
20883 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20884 p.kv_cache.max_seq_len = 6;
20885 p.sampler_config.temperature = 0.0;
20886 let _ = p.generate("abcd", 12, None, None).unwrap();
20887 assert!(
20888 p.kv_cache.seq_len() <= 6 + 1,
20889 "cache must stay bounded by max_seq_len (got {})",
20890 p.kv_cache.seq_len()
20891 );
20892 }
20893
20894 #[test]
20895 fn confidence_matches_tokens_and_is_a_probability() {
20896 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20897 p.sampler_config.temperature = 0.0;
20898 p.sampler_config.repetition_penalty = 1.0;
20899 let r = p.generate("abcd", 10, None, None).unwrap();
20900 assert_eq!(
20901 r.token_confidence.len(),
20902 r.token_ids.len(),
20903 "one confidence per emitted token"
20904 );
20905 for &c in &r.token_confidence {
20906 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20907 }
20908 let logits = [1.0f32, 3.0, 0.5, 3.0];
20910 let p0 = top1_prob_t(&logits, 1, 1.0);
20911 let p1 = top1_prob_t(&logits, 3, 1.0);
20912 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20913 assert!(p0 > 0.0 && p0 < 1.0);
20914 let sharp = top1_prob_t(&logits, 1, 1.0);
20916 let soft = top1_prob_t(&logits, 1, 2.0);
20917 assert!(soft < sharp, "higher temperature lowers peak confidence");
20918 }
20919
20920 #[test]
20921 fn trace_is_opt_in_and_parallels_the_output() {
20922 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20924 p.sampler_config.temperature = 0.0;
20925 p.sampler_config.repetition_penalty = 1.0;
20926 let r = p.generate("abcd", 10, None, None).unwrap();
20927 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20928
20929 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20931 p.sampler_config.temperature = 0.0;
20932 p.sampler_config.repetition_penalty = 1.0;
20933 p.set_trace(true);
20934 let r = p.generate("abcd", 10, None, None).unwrap();
20935 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20936 for (i, tr) in r.traces.iter().enumerate() {
20937 assert_eq!(tr.t, i, "trace index is sequential");
20938 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20939 assert_eq!(
20940 tr.confidence, r.token_confidence[i],
20941 "trace confidence matches the confidence channel"
20942 );
20943 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20945 }
20946 }
20947
20948 #[test]
20949 fn explain_prefill_logits_match_greedy_first_token() {
20950 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20954 p.sampler_config.temperature = 0.0;
20955 p.sampler_config.repetition_penalty = 1.0;
20956 let ids = p.tokenizer.encode("abcd");
20957 let logits = p.prefill_next_logits(&ids, None);
20958 let argmax = logits
20959 .iter()
20960 .enumerate()
20961 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20962 .unwrap()
20963 .0 as u32;
20964 let r = p.generate("abcd", 1, None, None).unwrap();
20965 assert_eq!(
20966 argmax, r.token_ids[0],
20967 "explain preview must match greedy emit"
20968 );
20969 }
20970
20971 #[test]
20972 fn laguna_shared_expert_is_unconditionally_added() {
20973 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20974 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20975 let zero_dense = || DenseFfn {
20976 gate_proj: matrix(vec![0.0; 4]),
20977 up_proj: matrix(vec![0.0; 4]),
20978 down_proj: matrix(vec![0.0; 4]),
20979 act: Act::Silu,
20980 down_t: None,
20981 segs: Vec::new(),
20982 };
20983 let shared = DenseFfn {
20984 gate_proj: identity(),
20985 up_proj: identity(),
20986 down_proj: identity(),
20987 act: Act::Silu,
20988 down_t: None,
20989 segs: Vec::new(),
20990 };
20991 let x = [1.0, 2.0];
20992 let expected = dense_ffn(&shared, &x, None);
20993 let moe = MoeFfn {
20994 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20995 experts: vec![zero_dense()],
20996 top_k: 1,
20997 norm_topk_prob: true,
20998 router_sigmoid: true,
20999 sigmoid_denom_eps: 1e-6,
21000 expert_bias: None,
21001 routed_scaling: 1.0,
21002 route_tau: None,
21003 shared: Some((shared, None)),
21004 stats: std::cell::RefCell::new(Vec::new()),
21005 act_sq: std::cell::RefCell::new(Vec::new()),
21006 act_rows: std::cell::RefCell::new(Vec::new()),
21007 mask: None,
21008 per_expert_scale: None,
21009 router_input_norm: false,
21010 resonance: None,
21011 grown: Vec::new(),
21012 };
21013 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
21014 for (actual, expected) in actual.iter().zip(expected) {
21015 assert!((actual - expected).abs() < 1e-6);
21016 }
21017 }
21018
21019 fn mimo_test_pipeline() -> Pipeline {
21028 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
21029 let kvh = [1usize, 2, 2, 1];
21030 let synth = |n: usize, salt: usize| -> Vec<f32> {
21031 (0..n)
21032 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21033 .collect()
21034 };
21035 let qt = |rows: usize, cols: usize, salt: usize| {
21036 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21037 };
21038 let dense = |inter: usize, salt: usize| DenseFfn {
21039 gate_proj: qt(inter, hs, salt),
21040 up_proj: qt(inter, hs, salt + 1),
21041 down_proj: qt(hs, inter, salt + 2),
21042 act: Act::Silu,
21043 down_t: None,
21044 segs: Vec::new(),
21045 };
21046 let layers: Vec<LayerWeights> = (0..4)
21047 .map(|li| LayerWeights {
21048 input_norm: vec![1.0; hs],
21049 post_norm: vec![1.0; hs],
21050 attn_out_norm: None,
21051 ffn_out_norm: None,
21052 layer_scale: None,
21053 ffn: if li == 0 {
21054 FfnKind::Dense(dense(inter, 50))
21055 } else {
21056 FfnKind::Moe(MoeFfn {
21057 router: qt(4, hs, 60 + li),
21058 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
21059 top_k: 2,
21060 norm_topk_prob: true,
21061 router_sigmoid: true,
21062 sigmoid_denom_eps: 1e-6,
21063 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
21064 routed_scaling: 1.0,
21065 route_tau: None,
21066 shared: None,
21067 stats: std::cell::RefCell::new(Vec::new()),
21068 act_sq: std::cell::RefCell::new(Vec::new()),
21069 act_rows: std::cell::RefCell::new(Vec::new()),
21070 mask: None,
21071 per_expert_scale: None,
21072 router_input_norm: false,
21073 resonance: None,
21074 grown: Vec::new(),
21075 })
21076 },
21077 attn: AttnKind::Full {
21078 wq: qt(nh * hd, hs, li * 10 + 1),
21079 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
21080 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
21081 wo: qt(hs, nh * vd, li * 10 + 4),
21082 q_norm: None,
21083 k_norm: None,
21084 output_gate: false,
21085 softplus_gate: None,
21086 bias: None,
21087 },
21088 })
21089 .collect();
21090 let mut p = Pipeline::new(
21091 Tokenizer::byte_level(),
21092 PipelineWeights {
21093 embed_tokens: qt(vocab, hs, 100),
21094 layers,
21095 lm_head: qt(vocab, hs, 200),
21096 final_norm: vec![1.0; hs],
21097 },
21098 hs,
21099 inter,
21100 nh,
21101 1, hd,
21103 4,
21104 4,
21105 false,
21106 vocab,
21107 1e-6,
21108 1e7,
21109 NormStyle::Qwen,
21110 4096,
21111 SamplerConfig {
21112 seed: Some(7),
21113 ..Default::default()
21114 },
21115 );
21116 p.layer_dump = None;
21118 p.set_rotary(4, 1e7);
21119 p.sliding_layers = Some(vec![false, true, true, false]);
21120 p.swa = Some((3, usize::MAX));
21121 p.rotary_dim_local = Some(4);
21122 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
21123 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
21124 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
21125 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
21126 p
21127 }
21128
21129 #[test]
21130 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
21131 let mut p = mimo_test_pipeline();
21132 p.speculative = false;
21133 p.ignore_eos = true;
21134 p.sampler_config.temperature = 0.0;
21135 p.sampler_config.repetition_penalty = 1.0;
21136 let a = vec![3, 5, 7, 9, 11, 13];
21137 let b = vec![4, 8, 12, 16, 20, 24];
21138 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
21139 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
21140 let actual = p
21144 .generate_from_embeds(&a, &rows, 8, None, None)
21145 .unwrap()
21146 .token_ids;
21147 assert_eq!(actual, expected);
21148 assert!(p.kv_history.is_empty());
21149 let mut extended = a.clone();
21150 extended.push(17);
21151 let after_media = p
21152 .generate_from_ids(&extended, 8, None, None)
21153 .unwrap()
21154 .token_ids;
21155 p.reset_session();
21156 let fresh = p
21157 .generate_from_ids(&extended, 8, None, None)
21158 .unwrap()
21159 .token_ids;
21160 assert_eq!(after_media, fresh);
21161 assert!(
21162 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
21163 .is_err()
21164 );
21165 p.reset_session();
21169 p.generate_from_ids(&a, 1, None, None).unwrap();
21170 let mut media_ids = p.kv_history.clone();
21171 assert!(!media_ids.is_empty());
21172 media_ids.extend_from_slice(&[19, 21, 23]);
21173 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
21174 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
21175 let mut oracle = mimo_test_pipeline();
21176 oracle.speculative = false;
21177 oracle.ignore_eos = true;
21178 oracle.sampler_config.temperature = 0.0;
21179 oracle.sampler_config.repetition_penalty = 1.0;
21180 let expected = oracle
21181 .generate_from_ids(&source_ids, 8, None, None)
21182 .unwrap()
21183 .token_ids;
21184 assert_eq!(
21185 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
21186 .unwrap()
21187 .token_ids,
21188 expected
21189 );
21190 assert!(p.kv_history.is_empty());
21191 let mut bad = rows;
21192 bad[0] = f32::NAN;
21193 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
21194 }
21195
21196 fn f32_bits(v: &[f32]) -> Vec<u32> {
21197 v.iter().map(|x| x.to_bits()).collect()
21198 }
21199
21200 #[test]
21207 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
21208 let mut p = mimo_test_pipeline();
21209 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
21210 assert_eq!(kv, vec![1, 2, 2, 1]);
21211 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
21212 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
21213 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
21214 let hs = p.hidden_size;
21215 let mut decode = Vec::new();
21216 for (pos, &id) in ids.iter().enumerate() {
21217 let e = p.embed_single(id);
21218 let h = p.forward_layers(&e, pos, None);
21219 decode.push(p.logits_from_hidden(&h));
21220 }
21221 for l in &p.kv_cache.layers {
21222 assert_eq!(l.seq_len, 12);
21223 assert_eq!(l.head_values(0).len(), 12 * 8);
21225 }
21226 assert!(decode.iter().flatten().all(|v| v.is_finite()));
21227
21228 p.clear_sequence_state();
21229 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21230 for pos in 0..ids.len() {
21231 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
21232 assert_eq!(
21233 f32_bits(&decode[pos]),
21234 f32_bits(&lg),
21235 "whole prompt, pos {pos}"
21236 );
21237 }
21238
21239 p.clear_sequence_state();
21240 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
21241 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
21242 for pos in 0..ids.len() {
21243 let row = if pos < 5 {
21244 &a[pos * hs..(pos + 1) * hs]
21245 } else {
21246 &b[(pos - 5) * hs..(pos - 4) * hs]
21247 };
21248 let lg = p.logits_from_hidden(row);
21249 assert_eq!(
21250 f32_bits(&decode[pos]),
21251 f32_bits(&lg),
21252 "two chunks, pos {pos}"
21253 );
21254 }
21255
21256 let last = |p: &mut Pipeline| {
21259 p.clear_sequence_state();
21260 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21261 p.logits_from_hidden(&hb[11 * hs..12 * hs])
21262 };
21263 let base = last(&mut p);
21264 let mut no_sinks = mimo_test_pipeline();
21265 for l in &mut no_sinks.kv_cache.layers {
21266 l.sinks = None;
21267 }
21268 assert_ne!(
21269 f32_bits(&last(&mut no_sinks)),
21270 f32_bits(&base),
21271 "sinks are live"
21272 );
21273 let mut wide = mimo_test_pipeline();
21274 wide.swa = Some((64, usize::MAX));
21275 assert_ne!(
21276 f32_bits(&last(&mut wide)),
21277 f32_bits(&base),
21278 "window is live"
21279 );
21280
21281 p.clear_sequence_state();
21283 p.ignore_eos = true;
21284 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
21285 assert_eq!(r.token_ids.len(), 4);
21286 }
21287
21288 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
21291 let (hs, nh) = (16usize, 4usize);
21292 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
21293 p.layer_dump = None;
21294 p.swa = Some((6, 4));
21295 p.proj_gate_sigmoid = true;
21296 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
21297 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
21298 let g: Vec<f32> = (0..nh * hs)
21299 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
21300 .collect();
21301 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
21302 }
21303 }
21304 p.swa_trim = trim;
21305 p
21306 }
21307
21308 #[test]
21314 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
21315 let mut a = spark_test_pipeline(None);
21316 let mut b = spark_test_pipeline(Some((2, 4)));
21317 assert_eq!(
21318 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
21319 vec![Some(6), Some(6), Some(6), None]
21320 );
21321 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
21322 let mut pos = 0usize;
21323 for &n in [5usize, 13, 7, 16].iter().cycle() {
21324 if pos >= ids.len() {
21325 break;
21326 }
21327 let end = (pos + n).min(ids.len());
21328 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21329 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21330 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
21331 pos = end;
21332 }
21333 for _ in 0..23 {
21334 let e = a.embed_single(((pos * 7) % 64) as u32);
21335 let ha = a.forward_layers(&e, pos, None);
21336 let hb = b.forward_layers(&e, pos, None);
21337 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
21338 for li in 0..3 {
21339 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
21340 }
21341 pos += 1;
21342 }
21343 for round in 0..9 {
21344 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
21345 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
21346 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
21347 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
21348 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
21349 if round % 2 == 1 {
21350 for p in [&mut a, &mut b] {
21352 for l in &mut p.kv_cache.layers {
21353 l.truncate_last(2);
21354 }
21355 }
21356 } else {
21357 pos += 2;
21358 }
21359 }
21360 let e = a.embed_single(9);
21361 let (ha, hb) = (
21362 a.forward_layers(&e, pos, None),
21363 b.forward_layers(&e, pos, None),
21364 );
21365 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
21366 for li in 0..3 {
21367 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
21368 assert!(lb.base() > 0, "layer {li} trimmed");
21369 assert_eq!(la.base(), 0);
21370 assert_eq!(lb.pos_len(), la.seq_len);
21371 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
21372 }
21373 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
21374 assert_eq!(
21375 (gb.base(), gb.seq_len),
21376 (0, ga.seq_len),
21377 "the global layer keeps all"
21378 );
21379 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
21380 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
21381 }
21382
21383 #[test]
21388 fn swa_trim_wire_handoff_continues_bitwise() {
21389 let mut a = spark_test_pipeline(None);
21390 let mut b = spark_test_pipeline(Some((2, 4)));
21391 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
21392 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
21393 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
21394 assert_eq!(f32_bits(&ha), f32_bits(&hb));
21395 let mut pos = ids.len();
21396 for _ in 0..5 {
21397 let e = a.embed_single(((pos * 5) % 64) as u32);
21398 assert_eq!(
21399 f32_bits(&a.forward_layers(&e, pos, None)),
21400 f32_bits(&b.forward_layers(&e, pos, None))
21401 );
21402 pos += 1;
21403 }
21404 let mut c = spark_test_pipeline(Some((2, 4)));
21405 for li in 0..4 {
21406 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
21407 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
21408 }
21409 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
21410 assert_eq!(c.kv_cache.seq_len(), pos);
21411 for step in 0..17 {
21412 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
21413 let ha = a.forward_layers(&e, pos, None);
21414 let hc = c.forward_layers(&e, pos, None);
21415 assert_eq!(
21416 f32_bits(&ha),
21417 f32_bits(&hc),
21418 "after the hand-off, step {step}"
21419 );
21420 pos += 1;
21421 }
21422 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
21423 }
21424
21425 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
21428 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
21429 let synth = |len: usize, salt: usize| -> Vec<f32> {
21430 (0..len)
21431 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
21432 .collect()
21433 };
21434 let qt = |rows: usize, cols: usize, salt: usize| {
21435 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21436 };
21437 let layers = (0..n)
21438 .map(|k| {
21439 let s = 500 + k * 40;
21440 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
21441 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
21442 MtpModule {
21443 enorm: vec![1.0; hs],
21444 hnorm: vec![1.0; hs],
21445 eh_proj: qt(hs, 2 * hs, s),
21446 layer: LayerWeights {
21447 input_norm: vec![1.0; hs],
21448 post_norm: vec![1.0; hs],
21449 attn_out_norm: None,
21450 ffn_out_norm: None,
21451 layer_scale: None,
21452 attn: AttnKind::Full {
21453 wq: qt(nh * hd, hs, s + 1),
21454 wk: qt(nkv * hd, hs, s + 2),
21455 wv: qt(nkv * vd, hs, s + 3),
21456 wo: qt(hs, nh * vd, s + 4),
21457 q_norm: None,
21458 k_norm: None,
21459 output_gate: false,
21460 softplus_gate: None,
21461 bias: None,
21462 },
21463 ffn: FfnKind::Dense(DenseFfn {
21464 gate_proj: qt(inter, hs, s + 5),
21465 up_proj: qt(inter, hs, s + 6),
21466 down_proj: qt(hs, inter, s + 7),
21467 act: Act::Silu,
21468 down_t: None,
21469 segs: Vec::new(),
21470 }),
21471 },
21472 final_norm: vec![1.0; hs],
21473 kv,
21474 }
21475 })
21476 .collect();
21477 mimo_mtp::MimoMtp::from_layers(layers)
21478 }
21479
21480 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
21481 p.clear_sequence_state();
21482 p.speculative = spec;
21483 p.ignore_eos = true;
21484 p.sampler_config.temperature = 0.0;
21485 p.generate_from_ids(ids, n, None, None).unwrap()
21486 }
21487
21488 #[test]
21494 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
21495 for post in [false, true] {
21498 let mut p = mimo_test_pipeline();
21499 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
21501 let mut st0 = mimo_test_mtp(3, 1.0);
21502 st0.post_norm_hidden = post;
21503 p.mimo_mtp = Some(st0);
21504 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
21505 let hs = p.hidden_size;
21506 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21507 p.mimo_note_rows(&hb, 0);
21508 let mut st = p.mimo_mtp.take().unwrap();
21509 let k = 3;
21512 let mut inc = Vec::new();
21513 for t in 0..ids.len() - k - 1 {
21514 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
21515 }
21516 let s = ids.len();
21519 let mut reference = vec![vec![0u32; k]; s - k - 1];
21520 let mut fresh = mimo_test_mtp(3, 1.0);
21521 for (layer, m) in fresh.layers.iter_mut().enumerate() {
21522 let n = s - layer - 1;
21523 let mut cats = vec![0.0f32; n * 2 * hs];
21524 for j in 0..n {
21525 let e = p.embed_single(ids[j + layer + 1]);
21526 let raw = &hb[j * hs..(j + 1) * hs];
21527 let g = if post {
21528 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
21529 } else {
21530 raw.to_vec()
21531 };
21532 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
21533 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
21534 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
21535 }
21536 let mut x = vec![0.0f32; n * hs];
21537 m.eh_proj.matmat(&cats, n, &mut x, None);
21538 p.mimo_mtp_block(m, &mut x, n, 0);
21539 for (t, row) in reference.iter_mut().enumerate() {
21540 let y = inference::rms_norm(
21541 &x[t * hs..(t + 1) * hs],
21542 &m.final_norm,
21543 p.rms_eps,
21544 p.norm_style,
21545 );
21546 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
21547 }
21548 }
21549 assert_eq!(inc, reference, "post_norm_hidden = {post}");
21550 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
21552 assert!(distinct.len() > 3, "{inc:?}");
21553 let last_t = ids.len() - k - 2;
21555 for m in &st.layers {
21556 assert_eq!(m.kv.seq_len, last_t + 1);
21557 }
21558 }
21559 }
21560
21561 #[test]
21568 fn mimo_speculative_greedy_equals_plain_greedy() {
21569 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21570 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
21571 let n = 24;
21572 let mut p = mimo_test_pipeline();
21573 let plain = mimo_greedy(&mut p, &ids, n, false);
21574 assert_eq!(plain.mtp_drafted, 0);
21575 assert_eq!(plain.token_ids.len(), n);
21576 let plain_kv = p.kv_cache.layers[0].seq_len;
21577
21578 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
21580 let spec = mimo_greedy(&mut p, &ids, n, true);
21581 assert!(spec.mtp_drafted > 0, "the round must draft");
21582 assert_eq!(spec.token_ids, plain.token_ids);
21583 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21584
21585 let mut truth: Vec<u32> = ids.clone();
21588 truth.extend(&plain.token_ids);
21589 let mut noisy = truth.clone();
21590 for (i, t) in noisy.iter_mut().enumerate() {
21591 if i % 5 == 0 {
21592 *t = (*t + 1) % 64;
21593 }
21594 }
21595 let mut st = mimo_test_mtp(3, 1.0);
21596 st.draft_override = Some(noisy);
21597 p.mimo_mtp = Some(st);
21598 let spec = mimo_greedy(&mut p, &ids, n, true);
21599 assert_eq!(spec.token_ids, plain.token_ids);
21600 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21601 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
21602 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
21603 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
21604 assert!(
21605 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
21606 "{:?}",
21607 stats.accept_hist
21608 );
21609 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
21610
21611 let mut st = mimo_test_mtp(3, 1.0);
21614 st.draft_override = Some(truth);
21615 p.mimo_mtp = Some(st);
21616 let spec = mimo_greedy(&mut p, &ids, n, true);
21617 assert_eq!(spec.token_ids, plain.token_ids);
21618 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
21619 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21620
21621 let off = mimo_greedy(&mut p, &ids, n, false);
21623 assert_eq!(off.token_ids, plain.token_ids);
21624 assert_eq!(off.mtp_drafted, 0);
21625 }
21626
21627 #[test]
21636 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
21637 let p = mimo_test_pipeline();
21638 assert_eq!(
21639 p.graph_attn_decline_reason(),
21640 Some("per-layer KV head counts")
21641 );
21642 assert_eq!(p.wgpu_graph_attn_decline(), None);
21643 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
21644 assert_eq!(
21645 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
21646 (1, 4, 4, None, false)
21647 );
21648 assert_eq!(g0.invf, p.inv_freq.as_slice());
21649 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
21650 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
21651 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
21652 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
21653 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
21654 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
21655 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
21656
21657 let emb = p.embed_single(3);
21660 let mut lg = Vec::new();
21661 assert!(
21662 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
21663 .is_none()
21664 );
21665 let mut hid = emb.clone();
21666 assert_eq!(
21667 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
21668 crate::gpu::BatchGraphOutcome::Declined
21669 );
21670 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
21671 assert!(p.try_multi_burst(3, 0, 4).is_none());
21672 assert!(
21673 p.graph_declines().is_empty(),
21674 "no attention decline logged: {:?}",
21675 p.graph_declines()
21676 );
21677 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
21682 assert_eq!(plain().graph_attn_decline_reason(), None);
21683 assert_eq!(plain().wgpu_graph_attn_decline(), None);
21684 assert!(
21685 plain().graph_attn_geom(0).is_none(),
21686 "uniform models keep the historical arms"
21687 );
21688 let mut q = plain();
21689 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
21690 assert_eq!(
21691 q.graph_attn_decline_reason(),
21692 Some("learned attention sinks")
21693 );
21694 assert_eq!(
21695 q.graph_attn_geom(1).unwrap().sink,
21696 Some(&[0.25f32, -0.25][..])
21697 );
21698 let mut q = plain();
21699 q.set_attn_geometry(None, Some(2)).unwrap();
21700 assert_eq!(
21701 q.graph_attn_decline_reason(),
21702 Some("V heads narrower than Q/K heads")
21703 );
21704 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
21705 let mut q = plain();
21706 q.sliding_layers = Some(vec![true, false]);
21707 q.swa = Some((4, usize::MAX));
21708 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
21709 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
21710 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
21711
21712 let mut q = mimo_test_pipeline();
21716 q.rope_scale = 2.0;
21717 q.rope_scale_local = 1.25;
21718 assert_eq!(q.wgpu_graph_attn_decline(), None);
21719 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
21720 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
21721
21722 let mut q = plain();
21725 q.rope_scale = 2.0;
21726 assert_eq!(
21727 q.wgpu_graph_attn_decline(),
21728 Some("scaled RoPE without per-layer geometry")
21729 );
21730 }
21731
21732 #[cfg(target_os = "macos")]
21738 #[test]
21739 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
21740 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
21744 p.swa = Some((16, usize::MAX));
21745 p.sliding_layers = Some(vec![true, true]);
21746 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
21747 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
21748
21749 assert_eq!(
21750 p.graph_attn_decline_reason(),
21751 Some("learned attention sinks")
21752 );
21753 let swa_graph = p.metal_graph_swa();
21754 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
21755 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
21756 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
21757 assert!(
21758 native_sink_attend,
21759 "the q1 sink admission must follow the SWA door"
21760 );
21761 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
21764 }
21765
21766 #[test]
21767 fn mimo_verify_rewind_preserves_lagging_host_caches() {
21768 let mut p = mimo_test_pipeline();
21769 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
21770 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
21771 for _ in 0..if li == 0 { 2 } else { 12 } {
21772 layer.append(&row, &row, &[]);
21773 }
21774 }
21775 p.mimo_verify_rewind(9).unwrap();
21776 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
21777 for layer in &p.kv_cache.layers[1..] {
21778 assert_eq!(layer.seq_len, 9);
21779 }
21780 }
21781
21782 #[test]
21786 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
21787 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
21788 let _ = std::fs::remove_dir_all(&dir);
21789 let mut p = mimo_test_pipeline();
21790 let hs = p.hidden_size;
21791 let ids = [5u32, 9, 11, 2, 40];
21792 p.layer_dump = Some(dir.join("decode"));
21793 for (pos, &id) in ids.iter().enumerate() {
21794 let e = p.embed_single(id);
21795 let _ = p.forward_layers(&e, pos, None);
21796 }
21797 p.clear_sequence_state();
21798 p.layer_dump = Some(dir.join("prefill"));
21799 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21800 for pos in 0..ids.len() {
21801 for li in 0..p.num_layers {
21802 let name = format!("p{pos:06}_l{li:02}.f32");
21803 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
21804 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
21805 assert_eq!(a.len(), hs * 4, "{name}");
21806 assert_eq!(a, b, "{name}");
21807 }
21808 }
21809 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
21810 let vals: Vec<f32> = last
21811 .chunks(4)
21812 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
21813 .collect();
21814 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
21815 let _ = std::fs::remove_dir_all(&dir);
21816 }
21817
21818 #[test]
21819 fn attn_geometry_and_sinks_are_validated() {
21820 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
21821 assert!(
21822 p.set_attn_geometry(Some(vec![2]), None).is_err(),
21823 "one entry per layer"
21824 );
21825 assert!(
21826 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
21827 "3 does not divide 4"
21828 );
21829 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
21830 assert!(p.set_attn_geometry(None, Some(0)).is_err());
21831 assert!(
21832 p.set_attn_geometry(None, Some(5)).is_err(),
21833 "V wider than the head"
21834 );
21835 p.set_attn_geometry(None, Some(4)).unwrap();
21836 assert_eq!(
21837 p.v_head_dim, None,
21838 "v_head_dim == head_dim is the uniform case"
21839 );
21840 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
21841 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
21842 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
21843 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
21844 assert!(
21845 p.kv_cache.layers[1].sinks.is_some(),
21846 "a reshape keeps the layer's sinks"
21847 );
21848 assert_eq!(p.layer_geom(1).0, 4);
21849 assert!(
21850 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
21851 "one sink per Q head"
21852 );
21853 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
21854 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
21855 }
21856
21857 #[test]
21860 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
21861 let cfg = || {
21862 Some(crate::nystrom::O1Cfg {
21863 layers: crate::nystrom::O1Layers::All,
21864 m: 4,
21865 w: 8,
21866 sink: 2,
21867 rect: crate::nystrom::O1Rect::Aggregate,
21868 })
21869 };
21870 let mut p = mimo_test_pipeline();
21871 p.set_o1(cfg());
21872 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
21873 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
21874 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
21875 q.sliding_layers = Some(vec![false, false, true]);
21876 q.swa = Some((4, usize::MAX));
21877 q.set_o1(cfg());
21878 assert_eq!(q.o1_flags, vec![true, false, false]);
21879 }
21880
21881 #[test]
21882 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21883 const B: usize = 19;
21884 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21885 p.set_o1(Some(crate::nystrom::O1Cfg {
21886 layers: crate::nystrom::O1Layers::All,
21887 m: 4,
21888 w: 8,
21889 sink: 2,
21890 rect: crate::nystrom::O1Rect::Aggregate,
21891 }));
21892 p.o1_begin_with_prefix(Some(B));
21893 let ids: Vec<u32> = (0..B as u32).collect();
21894 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21895
21896 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21897 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21898 let next = p.embed_single(B as u32);
21899 let _ = p.forward_layers(&next, B, None);
21900 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21901 }
21902
21903 #[test]
21904 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21905 const B: usize = 19;
21906 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21907 let gdn_cfg = crate::linear_core::GdnCfg {
21911 num_v_heads: 2,
21912 num_k_heads: 1,
21913 key_head_dim: 2,
21914 value_head_dim: 4,
21915 conv_kernel: 3,
21916 hidden_size: 8,
21917 rms_eps: 1e-6,
21918 output_gate_sigmoid: false,
21919 };
21920 let synth = |n: usize, salt: usize| -> Vec<f32> {
21921 (0..n)
21922 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21923 .collect()
21924 };
21925 let qt = |rows: usize, cols: usize, salt: usize| {
21926 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21927 };
21928 let c_dim = gdn_cfg.conv_dim();
21929 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21930 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21931 in_proj_qkv: qt(c_dim, 8, 1),
21932 in_proj_z: qt(vd, 8, 2),
21933 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21934 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21935 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21936 a_log: vec![0.2, 0.5],
21937 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21938 norm: vec![1.0; gdn_cfg.value_head_dim],
21939 out_proj: qt(8, vd, 7),
21940 });
21941 p.gdn_cfg = Some(gdn_cfg);
21942 p.set_o1(Some(crate::nystrom::O1Cfg {
21943 layers: crate::nystrom::O1Layers::All,
21944 m: 4,
21945 w: 8,
21946 sink: 2,
21947 rect: crate::nystrom::O1Rect::Aggregate,
21948 }));
21949 p.o1_begin_with_prefix(Some(B));
21950 for pos in 0..B - 2 {
21951 let emb = p.embed_single(pos as u32);
21952 let _ = p.forward_layers(&emb, pos, None);
21953 }
21954 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21955
21956 let e1 = p.embed_single((B - 2) as u32);
21957 let e2 = p.embed_single((B - 1) as u32);
21958 let _ = p.forward_pair(&e1, &e2, B - 2);
21959
21960 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21961 assert!(
21962 p.kv_cache
21963 .layers
21964 .iter()
21965 .enumerate()
21966 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21967 );
21968 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21969 assert_ne!(
21970 p.kv_cache.layers[0].linear_state, lane1_state,
21971 "real pair must commit GDN lane 2 before returning"
21972 );
21973 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21974 let next = p.embed_single(B as u32);
21975 let _ = p.forward_layers(&next, B, None);
21976 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21977 }
21978
21979 #[test]
21980 fn o1_error_observation_stays_terminal_until_reset() {
21981 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21982 p.set_o1(Some(crate::nystrom::O1Cfg {
21983 layers: crate::nystrom::O1Layers::All,
21984 m: 4,
21985 w: 8,
21986 sink: 2,
21987 rect: crate::nystrom::O1Rect::Aggregate,
21988 }));
21989 p.o1_begin();
21990 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21991
21992 assert!(p.o1_seal_checked().is_err());
21993 assert!(
21994 p.o1_seal_checked().is_err(),
21995 "retry must see the sticky error"
21996 );
21997 let k = vec![0.2f32; 4];
21998 let v = vec![0.3f32; 4];
21999 p.kv_cache.layers[0].append(&k, &v, &[]);
22000 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
22001
22002 p.reset_session();
22003 p.o1_begin();
22004 p.kv_cache.layers[0].append(&k, &v, &[]);
22005 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
22006 }
22007
22008 #[test]
22009 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
22010 let ids = vec![1u32, 2, 3, 4, 5, 6];
22011 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22012 p.graph_logits = Some(vec![123.0]);
22013 p.graph_want_logits = true;
22014 p.graph_failed
22015 .store(true, std::sync::atomic::Ordering::Relaxed);
22016 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22017 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
22018 assert!(err.contains("before NLL"));
22019 assert!(p.graph_logits.is_none());
22020 assert!(!p.graph_want_logits);
22021 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22022 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22023
22024 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22025 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22026 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22027 assert_eq!(actual.1, expected.1);
22028 assert!((actual.0 - expected.0).abs() < 1e-9);
22029 }
22030
22031 #[test]
22032 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
22033 let ids = vec![1u32, 2, 3, 4, 5, 6];
22034 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22035 p.nll_test_fail_at = Some(1);
22036 let err = p
22037 .nll_ids_from(&ids, 0)
22038 .expect_err("one-shot forward failure");
22039 assert!(err.contains("forward") || err.contains("score row"));
22040 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22041 assert!(!p.graph_want_logits);
22042 assert!(p.graph_logits.is_none());
22043 assert!(p.kv_history.is_empty());
22044
22045 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22046 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22047 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22048 assert_eq!(actual.1, expected.1);
22049 assert!((actual.0 - expected.0).abs() < 1e-9);
22050 }
22051
22052 #[test]
22053 fn nll_serial_failure_before_first_row_is_reported() {
22054 let ids = vec![1u32, 2, 3, 4];
22055 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22056 p.nll_test_force_serial = true;
22057 p.nll_test_fail_at = Some(0);
22058 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
22059 assert!(err.contains("serial forward"));
22060 assert!(p.kv_history.is_empty());
22061 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22062 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22063 }
22064
22065 #[test]
22066 fn ffn_probe_failure_discards_recorder_and_state() {
22067 let ids = vec![1u32, 2, 3, 4];
22068 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22069 p.nll_test_fail_at = Some(0);
22070 let err = p
22071 .probe_ffn_mass_batch(&ids)
22072 .expect_err("probe forward failure");
22073 assert!(err.contains("NLL"));
22074 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
22075 assert!(p.kv_history.is_empty());
22076 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22077 }
22078
22079 #[test]
22080 fn nll_test_controls_are_pipeline_scoped() {
22081 let ids = vec![1u32, 2, 3, 4];
22082 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22083 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22084 failing.nll_test_force_serial = true;
22085 failing.nll_test_fail_at = Some(0);
22086
22087 assert!(!failing.can_prefill_batched());
22088 assert!(unaffected.can_prefill_batched());
22089 let expected = unaffected
22090 .nll_ids_from(&ids, 0)
22091 .expect("unaffected pipeline remains usable");
22092 let err = failing
22093 .nll_ids_from(&ids, 0)
22094 .expect_err("failure injection belongs to failing pipeline");
22095 assert!(err.contains("serial forward"));
22096 assert!(failing.nll_test_fail_at.is_none());
22097 assert!(unaffected.can_prefill_batched());
22098 let actual = unaffected
22099 .nll_ids_from(&ids, 0)
22100 .expect("unaffected pipeline remains reusable");
22101 assert_eq!(actual.1, expected.1);
22102 assert!((actual.0 - expected.0).abs() < 1e-9);
22103 }
22104
22105 #[test]
22106 fn forward_ids_failure_channel_is_terminal_and_reusable() {
22107 let ids = vec![1u32, 2, 3, 4, 5, 6];
22108 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22109 p.graph_logits = Some(vec![123.0]);
22110 p.graph_want_logits = true;
22111 p.graph_failed
22112 .store(true, std::sync::atomic::Ordering::Relaxed);
22113 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22114
22115 let err = p
22116 .forward_ids(&ids, None)
22117 .expect_err("a failed forward must not become a valid head result");
22118 assert!(err.contains("forward_ids setup"));
22119 assert!(p.graph_logits.is_none());
22120 assert!(!p.graph_want_logits);
22121 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22122 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22123 assert_eq!(p.kv_cache.seq_len(), 0);
22124
22125 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
22126 .forward_ids(&ids, None)
22127 .expect("fresh forward_ids");
22128 let actual = p
22129 .forward_ids(&ids, None)
22130 .expect("pipeline remains reusable after a failed forward");
22131 assert_eq!(actual.len(), expected.len());
22132 assert!(
22133 actual
22134 .iter()
22135 .zip(expected)
22136 .all(|(a, b)| (a - b).abs() < 1e-9)
22137 );
22138 assert_eq!(p.kv_cache.seq_len(), ids.len());
22139 }
22140
22141 #[test]
22142 fn sigmoid_router_floor_is_explicit_per_architecture() {
22143 let zero = || DenseFfn {
22149 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22150 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22151 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22152 act: Act::Silu,
22153 down_t: None,
22154 segs: Vec::new(),
22155 };
22156 let m = MoeFfn {
22157 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
22158 experts: vec![zero(), zero()],
22159 top_k: 1,
22160 norm_topk_prob: true,
22161 router_sigmoid: true,
22162 sigmoid_denom_eps: 1e-20,
22163 expert_bias: None,
22164 routed_scaling: 2.5,
22165 route_tau: None,
22166 shared: None,
22167 stats: std::cell::RefCell::new(Vec::new()),
22168 act_sq: std::cell::RefCell::new(Vec::new()),
22169 act_rows: std::cell::RefCell::new(Vec::new()),
22170 mask: None,
22171 per_expert_scale: None,
22172 router_input_norm: false,
22173 resonance: None,
22174 grown: Vec::new(),
22175 };
22176 let logits = [-20.0f32, -20.0];
22177 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
22178 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
22179 let expected = (p[0] + 1e-20) / m.routed_scaling;
22180 assert!((flint_wsum - expected).abs() < 1e-15);
22181 assert!(legacy_wsum > flint_wsum * 100.0);
22182 }
22183
22184 #[test]
22185 fn resonance_scores_match_formula_and_stable_tie() {
22186 let r = Resonance {
22187 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
22189 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
22190 k: 1,
22191 bias: vec![1.5, 0.5, 0.0],
22192 shell: Vec::new(),
22193 };
22194 let x = [1.0f32, 1.0];
22195 let mut got = vec![0.0; 3];
22196 r.scores(&x, &mut got);
22197 assert!((got[0] - 0.5).abs() < 1e-6);
22201 assert!((got[1] - 0.5).abs() < 1e-6);
22202 assert!(got[2].abs() < 1e-6);
22203 let best = got
22204 .iter()
22205 .enumerate()
22206 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
22207 .map(|(i, _)| i);
22208 assert_eq!(best, Some(0));
22209 assert!(got.iter().all(|v| v.is_finite()));
22210 }
22211
22212 #[test]
22217 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
22218 let plain = Resonance {
22224 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
22225 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
22226 k: 1,
22227 bias: vec![1.5, 0.5, 0.0, 0.0],
22228 shell: Vec::new(),
22229 };
22230 let shelled = Resonance {
22231 mu: plain.mu.clone(),
22232 u: plain.u.clone(),
22233 k: 1,
22234 bias: plain.bias.clone(),
22235 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
22236 };
22237 assert!(!plain.has_shell());
22238 assert!(shelled.has_shell());
22239 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
22240 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
22241 set_growth_shell(Some(true));
22242 assert!(growth_shell_enabled());
22243 let x = [1.0f32, 1.0];
22246 plain.scores(&x, &mut a);
22247 shelled.scores(&x, &mut b);
22248 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
22249 assert!(a[2] == 0.0 && a[3] == 0.0);
22250 let xo = [3.0f32, 0.0];
22253 plain.scores(&xo, &mut a);
22254 shelled.scores(&xo, &mut b);
22255 assert_eq!(a[2], -6.0);
22256 assert_eq!(a[3], -6.0);
22257 assert_eq!(
22258 bits(&a[..3]),
22259 bits(&b[..3]),
22260 "trunk rows + the boundary row"
22261 );
22262 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
22263 assert_eq!(shelled.effective_shell(4), shelled.shell);
22264 set_growth_shell(Some(false));
22267 assert!(!growth_shell_enabled());
22268 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
22269 shelled.scores(&xo, &mut b);
22270 assert_eq!(bits(&a), bits(&b));
22271 set_growth_shell(None);
22272 let short = Resonance {
22275 shell: vec![f32::INFINITY, f32::INFINITY],
22276 ..shelled
22277 };
22278 set_growth_shell(Some(true));
22279 short.scores(&xo, &mut b);
22280 assert_eq!(bits(&a), bits(&b));
22281 set_growth_shell(None);
22282 }
22283
22284 #[test]
22288 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
22289 let zero = || DenseFfn {
22290 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22291 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22292 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22293 act: Act::Silu,
22294 down_t: None,
22295 segs: Vec::new(),
22296 };
22297 let moe = |sigmoid: bool| MoeFfn {
22298 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
22299 experts: vec![zero(), zero(), zero(), zero()],
22300 top_k: 1,
22301 norm_topk_prob: true,
22302 router_sigmoid: sigmoid,
22303 sigmoid_denom_eps: 1e-6,
22304 expert_bias: None,
22305 routed_scaling: 1.0,
22306 route_tau: None,
22307 shared: None,
22308 stats: std::cell::RefCell::new(Vec::new()),
22309 act_sq: std::cell::RefCell::new(Vec::new()),
22310 act_rows: std::cell::RefCell::new(Vec::new()),
22311 mask: None,
22312 per_expert_scale: None,
22313 router_input_norm: false,
22314 resonance: None,
22315 grown: Vec::new(),
22316 };
22317 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
22318 for sigmoid in [false, true] {
22319 let m = moe(sigmoid);
22320 let (idx, p, wsum) = moe_route(&logits, &m, None);
22321 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
22322 assert_eq!(p[1], 0.0);
22323 assert_eq!(p[3], 0.0);
22324 assert!(p[2] > p[0] && p[0] > 0.0);
22325 assert!(p.iter().all(|v| v.is_finite()));
22326 let w = p[2] / wsum;
22327 if sigmoid {
22328 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
22330 } else {
22331 assert_eq!(
22332 w, 1.0,
22333 "softmax: the renormalized top-1 weight is exactly 1"
22334 );
22335 }
22336 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
22339 assert_eq!(idx, vec![2]);
22340 }
22341 let m = moe(false);
22343 let (idx, _, _) = moe_route(
22344 &[
22345 f32::NEG_INFINITY,
22346 f32::NEG_INFINITY,
22347 f32::NEG_INFINITY,
22348 -9.0,
22349 ],
22350 &m,
22351 None,
22352 );
22353 assert_eq!(idx, vec![3]);
22354 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
22357 assert_eq!(idx, vec![0]);
22358 assert!(p.iter().all(|&v| v == 0.25));
22359 assert!(wsum.is_finite() && wsum > 0.0);
22360 }
22361
22362 #[test]
22367 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
22368 let zero = || DenseFfn {
22369 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22370 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22371 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22372 act: Act::Silu,
22373 down_t: None,
22374 segs: Vec::new(),
22375 };
22376 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
22377 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
22378 experts: vec![zero(), zero(), zero()],
22379 top_k: 1,
22380 norm_topk_prob: norm_topk,
22381 router_sigmoid: false,
22382 sigmoid_denom_eps: 1e-6,
22383 expert_bias: None,
22384 routed_scaling: 1.0,
22385 route_tau: None,
22386 shared: None,
22387 stats: std::cell::RefCell::new(Vec::new()),
22388 act_sq: std::cell::RefCell::new(Vec::new()),
22389 act_rows: std::cell::RefCell::new(Vec::new()),
22390 mask: None,
22391 per_expert_scale: None,
22392 router_input_norm: false,
22393 resonance: resonant.then(|| Resonance {
22394 mu: vec![0.0; 6],
22395 u: Vec::new(),
22396 k: 0,
22397 bias: vec![0.0; 3],
22398 shell: Vec::new(),
22399 }),
22400 grown: Vec::new(),
22401 };
22402 let lo = -0.1f32;
22405 let hi = f32::from_bits(lo.to_bits() - 1);
22406 assert!(hi > lo && hi - lo < 2f32.powi(-25));
22407 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
22408 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
22410 assert_eq!(
22411 idx,
22412 vec![0],
22413 "softmax tie → lower index (the gated contract)"
22414 );
22415 for norm in [true, false] {
22418 let m = moe(true, norm);
22419 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
22420 assert_eq!(idx, vec![1], "norm_topk {norm}");
22421 assert_eq!(p, vec![0.0, 1.0, 0.0]);
22422 assert_eq!(p[1] / wsum, 1.0);
22423 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
22426 assert_eq!(idx, vec![0]);
22427 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
22429 assert_eq!(idx, vec![2]);
22430 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
22431 assert_eq!(idx, vec![0]);
22432 assert_eq!(p[0] / wsum, 1.0);
22433 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
22436 assert_eq!(idx, vec![0]);
22437 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
22438 }
22439 }
22440}