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 anchor_core: Option<cortiq_core::AnchorCoreConfig>,
373 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
376 pub kv_prefix: KvPrefix,
380 pub last_prefill_tokens: usize,
383 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
386 pub rotary_dim_local: Option<usize>,
387 pub rope_scale: f32,
388 pub rope_scale_local: f32,
389 pub global_attn: Option<(usize, usize)>,
392 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
395 pub attn_v_norm: bool,
397 pub qk_norm_after_rope: bool,
399 pub proj_gate_sigmoid: bool,
402 pub final_softcap: Option<f32>,
404 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
408 pub attn_softcap: f32,
410 confidence_on: bool,
414 #[cfg(test)]
417 nll_test_fail_at: Option<usize>,
418 #[cfg(test)]
421 nll_test_force_serial: bool,
422}
423
424#[cfg(target_os = "macos")]
425impl Drop for Pipeline {
426 fn drop(&mut self) {
427 let _ = crate::gpu_metal::wait_replay();
429 crate::gpu::kv_mirror_drop(self.graph_kv_id);
430 }
431}
432
433#[cfg(not(target_os = "macos"))]
434impl Drop for Pipeline {
435 fn drop(&mut self) {
436 crate::gpu::graph_kv_reset(self.graph_kv_id);
440 }
441}
442
443pub struct PipelineWeights {
448 pub embed_tokens: QTensor,
450 pub layers: Vec<LayerWeights>,
452 pub lm_head: QTensor,
454 pub final_norm: Vec<f32>,
456}
457
458pub struct LayerWeights {
460 pub input_norm: Vec<f32>,
461 pub post_norm: Vec<f32>,
464 pub attn_out_norm: Option<Vec<f32>>,
467 pub layer_scale: Option<f32>,
469 pub ffn_out_norm: Option<Vec<f32>>,
472 pub ffn: FfnKind,
473 pub attn: AttnKind,
474}
475
476#[derive(Clone, Copy, PartialEq, Debug, Default)]
479pub enum Act {
480 #[default]
481 Silu,
482 GeluTanh,
483 Gelu,
485 Situ {
488 beta: f32,
489 linear_beta: f32,
490 },
491}
492
493impl Act {
494 pub fn from_arch(name: &str) -> Self {
495 if name == "gelu_tanh" {
496 Self::GeluTanh
497 } else if name == "gelu" {
498 Self::Gelu
499 } else {
500 Self::Silu
501 }
502 }
503
504 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
506 match arch.hidden_act.as_str() {
507 "situ" => Self::Situ {
508 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
509 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
510 },
511 other => Self::from_arch(other),
512 }
513 }
514
515 #[inline]
516 pub fn apply(self, x: f32) -> f32 {
517 match self {
518 Self::Silu => inference::silu(x),
519 Self::GeluTanh => inference::gelu_tanh(x),
520 Self::Gelu => inference::gelu_erf(x),
521 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
522 }
523 }
524
525 #[inline]
528 pub fn combine(self, g: f32, u: f32) -> f32 {
529 match self {
530 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
531 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
532 }
533 _ => self.apply(g) * u,
534 }
535 }
536
537 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
541 match self {
542 Self::Silu => Some(crate::gpu::GraphAct::Silu),
543 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
544 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
545 Self::Situ { .. } => None,
546 }
547 }
548}
549
550pub struct DenseFfn {
552 pub gate_proj: QTensor,
553 pub up_proj: QTensor,
554 pub down_proj: QTensor,
555 pub act: Act,
557 pub down_t: Option<QTensor>,
563 pub segs: Vec<FfnSeg>,
570}
571
572pub struct FfnSeg {
577 pub gate: QTensor,
578 pub up: QTensor,
579 pub down: QTensor,
580 pub start: usize,
581 pub width: usize,
582}
583
584pub enum FfnKind {
587 Dense(DenseFfn),
588 Moe(MoeFfn),
592 DenseMoe(Box<DenseMoeFfn>),
599}
600
601pub struct DenseMoeFfn {
603 pub dense: DenseFfn,
604 pub moe: MoeFfn,
605 pub post_norm_1: Vec<f32>,
607 pub pre_norm_2: Vec<f32>,
610 pub post_norm_2: Vec<f32>,
612}
613
614pub struct MoeFfn {
615 pub router: QTensor,
617 pub experts: Vec<DenseFfn>,
618 pub top_k: usize,
619 pub norm_topk_prob: bool,
620 pub router_sigmoid: bool,
623 pub expert_bias: Option<Vec<f32>>,
627 pub routed_scaling: f32,
630 pub route_tau: Option<f32>,
636 pub shared: Option<(DenseFfn, Option<QTensor>)>,
639 pub stats: std::cell::RefCell<Vec<u64>>,
643 pub act_sq: std::cell::RefCell<Vec<f64>>,
650 pub act_rows: std::cell::RefCell<Vec<f32>>,
656 pub mask: Option<Vec<bool>>,
661 pub per_expert_scale: Option<Vec<f32>>,
664 pub router_input_norm: bool,
668 pub resonance: Option<Resonance>,
672 pub grown: Vec<GrownExpert>,
678}
679
680#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct GrownExpert {
683 pub record: String,
685 pub record_index: usize,
687 pub layer: usize,
688 pub expert: usize,
692}
693
694pub struct Resonance {
696 pub mu: Vec<f32>,
698 pub u: Vec<f32>,
700 pub k: usize,
701 pub bias: Vec<f32>,
703 pub shell: Vec<f32>,
709}
710
711static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
714
715pub fn growth_shell_enabled() -> bool {
720 use std::sync::atomic::Ordering;
721 match GROWTH_SHELL.load(Ordering::Relaxed) {
722 1 => true,
723 2 => false,
724 _ => {
725 let off = std::env::var("CMF_GROWTH_SHELL")
726 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
727 .unwrap_or(false);
728 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
729 !off
730 }
731 }
732}
733
734pub fn set_growth_shell(on: Option<bool>) {
739 GROWTH_SHELL.store(
740 match on {
741 Some(true) => 1,
742 Some(false) => 2,
743 None => 0,
744 },
745 std::sync::atomic::Ordering::Relaxed,
746 );
747}
748
749impl Resonance {
750 pub fn has_shell(&self) -> bool {
752 self.shell.iter().any(|s| s.is_finite())
753 }
754
755 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
758 let mut out = vec![f32::INFINITY; ne];
759 if growth_shell_enabled() {
760 for (o, s) in out.iter_mut().zip(&self.shell) {
761 *o = *s;
762 }
763 }
764 out
765 }
766
767 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
772 let h = x.len();
773 let ne = out.len();
774 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
775 for e in 0..ne {
776 let mu = &self.mu[e * h..(e + 1) * h];
777 let mut d2 = 0.0f32;
778 for j in 0..h {
779 let d = x[j] - mu[j];
780 d2 += d * d;
781 }
782 let mut proj = 0.0f32;
783 for i in 0..self.k {
784 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
785 let mut p = 0.0f32;
786 for j in 0..h {
787 p += (x[j] - mu[j]) * u[j];
788 }
789 proj += p * p;
790 }
791 let err = d2 - proj;
792 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
793 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
794 out[e] = f32::NEG_INFINITY;
795 }
796 }
797 }
798}
799
800pub enum AttnKind {
803 Full {
805 wq: QTensor,
806 wk: QTensor,
807 wv: QTensor,
808 wo: QTensor,
809 q_norm: Option<Vec<f32>>,
810 k_norm: Option<Vec<f32>>,
811 output_gate: bool,
812 softplus_gate: Option<(QTensor, bool)>,
816 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
818 },
819 Linear(VmfPhaseWeights),
821 LinearGdn(GdnWeights),
823 ShortConv(ShortConvWeights),
826 Mla(Box<MlaWeights>),
834 Kda(Box<crate::linear_core::KdaWeights>),
838 Bounded(Box<crate::bounded::BoundedWeights>),
843}
844
845pub struct MlaWeights {
847 pub q_proj: QTensor,
851 pub q_a: Option<QTensor>,
854 pub q_a_norm: Option<Vec<f32>>,
855 pub kv_a: QTensor,
857 pub kv_a_norm: Vec<f32>,
859 pub kv_b: QTensor,
861 pub o_proj: QTensor,
863 pub nh: usize,
864 pub qk_rope: usize,
865 pub qk_nope: usize,
866 pub v_dim: usize,
867 pub lora: usize,
868 pub scale: f32,
870 pub nope: bool,
872}
873
874pub struct MtpModule {
879 pub enorm: Vec<f32>,
880 pub hnorm: Vec<f32>,
881 pub eh_proj: QTensor,
883 pub layer: LayerWeights,
884 pub final_norm: Vec<f32>,
885 pub kv: crate::kv_cache::LayerKvCache,
886}
887
888#[cfg(target_os = "macos")]
895enum MetalRowsItem<'a> {
896 Gdn {
897 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
898 first: usize,
899 },
900 Attn {
901 l: crate::gpu_metal::AttnGpuLayer<'a>,
902 li: usize,
903 q_norm: Option<&'a [f32]>,
904 k_norm: Option<&'a [f32]>,
905 output_gate: bool,
906 },
907}
908
909#[cfg(target_os = "macos")]
910struct MetalVerifyPending {
911 graph: crate::gpu_metal::VerifyGraph,
912 gdn_layers: Vec<usize>,
913 attn_layers: Vec<(usize, usize)>,
914}
915
916#[cfg(target_os = "macos")]
920struct MetalWarmPending {
921 graph: crate::gpu_metal::VerifyGraph,
922 cpu_stored: usize,
923 b: usize,
924}
925
926#[cfg(target_os = "macos")]
927enum MetalRowsRun {
928 Declined,
930 Failed,
933 Completed(MetalVerifyPending),
934}
935
936#[cfg(target_os = "macos")]
937enum MetalPrefillOutcome {
938 Declined,
939 Failed,
940 Completed(Vec<f32>),
941}
942
943#[cfg(target_os = "macos")]
944enum MetalBatchNllOutcome {
945 Declined,
946 Failed(String),
947 Completed(f64, usize),
948}
949
950#[derive(Clone, Copy)]
954enum SpecTrial {
955 Spec {
956 t0: std::time::Instant,
957 gen0: usize,
958 rounds: usize,
959 },
960 Plain {
961 t0: std::time::Instant,
962 gen0: usize,
963 },
964 Decided {
965 spec: bool,
966 recheck_at: usize,
967 },
968}
969
970pub(crate) fn spec_time_level() -> u8 {
974 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
975 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
976 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
977 Err(_) => 0,
978 })
979}
980
981struct SpecStampLog {
987 t_last: std::time::Instant,
988 items: Vec<(&'static str, f32)>,
989}
990
991static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
992
993pub(crate) fn spec_stamp(name: &'static str) {
994 if spec_time_level() == 0 {
995 return;
996 }
997 if let Ok(mut g) = SPEC_STAMPS.lock() {
998 if let Some(log) = g.as_mut() {
999 let now = std::time::Instant::now();
1000 log.items
1001 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1002 log.t_last = now;
1003 }
1004 }
1005}
1006
1007fn spec_stamps_begin() {
1008 if spec_time_level() == 0 {
1009 return;
1010 }
1011 if let Ok(mut g) = SPEC_STAMPS.lock() {
1012 *g = Some(SpecStampLog {
1013 t_last: std::time::Instant::now(),
1014 items: Vec::with_capacity(64),
1015 });
1016 }
1017}
1018
1019fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1020 SPEC_STAMPS
1021 .lock()
1022 .ok()
1023 .and_then(|mut g| g.take())
1024 .map(|l| l.items)
1025 .unwrap_or_default()
1026}
1027
1028fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1031 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1032 for &(n, ms) in items {
1033 match agg.iter_mut().find(|e| e.0 == n) {
1034 Some(e) => {
1035 e.1 += ms;
1036 e.2 += 1;
1037 }
1038 None => agg.push((n, ms, 1)),
1039 }
1040 }
1041 let mut s = String::with_capacity(agg.len() * 16);
1042 for (n, ms, k) in agg {
1043 if k > 1 {
1044 s.push_str(&format!("{n} {ms:.1}/{k} "));
1045 } else {
1046 s.push_str(&format!("{n} {ms:.1} "));
1047 }
1048 }
1049 s
1050}
1051
1052#[derive(Default, Clone, Copy)]
1074struct SpecMon {
1075 round_ms: f64,
1076 tokens: f64,
1077 plain_ms: f64,
1078 n: u32,
1079 fails: u32,
1080 metal: bool,
1081}
1082
1083const SPEC_PROXY_TOKENS: f64 = 3.5;
1086const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1089
1090impl SpecMon {
1091 fn round(&mut self, dt_ms: f64, produced: usize) {
1092 self.n += 1;
1093 if self.n == 1 {
1094 return; }
1096 let a = if self.n == 2 { 1.0 } else { 0.3 };
1097 self.round_ms += a * (dt_ms - self.round_ms);
1098 self.tokens += a * (produced as f64 - self.tokens);
1099 }
1100 fn pays(&self) -> bool {
1101 if self.plain_ms > 0.0 {
1102 self.tokens * self.plain_ms > self.round_ms * 1.03
1103 } else {
1104 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1105 }
1106 }
1107 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1109 let n = generated.saturating_sub(gen0);
1110 if n >= 8 {
1111 return true;
1112 }
1113 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1114 }
1115}
1116
1117pub const KV_PREFIX_TAIL: usize = 128;
1120
1121#[derive(Debug, Clone, Default)]
1127pub struct KvPrefix {
1128 len: usize,
1129 hash: u64,
1130 tail: Vec<u32>,
1131 device: bool,
1136}
1137
1138impl KvPrefix {
1139 #[inline]
1140 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1141 for &id in ids {
1142 h ^= id as u64;
1143 h = h.wrapping_mul(0x100000001b3);
1144 h ^= h >> 29;
1145 }
1146 h
1147 }
1148
1149 pub fn clear(&mut self) {
1150 self.len = 0;
1151 self.hash = 0xcbf29ce484222325;
1152 self.tail.clear();
1153 self.device = false;
1154 }
1155
1156 pub fn on_device(&self) -> bool {
1158 self.device
1159 }
1160
1161 pub fn set_on_device(&mut self, device: bool) {
1163 self.device = device;
1164 }
1165
1166 pub fn len(&self) -> usize {
1168 self.len
1169 }
1170
1171 pub fn is_empty(&self) -> bool {
1172 self.len == 0
1173 }
1174
1175 pub fn tail_len(&self) -> usize {
1177 self.tail.len()
1178 }
1179
1180 pub fn set(&mut self, ids: &[u32]) {
1182 self.clear();
1183 self.extend(ids);
1184 }
1185
1186 pub fn extend(&mut self, more: &[u32]) {
1188 if self.len == 0 && self.hash == 0 {
1189 self.hash = 0xcbf29ce484222325;
1190 }
1191 self.hash = Self::fold(self.hash, more);
1192 self.len += more.len();
1193 if more.len() >= KV_PREFIX_TAIL {
1194 self.tail.clear();
1195 self.tail.extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1196 } else {
1197 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1198 self.tail.drain(..drop);
1199 self.tail.extend_from_slice(more);
1200 }
1201 }
1202
1203 pub fn extension(&self, ids: &[u32]) -> usize {
1207 if self.len == 0 || ids.len() <= self.len {
1208 return 0;
1209 }
1210 let t = self.tail.len();
1211 if ids[self.len - t..self.len] != self.tail[..] {
1212 return 0;
1213 }
1214 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1215 return 0;
1216 }
1217 self.len
1218 }
1219}
1220
1221pub struct GenerateResult {
1223 pub text: String,
1224 pub token_ids: Vec<u32>,
1225 pub prompt_tokens: usize,
1226 pub tokens_generated: usize,
1227 pub finish_reason: String,
1228 pub mtp_drafted: usize,
1230 pub mtp_accepted: usize,
1231 pub token_confidence: Vec<f32>,
1236 pub traces: Vec<TokenTrace>,
1239}
1240
1241#[derive(Clone, Debug)]
1246pub struct TokenTrace {
1247 pub t: usize,
1249 pub token_id: u32,
1251 pub confidence: f32,
1253 pub active_skill: Option<String>,
1255 pub recon: Option<f32>,
1259 pub switched: bool,
1262}
1263
1264#[cfg_attr(not(test), allow(dead_code))]
1269fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1270 let t = if temp > 1e-3 { temp } else { 1.0 };
1271 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1272 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1273 if sum > 0.0 {
1274 (((logits[id as usize] - max) / t).exp()) / sum
1275 } else {
1276 0.0
1277 }
1278}
1279
1280fn prefill_batched() -> bool {
1283 std::env::var("CMF_PREFILL")
1284 .map(|v| v != "seq")
1285 .unwrap_or(true)
1286}
1287
1288#[inline]
1292fn nll_graph_policy(
1293 unmasked: bool,
1294 prefer_graph: bool,
1295 native_metal: bool,
1296) -> (bool, bool) {
1297 let graph_quality = unmasked && prefer_graph;
1298 let fused_head_quality = graph_quality && native_metal;
1299 (graph_quality, fused_head_quality)
1300}
1301
1302#[derive(Clone, Copy)]
1306enum PrefillIn<'a> {
1307 Ids(&'a [u32]),
1308 Hidden(&'a [f32]),
1309}
1310
1311impl Pipeline {
1318 fn can_prefill_batched(&self) -> bool {
1319 #[cfg(test)]
1320 let force_serial = self.nll_test_force_serial;
1321 #[cfg(not(test))]
1322 let force_serial = false;
1323 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1324 }
1325
1326 fn automatic_gpu_prefix(&self) -> Option<usize> {
1329 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1330 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1331 }
1332
1333 pub fn prefill_chunk(&self) -> usize {
1337 let env = env_prefill_chunk();
1338 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1339 return prefill_chunk_rule(env, ChunkHost::here(), false);
1340 }
1341 prefill_chunk_rule(None, ChunkHost::Other, self.chunk_stack_facts().dense_on_discrete())
1342 }
1343
1344 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1345 let plain_dense = !self.weights.layers.is_empty()
1346 && self.g3n.is_none()
1347 && self.dsv4.is_none()
1348 && self.dsv41.is_none()
1349 && self.qwen4_exp.is_none()
1350 && self.weights.layers.iter().all(|lw| {
1351 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1352 });
1353 let gpu_on = crate::gpu::enabled();
1354 ChunkStackFacts {
1355 plain_dense,
1356 discrete: gpu_on && crate::gpu::discrete(),
1357 gpu_on,
1358 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1361 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1362 multi_gpu: self.gpu_plan.is_some(),
1363 o1: self.o1_active(),
1364 }
1365 }
1366}
1367
1368pub fn prefill_chunk() -> usize {
1377 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1378}
1379
1380fn env_prefill_chunk() -> Option<usize> {
1381 std::env::var("CMF_PREFILL_CHUNK")
1382 .ok()
1383 .and_then(|v| v.parse::<usize>().ok())
1384}
1385
1386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1388enum ChunkHost {
1389 Macos,
1390 Aarch64,
1392 Other,
1394}
1395
1396impl ChunkHost {
1397 fn here() -> Self {
1398 if cfg!(target_os = "macos") {
1399 ChunkHost::Macos
1400 } else if cfg!(target_arch = "aarch64") {
1401 ChunkHost::Aarch64
1402 } else {
1403 ChunkHost::Other
1404 }
1405 }
1406}
1407
1408const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1415
1416fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1422 if let Some(n) = env {
1423 return n.max(1);
1424 }
1425 match host {
1426 ChunkHost::Macos => 512,
1427 ChunkHost::Aarch64 => 256,
1430 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1431 ChunkHost::Other => 48,
1432 }
1433}
1434
1435#[derive(Clone, Copy, Debug, Default)]
1437struct ChunkStackFacts {
1438 plain_dense: bool,
1441 discrete: bool,
1443 gpu_on: bool,
1445 capacity_split: bool,
1447 multi_gpu: bool,
1449 o1: bool,
1451}
1452
1453impl ChunkStackFacts {
1454 fn dense_on_discrete(self) -> bool {
1455 self.plain_dense
1456 && self.discrete
1457 && self.gpu_on
1458 && !self.capacity_split
1459 && !self.multi_gpu
1460 && !self.o1
1461 }
1462}
1463
1464#[inline]
1470fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1471 if end <= start || start >= input_len {
1472 return 0;
1473 }
1474 let rows = (end.min(input_len) - start).min(input_len - start);
1475 if end < input_len {
1476 rows
1477 } else {
1478 rows.saturating_sub(1)
1479 }
1480}
1481
1482pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1484
1485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1487pub(crate) struct ReuseLayer {
1488 pub full: bool,
1491 pub host_rows: usize,
1494 pub device_rows: Option<usize>,
1496 pub device_state: bool,
1498}
1499
1500#[derive(Debug, Clone, PartialEq, Eq)]
1502pub(crate) enum ReusePlan {
1503 Ready,
1505 Pull(Vec<(usize, usize, usize)>),
1508 Fresh,
1510}
1511
1512pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1522 let mut pulls = Vec::new();
1523 for (li, l) in layers.iter().enumerate() {
1524 if !l.full {
1525 if l.device_state {
1526 return ReusePlan::Fresh;
1527 }
1528 continue;
1529 }
1530 if l.host_rows == reuse_from {
1531 continue;
1532 }
1533 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1534 pulls.push((li, l.host_rows, reuse_from));
1535 continue;
1536 }
1537 return ReusePlan::Fresh;
1538 }
1539 if pulls.is_empty() {
1540 ReusePlan::Ready
1541 } else {
1542 ReusePlan::Pull(pulls)
1543 }
1544}
1545
1546impl Pipeline {
1547 fn clear_sequence_state(&mut self) {
1555 #[cfg(target_os = "macos")]
1558 let _ = crate::gpu_metal::wait_replay();
1559 self.kv_cache.clear();
1560 self.clear_history();
1563 self.graph_logits = None;
1564 if let Some(b) = &mut self.dsv41 {
1565 b.3.clear();
1566 }
1567 crate::gpu::graph_kv_reset(self.graph_kv_id);
1568 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1573 }
1574
1575 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1582 if self.graph_prefill_preferred() {
1583 return true;
1584 }
1585 let kv_id = self.graph_kv_id;
1586 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1587 .map(|li| {
1588 let full = matches!(
1589 self.weights.layers[self.phys_layer(li)].attn,
1590 AttnKind::Full { .. }
1591 );
1592 ReuseLayer {
1593 full,
1594 host_rows: self.kv_cache.layers[li].pos_len(),
1597 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1598 device_state: crate::gpu::graph_state_resident(kv_id, li),
1599 }
1600 })
1601 .collect();
1602 if layers
1606 .iter()
1607 .all(|l| l.device_rows.is_none() && !l.device_state)
1608 {
1609 return true;
1610 }
1611 let plan = kv_reuse_plan(reuse_from, &layers);
1612 let (what, rows, n) = match &plan {
1613 ReusePlan::Ready => ("host ready", 0, 0),
1614 ReusePlan::Fresh => ("fresh", 0, 0),
1615 ReusePlan::Pull(p) => (
1616 "pull",
1617 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1618 p.len(),
1619 ),
1620 };
1621 let t0 = std::time::Instant::now();
1622 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1623 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1624 eprintln!(
1625 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1626 if ok { "" } else { " (failed → fresh)" },
1627 t0.elapsed().as_secs_f64() * 1e3
1628 );
1629 }
1630 ok
1631 }
1632
1633 fn apply_kv_reuse_plan(
1634 &mut self,
1635 reuse_from: usize,
1636 plan: ReusePlan,
1637 layers: &[ReuseLayer],
1638 ) -> bool {
1639 let kv_id = self.graph_kv_id;
1640 match plan {
1641 ReusePlan::Fresh => return false,
1642 ReusePlan::Ready => {}
1643 ReusePlan::Pull(pulls) => {
1644 let (nkv, hd) = {
1649 let c = &self.kv_cache.layers[pulls[0].0];
1650 (c.num_kv_heads, c.head_dim)
1651 };
1652 let uniform = pulls.iter().all(|&(li, _, _)| {
1653 let c = &self.kv_cache.layers[li];
1654 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1655 });
1656 let batched = if uniform {
1657 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1658 } else {
1659 None
1660 };
1661 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1662 Some(rows) => rows,
1663 None => {
1664 let mut rows = Vec::with_capacity(pulls.len());
1665 for &(li, from, to) in &pulls {
1666 let (lnkv, lhd) = {
1667 let c = &self.kv_cache.layers[li];
1668 (c.num_kv_heads, c.head_dim)
1669 };
1670 let Some((k, v, first_valid)) =
1671 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1672 else {
1673 return false;
1674 };
1675 let need_from = match self.layer_window(li) {
1679 Some(w) => from.max((to + 1).saturating_sub(w)),
1680 None => from,
1681 };
1682 if first_valid > need_from {
1683 return false;
1684 }
1685 rows.push((k, v));
1686 }
1687 rows
1688 }
1689 };
1690 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1691 let cache = &mut self.kv_cache.layers[li];
1692 let row = cache.num_kv_heads * cache.head_dim;
1693 for p in 0..to - from {
1694 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1695 }
1696 if cache.pos_len() != to {
1697 return false;
1698 }
1699 }
1700 }
1701 }
1702 for (li, l) in layers.iter().enumerate() {
1706 if l.full
1707 && l.device_rows.is_some_and(|d| d > reuse_from)
1708 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1709 {
1710 return false;
1711 }
1712 }
1713 true
1714 }
1715
1716 fn finish_generation(
1722 &mut self,
1723 mtp: &mut Option<MtpModule>,
1724 router: &mut Option<crate::swarm::DynRouter>,
1725 clear_sequence: bool,
1726 ) {
1727 if router.is_some() {
1731 let _ = self.set_active_skill(None);
1732 }
1733 #[cfg(target_os = "macos")]
1740 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1741 if clear_sequence {
1742 self.clear_sequence_state();
1743 if let Some(m) = mtp.as_mut() {
1744 m.kv.clear();
1750 }
1751 if let Some(m) = self.mtp.as_mut() {
1752 m.kv.clear();
1756 }
1757 }
1758 self.graph_want_logits = false;
1759 self.graph_head_required = false;
1760 self.graph_logits = None;
1761 self.graph_failed
1762 .store(false, std::sync::atomic::Ordering::Relaxed);
1763 self.cancel
1764 .store(false, std::sync::atomic::Ordering::Relaxed);
1765 self.dyn_router = router.take().or(self.dyn_router.take());
1766 self.mtp = mtp.take().or(self.mtp.take());
1767 self.mtp_graph_mode = None;
1768 self.spec_forced = None;
1769 }
1770
1771 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1775 if self
1776 .graph_failed
1777 .swap(false, std::sync::atomic::Ordering::Relaxed)
1778 {
1779 self.cancel
1780 .store(false, std::sync::atomic::Ordering::Relaxed);
1781 self.clear_sequence_state();
1782 self.graph_logits = None;
1783 self.graph_want_logits = false;
1784 self.graph_head_required = false;
1785 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1786 }
1787 Ok(())
1788 }
1789
1790 #[cfg(target_os = "macos")]
1791 fn fail_metal_graph(&mut self, reason: &str) {
1792 crate::pipeline::METAL_GRAPH_ERRORS
1793 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1794 self.clear_sequence_state();
1795 self.graph_logits = None;
1796 self.graph_failed
1797 .store(true, std::sync::atomic::Ordering::Relaxed);
1798 self.cancel
1799 .store(true, std::sync::atomic::Ordering::Relaxed);
1800 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1801 }
1802
1803 fn nll_begin(&mut self) -> Result<(), String> {
1808 if self
1809 .graph_failed
1810 .swap(false, std::sync::atomic::Ordering::Relaxed)
1811 {
1812 self.cancel
1813 .store(false, std::sync::atomic::Ordering::Relaxed);
1814 self.clear_sequence_state();
1815 self.graph_logits = None;
1816 self.graph_want_logits = false;
1817 self.graph_head_required = false;
1818 return Err("GPU graph failed before NLL scoring".to_string());
1819 }
1820 self.clear_sequence_state();
1821 self.graph_logits = None;
1822 self.graph_want_logits = false;
1823 self.graph_head_required = false;
1824 Ok(())
1825 }
1826
1827 fn nll_end(&mut self) {
1831 self.clear_sequence_state();
1832 self.graph_logits = None;
1833 self.graph_want_logits = false;
1834 self.graph_head_required = false;
1835 self.graph_failed
1836 .store(false, std::sync::atomic::Ordering::Relaxed);
1837 }
1838
1839 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1842 #[cfg(test)]
1843 if self.nll_test_fail_at == Some(pos) {
1844 self.nll_test_fail_at = None;
1845 self.graph_failed
1846 .store(true, std::sync::atomic::Ordering::Relaxed);
1847 self.cancel
1848 .store(true, std::sync::atomic::Ordering::Relaxed);
1849 }
1850 if self
1851 .graph_failed
1852 .swap(false, std::sync::atomic::Ordering::Relaxed)
1853 {
1854 self.cancel
1855 .store(false, std::sync::atomic::Ordering::Relaxed);
1856 self.clear_sequence_state();
1857 self.graph_logits = None;
1858 self.graph_want_logits = false;
1859 return Err(format!(
1860 "GPU graph failed during NLL {phase} at position {pos}"
1861 ));
1862 }
1863 Ok(())
1864 }
1865
1866 #[inline]
1870 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1871 virtual_idx % self.physical_layers
1872 }
1873
1874 #[inline]
1877 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1878 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1879 }
1880
1881 #[allow(clippy::too_many_arguments)]
1883
1884 #[cfg(target_os = "macos")]
1903 fn graph_prefill_preferred(&self) -> bool {
1904 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1905 if !crate::gpu::enabled_here()
1906 || !graph_force
1907 || std::env::var("CMF_GPU_BLOCK")
1908 .map(|v| v == "0")
1909 .unwrap_or(false)
1910 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1913 {
1914 return false;
1915 }
1916 self.weights
1917 .layers
1918 .iter()
1919 .any(|lw| {
1920 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1921 })
1922 }
1923
1924 #[cfg(not(target_os = "macos"))]
1931 fn batch_prefix_prefill(&self) -> bool {
1932 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1933 Ok("0") => return false,
1934 Ok("1") => true,
1935 _ => false,
1936 };
1937 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1938 && crate::gpu::enabled_here()
1939 && !self.graph_refused()
1940 && (forced || self.graph_attn_decline_reason().is_some())
1941 && self.wgpu_graph_attn_decline().is_none()
1942 && self.attn_softcap == 0.0
1943 && self
1944 .weights
1945 .layers
1946 .iter()
1947 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1948 && self.automatic_gpu_prefix().is_some()
1949 }
1950
1951 fn g4_batch_prefill(&self) -> bool {
1957 #[cfg(target_os = "macos")]
1958 {
1959 false
1960 }
1961 #[cfg(not(target_os = "macos"))]
1962 {
1963 (self.global_attn.is_some() || self.attn_v_norm)
1964 && crate::gpu::g4_batch_on()
1965 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1966 && crate::gpu::enabled_here()
1967 && crate::gpu::discrete()
1968 && !self.graph_refused()
1969 && !self.o1_active()
1970 && self.attn_softcap == 0.0
1971 && self.wgpu_graph_attn_decline().is_none()
1972 && self.weights.layers.iter().all(|lw| {
1973 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1974 })
1975 && self.automatic_gpu_prefix().is_none()
1976 }
1977 }
1978
1979 #[cfg(not(target_os = "macos"))]
1986 fn g4_batch_ingest(
1987 &mut self,
1988 ids: &[u32],
1989 pos0: usize,
1990 keep_all: bool,
1991 ) -> Result<(usize, Vec<f32>), String> {
1992 let hs = self.hidden_size;
1993 let chunk = self.generation_batch_k().max(1);
1994 let end_all = pos0 + ids.len();
1995 let mut pos = pos0;
1996 let mut out = Vec::new();
1997 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1998 let end = (pos + chunk).min(end_all);
1999 let bk = end - pos;
2000 let mut hiddens = vec![0f32; bk * hs];
2001 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2002 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2003 }
2004 let positions: Vec<usize> = (pos..end).collect();
2005 let t_chunk = std::time::Instant::now();
2006 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2007 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2008 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2009 eprintln!(
2010 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2011 bk as f64 / (ms / 1000.0)
2012 );
2013 }
2014 match outcome {
2015 crate::gpu::BatchGraphOutcome::Completed => {
2016 if keep_all {
2017 out.extend_from_slice(&hiddens);
2018 } else {
2019 out = hiddens[(bk - 1) * hs..].to_vec();
2020 }
2021 pos = end;
2022 }
2023 crate::gpu::BatchGraphOutcome::Failed => {
2024 return Err("Gemma-4 batched prefill failed after admission".into());
2025 }
2026 crate::gpu::BatchGraphOutcome::Declined => {
2027 #[cfg(feature = "gpu")]
2028 if pos > pos0 {
2029 self.pull_lagging_host_kv(0, self.num_layers, pos);
2030 }
2031 break;
2032 }
2033 }
2034 }
2035 Ok((pos - pos0, out))
2036 }
2037
2038 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2043 #[cfg(not(target_os = "macos"))]
2044 {
2045 if ids.is_empty() || !self.g4_batch_prefill() {
2046 return None;
2047 }
2048 self.clear_sequence_state();
2049 match self.g4_batch_ingest(ids, 0, true) {
2050 Ok((n, rows)) if n == ids.len() => Some(rows),
2051 _ => {
2052 self.clear_sequence_state();
2053 None
2054 }
2055 }
2056 }
2057 #[cfg(target_os = "macos")]
2058 {
2059 let _ = ids;
2060 None
2061 }
2062 }
2063
2064 #[cfg(not(target_os = "macos"))]
2065 fn graph_prefill_preferred(&self) -> bool {
2066 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2074 if !graph_on || !crate::gpu::enabled_here() {
2075 return false;
2076 }
2077 if self.embryo_resident_eligible() {
2081 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2086 }
2087 if self.o1_active() {
2101 return false;
2102 }
2103 if self.wgpu_graph_attn_decline().is_some() {
2107 return false;
2108 }
2109 if self
2110 .weights
2111 .layers
2112 .iter()
2113 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2114 {
2115 return true;
2116 }
2117 self.weights
2128 .layers
2129 .iter()
2130 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2131 && self.automatic_gpu_prefix().is_none()
2132 }
2133
2134 #[cfg(target_os = "macos")]
2135 fn q1_graph_gpu(
2136 &mut self,
2137 start: usize,
2138 upto: Option<usize>,
2139 position: usize,
2140 h: &mut [f32],
2141 ) -> usize {
2142 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2144 const MOE_WORKING_SET_REASON: &str =
2145 "model weights exceed Metal working set for native MoE graph";
2146 if self
2150 .graph_declines
2151 .borrow()
2152 .iter()
2153 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2154 {
2155 return start;
2156 }
2157 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2158 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2160 || !crate::gpu::enabled_here()
2161 || !graph_force
2162 || std::env::var("CMF_GPU_BLOCK")
2163 .map(|v| v == "0")
2164 .unwrap_or(false)
2165 {
2166 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2167 eprintln!(
2168 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2169 self.attn_softcap > 0.0,
2170 crate::gpu::enabled_here(),
2171 graph_force,
2172 );
2173 }
2174 if self.graph_head_required {
2175 self.fail_metal_graph("native graph front gate refused");
2176 }
2177 return start;
2178 }
2179 let gemma4 = self.metal_gemma4();
2189 let swa_graph = self.metal_graph_swa() || gemma4;
2190 if (self.swa.is_some() && !swa_graph)
2191 || (self.global_attn.is_some() && !gemma4)
2192 || self.attention_heads_per_layer.is_some()
2193 || (self.attn_v_norm && !gemma4)
2194 || (self.graph_attn_decline_reason().is_some() && !swa_graph)
2196 || self.weights.layers.iter().any(|lw| {
2197 ((lw.attn_out_norm.is_some()
2198 || lw.ffn_out_norm.is_some()
2199 || lw.layer_scale.is_some())
2200 && !gemma4)
2201 || matches!(&lw.ffn, FfnKind::Dense(d)
2202 if !(matches!(d.act, Act::Silu | Act::Gelu)
2203 || (gemma4 && d.act == Act::GeluTanh)))
2204 })
2205 {
2206 if let Some(reason) = self.graph_attn_decline_reason().filter(|_| !swa_graph) {
2209 self.note_graph_decline("metal block graph", reason);
2210 }
2211 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2212 eprintln!(
2213 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2214 self.swa.is_some(),
2215 self.global_attn.is_some(),
2216 self.attention_heads_per_layer.is_some(),
2217 self.attn_v_norm,
2218 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2219 );
2220 }
2221 if self.graph_head_required {
2222 self.fail_metal_graph("native graph architecture gate refused");
2223 }
2224 return start;
2225 }
2226 let limit = upto
2229 .map(|u| u + 1)
2230 .unwrap_or(self.num_layers)
2231 .min(self.num_layers);
2232
2233 enum Item<'a> {
2234 Gdn {
2235 run: Vec<GdnGpuLayer<'a>>,
2236 first: usize,
2237 },
2238 Attn {
2239 l: AttnGpuLayer<'a>,
2240 li: usize,
2241 q_norm: Option<&'a [f32]>,
2242 k_norm: Option<&'a [f32]>,
2243 output_gate: bool,
2244 proj_gate: Option<(&'a QTensor, bool)>,
2248 head_gate_w: Option<&'a [f32]>,
2251 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2252 full_gpu: bool,
2255 },
2256 }
2257
2258 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2265 let attend_contract = attend_mode != "0"
2266 && attend_mode != "off"
2267 && self.head_dim % 4 == 0
2268 && self.head_dim <= 256
2269 && self.rotary_dim >= 2
2270 && self.rotary_dim <= self.head_dim
2271 && (self.rotary_dim / 2) % 32 == 0
2272 && self.num_kv_heads > 0
2273 && self.num_heads % self.num_kv_heads == 0;
2274
2275 let mut plan: Vec<Item> = Vec::new();
2276 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2277 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2279 let mut scan = start;
2280 while scan < limit {
2281 let lw = &self.weights.layers[self.phys_layer(scan)];
2282 let ffn = match &lw.ffn {
2283 FfnKind::Dense(d) if d.segs.is_empty() => {
2284 let (Some(g), Some(u), Some(dn)) = (
2285 d.gate_proj.metal_graph_parts(),
2286 d.up_proj.metal_graph_parts(),
2287 d.down_proj.metal_graph_parts(),
2288 ) else {
2289 if block_diag {
2290 eprintln!(
2291 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2292 );
2293 }
2294 break;
2295 };
2296 MetalFfn::Dense {
2297 gate: g,
2298 up: u,
2299 down: dn,
2300 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2303 }
2304 }
2305 FfnKind::Moe(m) => {
2306 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
2307 if block_diag {
2308 eprintln!(
2309 "block-graph: L{scan} MoE outside the graph contract — run ends"
2310 );
2311 }
2312 break;
2313 };
2314 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2315 model_ref.get_or_insert_with(|| model.clone());
2316 }
2317 MetalFfn::Moe(moe)
2318 }
2319 _ => {
2320 if block_diag {
2321 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2322 }
2323 break;
2324 }
2325 };
2326 match &lw.attn {
2327 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2328 let parts = (
2329 w.in_proj_qkv.metal_graph_parts(),
2330 w.in_proj_z.metal_graph_parts(),
2331 w.in_proj_a.f32_parts(),
2332 w.in_proj_b.f32_parts(),
2333 w.out_proj.metal_graph_parts(),
2334 );
2335 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2336 if block_diag {
2337 eprintln!(
2338 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2339 w.in_proj_qkv.metal_graph_parts().is_some(),
2340 w.in_proj_z.metal_graph_parts().is_some(),
2341 w.in_proj_a.f32_parts().is_some(),
2342 w.in_proj_b.f32_parts().is_some(),
2343 w.out_proj.metal_graph_parts().is_some(),
2344 );
2345 }
2346 break;
2347 };
2348 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2349 model_ref.get_or_insert_with(|| model.clone());
2350 }
2351 let gl = GdnGpuLayer {
2352 attn_norm: &lw.input_norm,
2353 post_norm: &lw.post_norm,
2354 qkv,
2355 z,
2356 a,
2357 b,
2358 out,
2359 ffn,
2360 conv1d: &w.conv1d,
2361 a_log: &w.a_log,
2362 dt_bias: &w.dt_bias,
2363 gnorm: &w.norm,
2364 };
2365 match plan.last_mut() {
2366 Some(Item::Gdn { run, .. }) => run.push(gl),
2367 _ => plan.push(Item::Gdn {
2368 run: vec![gl],
2369 first: scan,
2370 }),
2371 }
2372 }
2373 AttnKind::Full {
2374 wq,
2375 wk,
2376 wv,
2377 wo,
2378 q_norm,
2379 k_norm,
2380 output_gate,
2381 softplus_gate,
2382 bias,
2383 } if (!self.kv_cache.layers[scan].o1_sealed()
2384 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2389 && softplus_gate
2393 .as_ref()
2394 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2395 {
2396 let parts = (
2397 wq.metal_graph_parts(),
2398 wk.metal_graph_parts(),
2399 wv.metal_graph_parts(),
2400 wo.metal_graph_parts(),
2401 );
2402 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2403 break;
2404 };
2405 if let QTensor::Mapped { model, .. } = wq {
2406 model_ref.get_or_insert_with(|| model.clone());
2407 }
2408 let cache = &self.kv_cache.layers[scan];
2409 let o1_metal = cache.o1.is_some()
2413 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2414 && cache.o1_views().is_some();
2415 let head_gate_w = softplus_gate
2419 .as_ref()
2420 .and_then(|(g, _)| g.f32_parts())
2421 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2422 .map(|(d, _, _)| d);
2423 let (nkv_g, hd_g) = if gemma4 {
2426 let (a, b, _) = self.layer_geom(scan);
2427 (a, b)
2428 } else {
2429 (self.num_kv_heads, self.head_dim)
2430 };
2431 let full_gpu = attend_contract
2432 && softplus_gate.is_none() == head_gate_w.is_none()
2433 && cache.mode == crate::kv_cache::KvMode::F32
2434 && (cache.o1.is_none() || o1_metal)
2435 && bias.is_none()
2436 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2437 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2438 && pk.1 == nkv_g * hd_g
2439 && pv.1 == nkv_g * hd_g
2440 && po.2 == self.num_heads * hd_g;
2441 plan.push(Item::Attn {
2442 l: AttnGpuLayer {
2443 attn_norm: &lw.input_norm,
2444 post_norm: &lw.post_norm,
2445 wq: pq,
2446 wk: pk,
2447 wv: pv,
2448 wo: po,
2449 ffn,
2450 attn_out_norm: lw.attn_out_norm.as_deref(),
2451 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2452 layer_scale: lw.layer_scale,
2453 },
2454 li: scan,
2455 q_norm: q_norm.as_deref(),
2456 k_norm: k_norm.as_deref(),
2457 output_gate: *output_gate,
2458 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2459 head_gate_w,
2460 bias: bias
2461 .as_ref()
2462 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2463 full_gpu,
2464 });
2465 }
2466 _ => break,
2467 }
2468 scan += 1;
2469 }
2470 let Some(model) = model_ref else {
2471 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2472 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2473 }
2474 if self.graph_head_required {
2475 self.fail_metal_graph("native graph has no mapped model reference");
2476 }
2477 return start;
2478 };
2479 if plan.is_empty() {
2480 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2481 eprintln!("q1-graph: empty plan at layer {start}");
2482 }
2483 if self.graph_head_required {
2484 self.fail_metal_graph("native graph plan is empty");
2485 }
2486 return start;
2487 }
2488 let has_moe = plan.iter().any(|it| match it {
2489 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
2490 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
2491 });
2492 let metal_working_set = crate::gpu_metal::working_set_bytes();
2493 let model_bytes = model.primary_bytes().len() as u64;
2494 let pager_cap = has_moe
2503 .then(crate::gpu_metal::moe_pager_cap_bytes)
2504 .flatten();
2505 let pager_eligible = pager_cap.is_some()
2506 && !self.graph_head_required
2507 && metal_working_set != 0
2508 && model_bytes > metal_working_set;
2509 let mut pager_span: Option<(usize, usize)> = None;
2510 if pager_eligible {
2511 let cap = pager_cap.expect("checked above");
2512 let tail_span = (self.graph_want_logits
2516 && upto.is_none()
2517 && scan == self.num_layers
2518 && std::env::var("CMF_GPU_LMHEAD")
2519 .map(|v| v != "0")
2520 .unwrap_or(true))
2521 .then(|| self.weights.lm_head.metal_graph_parts())
2522 .flatten()
2523 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2524 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2525 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2526 Some(match span {
2527 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2528 None => (lo, hi),
2529 })
2530 };
2531 let page_span = |span: (usize, usize), include_tail: bool| {
2532 let (mut lo, mut hi) = span;
2533 if include_tail {
2534 if let Some((tail_lo, tail_hi)) = tail_span {
2535 lo = lo.min(tail_lo);
2536 hi = hi.max(tail_hi);
2537 }
2538 }
2539 crate::gpu_metal::page_round_span(lo, hi)
2540 };
2541 let mut selected = 0usize;
2542 let mut selected_span: Option<(usize, usize)> = None;
2543 let mut gdn_prefix: Option<(usize, usize)> = None;
2551 'items: for (i, item) in plan.iter().enumerate() {
2552 match item {
2553 Item::Attn { li, .. } => {
2554 let Some(span) = add_layer_span(selected_span, *li) else {
2555 break;
2556 };
2557 let Some((page_lo, page_hi)) =
2558 page_span(span, i + 1 == plan.len())
2559 else {
2560 break;
2561 };
2562 if page_hi - page_lo > cap {
2563 break;
2564 }
2565 selected = i + 1;
2566 selected_span = Some((page_lo, page_hi));
2567 }
2568 Item::Gdn { first, run } => {
2569 let mut whole = None;
2576 for take in 1..=run.len() {
2577 whole = add_layer_span(whole, *first + take - 1);
2578 if whole.is_none() {
2579 break;
2580 }
2581 }
2582 let whole_fits_empty_page = whole
2583 .and_then(|span| page_span(span, false))
2584 .is_some_and(|(lo, hi)| hi - lo <= cap);
2585 if whole_fits_empty_page {
2586 let mut span = selected_span;
2587 for take in 1..=run.len() {
2588 let Some(next) = add_layer_span(span, *first + take - 1) else {
2589 break 'items;
2590 };
2591 span = Some(next);
2592 }
2593 let Some(span) = span else {
2594 break;
2595 };
2596 let Some((page_lo, page_hi)) =
2597 page_span(span, i + 1 == plan.len())
2598 else {
2599 break;
2600 };
2601 if page_hi - page_lo > cap {
2602 break;
2603 }
2604 selected = i + 1;
2605 selected_span = Some((page_lo, page_hi));
2606 continue;
2607 }
2608 if selected != 0 {
2609 break;
2610 }
2611 let mut span = None;
2612 let mut best: Option<(usize, (usize, usize))> = None;
2613 for take in 1..=run.len() {
2614 let Some(next) = add_layer_span(span, *first + take - 1) else {
2615 break;
2616 };
2617 let Some((page_lo, page_hi)) = page_span(next, false) else {
2620 break;
2621 };
2622 if page_hi - page_lo > cap {
2623 break;
2624 }
2625 span = Some((page_lo, page_hi));
2626 best = Some((take, (page_lo, page_hi)));
2627 }
2628 let Some((take, page)) = best else {
2629 break;
2630 };
2631 selected = i + 1;
2632 selected_span = Some(page);
2633 if take != run.len() {
2634 gdn_prefix = Some((i, take));
2635 break 'items;
2636 }
2637 }
2638 }
2639 }
2640 if selected != 0 {
2641 if let Some((item, take)) = gdn_prefix {
2642 match &mut plan[item] {
2643 Item::Gdn { run, .. } => run.truncate(take),
2644 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2645 }
2646 }
2647 let (page_lo, page_hi) = selected_span.expect("selected page span");
2648 tracing::info!(
2649 page_start = page_lo,
2650 page_end = page_hi,
2651 page_mb = (page_hi - page_lo) / (1024 * 1024),
2652 cap_mb = cap / (1024 * 1024),
2653 graph_items = selected,
2654 "native Metal MoE graph uses transient bounded weight page"
2655 );
2656 plan.truncate(selected);
2657 pager_span = Some((page_lo, page_hi));
2658 } else {
2659 tracing::warn!(
2660 cap_mb = cap / (1024 * 1024),
2661 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2662 );
2663 }
2664 }
2665 if has_moe
2674 && metal_working_set != 0
2675 && model_bytes > metal_working_set
2676 && pager_span.is_none()
2677 {
2678 tracing::info!(
2679 model_bytes,
2680 metal_working_set,
2681 "native Metal MoE graph declined before allocation"
2682 );
2683 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2684 if self.graph_head_required {
2685 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2686 }
2687 return start;
2688 }
2689 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2690 let dev_attend = attend_contract
2691 && (self.head_dim <= 128
2692 || has_moe
2693 || (self.head_dim <= 256 && has_gdn)
2699 || (self.head_dim <= 256 && swa_graph)
2703 || attend_mode == "force"
2704 || attend_mode == "256");
2705 if !dev_attend {
2706 for it in &mut plan {
2707 if let Item::Attn { li, full_gpu, .. } = it {
2708 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2711 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2712 if !keep_o1 {
2713 *full_gpu = false;
2714 }
2715 }
2716 }
2717 }
2718 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2719 use std::sync::atomic::{AtomicBool, Ordering};
2720 static SAID: AtomicBool = AtomicBool::new(false);
2721 if !SAID.swap(true, Ordering::Relaxed) {
2722 let fg = plan
2723 .iter()
2724 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2725 .count();
2726 let att = plan
2727 .iter()
2728 .filter(|it| matches!(it, Item::Attn { .. }))
2729 .count();
2730 eprintln!(
2731 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2732 plan.len(),
2733 self.head_dim,
2734 self.rotary_dim,
2735 self.num_kv_heads,
2736 self.num_heads,
2737 );
2738 }
2739 }
2740 let dims = GraphDims {
2741 hidden: self.hidden_size,
2742 eps: self.rms_eps as f32,
2743 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2744 };
2745 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2749 crate::gpu_metal::MoePagerPageScope::new(
2750 _mt0,
2751 position,
2752 start,
2753 page_lo,
2754 page_hi,
2755 pager_cap.unwrap_or(0),
2759 )
2760 });
2761 let graph_new = match pager_span {
2762 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2763 None => TokenGraph::new(&model, dims, h),
2764 };
2765 let Some(mut graph) = graph_new else {
2766 if self.graph_head_required {
2767 self.fail_metal_graph("native TokenGraph allocation refused");
2768 }
2769 return start;
2770 };
2771 if swa_graph && self.head_dim > 128 {
2772 graph.set_attend_blk_from(64);
2778 }
2779 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2780 nv: cfg.num_v_heads,
2781 nk: cfg.num_k_heads,
2782 dk: cfg.key_head_dim,
2783 dv: cfg.value_head_dim,
2784 kk: cfg.conv_kernel,
2785 hidden: self.hidden_size,
2786 inter: self.intermediate_size,
2787 c_dim: cfg.conv_dim(),
2788 eps: cfg.rms_eps as f32,
2789 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2790 });
2791 let mut valid = 0usize;
2795 let mut end = start;
2796 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2798 static ONCE: std::sync::Once = std::sync::Once::new();
2799 ONCE.call_once(|| {
2800 for it in &plan {
2801 match it {
2802 Item::Gdn { first, run } => {
2803 eprintln!("plan: Gdn first={first} len={}", run.len())
2804 }
2805 Item::Attn { li, full_gpu, .. } => {
2806 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2807 }
2808 }
2809 }
2810 });
2811 }
2812 for item in &plan {
2813 let ok = match item {
2814 Item::Gdn { run, .. } => gcfg
2815 .as_ref()
2816 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2817 .unwrap_or(false),
2818 Item::Attn { l, .. } => graph.attn_ok(l),
2819 };
2820 if !ok {
2821 if block_diag {
2822 eprintln!(
2823 "block-graph: plan item {} ({}) failed graph preflight",
2824 valid,
2825 match item {
2826 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2827 Item::Attn { li, .. } => format!("Attn L{li}"),
2828 }
2829 );
2830 }
2831 break;
2832 }
2833 valid += 1;
2834 end += match item {
2835 Item::Gdn { run, .. } => run.len(),
2836 Item::Attn { .. } => 1,
2837 };
2838 }
2839 plan.truncate(valid);
2840 if plan.is_empty() {
2841 if self.graph_head_required {
2842 self.fail_metal_graph("native graph preflight produced no valid items");
2843 }
2844 return start;
2845 }
2846
2847 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2848 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2849 return start;
2850 }
2851
2852 let one_pass = |t: (usize, usize, usize)| {
2861 use cortiq_core::TensorDtype as D;
2862 matches!(
2863 model.tensors[t.0].dtype,
2864 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
2865 )
2866 };
2867 let dense_fast = plan.iter().all(|it| match it {
2868 Item::Attn {
2869 l, li, full_gpu, ..
2870 } => {
2871 *full_gpu
2872 && self.kv_cache.layers[*li].o1.is_none()
2873 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2874 && match l.ffn {
2875 MetalFfn::Dense { gate, up, down, .. } => {
2876 one_pass(gate) && one_pass(up) && one_pass(down)
2877 }
2878 _ => false,
2879 }
2880 }
2881 Item::Gdn { .. } => false,
2882 });
2883 let moe_plan = !dense_fast
2887 && plan.iter().all(|it| match it {
2888 Item::Attn {
2889 l, li, full_gpu, ..
2890 } => {
2891 *full_gpu
2892 && self.kv_cache.layers[*li].o1.is_none()
2893 && matches!(l.ffn, MetalFfn::Moe(_))
2894 }
2895 Item::Gdn { .. } => false,
2896 });
2897 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
2898 if block_diag {
2899 use std::sync::atomic::{AtomicBool, Ordering};
2900 static SAID_AB: AtomicBool = AtomicBool::new(false);
2901 if !SAID_AB.swap(true, Ordering::Relaxed) {
2902 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
2903 }
2904 }
2905 let _mv_fast = match ab {
2906 Some((bits, _)) => {
2907 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2908 crate::gpu_metal::MvFastGuard::set_raw(bits)
2909 }
2910 None if moe_plan => {
2911 graph.set_dense_concurrent(crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0);
2912 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
2913 }
2914 None => {
2915 graph.set_dense_concurrent(dense_fast);
2916 let q8r4 = if swa_graph {
2919 crate::gpu_metal::DENSE_Q8R4
2920 } else {
2921 0
2922 };
2923 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2924 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2925 } else {
2926 0
2927 })
2928 }
2929 };
2930
2931 let pool = self.pool.clone();
2934 let (nh, nkv, hd, hs, eps) = (
2935 self.num_heads,
2936 self.num_kv_heads,
2937 self.head_dim,
2938 self.hidden_size,
2939 self.rms_eps,
2940 );
2941 let norm_style = self.norm_style;
2942 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2943 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2944 let kv_id = self.graph_kv_id;
2945 let mut pending: Vec<(usize, usize)> = Vec::new();
2948 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
2951 for item in &plan {
2952 let _xt0 = std::time::Instant::now();
2953 let _xkind: u32 = match item {
2954 Item::Gdn { .. } => 2,
2955 Item::Attn { .. } => 3,
2956 };
2957 if self.loop_final_norm {
2959 let item_start = match item {
2960 Item::Gdn { first, .. } => *first,
2961 Item::Attn { li, .. } => *li,
2962 };
2963 if item_start > start && self.is_loop_end(item_start - 1) {
2964 graph.encode_loop_norm(&self.weights.final_norm);
2965 }
2966 }
2967 match item {
2968 Item::Gdn { run, first } => {
2969 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2970 if l.linear_state.len() != want {
2971 l.linear_state = vec![0f32; want];
2972 }
2973 }
2974 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2975 .iter()
2976 .map(|l| l.linear_state.as_slice())
2977 .collect();
2978 let _ig = std::time::Instant::now();
2979 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2980 tracing::error!("q1 graph: GDN run refused after validation");
2982 return start;
2983 }
2984 graph.commit_kind = 2;
2987 graph.commit();
2988 crate::gpu::stageprof(0, _ig.elapsed());
2989 pending.push((*first, run.len()));
2990 }
2991 Item::Attn {
2992 l,
2993 li,
2994 q_norm,
2995 k_norm,
2996 output_gate,
2997 proj_gate,
2998 head_gate_w,
2999 bias,
3000 full_gpu,
3001 } => {
3002 let _ia = std::time::Instant::now();
3003 let inv_freq_l = self.layer_inv_freq(*li);
3009 let rd_l = self.layer_geom(*li).2;
3010 let window_l = self.layer_window(*li);
3011 let head_gate_w = *head_gate_w;
3012 let (nkv, hd) = if gemma4 {
3015 let (a, b, _) = self.layer_geom(*li);
3016 (a, b)
3017 } else {
3018 (nkv, hd)
3019 };
3020 if *full_gpu {
3022 let cache = &self.kv_cache.layers[*li];
3023 let o1p = if cache.o1.is_some() {
3024 match cache.o1_views() {
3025 Some(views) => Some(crate::gpu::O1AttnParams {
3026 views,
3027 epoch: self.o1_epoch,
3028 }),
3029 None => None,
3031 }
3032 } else {
3033 None
3034 };
3035 let o1_layer = cache.o1.is_some();
3036 if o1_layer && o1p.is_none() {
3037 }
3039 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3040 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3041 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3042 debug_assert!(
3045 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3046 );
3047 let p = crate::gpu::AttnDeviceParams {
3048 kv_id,
3049 layer: *li,
3050 nh,
3051 nkv,
3052 hd,
3053 rd: rd_l,
3054 position,
3055 scale: self.attn_scale,
3056 eps: eps as f32,
3057 gemma,
3058 late_qk_norm: self.qk_norm_after_rope,
3059 output_gate: *output_gate,
3060 q_norm: *q_norm,
3061 k_norm: *k_norm,
3062 inv_freq: &inv_freq_l,
3063 rope_scale: self.layer_rope_scale(*li),
3064 cpu_k,
3065 cpu_v,
3066 cpu_stored,
3067 cpu_gen: cache.generation(),
3068 o1: o1p,
3069 window: window_l,
3070 head_gate: head_gate_w,
3071 v_norm: self.attn_v_norm,
3072 };
3073 let o1_bad = o1_layer && p.o1.is_none();
3074 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3075 {
3076 if p.o1.is_none() {
3078 dev_attn.push((*li, nkv, hd));
3079 }
3080 graph.commit_kind = 3;
3081 graph.commit();
3082 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3086 continue;
3087 }
3088 }
3090 graph.encode_attn_prefix(l);
3091 if let Err(err) = graph.sync_checked() {
3092 self.fail_metal_graph(&err);
3093 return start;
3094 }
3095 if !pending.is_empty() {
3096 let idxs: Vec<usize> =
3097 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3098 let mut outs: Vec<&mut [f32]> = self
3099 .kv_cache
3100 .layers
3101 .iter_mut()
3102 .enumerate()
3103 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3104 .map(|(_, s)| s.linear_state.as_mut_slice())
3105 .collect();
3106 graph.read_states(&mut outs);
3107 }
3108 let mut q_raw = attention::take_buf(l.wq.1);
3109 let mut k = attention::take_buf(l.wk.1);
3110 let mut v = attention::take_buf(l.wv.1);
3111 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3112 let mut gate_raw = proj_gate.map(|(gp, _)| {
3115 let mut normed = attention::take_buf(hs);
3116 graph.read_normed(&mut normed);
3117 let mut raw = attention::take_buf(gp.rows());
3118 gp.matvec(&normed, &mut raw, pool.as_deref());
3119 attention::recycle_buf(&mut normed);
3120 raw
3121 });
3122 let cfg = QwenAttnCfg {
3123 num_heads: nh,
3124 num_kv_heads: nkv,
3125 head_dim: hd,
3126 hidden_size: hs,
3127 position,
3128 inv_freq: &inv_freq_l,
3129 rotary_dim: rd_l,
3130 scale: self.attn_scale,
3131 softcap: self.attn_softcap,
3132 window: window_l,
3133 v_norm: self.attn_v_norm,
3136 qk_norm_after_rope: self.qk_norm_after_rope,
3137 gate_sigmoid: self.proj_gate_sigmoid,
3138 q_norm: *q_norm,
3139 k_norm: *k_norm,
3140 output_gate: *output_gate,
3141 softplus_gate: None,
3142 rope_scale: self.layer_rope_scale(*li),
3143 bias: *bias,
3144 rms_eps: eps,
3145 norm_style,
3146 pool: pool.as_deref(),
3147 v_head_dim: hd,
3148 };
3149 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3152 || std::env::var("CMF_ATTN_DUMP").is_ok();
3153 let _ = full_gpu;
3154 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3155 let mut ao = attention::qwen_attention_core(
3156 q_raw,
3157 k,
3158 v,
3159 &mut self.kv_cache.layers[*li],
3160 &cfg,
3161 );
3162 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3166 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3167 let (cq, _cg, _ck, _cv) =
3168 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3169 let cache = &self.kv_cache.layers[*li];
3170 let n = cache.head_keys(0).len() / hd;
3171 let mut bytes: Vec<u8> = Vec::new();
3172 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3173 bytes.extend_from_slice(&v.to_le_bytes());
3174 }
3175 for v in &cq {
3176 bytes.extend_from_slice(&v.to_le_bytes());
3177 }
3178 for g in 0..nkv {
3179 for v in cache.head_keys(g) {
3180 bytes.extend_from_slice(&v.to_le_bytes());
3181 }
3182 }
3183 for g in 0..nkv {
3184 for v in cache.head_values(g) {
3185 bytes.extend_from_slice(&v.to_le_bytes());
3186 }
3187 }
3188 let _ =
3189 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3190 }
3191 }
3192 if let Some((qr0, k0, v0)) =
3193 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3194 {
3195 let (cq, _cg, ck, cv) =
3196 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3197 let mut h_now = vec![0f32; hs];
3198 graph.read_h(&mut h_now);
3199 let cache = &self.kv_cache.layers[*li];
3200 let n_after = cache.head_keys(0).len() / hd;
3201 let stored = n_after.saturating_sub(1);
3205 let cpu_k: Vec<&[f32]> = (0..nkv)
3206 .map(|g| &cache.head_keys(g)[..stored * hd])
3207 .collect();
3208 let cpu_v: Vec<&[f32]> = (0..nkv)
3209 .map(|g| &cache.head_values(g)[..stored * hd])
3210 .collect();
3211 let p = crate::gpu::AttnDeviceParams {
3212 kv_id,
3213 layer: *li,
3214 nh,
3215 nkv,
3216 hd,
3217 rd: rd_l,
3221 position,
3222 scale: self.attn_scale,
3223 eps: eps as f32,
3224 gemma,
3225 late_qk_norm: self.qk_norm_after_rope,
3226 output_gate: *output_gate,
3227 q_norm: *q_norm,
3228 k_norm: *k_norm,
3229 inv_freq: &inv_freq_l,
3230 rope_scale: self.layer_rope_scale(*li),
3231 cpu_k,
3232 cpu_v,
3233 cpu_stored: stored,
3234 cpu_gen: cache.generation(),
3235 o1: None,
3236 window: window_l,
3237 head_gate: None,
3238 v_norm: self.attn_v_norm,
3239 };
3240 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3241 let md = |a: &[f32], b: &[f32]| {
3242 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3243 };
3244 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3245 eprintln!(
3246 "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}",
3247 nn(&cq),
3248 md(&cq, &dq),
3249 nn(&ck),
3250 md(&ck, &dk),
3251 nn(&cv),
3252 md(&cv, &dv),
3253 nn(&ao),
3254 md(&ao, &dao)
3255 );
3256 } else {
3257 eprintln!("attn-oracle L{li}: device probe declined");
3258 }
3259 }
3260 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3261 attention::apply_projected_gate(
3264 &mut ao,
3265 raw,
3266 per_head,
3267 hd,
3268 self.proj_gate_sigmoid,
3269 );
3270 }
3271 if let Some(mut raw) = gate_raw.take() {
3272 attention::recycle_buf(&mut raw);
3273 }
3274 graph.encode_attn_suffix(l, &ao);
3275 graph.commit();
3278 attention::recycle_buf(&mut ao);
3279 }
3280 }
3281
3282 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3283 }
3284 let mut lm_rows = None;
3289 if self.graph_want_logits
3290 && upto.is_none()
3291 && end == self.num_layers
3292 && std::env::var("CMF_GPU_LMHEAD")
3293 .map(|v| v != "0")
3294 .unwrap_or(true)
3295 {
3296 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3297 if graph.lm_head_ok(lm) {
3298 graph.encode_lm_head(&self.weights.final_norm, lm);
3299 lm_rows = Some(lm.1);
3300 }
3301 }
3302 }
3303 if self.graph_head_required && lm_rows.is_none() {
3304 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3305 self.fail_metal_graph("fused graph head was requested but not encodable");
3306 return start;
3307 }
3308 let _sy0 = std::time::Instant::now();
3309 if let Err(err) = graph.sync_checked() {
3310 if let Some(profile) = pager_profile_page.as_mut() {
3311 profile.finish(start, graph.pager_page_stats(), "sync-error");
3312 }
3313 self.fail_metal_graph(&err);
3314 return start;
3315 }
3316 let _rs0 = std::time::Instant::now();
3317 if !pending.is_empty() {
3318 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3319 let mut outs: Vec<&mut [f32]> = self
3320 .kv_cache
3321 .layers
3322 .iter_mut()
3323 .enumerate()
3324 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3325 .map(|(_, s)| s.linear_state.as_mut_slice())
3326 .collect();
3327 graph.read_states(&mut outs);
3328 }
3329 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3330 use std::sync::atomic::{AtomicU64, Ordering};
3331 static SY: AtomicU64 = AtomicU64::new(0);
3332 static RS: AtomicU64 = AtomicU64::new(0);
3333 static N: AtomicU64 = AtomicU64::new(0);
3334 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3335 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3336 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3337 if n % 100 == 0 {
3338 eprintln!(
3339 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3340 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3341 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3342 );
3343 }
3344 }
3345 if let Some(rows) = lm_rows {
3346 crate::gpu::hostprof_encode_done(_mt0);
3347 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3348 graph.read_logits(&mut lg);
3349 crate::gpu::hostprof_total(_mt0);
3350 lg.resize(self.vocab_size, 0.0);
3351 if let Some(c) = self.final_softcap {
3352 softcap_in_place(&mut lg, c, self.pool.as_deref());
3353 }
3354 self.graph_logits = Some(lg);
3355 }
3356 graph.read_h(h);
3357 if self.graph_head_required && self.graph_logits.is_none() {
3358 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3359 self.fail_metal_graph("fused graph head completed without logits readback");
3360 return start;
3361 }
3362 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3363 METAL_GRAPH_LAYERS.fetch_add(
3364 end.saturating_sub(start) as u64,
3365 std::sync::atomic::Ordering::Relaxed,
3366 );
3367 if self.graph_head_required {
3368 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3369 }
3370 for (li, nkv, hd) in dev_attn {
3374 let mut krow = attention::take_buf(nkv * hd);
3375 let mut vrow = attention::take_buf(nkv * hd);
3376 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3377 let cache = &mut self.kv_cache.layers[li];
3378 cache.append(&krow, &vrow, &[]);
3379 let n = cache.seq_len;
3380 let mut imp = attention::take_buf(n);
3381 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3382 cache.accumulate_imp(&imp);
3383 attention::recycle_buf(&mut imp);
3384 }
3385 attention::recycle_buf(&mut krow);
3386 attention::recycle_buf(&mut vrow);
3387 }
3388 if let Some(profile) = pager_profile_page.as_mut() {
3389 profile.finish(end, graph.pager_page_stats(), "page-complete");
3390 }
3391 if let Some((_, arm)) = ab {
3392 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3393 }
3394 end
3395 }
3396
3397 pub fn new(
3398 tokenizer: Tokenizer,
3399 weights: PipelineWeights,
3400 hidden_size: usize,
3401 intermediate_size: usize,
3402 num_heads: usize,
3403 num_kv_heads: usize,
3404 head_dim: usize,
3405 num_layers: usize,
3406 physical_layers: usize,
3407 loop_final_norm: bool,
3408 vocab_size: usize,
3409 rms_eps: f64,
3410 rope_base: f32,
3411 norm_style: NormStyle,
3412 max_seq_len: usize,
3413 sampler_config: SamplerConfig,
3414 ) -> Self {
3415 let rng = match sampler_config.seed {
3416 Some(s) => SplitMix64::new(s),
3417 None => SplitMix64::from_entropy(),
3418 };
3419 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3420 let pool = Pool::from_env();
3421 if let Some(p) = &pool {
3422 tracing::info!("worker pool: {} threads", p.n_workers());
3423 if let Some(model) = weights
3425 .lm_head
3426 .model_arc()
3427 .or_else(|| weights.embed_tokens.model_arc())
3428 {
3429 let regions: Vec<&[u8]> =
3430 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3431 p.bind_numa(®ions);
3432 }
3433 }
3434 Self {
3435 gpu_plan: None,
3436 tokenizer: std::sync::Arc::new(tokenizer),
3437 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3438 sampler_config,
3439 weights,
3440 hidden_size,
3441 intermediate_size,
3442 num_heads,
3443 num_kv_heads,
3444 head_dim,
3445 num_layers,
3446 physical_layers,
3447 loop_final_norm,
3448 vocab_size,
3449 rms_eps,
3450 rope_base,
3451 norm_style,
3452 rotary_dim: head_dim,
3453 attention_heads_per_layer: None,
3454 kv_heads_per_layer: None,
3455 v_head_dim: None,
3456 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3457 .filter(|v| !v.is_empty())
3458 .map(std::path::PathBuf::from),
3459 graph_declines: std::cell::RefCell::new(Vec::new()),
3460 mimo_moe: Default::default(),
3461 minicpm_tail: Default::default(),
3462 vmf_cfg: None,
3463 gdn_cfg: None,
3464 kda_cfg: None,
3465 g3n: None,
3466 dsv4: None,
3467 dsv41: None,
3468 dsv41_vision: None,
3469 dsv41_prefill: None,
3470 qwen4_exp: None,
3471 dsv4_mtp: Vec::new(),
3472 dspark: None,
3473 dspark_pending: Vec::new(),
3474 dspark_hist: Vec::new(),
3475 dspark_real: Vec::new(),
3476 dspark_trunk_picks: Vec::new(),
3477 dspark_exp: Vec::new(),
3478 dspark_draft_ns: 0,
3479 logit_multiplier: None,
3480 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3481 graph_failed: std::sync::atomic::AtomicBool::new(false),
3482 kv_history: Vec::new(),
3483 kv_history_device: false,
3484 short_conv_cfg: None,
3485 mtp: None,
3486 mimo_mtp: None,
3487 verify_exact_moe: false,
3488 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3489 ignore_eos: false,
3490 draft_full_streak: 0,
3491 spec_k_adapt: None,
3492 spec_acc_ewma: 0.7,
3493 rng,
3494 sampler_scratch: SamplerScratch::default(),
3495 spec_forced: None,
3496 spec_q: Vec::new(),
3497 spec_p: Vec::new(),
3498 spec_res: Vec::new(),
3499 spec_qs: Vec::new(),
3500 spec_ps: Vec::new(),
3501 spec_ress: Vec::new(),
3502 mtp_graph_mode: None,
3503 #[cfg(target_os = "macos")]
3504 metal_verify: None,
3505 inv_freq,
3506 ws: ForwardScratch::new(hidden_size),
3507 pool,
3508 model: None,
3509 dyn_force_f32: false,
3510 dyn_skill_layers: Vec::new(),
3511 dyn_active: None,
3512 dyn_blend_loaded: false,
3513 dyn_phi_layer: None,
3514 dyn_phi_ema: Vec::new(),
3515 dyn_phi_seen: 0,
3516 dyn_router: None,
3517 o1_cfg: None,
3518 o1_epoch: 0,
3519 o1_flags: Vec::new(),
3520 trace: false,
3521 calib_temp: 1.0,
3522 confidence_on: true,
3523 embed_multiplier: 1.0,
3524 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3525 swa: None,
3526 swa_trim: None,
3527 sliding_layers: None,
3528 anchor_core: None,
3529 bounded_rope: None,
3530 kv_prefix: KvPrefix::default(),
3531 last_prefill_tokens: 0,
3532 inv_freq_local: None,
3533 rotary_dim_local: None,
3534 rope_scale: 1.0,
3535 rope_scale_local: 1.0,
3536 global_attn: None,
3537 inv_freq_global: None,
3538 attn_v_norm: false,
3539 qk_norm_after_rope: false,
3540 proj_gate_sigmoid: false,
3541 final_softcap: None,
3542 head_clusters: None,
3543 attn_softcap: 0.0,
3544 graph_want_logits: false,
3545 graph_head_required: false,
3546 graph_logits: None,
3547 embryo_graph: None,
3548 graph_refused: std::sync::atomic::AtomicBool::new(false),
3549 graph_kv_id: {
3550 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3551 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3552 },
3553 #[cfg(test)]
3554 nll_test_fail_at: None,
3555 #[cfg(test)]
3556 nll_test_force_serial: false,
3557 }
3558 }
3559
3560 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3568 if let Err(e) = self.try_set_o1(cfg) {
3569 tracing::error!("{e}");
3570 }
3571 }
3572
3573 pub fn bounded_native(&self) -> bool {
3577 self.anchor_core.is_some()
3578 }
3579
3580 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3583 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3584 }
3585
3586 pub fn o1_refusal(&self) -> Option<String> {
3588 self.anchor_core.as_ref().map(|ac| {
3589 format!(
3590 "--o1 / CMF_O1 refused: the anchor is native bounded \
3591 (anchor_core kind={} window={} sink={}); the file's operator \
3592 is executed as-is and no post-hoc Nyström overlay applies",
3593 ac.kind, ac.window, ac.sink
3594 )
3595 })
3596 }
3597
3598 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3600 if let Some(c) = &cfg {
3601 if let Some(why) = self.o1_refusal() {
3602 self.o1_flags = Vec::new();
3603 self.o1_cfg = None;
3604 return Err(why);
3605 }
3606 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3607 self.o1_flags.clear();
3608 self.o1_cfg = None;
3609 return Err(format!(
3610 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3611 c.w, c.sink
3612 ));
3613 }
3614 }
3615 self.o1_flags = match &cfg {
3616 Some(c) => {
3617 let mut flags = c.layer_flags(self.num_layers);
3618 for (li, f) in flags.iter_mut().enumerate() {
3619 if *f
3625 && (!matches!(
3626 self.weights.layers[self.phys_layer(li)].attn,
3627 AttnKind::Full { .. }
3628 ) || self.layer_window(li).is_some()
3629 || self.kv_cache.layers[li].sinks.is_some()
3630 || self.layer_v_dim(li) != self.layer_geom(li).1)
3631 {
3632 *f = false;
3633 }
3634 }
3635 flags
3636 }
3637 None => Vec::new(),
3638 };
3639 if let Some(c) = &cfg {
3640 let n = self.o1_flags.iter().filter(|&&f| f).count();
3641 tracing::info!(
3642 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3643 self.num_layers,
3644 c.m,
3645 c.w,
3646 c.sink,
3647 c.rect
3648 );
3649 }
3650 self.o1_cfg = cfg;
3651 Ok(())
3652 }
3653
3654 pub fn install_bounded(
3660 &mut self,
3661 cfg: &cortiq_core::AnchorCoreConfig,
3662 ) -> Result<(), String> {
3663 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3664 return Err(format!(
3665 "anchor_core kind '{}' is not executable by this runtime",
3666 cfg.kind
3667 ));
3668 }
3669 if cfg.window == 0 {
3670 return Err("anchor_core.window must be >= 1".into());
3671 }
3672 let mut n = 0usize;
3673 for li in 0..self.num_layers {
3674 let pl = self.phys_layer(li);
3675 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3676 if w.window != cfg.window || w.sink != cfg.sink {
3677 return Err(format!(
3678 "layer {li}: bounded weights (window {} sink {}) disagree with \
3679 anchor_core (window {} sink {})",
3680 w.window, w.sink, cfg.window, cfg.sink
3681 ));
3682 }
3683 self.kv_cache.layers[li].install_bounded(cfg.window);
3684 n += 1;
3685 }
3686 }
3687 if n == 0 {
3688 return Err("anchor_core is present but no layer executes it".into());
3689 }
3690 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3691 self.bounded_rope = Some(std::sync::Arc::new(rope));
3692 self.anchor_core = Some(cfg.clone());
3693 self.embryo_graph = None;
3694 tracing::info!(
3695 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3696 cfg.kind,
3697 cfg.window,
3698 cfg.sink,
3699 self.kv_cache.layers.iter().map(|l| l.bounded_state_bytes()).max().unwrap_or(0)
3700 );
3701 Ok(())
3702 }
3703
3704 pub fn install_wire_identity(&mut self, identity: u64) {
3708 for li in 0..self.kv_cache.layers.len() {
3709 let pl = self.phys_layer(li);
3710 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3711 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3712 Some(AttnKind::Linear(_))
3713 | Some(AttnKind::LinearGdn(_))
3714 | Some(AttnKind::ShortConv(_))
3715 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3716 _ => crate::kv_cache::WireKind::Full,
3717 };
3718 let l = &mut self.kv_cache.layers[li];
3719 l.wire_kind = kind;
3720 l.wire_identity = identity;
3721 l.wire_layer = li as u32;
3724 }
3725 }
3726
3727 pub fn clear_history(&mut self) {
3729 self.kv_history.clear();
3730 self.kv_history_device = false;
3731 self.kv_prefix.clear();
3732 }
3733
3734 pub fn graph_refused(&self) -> bool {
3738 self.graph_refused
3739 .load(std::sync::atomic::Ordering::Relaxed)
3740 }
3741
3742 pub fn mark_graph_refused(&self) {
3744 if !self
3745 .graph_refused
3746 .swap(true, std::sync::atomic::Ordering::Relaxed)
3747 {
3748 tracing::info!(
3749 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3750 self.graph_kv_id
3751 );
3752 }
3753 }
3754
3755 pub fn device_sequence_position(&self) -> Option<usize> {
3759 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3760 }
3761
3762 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3767 let dev = self.device_sequence_position();
3768 if recorded_on_device {
3769 dev == Some(n) && self.embryo_resident_wanted()
3770 } else {
3771 dev.is_none()
3772 }
3773 }
3774
3775 pub(crate) fn invalidate_for_weight_change(&mut self) {
3783 self.clear_sequence_state();
3784 self.embryo_graph = None;
3785 }
3786
3787 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3793 let (n, on_device) = if self.bounded_native() {
3794 (self.kv_prefix.extension(input_ids), self.kv_prefix.on_device())
3795 } else {
3796 let h = &self.kv_history;
3797 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3798 (h.len(), self.kv_history_device)
3799 } else {
3800 (0, false)
3801 }
3802 };
3803 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3806 tracing::warn!(
3807 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3808 the device now holds {:?} — re-prefilling from zero",
3809 if on_device { "resident device" } else { "host" },
3810 self.device_sequence_position()
3811 );
3812 return 0;
3813 }
3814 n
3815 }
3816
3817 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3820 self.cached_prefix_len(input_ids)
3821 }
3822
3823 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3829 let on_device = self.device_sequence_position().is_some();
3830 if self.bounded_native() {
3831 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3832 let prev_device = self.kv_prefix.on_device();
3833 self.kv_history.clear();
3834 self.kv_history_device = false;
3835 if keep && prev_device == on_device {
3836 self.kv_prefix.extend(&consumed[reused..]);
3837 } else {
3838 self.kv_prefix.set(consumed);
3839 }
3840 self.kv_prefix.set_on_device(on_device);
3841 } else {
3842 self.kv_history = consumed.to_vec();
3843 self.kv_history_device = on_device;
3844 }
3845 }
3846
3847 pub fn o1_active(&self) -> bool {
3849 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3850 }
3851
3852 pub fn generation_batch_k(&self) -> usize {
3870 if let Some(k) = std::env::var("CMF_BATCH_K")
3871 .ok()
3872 .and_then(|v| v.parse::<usize>().ok())
3873 {
3874 return k;
3875 }
3876 if self.g4_batch_prefill() {
3879 return 128;
3880 }
3881 #[cfg(not(target_os = "macos"))]
3882 if self.graph_prefill_preferred() && !self.o1_active() {
3883 let moe = self
3886 .weights
3887 .layers
3888 .iter()
3889 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
3890 let q82 = crate::gpu::moe_q82_graph_on()
3896 && self.weights.layers.iter().any(|lw| {
3897 matches!(&lw.ffn, FfnKind::Moe(m)
3898 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
3899 });
3900 return if q82 {
3901 128
3902 } else if moe {
3903 32
3904 } else {
3905 128
3906 };
3907 }
3908 0
3909 }
3910
3911 pub fn generation_graph_prefill(&self) -> bool {
3912 let graph = self.graph_prefill_preferred();
3913 #[cfg(not(target_os = "macos"))]
3924 if graph
3925 && self.generation_batch_k() > 0
3926 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3927 {
3928 return false;
3929 }
3930 graph
3931 }
3932
3933 pub fn o1_device_stats(&self) -> (usize, u64) {
3938 crate::gpu::o1_device_stats(self.graph_kv_id)
3939 }
3940
3941 pub fn o1_begin(&mut self) {
3946 self.o1_begin_with_prefix(None);
3947 }
3948
3949 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
3953 if let Some(c) = &self.o1_cfg {
3954 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
3955 let boundary = requested_prefix.map(|p| {
3956 p.max(
3957 crate::nystrom::o1_deferred_boundary(w, sink)
3958 .expect("o1 config boundary validated in set_o1"),
3959 )
3960 });
3961 for (li, &f) in self.o1_flags.iter().enumerate() {
3962 if f {
3963 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
3964 }
3965 }
3966 }
3967 }
3968
3969 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
3971 self.o1_cfg.as_ref().and_then(|c| {
3972 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
3973 .map(|floor| requested_prefix.max(floor))
3974 })
3975 }
3976
3977 fn o1_note_transition(&mut self) {
3978 let mut transitioned = false;
3982 for (li, &flagged) in self.o1_flags.iter().enumerate() {
3983 if flagged {
3984 transitioned |= self.kv_cache.layers[li].take_o1_transition();
3985 }
3986 }
3987 if transitioned {
3988 self.o1_epoch = self.o1_epoch.wrapping_add(1);
3989 }
3990 }
3991
3992 fn o1_pending(&self) -> bool {
3993 self.o1_flags.iter().enumerate().any(|(li, &f)| {
3994 f && self.kv_cache.layers[li].seq_len > 0
3995 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
3996 })
3997 }
3998
3999 fn o1_fail(&mut self, err: String) {
4000 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4001 self.clear_sequence_state();
4002 self.graph_failed
4003 .store(true, std::sync::atomic::Ordering::Relaxed);
4004 self.cancel
4005 .store(true, std::sync::atomic::Ordering::Relaxed);
4006 }
4007
4008 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4013 if self.o1_cfg.is_none() {
4014 return Ok(false);
4015 }
4016 let mut participating = false;
4017 for li in 0..self.num_layers {
4018 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4019 continue;
4020 }
4021 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4022 return Err(err);
4023 }
4024 if self.kv_cache.layers[li].seq_len == 0 {
4025 continue;
4026 }
4027 participating = true;
4028 let num_heads = self.layer_num_heads(li);
4029 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4030 }
4031 self.o1_note_transition();
4032 for li in 0..self.num_layers {
4033 if self.o1_flags.get(li).copied().unwrap_or(false) {
4034 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4035 return Err(err);
4036 }
4037 }
4038 }
4039 Ok(participating
4040 && (0..self.num_layers).all(|li| {
4041 !self.o1_flags.get(li).copied().unwrap_or(false)
4042 || self.kv_cache.layers[li].seq_len == 0
4043 || self.kv_cache.layers[li].o1_sealed()
4044 }))
4045 }
4046
4047 fn o1_progress(&mut self) {
4050 if !self.o1_active() {
4051 return;
4052 }
4053 for li in 0..self.num_layers {
4054 if self.o1_flags.get(li).copied().unwrap_or(false) {
4055 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4056 self.o1_fail(err);
4057 return;
4058 }
4059 }
4060 }
4061 self.o1_note_transition();
4065 if !self.o1_pending() {
4066 return;
4067 }
4068 if let Err(err) = self.o1_seal_checked() {
4069 self.o1_fail(err);
4070 }
4071 }
4072
4073 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4078 if self
4079 .graph_failed
4080 .swap(false, std::sync::atomic::Ordering::Relaxed)
4081 {
4082 self.cancel
4083 .store(false, std::sync::atomic::Ordering::Relaxed);
4084 self.clear_sequence_state();
4085 return Err(format!("{phase}: deferred O(1) transition failed"));
4086 }
4087 Ok(())
4088 }
4089
4090 pub fn o1_seal(&mut self) {
4094 if let Err(err) = self.o1_seal_checked() {
4095 self.o1_fail(err);
4096 }
4097 }
4098
4099 pub fn set_trace(&mut self, on: bool) {
4101 self.trace = on;
4102 }
4103
4104 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4107 self.rng = match config.seed {
4108 Some(seed) => SplitMix64::new(seed),
4109 None => SplitMix64::from_entropy(),
4110 };
4111 self.sampler_config = config;
4112 }
4113
4114 pub fn set_confidence(&mut self, on: bool) {
4119 self.confidence_on = on;
4120 }
4121
4122 pub fn set_calib_temp(&mut self, t: f32) {
4125 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4126 }
4127
4128 pub fn calib_temp(&self) -> f32 {
4130 self.calib_temp
4131 }
4132
4133 pub fn rope_inv_freq(&self) -> &[f32] {
4137 self.inv_freq.as_slice()
4138 }
4139
4140 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4143 self.rotary_dim = rotary_dim.min(self.head_dim);
4144 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4145 self.embryo_graph = None;
4149 }
4150
4151 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4152 QwenAttnCfg {
4153 num_heads: self.num_heads,
4154 num_kv_heads: self.num_kv_heads,
4155 head_dim: self.head_dim,
4156 hidden_size: self.hidden_size,
4157 position,
4158 inv_freq: &self.inv_freq,
4159 rotary_dim: self.rotary_dim,
4160 scale: self.attn_scale,
4161 softcap: self.attn_softcap,
4162 window: None,
4163 v_norm: false,
4164 qk_norm_after_rope: self.qk_norm_after_rope,
4165 gate_sigmoid: self.proj_gate_sigmoid,
4166 q_norm: None,
4167 k_norm: None,
4168 output_gate: false,
4169 softplus_gate: None,
4170 rope_scale: self.rope_scale,
4171 bias: None,
4172 rms_eps: self.rms_eps,
4173 norm_style: self.norm_style,
4174 pool: self.pool.as_deref(),
4175 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4176 }
4177 }
4178
4179 pub fn generate(
4181 &mut self,
4182 prompt: &str,
4183 max_tokens: usize,
4184 task_mask: Option<&TaskMask>,
4185 on_token: Option<TokenCallback>,
4186 ) -> Result<GenerateResult, String> {
4187 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4188 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4189 }
4190
4191 pub fn generate_from_vl(
4194 &mut self,
4195 input: &crate::dsv41_vision::PreparedVlInputs,
4196 max_tokens: usize,
4197 task_mask: Option<&TaskMask>,
4198 on_token: Option<TokenCallback>,
4199 ) -> Result<GenerateResult, String> {
4200 let Some(dsv41) = &self.dsv41 else {
4201 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4202 };
4203 if input.token_ids.is_empty() {
4204 return Err("empty V4.1 multimodal prompt".into());
4205 }
4206 if input.token_types.len() != input.token_ids.len() {
4207 return Err(format!(
4208 "V4.1 token type count {} != token count {}",
4209 input.token_types.len(),
4210 input.token_ids.len()
4211 ));
4212 }
4213 let dim = dsv41.2.dim;
4214 let mut embeddings = vec![None; input.token_ids.len()];
4215 let mut participates = vec![true; input.token_ids.len()];
4216 if !input.images.is_empty() {
4217 let vision = self
4218 .dsv41_vision
4219 .as_ref()
4220 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4221 for image in &input.images {
4222 let end = image.start.saturating_add(image.types.len());
4223 if end > input.token_ids.len() {
4224 return Err(format!(
4225 "V4.1 image span {}..{} exceeds prompt length {}",
4226 image.start,
4227 end,
4228 input.token_ids.len()
4229 ));
4230 }
4231 let mut span = vec![0.0f32; image.types.len() * dim];
4232 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4233 for (offset, &kind) in image.types.iter().enumerate() {
4234 let pos = image.start + offset;
4235 if input.token_types[pos] != kind {
4236 return Err(format!(
4237 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4238 input.token_types[pos]
4239 ));
4240 }
4241 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4242 participates[pos] = false;
4243 }
4244 }
4245 }
4246 for (pos, &kind) in input.token_types.iter().enumerate() {
4247 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4248 return Err(format!("V4.1 text position {pos} has an image embedding"));
4249 }
4250 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4251 return Err(format!("V4.1 image position {pos} has no image embedding"));
4252 }
4253 }
4254 self.dsv41_prefill = Some((embeddings, participates));
4255 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4256 self.dsv41_prefill = None;
4257 result
4258 }
4259
4260 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4262 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4263 }
4264
4265 pub fn generate_from_ids(
4273 &mut self,
4274 input_ids: &[u32],
4275 max_tokens: usize,
4276 task_mask: Option<&TaskMask>,
4277 on_token: Option<TokenCallback>,
4278 ) -> Result<GenerateResult, String> {
4279 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4280 }
4281
4282 pub fn generate_from_embeds(
4288 &mut self,
4289 input_ids: &[u32],
4290 prompt_rows: &[f32],
4291 max_tokens: usize,
4292 task_mask: Option<&TaskMask>,
4293 on_token: Option<TokenCallback>,
4294 ) -> Result<GenerateResult, String> {
4295 if input_ids.is_empty()
4296 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4297 {
4298 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4299 }
4300 if prompt_rows.iter().any(|x| !x.is_finite()) {
4301 return Err("embedded prompt contains non-finite values".into());
4302 }
4303 if !self.can_prefill_batched() || self.dyn_router.is_some()
4304 || self.o1_active() || self.mtp.is_some() || self.gpu_plan.is_some()
4305 {
4306 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4307 }
4308 self.generate_with_prompt_rows(input_ids, Some(prompt_rows), max_tokens, task_mask, on_token)
4309 }
4310
4311 fn generate_with_prompt_rows(
4312 &mut self,
4313 input_ids: &[u32],
4314 prompt_rows: Option<&[f32]>,
4315 max_tokens: usize,
4316 task_mask: Option<&TaskMask>,
4317 mut on_token: Option<TokenCallback>,
4318 ) -> Result<GenerateResult, String> {
4319 #[cfg(target_os = "macos")]
4320 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4321 if std::env::var("CMF_TRACE_H").is_ok() {
4322 eprintln!("input_ids: {input_ids:?}");
4323 }
4324 if input_ids.is_empty() {
4325 return Err("empty prompt: nothing to generate from".to_string());
4326 }
4327 self.graph_failed
4331 .store(false, std::sync::atomic::Ordering::Relaxed);
4332 let task_mask = self.drop_open_mask(task_mask);
4337
4338 let mut reuse_from = {
4346 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4347 if on
4348 && prompt_rows.is_none()
4349 && task_mask.is_none()
4350 && self.mtp.is_none()
4351 && !(self.mimo_mtp.is_some() && self.speculative)
4352 && self.o1_cfg.is_none()
4353 && self.dsv41.is_none()
4354 {
4355 self.cached_prefix_len(input_ids)
4356 } else {
4357 0
4358 }
4359 };
4360 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4363 reuse_from = 0;
4364 }
4365 self.last_prefill_tokens = input_ids.len() - reuse_from;
4366 let bounded_native = self.bounded_native();
4367 if reuse_from == 0 {
4368 self.clear_sequence_state();
4370 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4371 eprintln!(
4372 "kv-reuse: {} of {} prompt positions already cached",
4373 reuse_from,
4374 input_ids.len()
4375 );
4376 }
4377 crate::gpu::graph_race_begin_generation();
4378 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4382 std::env::var("CMF_O1_PREFILL")
4383 .ok()
4384 .and_then(|v| v.parse::<usize>().ok())
4385 .filter(|&p| p > 0)
4386 } else {
4387 None
4388 };
4389 if task_mask.is_none() {
4390 self.o1_begin_with_prefix(o1_prefill);
4391 }
4392
4393 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4399 #[cfg(target_os = "macos")]
4438 let metal_graph = crate::gpu::q1_force()
4439 && crate::gpu::enabled_here()
4440 && std::env::var("CMF_GPU_BLOCK")
4441 .map(|v| v != "0")
4442 .unwrap_or(true);
4443 #[cfg(not(target_os = "macos"))]
4444 let metal_graph = false;
4445 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4446 let spec_cheap_round = self.sampler_config.temperature < 1e-6
4450 || sampler::sparse_ok(&self.sampler_config);
4451 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4452 || match spec_sample_env.as_deref() {
4453 Some("1") => true,
4454 Some(_) => false,
4455 None => metal_graph && spec_cheap_round,
4456 };
4457 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4473 for lw in &self.weights.layers {
4474 if let FfnKind::Dense(d) = &lw.ffn {
4475 dense_n += 1;
4476 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4477 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4478 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4479 {
4480 dense_q4tp += 1;
4481 }
4482 }
4483 }
4484 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4485 let penalized = !metal_graph
4497 && (self.sampler_config.repetition_penalty != 1.0
4498 || self.sampler_config.presence_penalty != 0.0
4499 || !self.sampler_config.suppress_tokens.is_empty());
4500 #[cfg(feature = "gpu")]
4505 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4506 #[cfg(not(feature = "gpu"))]
4507 let metal_wgpu = false;
4508 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4509 let spec_wanted = match spec_env.as_deref() {
4510 Some("0") => false,
4511 Some(_) => {
4512 if metal_wgpu {
4513 tracing::warn!(
4514 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4515 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4516 );
4517 }
4518 true
4519 }
4520 None => spec_default_ok && !penalized && !metal_wgpu,
4521 };
4522 let graph_spec = self.speculative
4526 && (graph_on || metal_graph)
4527 && self.mtp.is_some()
4528 && task_mask.is_none()
4529 && !self.o1_active()
4530 && spec_sampling_ok
4531 && spec_wanted;
4532 #[cfg(target_os = "macos")]
4536 if metal_graph {
4537 static SAID: std::sync::Once = std::sync::Once::new();
4538 SAID.call_once(|| {
4539 let spec = if graph_spec {
4540 let k = std::env::var("CMF_GRAPH_SPEC_K")
4541 .ok()
4542 .and_then(|v| v.parse::<usize>().ok())
4543 .filter(|&v| (1..=8).contains(&v))
4544 .unwrap_or(7);
4545 let arm = if self.sampler_config.temperature < 1e-6 {
4546 "greedy"
4547 } else {
4548 "sampling"
4549 };
4550 format!(
4551 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4552 Self::draft_vocab_rows(usize::MAX)
4553 )
4554 } else if !self.speculative {
4555 "spec off (CMF_MTP=0)".to_string()
4556 } else if self.mtp.is_none() {
4557 "spec off (no MTP head)".to_string()
4558 } else if !spec_sampling_ok {
4559 if spec_cheap_round {
4560 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4561 } else {
4562 "spec off (sampling without a top-k: the dense chain \
4563 costs more than it saves)"
4564 .to_string()
4565 }
4566 } else if !spec_wanted {
4567 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4568 } else if task_mask.is_some() {
4569 "spec off (task mask)".to_string()
4570 } else {
4571 "spec off (O(1) attention)".to_string()
4572 };
4573 let on = |var: &str| {
4574 if std::env::var(var).as_deref() == Ok("0") {
4575 "off"
4576 } else {
4577 "on"
4578 }
4579 };
4580 tracing::info!(
4581 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4582 MTP graph {}, attend {}, probe {}",
4583 if crate::gpu_metal::state4_on() { "on" } else { "off" },
4584 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
4585 on("CMF_METAL_PREFILL"),
4586 on("CMF_MTP_GRAPH"),
4587 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4588 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
4589 );
4590 });
4591 }
4592 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4599 let spec_active = self.speculative
4600 && self.mtp.is_some()
4601 && task_mask.is_none()
4602 && !self.o1_active()
4603 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4604 let mut mtp = if spec_active { self.mtp.take() } else { None };
4607 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4608 eprintln!(
4609 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4610 mtp.is_some(),
4611 self.speculative,
4612 self.sampler_config.temperature < 1e-6,
4613 );
4614 }
4615 if let Some(m) = &mut mtp {
4616 m.kv.clear();
4617 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4619 self.mtp_graph_mode = None;
4620 }
4621 let mimo_spec = self.speculative
4625 && self.mimo_mtp.is_some()
4626 && task_mask.is_none()
4627 && !self.o1_active()
4628 && self.dyn_router.is_none()
4629 && self.sampler_config.temperature < 1e-6
4630 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4631 if let Some(st) = self.mimo_mtp.as_mut() {
4632 st.reset();
4633 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4634 Self::mimo_mtp_hist_cap(st, input_ids.len());
4635 }
4636 }
4637 let mut router = if mtp.is_none() {
4641 self.dyn_router.take()
4642 } else {
4643 None
4644 };
4645 let mut reuse_from = reuse_from;
4646 if let Some(r) = &mut router {
4647 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4650 let _ = self.set_active_skill(None);
4653 reuse_from = 0;
4654 self.last_prefill_tokens = input_ids.len();
4655 }
4656 }
4657
4658 let mut all_ids = input_ids.to_vec();
4659 let mut generated = 0usize;
4660 let mut finish_reason = "max_tokens".to_string();
4661 let mut drafted = 0usize;
4662 let mut accepted = 0usize;
4663 let mut dsv4_spec_bad = 0usize;
4670 let mut dsv4_spec_retry_at = 0usize;
4671 let mut confidence: Vec<f32> = Vec::new();
4672 let trace_on = self.trace;
4673 let calib_temp = self.calib_temp;
4674 let mut traces: Vec<TokenTrace> = Vec::new();
4675
4676 let mut hidden = vec![0.0f32; self.hidden_size];
4682 let mut pos = reuse_from;
4683 let fuse_lm = mtp.is_none()
4692 && router.is_none()
4693 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4694 self.graph_logits = None;
4695 self.graph_want_logits = false;
4696 let _tpf = std::time::Instant::now();
4697 let batch_k = self.generation_batch_k();
4698 if let Some(rows) = prompt_rows {
4699 let hs = self.hidden_size;
4700 let chunk = self.prefill_chunk().max(1);
4701 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4702 let end = (pos + chunk).min(input_ids.len());
4703 let hb = match self.prefill_input_rows(
4704 PrefillIn::Hidden(&rows[pos * hs..end * hs]), pos, task_mask,
4705 ) {
4706 Ok(hb) => hb,
4707 Err(err) => {
4708 self.finish_generation(&mut mtp, &mut router, true);
4709 return Err(err);
4710 }
4711 };
4712 if mimo_spec { self.mimo_note_rows(&hb, pos); }
4713 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4714 pos = end;
4715 }
4716 }
4717 while self.qwen4_exp.is_some()
4728 && mtp.is_none()
4729 && pos < input_ids.len()
4730 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4731 {
4732 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4735 let want_logits = end == input_ids.len();
4736 let mut lg = Vec::new();
4737 if let Some(b) = &mut self.qwen4_exp {
4738 crate::qwen4_exp::forward_tokens(
4739 &b.0,
4740 &b.1,
4741 &b.2,
4742 &mut b.3,
4743 &input_ids[pos..end],
4744 pos,
4745 &self.inv_freq,
4746 self.pool.as_deref(),
4747 &mut lg,
4748 want_logits,
4749 );
4750 }
4751 if want_logits {
4752 self.graph_logits = Some(lg);
4753 }
4754 pos = end;
4755 hidden.fill(0.0);
4756 }
4757 while self.dsv4.is_some()
4758 && mtp.is_none()
4759 && pos < input_ids.len()
4760 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4761 {
4762 let end = (pos + prefill_chunk()).min(input_ids.len());
4763 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4764 let mut lg = Vec::new();
4765 if let Some(b) = &mut self.dsv4 {
4766 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4767 crate::dsv4::forward_chunk(
4768 g,
4769 layers,
4770 &cfg,
4771 st,
4772 &ids,
4773 pos,
4774 &self.inv_freq,
4775 self.pool.as_deref(),
4776 &mut lg,
4777 end == input_ids.len(),
4778 );
4779 }
4780 if end == input_ids.len() {
4781 self.graph_logits = Some(lg);
4782 }
4783 pos = end;
4784 hidden = vec![0.0; self.hidden_size];
4785 }
4786 let dsv41_prefill = self.dsv41_prefill.take();
4787 while self.dsv41.is_some()
4788 && mtp.is_none()
4789 && pos < input_ids.len()
4790 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4791 {
4792 let end = (pos + prefill_chunk()).min(input_ids.len());
4793 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4794 let mut lg = Vec::new();
4795 if let Some(b) = &mut self.dsv41 {
4796 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4797 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4798 crate::dsv41::forward_chunk_masked_with_embeddings(
4799 g,
4800 layers,
4801 cfg,
4802 st,
4803 &ids,
4804 pos,
4805 &embeddings[pos..end],
4806 &participates[pos..end],
4807 self.pool.as_deref(),
4808 &mut lg,
4809 );
4810 } else {
4811 crate::dsv41::forward_chunk(
4812 g,
4813 layers,
4814 cfg,
4815 st,
4816 &ids,
4817 pos,
4818 self.pool.as_deref(),
4819 &mut lg,
4820 );
4821 }
4822 }
4823 if end == input_ids.len() {
4824 self.graph_logits = Some(lg);
4825 }
4826 pos = end;
4827 hidden = vec![0.0; self.hidden_size];
4828 }
4829 let dyn_prefill = router.is_some();
4834 let o1_prefill_limit = o1_prefill
4842 .and_then(|requested| self.o1_effective_boundary(requested))
4843 .map(|boundary| boundary.min(input_ids.len()));
4844 let mut o1_sealed = false;
4845 if let Some(limit) = o1_prefill_limit {
4846 if self.can_prefill_batched() && limit > 2 {
4849 let chunk = self.prefill_chunk();
4850 let hs = self.hidden_size;
4851 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4852 let end = (pos + chunk).min(limit);
4853 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4854 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4855 pos = end;
4856 }
4857 } else {
4858 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4859 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4860 pos += 1;
4861 }
4862 }
4863 if pos >= limit {
4864 o1_sealed = match self.o1_seal_checked() {
4865 Ok(sealed) => sealed,
4866 Err(err) => {
4867 self.finish_generation(&mut mtp, &mut router, true);
4868 return Err(err);
4869 }
4870 };
4871 tracing::info!(
4872 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4873 o1_prefill.unwrap_or(0),
4874 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4875 .unwrap_or(limit),
4876 limit,
4877 input_ids.len()
4878 );
4879 }
4880 }
4881 let graph_prefill = self.graph_prefill_preferred();
4887 #[cfg(target_os = "macos")]
4895 if task_mask.is_none()
4896 && !dyn_prefill
4897 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4898 && crate::gpu::enabled_here()
4899 && self.gdn_cfg.is_some()
4900 && self.g3n.is_none()
4901 && input_ids.len() > 8
4902 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4903 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4904 {
4905 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4906 .ok()
4907 .and_then(|v| v.parse().ok())
4908 .filter(|&v| (16..=512).contains(&v))
4909 .unwrap_or(256);
4910 let hs = self.hidden_size;
4911 let _tp = std::time::Instant::now();
4912 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4913 let end = (pos + chunk).min(input_ids.len());
4914 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4915 MetalPrefillOutcome::Completed(hb) => hb,
4916 MetalPrefillOutcome::Declined => break,
4917 MetalPrefillOutcome::Failed => {
4918 self.finish_generation(&mut mtp, &mut router, true);
4919 return Err("ordinary Metal prefill failed after admission".into());
4920 }
4921 };
4922 if let Some(m) = &mut mtp {
4923 let n_pairs = if end < input_ids.len() {
4924 end - pos
4925 } else {
4926 end - pos - 1
4927 };
4928 if n_pairs > 0 {
4929 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
4930 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
4931 .collect();
4932 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
4933 for (j, (h, t)) in pairs.iter().enumerate() {
4934 let h = h.to_vec();
4935 let _ = self.mtp_step(m, &h, *t, pos + j);
4936 }
4937 }
4938 }
4939 }
4940 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4941 pos = end;
4942 }
4943 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4944 eprintln!(
4945 "metal-prefill: {} of {} tokens in {:.1} ms",
4946 pos,
4947 input_ids.len(),
4948 _tp.elapsed().as_secs_f64() * 1e3
4949 );
4950 }
4951 }
4952 #[cfg(not(target_os = "macos"))]
4957 if task_mask.is_none()
4958 && !dyn_prefill
4959 && mtp.is_none()
4960 && o1_prefill.is_none()
4961 && batch_k > 0
4962 && pos + 1 < input_ids.len()
4963 && self.g4_batch_prefill()
4964 {
4965 let t_g4 = std::time::Instant::now();
4966 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
4967 Ok((n, last)) => {
4968 if n > 0 {
4969 hidden.copy_from_slice(&last);
4970 }
4971 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4972 eprintln!(
4973 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
4974 input_ids.len() - pos,
4975 t_g4.elapsed().as_secs_f64() * 1e3
4976 );
4977 }
4978 pos += n;
4979 }
4980 Err(e) => {
4981 self.finish_generation(&mut mtp, &mut router, true);
4982 return Err(e);
4983 }
4984 }
4985 }
4986 self.mimo_moe_prepare();
4987 #[cfg(not(target_os = "macos"))]
4993 if task_mask.is_none()
4994 && !dyn_prefill
4995 && !graph_prefill
4996 && mtp.is_none()
4997 && o1_prefill.is_none()
4998 && !self.o1_active()
4999 && input_ids.len() > 2
5000 && self.batch_prefix_prefill()
5001 {
5002 let chunk = self.prefill_chunk().max(1);
5003 let hs = self.hidden_size;
5004 let t_bp = std::time::Instant::now();
5005 let pos0 = pos;
5006 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5007 let end = (pos + chunk).min(input_ids.len());
5008 let bk = end - pos;
5009 let mut hiddens = vec![0f32; bk * hs];
5010 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5011 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5012 }
5013 let positions: Vec<usize> = (pos..end).collect();
5014 let mut run = 0usize;
5015 let outcome = self.try_batch_graph_wgpu_prefix(
5016 &mut hiddens,
5017 &positions,
5018 bk,
5019 None,
5020 Some(&mut run),
5021 );
5022 match outcome {
5023 crate::gpu::BatchGraphOutcome::Completed => {
5024 let hb = if run < self.num_layers {
5025 self.prefill_batch_span(
5026 PrefillIn::Hidden(&hiddens),
5027 pos,
5028 None,
5029 run,
5030 self.num_layers,
5031 )
5032 } else {
5033 hiddens
5034 };
5035 if mimo_spec {
5036 self.mimo_note_rows(&hb, pos);
5037 }
5038 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5039 pos = end;
5040 }
5041 crate::gpu::BatchGraphOutcome::Failed => {
5042 self.finish_generation(&mut mtp, &mut router, true);
5043 return Err("batched prefix prefill failed after admission".into());
5044 }
5045 crate::gpu::BatchGraphOutcome::Declined => {
5046 #[cfg(feature = "gpu")]
5049 if pos > pos0 {
5050 self.pull_lagging_host_kv(0, self.num_layers, pos);
5051 }
5052 break;
5053 }
5054 }
5055 }
5056 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5057 eprintln!(
5058 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5059 pos - pos0,
5060 input_ids.len(),
5061 t_bp.elapsed().as_secs_f64() * 1e3
5062 );
5063 }
5064 }
5065 if task_mask.is_none()
5066 && !dyn_prefill
5067 && !graph_prefill
5068 && self.can_prefill_batched()
5069 && self.g3n.is_none()
5070 && o1_prefill.is_none()
5071 && input_ids.len() > 2
5072 {
5073 let chunk = self.prefill_chunk();
5079 let hs = self.hidden_size;
5080 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5081 let end = (pos + chunk).min(input_ids.len());
5082 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5083 if mimo_spec {
5084 self.mimo_note_rows(&hb, pos);
5085 }
5086 if let Some(m) = &mut mtp {
5087 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5088 .ok()
5089 .and_then(|v| v.parse().ok())
5090 .unwrap_or(0);
5091 for p in pos..end {
5092 if p + 1 < input_ids.len() {
5093 if probe >= 1 && p + 2 < input_ids.len() {
5094 let (d1, mut hx) = self.mtp_step_h(
5098 m,
5099 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5100 input_ids[p + 1],
5101 p,
5102 );
5103 let mut ok = d1 == input_ids[p + 2];
5104 Self::chain_probe_note(0, ok);
5105 let mut d_prev = d1;
5106 let mut extra = 0usize;
5107 for j in 1..probe {
5108 if p + 2 + j >= input_ids.len() {
5109 break;
5110 }
5111 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5112 extra += 1;
5113 ok = ok && dj == input_ids[p + 2 + j];
5114 Self::chain_probe_note(j, ok);
5115 d_prev = dj;
5116 hx = hj;
5117 }
5118 m.kv.truncate_last(extra);
5119 } else {
5120 let _ = self.mtp_step(
5121 m,
5122 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5123 input_ids[p + 1],
5124 p,
5125 );
5126 }
5127 }
5128 }
5129 }
5130 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5131 pos = end;
5132 }
5133 }
5134 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5135 if task_mask.is_none()
5136 && !dyn_prefill
5137 && !graph_prefill
5138 && !pair_off
5139 && self.pair_supported()
5140 && o1_prefill.is_none()
5141 {
5142 while pos + 1 < input_ids.len()
5143 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5144 {
5145 let e1 = self.embed_single(input_ids[pos]);
5146 let e2 = self.embed_single(input_ids[pos + 1]);
5147 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5148 if mimo_spec {
5149 self.mimo_note_rows(&h1, pos);
5150 self.mimo_note_rows(&h2, pos + 1);
5151 }
5152 self.commit_linear_scratch();
5154 if let Some(m) = &mut mtp {
5155 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5156 if pos + 2 < input_ids.len() {
5157 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5158 .ok()
5159 .and_then(|v| v.parse().ok())
5160 .unwrap_or(0);
5161 if probe >= 1 && pos + 3 < input_ids.len() {
5162 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5166 let mut ok = d1 == input_ids[pos + 3];
5167 Self::chain_probe_note(0, ok);
5168 let mut d_prev = d1;
5169 let mut extra = 0usize;
5170 for j in 1..probe {
5171 if pos + 3 + j >= input_ids.len() {
5172 break;
5173 }
5174 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5175 extra += 1;
5176 ok = ok && dj == input_ids[pos + 3 + j];
5177 Self::chain_probe_note(j, ok);
5178 d_prev = dj;
5179 hx = hj;
5180 }
5181 m.kv.truncate_last(extra);
5182 } else {
5183 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5184 }
5185 }
5186 }
5187 hidden = h2;
5188 pos += 2;
5189 }
5190 }
5191 let o1_batch_ready = o1_sealed
5204 && o1_prefill.is_some()
5205 && mtp.is_none()
5206 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5207 && (0..self.num_layers).all(|li| {
5208 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5209 cache.o1.is_none() || cache.o1_views().is_some()
5210 });
5211 let mtp_batch_prefill = mtp.is_some()
5216 && graph_prefill
5217 && task_mask.is_none()
5218 && !dyn_prefill
5219 && !self.o1_active()
5220 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5221 if batch_k > 0
5222 && (graph_prefill || o1_batch_ready)
5223 && task_mask.is_none()
5224 && (!self.o1_active() || o1_batch_ready)
5225 && (mtp.is_none() || mtp_batch_prefill)
5226 && !dyn_prefill
5227 && pos + 1 < input_ids.len()
5228 {
5229 let hs = self.hidden_size;
5230 let chunk = batch_k;
5231 while pos < input_ids.len() {
5232 let end = (pos + chunk).min(input_ids.len());
5233 let bk = end - pos;
5234 let mut hiddens = vec![0f32; bk * hs];
5235 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5236 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5237 }
5238 let positions: Vec<usize> = (pos..end).collect();
5239 let t_chunk = std::time::Instant::now();
5240 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5241 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5242 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5243 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5244 eprintln!(
5245 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5246 if o1_batch_ready {
5247 "o1"
5248 } else if mtp_batch_prefill {
5249 "ordinary_mtp"
5250 } else {
5251 "ordinary"
5252 },
5253 bk as f64 / (ms / 1000.0)
5254 );
5255 }
5256 {
5257 use std::sync::atomic::{AtomicBool, Ordering};
5258 static SAID: AtomicBool = AtomicBool::new(false);
5259 if !SAID.swap(true, Ordering::Relaxed) {
5260 if ok_b {
5261 tracing::info!(
5262 "batched prefill: ACTIVE mode={} (k={bk})",
5263 if o1_batch_ready {
5264 "o1"
5265 } else if mtp_batch_prefill {
5266 "ordinary_mtp"
5267 } else {
5268 "ordinary"
5269 }
5270 );
5271 } else {
5272 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5273 }
5274 }
5275 }
5276 if ok_b {
5277 if mimo_spec {
5278 self.mimo_note_rows(&hiddens, pos);
5279 }
5280 if mtp_batch_prefill {
5281 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5282 if n_pairs > 0 {
5283 let rows: Vec<Vec<f32>> = (0..n_pairs)
5289 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5290 .collect();
5291 let pairs: Vec<(&[f32], u32)> = rows
5292 .iter()
5293 .enumerate()
5294 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5295 .collect();
5296 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5297 eprintln!(
5298 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5299 pos,
5300 n_pairs,
5301 pos + n_pairs - 1,
5302 );
5303 }
5304 let warm_error = if let Some(m) = mtp.as_mut() {
5305 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5306 } else {
5307 None
5308 };
5309 if let Some(err) = warm_error {
5310 self.finish_generation(&mut mtp, &mut router, true);
5315 return Err(err.to_string());
5316 }
5317 }
5318 }
5319 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5320 pos = end;
5321 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5322 self.finish_generation(&mut mtp, &mut router, true);
5327 return Err(if o1_batch_ready {
5328 "sealed O(1) batch graph failed after admission".to_string()
5329 } else {
5330 "ordinary recurrent batch graph failed after admission".to_string()
5331 });
5332 } else {
5333 break; }
5335 }
5336 }
5337 if graph_prefill
5341 && task_mask.is_none()
5342 && mtp.is_none()
5343 && !dyn_prefill
5344 && pos == 0
5345 && input_ids.len() > 1
5346 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5347 {
5348 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5349 self.graph_logits = Some(lg);
5350 hidden = vec![0.0; self.hidden_size];
5351 pos = input_ids.len();
5352 }
5353 }
5354 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5355 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5356 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5357 if mimo_spec {
5358 self.mimo_note_rows(&hidden, pos);
5359 }
5360 if let Some(m) = &mut mtp {
5361 if pos + 1 < input_ids.len() {
5362 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5368 .ok()
5369 .and_then(|v| v.parse().ok())
5370 .unwrap_or(0);
5371 if probe >= 1 && pos + 2 < input_ids.len() {
5372 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5373 let mut ok = d1 == input_ids[pos + 2];
5374 Self::chain_probe_note(0, ok);
5375 let mut d_prev = d1;
5376 let mut extra = 0usize;
5377 for j in 1..probe {
5378 if pos + 2 + j >= input_ids.len() {
5379 break;
5380 }
5381 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5382 extra += 1;
5383 ok = ok && dj == input_ids[pos + 2 + j];
5384 Self::chain_probe_note(j, ok);
5385 d_prev = dj;
5386 hx = hj;
5387 }
5388 m.kv.truncate_last(extra);
5391 } else {
5392 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5393 }
5394 }
5395 }
5396 pos += 1;
5397 }
5398 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5399 eprintln!(
5400 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5401 input_ids.len(),
5402 _tpf.elapsed().as_secs_f64() * 1000.0
5403 );
5404 }
5405 if self
5406 .graph_failed
5407 .swap(false, std::sync::atomic::Ordering::Relaxed)
5408 {
5409 self.finish_generation(&mut mtp, &mut router, true);
5414 return Err("GPU token graph failed during prefill".to_string());
5415 }
5416 if self
5419 .cancel
5420 .swap(false, std::sync::atomic::Ordering::Relaxed)
5421 {
5422 self.finish_generation(&mut mtp, &mut router, true);
5426 return Ok(GenerateResult {
5427 text: String::new(),
5428 token_ids: Vec::new(),
5429 prompt_tokens: input_ids.len(),
5430 tokens_generated: 0,
5431 finish_reason: "cancelled".to_string(),
5432 mtp_drafted: 0,
5433 mtp_accepted: 0,
5434 token_confidence: Vec::new(),
5435 traces: Vec::new(),
5436 });
5437 }
5438
5439 if !o1_sealed {
5442 match self.o1_seal_checked() {
5443 Ok(_) => {}
5444 Err(err) => {
5445 self.finish_generation(&mut mtp, &mut router, true);
5446 return Err(err);
5447 }
5448 }
5449 }
5450
5451 macro_rules! commit {
5453 ($id:expr) => {{
5454 all_ids.push($id);
5455 generated += 1;
5456 self.note_draft_id($id);
5457 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5458 finish_reason = "stop".to_string();
5459 false
5460 } else {
5461 let token_text = self.tokenizer.decode_token($id);
5462 let mut go = true;
5463 if let Some(ref mut cb) = on_token {
5464 if !cb(&token_text) {
5465 finish_reason = "cancelled".to_string();
5466 go = false;
5467 }
5468 }
5469 go
5470 }
5471 }};
5472 }
5473
5474 let mut spec_trial = SpecTrial::Spec {
5485 t0: std::time::Instant::now(),
5486 gen0: generated,
5487 rounds: 0,
5488 };
5489 let mut spec_mon = SpecMon {
5495 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5496 ..SpecMon::default()
5497 };
5498 let mut spec_watchdog_off = false;
5499 let mut spec_walls: Vec<f32> = Vec::new();
5502 let mut spec_round_end: Option<std::time::Instant> = None;
5505 if mimo_spec {
5506 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5507 if let Some(mut st) = self.mimo_mtp.take() {
5508 self.mimo_mtp_probe(&mut st, input_ids, &path);
5509 self.mimo_mtp = Some(st);
5510 }
5511 }
5512 }
5513 let mut next_pos = input_ids.len();
5515 'decode: while generated < max_tokens {
5516 if self
5517 .graph_failed
5518 .swap(false, std::sync::atomic::Ordering::Relaxed)
5519 {
5520 self.finish_generation(&mut mtp, &mut router, true);
5525 return Err("GPU token graph failed during decode".to_string());
5526 }
5527 if self
5528 .cancel
5529 .swap(false, std::sync::atomic::Ordering::Relaxed)
5530 {
5531 finish_reason = "cancelled".to_string();
5532 break 'decode;
5533 }
5534 if mimo_spec && next_pos > 0 {
5539 self.mimo_note_rows(&hidden, next_pos - 1);
5542 }
5543 let forced = self.spec_forced.take();
5544 let mut logits = match (forced, self.graph_logits.take()) {
5545 (Some(_), _) => Vec::new(),
5546 (None, Some(lg)) => lg,
5547 (None, None) => {
5548 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5549 inference::rms_norm_into(
5550 &hidden,
5551 &self.weights.final_norm,
5552 self.rms_eps,
5553 self.norm_style,
5554 &mut self.ws.n1,
5555 );
5556 self.lm_head_forward(&self.ws.n1)
5557 }
5558 };
5559 if generated
5562 == std::env::var("CMF_LOGIT_DUMP_STEP")
5563 .ok()
5564 .and_then(|v| v.parse().ok())
5565 .unwrap_or(0)
5566 {
5567 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5568 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5569 for v in hidden.iter().chain(logits.iter()) {
5570 bytes.extend_from_slice(&v.to_le_bytes());
5571 }
5572 if let Err(e) = std::fs::write(&path, &bytes) {
5573 eprintln!("logit dump: failed to write {path}: {e}");
5574 self.finish_generation(&mut mtp, &mut router, true);
5575 return Err(format!("logit dump write failed: {e}"));
5576 }
5577 }
5578 }
5579 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5583 if !logits.is_empty() {
5584 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5585 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5586 if let Err(e) =
5587 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5588 {
5589 eprintln!("logit dump: failed to write {}: {e}", path.display());
5590 }
5591 }
5592 }
5593 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5594 Some(c) => c,
5595 None => {
5596 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5597 sampler::sample_with_scratch_pool(
5598 &logits,
5599 &self.sampler_config,
5600 self.sampler_config.penalty_past(&all_ids, bounded_native),
5601 &mut self.rng,
5602 &mut self.sampler_scratch,
5603 self.pool.as_deref(),
5604 )
5605 }
5606 };
5607 if self.confidence_on {
5608 confidence.push(if logits.is_empty() {
5609 0.0
5610 } else {
5611 sampler::top1_prob_pool(
5612 self.pool.as_deref(),
5613 &mut self.sampler_scratch,
5614 &logits,
5615 t_next,
5616 calib_temp,
5617 )
5618 });
5619 }
5620 if !logits.is_empty() {
5621 attention::recycle_buf(&mut logits);
5622 }
5623 if trace_on {
5624 let skill = router.as_ref().and_then(|r| r.active_id());
5628 traces.push(TokenTrace {
5629 t: generated,
5630 token_id: t_next,
5631 confidence: confidence.last().copied().unwrap_or(0.0),
5632 active_skill: skill,
5633 recon: None,
5634 switched: false,
5635 });
5636 }
5637 if !commit!(t_next) {
5638 break 'decode;
5639 }
5640 if generated >= max_tokens {
5641 break 'decode;
5642 }
5643
5644 self.swa_trim_tails();
5647 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5648 static SAID: std::sync::Once = std::sync::Once::new();
5654 SAID.call_once(|| {
5655 tracing::warn!(
5656 "KV cache full at {} positions — evicting half; quality \
5657 will degrade. Raise CMF_MAX_SEQ.",
5658 self.kv_cache.max_seq_len,
5659 );
5660 });
5661 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5662 self.kv_cache.evict(keep);
5663 }
5664
5665 if graph_spec {
5668 match spec_trial {
5669 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5670 spec_mon.plain_ms =
5671 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5672 let keep = spec_mon.pays();
5673 tracing::info!(
5674 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5675 spec_mon.tokens,
5676 spec_mon.round_ms,
5677 spec_mon.plain_ms,
5678 if keep { "speculating" } else { "plain" }
5679 );
5680 spec_mon.fails = 0;
5681 spec_trial = SpecTrial::Decided {
5682 spec: keep,
5683 recheck_at: if keep { usize::MAX } else { generated + 128 },
5684 };
5685 }
5686 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5687 spec_mon.n = 0;
5688 spec_trial = SpecTrial::Spec {
5689 t0: std::time::Instant::now(),
5690 gen0: generated,
5691 rounds: 0,
5692 };
5693 }
5694 _ => {}
5695 }
5696 spec_watchdog_off = matches!(
5697 spec_trial,
5698 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5699 );
5700 }
5701 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5703 let budget = max_tokens - generated - 1;
5704 if let Some(mut st) = self.mimo_mtp.take() {
5705 let k = st.depth.min(budget);
5706 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5707 self.mimo_mtp = Some(st);
5708 let r = match r {
5709 Ok(r) => r,
5710 Err(err) => {
5711 self.finish_generation(&mut mtp, &mut router, true);
5712 return Err(err);
5713 }
5714 };
5715 if let Some(r) = r {
5716 drafted += r.drafted;
5717 accepted += r.accepted.len();
5718 let mut stopped = false;
5719 for &id in &r.accepted {
5720 if self.confidence_on {
5721 confidence.push(0.0);
5722 }
5723 if !commit!(id) {
5724 stopped = true;
5725 break;
5726 }
5727 }
5728 if stopped {
5729 break 'decode;
5730 }
5731 next_pos += r.accepted.len() + 1;
5732 hidden = r.hidden;
5733 self.graph_logits = Some(r.logits);
5736 continue 'decode;
5737 }
5738 }
5739 }
5740 #[cfg(feature = "gpu")]
5743 if self.speculative
5744 && self.qwen4_exp.is_some()
5745 && task_mask.is_none()
5746 && self.sampler_config.temperature < 1e-6
5747 && generated + 1 < max_tokens
5748 && next_pos > 0
5749 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5750 {
5751 let r = match &mut self.qwen4_exp {
5752 Some(b) => crate::qwen4_exp::spec_round(
5753 &b.0,
5754 &b.1,
5755 &b.2,
5756 &mut b.3,
5757 next_pos,
5758 &all_ids,
5759 &self.inv_freq,
5760 self.pool.as_deref(),
5761 ),
5762 None => None,
5763 };
5764 if let Some(r) = r {
5765 drafted += r.drafted;
5766 accepted += r.accepted.len();
5767 let mut stopped = false;
5768 for &id in &r.accepted {
5769 if self.confidence_on {
5770 confidence.push(0.0);
5771 }
5772 if !commit!(id) {
5773 stopped = true;
5774 break;
5775 }
5776 }
5777 if stopped {
5778 break 'decode;
5779 }
5780 next_pos += r.accepted.len() + 1;
5781 hidden.fill(0.0);
5782 self.graph_logits = Some(r.logits);
5783 continue 'decode;
5784 }
5785 }
5786 match &mut mtp {
5787 #[cfg(feature = "gpu")]
5789 Some(m)
5790 if graph_spec
5791 && !spec_watchdog_off
5792 && generated + 1 < max_tokens
5793 && next_pos > 0 =>
5794 {
5795 let t_round = std::time::Instant::now();
5796 if spec_time_level() >= 2 {
5797 if let Some(t) = spec_round_end.take() {
5798 eprintln!(
5799 "spec-gap {:.2} ms (host between rounds)",
5800 t.elapsed().as_secs_f64() * 1e3
5801 );
5802 }
5803 }
5804 spec_stamps_begin();
5805 #[cfg(target_os = "macos")]
5810 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
5811 .load(std::sync::atomic::Ordering::Relaxed);
5812 #[cfg(not(target_os = "macos"))]
5813 let allocs0 = 0u64;
5814 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5815 m,
5816 &hidden,
5817 t_next,
5818 next_pos,
5819 &mut drafted,
5820 &mut accepted,
5821 &mut all_ids,
5822 max_tokens - generated,
5823 ) {
5824 next_pos = n_pos;
5825 hidden = new_h;
5826 let level = spec_time_level();
5827 if level > 0 {
5828 let wall = t_round.elapsed().as_secs_f32() * 1e3;
5829 let stamps = spec_stamps_take();
5830 let median = if spec_walls.len() >= 3 {
5833 let mut s = spec_walls.clone();
5834 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5835 Some(s[s.len() / 2])
5836 } else {
5837 None
5838 };
5839 let outlier = median.is_some_and(|m| wall > 1.4 * m);
5840 #[cfg(target_os = "macos")]
5841 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5842 .load(std::sync::atomic::Ordering::Relaxed)
5843 - allocs0;
5844 #[cfg(not(target_os = "macos"))]
5845 let allocs = allocs0;
5846 eprintln!(
5847 "spec-round wall {wall:.1} ms → {} tokens{}{}",
5848 extra.len() + 1,
5849 if allocs > 0 {
5850 format!(" [{allocs} new device buffers]")
5851 } else {
5852 String::new()
5853 },
5854 match (outlier, median) {
5855 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5856 _ => String::new(),
5857 }
5858 );
5859 if level >= 2 || outlier {
5860 let sum: f32 = stamps.iter().map(|s| s.1).sum();
5861 eprintln!(
5862 "spec-stamps: {}| untracked {:.1}",
5863 spec_stamps_format(&stamps),
5864 wall - sum
5865 );
5866 }
5867 if spec_mon.n >= 1 {
5868 spec_walls.push(wall);
5869 }
5870 }
5871 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5875 spec_trial = Self::spec_trial_round(
5878 spec_trial,
5879 &mut spec_mon,
5880 generated + extra.len() + 1,
5881 );
5882 let mut stopped = false;
5883 for &id in &extra {
5884 if self.confidence_on {
5885 confidence.push(0.0);
5886 }
5887 if !commit!(id) {
5888 stopped = true;
5889 break;
5890 }
5891 }
5892 if stopped {
5893 break 'decode;
5894 }
5895 if spec_time_level() >= 2 {
5896 spec_round_end = Some(std::time::Instant::now());
5897 }
5898 continue 'decode;
5899 }
5900 if self
5901 .graph_failed
5902 .swap(false, std::sync::atomic::Ordering::Relaxed)
5903 {
5904 self.finish_generation(&mut mtp, &mut router, true);
5910 return Err("GPU MTP graph failed during speculative decode".to_string());
5911 }
5912 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
5923 spec_mon.tokens = 0.0;
5924 spec_mon.fails = 3;
5925 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
5926 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
5927 next_pos += 1;
5928 continue 'decode;
5929 }
5930 Some(m) if !graph_spec && generated + 1 < max_tokens => {
5932 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
5933 drafted += 1;
5934 let emb1 = self.embed_single(t_next);
5935 let emb2 = self.embed_single(draft);
5936 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
5937
5938 inference::rms_norm_into(
5939 &h1,
5940 &self.weights.final_norm,
5941 self.rms_eps,
5942 self.norm_style,
5943 &mut self.ws.n1,
5944 );
5945 let mut logits1 = self.lm_head_forward(&self.ws.n1);
5946 let t_after = sampler::sample_with_scratch_pool(
5947 &logits1,
5948 &self.sampler_config,
5949 self.sampler_config.penalty_past(&all_ids, bounded_native),
5950 &mut self.rng,
5951 &mut self.sampler_scratch,
5952 self.pool.as_deref(),
5953 );
5954 if self.confidence_on {
5955 confidence.push(sampler::top1_prob_pool(
5956 self.pool.as_deref(),
5957 &mut self.sampler_scratch,
5958 &logits1,
5959 t_after,
5960 calib_temp,
5961 ));
5962 }
5963 attention::recycle_buf(&mut logits1);
5964 if trace_on {
5965 traces.push(TokenTrace {
5968 t: generated,
5969 token_id: t_after,
5970 confidence: confidence.last().copied().unwrap_or(0.0),
5971 active_skill: None,
5972 recon: None,
5973 switched: false,
5974 });
5975 }
5976 let stop = !commit!(t_after);
5977
5978 if t_after == draft {
5979 accepted += 1;
5980 self.commit_linear_scratch();
5981 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5982 hidden = h2;
5983 next_pos += 2;
5984 } else {
5985 for layer in &mut self.kv_cache.layers {
5987 layer.truncate_last(1);
5988 }
5989 if !stop {
5990 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5991 hidden = self.forward_layers(
5992 &self.embed_single(t_after),
5993 next_pos + 1,
5994 None,
5995 );
5996 }
5997 next_pos += 2;
5998 }
5999 if stop {
6000 break 'decode;
6001 }
6002 }
6003 _ => {
6005 #[cfg(feature = "gpu")]
6010 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6011 static SAID: std::sync::Once = std::sync::Once::new();
6012 SAID.call_once(|| {
6013 eprintln!(
6014 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6015 !self.dsv4_mtp.is_empty(),
6016 task_mask.is_none(),
6017 router.is_none(),
6018 !trace_on,
6019 self.sampler_config.temperature < 1e-6,
6020 self.sampler_config.repetition_penalty == 1.0,
6021 );
6022 });
6023 }
6024 #[cfg(feature = "gpu")]
6025 if Self::dsv4_spec_on()
6026 && self.dsv4.is_some()
6027 && !self.dsv4_mtp.is_empty()
6028 && task_mask.is_none()
6029 && router.is_none()
6030 && !trace_on
6031 && self.sampler_config.temperature < 1e-6
6032 && self.sampler_config.repetition_penalty == 1.0
6033 && generated + 1 < max_tokens
6034 && all_ids.len() >= 2
6035 && generated >= dsv4_spec_retry_at
6036 {
6037 let tip_token = all_ids[all_ids.len() - 2];
6038 let drafted0 = drafted;
6039 let round = self.dsv4_spec_step(
6040 tip_token,
6041 t_next,
6042 next_pos,
6043 max_tokens.saturating_sub(generated),
6044 &mut drafted,
6045 &mut accepted,
6046 );
6047 if drafted > drafted0 {
6048 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6049 if useful {
6050 dsv4_spec_bad = 0;
6051 } else {
6052 dsv4_spec_bad += 1;
6053 if dsv4_spec_bad >= 2 {
6054 dsv4_spec_bad = 0;
6055 dsv4_spec_retry_at = generated.saturating_add(32);
6056 tracing::info!(
6057 "dsv4: draft не окупился дважды — точный walk на 32 токена"
6058 );
6059 }
6060 }
6061 }
6062 if let Some((extra, n_pos)) = round {
6063 next_pos = n_pos;
6064 let mut stopped = false;
6065 for &id in &extra {
6066 if self.confidence_on {
6067 confidence.push(0.0);
6068 }
6069 if !commit!(id) {
6070 stopped = true;
6071 break;
6072 }
6073 }
6074 if stopped {
6075 break 'decode;
6076 }
6077 continue 'decode;
6078 }
6079 }
6080 self.graph_want_logits = fuse_lm;
6081 let mut t_fwd = t_next;
6087 let pure_greedy = self.sampler_config.temperature < 1e-6
6088 && self.sampler_config.repetition_penalty == 1.0
6089 && self.sampler_config.suppress_tokens.is_empty();
6090 let burst_k = std::env::var("CMF_MULTISTEP")
6095 .ok()
6096 .and_then(|v| v.parse::<usize>().ok())
6097 .unwrap_or(0);
6098 if pure_greedy
6099 && burst_k >= 1
6100 && fuse_lm
6101 && task_mask.is_none()
6102 && router.is_none()
6103 && !trace_on
6104 && !self.confidence_on
6105 {
6106 let mut stopped = false;
6107 loop {
6108 let room = max_tokens.saturating_sub(generated);
6109 if room <= 2 {
6110 break;
6111 }
6112 let k = burst_k.min(room - 1);
6113 if k < 1 {
6114 break;
6115 }
6116 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6117 if self
6118 .graph_failed
6119 .swap(false, std::sync::atomic::Ordering::Relaxed)
6120 {
6121 self.finish_generation(&mut mtp, &mut router, true);
6122 return Err(
6123 "GPU token graph failed during greedy burst".to_string()
6124 );
6125 }
6126 break;
6127 };
6128 next_pos += k;
6129 for &id in &ids {
6130 if !commit!(id) {
6131 stopped = true;
6132 break;
6133 }
6134 }
6135 if stopped {
6136 break;
6137 }
6138 t_fwd = *ids.last().unwrap();
6139 }
6140 if stopped {
6141 break 'decode;
6142 }
6143 }
6144 #[cfg(target_os = "macos")]
6154 if graph_spec
6155 && spec_watchdog_off
6156 && next_pos > 0
6157 && self.mtp_graph_mode == Some(true)
6158 && crate::gpu::q1_force()
6159 {
6160 if let Some(m) = mtp.as_mut() {
6161 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6162 }
6163 }
6164 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6165 next_pos += 1;
6166 if let Some(r) = &mut router {
6169 let phi = self.dyn_phi_ema.clone();
6170 let decision = r.step(&phi, generated);
6171 if let Some(new_active) = decision {
6172 let _ = self.set_active_skill(new_active);
6173 }
6174 if trace_on {
6177 if let Some(last) = traces.last_mut() {
6178 let e = r.last_best_e();
6179 last.recon = e.is_finite().then_some(e);
6180 last.switched = decision.is_some();
6181 }
6182 }
6183 }
6184 }
6185 }
6186 }
6187
6188 let cancelled = finish_reason == "cancelled";
6189 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6193 if mimo_spec {
6194 if let Some(st) = self.mimo_mtp.as_ref() {
6195 let line = st.stats.line();
6196 tracing::info!("{line}");
6197 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6198 eprintln!("{line}");
6199 }
6200 }
6201 }
6202 self.finish_generation(&mut mtp, &mut router, cancelled);
6203
6204 let output_ids = &all_ids[input_ids.len()..];
6205 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6209 let consumed = std::mem::take(&mut all_ids);
6214 if dyn_switched {
6215 self.clear_sequence_state();
6216 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6217 self.clear_history();
6218 } else {
6219 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6220 }
6221 all_ids = consumed;
6222 let output_ids = &all_ids[input_ids.len()..];
6223 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6225 Ok(GenerateResult {
6226 text: self.tokenizer.decode(output_ids),
6227 token_ids: output_ids.to_vec(),
6228 prompt_tokens: input_ids.len(),
6229 tokens_generated: generated,
6230 finish_reason,
6231 mtp_drafted: drafted,
6232 mtp_accepted: accepted,
6233 token_confidence: confidence,
6234 traces,
6235 })
6236 }
6237
6238 fn mtp_step(
6242 &mut self,
6243 m: &mut MtpModule,
6244 hidden: &[f32],
6245 next_token: u32,
6246 position: usize,
6247 ) -> u32 {
6248 self.mtp_step_h(m, hidden, next_token, position).0
6249 }
6250
6251 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6255 use std::sync::Mutex;
6256 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6257 let mut t = T.lock().unwrap();
6258 if t.len() <= depth {
6259 t.resize(depth + 1, (0, 0));
6260 }
6261 t[depth].0 += 1;
6262 t[depth].1 += prefix_ok as u64;
6263 if depth == 0 && t[0].0 % 128 == 0 {
6264 let line: Vec<String> = t
6265 .iter()
6266 .enumerate()
6267 .map(|(d, (n, k))| {
6268 format!(
6269 "d{}={:.0}%({n})",
6270 d + 1,
6271 100.0 * *k as f64 / (*n).max(1) as f64
6272 )
6273 })
6274 .collect();
6275 eprintln!("mtp-chain: {}", line.join(" "));
6276 }
6277 }
6278
6279 fn mtp_step_hl(
6287 &mut self,
6288 m: &mut MtpModule,
6289 hidden: &[f32],
6290 next_token: u32,
6291 position: usize,
6292 ) -> (Vec<f32>, Vec<f32>) {
6293 #[cfg(target_os = "macos")]
6298 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6299 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6300 self.mtp_graph_mode = Some(true);
6301 return r;
6302 }
6303 if self.mtp_graph_mode == Some(true) {
6304 tracing::error!("mtp Metal graph failed after admission");
6305 self.clear_sequence_state();
6306 self.graph_failed
6307 .store(true, std::sync::atomic::Ordering::Relaxed);
6308 self.cancel
6309 .store(true, std::sync::atomic::Ordering::Relaxed);
6310 return (Vec::new(), Vec::new());
6311 }
6312 self.mtp_graph_mode = Some(false);
6313 }
6314 #[cfg(feature = "gpu")]
6315 if self.mtp_graph_mode != Some(false) {
6316 if !self.mtp_graph_ok(m) {
6317 if self.mtp_graph_mode == Some(true) {
6318 tracing::error!("mtp graph became unavailable after admission");
6323 self.clear_sequence_state();
6324 self.graph_failed
6325 .store(true, std::sync::atomic::Ordering::Relaxed);
6326 self.cancel
6327 .store(true, std::sync::atomic::Ordering::Relaxed);
6328 return (Vec::new(), Vec::new());
6329 }
6330 self.mtp_graph_mode = Some(false);
6331 } else {
6332 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6333 self.mtp_graph_mode = Some(true);
6334 return r;
6335 }
6336 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6337 return (Vec::new(), Vec::new());
6344 }
6345 tracing::error!("mtp graph failed or declined after admission");
6349 self.clear_sequence_state();
6350 self.graph_failed
6351 .store(true, std::sync::atomic::Ordering::Relaxed);
6352 self.cancel
6353 .store(true, std::sync::atomic::Ordering::Relaxed);
6354 return (Vec::new(), Vec::new());
6355 }
6356 }
6357 let e = self.embed_single(next_token);
6361 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6362 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6363 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6364 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6365 let mut x = vec![0.0f32; self.hidden_size];
6366 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6367
6368 let lw = &m.layer;
6370 inference::rms_norm_into(
6371 &x,
6372 &lw.input_norm,
6373 self.rms_eps,
6374 self.norm_style,
6375 &mut self.ws.n1,
6376 );
6377 let attn = match &lw.attn {
6378 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6380 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6381 AttnKind::Full {
6382 wq,
6383 wk,
6384 wv,
6385 wo,
6386 q_norm,
6387 k_norm,
6388 output_gate,
6389 softplus_gate,
6390 bias,
6391 } => {
6392 let mut cfg = self.attn_cfg(position);
6393 cfg.q_norm = q_norm.as_deref();
6394 cfg.k_norm = k_norm.as_deref();
6395 cfg.output_gate = *output_gate;
6396 cfg.softplus_gate = softplus_gate
6397 .as_ref()
6398 .map(|(gate, per_head)| (gate, *per_head));
6399 cfg.bias = bias
6400 .as_ref()
6401 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6402 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6403 }
6404 AttnKind::Linear(_)
6405 | AttnKind::LinearGdn(_)
6406 | AttnKind::ShortConv(_)
6407 | AttnKind::Bounded(_) => {
6408 unreachable!("MTP block is full attention")
6409 }
6410 };
6411 for (i, &a) in attn.iter().enumerate() {
6412 x[i] += a;
6413 }
6414 inference::rms_norm_into(
6415 &x,
6416 &lw.post_norm,
6417 self.rms_eps,
6418 self.norm_style,
6419 &mut self.ws.p1,
6420 );
6421 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6422 for (i, &f) in ffn.iter().enumerate() {
6423 x[i] += f;
6424 }
6425
6426 inference::rms_norm_into(
6427 &x,
6428 &m.final_norm,
6429 self.rms_eps,
6430 self.norm_style,
6431 &mut self.ws.n1,
6432 );
6433 let lg = self.lm_head_forward(&self.ws.n1);
6434 (lg, x)
6435 }
6436
6437 fn mtp_step_h(
6439 &mut self,
6440 m: &mut MtpModule,
6441 hidden: &[f32],
6442 next_token: u32,
6443 position: usize,
6444 ) -> (u32, Vec<f32>) {
6445 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6446 let draft = sampler::argmax(&lg);
6447 attention::recycle_buf(&mut lg);
6448 (draft, x)
6449 }
6450
6451 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6457 match trial {
6458 SpecTrial::Spec { t0, gen0, rounds } => {
6459 let rounds = rounds + 1;
6460 if rounds >= 5 {
6461 if mon.plain_ms > 0.0 {
6462 let keep = mon.pays();
6463 mon.fails = 0;
6464 tracing::info!(
6465 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6466 mon.tokens,
6467 mon.round_ms,
6468 mon.plain_ms,
6469 if keep { "speculating" } else { "plain" }
6470 );
6471 SpecTrial::Decided {
6472 spec: keep,
6473 recheck_at: if keep { usize::MAX } else { generated + 128 },
6474 }
6475 } else if mon.pays() {
6476 mon.fails = 0;
6481 tracing::info!(
6482 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6483 mon.tokens,
6484 mon.round_ms,
6485 );
6486 SpecTrial::Decided {
6487 spec: true,
6488 recheck_at: usize::MAX,
6489 }
6490 } else {
6491 SpecTrial::Plain {
6492 t0: std::time::Instant::now(),
6493 gen0: generated,
6494 }
6495 }
6496 } else {
6497 SpecTrial::Spec { t0, gen0, rounds }
6498 }
6499 }
6500 SpecTrial::Decided { spec: true, .. } => {
6501 if mon.pays() {
6502 mon.fails = 0;
6503 trial
6504 } else {
6505 mon.fails += 1;
6506 if mon.fails >= 4 {
6507 if mon.plain_ms <= 0.0 {
6508 tracing::info!(
6512 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6513 mon.tokens,
6514 mon.round_ms,
6515 );
6516 return SpecTrial::Plain {
6517 t0: std::time::Instant::now(),
6518 gen0: generated,
6519 };
6520 }
6521 tracing::info!(
6522 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6523 mon.tokens,
6524 mon.round_ms,
6525 mon.plain_ms
6526 );
6527 SpecTrial::Decided {
6528 spec: false,
6529 recheck_at: generated + 128,
6530 }
6531 } else {
6532 trial
6533 }
6534 }
6535 }
6536 other => other,
6537 }
6538 }
6539
6540 fn mtp_kv_id(&self) -> u64 {
6543 self.graph_kv_id | (1u64 << 40)
6544 }
6545
6546 const MTP_LAYER_BASE: usize = 0;
6551
6552 #[cfg(feature = "gpu")]
6559 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6560 self.mtp_graph_mode != Some(true)
6561 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6562 }
6563
6564 #[cfg(feature = "gpu")]
6570 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6571 let mut ok = true;
6572 let mut expected = false;
6573 for li in 0..self.num_layers {
6574 if matches!(
6575 self.weights.layers[self.phys_layer(li)].attn,
6576 AttnKind::Full { .. }
6577 ) {
6578 expected = true;
6579 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6580 }
6581 }
6582 !expected || ok
6583 }
6584
6585 fn graph_gdn_layer_count(&self) -> usize {
6589 (0..self.num_layers)
6590 .filter(|&li| {
6591 matches!(
6592 &self.weights.layers[self.phys_layer(li)].attn,
6593 AttnKind::LinearGdn(_)
6594 )
6595 })
6596 .count()
6597 }
6598
6599 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6602 let e = self.embed_single(next_token);
6603 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6604 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6605 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6606 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6607 let mut x = vec![0.0f32; self.hidden_size];
6608 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6609 x
6610 }
6611
6612 #[cfg(feature = "gpu")]
6615 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6616 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6617 return false;
6618 }
6619 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6620 || !crate::gpu::enabled_here()
6621 || self.attn_softcap > 0.0
6622 || self.attention_heads_per_layer.is_some()
6623 || self.v_head_dim.is_some()
6626 {
6627 return false;
6628 }
6629 matches!(
6630 &m.layer.attn,
6631 AttnKind::Full {
6632 softplus_gate: None,
6633 ..
6634 }
6635 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6636 }
6637
6638 #[cfg(feature = "gpu")]
6642 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6643 if !self.mtp_block_graph_ok(m) {
6644 return false;
6645 }
6646 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6647 return false;
6648 };
6649 let FfnKind::Dense(d) = &m.layer.ffn else {
6650 return false;
6651 };
6652 d.segs.is_empty()
6653 && wq.graph_weight().is_some()
6654 && wk.graph_weight().is_some()
6655 && wv.graph_weight().is_some()
6656 && wo.graph_weight().is_some()
6657 && d.gate_proj.graph_weight().is_some()
6658 && d.up_proj.graph_weight().is_some()
6659 && d.down_proj.graph_weight().is_some()
6660 && self.weights.lm_head.graph_weight().is_some()
6661 }
6662
6663 #[cfg(feature = "gpu")]
6669 fn mtp_step_graph(
6670 &mut self,
6671 m: &mut MtpModule,
6672 hidden: &[f32],
6673 next_token: u32,
6674 position: usize,
6675 ) -> Option<(Vec<f32>, Vec<f32>)> {
6676 if !self.mtp_graph_ok(m) {
6677 return None;
6678 }
6679 let lw = &m.layer;
6680 let AttnKind::Full {
6681 wq,
6682 wk,
6683 wv,
6684 wo,
6685 q_norm,
6686 k_norm,
6687 output_gate,
6688 softplus_gate,
6689 bias,
6690 } = &lw.attn
6691 else {
6692 return None;
6693 };
6694 if softplus_gate.is_some() {
6695 return None;
6696 }
6697 let FfnKind::Dense(d) = &lw.ffn else {
6698 return None;
6699 };
6700 if !d.segs.is_empty() {
6701 return None; }
6703 let mut x = self.mtp_block_input(m, hidden, next_token);
6706 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6707 let (_, i, kind, rs) = t.graph_weight()?;
6708 Some(crate::gpu::GraphW {
6709 idx: i,
6710 kind,
6711 row_scale: rs,
6712 data: &[],
6713 prism: crate::gpu::GraphPrismOp::None,
6714 affine: false,
6715 })
6716 }
6717 let (model, _, _, _) = wq.graph_weight()?;
6718 let model = model.clone();
6719 let (lm_gw, lm_rows) = {
6720 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6721 let rows = if kind == 6 {
6725 self.draft_head_rows(self.weights.lm_head.rows())
6726 } else {
6727 self.weights.lm_head.rows()
6728 };
6729 (
6730 crate::gpu::GraphW {
6731 idx: i,
6732 kind,
6733 row_scale: rs,
6734 data: &[],
6735 prism: crate::gpu::GraphPrismOp::None,
6736 affine: false,
6737 },
6738 rows,
6739 )
6740 };
6741 let layer = crate::gpu::GraphLayer {
6742 input_norm: &lw.input_norm,
6743 attn: crate::gpu::GraphAttn::Full {
6744 wq: gw(wq)?,
6745 wk: gw(wk)?,
6746 wv: gw(wv)?,
6747 wo: gw(wo)?,
6748 q_norm: q_norm.as_deref(),
6749 k_norm: k_norm.as_deref(),
6750 late_qk_norm: self.qk_norm_after_rope,
6751 bias: bias
6752 .as_ref()
6753 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6754 output_gate: *output_gate,
6755 cpu_k: m.kv.k_heads(),
6756 cpu_v: m.kv.v_heads(),
6757 cpu_base: m.kv.base(),
6758 geom: None,
6759 head_gate: None,
6760 },
6761 post_norm: &lw.post_norm,
6762 attn_out_norm: None,
6763 ffn_out_norm: None,
6764 layer_scale: None,
6765 ffn: crate::gpu::GraphFfn::Dense {
6766 gate: gw(&d.gate_proj)?,
6767 up: gw(&d.up_proj)?,
6768 down: gw(&d.down_proj)?,
6769 act: d.act.graph_act()?,
6770 },
6771 };
6772 let nh = self.num_heads;
6773 let (nkv, hd, rd) = self.layer_geom(0);
6774 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6775 let mut logits = Vec::new();
6776 let ok = crate::gpu::forward_token_graph(
6777 &model,
6778 self.mtp_kv_id(),
6779 std::slice::from_ref(&layer),
6780 &[None],
6781 self.o1_epoch,
6782 &self.inv_freq,
6783 &mut x,
6784 nh,
6785 nkv,
6786 hd,
6787 self.attn_scale,
6788 rd,
6789 self.hidden_size,
6790 self.intermediate_size,
6791 position,
6792 self.kv_cache.max_seq_len,
6793 gemma,
6794 self.rms_eps as f32,
6795 Some((&lm_gw, lm_rows)),
6796 &m.final_norm,
6797 &mut logits,
6798 &[],
6799 1,
6800 None,
6801 None,
6802 None,
6803 Self::MTP_LAYER_BASE,
6804 true,
6805 );
6806 match ok {
6807 crate::gpu::TokenGraphOutcome::Completed => {}
6808 crate::gpu::TokenGraphOutcome::Declined => return None,
6809 crate::gpu::TokenGraphOutcome::Failed => {
6810 self.clear_sequence_state();
6814 self.graph_failed
6815 .store(true, std::sync::atomic::Ordering::Relaxed);
6816 self.cancel
6817 .store(true, std::sync::atomic::Ordering::Relaxed);
6818 return None;
6819 }
6820 }
6821 logits.resize(self.vocab_size, 0.0);
6822 Some((logits, x))
6823 }
6824
6825 #[cfg(feature = "gpu")]
6833 fn mtp_warm_graph(
6834 &mut self,
6835 m: &mut MtpModule,
6836 pairs: &[(&[f32], u32)],
6837 first_pos: usize,
6838 ) -> crate::gpu::BatchGraphOutcome {
6839 if pairs.is_empty() {
6840 return crate::gpu::BatchGraphOutcome::Completed;
6841 }
6842 if !self.mtp_block_graph_ok(m) {
6843 return crate::gpu::BatchGraphOutcome::Declined;
6844 }
6845 let hs = self.hidden_size;
6846 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6849 for (h, t) in pairs {
6850 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6851 }
6852 let lw = &m.layer;
6853 let AttnKind::Full {
6854 wq,
6855 wk,
6856 wv,
6857 wo,
6858 q_norm,
6859 k_norm,
6860 output_gate,
6861 bias,
6862 ..
6863 } = &lw.attn
6864 else {
6865 return crate::gpu::BatchGraphOutcome::Declined;
6866 };
6867 let FfnKind::Dense(d) = &lw.ffn else {
6868 return crate::gpu::BatchGraphOutcome::Declined;
6869 };
6870 if !d.segs.is_empty() {
6871 return crate::gpu::BatchGraphOutcome::Declined; }
6873 let (AttnKind::Full { softplus_gate: None, .. }, Some(gact)) =
6876 (&lw.attn, d.act.graph_act())
6877 else {
6878 return crate::gpu::BatchGraphOutcome::Declined;
6879 };
6880 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6881 let (_, i, kind, rs) = t.graph_weight()?;
6882 Some(crate::gpu::GraphW {
6883 idx: i,
6884 kind,
6885 row_scale: rs,
6886 data: &[],
6887 prism: crate::gpu::GraphPrismOp::None,
6888 affine: false,
6889 })
6890 }
6891 let Some((model, _, _, _)) = wq.graph_weight() else {
6892 return crate::gpu::BatchGraphOutcome::Declined;
6893 };
6894 let model = model.clone();
6895 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6896 gw(wq),
6897 gw(wk),
6898 gw(wv),
6899 gw(wo),
6900 gw(&d.gate_proj),
6901 gw(&d.up_proj),
6902 gw(&d.down_proj),
6903 ) else {
6904 return crate::gpu::BatchGraphOutcome::Declined;
6905 };
6906 let layer = crate::gpu::GraphLayer {
6907 input_norm: &lw.input_norm,
6908 attn: crate::gpu::GraphAttn::Full {
6909 wq: gwq,
6910 wk: gwk,
6911 wv: gwv,
6912 wo: gwo,
6913 q_norm: q_norm.as_deref(),
6914 k_norm: k_norm.as_deref(),
6915 late_qk_norm: self.qk_norm_after_rope,
6916 bias: bias
6917 .as_ref()
6918 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6919 output_gate: *output_gate,
6920 cpu_k: m.kv.k_heads(),
6921 cpu_v: m.kv.v_heads(),
6922 cpu_base: m.kv.base(),
6923 geom: None,
6924 head_gate: None,
6925 },
6926 post_norm: &lw.post_norm,
6927 attn_out_norm: None,
6928 ffn_out_norm: None,
6929 layer_scale: None,
6930 ffn: crate::gpu::GraphFfn::Dense {
6931 gate: gg,
6932 up: gu,
6933 down: gd,
6934 act: gact,
6935 },
6936 };
6937 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
6938 let nh = self.num_heads;
6939 let (nkv, hd, rd) = self.layer_geom(0);
6940 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6941 crate::gpu::forward_batch_graph(
6942 &model,
6943 self.mtp_kv_id(),
6944 std::slice::from_ref(&layer),
6945 &self.inv_freq,
6946 &mut hiddens,
6947 nh,
6948 nkv,
6949 hd,
6950 rd,
6951 hs,
6952 self.intermediate_size,
6953 &positions,
6954 self.kv_cache.max_seq_len,
6955 gemma,
6956 self.rms_eps as f32,
6957 self.attn_scale,
6958 pairs.len(),
6959 &[],
6960 0,
6961 None,
6962 None,
6963 )
6964 }
6965
6966 #[cfg(feature = "gpu")]
6973 fn mtp_warm_graph_fallback(
6974 &mut self,
6975 m: &mut MtpModule,
6976 pairs: &[(&[f32], u32)],
6977 first_pos: usize,
6978 ) -> bool {
6979 if pairs.is_empty() {
6980 return true;
6981 }
6982 let graphable = self.mtp_block_graph_ok(m);
6983 if !graphable {
6984 if self.mtp_graph_mode == Some(true) {
6988 return false;
6989 }
6990 self.mtp_graph_mode = Some(false);
6991 for (j, (h, t)) in pairs.iter().enumerate() {
6992 self.mtp_warm(m, h, *t, first_pos + j);
6993 }
6994 return true;
6995 }
6996
6997 for (j, (h, t)) in pairs.iter().enumerate() {
7002 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7003 return false;
7004 }
7005 }
7006 self.mtp_graph_mode = Some(true);
7007 true
7008 }
7009
7010 #[cfg(feature = "gpu")]
7015 fn mtp_warm_prefill_pairs(
7016 &mut self,
7017 m: &mut MtpModule,
7018 pairs: &[(&[f32], u32)],
7019 first_pos: usize,
7020 ) -> Result<(), &'static str> {
7021 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7026 if self.mtp_graph_mode == Some(true) {
7027 return Err("MTP token graph became unavailable after admission");
7028 }
7029 self.mtp_graph_mode = Some(false);
7030 for (j, (h, t)) in pairs.iter().enumerate() {
7031 self.mtp_warm(m, h, *t, first_pos + j);
7032 }
7033 return Ok(());
7034 }
7035 match self.mtp_warm_graph(m, pairs, first_pos) {
7036 crate::gpu::BatchGraphOutcome::Completed => {
7037 if !pairs.is_empty() {
7038 self.mtp_graph_mode = Some(true);
7039 }
7040 Ok(())
7041 }
7042 crate::gpu::BatchGraphOutcome::Declined => {
7043 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7044 Ok(())
7045 } else {
7046 Err("MTP warm-up fallback failed after device admission")
7047 }
7048 }
7049 crate::gpu::BatchGraphOutcome::Failed => {
7050 Err("MTP warm batch graph failed after admission")
7051 }
7052 }
7053 }
7054
7055 #[cfg(not(feature = "gpu"))]
7056 fn mtp_warm_prefill_pairs(
7057 &mut self,
7058 m: &mut MtpModule,
7059 pairs: &[(&[f32], u32)],
7060 first_pos: usize,
7061 ) -> Result<(), &'static str> {
7062 for (j, (h, t)) in pairs.iter().enumerate() {
7063 self.mtp_warm(m, h, *t, first_pos + j);
7064 }
7065 Ok(())
7066 }
7067
7068 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7072 let e = self.embed_single(next_token);
7073 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7074 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7075 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7076 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7077 let mut x = vec![0.0f32; self.hidden_size];
7078 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7079 inference::rms_norm_into(
7080 &x,
7081 &m.layer.input_norm,
7082 self.rms_eps,
7083 self.norm_style,
7084 &mut self.ws.n1,
7085 );
7086 let attn = match &m.layer.attn {
7087 AttnKind::Full {
7088 wq,
7089 wk,
7090 wv,
7091 wo,
7092 q_norm,
7093 k_norm,
7094 output_gate,
7095 softplus_gate,
7096 bias,
7097 } => {
7098 let mut cfg = self.attn_cfg(position);
7099 cfg.q_norm = q_norm.as_deref();
7100 cfg.k_norm = k_norm.as_deref();
7101 cfg.output_gate = *output_gate;
7102 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7103 cfg.bias = bias
7104 .as_ref()
7105 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7106 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7107 }
7108 _ => return,
7109 };
7110 let _ = attn;
7111 }
7112
7113 #[cfg(feature = "gpu")]
7120 #[allow(clippy::too_many_arguments)]
7121 fn graph_spec_step(
7122 &mut self,
7123 m: &mut MtpModule,
7124 hidden: &[f32],
7125 t_next: u32,
7126 next_pos: usize,
7127 drafted: &mut usize,
7128 accepted: &mut usize,
7129 all_ids: &mut Vec<u32>,
7133 room: usize,
7138 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7139 #[cfg(target_os = "macos")]
7150 let metal_native = crate::gpu::q1_force();
7151 #[cfg(not(target_os = "macos"))]
7152 let metal_native = false;
7153 #[cfg(feature = "gpu")]
7154 let k_default = if metal_native {
7155 7
7158 } else if crate::gpu_wgpu::verify_i8_on() {
7159 5
7160 } else {
7161 4
7162 };
7163 #[cfg(not(feature = "gpu"))]
7164 let k_default = 4;
7165 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7166 .ok()
7167 .and_then(|v| v.parse().ok())
7168 .filter(|&v| (1..=8).contains(&v));
7169 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
7174 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7175 let k_spec = k_full.min(room).max(1);
7176 let k_capped = k_spec < k_full;
7179 if next_pos == 0 {
7180 return None;
7181 }
7182 let t_round = std::time::Instant::now();
7183 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7199 let sub0 = subs();
7200 let cfg = self.sampler_config.clone();
7205 let penalized = !(cfg.repetition_penalty == 1.0
7206 && cfg.presence_penalty == 0.0
7207 && cfg.suppress_tokens.is_empty());
7208 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7213 let sampling = cfg.temperature >= 1e-6;
7214 let sparse = sampling && sampler::sparse_ok(&cfg);
7220 let base_len = all_ids.len();
7221 if sampling && !sparse && self.spec_q.len() < k_spec {
7222 self.spec_q.resize_with(k_spec, Vec::new);
7223 }
7224 if sparse && self.spec_qs.len() < k_spec {
7225 self.spec_qs.resize_with(k_spec, Vec::new);
7226 }
7227 let mut drafts = Vec::with_capacity(k_spec);
7232 let mut hx = hidden.to_vec();
7233 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7236 spec_stamp("pro");
7237 #[cfg(target_os = "macos")]
7243 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7244 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7245 Ok(ids) => {
7246 self.mtp_graph_mode = Some(true);
7247 drafts = ids;
7248 }
7249 Err(true) => {
7250 tracing::error!("mtp Metal draft chain failed after commit");
7251 self.clear_sequence_state();
7252 self.graph_failed
7253 .store(true, std::sync::atomic::Ordering::Relaxed);
7254 self.cancel
7255 .store(true, std::sync::atomic::Ordering::Relaxed);
7256 return None;
7257 }
7258 Err(false) => {}
7259 }
7260 }
7261 for j in drafts.len()..k_spec {
7262 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7263 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7264 if spec_dbg {
7265 let saved = self.mtp_graph_mode;
7266 self.mtp_graph_mode = Some(false);
7267 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7268 self.mtp_graph_mode = saved;
7269 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7270 return None;
7271 }
7272 m.kv.truncate_last(1);
7273 dbg_ref = Some(r);
7274 }
7275 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7276 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7277 return None;
7278 }
7279 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7280 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7281 let dl = lg
7282 .iter()
7283 .zip(&lg_cpu)
7284 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7285 let dh = hj
7286 .iter()
7287 .zip(&h_cpu)
7288 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7289 eprintln!(
7290 "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 {}",
7291 next_pos - 1 + j,
7292 sampler::argmax(&lg_cpu),
7293 sampler::argmax(&lg),
7294 n(&h_cpu),
7295 n(&hj),
7296 m.kv.seq_len
7297 );
7298 }
7299 let dj = if sparse {
7300 let mut q = std::mem::take(&mut self.spec_qs[j]);
7301 let ok = sampler::sparse_distribution_into(
7302 &lg,
7303 &cfg,
7304 all_ids,
7305 &mut self.sampler_scratch,
7306 self.pool.as_deref(),
7307 &mut q,
7308 );
7309 let d = if ok {
7310 sampler::draw_sparse(&q, &mut self.rng)
7311 } else {
7312 let t = sampler::argmax(&lg);
7314 q.clear();
7315 q.push((t, 1.0));
7316 t
7317 };
7318 self.spec_qs[j] = q;
7319 all_ids.push(d);
7320 d
7321 } else if sampling {
7322 let mut q = std::mem::take(&mut self.spec_q[j]);
7323 sampler::distribution_into(
7324 &lg,
7325 &cfg,
7326 all_ids,
7327 &mut self.sampler_scratch,
7328 self.pool.as_deref(),
7329 &mut q,
7330 );
7331 let d = sampler::draw(&q, &mut self.rng);
7332 self.spec_q[j] = q;
7333 all_ids.push(d); d
7335 } else if greedy_pen {
7336 let d = sampler::argmax_penalized(
7337 &lg,
7338 &cfg,
7339 all_ids,
7340 &mut self.sampler_scratch,
7341 self.pool.as_deref(),
7342 );
7343 all_ids.push(d);
7344 d
7345 } else {
7346 sampler::argmax(&lg)
7347 };
7348 attention::recycle_buf(&mut lg);
7349 drafts.push(dj);
7350 hx = hj;
7351 spec_stamp("d.pick");
7352 }
7353 all_ids.truncate(base_len);
7354 *drafted += k_spec;
7355 let t_draft = t_round.elapsed();
7356 let sub_draft = subs();
7357 let b = k_spec + 1;
7360 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7361 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7362 let e = self.embed_single(t);
7363 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7364 }
7365 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7366 spec_stamp("v.emb");
7367 let (lm_gw, lm_rows) = {
7368 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7369 (
7370 crate::gpu::GraphW {
7371 idx: i,
7372 kind,
7373 row_scale: rs,
7374 data: &[],
7375 prism: crate::gpu::GraphPrismOp::None,
7376 affine: false,
7377 },
7378 self.weights.lm_head.rows(),
7379 )
7380 };
7381 let mut logits = Vec::new();
7382 let final_norm = self.weights.final_norm.clone();
7383 #[cfg(target_os = "macos")]
7392 let greedy_dev = metal_native
7393 && !sampling
7394 && !greedy_pen
7395 && !self.confidence_on
7396 && self.final_softcap.is_none()
7397 && self.vocab_size == lm_rows
7403 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7404 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7405 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7406 #[cfg(not(target_os = "macos"))]
7407 let greedy_dev = false;
7408 let mut dev_ids: Vec<u32> = Vec::new();
7409 #[cfg(target_os = "macos")]
7410 let verify_outcome = if metal_native {
7411 let lm = self.weights.lm_head.q1_parts()?;
7412 let n_score = self.vocab_size.min(lm_rows);
7413 self.try_batch_graph_metal(
7414 &mut hiddens,
7415 &positions,
7416 b,
7417 Some((lm, &final_norm, &mut logits)),
7418 if greedy_dev {
7419 Some((n_score, &mut dev_ids))
7420 } else {
7421 None
7422 },
7423 )
7424 } else {
7425 self.try_batch_graph_wgpu(
7426 &mut hiddens,
7427 &positions,
7428 b,
7429 Some(crate::gpu::SpecTail {
7430 lm: lm_gw,
7431 lm_rows,
7432 final_norm: &final_norm,
7433 logits_out: &mut logits,
7434 }),
7435 )
7436 };
7437 #[cfg(not(target_os = "macos"))]
7438 let verify_outcome = self.try_batch_graph_wgpu(
7439 &mut hiddens,
7440 &positions,
7441 b,
7442 Some(crate::gpu::SpecTail {
7443 lm: lm_gw,
7444 lm_rows,
7445 final_norm: &final_norm,
7446 logits_out: &mut logits,
7447 }),
7448 );
7449 match verify_outcome {
7450 crate::gpu::BatchGraphOutcome::Completed => {}
7451 crate::gpu::BatchGraphOutcome::Declined => {
7452 m.kv.truncate_last(k_spec);
7456 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7457 self.clear_sequence_state();
7458 self.graph_failed
7459 .store(true, std::sync::atomic::Ordering::Relaxed);
7460 self.cancel
7461 .store(true, std::sync::atomic::Ordering::Relaxed);
7462 tracing::error!("MTP graph mirror rewind failed after verify decline");
7463 }
7464 return None;
7465 }
7466 crate::gpu::BatchGraphOutcome::Failed => {
7467 self.clear_sequence_state();
7471 self.graph_failed
7472 .store(true, std::sync::atomic::Ordering::Relaxed);
7473 self.cancel
7474 .store(true, std::sync::atomic::Ordering::Relaxed);
7475 tracing::error!("MTP verify batch graph failed after admission");
7476 return None;
7477 }
7478 }
7479 #[cfg(target_os = "macos")]
7485 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7486 let snap: Vec<Vec<f32>> = self
7487 .kv_cache
7488 .layers
7489 .iter()
7490 .map(|l| l.linear_state.clone())
7491 .collect();
7492 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7493 let toks: Vec<u32> = std::iter::once(t_next)
7494 .chain(drafts.iter().copied())
7495 .collect();
7496 let want_save = self.graph_want_logits;
7497 self.graph_want_logits = false;
7498 for (i, &t) in toks.iter().enumerate() {
7499 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7500 let _ = self.graph_logits.take();
7501 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7505 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7506 }
7507 let ref_lg = self.logits_from_hidden(&hi);
7508 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7509 let ra = sampler::argmax(&ref_lg);
7510 let va = sampler::argmax(row);
7511 let mut md = 0f32;
7512 let mut rms = 0f64;
7513 for j in 0..lm_rows.min(ref_lg.len()) {
7514 let d = (ref_lg[j] - row[j]).abs();
7515 md = md.max(d);
7516 rms += (d as f64) * (d as f64);
7517 }
7518 let mut hd = 0f32;
7519 for j in 0..self.hidden_size {
7520 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7521 }
7522 eprintln!(
7523 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7524 next_pos + i,
7525 if ra == va { "OK" } else { "MISMATCH" },
7526 (rms / lm_rows as f64).sqrt()
7527 );
7528 }
7529 self.graph_want_logits = want_save;
7530 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7533 if l.linear_state.len() == st.len() {
7534 l.linear_state.copy_from_slice(&st);
7535 } else {
7536 l.linear_state = st;
7537 }
7538 }
7539 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7540 let extra = l.seq_len.saturating_sub(n0);
7541 if extra > 0 {
7542 l.truncate_last(extra);
7543 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7544 }
7545 }
7546 }
7547 let t_verify = t_round.elapsed();
7548 let sub_verify = subs();
7549 let mut a = 0usize;
7554 let mut forced: Option<u32> = None;
7555 let ids: Vec<u32> = if sparse {
7556 let mut p = std::mem::take(&mut self.spec_ps);
7557 let mut res = std::mem::take(&mut self.spec_ress);
7558 while a < k_spec {
7559 let ok = sampler::sparse_distribution_into(
7560 &logits[a * lm_rows..(a + 1) * lm_rows],
7561 &cfg,
7562 all_ids,
7563 &mut self.sampler_scratch,
7564 self.pool.as_deref(),
7565 &mut p,
7566 );
7567 if !ok {
7568 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7569 p.clear();
7570 p.push((t, 1.0));
7571 }
7572 match sampler::spec_accept_or_correct_sparse(
7573 &p,
7574 &self.spec_qs[a],
7575 drafts[a],
7576 &mut self.rng,
7577 &mut res,
7578 ) {
7579 None => {
7580 all_ids.push(drafts[a]);
7581 a += 1;
7582 }
7583 Some(c) => {
7584 forced = Some(c);
7585 break;
7586 }
7587 }
7588 }
7589 all_ids.truncate(base_len);
7590 self.spec_ps = p;
7591 self.spec_ress = res;
7592 drafts.clone()
7593 } else if sampling {
7594 let mut p = std::mem::take(&mut self.spec_p);
7595 let mut res = std::mem::take(&mut self.spec_res);
7596 while a < k_spec {
7597 sampler::distribution_into(
7598 &logits[a * lm_rows..(a + 1) * lm_rows],
7599 &cfg,
7600 all_ids,
7601 &mut self.sampler_scratch,
7602 self.pool.as_deref(),
7603 &mut p,
7604 );
7605 match sampler::spec_accept_or_correct(
7606 &p,
7607 &self.spec_q[a],
7608 drafts[a],
7609 &mut self.rng,
7610 &mut res,
7611 self.pool.as_deref(),
7612 ) {
7613 None => {
7614 all_ids.push(drafts[a]);
7615 a += 1;
7616 }
7617 Some(c) => {
7618 forced = Some(c);
7619 break;
7620 }
7621 }
7622 }
7623 all_ids.truncate(base_len);
7624 self.spec_p = p;
7625 self.spec_res = res;
7626 drafts.clone()
7628 } else if greedy_pen {
7629 let mut ids: Vec<u32> = Vec::with_capacity(b);
7633 for i in 0..b {
7634 let t = sampler::argmax_penalized(
7635 &logits[i * lm_rows..(i + 1) * lm_rows],
7636 &cfg,
7637 all_ids,
7638 &mut self.sampler_scratch,
7639 self.pool.as_deref(),
7640 );
7641 ids.push(t);
7642 if i < k_spec && t == drafts[i] {
7643 all_ids.push(t);
7644 } else {
7645 break;
7646 }
7647 }
7648 all_ids.truncate(base_len);
7649 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7650 a += 1;
7651 }
7652 ids
7655 } else if greedy_dev && dev_ids.len() == b {
7656 let ids = std::mem::take(&mut dev_ids);
7657 while a < k_spec && ids[a] == drafts[a] {
7658 a += 1;
7659 }
7660 ids
7661 } else {
7662 if logits.len() < b * lm_rows {
7663 self.clear_sequence_state();
7666 self.graph_failed
7667 .store(true, std::sync::atomic::Ordering::Relaxed);
7668 self.cancel
7669 .store(true, std::sync::atomic::Ordering::Relaxed);
7670 tracing::error!("Metal verify returned neither logits nor argmax ids");
7671 return None;
7672 }
7673 let ids: Vec<u32> = (0..b)
7674 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7675 .collect();
7676 while a < k_spec && ids[a] == drafts[a] {
7677 a += 1;
7678 }
7679 ids
7680 };
7681 spec_stamp("acc");
7682 if spec_dbg {
7683 eprintln!(
7684 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7685 drafts, ids
7686 );
7687 }
7688 #[cfg(target_os = "macos")]
7692 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7693 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7694 {
7695 let snap: Vec<Vec<f32>> = self
7696 .kv_cache
7697 .layers
7698 .iter()
7699 .map(|l| l.linear_state.clone())
7700 .collect();
7701 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7702 let toks: Vec<u32> = std::iter::once(t_next)
7703 .chain(drafts.iter().copied())
7704 .collect();
7705 let want_save = self.graph_want_logits;
7706 self.graph_want_logits = false;
7707 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7708 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7709 let _ = self.graph_logits.take();
7710 }
7711 self.graph_want_logits = want_save;
7712 let plain_states: Vec<Vec<f32>> = self
7713 .kv_cache
7714 .layers
7715 .iter()
7716 .map(|l| l.linear_state.clone())
7717 .collect();
7718 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7719 let mut rows = Vec::new();
7720 for (li, (l, n0)) in self
7721 .kv_cache
7722 .layers
7723 .iter_mut()
7724 .zip(attn_lens.iter())
7725 .enumerate()
7726 {
7727 let extra = l.seq_len.saturating_sub(*n0);
7728 if extra > 0 {
7729 let mut kk = Vec::new();
7730 let mut vv = Vec::new();
7731 for g in 0..nkv {
7732 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7733 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7734 }
7735 rows.push((li, kk, vv));
7736 l.truncate_last(extra);
7737 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7738 }
7739 }
7740 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7741 if l.linear_state.len() == st.len() {
7742 l.linear_state.copy_from_slice(&st);
7743 } else {
7744 l.linear_state = st;
7745 }
7746 }
7747 Some((plain_states, rows))
7748 } else {
7749 None
7750 };
7751 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7752 #[cfg(target_os = "macos")]
7761 let mut warm_pending: Option<MetalWarmPending> = None;
7762 #[cfg(target_os = "macos")]
7763 if metal_native {
7764 m.kv.truncate_last(k_spec.saturating_sub(1));
7765 if self.mtp_graph_mode == Some(true) {
7766 crate::gpu_metal::kv_mirror_set_stored(
7769 self.mtp_kv_id(),
7770 Self::MTP_LAYER_BASE,
7771 m.kv.seq_len,
7772 );
7773 if !warm_off && a > 0 {
7774 let pairs: Vec<(&[f32], u32)> = (0..a)
7775 .map(|j| {
7776 (
7777 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7778 ids[j],
7779 )
7780 })
7781 .collect();
7782 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7783 }
7784 }
7785 spec_stamp("c.wsub");
7786 }
7787 #[cfg(target_os = "macos")]
7789 if metal_native {
7790 if !self.metal_verify_commit(a) {
7793 self.clear_sequence_state();
7794 self.graph_failed
7795 .store(true, std::sync::atomic::Ordering::Relaxed);
7796 self.cancel
7797 .store(true, std::sync::atomic::Ordering::Relaxed);
7798 tracing::error!("Metal verify state/KV handoff failed after admission");
7799 return None;
7800 }
7801 if let Some((plain_states, rows)) = commit_ref {
7802 crate::gpu_metal::queue_fence();
7803 let _ = crate::gpu_metal::wait_replay();
7806 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7807 let mut worst_s = 0f32;
7808 let mut worst_li = 0usize;
7809 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7810 if l.linear_state.len() != ps.len() || ps.is_empty() {
7811 continue;
7812 }
7813 let d = l
7814 .linear_state
7815 .iter()
7816 .zip(ps)
7817 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7818 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7819 let rel = d / n.max(1e-6);
7820 if rel > worst_s {
7821 worst_s = rel;
7822 worst_li = li;
7823 }
7824 }
7825 let mut worst_k = 0f32;
7826 for (li, kk, vv) in &rows {
7827 let l = &self.kv_cache.layers[*li];
7828 let n0 = l.seq_len - (kk.len() / (nkv * hd));
7829 let mut ck = Vec::new();
7830 let mut cv = Vec::new();
7831 for g in 0..nkv {
7832 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7833 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7834 }
7835 if ck.len() == kk.len() {
7836 let dk = ck
7837 .iter()
7838 .zip(kk)
7839 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7840 let dv = cv
7841 .iter()
7842 .zip(vv)
7843 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7844 worst_k = worst_k.max(dk).max(dv);
7845 } else {
7846 eprintln!(
7847 "commit-check L{li}: kv row count mismatch {} vs {}",
7848 ck.len(),
7849 kk.len()
7850 );
7851 }
7852 }
7853 eprintln!(
7854 "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}"
7855 );
7856 }
7857 }
7858 if !metal_native && a + 1 < b {
7859 let expected_gdn_layers = self.graph_gdn_layer_count();
7860 if expected_gdn_layers > 0
7861 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7862 {
7863 self.clear_sequence_state();
7864 self.graph_failed
7865 .store(true, std::sync::atomic::Ordering::Relaxed);
7866 self.cancel
7867 .store(true, std::sync::atomic::Ordering::Relaxed);
7868 tracing::error!("GDN speculative restore failed after verify");
7869 return None;
7870 }
7871 }
7872 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7873 self.clear_sequence_state();
7878 self.graph_failed
7879 .store(true, std::sync::atomic::Ordering::Relaxed);
7880 self.cancel
7881 .store(true, std::sync::atomic::Ordering::Relaxed);
7882 tracing::error!("trunk graph KV rewind failed after speculative verify");
7883 return None;
7884 }
7885 *accepted += a;
7886 if !metal_native {
7897 m.kv.truncate_last(k_spec.saturating_sub(1));
7899 }
7900 spec_stamp("c.trunc");
7901 if !metal_native
7902 && self.mtp_graph_mode == Some(true)
7903 && !self.rewind_mtp_graph_mirror(next_pos)
7904 {
7905 self.clear_sequence_state();
7909 self.graph_failed
7910 .store(true, std::sync::atomic::Ordering::Relaxed);
7911 self.cancel
7912 .store(true, std::sync::atomic::Ordering::Relaxed);
7913 tracing::error!("MTP graph mirror rewind failed after verify commit");
7914 return None;
7915 }
7916 if !warm_off && a > 0 {
7917 let mut warmed = false;
7920 #[cfg(target_os = "macos")]
7921 if metal_native && self.mtp_graph_mode == Some(true) {
7922 warmed = match warm_pending.take() {
7926 Some(p) => self.mtp_warm_batch_finish(m, p),
7927 None => false,
7928 };
7929 if !warmed {
7930 warmed = true;
7931 for j in 0..a {
7932 let row =
7933 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
7934 if self
7935 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
7936 .is_none()
7937 {
7938 warmed = false;
7939 break;
7940 }
7941 }
7942 }
7943 }
7944 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
7945 let rows: Vec<Vec<f32>> = (0..a)
7946 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
7947 .collect();
7948 let pairs: Vec<(&[f32], u32)> = rows
7949 .iter()
7950 .zip(ids.iter())
7951 .map(|(r, &t)| (r.as_slice(), t))
7952 .collect();
7953 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
7954 Ok(()) => warmed = true,
7955 Err(err) => {
7956 tracing::error!("{err}");
7962 self.clear_sequence_state();
7963 self.graph_failed
7964 .store(true, std::sync::atomic::Ordering::Relaxed);
7965 self.cancel
7966 .store(true, std::sync::atomic::Ordering::Relaxed);
7967 return None;
7968 }
7969 }
7970 }
7971 if !warmed {
7972 for j in 0..a {
7973 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
7974 let row = row.to_vec();
7975 self.mtp_warm(m, &row, ids[j], next_pos + j);
7976 }
7977 }
7978 }
7979 spec_stamp("c.warm");
7983 if let Some(c) = forced {
7984 self.spec_forced = Some(c);
7985 self.graph_logits = None;
7986 } else if greedy_dev && logits.is_empty() {
7987 self.spec_forced = Some(ids[a]);
7990 self.graph_logits = None;
7991 } else {
7992 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
7993 row.resize(self.vocab_size, 0.0);
7994 if let Some(c) = self.final_softcap {
7995 for l in row.iter_mut() {
7996 *l = c * (*l / c).tanh();
7997 }
7998 }
7999 self.graph_logits = Some(row);
8000 }
8001 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8002 spec_stamp("c.row");
8003 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8009 let end = subs();
8010 eprintln!(
8011 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8012 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8013 t_draft.as_secs_f64() * 1e3,
8014 sub_draft - sub0,
8015 (t_verify - t_draft).as_secs_f64() * 1e3,
8016 sub_verify - sub_draft,
8017 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8018 end - sub_verify,
8019 self.draft_full_streak,
8020 );
8021 }
8022 if k_env.is_none() && !metal_native && !k_capped {
8027 let f = a as f32 / k_spec.max(1) as f32;
8031 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8032 let mut k_next = k_spec;
8033 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8034 k_next = k_spec + 1;
8035 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8036 k_next = k_spec - 1;
8037 }
8038 if k_next != k_spec {
8039 self.spec_acc_ewma = 0.6;
8040 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8041 eprintln!("spec-k: {k_spec} → {k_next}");
8042 }
8043 }
8044 self.spec_k_adapt = Some(k_next);
8045 }
8046 spec_stamp("end");
8047 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8048 }
8049
8050 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8059 if !self.pair_supported() {
8060 return (0.0, 0.0);
8061 }
8062 if self.gdn_cfg.is_some()
8070 && self.graph_prefill_preferred()
8071 && self.automatic_gpu_prefix().is_some()
8072 {
8073 tracing::info!(
8074 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8075 );
8076 return (0.0, 0.0);
8077 }
8078 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8085 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8086 let emb1 = self.embed_single(1);
8087 let emb2 = self.embed_single(2);
8088 let pos = self.kv_cache.seq_len();
8089
8090 let t0 = std::time::Instant::now();
8091 for _ in 0..iters {
8092 let _ = self.forward_layers(&emb1, pos, None);
8093 let _ = self.forward_layers(&emb2, pos + 1, None);
8094 for l in &mut self.kv_cache.layers {
8095 l.truncate_last(2);
8096 }
8097 }
8098 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8099
8100 let t1 = std::time::Instant::now();
8101 for _ in 0..iters {
8102 let _ = self.forward_pair(&emb1, &emb2, pos);
8103 for l in &mut self.kv_cache.layers {
8104 l.truncate_last(2);
8105 }
8106 }
8107 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8108 match graph_env {
8109 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8110 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8111 }
8112 (singles_ms, pair_ms)
8113 }
8114
8115 fn pair_supported(&self) -> bool {
8123 !self.weights.layers.is_empty()
8130 && self.g3n.is_none()
8131 && !self
8132 .weights
8133 .layers
8134 .iter()
8135 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8136 }
8137
8138 fn forward_pair(
8139 &mut self,
8140 emb1: &[f32],
8141 emb2: &[f32],
8142 position: usize,
8143 ) -> (Vec<f32>, Vec<f32>) {
8144 self.mimo_moe_prepare();
8147 let mut h1 = emb1.to_vec();
8148 let mut h2 = emb2.to_vec();
8149 let (_nkv, _hd, hs, _rd, eps) = (
8150 self.num_kv_heads,
8151 self.head_dim,
8152 self.hidden_size,
8153 self.rotary_dim,
8154 self.rms_eps,
8155 );
8156 let pool = self.pool.clone();
8157 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8164
8165 for li in 0..self.num_layers {
8166 let _capacity_tail = automatic_gpu_prefix
8167 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8168 .map(|_| crate::gpu::enter_cpu_scope());
8169 crate::gpu::set_layer(li as i64);
8172 let lw = &self.weights.layers[self.phys_layer(li)];
8173 inference::rms_norm_into(
8176 &h1,
8177 &lw.input_norm,
8178 self.rms_eps,
8179 self.norm_style,
8180 &mut self.ws.n1,
8181 );
8182 inference::rms_norm_into(
8183 &h2,
8184 &lw.input_norm,
8185 self.rms_eps,
8186 self.norm_style,
8187 &mut self.ws.n2,
8188 );
8189
8190 let (a1, a2) = match &lw.attn {
8191 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8192 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8193 AttnKind::Bounded(w) => {
8194 let rope = self
8197 .bounded_rope
8198 .clone()
8199 .expect("bounded layer without an installed rotation table");
8200 let cfg = crate::bounded::BoundedAttnCfg {
8201 num_heads: self.num_heads,
8202 num_kv_heads: self.num_kv_heads,
8203 head_dim: self.head_dim,
8204 hidden_size: hs,
8205 scale: self.attn_scale,
8206 rope: &rope,
8207 pool: pool.as_deref(),
8208 };
8209 let a1 = crate::bounded::bounded_attention(
8210 &self.ws.n1,
8211 w,
8212 &mut self.kv_cache.layers[li],
8213 &cfg,
8214 );
8215 let a2 = crate::bounded::bounded_attention(
8216 &self.ws.n2,
8217 w,
8218 &mut self.kv_cache.layers[li],
8219 &cfg,
8220 );
8221 (a1, a2)
8222 }
8223 AttnKind::Linear(w) => {
8224 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8225 let layer = &mut self.kv_cache.layers[li];
8226 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8227 vmf_phase_pair(
8228 &self.ws.n1,
8229 &self.ws.n2,
8230 w,
8231 &cfg,
8232 state,
8233 scratch,
8234 self.pool.as_deref(),
8235 )
8236 }
8237 AttnKind::LinearGdn(w) => {
8238 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8239 let layer = &mut self.kv_cache.layers[li];
8240 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8241 gdn_pair(
8242 &self.ws.n1,
8243 &self.ws.n2,
8244 w,
8245 &cfg,
8246 state,
8247 scratch,
8248 self.pool.as_deref(),
8249 )
8250 }
8251 AttnKind::ShortConv(w) => {
8252 let cfg = self
8253 .short_conv_cfg
8254 .expect("short-conv layer without short_conv_cfg");
8255 let layer = &mut self.kv_cache.layers[li];
8256 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8257 short_conv_pair(
8258 &self.ws.n1,
8259 &self.ws.n2,
8260 w,
8261 &cfg,
8262 state,
8263 scratch,
8264 self.pool.as_deref(),
8265 )
8266 }
8267 AttnKind::Full {
8268 wq,
8269 wk,
8270 wv,
8271 wo,
8272 q_norm,
8273 k_norm,
8274 output_gate,
8275 softplus_gate,
8276 bias,
8277 } => {
8278 let inv_freq_l = self.layer_inv_freq(li);
8279 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8280 let cfg = QwenAttnCfg {
8281 num_heads: self.layer_num_heads(li),
8282 num_kv_heads: nkv_l,
8283 head_dim: hd_l,
8284 hidden_size: hs,
8285 position,
8286 inv_freq: &inv_freq_l,
8287 rotary_dim: rd_l,
8288 scale: self.attn_scale,
8289 softcap: self.attn_softcap,
8290 window: self.layer_window(li),
8291 v_norm: self.attn_v_norm,
8292 qk_norm_after_rope: self.qk_norm_after_rope,
8293 gate_sigmoid: self.proj_gate_sigmoid,
8294 q_norm: q_norm.as_deref(),
8295 k_norm: k_norm.as_deref(),
8296 output_gate: *output_gate,
8297 softplus_gate: softplus_gate
8298 .as_ref()
8299 .map(|(gate, per_head)| (gate, *per_head)),
8300 rope_scale: self.layer_rope_scale(li),
8301 bias: bias
8302 .as_ref()
8303 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8304 rms_eps: eps,
8305 norm_style: self.norm_style,
8306 pool: pool.as_deref(),
8307 v_head_dim: self.layer_v_dim(li),
8308 };
8309 attention::qwen_attention_pair(
8310 &self.ws.n1,
8311 &self.ws.n2,
8312 wq,
8313 wk,
8314 wv,
8315 wo,
8316 &mut self.kv_cache.layers[li],
8317 &cfg,
8318 )
8319 }
8320 };
8321 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8322 Some(w) => (
8323 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8324 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8325 ),
8326 None => (a1, a2),
8327 };
8328 for i in 0..self.hidden_size {
8329 h1[i] += a1[i];
8330 h2[i] += a2[i];
8331 }
8332 let (mut a1, mut a2) = (a1, a2);
8333 attention::recycle_buf(&mut a1);
8334 attention::recycle_buf(&mut a2);
8335
8336 let lw = &self.weights.layers[self.phys_layer(li)];
8337 inference::rms_norm_into(
8338 &h1,
8339 &lw.post_norm,
8340 self.rms_eps,
8341 self.norm_style,
8342 &mut self.ws.p1,
8343 );
8344 inference::rms_norm_into(
8345 &h2,
8346 &lw.post_norm,
8347 self.rms_eps,
8348 self.norm_style,
8349 &mut self.ws.p2,
8350 );
8351 let (f1, f2) = match &lw.ffn {
8352 FfnKind::DenseMoe(dm) => (
8355 dense_moe_ffn(
8356 dm,
8357 &self.ws.p1,
8358 &h1,
8359 self.rms_eps,
8360 self.norm_style,
8361 self.pool.as_deref(),
8362 ),
8363 dense_moe_ffn(
8364 dm,
8365 &self.ws.p2,
8366 &h2,
8367 self.rms_eps,
8368 self.norm_style,
8369 self.pool.as_deref(),
8370 ),
8371 ),
8372 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8373 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8374 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8375 ),
8376 _ => ffn_forward_pair(
8377 &lw.ffn,
8378 &self.ws.p1,
8379 &self.ws.p2,
8380 self.pool.as_deref(),
8381 None,
8382 ),
8383 };
8384 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8385 Some(w) => (
8386 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8387 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8388 ),
8389 None => (f1, f2),
8390 };
8391 for i in 0..self.hidden_size {
8392 h1[i] += f1[i];
8393 h2[i] += f2[i];
8394 }
8395 let (mut f1, mut f2) = (f1, f2);
8396 attention::recycle_buf(&mut f1);
8397 attention::recycle_buf(&mut f2);
8398 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8399 for i in 0..self.hidden_size {
8400 h1[i] *= sc;
8401 h2[i] *= sc;
8402 }
8403 }
8404 if self.is_loop_end(li) && li + 1 < self.num_layers {
8406 h1 = inference::rms_norm(
8407 &h1,
8408 &self.weights.final_norm,
8409 self.rms_eps,
8410 self.norm_style,
8411 );
8412 h2 = inference::rms_norm(
8413 &h2,
8414 &self.weights.final_norm,
8415 self.rms_eps,
8416 self.norm_style,
8417 );
8418 }
8419 }
8420 crate::gpu::set_layer(-1); if self.o1_active() {
8427 self.commit_linear_scratch();
8428 }
8429 self.o1_progress();
8430 self.swa_trim_tails();
8431 (h1, h2)
8432 }
8433
8434 fn commit_linear_scratch(&mut self) {
8436 for layer in &mut self.kv_cache.layers {
8437 if !layer.linear_scratch.is_empty() {
8438 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8439 layer.linear_scratch.clear();
8440 }
8441 }
8442 }
8443
8444 pub fn forward_ids(
8447 &mut self,
8448 ids: &[u32],
8449 task_mask: Option<&TaskMask>,
8450 ) -> Result<Vec<f32>, String> {
8451 #[cfg(target_os = "macos")]
8452 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8453 if ids.is_empty() {
8454 return Err("empty id sequence".to_string());
8455 }
8456 self.clear_sequence_state();
8457 self.check_forward_graph("forward_ids setup", 0)?;
8458 if task_mask.is_none() {
8459 self.o1_begin();
8460 }
8461 let mut hidden = vec![0.0f32; self.hidden_size];
8462 let mut pos = 0usize;
8463 if let Some(b) = &mut self.dsv41 {
8464 let pool = self.pool.clone();
8465 let mut logits = Vec::new();
8466 crate::dsv41::forward_chunk(
8467 &b.0,
8468 &b.1,
8469 &b.2,
8470 &mut b.3,
8471 ids,
8472 0,
8473 pool.as_deref(),
8474 &mut logits,
8475 );
8476 if let Err(err) = self.o1_seal_checked() {
8477 self.clear_sequence_state();
8478 return Err(err);
8479 }
8480 return Ok(logits);
8481 }
8482 #[cfg(not(target_os = "macos"))]
8493 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8494 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8495 Ok(r) => r,
8496 Err(e) => {
8497 self.clear_sequence_state();
8498 return Err(e);
8499 }
8500 };
8501 if n > 0 {
8502 hidden.copy_from_slice(&last);
8503 pos = n;
8504 }
8505 }
8506 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8507 let chunk = self.prefill_chunk();
8511 let hs = self.hidden_size;
8512 while pos < ids.len() {
8513 let end = (pos + chunk).min(ids.len());
8514 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8515 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8516 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8517 pos = end;
8518 }
8519 }
8520 if task_mask.is_none()
8529 && !self.graph_prefill_preferred()
8530 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8531 && self.pair_supported()
8532 {
8533 while pos + 1 < ids.len() {
8534 let e1 = self.embed_single(ids[pos]);
8535 let e2 = self.embed_single(ids[pos + 1]);
8536 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8537 self.check_forward_graph("forward_ids pair", pos + 1)?;
8538 self.commit_linear_scratch();
8539 hidden = h2;
8540 pos += 2;
8541 }
8542 }
8543 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8546 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8547 self.graph_logits = Some(lg);
8548 hidden = vec![0.0; self.hidden_size];
8549 pos = ids.len();
8550 }
8551 }
8552 while pos < ids.len() {
8553 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8554 self.check_forward_graph("forward_ids", pos)?;
8555 pos += 1;
8556 }
8557 if let Some(logits) = self.graph_logits.take() {
8558 if let Err(err) = self.o1_seal_checked() {
8562 self.clear_sequence_state();
8563 return Err(err);
8564 }
8565 return Ok(logits);
8566 }
8567 if let Err(err) = self.o1_seal_checked() {
8571 self.clear_sequence_state();
8572 return Err(err);
8573 }
8574 let normed = inference::rms_norm(
8575 &hidden,
8576 &self.weights.final_norm,
8577 self.rms_eps,
8578 self.norm_style,
8579 );
8580 Ok(self.lm_head_forward(&normed))
8581 }
8582
8583 #[doc(hidden)]
8587 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8588 #[cfg(target_os = "macos")]
8589 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8590 if ids.is_empty() {
8591 return Err("empty id sequence".to_string());
8592 }
8593 self.clear_sequence_state();
8594 self.dsv41
8595 .as_ref()
8596 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8597 self.o1_begin();
8598 let rows = {
8599 let pool = self.pool.clone();
8600 let b = self
8601 .dsv41
8602 .as_mut()
8603 .expect("dsv41 checked above; state cannot change during forward");
8604 let mut rows = Vec::with_capacity(ids.len());
8605 for (position, &id) in ids.iter().enumerate() {
8606 let mut logits = Vec::new();
8607 crate::dsv41::forward_token(
8608 &b.0,
8609 &b.1,
8610 &b.2,
8611 &mut b.3,
8612 id,
8613 position,
8614 pool.as_deref(),
8615 &mut logits,
8616 );
8617 rows.push(logits);
8618 }
8619 rows
8620 };
8621 self.o1_seal();
8622 Ok(rows)
8623 }
8624
8625 pub fn dsv41_moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
8629 #[cfg(target_os = "macos")]
8630 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8631 if ids.is_empty() {
8632 return Err("empty id sequence".to_string());
8633 }
8634 self.dsv41.as_ref().ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
8635 self.clear_sequence_state();
8636 let res = {
8637 let pool = self.pool.clone();
8638 let b = self.dsv41.as_mut().expect("dsv41 checked above");
8639 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
8640 };
8641 self.clear_sequence_state();
8642 res
8643 }
8644
8645 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8652 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8653 Ok((nll / cnt.max(1) as f64).exp())
8654 }
8655
8656 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
8663 if !self.can_prefill_batched() {
8664 return self.nll_ids_from(ids, 0).map(|_| ());
8665 }
8666 self.nll_begin()?;
8667 let result: Result<(), String> = (|| {
8668 let chunk = chunk.max(1);
8669 let mut pos = 0usize;
8670 while pos < ids.len() {
8671 let end = (pos + chunk).min(ids.len());
8672 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
8673 self.nll_check_graph("calibration prefill", pos)?;
8674 pos = end;
8675 }
8676 Ok(())
8677 })();
8678 self.nll_end();
8679 result
8680 }
8681
8682 pub fn calib_window_span(&mut self, ids: &[u32], input: Option<&[f32]>, from: usize, mid: usize, upto: usize) -> Result<Vec<f32>, String> {
8691 if !self.can_prefill_batched() {
8692 return Err("calib_window_span: this architecture has no batched prefill".into());
8693 }
8694 self.nll_begin()?;
8695 let result: Result<Vec<f32>, String> = (|| {
8696 let h = if mid > from {
8697 let inp = match input {
8698 Some(h) => PrefillIn::Hidden(h),
8699 None => PrefillIn::Ids(ids),
8700 };
8701 let h = self.prefill_batch_span(inp, 0, None, from, mid);
8702 self.nll_check_graph("calibration span", 0)?;
8703 h
8704 } else {
8705 input.map(<[f32]>::to_vec).unwrap_or_default()
8706 };
8707 if upto > mid {
8708 let inp = if mid > from || input.is_some() { PrefillIn::Hidden(&h) } else { PrefillIn::Ids(ids) };
8709 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
8710 self.nll_check_graph("calibration span", 0)?;
8711 }
8712 Ok(h)
8713 })();
8714 self.nll_end();
8715 result
8716 }
8717
8718 #[cfg(feature = "gpu")]
8722 pub fn qwen4_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
8723 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
8724 crate::qwen4_exp::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, &self.inv_freq, self.pool.as_deref())
8725 }
8726
8727 pub fn qwen4_expert_dirs(&self, li: usize) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
8729 let b = self.qwen4_exp.as_ref()?;
8730 crate::qwen4_exp::expert_dirs(&b.1, li)
8731 }
8732
8733 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8738 self.clear_sequence_state();
8739 FFN_PROBE.with(|p| {
8740 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8741 });
8742 crate::gpu::cpu_scope(|| {
8743 for (pos, &id) in ids.iter().enumerate() {
8744 let emb = self.embed_single(id);
8745 let _ = self.forward_layers(&emb, pos, None);
8746 }
8747 });
8748 self.clear_sequence_state();
8749 FFN_PROBE
8750 .with(|p| p.borrow_mut().take())
8751 .unwrap_or_default()
8752 }
8753
8754 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8758 if let Err(err) = self.nll_begin() {
8759 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8763 self.nll_end();
8764 return Err(err);
8765 }
8766 FFN_PROBE.with(|p| {
8767 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8768 });
8769 let result: Result<(), String> = (|| {
8770 for chunk in ids.chunks(256) {
8771 if chunk.len() < 2 {
8772 continue;
8773 }
8774 self.nll_ids_masked(chunk, 0, None)?;
8775 }
8776 Ok(())
8777 })();
8778 self.nll_end();
8779 let probe = FFN_PROBE
8780 .with(|p| p.borrow_mut().take())
8781 .unwrap_or_default();
8782 match result {
8783 Ok(()) => Ok(probe),
8784 Err(err) => {
8785 drop(probe);
8786 Err(err)
8787 }
8788 }
8789 }
8790
8791 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8795 self.nll_begin()?;
8796 let result: Result<f64, String> = (|| {
8797 let mut nll = 0f64;
8798 let mut cnt = 0usize;
8799 let mut hidden = vec![0f32; self.hidden_size];
8800 for (pos, &id) in ids.iter().enumerate() {
8801 if pos > 0 {
8802 inference::rms_norm_into(
8803 &hidden,
8804 &self.weights.final_norm,
8805 self.rms_eps,
8806 self.norm_style,
8807 &mut self.ws.n1,
8808 );
8809 let mut logits = self.lm_head_forward(&self.ws.n1);
8810 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8811 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8812 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8813 nll -= p.max(1e-300).ln();
8814 cnt += 1;
8815 attention::recycle_buf(&mut logits);
8816 }
8817 let emb = self.embed_single(id);
8818 hidden = self.forward_layers(&emb, pos, Some(mask));
8819 self.nll_check_graph("masked serial forward", pos)?;
8820 let _ = self.graph_logits.take();
8824 }
8825 Ok((nll / cnt.max(1) as f64).exp())
8826 })();
8827 self.nll_end();
8828 result
8829 }
8830
8831 pub fn nll_ids_masked(
8850 &mut self,
8851 ids: &[u32],
8852 start: usize,
8853 task_mask: Option<&TaskMask>,
8854 ) -> Result<(f64, usize), String> {
8855 let task_mask = self.drop_open_mask(task_mask);
8856 self.nll_ids_inner(ids, start, task_mask)
8857 }
8858
8859 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
8860 self.nll_ids_inner(ids, start, None)
8861 }
8862
8863 fn nll_ids_inner(
8864 &mut self,
8865 ids: &[u32],
8866 start: usize,
8867 task_mask: Option<&TaskMask>,
8868 ) -> Result<(f64, usize), String> {
8869 self.nll_begin()?;
8870 let result: Result<(f64, usize), String> = (|| {
8871 let mut nll = 0f64;
8872 let mut cnt = 0usize;
8873 let (graph_quality, fused_head_quality) = nll_graph_policy(
8886 task_mask.is_none(),
8887 self.graph_prefill_preferred(),
8888 crate::gpu::q1_force(),
8889 );
8890 self.graph_head_required = fused_head_quality;
8891 self.graph_want_logits = fused_head_quality;
8892 #[cfg(target_os = "macos")]
8893 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
8894 match self.nll_batch_metal(ids, start) {
8895 MetalBatchNllOutcome::Completed(nll, count) => {
8896 return Ok((nll, count));
8897 }
8898 MetalBatchNllOutcome::Declined => {}
8899 MetalBatchNllOutcome::Failed(err) => return Err(err),
8900 }
8901 }
8902 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
8907 if self.can_prefill_batched() && !graph_quality && !force_serial {
8908 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
8917 .ok()
8918 .and_then(|v| v.parse().ok())
8919 .filter(|&c: &usize| c > 0)
8920 .unwrap_or(128);
8921 const LM_SUB: usize = 32;
8922 let n = ids.len().saturating_sub(1);
8923 let hs = self.hidden_size;
8924 let rows = self.weights.lm_head.rows();
8925 let mut pos = 0usize;
8926 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
8927 while pos < n {
8928 let end = (pos + chunk_len).min(n);
8929 let bsz = end - pos;
8930 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8931 self.nll_check_graph("batched prefill", pos)?;
8932 if state_trace && end % 256 == 0 {
8933 self.trace_recurrent_state(end);
8934 }
8935 let mut k0 = 0usize;
8936 while k0 < bsz {
8937 let k1 = (k0 + LM_SUB).min(bsz);
8938 let sb = k1 - k0;
8939 if pos + k1 <= start {
8942 k0 = k1;
8943 continue;
8944 }
8945 let mut normed = vec![0.0f32; sb * hs];
8946 for k in 0..sb {
8947 let r = inference::rms_norm(
8948 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
8949 &self.weights.final_norm,
8950 self.rms_eps,
8951 self.norm_style,
8952 );
8953 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
8954 }
8955 let mut logits = vec![0.0f32; sb * rows];
8956 self.weights
8957 .lm_head
8958 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
8959 for k in 0..sb {
8960 if pos + k0 + k < start {
8961 continue;
8962 }
8963 self.nll_check_graph("batched score row", pos + k0 + k)?;
8964 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
8965 if let Some(mu) = self.logit_multiplier {
8966 for v in lg.iter_mut() {
8967 *v *= mu;
8968 }
8969 }
8970 if let Some(c) = self.final_softcap {
8974 softcap_in_place(lg, c, self.pool.as_deref());
8975 }
8976 if let Some(cm) = self.head_clusters.clone() {
8979 self.hierarchical_head_logprobs(
8980 &normed[k * hs..(k + 1) * hs],
8981 &cm,
8982 lg,
8983 );
8984 }
8985 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
8986 let target = ids[pos + k0 + k + 1] as usize;
8987 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8988 let lse: f64 = lg
8989 .iter()
8990 .map(|&v| ((v - max) as f64).exp())
8991 .sum::<f64>()
8992 .ln()
8993 + max as f64;
8994 nll += lse - lg[target] as f64;
8995 cnt += 1;
8996 if std::env::var("CMF_PPL_TRACE").is_ok() {
8997 let top = lg
8998 .iter()
8999 .enumerate()
9000 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9001 .map(|(i, _)| i)
9002 .unwrap_or(0);
9003 eprintln!(
9004 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9005 pos + k0 + k,
9006 target,
9007 lse - lg[target] as f64,
9008 top,
9009 lg[target],
9010 lg[top]
9011 );
9012 }
9013 }
9014 k0 = k1;
9015 }
9016 pos = end;
9017 }
9018 return Ok((nll, cnt));
9019 }
9020 for pos in 0..ids.len().saturating_sub(1) {
9021 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9022 self.nll_check_graph("serial forward", pos)?;
9023 let out_of_band = self.graph_logits.take();
9031 if self.graph_head_required && out_of_band.is_none() {
9032 METAL_GRAPH_HEAD_MISS.fetch_add(
9033 1,
9034 std::sync::atomic::Ordering::Relaxed,
9035 );
9036 return Err(format!(
9037 "fused Metal graph head did not complete at NLL position {pos}"
9038 ));
9039 }
9040 if pos < start {
9041 continue;
9042 }
9043 let logits = match out_of_band {
9044 Some(lg) => lg,
9045 None => {
9046 let normed = inference::rms_norm(
9047 &hidden,
9048 &self.weights.final_norm,
9049 self.rms_eps,
9050 self.norm_style,
9051 );
9052 self.lm_head_forward(&normed)
9056 }
9057 };
9058 let target = ids[pos + 1] as usize;
9059 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9060 let lse: f64 = logits
9061 .iter()
9062 .map(|&v| ((v - max) as f64).exp())
9063 .sum::<f64>()
9064 .ln()
9065 + max as f64;
9066 let tok_nll = lse - logits[target] as f64;
9067 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9068 let top = logits
9069 .iter()
9070 .enumerate()
9071 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9072 .map(|(i, _)| i)
9073 .unwrap_or(0);
9074 eprintln!(
9075 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9076 logits[target], logits[top]
9077 );
9078 }
9079 nll += tok_nll;
9080 cnt += 1;
9081 }
9082 Ok((nll, cnt))
9083 })();
9084 self.nll_end();
9085 result
9086 }
9087
9088 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9093 let normed = inference::rms_norm(
9094 hidden,
9095 &self.weights.final_norm,
9096 self.rms_eps,
9097 self.norm_style,
9098 );
9099 let mut logits = self.lm_head_forward(&normed);
9102 let target = target as usize;
9103 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9104 let lse: f64 = logits
9105 .iter()
9106 .map(|&v| ((v - max) as f64).exp())
9107 .sum::<f64>()
9108 .ln()
9109 + max as f64;
9110 let tok_nll = lse - logits[target] as f64;
9111 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9112 let top = logits
9113 .iter()
9114 .enumerate()
9115 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9116 .map(|(i, _)| i)
9117 .unwrap_or(0);
9118 eprintln!(
9119 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9120 logits[target], logits[top]
9121 );
9122 }
9123 attention::recycle_buf(&mut logits);
9124 tok_nll
9125 }
9126
9127 fn trace_recurrent_state(&self, pos: usize) {
9135 let stats = |v: &[f32]| -> (f64, f64) {
9136 if v.is_empty() {
9137 return (0.0, 0.0);
9138 }
9139 let (mut ss, mut mx) = (0f64, 0f64);
9140 for &x in v {
9141 ss += (x as f64) * (x as f64);
9142 mx = mx.max((x as f64).abs());
9143 }
9144 ((ss / v.len() as f64).sqrt(), mx)
9145 };
9146 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9147 let lw = &self.weights.layers[self.phys_layer(li)];
9148 let (kind, s_len) = match &lw.attn {
9149 AttnKind::Linear(_) => (
9150 "vmf",
9151 self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0),
9152 ),
9153 AttnKind::LinearGdn(_) => (
9154 "gdn",
9155 self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0),
9156 ),
9157 AttnKind::Bounded(_) => ("bounded", 0),
9158 AttnKind::Full { .. } => ("full", 0),
9159 _ => ("other", 0),
9160 };
9161 let (rms, max) = stats(&l.linear_state);
9162 let s_part = if kind == "vmf" {
9163 &l.linear_state[..s_len.min(l.linear_state.len())]
9164 } else if kind == "gdn" {
9165 let ring = l.linear_state.len().saturating_sub(s_len).min(l.linear_state.len());
9166 &l.linear_state[ring..]
9167 } else {
9168 &l.linear_state[..0]
9169 };
9170 let (s_rms, s_max) = stats(s_part);
9171 let (ring_rms, ring_len) = match &l.bounded {
9172 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9173 None => (0.0, 0),
9174 };
9175 eprintln!(
9176 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9177 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9178 l.linear_state.len(),
9179 l.seq_len
9180 );
9181 }
9182 }
9183
9184 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9202 self.nll_begin()?;
9207 let requested_prefix = (prefill > 0).then_some(prefill);
9208 self.o1_begin_with_prefix(requested_prefix);
9209 let n = ids.len().saturating_sub(1);
9210 let requested_start = prefill.min(n);
9211 let exact_end = if self.o1_active() {
9216 match requested_prefix {
9217 Some(requested) => self.o1_effective_boundary(requested),
9218 None => self
9219 .o1_cfg
9220 .as_ref()
9221 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9222 }
9223 .unwrap_or(requested_start)
9224 .min(n)
9225 } else {
9226 requested_start
9227 };
9228 let mut nll = 0f64;
9229 let mut cnt = 0usize;
9230
9231 let mut pos = 0usize;
9235 if self.can_prefill_batched() {
9236 const CHUNK: usize = 128;
9237 while pos < exact_end {
9238 let end = (pos + CHUNK).min(exact_end);
9239 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9240 if self
9241 .graph_failed
9242 .swap(false, std::sync::atomic::Ordering::Relaxed)
9243 {
9244 self.cancel
9245 .store(false, std::sync::atomic::Ordering::Relaxed);
9246 self.nll_end();
9247 return Err("GPU graph failed during O(1) NLL prefix".into());
9248 }
9249 for row in 0..end - pos {
9250 let score_pos = pos + row;
9251 if score_pos >= requested_start && score_pos < n {
9252 nll += self.nll_from_hidden(
9253 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9254 ids[score_pos + 1],
9255 score_pos,
9256 );
9257 cnt += 1;
9258 }
9259 }
9260 pos = end;
9261 }
9262 } else {
9263 while pos < exact_end {
9264 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9265 if self
9266 .graph_failed
9267 .swap(false, std::sync::atomic::Ordering::Relaxed)
9268 {
9269 self.cancel
9270 .store(false, std::sync::atomic::Ordering::Relaxed);
9271 self.nll_end();
9272 return Err("GPU graph failed during O(1) NLL prefix".into());
9273 }
9274 if pos >= requested_start && pos < n {
9275 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9276 cnt += 1;
9277 }
9278 pos += 1;
9279 }
9280 }
9281 self.o1_seal_checked().map_err(|err| {
9282 self.nll_end();
9283 err
9284 })?;
9285
9286 let batch_k = std::env::var("CMF_BATCH_K")
9295 .ok()
9296 .and_then(|v| v.parse::<usize>().ok())
9297 .unwrap_or(0);
9298 let batch_admitted = batch_k > 0
9299 && self.can_prefill_batched()
9300 && self.o1_active()
9301 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9302 && (0..self.num_layers).all(|li| {
9303 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9304 cache.o1.is_none() || cache.o1_views().is_some()
9305 });
9306 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9307 eprintln!(
9308 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9309 batch_admitted,
9310 batch_k,
9311 n.saturating_sub(exact_end),
9312 );
9313 }
9314 let mut batch_completed = false;
9315 if batch_admitted && exact_end < n {
9316 let hs = self.hidden_size;
9317 let mut batch_pos = exact_end;
9318 while batch_pos < n {
9319 let end = (batch_pos + batch_k).min(n);
9320 let bk = end - batch_pos;
9321 let mut hiddens = vec![0.0f32; bk * hs];
9322 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9323 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9324 }
9325 let positions: Vec<usize> = (batch_pos..end).collect();
9326 let t_batch = std::time::Instant::now();
9327 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9328 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9329 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9330 eprintln!(
9331 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9332 batch_pos,
9333 end.saturating_sub(1),
9334 bk as f64 / (ms / 1000.0),
9335 );
9336 }
9337 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9338 self.nll_end();
9339 return Err(err);
9340 }
9341 match outcome {
9342 crate::gpu::BatchGraphOutcome::Completed => {
9343 batch_completed = true;
9344 for row in 0..bk {
9345 nll += self.nll_from_hidden(
9346 &hiddens[row * hs..(row + 1) * hs],
9347 ids[batch_pos + row + 1],
9348 batch_pos + row,
9349 );
9350 cnt += 1;
9351 }
9352 batch_pos = end;
9353 }
9354 crate::gpu::BatchGraphOutcome::Declined => {
9355 if batch_completed {
9356 self.nll_end();
9357 return Err(format!(
9358 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9359 ));
9360 }
9361 break;
9362 }
9363 crate::gpu::BatchGraphOutcome::Failed => {
9364 self.nll_end();
9365 return Err(format!(
9366 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9367 ));
9368 }
9369 }
9370 }
9371 if batch_completed && cnt == n.saturating_sub(requested_start) {
9372 self.nll_end();
9373 return Ok((nll, cnt));
9374 }
9375 }
9376
9377 for pos in exact_end..n {
9382 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9383 if self
9384 .graph_failed
9385 .swap(false, std::sync::atomic::Ordering::Relaxed)
9386 {
9387 self.cancel
9388 .store(false, std::sync::atomic::Ordering::Relaxed);
9389 self.nll_end();
9390 return Err(format!(
9391 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9392 ));
9393 }
9394 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9395 cnt += 1;
9396 }
9397 self.nll_end();
9398 Ok((nll, cnt))
9399 }
9400
9401 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9409 self.clear_sequence_state();
9410 let n = ids.len().saturating_sub(1);
9411 let mut correct = Vec::with_capacity(n);
9412 let mut pmax = Vec::with_capacity(n);
9413 for pos in 0..n {
9414 let emb = self.embed_single(ids[pos]);
9415 let hidden = self.forward_layers(&emb, pos, None);
9416 let logits = if let Some(logits) = self.graph_logits.take() {
9417 logits
9418 } else {
9419 let normed = inference::rms_norm(
9420 &hidden,
9421 &self.weights.final_norm,
9422 self.rms_eps,
9423 self.norm_style,
9424 );
9425 self.lm_head_forward(&normed)
9429 };
9430 let target = ids[pos + 1] as usize;
9431 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9432 for (i, &v) in logits.iter().enumerate() {
9433 if v > mval {
9434 mval = v;
9435 amax = i;
9436 }
9437 }
9438 correct.push(amax == target);
9439 let row: Vec<f32> = temps
9440 .iter()
9441 .map(|&t| {
9442 let tt = t.max(1e-3);
9443 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9444 1.0 / s.max(1e-12) })
9446 .collect();
9447 pmax.push(row);
9448 }
9449 self.clear_sequence_state();
9450 (correct, pmax)
9451 }
9452
9453 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9460 if self.dyn_router.is_none() {
9461 return Ok((self.ppl_ids(ids)?, 0));
9462 }
9463 self.nll_begin()?;
9464 let saved_active = self.dyn_active;
9465 let mut router = self
9466 .dyn_router
9467 .take()
9468 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9469 router.reset();
9470 self.dyn_phi_seen = 0;
9471 let _ = self.set_active_skill(None);
9472
9473 let result: Result<(f64, usize), String> = (|| {
9474 let mut nll = 0f64;
9475 let mut cnt = 0usize;
9476 for pos in 0..ids.len().saturating_sub(1) {
9477 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9478 self.nll_check_graph("dynamic serial forward", pos)?;
9479 let out_of_band = self.graph_logits.take();
9480 let mut logits = match out_of_band {
9481 Some(lg) => lg,
9482 None => {
9483 let normed = inference::rms_norm(
9484 &hidden,
9485 &self.weights.final_norm,
9486 self.rms_eps,
9487 self.norm_style,
9488 );
9489 self.lm_head_forward(&normed)
9493 }
9494 };
9495 let target = ids[pos + 1] as usize;
9496 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9497 let lse: f64 = logits
9498 .iter()
9499 .map(|&v| ((v - max) as f64).exp())
9500 .sum::<f64>()
9501 .ln()
9502 + max as f64;
9503 let tok_nll = lse - logits[target] as f64;
9504 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9505 let top = logits
9506 .iter()
9507 .enumerate()
9508 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9509 .map(|(i, _)| i)
9510 .unwrap_or(0);
9511 eprintln!(
9512 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9513 logits[target], logits[top]
9514 );
9515 }
9516 nll += tok_nll;
9517 cnt += 1;
9518 attention::recycle_buf(&mut logits);
9519 let phi = self.dyn_phi_ema.clone();
9521 if let Some(new_active) = router.step(&phi, pos) {
9522 let _ = self.set_active_skill(new_active);
9523 }
9524 }
9525 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9526 })();
9527
9528 let _ = self.set_active_skill(saved_active);
9531 self.dyn_router = Some(router);
9532 self.nll_end();
9533 result
9534 }
9535
9536 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9538 self.clear_sequence_state();
9539 let mut acc = vec![0f32; self.hidden_size];
9540 for (pos, &id) in ids.iter().enumerate() {
9541 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9542 for (a, v) in acc.iter_mut().zip(&h) {
9543 *a += v;
9544 }
9545 }
9546 let n = ids.len().max(1) as f32;
9547 for a in acc.iter_mut() {
9548 *a /= n;
9549 }
9550 self.clear_sequence_state();
9551 acc
9552 }
9553
9554 pub fn probe_phi_span(
9568 &mut self,
9569 ids: &[u32],
9570 layer: usize,
9571 span: std::ops::Range<usize>,
9572 ) -> Vec<f32> {
9573 #[cfg(target_os = "macos")]
9574 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9575 let end = span.end.min(ids.len());
9576 let start = span.start.min(end);
9577 let reset = |p: &mut Self| p.clear_sequence_state();
9578 reset(self);
9579 let mut acc = vec![0f32; self.hidden_size];
9580 for (pos, &id) in ids[..end].iter().enumerate() {
9581 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9582 if pos >= start {
9583 for (a, v) in acc.iter_mut().zip(&h) {
9584 *a += v;
9585 }
9586 }
9587 }
9588 let n = end - start;
9589 if n > 0 {
9590 let n = n as f32;
9591 for a in acc.iter_mut() {
9592 *a /= n;
9593 }
9594 }
9595 reset(self);
9596 acc
9597 }
9598
9599 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9606 #[cfg(target_os = "macos")]
9607 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9608 self.graph_logits = None;
9609 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9610 if let Some(logits) = self.graph_logits.take() {
9611 return logits;
9612 }
9613 inference::rms_norm_into(
9614 &hidden,
9615 &self.weights.final_norm,
9616 self.rms_eps,
9617 self.norm_style,
9618 &mut self.ws.n1,
9619 );
9620 self.lm_head_forward(&self.ws.n1)
9621 }
9622
9623 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9629 self.prefill_batch_masked(ids, start_pos, None)
9630 }
9631
9632 fn prefill_batch_masked(
9638 &mut self,
9639 ids: &[u32],
9640 start_pos: usize,
9641 task_mask: Option<&TaskMask>,
9642 ) -> Vec<f32> {
9643 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9644 }
9645
9646 fn prefill_rows(
9654 &mut self,
9655 ids: &[u32],
9656 pos: usize,
9657 task_mask: Option<&TaskMask>,
9658 ) -> Result<Vec<f32>, String> {
9659 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9660 }
9661
9662 fn prefill_input_rows(
9663 &mut self,
9664 input: PrefillIn<'_>,
9665 pos: usize,
9666 task_mask: Option<&TaskMask>,
9667 ) -> Result<Vec<f32>, String> {
9668 self.mimo_moe_prepare();
9669 let hs = self.hidden_size;
9670 let bk = match input {
9671 PrefillIn::Ids(ids) => ids.len(),
9672 PrefillIn::Hidden(rows) => rows.len() / hs,
9673 };
9674 #[cfg(not(target_os = "macos"))]
9675 if task_mask.is_none()
9676 && !self.o1_active()
9677 && bk > 1
9678 && (self.batch_prefix_prefill()
9679 || (self.verify_exact_moe
9680 && crate::gpu::enabled_here()
9681 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9682 {
9683 let mut hiddens = match input {
9684 PrefillIn::Hidden(rows) => rows.to_vec(),
9685 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9686 };
9687 let positions: Vec<usize> = (pos..pos + bk).collect();
9688 let mut run = 0usize;
9689 match self.try_batch_graph_wgpu_prefix(
9690 &mut hiddens,
9691 &positions,
9692 bk,
9693 None,
9694 Some(&mut run),
9695 ) {
9696 crate::gpu::BatchGraphOutcome::Completed => {
9697 let out = if run < self.num_layers {
9698 self.prefill_batch_span(
9699 PrefillIn::Hidden(&hiddens),
9700 pos,
9701 None,
9702 run,
9703 self.num_layers,
9704 )
9705 } else {
9706 hiddens
9707 };
9708 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9709 Err("MiMo attention graph failed after admission".into())
9710 } else { Ok(out) };
9711 }
9712 crate::gpu::BatchGraphOutcome::Failed => {
9713 return Err("batched prefix prefill failed after admission".into());
9714 }
9715 crate::gpu::BatchGraphOutcome::Declined => {
9716 #[cfg(feature = "gpu")]
9718 self.pull_lagging_host_kv(0, self.num_layers, pos);
9719 }
9720 }
9721 }
9722 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9723 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9724 Err("batch tail graph failed after admission".into())
9725 } else { Ok(out) }
9726 }
9727
9728 fn prefill_batch_span(
9734 &mut self,
9735 input: PrefillIn<'_>,
9736 start_pos: usize,
9737 task_mask: Option<&TaskMask>,
9738 from: usize,
9739 upto_excl: usize,
9740 ) -> Vec<f32> {
9741 let hs = self.hidden_size;
9742 let b = match input {
9743 PrefillIn::Ids(ids) => ids.len(),
9744 PrefillIn::Hidden(hb) => hb.len() / hs,
9745 };
9746 let upto_excl = upto_excl.min(self.num_layers);
9747 let mut h: Vec<f32>;
9751 let mut h_ready;
9752 match input {
9753 PrefillIn::Ids(_) => {
9754 h = vec![0.0; b * hs];
9755 h_ready = false;
9756 }
9757 PrefillIn::Hidden(hb) => {
9758 h = hb.to_vec();
9759 h_ready = true;
9760 }
9761 }
9762 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9763 if let PrefillIn::Ids(ids) = input {
9764 for (bi, &id) in ids.iter().enumerate() {
9765 let e = me.embed_single(id);
9766 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9767 }
9768 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9769 if let Ok(t) = tp.parse::<usize>() {
9770 if t >= start_pos && t < start_pos + ids.len() {
9771 let bi = t - start_pos;
9772 let row = &h[bi * hs..(bi + 1) * hs];
9773 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9774 eprintln!(
9775 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9776 ids[bi],
9777 row[0],
9778 row[1],
9779 ids.len(),
9780 &ids[..ids.len().min(8)]
9781 );
9782 }
9783 }
9784 }
9785 }
9786 };
9787 let (_nkv, _hd, _rd, eps) = (
9788 self.num_kv_heads,
9789 self.head_dim,
9790 self.rotary_dim,
9791 self.rms_eps,
9792 );
9793 let pool = self.pool.clone();
9794 let norm_style = self.norm_style;
9795 self.mimo_moe_prepare();
9796 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9797
9798 #[cfg(target_os = "macos")]
9799 let mut chunk_skip_until = 0usize;
9800 for li in from..upto_excl {
9801 let _capacity_tail = automatic_gpu_prefix
9802 .filter(|&prefix| {
9803 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9804 })
9805 .map(|_| crate::gpu::enter_cpu_scope());
9806 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9811 let _fast_gemm = self
9812 .proj_gate_sigmoid
9813 .then(crate::gpu::enter_prefill_fast_gemm);
9814 #[cfg(target_os = "macos")]
9820 if task_mask.is_none() {
9821 if li < chunk_skip_until {
9822 continue;
9823 }
9824 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9830 fill_h(&mut h, self);
9831 h_ready = true;
9832 }
9833 let ids_for_embed = match input {
9834 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9835 PrefillIn::Hidden(_) => None,
9836 };
9837 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9838 if end > li {
9839 h_ready = true;
9840 chunk_skip_until = end;
9841 if self.is_loop_end(end - 1) && end < self.num_layers {
9844 for bi in 0..b {
9845 let normed = inference::rms_norm(
9846 &h[bi * hs..(bi + 1) * hs],
9847 &self.weights.final_norm,
9848 eps,
9849 norm_style,
9850 );
9851 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9852 }
9853 }
9854 continue;
9855 }
9856 }
9857 if !h_ready {
9858 fill_h(&mut h, self);
9859 h_ready = true;
9860 }
9861 if task_mask.is_none() && self.verify_exact_moe {
9862 let positions: Vec<_> = (start_pos..start_pos + b).collect();
9863 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
9864 crate::gpu::BatchGraphOutcome::Completed => continue,
9865 crate::gpu::BatchGraphOutcome::Failed => return h,
9866 crate::gpu::BatchGraphOutcome::Declined => {},
9867 }
9868 }
9869 #[cfg(feature = "gpu")]
9870 self.pull_lagging_host_kv(li, li + 1, start_pos);
9871 let lw = &self.weights.layers[self.phys_layer(li)];
9872 let t_attn = std::time::Instant::now();
9873 match &lw.attn {
9875 AttnKind::Kda(w) => {
9876 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9878 let mut normed = vec![0.0f32; b * hs];
9879 for bi in 0..b {
9880 inference::rms_norm_into(
9881 &h[bi * hs..(bi + 1) * hs],
9882 &lw.input_norm,
9883 eps,
9884 norm_style,
9885 &mut normed[bi * hs..(bi + 1) * hs],
9886 );
9887 }
9888 let attn = crate::linear_core::kda_forward_batch(
9889 &normed,
9890 b,
9891 w,
9892 &cfg,
9893 &mut self.kv_cache.layers[li].linear_state,
9894 pool.as_deref(),
9895 );
9896 for (dst, &a) in h.iter_mut().zip(&attn) {
9897 *dst += a;
9898 }
9899 }
9900 AttnKind::LinearGdn(w) => {
9901 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9903 let mut normed = vec![0.0f32; b * hs];
9904 for bi in 0..b {
9905 let r = inference::rms_norm(
9906 &h[bi * hs..(bi + 1) * hs],
9907 &lw.input_norm,
9908 eps,
9909 norm_style,
9910 );
9911 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9912 }
9913 let attn = crate::linear_core::gdn_forward_batch(
9914 &normed,
9915 b,
9916 w,
9917 &cfg,
9918 &mut self.kv_cache.layers[li].linear_state,
9919 pool.as_deref(),
9920 );
9921 for (dst, &a) in h.iter_mut().zip(&attn) {
9922 *dst += a;
9923 }
9924 }
9925 AttnKind::ShortConv(w) => {
9926 let cfg = self
9929 .short_conv_cfg
9930 .expect("short-conv layer without short_conv_cfg");
9931 let mut normed = vec![0.0f32; b * hs];
9932 for bi in 0..b {
9933 inference::rms_norm_into(
9934 &h[bi * hs..(bi + 1) * hs],
9935 &lw.input_norm,
9936 eps,
9937 norm_style,
9938 &mut normed[bi * hs..(bi + 1) * hs],
9939 );
9940 }
9941 let attn = short_conv_forward_batch(
9942 &normed,
9943 b,
9944 w,
9945 &cfg,
9946 &mut self.kv_cache.layers[li].linear_state,
9947 pool.as_deref(),
9948 );
9949 for (dst, &a) in h.iter_mut().zip(&attn) {
9950 *dst += a;
9951 }
9952 }
9953 AttnKind::Mla(w) => {
9954 let inv_freq_l = self.layer_inv_freq(li);
9957 let rs = self.layer_rope_scale(li);
9958 let mut normed = vec![0.0f32; hs];
9959 for bi in 0..b {
9960 inference::rms_norm_into(
9961 &h[bi * hs..(bi + 1) * hs],
9962 &lw.input_norm,
9963 eps,
9964 norm_style,
9965 &mut normed,
9966 );
9967 let ao = mla_attention(
9968 w,
9969 &normed,
9970 &mut self.kv_cache.layers[li],
9971 start_pos + bi,
9972 &inv_freq_l,
9973 rs,
9974 eps,
9975 pool.as_deref(),
9976 );
9977 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
9978 *dst += a;
9979 }
9980 }
9981 }
9982 AttnKind::Full {
9983 wq,
9984 wk,
9985 wv,
9986 wo,
9987 q_norm,
9988 k_norm,
9989 output_gate,
9990 softplus_gate,
9991 bias,
9992 } => {
9993 let mut normed = vec![0.0f32; b * hs];
9997 for bi in 0..b {
9998 inference::rms_norm_into(
9999 &h[bi * hs..(bi + 1) * hs],
10000 &lw.input_norm,
10001 eps,
10002 norm_style,
10003 &mut normed[bi * hs..(bi + 1) * hs],
10004 );
10005 }
10006 let inv_freq_l = self.layer_inv_freq(li);
10007 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10008 let cfg = QwenAttnCfg {
10009 num_heads: self.layer_num_heads(li),
10010 num_kv_heads: nkv_l,
10011 head_dim: hd_l,
10012 hidden_size: hs,
10013 position: start_pos,
10014 inv_freq: &inv_freq_l,
10015 rotary_dim: rd_l,
10016 scale: self.attn_scale,
10017 softcap: self.attn_softcap,
10018 window: self.layer_window(li),
10019 v_norm: self.attn_v_norm,
10020 qk_norm_after_rope: self.qk_norm_after_rope,
10021 gate_sigmoid: self.proj_gate_sigmoid,
10022 q_norm: q_norm.as_deref(),
10023 k_norm: k_norm.as_deref(),
10024 output_gate: *output_gate,
10025 softplus_gate: softplus_gate
10026 .as_ref()
10027 .map(|(gate, per_head)| (gate, *per_head)),
10028 rope_scale: self.layer_rope_scale(li),
10029 bias: bias
10030 .as_ref()
10031 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10032 rms_eps: eps,
10033 norm_style,
10034 pool: pool.as_deref(),
10035 v_head_dim: self.layer_v_dim(li),
10036 };
10037 #[cfg(feature = "gpu")]
10038 let _mirror = self
10039 .prefill_mirror_target(li)
10040 .map(crate::gpu::enter_prefill_mirror);
10041 let mut attn = attention::qwen_attention_batch(
10042 &normed,
10043 b,
10044 wq,
10045 wk,
10046 wv,
10047 wo,
10048 &mut self.kv_cache.layers[li],
10049 &cfg,
10050 );
10051 if let Some(w) = &lw.attn_out_norm {
10052 for bi in 0..b {
10053 inference::rms_norm_into(
10054 &attn[bi * hs..(bi + 1) * hs],
10055 w,
10056 eps,
10057 norm_style,
10058 &mut normed[bi * hs..(bi + 1) * hs],
10059 );
10060 }
10061 attn.copy_from_slice(&normed);
10062 }
10063 for (dst, &a) in h.iter_mut().zip(&attn) {
10064 *dst += a;
10065 }
10066 }
10067 AttnKind::Bounded(w) => {
10068 let mut normed = vec![0.0f32; b * hs];
10071 for bi in 0..b {
10072 inference::rms_norm_into(
10073 &h[bi * hs..(bi + 1) * hs],
10074 &lw.input_norm,
10075 eps,
10076 norm_style,
10077 &mut normed[bi * hs..(bi + 1) * hs],
10078 );
10079 }
10080 let rope = self
10081 .bounded_rope
10082 .clone()
10083 .expect("bounded layer without an installed rotation table");
10084 let cfg = crate::bounded::BoundedAttnCfg {
10085 num_heads: self.num_heads,
10086 num_kv_heads: self.num_kv_heads,
10087 head_dim: self.head_dim,
10088 hidden_size: hs,
10089 scale: self.attn_scale,
10090 rope: &rope,
10091 pool: pool.as_deref(),
10092 };
10093 let mut attn = crate::bounded::bounded_attention_batch(
10094 &normed,
10095 b,
10096 w,
10097 &mut self.kv_cache.layers[li],
10098 &cfg,
10099 );
10100 if let Some(wn) = &lw.attn_out_norm {
10101 for bi in 0..b {
10102 inference::rms_norm_into(
10103 &attn[bi * hs..(bi + 1) * hs],
10104 wn,
10105 eps,
10106 norm_style,
10107 &mut normed[bi * hs..(bi + 1) * hs],
10108 );
10109 }
10110 attn.copy_from_slice(&normed);
10111 }
10112 for (dst, &a) in h.iter_mut().zip(&attn) {
10113 *dst += a;
10114 }
10115 attention::recycle_buf(&mut attn);
10116 }
10117 AttnKind::Linear(w) => {
10118 for bi in 0..b {
10119 let normed = inference::rms_norm(
10120 &h[bi * hs..(bi + 1) * hs],
10121 &lw.input_norm,
10122 eps,
10123 norm_style,
10124 );
10125 vmf_phase_forward(
10126 &normed,
10127 w,
10128 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10129 &mut self.kv_cache.layers[li].linear_state,
10130 pool.as_deref(),
10131 )
10132 .iter()
10133 .enumerate()
10134 .for_each(|(i, &a)| h[bi * hs + i] += a);
10135 }
10136 }
10137 }
10138
10139 let lw = &self.weights.layers[self.phys_layer(li)];
10141 let mut post = vec![0.0f32; b * hs];
10142 for bi in 0..b {
10143 let r =
10144 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10145 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10146 }
10147 let mask_row = task_mask
10150 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10151 .and_then(|m| m.ffn_masks.get(li))
10152 .map(|v| v.as_slice());
10153 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10154 let t_ffn = std::time::Instant::now();
10155 let mut ffn = match &lw.ffn {
10156 FfnKind::Dense(d) if !d.segs.is_empty() => {
10157 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10158 }
10159 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10160 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10161 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10162 }
10163 FfnKind::Moe(m) if self.verify_exact_moe => {
10164 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10165 }
10166 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10169 let before = m.stats.borrow().clone();
10170 let out = crate::gpu::cpu_scope(|| {
10171 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10172 });
10173 self.mimo_moe.prime(li, m, &before);
10174 out
10175 }
10176 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10177 FfnKind::DenseMoe(dm) => {
10180 let mut out = vec![0.0f32; b * hs];
10181 for bi in 0..b {
10182 let r = dense_moe_ffn(
10183 dm,
10184 &post[bi * hs..(bi + 1) * hs],
10185 &h[bi * hs..(bi + 1) * hs],
10186 eps,
10187 norm_style,
10188 pool.as_deref(),
10189 );
10190 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10191 }
10192 out
10193 }
10194 };
10195 if prefill_prof_on() {
10196 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10197 PREFILL_SPLIT[1].fetch_add(t_ffn.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
10198 }
10199 if let Some(w) = &lw.ffn_out_norm {
10200 for bi in 0..b {
10201 inference::rms_norm_into(
10202 &ffn[bi * hs..(bi + 1) * hs],
10203 w,
10204 eps,
10205 norm_style,
10206 &mut post[bi * hs..(bi + 1) * hs],
10207 );
10208 }
10209 ffn.copy_from_slice(&post);
10210 }
10211 for (dst, &f) in h.iter_mut().zip(&ffn) {
10212 *dst += f;
10213 }
10214 if let Some(sc) = lw.layer_scale {
10215 for v in h.iter_mut() {
10216 *v *= sc;
10217 }
10218 }
10219 if self.layer_dump.is_some() {
10221 for bi in 0..b {
10222 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10223 }
10224 }
10225 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10226 if let Ok(t) = tp.parse::<usize>() {
10227 if t >= start_pos && t < start_pos + b {
10228 let bi = t - start_pos;
10229 let row = &h[bi * hs..(bi + 1) * hs];
10230 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10231 eprintln!(
10232 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10233 row[0], row[1]
10234 );
10235 }
10236 }
10237 }
10238 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10242 let row = &h[(b - 1) * hs..b * hs];
10243 let rms =
10244 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10245 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10246 eprintln!(
10247 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10248 match &self.weights.layers[self.phys_layer(li)].attn {
10249 AttnKind::LinearGdn(_) => "gdn",
10250 AttnKind::Linear(_) => "vmf",
10251 AttnKind::ShortConv(_) => "conv",
10252 _ => "attn",
10253 },
10254 match &lw.ffn {
10255 FfnKind::Moe(_) => "moe",
10256 FfnKind::Dense(_) => "dense",
10257 FfnKind::DenseMoe(_) => "dense+moe",
10258 },
10259 );
10260 }
10261 if self.is_loop_end(li) && li + 1 < self.num_layers {
10263 for bi in 0..b {
10264 let normed = inference::rms_norm(
10265 &h[bi * hs..(bi + 1) * hs],
10266 &self.weights.final_norm,
10267 eps,
10268 norm_style,
10269 );
10270 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10271 }
10272 }
10273 if std::env::var("CMF_TRACE_H").is_ok() {
10274 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10275 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10276 eprintln!(
10277 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10278 lw.layer_scale
10279 );
10280 }
10281 }
10282 crate::gpu::set_layer(-1); if prefill_prof_on() {
10284 let ms = |a: &std::sync::atomic::AtomicU64| {
10285 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10286 };
10287 let sp = &attention::ATTN_SPLIT;
10288 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10289 eprintln!(
10290 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10291 host gate {:.1}], host loop {:.1}, attend {:.1} \
10292 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10293 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10294 ms(&PREFILL_SPLIT[0]),
10295 ms(&sp[0]),
10296 qkv_enq,
10297 qkv_wait,
10298 ms(&sp[6]),
10299 ms(&sp[1]),
10300 ms(&sp[2]),
10301 ms(&sp[4]),
10302 ms(&sp[5]),
10303 ca_wait,
10304 ms(&sp[3]),
10305 ms(&PREFILL_SPLIT[1]),
10306 );
10307 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10308 eprintln!(
10309 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10310 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10311 fallbacks (cumulative)",
10312 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10313 );
10314 }
10315 self.o1_progress();
10320 self.swa_trim_tails();
10322 h
10323 }
10324
10325 fn embed_single(&self, id: u32) -> Vec<f32> {
10327 let mut out = vec![0.0f32; self.hidden_size];
10328 if (id as usize) < self.weights.embed_tokens.rows() {
10329 self.weights.embed_tokens.row_f32(id as usize, &mut out);
10330 }
10331 if self.embed_multiplier != 1.0 {
10332 for v in out.iter_mut() {
10333 *v *= self.embed_multiplier;
10334 }
10335 }
10336 if self.dsv4.is_some()
10340 || self.dsv41.is_some()
10341 || self.qwen4_exp.is_some()
10342 {
10343 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10344 v[0] = id as f32;
10345 return v;
10346 }
10347 if let Some(b) = &self.g3n {
10350 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10351 }
10352 out
10353 }
10354
10355 #[cfg(target_os = "macos")]
10361 fn chunk_run_gpu(
10362 &mut self,
10363 li0: usize,
10364 h: &mut [f32],
10365 b: usize,
10366 pos0: usize,
10367 embed_ids: Option<&[u32]>,
10368 cap: usize,
10369 ) -> usize {
10370 let gemma4 = self.metal_gemma4()
10377 && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10378 if !crate::gpu::enabled_here()
10379 || std::env::var("CMF_GPU_CHUNK")
10380 .map(|v| v == "0")
10381 .unwrap_or(false)
10382 || b < 32
10383 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10386 || (self.global_attn.is_some() && !gemma4)
10387 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10389 || self.o1_active()
10392 || (self.attn_v_norm && !gemma4)
10393 || !self.metal_rope_scale_ok()
10394 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10395 {
10396 return li0;
10397 }
10398 let Some(model) = self.model.clone() else {
10399 return li0;
10400 };
10401 let (nh, nkv, hd, hs) = (
10402 self.num_heads,
10403 self.num_kv_heads,
10404 self.head_dim,
10405 self.hidden_size,
10406 );
10407 let loop_end = if self.loop_final_norm {
10411 ((li0 / self.physical_layers) + 1) * self.physical_layers
10412 } else {
10413 self.num_layers
10414 };
10415 let run_end = self.num_layers.min(loop_end).min(cap);
10416 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10421 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10422 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10423 let mut stored_at: Vec<usize> = Vec::new();
10424 for li in li0..run_end {
10425 let lw = &self.weights.layers[self.phys_layer(li)];
10426 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10427 && !gemma4
10428 {
10429 break;
10430 }
10431 let AttnKind::Full {
10432 wq,
10433 wk,
10434 wv,
10435 wo,
10436 q_norm,
10437 k_norm,
10438 output_gate: false,
10439 softplus_gate,
10440 bias,
10441 } = &lw.attn
10442 else {
10443 break;
10444 };
10445 let head_gate = match softplus_gate {
10447 None => None,
10448 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10449 Some((d, r, c)) if r == nh && c == hs => Some(d),
10450 _ => break,
10451 },
10452 Some(_) => break,
10453 };
10454 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10458 FfnKind::Dense(d)
10459 if (matches!(d.act, Act::Silu | Act::Gelu)
10460 || (gemma4 && d.act == Act::GeluTanh))
10461 && d.segs.is_empty() =>
10462 {
10463 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10464 }
10465 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10466 Some((router, experts, q8)) => {
10467 let e0 = &m.experts[0];
10468 let route = Box::new(move |lg: &[f32]| {
10469 let (idx, p, wsum) = moe_route(lg, m, None);
10471 let mut st = m.stats.borrow_mut();
10472 if st.len() < m.experts.len() {
10473 st.resize(m.experts.len(), 0);
10474 }
10475 for &e in &idx {
10476 st[e] += 1;
10477 }
10478 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10479 (idx, w)
10480 });
10481 (
10482 &e0.gate_proj,
10483 &e0.up_proj,
10484 &e0.down_proj,
10485 Act::Silu,
10486 Some(crate::gpu_metal::ChunkMoe {
10487 router,
10488 experts,
10489 q8,
10490 route,
10491 }),
10492 )
10493 }
10494 None => break,
10495 },
10496 _ => break,
10497 };
10498 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10504 t.q8_row_parts()
10505 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10506 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10507 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10508 }
10509 fn cf(t: &QTensor) -> &[f32] {
10510 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10511 }
10512 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10513 if moe.is_none()
10522 && !gemma4
10523 && (col.iter().any(|c| !c.is_empty())
10524 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10525 {
10526 break;
10527 }
10528 let parts = (
10529 cw(wq),
10530 cw(wk),
10531 cw(wv),
10532 cw(wo),
10533 cw(gate_t),
10534 cw(up_t),
10535 cw(down_t),
10536 );
10537 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10538 else {
10539 break;
10540 };
10541 let layer = &self.kv_cache.layers[li];
10542 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10543 break;
10544 }
10545 debug_assert!(
10547 layer.base() == 0
10548 || self
10549 .layer_window(li)
10550 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10551 );
10552 stored_at.push(layer.head_len(0));
10553 let (nkv, hd) = if gemma4 {
10556 let (a, bb, _) = self.layer_geom(li);
10557 (a, bb)
10558 } else {
10559 (nkv, hd)
10560 };
10561 layers.push(crate::gpu_metal::ChunkLayer {
10562 model: &model,
10563 kv_id: self.graph_kv_id,
10564 layer: li,
10565 wq: pq,
10566 wk: pk,
10567 wv: pv,
10568 wo: po,
10569 gate: pg,
10570 up: pu,
10571 down: pd,
10572 input_norm: &lw.input_norm,
10573 post_norm: &lw.post_norm,
10574 bias: bias
10575 .as_ref()
10576 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10577 q_norm: q_norm.as_deref(),
10578 k_norm: k_norm.as_deref(),
10579 inv_freq: &tables[li - li0],
10580 rope_scale: self.layer_rope_scale(li),
10581 rd: self.layer_geom(li).2,
10582 nh,
10583 nkv,
10584 hd,
10585 hs,
10586 inter: gate_t.rows(),
10587 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10588 late_qk_norm: self.qk_norm_after_rope,
10589 eps: self.rms_eps as f32,
10590 window: self.layer_window(li),
10591 head_gate,
10592 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10593 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10594 v_norm: self.attn_v_norm,
10595 scale: self.attn_scale,
10596 attn_out_norm: lw.attn_out_norm.as_deref(),
10597 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10598 layer_scale: lw.layer_scale,
10599 }),
10600 col,
10601 moe,
10602 });
10603 }
10604 if layers.is_empty() {
10605 return li0;
10606 }
10607 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10609 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10610 .iter()
10611 .zip(&rows_kv)
10612 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10613 .collect();
10614 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10615 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10616 let li = layers[i].layer;
10617 let layer = &self.kv_cache.layers[li];
10618 let nkv = rows_kv[i].0;
10619 io.push(crate::gpu_metal::ChunkIo {
10620 cpu_stored: stored_at[i],
10621 cpu_gen: layer.generation(),
10622 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10623 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10624 out_k: ok,
10625 out_v: ov,
10626 imp: oi,
10627 });
10628 }
10629 let n_run = layers.len();
10630 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10631 let ep = embed_ids.and_then(|ids| {
10634 self.weights
10635 .embed_tokens
10636 .q8_row_parts()
10637 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10638 idx,
10639 rows,
10640 row_scale: rs,
10641 ids,
10642 mult: self.embed_multiplier,
10643 })
10644 });
10645 if embed_ids.is_some() && ep.is_none() {
10646 return li0;
10647 }
10648 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10649 return li0;
10650 }
10651 drop(io);
10652 drop(layers);
10653 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10656 let li = li0 + i;
10657 let row = rows_kv[i].1;
10658 let layer = &mut self.kv_cache.layers[li];
10659 for bi in 0..b {
10660 layer.append(
10661 &ok[bi * row..(bi + 1) * row],
10662 &ov[bi * row..(bi + 1) * row],
10663 &[],
10664 );
10665 }
10666 layer.accumulate_imp(oi);
10667 }
10668 last
10669 }
10670
10671 fn layer_is_local(&self, li: usize) -> bool {
10674 if let Some(layers) = &self.sliding_layers {
10675 return layers.get(li).copied().unwrap_or(false);
10676 }
10677 match self.swa {
10678 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10679 None => false,
10680 }
10681 }
10682
10683 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10686 if self.layer_is_local(li) {
10687 if let Some(f) = &self.inv_freq_local {
10688 return f.clone();
10689 }
10690 } else if let Some(f) = &self.inv_freq_global {
10691 return f.clone();
10692 }
10693 self.inv_freq.clone()
10694 }
10695
10696 fn layer_window(&self, li: usize) -> Option<usize> {
10698 self.swa
10699 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10700 }
10701
10702 fn swa_trim_tails(&mut self) {
10711 let Some((slack, align)) = self.swa_trim else {
10712 return;
10713 };
10714 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10715 return;
10716 }
10717 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10718 if let Some(w) = self.layer_window(li) {
10719 self.kv_cache.layers[li].trim_window(w, slack, align);
10720 }
10721 }
10722 }
10723
10724 fn layer_num_heads(&self, li: usize) -> usize {
10725 self.attention_heads_per_layer
10726 .as_ref()
10727 .and_then(|v| v.get(li).copied())
10728 .unwrap_or(self.num_heads)
10729 }
10730
10731 fn layer_rope_scale(&self, li: usize) -> f32 {
10732 if self.layer_is_local(li) {
10733 self.rope_scale_local
10734 } else {
10735 self.rope_scale
10736 }
10737 }
10738
10739 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10742 if !self.layer_is_local(li) {
10743 if let Some((ghd, gkv)) = self.global_attn {
10744 return (gkv, ghd, ghd);
10745 }
10746 }
10747 (
10748 self.layer_num_kv_heads(li),
10749 self.head_dim,
10750 if self.layer_is_local(li) {
10751 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10752 } else {
10753 self.rotary_dim
10754 },
10755 )
10756 }
10757
10758 fn layer_num_kv_heads(&self, li: usize) -> usize {
10761 self.kv_heads_per_layer
10762 .as_ref()
10763 .and_then(|v| v.get(self.phys_layer(li)).copied())
10764 .unwrap_or(self.num_kv_heads)
10765 }
10766
10767 fn layer_v_dim(&self, li: usize) -> usize {
10769 let (_, hd, _) = self.layer_geom(li);
10770 self.v_head_dim.unwrap_or(hd).min(hd)
10771 }
10772
10773 pub fn set_attn_geometry(
10782 &mut self,
10783 kv_heads_per_layer: Option<Vec<usize>>,
10784 v_head_dim: Option<usize>,
10785 ) -> Result<(), String> {
10786 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10787 if self.global_attn.is_some() {
10788 return Err(
10789 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10790 attention geometry"
10791 .into(),
10792 );
10793 }
10794 if self
10795 .weights
10796 .layers
10797 .iter()
10798 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10799 {
10800 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10801 }
10802 }
10803 if let Some(vd) = v_head_dim {
10804 if vd == 0 || vd > self.head_dim {
10805 return Err(format!(
10806 "v_head_dim {vd} must be in 1..={} (head_dim)",
10807 self.head_dim
10808 ));
10809 }
10810 }
10811 if let Some(v) = &kv_heads_per_layer {
10812 if v.len() != self.physical_layers {
10813 return Err(format!(
10814 "kv_heads_per_layer has {} entries, expected {} layers",
10815 v.len(),
10816 self.physical_layers
10817 ));
10818 }
10819 for (li, &nkv) in v.iter().enumerate() {
10820 let is_attn = matches!(
10821 self.weights.layers.get(li).map(|lw| &lw.attn),
10822 Some(AttnKind::Full { .. }) | None
10823 );
10824 if !is_attn {
10825 continue;
10826 }
10827 let nh = self
10828 .attention_heads_per_layer
10829 .as_ref()
10830 .and_then(|h| h.get(li).copied())
10831 .unwrap_or(self.num_heads);
10832 if nkv == 0 || nh % nkv != 0 {
10833 return Err(format!(
10834 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10835 ));
10836 }
10837 }
10838 }
10839 self.kv_heads_per_layer = kv_heads_per_layer;
10840 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10841 if self.kv_heads_per_layer.is_some() {
10842 for li in 0..self.kv_cache.layers.len() {
10843 let full = matches!(
10844 self.weights
10845 .layers
10846 .get(self.phys_layer(li))
10847 .map(|lw| &lw.attn),
10848 Some(AttnKind::Full { .. })
10849 );
10850 let nkv = self.layer_num_kv_heads(li);
10851 let cache = &self.kv_cache.layers[li];
10852 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
10853 let sinks = cache.sinks.clone();
10854 self.kv_cache.layers[li] =
10855 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
10856 self.kv_cache.layers[li].sinks = sinks;
10857 }
10858 }
10859 }
10860 Ok(())
10861 }
10862
10863 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
10867 let Some(lw) = self.weights.layers.get(phys) else {
10868 return Err(format!("sinks for layer {phys}: no such layer"));
10869 };
10870 if !matches!(lw.attn, AttnKind::Full { .. }) {
10871 return Err(format!(
10872 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
10873 ));
10874 }
10875 let nh = self
10876 .attention_heads_per_layer
10877 .as_ref()
10878 .and_then(|h| h.get(phys).copied())
10879 .unwrap_or(self.num_heads);
10880 if sinks.len() != nh {
10881 return Err(format!(
10882 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
10883 sinks.len()
10884 ));
10885 }
10886 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
10887 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
10888 }
10889 for li in 0..self.kv_cache.layers.len() {
10890 if self.phys_layer(li) == phys {
10891 self.kv_cache.layers[li].sinks = Some(sinks.clone());
10892 }
10893 }
10894 Ok(())
10895 }
10896
10897 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
10907 if self.kv_heads_per_layer.is_some() {
10908 return Some("per-layer KV head counts");
10909 }
10910 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
10911 return Some("V heads narrower than Q/K heads");
10912 }
10913 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
10914 return Some("learned attention sinks");
10915 }
10916 if self.swa.is_some() || self.sliding_layers.is_some() {
10917 return Some("sliding-window layers");
10918 }
10919 None
10920 }
10921
10922 #[cfg(target_os = "macos")]
10930 fn metal_rope_scale_ok(&self) -> bool {
10931 !(self.qk_norm_after_rope && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
10932 }
10933
10934 #[cfg(target_os = "macos")]
10948 fn metal_graph_swa(&self) -> bool {
10949 (self.swa.is_some() || self.sliding_layers.is_some())
10950 && self.attention_heads_per_layer.is_none()
10951 && !self.attn_v_norm
10952 && self.attn_softcap == 0.0
10953 && self.kv_heads_per_layer.is_none()
10954 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
10955 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10956 && self.global_attn.is_none()
10957 && self.inv_freq_global.is_none()
10958 && self.metal_rope_scale_ok()
10959 && !(0..self.num_layers).any(|li| {
10960 self.layer_is_local(li)
10961 && self.inv_freq_local.is_none()
10962 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10963 })
10964 }
10965
10966 #[cfg(target_os = "macos")]
10974 fn metal_gemma4(&self) -> bool {
10975 crate::gpu_metal::gemma4::enabled()
10976 && (self.global_attn.is_some() || self.attn_v_norm)
10977 && self.attention_heads_per_layer.is_none()
10978 && self.kv_heads_per_layer.is_none()
10979 && self.v_head_dim.is_none()
10980 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10981 && self.attn_softcap == 0.0
10982 && !self.qk_norm_after_rope
10983 && self.metal_rope_scale_ok()
10984 && self
10985 .weights
10986 .layers
10987 .iter()
10988 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
10989 && !(0..self.num_layers).any(|li| {
10990 self.layer_is_local(li)
10991 && self.inv_freq_local.is_none()
10992 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10993 })
10994 }
10995
10996 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11003 let geometry_reason = self.graph_attn_decline_reason();
11007 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11008 if has_scaled_rope && geometry_reason.is_none() {
11009 return Some("scaled RoPE without per-layer geometry");
11010 }
11011 if has_scaled_rope && self.qk_norm_after_rope {
11016 return Some("scaled RoPE with post-RoPE Q/K norm");
11017 }
11018 geometry_reason?;
11019 if self.attention_heads_per_layer.is_some() {
11025 return Some("per-layer Q head counts with per-layer geometry");
11026 }
11027 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11031 if !gemma4
11032 && self.weights.layers.iter().any(|lw| {
11033 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11034 })
11035 {
11036 return Some("sandwich norms / layer scale with per-layer geometry");
11037 }
11038 if self.weights.layers.iter().any(|lw| {
11042 matches!(
11043 &lw.attn,
11044 AttnKind::Full {
11045 output_gate: true,
11046 ..
11047 }
11048 )
11049 }) && self.v_head_dim.is_some()
11050 {
11051 return Some("gated attention with V narrower than K");
11052 }
11053 if (0..self.num_layers).any(|li| {
11054 self.layer_is_local(li)
11055 && self.inv_freq_local.is_none()
11056 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11057 }) {
11058 return Some("local rotary width without a local RoPE table");
11059 }
11060 None
11061 }
11062
11063 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11067 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11070 return None;
11071 }
11072 if self.global_attn.is_some() {
11073 return Some("per-layer head width (Gemma-4 global layers)");
11074 }
11075 if self.attn_v_norm {
11076 return Some("V norm");
11077 }
11078 if self.weights.layers.iter().any(|lw| {
11079 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11080 }) {
11081 return Some("sandwich norms / layer scale");
11082 }
11083 None
11084 }
11085
11086 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11091 self.graph_attn_decline_reason()?;
11092 let (nkv, hd, rd) = self.layer_geom(li);
11093 let invf: &[f32] = if self.layer_is_local(li) {
11094 match &self.inv_freq_local {
11095 Some(f) => f.as_slice(),
11096 None => self.inv_freq.as_slice(),
11097 }
11098 } else {
11099 match &self.inv_freq_global {
11100 Some(f) => f.as_slice(),
11101 None => self.inv_freq.as_slice(),
11102 }
11103 };
11104 Some(crate::gpu::GraphAttnGeom {
11105 nkv,
11106 hd,
11107 dv: self.layer_v_dim(li),
11108 rd,
11109 invf,
11110 rope_scale: self.layer_rope_scale(li),
11111 window: self.layer_window(li),
11112 sink: self.kv_cache.layers[li].sinks.as_deref(),
11113 v_norm: self.attn_v_norm,
11114 })
11115 }
11116
11117 #[cfg(feature = "gpu")]
11129 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11130 if !self.proj_gate_sigmoid
11131 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11132 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11133 || self.graph_refused()
11134 || self.o1_active()
11135 || self.attn_softcap > 0.0
11136 || self.wgpu_graph_attn_decline().is_some()
11137 {
11138 return None;
11139 }
11140 let g = self.graph_attn_geom(li)?;
11141 let (nkv, hd, _) = self.layer_geom(li);
11142 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11144 return None;
11145 }
11146 Some(crate::gpu::PrefillMirror {
11147 kv_id: self.graph_kv_id,
11148 layer: li,
11149 limit: self.kv_cache.max_seq_len,
11150 })
11151 }
11152
11153 #[cfg(feature = "gpu")]
11162 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11163 let kv_id = self.graph_kv_id;
11164 for li in from..upto.min(self.num_layers) {
11165 if !matches!(
11166 self.weights.layers[self.phys_layer(li)].attn,
11167 AttnKind::Full { .. }
11168 ) {
11169 continue;
11170 }
11171 let host = self.kv_cache.layers[li].pos_len();
11173 if host >= position {
11174 continue;
11175 }
11176 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11177 continue;
11178 };
11179 let to = dev.min(position);
11180 if to <= host {
11181 continue;
11182 }
11183 let (nkv, hd) = {
11184 let c = &self.kv_cache.layers[li];
11185 (c.num_kv_heads, c.head_dim)
11186 };
11187 let Some((k, v, first_valid)) =
11188 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11189 else {
11190 continue;
11191 };
11192 let need_from = match self.layer_window(li) {
11195 Some(w) => host.max((position + 1).saturating_sub(w)),
11196 None => host,
11197 };
11198 if first_valid > need_from {
11199 tracing::warn!(
11200 "layer {li}: device KV rows {host}..{to} no longer resident \
11201 (from {first_valid}); host attention will miss them"
11202 );
11203 }
11204 let row = nkv * hd;
11205 let cache = &mut self.kv_cache.layers[li];
11206 for p in 0..to - host {
11207 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11208 }
11209 }
11210 }
11211
11212 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11215 let mut seen = self.graph_declines.borrow_mut();
11216 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11217 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11218 seen.push((site, reason));
11219 }
11220 }
11221
11222 pub fn graph_declines(&self) -> Vec<String> {
11225 self.graph_declines
11226 .borrow()
11227 .iter()
11228 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11229 .collect()
11230 }
11231
11232 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11241 let Some(dir) = &self.layer_dump else {
11242 return;
11243 };
11244 let mut bytes = Vec::with_capacity(row.len() * 4);
11245 for v in row {
11246 bytes.extend_from_slice(&v.to_le_bytes());
11247 }
11248 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11249 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11250 use std::sync::atomic::{AtomicBool, Ordering};
11251 static SAID: AtomicBool = AtomicBool::new(false);
11252 if !SAID.swap(true, Ordering::Relaxed) {
11253 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11254 }
11255 }
11256 }
11257
11258 fn mimo_moe_prepare(&mut self) {
11261 if !self.mimo_moe.is_undecided() {
11262 return;
11263 }
11264 let slot = {
11265 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11266 .filter_map(
11267 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11268 FfnKind::Moe(m) => Some((li, m)),
11269 _ => None,
11270 },
11271 )
11272 .collect();
11273 if layers.is_empty()
11276 || self.physical_layers != self.num_layers
11277 || self.gpu_plan.is_some()
11278 {
11279 crate::mimo_moe::Slot::Off
11280 } else {
11281 let graph_prefix =
11285 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11286 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11287 }
11288 };
11289 self.mimo_moe = slot;
11290 }
11291
11292 #[cfg(test)]
11293 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11294 self.graph_kv_id
11295 }
11296
11297 pub(crate) fn mimo_graph_layer_rows(
11301 &mut self,
11302 li: usize,
11303 h: &mut [f32],
11304 positions: &[usize],
11305 ) -> crate::gpu::BatchGraphOutcome {
11306 use crate::gpu::BatchGraphOutcome as Out;
11307 let b = positions.len();
11308 if !(1..=4).contains(&b)
11309 || h.len() != b * self.hidden_size
11310 || !self.mimo_moe.is_dynamic(li, true)
11311 || !crate::gpu::enabled_here()
11312 || !crate::gpu::wgpu_active()
11313 || self.o1_active()
11314 || self.physical_layers != self.num_layers
11315 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11319 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11320 || self.wgpu_graph_attn_decline().is_some()
11321 {
11322 return Out::Declined;
11323 }
11324 let attn_started = std::time::Instant::now();
11325 let outcome = {
11326 let lw = &self.weights.layers[li];
11327 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11328 return Out::Declined;
11329 }
11330 let FfnKind::Moe(m) = &lw.ffn else {
11331 return Out::Declined;
11332 };
11333 let AttnKind::Full {
11334 wq,
11335 wk,
11336 wv,
11337 wo,
11338 q_norm,
11339 k_norm,
11340 output_gate,
11341 softplus_gate,
11342 bias,
11343 } = &lw.attn
11344 else {
11345 return Out::Declined;
11346 };
11347 if *output_gate || softplus_gate.is_some() {
11348 return Out::Declined;
11349 }
11350 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11351 m.experts
11352 .first()?
11353 .gate_proj
11354 .mapped_q4tp()
11355 .map(|(m, _)| m.clone())
11356 }) else {
11357 return Out::Declined;
11358 };
11359 fn gw<'a>(
11360 t: &'a QTensor,
11361 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11362 ) -> Option<crate::gpu::GraphW<'a>> {
11363 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11364 if m.uid() != owner.uid() || t.has_prism_contract() {
11365 return None;
11366 }
11367 return Some(crate::gpu::GraphW {
11368 idx,
11369 kind,
11370 row_scale: rs,
11371 data: &[],
11372 prism: crate::gpu::GraphPrismOp::None,
11373 affine: false,
11374 });
11375 }
11376 t.as_f32().map(|data| crate::gpu::GraphW {
11377 idx: 0,
11378 kind: 4,
11379 row_scale: &[],
11380 data,
11381 prism: crate::gpu::GraphPrismOp::None,
11382 affine: false,
11383 })
11384 }
11385 let (Some(q), Some(k), Some(v), Some(o)) = (
11386 gw(wq, &model),
11387 gw(wk, &model),
11388 gw(wv, &model),
11389 gw(wo, &model),
11390 ) else {
11391 return Out::Declined;
11392 };
11393 let layer = crate::gpu::GraphLayer {
11394 input_norm: &lw.input_norm,
11395 post_norm: &lw.post_norm,
11396 attn_out_norm: None,
11397 ffn_out_norm: None,
11398 layer_scale: None,
11399 ffn: crate::gpu::GraphFfn::AttentionOnly,
11400 attn: crate::gpu::GraphAttn::Full {
11401 wq: q,
11402 wk: k,
11403 wv: v,
11404 wo: o,
11405 q_norm: q_norm.as_deref(),
11406 k_norm: k_norm.as_deref(),
11407 late_qk_norm: self.qk_norm_after_rope,
11408 bias: bias
11409 .as_ref()
11410 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11411 output_gate: false,
11412 cpu_k: self.kv_cache.layers[li].k_heads(),
11413 cpu_v: self.kv_cache.layers[li].v_heads(),
11414 cpu_base: self.kv_cache.layers[li].base(),
11415 geom: self.graph_attn_geom(li),
11416 head_gate: None,
11417 },
11418 };
11419 let (nkv, hd, rd) = self.layer_geom(li);
11420 crate::gpu::forward_batch_graph_at(
11421 &model,
11422 self.graph_kv_id,
11423 li,
11424 &[layer],
11425 &self.inv_freq,
11426 h,
11427 self.layer_num_heads(li),
11428 nkv,
11429 hd,
11430 rd,
11431 self.hidden_size,
11432 1,
11433 positions,
11434 self.kv_cache.max_seq_len,
11435 self.norm_style == cortiq_core::NormStyle::Gemma,
11436 self.rms_eps as f32,
11437 self.attn_scale,
11438 b,
11439 &[],
11440 self.o1_epoch,
11441 None,
11442 None,
11443 )
11444 };
11445 match outcome {
11446 Out::Completed => {}
11447 Out::Declined => return Out::Declined,
11448 Out::Failed => {
11449 self.graph_failed
11450 .store(true, std::sync::atomic::Ordering::Relaxed);
11451 return Out::Failed;
11452 }
11453 }
11454 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11455 let hs = self.hidden_size;
11456 let lw = &self.weights.layers[li];
11457 let FfnKind::Moe(m) = &lw.ffn else {
11458 unreachable!()
11459 };
11460 let mut post = vec![0.0; h.len()];
11461 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11462 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11463 }
11464 let mut ffn = if b == 1 {
11465 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11466 } else {
11467 moe_ffn_banked_rows(
11468 &mut self.mimo_moe,
11469 li,
11470 m,
11471 &post,
11472 b,
11473 hs,
11474 self.pool.as_deref(),
11475 )
11476 };
11477 for (x, &f) in h.iter_mut().zip(&ffn) {
11478 *x += f;
11479 }
11480 attention::recycle_buf(&mut ffn);
11481 if self.layer_dump.is_some() {
11482 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11483 self.dump_layer_row(pos, li, row);
11484 }
11485 }
11486 crate::mimo_moe::note_attention_graph(b, attn_ns);
11487 Out::Completed
11488 }
11489
11490 fn layer_attn_plain(&self, li: usize) -> bool {
11491 self.kv_heads_per_layer.is_none()
11492 && self.v_head_dim.is_none()
11493 && self.global_attn.is_none()
11494 && self.layer_window(li).is_none()
11495 && self.kv_cache.layers[li].sinks.is_none()
11496 }
11497
11498 fn forward_layers(
11500 &mut self,
11501 hidden: &[f32],
11502 position: usize,
11503 task_mask: Option<&TaskMask>,
11504 ) -> Vec<f32> {
11505 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11506 self.o1_progress();
11507 self.swa_trim_tails();
11508 out
11509 }
11510
11511 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11519 self.embed_single(id)
11520 }
11521
11522 pub fn split_supported(&self) -> Result<(), String> {
11526 if self.dsv4.is_some() {
11527 return Err(
11528 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11529 );
11530 }
11531 if self.dsv41.is_some() {
11532 return Err(
11533 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11534 .into(),
11535 );
11536 }
11537 if self.qwen4_exp.is_some() {
11538 return Err(
11539 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11540 );
11541 }
11542 if self.g3n.is_some() {
11543 return Err(
11544 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11545 );
11546 }
11547 Ok(())
11548 }
11549
11550 pub fn forward_span(
11555 &mut self,
11556 hidden: &[f32],
11557 position: usize,
11558 from: usize,
11559 upto: usize,
11560 task_mask: Option<&TaskMask>,
11561 ) -> Result<Vec<f32>, String> {
11562 self.split_supported()?;
11563 if from > upto || upto >= self.num_layers {
11564 return Err(format!(
11565 "forward_span: layer range {from}..={upto} outside 0..{}",
11566 self.num_layers
11567 ));
11568 }
11569 if hidden.len() != self.hidden_size {
11570 return Err(format!(
11571 "forward_span: hidden len {} ≠ hidden_size {}",
11572 hidden.len(),
11573 self.hidden_size
11574 ));
11575 }
11576 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11577 self.o1_progress();
11578 self.swa_trim_tails();
11579 if self
11580 .graph_failed
11581 .swap(false, std::sync::atomic::Ordering::Relaxed)
11582 {
11583 self.cancel
11584 .store(false, std::sync::atomic::Ordering::Relaxed);
11585 self.clear_sequence_state();
11586 return Err("forward_span: deferred O(1) transition failed".into());
11587 }
11588 Ok(out)
11589 }
11590
11591 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11594 let normed = inference::rms_norm(
11595 hidden,
11596 &self.weights.final_norm,
11597 self.rms_eps,
11598 self.norm_style,
11599 );
11600 self.lm_head_forward(&normed)
11601 }
11602
11603 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11605 sampler::sample_with_scratch(
11606 logits,
11607 &self.sampler_config,
11608 past_tokens,
11609 &mut self.rng,
11610 &mut self.sampler_scratch,
11611 )
11612 }
11613
11614 pub fn reset_session(&mut self) {
11616 self.clear_sequence_state();
11617 }
11618
11619 pub fn prefill_span_ids(
11625 &mut self,
11626 ids: &[u32],
11627 start_pos: usize,
11628 upto: usize,
11629 task_mask: Option<&TaskMask>,
11630 ) -> Result<Vec<f32>, String> {
11631 self.split_supported()?;
11632 if upto >= self.num_layers {
11633 return Err(format!(
11634 "prefill_span_ids: upto {upto} outside 0..{}",
11635 self.num_layers
11636 ));
11637 }
11638 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11642 let out =
11643 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11644 self.check_o1_progress_failure("prefill_span_ids")?;
11645 Ok(out)
11646 } else {
11647 let hs = self.hidden_size;
11648 let mut out = Vec::with_capacity(ids.len() * hs);
11649 for (i, &id) in ids.iter().enumerate() {
11650 let emb = self.embed_id(id);
11651 out.extend_from_slice(&self.forward_span(
11652 &emb,
11653 start_pos + i,
11654 0,
11655 upto,
11656 task_mask,
11657 )?);
11658 }
11659 Ok(out)
11660 }
11661 }
11662
11663 pub fn prefill_span_hidden(
11666 &mut self,
11667 hidden: &[f32],
11668 start_pos: usize,
11669 from: usize,
11670 upto: usize,
11671 task_mask: Option<&TaskMask>,
11672 ) -> Result<Vec<f32>, String> {
11673 self.split_supported()?;
11674 let hs = self.hidden_size;
11675 if hidden.is_empty() || hidden.len() % hs != 0 {
11676 return Err(format!(
11677 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11678 hidden.len()
11679 ));
11680 }
11681 if from > upto || upto >= self.num_layers {
11682 return Err(format!(
11683 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11684 self.num_layers
11685 ));
11686 }
11687 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11688 let out = self.prefill_batch_span(
11689 PrefillIn::Hidden(hidden),
11690 start_pos,
11691 task_mask,
11692 from,
11693 upto + 1,
11694 );
11695 self.check_o1_progress_failure("prefill_span_hidden")?;
11696 Ok(out)
11697 } else {
11698 let b = hidden.len() / hs;
11699 let mut out = Vec::with_capacity(hidden.len());
11700 for i in 0..b {
11701 let h = self.forward_span(
11702 &hidden[i * hs..(i + 1) * hs],
11703 start_pos + i,
11704 from,
11705 upto,
11706 task_mask,
11707 )?;
11708 out.extend_from_slice(&h);
11709 }
11710 Ok(out)
11711 }
11712 }
11713
11714 fn try_token_graph_wgpu(
11718 &self,
11719 hidden: &[f32],
11720 position: usize,
11721 logits_out: &mut Vec<f32>,
11722 layers_run: &mut usize,
11723 ) -> Option<Result<Vec<f32>, ()>> {
11724 self.try_token_graph_wgpu_steps(
11725 hidden,
11726 position,
11727 logits_out,
11728 1,
11729 None,
11730 Some(layers_run),
11731 0,
11732 self.num_layers,
11733 )
11734 }
11735
11736 fn try_token_graph_wgpu_span(
11740 &self,
11741 hidden: &[f32],
11742 position: usize,
11743 logits_out: &mut Vec<f32>,
11744 from: usize,
11745 upto_excl: usize,
11746 layers_run: &mut usize,
11747 ) -> Option<Result<Vec<f32>, ()>> {
11748 self.try_token_graph_wgpu_steps(
11749 hidden,
11750 position,
11751 logits_out,
11752 1,
11753 None,
11754 Some(layers_run),
11755 from,
11756 upto_excl,
11757 )
11758 }
11759
11760 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11764 if self.o1_active() || self.attn_softcap > 0.0 {
11765 return None;
11766 }
11767 if let Some(reason) = self.wgpu_graph_attn_decline() {
11770 self.note_graph_decline("wgpu multi-burst", reason);
11771 return None;
11772 }
11773 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11774 if !graph_on || self.graph_refused() {
11775 return None;
11782 }
11783 let emb = self.embed_single(t_next);
11784 let mut lg = Vec::new();
11785 let mut ids = Vec::new();
11786 match self.try_token_graph_wgpu_steps(
11787 &emb,
11788 position,
11789 &mut lg,
11790 k,
11791 Some(&mut ids),
11792 None,
11793 0,
11794 self.num_layers,
11795 ) {
11796 Some(Ok(_)) => {}
11797 Some(Err(())) => {
11798 self.graph_failed
11803 .store(true, std::sync::atomic::Ordering::Relaxed);
11804 return None;
11805 }
11806 None => return None,
11807 }
11808 (ids.len() == k).then_some(ids)
11809 }
11810
11811 fn try_token_graph_wgpu_steps(
11815 &self,
11816 hidden: &[f32],
11817 position: usize,
11818 logits_out: &mut Vec<f32>,
11819 steps: usize,
11820 ids_out: Option<&mut Vec<u32>>,
11821 layers_run: Option<&mut usize>,
11822 from: usize,
11823 upto_excl: usize,
11824 ) -> Option<Result<Vec<f32>, ()>> {
11825 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11828 Some(end) if end < upto_excl => {
11829 if steps != 1 || layers_run.is_none() || from >= end {
11830 return None;
11831 }
11832 end
11833 }
11834 _ => upto_excl,
11835 };
11836 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11839 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11840 return None;
11844 }
11845 if let Some(reason) = self.wgpu_graph_attn_decline() {
11852 self.note_graph_decline("wgpu token graph", reason);
11853 return None;
11854 }
11855 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
11860 .map(|li| {
11861 if !o1_gpu {
11862 return None;
11863 }
11864 self.kv_cache.layers[self.phys_layer(li)].o1_views()
11865 })
11866 .collect();
11867 if self.o1_active() && o1_gpu {
11868 let want: usize = (from..upto_excl)
11871 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
11872 .count();
11873 let have = o1_views.iter().filter(|v| v.is_some()).count();
11874 if want == 0 || have != want {
11875 use std::sync::atomic::{AtomicUsize, Ordering};
11885 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
11886 let code = have * 1000 + want;
11887 if LAST.swap(code, Ordering::Relaxed) != code {
11888 tracing::warn!(
11889 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
11890 );
11891 }
11892 return None;
11893 }
11894 }
11895 let nh = self.num_heads;
11896 let (nkv, hd, rd) = self.layer_geom(0);
11897 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11898 let mut layers = Vec::with_capacity(upto_excl - from);
11899 let mut model = None;
11900 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
11901 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11902 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
11904 let name = &m.tensors[i].name;
11905 let prism = if crate::prism::is_inverse_embedding(m, name) {
11906 crate::gpu::GraphPrismOp::InverseEmbedding
11907 } else if crate::prism::is_forward_weight(m, name) {
11908 crate::gpu::GraphPrismOp::Forward
11909 } else {
11910 crate::gpu::GraphPrismOp::None
11911 };
11912 return Some(crate::gpu::GraphW {
11913 idx: i,
11914 kind,
11915 row_scale: rs,
11916 data: &[],
11917 prism,
11918 affine: crate::prism::is_affine_target(m, name),
11919 });
11920 }
11921 match t.as_f32() {
11923 Some(d) => Some(crate::gpu::GraphW {
11924 idx: 0,
11925 kind: 4,
11926 row_scale: &[],
11927 data: d,
11928 prism: crate::gpu::GraphPrismOp::None,
11929 affine: false,
11930 }),
11931 None => {
11932 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11933 eprintln!("batch graph: weight has no graph/f32 representation");
11934 }
11935 None
11936 }
11937 }
11938 }
11939 for li in from..upto_excl {
11940 let lw = &self.weights.layers[self.phys_layer(li)];
11941 if dbg {
11942 let ak = match &lw.attn {
11943 AttnKind::Mla(_) => "Mla".into(),
11944 AttnKind::Full {
11945 output_gate, bias, ..
11946 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11947 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11948 AttnKind::Kda(_) => "Kda".into(),
11949 AttnKind::Linear(_) => "Linear".into(),
11950 AttnKind::ShortConv(_) => "ShortConv".into(),
11951 AttnKind::Bounded(_) => "Bounded".into(),
11952 };
11953 let fk = match &lw.ffn {
11954 FfnKind::Dense(_) => "Dense",
11955 FfnKind::Moe(_) => "Moe",
11956 FfnKind::DenseMoe(_) => "DenseMoe",
11957 };
11958 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11959 }
11960 let gffn = match &lw.ffn {
11961 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11965 FfnKind::Dense(d) => {
11966 let Some(act) = d.act.graph_act() else {
11970 self.note_graph_decline(
11971 "wgpu token graph",
11972 "dense FFN activation without a graph kernel",
11973 );
11974 return None;
11975 };
11976 crate::gpu::GraphFfn::Dense {
11977 gate: gw(&d.gate_proj)?,
11978 up: gw(&d.up_proj)?,
11979 down: gw(&d.down_proj)?,
11980 act,
11981 }
11982 }
11983 FfnKind::Moe(m) => {
11984 if m.route_tau.is_some() || m.mask.is_some() {
11992 return None;
11993 }
11994 let shared = m.shared.as_ref();
11995 let has_shared = shared.is_some();
11996 let shared_gated = matches!(shared, Some((_, Some(_))));
11997 let sgate = match shared {
11998 Some((_, Some(sg))) => gw(sg)?,
11999 _ => gw(&m.router)?,
12003 };
12004 let router = gw(&m.router)?;
12005 if router.prism != crate::gpu::GraphPrismOp::None
12011 || sgate.prism != crate::gpu::GraphPrismOp::None
12012 || router.affine
12013 || sgate.affine
12014 {
12015 tracing::warn!(
12016 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
12017 );
12018 return None;
12019 }
12020 let inter = m.experts.first()?.gate_proj.rows();
12021 let mut experts = Vec::with_capacity(m.experts.len() + 1);
12022 let mut q4tp: Option<bool> = None;
12025 let mut gu_q2: Option<bool> = None;
12028 let mut q82: Option<bool> = None;
12030 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
12031 if !matches!(e.act, Act::Silu)
12032 || e.gate_proj.rows() != inter
12033 || e.up_proj.rows() != inter
12034 {
12035 return None;
12036 }
12037 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12042 let Some((em, ei, _, _)) = expert_weight
12043 .graph_weight()
12044 .or_else(|| expert_weight.graph_weight_descriptor())
12045 else {
12046 return None;
12047 };
12048 let name = &em.tensors[ei].name;
12049 if crate::prism::is_forward_weight(em, name)
12050 || crate::prism::is_inverse_embedding(em, name)
12051 || crate::prism::is_affine_target(em, name)
12052 {
12053 tracing::warn!(
12054 "resident MoE declined: expert Prism/affine transform is not implemented"
12055 );
12056 return None;
12057 }
12058 }
12059 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12060 Some((mm, gi)) => (
12061 mm,
12062 gi,
12063 e.up_proj.mapped_q4t()?.1,
12064 e.down_proj.mapped_q4t()?.1,
12065 false,
12066 false,
12067 false,
12068 ),
12069 None => match e.gate_proj.mapped_q2tp() {
12070 Some((mm, gi)) => (
12071 mm,
12072 gi,
12073 e.up_proj.mapped_q2tp()?.1,
12074 e.down_proj.mapped_q4tp()?.1,
12075 true,
12076 true,
12077 false,
12078 ),
12079 None => match e.gate_proj.mapped_q4tp() {
12080 Some((mm, gi)) => (
12081 mm,
12082 gi,
12083 e.up_proj.mapped_q4tp()?.1,
12084 e.down_proj.mapped_q4tp()?.1,
12085 true,
12086 false,
12087 false,
12088 ),
12089 None => {
12092 if !crate::gpu::moe_q82_graph_on() {
12093 return None;
12094 }
12095 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12096 (
12097 mm,
12098 gi,
12099 e.up_proj.mapped_q8_2f()?.1,
12100 e.down_proj.mapped_q8_2f()?.1,
12101 false,
12102 false,
12103 true,
12104 )
12105 }
12106 },
12107 },
12108 };
12109 if *q4tp.get_or_insert(is_p) != is_p
12110 || *gu_q2.get_or_insert(is_q2) != is_q2
12111 || *q82.get_or_insert(is_8) != is_8
12112 {
12113 tracing::warn!(
12119 "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."
12120 );
12121 return None;
12122 }
12123 model.get_or_insert_with(|| mm.clone());
12124 experts.push((gi, ui, di));
12125 }
12126 crate::gpu::GraphFfn::Moe {
12127 router,
12128 shared_gate: sgate,
12129 experts,
12130 n_exp: m.experts.len(),
12131 top_k: std::env::var("CMF_TOPK_PROBE")
12137 .ok()
12138 .and_then(|v| v.parse::<usize>().ok())
12139 .filter(|k| *k > 0 && *k <= m.top_k)
12140 .unwrap_or(m.top_k),
12141 inter,
12142 norm_topk: m.norm_topk_prob,
12143 q4tp: q4tp?,
12144 gu_q2: gu_q2.unwrap_or(false),
12145 q82: q82.unwrap_or(false),
12146 sigmoid: m.router_sigmoid,
12147 bias: m.expert_bias.as_deref(),
12148 has_shared,
12149 shared_gated,
12150 route_scale: m.routed_scaling,
12151 }
12152 }
12153 };
12154 let attn = match &lw.attn {
12155 AttnKind::Full {
12156 wq,
12157 wk,
12158 wv,
12159 wo,
12160 q_norm,
12161 k_norm,
12162 output_gate,
12163 softplus_gate,
12164 bias,
12165 } => {
12166 if self.attention_heads_per_layer.is_some() {
12167 return None;
12168 }
12169 let head_gate = match softplus_gate {
12173 None => None,
12174 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
12175 Some(gw(g)?)
12176 }
12177 Some(_) => {
12178 self.note_graph_decline(
12179 "wgpu token graph",
12180 "projected softplus / per-element output gate",
12181 );
12182 return None;
12183 }
12184 };
12185 let (m, _, _, _) = wq.graph_weight_or_blr()?;
12186 model = Some(m.clone());
12187 crate::gpu::GraphAttn::Full {
12188 wq: gw(wq)?,
12189 wk: gw(wk)?,
12190 wv: gw(wv)?,
12191 wo: gw(wo)?,
12192 q_norm: q_norm.as_deref(),
12193 k_norm: k_norm.as_deref(),
12194 late_qk_norm: self.qk_norm_after_rope,
12195 bias: bias
12196 .as_ref()
12197 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12198 output_gate: *output_gate,
12199 cpu_k: self.kv_cache.layers[li].k_heads(),
12200 cpu_v: self.kv_cache.layers[li].v_heads(),
12201 cpu_base: self.kv_cache.layers[li].base(),
12202 geom: self.graph_attn_geom(li),
12203 head_gate,
12204 }
12205 }
12206 AttnKind::LinearGdn(w) => {
12207 let cfg = self.gdn_cfg?;
12208 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
12209 model = Some(m.clone());
12210 crate::gpu::GraphAttn::Gdn {
12211 qkv: gw(&w.in_proj_qkv)?,
12212 z: gw(&w.in_proj_z)?,
12213 a: gw(&w.in_proj_a)?,
12214 b: gw(&w.in_proj_b)?,
12215 out: gw(&w.out_proj)?,
12216 conv1d: &w.conv1d,
12217 a_log: &w.a_log,
12218 dt_bias: &w.dt_bias,
12219 norm: &w.norm,
12220 nv: cfg.num_v_heads,
12221 nk: cfg.num_k_heads,
12222 dk: cfg.key_head_dim,
12223 dv: cfg.value_head_dim,
12224 kk: cfg.conv_kernel,
12225 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12226 }
12227 }
12228 AttnKind::ShortConv(w) => {
12229 let cfg = self.short_conv_cfg?;
12230 let (m, _, _, _) = w
12231 .in_proj
12232 .graph_weight()
12233 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12234 model = Some(m.clone());
12235 crate::gpu::GraphAttn::ShortConv {
12236 inp: gw(&w.in_proj)?,
12237 out: gw(&w.out_proj)?,
12238 taps: &w.conv,
12239 kernel: cfg.kernel,
12240 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12241 }
12242 }
12243 _ => return None,
12244 };
12245 layers.push(crate::gpu::GraphLayer {
12246 input_norm: &lw.input_norm,
12247 attn,
12248 post_norm: &lw.post_norm,
12249 attn_out_norm: lw.attn_out_norm.as_deref(),
12250 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12251 layer_scale: lw.layer_scale,
12252 ffn: gffn,
12253 });
12254 }
12255 let model = model?;
12256 let lm_gw = if upto_excl == self.num_layers
12262 && self.graph_want_logits
12263 && std::env::var("CMF_GPU_LMHEAD")
12264 .map(|v| v != "0")
12265 .unwrap_or(true)
12266 {
12267 self.weights
12268 .lm_head
12269 .graph_weight()
12270 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12271 .map(|(m, i, kind, rs)| {
12272 let name = &m.tensors[i].name;
12273 let prism = if crate::prism::is_inverse_embedding(m, name) {
12274 crate::gpu::GraphPrismOp::InverseEmbedding
12275 } else if crate::prism::is_forward_weight(m, name) {
12276 crate::gpu::GraphPrismOp::Forward
12277 } else {
12278 crate::gpu::GraphPrismOp::None
12279 };
12280 (
12281 crate::gpu::GraphW {
12282 idx: i,
12283 kind,
12284 row_scale: rs,
12285 data: &[],
12286 prism,
12287 affine: crate::prism::is_affine_target(m, name),
12288 },
12289 self.weights.lm_head.rows(),
12290 )
12291 })
12292 } else {
12293 None
12294 };
12295 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12296 let emb_gw = if steps > 1 {
12298 self.weights
12299 .embed_tokens
12300 .graph_weight()
12301 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12302 .map(|(m, i, kind, rs)| {
12303 let name = &m.tensors[i].name;
12304 let prism = if crate::prism::is_inverse_embedding(m, name) {
12305 crate::gpu::GraphPrismOp::InverseEmbedding
12306 } else if crate::prism::is_forward_weight(m, name) {
12307 crate::gpu::GraphPrismOp::Forward
12308 } else {
12309 crate::gpu::GraphPrismOp::None
12310 };
12311 (
12312 crate::gpu::GraphW {
12313 idx: i,
12314 kind,
12315 row_scale: rs,
12316 data: &[],
12317 prism,
12318 affine: crate::prism::is_affine_target(m, name),
12319 },
12320 self.weights.embed_tokens.rows(),
12321 self.embed_multiplier,
12322 )
12323 })
12324 } else {
12325 None
12326 };
12327
12328 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12334 (from..upto_excl.min(self.num_layers - 1))
12335 .filter(|&li| (li + 1) % self.physical_layers == 0)
12336 .map(|li| li - from)
12337 .collect()
12338 } else {
12339 Vec::new()
12340 };
12341 let mut h = hidden.to_vec();
12342 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12348 let outcome = crate::gpu::forward_token_graph(
12349 &model,
12350 self.graph_kv_id,
12351 &layers,
12352 &o1_views,
12353 self.o1_epoch,
12354 &self.inv_freq,
12355 &mut h,
12356 nh,
12357 nkv,
12358 hd,
12359 self.attn_scale,
12360 rd,
12361 self.hidden_size,
12362 self.intermediate_size,
12363 position,
12364 self.kv_cache.max_seq_len,
12365 gemma,
12366 self.rms_eps as f32,
12367 lm,
12368 &self.weights.final_norm,
12369 logits_out,
12370 &loop_norm_at,
12371 steps,
12372 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12373 ids_out,
12374 layers_run,
12375 from,
12376 dump_hidden,
12377 );
12378 match outcome {
12379 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12380 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12381 crate::gpu::TokenGraphOutcome::Declined => None,
12382 }
12383 }
12384
12385 #[cfg(target_os = "macos")]
12394 #[allow(clippy::type_complexity)]
12395 fn metal_rows_plan(
12396 &self,
12397 ) -> Option<(
12398 Vec<MetalRowsItem<'_>>,
12399 std::sync::Arc<cortiq_core::CmfModel>,
12400 Option<crate::gpu_metal::GdnGpuCfg>,
12401 )> {
12402 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12403 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12404 if !graph_force
12405 || !crate::gpu::enabled_here()
12406 || std::env::var("CMF_GPU_BLOCK")
12407 .map(|v| v == "0")
12408 .unwrap_or(false)
12409 || self.attn_softcap > 0.0
12410 || self.o1_active()
12411 || self.swa.is_some()
12412 || self.global_attn.is_some()
12413 || self.attention_heads_per_layer.is_some()
12414 || self.graph_attn_decline_reason().is_some()
12416 || self.attn_v_norm
12417 || self.loop_final_norm
12418 || !self.metal_rope_scale_ok()
12419 {
12420 return None;
12421 }
12422 let attend_contract = self.head_dim % 4 == 0
12423 && self.head_dim <= 256
12424 && self.rotary_dim >= 2
12425 && self.rotary_dim <= self.head_dim
12426 && (self.rotary_dim / 2) % 32 == 0
12427 && self.num_kv_heads > 0
12428 && self.num_heads % self.num_kv_heads == 0;
12429 if !attend_contract {
12430 return None;
12431 }
12432 let mut plan: Vec<MetalRowsItem> = Vec::new();
12433 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12434 for li in 0..self.num_layers {
12435 let lw = &self.weights.layers[self.phys_layer(li)];
12436 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12437 return None;
12438 }
12439 let ffn = match &lw.ffn {
12440 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12441 let (Some(g), Some(u), Some(dn)) = (
12442 d.gate_proj.metal_graph_parts(),
12443 d.up_proj.metal_graph_parts(),
12444 d.down_proj.metal_graph_parts(),
12445 ) else {
12446 return None;
12447 };
12448 MetalFfn::Dense {
12449 gate: g,
12450 up: u,
12451 down: dn,
12452 act: 0, }
12454 }
12455 _ => return None,
12456 };
12457 match &lw.attn {
12458 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12459 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12460 w.in_proj_qkv.metal_graph_parts(),
12461 w.in_proj_z.metal_graph_parts(),
12462 w.in_proj_a.f32_parts(),
12463 w.in_proj_b.f32_parts(),
12464 w.out_proj.metal_graph_parts(),
12465 ) else {
12466 return None;
12467 };
12468 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12469 model_ref.get_or_insert_with(|| model.clone());
12470 }
12471 let gl = GdnGpuLayer {
12472 attn_norm: &lw.input_norm,
12473 post_norm: &lw.post_norm,
12474 qkv,
12475 z,
12476 a,
12477 b: bb,
12478 out,
12479 ffn,
12480 conv1d: &w.conv1d,
12481 a_log: &w.a_log,
12482 dt_bias: &w.dt_bias,
12483 gnorm: &w.norm,
12484 };
12485 match plan.last_mut() {
12486 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12487 _ => plan.push(MetalRowsItem::Gdn {
12488 run: vec![gl],
12489 first: li,
12490 }),
12491 }
12492 }
12493 AttnKind::Full {
12494 wq,
12495 wk,
12496 wv,
12497 wo,
12498 q_norm,
12499 k_norm,
12500 output_gate,
12501 softplus_gate: None,
12502 bias: None,
12503 } => {
12504 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12505 (
12506 wq.metal_graph_parts(),
12507 wk.metal_graph_parts(),
12508 wv.metal_graph_parts(),
12509 wo.metal_graph_parts(),
12510 )
12511 else {
12512 return None;
12513 };
12514 if let QTensor::Mapped { model, .. } = wq {
12515 model_ref.get_or_insert_with(|| model.clone());
12516 }
12517 let cache = &self.kv_cache.layers[li];
12518 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12519 return None;
12520 }
12521 plan.push(MetalRowsItem::Attn {
12522 l: AttnGpuLayer {
12523 attn_norm: &lw.input_norm,
12524 post_norm: &lw.post_norm,
12525 wq: pq,
12526 wk: pk,
12527 wv: pv,
12528 wo: po,
12529 ffn,
12530 attn_out_norm: None,
12531 ffn_out_norm: None,
12532 layer_scale: None,
12533 },
12534 li,
12535 q_norm: q_norm.as_deref(),
12536 k_norm: k_norm.as_deref(),
12537 output_gate: *output_gate,
12538 });
12539 }
12540 _ => return None,
12541 }
12542 }
12543 let model = model_ref?;
12544 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12545 nv: cfg.num_v_heads,
12546 nk: cfg.num_k_heads,
12547 dk: cfg.key_head_dim,
12548 dv: cfg.value_head_dim,
12549 kk: cfg.conv_kernel,
12550 hidden: self.hidden_size,
12551 inter: self.intermediate_size,
12552 c_dim: cfg.conv_dim(),
12553 eps: cfg.rms_eps as f32,
12554 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12555 });
12556 Some((plan, model, gcfg))
12557 }
12558
12559 #[cfg(target_os = "macos")]
12561 #[allow(clippy::too_many_arguments)]
12562 fn metal_attn_params<'a>(
12563 li: usize,
12564 cache: &'a crate::kv_cache::LayerKvCache,
12565 q_norm: Option<&'a [f32]>,
12566 k_norm: Option<&'a [f32]>,
12567 output_gate: bool,
12568 inv_freq: &'a [f32],
12569 rope_scale: f32,
12570 geom: (usize, usize, usize, usize),
12571 pos0: usize,
12572 kv_id: u64,
12573 scale: f32,
12574 eps: f32,
12575 gemma: bool,
12576 late_qk_norm: bool,
12577 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12578 let (nh, nkv, hd, rd) = geom;
12579 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12580 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12581 let cpu_stored = cpu_k[0].len() / hd;
12582 (
12583 crate::gpu_metal::AttnDeviceParams {
12584 kv_id,
12585 layer: li,
12586 nh,
12587 nkv,
12588 hd,
12589 rd,
12590 position: pos0,
12591 scale,
12592 eps,
12593 gemma,
12594 late_qk_norm,
12595 output_gate,
12596 q_norm,
12597 k_norm,
12598 inv_freq,
12599 rope_scale,
12600 cpu_k,
12601 cpu_v,
12602 cpu_stored,
12603 cpu_gen: cache.generation(),
12604 o1: None,
12605 window: None,
12606 head_gate: None,
12607 v_norm: false,
12608 },
12609 cpu_stored,
12610 )
12611 }
12612
12613 #[cfg(target_os = "macos")]
12618 #[allow(clippy::type_complexity)]
12619 fn metal_rows_run(
12620 &mut self,
12621 hiddens: &mut [f32],
12622 pos0: usize,
12623 b: usize,
12624 prefill: bool,
12625 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12626 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12630 ) -> MetalRowsRun {
12631 use crate::gpu_metal::{GraphDims, VerifyGraph};
12632 if !crate::gpu_metal::wait_replay() {
12638 tracing::error!("Metal rows graph: the pending async replay failed");
12639 return MetalRowsRun::Failed;
12640 }
12641 spec_stamp("v.wait");
12642 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12648 if want > 0 {
12649 let phys = self.physical_layers.max(1);
12650 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12651 let is_gdn = self
12652 .weights
12653 .layers
12654 .get(li % phys)
12655 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12656 if is_gdn && l.linear_state.len() != want {
12657 l.linear_state = vec![0f32; want];
12658 }
12659 }
12660 }
12661 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12662 return MetalRowsRun::Declined;
12663 };
12664 spec_stamp("v.plan");
12665 let dims = GraphDims {
12666 hidden: self.hidden_size,
12667 eps: self.rms_eps as f32,
12668 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12669 };
12670 let Some(mut graph) = (if prefill {
12671 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12672 } else {
12673 VerifyGraph::new(&model, dims, hiddens, b)
12674 }) else {
12675 return MetalRowsRun::Declined;
12676 };
12677 let geom = (
12678 self.num_heads,
12679 self.num_kv_heads,
12680 self.head_dim,
12681 self.rotary_dim,
12682 );
12683 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12684 let eps = self.rms_eps as f32;
12685 let kv_id = self.graph_kv_id;
12686 let inv_freq = self.inv_freq.clone();
12687 for item in &plan {
12688 let ok = match item {
12689 MetalRowsItem::Gdn { run, .. } => gcfg
12690 .as_ref()
12691 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12692 .unwrap_or(false),
12693 MetalRowsItem::Attn {
12694 l,
12695 li,
12696 q_norm,
12697 k_norm,
12698 output_gate,
12699 } => {
12700 let (p, _) = Self::metal_attn_params(
12701 *li,
12702 &self.kv_cache.layers[*li],
12703 *q_norm,
12704 *k_norm,
12705 *output_gate,
12706 &inv_freq,
12707 self.layer_rope_scale(*li),
12708 geom,
12709 pos0,
12710 kv_id,
12711 self.attn_scale,
12712 eps,
12713 gemma,
12714 self.qk_norm_after_rope,
12715 );
12716 graph.attn_ok(l, &p)
12717 }
12718 };
12719 if !ok {
12720 use std::sync::atomic::{AtomicBool, Ordering};
12721 static SAID: AtomicBool = AtomicBool::new(false);
12722 if !SAID.swap(true, Ordering::Relaxed) {
12723 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12724 }
12725 return MetalRowsRun::Declined;
12726 }
12727 }
12728 let lm = match &spec {
12729 Some((lm, _, _)) => {
12730 if !graph.lm_head_ok(*lm) {
12731 return MetalRowsRun::Declined;
12732 }
12733 Some(*lm)
12734 }
12735 None => None,
12736 };
12737 let mut gdn_layers = Vec::new();
12738 let mut attn_layers = Vec::new();
12739 for item in &plan {
12740 match item {
12741 MetalRowsItem::Gdn { run, first } => {
12742 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12743 .iter()
12744 .map(|l| l.linear_state.as_slice())
12745 .collect();
12746 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12747 return MetalRowsRun::Declined;
12748 }
12749 gdn_layers.extend(*first..*first + run.len());
12750 }
12751 MetalRowsItem::Attn {
12752 l,
12753 li,
12754 q_norm,
12755 k_norm,
12756 output_gate,
12757 } => {
12758 let (p, cpu_stored) = Self::metal_attn_params(
12759 *li,
12760 &self.kv_cache.layers[*li],
12761 *q_norm,
12762 *k_norm,
12763 *output_gate,
12764 &inv_freq,
12765 self.layer_rope_scale(*li),
12766 geom,
12767 pos0,
12768 kv_id,
12769 self.attn_scale,
12770 eps,
12771 gemma,
12772 self.qk_norm_after_rope,
12773 );
12774 if !graph.encode_attn_b(l, &p) {
12775 return MetalRowsRun::Declined;
12776 }
12777 attn_layers.push((*li, cpu_stored));
12778 }
12779 }
12780 }
12781 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12782 if !graph.encode_lm_head_b(final_norm, lm) {
12783 return MetalRowsRun::Declined;
12784 }
12785 if let Some((n, _)) = argmax_out.as_ref() {
12790 if !graph.encode_argmax_b(*n) {
12791 argmax_out = None;
12792 }
12793 }
12794 }
12795 spec_stamp("v.enc");
12796 if !graph.sync() {
12797 return MetalRowsRun::Failed;
12798 }
12799 spec_stamp("v.gpu");
12800 match (spec, argmax_out) {
12801 (Some(_), Some((_, ids))) => {
12802 ids.resize(b, 0);
12803 if !graph.read_argmax(ids) {
12804 return MetalRowsRun::Failed;
12805 }
12806 spec_stamp("v.am");
12807 }
12808 (Some((lm, _, logits)), None) => {
12809 logits.resize(b * lm.1, 0.0);
12810 if !graph.read_logits(logits) {
12811 return MetalRowsRun::Failed;
12812 }
12813 spec_stamp("v.lg");
12814 }
12815 (None, _) => {}
12816 }
12817 if !graph.read_hidden(hiddens) {
12818 return MetalRowsRun::Failed;
12819 }
12820 spec_stamp("v.hid");
12821 MetalRowsRun::Completed(MetalVerifyPending {
12822 graph,
12823 gdn_layers,
12824 attn_layers,
12825 })
12826 }
12827
12828 #[cfg(target_os = "macos")]
12834 fn try_batch_graph_metal(
12835 &mut self,
12836 hiddens: &mut [f32],
12837 positions: &[usize],
12838 b: usize,
12839 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12840 argmax_out: Option<(usize, &mut Vec<u32>)>,
12841 ) -> crate::gpu::BatchGraphOutcome {
12842 let _t0 = std::time::Instant::now();
12843 if positions.len() != b
12844 || positions.windows(2).any(|w| w[1] != w[0] + 1)
12845 || hiddens.len() != b * self.hidden_size
12846 {
12847 return crate::gpu::BatchGraphOutcome::Declined;
12848 }
12849 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12850 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12851 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12852 MetalRowsRun::Completed(pending) => pending,
12853 };
12854 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12855 eprintln!(
12856 "metal-verify: {:.1} ms | b={b}",
12857 _t0.elapsed().as_secs_f64() * 1e3
12858 );
12859 }
12860 self.metal_verify = Some(pending);
12861 crate::gpu::BatchGraphOutcome::Completed
12862 }
12863
12864 #[cfg(target_os = "macos")]
12869 fn prefill_rows_metal(
12870 &mut self,
12871 ids: &[u32],
12872 start_pos: usize,
12873 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12874 ) -> MetalPrefillOutcome {
12875 let b = ids.len();
12876 if b == 0 || b > 512 {
12877 return MetalPrefillOutcome::Declined;
12878 }
12879 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12880 let with_head = spec.is_some();
12881 let hs = self.hidden_size;
12882 let mut hiddens = vec![0f32; b * hs];
12883 for (j, &id) in ids.iter().enumerate() {
12884 let e = self.embed_single(id);
12885 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12886 }
12887 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12888 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12889 MetalRowsRun::Failed => {
12890 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12891 return MetalPrefillOutcome::Failed;
12892 }
12893 MetalRowsRun::Completed(pending) => pending,
12894 };
12895 let idxs = pending.gdn_layers.clone();
12897 let mut outs: Vec<&mut [f32]> = self
12898 .kv_cache
12899 .layers
12900 .iter_mut()
12901 .enumerate()
12902 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12903 .map(|(_, l)| l.linear_state.as_mut_slice())
12904 .collect();
12905 if !pending.graph.finish_states(&mut outs) {
12906 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12907 return MetalPrefillOutcome::Failed;
12908 }
12909 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12910 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12914 for (li, cpu_stored) in &pending.attn_layers {
12915 let mut kbuf = vec![0f32; b * nkv * hd];
12916 let mut vbuf = vec![0f32; b * nkv * hd];
12917 if !crate::gpu_metal::kv_mirror_read_rows(
12918 self.graph_kv_id,
12919 *li,
12920 nkv,
12921 hd,
12922 *cpu_stored,
12923 b,
12924 &mut kbuf,
12925 &mut vbuf,
12926 ) {
12927 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12928 return MetalPrefillOutcome::Failed;
12929 }
12930 rows.push((*li, *cpu_stored, kbuf, vbuf));
12931 }
12932 for (li, cpu_stored, kbuf, vbuf) in rows {
12933 let cache = &mut self.kv_cache.layers[li];
12934 for r in 0..b {
12935 cache.append(
12936 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12937 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12938 &[],
12939 );
12940 }
12941 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12942 }
12943 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12944 if with_head {
12945 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12946 }
12947 MetalPrefillOutcome::Completed(hiddens)
12948 }
12949
12950 #[cfg(target_os = "macos")]
12951 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12952 self.prefill_rows_metal(ids, start_pos, None)
12953 }
12954
12955 #[cfg(target_os = "macos")]
12960 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12961 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12962 return MetalBatchNllOutcome::Declined;
12963 }
12964 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12965 return MetalBatchNllOutcome::Declined;
12966 };
12967 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12968 .ok()
12969 .and_then(|v| v.parse::<usize>().ok())
12970 .filter(|&v| (1..=512).contains(&v))
12971 .unwrap_or(32);
12972 let final_norm = self.weights.final_norm.clone();
12973 let mut nll = 0.0f64;
12974 let mut count = 0usize;
12975 let mut pos = 0usize;
12976 let mut completed = 0usize;
12977 while pos < ids.len() {
12978 let end = (pos + chunk).min(ids.len());
12979 let mut logits = Vec::new();
12980 let outcome = self.prefill_rows_metal(
12981 &ids[pos..end],
12982 pos,
12983 Some((lm, &final_norm, &mut logits)),
12984 );
12985 match outcome {
12986 MetalPrefillOutcome::Declined => {
12987 return if completed == 0 {
12988 MetalBatchNllOutcome::Declined
12989 } else {
12990 MetalBatchNllOutcome::Failed(format!(
12991 "ordinary Metal NLL batch declined after {completed} chunks"
12992 ))
12993 };
12994 }
12995 MetalPrefillOutcome::Failed => {
12996 return MetalBatchNllOutcome::Failed(
12997 "ordinary Metal NLL batch failed after admission".to_string(),
12998 );
12999 }
13000 MetalPrefillOutcome::Completed(_) => {}
13001 }
13002 completed += 1;
13003 let vocab = self.vocab_size.min(lm.1);
13004 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
13005 return MetalBatchNllOutcome::Failed(
13006 "ordinary Metal NLL head returned an invalid shape".to_string(),
13007 );
13008 }
13009 for row in 0..(end - pos) {
13010 let absolute = pos + row;
13011 if absolute < start || absolute + 1 >= ids.len() {
13012 continue;
13013 }
13014 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
13015 if let Some(mu) = self.logit_multiplier {
13016 for v in lg.iter_mut() {
13017 *v *= mu;
13018 }
13019 }
13020 if let Some(c) = self.final_softcap {
13021 softcap_in_place(lg, c, self.pool.as_deref());
13022 }
13023 let target = ids[absolute + 1] as usize;
13024 if target >= vocab {
13025 return MetalBatchNllOutcome::Failed(format!(
13026 "target token {target} exceeds Metal head rows {vocab}"
13027 ));
13028 }
13029 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13030 let lse: f64 = lg
13031 .iter()
13032 .map(|&v| ((v - max) as f64).exp())
13033 .sum::<f64>()
13034 .ln()
13035 + max as f64;
13036 nll += lse - lg[target] as f64;
13037 count += 1;
13038 }
13039 pos = end;
13040 }
13041 MetalBatchNllOutcome::Completed(nll, count)
13042 }
13043
13044 #[cfg(target_os = "macos")]
13048 fn metal_verify_commit(&mut self, a: usize) -> bool {
13049 let Some(mut pending) = self.metal_verify.take() else {
13050 return false;
13051 };
13052 let n = a + 1;
13053 let idxs = pending.gdn_layers.clone();
13055 let mut outs: Vec<&mut [f32]> = self
13056 .kv_cache
13057 .layers
13058 .iter_mut()
13059 .enumerate()
13060 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13061 .map(|(_, l)| l.linear_state.as_mut_slice())
13062 .collect();
13063 if !pending.graph.commit(n, &mut outs) {
13064 return false;
13065 }
13066 spec_stamp("c.replay");
13067 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13068 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13072 for (li, cpu_stored) in &pending.attn_layers {
13073 let mut kbuf = vec![0f32; n * nkv * hd];
13074 let mut vbuf = vec![0f32; n * nkv * hd];
13075 if !crate::gpu_metal::kv_mirror_read_rows(
13076 self.graph_kv_id,
13077 *li,
13078 nkv,
13079 hd,
13080 *cpu_stored,
13081 n,
13082 &mut kbuf,
13083 &mut vbuf,
13084 ) {
13085 return false;
13086 }
13087 rows.push((*li, *cpu_stored, kbuf, vbuf));
13088 }
13089 for (li, cpu_stored, kbuf, vbuf) in rows {
13090 let cache = &mut self.kv_cache.layers[li];
13091 for r in 0..n {
13092 cache.append(
13093 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13094 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13095 &[],
13096 );
13097 }
13098 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13099 }
13100 spec_stamp("c.kv");
13101 true
13102 }
13103
13104 #[cfg(target_os = "macos")]
13111 fn mtp_warm_batch_submit(
13112 &mut self,
13113 m: &mut MtpModule,
13114 pairs: &[(&[f32], u32)],
13115 first_pos: usize,
13116 ) -> Option<MetalWarmPending> {
13117 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13118 let b = pairs.len();
13119 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13120 return None;
13121 }
13122 let AttnKind::Full {
13123 wq,
13124 wk,
13125 wv,
13126 wo,
13127 q_norm,
13128 k_norm,
13129 output_gate,
13130 softplus_gate: None,
13131 bias: None,
13132 } = &m.layer.attn
13133 else {
13134 return None;
13135 };
13136 let FfnKind::Dense(d) = &m.layer.ffn else {
13137 return None;
13138 };
13139 if !d.segs.is_empty() {
13140 return None;
13141 }
13142 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13143 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13144 else {
13145 return None;
13146 };
13147 let (Some(g), Some(u), Some(dn)) = (
13148 d.gate_proj.q1_parts(),
13149 d.up_proj.q1_parts(),
13150 d.down_proj.q1_parts(),
13151 ) else {
13152 return None;
13153 };
13154 let Some(eh) = m.eh_proj.q1_parts() else {
13155 return None;
13156 };
13157 let QTensor::Mapped { model, .. } = wq else {
13158 return None;
13159 };
13160 let model = model.clone();
13161 let hs = self.hidden_size;
13162 let mut cat = vec![0f32; b * 2 * hs];
13164 for (j, (h, tok)) in pairs.iter().enumerate() {
13165 let e = self.embed_single(*tok);
13166 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13167 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13168 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13169 }
13170 let dims = GraphDims {
13171 hidden: hs,
13172 eps: self.rms_eps as f32,
13173 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13174 };
13175 spec_stamp("w.cat");
13176 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13177 return None;
13178 };
13179 spec_stamp("w.new");
13180 let l = AttnGpuLayer {
13181 attn_norm: &m.layer.input_norm,
13182 post_norm: &m.layer.post_norm,
13183 wq: pq,
13184 wk: pk,
13185 wv: pv,
13186 wo: po,
13187 ffn: MetalFfn::Dense {
13188 gate: g,
13189 up: u,
13190 down: dn,
13191 act: (d.act == Act::Gelu) as u8,
13192 },
13193 attn_out_norm: None,
13194 ffn_out_norm: None,
13195 layer_scale: None,
13196 };
13197 let (nh, nkv, hd, rd) = (
13198 self.num_heads,
13199 self.num_kv_heads,
13200 self.head_dim,
13201 self.rotary_dim,
13202 );
13203 let inv_freq = self.inv_freq.clone();
13204 let cpu_stored;
13205 {
13206 let cache = &m.kv;
13207 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13208 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13209 cpu_stored = cpu_k[0].len() / hd;
13210 if cpu_stored > first_pos {
13215 spec_stamp("w.decl");
13216 return None;
13217 }
13218 let p = AttnDeviceParams {
13219 kv_id: self.mtp_kv_id(),
13220 layer: Self::MTP_LAYER_BASE,
13221 nh,
13222 nkv,
13223 hd,
13224 rd,
13225 position: first_pos,
13226 scale: self.attn_scale,
13227 eps: self.rms_eps as f32,
13228 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13229 late_qk_norm: self.qk_norm_after_rope,
13230 output_gate: *output_gate,
13231 q_norm: q_norm.as_deref(),
13232 k_norm: k_norm.as_deref(),
13233 inv_freq: &inv_freq,
13234 rope_scale: self.rope_scale,
13235 cpu_k,
13236 cpu_v,
13237 cpu_stored,
13238 cpu_gen: cache.generation(),
13239 o1: None,
13240 window: None,
13241 head_gate: None,
13242 v_norm: false,
13243 };
13244 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13245 return None;
13246 }
13247 }
13248 spec_stamp("w.enc");
13249 if !graph.submit() {
13250 return None;
13251 }
13252 spec_stamp("w.sub");
13253 Some(MetalWarmPending {
13254 graph,
13255 cpu_stored,
13256 b,
13257 })
13258 }
13259
13260 #[cfg(target_os = "macos")]
13263 fn mtp_warm_batch_metal(
13264 &mut self,
13265 m: &mut MtpModule,
13266 pairs: &[(&[f32], u32)],
13267 first_pos: usize,
13268 ) -> bool {
13269 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13270 Some(p) => self.mtp_warm_batch_finish(m, p),
13271 None => false,
13272 }
13273 }
13274
13275 #[cfg(target_os = "macos")]
13280 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13281 let MetalWarmPending {
13282 mut graph,
13283 cpu_stored,
13284 b,
13285 } = pending;
13286 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13287 if !graph.sync() {
13288 return false;
13289 }
13290 spec_stamp("w.gpu");
13291 let mut kbuf = vec![0f32; b * nkv * hd];
13292 let mut vbuf = vec![0f32; b * nkv * hd];
13293 if !crate::gpu_metal::kv_mirror_read_rows(
13294 self.mtp_kv_id(),
13295 Self::MTP_LAYER_BASE,
13296 nkv,
13297 hd,
13298 cpu_stored,
13299 b,
13300 &mut kbuf,
13301 &mut vbuf,
13302 ) {
13303 return false;
13304 }
13305 for r in 0..b {
13306 m.kv.append(
13307 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13308 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13309 &[],
13310 );
13311 }
13312 crate::gpu_metal::kv_mirror_set_stored(
13313 self.mtp_kv_id(),
13314 Self::MTP_LAYER_BASE,
13315 cpu_stored + b,
13316 );
13317 spec_stamp("w.kv");
13318 true
13319 }
13320
13321 pub(crate) fn note_draft_id(&mut self, id: u32) {
13328 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13329 if (id as usize) >= cut {
13330 self.draft_full_streak = 16;
13331 } else {
13332 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13333 }
13334 }
13335
13336 fn draft_head_rows(&self, head_rows: usize) -> usize {
13339 if self.draft_full_streak > 0 {
13340 head_rows
13341 } else {
13342 Self::draft_vocab_rows(head_rows)
13343 }
13344 }
13345
13346 fn draft_vocab_rows(head_rows: usize) -> usize {
13349 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13350 let n = *N.get_or_init(|| {
13351 std::env::var("CMF_DRAFT_VOCAB")
13352 .ok()
13353 .and_then(|v| v.parse().ok())
13354 .unwrap_or(65536)
13355 });
13356 if n == 0 { head_rows } else { n.min(head_rows) }
13357 }
13358
13359 #[cfg(target_os = "macos")]
13364 fn mtp_step_metal(
13365 &mut self,
13366 m: &mut MtpModule,
13367 hidden: &[f32],
13368 next_token: u32,
13369 position: usize,
13370 want_logits: bool,
13371 ) -> Option<(Vec<f32>, Vec<f32>)> {
13372 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13373 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13374 || !crate::gpu::q1_force()
13375 || !crate::gpu::enabled_here()
13376 || self.attn_softcap > 0.0
13377 || self.attention_heads_per_layer.is_some()
13378 || m.kv.mode != crate::kv_cache::KvMode::F32
13379 || m.kv.o1.is_some()
13380 {
13381 return None;
13382 }
13383 let AttnKind::Full {
13384 wq,
13385 wk,
13386 wv,
13387 wo,
13388 q_norm,
13389 k_norm,
13390 output_gate,
13391 softplus_gate: None,
13392 bias: None,
13393 } = &m.layer.attn
13394 else {
13395 return None;
13396 };
13397 let FfnKind::Dense(d) = &m.layer.ffn else {
13398 return None;
13399 };
13400 if d.act != Act::Silu || !d.segs.is_empty() {
13401 return None;
13402 }
13403 let (pq, pk, pv, po) = (
13404 wq.q1_parts()?,
13405 wk.q1_parts()?,
13406 wv.q1_parts()?,
13407 wo.q1_parts()?,
13408 );
13409 let (g, u, dn) = (
13410 d.gate_proj.q1_parts()?,
13411 d.up_proj.q1_parts()?,
13412 d.down_proj.q1_parts()?,
13413 );
13414 let QTensor::Mapped { model, .. } = wq else {
13415 return None;
13416 };
13417 let model = model.clone();
13418 let lm = if want_logits {
13419 Some(self.weights.lm_head.q1_parts()?)
13420 } else {
13421 None
13422 };
13423 let dims = GraphDims {
13424 hidden: self.hidden_size,
13425 eps: self.rms_eps as f32,
13426 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13427 };
13428 let hs = self.hidden_size;
13431 let mut x = vec![0f32; hs];
13432 let mut graph = TokenGraph::new(&model, dims, &x)?;
13433 let mut folded = false;
13434 if let Some(eh) = m.eh_proj.q1_parts() {
13435 let e = self.embed_single(next_token);
13436 let mut cat = vec![0.0f32; 2 * hs];
13437 let (cat_e, cat_h) = cat.split_at_mut(hs);
13438 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13439 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13440 folded = graph.encode_input_proj(eh, &cat);
13441 }
13442 if !folded {
13443 x = self.mtp_block_input(m, hidden, next_token);
13444 graph = TokenGraph::new(&model, dims, &x)?;
13445 }
13446 spec_stamp("d.in");
13447 let l = AttnGpuLayer {
13448 attn_norm: &m.layer.input_norm,
13449 post_norm: &m.layer.post_norm,
13450 wq: pq,
13451 wk: pk,
13452 wv: pv,
13453 wo: po,
13454 ffn: MetalFfn::Dense {
13455 gate: g,
13456 up: u,
13457 down: dn,
13458 act: (d.act == Act::Gelu) as u8,
13459 },
13460 attn_out_norm: None,
13461 ffn_out_norm: None,
13462 layer_scale: None,
13463 };
13464 let (nh, nkv, hd, rd) = (
13465 self.num_heads,
13466 self.num_kv_heads,
13467 self.head_dim,
13468 self.rotary_dim,
13469 );
13470 let inv_freq = self.inv_freq.clone();
13471 {
13472 let cache = &m.kv;
13473 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13474 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13475 let cpu_stored = cpu_k[0].len() / hd;
13476 let p = AttnDeviceParams {
13477 kv_id: self.mtp_kv_id(),
13478 layer: Self::MTP_LAYER_BASE,
13479 nh,
13480 nkv,
13481 hd,
13482 rd,
13483 position,
13484 scale: self.attn_scale,
13485 eps: self.rms_eps as f32,
13486 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13487 late_qk_norm: self.qk_norm_after_rope,
13488 output_gate: *output_gate,
13489 q_norm: q_norm.as_deref(),
13490 k_norm: k_norm.as_deref(),
13491 inv_freq: &inv_freq,
13492 rope_scale: self.rope_scale,
13493 cpu_k,
13494 cpu_v,
13495 cpu_stored,
13496 cpu_gen: cache.generation(),
13497 o1: None,
13498 window: None,
13499 head_gate: None,
13500 v_norm: false,
13501 };
13502 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13503 return None;
13504 }
13505 }
13506 let draft_rows = if let Some(lm) = lm {
13512 self.draft_head_rows(lm.1)
13513 } else {
13514 0
13515 };
13516 if let Some(lm) = lm {
13517 if !graph.lm_head_ok(lm) {
13518 return None;
13519 }
13520 if draft_rows < lm.1 {
13521 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13522 return None;
13523 }
13524 } else {
13525 graph.encode_lm_head(&m.final_norm, lm);
13526 }
13527 }
13528 spec_stamp("d.enc");
13529 if graph.sync_checked().is_err() {
13530 return None;
13531 }
13532 spec_stamp("d.gpu");
13533 let mut logits = Vec::new();
13534 if let Some(lm) = lm {
13535 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13536 logits = attention::take_buf(n_read);
13537 graph.read_logits(&mut logits);
13538 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13540 }
13541 graph.finish(&mut x);
13542 let mut krow = attention::take_buf(nkv * hd);
13543 let mut vrow = attention::take_buf(nkv * hd);
13544 if crate::gpu_metal::kv_mirror_read_last(
13545 self.mtp_kv_id(),
13546 Self::MTP_LAYER_BASE,
13547 nkv,
13548 hd,
13549 &mut krow,
13550 &mut vrow,
13551 ) {
13552 m.kv.append(&krow, &vrow, &[]);
13553 }
13554 attention::recycle_buf(&mut krow);
13555 attention::recycle_buf(&mut vrow);
13556 spec_stamp("d.rd");
13557 Some((logits, x))
13558 }
13559
13560 fn mtp_chain_on() -> bool {
13573 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13574 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13575 }
13576
13577 #[cfg(target_os = "macos")]
13589 fn mtp_draft_chain_metal(
13590 &mut self,
13591 m: &mut MtpModule,
13592 hidden: &[f32],
13593 t_next: u32,
13594 position: usize,
13595 k: usize,
13596 ) -> Result<Vec<u32>, bool> {
13597 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13598 if k == 0
13599 || k > 64
13600 || !Self::mtp_chain_on()
13601 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13602 || !crate::gpu::q1_force()
13603 || !crate::gpu::enabled_here()
13604 || self.attn_softcap > 0.0
13605 || self.attention_heads_per_layer.is_some()
13606 || m.kv.mode != crate::kv_cache::KvMode::F32
13607 || m.kv.o1.is_some()
13608 || self.dsv4.is_some()
13610 || self.dsv41.is_some()
13611 || self.qwen4_exp.is_some()
13612 || self.g3n.is_some()
13613 {
13614 return Err(false);
13615 }
13616 let AttnKind::Full {
13617 wq,
13618 wk,
13619 wv,
13620 wo,
13621 q_norm,
13622 k_norm,
13623 output_gate,
13624 softplus_gate: None,
13625 bias: None,
13626 } = &m.layer.attn
13627 else {
13628 return Err(false);
13629 };
13630 let FfnKind::Dense(d) = &m.layer.ffn else {
13631 return Err(false);
13632 };
13633 if d.act != Act::Silu || !d.segs.is_empty() {
13634 return Err(false);
13635 }
13636 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13637 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13638 else {
13639 return Err(false);
13640 };
13641 let (Some(g), Some(u), Some(dn)) = (
13642 d.gate_proj.q1_parts(),
13643 d.up_proj.q1_parts(),
13644 d.down_proj.q1_parts(),
13645 ) else {
13646 return Err(false);
13647 };
13648 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13649 return Err(false);
13650 };
13651 let QTensor::Mapped { model, .. } = wq else {
13652 return Err(false);
13653 };
13654 let model = model.clone();
13655 let QTensor::Mapped {
13658 model: em,
13659 idx: eidx,
13660 dtype: cortiq_core::TensorDtype::Q4TiledP,
13661 ..
13662 } = &self.weights.embed_tokens
13663 else {
13664 return Err(false);
13665 };
13666 if !std::sync::Arc::ptr_eq(em, &model)
13667 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13668 {
13669 return Err(false);
13670 }
13671 let embed = (
13672 *eidx,
13673 self.weights.embed_tokens.rows(),
13674 self.weights.embed_tokens.cols(),
13675 );
13676 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13677 return Err(false);
13678 }
13679 let dims = GraphDims {
13680 hidden: self.hidden_size,
13681 eps: self.rms_eps as f32,
13682 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13683 };
13684 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13685 return Err(false);
13686 };
13687 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13688 return Err(false);
13689 }
13690 let l = AttnGpuLayer {
13691 attn_norm: &m.layer.input_norm,
13692 post_norm: &m.layer.post_norm,
13693 wq: pq,
13694 wk: pk,
13695 wv: pv,
13696 wo: po,
13697 ffn: MetalFfn::Dense {
13698 gate: g,
13699 up: u,
13700 down: dn,
13701 act: (d.act == Act::Gelu) as u8,
13702 },
13703 attn_out_norm: None,
13704 ffn_out_norm: None,
13705 layer_scale: None,
13706 };
13707 let (nh, nkv, hd, rd) = (
13708 self.num_heads,
13709 self.num_kv_heads,
13710 self.head_dim,
13711 self.rotary_dim,
13712 );
13713 let inv_freq = self.inv_freq.clone();
13714 let draft_rows = self.draft_head_rows(lm.1);
13715 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13716 if n_arg == 0 {
13717 return Err(false);
13718 }
13719 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13726 let t_chain = std::time::Instant::now();
13727 graph.chain_ids_init(t_next, k);
13728 let cpu_stored;
13729 {
13730 let cache = &m.kv;
13731 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13732 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13733 cpu_stored = cpu_k[0].len() / hd;
13734 for j in 0..k {
13735 if !graph.encode_chain_input(
13736 embed,
13737 j as u32,
13738 &m.enorm,
13739 &m.hnorm,
13740 self.embed_multiplier,
13741 eh,
13742 ) {
13743 return Err(false);
13744 }
13745 let p = AttnDeviceParams {
13749 kv_id: self.mtp_kv_id(),
13750 layer: Self::MTP_LAYER_BASE,
13751 nh,
13752 nkv,
13753 hd,
13754 rd,
13755 position: position + j,
13756 scale: self.attn_scale,
13757 eps: self.rms_eps as f32,
13758 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13759 late_qk_norm: self.qk_norm_after_rope,
13760 output_gate: *output_gate,
13761 q_norm: q_norm.as_deref(),
13762 k_norm: k_norm.as_deref(),
13763 inv_freq: &inv_freq,
13764 rope_scale: self.rope_scale,
13765 cpu_k: cpu_k.clone(),
13766 cpu_v: cpu_v.clone(),
13767 cpu_stored: cpu_stored + j,
13768 cpu_gen: cache.generation(),
13769 o1: None,
13770 window: None,
13771 head_gate: None,
13772 v_norm: false,
13773 };
13774 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13775 return Err(false);
13776 }
13777 if draft_rows < lm.1 {
13778 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13779 return Err(false);
13780 }
13781 } else {
13782 graph.encode_lm_head(&m.final_norm, lm);
13783 }
13784 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13785 return Err(false);
13786 }
13787 if split {
13788 graph.commit();
13791 }
13792 }
13793 }
13794 let t_enc = t_chain.elapsed();
13795 if graph.sync_checked().is_err() {
13796 return Err(true);
13797 }
13798 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
13799 eprintln!(
13800 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
13801 t_enc.as_secs_f64() * 1e3,
13802 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
13803 if split { ", split" } else { "" }
13804 );
13805 }
13806 let mut ids = vec![0u32; k];
13807 if !graph.chain_ids_read(&mut ids) {
13808 return Err(true);
13809 }
13810 let mut kbuf = vec![0f32; k * nkv * hd];
13811 let mut vbuf = vec![0f32; k * nkv * hd];
13812 if !crate::gpu_metal::kv_mirror_read_rows(
13813 self.mtp_kv_id(),
13814 Self::MTP_LAYER_BASE,
13815 nkv,
13816 hd,
13817 cpu_stored,
13818 k,
13819 &mut kbuf,
13820 &mut vbuf,
13821 ) {
13822 return Err(true);
13823 }
13824 for r in 0..k {
13825 m.kv.append(
13826 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13827 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13828 &[],
13829 );
13830 }
13831 Ok(ids)
13832 }
13833
13834 fn try_batch_graph_wgpu(
13835 &self,
13836 hiddens: &mut [f32],
13837 positions: &[usize],
13838 k: usize,
13839 spec: Option<crate::gpu::SpecTail<'_>>,
13840 ) -> crate::gpu::BatchGraphOutcome {
13841 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
13842 }
13843
13844 fn try_batch_graph_wgpu_prefix(
13849 &self,
13850 hiddens: &mut [f32],
13851 positions: &[usize],
13852 k: usize,
13853 spec: Option<crate::gpu::SpecTail<'_>>,
13854 layers_run: Option<&mut usize>,
13855 ) -> crate::gpu::BatchGraphOutcome {
13856 let graph_end = match self.mimo_moe.graph_prefix_end() {
13857 Some(end) if end < self.num_layers => {
13858 if layers_run.is_none() || spec.is_some() || end == 0 {
13859 return crate::gpu::BatchGraphOutcome::Declined;
13860 }
13861 end
13862 }
13863 _ => self.num_layers,
13864 };
13865 let _tb = std::time::Instant::now();
13866 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13867 if self.attn_softcap > 0.0 {
13868 return crate::gpu::BatchGraphOutcome::Declined; }
13870 if let Some(reason) = self
13873 .wgpu_graph_attn_decline()
13874 .or_else(|| self.batch_graph_gemma_decline())
13875 {
13876 self.note_graph_decline("wgpu batch graph", reason);
13877 return crate::gpu::BatchGraphOutcome::Declined;
13878 }
13879 let nh = self.num_heads;
13880 let (nkv, hd, rd) = self.layer_geom(0);
13881 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13882 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13883 if let Some((m, i, kind, rs)) = t
13884 .graph_weight()
13885 .or_else(|| t.graph_weight_descriptor())
13886 {
13887 let name = &m.tensors[i].name;
13888 let prism = if crate::prism::is_inverse_embedding(m, name) {
13889 crate::gpu::GraphPrismOp::InverseEmbedding
13890 } else if crate::prism::is_forward_weight(m, name) {
13891 crate::gpu::GraphPrismOp::Forward
13892 } else {
13893 crate::gpu::GraphPrismOp::None
13894 };
13895 return Some(crate::gpu::GraphW {
13896 idx: i,
13897 kind,
13898 row_scale: rs,
13899 data: &[],
13900 prism,
13901 affine: crate::prism::is_affine_target(m, name),
13902 });
13903 }
13904 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13905 eprintln!(
13906 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13907 t.rows(),
13908 t.cols()
13909 );
13910 }
13911 t.as_f32().map(|d| crate::gpu::GraphW {
13912 idx: 0,
13913 kind: 4,
13914 row_scale: &[],
13915 data: d,
13916 prism: crate::gpu::GraphPrismOp::None,
13917 affine: false,
13918 })
13919 }
13920 let built: Option<(
13921 Vec<crate::gpu::GraphLayer<'_>>,
13922 std::sync::Arc<cortiq_core::CmfModel>,
13923 )> = (|| {
13924 let mut layers = Vec::with_capacity(graph_end);
13925 let mut model = None;
13926 for li in 0..graph_end {
13927 let lw = &self.weights.layers[self.phys_layer(li)];
13928 let gffn = match &lw.ffn {
13935 FfnKind::Dense(d) if !d.segs.is_empty() => {
13936 if batch_debug {
13937 eprintln!("batch graph: dense segmented FFN at layer {li}");
13938 }
13939 return None;
13940 }
13941 FfnKind::Dense(d) => {
13942 let Some(act) = d.act.graph_act() else {
13943 if batch_debug {
13944 eprintln!(
13945 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13946 d.act
13947 );
13948 }
13949 return None;
13950 };
13951 crate::gpu::GraphFfn::Dense {
13952 gate: gw(&d.gate_proj)?,
13953 up: gw(&d.up_proj)?,
13954 down: gw(&d.down_proj)?,
13955 act,
13956 }
13957 }
13958 FfnKind::Moe(m) => {
13959 if m.route_tau.is_some() || m.mask.is_some() {
13966 return None;
13967 }
13968 let shared = m.shared.as_ref();
13972 let has_shared = shared.is_some();
13973 let shared_gated = matches!(shared, Some((_, Some(_))));
13974 let sgate = match shared {
13975 Some((_, Some(sg))) => gw(sg)?,
13976 _ => gw(&m.router)?,
13980 };
13981 let router = gw(&m.router)?;
13982 if router.prism != crate::gpu::GraphPrismOp::None
13988 || router.affine
13989 || sgate.prism != crate::gpu::GraphPrismOp::None
13990 || sgate.affine
13991 {
13992 return None;
13993 }
13994 let inter = m.experts.first()?.gate_proj.rows();
13995 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13996 let mut q4tp: Option<bool> = None;
13997 let mut gu_q2: Option<bool> = None;
13998 let mut q82: Option<bool> = None;
13999 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
14000 if !matches!(e.act, Act::Silu)
14001 || e.gate_proj.rows() != inter
14002 || e.up_proj.rows() != inter
14003 {
14004 return None;
14005 }
14006 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
14010 {
14011 Some((mm, gi)) => (
14012 mm,
14013 gi,
14014 e.up_proj.mapped_q4t()?.1,
14015 e.down_proj.mapped_q4t()?.1,
14016 false,
14017 false,
14018 false,
14019 ),
14020 None => match e.gate_proj.mapped_q2tp() {
14021 Some((mm, gi)) => (
14022 mm,
14023 gi,
14024 e.up_proj.mapped_q2tp()?.1,
14025 e.down_proj.mapped_q4tp()?.1,
14026 true,
14027 true,
14028 false,
14029 ),
14030 None => match e.gate_proj.mapped_q4tp() {
14031 Some((mm, gi)) => (
14032 mm,
14033 gi,
14034 e.up_proj.mapped_q4tp()?.1,
14035 e.down_proj.mapped_q4tp()?.1,
14036 true,
14037 false,
14038 false,
14039 ),
14040 None => {
14041 if !crate::gpu::moe_q82_graph_on() {
14042 return None;
14043 }
14044 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14045 (
14046 mm,
14047 gi,
14048 e.up_proj.mapped_q8_2f()?.1,
14049 e.down_proj.mapped_q8_2f()?.1,
14050 false,
14051 false,
14052 true,
14053 )
14054 }
14055 },
14056 },
14057 };
14058 if *q4tp.get_or_insert(is_p) != is_p
14059 || *gu_q2.get_or_insert(is_q2) != is_q2
14060 || *q82.get_or_insert(is_8) != is_8
14061 {
14062 return None;
14063 }
14064 if [gi, ui, di].into_iter().any(|idx| {
14065 mm.tensors
14066 .get(idx)
14067 .is_some_and(|t| {
14068 crate::prism::is_forward_weight(mm, &t.name)
14069 || crate::prism::is_affine_target(mm, &t.name)
14070 })
14071 }) {
14072 return None;
14073 }
14074 model.get_or_insert_with(|| mm.clone());
14075 experts.push((gi, ui, di));
14076 }
14077 crate::gpu::GraphFfn::Moe {
14078 router,
14079 shared_gate: sgate,
14080 experts,
14081 n_exp: m.experts.len(),
14082 top_k: m.top_k,
14083 inter,
14084 norm_topk: m.norm_topk_prob,
14085 q4tp: q4tp?,
14086 gu_q2: gu_q2.unwrap_or(false),
14087 q82: q82.unwrap_or(false),
14088 sigmoid: m.router_sigmoid,
14089 bias: m.expert_bias.as_deref(),
14090 has_shared,
14091 shared_gated,
14092 route_scale: m.routed_scaling,
14093 }
14094 }
14095 _ => return None,
14096 };
14097 let attn = match &lw.attn {
14098 AttnKind::Full {
14099 wq,
14100 wk,
14101 wv,
14102 wo,
14103 q_norm,
14104 k_norm,
14105 output_gate,
14106 softplus_gate,
14107 bias,
14108 } => {
14109 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14110 if batch_debug {
14111 eprintln!(
14112 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14113 softplus_gate.is_some(),
14114 self.attention_heads_per_layer.is_some()
14115 );
14116 }
14117 return None;
14118 }
14119 let (m, _, _, _) = wq
14120 .graph_weight()
14121 .or_else(|| wq.graph_weight_descriptor())?;
14122 model = Some(m.clone());
14123 crate::gpu::GraphAttn::Full {
14124 wq: gw(wq)?,
14125 wk: gw(wk)?,
14126 wv: gw(wv)?,
14127 wo: gw(wo)?,
14128 q_norm: q_norm.as_deref(),
14129 k_norm: k_norm.as_deref(),
14130 late_qk_norm: self.qk_norm_after_rope,
14131 bias: bias
14132 .as_ref()
14133 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14134 output_gate: *output_gate,
14135 cpu_k: self.kv_cache.layers[li].k_heads(),
14136 cpu_v: self.kv_cache.layers[li].v_heads(),
14137 cpu_base: self.kv_cache.layers[li].base(),
14138 geom: self.graph_attn_geom(li),
14139 head_gate: None,
14142 }
14143 }
14144 AttnKind::LinearGdn(w) => {
14145 let Some(cfg) = self.gdn_cfg else {
14146 if batch_debug {
14147 eprintln!("batch graph: no GDN config at layer {li}");
14148 }
14149 return None;
14150 };
14151 let (m, _, _, _) = w
14152 .in_proj_qkv
14153 .graph_weight()
14154 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14155 model = Some(m.clone());
14156 crate::gpu::GraphAttn::Gdn {
14157 qkv: gw(&w.in_proj_qkv)?,
14158 z: gw(&w.in_proj_z)?,
14159 a: gw(&w.in_proj_a)?,
14160 b: gw(&w.in_proj_b)?,
14161 out: gw(&w.out_proj)?,
14162 conv1d: &w.conv1d,
14163 a_log: &w.a_log,
14164 dt_bias: &w.dt_bias,
14165 norm: &w.norm,
14166 nv: cfg.num_v_heads,
14167 nk: cfg.num_k_heads,
14168 dk: cfg.key_head_dim,
14169 dv: cfg.value_head_dim,
14170 kk: cfg.conv_kernel,
14171 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14172 }
14173 }
14174 _ => return None,
14175 };
14176 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14179 layers.push(crate::gpu::GraphLayer {
14180 input_norm: &lw.input_norm,
14181 attn,
14182 post_norm: &lw.post_norm,
14183 attn_out_norm: if g4 { lw.attn_out_norm.as_deref() } else { None },
14184 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14185 layer_scale: if g4 { lw.layer_scale } else { None },
14186 ffn: gffn,
14187 });
14188 }
14189 Some((layers, model?))
14190 })();
14191 let Some((layers, model)) = built else {
14192 {
14193 use std::sync::atomic::{AtomicBool, Ordering};
14194 static SAID: AtomicBool = AtomicBool::new(false);
14195 if !SAID.swap(true, Ordering::Relaxed) {
14196 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14197 }
14198 }
14199 return crate::gpu::BatchGraphOutcome::Declined;
14200 };
14201 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14202 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14203 }
14204 crate::gpu::forward_batch_graph(
14205 &model,
14206 self.graph_kv_id,
14207 &layers,
14208 &self.inv_freq,
14209 hiddens,
14210 nh,
14211 nkv,
14212 hd,
14213 rd,
14214 self.hidden_size,
14215 self.intermediate_size,
14216 positions,
14217 self.kv_cache.max_seq_len,
14218 gemma,
14219 self.rms_eps as f32,
14220 self.attn_scale,
14221 k,
14222 &(0..graph_end)
14223 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14224 .collect::<Vec<_>>(),
14225 self.o1_epoch,
14226 spec,
14227 layers_run,
14228 )
14229 }
14230
14231 fn draft_probe() -> bool {
14235 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14236 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14237 }
14238
14239 #[cfg(feature = "gpu")]
14251 fn dsv4_spec_on() -> bool {
14252 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14253 *ON.get_or_init(|| {
14254 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14258 return v != "0";
14259 }
14260 std::env::var("CMF_DSV4_SPEC")
14267 .map(|v| v != "0")
14268 .unwrap_or_else(|_| {
14269 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14270 })
14271 })
14272 }
14273
14274 #[cfg(feature = "gpu")]
14281 fn dsv4_spec_step(
14282 &mut self,
14283 tip_token: u32,
14284 t_next: u32,
14285 next_pos: usize,
14286 max_extra: usize,
14287 drafted: &mut usize,
14288 accepted_ctr: &mut usize,
14289 ) -> Option<(Vec<u32>, usize)> {
14290 let t_all = std::time::Instant::now();
14291 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14292 thread_local! {
14293 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14294 const { std::cell::Cell::new(None) };
14295 }
14296 LAST.with(|l| {
14297 if let Some(prev) = l.get() {
14298 eprintln!(
14299 "между раундами {:.1} мс",
14300 prev.elapsed().as_secs_f64() * 1e3
14301 );
14302 }
14303 l.set(Some(std::time::Instant::now()));
14304 });
14305 }
14306 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14307 eprintln!("spec_step: вход pos={next_pos}");
14308 }
14309 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14310 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14311 if self.dspark.is_none() {
14313 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14314 if t.is_empty() {
14315 return None;
14316 }
14317 crate::dsv4::dspark_arm(&t, cfg.dim);
14318 self.dspark = Some(crate::dsv4::DsparkState::new(
14319 self.dsv4_mtp.len(),
14320 &cfg,
14321 t.len(),
14322 ));
14323 }
14324 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14325 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
14326 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14327 eprintln!("spec_step: пак не построился (targets {targets:?})");
14328 }
14329 let pack = pack?;
14330 let block = crate::dsv4::dspark_block();
14331 let b_box = self.dsv4.as_mut()?;
14332 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
14333 let ds = self.dspark.as_mut()?;
14334 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14337 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14338 if dbg {
14339 eprintln!("spec_step: нет захвата");
14340 }
14341 return None;
14342 }
14343 ds.have_hidden = true;
14344 let tip_pos = next_pos.checked_sub(1)?;
14345 let draft_started = std::time::Instant::now();
14346 let mut conf = Vec::new();
14347 let props = crate::dsv4::dspark_draft_gpu(
14348 g,
14349 &self.dsv4_mtp,
14350 &cfg,
14351 ds,
14352 pack,
14353 st.kv_id,
14354 tip_token,
14355 tip_pos,
14356 self.pool.as_deref(),
14357 &mut conf,
14358 );
14359 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14360 *drafted += block;
14361 if props.is_empty() || props[0] != t_next {
14362 if dbg {
14363 eprintln!(
14364 "spec_step: черновик {} (props0={:?} t_next={t_next})",
14365 if props.is_empty() {
14366 "пуст"
14367 } else {
14368 "мимо"
14369 },
14370 props.first()
14371 );
14372 }
14373 return None;
14374 }
14375 let mut k_verify = crate::dsv4::dspark_verify_k()
14382 .min(props.len())
14383 .min(max_extra.saturating_add(1));
14384 let conf_min = {
14390 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14391 *M.get_or_init(|| {
14392 std::env::var("CMF_DSPARK_CONF_MIN")
14393 .ok()
14394 .and_then(|v| v.parse().ok())
14395 .unwrap_or(0.0)
14396 })
14397 };
14398 if conf_min > 0.0 && conf.len() >= props.len() {
14399 let mut keep = 1usize;
14400 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14401 keep += 1;
14402 }
14403 k_verify = k_verify.min(keep.max(2));
14404 }
14405 if k_verify < 2 {
14406 return None;
14407 }
14408 let mut fed = Vec::with_capacity(k_verify);
14409 fed.push(t_next);
14410 fed.extend_from_slice(&props[1..k_verify]);
14411 let mut argmax = Vec::new();
14412 let mut logits_all = Vec::new();
14413 let mut walked = Vec::new();
14414 let txn = crate::dsv4::dsv4_verify_chunk(
14415 g,
14416 layers,
14417 &cfg,
14418 st,
14419 &fed,
14420 next_pos,
14421 &self.inv_freq,
14422 self.pool.as_deref(),
14423 &targets,
14424 &mut argmax,
14425 &mut logits_all,
14426 &mut walked,
14427 );
14428 if txn.is_none() && dbg {
14429 eprintln!("spec_step: verify отказал");
14430 }
14431 let txn = txn?;
14432 let spec_gpu_end = txn.gpu_end;
14433 let b = fed.len();
14434 let mut accepted = 1usize;
14435 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14436 accepted += 1;
14437 }
14438 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14443 accepted = 1;
14444 }
14445 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14446 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14447 }
14448 let t_fin = std::time::Instant::now();
14449 if !crate::dsv4::dsv4_spec_finish(
14450 g,
14451 layers,
14452 &cfg,
14453 st,
14454 txn,
14455 accepted,
14456 &fed,
14457 &self.inv_freq,
14458 self.pool.as_deref(),
14459 ) {
14460 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14461 return None;
14462 }
14463 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14464 eprintln!(
14465 "finish(k={accepted}): {:.1} мс",
14466 t_fin.elapsed().as_secs_f64() * 1e3
14467 );
14468 }
14469 *accepted_ctr += accepted - 1;
14470 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14475 let dev_caps: Vec<usize> = targets
14480 .iter()
14481 .copied()
14482 .filter(|&t| t < spec_gpu_end)
14483 .collect();
14484 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14485 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14486 return None;
14487 }
14488 for t in 0..accepted {
14489 let tip = t + 1 == accepted;
14490 for (slot, &tl) in targets.iter().enumerate() {
14491 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14492 let lo = (di * b + t) * hc * dim;
14493 crate::dsv4::dspark_capture(
14494 &caps_all[lo..lo + hc * dim],
14495 &cfg,
14496 slot,
14497 &mut ds.main_hidden,
14498 );
14499 } else if tip
14500 && crate::dsv4::dspark_peek_slot(slot, dim, {
14501 let lo = slot * dim;
14502 &mut ds.main_hidden[lo..lo + dim]
14503 })
14504 {
14505 } else {
14510 crate::dsv4::dspark_capture(
14514 &walked[t * hc * dim..(t + 1) * hc * dim],
14515 &cfg,
14516 slot,
14517 &mut ds.main_hidden,
14518 );
14519 }
14520 }
14521 crate::dsv4::dspark_ring_append(
14522 g,
14523 &self.dsv4_mtp,
14524 &cfg,
14525 ds,
14526 next_pos + t,
14527 self.pool.as_deref(),
14528 );
14529 }
14530 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14531 self.graph_logits = Some(row);
14532 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14537 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14538 crate::dsv4::pick_tally_arm();
14539 }
14540 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14541 eprintln!(
14542 "spec_step total {:.1} мс (k={accepted})",
14543 t_all.elapsed().as_secs_f64() * 1e3
14544 );
14545 }
14546 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14547 }
14548
14549 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14550 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14551 return;
14552 }
14553 let trunk_now = crate::dsv4::pick_tally_take();
14555 crate::dsv4::trunk_freq_note(&trunk_now);
14556 if !trunk_now.is_empty() {
14557 self.dspark_trunk_picks.push(trunk_now);
14558 let keep = crate::dsv4::dspark_block();
14559 if self.dspark_trunk_picks.len() > keep {
14560 self.dspark_trunk_picks.remove(0);
14561 }
14562 }
14563 for p in std::mem::take(&mut self.dspark_pending) {
14566 let Some(i) = position.checked_sub(p.0 + 1) else {
14567 continue;
14568 };
14569 let mut p = p;
14570 if i < p.1.len() {
14571 if p.2 && p.1[i] == token_id {
14572 p.3 = i + 1;
14573 } else {
14574 p.2 = false;
14575 }
14576 if i + 1 < p.1.len() {
14577 self.dspark_pending.push(p);
14578 continue;
14579 }
14580 }
14581 self.dspark_hist.push(p.3);
14582 self.dspark_real.push(token_id);
14583 }
14584 let Some(b) = &mut self.dsv4 else { return };
14585 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14586 let n_layers = layers.len();
14587 if self.dspark.is_none() {
14588 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14589 if t.is_empty() {
14590 return;
14591 }
14592 eprintln!(
14593 "DSpark: захват со слоёв {t:?}, блок {}",
14594 crate::dsv4::dspark_block()
14595 );
14596 crate::dsv4::dspark_arm(&t, cfg.dim);
14597 self.dspark = Some(crate::dsv4::DsparkState::new(
14598 self.dsv4_mtp.len(),
14599 &cfg,
14600 t.len(),
14601 ));
14602 }
14603 let ds = self.dspark.as_mut().unwrap();
14604 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14605 return; }
14607 let mut conf = Vec::new();
14608 crate::dsv4::pick_tally_arm();
14609 let draft_started = std::time::Instant::now();
14614 #[cfg(feature = "gpu")]
14615 let gpu_draft = crate::dsv4::dspark_gpu_on();
14616 #[cfg(not(feature = "gpu"))]
14617 let gpu_draft = false;
14618 let props = if gpu_draft {
14619 #[cfg(feature = "gpu")]
14620 {
14621 let kv_id = b.3.kv_id;
14622 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14623 Some(pk) => crate::dsv4::dspark_draft_gpu(
14624 g,
14625 &self.dsv4_mtp,
14626 &cfg,
14627 ds,
14628 pk,
14629 kv_id,
14630 token_id,
14631 position,
14632 self.pool.as_deref(),
14633 &mut conf,
14634 ),
14635 None => Vec::new(),
14636 }
14637 }
14638 #[cfg(not(feature = "gpu"))]
14639 Vec::new()
14640 } else {
14641 crate::gpu::cpu_scope(|| {
14642 crate::dsv4::dspark_draft(
14643 g,
14644 &self.dsv4_mtp,
14645 &cfg,
14646 ds,
14647 token_id,
14648 position,
14649 self.pool.as_deref(),
14650 &mut conf,
14651 )
14652 })
14653 };
14654 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14655 let draft_picks = crate::dsv4::pick_tally_take();
14656 crate::dsv4::dspark_freq_note(&draft_picks);
14657 crate::dsv4::pick_tally_arm();
14660 if !props.is_empty() {
14661 let (tu, tt) = {
14665 let flat: Vec<(usize, Vec<usize>)> = self
14666 .dspark_trunk_picks
14667 .iter()
14668 .flat_map(|v| v.iter().cloned())
14669 .collect();
14670 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14672 std::collections::HashMap::new();
14673 for (li, picks) in flat {
14674 per.entry(li).or_default().extend(picks);
14675 }
14676 let n = per.len().max(1);
14677 let mut u = 0usize;
14678 let mut t = 0usize;
14679 for (_, v) in per {
14680 t += v.len();
14681 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14682 }
14683 (u / n, t / n)
14684 };
14685 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14686 self.dspark_exp.push((tu, tt, du, dt));
14687 self.dspark_pending.push((position, props, true, 0));
14688 }
14689 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14690 let n = self.dspark_hist.len() as f32;
14691 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14692 let block = crate::dsv4::dspark_block();
14693 let mut at = vec![0usize; block + 1];
14694 for &k in &self.dspark_hist {
14695 at[k] += 1;
14696 }
14697 let mut surv = Vec::with_capacity(block);
14699 for i in 1..=block {
14700 let k = at[i..].iter().sum::<usize>() as f32 / n;
14701 surv.push(format!("{k:.2}"));
14702 }
14703 let distinct = self
14704 .dspark_real
14705 .iter()
14706 .collect::<std::collections::HashSet<_>>()
14707 .len();
14708 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14709 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14710 });
14711 let m = self.dspark_exp.len().max(1);
14712 eprintln!(
14713 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14714 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14715 self.dspark_hist.len(),
14716 mean + 1.0,
14717 surv.join(" ")
14718 );
14719 eprintln!(
14720 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14721 эксперты ствол {}/{} на слой за {block} токенов, \
14722 черновик {}/{} за блок, draft {:.2} мс/блок",
14723 self.dspark_real.len(),
14724 tu / m,
14725 tt / m,
14726 du / m,
14727 dt / m,
14728 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14729 );
14730 }
14731 }
14732
14733 fn forward_layers_upto(
14734 &mut self,
14735 hidden: &[f32],
14736 position: usize,
14737 task_mask: Option<&TaskMask>,
14738 upto: Option<usize>,
14739 ) -> Vec<f32> {
14740 if let Some(plan) = self.gpu_plan.clone() {
14746 if upto.is_none() && plan.len() > 1 {
14747 let mut h = hidden.to_vec();
14748 for &(dev, from, upto_incl) in plan.iter() {
14749 h = crate::gpu::with_device(dev, || {
14750 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14751 });
14752 }
14753 return h;
14754 }
14755 }
14756 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14757 }
14758
14759 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14764 self.set_gpu_plan_at(devices, None)
14765 }
14766
14767 pub fn set_gpu_plan_at(
14771 &mut self,
14772 devices: Option<&[usize]>,
14773 at: Option<usize>,
14774 ) -> Result<(), String> {
14775 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14776 self.gpu_plan = None;
14777 return Ok(());
14778 };
14779 self.split_supported()?;
14780 let n = self.num_layers;
14781 if devs.len() > n {
14782 return Err(format!("{} devices for {n} layers", devs.len()));
14783 }
14784 if let Some(k) = at {
14785 if k == 0 || k >= n {
14786 return Err(format!("split at {k}: the model has {n} layers"));
14787 }
14788 if devs.len() == 2 {
14789 self.gpu_plan = Some(std::sync::Arc::new(vec![
14790 (devs[0], 0, k - 1),
14791 (devs[1], k, n - 1),
14792 ]));
14793 return Ok(());
14794 }
14795 return Err(format!(
14796 "an explicit split point takes exactly 2 devices, got {}",
14797 devs.len()
14798 ));
14799 }
14800 let per = n.div_ceil(devs.len());
14801 let mut plan = Vec::with_capacity(devs.len());
14802 let mut from = 0usize;
14803 for &d in devs {
14804 if from >= n {
14805 break;
14806 }
14807 let upto = (from + per - 1).min(n - 1);
14808 plan.push((d, from, upto));
14809 from = upto + 1;
14810 }
14811 self.gpu_plan = Some(std::sync::Arc::new(plan));
14812 Ok(())
14813 }
14814
14815 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
14817 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
14818 }
14819
14820 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
14826 if let Some(x) = t.as_f32() {
14827 return x.to_vec();
14828 }
14829 let mut out = vec![0.0; t.rows() * t.cols()];
14830 for r in 0..t.rows() {
14831 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
14832 }
14833 out
14834 }
14835
14836 fn embryo_resident_eligible(&self) -> bool {
14837 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
14840 || self.num_layers != self.physical_layers
14841 || self.loop_final_norm
14842 || self.weights.layers.len() != self.num_layers
14843 || self.head_clusters.is_none()
14844 || self.final_softcap.is_some()
14845 || self.logit_multiplier.is_some()
14846 || self.attn_softcap != 0.0
14847 || self.mtp.is_some()
14848 || self.g3n.is_some()
14849 || self.dsv4.is_some()
14850 || self.dsv41.is_some()
14851 || self.qwen4_exp.is_some()
14852 || self.dyn_router.is_some()
14859 || self.dyn_phi_layer.is_some()
14860 || self.dyn_blend_loaded
14861 || self.o1_cfg.is_some()
14862 || self.swa.is_some()
14863 || self.sliding_layers.is_some()
14864 || self.global_attn.is_some()
14865 || self.attention_heads_per_layer.is_some()
14866 || self.attn_v_norm
14867 || self
14868 .kv_cache
14869 .layers
14870 .iter()
14871 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
14872 || self.rope_scale != 1.0
14873 || self.rope_scale_local != 1.0
14874 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
14875 || self.hidden_size == 0
14876 || self.hidden_size > 1024
14877 || self.intermediate_size > 1024
14878 || self.num_heads == 0
14879 || self.num_kv_heads == 0
14880 || self.num_heads % self.num_kv_heads != 0
14881 || self.num_heads.saturating_mul(self.head_dim) > 1024
14882 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
14883 || self.vocab_size == 0
14884 || self.kv_cache.max_seq_len == 0
14885 || self.rotary_dim == 0
14886 || self.rotary_dim > self.head_dim
14887 || self.rotary_dim % 2 != 0
14888 || self.inv_freq.len() < self.rotary_dim / 2
14889 {
14890 return false;
14891 }
14892 if self.weights.lm_head.as_f32().is_none()
14903 || self.weights.embed_tokens.as_f32().is_none()
14904 || self.weights.lm_head.rows() < self.vocab_size
14905 || self.weights.lm_head.cols() != self.hidden_size
14906 || self.weights.embed_tokens.rows() < self.vocab_size
14907 || self.weights.embed_tokens.cols() != self.hidden_size
14908 || self.weights.final_norm.len() != self.hidden_size
14909 {
14910 return false;
14911 }
14912 if let Some(cfg) = self.vmf_cfg {
14913 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14914 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14915 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14916 || cfg.state_len() == 0
14917 {
14918 return false;
14919 }
14920 }
14921 if let Some(g) = self.gdn_cfg {
14922 if g.num_v_heads == 0
14927 || g.num_k_heads == 0
14928 || g.num_v_heads % g.num_k_heads != 0
14929 || g.key_head_dim == 0
14930 || g.key_head_dim > 128
14931 || g.value_head_dim == 0
14932 || g.value_head_dim > 256
14933 || g.value_head_dim % 4 != 0
14934 || g.conv_kernel == 0
14935 || g.num_v_heads > 512
14936 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14937 || g.conv_dim() > 2048
14938 || g.conv_dim() % 4 != 0
14939 || g.hidden_size != self.hidden_size
14940 || g.output_gate_sigmoid
14941 || g.rms_eps != self.rms_eps
14942 || g.state_len() == 0
14943 {
14944 return false;
14945 }
14946 }
14947 let mut full_seen = false;
14948 for lw in &self.weights.layers {
14949 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14950 return false;
14951 }
14952 match &lw.attn {
14953 AttnKind::LinearGdn(w) => {
14954 let Some(g) = self.gdn_cfg else {
14955 return false;
14956 };
14957 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14958 if w.in_proj_qkv.rows() != g.conv_dim()
14959 || w.in_proj_qkv.cols() != self.hidden_size
14960 || w.in_proj_qkv.as_f32().is_none()
14961 || w.in_proj_z.rows() != nv * dv
14962 || w.in_proj_z.cols() != self.hidden_size
14963 || w.in_proj_z.as_f32().is_none()
14964 || w.in_proj_a.rows() != nv
14965 || w.in_proj_a.cols() != self.hidden_size
14966 || w.in_proj_a.as_f32().is_none()
14967 || w.in_proj_b.rows() != nv
14968 || w.in_proj_b.cols() != self.hidden_size
14969 || w.in_proj_b.as_f32().is_none()
14970 || w.conv1d.len() != g.conv_dim() * kk
14971 || w.a_log.len() != nv
14972 || w.dt_bias.len() != nv
14973 || w.norm.len() != dv
14974 || w.out_proj.rows() != self.hidden_size
14975 || w.out_proj.cols() != nv * dv
14976 || w.out_proj.as_f32().is_none()
14977 {
14978 return false;
14979 }
14980 }
14981 AttnKind::Linear(w) => {
14982 let Some(cfg) = self.vmf_cfg else {
14983 return false;
14984 };
14985 if w.thq.rows() != cfg.num_heads * cfg.nphase
14986 || w.thq.cols() != self.hidden_size
14987 || w.thq.as_f32().is_none()
14988 || w.thk.rows() != cfg.num_heads * cfg.nphase
14989 || w.thk.cols() != self.hidden_size
14990 || w.thk.as_f32().is_none()
14991 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14992 || w.v_proj.cols() != self.hidden_size
14993 || w.v_proj.as_f32().is_none()
14994 || w.out_proj.rows() != self.hidden_size
14995 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14996 || w.out_proj.as_f32().is_none()
14997 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14998 {
14999 return false;
15000 }
15001 if let Some((kg, kb)) = &w.k_gate {
15002 if kg.rows() != cfg.num_heads
15003 || kg.cols() != self.hidden_size
15004 || kg.as_f32().is_none()
15005 || kb.len() != cfg.num_heads
15006 {
15007 return false;
15008 }
15009 }
15010 if let Some(conv) = &w.conv {
15011 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
15012 return false;
15013 }
15014 }
15015 }
15016 AttnKind::Full {
15017 wq,
15018 wk,
15019 wv,
15020 wo,
15021 q_norm,
15022 k_norm,
15023 output_gate,
15024 softplus_gate,
15025 bias,
15026 } => {
15027 if full_seen
15028 || q_norm.is_some()
15029 || k_norm.is_some()
15030 || *output_gate
15031 || softplus_gate.is_some()
15032 || bias.is_some()
15033 || wq.as_f32().is_none()
15034 || wk.as_f32().is_none()
15035 || wv.as_f32().is_none()
15036 || wo.as_f32().is_none()
15037 || wq.rows() != self.num_heads * self.head_dim
15038 || wk.rows() != self.num_kv_heads * self.head_dim
15039 || wv.rows() != self.num_kv_heads * self.head_dim
15040 || wq.cols() != self.hidden_size
15041 || wk.cols() != self.hidden_size
15042 || wv.cols() != self.hidden_size
15043 || wo.rows() != self.hidden_size
15044 || wo.cols() != self.num_heads * self.head_dim
15045 {
15046 return false;
15047 }
15048 full_seen = true;
15049 }
15050 AttnKind::Bounded(w) => {
15051 let Some(ac) = self.anchor_core.as_ref() else {
15054 return false;
15055 };
15056 if self.bounded_rope.is_none()
15057 || w.window != ac.window
15058 || w.sink != ac.sink
15059 || w.window == 0
15060 || w.window + w.sink > 256
15061 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15062 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15063 || w.wq.as_f32().is_none()
15064 || w.wk.as_f32().is_none()
15065 || w.wv.as_f32().is_none()
15066 || w.wo.as_f32().is_none()
15067 || w.wq.rows() != self.num_heads * self.head_dim
15068 || w.wk.rows() != self.num_kv_heads * self.head_dim
15069 || w.wv.rows() != self.num_kv_heads * self.head_dim
15070 || w.wq.cols() != self.hidden_size
15071 || w.wk.cols() != self.hidden_size
15072 || w.wv.cols() != self.hidden_size
15073 || w.wo.rows() != self.hidden_size
15074 || w.wo.cols() != self.num_heads * self.head_dim
15075 {
15076 return false;
15077 }
15078 }
15079 _ => return false,
15080 }
15081 match &lw.ffn {
15082 FfnKind::Dense(d) => {
15083 if d.act != Act::Silu
15084 || !d.segs.is_empty()
15085 || d.gate_proj.as_f32().is_none()
15086 || d.up_proj.as_f32().is_none()
15087 || d.down_proj.as_f32().is_none()
15088 || d.gate_proj.rows() != self.intermediate_size
15089 || d.gate_proj.cols() != self.hidden_size
15090 || d.up_proj.rows() != self.intermediate_size
15091 || d.up_proj.cols() != self.hidden_size
15092 || d.down_proj.rows() != self.hidden_size
15093 || d.down_proj.cols() != self.intermediate_size
15094 {
15095 return false;
15096 }
15097 }
15098 FfnKind::Moe(m) => {
15099 if m.resonance.is_none()
15100 || m.top_k != 1
15101 || m.router_sigmoid
15102 || !m.norm_topk_prob
15103 || m.expert_bias.is_some()
15104 || m.routed_scaling != 1.0
15105 || m.route_tau.is_some()
15106 || m.shared.is_none()
15107 || m.mask.is_some()
15108 || m.per_expert_scale.is_some()
15109 || m.router_input_norm
15110 || m.experts.is_empty()
15111 || m.experts.len() > 8
15112 {
15113 return false;
15114 }
15115 let r = m.resonance.as_ref().unwrap();
15116 if r.mu.len() != m.experts.len() * self.hidden_size
15117 || r.bias.len() != m.experts.len()
15118 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15119 || r.k > 128
15120 {
15121 return false;
15122 }
15123 let Some((shared, gate)) = &m.shared else {
15124 return false;
15125 };
15126 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15127 return false;
15128 }
15129 if shared.gate_proj.as_f32().is_none()
15130 || shared.up_proj.as_f32().is_none()
15131 || shared.down_proj.as_f32().is_none()
15132 || shared.gate_proj.rows() != self.intermediate_size
15133 || shared.gate_proj.cols() != self.hidden_size
15134 || shared.up_proj.rows() != self.intermediate_size
15135 || shared.up_proj.cols() != self.hidden_size
15136 || shared.down_proj.rows() != self.hidden_size
15137 || shared.down_proj.cols() != self.intermediate_size
15138 {
15139 return false;
15140 }
15141 for e in &m.experts {
15142 if e.act != Act::Silu
15143 || !e.segs.is_empty()
15144 || e.gate_proj.as_f32().is_none()
15145 || e.up_proj.as_f32().is_none()
15146 || e.down_proj.as_f32().is_none()
15147 || e.gate_proj.rows() != self.intermediate_size
15148 || e.gate_proj.cols() != self.hidden_size
15149 || e.up_proj.rows() != self.intermediate_size
15150 || e.up_proj.cols() != self.hidden_size
15151 || e.down_proj.rows() != self.hidden_size
15152 || e.down_proj.cols() != self.intermediate_size
15153 {
15154 return false;
15155 }
15156 }
15157 }
15158 FfnKind::DenseMoe(_) => return false,
15159 }
15160 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15161 return false;
15162 }
15163 }
15164 if full_seen && self.anchor_core.is_some() {
15165 return false;
15166 }
15167 full_seen || self.num_layers > 0
15168 }
15169
15170 fn embryo_resident_wanted(&self) -> bool {
15175 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15176 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15177 && matches!(
15178 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15179 Ok("1") | Ok("parallel")
15180 )
15181 && crate::gpu::enabled_here()
15182 && !self.graph_refused()
15183 && self.embryo_resident_eligible()
15184 }
15185
15186 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15194 if ids.len() < 2
15195 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15196 || !self.embryo_resident_wanted()
15197 {
15198 return None;
15199 }
15200 let model = self.ensure_embryo_graph()?;
15201 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15202 .ok()
15203 .and_then(|v| v.parse::<usize>().ok())
15204 .filter(|&v| v >= 1)
15205 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15206 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15207 let hs = self.hidden_size;
15208 let n = ids.len();
15209 let mut pos = start;
15210 let mut last = None;
15211 let mut rows = Vec::with_capacity(cmax * hs);
15212 while pos < n {
15213 let end = (pos + cmax).min(n);
15214 rows.clear();
15215 for &id in &ids[pos..end] {
15216 rows.extend_from_slice(&self.embed_single(id));
15217 }
15218 let mut lg = Vec::new();
15219 if !crate::gpu::forward_embryo_graph_chunk(
15220 &model,
15221 self.graph_kv_id,
15222 &rows,
15223 pos,
15224 end - pos,
15225 &mut lg,
15226 ) {
15227 if pos == start {
15228 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15229 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15230 }
15231 return None;
15232 }
15233 self.kv_cache.clear();
15237 self.clear_history();
15238 crate::gpu::graph_kv_reset(self.graph_kv_id);
15239 panic!(
15240 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15241 );
15242 }
15243 last = Some(lg);
15244 pos = end;
15245 }
15246 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15247 eprintln!(
15248 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15249 n - start,
15250 (n - start).div_ceil(cmax)
15251 );
15252 }
15253 last
15254 }
15255
15256 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15257 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15258 const UMAX: u32 = u32::MAX;
15259 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15260 const REC: usize = 64;
15261 struct Pack {
15262 data: Vec<f32>,
15263 }
15264 impl Pack {
15265 fn put(&mut self, x: &[f32]) -> u32 {
15266 if x.is_empty() {
15267 return u32::MAX;
15268 }
15269 let off = self.data.len();
15270 self.data.extend_from_slice(x);
15271 off as u32
15272 }
15273 }
15274 let vmf = self.vmf_cfg;
15278 let gdn = self.gdn_cfg;
15279 let mut pack = Pack { data: Vec::new() };
15280 let mut meta = vec![0u32; HEADER];
15281 meta[0] = self.hidden_size as u32;
15282 meta[1] = self.intermediate_size as u32;
15283 meta[2] = self.vocab_size as u32;
15284 meta[3] = self.num_layers as u32;
15285 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15286 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15287 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15288 if let Some(g) = gdn {
15289 meta[24] = g.num_v_heads as u32;
15290 meta[25] = g.num_k_heads as u32;
15291 meta[26] = g.key_head_dim as u32;
15292 meta[27] = g.value_head_dim as u32;
15293 meta[28] = g.conv_kernel as u32;
15294 meta[29] = g.conv_dim() as u32;
15295 }
15296 meta[7] = self.num_heads as u32;
15297 meta[8] = self.num_kv_heads as u32;
15298 meta[9] = self.head_dim as u32;
15299 meta[10] = self.kv_cache.max_seq_len as u32;
15300 let clusters = self.head_clusters.as_ref().unwrap();
15301 let cluster_count = clusters.len() / self.hidden_size;
15302 if clusters.len() % self.hidden_size != 0
15303 || cluster_count == 0
15304 || cluster_count > 1024
15305 || self.vocab_size % cluster_count != 0
15306 || self.weights.lm_head.rows() < self.vocab_size
15307 || self.weights.final_norm.len() != self.hidden_size
15308 {
15309 return None;
15310 }
15311 meta[11] = cluster_count as u32;
15312 meta[12] = (self.vocab_size / cluster_count) as u32;
15313 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15314 meta[16] = self.rotary_dim as u32;
15315 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15316 meta[19] = (self.rms_eps as f32).to_bits();
15317 let max_conv = self
15318 .weights
15319 .layers
15320 .iter()
15321 .filter_map(|lw| match &lw.attn {
15322 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
15323 _ => None,
15324 })
15325 .max()
15326 .unwrap_or(1);
15327 let phase_stride = vmf
15333 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15334 .unwrap_or(0);
15335 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15336 let state_stride = phase_stride.max(gdn_stride);
15337 let bounded = self.anchor_core.clone();
15341 let (anchor_window, anchor_sink) = bounded
15342 .as_ref()
15343 .map(|ac| (ac.window, ac.sink))
15344 .unwrap_or((0, 0));
15345 let kv_stride = if bounded.is_some() {
15346 2usize
15347 .saturating_mul(self.num_kv_heads)
15348 .saturating_mul(anchor_window)
15349 .saturating_mul(self.head_dim)
15350 } else {
15351 2usize
15352 .saturating_mul(self.num_kv_heads)
15353 .saturating_mul(self.kv_cache.max_seq_len)
15354 .saturating_mul(self.head_dim)
15355 };
15356 meta[14] = state_stride as u32;
15357 meta[15] = kv_stride as u32;
15358 meta[18] = anchor_window as u32;
15359 meta[20] = anchor_sink as u32;
15360 meta[21] = match &self.bounded_rope {
15361 Some(rope) => {
15362 let off = pack.put(&rope.cos);
15364 let _ = pack.put(&rope.sin);
15365 off
15366 }
15367 None => UMAX,
15368 };
15369 let mut full_seen = false;
15370 let mut bounded_seen = 0usize;
15371 let mut phase_seen = 0usize;
15375 let mut gdn_seen = 0usize;
15376 for (li, lw) in self.weights.layers.iter().enumerate() {
15377 let base = meta.len();
15378 meta.resize(base + REC, UMAX);
15379 meta[base] = match &lw.attn {
15380 AttnKind::Linear(w) if w.phase_delta => 1,
15381 AttnKind::Linear(_) => 0,
15382 AttnKind::Full { .. } => 2,
15383 AttnKind::Bounded(_) => 3,
15384 AttnKind::LinearGdn(_) => 4,
15385 _ => UMAX,
15386 };
15387 meta[base + 1] = pack.put(&lw.input_norm);
15388 meta[base + 2] = pack.put(&lw.post_norm);
15389 meta[base + 25] = match &lw.attn {
15390 AttnKind::Linear(_) => {
15391 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15392 phase_seen += 1;
15393 off
15394 }
15395 AttnKind::LinearGdn(_) => {
15396 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15397 gdn_seen += 1;
15398 off
15399 }
15400 _ => UMAX,
15401 };
15402 match &lw.attn {
15403 AttnKind::LinearGdn(w) => {
15404 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15407 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15408 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15409 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15410 meta[base + 60] = pack.put(&w.conv1d);
15411 meta[base + 61] = pack.put(&w.a_log);
15412 meta[base + 62] = pack.put(&w.dt_bias);
15413 meta[base + 63] = pack.put(&w.norm);
15414 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15415 meta[base + 24] = 0;
15416 }
15417 AttnKind::Linear(w) => {
15418 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15419 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15420 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15421 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15422 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15423 meta[base + 7] = pack.put(&decay);
15424 if let Some((kg, kb)) = &w.k_gate {
15425 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15426 meta[base + 9] = pack.put(kb);
15427 }
15428 if let Some(conv) = &w.conv {
15429 meta[base + 10] = pack.put(conv);
15430 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15431 } else {
15432 meta[base + 24] = 0;
15433 }
15434 }
15435 AttnKind::Full { wq, wk, wv, wo, .. } => {
15436 full_seen = true;
15437 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15438 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15439 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15440 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15441 meta[base + 26] = (li * kv_stride) as u32;
15442 }
15443 AttnKind::Bounded(w) => {
15444 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15445 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15446 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15447 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15448 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15450 meta[base + 27] = pack.put(&w.sink_k);
15451 meta[base + 28] = pack.put(&w.sink_v);
15452 bounded_seen += 1;
15453 }
15454 _ => return None,
15455 }
15456 match &lw.ffn {
15457 FfnKind::Dense(d) => {
15458 meta[base + 15] = 0;
15459 meta[base + 16] = 0;
15460 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15461 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15462 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15463 }
15464 FfnKind::Moe(m) => {
15465 let r = m.resonance.as_ref().unwrap();
15466 let (shared, _) = m.shared.as_ref().unwrap();
15467 meta[base + 15] = 1;
15468 meta[base + 16] = m.experts.len() as u32;
15469 meta[base + 17] = pack.put(&r.mu);
15470 meta[base + 18] = pack.put(&r.u);
15471 meta[base + 19] = pack.put(&r.bias);
15472 meta[base + 20] = r.k as u32;
15473 let mut shell = r.effective_shell(m.experts.len());
15482 shell.push(f32::NEG_INFINITY);
15483 meta[base + 30] = pack.put(&shell);
15484 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15485 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15486 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15487 for (e, ex) in m.experts.iter().enumerate() {
15488 meta[base + 32 + e * 3] =
15489 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15490 meta[base + 33 + e * 3] =
15491 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15492 meta[base + 34 + e * 3] =
15493 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15494 }
15495 }
15496 FfnKind::DenseMoe(_) => return None,
15497 }
15498 }
15499 if !full_seen && self.num_layers == 0 {
15500 return None;
15501 }
15502 let id = {
15503 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15504 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15505 };
15506 let model = crate::gpu::EmbryoGraphModel {
15507 id,
15508 hidden: self.hidden_size,
15509 intermediate: self.intermediate_size,
15510 vocab: self.vocab_size,
15511 layers: self.num_layers,
15512 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15513 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15514 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15515 anchor_q_heads: self.num_heads,
15516 anchor_kv_heads: self.num_kv_heads,
15517 anchor_head_dim: self.head_dim,
15518 rotary_dim: self.rotary_dim,
15519 max_seq: self.kv_cache.max_seq_len,
15520 cluster_count,
15521 cluster_size: self.vocab_size / cluster_count,
15522 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15523 state_stride,
15524 kv_stride,
15525 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15526 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15527 weights: pack.data,
15528 meta,
15529 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15530 clusters: clusters.as_ref().clone(),
15531 final_norm: self.weights.final_norm.clone(),
15532 inv_freq: self.inv_freq.as_ref().clone(),
15533 bounded: bounded.is_some(),
15534 kv_layers: if bounded.is_some() {
15535 bounded_seen
15536 } else {
15537 self.num_layers
15538 },
15539 state_layers: phase_seen + gdn_seen,
15540 anchor_window,
15541 anchor_sink,
15542 phase_layers: phase_seen,
15543 gdn_layers: gdn_seen,
15544 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15545 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15546 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15547 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15548 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15549 };
15550 self.embryo_graph = Some(std::sync::Arc::new(model));
15551 }
15552 self.embryo_graph.clone()
15553 }
15554
15555 fn forward_layers_span(
15556 &mut self,
15557 hidden: &[f32],
15558 position: usize,
15559 task_mask: Option<&TaskMask>,
15560 from: usize,
15561 upto: Option<usize>,
15562 ) -> Vec<f32> {
15563 #[cfg(target_os = "macos")]
15564 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
15565 position,
15566 from,
15567 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
15568 );
15569 debug_assert!(
15570 from == 0
15571 || (self.dsv4.is_none()
15572 && self.dsv41.is_none()
15573 && self.qwen4_exp.is_none()
15574 && self.g3n.is_none())
15575 );
15576 #[cfg(target_os = "macos")]
15582 if !crate::gpu_metal::wait_replay() {
15583 self.fail_metal_graph("the pending async replay failed before a plain forward");
15584 return vec![0.0; self.hidden_size];
15585 }
15586 if let Some(b) = &mut self.qwen4_exp {
15587 let _ = (task_mask, upto);
15588 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15589 let mut logits = Vec::new();
15590 crate::qwen4_exp::forward_token(
15591 &b.0,
15592 &b.1,
15593 &b.2,
15594 &mut b.3,
15595 token_id,
15596 position,
15597 &self.inv_freq,
15598 self.pool.as_deref(),
15599 &mut logits,
15600 true,
15601 );
15602 self.graph_logits = Some(logits);
15603 return vec![0.0; self.hidden_size];
15604 }
15605 if let Some(b) = &mut self.dsv4 {
15611 let _ = (task_mask, upto);
15612 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15613 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15614 st.pos = position;
15615 let mut logits = Vec::new();
15616 crate::dsv4::forward_token(
15617 g,
15618 layers,
15619 &cfg,
15620 st,
15621 token_id,
15622 &self.inv_freq,
15623 self.pool.as_deref(),
15624 &mut logits,
15625 );
15626 self.graph_logits = Some(logits);
15627 self.dspark_probe(position, token_id);
15628 return vec![0.0; self.hidden_size];
15631 }
15632 if let Some(b) = &mut self.dsv41 {
15634 let _ = (task_mask, upto);
15635 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15636 let mut logits = Vec::new();
15637 crate::dsv41::forward_token(
15638 &b.0,
15639 &b.1,
15640 &b.2,
15641 &mut b.3,
15642 token_id,
15643 position,
15644 self.pool.as_deref(),
15645 &mut logits,
15646 );
15647 self.graph_logits = Some(logits);
15648 return vec![0.0; self.hidden_size];
15649 }
15650 if let Some(b) = &self.g3n {
15653 let _ = (task_mask, upto);
15654 return crate::g3n::g3n_forward(
15655 &b.0,
15656 &b.1,
15657 hidden,
15658 position,
15659 &mut self.kv_cache.layers,
15660 self.num_heads,
15661 self.num_kv_heads,
15662 self.head_dim,
15663 self.pool.as_deref(),
15664 );
15665 }
15666 if from == 0
15673 && upto.is_none()
15674 && task_mask.is_none()
15675 && self.anchor_core.is_some()
15676 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15677 {
15678 static ONCE: std::sync::Once = std::sync::Once::new();
15679 ONCE.call_once(|| {
15680 eprintln!(
15681 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15682 unsupported={} eligible={}",
15683 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15684 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15685 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15686 crate::gpu::enabled_here(),
15687 self.graph_refused(),
15688 self.embryo_resident_eligible(),
15689 );
15690 });
15691 }
15692 if from == 0
15693 && upto.is_none()
15694 && task_mask.is_none()
15695 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15699 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15700 && matches!(
15704 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15705 Ok("1") | Ok("parallel")
15706 )
15707 && crate::gpu::enabled_here()
15708 && !self.graph_refused()
15709 && (position == 0 || self.device_sequence_position().is_some())
15715 && self.embryo_resident_eligible()
15716 && let Some(model) = self.ensure_embryo_graph()
15717 {
15718 let mut lg = Vec::new();
15719 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15720 {
15721 self.graph_logits = Some(lg);
15722 return vec![0.0; self.hidden_size];
15723 }
15724 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15725 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15726 }
15727 self.mark_graph_refused();
15733 if position != 0 {
15734 self.kv_cache.clear();
15738 self.clear_history();
15739 crate::gpu::graph_kv_reset(self.graph_kv_id);
15740 panic!(
15741 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15742 );
15743 }
15744 }
15745 let mut h = hidden.to_vec();
15746 self.mimo_moe_prepare();
15749 let _mimo_q8 = self.mimo_moe.is_on()
15750 .then(crate::qtensor::enter_full_gpu_q8_scope);
15751 let (nh, _nkv, _hd, hs, _rd, eps) = (
15754 self.num_heads,
15755 self.num_kv_heads,
15756 self.head_dim,
15757 self.hidden_size,
15758 self.rotary_dim,
15759 self.rms_eps,
15760 );
15761 let pool = self.pool.clone();
15762 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15774 let graph_on = match graph_env.as_deref() {
15775 Some("0") => false,
15776 Some("prefill") => false, Some(_) => true,
15778 None => crate::gpu::wgpu_graph_default(),
15784 };
15785 let graph_trusted =
15786 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15787 let race_eligible = graph_on
15788 && upto.is_none()
15789 && task_mask.is_none()
15790 && from == 0
15791 && !self.graph_refused();
15792 let mut tail_start = 0usize;
15793 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15794 let t_graph = std::time::Instant::now();
15795 let mut lg = Vec::new();
15796 let mut gl = 0usize;
15797 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
15798 let declined = built.is_none();
15799 let built = match built {
15800 Some(Ok(hh)) => Some(hh),
15801 Some(Err(())) => {
15802 self.clear_sequence_state();
15806 self.graph_failed
15807 .store(true, std::sync::atomic::Ordering::Relaxed);
15808 self.cancel
15809 .store(true, std::sync::atomic::Ordering::Relaxed);
15810 tracing::error!("token graph failed after admission; sequence state cleared");
15811 return vec![0.0; self.hidden_size];
15812 }
15813 None => None,
15814 };
15815 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
15820 self.mark_graph_refused();
15821 }
15822 graph_note(built.is_some(), gl, self.num_layers);
15823 if let Some(hh) = built {
15824 let dur = t_graph.elapsed();
15825 if std::env::var("CMF_GRAPH_PROF").is_ok() {
15826 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
15827 }
15828 if gl > 0 && gl < self.num_layers {
15829 h = hh;
15835 tail_start = gl;
15836 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
15837 if !graph_trusted {
15838 crate::gpu::graph_race_record(true, dur);
15839 }
15840 if !lg.is_empty() {
15841 lg.resize(self.vocab_size, 0.0);
15844 if let Some(c) = self.final_softcap {
15845 softcap_in_place(&mut lg, c, self.pool.as_deref());
15846 }
15847 self.graph_logits = Some(lg);
15848 }
15849 return hh;
15850 }
15851 }
15857 }
15858 let span = from > 0 || upto.is_some();
15882 if span && graph_on && task_mask.is_none() && graph_trusted {
15883 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
15884 let mut lg = Vec::new();
15885 let mut gl = 0usize;
15886 let span_res =
15887 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
15888 let span_res = match span_res {
15889 Some(Ok(hh)) => Some(hh),
15890 Some(Err(())) => {
15891 self.clear_sequence_state();
15892 self.graph_failed
15893 .store(true, std::sync::atomic::Ordering::Relaxed);
15894 self.cancel
15895 .store(true, std::sync::atomic::Ordering::Relaxed);
15896 tracing::error!(
15897 "span token graph failed after admission; sequence state cleared"
15898 );
15899 return vec![0.0; self.hidden_size];
15900 }
15901 None => None,
15902 };
15903 graph_note(span_res.is_some(), gl, upto_excl - from);
15904 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15905 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15909 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15910 eprintln!(
15911 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15912 upto_excl - from,
15913 span_res.is_some()
15914 );
15915 }
15916 }
15917 if let Some(hh) = span_res {
15918 if gl == upto_excl - from {
15919 if !lg.is_empty() {
15920 lg.resize(self.vocab_size, 0.0);
15921 if let Some(c) = self.final_softcap {
15922 softcap_in_place(&mut lg, c, self.pool.as_deref());
15923 }
15924 self.graph_logits = Some(lg);
15925 }
15926 crate::gpu::set_layer(-1);
15927 return hh;
15928 }
15929 h = hh;
15931 tail_start = from + gl;
15932 }
15933 }
15934 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15939
15940 let host_tail = tail_start > from;
15950 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15951 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15952
15953 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15954 #[cfg(target_os = "macos")]
15955 let mut gpu_skip_until = 0usize;
15956 for li in tail_start.max(from)..self.num_layers {
15957 let _capacity_tail = automatic_gpu_prefix
15958 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15959 .map(|_| crate::gpu::enter_cpu_scope());
15960 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15962 if li > u {
15963 break;
15964 }
15965 }
15966 if let Some(mask) = task_mask {
15967 if !mask.layer_alive(li) {
15968 continue; }
15970 }
15971 #[cfg(target_os = "macos")]
15975 {
15976 if li < gpu_skip_until {
15977 continue;
15978 }
15979 if task_mask.is_none() {
15980 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15981 if self
15982 .graph_failed
15983 .load(std::sync::atomic::Ordering::Relaxed)
15984 {
15985 return vec![0.0; self.hidden_size];
15989 }
15990 if end > li {
15991 gpu_skip_until = end;
15992 if self.is_loop_end(end - 1) && end < self.num_layers {
15995 h = inference::rms_norm(
15996 &h,
15997 &self.weights.final_norm,
15998 self.rms_eps,
15999 self.norm_style,
16000 );
16001 }
16002 continue;
16003 }
16004 }
16005 }
16006
16007 if task_mask.is_none() {
16008 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
16009 crate::gpu::BatchGraphOutcome::Completed => continue,
16010 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
16011 crate::gpu::BatchGraphOutcome::Declined => {},
16012 }
16013 }
16014 #[cfg(feature = "gpu")]
16015 self.pull_lagging_host_kv(li, li + 1, position);
16016 let lw = &self.weights.layers[self.phys_layer(li)];
16017 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
16018 if tp.parse::<usize>().ok() == Some(position) {
16019 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16020 eprintln!(
16021 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16022 h[0], h[1]
16023 );
16024 }
16025 }
16026 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16029 inference::rms_norm_into(
16030 &h,
16031 &lw.input_norm,
16032 self.rms_eps,
16033 self.norm_style,
16034 &mut self.ws.n1,
16035 );
16036 drop(prof);
16037
16038 let attn_out = match &lw.attn {
16039 AttnKind::Mla(w) => {
16040 let inv_freq_l = self.layer_inv_freq(li);
16041 let rs = self.layer_rope_scale(li);
16042 let eps = self.rms_eps;
16043 let pool = self.pool.clone();
16044 mla_attention(
16045 w,
16046 &self.ws.n1,
16047 &mut self.kv_cache.layers[li],
16048 position,
16049 &inv_freq_l,
16050 rs,
16051 eps,
16052 pool.as_deref(),
16053 )
16054 }
16055 AttnKind::Linear(w) => {
16056 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16057 vmf_phase_forward(
16058 &self.ws.n1,
16059 w,
16060 &cfg,
16061 &mut self.kv_cache.layers[li].linear_state,
16062 self.pool.as_deref(),
16063 )
16064 }
16065 AttnKind::Kda(w) => {
16066 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16067 crate::linear_core::kda_forward(
16068 &self.ws.n1,
16069 w,
16070 &cfg,
16071 &mut self.kv_cache.layers[li].linear_state,
16072 self.pool.as_deref(),
16073 )
16074 }
16075 AttnKind::LinearGdn(w) => {
16076 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16077 gdn_forward(
16078 &self.ws.n1,
16079 w,
16080 &cfg,
16081 &mut self.kv_cache.layers[li].linear_state,
16082 self.pool.as_deref(),
16083 )
16084 }
16085 AttnKind::ShortConv(w) => {
16086 let cfg = self
16087 .short_conv_cfg
16088 .expect("short-conv layer without short_conv_cfg");
16089 short_conv_forward(
16090 &self.ws.n1,
16091 w,
16092 &cfg,
16093 &mut self.kv_cache.layers[li].linear_state,
16094 self.pool.as_deref(),
16095 )
16096 }
16097 AttnKind::Bounded(w) => {
16098 let rope = self
16101 .bounded_rope
16102 .clone()
16103 .expect("bounded layer without an installed rotation table");
16104 let cfg = crate::bounded::BoundedAttnCfg {
16105 num_heads: self.num_heads,
16106 num_kv_heads: self.num_kv_heads,
16107 head_dim: self.head_dim,
16108 hidden_size: hs,
16109 scale: self.attn_scale,
16110 rope: &rope,
16111 pool: pool.as_deref(),
16112 };
16113 crate::bounded::bounded_attention(
16114 &self.ws.n1,
16115 w,
16116 &mut self.kv_cache.layers[li],
16117 &cfg,
16118 )
16119 }
16120 AttnKind::Full {
16121 wq,
16122 wk,
16123 wv,
16124 wo,
16125 q_norm,
16126 k_norm,
16127 output_gate,
16128 softplus_gate,
16129 bias,
16130 } if self.kv_cache.layers[li].o1_sealed() => {
16131 let inv_freq_l = self.layer_inv_freq(li);
16134 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16135 let cfg = QwenAttnCfg {
16136 num_heads: self.layer_num_heads(li),
16137 num_kv_heads: nkv_l,
16138 head_dim: hd_l,
16139 hidden_size: hs,
16140 position,
16141 inv_freq: &inv_freq_l,
16142 rotary_dim: rd_l,
16143 scale: self.attn_scale,
16144 softcap: self.attn_softcap,
16145 window: None,
16146 v_norm: self.attn_v_norm,
16147 qk_norm_after_rope: self.qk_norm_after_rope,
16148 gate_sigmoid: self.proj_gate_sigmoid,
16149 q_norm: q_norm.as_deref(),
16150 k_norm: k_norm.as_deref(),
16151 output_gate: *output_gate,
16152 softplus_gate: softplus_gate
16153 .as_ref()
16154 .map(|(gate, per_head)| (gate, *per_head)),
16155 rope_scale: self.layer_rope_scale(li),
16156 bias: bias
16157 .as_ref()
16158 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16159 rms_eps: eps,
16160 norm_style: self.norm_style,
16161 pool: pool.as_deref(),
16162 v_head_dim: self.layer_v_dim(li),
16163 };
16164 attention::qwen_attention_nystrom(
16165 &self.ws.n1,
16166 wq,
16167 wk,
16168 wv,
16169 wo,
16170 &mut self.kv_cache.layers[li],
16171 &cfg,
16172 )
16173 }
16174 AttnKind::Full {
16175 wq,
16176 wk,
16177 wv,
16178 wo,
16179 q_norm,
16180 k_norm,
16181 output_gate,
16182 softplus_gate,
16183 bias,
16184 } => 'attn: {
16185 let dropin_reason =
16190 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16191 if let Some(reason) = dropin_reason {
16192 self.note_graph_decline("wgpu attn dropin", reason);
16193 }
16194 if graph_on
16195 && dropin_reason.is_none()
16196 && !*output_gate
16197 && softplus_gate.is_none()
16198 && self.attention_heads_per_layer.is_none()
16199 && bias.is_none()
16200 && task_mask.is_none()
16201 {
16202 let inv_freq_l = self.layer_inv_freq(li);
16203 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16204 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16205 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16206 wq.mapped_q1(),
16207 wk.mapped_q1(),
16208 wv.mapped_q1(),
16209 wo.mapped_q1(),
16210 ) {
16211 let gm = gm.clone();
16212 let mut out = vec![0f32; hs];
16213 let cache = &self.kv_cache.layers[li];
16214 if crate::gpu::attn_dropin(
16215 &gm,
16216 self.graph_kv_id,
16217 li,
16218 &self.ws.n1,
16219 qi,
16220 ki,
16221 vi,
16222 oi,
16223 q_norm.as_deref(),
16224 k_norm.as_deref(),
16225 self.qk_norm_after_rope,
16226 &inv_freq_l,
16227 nh,
16228 nkv_l,
16229 hd_l,
16230 rd_l,
16231 hs,
16232 position,
16233 self.kv_cache.max_seq_len,
16234 gemma,
16235 eps as f32,
16236 cache.k_heads(),
16237 cache.v_heads(),
16238 &mut out,
16239 ) {
16240 break 'attn out;
16241 }
16242 }
16243 }
16244 let masked = task_mask
16245 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16246 .unwrap_or(false);
16247 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16248 let plain = self.layer_attn_plain(li);
16251 match (masked, f32_view) {
16252 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16255 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16256 attention::multi_head_attention(
16257 &self.ws.n1,
16258 q,
16259 k,
16260 v,
16261 o,
16262 &mut self.kv_cache.layers[li],
16263 self.num_heads,
16264 self.num_kv_heads,
16265 self.head_dim,
16266 self.hidden_size,
16267 position,
16268 &active_heads,
16269 &self.inv_freq,
16270 )
16271 }
16272 (masked, _) => {
16273 if masked {
16274 tracing::warn!(
16275 "layer {li}: head mask on quantized weights or on a \
16276 window/sink/per-layer-geometry layer not supported \
16277 yet — executing dense"
16278 );
16279 }
16280 let inv_freq_l = self.layer_inv_freq(li);
16281 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16282 let cfg = QwenAttnCfg {
16283 num_heads: self.layer_num_heads(li),
16284 num_kv_heads: nkv_l,
16285 head_dim: hd_l,
16286 hidden_size: hs,
16287 position,
16288 inv_freq: &inv_freq_l,
16289 rotary_dim: rd_l,
16290 scale: self.attn_scale,
16291 softcap: self.attn_softcap,
16292 window: self.layer_window(li),
16293 v_norm: self.attn_v_norm,
16294 qk_norm_after_rope: self.qk_norm_after_rope,
16295 gate_sigmoid: self.proj_gate_sigmoid,
16296 q_norm: q_norm.as_deref(),
16297 k_norm: k_norm.as_deref(),
16298 output_gate: *output_gate,
16299 softplus_gate: softplus_gate
16300 .as_ref()
16301 .map(|(gate, per_head)| (gate, *per_head)),
16302 rope_scale: self.layer_rope_scale(li),
16303 bias: bias
16304 .as_ref()
16305 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16306 rms_eps: eps,
16307 norm_style: self.norm_style,
16308 pool: pool.as_deref(),
16309 v_head_dim: self.layer_v_dim(li),
16310 };
16311 attention::qwen_attention(
16312 &self.ws.n1,
16313 wq,
16314 wk,
16315 wv,
16316 wo,
16317 &mut self.kv_cache.layers[li],
16318 &cfg,
16319 )
16320 }
16321 }
16322 }
16323 };
16324 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
16327 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
16328 None => attn_out,
16329 };
16330 let lw = &self.weights.layers[self.phys_layer(li)];
16331 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16332 inference::add_rmsnorm_fused_into(
16333 &mut h,
16334 &attn_out,
16335 &lw.post_norm,
16336 self.rms_eps,
16337 self.norm_style,
16338 &mut self.ws.p1,
16339 );
16340 drop(prof);
16341 let mut attn_out = attn_out;
16342 attention::recycle_buf(&mut attn_out);
16343 let post_normed = &self.ws.p1;
16344
16345 let ffn_masked = task_mask
16346 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16347 .unwrap_or(false);
16348 let ffn_out = match (ffn_masked, &lw.ffn) {
16360 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16364 let row = task_mask
16365 .and_then(|tm| tm.ffn_masks.get(li))
16366 .map(|v| v.as_slice());
16367 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16368 }
16369 (true, FfnKind::Dense(d)) => {
16370 let tm = task_mask.unwrap();
16371 let alive = tm.ffn_active_count(li);
16372 let deep = alive * 2 <= self.intermediate_size;
16373 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16374 let active = tm.ffn_active_indices(li);
16375 sparse_ffn_quant(
16376 d,
16377 post_normed,
16378 &active,
16379 self.hidden_size,
16380 self.pool.as_deref(),
16381 )
16382 } else if deep
16383 && let (Some(g), Some(u), Some(dn)) = (
16384 d.gate_proj.as_f32(),
16385 d.up_proj.as_f32(),
16386 d.down_proj.as_f32(),
16387 )
16388 {
16389 let active = tm.ffn_active_indices(li);
16390 inference::sparse_ffn_forward(
16391 post_normed,
16392 g,
16393 u,
16394 dn,
16395 self.hidden_size,
16396 self.intermediate_size,
16397 &active,
16398 self.pool.as_deref(),
16399 )
16400 } else {
16401 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16402 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16403 }
16404 }
16405 (true, FfnKind::Moe(m)) => {
16406 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16410 ffn_forward(
16411 &lw.ffn,
16412 post_normed,
16413 self.pool.as_deref(),
16414 allowed.as_deref(),
16415 )
16416 }
16417 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16418 dm,
16419 post_normed,
16420 &h,
16421 self.rms_eps,
16422 self.norm_style,
16423 self.pool.as_deref(),
16424 ),
16425 (false, _) => match &lw.ffn {
16426 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16427 dm,
16428 post_normed,
16429 &h,
16430 self.rms_eps,
16431 self.norm_style,
16432 self.pool.as_deref(),
16433 ),
16434 FfnKind::Moe(m)
16435 if task_mask.is_none()
16436 && host_tail
16437 && li >= tail_start
16438 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
16439 {
16440 moe_ffn_minicpm_tail(
16441 &mut self.minicpm_tail,
16442 li,
16443 self.num_layers,
16444 m,
16445 post_normed,
16446 self.pool.as_deref(),
16447 )
16448 }
16449 FfnKind::Moe(m)
16450 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16451 {
16452 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16453 }
16454 _ => {
16455 let allowed = match (&lw.ffn, task_mask) {
16456 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16457 _ => None,
16458 };
16459 ffn_forward(
16460 &lw.ffn,
16461 post_normed,
16462 self.pool.as_deref(),
16463 allowed.as_deref(),
16464 )
16465 }
16466 },
16467 };
16468 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16469 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16470 None => ffn_out,
16471 };
16472 for (i, &f) in ffn_out.iter().enumerate() {
16473 h[i] += f;
16474 }
16475 let mut ffn_out = ffn_out;
16476 attention::recycle_buf(&mut ffn_out);
16477
16478 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16480 for v in h.iter_mut() {
16481 *v *= sc;
16482 }
16483 }
16484 if self.layer_dump.is_some() {
16486 self.dump_layer_row(position, li, &h);
16487 }
16488
16489 if self.is_loop_end(li) && li + 1 < self.num_layers {
16492 h = inference::rms_norm(
16493 &h,
16494 &self.weights.final_norm,
16495 self.rms_eps,
16496 self.norm_style,
16497 );
16498 }
16499
16500 if self.dyn_phi_layer == Some(li) {
16504 self.update_dyn_phi(&h);
16505 }
16506 }
16507 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16509 crate::gpu::graph_race_record(false, t.elapsed());
16510 }
16511
16512 h
16513 }
16514
16515 fn update_dyn_phi(&mut self, h: &[f32]) {
16518 const A: f32 = 0.2;
16519 if self.dyn_phi_ema.len() != h.len() {
16520 self.dyn_phi_ema = vec![0.0; h.len()];
16521 self.dyn_phi_seen = 0;
16522 }
16523 if self.dyn_phi_seen == 0 {
16524 self.dyn_phi_ema.copy_from_slice(h);
16525 } else {
16526 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16527 *e = (1.0 - A) * *e + A * v;
16528 }
16529 }
16530 self.dyn_phi_seen += 1;
16531 }
16532
16533 pub fn dyn_phi(&self) -> &[f32] {
16535 &self.dyn_phi_ema
16536 }
16537
16538 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16540 self.dyn_phi_layer = layer;
16541 self.dyn_phi_ema.clear();
16542 self.dyn_phi_seen = 0;
16543 }
16544
16545 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16547 let Some(model) = &self.model else {
16548 return Vec::new();
16549 };
16550 model
16551 .header
16552 .skills
16553 .iter()
16554 .enumerate()
16555 .filter_map(|(i, sk)| {
16556 if sk.is_v2() {
16560 return None;
16561 }
16562 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16563 let sel = sk.selection.as_ref()?;
16564 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16565 })
16566 .collect()
16567 }
16568
16569 pub fn active_skill(&self) -> Option<usize> {
16571 self.dyn_active
16572 }
16573
16574 pub fn enable_dynamic_routing(&mut self) -> usize {
16579 use crate::swarm::{DynRouter, RoutableSkill};
16580 let Some(model) = self.model.clone() else {
16581 return 0;
16582 };
16583 if let Some(r) = &model.header.router {
16590 tracing::warn!(
16591 "dynamic routing disabled: this file declares router policy '{}' with \
16592 granularity \"{}\" — the request-level decision applies instead",
16593 r.policy,
16594 r.granularity
16595 );
16596 return 0;
16597 }
16598 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16604 || model.header.skills.iter().any(|s| s.is_v2())
16605 {
16606 tracing::warn!(
16607 "dynamic routing disabled: this file carries format-v2 skill records \
16608 (SKILLS_V2) — they route per request through a router policy only"
16609 );
16610 return 0;
16611 }
16612 if self.dyn_blend_loaded {
16615 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16616 return 0;
16617 }
16618 if let Some(a) = self.dyn_active {
16622 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16623 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16624 return 0;
16625 }
16626 }
16627 let hidden = self.hidden_size;
16628 let mut skills = Vec::new();
16629 for (idx, id, _phi) in self.dynamic_skills() {
16630 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16631 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16632 skills.push(rs);
16633 }
16634 }
16635 }
16636 if skills.is_empty() {
16637 return 0;
16638 }
16639 let phi = skills[0].phi_layer;
16641 if skills.iter().any(|s| s.phi_layer != phi) {
16642 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16643 }
16644 let n = skills.len();
16645 self.set_dyn_phi_layer(Some(phi));
16646 self.dyn_router = Some(DynRouter::new(skills));
16647 n
16648 }
16649
16650 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16652 self.dyn_router
16653 .as_ref()
16654 .map(|r| r.switches.clone())
16655 .unwrap_or_default()
16656 }
16657
16658 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16661 let _mimo_q8 = self.mimo_moe.is_on()
16662 .then(crate::qtensor::enter_full_gpu_q8_scope);
16663 let rows = self.weights.lm_head.rows();
16664 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16665 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
16669 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
16670 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
16671 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
16672 rows, self.hidden_size, &mut logits)
16673 });
16674 if !served {
16675 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
16676 }
16677 logits.resize(self.vocab_size, 0.0);
16678 if let Some(m) = self.logit_multiplier {
16679 for l in logits.iter_mut() {
16680 *l *= m;
16681 }
16682 }
16683 if let Some(c) = self.final_softcap {
16684 softcap_in_place(&mut logits, c, self.pool.as_deref());
16685 }
16686 if let Some(cm) = self.head_clusters.as_ref() {
16687 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16688 }
16689 logits
16690 }
16691
16692 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16695 let h = hidden.len();
16696 let ncl = cm.len() / h.max(1);
16697 if ncl == 0 || logits.len() % ncl != 0 {
16698 return;
16699 }
16700 let cs = logits.len() / ncl;
16701 let mut lc = vec![0.0f32; ncl];
16703 for c in 0..ncl {
16704 let row = &cm[c * h..(c + 1) * h];
16705 let mut s = 0.0f32;
16706 for j in 0..h {
16707 s += row[j] * hidden[j];
16708 }
16709 lc[c] = s;
16710 }
16711 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16712 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16713 for c in 0..ncl {
16714 let blk = &mut logits[c * cs..(c + 1) * cs];
16715 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16716 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16717 let add = lc[c] - lse - bl;
16718 for v in blk.iter_mut() {
16719 *v += add;
16720 }
16721 }
16722 }
16723
16724 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16729 #[cfg(target_os = "macos")]
16730 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16731 self.clear_sequence_state();
16732 crate::gpu::graph_race_begin_generation();
16736 if task_mask.is_none() {
16737 self.o1_begin();
16738 }
16739 let mut hidden = vec![0.0f32; self.hidden_size];
16740 for (pos, &id) in ids.iter().enumerate() {
16741 let emb = self.embed_single(id);
16742 hidden = self.forward_layers(&emb, pos, task_mask);
16743 }
16744 if let Err(err) = self.o1_seal_checked() {
16745 self.o1_fail(err);
16746 }
16747 if let Some(logits) = self.graph_logits.take() {
16750 return logits;
16751 }
16752 inference::rms_norm_into(
16753 &hidden,
16754 &self.weights.final_norm,
16755 self.rms_eps,
16756 self.norm_style,
16757 &mut self.ws.n1,
16758 );
16759 self.lm_head_forward(&self.ws.n1)
16760 }
16761}
16762
16763pub fn create_test_pipeline(
16765 hidden_size: usize,
16766 intermediate_size: usize,
16767 num_heads: usize,
16768 num_kv_heads: usize,
16769 head_dim: usize,
16770 num_layers: usize,
16771 vocab_size: usize,
16772) -> Pipeline {
16773 let synth = |n: usize, salt: usize| -> Vec<f32> {
16776 (0..n)
16777 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16778 .collect()
16779 };
16780 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16781 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16782 };
16783 let layer_weights: Vec<LayerWeights> = (0..num_layers)
16784 .map(|li| LayerWeights {
16785 input_norm: vec![1.0; hidden_size],
16786 post_norm: vec![1.0; hidden_size],
16787 attn_out_norm: None,
16788 ffn_out_norm: None,
16789 layer_scale: None,
16790 ffn: FfnKind::Dense(DenseFfn {
16791 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
16792 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
16793 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
16794 act: Act::Silu,
16795 down_t: None,
16796 segs: Vec::new(),
16797 }),
16798 attn: AttnKind::Full {
16799 bias: None,
16800 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
16801 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
16802 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
16803 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
16804 q_norm: None,
16805 k_norm: None,
16806 output_gate: false,
16807 softplus_gate: None,
16808 },
16809 })
16810 .collect();
16811
16812 Pipeline::new(
16813 Tokenizer::byte_level(),
16814 PipelineWeights {
16815 embed_tokens: qt(vocab_size, hidden_size, 100),
16816 layers: layer_weights,
16817 lm_head: qt(vocab_size, hidden_size, 200),
16818 final_norm: vec![1.0; hidden_size],
16819 },
16820 hidden_size,
16821 intermediate_size,
16822 num_heads,
16823 num_kv_heads,
16824 head_dim,
16825 num_layers,
16826 num_layers, false, vocab_size,
16829 1e-6,
16830 10_000.0,
16831 NormStyle::Qwen,
16832 4096,
16833 SamplerConfig {
16834 seed: Some(42),
16835 ..Default::default()
16836 },
16837 )
16838}
16839
16840#[inline]
16845fn mask_bit(row: &[u8], j: usize) -> bool {
16846 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
16847}
16848
16849fn mask_gain() -> f32 {
16860 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16861 *G.get_or_init(|| {
16862 std::env::var("CMF_FFN_MASK_GAIN")
16863 .ok()
16864 .and_then(|v| v.parse().ok())
16865 .unwrap_or(1.0)
16866 })
16867}
16868
16869fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
16870 let fill = meanfill().and_then(|(i, v)| {
16873 let li = crate::gpu::cur_layer();
16874 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
16875 });
16876 for r in 0..rows {
16877 let base = r * inter;
16878 for (bi, &byte) in row.iter().enumerate() {
16879 if byte == 0xFF {
16880 continue;
16881 }
16882 let j0 = bi * 8;
16883 for bit in 0..8 {
16884 let j = j0 + bit;
16885 if j < inter && byte & (1 << bit) == 0 {
16886 g[base + j] = fill.map_or(0.0, |f| f[j]);
16887 }
16888 }
16889 }
16890 }
16891 let gain = mask_gain();
16892 if gain != 1.0 {
16893 for v in g[..rows * inter].iter_mut() {
16894 *v *= gain;
16895 }
16896 }
16897}
16898
16899#[inline]
16901fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
16902 row.is_none_or(|r| mask_bit(r, i))
16903}
16904
16905fn all_bits_on(row: &[u8], n: usize) -> bool {
16908 (0..n).all(|i| mask_bit(row, i))
16909}
16910
16911fn tube_topk() -> usize {
16919 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16920 *K.get_or_init(|| {
16921 std::env::var("CMF_TUBE_TOPK")
16922 .ok()
16923 .and_then(|v| v.parse().ok())
16924 .unwrap_or(0)
16925 })
16926}
16927
16928fn tube_score_oracle() -> bool {
16929 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16930 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16931}
16932
16933fn tube_ffn_routed(
16940 d: &DenseFfn,
16941 xs: &[f32],
16942 b: usize,
16943 pool: Option<&Pool>,
16944 mask_row: Option<&[u8]>,
16945 k: usize,
16946) -> Vec<f32> {
16947 let hidden = d.down_proj.rows();
16948 let core = d.gate_proj.rows();
16949 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16950 let mut out = match (b, core_full, mask_row) {
16951 (1, true, _) => dense_ffn(d, xs, pool),
16952 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16953 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16954 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16955 };
16956 let cand: Vec<usize> = (0..d.segs.len())
16957 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16958 .collect();
16959 if cand.is_empty() {
16960 return out;
16961 }
16962 let oracle = tube_score_oracle();
16966 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16967 let mut scores = vec![0f32; b * cand.len()];
16968 for (ci, &i) in cand.iter().enumerate() {
16969 let seg = &d.segs[i];
16970 let w = seg.width;
16971 let mut g = vec![0.0f32; b * w];
16972 if b == 1 {
16973 seg.gate.matvec(xs, &mut g, pool);
16974 } else {
16975 seg.gate.matmat(xs, b, &mut g, pool);
16976 }
16977 for v in g.iter_mut() {
16978 *v = Act::Silu.combine(*v, 1.0);
16979 }
16980 if !oracle {
16981 for t in 0..b {
16982 scores[t * cand.len() + ci] =
16983 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16984 }
16985 }
16986 if oracle || b > 1 {
16987 let mut u = vec![0.0f32; b * w];
16988 if b == 1 {
16989 seg.up.matvec(xs, &mut u, pool);
16990 } else {
16991 seg.up.matmat(xs, b, &mut u, pool);
16992 }
16993 for (a, &v) in g.iter_mut().zip(u.iter()) {
16994 *a *= v;
16995 }
16996 if oracle {
16997 for t in 0..b {
16998 scores[t * cand.len() + ci] =
16999 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17000 }
17001 }
17002 }
17003 acts.push(g);
17004 }
17005 let keep = k.min(cand.len());
17007 let mut scratch: Vec<f32> = Vec::new();
17008 for t in 0..b {
17009 let mut sc: Vec<(f32, usize)> = (0..cand.len())
17010 .map(|ci| (scores[t * cand.len() + ci], ci))
17011 .collect();
17012 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
17013 let mut alive = vec![false; cand.len()];
17014 for &(_, ci) in sc.iter().take(keep) {
17015 alive[ci] = true;
17016 }
17017 if b > 1 {
17018 for (ci, a) in acts.iter_mut().enumerate() {
17019 if !alive[ci] {
17020 let w = d.segs[cand[ci]].width;
17021 a[t * w..(t + 1) * w].fill(0.0);
17022 }
17023 }
17024 } else {
17025 for (ci, &i) in cand.iter().enumerate() {
17029 if !alive[ci] {
17030 continue;
17031 }
17032 let seg = &d.segs[i];
17033 let w = seg.width;
17034 let g = &mut acts[ci];
17035 if !tube_score_oracle() {
17036 scratch.clear();
17037 scratch.resize(w, 0.0);
17038 seg.up.matvec(xs, &mut scratch, pool);
17039 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17040 *a *= v;
17041 }
17042 }
17043 let mut acc = vec![0.0f32; hidden];
17044 seg.down.matvec(g, &mut acc, pool);
17045 for (o, a) in out.iter_mut().zip(&acc) {
17046 *o += *a;
17047 }
17048 }
17049 }
17050 }
17051 if b > 1 {
17052 for (ci, &i) in cand.iter().enumerate() {
17053 let seg = &d.segs[i];
17054 let mut acc = vec![0.0f32; b * hidden];
17055 seg.down.matmat(&acts[ci], b, &mut acc, pool);
17056 for (o, a) in out.iter_mut().zip(&acc) {
17057 *o += *a;
17058 }
17059 }
17060 }
17061 out
17062}
17063
17064fn tube_ffn(
17070 d: &DenseFfn,
17071 xs: &[f32],
17072 b: usize,
17073 pool: Option<&Pool>,
17074 mask_row: Option<&[u8]>,
17075) -> Vec<f32> {
17076 if tube_topk() > 0 {
17077 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17078 }
17079 let hidden = d.down_proj.rows();
17080 let core = d.gate_proj.rows();
17081 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17082 let mut out = match (b, core_full, mask_row) {
17083 (1, true, _) => dense_ffn(d, xs, pool),
17084 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17085 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17086 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17087 };
17088 TUBE_SCRATCH.with(|sc| {
17089 let mut sc = sc.borrow_mut();
17090 let [g, u, acc] = &mut *sc;
17091 for seg in &d.segs {
17092 if !tube_bit(mask_row, seg.start) {
17093 continue;
17094 }
17095 let w = seg.width;
17096 g.resize(b * w, 0.0);
17097 if b == 1
17098 && d.act == Act::Silu
17099 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17100 {
17101 } else {
17103 u.resize(b * w, 0.0);
17104 if b == 1 {
17105 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17106 } else {
17107 seg.gate.matmat(xs, b, g, pool);
17108 seg.up.matmat(xs, b, u, pool);
17109 }
17110 for i in 0..b * w {
17111 g[i] = d.act.combine(g[i], u[i]);
17112 }
17113 }
17114 acc.resize(b * hidden, 0.0);
17115 acc.fill(0.0);
17116 if b == 1 {
17117 seg.down.matvec(g, acc, pool);
17118 } else {
17119 seg.down.matmat(g, b, acc, pool);
17120 }
17121 for (o, a) in out.iter_mut().zip(acc.iter()) {
17122 *o += *a;
17123 }
17124 }
17125 out
17126 })
17127}
17128
17129thread_local! {
17130 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17134 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17135}
17136
17137static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
17140 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
17141
17142fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17146 #[cfg(feature = "gpu")]
17147 {
17148 use std::sync::atomic::Ordering::Relaxed;
17149 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17150 (
17151 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17152 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17153 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17154 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17155 )
17156 }
17157 #[cfg(not(feature = "gpu"))]
17158 (0.0, 0.0, 0.0, 0)
17159}
17160
17161fn wgpu_mirror_counters() -> [u64; 6] {
17164 #[cfg(feature = "gpu")]
17165 {
17166 crate::gpu_wgpu::MIRROR_EVENTS
17167 .each_ref()
17168 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17169 }
17170 #[cfg(not(feature = "gpu"))]
17171 [0; 6]
17172}
17173
17174fn prefill_prof_on() -> bool {
17175 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17176 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17177}
17178
17179fn dense_ffn_batch(
17180 d: &DenseFfn,
17181 xs: &[f32],
17182 b: usize,
17183 pool: Option<&Pool>,
17184 mask_row: Option<&[u8]>,
17185) -> Vec<f32> {
17186 let inter = d.gate_proj.rows();
17187 let hidden = d.down_proj.rows();
17188 let fused_act = d.act.graph_act();
17198 if mask_row.is_none()
17199 && fused_act.is_some()
17200 && b >= 32
17201 && crate::gpu::enabled_here()
17202 && !crate::gpu::mm_killed()
17203 && refit_dir().is_none()
17208 && !ffn_probe_active()
17213 && !crate::gptq_capture::capturing()
17217 {
17218 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17219 d.gate_proj.mapped_q4t(),
17220 d.up_proj.mapped_q4t(),
17221 d.down_proj.mapped_q4t(),
17222 ) {
17223 let mut out = vec![0.0f32; b * hidden];
17224 let act = fused_act.expect("checked above");
17225 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
17226 {
17227 return out;
17228 }
17229 }
17230 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17235 d.gate_proj.mapped_q4tp(),
17236 d.up_proj.mapped_q4tp(),
17237 d.down_proj.mapped_q4tp(),
17238 ) {
17239 let mut out = vec![0.0f32; b * hidden];
17240 let act = fused_act.expect("checked above");
17241 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17242 {
17243 return out;
17244 }
17245 }
17246 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17252 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17253 d.gate_proj.mapped_device_gemm(),
17254 d.up_proj.mapped_device_gemm(),
17255 d.down_proj.mapped_device_gemm(),
17256 ) {
17257 let mut out = vec![0.0f32; b * hidden];
17258 let act = fused_act.expect("checked above");
17259 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17260 return out;
17261 }
17262 }
17263 }
17264 let mut g = vec![0.0f32; b * inter];
17265 d.gate_proj.matmat(xs, b, &mut g, pool);
17266 let mut u = vec![0.0f32; b * inter];
17267 d.up_proj.matmat(xs, b, &mut u, pool);
17268 if gate_topk() > 0 && d.act == Act::Silu {
17269 for t in 0..b {
17270 let row = &mut g[t * inter..(t + 1) * inter];
17271 for v in row.iter_mut() {
17272 *v = Act::Silu.combine(*v, 1.0);
17273 }
17274 keep_top_k(row, gate_topk());
17275 }
17276 for i in 0..b * inter {
17277 g[i] *= u[i];
17278 }
17279 } else {
17280 for i in 0..b * inter {
17281 g[i] = d.act.combine(g[i], u[i]);
17282 }
17283 }
17284 if let Some(row) = mask_row {
17285 zero_masked_cols(&mut g, b, inter, row);
17286 }
17287 if oracle_topk() > 0 {
17288 for t in 0..b {
17289 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
17290 }
17291 }
17292 let mut out = vec![0.0f32; b * hidden];
17293 d.down_proj.matmat(&g, b, &mut out, pool);
17294 if refit_dir().is_some() {
17295 let li = crate::gpu::cur_layer();
17296 if li >= 0 {
17297 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
17298 }
17299 }
17300 FFN_PROBE.with(|pr| {
17304 if let Some(acc) = pr.borrow_mut().as_mut() {
17305 let li = crate::gpu::cur_layer();
17306 if li < 0 {
17307 return;
17308 }
17309 let Some(row) = acc.get_mut(li as usize) else {
17310 return;
17311 };
17312 let sq = probe_sq();
17313 for t in 0..b {
17314 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
17315 *a += if sq {
17316 (v as f64) * (v as f64)
17317 } else {
17318 (v as f64).abs()
17319 };
17320 }
17321 }
17322 }
17323 });
17324 out
17325}
17326
17327fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
17332 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17333 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17334 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
17335 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
17336 if (!on && !dump) || b == 0 {
17337 return;
17338 }
17339 let hidden = xs.len() / b;
17340 if on {
17341 let mut acc = m.act_sq.borrow_mut();
17342 if acc.len() < hidden {
17343 acc.resize(hidden, 0.0);
17344 }
17345 for t in 0..b {
17346 let row = &xs[t * hidden..(t + 1) * hidden];
17347 for (a, &v) in acc.iter_mut().zip(row) {
17348 *a += (v as f64) * (v as f64);
17349 }
17350 }
17351 }
17352 if dump {
17353 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
17356 .ok()
17357 .and_then(|v| v.parse().ok())
17358 .unwrap_or(4096);
17359 let mut rows = m.act_rows.borrow_mut();
17360 if rows.len() < cap * hidden {
17361 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17362 rows.extend_from_slice(&xs[..take * hidden]);
17363 }
17364 }
17365}
17366
17367#[derive(Clone, Copy)]
17370struct SendVecs(*mut Vec<f32>);
17371unsafe impl Send for SendVecs {}
17372unsafe impl Sync for SendVecs {}
17373impl SendVecs {
17374 #[inline]
17375 fn at(self, i: usize) -> *mut Vec<f32> {
17376 unsafe { self.0.add(i) }
17377 }
17378}
17379
17380fn moe_ffn_batch(
17381 m: &MoeFfn,
17382 xs: &[f32],
17383 b: usize,
17384 hidden: usize,
17385 pool: Option<&Pool>,
17386 allowed: Option<&[bool]>,
17387) -> Vec<f32> {
17388 accumulate_act(m, xs, b);
17389 let ne = m.experts.len();
17390 let mut logits = vec![0.0f32; b * ne];
17391 match &m.resonance {
17392 Some(r) => {
17393 let hdim = xs.len() / b.max(1);
17394 for bi in 0..b {
17395 r.scores(
17396 &xs[bi * hdim..(bi + 1) * hdim],
17397 &mut logits[bi * ne..(bi + 1) * ne],
17398 );
17399 }
17400 }
17401 None => m.router.matmat(xs, b, &mut logits, pool),
17402 }
17403
17404 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17407 {
17408 let mut st = m.stats.borrow_mut();
17409 if st.len() < ne {
17410 st.resize(ne, 0);
17411 }
17412 for bi in 0..b {
17413 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17414 for &e in &idx {
17415 st[e] += 1;
17416 assign[e].push((bi, p[e] / wsum));
17417 }
17418 }
17419 }
17420
17421 let mut out = vec![0.0f32; b * hidden];
17422 let cols = m.experts[0].gate_proj.cols();
17423 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17424 let sb = list.len();
17425 let mut sub = vec![0.0f32; sb * cols];
17426 for (k, &(bi, _)) in list.iter().enumerate() {
17427 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17428 }
17429 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17430 for (k, &(bi, w)) in list.iter().enumerate() {
17431 for i in 0..hidden {
17432 out[bi * hidden + i] += w * eo[k * hidden + i];
17433 }
17434 }
17435 };
17436 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17442 if pool.is_some() && active.len() >= 8 {
17443 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17444 {
17445 let panel_ptr = SendVecs(panels.as_mut_ptr());
17446 let experts = &m.experts;
17449 let (active_r, assign_r) = (&active, &assign);
17450 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17451 let run = |start: usize, end: usize| {
17452 let _cpu_scope = inherit_cpu();
17453 #[cfg(target_os = "macos")]
17461 let _metal_cpu = crate::gpu::enter_cpu_scope();
17462 for ai in start..end {
17463 let e = active_r[ai];
17464 let list = &assign_r[e];
17465 let sb = list.len();
17466 let mut sub = vec![0.0f32; sb * cols];
17467 for (k, &(bi, _)) in list.iter().enumerate() {
17468 sub[k * cols..(k + 1) * cols]
17469 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17470 }
17471 unsafe {
17473 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17474 }
17475 }
17476 };
17477 match pool {
17478 Some(p) => p.run_rows(active.len(), &run),
17479 None => run(0, active.len()),
17480 }
17481 }
17482 for (ai, &e) in active.iter().enumerate() {
17483 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17484 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17485 for i in 0..hidden {
17486 out[bi * hidden + i] += w * eo[i];
17487 }
17488 }
17489 }
17490 } else {
17491 for &e in &active {
17492 run_expert(&m.experts[e], &assign[e], &mut out);
17493 }
17494 }
17495 if let Some((se, gate)) = &m.shared {
17496 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17497 let mut gl = vec![0.0f32; b];
17498 gate.matmat(xs, b, &mut gl, pool);
17499 (0..b)
17500 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17501 .collect()
17502 } else {
17503 (0..b).map(|bi| (bi, 1.0)).collect()
17504 };
17505 run_expert(se, &all, &mut out);
17506 }
17507 out
17508}
17509
17510fn moe_ffn_rows_exact(
17522 m: &MoeFfn,
17523 xs: &[f32],
17524 b: usize,
17525 hidden: usize,
17526 pool: Option<&Pool>,
17527) -> Vec<f32> {
17528 let mut out = vec![0.0f32; b * hidden];
17529 let per_row = |out: &mut [f32]| {
17530 for r in 0..b {
17531 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17532 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17533 }
17534 };
17535 let covered = !crate::gpu::enabled_here()
17536 && moe_batch_enabled()
17537 && m.shared.is_none()
17538 && m.resonance.is_none()
17539 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17540 && m.experts.iter().all(|d| d.act == Act::Silu);
17541 if !covered {
17542 per_row(&mut out);
17543 return out;
17544 }
17545 let ne = m.experts.len();
17546 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17548 for r in 0..b {
17549 let x = &xs[r * hidden..(r + 1) * hidden];
17550 accumulate_act(m, x, 1);
17551 let mut logits = vec![0.0f32; ne];
17552 m.router.matvec(x, &mut logits, pool);
17553 let (idx, p, wsum) = moe_route(&logits, m, None);
17554 {
17555 let mut st = m.stats.borrow_mut();
17556 if st.len() < ne {
17557 st.resize(ne, 0);
17558 }
17559 for &e in &idx {
17560 st[e] += 1;
17561 }
17562 }
17563 let w: Vec<f32> = idx
17564 .iter()
17565 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17566 .collect();
17567 routes.push((idx, w));
17568 }
17569 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17570 per_row(&mut out);
17571 return out;
17572 }
17573 let mut experts: Vec<usize> = Vec::new();
17575 let mut groups: Vec<Vec<usize>> = Vec::new();
17576 for (r, (idx, _)) in routes.iter().enumerate() {
17577 for &e in idx {
17578 match experts.iter().position(|&x| x == e) {
17579 Some(g) => groups[g].push(r),
17580 None => {
17581 experts.push(e);
17582 groups.push(vec![r]);
17583 }
17584 }
17585 }
17586 }
17587 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17588 let inter = m.experts[experts[0]].gate_proj.rows();
17589 let pairs: Vec<(&QTensor, &QTensor)> = experts
17590 .iter()
17591 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17592 .collect();
17593 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17594 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17595 per_row(&mut out);
17596 return out;
17597 }
17598 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17599 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17600 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17601 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17602 per_row(&mut out);
17603 return out;
17604 }
17605 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17607 let mut p = 0usize;
17608 for (g, &e) in experts.iter().enumerate() {
17609 for &r in &groups[g] {
17610 slot.insert((r, e), p);
17611 p += 1;
17612 }
17613 }
17614 for (r, (idx, w)) in routes.iter().enumerate() {
17615 let terms: Vec<(&[f32], f32)> = idx
17616 .iter()
17617 .zip(w)
17618 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17619 .collect();
17620 let row = &mut out[r * hidden..(r + 1) * hidden];
17621 for (i, dst) in row.iter_mut().enumerate() {
17622 let mut acc = 0f32;
17624 for (d, we) in &terms {
17625 acc += we * d[i];
17626 }
17627 *dst = acc;
17628 }
17629 }
17630 out
17631}
17632
17633pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17637 match pool {
17638 Some(p) if v.len() >= 1 << 15 => {
17639 let base = v.as_mut_ptr() as usize;
17640 p.run_rows(v.len(), &|s, e| {
17641 let sl = unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17644 for x in sl {
17645 *x = c * (*x / c).tanh();
17646 }
17647 });
17648 }
17649 _ => {
17650 for x in v.iter_mut() {
17651 *x = c * (*x / c).tanh();
17652 }
17653 }
17654 }
17655}
17656
17657thread_local! {
17658 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17662 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17663}
17664
17665fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17667 if gate_topk() > 0
17670 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17671 {
17672 return out;
17673 }
17674 let prism_body = d.gate_proj.has_prism_contract()
17690 || d.up_proj.has_prism_contract()
17691 || d.down_proj.has_prism_contract();
17692 if !prism_body
17693 && crate::gpu::enabled_here()
17694 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17695 {
17696 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17697 crate::gpu::ProbeArm::Gpu
17698 } else {
17699 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17700 };
17701 match arm {
17702 crate::gpu::ProbeArm::Gpu => {
17703 let t0 = std::time::Instant::now();
17704 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17705 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17706 return out;
17707 }
17708 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17712 }
17713 crate::gpu::ProbeArm::CpuTimed => {
17714 let t0 = std::time::Instant::now();
17715 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17716 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17717 return out;
17718 }
17719 crate::gpu::ProbeArm::Cpu => {
17720 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17721 }
17722 }
17723 }
17724 dense_ffn_cpu(d, x, pool)
17725}
17726
17727fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17729 let inter = d.gate_proj.rows();
17730 FFN_SCRATCH.with(|s| {
17731 let mut s = s.borrow_mut();
17732 let [g, u, ..] = &mut *s;
17733 g.resize(inter, 0.0);
17734 if gate_topk() > 0 {
17737 u.resize(inter, 0.0);
17741 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17742 for i in 0..inter {
17743 g[i] = Act::Silu.combine(g[i], 1.0);
17744 }
17745 keep_top_k(g, gate_topk());
17746 for i in 0..inter {
17747 g[i] *= u[i];
17748 }
17749 } else if d.act == Act::Silu && {
17750 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17751 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17752 } {
17753 } else {
17755 u.resize(inter, 0.0);
17756 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17758 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17759 for i in 0..inter {
17760 g[i] = d.act.combine(g[i], u[i]);
17761 }
17762 }
17763 FFN_PROBE.with(|pr| {
17771 if let Some(acc) = pr.borrow_mut().as_mut() {
17772 let li = crate::gpu::cur_layer();
17773 if li >= 0 {
17774 if let Some(row) = acc.get_mut(li as usize) {
17775 match probe_topk() {
17776 0 if probe_sq() => {
17777 for (a, &v) in row.iter_mut().zip(g.iter()) {
17778 *a += (v as f64) * (v as f64);
17779 }
17780 }
17781 0 if probe_signed() => {
17782 for (a, &v) in row.iter_mut().zip(g.iter()) {
17783 *a += v as f64;
17784 }
17785 }
17786 0 => {
17787 for (a, &v) in row.iter_mut().zip(g.iter()) {
17788 *a += (v as f64).abs();
17789 }
17790 }
17791 k => {
17792 let n = g.len();
17793 let k = k.min(n);
17794 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17795 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17796 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17797 });
17798 let thr = *kth;
17799 for (a, &v) in row.iter_mut().zip(g.iter()) {
17800 if v.abs() >= thr {
17801 *a += 1.0;
17802 }
17803 }
17804 }
17805 }
17806 }
17807 }
17808 }
17809 });
17810 if oracle_topk() > 0 {
17811 keep_top_k(g, oracle_topk());
17812 }
17813 {
17814 let li = crate::gpu::cur_layer();
17815 if li >= 0 {
17816 adump_row(li as usize, g);
17817 }
17818 }
17819 let mut out = attention::take_buf(d.down_proj.rows());
17820 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
17821 d.down_proj.matvec(g, &mut out, pool);
17822 out
17823 })
17824}
17825
17826pub struct RefitAcc {
17839 pub support: Vec<u32>,
17840 pub gss: Vec<f32>,
17841 pub ya: Vec<f32>,
17842 pub hidden: usize,
17843 pub tokens: u64,
17844 pub buf_g: Vec<f32>,
17850 pub buf_o: Vec<f32>,
17851 pub buf_t: usize,
17852}
17853
17854type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
17858
17859static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
17860 std::sync::OnceLock::new();
17861
17862fn ffn_probe_active() -> bool {
17865 FFN_PROBE.with(|p| p.borrow().is_some())
17866}
17867
17868fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
17869 REFIT
17870 .get_or_init(|| {
17871 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
17872 (
17873 d,
17874 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
17875 )
17876 })
17877 })
17878 .as_ref()
17879}
17880
17881fn refit_accumulate(
17883 li: usize,
17884 g: &[f32],
17885 b: usize,
17886 inter: usize,
17887 out: &[f32],
17888 hidden: usize,
17889 pool: Option<&Pool>,
17890) {
17891 let Some((dir, map)) = refit_dir() else {
17892 return;
17893 };
17894 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17895 let (from, to) = *SPAN.get_or_init(|| {
17896 let g = |k: &str, d: usize| {
17897 std::env::var(k)
17898 .ok()
17899 .and_then(|v| v.parse().ok())
17900 .unwrap_or(d)
17901 };
17902 (
17903 g("CMF_FFN_REFIT_FROM", 0),
17904 g("CMF_FFN_REFIT_TO", usize::MAX),
17905 )
17906 });
17907 if li < from || li > to {
17908 return;
17909 }
17910 let mut guard = map.lock().unwrap();
17911 let (map, shared) = &mut *guard;
17912 let acc = match map.entry(li) {
17913 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
17914 std::collections::hash_map::Entry::Vacant(e) => {
17915 let path = format!("{dir}/support.{li}.u32");
17916 let Ok(bytes) = std::fs::read(&path) else {
17917 eprintln!("refit: no {path} — layer {li} skipped");
17918 return;
17919 };
17920 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
17921 let support: Vec<u32> = bytes[4..4 + n * 4]
17922 .chunks_exact(4)
17923 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17924 .collect();
17925 eprintln!(
17926 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
17927 (n * n + hidden * n) as f64 * 4.0 / 1e6
17928 );
17929 e.insert(RefitAcc {
17930 gss: vec![0.0; n * n],
17931 ya: vec![0.0; hidden * n],
17932 buf_g: Vec::new(),
17933 buf_o: Vec::new(),
17934 buf_t: 0,
17935 support,
17936 hidden,
17937 tokens: 0,
17938 })
17939 }
17940 };
17941 let ns = acc.support.len();
17942 let cap = refit_batch();
17944 if acc.buf_g.is_empty() {
17945 acc.buf_g = vec![0.0; ns * cap];
17946 acc.buf_o = vec![0.0; hidden * cap];
17947 }
17948 let take = b.min(cap - acc.buf_t);
17949 for t in 0..take {
17950 let col = acc.buf_t + t;
17951 for (j, &n) in acc.support.iter().enumerate() {
17952 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17953 }
17954 for h in 0..hidden {
17955 acc.buf_o[h * cap + col] = out[t * hidden + h];
17956 }
17957 }
17958 acc.buf_t += take;
17959 acc.tokens += take as u64;
17960 if acc.buf_t < cap {
17961 return;
17962 }
17963 let bt = acc.buf_t;
17964 acc.buf_t = 0;
17965 let RefitAcc {
17975 gss,
17976 ya,
17977 buf_g,
17978 buf_o,
17979 ..
17980 } = acc;
17981 let need = (ns * ns).max(hidden * ns);
17982 if shared.len() < need {
17983 shared.resize(need, 0.0);
17984 }
17985 let scratch = &mut shared[..];
17986 let _ = bt;
17987 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17988 add_into(gss, &scratch[..ns * ns], pool);
17989 if crate::gpu::gemm_nt_f32_transient(
17990 buf_o,
17991 buf_g,
17992 &mut scratch[..hidden * ns],
17993 hidden,
17994 cap,
17995 ns,
17996 ) {
17997 add_into(ya, &scratch[..hidden * ns], pool);
17998 } else {
17999 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18000 }
18001 } else {
18002 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
18003 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18004 }
18005 }
18009
18010fn refit_batch() -> usize {
18012 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18013 *B.get_or_init(|| {
18014 std::env::var("CMF_FFN_REFIT_BATCH")
18015 .ok()
18016 .and_then(|v| v.parse().ok())
18017 .unwrap_or(4096)
18018 })
18019}
18020
18021fn accum_outer_t(
18024 c: &mut [f32],
18025 m: usize,
18026 n: usize,
18027 b: usize,
18028 left: &[f32],
18029 right: &[f32],
18030 pool: Option<&Pool>,
18031) {
18032 let ptr = SendMut(c.as_mut_ptr());
18033 let body = |i: usize| {
18034 let ptr = &ptr;
18035 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18036 for t in 0..b {
18037 let a = left[i * b + t];
18038 if a == 0.0 {
18039 continue;
18040 }
18041 for (j, o) in row.iter_mut().enumerate() {
18042 *o += a * right[j * b + t];
18043 }
18044 }
18045 };
18046 match pool {
18047 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18048 for i in s..e {
18049 body(i);
18050 }
18051 }),
18052 _ => {
18053 for i in 0..m {
18054 body(i);
18055 }
18056 }
18057 }
18058}
18059
18060fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18063 let n = dst.len().min(src.len());
18064 match pool {
18065 Some(p) if n >= 1 << 16 => {
18066 let ptr = SendMut(dst.as_mut_ptr());
18067 let f = |s: usize, e: usize| {
18068 let ptr = &ptr;
18069 for blk in s..e {
18070 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18071 for i in a..b {
18072 unsafe { *ptr.0.add(i) += src[i] };
18073 }
18074 }
18075 };
18076 p.run_rows(n.div_ceil(4096), &f);
18077 }
18078 _ => {
18079 for (d, v) in dst.iter_mut().zip(&src[..n]) {
18080 *d += *v;
18081 }
18082 }
18083 }
18084}
18085
18086fn accum_outer(
18091 c: &mut [f32],
18092 m: usize,
18093 n: usize,
18094 b: usize,
18095 left: &[f32],
18096 right: &[f32],
18097 pool: Option<&Pool>,
18098) {
18099 const TILE: usize = 32;
18100 let tiles = m.div_ceil(TILE);
18101 let cp = SendMut(c.as_mut_ptr());
18102 let body = |ti: usize| {
18103 let cp = &cp;
18104 let i0 = ti * TILE;
18105 let i1 = (i0 + TILE).min(m);
18106 for t in 0..b {
18107 let r = &right[t * n..t * n + n];
18108 for i in i0..i1 {
18109 let a = left[i * b + t];
18110 if a == 0.0 {
18111 continue;
18112 }
18113 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18115 for (o, v) in row.iter_mut().zip(r) {
18116 *o += a * *v;
18117 }
18118 }
18119 }
18120 };
18121 match pool {
18122 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18123 for ti in s..e {
18124 body(ti);
18125 }
18126 }),
18127 _ => {
18128 for ti in 0..tiles {
18129 body(ti);
18130 }
18131 }
18132 }
18133}
18134
18135pub fn refit_flush() -> usize {
18137 let Some((dir, map)) = refit_dir() else {
18138 return 0;
18139 };
18140 let guard = map.lock().unwrap();
18141 let mut n = 0;
18142 for (li, acc) in guard.0.iter() {
18143 let w = |name: &str, v: &[f32]| {
18146 let path = format!("{dir}/{name}.{li}.f32");
18147 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18148 match std::fs::write(&path, &bytes) {
18149 Ok(()) => {}
18150 Err(e) => eprintln!(
18151 "refit: FAILED to write {path} ({} MB): {e}",
18152 bytes.len() / 1_000_000
18153 ),
18154 }
18155 };
18156 w("gss", &acc.gss);
18157 w("ya", &acc.ya);
18158 println!(
18159 "refit L{li}: {} support, {} tokens, hidden {}",
18160 acc.support.len(),
18161 acc.tokens,
18162 acc.hidden
18163 );
18164 n += 1;
18165 }
18166 n
18167}
18168
18169fn adump_row(li: usize, g: &[f32]) {
18174 use std::io::Write as _;
18175 static FILES: std::sync::OnceLock<
18176 Option<(
18177 String,
18178 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18179 )>,
18180 > = std::sync::OnceLock::new();
18181 let Some((prefix, map)) = FILES
18182 .get_or_init(|| {
18183 std::env::var("CMF_FFN_ADUMP")
18184 .ok()
18185 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18186 })
18187 .as_ref()
18188 else {
18189 return;
18190 };
18191 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18194 let (from, to) = *SPAN.get_or_init(|| {
18195 let g = |k: &str, d: usize| {
18196 std::env::var(k)
18197 .ok()
18198 .and_then(|v| v.parse().ok())
18199 .unwrap_or(d)
18200 };
18201 (
18202 g("CMF_FFN_ADUMP_FROM", 0),
18203 g("CMF_FFN_ADUMP_TO", usize::MAX),
18204 )
18205 });
18206 if li < from || li > to {
18207 return;
18208 }
18209 let mut map = map.lock().unwrap();
18210 let f = map.entry(li).or_insert_with(|| {
18211 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18212 });
18213 let mut bytes = Vec::with_capacity(g.len() * 2);
18214 for v in g {
18215 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18216 }
18217 let _ = f.write_all(&bytes);
18218}
18219
18220fn oracle_topk() -> usize {
18226 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18227 *K.get_or_init(|| {
18228 std::env::var("CMF_FFN_ORACLE_TOPK")
18229 .ok()
18230 .and_then(|v| v.parse().ok())
18231 .unwrap_or(0)
18232 })
18233}
18234
18235fn gate_topk() -> usize {
18241 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18242 *K.get_or_init(|| {
18243 std::env::var("CMF_FFN_GATE_TOPK")
18244 .ok()
18245 .and_then(|v| v.parse().ok())
18246 .unwrap_or(0)
18247 })
18248}
18249
18250fn gate_block() -> usize {
18257 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18258 *B.get_or_init(|| {
18259 std::env::var("CMF_FFN_GATE_BLOCK")
18260 .ok()
18261 .and_then(|v| v.parse().ok())
18262 .unwrap_or(1)
18263 })
18264}
18265
18266fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18268 let n = g.len();
18269 let nb = n.div_ceil(block);
18270 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18271 if kb >= nb {
18272 return;
18273 }
18274 let mut score: Vec<f32> = (0..nb)
18275 .map(|b| {
18276 g[b * block..((b + 1) * block).min(n)]
18277 .iter()
18278 .map(|v| v * v)
18279 .sum::<f32>()
18280 })
18281 .collect();
18282 let mut ord = score.clone();
18283 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
18284 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18285 });
18286 let thr = *kth;
18287 for b in 0..nb {
18288 if score[b] < thr {
18289 g[b * block..((b + 1) * block).min(n)].fill(0.0);
18290 }
18291 }
18292 score.clear();
18293}
18294
18295fn keep_top_k(g: &mut [f32], k: usize) {
18297 if gate_block() > 1 {
18298 return keep_top_blocks(g, k, gate_block());
18299 }
18300 let n = g.len();
18301 if k == 0 || k >= n {
18302 return;
18303 }
18304 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18305 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18306 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18307 });
18308 let thr = *kth;
18309 for v in g.iter_mut() {
18310 if v.abs() < thr {
18311 *v = 0.0;
18312 }
18313 }
18314}
18315
18316fn probe_sq() -> bool {
18320 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18321 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
18322}
18323
18324fn probe_signed() -> bool {
18328 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18329 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
18330}
18331
18332fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
18340 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
18341 M.get_or_init(|| {
18342 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
18343 let b = std::fs::read(&p).ok()?;
18344 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
18345 let vals: Vec<f32> = b[8..]
18346 .chunks_exact(4)
18347 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18348 .collect();
18349 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
18350 Some((inter, vals))
18351 })
18352 .as_ref()
18353}
18354
18355fn probe_topk() -> usize {
18358 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18359 *K.get_or_init(|| {
18360 std::env::var("CMF_FFN_PROBE_TOPK")
18361 .ok()
18362 .and_then(|v| v.parse().ok())
18363 .unwrap_or(0)
18364 })
18365}
18366
18367thread_local! {
18368 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18371 const { std::cell::RefCell::new(None) };
18372}
18373
18374fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18387 if d.gate_proj.has_prism_contract()
18392 || d.up_proj.has_prism_contract()
18393 || d.down_proj.has_prism_contract()
18394 {
18395 return None;
18396 }
18397 let dt = d.down_t.as_ref()?;
18398 let inter = d.gate_proj.rows();
18399 let hidden = dt.cols();
18400 if k == 0 || k >= inter || d.act != Act::Silu {
18401 return None;
18402 }
18403 DYN_SCRATCH.with(|sc| {
18404 let mut sc = sc.borrow_mut();
18405 let DynScratch {
18406 g,
18407 mag,
18408 live,
18409 parts,
18410 } = &mut *sc;
18411 g.resize(inter, 0.0);
18412 d.gate_proj.matvec(x, g, pool);
18413 for v in g.iter_mut() {
18414 *v = inference::silu(*v);
18415 }
18416 mag.clear();
18419 mag.extend(g.iter().map(|v| v.abs()));
18420 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18421 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18422 });
18423 let thr = *kth;
18424 live.clear();
18425 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18426 let mut out = vec![0.0f32; hidden];
18427 match pool {
18428 Some(p) if live.len() >= 64 => {
18429 let nw = p.n_workers() + 1;
18430 parts.clear();
18431 parts.resize(nw * hidden, 0.0);
18432 let ptr = SendMut(parts.as_mut_ptr());
18433 let n = live.len();
18434 let live_ref: &[u32] = live;
18435 let g_ref: &[f32] = g;
18436 p.run(&|w, workers| {
18437 let chunk = n.div_ceil(workers);
18438 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18439 if s >= e {
18440 return;
18441 }
18442 WORKER_SCRATCH.with(|ws| {
18443 let mut ws = ws.borrow_mut();
18444 let [scratch, acc] = &mut *ws;
18445 scratch.resize(hidden.max(x.len()), 0.0);
18446 acc.clear();
18447 acc.resize(hidden, 0.0);
18448 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18449 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18452 d.up_proj.prefetch_row(nx as usize);
18453 dt.prefetch_row(nx as usize);
18454 }
18455 let idx = nrm as usize;
18456 let up = d.up_proj.row_dot(idx, x, scratch);
18457 let a = g_ref[idx] * up;
18458 if a != 0.0 {
18459 dt.add_row_scaled(idx, a, acc, scratch);
18460 }
18461 }
18462 for (j, v) in acc.iter().enumerate() {
18463 unsafe { *ptr.at(w * hidden + j) = *v };
18464 }
18465 });
18466 });
18467 for w in 0..nw {
18468 for (j, o) in out.iter_mut().enumerate() {
18469 *o += parts[w * hidden + j];
18470 }
18471 }
18472 }
18473 _ => {
18474 WORKER_SCRATCH.with(|ws| {
18475 let mut ws = ws.borrow_mut();
18476 let [scratch, _acc] = &mut *ws;
18477 scratch.resize(hidden.max(x.len()), 0.0);
18478 for &nrm in live.iter() {
18479 let idx = nrm as usize;
18480 let up = d.up_proj.row_dot(idx, x, scratch);
18481 let a = g[idx] * up;
18482 if a != 0.0 {
18483 dt.add_row_scaled(idx, a, &mut out, scratch);
18484 }
18485 }
18486 });
18487 }
18488 }
18489 Some(out)
18490 })
18491}
18492
18493struct DynScratch {
18496 g: Vec<f32>,
18497 mag: Vec<f32>,
18498 live: Vec<u32>,
18499 parts: Vec<f32>,
18500}
18501
18502thread_local! {
18503 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18504 std::cell::RefCell::new(DynScratch {
18505 g: Vec::new(),
18506 mag: Vec::new(),
18507 live: Vec::new(),
18508 parts: Vec::new(),
18509 })
18510 };
18511 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18513 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18514}
18515
18516fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18521 let inter = d.gate_proj.rows();
18522 FFN_SCRATCH.with(|s| {
18523 let mut s = s.borrow_mut();
18524 let [g, u, ..] = &mut *s;
18525 g.resize(inter, 0.0);
18526 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18527 } else {
18529 u.resize(inter, 0.0);
18530 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18531 for i in 0..inter {
18532 g[i] = d.act.combine(g[i], u[i]);
18533 }
18534 }
18535 zero_masked_cols(g, 1, inter, mask_row);
18536 let mut out = attention::take_buf(d.down_proj.rows());
18537 d.down_proj.matvec(g, &mut out, pool);
18538 out
18539 })
18540}
18541
18542fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18548 if d.gate_proj.has_prism_contract()
18549 || d.up_proj.has_prism_contract()
18550 || d.down_proj.has_prism_contract()
18551 {
18552 return None;
18553 }
18554 d.act.graph_act()?;
18557 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18560 return None;
18561 }
18562 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18563 let mut model_ref = None;
18564 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18565 let model = model_ref?;
18566 let hidden = jobs[0].down.1;
18567 let mut out = attention::take_buf(hidden);
18568 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18569 Some(out)
18570 } else {
18571 let mut out = out;
18572 attention::recycle_buf(&mut out);
18573 None
18574 }
18575}
18576
18577#[allow(clippy::type_complexity)]
18582#[allow(clippy::type_complexity)]
18583pub(crate) fn moe_parts(
18584 t: &QTensor,
18585) -> Option<(
18586 &std::sync::Arc<cortiq_core::CmfModel>,
18587 usize,
18588 usize,
18589 usize,
18590 &[f32],
18591 &[f32],
18592 bool,
18593 bool,
18594 bool,
18595)> {
18596 match t {
18597 QTensor::Mapped {
18598 model,
18599 idx,
18600 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18601 rows,
18602 cols,
18603 row_scale,
18604 col_field,
18605 ..
18606 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18607 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18608 )),
18609 QTensor::Mapped {
18611 model,
18612 idx,
18613 dtype: cortiq_core::TensorDtype::Q1,
18614 rows,
18615 cols,
18616 ..
18617 } => Some((
18618 model,
18619 *idx,
18620 *rows,
18621 *cols,
18622 &[][..],
18623 &[][..],
18624 true,
18625 false,
18626 false,
18627 )),
18628 QTensor::Mapped {
18630 model,
18631 idx,
18632 dtype: cortiq_core::TensorDtype::Q4Tiled,
18633 rows,
18634 cols,
18635 ..
18636 } => Some((
18637 model,
18638 *idx,
18639 *rows,
18640 *cols,
18641 &[][..],
18642 &[][..],
18643 false,
18644 true,
18645 false,
18646 )),
18647 QTensor::Mapped {
18649 model,
18650 idx,
18651 dtype: cortiq_core::TensorDtype::Q4TiledP,
18652 rows,
18653 cols,
18654 ..
18655 } => Some((
18656 model,
18657 *idx,
18658 *rows,
18659 *cols,
18660 &[][..],
18661 &[][..],
18662 false,
18663 true,
18664 false,
18665 )),
18666 QTensor::Mapped {
18670 model,
18671 idx,
18672 dtype: cortiq_core::TensorDtype::Q2TiledP,
18673 rows,
18674 cols,
18675 ..
18676 } => Some((
18677 model,
18678 *idx,
18679 *rows,
18680 *cols,
18681 &[][..],
18682 &[][..],
18683 false,
18684 true,
18685 true,
18686 )),
18687 _ => None,
18688 }
18689}
18690
18691#[cfg(target_os = "macos")]
18698#[allow(clippy::type_complexity)]
18699fn chunk_moe_parts(m: &MoeFfn, hidden: usize) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18700 if m.shared.is_some()
18701 || m.per_expert_scale.is_some()
18702 || m.resonance.is_some()
18703 || m.route_tau.is_some()
18704 || m.router_input_norm
18705 || m.experts.is_empty()
18706 || m.top_k == 0
18707 || std::env::var("CMF_RMS_TRACE").is_ok()
18708 || std::env::var("CMF_ACT_DUMP").is_ok()
18709 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18710 {
18711 return None;
18712 }
18713 let (rf, rr, rc) = m.router.f32_parts()?;
18714 if rr != m.experts.len() || rc != hidden {
18715 return None;
18716 }
18717 let inter = m.experts[0].gate_proj.rows();
18718 let q8 = crate::gpu_metal::moe_q8::enabled()
18721 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18722 let experts = m
18723 .experts
18724 .iter()
18725 .map(|e| {
18726 if e.act != Act::Silu
18727 || !e.segs.is_empty()
18728 || e.gate_proj.rows() != inter
18729 || e.up_proj.rows() != inter
18730 || e.down_proj.cols() != inter
18731 || e.gate_proj.cols() != hidden
18732 || e.down_proj.rows() != hidden
18733 {
18734 return None;
18735 }
18736 if q8 {
18737 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18738 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18739 }
18740 Some((
18741 e.gate_proj.mapped_q4tp()?.1,
18742 e.up_proj.mapped_q4tp()?.1,
18743 e.down_proj.mapped_q4tp()?.1,
18744 ))
18745 })
18746 .collect::<Option<Vec<_>>>()?;
18747 Some((rf, experts, q8))
18748}
18749
18750#[cfg(target_os = "macos")]
18758fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
18759 if m.router_input_norm
18760 || m.route_tau.is_some()
18761 || m.mask.is_some()
18762 || m.per_expert_scale.is_some()
18763 || m.experts.is_empty()
18764 || m.top_k == 0
18765 || m.resonance.is_some()
18766 {
18767 return None;
18768 }
18769 let (sh, sg) = match &m.shared {
18774 Some((sh, sg)) => (Some(sh), sg.as_ref()),
18775 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
18776 None => return None,
18777 };
18778 let (rf, rr, rc) = m.router.f32_parts()?;
18779 if rr != m.experts.len() || rc != hidden {
18780 return None;
18781 }
18782 let shared_gated = sg.is_some();
18783 let sf = match sg {
18784 Some(sg) => {
18785 let (sf, sr, sc) = sg.f32_parts()?;
18786 if sr * sc != hidden {
18787 return None;
18788 }
18789 sf
18790 }
18791 None => &rf[..hidden],
18794 };
18795 if let Some(b) = &m.expert_bias {
18796 if b.len() != m.experts.len() {
18797 return None;
18798 }
18799 }
18800 let inter = m.experts[0].gate_proj.rows();
18801 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
18804 let q8 = crate::gpu_metal::moe_q8::enabled()
18808 && m.shared.is_none()
18809 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18810 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
18811 if e.act != Act::Silu
18812 || e.gate_proj.rows() != inter
18813 || e.gate_proj.cols() != hidden
18814 || e.up_proj.rows() != inter
18815 || e.up_proj.cols() != hidden
18816 || e.down_proj.rows() != hidden
18817 || e.down_proj.cols() != inter
18818 {
18819 return None;
18820 }
18821 if q8 {
18822 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18823 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18824 }
18825 let pick = |t: &QTensor| -> Option<usize> {
18826 if gu_q2 {
18827 t.mapped_q2tp().map(|(_, i)| i)
18828 } else {
18829 t.mapped_q4tp().map(|(_, i)| i)
18830 }
18831 };
18832 Some((
18833 pick(&e.gate_proj)?,
18834 pick(&e.up_proj)?,
18835 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
18836 ))
18837 };
18838 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
18839 let shared = match sh {
18840 Some(sh) => Some(trio(sh)?),
18841 None => None,
18842 };
18843 Some(crate::gpu::GpuMoe {
18844 router: rf,
18845 sgate: sf,
18846 experts,
18847 shared,
18848 n_exp: m.experts.len(),
18849 top_k: m.top_k,
18850 inter,
18851 norm_topk: m.norm_topk_prob,
18852 route_scale: m.routed_scaling,
18853 gu_q2,
18854 q8,
18855 sigmoid: m.router_sigmoid,
18856 bias: m.expert_bias.as_deref(),
18857 shared_gated,
18858 })
18859}
18860
18861fn forced_ids_at(step: usize) -> Option<u32> {
18867 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
18868 IDS.get_or_init(|| {
18869 std::env::var("CMF_FORCE_IDS")
18870 .ok()
18871 .and_then(|p| std::fs::read_to_string(p).ok())
18872 .map(|s| s.split_whitespace().filter_map(|t| t.parse().ok()).collect())
18873 .unwrap_or_default()
18874 })
18875 .get(step)
18876 .copied()
18877}
18878
18879pub(crate) fn moe_push_job_parts<'a>(
18883 gate: &'a QTensor,
18884 up: &'a QTensor,
18885 down: &'a QTensor,
18886 x: &[f32],
18887 w: f32,
18888 swiglu_limit: f32,
18889 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18890 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18891) -> Option<()> {
18892 use crate::qtensor::prescale;
18893 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
18894 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
18895 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
18896 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18897 return None; }
18899 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
18902 return None;
18903 }
18904 if !gq2 && dq2 {
18905 return None;
18906 }
18907 model_ref.get_or_insert_with(|| gm.clone());
18908 let dt = |cf: &[f32]| {
18909 if cf.is_empty() {
18910 cortiq_core::TensorDtype::Q8Row
18911 } else {
18912 cortiq_core::TensorDtype::Q8_2f
18913 }
18914 };
18915 jobs.push(crate::gpu::MoeJob {
18916 gate: (gi, gr, gc, grs),
18917 up: (ui, ur, uc, urs),
18918 down: (di, dr, dc, drs),
18919 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
18920 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
18921 down_col: dcf,
18922 w,
18923 q1: gq1,
18924 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
18925 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
18926 gu_q2: gq2,
18927 swiglu_limit,
18928 act: 0,
18929 });
18930 Some(())
18931}
18932
18933fn moe_push_job<'a>(
18935 d: &'a DenseFfn,
18936 x: &[f32],
18937 w: f32,
18938 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18939 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18940) -> Option<()> {
18941 use crate::qtensor::prescale;
18942 let act = d.act.graph_act()?.code();
18945 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
18946 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
18947 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
18948 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18949 return None; }
18951 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
18952 return None;
18953 }
18954 if !gq2 && dq2 {
18955 return None;
18956 }
18957 model_ref.get_or_insert_with(|| gm.clone());
18958 let gdt = if gcf.is_empty() {
18959 cortiq_core::TensorDtype::Q8Row
18960 } else {
18961 cortiq_core::TensorDtype::Q8_2f
18962 };
18963 let udt = if ucf.is_empty() {
18964 cortiq_core::TensorDtype::Q8Row
18965 } else {
18966 cortiq_core::TensorDtype::Q8_2f
18967 };
18968 jobs.push(crate::gpu::MoeJob {
18969 gate: (gi, gr, gc, grs),
18970 up: (ui, ur, uc, urs),
18971 down: (di, dr, dc, drs),
18972 xs_gate: prescale(x, gcf, gdt).into_owned(),
18973 xs_up: prescale(x, ucf, udt).into_owned(),
18974 down_col: dcf,
18975 w,
18976 q1: gq1,
18977 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
18978 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
18979 gu_q2: gq2,
18980 swiglu_limit: 0.0,
18981 act,
18982 });
18983 Some(())
18984}
18985
18986fn sparse_ffn_quant(
18993 d: &DenseFfn,
18994 x: &[f32],
18995 active: &[u16],
18996 hidden: usize,
18997 pool: Option<&Pool>,
18998) -> Vec<f32> {
18999 let n = active.len();
19000 let inter = d.gate_proj.rows();
19001 let mut act = vec![0.0f32; n];
19002 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
19005 let compute = |ai: usize| -> f32 {
19006 let idx = active[ai] as usize;
19007 if idx >= inter {
19008 return 0.0; }
19010 let mut s = if need_scratch {
19011 vec![0.0f32; hidden]
19012 } else {
19013 Vec::new()
19014 };
19015 let gate = d.gate_proj.row_dot(idx, x, &mut s);
19016 let up = d.up_proj.row_dot(idx, x, &mut s);
19017 d.act.combine(gate, up)
19018 };
19019 match pool {
19020 Some(p) if n >= 256 => {
19021 let ptr = SendMut(act.as_mut_ptr());
19022 p.run(&|widx, nw| {
19023 let chunk = n.div_ceil(nw);
19024 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19025 for ai in s..e {
19026 unsafe { *ptr.at(ai) = compute(ai) };
19027 }
19028 });
19029 }
19030 _ => {
19031 for (ai, a) in act.iter_mut().enumerate() {
19032 *a = compute(ai);
19033 }
19034 }
19035 }
19036 let mut out = vec![0.0f32; hidden];
19038 for (ai, &idx) in active.iter().enumerate() {
19039 let w = act[ai];
19040 if w.abs() >= 1e-12 && (idx as usize) < inter {
19041 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19042 }
19043 }
19044 out
19045}
19046
19047#[doc(hidden)]
19049pub fn sparse_ffn_quant_for_test(
19050 d: &DenseFfn,
19051 x: &[f32],
19052 active: &[u16],
19053 hidden: usize,
19054) -> Vec<f32> {
19055 sparse_ffn_quant(d, x, active, hidden, None)
19056}
19057
19058fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19062 let deq = |t: &QTensor| -> Vec<f32> {
19063 let (rows, cols) = (t.rows(), t.cols());
19064 let mut out = vec![0.0f32; rows * cols];
19065 for r in 0..rows {
19066 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19067 }
19068 out
19069 };
19070 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19071}
19072
19073struct SendMut(*mut f32);
19075unsafe impl Send for SendMut {}
19076unsafe impl Sync for SendMut {}
19077impl SendMut {
19078 #[inline]
19079 #[allow(clippy::mut_from_ref)]
19082 unsafe fn at(&self, i: usize) -> &mut f32 {
19083 unsafe { &mut *self.0.add(i) }
19084 }
19085}
19086
19087pub(crate) fn moe_route(
19100 logits: &[f32],
19101 m: &MoeFfn,
19102 allowed: Option<&[bool]>,
19103) -> (Vec<usize>, Vec<f32>, f32) {
19104 moe_route_with_eps(logits, m, allowed, 1e-6)
19105}
19106
19107pub(crate) fn moe_route_with_eps(
19116 logits: &[f32],
19117 m: &MoeFfn,
19118 allowed: Option<&[bool]>,
19119 sigmoid_denom_eps: f32,
19120) -> (Vec<usize>, Vec<f32>, f32) {
19121 let ne = logits.len();
19122 let admit = |e: usize| {
19128 m.mask.as_ref().is_none_or(|mk| mk[e])
19129 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19130 };
19131 if m.resonance.is_some() && m.top_k == 1 {
19144 let mut best: Option<usize> = None;
19145 for e in (0..ne).filter(|&e| admit(e)) {
19146 let l = logits[e];
19147 if l == f32::NEG_INFINITY || l.is_nan() {
19148 continue;
19149 }
19150 if best.is_none_or(|b| l > logits[b]) {
19151 best = Some(e);
19152 }
19153 }
19154 if let Some(b) = best {
19155 let mut p = vec![0.0f32; ne];
19156 p[b] = 1.0;
19157 return (vec![b], p, 1.0 / m.routed_scaling);
19158 }
19159 }
19160 let p: Vec<f32> = if m.router_sigmoid {
19166 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19167 } else {
19168 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19169 if mx == f32::NEG_INFINITY {
19170 vec![1.0 / ne.max(1) as f32; ne]
19171 } else {
19172 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
19173 let s: f32 = e.iter().sum();
19174 for v in &mut e {
19175 *v /= s;
19176 }
19177 e
19178 }
19179 };
19180 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
19181 match &m.expert_bias {
19183 Some(b) => idx.sort_unstable_by(|&x, &y| {
19184 (p[y] + b[y])
19185 .partial_cmp(&(p[x] + b[x]))
19186 .unwrap()
19187 .then(x.cmp(&y))
19188 }),
19189 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
19190 }
19191 idx.truncate(m.top_k);
19192 if let Some(tau) = m.route_tau {
19196 let total: f32 = idx.iter().map(|&e| p[e]).sum();
19197 if total > 0.0 {
19198 let mut acc = 0.0f32;
19199 let mut keep = idx.len();
19200 for (i, &e) in idx.iter().enumerate() {
19201 acc += p[e];
19202 if acc >= tau * total {
19203 keep = i + 1;
19204 break;
19205 }
19206 }
19207 idx.truncate(keep);
19208 }
19209 }
19210 let wsum: f32 = if m.norm_topk_prob {
19211 let s: f32 = idx.iter().map(|&e| p[e]).sum();
19212 (if m.router_sigmoid {
19216 s + sigmoid_denom_eps
19217 } else {
19218 s
19219 }) / m.routed_scaling
19220 } else {
19221 1.0 / m.routed_scaling
19222 };
19223 (idx, p, wsum)
19224}
19225
19226fn moe_trace(idx: &[usize]) {
19228 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
19229}
19230
19231pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
19234 use std::io::Write;
19235 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
19236 std::sync::OnceLock::new();
19237 let Some(f) = F.get_or_init(|| {
19238 let p = std::env::var("CMF_MOE_TRACE").ok()?;
19239 Some(std::sync::Mutex::new(
19240 std::fs::OpenOptions::new()
19241 .create(true)
19242 .append(true)
19243 .open(p)
19244 .ok()?,
19245 ))
19246 }) else {
19247 return;
19248 };
19249 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
19250 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
19251}
19252
19253pub(crate) fn moe_ffn(
19256 m: &MoeFfn,
19257 x: &[f32],
19258 pool: Option<&Pool>,
19259 allowed: Option<&[bool]>,
19260) -> Vec<f32> {
19261 let r = moe_ffn_route(m, x, pool, allowed);
19262 moe_ffn_experts(m, x, &r, pool)
19263}
19264
19265pub(crate) struct MoeRoute {
19269 pub idx: Vec<usize>,
19270 pub p: Vec<f32>,
19271 pub wsum: f32,
19272 pub logits: Vec<f32>,
19273}
19274
19275pub(crate) fn moe_ffn_route(
19281 m: &MoeFfn,
19282 x: &[f32],
19283 pool: Option<&Pool>,
19284 allowed: Option<&[bool]>,
19285) -> MoeRoute {
19286 accumulate_act(m, x, 1);
19287 let ne = m.experts.len();
19288 let mut logits = vec![0.0f32; ne];
19289 match &m.resonance {
19290 Some(r) => r.scores(x, &mut logits),
19291 None => m.router.matvec(x, &mut logits, pool),
19292 }
19293 let (idx, p, wsum) = moe_route(&logits, m, allowed);
19294 {
19295 let mut st = m.stats.borrow_mut();
19296 if st.len() < ne {
19297 st.resize(ne, 0);
19298 }
19299 for &e in &idx {
19300 st[e] += 1;
19301 }
19302 }
19303 moe_trace(&idx);
19309 MoeRoute {
19310 idx,
19311 p,
19312 wsum,
19313 logits,
19314 }
19315}
19316
19317pub(crate) fn moe_ffn_experts(
19320 m: &MoeFfn,
19321 x: &[f32],
19322 r: &MoeRoute,
19323 pool: Option<&Pool>,
19324) -> Vec<f32> {
19325 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
19326 if crate::gpu::enabled_here() {
19331 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
19332 crate::gpu::ProbeArm::Gpu => {
19333 let t0 = std::time::Instant::now();
19334 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
19335 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
19336 return out;
19337 }
19338 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
19339 }
19340 crate::gpu::ProbeArm::CpuTimed => {
19341 let t0 = std::time::Instant::now();
19342 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19343 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
19344 return out;
19345 }
19346 crate::gpu::ProbeArm::Cpu => {
19347 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19348 }
19349 }
19350 }
19351 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
19352}
19353
19354fn moe_ffn_minicpm_tail(
19358 slot: &mut crate::minicpm_tail::MiniCpmTail,
19359 li: usize,
19360 total_layers: usize,
19361 m: &MoeFfn,
19362 x: &[f32],
19363 pool: Option<&Pool>,
19364) -> Vec<f32> {
19365 let route = moe_ffn_route(m, x, pool, None);
19366 match slot.forward(li, m, x, &route, pool, total_layers) {
19367 Some(out) => out,
19368 None => moe_ffn_experts(m, x, &route, pool),
19373 }
19374}
19375
19376fn moe_ffn_banked(
19380 slot: &mut crate::mimo_moe::Slot,
19381 li: usize,
19382 m: &MoeFfn,
19383 x: &[f32],
19384 pool: Option<&Pool>,
19385) -> Vec<f32> {
19386 let t0 = std::time::Instant::now();
19387 let r = moe_ffn_route(m, x, pool, None);
19388 slot.note_route(t0.elapsed().as_nanos() as u64);
19389 match slot.forward(li, m, x, &r, pool) {
19390 Some(out) => out,
19391 None => crate::qtensor::float_activations_scope(|| {
19392 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19393 }),
19394 }
19395}
19396
19397fn moe_ffn_banked_rows(
19399 slot: &mut crate::mimo_moe::Slot,
19400 li: usize,
19401 m: &MoeFfn,
19402 xs: &[f32],
19403 b: usize,
19404 hidden: usize,
19405 pool: Option<&Pool>,
19406) -> Vec<f32> {
19407 let t0 = std::time::Instant::now();
19408 let routes: Vec<_> = xs
19409 .chunks_exact(hidden)
19410 .map(|x| moe_ffn_route(m, x, pool, None))
19411 .collect();
19412 slot.note_route(t0.elapsed().as_nanos() as u64);
19413 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19414 return out;
19415 }
19416 let mut out = Vec::with_capacity(b * hidden);
19417 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19418 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19419 crate::qtensor::float_activations_scope(|| {
19421 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19422 })
19423 });
19424 out.extend(row);
19425 }
19426 out
19427}
19428
19429fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19434 use std::sync::atomic::{AtomicBool, Ordering};
19435 if built {
19436 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19437 if total_layers > 0 && layers_run < total_layers {
19438 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19439 } else {
19440 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19441 }
19442 } else {
19443 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19444 }
19445 static SAID: AtomicBool = AtomicBool::new(false);
19446 if !SAID.swap(true, Ordering::Relaxed) {
19447 if built {
19448 tracing::info!("wgpu whole-token graph: ACTIVE");
19449 } else {
19450 tracing::warn!("wgpu whole-token graph refused — per-op path");
19451 }
19452 }
19453}
19454
19455pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19459pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19460pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19464pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19466
19467pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
19471 std::sync::atomic::AtomicU64::new(0);
19472pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
19473 std::sync::atomic::AtomicU64::new(0);
19474pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19475 std::sync::atomic::AtomicU64::new(0);
19476pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
19477 std::sync::atomic::AtomicU64::new(0);
19478pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
19479 std::sync::atomic::AtomicU64::new(0);
19480pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19484 std::sync::atomic::AtomicU64::new(0);
19485pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
19486 std::sync::atomic::AtomicU64::new(0);
19487pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19488 std::sync::atomic::AtomicU64::new(0);
19489pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19490 std::sync::atomic::AtomicU64::new(0);
19491
19492fn moe_batch_enabled() -> bool {
19495 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19496 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19497}
19498
19499fn moe_ffn_cpu_batched(
19505 m: &MoeFfn,
19506 x: &[f32],
19507 idx: &[usize],
19508 p: &[f32],
19509 wsum: f32,
19510 pool: Option<&Pool>,
19511) -> Option<Vec<f32>> {
19512 if idx.is_empty() || !moe_batch_enabled() {
19513 return None;
19514 }
19515 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19519 return None;
19520 }
19521 let n = idx.len() + usize::from(m.shared.is_some());
19522 let mut pairs = Vec::with_capacity(n);
19523 let mut downs = Vec::with_capacity(n);
19524 let mut ws = Vec::with_capacity(n);
19525 for &e in idx {
19526 let d = &m.experts[e];
19527 if d.act != Act::Silu {
19528 return None;
19529 }
19530 pairs.push((&d.gate_proj, &d.up_proj));
19531 downs.push(&d.down_proj);
19532 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19533 }
19534 if let Some((se, gate)) = &m.shared {
19537 if se.act != Act::Silu {
19538 return None;
19539 }
19540 let g = gate.as_ref().map_or(1.0, |gate| {
19541 let mut gl = [0.0f32; 1];
19542 gate.matvec(x, &mut gl, pool);
19543 1.0 / (1.0 + (-gl[0]).exp())
19544 });
19545 pairs.push((&se.gate_proj, &se.up_proj));
19546 downs.push(&se.down_proj);
19547 ws.push(g);
19548 }
19549 let inter = pairs[0].0.rows();
19550 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19551 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19552 return None;
19553 }
19554 let mut out = attention::take_buf(x.len());
19555 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19556 attention::recycle_buf(&mut out);
19557 return None;
19558 }
19559 Some(out)
19560}
19561
19562pub(crate) fn moe_cold_experts_cpu(
19568 experts: &[(&DenseFfn, f32)],
19569 x: &[f32],
19570 pool: Option<&Pool>,
19571) -> Vec<f32> {
19572 let mut out = attention::take_buf(x.len());
19573 if experts.is_empty() {
19574 return out;
19575 }
19576 let pairs: Vec<_> = experts
19577 .iter()
19578 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19579 .collect();
19580 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19581 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19582 let inter = experts[0].0.gate_proj.rows();
19583 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19584 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19585 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19586 {
19587 return out;
19588 }
19589 out.fill(0.0);
19590 for &(expert, weight) in experts {
19591 let mut one = dense_ffn(expert, x, pool);
19592 for (o, v) in out.iter_mut().zip(&one) {
19593 *o += weight * v;
19594 }
19595 attention::recycle_buf(&mut one);
19596 }
19597 out
19598}
19599
19600pub(crate) fn moe_cold_experts_rows_cpu(
19604 jobs: &[Vec<(&DenseFfn, f32)>],
19605 xs: &[f32],
19606 hidden: usize,
19607 pool: Option<&Pool>,
19608) -> Vec<f32> {
19609 let mut out = vec![0.0; xs.len()];
19610 let mut experts: Vec<&DenseFfn> = Vec::new();
19611 let mut groups: Vec<Vec<usize>> = Vec::new();
19612 let mut terms = vec![Vec::new(); jobs.len()];
19613 for (r, row) in jobs.iter().enumerate() {
19614 for &(e, w) in row {
19615 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
19616 Some(g) => g,
19617 None => {
19618 experts.push(e);
19619 groups.push(Vec::new());
19620 groups.len() - 1
19621 }
19622 };
19623 terms[r].push((g, groups[g].len(), w));
19624 groups[g].push(r);
19625 }
19626 }
19627 if experts.is_empty() {
19628 return out;
19629 }
19630 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
19631 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
19632 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
19633 let count: usize = lens.iter().sum();
19634 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
19635 let mut ds = vec![vec![0.0; hidden]; count];
19636 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
19637 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
19638 {
19639 let mut offset = 0;
19640 let offsets: Vec<_> = lens
19641 .iter()
19642 .map(|&n| {
19643 let start = offset;
19644 offset += n;
19645 start
19646 })
19647 .collect();
19648 for (r, terms) in terms.iter().enumerate() {
19649 for &(g, slot, w) in terms {
19650 for (o, &v) in out[r * hidden..(r + 1) * hidden]
19651 .iter_mut()
19652 .zip(&ds[offsets[g] + slot])
19653 {
19654 *o += w * v;
19655 }
19656 }
19657 }
19658 } else {
19659 for (r, jobs) in jobs.iter().enumerate() {
19660 if !jobs.is_empty() {
19661 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
19662 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
19663 attention::recycle_buf(&mut row);
19664 }
19665 }
19666 }
19667 out
19668}
19669
19670fn moe_ffn_cpu(
19672 m: &MoeFfn,
19673 x: &[f32],
19674 idx: &[usize],
19675 p: &[f32],
19676 wsum: f32,
19677 pool: Option<&Pool>,
19678) -> Vec<f32> {
19679 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
19680 return out;
19681 }
19682 let mut out = attention::take_buf(x.len());
19683 for &e in idx {
19684 let mut eo = dense_ffn(&m.experts[e], x, pool);
19685 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
19686 for i in 0..out.len() {
19687 out[i] += w * eo[i];
19688 }
19689 attention::recycle_buf(&mut eo);
19690 }
19691 if let Some((se, gate)) = &m.shared {
19692 let mut so = dense_ffn(se, x, pool);
19693 let g = gate.as_ref().map_or(1.0, |gate| {
19694 let mut gl = [0.0f32; 1];
19695 gate.matvec(x, &mut gl, pool);
19696 1.0 / (1.0 + (-gl[0]).exp())
19697 });
19698 for i in 0..out.len() {
19699 out[i] += g * so[i];
19700 }
19701 attention::recycle_buf(&mut so);
19702 }
19703 out
19704}
19705
19706#[allow(clippy::too_many_arguments)]
19714pub(crate) fn mla_attention(
19715 w: &MlaWeights,
19716 normed: &[f32],
19717 cache: &mut crate::kv_cache::LayerKvCache,
19718 position: usize,
19719 inv_freq: &[f32],
19720 rope_scale: f32,
19721 eps: f64,
19722 pool: Option<&Pool>,
19723) -> Vec<f32> {
19724 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
19725 let hd = dr + dn;
19726 let mut q = vec![0.0f32; nh * hd];
19727 match (&w.q_a, &w.q_a_norm) {
19728 (Some(qa), Some(qn)) => {
19729 let mut t = vec![0.0f32; qa.rows()];
19730 qa.matvec(normed, &mut t, pool);
19731 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
19732 w.q_proj.matvec(&tn, &mut q, pool);
19733 }
19734 _ => w.q_proj.matvec(normed, &mut q, pool),
19735 }
19736 let mut ca = vec![0.0f32; lora + dr];
19737 w.kv_a.matvec(normed, &mut ca, pool);
19738 let (c_lat, k_rope) = ca.split_at_mut(lora);
19739 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
19740 let mut kvb = vec![0.0f32; nh * (dn + dv)];
19741 w.kv_b.matvec(&latn, &mut kvb, pool);
19742 if !w.nope {
19743 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
19744 }
19745 for h in 0..nh {
19746 if !w.nope {
19747 attention::rope_rotate_scaled(
19748 &mut q[h * hd..h * hd + dr],
19749 position,
19750 inv_freq,
19751 rope_scale,
19752 );
19753 }
19754 }
19755 let mut k = vec![0.0f32; nh * hd];
19756 let mut v = vec![0.0f32; nh * hd];
19757 for h in 0..nh {
19758 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
19759 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
19760 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
19761 }
19762 cache.append(&k, &v, &vec![true; nh]);
19763 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
19764 attention::recycle_buf(&mut imp);
19765 let mut ov = vec![0.0f32; nh * dv];
19766 for h in 0..nh {
19767 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
19768 }
19769 let mut out = vec![0.0f32; w.o_proj.rows()];
19770 w.o_proj.matvec(&ov, &mut out, pool);
19771 out
19772}
19773
19774fn dense_moe_ffn(
19781 dm: &DenseMoeFfn,
19782 x_normed: &[f32],
19783 h_raw: &[f32],
19784 eps: f64,
19785 norm_style: NormStyle,
19786 pool: Option<&Pool>,
19787) -> Vec<f32> {
19788 let mut d = dense_ffn(&dm.dense, x_normed, pool);
19789 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
19790 let m = &dm.moe;
19791 let ne = m.experts.len();
19792 let mut logits = vec![0.0f32; ne];
19793 if m.router_input_norm {
19794 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
19795 let inv = 1.0 / (ss + eps as f32).sqrt();
19796 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
19797 m.router.matvec(&xr, &mut logits, pool);
19798 } else {
19799 m.router.matvec(h_raw, &mut logits, pool);
19800 }
19801 let (idx, p, wsum) = moe_route(&logits, m, None);
19802 {
19803 let mut st = m.stats.borrow_mut();
19804 if st.len() < ne {
19805 st.resize(ne, 0);
19806 }
19807 for &e in &idx {
19808 st[e] += 1;
19809 }
19810 }
19811 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
19812 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
19813 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
19814 for (di, mi) in d.iter_mut().zip(&mo) {
19815 *di += mi;
19816 }
19817 d
19818}
19819
19820fn moe_gpu_refused(why: &'static str) {
19827 use std::sync::atomic::{AtomicBool, Ordering};
19828 static SAID: AtomicBool = AtomicBool::new(false);
19829 if !SAID.swap(true, Ordering::Relaxed) {
19830 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
19831 }
19832}
19833
19834fn moe_ffn_gpu(
19835 m: &MoeFfn,
19836 x: &[f32],
19837 idx: &[usize],
19838 p: &[f32],
19839 wsum: f32,
19840 pool: Option<&Pool>,
19841) -> Option<Vec<f32>> {
19842 use crate::gpu::MoeJob;
19843
19844 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
19845 let mut model_ref = None;
19846 for &e in idx {
19847 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
19848 moe_gpu_refused("push_job(expert)");
19849 return None;
19850 }
19851 }
19852 if let Some((se, gate)) = &m.shared {
19853 let g = gate.as_ref().map_or(1.0, |gate| {
19854 let mut gl = [0.0f32; 1];
19855 gate.matvec(x, &mut gl, pool);
19856 1.0 / (1.0 + (-gl[0]).exp())
19857 });
19858 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
19859 moe_gpu_refused("push_job(shared)");
19860 return None;
19861 }
19862 }
19863 let Some(model) = model_ref else {
19864 moe_gpu_refused("no model_ref");
19865 return None;
19866 };
19867 let hidden = jobs[0].down.1;
19868 let mut out = vec![0.0f32; hidden];
19869 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19870 Some(out)
19871 } else {
19872 moe_gpu_refused("gpu::moe_block");
19873 None
19874 }
19875}
19876
19877fn ffn_forward(
19879 ffn: &FfnKind,
19880 x: &[f32],
19881 pool: Option<&Pool>,
19882 experts_allowed: Option<&[bool]>,
19883) -> Vec<f32> {
19884 match ffn {
19885 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
19886 FfnKind::Dense(d) => dense_ffn(d, x, pool),
19887 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
19888 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19892 }
19893}
19894
19895fn ffn_forward_pair(
19899 ffn: &FfnKind,
19900 x1: &[f32],
19901 x2: &[f32],
19902 pool: Option<&Pool>,
19903 experts_allowed: Option<&[bool]>,
19904) -> (Vec<f32>, Vec<f32>) {
19905 let d = match ffn {
19906 FfnKind::Dense(d) if !d.segs.is_empty() => {
19909 return (
19910 tube_ffn(d, x1, 1, pool, None),
19911 tube_ffn(d, x2, 1, pool, None),
19912 );
19913 }
19914 FfnKind::Dense(d) => d,
19915 FfnKind::Moe(m) => {
19916 return (
19917 moe_ffn(m, x1, pool, experts_allowed),
19918 moe_ffn(m, x2, pool, experts_allowed),
19919 );
19920 }
19921 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19922 };
19923 let inter = d.gate_proj.rows();
19924 FFN_SCRATCH.with(|s| {
19925 let mut s = s.borrow_mut();
19926 let [g1, g2, u1, u2] = &mut *s;
19927 g1.resize(inter, 0.0);
19928 g2.resize(inter, 0.0);
19929 u1.resize(inter, 0.0);
19930 u2.resize(inter, 0.0);
19931 QTensor::matvec2_many(
19934 [&d.gate_proj, &d.up_proj],
19935 x1,
19936 x2,
19937 [g1.as_mut_slice(), u1.as_mut_slice()],
19938 [g2.as_mut_slice(), u2.as_mut_slice()],
19939 pool,
19940 );
19941 for i in 0..inter {
19942 g1[i] = d.act.combine(g1[i], u1[i]);
19943 g2[i] = d.act.combine(g2[i], u2[i]);
19944 }
19945 let mut o1 = attention::take_buf(d.down_proj.rows());
19946 let mut o2 = attention::take_buf(d.down_proj.rows());
19947 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
19948 (o1, o2)
19949 })
19950}
19951
19952#[cfg(test)]
19953mod tests {
19954
19955 #[test]
19960 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
19961 use super::{
19962 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
19963 };
19964 let dense_card = ChunkStackFacts {
19965 plain_dense: true,
19966 discrete: true,
19967 gpu_on: true,
19968 ..Default::default()
19969 };
19970 assert!(dense_card.dense_on_discrete());
19971 assert_eq!(
19973 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
19974 DISCRETE_DENSE_PREFILL_CHUNK
19975 );
19976 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
19977 for (label, facts) in [
19978 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
19979 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
19980 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
19981 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
19982 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
19983 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
19984 ] {
19985 assert!(!facts.dense_on_discrete(), "{label}");
19986 assert_eq!(
19987 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
19988 48,
19989 "{label} keeps the historical x86 chunk"
19990 );
19991 }
19992 for dense in [false, true] {
19994 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
19995 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
19996 }
19997 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
19999 for dense in [false, true] {
20000 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
20001 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
20002 }
20003 }
20004 }
20005
20006 #[test]
20007 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
20008 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
20009 let full = |host_rows, device_rows| ReuseLayer {
20010 full: true,
20011 host_rows,
20012 device_rows,
20013 device_state: false,
20014 };
20015 assert_eq!(
20018 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
20019 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
20020 );
20021 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
20023 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
20025 assert_eq!(
20027 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
20028 ReusePlan::Pull(vec![(0, 300, 339)])
20029 );
20030 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
20032 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
20033 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
20034 let conv = |device_state| ReuseLayer {
20037 full: false,
20038 host_rows: 0,
20039 device_rows: None,
20040 device_state,
20041 };
20042 assert_eq!(
20043 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
20044 ReusePlan::Fresh
20045 );
20046 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
20047 }
20048
20049 #[test]
20050 fn nll_graph_policy_scopes_only_the_fused_head() {
20051 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
20052 ("vulkan graph", true, true, false, true, false),
20054 ("native Metal graph", true, true, true, true, true),
20056 ("masked", false, true, false, false, false),
20058 ("graph disabled", true, false, true, false, false),
20059 ] {
20060 let (graph_quality, graph_head_required) =
20061 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
20062 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
20063 assert_eq!(graph_head_required, want_head, "{label}: fused head");
20064 }
20065 }
20066
20067 #[test]
20068 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
20069 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
20070 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
20071 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
20072 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
20073 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
20074 }
20075
20076 #[test]
20077 fn cancel_flag_stops_generation() {
20078 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
20079 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20082 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
20083 assert_eq!(r.finish_reason, "cancelled");
20084 assert!(
20085 r.token_ids.is_empty(),
20086 "no tokens after cancel: {:?}",
20087 r.token_ids
20088 );
20089 assert_eq!(p.kv_cache.seq_len(), 0);
20090 assert!(p.kv_history.is_empty());
20091 assert!(!p.graph_want_logits);
20092 assert!(p.graph_logits.is_none());
20093 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
20095 assert_ne!(r2.finish_reason, "cancelled");
20096 }
20097 use super::*;
20098
20099 #[test]
20100 fn explicit_sliding_schedule_selects_local_rope_profile() {
20101 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
20106 p.swa = Some((1024, usize::MAX));
20107 p.sliding_layers = Some(vec![true, true, true, false]);
20108 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
20109 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
20110 p.rope_scale = 1.277_258_9;
20111 p.rope_scale_local = 1.0;
20112
20113 for li in 0..3 {
20114 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
20115 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
20116 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
20117 }
20118 assert_eq!(p.layer_window(3), None);
20119 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
20120 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
20121 }
20122
20123 #[test]
20131 fn dynamic_ffn_equals_the_zeroing_arm() {
20132 let (hidden, inter) = (8usize, 32usize);
20133 let synth = |n: usize, salt: usize| -> Vec<f32> {
20134 (0..n)
20135 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
20136 .collect()
20137 };
20138 let down = synth(hidden * inter, 3);
20139 let mut down_t = vec![0.0f32; inter * hidden];
20140 for r in 0..hidden {
20141 for c in 0..inter {
20142 down_t[c * hidden + r] = down[r * inter + c];
20143 }
20144 }
20145 let d = DenseFfn {
20146 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20147 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20148 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
20149 act: Act::Silu,
20150 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
20151 segs: Vec::new(),
20152 };
20153 let x = synth(hidden, 11);
20154 let k = 12usize;
20155 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
20156 let mut g = vec![0.0f32; inter];
20158 d.gate_proj.matvec(&x, &mut g, None);
20159 let mut u = vec![0.0f32; inter];
20160 d.up_proj.matvec(&x, &mut u, None);
20161 for v in g.iter_mut() {
20162 *v = inference::silu(*v);
20163 }
20164 keep_top_k(&mut g, k);
20165 for i in 0..inter {
20166 g[i] *= u[i];
20167 }
20168 let mut want = vec![0.0f32; hidden];
20169 d.down_proj.matvec(&g, &mut want, None);
20170 for (a, b) in want.iter().zip(&got) {
20171 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
20172 }
20173 }
20174
20175 #[test]
20181 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
20182 let (hidden, core, tube) = (8usize, 12usize, 8usize);
20183 let inter = core + tube;
20184 let synth = |n: usize, salt: usize| -> Vec<f32> {
20185 (0..n)
20186 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
20187 .collect()
20188 };
20189 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
20190 let d_all = synth(hidden * inter, 3);
20191 let dense = DenseFfn {
20193 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
20194 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
20195 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
20196 act: Act::Silu,
20197 down_t: None,
20198 segs: Vec::new(),
20199 };
20200 let rows =
20201 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
20202 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
20203 let mut o = Vec::with_capacity(hidden * (b - a));
20204 for r in 0..hidden {
20205 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
20206 }
20207 o
20208 };
20209 let tubed = DenseFfn {
20210 down_t: None,
20211 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
20212 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
20213 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
20214 act: Act::Silu,
20215 segs: vec![FfnSeg {
20216 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
20217 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
20218 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
20219 start: core,
20220 width: tube,
20221 }],
20222 };
20223 let x = synth(hidden, 7);
20224 let want = dense_ffn(&dense, &x, None);
20225 let got = tube_ffn(&tubed, &x, 1, None, None);
20226 for (a, b) in want.iter().zip(&got) {
20227 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
20228 }
20229 let mut bits = vec![0u8; inter.div_ceil(8)];
20231 for n in 0..core {
20232 bits[n / 8] |= 1 << (n % 8);
20233 }
20234 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20235 let masked = dense_ffn_masked(&dense, &x, None, &bits);
20236 for (a, b) in masked.iter().zip(&closed) {
20237 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
20238 }
20239 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20241 for (a, b) in closed.iter().zip(&batch) {
20242 assert_eq!(a, b, "batch arm disagrees with decode arm");
20243 }
20244 }
20245
20246 #[test]
20248 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
20249 let (hidden, inter) = (16usize, 40usize);
20250 let synth = |n: usize, salt: usize| -> Vec<f32> {
20251 (0..n)
20252 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
20253 .collect()
20254 };
20255 let d = DenseFfn {
20256 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20257 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20258 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
20259 act: Act::Silu,
20260 down_t: None,
20261 segs: Vec::new(),
20262 };
20263 let x = synth(hidden, 9);
20264 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
20266
20267 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
20268
20269 let mut g = vec![0.0f32; inter];
20271 d.gate_proj.matvec(&x, &mut g, None);
20272 let mut u = vec![0.0f32; inter];
20273 d.up_proj.matvec(&x, &mut u, None);
20274 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
20275 for i in 0..inter {
20276 g[i] = if act_set.contains(&(i as u16)) {
20277 inference::silu(g[i]) * u[i]
20278 } else {
20279 0.0
20280 };
20281 }
20282 let mut reference = vec![0.0f32; hidden];
20283 d.down_proj.matvec(&g, &mut reference, None);
20284
20285 let max_d = sparse
20286 .iter()
20287 .zip(&reference)
20288 .map(|(a, b)| (a - b).abs())
20289 .fold(0.0f32, f32::max);
20290 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
20291 }
20292
20293 fn attach_test_mtp(p: &mut Pipeline) {
20295 let (h, inter, heads, kv, hd) = (
20296 p.hidden_size,
20297 p.intermediate_size,
20298 p.num_heads,
20299 p.num_kv_heads,
20300 p.head_dim,
20301 );
20302 let synth = |n: usize, salt: usize| -> Vec<f32> {
20303 (0..n)
20304 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
20305 .collect()
20306 };
20307 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
20308 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20309 };
20310 p.mtp = Some(MtpModule {
20311 enorm: vec![1.0; h],
20312 hnorm: vec![1.0; h],
20313 eh_proj: qt(h, 2 * h, 301),
20314 layer: LayerWeights {
20315 input_norm: vec![1.0; h],
20316 post_norm: vec![1.0; h],
20317 attn_out_norm: None,
20318 ffn_out_norm: None,
20319 layer_scale: None,
20320 ffn: FfnKind::Dense(DenseFfn {
20321 gate_proj: qt(inter, h, 315),
20322 up_proj: qt(inter, h, 316),
20323 down_proj: qt(h, inter, 317),
20324 act: Act::Silu,
20325 down_t: None,
20326 segs: Vec::new(),
20327 }),
20328 attn: AttnKind::Full {
20329 bias: None,
20330 wq: qt(heads * hd, h, 311),
20331 wk: qt(kv * hd, h, 312),
20332 wv: qt(kv * hd, h, 313),
20333 wo: qt(h, heads * hd, 314),
20334 q_norm: None,
20335 k_norm: None,
20336 output_gate: false,
20337 softplus_gate: None,
20338 },
20339 },
20340 final_norm: vec![1.0; h],
20341 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
20342 });
20343 }
20344
20345 #[test]
20346 fn speculative_equals_vanilla_greedy() {
20347 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20351 let run = |spec: bool| {
20352 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20353 p.sampler_config.temperature = 0.0;
20354 attach_test_mtp(&mut p);
20355 p.speculative = spec;
20356 let r = p.generate("abcdef", 12, None, None).unwrap();
20357 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
20358 };
20359 let (vanilla, d0, _) = run(false);
20360 let (spec, d1, a1) = run(true);
20361 assert_eq!(d0, 0, "vanilla path must not draft");
20362 assert!(d1 > 0, "speculative path must draft");
20363 assert_eq!(
20364 vanilla, spec,
20365 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
20366 );
20367 }
20368
20369 #[test]
20370 fn speculative_accepts_constant_oracle() {
20371 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20373 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20374 p.sampler_config.temperature = 0.0;
20375 p.sampler_config.repetition_penalty = 1.0;
20376 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
20379 attach_test_mtp(&mut p);
20380 p.speculative = true;
20381 let r = p.generate("abcd", 10, None, None).unwrap();
20382 assert!(r.mtp_drafted > 0);
20383 assert_eq!(
20384 r.mtp_accepted, r.mtp_drafted,
20385 "constant logits → every draft accepted"
20386 );
20387 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20390 }
20391
20392 #[test]
20393 fn empty_prompt_is_an_error_not_a_panic() {
20394 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20395 let r = p.generate("", 4, None, None);
20396 assert!(r.is_err(), "empty prompt must be a clean error");
20397 }
20398
20399 #[test]
20400 fn every_token_enters_kv_exactly_once() {
20401 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20402 p.sampler_config.temperature = 0.0;
20404 let r = p.generate("abc", 2, None, None).unwrap();
20405 assert_eq!(r.prompt_tokens, 3);
20406 assert_eq!(
20410 p.kv_cache.seq_len(),
20411 3 + r.tokens_generated - 1,
20412 "each token must be cached exactly once (v1 cached the last prompt token twice)"
20413 );
20414 }
20415
20416 #[test]
20417 fn generation_is_reproducible_with_seed() {
20418 let run = || {
20419 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20420 p.generate("hello", 8, None, None).unwrap().token_ids
20421 };
20422 assert_eq!(run(), run());
20423 }
20424
20425 #[test]
20426 fn resetting_sampler_restarts_the_seeded_stream() {
20427 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20428 let config = SamplerConfig {
20429 seed: Some(1234),
20430 ..SamplerConfig::default()
20431 };
20432 p.set_sampler_config(config.clone());
20433 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20434 p.set_sampler_config(config);
20435 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20436 assert_eq!(first, second);
20437 }
20438
20439 #[test]
20440 fn eviction_bounds_the_cache() {
20441 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20442 p.kv_cache.max_seq_len = 6;
20443 p.sampler_config.temperature = 0.0;
20444 let _ = p.generate("abcd", 12, None, None).unwrap();
20445 assert!(
20446 p.kv_cache.seq_len() <= 6 + 1,
20447 "cache must stay bounded by max_seq_len (got {})",
20448 p.kv_cache.seq_len()
20449 );
20450 }
20451
20452 #[test]
20453 fn confidence_matches_tokens_and_is_a_probability() {
20454 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20455 p.sampler_config.temperature = 0.0;
20456 p.sampler_config.repetition_penalty = 1.0;
20457 let r = p.generate("abcd", 10, None, None).unwrap();
20458 assert_eq!(
20459 r.token_confidence.len(),
20460 r.token_ids.len(),
20461 "one confidence per emitted token"
20462 );
20463 for &c in &r.token_confidence {
20464 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20465 }
20466 let logits = [1.0f32, 3.0, 0.5, 3.0];
20468 let p0 = top1_prob_t(&logits, 1, 1.0);
20469 let p1 = top1_prob_t(&logits, 3, 1.0);
20470 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20471 assert!(p0 > 0.0 && p0 < 1.0);
20472 let sharp = top1_prob_t(&logits, 1, 1.0);
20474 let soft = top1_prob_t(&logits, 1, 2.0);
20475 assert!(soft < sharp, "higher temperature lowers peak confidence");
20476 }
20477
20478 #[test]
20479 fn trace_is_opt_in_and_parallels_the_output() {
20480 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20482 p.sampler_config.temperature = 0.0;
20483 p.sampler_config.repetition_penalty = 1.0;
20484 let r = p.generate("abcd", 10, None, None).unwrap();
20485 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20486
20487 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20489 p.sampler_config.temperature = 0.0;
20490 p.sampler_config.repetition_penalty = 1.0;
20491 p.set_trace(true);
20492 let r = p.generate("abcd", 10, None, None).unwrap();
20493 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20494 for (i, tr) in r.traces.iter().enumerate() {
20495 assert_eq!(tr.t, i, "trace index is sequential");
20496 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20497 assert_eq!(
20498 tr.confidence, r.token_confidence[i],
20499 "trace confidence matches the confidence channel"
20500 );
20501 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20503 }
20504 }
20505
20506 #[test]
20507 fn explain_prefill_logits_match_greedy_first_token() {
20508 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20512 p.sampler_config.temperature = 0.0;
20513 p.sampler_config.repetition_penalty = 1.0;
20514 let ids = p.tokenizer.encode("abcd");
20515 let logits = p.prefill_next_logits(&ids, None);
20516 let argmax = logits
20517 .iter()
20518 .enumerate()
20519 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20520 .unwrap()
20521 .0 as u32;
20522 let r = p.generate("abcd", 1, None, None).unwrap();
20523 assert_eq!(
20524 argmax, r.token_ids[0],
20525 "explain preview must match greedy emit"
20526 );
20527 }
20528
20529 #[test]
20530 fn laguna_shared_expert_is_unconditionally_added() {
20531 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20532 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20533 let zero_dense = || DenseFfn {
20534 gate_proj: matrix(vec![0.0; 4]),
20535 up_proj: matrix(vec![0.0; 4]),
20536 down_proj: matrix(vec![0.0; 4]),
20537 act: Act::Silu,
20538 down_t: None,
20539 segs: Vec::new(),
20540 };
20541 let shared = DenseFfn {
20542 gate_proj: identity(),
20543 up_proj: identity(),
20544 down_proj: identity(),
20545 act: Act::Silu,
20546 down_t: None,
20547 segs: Vec::new(),
20548 };
20549 let x = [1.0, 2.0];
20550 let expected = dense_ffn(&shared, &x, None);
20551 let moe = MoeFfn {
20552 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20553 experts: vec![zero_dense()],
20554 top_k: 1,
20555 norm_topk_prob: true,
20556 router_sigmoid: true,
20557 expert_bias: None,
20558 routed_scaling: 1.0,
20559 route_tau: None,
20560 shared: Some((shared, None)),
20561 stats: std::cell::RefCell::new(Vec::new()),
20562 act_sq: std::cell::RefCell::new(Vec::new()),
20563 act_rows: std::cell::RefCell::new(Vec::new()),
20564 mask: None,
20565 per_expert_scale: None,
20566 router_input_norm: false,
20567 resonance: None,
20568 grown: Vec::new(),
20569 };
20570 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
20571 for (actual, expected) in actual.iter().zip(expected) {
20572 assert!((actual - expected).abs() < 1e-6);
20573 }
20574 }
20575
20576 fn mimo_test_pipeline() -> Pipeline {
20585 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
20586 let kvh = [1usize, 2, 2, 1];
20587 let synth = |n: usize, salt: usize| -> Vec<f32> {
20588 (0..n)
20589 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20590 .collect()
20591 };
20592 let qt = |rows: usize, cols: usize, salt: usize| {
20593 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20594 };
20595 let dense = |inter: usize, salt: usize| DenseFfn {
20596 gate_proj: qt(inter, hs, salt),
20597 up_proj: qt(inter, hs, salt + 1),
20598 down_proj: qt(hs, inter, salt + 2),
20599 act: Act::Silu,
20600 down_t: None,
20601 segs: Vec::new(),
20602 };
20603 let layers: Vec<LayerWeights> = (0..4)
20604 .map(|li| LayerWeights {
20605 input_norm: vec![1.0; hs],
20606 post_norm: vec![1.0; hs],
20607 attn_out_norm: None,
20608 ffn_out_norm: None,
20609 layer_scale: None,
20610 ffn: if li == 0 {
20611 FfnKind::Dense(dense(inter, 50))
20612 } else {
20613 FfnKind::Moe(MoeFfn {
20614 router: qt(4, hs, 60 + li),
20615 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
20616 top_k: 2,
20617 norm_topk_prob: true,
20618 router_sigmoid: true,
20619 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
20620 routed_scaling: 1.0,
20621 route_tau: None,
20622 shared: None,
20623 stats: std::cell::RefCell::new(Vec::new()),
20624 act_sq: std::cell::RefCell::new(Vec::new()),
20625 act_rows: std::cell::RefCell::new(Vec::new()),
20626 mask: None,
20627 per_expert_scale: None,
20628 router_input_norm: false,
20629 resonance: None,
20630 grown: Vec::new(),
20631 })
20632 },
20633 attn: AttnKind::Full {
20634 wq: qt(nh * hd, hs, li * 10 + 1),
20635 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
20636 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
20637 wo: qt(hs, nh * vd, li * 10 + 4),
20638 q_norm: None,
20639 k_norm: None,
20640 output_gate: false,
20641 softplus_gate: None,
20642 bias: None,
20643 },
20644 })
20645 .collect();
20646 let mut p = Pipeline::new(
20647 Tokenizer::byte_level(),
20648 PipelineWeights {
20649 embed_tokens: qt(vocab, hs, 100),
20650 layers,
20651 lm_head: qt(vocab, hs, 200),
20652 final_norm: vec![1.0; hs],
20653 },
20654 hs,
20655 inter,
20656 nh,
20657 1, hd,
20659 4,
20660 4,
20661 false,
20662 vocab,
20663 1e-6,
20664 1e7,
20665 NormStyle::Qwen,
20666 4096,
20667 SamplerConfig {
20668 seed: Some(7),
20669 ..Default::default()
20670 },
20671 );
20672 p.layer_dump = None;
20674 p.set_rotary(4, 1e7);
20675 p.sliding_layers = Some(vec![false, true, true, false]);
20676 p.swa = Some((3, usize::MAX));
20677 p.rotary_dim_local = Some(4);
20678 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
20679 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
20680 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
20681 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
20682 p
20683 }
20684
20685 #[test]
20686 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
20687 let mut p = mimo_test_pipeline();
20688 p.speculative = false;
20689 p.ignore_eos = true;
20690 p.sampler_config.temperature = 0.0;
20691 p.sampler_config.repetition_penalty = 1.0;
20692 let a = vec![3, 5, 7, 9, 11, 13];
20693 let b = vec![4, 8, 12, 16, 20, 24];
20694 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
20695 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
20696 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
20700 assert_eq!(actual, expected);
20701 assert!(p.kv_history.is_empty());
20702 let mut extended = a.clone();
20703 extended.push(17);
20704 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20705 p.reset_session();
20706 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20707 assert_eq!(after_media, fresh);
20708 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
20709 p.reset_session();
20713 p.generate_from_ids(&a, 1, None, None).unwrap();
20714 let mut media_ids = p.kv_history.clone();
20715 assert!(!media_ids.is_empty());
20716 media_ids.extend_from_slice(&[19, 21, 23]);
20717 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
20718 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
20719 let mut oracle = mimo_test_pipeline();
20720 oracle.speculative = false;
20721 oracle.ignore_eos = true;
20722 oracle.sampler_config.temperature = 0.0;
20723 oracle.sampler_config.repetition_penalty = 1.0;
20724 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
20725 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
20726 assert!(p.kv_history.is_empty());
20727 let mut bad = rows;
20728 bad[0] = f32::NAN;
20729 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
20730 }
20731
20732 fn f32_bits(v: &[f32]) -> Vec<u32> {
20733 v.iter().map(|x| x.to_bits()).collect()
20734 }
20735
20736 #[test]
20743 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
20744 let mut p = mimo_test_pipeline();
20745 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
20746 assert_eq!(kv, vec![1, 2, 2, 1]);
20747 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
20748 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
20749 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
20750 let hs = p.hidden_size;
20751 let mut decode = Vec::new();
20752 for (pos, &id) in ids.iter().enumerate() {
20753 let e = p.embed_single(id);
20754 let h = p.forward_layers(&e, pos, None);
20755 decode.push(p.logits_from_hidden(&h));
20756 }
20757 for l in &p.kv_cache.layers {
20758 assert_eq!(l.seq_len, 12);
20759 assert_eq!(l.head_values(0).len(), 12 * 8);
20761 }
20762 assert!(decode.iter().flatten().all(|v| v.is_finite()));
20763
20764 p.clear_sequence_state();
20765 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20766 for pos in 0..ids.len() {
20767 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
20768 assert_eq!(
20769 f32_bits(&decode[pos]),
20770 f32_bits(&lg),
20771 "whole prompt, pos {pos}"
20772 );
20773 }
20774
20775 p.clear_sequence_state();
20776 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
20777 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
20778 for pos in 0..ids.len() {
20779 let row = if pos < 5 {
20780 &a[pos * hs..(pos + 1) * hs]
20781 } else {
20782 &b[(pos - 5) * hs..(pos - 4) * hs]
20783 };
20784 let lg = p.logits_from_hidden(row);
20785 assert_eq!(
20786 f32_bits(&decode[pos]),
20787 f32_bits(&lg),
20788 "two chunks, pos {pos}"
20789 );
20790 }
20791
20792 let last = |p: &mut Pipeline| {
20795 p.clear_sequence_state();
20796 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20797 p.logits_from_hidden(&hb[11 * hs..12 * hs])
20798 };
20799 let base = last(&mut p);
20800 let mut no_sinks = mimo_test_pipeline();
20801 for l in &mut no_sinks.kv_cache.layers {
20802 l.sinks = None;
20803 }
20804 assert_ne!(
20805 f32_bits(&last(&mut no_sinks)),
20806 f32_bits(&base),
20807 "sinks are live"
20808 );
20809 let mut wide = mimo_test_pipeline();
20810 wide.swa = Some((64, usize::MAX));
20811 assert_ne!(
20812 f32_bits(&last(&mut wide)),
20813 f32_bits(&base),
20814 "window is live"
20815 );
20816
20817 p.clear_sequence_state();
20819 p.ignore_eos = true;
20820 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
20821 assert_eq!(r.token_ids.len(), 4);
20822 }
20823
20824 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
20827 let (hs, nh) = (16usize, 4usize);
20828 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
20829 p.layer_dump = None;
20830 p.swa = Some((6, 4));
20831 p.proj_gate_sigmoid = true;
20832 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
20833 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
20834 let g: Vec<f32> = (0..nh * hs)
20835 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
20836 .collect();
20837 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
20838 }
20839 }
20840 p.swa_trim = trim;
20841 p
20842 }
20843
20844 #[test]
20850 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
20851 let mut a = spark_test_pipeline(None);
20852 let mut b = spark_test_pipeline(Some((2, 4)));
20853 assert_eq!(
20854 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
20855 vec![Some(6), Some(6), Some(6), None]
20856 );
20857 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
20858 let mut pos = 0usize;
20859 for &n in [5usize, 13, 7, 16].iter().cycle() {
20860 if pos >= ids.len() {
20861 break;
20862 }
20863 let end = (pos + n).min(ids.len());
20864 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20865 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20866 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
20867 pos = end;
20868 }
20869 for _ in 0..23 {
20870 let e = a.embed_single(((pos * 7) % 64) as u32);
20871 let ha = a.forward_layers(&e, pos, None);
20872 let hb = b.forward_layers(&e, pos, None);
20873 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
20874 for li in 0..3 {
20875 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
20876 }
20877 pos += 1;
20878 }
20879 for round in 0..9 {
20880 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
20881 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
20882 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
20883 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
20884 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
20885 if round % 2 == 1 {
20886 for p in [&mut a, &mut b] {
20888 for l in &mut p.kv_cache.layers {
20889 l.truncate_last(2);
20890 }
20891 }
20892 } else {
20893 pos += 2;
20894 }
20895 }
20896 let e = a.embed_single(9);
20897 let (ha, hb) = (
20898 a.forward_layers(&e, pos, None),
20899 b.forward_layers(&e, pos, None),
20900 );
20901 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
20902 for li in 0..3 {
20903 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
20904 assert!(lb.base() > 0, "layer {li} trimmed");
20905 assert_eq!(la.base(), 0);
20906 assert_eq!(lb.pos_len(), la.seq_len);
20907 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
20908 }
20909 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
20910 assert_eq!(
20911 (gb.base(), gb.seq_len),
20912 (0, ga.seq_len),
20913 "the global layer keeps all"
20914 );
20915 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
20916 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
20917 }
20918
20919 #[test]
20924 fn swa_trim_wire_handoff_continues_bitwise() {
20925 let mut a = spark_test_pipeline(None);
20926 let mut b = spark_test_pipeline(Some((2, 4)));
20927 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
20928 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20929 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20930 assert_eq!(f32_bits(&ha), f32_bits(&hb));
20931 let mut pos = ids.len();
20932 for _ in 0..5 {
20933 let e = a.embed_single(((pos * 5) % 64) as u32);
20934 assert_eq!(
20935 f32_bits(&a.forward_layers(&e, pos, None)),
20936 f32_bits(&b.forward_layers(&e, pos, None))
20937 );
20938 pos += 1;
20939 }
20940 let mut c = spark_test_pipeline(Some((2, 4)));
20941 for li in 0..4 {
20942 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
20943 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
20944 }
20945 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
20946 assert_eq!(c.kv_cache.seq_len(), pos);
20947 for step in 0..17 {
20948 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
20949 let ha = a.forward_layers(&e, pos, None);
20950 let hc = c.forward_layers(&e, pos, None);
20951 assert_eq!(
20952 f32_bits(&ha),
20953 f32_bits(&hc),
20954 "after the hand-off, step {step}"
20955 );
20956 pos += 1;
20957 }
20958 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
20959 }
20960
20961 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
20964 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
20965 let synth = |len: usize, salt: usize| -> Vec<f32> {
20966 (0..len)
20967 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
20968 .collect()
20969 };
20970 let qt = |rows: usize, cols: usize, salt: usize| {
20971 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20972 };
20973 let layers = (0..n)
20974 .map(|k| {
20975 let s = 500 + k * 40;
20976 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
20977 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
20978 MtpModule {
20979 enorm: vec![1.0; hs],
20980 hnorm: vec![1.0; hs],
20981 eh_proj: qt(hs, 2 * hs, s),
20982 layer: LayerWeights {
20983 input_norm: vec![1.0; hs],
20984 post_norm: vec![1.0; hs],
20985 attn_out_norm: None,
20986 ffn_out_norm: None,
20987 layer_scale: None,
20988 attn: AttnKind::Full {
20989 wq: qt(nh * hd, hs, s + 1),
20990 wk: qt(nkv * hd, hs, s + 2),
20991 wv: qt(nkv * vd, hs, s + 3),
20992 wo: qt(hs, nh * vd, s + 4),
20993 q_norm: None,
20994 k_norm: None,
20995 output_gate: false,
20996 softplus_gate: None,
20997 bias: None,
20998 },
20999 ffn: FfnKind::Dense(DenseFfn {
21000 gate_proj: qt(inter, hs, s + 5),
21001 up_proj: qt(inter, hs, s + 6),
21002 down_proj: qt(hs, inter, s + 7),
21003 act: Act::Silu,
21004 down_t: None,
21005 segs: Vec::new(),
21006 }),
21007 },
21008 final_norm: vec![1.0; hs],
21009 kv,
21010 }
21011 })
21012 .collect();
21013 mimo_mtp::MimoMtp::from_layers(layers)
21014 }
21015
21016 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
21017 p.clear_sequence_state();
21018 p.speculative = spec;
21019 p.ignore_eos = true;
21020 p.sampler_config.temperature = 0.0;
21021 p.generate_from_ids(ids, n, None, None).unwrap()
21022 }
21023
21024 #[test]
21030 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
21031 for post in [false, true] {
21034 let mut p = mimo_test_pipeline();
21035 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
21037 let mut st0 = mimo_test_mtp(3, 1.0);
21038 st0.post_norm_hidden = post;
21039 p.mimo_mtp = Some(st0);
21040 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
21041 let hs = p.hidden_size;
21042 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21043 p.mimo_note_rows(&hb, 0);
21044 let mut st = p.mimo_mtp.take().unwrap();
21045 let k = 3;
21048 let mut inc = Vec::new();
21049 for t in 0..ids.len() - k - 1 {
21050 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
21051 }
21052 let s = ids.len();
21055 let mut reference = vec![vec![0u32; k]; s - k - 1];
21056 let mut fresh = mimo_test_mtp(3, 1.0);
21057 for (layer, m) in fresh.layers.iter_mut().enumerate() {
21058 let n = s - layer - 1;
21059 let mut cats = vec![0.0f32; n * 2 * hs];
21060 for j in 0..n {
21061 let e = p.embed_single(ids[j + layer + 1]);
21062 let raw = &hb[j * hs..(j + 1) * hs];
21063 let g = if post {
21064 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
21065 } else {
21066 raw.to_vec()
21067 };
21068 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
21069 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
21070 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
21071 }
21072 let mut x = vec![0.0f32; n * hs];
21073 m.eh_proj.matmat(&cats, n, &mut x, None);
21074 p.mimo_mtp_block(m, &mut x, n, 0);
21075 for (t, row) in reference.iter_mut().enumerate() {
21076 let y = inference::rms_norm(
21077 &x[t * hs..(t + 1) * hs],
21078 &m.final_norm,
21079 p.rms_eps,
21080 p.norm_style,
21081 );
21082 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
21083 }
21084 }
21085 assert_eq!(inc, reference, "post_norm_hidden = {post}");
21086 let distinct: std::collections::HashSet<u32> =
21088 inc.iter().flatten().copied().collect();
21089 assert!(distinct.len() > 3, "{inc:?}");
21090 let last_t = ids.len() - k - 2;
21092 for m in &st.layers {
21093 assert_eq!(m.kv.seq_len, last_t + 1);
21094 }
21095 }
21096 }
21097
21098 #[test]
21105 fn mimo_speculative_greedy_equals_plain_greedy() {
21106 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21107 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
21108 let n = 24;
21109 let mut p = mimo_test_pipeline();
21110 let plain = mimo_greedy(&mut p, &ids, n, false);
21111 assert_eq!(plain.mtp_drafted, 0);
21112 assert_eq!(plain.token_ids.len(), n);
21113 let plain_kv = p.kv_cache.layers[0].seq_len;
21114
21115 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
21117 let spec = mimo_greedy(&mut p, &ids, n, true);
21118 assert!(spec.mtp_drafted > 0, "the round must draft");
21119 assert_eq!(spec.token_ids, plain.token_ids);
21120 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21121
21122 let mut truth: Vec<u32> = ids.clone();
21125 truth.extend(&plain.token_ids);
21126 let mut noisy = truth.clone();
21127 for (i, t) in noisy.iter_mut().enumerate() {
21128 if i % 5 == 0 {
21129 *t = (*t + 1) % 64;
21130 }
21131 }
21132 let mut st = mimo_test_mtp(3, 1.0);
21133 st.draft_override = Some(noisy);
21134 p.mimo_mtp = Some(st);
21135 let spec = mimo_greedy(&mut p, &ids, n, true);
21136 assert_eq!(spec.token_ids, plain.token_ids);
21137 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21138 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
21139 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
21140 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
21141 assert!(
21142 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
21143 "{:?}",
21144 stats.accept_hist
21145 );
21146 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
21147
21148 let mut st = mimo_test_mtp(3, 1.0);
21151 st.draft_override = Some(truth);
21152 p.mimo_mtp = Some(st);
21153 let spec = mimo_greedy(&mut p, &ids, n, true);
21154 assert_eq!(spec.token_ids, plain.token_ids);
21155 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
21156 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21157
21158 let off = mimo_greedy(&mut p, &ids, n, false);
21160 assert_eq!(off.token_ids, plain.token_ids);
21161 assert_eq!(off.mtp_drafted, 0);
21162 }
21163
21164 #[test]
21173 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
21174 let p = mimo_test_pipeline();
21175 assert_eq!(
21176 p.graph_attn_decline_reason(),
21177 Some("per-layer KV head counts")
21178 );
21179 assert_eq!(p.wgpu_graph_attn_decline(), None);
21180 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
21181 assert_eq!(
21182 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
21183 (1, 4, 4, None, false)
21184 );
21185 assert_eq!(g0.invf, p.inv_freq.as_slice());
21186 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
21187 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
21188 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
21189 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
21190 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
21191 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
21192 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
21193
21194 let emb = p.embed_single(3);
21197 let mut lg = Vec::new();
21198 assert!(
21199 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
21200 .is_none()
21201 );
21202 let mut hid = emb.clone();
21203 assert_eq!(
21204 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
21205 crate::gpu::BatchGraphOutcome::Declined
21206 );
21207 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
21208 assert!(p.try_multi_burst(3, 0, 4).is_none());
21209 assert!(
21210 p.graph_declines().is_empty(),
21211 "no attention decline logged: {:?}",
21212 p.graph_declines()
21213 );
21214 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
21219 assert_eq!(plain().graph_attn_decline_reason(), None);
21220 assert_eq!(plain().wgpu_graph_attn_decline(), None);
21221 assert!(
21222 plain().graph_attn_geom(0).is_none(),
21223 "uniform models keep the historical arms"
21224 );
21225 let mut q = plain();
21226 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
21227 assert_eq!(
21228 q.graph_attn_decline_reason(),
21229 Some("learned attention sinks")
21230 );
21231 assert_eq!(
21232 q.graph_attn_geom(1).unwrap().sink,
21233 Some(&[0.25f32, -0.25][..])
21234 );
21235 let mut q = plain();
21236 q.set_attn_geometry(None, Some(2)).unwrap();
21237 assert_eq!(
21238 q.graph_attn_decline_reason(),
21239 Some("V heads narrower than Q/K heads")
21240 );
21241 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
21242 let mut q = plain();
21243 q.sliding_layers = Some(vec![true, false]);
21244 q.swa = Some((4, usize::MAX));
21245 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
21246 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
21247 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
21248
21249 let mut q = mimo_test_pipeline();
21253 q.rope_scale = 2.0;
21254 q.rope_scale_local = 1.25;
21255 assert_eq!(q.wgpu_graph_attn_decline(), None);
21256 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
21257 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
21258
21259 let mut q = plain();
21262 q.rope_scale = 2.0;
21263 assert_eq!(
21264 q.wgpu_graph_attn_decline(),
21265 Some("scaled RoPE without per-layer geometry")
21266 );
21267 }
21268
21269 #[test]
21270 fn mimo_verify_rewind_preserves_lagging_host_caches() {
21271 let mut p = mimo_test_pipeline();
21272 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
21273 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
21274 for _ in 0..if li == 0 { 2 } else { 12 } {
21275 layer.append(&row, &row, &[]);
21276 }
21277 }
21278 p.mimo_verify_rewind(9).unwrap();
21279 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
21280 for layer in &p.kv_cache.layers[1..] {
21281 assert_eq!(layer.seq_len, 9);
21282 }
21283 }
21284
21285 #[test]
21289 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
21290 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
21291 let _ = std::fs::remove_dir_all(&dir);
21292 let mut p = mimo_test_pipeline();
21293 let hs = p.hidden_size;
21294 let ids = [5u32, 9, 11, 2, 40];
21295 p.layer_dump = Some(dir.join("decode"));
21296 for (pos, &id) in ids.iter().enumerate() {
21297 let e = p.embed_single(id);
21298 let _ = p.forward_layers(&e, pos, None);
21299 }
21300 p.clear_sequence_state();
21301 p.layer_dump = Some(dir.join("prefill"));
21302 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21303 for pos in 0..ids.len() {
21304 for li in 0..p.num_layers {
21305 let name = format!("p{pos:06}_l{li:02}.f32");
21306 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
21307 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
21308 assert_eq!(a.len(), hs * 4, "{name}");
21309 assert_eq!(a, b, "{name}");
21310 }
21311 }
21312 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
21313 let vals: Vec<f32> = last
21314 .chunks(4)
21315 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
21316 .collect();
21317 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
21318 let _ = std::fs::remove_dir_all(&dir);
21319 }
21320
21321 #[test]
21322 fn attn_geometry_and_sinks_are_validated() {
21323 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
21324 assert!(
21325 p.set_attn_geometry(Some(vec![2]), None).is_err(),
21326 "one entry per layer"
21327 );
21328 assert!(
21329 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
21330 "3 does not divide 4"
21331 );
21332 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
21333 assert!(p.set_attn_geometry(None, Some(0)).is_err());
21334 assert!(
21335 p.set_attn_geometry(None, Some(5)).is_err(),
21336 "V wider than the head"
21337 );
21338 p.set_attn_geometry(None, Some(4)).unwrap();
21339 assert_eq!(
21340 p.v_head_dim, None,
21341 "v_head_dim == head_dim is the uniform case"
21342 );
21343 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
21344 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
21345 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
21346 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
21347 assert!(
21348 p.kv_cache.layers[1].sinks.is_some(),
21349 "a reshape keeps the layer's sinks"
21350 );
21351 assert_eq!(p.layer_geom(1).0, 4);
21352 assert!(
21353 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
21354 "one sink per Q head"
21355 );
21356 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
21357 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
21358 }
21359
21360 #[test]
21363 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
21364 let cfg = || {
21365 Some(crate::nystrom::O1Cfg {
21366 layers: crate::nystrom::O1Layers::All,
21367 m: 4,
21368 w: 8,
21369 sink: 2,
21370 rect: crate::nystrom::O1Rect::Aggregate,
21371 })
21372 };
21373 let mut p = mimo_test_pipeline();
21374 p.set_o1(cfg());
21375 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
21376 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
21377 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
21378 q.sliding_layers = Some(vec![false, false, true]);
21379 q.swa = Some((4, usize::MAX));
21380 q.set_o1(cfg());
21381 assert_eq!(q.o1_flags, vec![true, false, false]);
21382 }
21383
21384 #[test]
21385 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21386 const B: usize = 19;
21387 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21388 p.set_o1(Some(crate::nystrom::O1Cfg {
21389 layers: crate::nystrom::O1Layers::All,
21390 m: 4,
21391 w: 8,
21392 sink: 2,
21393 rect: crate::nystrom::O1Rect::Aggregate,
21394 }));
21395 p.o1_begin_with_prefix(Some(B));
21396 let ids: Vec<u32> = (0..B as u32).collect();
21397 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21398
21399 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21400 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21401 let next = p.embed_single(B as u32);
21402 let _ = p.forward_layers(&next, B, None);
21403 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21404 }
21405
21406 #[test]
21407 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21408 const B: usize = 19;
21409 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21410 let gdn_cfg = crate::linear_core::GdnCfg {
21414 num_v_heads: 2,
21415 num_k_heads: 1,
21416 key_head_dim: 2,
21417 value_head_dim: 4,
21418 conv_kernel: 3,
21419 hidden_size: 8,
21420 rms_eps: 1e-6,
21421 output_gate_sigmoid: false,
21422 };
21423 let synth = |n: usize, salt: usize| -> Vec<f32> {
21424 (0..n)
21425 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21426 .collect()
21427 };
21428 let qt = |rows: usize, cols: usize, salt: usize| {
21429 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21430 };
21431 let c_dim = gdn_cfg.conv_dim();
21432 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21433 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21434 in_proj_qkv: qt(c_dim, 8, 1),
21435 in_proj_z: qt(vd, 8, 2),
21436 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21437 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21438 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21439 a_log: vec![0.2, 0.5],
21440 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21441 norm: vec![1.0; gdn_cfg.value_head_dim],
21442 out_proj: qt(8, vd, 7),
21443 });
21444 p.gdn_cfg = Some(gdn_cfg);
21445 p.set_o1(Some(crate::nystrom::O1Cfg {
21446 layers: crate::nystrom::O1Layers::All,
21447 m: 4,
21448 w: 8,
21449 sink: 2,
21450 rect: crate::nystrom::O1Rect::Aggregate,
21451 }));
21452 p.o1_begin_with_prefix(Some(B));
21453 for pos in 0..B - 2 {
21454 let emb = p.embed_single(pos as u32);
21455 let _ = p.forward_layers(&emb, pos, None);
21456 }
21457 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21458
21459 let e1 = p.embed_single((B - 2) as u32);
21460 let e2 = p.embed_single((B - 1) as u32);
21461 let _ = p.forward_pair(&e1, &e2, B - 2);
21462
21463 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21464 assert!(
21465 p.kv_cache
21466 .layers
21467 .iter()
21468 .enumerate()
21469 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21470 );
21471 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21472 assert_ne!(
21473 p.kv_cache.layers[0].linear_state, lane1_state,
21474 "real pair must commit GDN lane 2 before returning"
21475 );
21476 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21477 let next = p.embed_single(B as u32);
21478 let _ = p.forward_layers(&next, B, None);
21479 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21480 }
21481
21482 #[test]
21483 fn o1_error_observation_stays_terminal_until_reset() {
21484 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21485 p.set_o1(Some(crate::nystrom::O1Cfg {
21486 layers: crate::nystrom::O1Layers::All,
21487 m: 4,
21488 w: 8,
21489 sink: 2,
21490 rect: crate::nystrom::O1Rect::Aggregate,
21491 }));
21492 p.o1_begin();
21493 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21494
21495 assert!(p.o1_seal_checked().is_err());
21496 assert!(
21497 p.o1_seal_checked().is_err(),
21498 "retry must see the sticky error"
21499 );
21500 let k = vec![0.2f32; 4];
21501 let v = vec![0.3f32; 4];
21502 p.kv_cache.layers[0].append(&k, &v, &[]);
21503 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
21504
21505 p.reset_session();
21506 p.o1_begin();
21507 p.kv_cache.layers[0].append(&k, &v, &[]);
21508 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
21509 }
21510
21511 #[test]
21512 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
21513 let ids = vec![1u32, 2, 3, 4, 5, 6];
21514 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21515 p.graph_logits = Some(vec![123.0]);
21516 p.graph_want_logits = true;
21517 p.graph_failed
21518 .store(true, std::sync::atomic::Ordering::Relaxed);
21519 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21520 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
21521 assert!(err.contains("before NLL"));
21522 assert!(p.graph_logits.is_none());
21523 assert!(!p.graph_want_logits);
21524 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21525 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21526
21527 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21528 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21529 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21530 assert_eq!(actual.1, expected.1);
21531 assert!((actual.0 - expected.0).abs() < 1e-9);
21532 }
21533
21534 #[test]
21535 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
21536 let ids = vec![1u32, 2, 3, 4, 5, 6];
21537 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21538 p.nll_test_fail_at = Some(1);
21539 let err = p
21540 .nll_ids_from(&ids, 0)
21541 .expect_err("one-shot forward failure");
21542 assert!(err.contains("forward") || err.contains("score row"));
21543 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21544 assert!(!p.graph_want_logits);
21545 assert!(p.graph_logits.is_none());
21546 assert!(p.kv_history.is_empty());
21547
21548 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21549 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21550 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21551 assert_eq!(actual.1, expected.1);
21552 assert!((actual.0 - expected.0).abs() < 1e-9);
21553 }
21554
21555 #[test]
21556 fn nll_serial_failure_before_first_row_is_reported() {
21557 let ids = vec![1u32, 2, 3, 4];
21558 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21559 p.nll_test_force_serial = true;
21560 p.nll_test_fail_at = Some(0);
21561 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
21562 assert!(err.contains("serial forward"));
21563 assert!(p.kv_history.is_empty());
21564 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21565 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21566 }
21567
21568 #[test]
21569 fn ffn_probe_failure_discards_recorder_and_state() {
21570 let ids = vec![1u32, 2, 3, 4];
21571 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21572 p.nll_test_fail_at = Some(0);
21573 let err = p
21574 .probe_ffn_mass_batch(&ids)
21575 .expect_err("probe forward failure");
21576 assert!(err.contains("NLL"));
21577 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
21578 assert!(p.kv_history.is_empty());
21579 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21580 }
21581
21582 #[test]
21583 fn nll_test_controls_are_pipeline_scoped() {
21584 let ids = vec![1u32, 2, 3, 4];
21585 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21586 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21587 failing.nll_test_force_serial = true;
21588 failing.nll_test_fail_at = Some(0);
21589
21590 assert!(!failing.can_prefill_batched());
21591 assert!(unaffected.can_prefill_batched());
21592 let expected = unaffected
21593 .nll_ids_from(&ids, 0)
21594 .expect("unaffected pipeline remains usable");
21595 let err = failing
21596 .nll_ids_from(&ids, 0)
21597 .expect_err("failure injection belongs to failing pipeline");
21598 assert!(err.contains("serial forward"));
21599 assert!(failing.nll_test_fail_at.is_none());
21600 assert!(unaffected.can_prefill_batched());
21601 let actual = unaffected
21602 .nll_ids_from(&ids, 0)
21603 .expect("unaffected pipeline remains reusable");
21604 assert_eq!(actual.1, expected.1);
21605 assert!((actual.0 - expected.0).abs() < 1e-9);
21606 }
21607
21608 #[test]
21609 fn forward_ids_failure_channel_is_terminal_and_reusable() {
21610 let ids = vec![1u32, 2, 3, 4, 5, 6];
21611 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21612 p.graph_logits = Some(vec![123.0]);
21613 p.graph_want_logits = true;
21614 p.graph_failed
21615 .store(true, std::sync::atomic::Ordering::Relaxed);
21616 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21617
21618 let err = p
21619 .forward_ids(&ids, None)
21620 .expect_err("a failed forward must not become a valid head result");
21621 assert!(err.contains("forward_ids setup"));
21622 assert!(p.graph_logits.is_none());
21623 assert!(!p.graph_want_logits);
21624 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21625 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21626 assert_eq!(p.kv_cache.seq_len(), 0);
21627
21628 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
21629 .forward_ids(&ids, None)
21630 .expect("fresh forward_ids");
21631 let actual = p
21632 .forward_ids(&ids, None)
21633 .expect("pipeline remains reusable after a failed forward");
21634 assert_eq!(actual.len(), expected.len());
21635 assert!(
21636 actual
21637 .iter()
21638 .zip(expected)
21639 .all(|(a, b)| (a - b).abs() < 1e-9)
21640 );
21641 assert_eq!(p.kv_cache.seq_len(), ids.len());
21642 }
21643
21644 #[test]
21645 fn sigmoid_router_floor_is_explicit_per_architecture() {
21646 let zero = || DenseFfn {
21652 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21653 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21654 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21655 act: Act::Silu,
21656 down_t: None,
21657 segs: Vec::new(),
21658 };
21659 let m = MoeFfn {
21660 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
21661 experts: vec![zero(), zero()],
21662 top_k: 1,
21663 norm_topk_prob: true,
21664 router_sigmoid: true,
21665 expert_bias: None,
21666 routed_scaling: 2.5,
21667 route_tau: None,
21668 shared: None,
21669 stats: std::cell::RefCell::new(Vec::new()),
21670 act_sq: std::cell::RefCell::new(Vec::new()),
21671 act_rows: std::cell::RefCell::new(Vec::new()),
21672 mask: None,
21673 per_expert_scale: None,
21674 router_input_norm: false,
21675 resonance: None,
21676 grown: Vec::new(),
21677 };
21678 let logits = [-20.0f32, -20.0];
21679 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
21680 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
21681 let expected = (p[0] + 1e-20) / m.routed_scaling;
21682 assert!((glm_wsum - expected).abs() < 1e-15);
21683 assert!(generic_wsum > glm_wsum * 100.0);
21684 }
21685
21686 #[test]
21687 fn resonance_scores_match_formula_and_stable_tie() {
21688 let r = Resonance {
21689 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
21691 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
21692 k: 1,
21693 bias: vec![1.5, 0.5, 0.0],
21694 shell: Vec::new(),
21695 };
21696 let x = [1.0f32, 1.0];
21697 let mut got = vec![0.0; 3];
21698 r.scores(&x, &mut got);
21699 assert!((got[0] - 0.5).abs() < 1e-6);
21703 assert!((got[1] - 0.5).abs() < 1e-6);
21704 assert!(got[2].abs() < 1e-6);
21705 let best = got
21706 .iter()
21707 .enumerate()
21708 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
21709 .map(|(i, _)| i);
21710 assert_eq!(best, Some(0));
21711 assert!(got.iter().all(|v| v.is_finite()));
21712 }
21713
21714 #[test]
21719 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
21720 let plain = Resonance {
21726 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
21727 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
21728 k: 1,
21729 bias: vec![1.5, 0.5, 0.0, 0.0],
21730 shell: Vec::new(),
21731 };
21732 let shelled = Resonance {
21733 mu: plain.mu.clone(),
21734 u: plain.u.clone(),
21735 k: 1,
21736 bias: plain.bias.clone(),
21737 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
21738 };
21739 assert!(!plain.has_shell());
21740 assert!(shelled.has_shell());
21741 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
21742 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
21743 set_growth_shell(Some(true));
21744 assert!(growth_shell_enabled());
21745 let x = [1.0f32, 1.0];
21748 plain.scores(&x, &mut a);
21749 shelled.scores(&x, &mut b);
21750 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
21751 assert!(a[2] == 0.0 && a[3] == 0.0);
21752 let xo = [3.0f32, 0.0];
21755 plain.scores(&xo, &mut a);
21756 shelled.scores(&xo, &mut b);
21757 assert_eq!(a[2], -6.0);
21758 assert_eq!(a[3], -6.0);
21759 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
21760 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
21761 assert_eq!(shelled.effective_shell(4), shelled.shell);
21762 set_growth_shell(Some(false));
21765 assert!(!growth_shell_enabled());
21766 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
21767 shelled.scores(&xo, &mut b);
21768 assert_eq!(bits(&a), bits(&b));
21769 set_growth_shell(None);
21770 let short = Resonance {
21773 shell: vec![f32::INFINITY, f32::INFINITY],
21774 ..shelled
21775 };
21776 set_growth_shell(Some(true));
21777 short.scores(&xo, &mut b);
21778 assert_eq!(bits(&a), bits(&b));
21779 set_growth_shell(None);
21780 }
21781
21782 #[test]
21786 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
21787 let zero = || DenseFfn {
21788 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21789 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21790 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21791 act: Act::Silu,
21792 down_t: None,
21793 segs: Vec::new(),
21794 };
21795 let moe = |sigmoid: bool| MoeFfn {
21796 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
21797 experts: vec![zero(), zero(), zero(), zero()],
21798 top_k: 1,
21799 norm_topk_prob: true,
21800 router_sigmoid: sigmoid,
21801 expert_bias: None,
21802 routed_scaling: 1.0,
21803 route_tau: None,
21804 shared: None,
21805 stats: std::cell::RefCell::new(Vec::new()),
21806 act_sq: std::cell::RefCell::new(Vec::new()),
21807 act_rows: std::cell::RefCell::new(Vec::new()),
21808 mask: None,
21809 per_expert_scale: None,
21810 router_input_norm: false,
21811 resonance: None,
21812 grown: Vec::new(),
21813 };
21814 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
21815 for sigmoid in [false, true] {
21816 let m = moe(sigmoid);
21817 let (idx, p, wsum) = moe_route(&logits, &m, None);
21818 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
21819 assert_eq!(p[1], 0.0);
21820 assert_eq!(p[3], 0.0);
21821 assert!(p[2] > p[0] && p[0] > 0.0);
21822 assert!(p.iter().all(|v| v.is_finite()));
21823 let w = p[2] / wsum;
21824 if sigmoid {
21825 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
21827 } else {
21828 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
21829 }
21830 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
21833 assert_eq!(idx, vec![2]);
21834 }
21835 let m = moe(false);
21837 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
21838 assert_eq!(idx, vec![3]);
21839 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
21842 assert_eq!(idx, vec![0]);
21843 assert!(p.iter().all(|&v| v == 0.25));
21844 assert!(wsum.is_finite() && wsum > 0.0);
21845 }
21846
21847 #[test]
21852 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
21853 let zero = || DenseFfn {
21854 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21855 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21856 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21857 act: Act::Silu,
21858 down_t: None,
21859 segs: Vec::new(),
21860 };
21861 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
21862 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
21863 experts: vec![zero(), zero(), zero()],
21864 top_k: 1,
21865 norm_topk_prob: norm_topk,
21866 router_sigmoid: false,
21867 expert_bias: None,
21868 routed_scaling: 1.0,
21869 route_tau: None,
21870 shared: None,
21871 stats: std::cell::RefCell::new(Vec::new()),
21872 act_sq: std::cell::RefCell::new(Vec::new()),
21873 act_rows: std::cell::RefCell::new(Vec::new()),
21874 mask: None,
21875 per_expert_scale: None,
21876 router_input_norm: false,
21877 resonance: resonant.then(|| Resonance {
21878 mu: vec![0.0; 6],
21879 u: Vec::new(),
21880 k: 0,
21881 bias: vec![0.0; 3],
21882 shell: Vec::new(),
21883 }),
21884 grown: Vec::new(),
21885 };
21886 let lo = -0.1f32;
21889 let hi = f32::from_bits(lo.to_bits() - 1);
21890 assert!(hi > lo && hi - lo < 2f32.powi(-25));
21891 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
21892 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
21894 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
21895 for norm in [true, false] {
21898 let m = moe(true, norm);
21899 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
21900 assert_eq!(idx, vec![1], "norm_topk {norm}");
21901 assert_eq!(p, vec![0.0, 1.0, 0.0]);
21902 assert_eq!(p[1] / wsum, 1.0);
21903 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
21906 assert_eq!(idx, vec![0]);
21907 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
21909 assert_eq!(idx, vec![2]);
21910 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
21911 assert_eq!(idx, vec![0]);
21912 assert_eq!(p[0] / wsum, 1.0);
21913 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
21916 assert_eq!(idx, vec![0]);
21917 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
21918 }
21919 }
21920}