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 ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8632 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8633 Ok((nll / cnt.max(1) as f64).exp())
8634 }
8635
8636 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
8643 if !self.can_prefill_batched() {
8644 return self.nll_ids_from(ids, 0).map(|_| ());
8645 }
8646 self.nll_begin()?;
8647 let result: Result<(), String> = (|| {
8648 let chunk = chunk.max(1);
8649 let mut pos = 0usize;
8650 while pos < ids.len() {
8651 let end = (pos + chunk).min(ids.len());
8652 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
8653 self.nll_check_graph("calibration prefill", pos)?;
8654 pos = end;
8655 }
8656 Ok(())
8657 })();
8658 self.nll_end();
8659 result
8660 }
8661
8662 pub fn calib_window_span(&mut self, ids: &[u32], input: Option<&[f32]>, from: usize, mid: usize, upto: usize) -> Result<Vec<f32>, String> {
8671 if !self.can_prefill_batched() {
8672 return Err("calib_window_span: this architecture has no batched prefill".into());
8673 }
8674 self.nll_begin()?;
8675 let result: Result<Vec<f32>, String> = (|| {
8676 let h = if mid > from {
8677 let inp = match input {
8678 Some(h) => PrefillIn::Hidden(h),
8679 None => PrefillIn::Ids(ids),
8680 };
8681 let h = self.prefill_batch_span(inp, 0, None, from, mid);
8682 self.nll_check_graph("calibration span", 0)?;
8683 h
8684 } else {
8685 input.map(<[f32]>::to_vec).unwrap_or_default()
8686 };
8687 if upto > mid {
8688 let inp = if mid > from || input.is_some() { PrefillIn::Hidden(&h) } else { PrefillIn::Ids(ids) };
8689 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
8690 self.nll_check_graph("calibration span", 0)?;
8691 }
8692 Ok(h)
8693 })();
8694 self.nll_end();
8695 result
8696 }
8697
8698 #[cfg(feature = "gpu")]
8702 pub fn qwen4_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
8703 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
8704 crate::qwen4_exp::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, &self.inv_freq, self.pool.as_deref())
8705 }
8706
8707 pub fn qwen4_expert_dirs(&self, li: usize) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
8709 let b = self.qwen4_exp.as_ref()?;
8710 crate::qwen4_exp::expert_dirs(&b.1, li)
8711 }
8712
8713 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8718 self.clear_sequence_state();
8719 FFN_PROBE.with(|p| {
8720 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8721 });
8722 crate::gpu::cpu_scope(|| {
8723 for (pos, &id) in ids.iter().enumerate() {
8724 let emb = self.embed_single(id);
8725 let _ = self.forward_layers(&emb, pos, None);
8726 }
8727 });
8728 self.clear_sequence_state();
8729 FFN_PROBE
8730 .with(|p| p.borrow_mut().take())
8731 .unwrap_or_default()
8732 }
8733
8734 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8738 if let Err(err) = self.nll_begin() {
8739 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8743 self.nll_end();
8744 return Err(err);
8745 }
8746 FFN_PROBE.with(|p| {
8747 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8748 });
8749 let result: Result<(), String> = (|| {
8750 for chunk in ids.chunks(256) {
8751 if chunk.len() < 2 {
8752 continue;
8753 }
8754 self.nll_ids_masked(chunk, 0, None)?;
8755 }
8756 Ok(())
8757 })();
8758 self.nll_end();
8759 let probe = FFN_PROBE
8760 .with(|p| p.borrow_mut().take())
8761 .unwrap_or_default();
8762 match result {
8763 Ok(()) => Ok(probe),
8764 Err(err) => {
8765 drop(probe);
8766 Err(err)
8767 }
8768 }
8769 }
8770
8771 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8775 self.nll_begin()?;
8776 let result: Result<f64, String> = (|| {
8777 let mut nll = 0f64;
8778 let mut cnt = 0usize;
8779 let mut hidden = vec![0f32; self.hidden_size];
8780 for (pos, &id) in ids.iter().enumerate() {
8781 if pos > 0 {
8782 inference::rms_norm_into(
8783 &hidden,
8784 &self.weights.final_norm,
8785 self.rms_eps,
8786 self.norm_style,
8787 &mut self.ws.n1,
8788 );
8789 let mut logits = self.lm_head_forward(&self.ws.n1);
8790 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8791 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8792 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8793 nll -= p.max(1e-300).ln();
8794 cnt += 1;
8795 attention::recycle_buf(&mut logits);
8796 }
8797 let emb = self.embed_single(id);
8798 hidden = self.forward_layers(&emb, pos, Some(mask));
8799 self.nll_check_graph("masked serial forward", pos)?;
8800 let _ = self.graph_logits.take();
8804 }
8805 Ok((nll / cnt.max(1) as f64).exp())
8806 })();
8807 self.nll_end();
8808 result
8809 }
8810
8811 pub fn nll_ids_masked(
8830 &mut self,
8831 ids: &[u32],
8832 start: usize,
8833 task_mask: Option<&TaskMask>,
8834 ) -> Result<(f64, usize), String> {
8835 let task_mask = self.drop_open_mask(task_mask);
8836 self.nll_ids_inner(ids, start, task_mask)
8837 }
8838
8839 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
8840 self.nll_ids_inner(ids, start, None)
8841 }
8842
8843 fn nll_ids_inner(
8844 &mut self,
8845 ids: &[u32],
8846 start: usize,
8847 task_mask: Option<&TaskMask>,
8848 ) -> Result<(f64, usize), String> {
8849 self.nll_begin()?;
8850 let result: Result<(f64, usize), String> = (|| {
8851 let mut nll = 0f64;
8852 let mut cnt = 0usize;
8853 let (graph_quality, fused_head_quality) = nll_graph_policy(
8866 task_mask.is_none(),
8867 self.graph_prefill_preferred(),
8868 crate::gpu::q1_force(),
8869 );
8870 self.graph_head_required = fused_head_quality;
8871 self.graph_want_logits = fused_head_quality;
8872 #[cfg(target_os = "macos")]
8873 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
8874 match self.nll_batch_metal(ids, start) {
8875 MetalBatchNllOutcome::Completed(nll, count) => {
8876 return Ok((nll, count));
8877 }
8878 MetalBatchNllOutcome::Declined => {}
8879 MetalBatchNllOutcome::Failed(err) => return Err(err),
8880 }
8881 }
8882 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
8887 if self.can_prefill_batched() && !graph_quality && !force_serial {
8888 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
8897 .ok()
8898 .and_then(|v| v.parse().ok())
8899 .filter(|&c: &usize| c > 0)
8900 .unwrap_or(128);
8901 const LM_SUB: usize = 32;
8902 let n = ids.len().saturating_sub(1);
8903 let hs = self.hidden_size;
8904 let rows = self.weights.lm_head.rows();
8905 let mut pos = 0usize;
8906 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
8907 while pos < n {
8908 let end = (pos + chunk_len).min(n);
8909 let bsz = end - pos;
8910 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8911 self.nll_check_graph("batched prefill", pos)?;
8912 if state_trace && end % 256 == 0 {
8913 self.trace_recurrent_state(end);
8914 }
8915 let mut k0 = 0usize;
8916 while k0 < bsz {
8917 let k1 = (k0 + LM_SUB).min(bsz);
8918 let sb = k1 - k0;
8919 if pos + k1 <= start {
8922 k0 = k1;
8923 continue;
8924 }
8925 let mut normed = vec![0.0f32; sb * hs];
8926 for k in 0..sb {
8927 let r = inference::rms_norm(
8928 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
8929 &self.weights.final_norm,
8930 self.rms_eps,
8931 self.norm_style,
8932 );
8933 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
8934 }
8935 let mut logits = vec![0.0f32; sb * rows];
8936 self.weights
8937 .lm_head
8938 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
8939 for k in 0..sb {
8940 if pos + k0 + k < start {
8941 continue;
8942 }
8943 self.nll_check_graph("batched score row", pos + k0 + k)?;
8944 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
8945 if let Some(mu) = self.logit_multiplier {
8946 for v in lg.iter_mut() {
8947 *v *= mu;
8948 }
8949 }
8950 if let Some(c) = self.final_softcap {
8954 softcap_in_place(lg, c, self.pool.as_deref());
8955 }
8956 if let Some(cm) = self.head_clusters.clone() {
8959 self.hierarchical_head_logprobs(
8960 &normed[k * hs..(k + 1) * hs],
8961 &cm,
8962 lg,
8963 );
8964 }
8965 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
8966 let target = ids[pos + k0 + k + 1] as usize;
8967 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8968 let lse: f64 = lg
8969 .iter()
8970 .map(|&v| ((v - max) as f64).exp())
8971 .sum::<f64>()
8972 .ln()
8973 + max as f64;
8974 nll += lse - lg[target] as f64;
8975 cnt += 1;
8976 if std::env::var("CMF_PPL_TRACE").is_ok() {
8977 let top = lg
8978 .iter()
8979 .enumerate()
8980 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8981 .map(|(i, _)| i)
8982 .unwrap_or(0);
8983 eprintln!(
8984 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
8985 pos + k0 + k,
8986 target,
8987 lse - lg[target] as f64,
8988 top,
8989 lg[target],
8990 lg[top]
8991 );
8992 }
8993 }
8994 k0 = k1;
8995 }
8996 pos = end;
8997 }
8998 return Ok((nll, cnt));
8999 }
9000 for pos in 0..ids.len().saturating_sub(1) {
9001 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9002 self.nll_check_graph("serial forward", pos)?;
9003 let out_of_band = self.graph_logits.take();
9011 if self.graph_head_required && out_of_band.is_none() {
9012 METAL_GRAPH_HEAD_MISS.fetch_add(
9013 1,
9014 std::sync::atomic::Ordering::Relaxed,
9015 );
9016 return Err(format!(
9017 "fused Metal graph head did not complete at NLL position {pos}"
9018 ));
9019 }
9020 if pos < start {
9021 continue;
9022 }
9023 let logits = match out_of_band {
9024 Some(lg) => lg,
9025 None => {
9026 let normed = inference::rms_norm(
9027 &hidden,
9028 &self.weights.final_norm,
9029 self.rms_eps,
9030 self.norm_style,
9031 );
9032 self.lm_head_forward(&normed)
9036 }
9037 };
9038 let target = ids[pos + 1] as usize;
9039 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9040 let lse: f64 = logits
9041 .iter()
9042 .map(|&v| ((v - max) as f64).exp())
9043 .sum::<f64>()
9044 .ln()
9045 + max as f64;
9046 let tok_nll = lse - logits[target] as f64;
9047 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9048 let top = logits
9049 .iter()
9050 .enumerate()
9051 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9052 .map(|(i, _)| i)
9053 .unwrap_or(0);
9054 eprintln!(
9055 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9056 logits[target], logits[top]
9057 );
9058 }
9059 nll += tok_nll;
9060 cnt += 1;
9061 }
9062 Ok((nll, cnt))
9063 })();
9064 self.nll_end();
9065 result
9066 }
9067
9068 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9073 let normed = inference::rms_norm(
9074 hidden,
9075 &self.weights.final_norm,
9076 self.rms_eps,
9077 self.norm_style,
9078 );
9079 let mut logits = self.lm_head_forward(&normed);
9082 let target = target as usize;
9083 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9084 let lse: f64 = logits
9085 .iter()
9086 .map(|&v| ((v - max) as f64).exp())
9087 .sum::<f64>()
9088 .ln()
9089 + max as f64;
9090 let tok_nll = lse - logits[target] as f64;
9091 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9092 let top = logits
9093 .iter()
9094 .enumerate()
9095 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9096 .map(|(i, _)| i)
9097 .unwrap_or(0);
9098 eprintln!(
9099 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9100 logits[target], logits[top]
9101 );
9102 }
9103 attention::recycle_buf(&mut logits);
9104 tok_nll
9105 }
9106
9107 fn trace_recurrent_state(&self, pos: usize) {
9115 let stats = |v: &[f32]| -> (f64, f64) {
9116 if v.is_empty() {
9117 return (0.0, 0.0);
9118 }
9119 let (mut ss, mut mx) = (0f64, 0f64);
9120 for &x in v {
9121 ss += (x as f64) * (x as f64);
9122 mx = mx.max((x as f64).abs());
9123 }
9124 ((ss / v.len() as f64).sqrt(), mx)
9125 };
9126 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9127 let lw = &self.weights.layers[self.phys_layer(li)];
9128 let (kind, s_len) = match &lw.attn {
9129 AttnKind::Linear(_) => (
9130 "vmf",
9131 self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0),
9132 ),
9133 AttnKind::LinearGdn(_) => (
9134 "gdn",
9135 self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0),
9136 ),
9137 AttnKind::Bounded(_) => ("bounded", 0),
9138 AttnKind::Full { .. } => ("full", 0),
9139 _ => ("other", 0),
9140 };
9141 let (rms, max) = stats(&l.linear_state);
9142 let s_part = if kind == "vmf" {
9143 &l.linear_state[..s_len.min(l.linear_state.len())]
9144 } else if kind == "gdn" {
9145 let ring = l.linear_state.len().saturating_sub(s_len).min(l.linear_state.len());
9146 &l.linear_state[ring..]
9147 } else {
9148 &l.linear_state[..0]
9149 };
9150 let (s_rms, s_max) = stats(s_part);
9151 let (ring_rms, ring_len) = match &l.bounded {
9152 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9153 None => (0.0, 0),
9154 };
9155 eprintln!(
9156 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9157 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9158 l.linear_state.len(),
9159 l.seq_len
9160 );
9161 }
9162 }
9163
9164 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9182 self.nll_begin()?;
9187 let requested_prefix = (prefill > 0).then_some(prefill);
9188 self.o1_begin_with_prefix(requested_prefix);
9189 let n = ids.len().saturating_sub(1);
9190 let requested_start = prefill.min(n);
9191 let exact_end = if self.o1_active() {
9196 match requested_prefix {
9197 Some(requested) => self.o1_effective_boundary(requested),
9198 None => self
9199 .o1_cfg
9200 .as_ref()
9201 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9202 }
9203 .unwrap_or(requested_start)
9204 .min(n)
9205 } else {
9206 requested_start
9207 };
9208 let mut nll = 0f64;
9209 let mut cnt = 0usize;
9210
9211 let mut pos = 0usize;
9215 if self.can_prefill_batched() {
9216 const CHUNK: usize = 128;
9217 while pos < exact_end {
9218 let end = (pos + CHUNK).min(exact_end);
9219 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9220 if self
9221 .graph_failed
9222 .swap(false, std::sync::atomic::Ordering::Relaxed)
9223 {
9224 self.cancel
9225 .store(false, std::sync::atomic::Ordering::Relaxed);
9226 self.nll_end();
9227 return Err("GPU graph failed during O(1) NLL prefix".into());
9228 }
9229 for row in 0..end - pos {
9230 let score_pos = pos + row;
9231 if score_pos >= requested_start && score_pos < n {
9232 nll += self.nll_from_hidden(
9233 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9234 ids[score_pos + 1],
9235 score_pos,
9236 );
9237 cnt += 1;
9238 }
9239 }
9240 pos = end;
9241 }
9242 } else {
9243 while pos < exact_end {
9244 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9245 if self
9246 .graph_failed
9247 .swap(false, std::sync::atomic::Ordering::Relaxed)
9248 {
9249 self.cancel
9250 .store(false, std::sync::atomic::Ordering::Relaxed);
9251 self.nll_end();
9252 return Err("GPU graph failed during O(1) NLL prefix".into());
9253 }
9254 if pos >= requested_start && pos < n {
9255 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9256 cnt += 1;
9257 }
9258 pos += 1;
9259 }
9260 }
9261 self.o1_seal_checked().map_err(|err| {
9262 self.nll_end();
9263 err
9264 })?;
9265
9266 let batch_k = std::env::var("CMF_BATCH_K")
9275 .ok()
9276 .and_then(|v| v.parse::<usize>().ok())
9277 .unwrap_or(0);
9278 let batch_admitted = batch_k > 0
9279 && self.can_prefill_batched()
9280 && self.o1_active()
9281 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9282 && (0..self.num_layers).all(|li| {
9283 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9284 cache.o1.is_none() || cache.o1_views().is_some()
9285 });
9286 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9287 eprintln!(
9288 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9289 batch_admitted,
9290 batch_k,
9291 n.saturating_sub(exact_end),
9292 );
9293 }
9294 let mut batch_completed = false;
9295 if batch_admitted && exact_end < n {
9296 let hs = self.hidden_size;
9297 let mut batch_pos = exact_end;
9298 while batch_pos < n {
9299 let end = (batch_pos + batch_k).min(n);
9300 let bk = end - batch_pos;
9301 let mut hiddens = vec![0.0f32; bk * hs];
9302 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9303 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9304 }
9305 let positions: Vec<usize> = (batch_pos..end).collect();
9306 let t_batch = std::time::Instant::now();
9307 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9308 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9309 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9310 eprintln!(
9311 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9312 batch_pos,
9313 end.saturating_sub(1),
9314 bk as f64 / (ms / 1000.0),
9315 );
9316 }
9317 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9318 self.nll_end();
9319 return Err(err);
9320 }
9321 match outcome {
9322 crate::gpu::BatchGraphOutcome::Completed => {
9323 batch_completed = true;
9324 for row in 0..bk {
9325 nll += self.nll_from_hidden(
9326 &hiddens[row * hs..(row + 1) * hs],
9327 ids[batch_pos + row + 1],
9328 batch_pos + row,
9329 );
9330 cnt += 1;
9331 }
9332 batch_pos = end;
9333 }
9334 crate::gpu::BatchGraphOutcome::Declined => {
9335 if batch_completed {
9336 self.nll_end();
9337 return Err(format!(
9338 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9339 ));
9340 }
9341 break;
9342 }
9343 crate::gpu::BatchGraphOutcome::Failed => {
9344 self.nll_end();
9345 return Err(format!(
9346 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9347 ));
9348 }
9349 }
9350 }
9351 if batch_completed && cnt == n.saturating_sub(requested_start) {
9352 self.nll_end();
9353 return Ok((nll, cnt));
9354 }
9355 }
9356
9357 for pos in exact_end..n {
9362 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9363 if self
9364 .graph_failed
9365 .swap(false, std::sync::atomic::Ordering::Relaxed)
9366 {
9367 self.cancel
9368 .store(false, std::sync::atomic::Ordering::Relaxed);
9369 self.nll_end();
9370 return Err(format!(
9371 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9372 ));
9373 }
9374 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9375 cnt += 1;
9376 }
9377 self.nll_end();
9378 Ok((nll, cnt))
9379 }
9380
9381 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9389 self.clear_sequence_state();
9390 let n = ids.len().saturating_sub(1);
9391 let mut correct = Vec::with_capacity(n);
9392 let mut pmax = Vec::with_capacity(n);
9393 for pos in 0..n {
9394 let emb = self.embed_single(ids[pos]);
9395 let hidden = self.forward_layers(&emb, pos, None);
9396 let logits = if let Some(logits) = self.graph_logits.take() {
9397 logits
9398 } else {
9399 let normed = inference::rms_norm(
9400 &hidden,
9401 &self.weights.final_norm,
9402 self.rms_eps,
9403 self.norm_style,
9404 );
9405 self.lm_head_forward(&normed)
9409 };
9410 let target = ids[pos + 1] as usize;
9411 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9412 for (i, &v) in logits.iter().enumerate() {
9413 if v > mval {
9414 mval = v;
9415 amax = i;
9416 }
9417 }
9418 correct.push(amax == target);
9419 let row: Vec<f32> = temps
9420 .iter()
9421 .map(|&t| {
9422 let tt = t.max(1e-3);
9423 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9424 1.0 / s.max(1e-12) })
9426 .collect();
9427 pmax.push(row);
9428 }
9429 self.clear_sequence_state();
9430 (correct, pmax)
9431 }
9432
9433 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9440 if self.dyn_router.is_none() {
9441 return Ok((self.ppl_ids(ids)?, 0));
9442 }
9443 self.nll_begin()?;
9444 let saved_active = self.dyn_active;
9445 let mut router = self
9446 .dyn_router
9447 .take()
9448 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9449 router.reset();
9450 self.dyn_phi_seen = 0;
9451 let _ = self.set_active_skill(None);
9452
9453 let result: Result<(f64, usize), String> = (|| {
9454 let mut nll = 0f64;
9455 let mut cnt = 0usize;
9456 for pos in 0..ids.len().saturating_sub(1) {
9457 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9458 self.nll_check_graph("dynamic serial forward", pos)?;
9459 let out_of_band = self.graph_logits.take();
9460 let mut logits = match out_of_band {
9461 Some(lg) => lg,
9462 None => {
9463 let normed = inference::rms_norm(
9464 &hidden,
9465 &self.weights.final_norm,
9466 self.rms_eps,
9467 self.norm_style,
9468 );
9469 self.lm_head_forward(&normed)
9473 }
9474 };
9475 let target = ids[pos + 1] as usize;
9476 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9477 let lse: f64 = logits
9478 .iter()
9479 .map(|&v| ((v - max) as f64).exp())
9480 .sum::<f64>()
9481 .ln()
9482 + max as f64;
9483 let tok_nll = lse - logits[target] as f64;
9484 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9485 let top = logits
9486 .iter()
9487 .enumerate()
9488 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9489 .map(|(i, _)| i)
9490 .unwrap_or(0);
9491 eprintln!(
9492 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9493 logits[target], logits[top]
9494 );
9495 }
9496 nll += tok_nll;
9497 cnt += 1;
9498 attention::recycle_buf(&mut logits);
9499 let phi = self.dyn_phi_ema.clone();
9501 if let Some(new_active) = router.step(&phi, pos) {
9502 let _ = self.set_active_skill(new_active);
9503 }
9504 }
9505 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9506 })();
9507
9508 let _ = self.set_active_skill(saved_active);
9511 self.dyn_router = Some(router);
9512 self.nll_end();
9513 result
9514 }
9515
9516 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9518 self.clear_sequence_state();
9519 let mut acc = vec![0f32; self.hidden_size];
9520 for (pos, &id) in ids.iter().enumerate() {
9521 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9522 for (a, v) in acc.iter_mut().zip(&h) {
9523 *a += v;
9524 }
9525 }
9526 let n = ids.len().max(1) as f32;
9527 for a in acc.iter_mut() {
9528 *a /= n;
9529 }
9530 self.clear_sequence_state();
9531 acc
9532 }
9533
9534 pub fn probe_phi_span(
9548 &mut self,
9549 ids: &[u32],
9550 layer: usize,
9551 span: std::ops::Range<usize>,
9552 ) -> Vec<f32> {
9553 #[cfg(target_os = "macos")]
9554 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9555 let end = span.end.min(ids.len());
9556 let start = span.start.min(end);
9557 let reset = |p: &mut Self| p.clear_sequence_state();
9558 reset(self);
9559 let mut acc = vec![0f32; self.hidden_size];
9560 for (pos, &id) in ids[..end].iter().enumerate() {
9561 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9562 if pos >= start {
9563 for (a, v) in acc.iter_mut().zip(&h) {
9564 *a += v;
9565 }
9566 }
9567 }
9568 let n = end - start;
9569 if n > 0 {
9570 let n = n as f32;
9571 for a in acc.iter_mut() {
9572 *a /= n;
9573 }
9574 }
9575 reset(self);
9576 acc
9577 }
9578
9579 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9586 #[cfg(target_os = "macos")]
9587 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9588 self.graph_logits = None;
9589 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9590 if let Some(logits) = self.graph_logits.take() {
9591 return logits;
9592 }
9593 inference::rms_norm_into(
9594 &hidden,
9595 &self.weights.final_norm,
9596 self.rms_eps,
9597 self.norm_style,
9598 &mut self.ws.n1,
9599 );
9600 self.lm_head_forward(&self.ws.n1)
9601 }
9602
9603 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9609 self.prefill_batch_masked(ids, start_pos, None)
9610 }
9611
9612 fn prefill_batch_masked(
9618 &mut self,
9619 ids: &[u32],
9620 start_pos: usize,
9621 task_mask: Option<&TaskMask>,
9622 ) -> Vec<f32> {
9623 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9624 }
9625
9626 fn prefill_rows(
9634 &mut self,
9635 ids: &[u32],
9636 pos: usize,
9637 task_mask: Option<&TaskMask>,
9638 ) -> Result<Vec<f32>, String> {
9639 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9640 }
9641
9642 fn prefill_input_rows(
9643 &mut self,
9644 input: PrefillIn<'_>,
9645 pos: usize,
9646 task_mask: Option<&TaskMask>,
9647 ) -> Result<Vec<f32>, String> {
9648 self.mimo_moe_prepare();
9649 let hs = self.hidden_size;
9650 let bk = match input {
9651 PrefillIn::Ids(ids) => ids.len(),
9652 PrefillIn::Hidden(rows) => rows.len() / hs,
9653 };
9654 #[cfg(not(target_os = "macos"))]
9655 if task_mask.is_none()
9656 && !self.o1_active()
9657 && bk > 1
9658 && (self.batch_prefix_prefill()
9659 || (self.verify_exact_moe
9660 && crate::gpu::enabled_here()
9661 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9662 {
9663 let mut hiddens = match input {
9664 PrefillIn::Hidden(rows) => rows.to_vec(),
9665 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9666 };
9667 let positions: Vec<usize> = (pos..pos + bk).collect();
9668 let mut run = 0usize;
9669 match self.try_batch_graph_wgpu_prefix(
9670 &mut hiddens,
9671 &positions,
9672 bk,
9673 None,
9674 Some(&mut run),
9675 ) {
9676 crate::gpu::BatchGraphOutcome::Completed => {
9677 let out = if run < self.num_layers {
9678 self.prefill_batch_span(
9679 PrefillIn::Hidden(&hiddens),
9680 pos,
9681 None,
9682 run,
9683 self.num_layers,
9684 )
9685 } else {
9686 hiddens
9687 };
9688 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9689 Err("MiMo attention graph failed after admission".into())
9690 } else { Ok(out) };
9691 }
9692 crate::gpu::BatchGraphOutcome::Failed => {
9693 return Err("batched prefix prefill failed after admission".into());
9694 }
9695 crate::gpu::BatchGraphOutcome::Declined => {
9696 #[cfg(feature = "gpu")]
9698 self.pull_lagging_host_kv(0, self.num_layers, pos);
9699 }
9700 }
9701 }
9702 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9703 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9704 Err("batch tail graph failed after admission".into())
9705 } else { Ok(out) }
9706 }
9707
9708 fn prefill_batch_span(
9714 &mut self,
9715 input: PrefillIn<'_>,
9716 start_pos: usize,
9717 task_mask: Option<&TaskMask>,
9718 from: usize,
9719 upto_excl: usize,
9720 ) -> Vec<f32> {
9721 let hs = self.hidden_size;
9722 let b = match input {
9723 PrefillIn::Ids(ids) => ids.len(),
9724 PrefillIn::Hidden(hb) => hb.len() / hs,
9725 };
9726 let upto_excl = upto_excl.min(self.num_layers);
9727 let mut h: Vec<f32>;
9731 let mut h_ready;
9732 match input {
9733 PrefillIn::Ids(_) => {
9734 h = vec![0.0; b * hs];
9735 h_ready = false;
9736 }
9737 PrefillIn::Hidden(hb) => {
9738 h = hb.to_vec();
9739 h_ready = true;
9740 }
9741 }
9742 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9743 if let PrefillIn::Ids(ids) = input {
9744 for (bi, &id) in ids.iter().enumerate() {
9745 let e = me.embed_single(id);
9746 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9747 }
9748 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9749 if let Ok(t) = tp.parse::<usize>() {
9750 if t >= start_pos && t < start_pos + ids.len() {
9751 let bi = t - start_pos;
9752 let row = &h[bi * hs..(bi + 1) * hs];
9753 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9754 eprintln!(
9755 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9756 ids[bi],
9757 row[0],
9758 row[1],
9759 ids.len(),
9760 &ids[..ids.len().min(8)]
9761 );
9762 }
9763 }
9764 }
9765 }
9766 };
9767 let (_nkv, _hd, _rd, eps) = (
9768 self.num_kv_heads,
9769 self.head_dim,
9770 self.rotary_dim,
9771 self.rms_eps,
9772 );
9773 let pool = self.pool.clone();
9774 let norm_style = self.norm_style;
9775 self.mimo_moe_prepare();
9776 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9777
9778 #[cfg(target_os = "macos")]
9779 let mut chunk_skip_until = 0usize;
9780 for li in from..upto_excl {
9781 let _capacity_tail = automatic_gpu_prefix
9782 .filter(|&prefix| {
9783 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9784 })
9785 .map(|_| crate::gpu::enter_cpu_scope());
9786 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9791 let _fast_gemm = self
9792 .proj_gate_sigmoid
9793 .then(crate::gpu::enter_prefill_fast_gemm);
9794 #[cfg(target_os = "macos")]
9800 if task_mask.is_none() {
9801 if li < chunk_skip_until {
9802 continue;
9803 }
9804 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9810 fill_h(&mut h, self);
9811 h_ready = true;
9812 }
9813 let ids_for_embed = match input {
9814 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9815 PrefillIn::Hidden(_) => None,
9816 };
9817 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9818 if end > li {
9819 h_ready = true;
9820 chunk_skip_until = end;
9821 if self.is_loop_end(end - 1) && end < self.num_layers {
9824 for bi in 0..b {
9825 let normed = inference::rms_norm(
9826 &h[bi * hs..(bi + 1) * hs],
9827 &self.weights.final_norm,
9828 eps,
9829 norm_style,
9830 );
9831 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9832 }
9833 }
9834 continue;
9835 }
9836 }
9837 if !h_ready {
9838 fill_h(&mut h, self);
9839 h_ready = true;
9840 }
9841 if task_mask.is_none() && self.verify_exact_moe {
9842 let positions: Vec<_> = (start_pos..start_pos + b).collect();
9843 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
9844 crate::gpu::BatchGraphOutcome::Completed => continue,
9845 crate::gpu::BatchGraphOutcome::Failed => return h,
9846 crate::gpu::BatchGraphOutcome::Declined => {},
9847 }
9848 }
9849 #[cfg(feature = "gpu")]
9850 self.pull_lagging_host_kv(li, li + 1, start_pos);
9851 let lw = &self.weights.layers[self.phys_layer(li)];
9852 let t_attn = std::time::Instant::now();
9853 match &lw.attn {
9855 AttnKind::Kda(w) => {
9856 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9858 let mut normed = vec![0.0f32; b * hs];
9859 for bi in 0..b {
9860 inference::rms_norm_into(
9861 &h[bi * hs..(bi + 1) * hs],
9862 &lw.input_norm,
9863 eps,
9864 norm_style,
9865 &mut normed[bi * hs..(bi + 1) * hs],
9866 );
9867 }
9868 let attn = crate::linear_core::kda_forward_batch(
9869 &normed,
9870 b,
9871 w,
9872 &cfg,
9873 &mut self.kv_cache.layers[li].linear_state,
9874 pool.as_deref(),
9875 );
9876 for (dst, &a) in h.iter_mut().zip(&attn) {
9877 *dst += a;
9878 }
9879 }
9880 AttnKind::LinearGdn(w) => {
9881 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9883 let mut normed = vec![0.0f32; b * hs];
9884 for bi in 0..b {
9885 let r = inference::rms_norm(
9886 &h[bi * hs..(bi + 1) * hs],
9887 &lw.input_norm,
9888 eps,
9889 norm_style,
9890 );
9891 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9892 }
9893 let attn = crate::linear_core::gdn_forward_batch(
9894 &normed,
9895 b,
9896 w,
9897 &cfg,
9898 &mut self.kv_cache.layers[li].linear_state,
9899 pool.as_deref(),
9900 );
9901 for (dst, &a) in h.iter_mut().zip(&attn) {
9902 *dst += a;
9903 }
9904 }
9905 AttnKind::ShortConv(w) => {
9906 let cfg = self
9909 .short_conv_cfg
9910 .expect("short-conv layer without short_conv_cfg");
9911 let mut normed = vec![0.0f32; b * hs];
9912 for bi in 0..b {
9913 inference::rms_norm_into(
9914 &h[bi * hs..(bi + 1) * hs],
9915 &lw.input_norm,
9916 eps,
9917 norm_style,
9918 &mut normed[bi * hs..(bi + 1) * hs],
9919 );
9920 }
9921 let attn = short_conv_forward_batch(
9922 &normed,
9923 b,
9924 w,
9925 &cfg,
9926 &mut self.kv_cache.layers[li].linear_state,
9927 pool.as_deref(),
9928 );
9929 for (dst, &a) in h.iter_mut().zip(&attn) {
9930 *dst += a;
9931 }
9932 }
9933 AttnKind::Mla(w) => {
9934 let inv_freq_l = self.layer_inv_freq(li);
9937 let rs = self.layer_rope_scale(li);
9938 let mut normed = vec![0.0f32; hs];
9939 for bi in 0..b {
9940 inference::rms_norm_into(
9941 &h[bi * hs..(bi + 1) * hs],
9942 &lw.input_norm,
9943 eps,
9944 norm_style,
9945 &mut normed,
9946 );
9947 let ao = mla_attention(
9948 w,
9949 &normed,
9950 &mut self.kv_cache.layers[li],
9951 start_pos + bi,
9952 &inv_freq_l,
9953 rs,
9954 eps,
9955 pool.as_deref(),
9956 );
9957 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
9958 *dst += a;
9959 }
9960 }
9961 }
9962 AttnKind::Full {
9963 wq,
9964 wk,
9965 wv,
9966 wo,
9967 q_norm,
9968 k_norm,
9969 output_gate,
9970 softplus_gate,
9971 bias,
9972 } => {
9973 let mut normed = vec![0.0f32; b * hs];
9977 for bi in 0..b {
9978 inference::rms_norm_into(
9979 &h[bi * hs..(bi + 1) * hs],
9980 &lw.input_norm,
9981 eps,
9982 norm_style,
9983 &mut normed[bi * hs..(bi + 1) * hs],
9984 );
9985 }
9986 let inv_freq_l = self.layer_inv_freq(li);
9987 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9988 let cfg = QwenAttnCfg {
9989 num_heads: self.layer_num_heads(li),
9990 num_kv_heads: nkv_l,
9991 head_dim: hd_l,
9992 hidden_size: hs,
9993 position: start_pos,
9994 inv_freq: &inv_freq_l,
9995 rotary_dim: rd_l,
9996 scale: self.attn_scale,
9997 softcap: self.attn_softcap,
9998 window: self.layer_window(li),
9999 v_norm: self.attn_v_norm,
10000 qk_norm_after_rope: self.qk_norm_after_rope,
10001 gate_sigmoid: self.proj_gate_sigmoid,
10002 q_norm: q_norm.as_deref(),
10003 k_norm: k_norm.as_deref(),
10004 output_gate: *output_gate,
10005 softplus_gate: softplus_gate
10006 .as_ref()
10007 .map(|(gate, per_head)| (gate, *per_head)),
10008 rope_scale: self.layer_rope_scale(li),
10009 bias: bias
10010 .as_ref()
10011 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10012 rms_eps: eps,
10013 norm_style,
10014 pool: pool.as_deref(),
10015 v_head_dim: self.layer_v_dim(li),
10016 };
10017 #[cfg(feature = "gpu")]
10018 let _mirror = self
10019 .prefill_mirror_target(li)
10020 .map(crate::gpu::enter_prefill_mirror);
10021 let mut attn = attention::qwen_attention_batch(
10022 &normed,
10023 b,
10024 wq,
10025 wk,
10026 wv,
10027 wo,
10028 &mut self.kv_cache.layers[li],
10029 &cfg,
10030 );
10031 if let Some(w) = &lw.attn_out_norm {
10032 for bi in 0..b {
10033 inference::rms_norm_into(
10034 &attn[bi * hs..(bi + 1) * hs],
10035 w,
10036 eps,
10037 norm_style,
10038 &mut normed[bi * hs..(bi + 1) * hs],
10039 );
10040 }
10041 attn.copy_from_slice(&normed);
10042 }
10043 for (dst, &a) in h.iter_mut().zip(&attn) {
10044 *dst += a;
10045 }
10046 }
10047 AttnKind::Bounded(w) => {
10048 let mut normed = vec![0.0f32; b * hs];
10051 for bi in 0..b {
10052 inference::rms_norm_into(
10053 &h[bi * hs..(bi + 1) * hs],
10054 &lw.input_norm,
10055 eps,
10056 norm_style,
10057 &mut normed[bi * hs..(bi + 1) * hs],
10058 );
10059 }
10060 let rope = self
10061 .bounded_rope
10062 .clone()
10063 .expect("bounded layer without an installed rotation table");
10064 let cfg = crate::bounded::BoundedAttnCfg {
10065 num_heads: self.num_heads,
10066 num_kv_heads: self.num_kv_heads,
10067 head_dim: self.head_dim,
10068 hidden_size: hs,
10069 scale: self.attn_scale,
10070 rope: &rope,
10071 pool: pool.as_deref(),
10072 };
10073 let mut attn = crate::bounded::bounded_attention_batch(
10074 &normed,
10075 b,
10076 w,
10077 &mut self.kv_cache.layers[li],
10078 &cfg,
10079 );
10080 if let Some(wn) = &lw.attn_out_norm {
10081 for bi in 0..b {
10082 inference::rms_norm_into(
10083 &attn[bi * hs..(bi + 1) * hs],
10084 wn,
10085 eps,
10086 norm_style,
10087 &mut normed[bi * hs..(bi + 1) * hs],
10088 );
10089 }
10090 attn.copy_from_slice(&normed);
10091 }
10092 for (dst, &a) in h.iter_mut().zip(&attn) {
10093 *dst += a;
10094 }
10095 attention::recycle_buf(&mut attn);
10096 }
10097 AttnKind::Linear(w) => {
10098 for bi in 0..b {
10099 let normed = inference::rms_norm(
10100 &h[bi * hs..(bi + 1) * hs],
10101 &lw.input_norm,
10102 eps,
10103 norm_style,
10104 );
10105 vmf_phase_forward(
10106 &normed,
10107 w,
10108 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10109 &mut self.kv_cache.layers[li].linear_state,
10110 pool.as_deref(),
10111 )
10112 .iter()
10113 .enumerate()
10114 .for_each(|(i, &a)| h[bi * hs + i] += a);
10115 }
10116 }
10117 }
10118
10119 let lw = &self.weights.layers[self.phys_layer(li)];
10121 let mut post = vec![0.0f32; b * hs];
10122 for bi in 0..b {
10123 let r =
10124 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10125 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10126 }
10127 let mask_row = task_mask
10130 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10131 .and_then(|m| m.ffn_masks.get(li))
10132 .map(|v| v.as_slice());
10133 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10134 let t_ffn = std::time::Instant::now();
10135 let mut ffn = match &lw.ffn {
10136 FfnKind::Dense(d) if !d.segs.is_empty() => {
10137 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10138 }
10139 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10140 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10141 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10142 }
10143 FfnKind::Moe(m) if self.verify_exact_moe => {
10144 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10145 }
10146 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10149 let before = m.stats.borrow().clone();
10150 let out = crate::gpu::cpu_scope(|| {
10151 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10152 });
10153 self.mimo_moe.prime(li, m, &before);
10154 out
10155 }
10156 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10157 FfnKind::DenseMoe(dm) => {
10160 let mut out = vec![0.0f32; b * hs];
10161 for bi in 0..b {
10162 let r = dense_moe_ffn(
10163 dm,
10164 &post[bi * hs..(bi + 1) * hs],
10165 &h[bi * hs..(bi + 1) * hs],
10166 eps,
10167 norm_style,
10168 pool.as_deref(),
10169 );
10170 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10171 }
10172 out
10173 }
10174 };
10175 if prefill_prof_on() {
10176 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10177 PREFILL_SPLIT[1].fetch_add(t_ffn.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
10178 }
10179 if let Some(w) = &lw.ffn_out_norm {
10180 for bi in 0..b {
10181 inference::rms_norm_into(
10182 &ffn[bi * hs..(bi + 1) * hs],
10183 w,
10184 eps,
10185 norm_style,
10186 &mut post[bi * hs..(bi + 1) * hs],
10187 );
10188 }
10189 ffn.copy_from_slice(&post);
10190 }
10191 for (dst, &f) in h.iter_mut().zip(&ffn) {
10192 *dst += f;
10193 }
10194 if let Some(sc) = lw.layer_scale {
10195 for v in h.iter_mut() {
10196 *v *= sc;
10197 }
10198 }
10199 if self.layer_dump.is_some() {
10201 for bi in 0..b {
10202 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10203 }
10204 }
10205 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10206 if let Ok(t) = tp.parse::<usize>() {
10207 if t >= start_pos && t < start_pos + b {
10208 let bi = t - start_pos;
10209 let row = &h[bi * hs..(bi + 1) * hs];
10210 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10211 eprintln!(
10212 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10213 row[0], row[1]
10214 );
10215 }
10216 }
10217 }
10218 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10222 let row = &h[(b - 1) * hs..b * hs];
10223 let rms =
10224 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10225 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10226 eprintln!(
10227 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10228 match &self.weights.layers[self.phys_layer(li)].attn {
10229 AttnKind::LinearGdn(_) => "gdn",
10230 AttnKind::Linear(_) => "vmf",
10231 AttnKind::ShortConv(_) => "conv",
10232 _ => "attn",
10233 },
10234 match &lw.ffn {
10235 FfnKind::Moe(_) => "moe",
10236 FfnKind::Dense(_) => "dense",
10237 FfnKind::DenseMoe(_) => "dense+moe",
10238 },
10239 );
10240 }
10241 if self.is_loop_end(li) && li + 1 < self.num_layers {
10243 for bi in 0..b {
10244 let normed = inference::rms_norm(
10245 &h[bi * hs..(bi + 1) * hs],
10246 &self.weights.final_norm,
10247 eps,
10248 norm_style,
10249 );
10250 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10251 }
10252 }
10253 if std::env::var("CMF_TRACE_H").is_ok() {
10254 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10255 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10256 eprintln!(
10257 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10258 lw.layer_scale
10259 );
10260 }
10261 }
10262 crate::gpu::set_layer(-1); if prefill_prof_on() {
10264 let ms = |a: &std::sync::atomic::AtomicU64| {
10265 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10266 };
10267 let sp = &attention::ATTN_SPLIT;
10268 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10269 eprintln!(
10270 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10271 host gate {:.1}], host loop {:.1}, attend {:.1} \
10272 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10273 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10274 ms(&PREFILL_SPLIT[0]),
10275 ms(&sp[0]),
10276 qkv_enq,
10277 qkv_wait,
10278 ms(&sp[6]),
10279 ms(&sp[1]),
10280 ms(&sp[2]),
10281 ms(&sp[4]),
10282 ms(&sp[5]),
10283 ca_wait,
10284 ms(&sp[3]),
10285 ms(&PREFILL_SPLIT[1]),
10286 );
10287 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10288 eprintln!(
10289 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10290 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10291 fallbacks (cumulative)",
10292 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10293 );
10294 }
10295 self.o1_progress();
10300 self.swa_trim_tails();
10302 h
10303 }
10304
10305 fn embed_single(&self, id: u32) -> Vec<f32> {
10307 let mut out = vec![0.0f32; self.hidden_size];
10308 if (id as usize) < self.weights.embed_tokens.rows() {
10309 self.weights.embed_tokens.row_f32(id as usize, &mut out);
10310 }
10311 if self.embed_multiplier != 1.0 {
10312 for v in out.iter_mut() {
10313 *v *= self.embed_multiplier;
10314 }
10315 }
10316 if self.dsv4.is_some()
10320 || self.dsv41.is_some()
10321 || self.qwen4_exp.is_some()
10322 {
10323 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10324 v[0] = id as f32;
10325 return v;
10326 }
10327 if let Some(b) = &self.g3n {
10330 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10331 }
10332 out
10333 }
10334
10335 #[cfg(target_os = "macos")]
10341 fn chunk_run_gpu(
10342 &mut self,
10343 li0: usize,
10344 h: &mut [f32],
10345 b: usize,
10346 pos0: usize,
10347 embed_ids: Option<&[u32]>,
10348 cap: usize,
10349 ) -> usize {
10350 let gemma4 = self.metal_gemma4()
10357 && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10358 if !crate::gpu::enabled_here()
10359 || std::env::var("CMF_GPU_CHUNK")
10360 .map(|v| v == "0")
10361 .unwrap_or(false)
10362 || b < 32
10363 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10366 || (self.global_attn.is_some() && !gemma4)
10367 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10369 || self.o1_active()
10372 || (self.attn_v_norm && !gemma4)
10373 || !self.metal_rope_scale_ok()
10374 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10375 {
10376 return li0;
10377 }
10378 let Some(model) = self.model.clone() else {
10379 return li0;
10380 };
10381 let (nh, nkv, hd, hs) = (
10382 self.num_heads,
10383 self.num_kv_heads,
10384 self.head_dim,
10385 self.hidden_size,
10386 );
10387 let loop_end = if self.loop_final_norm {
10391 ((li0 / self.physical_layers) + 1) * self.physical_layers
10392 } else {
10393 self.num_layers
10394 };
10395 let run_end = self.num_layers.min(loop_end).min(cap);
10396 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10401 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10402 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10403 let mut stored_at: Vec<usize> = Vec::new();
10404 for li in li0..run_end {
10405 let lw = &self.weights.layers[self.phys_layer(li)];
10406 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10407 && !gemma4
10408 {
10409 break;
10410 }
10411 let AttnKind::Full {
10412 wq,
10413 wk,
10414 wv,
10415 wo,
10416 q_norm,
10417 k_norm,
10418 output_gate: false,
10419 softplus_gate,
10420 bias,
10421 } = &lw.attn
10422 else {
10423 break;
10424 };
10425 let head_gate = match softplus_gate {
10427 None => None,
10428 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10429 Some((d, r, c)) if r == nh && c == hs => Some(d),
10430 _ => break,
10431 },
10432 Some(_) => break,
10433 };
10434 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10438 FfnKind::Dense(d)
10439 if (matches!(d.act, Act::Silu | Act::Gelu)
10440 || (gemma4 && d.act == Act::GeluTanh))
10441 && d.segs.is_empty() =>
10442 {
10443 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10444 }
10445 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10446 Some((router, experts, q8)) => {
10447 let e0 = &m.experts[0];
10448 let route = Box::new(move |lg: &[f32]| {
10449 let (idx, p, wsum) = moe_route(lg, m, None);
10451 let mut st = m.stats.borrow_mut();
10452 if st.len() < m.experts.len() {
10453 st.resize(m.experts.len(), 0);
10454 }
10455 for &e in &idx {
10456 st[e] += 1;
10457 }
10458 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10459 (idx, w)
10460 });
10461 (
10462 &e0.gate_proj,
10463 &e0.up_proj,
10464 &e0.down_proj,
10465 Act::Silu,
10466 Some(crate::gpu_metal::ChunkMoe {
10467 router,
10468 experts,
10469 q8,
10470 route,
10471 }),
10472 )
10473 }
10474 None => break,
10475 },
10476 _ => break,
10477 };
10478 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10484 t.q8_row_parts()
10485 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10486 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10487 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10488 }
10489 fn cf(t: &QTensor) -> &[f32] {
10490 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10491 }
10492 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10493 if moe.is_none()
10502 && !gemma4
10503 && (col.iter().any(|c| !c.is_empty())
10504 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10505 {
10506 break;
10507 }
10508 let parts = (
10509 cw(wq),
10510 cw(wk),
10511 cw(wv),
10512 cw(wo),
10513 cw(gate_t),
10514 cw(up_t),
10515 cw(down_t),
10516 );
10517 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10518 else {
10519 break;
10520 };
10521 let layer = &self.kv_cache.layers[li];
10522 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10523 break;
10524 }
10525 debug_assert!(
10527 layer.base() == 0
10528 || self
10529 .layer_window(li)
10530 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10531 );
10532 stored_at.push(layer.head_len(0));
10533 let (nkv, hd) = if gemma4 {
10536 let (a, bb, _) = self.layer_geom(li);
10537 (a, bb)
10538 } else {
10539 (nkv, hd)
10540 };
10541 layers.push(crate::gpu_metal::ChunkLayer {
10542 model: &model,
10543 kv_id: self.graph_kv_id,
10544 layer: li,
10545 wq: pq,
10546 wk: pk,
10547 wv: pv,
10548 wo: po,
10549 gate: pg,
10550 up: pu,
10551 down: pd,
10552 input_norm: &lw.input_norm,
10553 post_norm: &lw.post_norm,
10554 bias: bias
10555 .as_ref()
10556 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10557 q_norm: q_norm.as_deref(),
10558 k_norm: k_norm.as_deref(),
10559 inv_freq: &tables[li - li0],
10560 rope_scale: self.layer_rope_scale(li),
10561 rd: self.layer_geom(li).2,
10562 nh,
10563 nkv,
10564 hd,
10565 hs,
10566 inter: gate_t.rows(),
10567 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10568 late_qk_norm: self.qk_norm_after_rope,
10569 eps: self.rms_eps as f32,
10570 window: self.layer_window(li),
10571 head_gate,
10572 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10573 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10574 v_norm: self.attn_v_norm,
10575 scale: self.attn_scale,
10576 attn_out_norm: lw.attn_out_norm.as_deref(),
10577 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10578 layer_scale: lw.layer_scale,
10579 }),
10580 col,
10581 moe,
10582 });
10583 }
10584 if layers.is_empty() {
10585 return li0;
10586 }
10587 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10589 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10590 .iter()
10591 .zip(&rows_kv)
10592 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10593 .collect();
10594 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10595 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10596 let li = layers[i].layer;
10597 let layer = &self.kv_cache.layers[li];
10598 let nkv = rows_kv[i].0;
10599 io.push(crate::gpu_metal::ChunkIo {
10600 cpu_stored: stored_at[i],
10601 cpu_gen: layer.generation(),
10602 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10603 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10604 out_k: ok,
10605 out_v: ov,
10606 imp: oi,
10607 });
10608 }
10609 let n_run = layers.len();
10610 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10611 let ep = embed_ids.and_then(|ids| {
10614 self.weights
10615 .embed_tokens
10616 .q8_row_parts()
10617 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10618 idx,
10619 rows,
10620 row_scale: rs,
10621 ids,
10622 mult: self.embed_multiplier,
10623 })
10624 });
10625 if embed_ids.is_some() && ep.is_none() {
10626 return li0;
10627 }
10628 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10629 return li0;
10630 }
10631 drop(io);
10632 drop(layers);
10633 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10636 let li = li0 + i;
10637 let row = rows_kv[i].1;
10638 let layer = &mut self.kv_cache.layers[li];
10639 for bi in 0..b {
10640 layer.append(
10641 &ok[bi * row..(bi + 1) * row],
10642 &ov[bi * row..(bi + 1) * row],
10643 &[],
10644 );
10645 }
10646 layer.accumulate_imp(oi);
10647 }
10648 last
10649 }
10650
10651 fn layer_is_local(&self, li: usize) -> bool {
10654 if let Some(layers) = &self.sliding_layers {
10655 return layers.get(li).copied().unwrap_or(false);
10656 }
10657 match self.swa {
10658 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10659 None => false,
10660 }
10661 }
10662
10663 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10666 if self.layer_is_local(li) {
10667 if let Some(f) = &self.inv_freq_local {
10668 return f.clone();
10669 }
10670 } else if let Some(f) = &self.inv_freq_global {
10671 return f.clone();
10672 }
10673 self.inv_freq.clone()
10674 }
10675
10676 fn layer_window(&self, li: usize) -> Option<usize> {
10678 self.swa
10679 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10680 }
10681
10682 fn swa_trim_tails(&mut self) {
10691 let Some((slack, align)) = self.swa_trim else {
10692 return;
10693 };
10694 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10695 return;
10696 }
10697 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10698 if let Some(w) = self.layer_window(li) {
10699 self.kv_cache.layers[li].trim_window(w, slack, align);
10700 }
10701 }
10702 }
10703
10704 fn layer_num_heads(&self, li: usize) -> usize {
10705 self.attention_heads_per_layer
10706 .as_ref()
10707 .and_then(|v| v.get(li).copied())
10708 .unwrap_or(self.num_heads)
10709 }
10710
10711 fn layer_rope_scale(&self, li: usize) -> f32 {
10712 if self.layer_is_local(li) {
10713 self.rope_scale_local
10714 } else {
10715 self.rope_scale
10716 }
10717 }
10718
10719 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10722 if !self.layer_is_local(li) {
10723 if let Some((ghd, gkv)) = self.global_attn {
10724 return (gkv, ghd, ghd);
10725 }
10726 }
10727 (
10728 self.layer_num_kv_heads(li),
10729 self.head_dim,
10730 if self.layer_is_local(li) {
10731 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10732 } else {
10733 self.rotary_dim
10734 },
10735 )
10736 }
10737
10738 fn layer_num_kv_heads(&self, li: usize) -> usize {
10741 self.kv_heads_per_layer
10742 .as_ref()
10743 .and_then(|v| v.get(self.phys_layer(li)).copied())
10744 .unwrap_or(self.num_kv_heads)
10745 }
10746
10747 fn layer_v_dim(&self, li: usize) -> usize {
10749 let (_, hd, _) = self.layer_geom(li);
10750 self.v_head_dim.unwrap_or(hd).min(hd)
10751 }
10752
10753 pub fn set_attn_geometry(
10762 &mut self,
10763 kv_heads_per_layer: Option<Vec<usize>>,
10764 v_head_dim: Option<usize>,
10765 ) -> Result<(), String> {
10766 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10767 if self.global_attn.is_some() {
10768 return Err(
10769 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10770 attention geometry"
10771 .into(),
10772 );
10773 }
10774 if self
10775 .weights
10776 .layers
10777 .iter()
10778 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10779 {
10780 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10781 }
10782 }
10783 if let Some(vd) = v_head_dim {
10784 if vd == 0 || vd > self.head_dim {
10785 return Err(format!(
10786 "v_head_dim {vd} must be in 1..={} (head_dim)",
10787 self.head_dim
10788 ));
10789 }
10790 }
10791 if let Some(v) = &kv_heads_per_layer {
10792 if v.len() != self.physical_layers {
10793 return Err(format!(
10794 "kv_heads_per_layer has {} entries, expected {} layers",
10795 v.len(),
10796 self.physical_layers
10797 ));
10798 }
10799 for (li, &nkv) in v.iter().enumerate() {
10800 let is_attn = matches!(
10801 self.weights.layers.get(li).map(|lw| &lw.attn),
10802 Some(AttnKind::Full { .. }) | None
10803 );
10804 if !is_attn {
10805 continue;
10806 }
10807 let nh = self
10808 .attention_heads_per_layer
10809 .as_ref()
10810 .and_then(|h| h.get(li).copied())
10811 .unwrap_or(self.num_heads);
10812 if nkv == 0 || nh % nkv != 0 {
10813 return Err(format!(
10814 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10815 ));
10816 }
10817 }
10818 }
10819 self.kv_heads_per_layer = kv_heads_per_layer;
10820 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10821 if self.kv_heads_per_layer.is_some() {
10822 for li in 0..self.kv_cache.layers.len() {
10823 let full = matches!(
10824 self.weights
10825 .layers
10826 .get(self.phys_layer(li))
10827 .map(|lw| &lw.attn),
10828 Some(AttnKind::Full { .. })
10829 );
10830 let nkv = self.layer_num_kv_heads(li);
10831 let cache = &self.kv_cache.layers[li];
10832 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
10833 let sinks = cache.sinks.clone();
10834 self.kv_cache.layers[li] =
10835 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
10836 self.kv_cache.layers[li].sinks = sinks;
10837 }
10838 }
10839 }
10840 Ok(())
10841 }
10842
10843 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
10847 let Some(lw) = self.weights.layers.get(phys) else {
10848 return Err(format!("sinks for layer {phys}: no such layer"));
10849 };
10850 if !matches!(lw.attn, AttnKind::Full { .. }) {
10851 return Err(format!(
10852 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
10853 ));
10854 }
10855 let nh = self
10856 .attention_heads_per_layer
10857 .as_ref()
10858 .and_then(|h| h.get(phys).copied())
10859 .unwrap_or(self.num_heads);
10860 if sinks.len() != nh {
10861 return Err(format!(
10862 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
10863 sinks.len()
10864 ));
10865 }
10866 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
10867 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
10868 }
10869 for li in 0..self.kv_cache.layers.len() {
10870 if self.phys_layer(li) == phys {
10871 self.kv_cache.layers[li].sinks = Some(sinks.clone());
10872 }
10873 }
10874 Ok(())
10875 }
10876
10877 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
10887 if self.kv_heads_per_layer.is_some() {
10888 return Some("per-layer KV head counts");
10889 }
10890 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
10891 return Some("V heads narrower than Q/K heads");
10892 }
10893 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
10894 return Some("learned attention sinks");
10895 }
10896 if self.swa.is_some() || self.sliding_layers.is_some() {
10897 return Some("sliding-window layers");
10898 }
10899 None
10900 }
10901
10902 #[cfg(target_os = "macos")]
10910 fn metal_rope_scale_ok(&self) -> bool {
10911 !(self.qk_norm_after_rope && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
10912 }
10913
10914 #[cfg(target_os = "macos")]
10928 fn metal_graph_swa(&self) -> bool {
10929 (self.swa.is_some() || self.sliding_layers.is_some())
10930 && self.attention_heads_per_layer.is_none()
10931 && !self.attn_v_norm
10932 && self.attn_softcap == 0.0
10933 && self.kv_heads_per_layer.is_none()
10934 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
10935 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10936 && self.global_attn.is_none()
10937 && self.inv_freq_global.is_none()
10938 && self.metal_rope_scale_ok()
10939 && !(0..self.num_layers).any(|li| {
10940 self.layer_is_local(li)
10941 && self.inv_freq_local.is_none()
10942 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10943 })
10944 }
10945
10946 #[cfg(target_os = "macos")]
10954 fn metal_gemma4(&self) -> bool {
10955 crate::gpu_metal::gemma4::enabled()
10956 && (self.global_attn.is_some() || self.attn_v_norm)
10957 && self.attention_heads_per_layer.is_none()
10958 && self.kv_heads_per_layer.is_none()
10959 && self.v_head_dim.is_none()
10960 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10961 && self.attn_softcap == 0.0
10962 && !self.qk_norm_after_rope
10963 && self.metal_rope_scale_ok()
10964 && self
10965 .weights
10966 .layers
10967 .iter()
10968 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
10969 && !(0..self.num_layers).any(|li| {
10970 self.layer_is_local(li)
10971 && self.inv_freq_local.is_none()
10972 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10973 })
10974 }
10975
10976 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
10983 let geometry_reason = self.graph_attn_decline_reason();
10987 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
10988 if has_scaled_rope && geometry_reason.is_none() {
10989 return Some("scaled RoPE without per-layer geometry");
10990 }
10991 if has_scaled_rope && self.qk_norm_after_rope {
10996 return Some("scaled RoPE with post-RoPE Q/K norm");
10997 }
10998 geometry_reason?;
10999 if self.attention_heads_per_layer.is_some() {
11005 return Some("per-layer Q head counts with per-layer geometry");
11006 }
11007 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11011 if !gemma4
11012 && self.weights.layers.iter().any(|lw| {
11013 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11014 })
11015 {
11016 return Some("sandwich norms / layer scale with per-layer geometry");
11017 }
11018 if self.weights.layers.iter().any(|lw| {
11022 matches!(
11023 &lw.attn,
11024 AttnKind::Full {
11025 output_gate: true,
11026 ..
11027 }
11028 )
11029 }) && self.v_head_dim.is_some()
11030 {
11031 return Some("gated attention with V narrower than K");
11032 }
11033 if (0..self.num_layers).any(|li| {
11034 self.layer_is_local(li)
11035 && self.inv_freq_local.is_none()
11036 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11037 }) {
11038 return Some("local rotary width without a local RoPE table");
11039 }
11040 None
11041 }
11042
11043 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11047 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11050 return None;
11051 }
11052 if self.global_attn.is_some() {
11053 return Some("per-layer head width (Gemma-4 global layers)");
11054 }
11055 if self.attn_v_norm {
11056 return Some("V norm");
11057 }
11058 if self.weights.layers.iter().any(|lw| {
11059 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11060 }) {
11061 return Some("sandwich norms / layer scale");
11062 }
11063 None
11064 }
11065
11066 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11071 self.graph_attn_decline_reason()?;
11072 let (nkv, hd, rd) = self.layer_geom(li);
11073 let invf: &[f32] = if self.layer_is_local(li) {
11074 match &self.inv_freq_local {
11075 Some(f) => f.as_slice(),
11076 None => self.inv_freq.as_slice(),
11077 }
11078 } else {
11079 match &self.inv_freq_global {
11080 Some(f) => f.as_slice(),
11081 None => self.inv_freq.as_slice(),
11082 }
11083 };
11084 Some(crate::gpu::GraphAttnGeom {
11085 nkv,
11086 hd,
11087 dv: self.layer_v_dim(li),
11088 rd,
11089 invf,
11090 rope_scale: self.layer_rope_scale(li),
11091 window: self.layer_window(li),
11092 sink: self.kv_cache.layers[li].sinks.as_deref(),
11093 v_norm: self.attn_v_norm,
11094 })
11095 }
11096
11097 #[cfg(feature = "gpu")]
11109 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11110 if !self.proj_gate_sigmoid
11111 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11112 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11113 || self.graph_refused()
11114 || self.o1_active()
11115 || self.attn_softcap > 0.0
11116 || self.wgpu_graph_attn_decline().is_some()
11117 {
11118 return None;
11119 }
11120 let g = self.graph_attn_geom(li)?;
11121 let (nkv, hd, _) = self.layer_geom(li);
11122 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11124 return None;
11125 }
11126 Some(crate::gpu::PrefillMirror {
11127 kv_id: self.graph_kv_id,
11128 layer: li,
11129 limit: self.kv_cache.max_seq_len,
11130 })
11131 }
11132
11133 #[cfg(feature = "gpu")]
11142 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11143 let kv_id = self.graph_kv_id;
11144 for li in from..upto.min(self.num_layers) {
11145 if !matches!(
11146 self.weights.layers[self.phys_layer(li)].attn,
11147 AttnKind::Full { .. }
11148 ) {
11149 continue;
11150 }
11151 let host = self.kv_cache.layers[li].pos_len();
11153 if host >= position {
11154 continue;
11155 }
11156 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11157 continue;
11158 };
11159 let to = dev.min(position);
11160 if to <= host {
11161 continue;
11162 }
11163 let (nkv, hd) = {
11164 let c = &self.kv_cache.layers[li];
11165 (c.num_kv_heads, c.head_dim)
11166 };
11167 let Some((k, v, first_valid)) =
11168 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11169 else {
11170 continue;
11171 };
11172 let need_from = match self.layer_window(li) {
11175 Some(w) => host.max((position + 1).saturating_sub(w)),
11176 None => host,
11177 };
11178 if first_valid > need_from {
11179 tracing::warn!(
11180 "layer {li}: device KV rows {host}..{to} no longer resident \
11181 (from {first_valid}); host attention will miss them"
11182 );
11183 }
11184 let row = nkv * hd;
11185 let cache = &mut self.kv_cache.layers[li];
11186 for p in 0..to - host {
11187 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11188 }
11189 }
11190 }
11191
11192 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11195 let mut seen = self.graph_declines.borrow_mut();
11196 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11197 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11198 seen.push((site, reason));
11199 }
11200 }
11201
11202 pub fn graph_declines(&self) -> Vec<String> {
11205 self.graph_declines
11206 .borrow()
11207 .iter()
11208 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11209 .collect()
11210 }
11211
11212 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11221 let Some(dir) = &self.layer_dump else {
11222 return;
11223 };
11224 let mut bytes = Vec::with_capacity(row.len() * 4);
11225 for v in row {
11226 bytes.extend_from_slice(&v.to_le_bytes());
11227 }
11228 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11229 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11230 use std::sync::atomic::{AtomicBool, Ordering};
11231 static SAID: AtomicBool = AtomicBool::new(false);
11232 if !SAID.swap(true, Ordering::Relaxed) {
11233 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11234 }
11235 }
11236 }
11237
11238 fn mimo_moe_prepare(&mut self) {
11241 if !self.mimo_moe.is_undecided() {
11242 return;
11243 }
11244 let slot = {
11245 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11246 .filter_map(
11247 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11248 FfnKind::Moe(m) => Some((li, m)),
11249 _ => None,
11250 },
11251 )
11252 .collect();
11253 if layers.is_empty()
11256 || self.physical_layers != self.num_layers
11257 || self.gpu_plan.is_some()
11258 {
11259 crate::mimo_moe::Slot::Off
11260 } else {
11261 let graph_prefix =
11265 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11266 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11267 }
11268 };
11269 self.mimo_moe = slot;
11270 }
11271
11272 #[cfg(test)]
11273 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11274 self.graph_kv_id
11275 }
11276
11277 pub(crate) fn mimo_graph_layer_rows(
11281 &mut self,
11282 li: usize,
11283 h: &mut [f32],
11284 positions: &[usize],
11285 ) -> crate::gpu::BatchGraphOutcome {
11286 use crate::gpu::BatchGraphOutcome as Out;
11287 let b = positions.len();
11288 if !(1..=4).contains(&b)
11289 || h.len() != b * self.hidden_size
11290 || !self.mimo_moe.is_dynamic(li, true)
11291 || !crate::gpu::enabled_here()
11292 || !crate::gpu::wgpu_active()
11293 || self.o1_active()
11294 || self.physical_layers != self.num_layers
11295 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11299 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11300 || self.wgpu_graph_attn_decline().is_some()
11301 {
11302 return Out::Declined;
11303 }
11304 let attn_started = std::time::Instant::now();
11305 let outcome = {
11306 let lw = &self.weights.layers[li];
11307 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11308 return Out::Declined;
11309 }
11310 let FfnKind::Moe(m) = &lw.ffn else {
11311 return Out::Declined;
11312 };
11313 let AttnKind::Full {
11314 wq,
11315 wk,
11316 wv,
11317 wo,
11318 q_norm,
11319 k_norm,
11320 output_gate,
11321 softplus_gate,
11322 bias,
11323 } = &lw.attn
11324 else {
11325 return Out::Declined;
11326 };
11327 if *output_gate || softplus_gate.is_some() {
11328 return Out::Declined;
11329 }
11330 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11331 m.experts
11332 .first()?
11333 .gate_proj
11334 .mapped_q4tp()
11335 .map(|(m, _)| m.clone())
11336 }) else {
11337 return Out::Declined;
11338 };
11339 fn gw<'a>(
11340 t: &'a QTensor,
11341 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11342 ) -> Option<crate::gpu::GraphW<'a>> {
11343 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11344 if m.uid() != owner.uid() || t.has_prism_contract() {
11345 return None;
11346 }
11347 return Some(crate::gpu::GraphW {
11348 idx,
11349 kind,
11350 row_scale: rs,
11351 data: &[],
11352 prism: crate::gpu::GraphPrismOp::None,
11353 affine: false,
11354 });
11355 }
11356 t.as_f32().map(|data| crate::gpu::GraphW {
11357 idx: 0,
11358 kind: 4,
11359 row_scale: &[],
11360 data,
11361 prism: crate::gpu::GraphPrismOp::None,
11362 affine: false,
11363 })
11364 }
11365 let (Some(q), Some(k), Some(v), Some(o)) = (
11366 gw(wq, &model),
11367 gw(wk, &model),
11368 gw(wv, &model),
11369 gw(wo, &model),
11370 ) else {
11371 return Out::Declined;
11372 };
11373 let layer = crate::gpu::GraphLayer {
11374 input_norm: &lw.input_norm,
11375 post_norm: &lw.post_norm,
11376 attn_out_norm: None,
11377 ffn_out_norm: None,
11378 layer_scale: None,
11379 ffn: crate::gpu::GraphFfn::AttentionOnly,
11380 attn: crate::gpu::GraphAttn::Full {
11381 wq: q,
11382 wk: k,
11383 wv: v,
11384 wo: o,
11385 q_norm: q_norm.as_deref(),
11386 k_norm: k_norm.as_deref(),
11387 late_qk_norm: self.qk_norm_after_rope,
11388 bias: bias
11389 .as_ref()
11390 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11391 output_gate: false,
11392 cpu_k: self.kv_cache.layers[li].k_heads(),
11393 cpu_v: self.kv_cache.layers[li].v_heads(),
11394 cpu_base: self.kv_cache.layers[li].base(),
11395 geom: self.graph_attn_geom(li),
11396 head_gate: None,
11397 },
11398 };
11399 let (nkv, hd, rd) = self.layer_geom(li);
11400 crate::gpu::forward_batch_graph_at(
11401 &model,
11402 self.graph_kv_id,
11403 li,
11404 &[layer],
11405 &self.inv_freq,
11406 h,
11407 self.layer_num_heads(li),
11408 nkv,
11409 hd,
11410 rd,
11411 self.hidden_size,
11412 1,
11413 positions,
11414 self.kv_cache.max_seq_len,
11415 self.norm_style == cortiq_core::NormStyle::Gemma,
11416 self.rms_eps as f32,
11417 self.attn_scale,
11418 b,
11419 &[],
11420 self.o1_epoch,
11421 None,
11422 None,
11423 )
11424 };
11425 match outcome {
11426 Out::Completed => {}
11427 Out::Declined => return Out::Declined,
11428 Out::Failed => {
11429 self.graph_failed
11430 .store(true, std::sync::atomic::Ordering::Relaxed);
11431 return Out::Failed;
11432 }
11433 }
11434 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11435 let hs = self.hidden_size;
11436 let lw = &self.weights.layers[li];
11437 let FfnKind::Moe(m) = &lw.ffn else {
11438 unreachable!()
11439 };
11440 let mut post = vec![0.0; h.len()];
11441 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11442 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11443 }
11444 let mut ffn = if b == 1 {
11445 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11446 } else {
11447 moe_ffn_banked_rows(
11448 &mut self.mimo_moe,
11449 li,
11450 m,
11451 &post,
11452 b,
11453 hs,
11454 self.pool.as_deref(),
11455 )
11456 };
11457 for (x, &f) in h.iter_mut().zip(&ffn) {
11458 *x += f;
11459 }
11460 attention::recycle_buf(&mut ffn);
11461 if self.layer_dump.is_some() {
11462 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11463 self.dump_layer_row(pos, li, row);
11464 }
11465 }
11466 crate::mimo_moe::note_attention_graph(b, attn_ns);
11467 Out::Completed
11468 }
11469
11470 fn layer_attn_plain(&self, li: usize) -> bool {
11471 self.kv_heads_per_layer.is_none()
11472 && self.v_head_dim.is_none()
11473 && self.global_attn.is_none()
11474 && self.layer_window(li).is_none()
11475 && self.kv_cache.layers[li].sinks.is_none()
11476 }
11477
11478 fn forward_layers(
11480 &mut self,
11481 hidden: &[f32],
11482 position: usize,
11483 task_mask: Option<&TaskMask>,
11484 ) -> Vec<f32> {
11485 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11486 self.o1_progress();
11487 self.swa_trim_tails();
11488 out
11489 }
11490
11491 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11499 self.embed_single(id)
11500 }
11501
11502 pub fn split_supported(&self) -> Result<(), String> {
11506 if self.dsv4.is_some() {
11507 return Err(
11508 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11509 );
11510 }
11511 if self.dsv41.is_some() {
11512 return Err(
11513 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11514 .into(),
11515 );
11516 }
11517 if self.qwen4_exp.is_some() {
11518 return Err(
11519 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11520 );
11521 }
11522 if self.g3n.is_some() {
11523 return Err(
11524 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11525 );
11526 }
11527 Ok(())
11528 }
11529
11530 pub fn forward_span(
11535 &mut self,
11536 hidden: &[f32],
11537 position: usize,
11538 from: usize,
11539 upto: usize,
11540 task_mask: Option<&TaskMask>,
11541 ) -> Result<Vec<f32>, String> {
11542 self.split_supported()?;
11543 if from > upto || upto >= self.num_layers {
11544 return Err(format!(
11545 "forward_span: layer range {from}..={upto} outside 0..{}",
11546 self.num_layers
11547 ));
11548 }
11549 if hidden.len() != self.hidden_size {
11550 return Err(format!(
11551 "forward_span: hidden len {} ≠ hidden_size {}",
11552 hidden.len(),
11553 self.hidden_size
11554 ));
11555 }
11556 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11557 self.o1_progress();
11558 self.swa_trim_tails();
11559 if self
11560 .graph_failed
11561 .swap(false, std::sync::atomic::Ordering::Relaxed)
11562 {
11563 self.cancel
11564 .store(false, std::sync::atomic::Ordering::Relaxed);
11565 self.clear_sequence_state();
11566 return Err("forward_span: deferred O(1) transition failed".into());
11567 }
11568 Ok(out)
11569 }
11570
11571 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11574 let normed = inference::rms_norm(
11575 hidden,
11576 &self.weights.final_norm,
11577 self.rms_eps,
11578 self.norm_style,
11579 );
11580 self.lm_head_forward(&normed)
11581 }
11582
11583 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11585 sampler::sample_with_scratch(
11586 logits,
11587 &self.sampler_config,
11588 past_tokens,
11589 &mut self.rng,
11590 &mut self.sampler_scratch,
11591 )
11592 }
11593
11594 pub fn reset_session(&mut self) {
11596 self.clear_sequence_state();
11597 }
11598
11599 pub fn prefill_span_ids(
11605 &mut self,
11606 ids: &[u32],
11607 start_pos: usize,
11608 upto: usize,
11609 task_mask: Option<&TaskMask>,
11610 ) -> Result<Vec<f32>, String> {
11611 self.split_supported()?;
11612 if upto >= self.num_layers {
11613 return Err(format!(
11614 "prefill_span_ids: upto {upto} outside 0..{}",
11615 self.num_layers
11616 ));
11617 }
11618 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11622 let out =
11623 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11624 self.check_o1_progress_failure("prefill_span_ids")?;
11625 Ok(out)
11626 } else {
11627 let hs = self.hidden_size;
11628 let mut out = Vec::with_capacity(ids.len() * hs);
11629 for (i, &id) in ids.iter().enumerate() {
11630 let emb = self.embed_id(id);
11631 out.extend_from_slice(&self.forward_span(
11632 &emb,
11633 start_pos + i,
11634 0,
11635 upto,
11636 task_mask,
11637 )?);
11638 }
11639 Ok(out)
11640 }
11641 }
11642
11643 pub fn prefill_span_hidden(
11646 &mut self,
11647 hidden: &[f32],
11648 start_pos: usize,
11649 from: usize,
11650 upto: usize,
11651 task_mask: Option<&TaskMask>,
11652 ) -> Result<Vec<f32>, String> {
11653 self.split_supported()?;
11654 let hs = self.hidden_size;
11655 if hidden.is_empty() || hidden.len() % hs != 0 {
11656 return Err(format!(
11657 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11658 hidden.len()
11659 ));
11660 }
11661 if from > upto || upto >= self.num_layers {
11662 return Err(format!(
11663 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11664 self.num_layers
11665 ));
11666 }
11667 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11668 let out = self.prefill_batch_span(
11669 PrefillIn::Hidden(hidden),
11670 start_pos,
11671 task_mask,
11672 from,
11673 upto + 1,
11674 );
11675 self.check_o1_progress_failure("prefill_span_hidden")?;
11676 Ok(out)
11677 } else {
11678 let b = hidden.len() / hs;
11679 let mut out = Vec::with_capacity(hidden.len());
11680 for i in 0..b {
11681 let h = self.forward_span(
11682 &hidden[i * hs..(i + 1) * hs],
11683 start_pos + i,
11684 from,
11685 upto,
11686 task_mask,
11687 )?;
11688 out.extend_from_slice(&h);
11689 }
11690 Ok(out)
11691 }
11692 }
11693
11694 fn try_token_graph_wgpu(
11698 &self,
11699 hidden: &[f32],
11700 position: usize,
11701 logits_out: &mut Vec<f32>,
11702 layers_run: &mut usize,
11703 ) -> Option<Result<Vec<f32>, ()>> {
11704 self.try_token_graph_wgpu_steps(
11705 hidden,
11706 position,
11707 logits_out,
11708 1,
11709 None,
11710 Some(layers_run),
11711 0,
11712 self.num_layers,
11713 )
11714 }
11715
11716 fn try_token_graph_wgpu_span(
11720 &self,
11721 hidden: &[f32],
11722 position: usize,
11723 logits_out: &mut Vec<f32>,
11724 from: usize,
11725 upto_excl: usize,
11726 layers_run: &mut usize,
11727 ) -> Option<Result<Vec<f32>, ()>> {
11728 self.try_token_graph_wgpu_steps(
11729 hidden,
11730 position,
11731 logits_out,
11732 1,
11733 None,
11734 Some(layers_run),
11735 from,
11736 upto_excl,
11737 )
11738 }
11739
11740 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11744 if self.o1_active() || self.attn_softcap > 0.0 {
11745 return None;
11746 }
11747 if let Some(reason) = self.wgpu_graph_attn_decline() {
11750 self.note_graph_decline("wgpu multi-burst", reason);
11751 return None;
11752 }
11753 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11754 if !graph_on || self.graph_refused() {
11755 return None;
11762 }
11763 let emb = self.embed_single(t_next);
11764 let mut lg = Vec::new();
11765 let mut ids = Vec::new();
11766 match self.try_token_graph_wgpu_steps(
11767 &emb,
11768 position,
11769 &mut lg,
11770 k,
11771 Some(&mut ids),
11772 None,
11773 0,
11774 self.num_layers,
11775 ) {
11776 Some(Ok(_)) => {}
11777 Some(Err(())) => {
11778 self.graph_failed
11783 .store(true, std::sync::atomic::Ordering::Relaxed);
11784 return None;
11785 }
11786 None => return None,
11787 }
11788 (ids.len() == k).then_some(ids)
11789 }
11790
11791 fn try_token_graph_wgpu_steps(
11795 &self,
11796 hidden: &[f32],
11797 position: usize,
11798 logits_out: &mut Vec<f32>,
11799 steps: usize,
11800 ids_out: Option<&mut Vec<u32>>,
11801 layers_run: Option<&mut usize>,
11802 from: usize,
11803 upto_excl: usize,
11804 ) -> Option<Result<Vec<f32>, ()>> {
11805 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11808 Some(end) if end < upto_excl => {
11809 if steps != 1 || layers_run.is_none() || from >= end {
11810 return None;
11811 }
11812 end
11813 }
11814 _ => upto_excl,
11815 };
11816 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11819 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11820 return None;
11824 }
11825 if let Some(reason) = self.wgpu_graph_attn_decline() {
11832 self.note_graph_decline("wgpu token graph", reason);
11833 return None;
11834 }
11835 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
11840 .map(|li| {
11841 if !o1_gpu {
11842 return None;
11843 }
11844 self.kv_cache.layers[self.phys_layer(li)].o1_views()
11845 })
11846 .collect();
11847 if self.o1_active() && o1_gpu {
11848 let want: usize = (from..upto_excl)
11851 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
11852 .count();
11853 let have = o1_views.iter().filter(|v| v.is_some()).count();
11854 if want == 0 || have != want {
11855 use std::sync::atomic::{AtomicUsize, Ordering};
11865 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
11866 let code = have * 1000 + want;
11867 if LAST.swap(code, Ordering::Relaxed) != code {
11868 tracing::warn!(
11869 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
11870 );
11871 }
11872 return None;
11873 }
11874 }
11875 let nh = self.num_heads;
11876 let (nkv, hd, rd) = self.layer_geom(0);
11877 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11878 let mut layers = Vec::with_capacity(upto_excl - from);
11879 let mut model = None;
11880 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
11881 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11882 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
11884 let name = &m.tensors[i].name;
11885 let prism = if crate::prism::is_inverse_embedding(m, name) {
11886 crate::gpu::GraphPrismOp::InverseEmbedding
11887 } else if crate::prism::is_forward_weight(m, name) {
11888 crate::gpu::GraphPrismOp::Forward
11889 } else {
11890 crate::gpu::GraphPrismOp::None
11891 };
11892 return Some(crate::gpu::GraphW {
11893 idx: i,
11894 kind,
11895 row_scale: rs,
11896 data: &[],
11897 prism,
11898 affine: crate::prism::is_affine_target(m, name),
11899 });
11900 }
11901 match t.as_f32() {
11903 Some(d) => Some(crate::gpu::GraphW {
11904 idx: 0,
11905 kind: 4,
11906 row_scale: &[],
11907 data: d,
11908 prism: crate::gpu::GraphPrismOp::None,
11909 affine: false,
11910 }),
11911 None => {
11912 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11913 eprintln!("batch graph: weight has no graph/f32 representation");
11914 }
11915 None
11916 }
11917 }
11918 }
11919 for li in from..upto_excl {
11920 let lw = &self.weights.layers[self.phys_layer(li)];
11921 if dbg {
11922 let ak = match &lw.attn {
11923 AttnKind::Mla(_) => "Mla".into(),
11924 AttnKind::Full {
11925 output_gate, bias, ..
11926 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11927 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11928 AttnKind::Kda(_) => "Kda".into(),
11929 AttnKind::Linear(_) => "Linear".into(),
11930 AttnKind::ShortConv(_) => "ShortConv".into(),
11931 AttnKind::Bounded(_) => "Bounded".into(),
11932 };
11933 let fk = match &lw.ffn {
11934 FfnKind::Dense(_) => "Dense",
11935 FfnKind::Moe(_) => "Moe",
11936 FfnKind::DenseMoe(_) => "DenseMoe",
11937 };
11938 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11939 }
11940 let gffn = match &lw.ffn {
11941 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11945 FfnKind::Dense(d) => {
11946 let Some(act) = d.act.graph_act() else {
11950 self.note_graph_decline(
11951 "wgpu token graph",
11952 "dense FFN activation without a graph kernel",
11953 );
11954 return None;
11955 };
11956 crate::gpu::GraphFfn::Dense {
11957 gate: gw(&d.gate_proj)?,
11958 up: gw(&d.up_proj)?,
11959 down: gw(&d.down_proj)?,
11960 act,
11961 }
11962 }
11963 FfnKind::Moe(m) => {
11964 if m.route_tau.is_some() || m.mask.is_some() {
11972 return None;
11973 }
11974 let shared = m.shared.as_ref();
11975 let has_shared = shared.is_some();
11976 let shared_gated = matches!(shared, Some((_, Some(_))));
11977 let sgate = match shared {
11978 Some((_, Some(sg))) => gw(sg)?,
11979 _ => gw(&m.router)?,
11983 };
11984 let router = gw(&m.router)?;
11985 if router.prism != crate::gpu::GraphPrismOp::None
11991 || sgate.prism != crate::gpu::GraphPrismOp::None
11992 || router.affine
11993 || sgate.affine
11994 {
11995 tracing::warn!(
11996 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
11997 );
11998 return None;
11999 }
12000 let inter = m.experts.first()?.gate_proj.rows();
12001 let mut experts = Vec::with_capacity(m.experts.len() + 1);
12002 let mut q4tp: Option<bool> = None;
12005 let mut gu_q2: Option<bool> = None;
12008 let mut q82: Option<bool> = None;
12010 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
12011 if !matches!(e.act, Act::Silu)
12012 || e.gate_proj.rows() != inter
12013 || e.up_proj.rows() != inter
12014 {
12015 return None;
12016 }
12017 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12022 let Some((em, ei, _, _)) = expert_weight
12023 .graph_weight()
12024 .or_else(|| expert_weight.graph_weight_descriptor())
12025 else {
12026 return None;
12027 };
12028 let name = &em.tensors[ei].name;
12029 if crate::prism::is_forward_weight(em, name)
12030 || crate::prism::is_inverse_embedding(em, name)
12031 || crate::prism::is_affine_target(em, name)
12032 {
12033 tracing::warn!(
12034 "resident MoE declined: expert Prism/affine transform is not implemented"
12035 );
12036 return None;
12037 }
12038 }
12039 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12040 Some((mm, gi)) => (
12041 mm,
12042 gi,
12043 e.up_proj.mapped_q4t()?.1,
12044 e.down_proj.mapped_q4t()?.1,
12045 false,
12046 false,
12047 false,
12048 ),
12049 None => match e.gate_proj.mapped_q2tp() {
12050 Some((mm, gi)) => (
12051 mm,
12052 gi,
12053 e.up_proj.mapped_q2tp()?.1,
12054 e.down_proj.mapped_q4tp()?.1,
12055 true,
12056 true,
12057 false,
12058 ),
12059 None => match e.gate_proj.mapped_q4tp() {
12060 Some((mm, gi)) => (
12061 mm,
12062 gi,
12063 e.up_proj.mapped_q4tp()?.1,
12064 e.down_proj.mapped_q4tp()?.1,
12065 true,
12066 false,
12067 false,
12068 ),
12069 None => {
12072 if !crate::gpu::moe_q82_graph_on() {
12073 return None;
12074 }
12075 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12076 (
12077 mm,
12078 gi,
12079 e.up_proj.mapped_q8_2f()?.1,
12080 e.down_proj.mapped_q8_2f()?.1,
12081 false,
12082 false,
12083 true,
12084 )
12085 }
12086 },
12087 },
12088 };
12089 if *q4tp.get_or_insert(is_p) != is_p
12090 || *gu_q2.get_or_insert(is_q2) != is_q2
12091 || *q82.get_or_insert(is_8) != is_8
12092 {
12093 tracing::warn!(
12099 "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."
12100 );
12101 return None;
12102 }
12103 model.get_or_insert_with(|| mm.clone());
12104 experts.push((gi, ui, di));
12105 }
12106 crate::gpu::GraphFfn::Moe {
12107 router,
12108 shared_gate: sgate,
12109 experts,
12110 n_exp: m.experts.len(),
12111 top_k: std::env::var("CMF_TOPK_PROBE")
12117 .ok()
12118 .and_then(|v| v.parse::<usize>().ok())
12119 .filter(|k| *k > 0 && *k <= m.top_k)
12120 .unwrap_or(m.top_k),
12121 inter,
12122 norm_topk: m.norm_topk_prob,
12123 q4tp: q4tp?,
12124 gu_q2: gu_q2.unwrap_or(false),
12125 q82: q82.unwrap_or(false),
12126 sigmoid: m.router_sigmoid,
12127 bias: m.expert_bias.as_deref(),
12128 has_shared,
12129 shared_gated,
12130 route_scale: m.routed_scaling,
12131 }
12132 }
12133 };
12134 let attn = match &lw.attn {
12135 AttnKind::Full {
12136 wq,
12137 wk,
12138 wv,
12139 wo,
12140 q_norm,
12141 k_norm,
12142 output_gate,
12143 softplus_gate,
12144 bias,
12145 } => {
12146 if self.attention_heads_per_layer.is_some() {
12147 return None;
12148 }
12149 let head_gate = match softplus_gate {
12153 None => None,
12154 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
12155 Some(gw(g)?)
12156 }
12157 Some(_) => {
12158 self.note_graph_decline(
12159 "wgpu token graph",
12160 "projected softplus / per-element output gate",
12161 );
12162 return None;
12163 }
12164 };
12165 let (m, _, _, _) = wq.graph_weight_or_blr()?;
12166 model = Some(m.clone());
12167 crate::gpu::GraphAttn::Full {
12168 wq: gw(wq)?,
12169 wk: gw(wk)?,
12170 wv: gw(wv)?,
12171 wo: gw(wo)?,
12172 q_norm: q_norm.as_deref(),
12173 k_norm: k_norm.as_deref(),
12174 late_qk_norm: self.qk_norm_after_rope,
12175 bias: bias
12176 .as_ref()
12177 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12178 output_gate: *output_gate,
12179 cpu_k: self.kv_cache.layers[li].k_heads(),
12180 cpu_v: self.kv_cache.layers[li].v_heads(),
12181 cpu_base: self.kv_cache.layers[li].base(),
12182 geom: self.graph_attn_geom(li),
12183 head_gate,
12184 }
12185 }
12186 AttnKind::LinearGdn(w) => {
12187 let cfg = self.gdn_cfg?;
12188 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
12189 model = Some(m.clone());
12190 crate::gpu::GraphAttn::Gdn {
12191 qkv: gw(&w.in_proj_qkv)?,
12192 z: gw(&w.in_proj_z)?,
12193 a: gw(&w.in_proj_a)?,
12194 b: gw(&w.in_proj_b)?,
12195 out: gw(&w.out_proj)?,
12196 conv1d: &w.conv1d,
12197 a_log: &w.a_log,
12198 dt_bias: &w.dt_bias,
12199 norm: &w.norm,
12200 nv: cfg.num_v_heads,
12201 nk: cfg.num_k_heads,
12202 dk: cfg.key_head_dim,
12203 dv: cfg.value_head_dim,
12204 kk: cfg.conv_kernel,
12205 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12206 }
12207 }
12208 AttnKind::ShortConv(w) => {
12209 let cfg = self.short_conv_cfg?;
12210 let (m, _, _, _) = w
12211 .in_proj
12212 .graph_weight()
12213 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12214 model = Some(m.clone());
12215 crate::gpu::GraphAttn::ShortConv {
12216 inp: gw(&w.in_proj)?,
12217 out: gw(&w.out_proj)?,
12218 taps: &w.conv,
12219 kernel: cfg.kernel,
12220 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12221 }
12222 }
12223 _ => return None,
12224 };
12225 layers.push(crate::gpu::GraphLayer {
12226 input_norm: &lw.input_norm,
12227 attn,
12228 post_norm: &lw.post_norm,
12229 attn_out_norm: lw.attn_out_norm.as_deref(),
12230 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12231 layer_scale: lw.layer_scale,
12232 ffn: gffn,
12233 });
12234 }
12235 let model = model?;
12236 let lm_gw = if upto_excl == self.num_layers
12242 && self.graph_want_logits
12243 && std::env::var("CMF_GPU_LMHEAD")
12244 .map(|v| v != "0")
12245 .unwrap_or(true)
12246 {
12247 self.weights
12248 .lm_head
12249 .graph_weight()
12250 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12251 .map(|(m, i, kind, rs)| {
12252 let name = &m.tensors[i].name;
12253 let prism = if crate::prism::is_inverse_embedding(m, name) {
12254 crate::gpu::GraphPrismOp::InverseEmbedding
12255 } else if crate::prism::is_forward_weight(m, name) {
12256 crate::gpu::GraphPrismOp::Forward
12257 } else {
12258 crate::gpu::GraphPrismOp::None
12259 };
12260 (
12261 crate::gpu::GraphW {
12262 idx: i,
12263 kind,
12264 row_scale: rs,
12265 data: &[],
12266 prism,
12267 affine: crate::prism::is_affine_target(m, name),
12268 },
12269 self.weights.lm_head.rows(),
12270 )
12271 })
12272 } else {
12273 None
12274 };
12275 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12276 let emb_gw = if steps > 1 {
12278 self.weights
12279 .embed_tokens
12280 .graph_weight()
12281 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12282 .map(|(m, i, kind, rs)| {
12283 let name = &m.tensors[i].name;
12284 let prism = if crate::prism::is_inverse_embedding(m, name) {
12285 crate::gpu::GraphPrismOp::InverseEmbedding
12286 } else if crate::prism::is_forward_weight(m, name) {
12287 crate::gpu::GraphPrismOp::Forward
12288 } else {
12289 crate::gpu::GraphPrismOp::None
12290 };
12291 (
12292 crate::gpu::GraphW {
12293 idx: i,
12294 kind,
12295 row_scale: rs,
12296 data: &[],
12297 prism,
12298 affine: crate::prism::is_affine_target(m, name),
12299 },
12300 self.weights.embed_tokens.rows(),
12301 self.embed_multiplier,
12302 )
12303 })
12304 } else {
12305 None
12306 };
12307
12308 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12314 (from..upto_excl.min(self.num_layers - 1))
12315 .filter(|&li| (li + 1) % self.physical_layers == 0)
12316 .map(|li| li - from)
12317 .collect()
12318 } else {
12319 Vec::new()
12320 };
12321 let mut h = hidden.to_vec();
12322 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12328 let outcome = crate::gpu::forward_token_graph(
12329 &model,
12330 self.graph_kv_id,
12331 &layers,
12332 &o1_views,
12333 self.o1_epoch,
12334 &self.inv_freq,
12335 &mut h,
12336 nh,
12337 nkv,
12338 hd,
12339 self.attn_scale,
12340 rd,
12341 self.hidden_size,
12342 self.intermediate_size,
12343 position,
12344 self.kv_cache.max_seq_len,
12345 gemma,
12346 self.rms_eps as f32,
12347 lm,
12348 &self.weights.final_norm,
12349 logits_out,
12350 &loop_norm_at,
12351 steps,
12352 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12353 ids_out,
12354 layers_run,
12355 from,
12356 dump_hidden,
12357 );
12358 match outcome {
12359 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12360 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12361 crate::gpu::TokenGraphOutcome::Declined => None,
12362 }
12363 }
12364
12365 #[cfg(target_os = "macos")]
12374 #[allow(clippy::type_complexity)]
12375 fn metal_rows_plan(
12376 &self,
12377 ) -> Option<(
12378 Vec<MetalRowsItem<'_>>,
12379 std::sync::Arc<cortiq_core::CmfModel>,
12380 Option<crate::gpu_metal::GdnGpuCfg>,
12381 )> {
12382 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12383 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12384 if !graph_force
12385 || !crate::gpu::enabled_here()
12386 || std::env::var("CMF_GPU_BLOCK")
12387 .map(|v| v == "0")
12388 .unwrap_or(false)
12389 || self.attn_softcap > 0.0
12390 || self.o1_active()
12391 || self.swa.is_some()
12392 || self.global_attn.is_some()
12393 || self.attention_heads_per_layer.is_some()
12394 || self.graph_attn_decline_reason().is_some()
12396 || self.attn_v_norm
12397 || self.loop_final_norm
12398 || !self.metal_rope_scale_ok()
12399 {
12400 return None;
12401 }
12402 let attend_contract = self.head_dim % 4 == 0
12403 && self.head_dim <= 256
12404 && self.rotary_dim >= 2
12405 && self.rotary_dim <= self.head_dim
12406 && (self.rotary_dim / 2) % 32 == 0
12407 && self.num_kv_heads > 0
12408 && self.num_heads % self.num_kv_heads == 0;
12409 if !attend_contract {
12410 return None;
12411 }
12412 let mut plan: Vec<MetalRowsItem> = Vec::new();
12413 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12414 for li in 0..self.num_layers {
12415 let lw = &self.weights.layers[self.phys_layer(li)];
12416 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12417 return None;
12418 }
12419 let ffn = match &lw.ffn {
12420 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12421 let (Some(g), Some(u), Some(dn)) = (
12422 d.gate_proj.metal_graph_parts(),
12423 d.up_proj.metal_graph_parts(),
12424 d.down_proj.metal_graph_parts(),
12425 ) else {
12426 return None;
12427 };
12428 MetalFfn::Dense {
12429 gate: g,
12430 up: u,
12431 down: dn,
12432 act: 0, }
12434 }
12435 _ => return None,
12436 };
12437 match &lw.attn {
12438 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12439 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12440 w.in_proj_qkv.metal_graph_parts(),
12441 w.in_proj_z.metal_graph_parts(),
12442 w.in_proj_a.f32_parts(),
12443 w.in_proj_b.f32_parts(),
12444 w.out_proj.metal_graph_parts(),
12445 ) else {
12446 return None;
12447 };
12448 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12449 model_ref.get_or_insert_with(|| model.clone());
12450 }
12451 let gl = GdnGpuLayer {
12452 attn_norm: &lw.input_norm,
12453 post_norm: &lw.post_norm,
12454 qkv,
12455 z,
12456 a,
12457 b: bb,
12458 out,
12459 ffn,
12460 conv1d: &w.conv1d,
12461 a_log: &w.a_log,
12462 dt_bias: &w.dt_bias,
12463 gnorm: &w.norm,
12464 };
12465 match plan.last_mut() {
12466 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12467 _ => plan.push(MetalRowsItem::Gdn {
12468 run: vec![gl],
12469 first: li,
12470 }),
12471 }
12472 }
12473 AttnKind::Full {
12474 wq,
12475 wk,
12476 wv,
12477 wo,
12478 q_norm,
12479 k_norm,
12480 output_gate,
12481 softplus_gate: None,
12482 bias: None,
12483 } => {
12484 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12485 (
12486 wq.metal_graph_parts(),
12487 wk.metal_graph_parts(),
12488 wv.metal_graph_parts(),
12489 wo.metal_graph_parts(),
12490 )
12491 else {
12492 return None;
12493 };
12494 if let QTensor::Mapped { model, .. } = wq {
12495 model_ref.get_or_insert_with(|| model.clone());
12496 }
12497 let cache = &self.kv_cache.layers[li];
12498 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12499 return None;
12500 }
12501 plan.push(MetalRowsItem::Attn {
12502 l: AttnGpuLayer {
12503 attn_norm: &lw.input_norm,
12504 post_norm: &lw.post_norm,
12505 wq: pq,
12506 wk: pk,
12507 wv: pv,
12508 wo: po,
12509 ffn,
12510 attn_out_norm: None,
12511 ffn_out_norm: None,
12512 layer_scale: None,
12513 },
12514 li,
12515 q_norm: q_norm.as_deref(),
12516 k_norm: k_norm.as_deref(),
12517 output_gate: *output_gate,
12518 });
12519 }
12520 _ => return None,
12521 }
12522 }
12523 let model = model_ref?;
12524 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12525 nv: cfg.num_v_heads,
12526 nk: cfg.num_k_heads,
12527 dk: cfg.key_head_dim,
12528 dv: cfg.value_head_dim,
12529 kk: cfg.conv_kernel,
12530 hidden: self.hidden_size,
12531 inter: self.intermediate_size,
12532 c_dim: cfg.conv_dim(),
12533 eps: cfg.rms_eps as f32,
12534 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12535 });
12536 Some((plan, model, gcfg))
12537 }
12538
12539 #[cfg(target_os = "macos")]
12541 #[allow(clippy::too_many_arguments)]
12542 fn metal_attn_params<'a>(
12543 li: usize,
12544 cache: &'a crate::kv_cache::LayerKvCache,
12545 q_norm: Option<&'a [f32]>,
12546 k_norm: Option<&'a [f32]>,
12547 output_gate: bool,
12548 inv_freq: &'a [f32],
12549 rope_scale: f32,
12550 geom: (usize, usize, usize, usize),
12551 pos0: usize,
12552 kv_id: u64,
12553 scale: f32,
12554 eps: f32,
12555 gemma: bool,
12556 late_qk_norm: bool,
12557 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12558 let (nh, nkv, hd, rd) = geom;
12559 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12560 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12561 let cpu_stored = cpu_k[0].len() / hd;
12562 (
12563 crate::gpu_metal::AttnDeviceParams {
12564 kv_id,
12565 layer: li,
12566 nh,
12567 nkv,
12568 hd,
12569 rd,
12570 position: pos0,
12571 scale,
12572 eps,
12573 gemma,
12574 late_qk_norm,
12575 output_gate,
12576 q_norm,
12577 k_norm,
12578 inv_freq,
12579 rope_scale,
12580 cpu_k,
12581 cpu_v,
12582 cpu_stored,
12583 cpu_gen: cache.generation(),
12584 o1: None,
12585 window: None,
12586 head_gate: None,
12587 v_norm: false,
12588 },
12589 cpu_stored,
12590 )
12591 }
12592
12593 #[cfg(target_os = "macos")]
12598 #[allow(clippy::type_complexity)]
12599 fn metal_rows_run(
12600 &mut self,
12601 hiddens: &mut [f32],
12602 pos0: usize,
12603 b: usize,
12604 prefill: bool,
12605 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12606 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12610 ) -> MetalRowsRun {
12611 use crate::gpu_metal::{GraphDims, VerifyGraph};
12612 if !crate::gpu_metal::wait_replay() {
12618 tracing::error!("Metal rows graph: the pending async replay failed");
12619 return MetalRowsRun::Failed;
12620 }
12621 spec_stamp("v.wait");
12622 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12628 if want > 0 {
12629 let phys = self.physical_layers.max(1);
12630 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12631 let is_gdn = self
12632 .weights
12633 .layers
12634 .get(li % phys)
12635 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12636 if is_gdn && l.linear_state.len() != want {
12637 l.linear_state = vec![0f32; want];
12638 }
12639 }
12640 }
12641 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12642 return MetalRowsRun::Declined;
12643 };
12644 spec_stamp("v.plan");
12645 let dims = GraphDims {
12646 hidden: self.hidden_size,
12647 eps: self.rms_eps as f32,
12648 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12649 };
12650 let Some(mut graph) = (if prefill {
12651 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12652 } else {
12653 VerifyGraph::new(&model, dims, hiddens, b)
12654 }) else {
12655 return MetalRowsRun::Declined;
12656 };
12657 let geom = (
12658 self.num_heads,
12659 self.num_kv_heads,
12660 self.head_dim,
12661 self.rotary_dim,
12662 );
12663 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12664 let eps = self.rms_eps as f32;
12665 let kv_id = self.graph_kv_id;
12666 let inv_freq = self.inv_freq.clone();
12667 for item in &plan {
12668 let ok = match item {
12669 MetalRowsItem::Gdn { run, .. } => gcfg
12670 .as_ref()
12671 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12672 .unwrap_or(false),
12673 MetalRowsItem::Attn {
12674 l,
12675 li,
12676 q_norm,
12677 k_norm,
12678 output_gate,
12679 } => {
12680 let (p, _) = Self::metal_attn_params(
12681 *li,
12682 &self.kv_cache.layers[*li],
12683 *q_norm,
12684 *k_norm,
12685 *output_gate,
12686 &inv_freq,
12687 self.layer_rope_scale(*li),
12688 geom,
12689 pos0,
12690 kv_id,
12691 self.attn_scale,
12692 eps,
12693 gemma,
12694 self.qk_norm_after_rope,
12695 );
12696 graph.attn_ok(l, &p)
12697 }
12698 };
12699 if !ok {
12700 use std::sync::atomic::{AtomicBool, Ordering};
12701 static SAID: AtomicBool = AtomicBool::new(false);
12702 if !SAID.swap(true, Ordering::Relaxed) {
12703 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12704 }
12705 return MetalRowsRun::Declined;
12706 }
12707 }
12708 let lm = match &spec {
12709 Some((lm, _, _)) => {
12710 if !graph.lm_head_ok(*lm) {
12711 return MetalRowsRun::Declined;
12712 }
12713 Some(*lm)
12714 }
12715 None => None,
12716 };
12717 let mut gdn_layers = Vec::new();
12718 let mut attn_layers = Vec::new();
12719 for item in &plan {
12720 match item {
12721 MetalRowsItem::Gdn { run, first } => {
12722 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12723 .iter()
12724 .map(|l| l.linear_state.as_slice())
12725 .collect();
12726 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12727 return MetalRowsRun::Declined;
12728 }
12729 gdn_layers.extend(*first..*first + run.len());
12730 }
12731 MetalRowsItem::Attn {
12732 l,
12733 li,
12734 q_norm,
12735 k_norm,
12736 output_gate,
12737 } => {
12738 let (p, cpu_stored) = Self::metal_attn_params(
12739 *li,
12740 &self.kv_cache.layers[*li],
12741 *q_norm,
12742 *k_norm,
12743 *output_gate,
12744 &inv_freq,
12745 self.layer_rope_scale(*li),
12746 geom,
12747 pos0,
12748 kv_id,
12749 self.attn_scale,
12750 eps,
12751 gemma,
12752 self.qk_norm_after_rope,
12753 );
12754 if !graph.encode_attn_b(l, &p) {
12755 return MetalRowsRun::Declined;
12756 }
12757 attn_layers.push((*li, cpu_stored));
12758 }
12759 }
12760 }
12761 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12762 if !graph.encode_lm_head_b(final_norm, lm) {
12763 return MetalRowsRun::Declined;
12764 }
12765 if let Some((n, _)) = argmax_out.as_ref() {
12770 if !graph.encode_argmax_b(*n) {
12771 argmax_out = None;
12772 }
12773 }
12774 }
12775 spec_stamp("v.enc");
12776 if !graph.sync() {
12777 return MetalRowsRun::Failed;
12778 }
12779 spec_stamp("v.gpu");
12780 match (spec, argmax_out) {
12781 (Some(_), Some((_, ids))) => {
12782 ids.resize(b, 0);
12783 if !graph.read_argmax(ids) {
12784 return MetalRowsRun::Failed;
12785 }
12786 spec_stamp("v.am");
12787 }
12788 (Some((lm, _, logits)), None) => {
12789 logits.resize(b * lm.1, 0.0);
12790 if !graph.read_logits(logits) {
12791 return MetalRowsRun::Failed;
12792 }
12793 spec_stamp("v.lg");
12794 }
12795 (None, _) => {}
12796 }
12797 if !graph.read_hidden(hiddens) {
12798 return MetalRowsRun::Failed;
12799 }
12800 spec_stamp("v.hid");
12801 MetalRowsRun::Completed(MetalVerifyPending {
12802 graph,
12803 gdn_layers,
12804 attn_layers,
12805 })
12806 }
12807
12808 #[cfg(target_os = "macos")]
12814 fn try_batch_graph_metal(
12815 &mut self,
12816 hiddens: &mut [f32],
12817 positions: &[usize],
12818 b: usize,
12819 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12820 argmax_out: Option<(usize, &mut Vec<u32>)>,
12821 ) -> crate::gpu::BatchGraphOutcome {
12822 let _t0 = std::time::Instant::now();
12823 if positions.len() != b
12824 || positions.windows(2).any(|w| w[1] != w[0] + 1)
12825 || hiddens.len() != b * self.hidden_size
12826 {
12827 return crate::gpu::BatchGraphOutcome::Declined;
12828 }
12829 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12830 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12831 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12832 MetalRowsRun::Completed(pending) => pending,
12833 };
12834 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12835 eprintln!(
12836 "metal-verify: {:.1} ms | b={b}",
12837 _t0.elapsed().as_secs_f64() * 1e3
12838 );
12839 }
12840 self.metal_verify = Some(pending);
12841 crate::gpu::BatchGraphOutcome::Completed
12842 }
12843
12844 #[cfg(target_os = "macos")]
12849 fn prefill_rows_metal(
12850 &mut self,
12851 ids: &[u32],
12852 start_pos: usize,
12853 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12854 ) -> MetalPrefillOutcome {
12855 let b = ids.len();
12856 if b == 0 || b > 512 {
12857 return MetalPrefillOutcome::Declined;
12858 }
12859 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12860 let with_head = spec.is_some();
12861 let hs = self.hidden_size;
12862 let mut hiddens = vec![0f32; b * hs];
12863 for (j, &id) in ids.iter().enumerate() {
12864 let e = self.embed_single(id);
12865 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12866 }
12867 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12868 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12869 MetalRowsRun::Failed => {
12870 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12871 return MetalPrefillOutcome::Failed;
12872 }
12873 MetalRowsRun::Completed(pending) => pending,
12874 };
12875 let idxs = pending.gdn_layers.clone();
12877 let mut outs: Vec<&mut [f32]> = self
12878 .kv_cache
12879 .layers
12880 .iter_mut()
12881 .enumerate()
12882 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12883 .map(|(_, l)| l.linear_state.as_mut_slice())
12884 .collect();
12885 if !pending.graph.finish_states(&mut outs) {
12886 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12887 return MetalPrefillOutcome::Failed;
12888 }
12889 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12890 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12894 for (li, cpu_stored) in &pending.attn_layers {
12895 let mut kbuf = vec![0f32; b * nkv * hd];
12896 let mut vbuf = vec![0f32; b * nkv * hd];
12897 if !crate::gpu_metal::kv_mirror_read_rows(
12898 self.graph_kv_id,
12899 *li,
12900 nkv,
12901 hd,
12902 *cpu_stored,
12903 b,
12904 &mut kbuf,
12905 &mut vbuf,
12906 ) {
12907 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12908 return MetalPrefillOutcome::Failed;
12909 }
12910 rows.push((*li, *cpu_stored, kbuf, vbuf));
12911 }
12912 for (li, cpu_stored, kbuf, vbuf) in rows {
12913 let cache = &mut self.kv_cache.layers[li];
12914 for r in 0..b {
12915 cache.append(
12916 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12917 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12918 &[],
12919 );
12920 }
12921 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12922 }
12923 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12924 if with_head {
12925 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12926 }
12927 MetalPrefillOutcome::Completed(hiddens)
12928 }
12929
12930 #[cfg(target_os = "macos")]
12931 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12932 self.prefill_rows_metal(ids, start_pos, None)
12933 }
12934
12935 #[cfg(target_os = "macos")]
12940 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12941 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12942 return MetalBatchNllOutcome::Declined;
12943 }
12944 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12945 return MetalBatchNllOutcome::Declined;
12946 };
12947 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12948 .ok()
12949 .and_then(|v| v.parse::<usize>().ok())
12950 .filter(|&v| (1..=512).contains(&v))
12951 .unwrap_or(32);
12952 let final_norm = self.weights.final_norm.clone();
12953 let mut nll = 0.0f64;
12954 let mut count = 0usize;
12955 let mut pos = 0usize;
12956 let mut completed = 0usize;
12957 while pos < ids.len() {
12958 let end = (pos + chunk).min(ids.len());
12959 let mut logits = Vec::new();
12960 let outcome = self.prefill_rows_metal(
12961 &ids[pos..end],
12962 pos,
12963 Some((lm, &final_norm, &mut logits)),
12964 );
12965 match outcome {
12966 MetalPrefillOutcome::Declined => {
12967 return if completed == 0 {
12968 MetalBatchNllOutcome::Declined
12969 } else {
12970 MetalBatchNllOutcome::Failed(format!(
12971 "ordinary Metal NLL batch declined after {completed} chunks"
12972 ))
12973 };
12974 }
12975 MetalPrefillOutcome::Failed => {
12976 return MetalBatchNllOutcome::Failed(
12977 "ordinary Metal NLL batch failed after admission".to_string(),
12978 );
12979 }
12980 MetalPrefillOutcome::Completed(_) => {}
12981 }
12982 completed += 1;
12983 let vocab = self.vocab_size.min(lm.1);
12984 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
12985 return MetalBatchNllOutcome::Failed(
12986 "ordinary Metal NLL head returned an invalid shape".to_string(),
12987 );
12988 }
12989 for row in 0..(end - pos) {
12990 let absolute = pos + row;
12991 if absolute < start || absolute + 1 >= ids.len() {
12992 continue;
12993 }
12994 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
12995 if let Some(mu) = self.logit_multiplier {
12996 for v in lg.iter_mut() {
12997 *v *= mu;
12998 }
12999 }
13000 if let Some(c) = self.final_softcap {
13001 softcap_in_place(lg, c, self.pool.as_deref());
13002 }
13003 let target = ids[absolute + 1] as usize;
13004 if target >= vocab {
13005 return MetalBatchNllOutcome::Failed(format!(
13006 "target token {target} exceeds Metal head rows {vocab}"
13007 ));
13008 }
13009 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13010 let lse: f64 = lg
13011 .iter()
13012 .map(|&v| ((v - max) as f64).exp())
13013 .sum::<f64>()
13014 .ln()
13015 + max as f64;
13016 nll += lse - lg[target] as f64;
13017 count += 1;
13018 }
13019 pos = end;
13020 }
13021 MetalBatchNllOutcome::Completed(nll, count)
13022 }
13023
13024 #[cfg(target_os = "macos")]
13028 fn metal_verify_commit(&mut self, a: usize) -> bool {
13029 let Some(mut pending) = self.metal_verify.take() else {
13030 return false;
13031 };
13032 let n = a + 1;
13033 let idxs = pending.gdn_layers.clone();
13035 let mut outs: Vec<&mut [f32]> = self
13036 .kv_cache
13037 .layers
13038 .iter_mut()
13039 .enumerate()
13040 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13041 .map(|(_, l)| l.linear_state.as_mut_slice())
13042 .collect();
13043 if !pending.graph.commit(n, &mut outs) {
13044 return false;
13045 }
13046 spec_stamp("c.replay");
13047 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13048 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13052 for (li, cpu_stored) in &pending.attn_layers {
13053 let mut kbuf = vec![0f32; n * nkv * hd];
13054 let mut vbuf = vec![0f32; n * nkv * hd];
13055 if !crate::gpu_metal::kv_mirror_read_rows(
13056 self.graph_kv_id,
13057 *li,
13058 nkv,
13059 hd,
13060 *cpu_stored,
13061 n,
13062 &mut kbuf,
13063 &mut vbuf,
13064 ) {
13065 return false;
13066 }
13067 rows.push((*li, *cpu_stored, kbuf, vbuf));
13068 }
13069 for (li, cpu_stored, kbuf, vbuf) in rows {
13070 let cache = &mut self.kv_cache.layers[li];
13071 for r in 0..n {
13072 cache.append(
13073 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13074 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13075 &[],
13076 );
13077 }
13078 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13079 }
13080 spec_stamp("c.kv");
13081 true
13082 }
13083
13084 #[cfg(target_os = "macos")]
13091 fn mtp_warm_batch_submit(
13092 &mut self,
13093 m: &mut MtpModule,
13094 pairs: &[(&[f32], u32)],
13095 first_pos: usize,
13096 ) -> Option<MetalWarmPending> {
13097 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13098 let b = pairs.len();
13099 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13100 return None;
13101 }
13102 let AttnKind::Full {
13103 wq,
13104 wk,
13105 wv,
13106 wo,
13107 q_norm,
13108 k_norm,
13109 output_gate,
13110 softplus_gate: None,
13111 bias: None,
13112 } = &m.layer.attn
13113 else {
13114 return None;
13115 };
13116 let FfnKind::Dense(d) = &m.layer.ffn else {
13117 return None;
13118 };
13119 if !d.segs.is_empty() {
13120 return None;
13121 }
13122 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13123 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13124 else {
13125 return None;
13126 };
13127 let (Some(g), Some(u), Some(dn)) = (
13128 d.gate_proj.q1_parts(),
13129 d.up_proj.q1_parts(),
13130 d.down_proj.q1_parts(),
13131 ) else {
13132 return None;
13133 };
13134 let Some(eh) = m.eh_proj.q1_parts() else {
13135 return None;
13136 };
13137 let QTensor::Mapped { model, .. } = wq else {
13138 return None;
13139 };
13140 let model = model.clone();
13141 let hs = self.hidden_size;
13142 let mut cat = vec![0f32; b * 2 * hs];
13144 for (j, (h, tok)) in pairs.iter().enumerate() {
13145 let e = self.embed_single(*tok);
13146 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13147 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13148 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13149 }
13150 let dims = GraphDims {
13151 hidden: hs,
13152 eps: self.rms_eps as f32,
13153 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13154 };
13155 spec_stamp("w.cat");
13156 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13157 return None;
13158 };
13159 spec_stamp("w.new");
13160 let l = AttnGpuLayer {
13161 attn_norm: &m.layer.input_norm,
13162 post_norm: &m.layer.post_norm,
13163 wq: pq,
13164 wk: pk,
13165 wv: pv,
13166 wo: po,
13167 ffn: MetalFfn::Dense {
13168 gate: g,
13169 up: u,
13170 down: dn,
13171 act: (d.act == Act::Gelu) as u8,
13172 },
13173 attn_out_norm: None,
13174 ffn_out_norm: None,
13175 layer_scale: None,
13176 };
13177 let (nh, nkv, hd, rd) = (
13178 self.num_heads,
13179 self.num_kv_heads,
13180 self.head_dim,
13181 self.rotary_dim,
13182 );
13183 let inv_freq = self.inv_freq.clone();
13184 let cpu_stored;
13185 {
13186 let cache = &m.kv;
13187 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13188 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13189 cpu_stored = cpu_k[0].len() / hd;
13190 if cpu_stored > first_pos {
13195 spec_stamp("w.decl");
13196 return None;
13197 }
13198 let p = AttnDeviceParams {
13199 kv_id: self.mtp_kv_id(),
13200 layer: Self::MTP_LAYER_BASE,
13201 nh,
13202 nkv,
13203 hd,
13204 rd,
13205 position: first_pos,
13206 scale: self.attn_scale,
13207 eps: self.rms_eps as f32,
13208 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13209 late_qk_norm: self.qk_norm_after_rope,
13210 output_gate: *output_gate,
13211 q_norm: q_norm.as_deref(),
13212 k_norm: k_norm.as_deref(),
13213 inv_freq: &inv_freq,
13214 rope_scale: self.rope_scale,
13215 cpu_k,
13216 cpu_v,
13217 cpu_stored,
13218 cpu_gen: cache.generation(),
13219 o1: None,
13220 window: None,
13221 head_gate: None,
13222 v_norm: false,
13223 };
13224 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13225 return None;
13226 }
13227 }
13228 spec_stamp("w.enc");
13229 if !graph.submit() {
13230 return None;
13231 }
13232 spec_stamp("w.sub");
13233 Some(MetalWarmPending {
13234 graph,
13235 cpu_stored,
13236 b,
13237 })
13238 }
13239
13240 #[cfg(target_os = "macos")]
13243 fn mtp_warm_batch_metal(
13244 &mut self,
13245 m: &mut MtpModule,
13246 pairs: &[(&[f32], u32)],
13247 first_pos: usize,
13248 ) -> bool {
13249 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13250 Some(p) => self.mtp_warm_batch_finish(m, p),
13251 None => false,
13252 }
13253 }
13254
13255 #[cfg(target_os = "macos")]
13260 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13261 let MetalWarmPending {
13262 mut graph,
13263 cpu_stored,
13264 b,
13265 } = pending;
13266 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13267 if !graph.sync() {
13268 return false;
13269 }
13270 spec_stamp("w.gpu");
13271 let mut kbuf = vec![0f32; b * nkv * hd];
13272 let mut vbuf = vec![0f32; b * nkv * hd];
13273 if !crate::gpu_metal::kv_mirror_read_rows(
13274 self.mtp_kv_id(),
13275 Self::MTP_LAYER_BASE,
13276 nkv,
13277 hd,
13278 cpu_stored,
13279 b,
13280 &mut kbuf,
13281 &mut vbuf,
13282 ) {
13283 return false;
13284 }
13285 for r in 0..b {
13286 m.kv.append(
13287 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13288 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13289 &[],
13290 );
13291 }
13292 crate::gpu_metal::kv_mirror_set_stored(
13293 self.mtp_kv_id(),
13294 Self::MTP_LAYER_BASE,
13295 cpu_stored + b,
13296 );
13297 spec_stamp("w.kv");
13298 true
13299 }
13300
13301 pub(crate) fn note_draft_id(&mut self, id: u32) {
13308 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13309 if (id as usize) >= cut {
13310 self.draft_full_streak = 16;
13311 } else {
13312 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13313 }
13314 }
13315
13316 fn draft_head_rows(&self, head_rows: usize) -> usize {
13319 if self.draft_full_streak > 0 {
13320 head_rows
13321 } else {
13322 Self::draft_vocab_rows(head_rows)
13323 }
13324 }
13325
13326 fn draft_vocab_rows(head_rows: usize) -> usize {
13329 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13330 let n = *N.get_or_init(|| {
13331 std::env::var("CMF_DRAFT_VOCAB")
13332 .ok()
13333 .and_then(|v| v.parse().ok())
13334 .unwrap_or(65536)
13335 });
13336 if n == 0 { head_rows } else { n.min(head_rows) }
13337 }
13338
13339 #[cfg(target_os = "macos")]
13344 fn mtp_step_metal(
13345 &mut self,
13346 m: &mut MtpModule,
13347 hidden: &[f32],
13348 next_token: u32,
13349 position: usize,
13350 want_logits: bool,
13351 ) -> Option<(Vec<f32>, Vec<f32>)> {
13352 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13353 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13354 || !crate::gpu::q1_force()
13355 || !crate::gpu::enabled_here()
13356 || self.attn_softcap > 0.0
13357 || self.attention_heads_per_layer.is_some()
13358 || m.kv.mode != crate::kv_cache::KvMode::F32
13359 || m.kv.o1.is_some()
13360 {
13361 return None;
13362 }
13363 let AttnKind::Full {
13364 wq,
13365 wk,
13366 wv,
13367 wo,
13368 q_norm,
13369 k_norm,
13370 output_gate,
13371 softplus_gate: None,
13372 bias: None,
13373 } = &m.layer.attn
13374 else {
13375 return None;
13376 };
13377 let FfnKind::Dense(d) = &m.layer.ffn else {
13378 return None;
13379 };
13380 if d.act != Act::Silu || !d.segs.is_empty() {
13381 return None;
13382 }
13383 let (pq, pk, pv, po) = (
13384 wq.q1_parts()?,
13385 wk.q1_parts()?,
13386 wv.q1_parts()?,
13387 wo.q1_parts()?,
13388 );
13389 let (g, u, dn) = (
13390 d.gate_proj.q1_parts()?,
13391 d.up_proj.q1_parts()?,
13392 d.down_proj.q1_parts()?,
13393 );
13394 let QTensor::Mapped { model, .. } = wq else {
13395 return None;
13396 };
13397 let model = model.clone();
13398 let lm = if want_logits {
13399 Some(self.weights.lm_head.q1_parts()?)
13400 } else {
13401 None
13402 };
13403 let dims = GraphDims {
13404 hidden: self.hidden_size,
13405 eps: self.rms_eps as f32,
13406 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13407 };
13408 let hs = self.hidden_size;
13411 let mut x = vec![0f32; hs];
13412 let mut graph = TokenGraph::new(&model, dims, &x)?;
13413 let mut folded = false;
13414 if let Some(eh) = m.eh_proj.q1_parts() {
13415 let e = self.embed_single(next_token);
13416 let mut cat = vec![0.0f32; 2 * hs];
13417 let (cat_e, cat_h) = cat.split_at_mut(hs);
13418 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13419 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13420 folded = graph.encode_input_proj(eh, &cat);
13421 }
13422 if !folded {
13423 x = self.mtp_block_input(m, hidden, next_token);
13424 graph = TokenGraph::new(&model, dims, &x)?;
13425 }
13426 spec_stamp("d.in");
13427 let l = AttnGpuLayer {
13428 attn_norm: &m.layer.input_norm,
13429 post_norm: &m.layer.post_norm,
13430 wq: pq,
13431 wk: pk,
13432 wv: pv,
13433 wo: po,
13434 ffn: MetalFfn::Dense {
13435 gate: g,
13436 up: u,
13437 down: dn,
13438 act: (d.act == Act::Gelu) as u8,
13439 },
13440 attn_out_norm: None,
13441 ffn_out_norm: None,
13442 layer_scale: None,
13443 };
13444 let (nh, nkv, hd, rd) = (
13445 self.num_heads,
13446 self.num_kv_heads,
13447 self.head_dim,
13448 self.rotary_dim,
13449 );
13450 let inv_freq = self.inv_freq.clone();
13451 {
13452 let cache = &m.kv;
13453 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13454 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13455 let cpu_stored = cpu_k[0].len() / hd;
13456 let p = AttnDeviceParams {
13457 kv_id: self.mtp_kv_id(),
13458 layer: Self::MTP_LAYER_BASE,
13459 nh,
13460 nkv,
13461 hd,
13462 rd,
13463 position,
13464 scale: self.attn_scale,
13465 eps: self.rms_eps as f32,
13466 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13467 late_qk_norm: self.qk_norm_after_rope,
13468 output_gate: *output_gate,
13469 q_norm: q_norm.as_deref(),
13470 k_norm: k_norm.as_deref(),
13471 inv_freq: &inv_freq,
13472 rope_scale: self.rope_scale,
13473 cpu_k,
13474 cpu_v,
13475 cpu_stored,
13476 cpu_gen: cache.generation(),
13477 o1: None,
13478 window: None,
13479 head_gate: None,
13480 v_norm: false,
13481 };
13482 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13483 return None;
13484 }
13485 }
13486 let draft_rows = if let Some(lm) = lm {
13492 self.draft_head_rows(lm.1)
13493 } else {
13494 0
13495 };
13496 if let Some(lm) = lm {
13497 if !graph.lm_head_ok(lm) {
13498 return None;
13499 }
13500 if draft_rows < lm.1 {
13501 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13502 return None;
13503 }
13504 } else {
13505 graph.encode_lm_head(&m.final_norm, lm);
13506 }
13507 }
13508 spec_stamp("d.enc");
13509 if graph.sync_checked().is_err() {
13510 return None;
13511 }
13512 spec_stamp("d.gpu");
13513 let mut logits = Vec::new();
13514 if let Some(lm) = lm {
13515 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13516 logits = attention::take_buf(n_read);
13517 graph.read_logits(&mut logits);
13518 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13520 }
13521 graph.finish(&mut x);
13522 let mut krow = attention::take_buf(nkv * hd);
13523 let mut vrow = attention::take_buf(nkv * hd);
13524 if crate::gpu_metal::kv_mirror_read_last(
13525 self.mtp_kv_id(),
13526 Self::MTP_LAYER_BASE,
13527 nkv,
13528 hd,
13529 &mut krow,
13530 &mut vrow,
13531 ) {
13532 m.kv.append(&krow, &vrow, &[]);
13533 }
13534 attention::recycle_buf(&mut krow);
13535 attention::recycle_buf(&mut vrow);
13536 spec_stamp("d.rd");
13537 Some((logits, x))
13538 }
13539
13540 fn mtp_chain_on() -> bool {
13553 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13554 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13555 }
13556
13557 #[cfg(target_os = "macos")]
13569 fn mtp_draft_chain_metal(
13570 &mut self,
13571 m: &mut MtpModule,
13572 hidden: &[f32],
13573 t_next: u32,
13574 position: usize,
13575 k: usize,
13576 ) -> Result<Vec<u32>, bool> {
13577 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13578 if k == 0
13579 || k > 64
13580 || !Self::mtp_chain_on()
13581 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13582 || !crate::gpu::q1_force()
13583 || !crate::gpu::enabled_here()
13584 || self.attn_softcap > 0.0
13585 || self.attention_heads_per_layer.is_some()
13586 || m.kv.mode != crate::kv_cache::KvMode::F32
13587 || m.kv.o1.is_some()
13588 || self.dsv4.is_some()
13590 || self.dsv41.is_some()
13591 || self.qwen4_exp.is_some()
13592 || self.g3n.is_some()
13593 {
13594 return Err(false);
13595 }
13596 let AttnKind::Full {
13597 wq,
13598 wk,
13599 wv,
13600 wo,
13601 q_norm,
13602 k_norm,
13603 output_gate,
13604 softplus_gate: None,
13605 bias: None,
13606 } = &m.layer.attn
13607 else {
13608 return Err(false);
13609 };
13610 let FfnKind::Dense(d) = &m.layer.ffn else {
13611 return Err(false);
13612 };
13613 if d.act != Act::Silu || !d.segs.is_empty() {
13614 return Err(false);
13615 }
13616 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13617 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13618 else {
13619 return Err(false);
13620 };
13621 let (Some(g), Some(u), Some(dn)) = (
13622 d.gate_proj.q1_parts(),
13623 d.up_proj.q1_parts(),
13624 d.down_proj.q1_parts(),
13625 ) else {
13626 return Err(false);
13627 };
13628 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13629 return Err(false);
13630 };
13631 let QTensor::Mapped { model, .. } = wq else {
13632 return Err(false);
13633 };
13634 let model = model.clone();
13635 let QTensor::Mapped {
13638 model: em,
13639 idx: eidx,
13640 dtype: cortiq_core::TensorDtype::Q4TiledP,
13641 ..
13642 } = &self.weights.embed_tokens
13643 else {
13644 return Err(false);
13645 };
13646 if !std::sync::Arc::ptr_eq(em, &model)
13647 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13648 {
13649 return Err(false);
13650 }
13651 let embed = (
13652 *eidx,
13653 self.weights.embed_tokens.rows(),
13654 self.weights.embed_tokens.cols(),
13655 );
13656 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13657 return Err(false);
13658 }
13659 let dims = GraphDims {
13660 hidden: self.hidden_size,
13661 eps: self.rms_eps as f32,
13662 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13663 };
13664 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13665 return Err(false);
13666 };
13667 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13668 return Err(false);
13669 }
13670 let l = AttnGpuLayer {
13671 attn_norm: &m.layer.input_norm,
13672 post_norm: &m.layer.post_norm,
13673 wq: pq,
13674 wk: pk,
13675 wv: pv,
13676 wo: po,
13677 ffn: MetalFfn::Dense {
13678 gate: g,
13679 up: u,
13680 down: dn,
13681 act: (d.act == Act::Gelu) as u8,
13682 },
13683 attn_out_norm: None,
13684 ffn_out_norm: None,
13685 layer_scale: None,
13686 };
13687 let (nh, nkv, hd, rd) = (
13688 self.num_heads,
13689 self.num_kv_heads,
13690 self.head_dim,
13691 self.rotary_dim,
13692 );
13693 let inv_freq = self.inv_freq.clone();
13694 let draft_rows = self.draft_head_rows(lm.1);
13695 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13696 if n_arg == 0 {
13697 return Err(false);
13698 }
13699 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13706 let t_chain = std::time::Instant::now();
13707 graph.chain_ids_init(t_next, k);
13708 let cpu_stored;
13709 {
13710 let cache = &m.kv;
13711 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13712 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13713 cpu_stored = cpu_k[0].len() / hd;
13714 for j in 0..k {
13715 if !graph.encode_chain_input(
13716 embed,
13717 j as u32,
13718 &m.enorm,
13719 &m.hnorm,
13720 self.embed_multiplier,
13721 eh,
13722 ) {
13723 return Err(false);
13724 }
13725 let p = AttnDeviceParams {
13729 kv_id: self.mtp_kv_id(),
13730 layer: Self::MTP_LAYER_BASE,
13731 nh,
13732 nkv,
13733 hd,
13734 rd,
13735 position: position + j,
13736 scale: self.attn_scale,
13737 eps: self.rms_eps as f32,
13738 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13739 late_qk_norm: self.qk_norm_after_rope,
13740 output_gate: *output_gate,
13741 q_norm: q_norm.as_deref(),
13742 k_norm: k_norm.as_deref(),
13743 inv_freq: &inv_freq,
13744 rope_scale: self.rope_scale,
13745 cpu_k: cpu_k.clone(),
13746 cpu_v: cpu_v.clone(),
13747 cpu_stored: cpu_stored + j,
13748 cpu_gen: cache.generation(),
13749 o1: None,
13750 window: None,
13751 head_gate: None,
13752 v_norm: false,
13753 };
13754 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13755 return Err(false);
13756 }
13757 if draft_rows < lm.1 {
13758 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13759 return Err(false);
13760 }
13761 } else {
13762 graph.encode_lm_head(&m.final_norm, lm);
13763 }
13764 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13765 return Err(false);
13766 }
13767 if split {
13768 graph.commit();
13771 }
13772 }
13773 }
13774 let t_enc = t_chain.elapsed();
13775 if graph.sync_checked().is_err() {
13776 return Err(true);
13777 }
13778 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
13779 eprintln!(
13780 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
13781 t_enc.as_secs_f64() * 1e3,
13782 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
13783 if split { ", split" } else { "" }
13784 );
13785 }
13786 let mut ids = vec![0u32; k];
13787 if !graph.chain_ids_read(&mut ids) {
13788 return Err(true);
13789 }
13790 let mut kbuf = vec![0f32; k * nkv * hd];
13791 let mut vbuf = vec![0f32; k * nkv * hd];
13792 if !crate::gpu_metal::kv_mirror_read_rows(
13793 self.mtp_kv_id(),
13794 Self::MTP_LAYER_BASE,
13795 nkv,
13796 hd,
13797 cpu_stored,
13798 k,
13799 &mut kbuf,
13800 &mut vbuf,
13801 ) {
13802 return Err(true);
13803 }
13804 for r in 0..k {
13805 m.kv.append(
13806 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13807 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13808 &[],
13809 );
13810 }
13811 Ok(ids)
13812 }
13813
13814 fn try_batch_graph_wgpu(
13815 &self,
13816 hiddens: &mut [f32],
13817 positions: &[usize],
13818 k: usize,
13819 spec: Option<crate::gpu::SpecTail<'_>>,
13820 ) -> crate::gpu::BatchGraphOutcome {
13821 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
13822 }
13823
13824 fn try_batch_graph_wgpu_prefix(
13829 &self,
13830 hiddens: &mut [f32],
13831 positions: &[usize],
13832 k: usize,
13833 spec: Option<crate::gpu::SpecTail<'_>>,
13834 layers_run: Option<&mut usize>,
13835 ) -> crate::gpu::BatchGraphOutcome {
13836 let graph_end = match self.mimo_moe.graph_prefix_end() {
13837 Some(end) if end < self.num_layers => {
13838 if layers_run.is_none() || spec.is_some() || end == 0 {
13839 return crate::gpu::BatchGraphOutcome::Declined;
13840 }
13841 end
13842 }
13843 _ => self.num_layers,
13844 };
13845 let _tb = std::time::Instant::now();
13846 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13847 if self.attn_softcap > 0.0 {
13848 return crate::gpu::BatchGraphOutcome::Declined; }
13850 if let Some(reason) = self
13853 .wgpu_graph_attn_decline()
13854 .or_else(|| self.batch_graph_gemma_decline())
13855 {
13856 self.note_graph_decline("wgpu batch graph", reason);
13857 return crate::gpu::BatchGraphOutcome::Declined;
13858 }
13859 let nh = self.num_heads;
13860 let (nkv, hd, rd) = self.layer_geom(0);
13861 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13862 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13863 if let Some((m, i, kind, rs)) = t
13864 .graph_weight()
13865 .or_else(|| t.graph_weight_descriptor())
13866 {
13867 let name = &m.tensors[i].name;
13868 let prism = if crate::prism::is_inverse_embedding(m, name) {
13869 crate::gpu::GraphPrismOp::InverseEmbedding
13870 } else if crate::prism::is_forward_weight(m, name) {
13871 crate::gpu::GraphPrismOp::Forward
13872 } else {
13873 crate::gpu::GraphPrismOp::None
13874 };
13875 return Some(crate::gpu::GraphW {
13876 idx: i,
13877 kind,
13878 row_scale: rs,
13879 data: &[],
13880 prism,
13881 affine: crate::prism::is_affine_target(m, name),
13882 });
13883 }
13884 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13885 eprintln!(
13886 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13887 t.rows(),
13888 t.cols()
13889 );
13890 }
13891 t.as_f32().map(|d| crate::gpu::GraphW {
13892 idx: 0,
13893 kind: 4,
13894 row_scale: &[],
13895 data: d,
13896 prism: crate::gpu::GraphPrismOp::None,
13897 affine: false,
13898 })
13899 }
13900 let built: Option<(
13901 Vec<crate::gpu::GraphLayer<'_>>,
13902 std::sync::Arc<cortiq_core::CmfModel>,
13903 )> = (|| {
13904 let mut layers = Vec::with_capacity(graph_end);
13905 let mut model = None;
13906 for li in 0..graph_end {
13907 let lw = &self.weights.layers[self.phys_layer(li)];
13908 let gffn = match &lw.ffn {
13915 FfnKind::Dense(d) if !d.segs.is_empty() => {
13916 if batch_debug {
13917 eprintln!("batch graph: dense segmented FFN at layer {li}");
13918 }
13919 return None;
13920 }
13921 FfnKind::Dense(d) => {
13922 let Some(act) = d.act.graph_act() else {
13923 if batch_debug {
13924 eprintln!(
13925 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13926 d.act
13927 );
13928 }
13929 return None;
13930 };
13931 crate::gpu::GraphFfn::Dense {
13932 gate: gw(&d.gate_proj)?,
13933 up: gw(&d.up_proj)?,
13934 down: gw(&d.down_proj)?,
13935 act,
13936 }
13937 }
13938 FfnKind::Moe(m) => {
13939 if m.route_tau.is_some() || m.mask.is_some() {
13946 return None;
13947 }
13948 let shared = m.shared.as_ref();
13952 let has_shared = shared.is_some();
13953 let shared_gated = matches!(shared, Some((_, Some(_))));
13954 let sgate = match shared {
13955 Some((_, Some(sg))) => gw(sg)?,
13956 _ => gw(&m.router)?,
13960 };
13961 let router = gw(&m.router)?;
13962 if router.prism != crate::gpu::GraphPrismOp::None
13968 || router.affine
13969 || sgate.prism != crate::gpu::GraphPrismOp::None
13970 || sgate.affine
13971 {
13972 return None;
13973 }
13974 let inter = m.experts.first()?.gate_proj.rows();
13975 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13976 let mut q4tp: Option<bool> = None;
13977 let mut gu_q2: Option<bool> = None;
13978 let mut q82: Option<bool> = None;
13979 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
13980 if !matches!(e.act, Act::Silu)
13981 || e.gate_proj.rows() != inter
13982 || e.up_proj.rows() != inter
13983 {
13984 return None;
13985 }
13986 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
13990 {
13991 Some((mm, gi)) => (
13992 mm,
13993 gi,
13994 e.up_proj.mapped_q4t()?.1,
13995 e.down_proj.mapped_q4t()?.1,
13996 false,
13997 false,
13998 false,
13999 ),
14000 None => match e.gate_proj.mapped_q2tp() {
14001 Some((mm, gi)) => (
14002 mm,
14003 gi,
14004 e.up_proj.mapped_q2tp()?.1,
14005 e.down_proj.mapped_q4tp()?.1,
14006 true,
14007 true,
14008 false,
14009 ),
14010 None => match e.gate_proj.mapped_q4tp() {
14011 Some((mm, gi)) => (
14012 mm,
14013 gi,
14014 e.up_proj.mapped_q4tp()?.1,
14015 e.down_proj.mapped_q4tp()?.1,
14016 true,
14017 false,
14018 false,
14019 ),
14020 None => {
14021 if !crate::gpu::moe_q82_graph_on() {
14022 return None;
14023 }
14024 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14025 (
14026 mm,
14027 gi,
14028 e.up_proj.mapped_q8_2f()?.1,
14029 e.down_proj.mapped_q8_2f()?.1,
14030 false,
14031 false,
14032 true,
14033 )
14034 }
14035 },
14036 },
14037 };
14038 if *q4tp.get_or_insert(is_p) != is_p
14039 || *gu_q2.get_or_insert(is_q2) != is_q2
14040 || *q82.get_or_insert(is_8) != is_8
14041 {
14042 return None;
14043 }
14044 if [gi, ui, di].into_iter().any(|idx| {
14045 mm.tensors
14046 .get(idx)
14047 .is_some_and(|t| {
14048 crate::prism::is_forward_weight(mm, &t.name)
14049 || crate::prism::is_affine_target(mm, &t.name)
14050 })
14051 }) {
14052 return None;
14053 }
14054 model.get_or_insert_with(|| mm.clone());
14055 experts.push((gi, ui, di));
14056 }
14057 crate::gpu::GraphFfn::Moe {
14058 router,
14059 shared_gate: sgate,
14060 experts,
14061 n_exp: m.experts.len(),
14062 top_k: m.top_k,
14063 inter,
14064 norm_topk: m.norm_topk_prob,
14065 q4tp: q4tp?,
14066 gu_q2: gu_q2.unwrap_or(false),
14067 q82: q82.unwrap_or(false),
14068 sigmoid: m.router_sigmoid,
14069 bias: m.expert_bias.as_deref(),
14070 has_shared,
14071 shared_gated,
14072 route_scale: m.routed_scaling,
14073 }
14074 }
14075 _ => return None,
14076 };
14077 let attn = match &lw.attn {
14078 AttnKind::Full {
14079 wq,
14080 wk,
14081 wv,
14082 wo,
14083 q_norm,
14084 k_norm,
14085 output_gate,
14086 softplus_gate,
14087 bias,
14088 } => {
14089 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14090 if batch_debug {
14091 eprintln!(
14092 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14093 softplus_gate.is_some(),
14094 self.attention_heads_per_layer.is_some()
14095 );
14096 }
14097 return None;
14098 }
14099 let (m, _, _, _) = wq
14100 .graph_weight()
14101 .or_else(|| wq.graph_weight_descriptor())?;
14102 model = Some(m.clone());
14103 crate::gpu::GraphAttn::Full {
14104 wq: gw(wq)?,
14105 wk: gw(wk)?,
14106 wv: gw(wv)?,
14107 wo: gw(wo)?,
14108 q_norm: q_norm.as_deref(),
14109 k_norm: k_norm.as_deref(),
14110 late_qk_norm: self.qk_norm_after_rope,
14111 bias: bias
14112 .as_ref()
14113 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14114 output_gate: *output_gate,
14115 cpu_k: self.kv_cache.layers[li].k_heads(),
14116 cpu_v: self.kv_cache.layers[li].v_heads(),
14117 cpu_base: self.kv_cache.layers[li].base(),
14118 geom: self.graph_attn_geom(li),
14119 head_gate: None,
14122 }
14123 }
14124 AttnKind::LinearGdn(w) => {
14125 let Some(cfg) = self.gdn_cfg else {
14126 if batch_debug {
14127 eprintln!("batch graph: no GDN config at layer {li}");
14128 }
14129 return None;
14130 };
14131 let (m, _, _, _) = w
14132 .in_proj_qkv
14133 .graph_weight()
14134 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14135 model = Some(m.clone());
14136 crate::gpu::GraphAttn::Gdn {
14137 qkv: gw(&w.in_proj_qkv)?,
14138 z: gw(&w.in_proj_z)?,
14139 a: gw(&w.in_proj_a)?,
14140 b: gw(&w.in_proj_b)?,
14141 out: gw(&w.out_proj)?,
14142 conv1d: &w.conv1d,
14143 a_log: &w.a_log,
14144 dt_bias: &w.dt_bias,
14145 norm: &w.norm,
14146 nv: cfg.num_v_heads,
14147 nk: cfg.num_k_heads,
14148 dk: cfg.key_head_dim,
14149 dv: cfg.value_head_dim,
14150 kk: cfg.conv_kernel,
14151 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14152 }
14153 }
14154 _ => return None,
14155 };
14156 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14159 layers.push(crate::gpu::GraphLayer {
14160 input_norm: &lw.input_norm,
14161 attn,
14162 post_norm: &lw.post_norm,
14163 attn_out_norm: if g4 { lw.attn_out_norm.as_deref() } else { None },
14164 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14165 layer_scale: if g4 { lw.layer_scale } else { None },
14166 ffn: gffn,
14167 });
14168 }
14169 Some((layers, model?))
14170 })();
14171 let Some((layers, model)) = built else {
14172 {
14173 use std::sync::atomic::{AtomicBool, Ordering};
14174 static SAID: AtomicBool = AtomicBool::new(false);
14175 if !SAID.swap(true, Ordering::Relaxed) {
14176 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14177 }
14178 }
14179 return crate::gpu::BatchGraphOutcome::Declined;
14180 };
14181 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14182 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14183 }
14184 crate::gpu::forward_batch_graph(
14185 &model,
14186 self.graph_kv_id,
14187 &layers,
14188 &self.inv_freq,
14189 hiddens,
14190 nh,
14191 nkv,
14192 hd,
14193 rd,
14194 self.hidden_size,
14195 self.intermediate_size,
14196 positions,
14197 self.kv_cache.max_seq_len,
14198 gemma,
14199 self.rms_eps as f32,
14200 self.attn_scale,
14201 k,
14202 &(0..graph_end)
14203 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14204 .collect::<Vec<_>>(),
14205 self.o1_epoch,
14206 spec,
14207 layers_run,
14208 )
14209 }
14210
14211 fn draft_probe() -> bool {
14215 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14216 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14217 }
14218
14219 #[cfg(feature = "gpu")]
14231 fn dsv4_spec_on() -> bool {
14232 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14233 *ON.get_or_init(|| {
14234 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14238 return v != "0";
14239 }
14240 std::env::var("CMF_DSV4_SPEC")
14247 .map(|v| v != "0")
14248 .unwrap_or_else(|_| {
14249 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14250 })
14251 })
14252 }
14253
14254 #[cfg(feature = "gpu")]
14261 fn dsv4_spec_step(
14262 &mut self,
14263 tip_token: u32,
14264 t_next: u32,
14265 next_pos: usize,
14266 max_extra: usize,
14267 drafted: &mut usize,
14268 accepted_ctr: &mut usize,
14269 ) -> Option<(Vec<u32>, usize)> {
14270 let t_all = std::time::Instant::now();
14271 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14272 thread_local! {
14273 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14274 const { std::cell::Cell::new(None) };
14275 }
14276 LAST.with(|l| {
14277 if let Some(prev) = l.get() {
14278 eprintln!(
14279 "между раундами {:.1} мс",
14280 prev.elapsed().as_secs_f64() * 1e3
14281 );
14282 }
14283 l.set(Some(std::time::Instant::now()));
14284 });
14285 }
14286 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14287 eprintln!("spec_step: вход pos={next_pos}");
14288 }
14289 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14290 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14291 if self.dspark.is_none() {
14293 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14294 if t.is_empty() {
14295 return None;
14296 }
14297 crate::dsv4::dspark_arm(&t, cfg.dim);
14298 self.dspark = Some(crate::dsv4::DsparkState::new(
14299 self.dsv4_mtp.len(),
14300 &cfg,
14301 t.len(),
14302 ));
14303 }
14304 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14305 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
14306 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14307 eprintln!("spec_step: пак не построился (targets {targets:?})");
14308 }
14309 let pack = pack?;
14310 let block = crate::dsv4::dspark_block();
14311 let b_box = self.dsv4.as_mut()?;
14312 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
14313 let ds = self.dspark.as_mut()?;
14314 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14317 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14318 if dbg {
14319 eprintln!("spec_step: нет захвата");
14320 }
14321 return None;
14322 }
14323 ds.have_hidden = true;
14324 let tip_pos = next_pos.checked_sub(1)?;
14325 let draft_started = std::time::Instant::now();
14326 let mut conf = Vec::new();
14327 let props = crate::dsv4::dspark_draft_gpu(
14328 g,
14329 &self.dsv4_mtp,
14330 &cfg,
14331 ds,
14332 pack,
14333 st.kv_id,
14334 tip_token,
14335 tip_pos,
14336 self.pool.as_deref(),
14337 &mut conf,
14338 );
14339 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14340 *drafted += block;
14341 if props.is_empty() || props[0] != t_next {
14342 if dbg {
14343 eprintln!(
14344 "spec_step: черновик {} (props0={:?} t_next={t_next})",
14345 if props.is_empty() {
14346 "пуст"
14347 } else {
14348 "мимо"
14349 },
14350 props.first()
14351 );
14352 }
14353 return None;
14354 }
14355 let mut k_verify = crate::dsv4::dspark_verify_k()
14362 .min(props.len())
14363 .min(max_extra.saturating_add(1));
14364 let conf_min = {
14370 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14371 *M.get_or_init(|| {
14372 std::env::var("CMF_DSPARK_CONF_MIN")
14373 .ok()
14374 .and_then(|v| v.parse().ok())
14375 .unwrap_or(0.0)
14376 })
14377 };
14378 if conf_min > 0.0 && conf.len() >= props.len() {
14379 let mut keep = 1usize;
14380 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14381 keep += 1;
14382 }
14383 k_verify = k_verify.min(keep.max(2));
14384 }
14385 if k_verify < 2 {
14386 return None;
14387 }
14388 let mut fed = Vec::with_capacity(k_verify);
14389 fed.push(t_next);
14390 fed.extend_from_slice(&props[1..k_verify]);
14391 let mut argmax = Vec::new();
14392 let mut logits_all = Vec::new();
14393 let mut walked = Vec::new();
14394 let txn = crate::dsv4::dsv4_verify_chunk(
14395 g,
14396 layers,
14397 &cfg,
14398 st,
14399 &fed,
14400 next_pos,
14401 &self.inv_freq,
14402 self.pool.as_deref(),
14403 &targets,
14404 &mut argmax,
14405 &mut logits_all,
14406 &mut walked,
14407 );
14408 if txn.is_none() && dbg {
14409 eprintln!("spec_step: verify отказал");
14410 }
14411 let txn = txn?;
14412 let spec_gpu_end = txn.gpu_end;
14413 let b = fed.len();
14414 let mut accepted = 1usize;
14415 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14416 accepted += 1;
14417 }
14418 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14423 accepted = 1;
14424 }
14425 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14426 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14427 }
14428 let t_fin = std::time::Instant::now();
14429 if !crate::dsv4::dsv4_spec_finish(
14430 g,
14431 layers,
14432 &cfg,
14433 st,
14434 txn,
14435 accepted,
14436 &fed,
14437 &self.inv_freq,
14438 self.pool.as_deref(),
14439 ) {
14440 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14441 return None;
14442 }
14443 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14444 eprintln!(
14445 "finish(k={accepted}): {:.1} мс",
14446 t_fin.elapsed().as_secs_f64() * 1e3
14447 );
14448 }
14449 *accepted_ctr += accepted - 1;
14450 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14455 let dev_caps: Vec<usize> = targets
14460 .iter()
14461 .copied()
14462 .filter(|&t| t < spec_gpu_end)
14463 .collect();
14464 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14465 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14466 return None;
14467 }
14468 for t in 0..accepted {
14469 let tip = t + 1 == accepted;
14470 for (slot, &tl) in targets.iter().enumerate() {
14471 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14472 let lo = (di * b + t) * hc * dim;
14473 crate::dsv4::dspark_capture(
14474 &caps_all[lo..lo + hc * dim],
14475 &cfg,
14476 slot,
14477 &mut ds.main_hidden,
14478 );
14479 } else if tip
14480 && crate::dsv4::dspark_peek_slot(slot, dim, {
14481 let lo = slot * dim;
14482 &mut ds.main_hidden[lo..lo + dim]
14483 })
14484 {
14485 } else {
14490 crate::dsv4::dspark_capture(
14494 &walked[t * hc * dim..(t + 1) * hc * dim],
14495 &cfg,
14496 slot,
14497 &mut ds.main_hidden,
14498 );
14499 }
14500 }
14501 crate::dsv4::dspark_ring_append(
14502 g,
14503 &self.dsv4_mtp,
14504 &cfg,
14505 ds,
14506 next_pos + t,
14507 self.pool.as_deref(),
14508 );
14509 }
14510 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14511 self.graph_logits = Some(row);
14512 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14517 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14518 crate::dsv4::pick_tally_arm();
14519 }
14520 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14521 eprintln!(
14522 "spec_step total {:.1} мс (k={accepted})",
14523 t_all.elapsed().as_secs_f64() * 1e3
14524 );
14525 }
14526 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14527 }
14528
14529 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14530 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14531 return;
14532 }
14533 let trunk_now = crate::dsv4::pick_tally_take();
14535 crate::dsv4::trunk_freq_note(&trunk_now);
14536 if !trunk_now.is_empty() {
14537 self.dspark_trunk_picks.push(trunk_now);
14538 let keep = crate::dsv4::dspark_block();
14539 if self.dspark_trunk_picks.len() > keep {
14540 self.dspark_trunk_picks.remove(0);
14541 }
14542 }
14543 for p in std::mem::take(&mut self.dspark_pending) {
14546 let Some(i) = position.checked_sub(p.0 + 1) else {
14547 continue;
14548 };
14549 let mut p = p;
14550 if i < p.1.len() {
14551 if p.2 && p.1[i] == token_id {
14552 p.3 = i + 1;
14553 } else {
14554 p.2 = false;
14555 }
14556 if i + 1 < p.1.len() {
14557 self.dspark_pending.push(p);
14558 continue;
14559 }
14560 }
14561 self.dspark_hist.push(p.3);
14562 self.dspark_real.push(token_id);
14563 }
14564 let Some(b) = &mut self.dsv4 else { return };
14565 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14566 let n_layers = layers.len();
14567 if self.dspark.is_none() {
14568 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14569 if t.is_empty() {
14570 return;
14571 }
14572 eprintln!(
14573 "DSpark: захват со слоёв {t:?}, блок {}",
14574 crate::dsv4::dspark_block()
14575 );
14576 crate::dsv4::dspark_arm(&t, cfg.dim);
14577 self.dspark = Some(crate::dsv4::DsparkState::new(
14578 self.dsv4_mtp.len(),
14579 &cfg,
14580 t.len(),
14581 ));
14582 }
14583 let ds = self.dspark.as_mut().unwrap();
14584 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14585 return; }
14587 let mut conf = Vec::new();
14588 crate::dsv4::pick_tally_arm();
14589 let draft_started = std::time::Instant::now();
14594 #[cfg(feature = "gpu")]
14595 let gpu_draft = crate::dsv4::dspark_gpu_on();
14596 #[cfg(not(feature = "gpu"))]
14597 let gpu_draft = false;
14598 let props = if gpu_draft {
14599 #[cfg(feature = "gpu")]
14600 {
14601 let kv_id = b.3.kv_id;
14602 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14603 Some(pk) => crate::dsv4::dspark_draft_gpu(
14604 g,
14605 &self.dsv4_mtp,
14606 &cfg,
14607 ds,
14608 pk,
14609 kv_id,
14610 token_id,
14611 position,
14612 self.pool.as_deref(),
14613 &mut conf,
14614 ),
14615 None => Vec::new(),
14616 }
14617 }
14618 #[cfg(not(feature = "gpu"))]
14619 Vec::new()
14620 } else {
14621 crate::gpu::cpu_scope(|| {
14622 crate::dsv4::dspark_draft(
14623 g,
14624 &self.dsv4_mtp,
14625 &cfg,
14626 ds,
14627 token_id,
14628 position,
14629 self.pool.as_deref(),
14630 &mut conf,
14631 )
14632 })
14633 };
14634 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14635 let draft_picks = crate::dsv4::pick_tally_take();
14636 crate::dsv4::dspark_freq_note(&draft_picks);
14637 crate::dsv4::pick_tally_arm();
14640 if !props.is_empty() {
14641 let (tu, tt) = {
14645 let flat: Vec<(usize, Vec<usize>)> = self
14646 .dspark_trunk_picks
14647 .iter()
14648 .flat_map(|v| v.iter().cloned())
14649 .collect();
14650 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14652 std::collections::HashMap::new();
14653 for (li, picks) in flat {
14654 per.entry(li).or_default().extend(picks);
14655 }
14656 let n = per.len().max(1);
14657 let mut u = 0usize;
14658 let mut t = 0usize;
14659 for (_, v) in per {
14660 t += v.len();
14661 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14662 }
14663 (u / n, t / n)
14664 };
14665 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14666 self.dspark_exp.push((tu, tt, du, dt));
14667 self.dspark_pending.push((position, props, true, 0));
14668 }
14669 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14670 let n = self.dspark_hist.len() as f32;
14671 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14672 let block = crate::dsv4::dspark_block();
14673 let mut at = vec![0usize; block + 1];
14674 for &k in &self.dspark_hist {
14675 at[k] += 1;
14676 }
14677 let mut surv = Vec::with_capacity(block);
14679 for i in 1..=block {
14680 let k = at[i..].iter().sum::<usize>() as f32 / n;
14681 surv.push(format!("{k:.2}"));
14682 }
14683 let distinct = self
14684 .dspark_real
14685 .iter()
14686 .collect::<std::collections::HashSet<_>>()
14687 .len();
14688 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14689 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14690 });
14691 let m = self.dspark_exp.len().max(1);
14692 eprintln!(
14693 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14694 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14695 self.dspark_hist.len(),
14696 mean + 1.0,
14697 surv.join(" ")
14698 );
14699 eprintln!(
14700 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14701 эксперты ствол {}/{} на слой за {block} токенов, \
14702 черновик {}/{} за блок, draft {:.2} мс/блок",
14703 self.dspark_real.len(),
14704 tu / m,
14705 tt / m,
14706 du / m,
14707 dt / m,
14708 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14709 );
14710 }
14711 }
14712
14713 fn forward_layers_upto(
14714 &mut self,
14715 hidden: &[f32],
14716 position: usize,
14717 task_mask: Option<&TaskMask>,
14718 upto: Option<usize>,
14719 ) -> Vec<f32> {
14720 if let Some(plan) = self.gpu_plan.clone() {
14726 if upto.is_none() && plan.len() > 1 {
14727 let mut h = hidden.to_vec();
14728 for &(dev, from, upto_incl) in plan.iter() {
14729 h = crate::gpu::with_device(dev, || {
14730 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14731 });
14732 }
14733 return h;
14734 }
14735 }
14736 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14737 }
14738
14739 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14744 self.set_gpu_plan_at(devices, None)
14745 }
14746
14747 pub fn set_gpu_plan_at(
14751 &mut self,
14752 devices: Option<&[usize]>,
14753 at: Option<usize>,
14754 ) -> Result<(), String> {
14755 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14756 self.gpu_plan = None;
14757 return Ok(());
14758 };
14759 self.split_supported()?;
14760 let n = self.num_layers;
14761 if devs.len() > n {
14762 return Err(format!("{} devices for {n} layers", devs.len()));
14763 }
14764 if let Some(k) = at {
14765 if k == 0 || k >= n {
14766 return Err(format!("split at {k}: the model has {n} layers"));
14767 }
14768 if devs.len() == 2 {
14769 self.gpu_plan = Some(std::sync::Arc::new(vec![
14770 (devs[0], 0, k - 1),
14771 (devs[1], k, n - 1),
14772 ]));
14773 return Ok(());
14774 }
14775 return Err(format!(
14776 "an explicit split point takes exactly 2 devices, got {}",
14777 devs.len()
14778 ));
14779 }
14780 let per = n.div_ceil(devs.len());
14781 let mut plan = Vec::with_capacity(devs.len());
14782 let mut from = 0usize;
14783 for &d in devs {
14784 if from >= n {
14785 break;
14786 }
14787 let upto = (from + per - 1).min(n - 1);
14788 plan.push((d, from, upto));
14789 from = upto + 1;
14790 }
14791 self.gpu_plan = Some(std::sync::Arc::new(plan));
14792 Ok(())
14793 }
14794
14795 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
14797 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
14798 }
14799
14800 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
14806 if let Some(x) = t.as_f32() {
14807 return x.to_vec();
14808 }
14809 let mut out = vec![0.0; t.rows() * t.cols()];
14810 for r in 0..t.rows() {
14811 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
14812 }
14813 out
14814 }
14815
14816 fn embryo_resident_eligible(&self) -> bool {
14817 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
14820 || self.num_layers != self.physical_layers
14821 || self.loop_final_norm
14822 || self.weights.layers.len() != self.num_layers
14823 || self.head_clusters.is_none()
14824 || self.final_softcap.is_some()
14825 || self.logit_multiplier.is_some()
14826 || self.attn_softcap != 0.0
14827 || self.mtp.is_some()
14828 || self.g3n.is_some()
14829 || self.dsv4.is_some()
14830 || self.dsv41.is_some()
14831 || self.qwen4_exp.is_some()
14832 || self.dyn_router.is_some()
14839 || self.dyn_phi_layer.is_some()
14840 || self.dyn_blend_loaded
14841 || self.o1_cfg.is_some()
14842 || self.swa.is_some()
14843 || self.sliding_layers.is_some()
14844 || self.global_attn.is_some()
14845 || self.attention_heads_per_layer.is_some()
14846 || self.attn_v_norm
14847 || self
14848 .kv_cache
14849 .layers
14850 .iter()
14851 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
14852 || self.rope_scale != 1.0
14853 || self.rope_scale_local != 1.0
14854 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
14855 || self.hidden_size == 0
14856 || self.hidden_size > 1024
14857 || self.intermediate_size > 1024
14858 || self.num_heads == 0
14859 || self.num_kv_heads == 0
14860 || self.num_heads % self.num_kv_heads != 0
14861 || self.num_heads.saturating_mul(self.head_dim) > 1024
14862 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
14863 || self.vocab_size == 0
14864 || self.kv_cache.max_seq_len == 0
14865 || self.rotary_dim == 0
14866 || self.rotary_dim > self.head_dim
14867 || self.rotary_dim % 2 != 0
14868 || self.inv_freq.len() < self.rotary_dim / 2
14869 {
14870 return false;
14871 }
14872 if self.weights.lm_head.as_f32().is_none()
14883 || self.weights.embed_tokens.as_f32().is_none()
14884 || self.weights.lm_head.rows() < self.vocab_size
14885 || self.weights.lm_head.cols() != self.hidden_size
14886 || self.weights.embed_tokens.rows() < self.vocab_size
14887 || self.weights.embed_tokens.cols() != self.hidden_size
14888 || self.weights.final_norm.len() != self.hidden_size
14889 {
14890 return false;
14891 }
14892 if let Some(cfg) = self.vmf_cfg {
14893 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14894 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14895 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14896 || cfg.state_len() == 0
14897 {
14898 return false;
14899 }
14900 }
14901 if let Some(g) = self.gdn_cfg {
14902 if g.num_v_heads == 0
14907 || g.num_k_heads == 0
14908 || g.num_v_heads % g.num_k_heads != 0
14909 || g.key_head_dim == 0
14910 || g.key_head_dim > 128
14911 || g.value_head_dim == 0
14912 || g.value_head_dim > 256
14913 || g.value_head_dim % 4 != 0
14914 || g.conv_kernel == 0
14915 || g.num_v_heads > 512
14916 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14917 || g.conv_dim() > 2048
14918 || g.conv_dim() % 4 != 0
14919 || g.hidden_size != self.hidden_size
14920 || g.output_gate_sigmoid
14921 || g.rms_eps != self.rms_eps
14922 || g.state_len() == 0
14923 {
14924 return false;
14925 }
14926 }
14927 let mut full_seen = false;
14928 for lw in &self.weights.layers {
14929 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14930 return false;
14931 }
14932 match &lw.attn {
14933 AttnKind::LinearGdn(w) => {
14934 let Some(g) = self.gdn_cfg else {
14935 return false;
14936 };
14937 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14938 if w.in_proj_qkv.rows() != g.conv_dim()
14939 || w.in_proj_qkv.cols() != self.hidden_size
14940 || w.in_proj_qkv.as_f32().is_none()
14941 || w.in_proj_z.rows() != nv * dv
14942 || w.in_proj_z.cols() != self.hidden_size
14943 || w.in_proj_z.as_f32().is_none()
14944 || w.in_proj_a.rows() != nv
14945 || w.in_proj_a.cols() != self.hidden_size
14946 || w.in_proj_a.as_f32().is_none()
14947 || w.in_proj_b.rows() != nv
14948 || w.in_proj_b.cols() != self.hidden_size
14949 || w.in_proj_b.as_f32().is_none()
14950 || w.conv1d.len() != g.conv_dim() * kk
14951 || w.a_log.len() != nv
14952 || w.dt_bias.len() != nv
14953 || w.norm.len() != dv
14954 || w.out_proj.rows() != self.hidden_size
14955 || w.out_proj.cols() != nv * dv
14956 || w.out_proj.as_f32().is_none()
14957 {
14958 return false;
14959 }
14960 }
14961 AttnKind::Linear(w) => {
14962 let Some(cfg) = self.vmf_cfg else {
14963 return false;
14964 };
14965 if w.thq.rows() != cfg.num_heads * cfg.nphase
14966 || w.thq.cols() != self.hidden_size
14967 || w.thq.as_f32().is_none()
14968 || w.thk.rows() != cfg.num_heads * cfg.nphase
14969 || w.thk.cols() != self.hidden_size
14970 || w.thk.as_f32().is_none()
14971 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14972 || w.v_proj.cols() != self.hidden_size
14973 || w.v_proj.as_f32().is_none()
14974 || w.out_proj.rows() != self.hidden_size
14975 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14976 || w.out_proj.as_f32().is_none()
14977 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14978 {
14979 return false;
14980 }
14981 if let Some((kg, kb)) = &w.k_gate {
14982 if kg.rows() != cfg.num_heads
14983 || kg.cols() != self.hidden_size
14984 || kg.as_f32().is_none()
14985 || kb.len() != cfg.num_heads
14986 {
14987 return false;
14988 }
14989 }
14990 if let Some(conv) = &w.conv {
14991 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
14992 return false;
14993 }
14994 }
14995 }
14996 AttnKind::Full {
14997 wq,
14998 wk,
14999 wv,
15000 wo,
15001 q_norm,
15002 k_norm,
15003 output_gate,
15004 softplus_gate,
15005 bias,
15006 } => {
15007 if full_seen
15008 || q_norm.is_some()
15009 || k_norm.is_some()
15010 || *output_gate
15011 || softplus_gate.is_some()
15012 || bias.is_some()
15013 || wq.as_f32().is_none()
15014 || wk.as_f32().is_none()
15015 || wv.as_f32().is_none()
15016 || wo.as_f32().is_none()
15017 || wq.rows() != self.num_heads * self.head_dim
15018 || wk.rows() != self.num_kv_heads * self.head_dim
15019 || wv.rows() != self.num_kv_heads * self.head_dim
15020 || wq.cols() != self.hidden_size
15021 || wk.cols() != self.hidden_size
15022 || wv.cols() != self.hidden_size
15023 || wo.rows() != self.hidden_size
15024 || wo.cols() != self.num_heads * self.head_dim
15025 {
15026 return false;
15027 }
15028 full_seen = true;
15029 }
15030 AttnKind::Bounded(w) => {
15031 let Some(ac) = self.anchor_core.as_ref() else {
15034 return false;
15035 };
15036 if self.bounded_rope.is_none()
15037 || w.window != ac.window
15038 || w.sink != ac.sink
15039 || w.window == 0
15040 || w.window + w.sink > 256
15041 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15042 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15043 || w.wq.as_f32().is_none()
15044 || w.wk.as_f32().is_none()
15045 || w.wv.as_f32().is_none()
15046 || w.wo.as_f32().is_none()
15047 || w.wq.rows() != self.num_heads * self.head_dim
15048 || w.wk.rows() != self.num_kv_heads * self.head_dim
15049 || w.wv.rows() != self.num_kv_heads * self.head_dim
15050 || w.wq.cols() != self.hidden_size
15051 || w.wk.cols() != self.hidden_size
15052 || w.wv.cols() != self.hidden_size
15053 || w.wo.rows() != self.hidden_size
15054 || w.wo.cols() != self.num_heads * self.head_dim
15055 {
15056 return false;
15057 }
15058 }
15059 _ => return false,
15060 }
15061 match &lw.ffn {
15062 FfnKind::Dense(d) => {
15063 if d.act != Act::Silu
15064 || !d.segs.is_empty()
15065 || d.gate_proj.as_f32().is_none()
15066 || d.up_proj.as_f32().is_none()
15067 || d.down_proj.as_f32().is_none()
15068 || d.gate_proj.rows() != self.intermediate_size
15069 || d.gate_proj.cols() != self.hidden_size
15070 || d.up_proj.rows() != self.intermediate_size
15071 || d.up_proj.cols() != self.hidden_size
15072 || d.down_proj.rows() != self.hidden_size
15073 || d.down_proj.cols() != self.intermediate_size
15074 {
15075 return false;
15076 }
15077 }
15078 FfnKind::Moe(m) => {
15079 if m.resonance.is_none()
15080 || m.top_k != 1
15081 || m.router_sigmoid
15082 || !m.norm_topk_prob
15083 || m.expert_bias.is_some()
15084 || m.routed_scaling != 1.0
15085 || m.route_tau.is_some()
15086 || m.shared.is_none()
15087 || m.mask.is_some()
15088 || m.per_expert_scale.is_some()
15089 || m.router_input_norm
15090 || m.experts.is_empty()
15091 || m.experts.len() > 8
15092 {
15093 return false;
15094 }
15095 let r = m.resonance.as_ref().unwrap();
15096 if r.mu.len() != m.experts.len() * self.hidden_size
15097 || r.bias.len() != m.experts.len()
15098 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15099 || r.k > 128
15100 {
15101 return false;
15102 }
15103 let Some((shared, gate)) = &m.shared else {
15104 return false;
15105 };
15106 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15107 return false;
15108 }
15109 if shared.gate_proj.as_f32().is_none()
15110 || shared.up_proj.as_f32().is_none()
15111 || shared.down_proj.as_f32().is_none()
15112 || shared.gate_proj.rows() != self.intermediate_size
15113 || shared.gate_proj.cols() != self.hidden_size
15114 || shared.up_proj.rows() != self.intermediate_size
15115 || shared.up_proj.cols() != self.hidden_size
15116 || shared.down_proj.rows() != self.hidden_size
15117 || shared.down_proj.cols() != self.intermediate_size
15118 {
15119 return false;
15120 }
15121 for e in &m.experts {
15122 if e.act != Act::Silu
15123 || !e.segs.is_empty()
15124 || e.gate_proj.as_f32().is_none()
15125 || e.up_proj.as_f32().is_none()
15126 || e.down_proj.as_f32().is_none()
15127 || e.gate_proj.rows() != self.intermediate_size
15128 || e.gate_proj.cols() != self.hidden_size
15129 || e.up_proj.rows() != self.intermediate_size
15130 || e.up_proj.cols() != self.hidden_size
15131 || e.down_proj.rows() != self.hidden_size
15132 || e.down_proj.cols() != self.intermediate_size
15133 {
15134 return false;
15135 }
15136 }
15137 }
15138 FfnKind::DenseMoe(_) => return false,
15139 }
15140 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15141 return false;
15142 }
15143 }
15144 if full_seen && self.anchor_core.is_some() {
15145 return false;
15146 }
15147 full_seen || self.num_layers > 0
15148 }
15149
15150 fn embryo_resident_wanted(&self) -> bool {
15155 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15156 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15157 && matches!(
15158 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15159 Ok("1") | Ok("parallel")
15160 )
15161 && crate::gpu::enabled_here()
15162 && !self.graph_refused()
15163 && self.embryo_resident_eligible()
15164 }
15165
15166 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15174 if ids.len() < 2
15175 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15176 || !self.embryo_resident_wanted()
15177 {
15178 return None;
15179 }
15180 let model = self.ensure_embryo_graph()?;
15181 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15182 .ok()
15183 .and_then(|v| v.parse::<usize>().ok())
15184 .filter(|&v| v >= 1)
15185 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15186 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15187 let hs = self.hidden_size;
15188 let n = ids.len();
15189 let mut pos = start;
15190 let mut last = None;
15191 let mut rows = Vec::with_capacity(cmax * hs);
15192 while pos < n {
15193 let end = (pos + cmax).min(n);
15194 rows.clear();
15195 for &id in &ids[pos..end] {
15196 rows.extend_from_slice(&self.embed_single(id));
15197 }
15198 let mut lg = Vec::new();
15199 if !crate::gpu::forward_embryo_graph_chunk(
15200 &model,
15201 self.graph_kv_id,
15202 &rows,
15203 pos,
15204 end - pos,
15205 &mut lg,
15206 ) {
15207 if pos == start {
15208 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15209 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15210 }
15211 return None;
15212 }
15213 self.kv_cache.clear();
15217 self.clear_history();
15218 crate::gpu::graph_kv_reset(self.graph_kv_id);
15219 panic!(
15220 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15221 );
15222 }
15223 last = Some(lg);
15224 pos = end;
15225 }
15226 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15227 eprintln!(
15228 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15229 n - start,
15230 (n - start).div_ceil(cmax)
15231 );
15232 }
15233 last
15234 }
15235
15236 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15237 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15238 const UMAX: u32 = u32::MAX;
15239 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15240 const REC: usize = 64;
15241 struct Pack {
15242 data: Vec<f32>,
15243 }
15244 impl Pack {
15245 fn put(&mut self, x: &[f32]) -> u32 {
15246 if x.is_empty() {
15247 return u32::MAX;
15248 }
15249 let off = self.data.len();
15250 self.data.extend_from_slice(x);
15251 off as u32
15252 }
15253 }
15254 let vmf = self.vmf_cfg;
15258 let gdn = self.gdn_cfg;
15259 let mut pack = Pack { data: Vec::new() };
15260 let mut meta = vec![0u32; HEADER];
15261 meta[0] = self.hidden_size as u32;
15262 meta[1] = self.intermediate_size as u32;
15263 meta[2] = self.vocab_size as u32;
15264 meta[3] = self.num_layers as u32;
15265 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15266 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15267 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15268 if let Some(g) = gdn {
15269 meta[24] = g.num_v_heads as u32;
15270 meta[25] = g.num_k_heads as u32;
15271 meta[26] = g.key_head_dim as u32;
15272 meta[27] = g.value_head_dim as u32;
15273 meta[28] = g.conv_kernel as u32;
15274 meta[29] = g.conv_dim() as u32;
15275 }
15276 meta[7] = self.num_heads as u32;
15277 meta[8] = self.num_kv_heads as u32;
15278 meta[9] = self.head_dim as u32;
15279 meta[10] = self.kv_cache.max_seq_len as u32;
15280 let clusters = self.head_clusters.as_ref().unwrap();
15281 let cluster_count = clusters.len() / self.hidden_size;
15282 if clusters.len() % self.hidden_size != 0
15283 || cluster_count == 0
15284 || cluster_count > 1024
15285 || self.vocab_size % cluster_count != 0
15286 || self.weights.lm_head.rows() < self.vocab_size
15287 || self.weights.final_norm.len() != self.hidden_size
15288 {
15289 return None;
15290 }
15291 meta[11] = cluster_count as u32;
15292 meta[12] = (self.vocab_size / cluster_count) as u32;
15293 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15294 meta[16] = self.rotary_dim as u32;
15295 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15296 meta[19] = (self.rms_eps as f32).to_bits();
15297 let max_conv = self
15298 .weights
15299 .layers
15300 .iter()
15301 .filter_map(|lw| match &lw.attn {
15302 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
15303 _ => None,
15304 })
15305 .max()
15306 .unwrap_or(1);
15307 let phase_stride = vmf
15313 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15314 .unwrap_or(0);
15315 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15316 let state_stride = phase_stride.max(gdn_stride);
15317 let bounded = self.anchor_core.clone();
15321 let (anchor_window, anchor_sink) = bounded
15322 .as_ref()
15323 .map(|ac| (ac.window, ac.sink))
15324 .unwrap_or((0, 0));
15325 let kv_stride = if bounded.is_some() {
15326 2usize
15327 .saturating_mul(self.num_kv_heads)
15328 .saturating_mul(anchor_window)
15329 .saturating_mul(self.head_dim)
15330 } else {
15331 2usize
15332 .saturating_mul(self.num_kv_heads)
15333 .saturating_mul(self.kv_cache.max_seq_len)
15334 .saturating_mul(self.head_dim)
15335 };
15336 meta[14] = state_stride as u32;
15337 meta[15] = kv_stride as u32;
15338 meta[18] = anchor_window as u32;
15339 meta[20] = anchor_sink as u32;
15340 meta[21] = match &self.bounded_rope {
15341 Some(rope) => {
15342 let off = pack.put(&rope.cos);
15344 let _ = pack.put(&rope.sin);
15345 off
15346 }
15347 None => UMAX,
15348 };
15349 let mut full_seen = false;
15350 let mut bounded_seen = 0usize;
15351 let mut phase_seen = 0usize;
15355 let mut gdn_seen = 0usize;
15356 for (li, lw) in self.weights.layers.iter().enumerate() {
15357 let base = meta.len();
15358 meta.resize(base + REC, UMAX);
15359 meta[base] = match &lw.attn {
15360 AttnKind::Linear(w) if w.phase_delta => 1,
15361 AttnKind::Linear(_) => 0,
15362 AttnKind::Full { .. } => 2,
15363 AttnKind::Bounded(_) => 3,
15364 AttnKind::LinearGdn(_) => 4,
15365 _ => UMAX,
15366 };
15367 meta[base + 1] = pack.put(&lw.input_norm);
15368 meta[base + 2] = pack.put(&lw.post_norm);
15369 meta[base + 25] = match &lw.attn {
15370 AttnKind::Linear(_) => {
15371 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15372 phase_seen += 1;
15373 off
15374 }
15375 AttnKind::LinearGdn(_) => {
15376 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15377 gdn_seen += 1;
15378 off
15379 }
15380 _ => UMAX,
15381 };
15382 match &lw.attn {
15383 AttnKind::LinearGdn(w) => {
15384 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15387 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15388 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15389 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15390 meta[base + 60] = pack.put(&w.conv1d);
15391 meta[base + 61] = pack.put(&w.a_log);
15392 meta[base + 62] = pack.put(&w.dt_bias);
15393 meta[base + 63] = pack.put(&w.norm);
15394 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15395 meta[base + 24] = 0;
15396 }
15397 AttnKind::Linear(w) => {
15398 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15399 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15400 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15401 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15402 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15403 meta[base + 7] = pack.put(&decay);
15404 if let Some((kg, kb)) = &w.k_gate {
15405 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15406 meta[base + 9] = pack.put(kb);
15407 }
15408 if let Some(conv) = &w.conv {
15409 meta[base + 10] = pack.put(conv);
15410 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15411 } else {
15412 meta[base + 24] = 0;
15413 }
15414 }
15415 AttnKind::Full { wq, wk, wv, wo, .. } => {
15416 full_seen = true;
15417 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15418 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15419 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15420 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15421 meta[base + 26] = (li * kv_stride) as u32;
15422 }
15423 AttnKind::Bounded(w) => {
15424 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15425 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15426 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15427 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15428 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15430 meta[base + 27] = pack.put(&w.sink_k);
15431 meta[base + 28] = pack.put(&w.sink_v);
15432 bounded_seen += 1;
15433 }
15434 _ => return None,
15435 }
15436 match &lw.ffn {
15437 FfnKind::Dense(d) => {
15438 meta[base + 15] = 0;
15439 meta[base + 16] = 0;
15440 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15441 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15442 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15443 }
15444 FfnKind::Moe(m) => {
15445 let r = m.resonance.as_ref().unwrap();
15446 let (shared, _) = m.shared.as_ref().unwrap();
15447 meta[base + 15] = 1;
15448 meta[base + 16] = m.experts.len() as u32;
15449 meta[base + 17] = pack.put(&r.mu);
15450 meta[base + 18] = pack.put(&r.u);
15451 meta[base + 19] = pack.put(&r.bias);
15452 meta[base + 20] = r.k as u32;
15453 let mut shell = r.effective_shell(m.experts.len());
15462 shell.push(f32::NEG_INFINITY);
15463 meta[base + 30] = pack.put(&shell);
15464 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15465 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15466 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15467 for (e, ex) in m.experts.iter().enumerate() {
15468 meta[base + 32 + e * 3] =
15469 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15470 meta[base + 33 + e * 3] =
15471 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15472 meta[base + 34 + e * 3] =
15473 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15474 }
15475 }
15476 FfnKind::DenseMoe(_) => return None,
15477 }
15478 }
15479 if !full_seen && self.num_layers == 0 {
15480 return None;
15481 }
15482 let id = {
15483 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15484 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15485 };
15486 let model = crate::gpu::EmbryoGraphModel {
15487 id,
15488 hidden: self.hidden_size,
15489 intermediate: self.intermediate_size,
15490 vocab: self.vocab_size,
15491 layers: self.num_layers,
15492 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15493 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15494 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15495 anchor_q_heads: self.num_heads,
15496 anchor_kv_heads: self.num_kv_heads,
15497 anchor_head_dim: self.head_dim,
15498 rotary_dim: self.rotary_dim,
15499 max_seq: self.kv_cache.max_seq_len,
15500 cluster_count,
15501 cluster_size: self.vocab_size / cluster_count,
15502 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15503 state_stride,
15504 kv_stride,
15505 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15506 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15507 weights: pack.data,
15508 meta,
15509 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15510 clusters: clusters.as_ref().clone(),
15511 final_norm: self.weights.final_norm.clone(),
15512 inv_freq: self.inv_freq.as_ref().clone(),
15513 bounded: bounded.is_some(),
15514 kv_layers: if bounded.is_some() {
15515 bounded_seen
15516 } else {
15517 self.num_layers
15518 },
15519 state_layers: phase_seen + gdn_seen,
15520 anchor_window,
15521 anchor_sink,
15522 phase_layers: phase_seen,
15523 gdn_layers: gdn_seen,
15524 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15525 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15526 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15527 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15528 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15529 };
15530 self.embryo_graph = Some(std::sync::Arc::new(model));
15531 }
15532 self.embryo_graph.clone()
15533 }
15534
15535 fn forward_layers_span(
15536 &mut self,
15537 hidden: &[f32],
15538 position: usize,
15539 task_mask: Option<&TaskMask>,
15540 from: usize,
15541 upto: Option<usize>,
15542 ) -> Vec<f32> {
15543 #[cfg(target_os = "macos")]
15544 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
15545 position,
15546 from,
15547 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
15548 );
15549 debug_assert!(
15550 from == 0
15551 || (self.dsv4.is_none()
15552 && self.dsv41.is_none()
15553 && self.qwen4_exp.is_none()
15554 && self.g3n.is_none())
15555 );
15556 #[cfg(target_os = "macos")]
15562 if !crate::gpu_metal::wait_replay() {
15563 self.fail_metal_graph("the pending async replay failed before a plain forward");
15564 return vec![0.0; self.hidden_size];
15565 }
15566 if let Some(b) = &mut self.qwen4_exp {
15567 let _ = (task_mask, upto);
15568 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15569 let mut logits = Vec::new();
15570 crate::qwen4_exp::forward_token(
15571 &b.0,
15572 &b.1,
15573 &b.2,
15574 &mut b.3,
15575 token_id,
15576 position,
15577 &self.inv_freq,
15578 self.pool.as_deref(),
15579 &mut logits,
15580 true,
15581 );
15582 self.graph_logits = Some(logits);
15583 return vec![0.0; self.hidden_size];
15584 }
15585 if let Some(b) = &mut self.dsv4 {
15591 let _ = (task_mask, upto);
15592 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15593 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15594 st.pos = position;
15595 let mut logits = Vec::new();
15596 crate::dsv4::forward_token(
15597 g,
15598 layers,
15599 &cfg,
15600 st,
15601 token_id,
15602 &self.inv_freq,
15603 self.pool.as_deref(),
15604 &mut logits,
15605 );
15606 self.graph_logits = Some(logits);
15607 self.dspark_probe(position, token_id);
15608 return vec![0.0; self.hidden_size];
15611 }
15612 if let Some(b) = &mut self.dsv41 {
15614 let _ = (task_mask, upto);
15615 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15616 let mut logits = Vec::new();
15617 crate::dsv41::forward_token(
15618 &b.0,
15619 &b.1,
15620 &b.2,
15621 &mut b.3,
15622 token_id,
15623 position,
15624 self.pool.as_deref(),
15625 &mut logits,
15626 );
15627 self.graph_logits = Some(logits);
15628 return vec![0.0; self.hidden_size];
15629 }
15630 if let Some(b) = &self.g3n {
15633 let _ = (task_mask, upto);
15634 return crate::g3n::g3n_forward(
15635 &b.0,
15636 &b.1,
15637 hidden,
15638 position,
15639 &mut self.kv_cache.layers,
15640 self.num_heads,
15641 self.num_kv_heads,
15642 self.head_dim,
15643 self.pool.as_deref(),
15644 );
15645 }
15646 if from == 0
15653 && upto.is_none()
15654 && task_mask.is_none()
15655 && self.anchor_core.is_some()
15656 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15657 {
15658 static ONCE: std::sync::Once = std::sync::Once::new();
15659 ONCE.call_once(|| {
15660 eprintln!(
15661 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15662 unsupported={} eligible={}",
15663 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15664 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15665 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15666 crate::gpu::enabled_here(),
15667 self.graph_refused(),
15668 self.embryo_resident_eligible(),
15669 );
15670 });
15671 }
15672 if from == 0
15673 && upto.is_none()
15674 && task_mask.is_none()
15675 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15679 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15680 && matches!(
15684 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15685 Ok("1") | Ok("parallel")
15686 )
15687 && crate::gpu::enabled_here()
15688 && !self.graph_refused()
15689 && (position == 0 || self.device_sequence_position().is_some())
15695 && self.embryo_resident_eligible()
15696 && let Some(model) = self.ensure_embryo_graph()
15697 {
15698 let mut lg = Vec::new();
15699 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15700 {
15701 self.graph_logits = Some(lg);
15702 return vec![0.0; self.hidden_size];
15703 }
15704 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15705 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15706 }
15707 self.mark_graph_refused();
15713 if position != 0 {
15714 self.kv_cache.clear();
15718 self.clear_history();
15719 crate::gpu::graph_kv_reset(self.graph_kv_id);
15720 panic!(
15721 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15722 );
15723 }
15724 }
15725 let mut h = hidden.to_vec();
15726 self.mimo_moe_prepare();
15729 let _mimo_q8 = self.mimo_moe.is_on()
15730 .then(crate::qtensor::enter_full_gpu_q8_scope);
15731 let (nh, _nkv, _hd, hs, _rd, eps) = (
15734 self.num_heads,
15735 self.num_kv_heads,
15736 self.head_dim,
15737 self.hidden_size,
15738 self.rotary_dim,
15739 self.rms_eps,
15740 );
15741 let pool = self.pool.clone();
15742 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15754 let graph_on = match graph_env.as_deref() {
15755 Some("0") => false,
15756 Some("prefill") => false, Some(_) => true,
15758 None => crate::gpu::wgpu_graph_default(),
15764 };
15765 let graph_trusted =
15766 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15767 let race_eligible = graph_on
15768 && upto.is_none()
15769 && task_mask.is_none()
15770 && from == 0
15771 && !self.graph_refused();
15772 let mut tail_start = 0usize;
15773 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15774 let t_graph = std::time::Instant::now();
15775 let mut lg = Vec::new();
15776 let mut gl = 0usize;
15777 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
15778 let declined = built.is_none();
15779 let built = match built {
15780 Some(Ok(hh)) => Some(hh),
15781 Some(Err(())) => {
15782 self.clear_sequence_state();
15786 self.graph_failed
15787 .store(true, std::sync::atomic::Ordering::Relaxed);
15788 self.cancel
15789 .store(true, std::sync::atomic::Ordering::Relaxed);
15790 tracing::error!("token graph failed after admission; sequence state cleared");
15791 return vec![0.0; self.hidden_size];
15792 }
15793 None => None,
15794 };
15795 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
15800 self.mark_graph_refused();
15801 }
15802 graph_note(built.is_some(), gl, self.num_layers);
15803 if let Some(hh) = built {
15804 let dur = t_graph.elapsed();
15805 if std::env::var("CMF_GRAPH_PROF").is_ok() {
15806 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
15807 }
15808 if gl > 0 && gl < self.num_layers {
15809 h = hh;
15815 tail_start = gl;
15816 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
15817 if !graph_trusted {
15818 crate::gpu::graph_race_record(true, dur);
15819 }
15820 if !lg.is_empty() {
15821 lg.resize(self.vocab_size, 0.0);
15824 if let Some(c) = self.final_softcap {
15825 softcap_in_place(&mut lg, c, self.pool.as_deref());
15826 }
15827 self.graph_logits = Some(lg);
15828 }
15829 return hh;
15830 }
15831 }
15837 }
15838 let span = from > 0 || upto.is_some();
15862 if span && graph_on && task_mask.is_none() && graph_trusted {
15863 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
15864 let mut lg = Vec::new();
15865 let mut gl = 0usize;
15866 let span_res =
15867 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
15868 let span_res = match span_res {
15869 Some(Ok(hh)) => Some(hh),
15870 Some(Err(())) => {
15871 self.clear_sequence_state();
15872 self.graph_failed
15873 .store(true, std::sync::atomic::Ordering::Relaxed);
15874 self.cancel
15875 .store(true, std::sync::atomic::Ordering::Relaxed);
15876 tracing::error!(
15877 "span token graph failed after admission; sequence state cleared"
15878 );
15879 return vec![0.0; self.hidden_size];
15880 }
15881 None => None,
15882 };
15883 graph_note(span_res.is_some(), gl, upto_excl - from);
15884 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15885 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15889 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15890 eprintln!(
15891 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15892 upto_excl - from,
15893 span_res.is_some()
15894 );
15895 }
15896 }
15897 if let Some(hh) = span_res {
15898 if gl == upto_excl - from {
15899 if !lg.is_empty() {
15900 lg.resize(self.vocab_size, 0.0);
15901 if let Some(c) = self.final_softcap {
15902 softcap_in_place(&mut lg, c, self.pool.as_deref());
15903 }
15904 self.graph_logits = Some(lg);
15905 }
15906 crate::gpu::set_layer(-1);
15907 return hh;
15908 }
15909 h = hh;
15911 tail_start = from + gl;
15912 }
15913 }
15914 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15919
15920 let host_tail = tail_start > from;
15930 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15931 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15932
15933 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15934 #[cfg(target_os = "macos")]
15935 let mut gpu_skip_until = 0usize;
15936 for li in tail_start.max(from)..self.num_layers {
15937 let _capacity_tail = automatic_gpu_prefix
15938 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15939 .map(|_| crate::gpu::enter_cpu_scope());
15940 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15942 if li > u {
15943 break;
15944 }
15945 }
15946 if let Some(mask) = task_mask {
15947 if !mask.layer_alive(li) {
15948 continue; }
15950 }
15951 #[cfg(target_os = "macos")]
15955 {
15956 if li < gpu_skip_until {
15957 continue;
15958 }
15959 if task_mask.is_none() {
15960 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15961 if self
15962 .graph_failed
15963 .load(std::sync::atomic::Ordering::Relaxed)
15964 {
15965 return vec![0.0; self.hidden_size];
15969 }
15970 if end > li {
15971 gpu_skip_until = end;
15972 if self.is_loop_end(end - 1) && end < self.num_layers {
15975 h = inference::rms_norm(
15976 &h,
15977 &self.weights.final_norm,
15978 self.rms_eps,
15979 self.norm_style,
15980 );
15981 }
15982 continue;
15983 }
15984 }
15985 }
15986
15987 if task_mask.is_none() {
15988 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
15989 crate::gpu::BatchGraphOutcome::Completed => continue,
15990 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
15991 crate::gpu::BatchGraphOutcome::Declined => {},
15992 }
15993 }
15994 #[cfg(feature = "gpu")]
15995 self.pull_lagging_host_kv(li, li + 1, position);
15996 let lw = &self.weights.layers[self.phys_layer(li)];
15997 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15998 if tp.parse::<usize>().ok() == Some(position) {
15999 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16000 eprintln!(
16001 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16002 h[0], h[1]
16003 );
16004 }
16005 }
16006 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16009 inference::rms_norm_into(
16010 &h,
16011 &lw.input_norm,
16012 self.rms_eps,
16013 self.norm_style,
16014 &mut self.ws.n1,
16015 );
16016 drop(prof);
16017
16018 let attn_out = match &lw.attn {
16019 AttnKind::Mla(w) => {
16020 let inv_freq_l = self.layer_inv_freq(li);
16021 let rs = self.layer_rope_scale(li);
16022 let eps = self.rms_eps;
16023 let pool = self.pool.clone();
16024 mla_attention(
16025 w,
16026 &self.ws.n1,
16027 &mut self.kv_cache.layers[li],
16028 position,
16029 &inv_freq_l,
16030 rs,
16031 eps,
16032 pool.as_deref(),
16033 )
16034 }
16035 AttnKind::Linear(w) => {
16036 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16037 vmf_phase_forward(
16038 &self.ws.n1,
16039 w,
16040 &cfg,
16041 &mut self.kv_cache.layers[li].linear_state,
16042 self.pool.as_deref(),
16043 )
16044 }
16045 AttnKind::Kda(w) => {
16046 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16047 crate::linear_core::kda_forward(
16048 &self.ws.n1,
16049 w,
16050 &cfg,
16051 &mut self.kv_cache.layers[li].linear_state,
16052 self.pool.as_deref(),
16053 )
16054 }
16055 AttnKind::LinearGdn(w) => {
16056 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16057 gdn_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::ShortConv(w) => {
16066 let cfg = self
16067 .short_conv_cfg
16068 .expect("short-conv layer without short_conv_cfg");
16069 short_conv_forward(
16070 &self.ws.n1,
16071 w,
16072 &cfg,
16073 &mut self.kv_cache.layers[li].linear_state,
16074 self.pool.as_deref(),
16075 )
16076 }
16077 AttnKind::Bounded(w) => {
16078 let rope = self
16081 .bounded_rope
16082 .clone()
16083 .expect("bounded layer without an installed rotation table");
16084 let cfg = crate::bounded::BoundedAttnCfg {
16085 num_heads: self.num_heads,
16086 num_kv_heads: self.num_kv_heads,
16087 head_dim: self.head_dim,
16088 hidden_size: hs,
16089 scale: self.attn_scale,
16090 rope: &rope,
16091 pool: pool.as_deref(),
16092 };
16093 crate::bounded::bounded_attention(
16094 &self.ws.n1,
16095 w,
16096 &mut self.kv_cache.layers[li],
16097 &cfg,
16098 )
16099 }
16100 AttnKind::Full {
16101 wq,
16102 wk,
16103 wv,
16104 wo,
16105 q_norm,
16106 k_norm,
16107 output_gate,
16108 softplus_gate,
16109 bias,
16110 } if self.kv_cache.layers[li].o1_sealed() => {
16111 let inv_freq_l = self.layer_inv_freq(li);
16114 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16115 let cfg = QwenAttnCfg {
16116 num_heads: self.layer_num_heads(li),
16117 num_kv_heads: nkv_l,
16118 head_dim: hd_l,
16119 hidden_size: hs,
16120 position,
16121 inv_freq: &inv_freq_l,
16122 rotary_dim: rd_l,
16123 scale: self.attn_scale,
16124 softcap: self.attn_softcap,
16125 window: None,
16126 v_norm: self.attn_v_norm,
16127 qk_norm_after_rope: self.qk_norm_after_rope,
16128 gate_sigmoid: self.proj_gate_sigmoid,
16129 q_norm: q_norm.as_deref(),
16130 k_norm: k_norm.as_deref(),
16131 output_gate: *output_gate,
16132 softplus_gate: softplus_gate
16133 .as_ref()
16134 .map(|(gate, per_head)| (gate, *per_head)),
16135 rope_scale: self.layer_rope_scale(li),
16136 bias: bias
16137 .as_ref()
16138 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16139 rms_eps: eps,
16140 norm_style: self.norm_style,
16141 pool: pool.as_deref(),
16142 v_head_dim: self.layer_v_dim(li),
16143 };
16144 attention::qwen_attention_nystrom(
16145 &self.ws.n1,
16146 wq,
16147 wk,
16148 wv,
16149 wo,
16150 &mut self.kv_cache.layers[li],
16151 &cfg,
16152 )
16153 }
16154 AttnKind::Full {
16155 wq,
16156 wk,
16157 wv,
16158 wo,
16159 q_norm,
16160 k_norm,
16161 output_gate,
16162 softplus_gate,
16163 bias,
16164 } => 'attn: {
16165 let dropin_reason =
16170 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16171 if let Some(reason) = dropin_reason {
16172 self.note_graph_decline("wgpu attn dropin", reason);
16173 }
16174 if graph_on
16175 && dropin_reason.is_none()
16176 && !*output_gate
16177 && softplus_gate.is_none()
16178 && self.attention_heads_per_layer.is_none()
16179 && bias.is_none()
16180 && task_mask.is_none()
16181 {
16182 let inv_freq_l = self.layer_inv_freq(li);
16183 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16184 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16185 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16186 wq.mapped_q1(),
16187 wk.mapped_q1(),
16188 wv.mapped_q1(),
16189 wo.mapped_q1(),
16190 ) {
16191 let gm = gm.clone();
16192 let mut out = vec![0f32; hs];
16193 let cache = &self.kv_cache.layers[li];
16194 if crate::gpu::attn_dropin(
16195 &gm,
16196 self.graph_kv_id,
16197 li,
16198 &self.ws.n1,
16199 qi,
16200 ki,
16201 vi,
16202 oi,
16203 q_norm.as_deref(),
16204 k_norm.as_deref(),
16205 self.qk_norm_after_rope,
16206 &inv_freq_l,
16207 nh,
16208 nkv_l,
16209 hd_l,
16210 rd_l,
16211 hs,
16212 position,
16213 self.kv_cache.max_seq_len,
16214 gemma,
16215 eps as f32,
16216 cache.k_heads(),
16217 cache.v_heads(),
16218 &mut out,
16219 ) {
16220 break 'attn out;
16221 }
16222 }
16223 }
16224 let masked = task_mask
16225 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16226 .unwrap_or(false);
16227 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16228 let plain = self.layer_attn_plain(li);
16231 match (masked, f32_view) {
16232 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16235 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16236 attention::multi_head_attention(
16237 &self.ws.n1,
16238 q,
16239 k,
16240 v,
16241 o,
16242 &mut self.kv_cache.layers[li],
16243 self.num_heads,
16244 self.num_kv_heads,
16245 self.head_dim,
16246 self.hidden_size,
16247 position,
16248 &active_heads,
16249 &self.inv_freq,
16250 )
16251 }
16252 (masked, _) => {
16253 if masked {
16254 tracing::warn!(
16255 "layer {li}: head mask on quantized weights or on a \
16256 window/sink/per-layer-geometry layer not supported \
16257 yet — executing dense"
16258 );
16259 }
16260 let inv_freq_l = self.layer_inv_freq(li);
16261 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16262 let cfg = QwenAttnCfg {
16263 num_heads: self.layer_num_heads(li),
16264 num_kv_heads: nkv_l,
16265 head_dim: hd_l,
16266 hidden_size: hs,
16267 position,
16268 inv_freq: &inv_freq_l,
16269 rotary_dim: rd_l,
16270 scale: self.attn_scale,
16271 softcap: self.attn_softcap,
16272 window: self.layer_window(li),
16273 v_norm: self.attn_v_norm,
16274 qk_norm_after_rope: self.qk_norm_after_rope,
16275 gate_sigmoid: self.proj_gate_sigmoid,
16276 q_norm: q_norm.as_deref(),
16277 k_norm: k_norm.as_deref(),
16278 output_gate: *output_gate,
16279 softplus_gate: softplus_gate
16280 .as_ref()
16281 .map(|(gate, per_head)| (gate, *per_head)),
16282 rope_scale: self.layer_rope_scale(li),
16283 bias: bias
16284 .as_ref()
16285 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16286 rms_eps: eps,
16287 norm_style: self.norm_style,
16288 pool: pool.as_deref(),
16289 v_head_dim: self.layer_v_dim(li),
16290 };
16291 attention::qwen_attention(
16292 &self.ws.n1,
16293 wq,
16294 wk,
16295 wv,
16296 wo,
16297 &mut self.kv_cache.layers[li],
16298 &cfg,
16299 )
16300 }
16301 }
16302 }
16303 };
16304 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
16307 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
16308 None => attn_out,
16309 };
16310 let lw = &self.weights.layers[self.phys_layer(li)];
16311 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16312 inference::add_rmsnorm_fused_into(
16313 &mut h,
16314 &attn_out,
16315 &lw.post_norm,
16316 self.rms_eps,
16317 self.norm_style,
16318 &mut self.ws.p1,
16319 );
16320 drop(prof);
16321 let mut attn_out = attn_out;
16322 attention::recycle_buf(&mut attn_out);
16323 let post_normed = &self.ws.p1;
16324
16325 let ffn_masked = task_mask
16326 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16327 .unwrap_or(false);
16328 let ffn_out = match (ffn_masked, &lw.ffn) {
16340 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16344 let row = task_mask
16345 .and_then(|tm| tm.ffn_masks.get(li))
16346 .map(|v| v.as_slice());
16347 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16348 }
16349 (true, FfnKind::Dense(d)) => {
16350 let tm = task_mask.unwrap();
16351 let alive = tm.ffn_active_count(li);
16352 let deep = alive * 2 <= self.intermediate_size;
16353 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16354 let active = tm.ffn_active_indices(li);
16355 sparse_ffn_quant(
16356 d,
16357 post_normed,
16358 &active,
16359 self.hidden_size,
16360 self.pool.as_deref(),
16361 )
16362 } else if deep
16363 && let (Some(g), Some(u), Some(dn)) = (
16364 d.gate_proj.as_f32(),
16365 d.up_proj.as_f32(),
16366 d.down_proj.as_f32(),
16367 )
16368 {
16369 let active = tm.ffn_active_indices(li);
16370 inference::sparse_ffn_forward(
16371 post_normed,
16372 g,
16373 u,
16374 dn,
16375 self.hidden_size,
16376 self.intermediate_size,
16377 &active,
16378 self.pool.as_deref(),
16379 )
16380 } else {
16381 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16382 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16383 }
16384 }
16385 (true, FfnKind::Moe(m)) => {
16386 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16390 ffn_forward(
16391 &lw.ffn,
16392 post_normed,
16393 self.pool.as_deref(),
16394 allowed.as_deref(),
16395 )
16396 }
16397 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16398 dm,
16399 post_normed,
16400 &h,
16401 self.rms_eps,
16402 self.norm_style,
16403 self.pool.as_deref(),
16404 ),
16405 (false, _) => match &lw.ffn {
16406 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16407 dm,
16408 post_normed,
16409 &h,
16410 self.rms_eps,
16411 self.norm_style,
16412 self.pool.as_deref(),
16413 ),
16414 FfnKind::Moe(m)
16415 if task_mask.is_none()
16416 && host_tail
16417 && li >= tail_start
16418 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
16419 {
16420 moe_ffn_minicpm_tail(
16421 &mut self.minicpm_tail,
16422 li,
16423 self.num_layers,
16424 m,
16425 post_normed,
16426 self.pool.as_deref(),
16427 )
16428 }
16429 FfnKind::Moe(m)
16430 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16431 {
16432 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16433 }
16434 _ => {
16435 let allowed = match (&lw.ffn, task_mask) {
16436 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16437 _ => None,
16438 };
16439 ffn_forward(
16440 &lw.ffn,
16441 post_normed,
16442 self.pool.as_deref(),
16443 allowed.as_deref(),
16444 )
16445 }
16446 },
16447 };
16448 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16449 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16450 None => ffn_out,
16451 };
16452 for (i, &f) in ffn_out.iter().enumerate() {
16453 h[i] += f;
16454 }
16455 let mut ffn_out = ffn_out;
16456 attention::recycle_buf(&mut ffn_out);
16457
16458 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16460 for v in h.iter_mut() {
16461 *v *= sc;
16462 }
16463 }
16464 if self.layer_dump.is_some() {
16466 self.dump_layer_row(position, li, &h);
16467 }
16468
16469 if self.is_loop_end(li) && li + 1 < self.num_layers {
16472 h = inference::rms_norm(
16473 &h,
16474 &self.weights.final_norm,
16475 self.rms_eps,
16476 self.norm_style,
16477 );
16478 }
16479
16480 if self.dyn_phi_layer == Some(li) {
16484 self.update_dyn_phi(&h);
16485 }
16486 }
16487 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16489 crate::gpu::graph_race_record(false, t.elapsed());
16490 }
16491
16492 h
16493 }
16494
16495 fn update_dyn_phi(&mut self, h: &[f32]) {
16498 const A: f32 = 0.2;
16499 if self.dyn_phi_ema.len() != h.len() {
16500 self.dyn_phi_ema = vec![0.0; h.len()];
16501 self.dyn_phi_seen = 0;
16502 }
16503 if self.dyn_phi_seen == 0 {
16504 self.dyn_phi_ema.copy_from_slice(h);
16505 } else {
16506 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16507 *e = (1.0 - A) * *e + A * v;
16508 }
16509 }
16510 self.dyn_phi_seen += 1;
16511 }
16512
16513 pub fn dyn_phi(&self) -> &[f32] {
16515 &self.dyn_phi_ema
16516 }
16517
16518 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16520 self.dyn_phi_layer = layer;
16521 self.dyn_phi_ema.clear();
16522 self.dyn_phi_seen = 0;
16523 }
16524
16525 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16527 let Some(model) = &self.model else {
16528 return Vec::new();
16529 };
16530 model
16531 .header
16532 .skills
16533 .iter()
16534 .enumerate()
16535 .filter_map(|(i, sk)| {
16536 if sk.is_v2() {
16540 return None;
16541 }
16542 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16543 let sel = sk.selection.as_ref()?;
16544 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16545 })
16546 .collect()
16547 }
16548
16549 pub fn active_skill(&self) -> Option<usize> {
16551 self.dyn_active
16552 }
16553
16554 pub fn enable_dynamic_routing(&mut self) -> usize {
16559 use crate::swarm::{DynRouter, RoutableSkill};
16560 let Some(model) = self.model.clone() else {
16561 return 0;
16562 };
16563 if let Some(r) = &model.header.router {
16570 tracing::warn!(
16571 "dynamic routing disabled: this file declares router policy '{}' with \
16572 granularity \"{}\" — the request-level decision applies instead",
16573 r.policy,
16574 r.granularity
16575 );
16576 return 0;
16577 }
16578 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16584 || model.header.skills.iter().any(|s| s.is_v2())
16585 {
16586 tracing::warn!(
16587 "dynamic routing disabled: this file carries format-v2 skill records \
16588 (SKILLS_V2) — they route per request through a router policy only"
16589 );
16590 return 0;
16591 }
16592 if self.dyn_blend_loaded {
16595 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16596 return 0;
16597 }
16598 if let Some(a) = self.dyn_active {
16602 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16603 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16604 return 0;
16605 }
16606 }
16607 let hidden = self.hidden_size;
16608 let mut skills = Vec::new();
16609 for (idx, id, _phi) in self.dynamic_skills() {
16610 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16611 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16612 skills.push(rs);
16613 }
16614 }
16615 }
16616 if skills.is_empty() {
16617 return 0;
16618 }
16619 let phi = skills[0].phi_layer;
16621 if skills.iter().any(|s| s.phi_layer != phi) {
16622 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16623 }
16624 let n = skills.len();
16625 self.set_dyn_phi_layer(Some(phi));
16626 self.dyn_router = Some(DynRouter::new(skills));
16627 n
16628 }
16629
16630 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16632 self.dyn_router
16633 .as_ref()
16634 .map(|r| r.switches.clone())
16635 .unwrap_or_default()
16636 }
16637
16638 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16641 let _mimo_q8 = self.mimo_moe.is_on()
16642 .then(crate::qtensor::enter_full_gpu_q8_scope);
16643 let rows = self.weights.lm_head.rows();
16644 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16645 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
16649 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
16650 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
16651 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
16652 rows, self.hidden_size, &mut logits)
16653 });
16654 if !served {
16655 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
16656 }
16657 logits.resize(self.vocab_size, 0.0);
16658 if let Some(m) = self.logit_multiplier {
16659 for l in logits.iter_mut() {
16660 *l *= m;
16661 }
16662 }
16663 if let Some(c) = self.final_softcap {
16664 softcap_in_place(&mut logits, c, self.pool.as_deref());
16665 }
16666 if let Some(cm) = self.head_clusters.as_ref() {
16667 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16668 }
16669 logits
16670 }
16671
16672 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16675 let h = hidden.len();
16676 let ncl = cm.len() / h.max(1);
16677 if ncl == 0 || logits.len() % ncl != 0 {
16678 return;
16679 }
16680 let cs = logits.len() / ncl;
16681 let mut lc = vec![0.0f32; ncl];
16683 for c in 0..ncl {
16684 let row = &cm[c * h..(c + 1) * h];
16685 let mut s = 0.0f32;
16686 for j in 0..h {
16687 s += row[j] * hidden[j];
16688 }
16689 lc[c] = s;
16690 }
16691 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16692 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16693 for c in 0..ncl {
16694 let blk = &mut logits[c * cs..(c + 1) * cs];
16695 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16696 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16697 let add = lc[c] - lse - bl;
16698 for v in blk.iter_mut() {
16699 *v += add;
16700 }
16701 }
16702 }
16703
16704 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16709 #[cfg(target_os = "macos")]
16710 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16711 self.clear_sequence_state();
16712 crate::gpu::graph_race_begin_generation();
16716 if task_mask.is_none() {
16717 self.o1_begin();
16718 }
16719 let mut hidden = vec![0.0f32; self.hidden_size];
16720 for (pos, &id) in ids.iter().enumerate() {
16721 let emb = self.embed_single(id);
16722 hidden = self.forward_layers(&emb, pos, task_mask);
16723 }
16724 if let Err(err) = self.o1_seal_checked() {
16725 self.o1_fail(err);
16726 }
16727 if let Some(logits) = self.graph_logits.take() {
16730 return logits;
16731 }
16732 inference::rms_norm_into(
16733 &hidden,
16734 &self.weights.final_norm,
16735 self.rms_eps,
16736 self.norm_style,
16737 &mut self.ws.n1,
16738 );
16739 self.lm_head_forward(&self.ws.n1)
16740 }
16741}
16742
16743pub fn create_test_pipeline(
16745 hidden_size: usize,
16746 intermediate_size: usize,
16747 num_heads: usize,
16748 num_kv_heads: usize,
16749 head_dim: usize,
16750 num_layers: usize,
16751 vocab_size: usize,
16752) -> Pipeline {
16753 let synth = |n: usize, salt: usize| -> Vec<f32> {
16756 (0..n)
16757 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16758 .collect()
16759 };
16760 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16761 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16762 };
16763 let layer_weights: Vec<LayerWeights> = (0..num_layers)
16764 .map(|li| LayerWeights {
16765 input_norm: vec![1.0; hidden_size],
16766 post_norm: vec![1.0; hidden_size],
16767 attn_out_norm: None,
16768 ffn_out_norm: None,
16769 layer_scale: None,
16770 ffn: FfnKind::Dense(DenseFfn {
16771 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
16772 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
16773 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
16774 act: Act::Silu,
16775 down_t: None,
16776 segs: Vec::new(),
16777 }),
16778 attn: AttnKind::Full {
16779 bias: None,
16780 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
16781 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
16782 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
16783 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
16784 q_norm: None,
16785 k_norm: None,
16786 output_gate: false,
16787 softplus_gate: None,
16788 },
16789 })
16790 .collect();
16791
16792 Pipeline::new(
16793 Tokenizer::byte_level(),
16794 PipelineWeights {
16795 embed_tokens: qt(vocab_size, hidden_size, 100),
16796 layers: layer_weights,
16797 lm_head: qt(vocab_size, hidden_size, 200),
16798 final_norm: vec![1.0; hidden_size],
16799 },
16800 hidden_size,
16801 intermediate_size,
16802 num_heads,
16803 num_kv_heads,
16804 head_dim,
16805 num_layers,
16806 num_layers, false, vocab_size,
16809 1e-6,
16810 10_000.0,
16811 NormStyle::Qwen,
16812 4096,
16813 SamplerConfig {
16814 seed: Some(42),
16815 ..Default::default()
16816 },
16817 )
16818}
16819
16820#[inline]
16825fn mask_bit(row: &[u8], j: usize) -> bool {
16826 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
16827}
16828
16829fn mask_gain() -> f32 {
16840 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16841 *G.get_or_init(|| {
16842 std::env::var("CMF_FFN_MASK_GAIN")
16843 .ok()
16844 .and_then(|v| v.parse().ok())
16845 .unwrap_or(1.0)
16846 })
16847}
16848
16849fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
16850 let fill = meanfill().and_then(|(i, v)| {
16853 let li = crate::gpu::cur_layer();
16854 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
16855 });
16856 for r in 0..rows {
16857 let base = r * inter;
16858 for (bi, &byte) in row.iter().enumerate() {
16859 if byte == 0xFF {
16860 continue;
16861 }
16862 let j0 = bi * 8;
16863 for bit in 0..8 {
16864 let j = j0 + bit;
16865 if j < inter && byte & (1 << bit) == 0 {
16866 g[base + j] = fill.map_or(0.0, |f| f[j]);
16867 }
16868 }
16869 }
16870 }
16871 let gain = mask_gain();
16872 if gain != 1.0 {
16873 for v in g[..rows * inter].iter_mut() {
16874 *v *= gain;
16875 }
16876 }
16877}
16878
16879#[inline]
16881fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
16882 row.is_none_or(|r| mask_bit(r, i))
16883}
16884
16885fn all_bits_on(row: &[u8], n: usize) -> bool {
16888 (0..n).all(|i| mask_bit(row, i))
16889}
16890
16891fn tube_topk() -> usize {
16899 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16900 *K.get_or_init(|| {
16901 std::env::var("CMF_TUBE_TOPK")
16902 .ok()
16903 .and_then(|v| v.parse().ok())
16904 .unwrap_or(0)
16905 })
16906}
16907
16908fn tube_score_oracle() -> bool {
16909 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16910 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16911}
16912
16913fn tube_ffn_routed(
16920 d: &DenseFfn,
16921 xs: &[f32],
16922 b: usize,
16923 pool: Option<&Pool>,
16924 mask_row: Option<&[u8]>,
16925 k: usize,
16926) -> Vec<f32> {
16927 let hidden = d.down_proj.rows();
16928 let core = d.gate_proj.rows();
16929 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16930 let mut out = match (b, core_full, mask_row) {
16931 (1, true, _) => dense_ffn(d, xs, pool),
16932 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16933 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16934 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16935 };
16936 let cand: Vec<usize> = (0..d.segs.len())
16937 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16938 .collect();
16939 if cand.is_empty() {
16940 return out;
16941 }
16942 let oracle = tube_score_oracle();
16946 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16947 let mut scores = vec![0f32; b * cand.len()];
16948 for (ci, &i) in cand.iter().enumerate() {
16949 let seg = &d.segs[i];
16950 let w = seg.width;
16951 let mut g = vec![0.0f32; b * w];
16952 if b == 1 {
16953 seg.gate.matvec(xs, &mut g, pool);
16954 } else {
16955 seg.gate.matmat(xs, b, &mut g, pool);
16956 }
16957 for v in g.iter_mut() {
16958 *v = Act::Silu.combine(*v, 1.0);
16959 }
16960 if !oracle {
16961 for t in 0..b {
16962 scores[t * cand.len() + ci] =
16963 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16964 }
16965 }
16966 if oracle || b > 1 {
16967 let mut u = vec![0.0f32; b * w];
16968 if b == 1 {
16969 seg.up.matvec(xs, &mut u, pool);
16970 } else {
16971 seg.up.matmat(xs, b, &mut u, pool);
16972 }
16973 for (a, &v) in g.iter_mut().zip(u.iter()) {
16974 *a *= v;
16975 }
16976 if oracle {
16977 for t in 0..b {
16978 scores[t * cand.len() + ci] =
16979 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16980 }
16981 }
16982 }
16983 acts.push(g);
16984 }
16985 let keep = k.min(cand.len());
16987 let mut scratch: Vec<f32> = Vec::new();
16988 for t in 0..b {
16989 let mut sc: Vec<(f32, usize)> = (0..cand.len())
16990 .map(|ci| (scores[t * cand.len() + ci], ci))
16991 .collect();
16992 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
16993 let mut alive = vec![false; cand.len()];
16994 for &(_, ci) in sc.iter().take(keep) {
16995 alive[ci] = true;
16996 }
16997 if b > 1 {
16998 for (ci, a) in acts.iter_mut().enumerate() {
16999 if !alive[ci] {
17000 let w = d.segs[cand[ci]].width;
17001 a[t * w..(t + 1) * w].fill(0.0);
17002 }
17003 }
17004 } else {
17005 for (ci, &i) in cand.iter().enumerate() {
17009 if !alive[ci] {
17010 continue;
17011 }
17012 let seg = &d.segs[i];
17013 let w = seg.width;
17014 let g = &mut acts[ci];
17015 if !tube_score_oracle() {
17016 scratch.clear();
17017 scratch.resize(w, 0.0);
17018 seg.up.matvec(xs, &mut scratch, pool);
17019 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17020 *a *= v;
17021 }
17022 }
17023 let mut acc = vec![0.0f32; hidden];
17024 seg.down.matvec(g, &mut acc, pool);
17025 for (o, a) in out.iter_mut().zip(&acc) {
17026 *o += *a;
17027 }
17028 }
17029 }
17030 }
17031 if b > 1 {
17032 for (ci, &i) in cand.iter().enumerate() {
17033 let seg = &d.segs[i];
17034 let mut acc = vec![0.0f32; b * hidden];
17035 seg.down.matmat(&acts[ci], b, &mut acc, pool);
17036 for (o, a) in out.iter_mut().zip(&acc) {
17037 *o += *a;
17038 }
17039 }
17040 }
17041 out
17042}
17043
17044fn tube_ffn(
17050 d: &DenseFfn,
17051 xs: &[f32],
17052 b: usize,
17053 pool: Option<&Pool>,
17054 mask_row: Option<&[u8]>,
17055) -> Vec<f32> {
17056 if tube_topk() > 0 {
17057 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17058 }
17059 let hidden = d.down_proj.rows();
17060 let core = d.gate_proj.rows();
17061 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17062 let mut out = match (b, core_full, mask_row) {
17063 (1, true, _) => dense_ffn(d, xs, pool),
17064 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17065 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17066 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17067 };
17068 TUBE_SCRATCH.with(|sc| {
17069 let mut sc = sc.borrow_mut();
17070 let [g, u, acc] = &mut *sc;
17071 for seg in &d.segs {
17072 if !tube_bit(mask_row, seg.start) {
17073 continue;
17074 }
17075 let w = seg.width;
17076 g.resize(b * w, 0.0);
17077 if b == 1
17078 && d.act == Act::Silu
17079 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17080 {
17081 } else {
17083 u.resize(b * w, 0.0);
17084 if b == 1 {
17085 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17086 } else {
17087 seg.gate.matmat(xs, b, g, pool);
17088 seg.up.matmat(xs, b, u, pool);
17089 }
17090 for i in 0..b * w {
17091 g[i] = d.act.combine(g[i], u[i]);
17092 }
17093 }
17094 acc.resize(b * hidden, 0.0);
17095 acc.fill(0.0);
17096 if b == 1 {
17097 seg.down.matvec(g, acc, pool);
17098 } else {
17099 seg.down.matmat(g, b, acc, pool);
17100 }
17101 for (o, a) in out.iter_mut().zip(acc.iter()) {
17102 *o += *a;
17103 }
17104 }
17105 out
17106 })
17107}
17108
17109thread_local! {
17110 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17114 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17115}
17116
17117static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
17120 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
17121
17122fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17126 #[cfg(feature = "gpu")]
17127 {
17128 use std::sync::atomic::Ordering::Relaxed;
17129 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17130 (
17131 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17132 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17133 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17134 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17135 )
17136 }
17137 #[cfg(not(feature = "gpu"))]
17138 (0.0, 0.0, 0.0, 0)
17139}
17140
17141fn wgpu_mirror_counters() -> [u64; 6] {
17144 #[cfg(feature = "gpu")]
17145 {
17146 crate::gpu_wgpu::MIRROR_EVENTS
17147 .each_ref()
17148 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17149 }
17150 #[cfg(not(feature = "gpu"))]
17151 [0; 6]
17152}
17153
17154fn prefill_prof_on() -> bool {
17155 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17156 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17157}
17158
17159fn dense_ffn_batch(
17160 d: &DenseFfn,
17161 xs: &[f32],
17162 b: usize,
17163 pool: Option<&Pool>,
17164 mask_row: Option<&[u8]>,
17165) -> Vec<f32> {
17166 let inter = d.gate_proj.rows();
17167 let hidden = d.down_proj.rows();
17168 let fused_act = d.act.graph_act();
17178 if mask_row.is_none()
17179 && fused_act.is_some()
17180 && b >= 32
17181 && crate::gpu::enabled_here()
17182 && !crate::gpu::mm_killed()
17183 && refit_dir().is_none()
17188 && !ffn_probe_active()
17193 && !crate::gptq_capture::capturing()
17197 {
17198 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17199 d.gate_proj.mapped_q4t(),
17200 d.up_proj.mapped_q4t(),
17201 d.down_proj.mapped_q4t(),
17202 ) {
17203 let mut out = vec![0.0f32; b * hidden];
17204 let act = fused_act.expect("checked above");
17205 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
17206 {
17207 return out;
17208 }
17209 }
17210 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17215 d.gate_proj.mapped_q4tp(),
17216 d.up_proj.mapped_q4tp(),
17217 d.down_proj.mapped_q4tp(),
17218 ) {
17219 let mut out = vec![0.0f32; b * hidden];
17220 let act = fused_act.expect("checked above");
17221 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17222 {
17223 return out;
17224 }
17225 }
17226 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17232 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17233 d.gate_proj.mapped_device_gemm(),
17234 d.up_proj.mapped_device_gemm(),
17235 d.down_proj.mapped_device_gemm(),
17236 ) {
17237 let mut out = vec![0.0f32; b * hidden];
17238 let act = fused_act.expect("checked above");
17239 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17240 return out;
17241 }
17242 }
17243 }
17244 let mut g = vec![0.0f32; b * inter];
17245 d.gate_proj.matmat(xs, b, &mut g, pool);
17246 let mut u = vec![0.0f32; b * inter];
17247 d.up_proj.matmat(xs, b, &mut u, pool);
17248 if gate_topk() > 0 && d.act == Act::Silu {
17249 for t in 0..b {
17250 let row = &mut g[t * inter..(t + 1) * inter];
17251 for v in row.iter_mut() {
17252 *v = Act::Silu.combine(*v, 1.0);
17253 }
17254 keep_top_k(row, gate_topk());
17255 }
17256 for i in 0..b * inter {
17257 g[i] *= u[i];
17258 }
17259 } else {
17260 for i in 0..b * inter {
17261 g[i] = d.act.combine(g[i], u[i]);
17262 }
17263 }
17264 if let Some(row) = mask_row {
17265 zero_masked_cols(&mut g, b, inter, row);
17266 }
17267 if oracle_topk() > 0 {
17268 for t in 0..b {
17269 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
17270 }
17271 }
17272 let mut out = vec![0.0f32; b * hidden];
17273 d.down_proj.matmat(&g, b, &mut out, pool);
17274 if refit_dir().is_some() {
17275 let li = crate::gpu::cur_layer();
17276 if li >= 0 {
17277 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
17278 }
17279 }
17280 FFN_PROBE.with(|pr| {
17284 if let Some(acc) = pr.borrow_mut().as_mut() {
17285 let li = crate::gpu::cur_layer();
17286 if li < 0 {
17287 return;
17288 }
17289 let Some(row) = acc.get_mut(li as usize) else {
17290 return;
17291 };
17292 let sq = probe_sq();
17293 for t in 0..b {
17294 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
17295 *a += if sq {
17296 (v as f64) * (v as f64)
17297 } else {
17298 (v as f64).abs()
17299 };
17300 }
17301 }
17302 }
17303 });
17304 out
17305}
17306
17307fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
17312 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17313 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17314 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
17315 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
17316 if (!on && !dump) || b == 0 {
17317 return;
17318 }
17319 let hidden = xs.len() / b;
17320 if on {
17321 let mut acc = m.act_sq.borrow_mut();
17322 if acc.len() < hidden {
17323 acc.resize(hidden, 0.0);
17324 }
17325 for t in 0..b {
17326 let row = &xs[t * hidden..(t + 1) * hidden];
17327 for (a, &v) in acc.iter_mut().zip(row) {
17328 *a += (v as f64) * (v as f64);
17329 }
17330 }
17331 }
17332 if dump {
17333 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
17336 .ok()
17337 .and_then(|v| v.parse().ok())
17338 .unwrap_or(4096);
17339 let mut rows = m.act_rows.borrow_mut();
17340 if rows.len() < cap * hidden {
17341 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17342 rows.extend_from_slice(&xs[..take * hidden]);
17343 }
17344 }
17345}
17346
17347#[derive(Clone, Copy)]
17350struct SendVecs(*mut Vec<f32>);
17351unsafe impl Send for SendVecs {}
17352unsafe impl Sync for SendVecs {}
17353impl SendVecs {
17354 #[inline]
17355 fn at(self, i: usize) -> *mut Vec<f32> {
17356 unsafe { self.0.add(i) }
17357 }
17358}
17359
17360fn moe_ffn_batch(
17361 m: &MoeFfn,
17362 xs: &[f32],
17363 b: usize,
17364 hidden: usize,
17365 pool: Option<&Pool>,
17366 allowed: Option<&[bool]>,
17367) -> Vec<f32> {
17368 accumulate_act(m, xs, b);
17369 let ne = m.experts.len();
17370 let mut logits = vec![0.0f32; b * ne];
17371 match &m.resonance {
17372 Some(r) => {
17373 let hdim = xs.len() / b.max(1);
17374 for bi in 0..b {
17375 r.scores(
17376 &xs[bi * hdim..(bi + 1) * hdim],
17377 &mut logits[bi * ne..(bi + 1) * ne],
17378 );
17379 }
17380 }
17381 None => m.router.matmat(xs, b, &mut logits, pool),
17382 }
17383
17384 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17387 {
17388 let mut st = m.stats.borrow_mut();
17389 if st.len() < ne {
17390 st.resize(ne, 0);
17391 }
17392 for bi in 0..b {
17393 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17394 for &e in &idx {
17395 st[e] += 1;
17396 assign[e].push((bi, p[e] / wsum));
17397 }
17398 }
17399 }
17400
17401 let mut out = vec![0.0f32; b * hidden];
17402 let cols = m.experts[0].gate_proj.cols();
17403 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17404 let sb = list.len();
17405 let mut sub = vec![0.0f32; sb * cols];
17406 for (k, &(bi, _)) in list.iter().enumerate() {
17407 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17408 }
17409 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17410 for (k, &(bi, w)) in list.iter().enumerate() {
17411 for i in 0..hidden {
17412 out[bi * hidden + i] += w * eo[k * hidden + i];
17413 }
17414 }
17415 };
17416 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17422 if pool.is_some() && active.len() >= 8 {
17423 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17424 {
17425 let panel_ptr = SendVecs(panels.as_mut_ptr());
17426 let experts = &m.experts;
17429 let (active_r, assign_r) = (&active, &assign);
17430 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17431 let run = |start: usize, end: usize| {
17432 let _cpu_scope = inherit_cpu();
17433 #[cfg(target_os = "macos")]
17441 let _metal_cpu = crate::gpu::enter_cpu_scope();
17442 for ai in start..end {
17443 let e = active_r[ai];
17444 let list = &assign_r[e];
17445 let sb = list.len();
17446 let mut sub = vec![0.0f32; sb * cols];
17447 for (k, &(bi, _)) in list.iter().enumerate() {
17448 sub[k * cols..(k + 1) * cols]
17449 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17450 }
17451 unsafe {
17453 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17454 }
17455 }
17456 };
17457 match pool {
17458 Some(p) => p.run_rows(active.len(), &run),
17459 None => run(0, active.len()),
17460 }
17461 }
17462 for (ai, &e) in active.iter().enumerate() {
17463 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17464 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17465 for i in 0..hidden {
17466 out[bi * hidden + i] += w * eo[i];
17467 }
17468 }
17469 }
17470 } else {
17471 for &e in &active {
17472 run_expert(&m.experts[e], &assign[e], &mut out);
17473 }
17474 }
17475 if let Some((se, gate)) = &m.shared {
17476 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17477 let mut gl = vec![0.0f32; b];
17478 gate.matmat(xs, b, &mut gl, pool);
17479 (0..b)
17480 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17481 .collect()
17482 } else {
17483 (0..b).map(|bi| (bi, 1.0)).collect()
17484 };
17485 run_expert(se, &all, &mut out);
17486 }
17487 out
17488}
17489
17490fn moe_ffn_rows_exact(
17502 m: &MoeFfn,
17503 xs: &[f32],
17504 b: usize,
17505 hidden: usize,
17506 pool: Option<&Pool>,
17507) -> Vec<f32> {
17508 let mut out = vec![0.0f32; b * hidden];
17509 let per_row = |out: &mut [f32]| {
17510 for r in 0..b {
17511 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17512 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17513 }
17514 };
17515 let covered = !crate::gpu::enabled_here()
17516 && moe_batch_enabled()
17517 && m.shared.is_none()
17518 && m.resonance.is_none()
17519 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17520 && m.experts.iter().all(|d| d.act == Act::Silu);
17521 if !covered {
17522 per_row(&mut out);
17523 return out;
17524 }
17525 let ne = m.experts.len();
17526 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17528 for r in 0..b {
17529 let x = &xs[r * hidden..(r + 1) * hidden];
17530 accumulate_act(m, x, 1);
17531 let mut logits = vec![0.0f32; ne];
17532 m.router.matvec(x, &mut logits, pool);
17533 let (idx, p, wsum) = moe_route(&logits, m, None);
17534 {
17535 let mut st = m.stats.borrow_mut();
17536 if st.len() < ne {
17537 st.resize(ne, 0);
17538 }
17539 for &e in &idx {
17540 st[e] += 1;
17541 }
17542 }
17543 let w: Vec<f32> = idx
17544 .iter()
17545 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17546 .collect();
17547 routes.push((idx, w));
17548 }
17549 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17550 per_row(&mut out);
17551 return out;
17552 }
17553 let mut experts: Vec<usize> = Vec::new();
17555 let mut groups: Vec<Vec<usize>> = Vec::new();
17556 for (r, (idx, _)) in routes.iter().enumerate() {
17557 for &e in idx {
17558 match experts.iter().position(|&x| x == e) {
17559 Some(g) => groups[g].push(r),
17560 None => {
17561 experts.push(e);
17562 groups.push(vec![r]);
17563 }
17564 }
17565 }
17566 }
17567 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17568 let inter = m.experts[experts[0]].gate_proj.rows();
17569 let pairs: Vec<(&QTensor, &QTensor)> = experts
17570 .iter()
17571 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17572 .collect();
17573 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17574 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17575 per_row(&mut out);
17576 return out;
17577 }
17578 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17579 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17580 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17581 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17582 per_row(&mut out);
17583 return out;
17584 }
17585 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17587 let mut p = 0usize;
17588 for (g, &e) in experts.iter().enumerate() {
17589 for &r in &groups[g] {
17590 slot.insert((r, e), p);
17591 p += 1;
17592 }
17593 }
17594 for (r, (idx, w)) in routes.iter().enumerate() {
17595 let terms: Vec<(&[f32], f32)> = idx
17596 .iter()
17597 .zip(w)
17598 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17599 .collect();
17600 let row = &mut out[r * hidden..(r + 1) * hidden];
17601 for (i, dst) in row.iter_mut().enumerate() {
17602 let mut acc = 0f32;
17604 for (d, we) in &terms {
17605 acc += we * d[i];
17606 }
17607 *dst = acc;
17608 }
17609 }
17610 out
17611}
17612
17613pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17617 match pool {
17618 Some(p) if v.len() >= 1 << 15 => {
17619 let base = v.as_mut_ptr() as usize;
17620 p.run_rows(v.len(), &|s, e| {
17621 let sl = unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17624 for x in sl {
17625 *x = c * (*x / c).tanh();
17626 }
17627 });
17628 }
17629 _ => {
17630 for x in v.iter_mut() {
17631 *x = c * (*x / c).tanh();
17632 }
17633 }
17634 }
17635}
17636
17637thread_local! {
17638 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17642 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17643}
17644
17645fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17647 if gate_topk() > 0
17650 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17651 {
17652 return out;
17653 }
17654 let prism_body = d.gate_proj.has_prism_contract()
17670 || d.up_proj.has_prism_contract()
17671 || d.down_proj.has_prism_contract();
17672 if !prism_body
17673 && crate::gpu::enabled_here()
17674 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17675 {
17676 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17677 crate::gpu::ProbeArm::Gpu
17678 } else {
17679 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17680 };
17681 match arm {
17682 crate::gpu::ProbeArm::Gpu => {
17683 let t0 = std::time::Instant::now();
17684 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17685 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17686 return out;
17687 }
17688 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17692 }
17693 crate::gpu::ProbeArm::CpuTimed => {
17694 let t0 = std::time::Instant::now();
17695 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17696 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17697 return out;
17698 }
17699 crate::gpu::ProbeArm::Cpu => {
17700 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17701 }
17702 }
17703 }
17704 dense_ffn_cpu(d, x, pool)
17705}
17706
17707fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17709 let inter = d.gate_proj.rows();
17710 FFN_SCRATCH.with(|s| {
17711 let mut s = s.borrow_mut();
17712 let [g, u, ..] = &mut *s;
17713 g.resize(inter, 0.0);
17714 if gate_topk() > 0 {
17717 u.resize(inter, 0.0);
17721 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17722 for i in 0..inter {
17723 g[i] = Act::Silu.combine(g[i], 1.0);
17724 }
17725 keep_top_k(g, gate_topk());
17726 for i in 0..inter {
17727 g[i] *= u[i];
17728 }
17729 } else if d.act == Act::Silu && {
17730 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17731 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17732 } {
17733 } else {
17735 u.resize(inter, 0.0);
17736 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17738 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17739 for i in 0..inter {
17740 g[i] = d.act.combine(g[i], u[i]);
17741 }
17742 }
17743 FFN_PROBE.with(|pr| {
17751 if let Some(acc) = pr.borrow_mut().as_mut() {
17752 let li = crate::gpu::cur_layer();
17753 if li >= 0 {
17754 if let Some(row) = acc.get_mut(li as usize) {
17755 match probe_topk() {
17756 0 if probe_sq() => {
17757 for (a, &v) in row.iter_mut().zip(g.iter()) {
17758 *a += (v as f64) * (v as f64);
17759 }
17760 }
17761 0 if probe_signed() => {
17762 for (a, &v) in row.iter_mut().zip(g.iter()) {
17763 *a += v as f64;
17764 }
17765 }
17766 0 => {
17767 for (a, &v) in row.iter_mut().zip(g.iter()) {
17768 *a += (v as f64).abs();
17769 }
17770 }
17771 k => {
17772 let n = g.len();
17773 let k = k.min(n);
17774 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17775 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17776 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17777 });
17778 let thr = *kth;
17779 for (a, &v) in row.iter_mut().zip(g.iter()) {
17780 if v.abs() >= thr {
17781 *a += 1.0;
17782 }
17783 }
17784 }
17785 }
17786 }
17787 }
17788 }
17789 });
17790 if oracle_topk() > 0 {
17791 keep_top_k(g, oracle_topk());
17792 }
17793 {
17794 let li = crate::gpu::cur_layer();
17795 if li >= 0 {
17796 adump_row(li as usize, g);
17797 }
17798 }
17799 let mut out = attention::take_buf(d.down_proj.rows());
17800 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
17801 d.down_proj.matvec(g, &mut out, pool);
17802 out
17803 })
17804}
17805
17806pub struct RefitAcc {
17819 pub support: Vec<u32>,
17820 pub gss: Vec<f32>,
17821 pub ya: Vec<f32>,
17822 pub hidden: usize,
17823 pub tokens: u64,
17824 pub buf_g: Vec<f32>,
17830 pub buf_o: Vec<f32>,
17831 pub buf_t: usize,
17832}
17833
17834type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
17838
17839static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
17840 std::sync::OnceLock::new();
17841
17842fn ffn_probe_active() -> bool {
17845 FFN_PROBE.with(|p| p.borrow().is_some())
17846}
17847
17848fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
17849 REFIT
17850 .get_or_init(|| {
17851 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
17852 (
17853 d,
17854 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
17855 )
17856 })
17857 })
17858 .as_ref()
17859}
17860
17861fn refit_accumulate(
17863 li: usize,
17864 g: &[f32],
17865 b: usize,
17866 inter: usize,
17867 out: &[f32],
17868 hidden: usize,
17869 pool: Option<&Pool>,
17870) {
17871 let Some((dir, map)) = refit_dir() else {
17872 return;
17873 };
17874 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17875 let (from, to) = *SPAN.get_or_init(|| {
17876 let g = |k: &str, d: usize| {
17877 std::env::var(k)
17878 .ok()
17879 .and_then(|v| v.parse().ok())
17880 .unwrap_or(d)
17881 };
17882 (
17883 g("CMF_FFN_REFIT_FROM", 0),
17884 g("CMF_FFN_REFIT_TO", usize::MAX),
17885 )
17886 });
17887 if li < from || li > to {
17888 return;
17889 }
17890 let mut guard = map.lock().unwrap();
17891 let (map, shared) = &mut *guard;
17892 let acc = match map.entry(li) {
17893 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
17894 std::collections::hash_map::Entry::Vacant(e) => {
17895 let path = format!("{dir}/support.{li}.u32");
17896 let Ok(bytes) = std::fs::read(&path) else {
17897 eprintln!("refit: no {path} — layer {li} skipped");
17898 return;
17899 };
17900 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
17901 let support: Vec<u32> = bytes[4..4 + n * 4]
17902 .chunks_exact(4)
17903 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17904 .collect();
17905 eprintln!(
17906 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
17907 (n * n + hidden * n) as f64 * 4.0 / 1e6
17908 );
17909 e.insert(RefitAcc {
17910 gss: vec![0.0; n * n],
17911 ya: vec![0.0; hidden * n],
17912 buf_g: Vec::new(),
17913 buf_o: Vec::new(),
17914 buf_t: 0,
17915 support,
17916 hidden,
17917 tokens: 0,
17918 })
17919 }
17920 };
17921 let ns = acc.support.len();
17922 let cap = refit_batch();
17924 if acc.buf_g.is_empty() {
17925 acc.buf_g = vec![0.0; ns * cap];
17926 acc.buf_o = vec![0.0; hidden * cap];
17927 }
17928 let take = b.min(cap - acc.buf_t);
17929 for t in 0..take {
17930 let col = acc.buf_t + t;
17931 for (j, &n) in acc.support.iter().enumerate() {
17932 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17933 }
17934 for h in 0..hidden {
17935 acc.buf_o[h * cap + col] = out[t * hidden + h];
17936 }
17937 }
17938 acc.buf_t += take;
17939 acc.tokens += take as u64;
17940 if acc.buf_t < cap {
17941 return;
17942 }
17943 let bt = acc.buf_t;
17944 acc.buf_t = 0;
17945 let RefitAcc {
17955 gss,
17956 ya,
17957 buf_g,
17958 buf_o,
17959 ..
17960 } = acc;
17961 let need = (ns * ns).max(hidden * ns);
17962 if shared.len() < need {
17963 shared.resize(need, 0.0);
17964 }
17965 let scratch = &mut shared[..];
17966 let _ = bt;
17967 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17968 add_into(gss, &scratch[..ns * ns], pool);
17969 if crate::gpu::gemm_nt_f32_transient(
17970 buf_o,
17971 buf_g,
17972 &mut scratch[..hidden * ns],
17973 hidden,
17974 cap,
17975 ns,
17976 ) {
17977 add_into(ya, &scratch[..hidden * ns], pool);
17978 } else {
17979 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17980 }
17981 } else {
17982 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
17983 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17984 }
17985 }
17989
17990fn refit_batch() -> usize {
17992 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17993 *B.get_or_init(|| {
17994 std::env::var("CMF_FFN_REFIT_BATCH")
17995 .ok()
17996 .and_then(|v| v.parse().ok())
17997 .unwrap_or(4096)
17998 })
17999}
18000
18001fn accum_outer_t(
18004 c: &mut [f32],
18005 m: usize,
18006 n: usize,
18007 b: usize,
18008 left: &[f32],
18009 right: &[f32],
18010 pool: Option<&Pool>,
18011) {
18012 let ptr = SendMut(c.as_mut_ptr());
18013 let body = |i: usize| {
18014 let ptr = &ptr;
18015 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18016 for t in 0..b {
18017 let a = left[i * b + t];
18018 if a == 0.0 {
18019 continue;
18020 }
18021 for (j, o) in row.iter_mut().enumerate() {
18022 *o += a * right[j * b + t];
18023 }
18024 }
18025 };
18026 match pool {
18027 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18028 for i in s..e {
18029 body(i);
18030 }
18031 }),
18032 _ => {
18033 for i in 0..m {
18034 body(i);
18035 }
18036 }
18037 }
18038}
18039
18040fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18043 let n = dst.len().min(src.len());
18044 match pool {
18045 Some(p) if n >= 1 << 16 => {
18046 let ptr = SendMut(dst.as_mut_ptr());
18047 let f = |s: usize, e: usize| {
18048 let ptr = &ptr;
18049 for blk in s..e {
18050 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18051 for i in a..b {
18052 unsafe { *ptr.0.add(i) += src[i] };
18053 }
18054 }
18055 };
18056 p.run_rows(n.div_ceil(4096), &f);
18057 }
18058 _ => {
18059 for (d, v) in dst.iter_mut().zip(&src[..n]) {
18060 *d += *v;
18061 }
18062 }
18063 }
18064}
18065
18066fn accum_outer(
18071 c: &mut [f32],
18072 m: usize,
18073 n: usize,
18074 b: usize,
18075 left: &[f32],
18076 right: &[f32],
18077 pool: Option<&Pool>,
18078) {
18079 const TILE: usize = 32;
18080 let tiles = m.div_ceil(TILE);
18081 let cp = SendMut(c.as_mut_ptr());
18082 let body = |ti: usize| {
18083 let cp = &cp;
18084 let i0 = ti * TILE;
18085 let i1 = (i0 + TILE).min(m);
18086 for t in 0..b {
18087 let r = &right[t * n..t * n + n];
18088 for i in i0..i1 {
18089 let a = left[i * b + t];
18090 if a == 0.0 {
18091 continue;
18092 }
18093 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18095 for (o, v) in row.iter_mut().zip(r) {
18096 *o += a * *v;
18097 }
18098 }
18099 }
18100 };
18101 match pool {
18102 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18103 for ti in s..e {
18104 body(ti);
18105 }
18106 }),
18107 _ => {
18108 for ti in 0..tiles {
18109 body(ti);
18110 }
18111 }
18112 }
18113}
18114
18115pub fn refit_flush() -> usize {
18117 let Some((dir, map)) = refit_dir() else {
18118 return 0;
18119 };
18120 let guard = map.lock().unwrap();
18121 let mut n = 0;
18122 for (li, acc) in guard.0.iter() {
18123 let w = |name: &str, v: &[f32]| {
18126 let path = format!("{dir}/{name}.{li}.f32");
18127 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18128 match std::fs::write(&path, &bytes) {
18129 Ok(()) => {}
18130 Err(e) => eprintln!(
18131 "refit: FAILED to write {path} ({} MB): {e}",
18132 bytes.len() / 1_000_000
18133 ),
18134 }
18135 };
18136 w("gss", &acc.gss);
18137 w("ya", &acc.ya);
18138 println!(
18139 "refit L{li}: {} support, {} tokens, hidden {}",
18140 acc.support.len(),
18141 acc.tokens,
18142 acc.hidden
18143 );
18144 n += 1;
18145 }
18146 n
18147}
18148
18149fn adump_row(li: usize, g: &[f32]) {
18154 use std::io::Write as _;
18155 static FILES: std::sync::OnceLock<
18156 Option<(
18157 String,
18158 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18159 )>,
18160 > = std::sync::OnceLock::new();
18161 let Some((prefix, map)) = FILES
18162 .get_or_init(|| {
18163 std::env::var("CMF_FFN_ADUMP")
18164 .ok()
18165 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18166 })
18167 .as_ref()
18168 else {
18169 return;
18170 };
18171 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18174 let (from, to) = *SPAN.get_or_init(|| {
18175 let g = |k: &str, d: usize| {
18176 std::env::var(k)
18177 .ok()
18178 .and_then(|v| v.parse().ok())
18179 .unwrap_or(d)
18180 };
18181 (
18182 g("CMF_FFN_ADUMP_FROM", 0),
18183 g("CMF_FFN_ADUMP_TO", usize::MAX),
18184 )
18185 });
18186 if li < from || li > to {
18187 return;
18188 }
18189 let mut map = map.lock().unwrap();
18190 let f = map.entry(li).or_insert_with(|| {
18191 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18192 });
18193 let mut bytes = Vec::with_capacity(g.len() * 2);
18194 for v in g {
18195 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18196 }
18197 let _ = f.write_all(&bytes);
18198}
18199
18200fn oracle_topk() -> usize {
18206 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18207 *K.get_or_init(|| {
18208 std::env::var("CMF_FFN_ORACLE_TOPK")
18209 .ok()
18210 .and_then(|v| v.parse().ok())
18211 .unwrap_or(0)
18212 })
18213}
18214
18215fn gate_topk() -> usize {
18221 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18222 *K.get_or_init(|| {
18223 std::env::var("CMF_FFN_GATE_TOPK")
18224 .ok()
18225 .and_then(|v| v.parse().ok())
18226 .unwrap_or(0)
18227 })
18228}
18229
18230fn gate_block() -> usize {
18237 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18238 *B.get_or_init(|| {
18239 std::env::var("CMF_FFN_GATE_BLOCK")
18240 .ok()
18241 .and_then(|v| v.parse().ok())
18242 .unwrap_or(1)
18243 })
18244}
18245
18246fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18248 let n = g.len();
18249 let nb = n.div_ceil(block);
18250 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18251 if kb >= nb {
18252 return;
18253 }
18254 let mut score: Vec<f32> = (0..nb)
18255 .map(|b| {
18256 g[b * block..((b + 1) * block).min(n)]
18257 .iter()
18258 .map(|v| v * v)
18259 .sum::<f32>()
18260 })
18261 .collect();
18262 let mut ord = score.clone();
18263 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
18264 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18265 });
18266 let thr = *kth;
18267 for b in 0..nb {
18268 if score[b] < thr {
18269 g[b * block..((b + 1) * block).min(n)].fill(0.0);
18270 }
18271 }
18272 score.clear();
18273}
18274
18275fn keep_top_k(g: &mut [f32], k: usize) {
18277 if gate_block() > 1 {
18278 return keep_top_blocks(g, k, gate_block());
18279 }
18280 let n = g.len();
18281 if k == 0 || k >= n {
18282 return;
18283 }
18284 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18285 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18286 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18287 });
18288 let thr = *kth;
18289 for v in g.iter_mut() {
18290 if v.abs() < thr {
18291 *v = 0.0;
18292 }
18293 }
18294}
18295
18296fn probe_sq() -> bool {
18300 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18301 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
18302}
18303
18304fn probe_signed() -> bool {
18308 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18309 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
18310}
18311
18312fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
18320 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
18321 M.get_or_init(|| {
18322 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
18323 let b = std::fs::read(&p).ok()?;
18324 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
18325 let vals: Vec<f32> = b[8..]
18326 .chunks_exact(4)
18327 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18328 .collect();
18329 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
18330 Some((inter, vals))
18331 })
18332 .as_ref()
18333}
18334
18335fn probe_topk() -> usize {
18338 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18339 *K.get_or_init(|| {
18340 std::env::var("CMF_FFN_PROBE_TOPK")
18341 .ok()
18342 .and_then(|v| v.parse().ok())
18343 .unwrap_or(0)
18344 })
18345}
18346
18347thread_local! {
18348 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18351 const { std::cell::RefCell::new(None) };
18352}
18353
18354fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18367 if d.gate_proj.has_prism_contract()
18372 || d.up_proj.has_prism_contract()
18373 || d.down_proj.has_prism_contract()
18374 {
18375 return None;
18376 }
18377 let dt = d.down_t.as_ref()?;
18378 let inter = d.gate_proj.rows();
18379 let hidden = dt.cols();
18380 if k == 0 || k >= inter || d.act != Act::Silu {
18381 return None;
18382 }
18383 DYN_SCRATCH.with(|sc| {
18384 let mut sc = sc.borrow_mut();
18385 let DynScratch {
18386 g,
18387 mag,
18388 live,
18389 parts,
18390 } = &mut *sc;
18391 g.resize(inter, 0.0);
18392 d.gate_proj.matvec(x, g, pool);
18393 for v in g.iter_mut() {
18394 *v = inference::silu(*v);
18395 }
18396 mag.clear();
18399 mag.extend(g.iter().map(|v| v.abs()));
18400 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18401 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18402 });
18403 let thr = *kth;
18404 live.clear();
18405 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18406 let mut out = vec![0.0f32; hidden];
18407 match pool {
18408 Some(p) if live.len() >= 64 => {
18409 let nw = p.n_workers() + 1;
18410 parts.clear();
18411 parts.resize(nw * hidden, 0.0);
18412 let ptr = SendMut(parts.as_mut_ptr());
18413 let n = live.len();
18414 let live_ref: &[u32] = live;
18415 let g_ref: &[f32] = g;
18416 p.run(&|w, workers| {
18417 let chunk = n.div_ceil(workers);
18418 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18419 if s >= e {
18420 return;
18421 }
18422 WORKER_SCRATCH.with(|ws| {
18423 let mut ws = ws.borrow_mut();
18424 let [scratch, acc] = &mut *ws;
18425 scratch.resize(hidden.max(x.len()), 0.0);
18426 acc.clear();
18427 acc.resize(hidden, 0.0);
18428 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18429 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18432 d.up_proj.prefetch_row(nx as usize);
18433 dt.prefetch_row(nx as usize);
18434 }
18435 let idx = nrm as usize;
18436 let up = d.up_proj.row_dot(idx, x, scratch);
18437 let a = g_ref[idx] * up;
18438 if a != 0.0 {
18439 dt.add_row_scaled(idx, a, acc, scratch);
18440 }
18441 }
18442 for (j, v) in acc.iter().enumerate() {
18443 unsafe { *ptr.at(w * hidden + j) = *v };
18444 }
18445 });
18446 });
18447 for w in 0..nw {
18448 for (j, o) in out.iter_mut().enumerate() {
18449 *o += parts[w * hidden + j];
18450 }
18451 }
18452 }
18453 _ => {
18454 WORKER_SCRATCH.with(|ws| {
18455 let mut ws = ws.borrow_mut();
18456 let [scratch, _acc] = &mut *ws;
18457 scratch.resize(hidden.max(x.len()), 0.0);
18458 for &nrm in live.iter() {
18459 let idx = nrm as usize;
18460 let up = d.up_proj.row_dot(idx, x, scratch);
18461 let a = g[idx] * up;
18462 if a != 0.0 {
18463 dt.add_row_scaled(idx, a, &mut out, scratch);
18464 }
18465 }
18466 });
18467 }
18468 }
18469 Some(out)
18470 })
18471}
18472
18473struct DynScratch {
18476 g: Vec<f32>,
18477 mag: Vec<f32>,
18478 live: Vec<u32>,
18479 parts: Vec<f32>,
18480}
18481
18482thread_local! {
18483 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18484 std::cell::RefCell::new(DynScratch {
18485 g: Vec::new(),
18486 mag: Vec::new(),
18487 live: Vec::new(),
18488 parts: Vec::new(),
18489 })
18490 };
18491 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18493 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18494}
18495
18496fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18501 let inter = d.gate_proj.rows();
18502 FFN_SCRATCH.with(|s| {
18503 let mut s = s.borrow_mut();
18504 let [g, u, ..] = &mut *s;
18505 g.resize(inter, 0.0);
18506 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18507 } else {
18509 u.resize(inter, 0.0);
18510 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18511 for i in 0..inter {
18512 g[i] = d.act.combine(g[i], u[i]);
18513 }
18514 }
18515 zero_masked_cols(g, 1, inter, mask_row);
18516 let mut out = attention::take_buf(d.down_proj.rows());
18517 d.down_proj.matvec(g, &mut out, pool);
18518 out
18519 })
18520}
18521
18522fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18528 if d.gate_proj.has_prism_contract()
18529 || d.up_proj.has_prism_contract()
18530 || d.down_proj.has_prism_contract()
18531 {
18532 return None;
18533 }
18534 d.act.graph_act()?;
18537 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18540 return None;
18541 }
18542 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18543 let mut model_ref = None;
18544 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18545 let model = model_ref?;
18546 let hidden = jobs[0].down.1;
18547 let mut out = attention::take_buf(hidden);
18548 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18549 Some(out)
18550 } else {
18551 let mut out = out;
18552 attention::recycle_buf(&mut out);
18553 None
18554 }
18555}
18556
18557#[allow(clippy::type_complexity)]
18562#[allow(clippy::type_complexity)]
18563pub(crate) fn moe_parts(
18564 t: &QTensor,
18565) -> Option<(
18566 &std::sync::Arc<cortiq_core::CmfModel>,
18567 usize,
18568 usize,
18569 usize,
18570 &[f32],
18571 &[f32],
18572 bool,
18573 bool,
18574 bool,
18575)> {
18576 match t {
18577 QTensor::Mapped {
18578 model,
18579 idx,
18580 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18581 rows,
18582 cols,
18583 row_scale,
18584 col_field,
18585 ..
18586 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18587 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18588 )),
18589 QTensor::Mapped {
18591 model,
18592 idx,
18593 dtype: cortiq_core::TensorDtype::Q1,
18594 rows,
18595 cols,
18596 ..
18597 } => Some((
18598 model,
18599 *idx,
18600 *rows,
18601 *cols,
18602 &[][..],
18603 &[][..],
18604 true,
18605 false,
18606 false,
18607 )),
18608 QTensor::Mapped {
18610 model,
18611 idx,
18612 dtype: cortiq_core::TensorDtype::Q4Tiled,
18613 rows,
18614 cols,
18615 ..
18616 } => Some((
18617 model,
18618 *idx,
18619 *rows,
18620 *cols,
18621 &[][..],
18622 &[][..],
18623 false,
18624 true,
18625 false,
18626 )),
18627 QTensor::Mapped {
18629 model,
18630 idx,
18631 dtype: cortiq_core::TensorDtype::Q4TiledP,
18632 rows,
18633 cols,
18634 ..
18635 } => Some((
18636 model,
18637 *idx,
18638 *rows,
18639 *cols,
18640 &[][..],
18641 &[][..],
18642 false,
18643 true,
18644 false,
18645 )),
18646 QTensor::Mapped {
18650 model,
18651 idx,
18652 dtype: cortiq_core::TensorDtype::Q2TiledP,
18653 rows,
18654 cols,
18655 ..
18656 } => Some((
18657 model,
18658 *idx,
18659 *rows,
18660 *cols,
18661 &[][..],
18662 &[][..],
18663 false,
18664 true,
18665 true,
18666 )),
18667 _ => None,
18668 }
18669}
18670
18671#[cfg(target_os = "macos")]
18678#[allow(clippy::type_complexity)]
18679fn chunk_moe_parts(m: &MoeFfn, hidden: usize) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18680 if m.shared.is_some()
18681 || m.per_expert_scale.is_some()
18682 || m.resonance.is_some()
18683 || m.route_tau.is_some()
18684 || m.router_input_norm
18685 || m.experts.is_empty()
18686 || m.top_k == 0
18687 || std::env::var("CMF_RMS_TRACE").is_ok()
18688 || std::env::var("CMF_ACT_DUMP").is_ok()
18689 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18690 {
18691 return None;
18692 }
18693 let (rf, rr, rc) = m.router.f32_parts()?;
18694 if rr != m.experts.len() || rc != hidden {
18695 return None;
18696 }
18697 let inter = m.experts[0].gate_proj.rows();
18698 let q8 = crate::gpu_metal::moe_q8::enabled()
18701 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18702 let experts = m
18703 .experts
18704 .iter()
18705 .map(|e| {
18706 if e.act != Act::Silu
18707 || !e.segs.is_empty()
18708 || e.gate_proj.rows() != inter
18709 || e.up_proj.rows() != inter
18710 || e.down_proj.cols() != inter
18711 || e.gate_proj.cols() != hidden
18712 || e.down_proj.rows() != hidden
18713 {
18714 return None;
18715 }
18716 if q8 {
18717 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18718 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18719 }
18720 Some((
18721 e.gate_proj.mapped_q4tp()?.1,
18722 e.up_proj.mapped_q4tp()?.1,
18723 e.down_proj.mapped_q4tp()?.1,
18724 ))
18725 })
18726 .collect::<Option<Vec<_>>>()?;
18727 Some((rf, experts, q8))
18728}
18729
18730#[cfg(target_os = "macos")]
18738fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
18739 if m.router_input_norm
18740 || m.route_tau.is_some()
18741 || m.mask.is_some()
18742 || m.per_expert_scale.is_some()
18743 || m.experts.is_empty()
18744 || m.top_k == 0
18745 || m.resonance.is_some()
18746 {
18747 return None;
18748 }
18749 let (sh, sg) = match &m.shared {
18754 Some((sh, sg)) => (Some(sh), sg.as_ref()),
18755 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
18756 None => return None,
18757 };
18758 let (rf, rr, rc) = m.router.f32_parts()?;
18759 if rr != m.experts.len() || rc != hidden {
18760 return None;
18761 }
18762 let shared_gated = sg.is_some();
18763 let sf = match sg {
18764 Some(sg) => {
18765 let (sf, sr, sc) = sg.f32_parts()?;
18766 if sr * sc != hidden {
18767 return None;
18768 }
18769 sf
18770 }
18771 None => &rf[..hidden],
18774 };
18775 if let Some(b) = &m.expert_bias {
18776 if b.len() != m.experts.len() {
18777 return None;
18778 }
18779 }
18780 let inter = m.experts[0].gate_proj.rows();
18781 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
18784 let q8 = crate::gpu_metal::moe_q8::enabled()
18788 && m.shared.is_none()
18789 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18790 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
18791 if e.act != Act::Silu
18792 || e.gate_proj.rows() != inter
18793 || e.gate_proj.cols() != hidden
18794 || e.up_proj.rows() != inter
18795 || e.up_proj.cols() != hidden
18796 || e.down_proj.rows() != hidden
18797 || e.down_proj.cols() != inter
18798 {
18799 return None;
18800 }
18801 if q8 {
18802 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18803 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18804 }
18805 let pick = |t: &QTensor| -> Option<usize> {
18806 if gu_q2 {
18807 t.mapped_q2tp().map(|(_, i)| i)
18808 } else {
18809 t.mapped_q4tp().map(|(_, i)| i)
18810 }
18811 };
18812 Some((
18813 pick(&e.gate_proj)?,
18814 pick(&e.up_proj)?,
18815 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
18816 ))
18817 };
18818 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
18819 let shared = match sh {
18820 Some(sh) => Some(trio(sh)?),
18821 None => None,
18822 };
18823 Some(crate::gpu::GpuMoe {
18824 router: rf,
18825 sgate: sf,
18826 experts,
18827 shared,
18828 n_exp: m.experts.len(),
18829 top_k: m.top_k,
18830 inter,
18831 norm_topk: m.norm_topk_prob,
18832 route_scale: m.routed_scaling,
18833 gu_q2,
18834 q8,
18835 sigmoid: m.router_sigmoid,
18836 bias: m.expert_bias.as_deref(),
18837 shared_gated,
18838 })
18839}
18840
18841fn forced_ids_at(step: usize) -> Option<u32> {
18847 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
18848 IDS.get_or_init(|| {
18849 std::env::var("CMF_FORCE_IDS")
18850 .ok()
18851 .and_then(|p| std::fs::read_to_string(p).ok())
18852 .map(|s| s.split_whitespace().filter_map(|t| t.parse().ok()).collect())
18853 .unwrap_or_default()
18854 })
18855 .get(step)
18856 .copied()
18857}
18858
18859pub(crate) fn moe_push_job_parts<'a>(
18863 gate: &'a QTensor,
18864 up: &'a QTensor,
18865 down: &'a QTensor,
18866 x: &[f32],
18867 w: f32,
18868 swiglu_limit: f32,
18869 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18870 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18871) -> Option<()> {
18872 use crate::qtensor::prescale;
18873 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
18874 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
18875 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
18876 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18877 return None; }
18879 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
18882 return None;
18883 }
18884 if !gq2 && dq2 {
18885 return None;
18886 }
18887 model_ref.get_or_insert_with(|| gm.clone());
18888 let dt = |cf: &[f32]| {
18889 if cf.is_empty() {
18890 cortiq_core::TensorDtype::Q8Row
18891 } else {
18892 cortiq_core::TensorDtype::Q8_2f
18893 }
18894 };
18895 jobs.push(crate::gpu::MoeJob {
18896 gate: (gi, gr, gc, grs),
18897 up: (ui, ur, uc, urs),
18898 down: (di, dr, dc, drs),
18899 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
18900 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
18901 down_col: dcf,
18902 w,
18903 q1: gq1,
18904 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
18905 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
18906 gu_q2: gq2,
18907 swiglu_limit,
18908 act: 0,
18909 });
18910 Some(())
18911}
18912
18913fn moe_push_job<'a>(
18915 d: &'a DenseFfn,
18916 x: &[f32],
18917 w: f32,
18918 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18919 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18920) -> Option<()> {
18921 use crate::qtensor::prescale;
18922 let act = d.act.graph_act()?.code();
18925 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
18926 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
18927 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
18928 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18929 return None; }
18931 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
18932 return None;
18933 }
18934 if !gq2 && dq2 {
18935 return None;
18936 }
18937 model_ref.get_or_insert_with(|| gm.clone());
18938 let gdt = if gcf.is_empty() {
18939 cortiq_core::TensorDtype::Q8Row
18940 } else {
18941 cortiq_core::TensorDtype::Q8_2f
18942 };
18943 let udt = if ucf.is_empty() {
18944 cortiq_core::TensorDtype::Q8Row
18945 } else {
18946 cortiq_core::TensorDtype::Q8_2f
18947 };
18948 jobs.push(crate::gpu::MoeJob {
18949 gate: (gi, gr, gc, grs),
18950 up: (ui, ur, uc, urs),
18951 down: (di, dr, dc, drs),
18952 xs_gate: prescale(x, gcf, gdt).into_owned(),
18953 xs_up: prescale(x, ucf, udt).into_owned(),
18954 down_col: dcf,
18955 w,
18956 q1: gq1,
18957 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
18958 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
18959 gu_q2: gq2,
18960 swiglu_limit: 0.0,
18961 act,
18962 });
18963 Some(())
18964}
18965
18966fn sparse_ffn_quant(
18973 d: &DenseFfn,
18974 x: &[f32],
18975 active: &[u16],
18976 hidden: usize,
18977 pool: Option<&Pool>,
18978) -> Vec<f32> {
18979 let n = active.len();
18980 let inter = d.gate_proj.rows();
18981 let mut act = vec![0.0f32; n];
18982 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
18985 let compute = |ai: usize| -> f32 {
18986 let idx = active[ai] as usize;
18987 if idx >= inter {
18988 return 0.0; }
18990 let mut s = if need_scratch {
18991 vec![0.0f32; hidden]
18992 } else {
18993 Vec::new()
18994 };
18995 let gate = d.gate_proj.row_dot(idx, x, &mut s);
18996 let up = d.up_proj.row_dot(idx, x, &mut s);
18997 d.act.combine(gate, up)
18998 };
18999 match pool {
19000 Some(p) if n >= 256 => {
19001 let ptr = SendMut(act.as_mut_ptr());
19002 p.run(&|widx, nw| {
19003 let chunk = n.div_ceil(nw);
19004 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19005 for ai in s..e {
19006 unsafe { *ptr.at(ai) = compute(ai) };
19007 }
19008 });
19009 }
19010 _ => {
19011 for (ai, a) in act.iter_mut().enumerate() {
19012 *a = compute(ai);
19013 }
19014 }
19015 }
19016 let mut out = vec![0.0f32; hidden];
19018 for (ai, &idx) in active.iter().enumerate() {
19019 let w = act[ai];
19020 if w.abs() >= 1e-12 && (idx as usize) < inter {
19021 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19022 }
19023 }
19024 out
19025}
19026
19027#[doc(hidden)]
19029pub fn sparse_ffn_quant_for_test(
19030 d: &DenseFfn,
19031 x: &[f32],
19032 active: &[u16],
19033 hidden: usize,
19034) -> Vec<f32> {
19035 sparse_ffn_quant(d, x, active, hidden, None)
19036}
19037
19038fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19042 let deq = |t: &QTensor| -> Vec<f32> {
19043 let (rows, cols) = (t.rows(), t.cols());
19044 let mut out = vec![0.0f32; rows * cols];
19045 for r in 0..rows {
19046 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19047 }
19048 out
19049 };
19050 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19051}
19052
19053struct SendMut(*mut f32);
19055unsafe impl Send for SendMut {}
19056unsafe impl Sync for SendMut {}
19057impl SendMut {
19058 #[inline]
19059 #[allow(clippy::mut_from_ref)]
19062 unsafe fn at(&self, i: usize) -> &mut f32 {
19063 unsafe { &mut *self.0.add(i) }
19064 }
19065}
19066
19067pub(crate) fn moe_route(
19080 logits: &[f32],
19081 m: &MoeFfn,
19082 allowed: Option<&[bool]>,
19083) -> (Vec<usize>, Vec<f32>, f32) {
19084 moe_route_with_eps(logits, m, allowed, 1e-6)
19085}
19086
19087pub(crate) fn moe_route_with_eps(
19096 logits: &[f32],
19097 m: &MoeFfn,
19098 allowed: Option<&[bool]>,
19099 sigmoid_denom_eps: f32,
19100) -> (Vec<usize>, Vec<f32>, f32) {
19101 let ne = logits.len();
19102 let admit = |e: usize| {
19108 m.mask.as_ref().is_none_or(|mk| mk[e])
19109 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19110 };
19111 if m.resonance.is_some() && m.top_k == 1 {
19124 let mut best: Option<usize> = None;
19125 for e in (0..ne).filter(|&e| admit(e)) {
19126 let l = logits[e];
19127 if l == f32::NEG_INFINITY || l.is_nan() {
19128 continue;
19129 }
19130 if best.is_none_or(|b| l > logits[b]) {
19131 best = Some(e);
19132 }
19133 }
19134 if let Some(b) = best {
19135 let mut p = vec![0.0f32; ne];
19136 p[b] = 1.0;
19137 return (vec![b], p, 1.0 / m.routed_scaling);
19138 }
19139 }
19140 let p: Vec<f32> = if m.router_sigmoid {
19146 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19147 } else {
19148 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19149 if mx == f32::NEG_INFINITY {
19150 vec![1.0 / ne.max(1) as f32; ne]
19151 } else {
19152 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
19153 let s: f32 = e.iter().sum();
19154 for v in &mut e {
19155 *v /= s;
19156 }
19157 e
19158 }
19159 };
19160 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
19161 match &m.expert_bias {
19163 Some(b) => idx.sort_unstable_by(|&x, &y| {
19164 (p[y] + b[y])
19165 .partial_cmp(&(p[x] + b[x]))
19166 .unwrap()
19167 .then(x.cmp(&y))
19168 }),
19169 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
19170 }
19171 idx.truncate(m.top_k);
19172 if let Some(tau) = m.route_tau {
19176 let total: f32 = idx.iter().map(|&e| p[e]).sum();
19177 if total > 0.0 {
19178 let mut acc = 0.0f32;
19179 let mut keep = idx.len();
19180 for (i, &e) in idx.iter().enumerate() {
19181 acc += p[e];
19182 if acc >= tau * total {
19183 keep = i + 1;
19184 break;
19185 }
19186 }
19187 idx.truncate(keep);
19188 }
19189 }
19190 let wsum: f32 = if m.norm_topk_prob {
19191 let s: f32 = idx.iter().map(|&e| p[e]).sum();
19192 (if m.router_sigmoid {
19196 s + sigmoid_denom_eps
19197 } else {
19198 s
19199 }) / m.routed_scaling
19200 } else {
19201 1.0 / m.routed_scaling
19202 };
19203 (idx, p, wsum)
19204}
19205
19206fn moe_trace(idx: &[usize]) {
19208 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
19209}
19210
19211pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
19214 use std::io::Write;
19215 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
19216 std::sync::OnceLock::new();
19217 let Some(f) = F.get_or_init(|| {
19218 let p = std::env::var("CMF_MOE_TRACE").ok()?;
19219 Some(std::sync::Mutex::new(
19220 std::fs::OpenOptions::new()
19221 .create(true)
19222 .append(true)
19223 .open(p)
19224 .ok()?,
19225 ))
19226 }) else {
19227 return;
19228 };
19229 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
19230 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
19231}
19232
19233pub(crate) fn moe_ffn(
19236 m: &MoeFfn,
19237 x: &[f32],
19238 pool: Option<&Pool>,
19239 allowed: Option<&[bool]>,
19240) -> Vec<f32> {
19241 let r = moe_ffn_route(m, x, pool, allowed);
19242 moe_ffn_experts(m, x, &r, pool)
19243}
19244
19245pub(crate) struct MoeRoute {
19249 pub idx: Vec<usize>,
19250 pub p: Vec<f32>,
19251 pub wsum: f32,
19252 pub logits: Vec<f32>,
19253}
19254
19255pub(crate) fn moe_ffn_route(
19261 m: &MoeFfn,
19262 x: &[f32],
19263 pool: Option<&Pool>,
19264 allowed: Option<&[bool]>,
19265) -> MoeRoute {
19266 accumulate_act(m, x, 1);
19267 let ne = m.experts.len();
19268 let mut logits = vec![0.0f32; ne];
19269 match &m.resonance {
19270 Some(r) => r.scores(x, &mut logits),
19271 None => m.router.matvec(x, &mut logits, pool),
19272 }
19273 let (idx, p, wsum) = moe_route(&logits, m, allowed);
19274 {
19275 let mut st = m.stats.borrow_mut();
19276 if st.len() < ne {
19277 st.resize(ne, 0);
19278 }
19279 for &e in &idx {
19280 st[e] += 1;
19281 }
19282 }
19283 moe_trace(&idx);
19289 MoeRoute {
19290 idx,
19291 p,
19292 wsum,
19293 logits,
19294 }
19295}
19296
19297pub(crate) fn moe_ffn_experts(
19300 m: &MoeFfn,
19301 x: &[f32],
19302 r: &MoeRoute,
19303 pool: Option<&Pool>,
19304) -> Vec<f32> {
19305 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
19306 if crate::gpu::enabled_here() {
19311 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
19312 crate::gpu::ProbeArm::Gpu => {
19313 let t0 = std::time::Instant::now();
19314 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
19315 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
19316 return out;
19317 }
19318 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
19319 }
19320 crate::gpu::ProbeArm::CpuTimed => {
19321 let t0 = std::time::Instant::now();
19322 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19323 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
19324 return out;
19325 }
19326 crate::gpu::ProbeArm::Cpu => {
19327 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19328 }
19329 }
19330 }
19331 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
19332}
19333
19334fn moe_ffn_minicpm_tail(
19338 slot: &mut crate::minicpm_tail::MiniCpmTail,
19339 li: usize,
19340 total_layers: usize,
19341 m: &MoeFfn,
19342 x: &[f32],
19343 pool: Option<&Pool>,
19344) -> Vec<f32> {
19345 let route = moe_ffn_route(m, x, pool, None);
19346 match slot.forward(li, m, x, &route, pool, total_layers) {
19347 Some(out) => out,
19348 None => moe_ffn_experts(m, x, &route, pool),
19353 }
19354}
19355
19356fn moe_ffn_banked(
19360 slot: &mut crate::mimo_moe::Slot,
19361 li: usize,
19362 m: &MoeFfn,
19363 x: &[f32],
19364 pool: Option<&Pool>,
19365) -> Vec<f32> {
19366 let t0 = std::time::Instant::now();
19367 let r = moe_ffn_route(m, x, pool, None);
19368 slot.note_route(t0.elapsed().as_nanos() as u64);
19369 match slot.forward(li, m, x, &r, pool) {
19370 Some(out) => out,
19371 None => crate::qtensor::float_activations_scope(|| {
19372 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19373 }),
19374 }
19375}
19376
19377fn moe_ffn_banked_rows(
19379 slot: &mut crate::mimo_moe::Slot,
19380 li: usize,
19381 m: &MoeFfn,
19382 xs: &[f32],
19383 b: usize,
19384 hidden: usize,
19385 pool: Option<&Pool>,
19386) -> Vec<f32> {
19387 let t0 = std::time::Instant::now();
19388 let routes: Vec<_> = xs
19389 .chunks_exact(hidden)
19390 .map(|x| moe_ffn_route(m, x, pool, None))
19391 .collect();
19392 slot.note_route(t0.elapsed().as_nanos() as u64);
19393 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19394 return out;
19395 }
19396 let mut out = Vec::with_capacity(b * hidden);
19397 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19398 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19399 crate::qtensor::float_activations_scope(|| {
19401 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19402 })
19403 });
19404 out.extend(row);
19405 }
19406 out
19407}
19408
19409fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19414 use std::sync::atomic::{AtomicBool, Ordering};
19415 if built {
19416 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19417 if total_layers > 0 && layers_run < total_layers {
19418 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19419 } else {
19420 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19421 }
19422 } else {
19423 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19424 }
19425 static SAID: AtomicBool = AtomicBool::new(false);
19426 if !SAID.swap(true, Ordering::Relaxed) {
19427 if built {
19428 tracing::info!("wgpu whole-token graph: ACTIVE");
19429 } else {
19430 tracing::warn!("wgpu whole-token graph refused — per-op path");
19431 }
19432 }
19433}
19434
19435pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19439pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19440pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19444pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19446
19447pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
19451 std::sync::atomic::AtomicU64::new(0);
19452pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
19453 std::sync::atomic::AtomicU64::new(0);
19454pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19455 std::sync::atomic::AtomicU64::new(0);
19456pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
19457 std::sync::atomic::AtomicU64::new(0);
19458pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
19459 std::sync::atomic::AtomicU64::new(0);
19460pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19464 std::sync::atomic::AtomicU64::new(0);
19465pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
19466 std::sync::atomic::AtomicU64::new(0);
19467pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19468 std::sync::atomic::AtomicU64::new(0);
19469pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19470 std::sync::atomic::AtomicU64::new(0);
19471
19472fn moe_batch_enabled() -> bool {
19475 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19476 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19477}
19478
19479fn moe_ffn_cpu_batched(
19485 m: &MoeFfn,
19486 x: &[f32],
19487 idx: &[usize],
19488 p: &[f32],
19489 wsum: f32,
19490 pool: Option<&Pool>,
19491) -> Option<Vec<f32>> {
19492 if idx.is_empty() || !moe_batch_enabled() {
19493 return None;
19494 }
19495 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19499 return None;
19500 }
19501 let n = idx.len() + usize::from(m.shared.is_some());
19502 let mut pairs = Vec::with_capacity(n);
19503 let mut downs = Vec::with_capacity(n);
19504 let mut ws = Vec::with_capacity(n);
19505 for &e in idx {
19506 let d = &m.experts[e];
19507 if d.act != Act::Silu {
19508 return None;
19509 }
19510 pairs.push((&d.gate_proj, &d.up_proj));
19511 downs.push(&d.down_proj);
19512 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19513 }
19514 if let Some((se, gate)) = &m.shared {
19517 if se.act != Act::Silu {
19518 return None;
19519 }
19520 let g = gate.as_ref().map_or(1.0, |gate| {
19521 let mut gl = [0.0f32; 1];
19522 gate.matvec(x, &mut gl, pool);
19523 1.0 / (1.0 + (-gl[0]).exp())
19524 });
19525 pairs.push((&se.gate_proj, &se.up_proj));
19526 downs.push(&se.down_proj);
19527 ws.push(g);
19528 }
19529 let inter = pairs[0].0.rows();
19530 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19531 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19532 return None;
19533 }
19534 let mut out = attention::take_buf(x.len());
19535 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19536 attention::recycle_buf(&mut out);
19537 return None;
19538 }
19539 Some(out)
19540}
19541
19542pub(crate) fn moe_cold_experts_cpu(
19548 experts: &[(&DenseFfn, f32)],
19549 x: &[f32],
19550 pool: Option<&Pool>,
19551) -> Vec<f32> {
19552 let mut out = attention::take_buf(x.len());
19553 if experts.is_empty() {
19554 return out;
19555 }
19556 let pairs: Vec<_> = experts
19557 .iter()
19558 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19559 .collect();
19560 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19561 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19562 let inter = experts[0].0.gate_proj.rows();
19563 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19564 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19565 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19566 {
19567 return out;
19568 }
19569 out.fill(0.0);
19570 for &(expert, weight) in experts {
19571 let mut one = dense_ffn(expert, x, pool);
19572 for (o, v) in out.iter_mut().zip(&one) {
19573 *o += weight * v;
19574 }
19575 attention::recycle_buf(&mut one);
19576 }
19577 out
19578}
19579
19580pub(crate) fn moe_cold_experts_rows_cpu(
19584 jobs: &[Vec<(&DenseFfn, f32)>],
19585 xs: &[f32],
19586 hidden: usize,
19587 pool: Option<&Pool>,
19588) -> Vec<f32> {
19589 let mut out = vec![0.0; xs.len()];
19590 let mut experts: Vec<&DenseFfn> = Vec::new();
19591 let mut groups: Vec<Vec<usize>> = Vec::new();
19592 let mut terms = vec![Vec::new(); jobs.len()];
19593 for (r, row) in jobs.iter().enumerate() {
19594 for &(e, w) in row {
19595 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
19596 Some(g) => g,
19597 None => {
19598 experts.push(e);
19599 groups.push(Vec::new());
19600 groups.len() - 1
19601 }
19602 };
19603 terms[r].push((g, groups[g].len(), w));
19604 groups[g].push(r);
19605 }
19606 }
19607 if experts.is_empty() {
19608 return out;
19609 }
19610 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
19611 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
19612 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
19613 let count: usize = lens.iter().sum();
19614 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
19615 let mut ds = vec![vec![0.0; hidden]; count];
19616 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
19617 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
19618 {
19619 let mut offset = 0;
19620 let offsets: Vec<_> = lens
19621 .iter()
19622 .map(|&n| {
19623 let start = offset;
19624 offset += n;
19625 start
19626 })
19627 .collect();
19628 for (r, terms) in terms.iter().enumerate() {
19629 for &(g, slot, w) in terms {
19630 for (o, &v) in out[r * hidden..(r + 1) * hidden]
19631 .iter_mut()
19632 .zip(&ds[offsets[g] + slot])
19633 {
19634 *o += w * v;
19635 }
19636 }
19637 }
19638 } else {
19639 for (r, jobs) in jobs.iter().enumerate() {
19640 if !jobs.is_empty() {
19641 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
19642 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
19643 attention::recycle_buf(&mut row);
19644 }
19645 }
19646 }
19647 out
19648}
19649
19650fn moe_ffn_cpu(
19652 m: &MoeFfn,
19653 x: &[f32],
19654 idx: &[usize],
19655 p: &[f32],
19656 wsum: f32,
19657 pool: Option<&Pool>,
19658) -> Vec<f32> {
19659 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
19660 return out;
19661 }
19662 let mut out = attention::take_buf(x.len());
19663 for &e in idx {
19664 let mut eo = dense_ffn(&m.experts[e], x, pool);
19665 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
19666 for i in 0..out.len() {
19667 out[i] += w * eo[i];
19668 }
19669 attention::recycle_buf(&mut eo);
19670 }
19671 if let Some((se, gate)) = &m.shared {
19672 let mut so = dense_ffn(se, x, pool);
19673 let g = gate.as_ref().map_or(1.0, |gate| {
19674 let mut gl = [0.0f32; 1];
19675 gate.matvec(x, &mut gl, pool);
19676 1.0 / (1.0 + (-gl[0]).exp())
19677 });
19678 for i in 0..out.len() {
19679 out[i] += g * so[i];
19680 }
19681 attention::recycle_buf(&mut so);
19682 }
19683 out
19684}
19685
19686#[allow(clippy::too_many_arguments)]
19694pub(crate) fn mla_attention(
19695 w: &MlaWeights,
19696 normed: &[f32],
19697 cache: &mut crate::kv_cache::LayerKvCache,
19698 position: usize,
19699 inv_freq: &[f32],
19700 rope_scale: f32,
19701 eps: f64,
19702 pool: Option<&Pool>,
19703) -> Vec<f32> {
19704 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
19705 let hd = dr + dn;
19706 let mut q = vec![0.0f32; nh * hd];
19707 match (&w.q_a, &w.q_a_norm) {
19708 (Some(qa), Some(qn)) => {
19709 let mut t = vec![0.0f32; qa.rows()];
19710 qa.matvec(normed, &mut t, pool);
19711 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
19712 w.q_proj.matvec(&tn, &mut q, pool);
19713 }
19714 _ => w.q_proj.matvec(normed, &mut q, pool),
19715 }
19716 let mut ca = vec![0.0f32; lora + dr];
19717 w.kv_a.matvec(normed, &mut ca, pool);
19718 let (c_lat, k_rope) = ca.split_at_mut(lora);
19719 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
19720 let mut kvb = vec![0.0f32; nh * (dn + dv)];
19721 w.kv_b.matvec(&latn, &mut kvb, pool);
19722 if !w.nope {
19723 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
19724 }
19725 for h in 0..nh {
19726 if !w.nope {
19727 attention::rope_rotate_scaled(
19728 &mut q[h * hd..h * hd + dr],
19729 position,
19730 inv_freq,
19731 rope_scale,
19732 );
19733 }
19734 }
19735 let mut k = vec![0.0f32; nh * hd];
19736 let mut v = vec![0.0f32; nh * hd];
19737 for h in 0..nh {
19738 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
19739 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
19740 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
19741 }
19742 cache.append(&k, &v, &vec![true; nh]);
19743 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
19744 attention::recycle_buf(&mut imp);
19745 let mut ov = vec![0.0f32; nh * dv];
19746 for h in 0..nh {
19747 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
19748 }
19749 let mut out = vec![0.0f32; w.o_proj.rows()];
19750 w.o_proj.matvec(&ov, &mut out, pool);
19751 out
19752}
19753
19754fn dense_moe_ffn(
19761 dm: &DenseMoeFfn,
19762 x_normed: &[f32],
19763 h_raw: &[f32],
19764 eps: f64,
19765 norm_style: NormStyle,
19766 pool: Option<&Pool>,
19767) -> Vec<f32> {
19768 let mut d = dense_ffn(&dm.dense, x_normed, pool);
19769 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
19770 let m = &dm.moe;
19771 let ne = m.experts.len();
19772 let mut logits = vec![0.0f32; ne];
19773 if m.router_input_norm {
19774 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
19775 let inv = 1.0 / (ss + eps as f32).sqrt();
19776 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
19777 m.router.matvec(&xr, &mut logits, pool);
19778 } else {
19779 m.router.matvec(h_raw, &mut logits, pool);
19780 }
19781 let (idx, p, wsum) = moe_route(&logits, m, None);
19782 {
19783 let mut st = m.stats.borrow_mut();
19784 if st.len() < ne {
19785 st.resize(ne, 0);
19786 }
19787 for &e in &idx {
19788 st[e] += 1;
19789 }
19790 }
19791 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
19792 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
19793 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
19794 for (di, mi) in d.iter_mut().zip(&mo) {
19795 *di += mi;
19796 }
19797 d
19798}
19799
19800fn moe_gpu_refused(why: &'static str) {
19807 use std::sync::atomic::{AtomicBool, Ordering};
19808 static SAID: AtomicBool = AtomicBool::new(false);
19809 if !SAID.swap(true, Ordering::Relaxed) {
19810 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
19811 }
19812}
19813
19814fn moe_ffn_gpu(
19815 m: &MoeFfn,
19816 x: &[f32],
19817 idx: &[usize],
19818 p: &[f32],
19819 wsum: f32,
19820 pool: Option<&Pool>,
19821) -> Option<Vec<f32>> {
19822 use crate::gpu::MoeJob;
19823
19824 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
19825 let mut model_ref = None;
19826 for &e in idx {
19827 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
19828 moe_gpu_refused("push_job(expert)");
19829 return None;
19830 }
19831 }
19832 if let Some((se, gate)) = &m.shared {
19833 let g = gate.as_ref().map_or(1.0, |gate| {
19834 let mut gl = [0.0f32; 1];
19835 gate.matvec(x, &mut gl, pool);
19836 1.0 / (1.0 + (-gl[0]).exp())
19837 });
19838 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
19839 moe_gpu_refused("push_job(shared)");
19840 return None;
19841 }
19842 }
19843 let Some(model) = model_ref else {
19844 moe_gpu_refused("no model_ref");
19845 return None;
19846 };
19847 let hidden = jobs[0].down.1;
19848 let mut out = vec![0.0f32; hidden];
19849 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19850 Some(out)
19851 } else {
19852 moe_gpu_refused("gpu::moe_block");
19853 None
19854 }
19855}
19856
19857fn ffn_forward(
19859 ffn: &FfnKind,
19860 x: &[f32],
19861 pool: Option<&Pool>,
19862 experts_allowed: Option<&[bool]>,
19863) -> Vec<f32> {
19864 match ffn {
19865 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
19866 FfnKind::Dense(d) => dense_ffn(d, x, pool),
19867 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
19868 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19872 }
19873}
19874
19875fn ffn_forward_pair(
19879 ffn: &FfnKind,
19880 x1: &[f32],
19881 x2: &[f32],
19882 pool: Option<&Pool>,
19883 experts_allowed: Option<&[bool]>,
19884) -> (Vec<f32>, Vec<f32>) {
19885 let d = match ffn {
19886 FfnKind::Dense(d) if !d.segs.is_empty() => {
19889 return (
19890 tube_ffn(d, x1, 1, pool, None),
19891 tube_ffn(d, x2, 1, pool, None),
19892 );
19893 }
19894 FfnKind::Dense(d) => d,
19895 FfnKind::Moe(m) => {
19896 return (
19897 moe_ffn(m, x1, pool, experts_allowed),
19898 moe_ffn(m, x2, pool, experts_allowed),
19899 );
19900 }
19901 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19902 };
19903 let inter = d.gate_proj.rows();
19904 FFN_SCRATCH.with(|s| {
19905 let mut s = s.borrow_mut();
19906 let [g1, g2, u1, u2] = &mut *s;
19907 g1.resize(inter, 0.0);
19908 g2.resize(inter, 0.0);
19909 u1.resize(inter, 0.0);
19910 u2.resize(inter, 0.0);
19911 QTensor::matvec2_many(
19914 [&d.gate_proj, &d.up_proj],
19915 x1,
19916 x2,
19917 [g1.as_mut_slice(), u1.as_mut_slice()],
19918 [g2.as_mut_slice(), u2.as_mut_slice()],
19919 pool,
19920 );
19921 for i in 0..inter {
19922 g1[i] = d.act.combine(g1[i], u1[i]);
19923 g2[i] = d.act.combine(g2[i], u2[i]);
19924 }
19925 let mut o1 = attention::take_buf(d.down_proj.rows());
19926 let mut o2 = attention::take_buf(d.down_proj.rows());
19927 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
19928 (o1, o2)
19929 })
19930}
19931
19932#[cfg(test)]
19933mod tests {
19934
19935 #[test]
19940 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
19941 use super::{
19942 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
19943 };
19944 let dense_card = ChunkStackFacts {
19945 plain_dense: true,
19946 discrete: true,
19947 gpu_on: true,
19948 ..Default::default()
19949 };
19950 assert!(dense_card.dense_on_discrete());
19951 assert_eq!(
19953 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
19954 DISCRETE_DENSE_PREFILL_CHUNK
19955 );
19956 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
19957 for (label, facts) in [
19958 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
19959 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
19960 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
19961 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
19962 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
19963 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
19964 ] {
19965 assert!(!facts.dense_on_discrete(), "{label}");
19966 assert_eq!(
19967 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
19968 48,
19969 "{label} keeps the historical x86 chunk"
19970 );
19971 }
19972 for dense in [false, true] {
19974 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
19975 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
19976 }
19977 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
19979 for dense in [false, true] {
19980 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
19981 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
19982 }
19983 }
19984 }
19985
19986 #[test]
19987 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
19988 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
19989 let full = |host_rows, device_rows| ReuseLayer {
19990 full: true,
19991 host_rows,
19992 device_rows,
19993 device_state: false,
19994 };
19995 assert_eq!(
19998 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
19999 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
20000 );
20001 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
20003 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
20005 assert_eq!(
20007 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
20008 ReusePlan::Pull(vec![(0, 300, 339)])
20009 );
20010 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
20012 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
20013 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
20014 let conv = |device_state| ReuseLayer {
20017 full: false,
20018 host_rows: 0,
20019 device_rows: None,
20020 device_state,
20021 };
20022 assert_eq!(
20023 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
20024 ReusePlan::Fresh
20025 );
20026 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
20027 }
20028
20029 #[test]
20030 fn nll_graph_policy_scopes_only_the_fused_head() {
20031 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
20032 ("vulkan graph", true, true, false, true, false),
20034 ("native Metal graph", true, true, true, true, true),
20036 ("masked", false, true, false, false, false),
20038 ("graph disabled", true, false, true, false, false),
20039 ] {
20040 let (graph_quality, graph_head_required) =
20041 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
20042 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
20043 assert_eq!(graph_head_required, want_head, "{label}: fused head");
20044 }
20045 }
20046
20047 #[test]
20048 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
20049 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
20050 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
20051 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
20052 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
20053 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
20054 }
20055
20056 #[test]
20057 fn cancel_flag_stops_generation() {
20058 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
20059 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20062 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
20063 assert_eq!(r.finish_reason, "cancelled");
20064 assert!(
20065 r.token_ids.is_empty(),
20066 "no tokens after cancel: {:?}",
20067 r.token_ids
20068 );
20069 assert_eq!(p.kv_cache.seq_len(), 0);
20070 assert!(p.kv_history.is_empty());
20071 assert!(!p.graph_want_logits);
20072 assert!(p.graph_logits.is_none());
20073 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
20075 assert_ne!(r2.finish_reason, "cancelled");
20076 }
20077 use super::*;
20078
20079 #[test]
20080 fn explicit_sliding_schedule_selects_local_rope_profile() {
20081 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
20086 p.swa = Some((1024, usize::MAX));
20087 p.sliding_layers = Some(vec![true, true, true, false]);
20088 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
20089 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
20090 p.rope_scale = 1.277_258_9;
20091 p.rope_scale_local = 1.0;
20092
20093 for li in 0..3 {
20094 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
20095 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
20096 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
20097 }
20098 assert_eq!(p.layer_window(3), None);
20099 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
20100 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
20101 }
20102
20103 #[test]
20111 fn dynamic_ffn_equals_the_zeroing_arm() {
20112 let (hidden, inter) = (8usize, 32usize);
20113 let synth = |n: usize, salt: usize| -> Vec<f32> {
20114 (0..n)
20115 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
20116 .collect()
20117 };
20118 let down = synth(hidden * inter, 3);
20119 let mut down_t = vec![0.0f32; inter * hidden];
20120 for r in 0..hidden {
20121 for c in 0..inter {
20122 down_t[c * hidden + r] = down[r * inter + c];
20123 }
20124 }
20125 let d = DenseFfn {
20126 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20127 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20128 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
20129 act: Act::Silu,
20130 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
20131 segs: Vec::new(),
20132 };
20133 let x = synth(hidden, 11);
20134 let k = 12usize;
20135 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
20136 let mut g = vec![0.0f32; inter];
20138 d.gate_proj.matvec(&x, &mut g, None);
20139 let mut u = vec![0.0f32; inter];
20140 d.up_proj.matvec(&x, &mut u, None);
20141 for v in g.iter_mut() {
20142 *v = inference::silu(*v);
20143 }
20144 keep_top_k(&mut g, k);
20145 for i in 0..inter {
20146 g[i] *= u[i];
20147 }
20148 let mut want = vec![0.0f32; hidden];
20149 d.down_proj.matvec(&g, &mut want, None);
20150 for (a, b) in want.iter().zip(&got) {
20151 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
20152 }
20153 }
20154
20155 #[test]
20161 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
20162 let (hidden, core, tube) = (8usize, 12usize, 8usize);
20163 let inter = core + tube;
20164 let synth = |n: usize, salt: usize| -> Vec<f32> {
20165 (0..n)
20166 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
20167 .collect()
20168 };
20169 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
20170 let d_all = synth(hidden * inter, 3);
20171 let dense = DenseFfn {
20173 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
20174 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
20175 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
20176 act: Act::Silu,
20177 down_t: None,
20178 segs: Vec::new(),
20179 };
20180 let rows =
20181 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
20182 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
20183 let mut o = Vec::with_capacity(hidden * (b - a));
20184 for r in 0..hidden {
20185 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
20186 }
20187 o
20188 };
20189 let tubed = DenseFfn {
20190 down_t: None,
20191 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
20192 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
20193 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
20194 act: Act::Silu,
20195 segs: vec![FfnSeg {
20196 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
20197 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
20198 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
20199 start: core,
20200 width: tube,
20201 }],
20202 };
20203 let x = synth(hidden, 7);
20204 let want = dense_ffn(&dense, &x, None);
20205 let got = tube_ffn(&tubed, &x, 1, None, None);
20206 for (a, b) in want.iter().zip(&got) {
20207 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
20208 }
20209 let mut bits = vec![0u8; inter.div_ceil(8)];
20211 for n in 0..core {
20212 bits[n / 8] |= 1 << (n % 8);
20213 }
20214 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20215 let masked = dense_ffn_masked(&dense, &x, None, &bits);
20216 for (a, b) in masked.iter().zip(&closed) {
20217 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
20218 }
20219 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20221 for (a, b) in closed.iter().zip(&batch) {
20222 assert_eq!(a, b, "batch arm disagrees with decode arm");
20223 }
20224 }
20225
20226 #[test]
20228 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
20229 let (hidden, inter) = (16usize, 40usize);
20230 let synth = |n: usize, salt: usize| -> Vec<f32> {
20231 (0..n)
20232 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
20233 .collect()
20234 };
20235 let d = DenseFfn {
20236 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20237 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20238 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
20239 act: Act::Silu,
20240 down_t: None,
20241 segs: Vec::new(),
20242 };
20243 let x = synth(hidden, 9);
20244 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
20246
20247 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
20248
20249 let mut g = vec![0.0f32; inter];
20251 d.gate_proj.matvec(&x, &mut g, None);
20252 let mut u = vec![0.0f32; inter];
20253 d.up_proj.matvec(&x, &mut u, None);
20254 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
20255 for i in 0..inter {
20256 g[i] = if act_set.contains(&(i as u16)) {
20257 inference::silu(g[i]) * u[i]
20258 } else {
20259 0.0
20260 };
20261 }
20262 let mut reference = vec![0.0f32; hidden];
20263 d.down_proj.matvec(&g, &mut reference, None);
20264
20265 let max_d = sparse
20266 .iter()
20267 .zip(&reference)
20268 .map(|(a, b)| (a - b).abs())
20269 .fold(0.0f32, f32::max);
20270 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
20271 }
20272
20273 fn attach_test_mtp(p: &mut Pipeline) {
20275 let (h, inter, heads, kv, hd) = (
20276 p.hidden_size,
20277 p.intermediate_size,
20278 p.num_heads,
20279 p.num_kv_heads,
20280 p.head_dim,
20281 );
20282 let synth = |n: usize, salt: usize| -> Vec<f32> {
20283 (0..n)
20284 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
20285 .collect()
20286 };
20287 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
20288 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20289 };
20290 p.mtp = Some(MtpModule {
20291 enorm: vec![1.0; h],
20292 hnorm: vec![1.0; h],
20293 eh_proj: qt(h, 2 * h, 301),
20294 layer: LayerWeights {
20295 input_norm: vec![1.0; h],
20296 post_norm: vec![1.0; h],
20297 attn_out_norm: None,
20298 ffn_out_norm: None,
20299 layer_scale: None,
20300 ffn: FfnKind::Dense(DenseFfn {
20301 gate_proj: qt(inter, h, 315),
20302 up_proj: qt(inter, h, 316),
20303 down_proj: qt(h, inter, 317),
20304 act: Act::Silu,
20305 down_t: None,
20306 segs: Vec::new(),
20307 }),
20308 attn: AttnKind::Full {
20309 bias: None,
20310 wq: qt(heads * hd, h, 311),
20311 wk: qt(kv * hd, h, 312),
20312 wv: qt(kv * hd, h, 313),
20313 wo: qt(h, heads * hd, 314),
20314 q_norm: None,
20315 k_norm: None,
20316 output_gate: false,
20317 softplus_gate: None,
20318 },
20319 },
20320 final_norm: vec![1.0; h],
20321 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
20322 });
20323 }
20324
20325 #[test]
20326 fn speculative_equals_vanilla_greedy() {
20327 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20331 let run = |spec: bool| {
20332 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20333 p.sampler_config.temperature = 0.0;
20334 attach_test_mtp(&mut p);
20335 p.speculative = spec;
20336 let r = p.generate("abcdef", 12, None, None).unwrap();
20337 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
20338 };
20339 let (vanilla, d0, _) = run(false);
20340 let (spec, d1, a1) = run(true);
20341 assert_eq!(d0, 0, "vanilla path must not draft");
20342 assert!(d1 > 0, "speculative path must draft");
20343 assert_eq!(
20344 vanilla, spec,
20345 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
20346 );
20347 }
20348
20349 #[test]
20350 fn speculative_accepts_constant_oracle() {
20351 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20353 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20354 p.sampler_config.temperature = 0.0;
20355 p.sampler_config.repetition_penalty = 1.0;
20356 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
20359 attach_test_mtp(&mut p);
20360 p.speculative = true;
20361 let r = p.generate("abcd", 10, None, None).unwrap();
20362 assert!(r.mtp_drafted > 0);
20363 assert_eq!(
20364 r.mtp_accepted, r.mtp_drafted,
20365 "constant logits → every draft accepted"
20366 );
20367 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20370 }
20371
20372 #[test]
20373 fn empty_prompt_is_an_error_not_a_panic() {
20374 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20375 let r = p.generate("", 4, None, None);
20376 assert!(r.is_err(), "empty prompt must be a clean error");
20377 }
20378
20379 #[test]
20380 fn every_token_enters_kv_exactly_once() {
20381 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20382 p.sampler_config.temperature = 0.0;
20384 let r = p.generate("abc", 2, None, None).unwrap();
20385 assert_eq!(r.prompt_tokens, 3);
20386 assert_eq!(
20390 p.kv_cache.seq_len(),
20391 3 + r.tokens_generated - 1,
20392 "each token must be cached exactly once (v1 cached the last prompt token twice)"
20393 );
20394 }
20395
20396 #[test]
20397 fn generation_is_reproducible_with_seed() {
20398 let run = || {
20399 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20400 p.generate("hello", 8, None, None).unwrap().token_ids
20401 };
20402 assert_eq!(run(), run());
20403 }
20404
20405 #[test]
20406 fn resetting_sampler_restarts_the_seeded_stream() {
20407 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20408 let config = SamplerConfig {
20409 seed: Some(1234),
20410 ..SamplerConfig::default()
20411 };
20412 p.set_sampler_config(config.clone());
20413 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20414 p.set_sampler_config(config);
20415 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20416 assert_eq!(first, second);
20417 }
20418
20419 #[test]
20420 fn eviction_bounds_the_cache() {
20421 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20422 p.kv_cache.max_seq_len = 6;
20423 p.sampler_config.temperature = 0.0;
20424 let _ = p.generate("abcd", 12, None, None).unwrap();
20425 assert!(
20426 p.kv_cache.seq_len() <= 6 + 1,
20427 "cache must stay bounded by max_seq_len (got {})",
20428 p.kv_cache.seq_len()
20429 );
20430 }
20431
20432 #[test]
20433 fn confidence_matches_tokens_and_is_a_probability() {
20434 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20435 p.sampler_config.temperature = 0.0;
20436 p.sampler_config.repetition_penalty = 1.0;
20437 let r = p.generate("abcd", 10, None, None).unwrap();
20438 assert_eq!(
20439 r.token_confidence.len(),
20440 r.token_ids.len(),
20441 "one confidence per emitted token"
20442 );
20443 for &c in &r.token_confidence {
20444 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20445 }
20446 let logits = [1.0f32, 3.0, 0.5, 3.0];
20448 let p0 = top1_prob_t(&logits, 1, 1.0);
20449 let p1 = top1_prob_t(&logits, 3, 1.0);
20450 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20451 assert!(p0 > 0.0 && p0 < 1.0);
20452 let sharp = top1_prob_t(&logits, 1, 1.0);
20454 let soft = top1_prob_t(&logits, 1, 2.0);
20455 assert!(soft < sharp, "higher temperature lowers peak confidence");
20456 }
20457
20458 #[test]
20459 fn trace_is_opt_in_and_parallels_the_output() {
20460 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20462 p.sampler_config.temperature = 0.0;
20463 p.sampler_config.repetition_penalty = 1.0;
20464 let r = p.generate("abcd", 10, None, None).unwrap();
20465 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20466
20467 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20469 p.sampler_config.temperature = 0.0;
20470 p.sampler_config.repetition_penalty = 1.0;
20471 p.set_trace(true);
20472 let r = p.generate("abcd", 10, None, None).unwrap();
20473 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20474 for (i, tr) in r.traces.iter().enumerate() {
20475 assert_eq!(tr.t, i, "trace index is sequential");
20476 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20477 assert_eq!(
20478 tr.confidence, r.token_confidence[i],
20479 "trace confidence matches the confidence channel"
20480 );
20481 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20483 }
20484 }
20485
20486 #[test]
20487 fn explain_prefill_logits_match_greedy_first_token() {
20488 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20492 p.sampler_config.temperature = 0.0;
20493 p.sampler_config.repetition_penalty = 1.0;
20494 let ids = p.tokenizer.encode("abcd");
20495 let logits = p.prefill_next_logits(&ids, None);
20496 let argmax = logits
20497 .iter()
20498 .enumerate()
20499 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20500 .unwrap()
20501 .0 as u32;
20502 let r = p.generate("abcd", 1, None, None).unwrap();
20503 assert_eq!(
20504 argmax, r.token_ids[0],
20505 "explain preview must match greedy emit"
20506 );
20507 }
20508
20509 #[test]
20510 fn laguna_shared_expert_is_unconditionally_added() {
20511 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20512 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20513 let zero_dense = || DenseFfn {
20514 gate_proj: matrix(vec![0.0; 4]),
20515 up_proj: matrix(vec![0.0; 4]),
20516 down_proj: matrix(vec![0.0; 4]),
20517 act: Act::Silu,
20518 down_t: None,
20519 segs: Vec::new(),
20520 };
20521 let shared = DenseFfn {
20522 gate_proj: identity(),
20523 up_proj: identity(),
20524 down_proj: identity(),
20525 act: Act::Silu,
20526 down_t: None,
20527 segs: Vec::new(),
20528 };
20529 let x = [1.0, 2.0];
20530 let expected = dense_ffn(&shared, &x, None);
20531 let moe = MoeFfn {
20532 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20533 experts: vec![zero_dense()],
20534 top_k: 1,
20535 norm_topk_prob: true,
20536 router_sigmoid: true,
20537 expert_bias: None,
20538 routed_scaling: 1.0,
20539 route_tau: None,
20540 shared: Some((shared, None)),
20541 stats: std::cell::RefCell::new(Vec::new()),
20542 act_sq: std::cell::RefCell::new(Vec::new()),
20543 act_rows: std::cell::RefCell::new(Vec::new()),
20544 mask: None,
20545 per_expert_scale: None,
20546 router_input_norm: false,
20547 resonance: None,
20548 grown: Vec::new(),
20549 };
20550 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
20551 for (actual, expected) in actual.iter().zip(expected) {
20552 assert!((actual - expected).abs() < 1e-6);
20553 }
20554 }
20555
20556 fn mimo_test_pipeline() -> Pipeline {
20565 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
20566 let kvh = [1usize, 2, 2, 1];
20567 let synth = |n: usize, salt: usize| -> Vec<f32> {
20568 (0..n)
20569 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20570 .collect()
20571 };
20572 let qt = |rows: usize, cols: usize, salt: usize| {
20573 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20574 };
20575 let dense = |inter: usize, salt: usize| DenseFfn {
20576 gate_proj: qt(inter, hs, salt),
20577 up_proj: qt(inter, hs, salt + 1),
20578 down_proj: qt(hs, inter, salt + 2),
20579 act: Act::Silu,
20580 down_t: None,
20581 segs: Vec::new(),
20582 };
20583 let layers: Vec<LayerWeights> = (0..4)
20584 .map(|li| LayerWeights {
20585 input_norm: vec![1.0; hs],
20586 post_norm: vec![1.0; hs],
20587 attn_out_norm: None,
20588 ffn_out_norm: None,
20589 layer_scale: None,
20590 ffn: if li == 0 {
20591 FfnKind::Dense(dense(inter, 50))
20592 } else {
20593 FfnKind::Moe(MoeFfn {
20594 router: qt(4, hs, 60 + li),
20595 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
20596 top_k: 2,
20597 norm_topk_prob: true,
20598 router_sigmoid: true,
20599 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
20600 routed_scaling: 1.0,
20601 route_tau: None,
20602 shared: None,
20603 stats: std::cell::RefCell::new(Vec::new()),
20604 act_sq: std::cell::RefCell::new(Vec::new()),
20605 act_rows: std::cell::RefCell::new(Vec::new()),
20606 mask: None,
20607 per_expert_scale: None,
20608 router_input_norm: false,
20609 resonance: None,
20610 grown: Vec::new(),
20611 })
20612 },
20613 attn: AttnKind::Full {
20614 wq: qt(nh * hd, hs, li * 10 + 1),
20615 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
20616 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
20617 wo: qt(hs, nh * vd, li * 10 + 4),
20618 q_norm: None,
20619 k_norm: None,
20620 output_gate: false,
20621 softplus_gate: None,
20622 bias: None,
20623 },
20624 })
20625 .collect();
20626 let mut p = Pipeline::new(
20627 Tokenizer::byte_level(),
20628 PipelineWeights {
20629 embed_tokens: qt(vocab, hs, 100),
20630 layers,
20631 lm_head: qt(vocab, hs, 200),
20632 final_norm: vec![1.0; hs],
20633 },
20634 hs,
20635 inter,
20636 nh,
20637 1, hd,
20639 4,
20640 4,
20641 false,
20642 vocab,
20643 1e-6,
20644 1e7,
20645 NormStyle::Qwen,
20646 4096,
20647 SamplerConfig {
20648 seed: Some(7),
20649 ..Default::default()
20650 },
20651 );
20652 p.layer_dump = None;
20654 p.set_rotary(4, 1e7);
20655 p.sliding_layers = Some(vec![false, true, true, false]);
20656 p.swa = Some((3, usize::MAX));
20657 p.rotary_dim_local = Some(4);
20658 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
20659 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
20660 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
20661 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
20662 p
20663 }
20664
20665 #[test]
20666 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
20667 let mut p = mimo_test_pipeline();
20668 p.speculative = false;
20669 p.ignore_eos = true;
20670 p.sampler_config.temperature = 0.0;
20671 p.sampler_config.repetition_penalty = 1.0;
20672 let a = vec![3, 5, 7, 9, 11, 13];
20673 let b = vec![4, 8, 12, 16, 20, 24];
20674 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
20675 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
20676 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
20680 assert_eq!(actual, expected);
20681 assert!(p.kv_history.is_empty());
20682 let mut extended = a.clone();
20683 extended.push(17);
20684 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20685 p.reset_session();
20686 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20687 assert_eq!(after_media, fresh);
20688 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
20689 p.reset_session();
20693 p.generate_from_ids(&a, 1, None, None).unwrap();
20694 let mut media_ids = p.kv_history.clone();
20695 assert!(!media_ids.is_empty());
20696 media_ids.extend_from_slice(&[19, 21, 23]);
20697 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
20698 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
20699 let mut oracle = mimo_test_pipeline();
20700 oracle.speculative = false;
20701 oracle.ignore_eos = true;
20702 oracle.sampler_config.temperature = 0.0;
20703 oracle.sampler_config.repetition_penalty = 1.0;
20704 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
20705 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
20706 assert!(p.kv_history.is_empty());
20707 let mut bad = rows;
20708 bad[0] = f32::NAN;
20709 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
20710 }
20711
20712 fn f32_bits(v: &[f32]) -> Vec<u32> {
20713 v.iter().map(|x| x.to_bits()).collect()
20714 }
20715
20716 #[test]
20723 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
20724 let mut p = mimo_test_pipeline();
20725 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
20726 assert_eq!(kv, vec![1, 2, 2, 1]);
20727 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
20728 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
20729 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
20730 let hs = p.hidden_size;
20731 let mut decode = Vec::new();
20732 for (pos, &id) in ids.iter().enumerate() {
20733 let e = p.embed_single(id);
20734 let h = p.forward_layers(&e, pos, None);
20735 decode.push(p.logits_from_hidden(&h));
20736 }
20737 for l in &p.kv_cache.layers {
20738 assert_eq!(l.seq_len, 12);
20739 assert_eq!(l.head_values(0).len(), 12 * 8);
20741 }
20742 assert!(decode.iter().flatten().all(|v| v.is_finite()));
20743
20744 p.clear_sequence_state();
20745 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20746 for pos in 0..ids.len() {
20747 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
20748 assert_eq!(
20749 f32_bits(&decode[pos]),
20750 f32_bits(&lg),
20751 "whole prompt, pos {pos}"
20752 );
20753 }
20754
20755 p.clear_sequence_state();
20756 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
20757 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
20758 for pos in 0..ids.len() {
20759 let row = if pos < 5 {
20760 &a[pos * hs..(pos + 1) * hs]
20761 } else {
20762 &b[(pos - 5) * hs..(pos - 4) * hs]
20763 };
20764 let lg = p.logits_from_hidden(row);
20765 assert_eq!(
20766 f32_bits(&decode[pos]),
20767 f32_bits(&lg),
20768 "two chunks, pos {pos}"
20769 );
20770 }
20771
20772 let last = |p: &mut Pipeline| {
20775 p.clear_sequence_state();
20776 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20777 p.logits_from_hidden(&hb[11 * hs..12 * hs])
20778 };
20779 let base = last(&mut p);
20780 let mut no_sinks = mimo_test_pipeline();
20781 for l in &mut no_sinks.kv_cache.layers {
20782 l.sinks = None;
20783 }
20784 assert_ne!(
20785 f32_bits(&last(&mut no_sinks)),
20786 f32_bits(&base),
20787 "sinks are live"
20788 );
20789 let mut wide = mimo_test_pipeline();
20790 wide.swa = Some((64, usize::MAX));
20791 assert_ne!(
20792 f32_bits(&last(&mut wide)),
20793 f32_bits(&base),
20794 "window is live"
20795 );
20796
20797 p.clear_sequence_state();
20799 p.ignore_eos = true;
20800 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
20801 assert_eq!(r.token_ids.len(), 4);
20802 }
20803
20804 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
20807 let (hs, nh) = (16usize, 4usize);
20808 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
20809 p.layer_dump = None;
20810 p.swa = Some((6, 4));
20811 p.proj_gate_sigmoid = true;
20812 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
20813 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
20814 let g: Vec<f32> = (0..nh * hs)
20815 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
20816 .collect();
20817 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
20818 }
20819 }
20820 p.swa_trim = trim;
20821 p
20822 }
20823
20824 #[test]
20830 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
20831 let mut a = spark_test_pipeline(None);
20832 let mut b = spark_test_pipeline(Some((2, 4)));
20833 assert_eq!(
20834 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
20835 vec![Some(6), Some(6), Some(6), None]
20836 );
20837 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
20838 let mut pos = 0usize;
20839 for &n in [5usize, 13, 7, 16].iter().cycle() {
20840 if pos >= ids.len() {
20841 break;
20842 }
20843 let end = (pos + n).min(ids.len());
20844 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20845 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20846 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
20847 pos = end;
20848 }
20849 for _ in 0..23 {
20850 let e = a.embed_single(((pos * 7) % 64) as u32);
20851 let ha = a.forward_layers(&e, pos, None);
20852 let hb = b.forward_layers(&e, pos, None);
20853 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
20854 for li in 0..3 {
20855 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
20856 }
20857 pos += 1;
20858 }
20859 for round in 0..9 {
20860 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
20861 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
20862 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
20863 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
20864 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
20865 if round % 2 == 1 {
20866 for p in [&mut a, &mut b] {
20868 for l in &mut p.kv_cache.layers {
20869 l.truncate_last(2);
20870 }
20871 }
20872 } else {
20873 pos += 2;
20874 }
20875 }
20876 let e = a.embed_single(9);
20877 let (ha, hb) = (
20878 a.forward_layers(&e, pos, None),
20879 b.forward_layers(&e, pos, None),
20880 );
20881 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
20882 for li in 0..3 {
20883 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
20884 assert!(lb.base() > 0, "layer {li} trimmed");
20885 assert_eq!(la.base(), 0);
20886 assert_eq!(lb.pos_len(), la.seq_len);
20887 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
20888 }
20889 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
20890 assert_eq!(
20891 (gb.base(), gb.seq_len),
20892 (0, ga.seq_len),
20893 "the global layer keeps all"
20894 );
20895 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
20896 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
20897 }
20898
20899 #[test]
20904 fn swa_trim_wire_handoff_continues_bitwise() {
20905 let mut a = spark_test_pipeline(None);
20906 let mut b = spark_test_pipeline(Some((2, 4)));
20907 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
20908 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20909 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20910 assert_eq!(f32_bits(&ha), f32_bits(&hb));
20911 let mut pos = ids.len();
20912 for _ in 0..5 {
20913 let e = a.embed_single(((pos * 5) % 64) as u32);
20914 assert_eq!(
20915 f32_bits(&a.forward_layers(&e, pos, None)),
20916 f32_bits(&b.forward_layers(&e, pos, None))
20917 );
20918 pos += 1;
20919 }
20920 let mut c = spark_test_pipeline(Some((2, 4)));
20921 for li in 0..4 {
20922 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
20923 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
20924 }
20925 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
20926 assert_eq!(c.kv_cache.seq_len(), pos);
20927 for step in 0..17 {
20928 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
20929 let ha = a.forward_layers(&e, pos, None);
20930 let hc = c.forward_layers(&e, pos, None);
20931 assert_eq!(
20932 f32_bits(&ha),
20933 f32_bits(&hc),
20934 "after the hand-off, step {step}"
20935 );
20936 pos += 1;
20937 }
20938 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
20939 }
20940
20941 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
20944 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
20945 let synth = |len: usize, salt: usize| -> Vec<f32> {
20946 (0..len)
20947 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
20948 .collect()
20949 };
20950 let qt = |rows: usize, cols: usize, salt: usize| {
20951 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20952 };
20953 let layers = (0..n)
20954 .map(|k| {
20955 let s = 500 + k * 40;
20956 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
20957 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
20958 MtpModule {
20959 enorm: vec![1.0; hs],
20960 hnorm: vec![1.0; hs],
20961 eh_proj: qt(hs, 2 * hs, s),
20962 layer: LayerWeights {
20963 input_norm: vec![1.0; hs],
20964 post_norm: vec![1.0; hs],
20965 attn_out_norm: None,
20966 ffn_out_norm: None,
20967 layer_scale: None,
20968 attn: AttnKind::Full {
20969 wq: qt(nh * hd, hs, s + 1),
20970 wk: qt(nkv * hd, hs, s + 2),
20971 wv: qt(nkv * vd, hs, s + 3),
20972 wo: qt(hs, nh * vd, s + 4),
20973 q_norm: None,
20974 k_norm: None,
20975 output_gate: false,
20976 softplus_gate: None,
20977 bias: None,
20978 },
20979 ffn: FfnKind::Dense(DenseFfn {
20980 gate_proj: qt(inter, hs, s + 5),
20981 up_proj: qt(inter, hs, s + 6),
20982 down_proj: qt(hs, inter, s + 7),
20983 act: Act::Silu,
20984 down_t: None,
20985 segs: Vec::new(),
20986 }),
20987 },
20988 final_norm: vec![1.0; hs],
20989 kv,
20990 }
20991 })
20992 .collect();
20993 mimo_mtp::MimoMtp::from_layers(layers)
20994 }
20995
20996 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
20997 p.clear_sequence_state();
20998 p.speculative = spec;
20999 p.ignore_eos = true;
21000 p.sampler_config.temperature = 0.0;
21001 p.generate_from_ids(ids, n, None, None).unwrap()
21002 }
21003
21004 #[test]
21010 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
21011 for post in [false, true] {
21014 let mut p = mimo_test_pipeline();
21015 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
21017 let mut st0 = mimo_test_mtp(3, 1.0);
21018 st0.post_norm_hidden = post;
21019 p.mimo_mtp = Some(st0);
21020 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
21021 let hs = p.hidden_size;
21022 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21023 p.mimo_note_rows(&hb, 0);
21024 let mut st = p.mimo_mtp.take().unwrap();
21025 let k = 3;
21028 let mut inc = Vec::new();
21029 for t in 0..ids.len() - k - 1 {
21030 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
21031 }
21032 let s = ids.len();
21035 let mut reference = vec![vec![0u32; k]; s - k - 1];
21036 let mut fresh = mimo_test_mtp(3, 1.0);
21037 for (layer, m) in fresh.layers.iter_mut().enumerate() {
21038 let n = s - layer - 1;
21039 let mut cats = vec![0.0f32; n * 2 * hs];
21040 for j in 0..n {
21041 let e = p.embed_single(ids[j + layer + 1]);
21042 let raw = &hb[j * hs..(j + 1) * hs];
21043 let g = if post {
21044 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
21045 } else {
21046 raw.to_vec()
21047 };
21048 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
21049 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
21050 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
21051 }
21052 let mut x = vec![0.0f32; n * hs];
21053 m.eh_proj.matmat(&cats, n, &mut x, None);
21054 p.mimo_mtp_block(m, &mut x, n, 0);
21055 for (t, row) in reference.iter_mut().enumerate() {
21056 let y = inference::rms_norm(
21057 &x[t * hs..(t + 1) * hs],
21058 &m.final_norm,
21059 p.rms_eps,
21060 p.norm_style,
21061 );
21062 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
21063 }
21064 }
21065 assert_eq!(inc, reference, "post_norm_hidden = {post}");
21066 let distinct: std::collections::HashSet<u32> =
21068 inc.iter().flatten().copied().collect();
21069 assert!(distinct.len() > 3, "{inc:?}");
21070 let last_t = ids.len() - k - 2;
21072 for m in &st.layers {
21073 assert_eq!(m.kv.seq_len, last_t + 1);
21074 }
21075 }
21076 }
21077
21078 #[test]
21085 fn mimo_speculative_greedy_equals_plain_greedy() {
21086 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21087 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
21088 let n = 24;
21089 let mut p = mimo_test_pipeline();
21090 let plain = mimo_greedy(&mut p, &ids, n, false);
21091 assert_eq!(plain.mtp_drafted, 0);
21092 assert_eq!(plain.token_ids.len(), n);
21093 let plain_kv = p.kv_cache.layers[0].seq_len;
21094
21095 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
21097 let spec = mimo_greedy(&mut p, &ids, n, true);
21098 assert!(spec.mtp_drafted > 0, "the round must draft");
21099 assert_eq!(spec.token_ids, plain.token_ids);
21100 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21101
21102 let mut truth: Vec<u32> = ids.clone();
21105 truth.extend(&plain.token_ids);
21106 let mut noisy = truth.clone();
21107 for (i, t) in noisy.iter_mut().enumerate() {
21108 if i % 5 == 0 {
21109 *t = (*t + 1) % 64;
21110 }
21111 }
21112 let mut st = mimo_test_mtp(3, 1.0);
21113 st.draft_override = Some(noisy);
21114 p.mimo_mtp = Some(st);
21115 let spec = mimo_greedy(&mut p, &ids, n, true);
21116 assert_eq!(spec.token_ids, plain.token_ids);
21117 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21118 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
21119 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
21120 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
21121 assert!(
21122 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
21123 "{:?}",
21124 stats.accept_hist
21125 );
21126 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
21127
21128 let mut st = mimo_test_mtp(3, 1.0);
21131 st.draft_override = Some(truth);
21132 p.mimo_mtp = Some(st);
21133 let spec = mimo_greedy(&mut p, &ids, n, true);
21134 assert_eq!(spec.token_ids, plain.token_ids);
21135 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
21136 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21137
21138 let off = mimo_greedy(&mut p, &ids, n, false);
21140 assert_eq!(off.token_ids, plain.token_ids);
21141 assert_eq!(off.mtp_drafted, 0);
21142 }
21143
21144 #[test]
21153 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
21154 let p = mimo_test_pipeline();
21155 assert_eq!(
21156 p.graph_attn_decline_reason(),
21157 Some("per-layer KV head counts")
21158 );
21159 assert_eq!(p.wgpu_graph_attn_decline(), None);
21160 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
21161 assert_eq!(
21162 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
21163 (1, 4, 4, None, false)
21164 );
21165 assert_eq!(g0.invf, p.inv_freq.as_slice());
21166 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
21167 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
21168 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
21169 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
21170 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
21171 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
21172 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
21173
21174 let emb = p.embed_single(3);
21177 let mut lg = Vec::new();
21178 assert!(
21179 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
21180 .is_none()
21181 );
21182 let mut hid = emb.clone();
21183 assert_eq!(
21184 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
21185 crate::gpu::BatchGraphOutcome::Declined
21186 );
21187 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
21188 assert!(p.try_multi_burst(3, 0, 4).is_none());
21189 assert!(
21190 p.graph_declines().is_empty(),
21191 "no attention decline logged: {:?}",
21192 p.graph_declines()
21193 );
21194 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
21199 assert_eq!(plain().graph_attn_decline_reason(), None);
21200 assert_eq!(plain().wgpu_graph_attn_decline(), None);
21201 assert!(
21202 plain().graph_attn_geom(0).is_none(),
21203 "uniform models keep the historical arms"
21204 );
21205 let mut q = plain();
21206 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
21207 assert_eq!(
21208 q.graph_attn_decline_reason(),
21209 Some("learned attention sinks")
21210 );
21211 assert_eq!(
21212 q.graph_attn_geom(1).unwrap().sink,
21213 Some(&[0.25f32, -0.25][..])
21214 );
21215 let mut q = plain();
21216 q.set_attn_geometry(None, Some(2)).unwrap();
21217 assert_eq!(
21218 q.graph_attn_decline_reason(),
21219 Some("V heads narrower than Q/K heads")
21220 );
21221 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
21222 let mut q = plain();
21223 q.sliding_layers = Some(vec![true, false]);
21224 q.swa = Some((4, usize::MAX));
21225 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
21226 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
21227 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
21228
21229 let mut q = mimo_test_pipeline();
21233 q.rope_scale = 2.0;
21234 q.rope_scale_local = 1.25;
21235 assert_eq!(q.wgpu_graph_attn_decline(), None);
21236 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
21237 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
21238
21239 let mut q = plain();
21242 q.rope_scale = 2.0;
21243 assert_eq!(
21244 q.wgpu_graph_attn_decline(),
21245 Some("scaled RoPE without per-layer geometry")
21246 );
21247 }
21248
21249 #[test]
21250 fn mimo_verify_rewind_preserves_lagging_host_caches() {
21251 let mut p = mimo_test_pipeline();
21252 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
21253 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
21254 for _ in 0..if li == 0 { 2 } else { 12 } {
21255 layer.append(&row, &row, &[]);
21256 }
21257 }
21258 p.mimo_verify_rewind(9).unwrap();
21259 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
21260 for layer in &p.kv_cache.layers[1..] {
21261 assert_eq!(layer.seq_len, 9);
21262 }
21263 }
21264
21265 #[test]
21269 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
21270 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
21271 let _ = std::fs::remove_dir_all(&dir);
21272 let mut p = mimo_test_pipeline();
21273 let hs = p.hidden_size;
21274 let ids = [5u32, 9, 11, 2, 40];
21275 p.layer_dump = Some(dir.join("decode"));
21276 for (pos, &id) in ids.iter().enumerate() {
21277 let e = p.embed_single(id);
21278 let _ = p.forward_layers(&e, pos, None);
21279 }
21280 p.clear_sequence_state();
21281 p.layer_dump = Some(dir.join("prefill"));
21282 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21283 for pos in 0..ids.len() {
21284 for li in 0..p.num_layers {
21285 let name = format!("p{pos:06}_l{li:02}.f32");
21286 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
21287 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
21288 assert_eq!(a.len(), hs * 4, "{name}");
21289 assert_eq!(a, b, "{name}");
21290 }
21291 }
21292 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
21293 let vals: Vec<f32> = last
21294 .chunks(4)
21295 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
21296 .collect();
21297 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
21298 let _ = std::fs::remove_dir_all(&dir);
21299 }
21300
21301 #[test]
21302 fn attn_geometry_and_sinks_are_validated() {
21303 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
21304 assert!(
21305 p.set_attn_geometry(Some(vec![2]), None).is_err(),
21306 "one entry per layer"
21307 );
21308 assert!(
21309 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
21310 "3 does not divide 4"
21311 );
21312 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
21313 assert!(p.set_attn_geometry(None, Some(0)).is_err());
21314 assert!(
21315 p.set_attn_geometry(None, Some(5)).is_err(),
21316 "V wider than the head"
21317 );
21318 p.set_attn_geometry(None, Some(4)).unwrap();
21319 assert_eq!(
21320 p.v_head_dim, None,
21321 "v_head_dim == head_dim is the uniform case"
21322 );
21323 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
21324 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
21325 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
21326 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
21327 assert!(
21328 p.kv_cache.layers[1].sinks.is_some(),
21329 "a reshape keeps the layer's sinks"
21330 );
21331 assert_eq!(p.layer_geom(1).0, 4);
21332 assert!(
21333 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
21334 "one sink per Q head"
21335 );
21336 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
21337 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
21338 }
21339
21340 #[test]
21343 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
21344 let cfg = || {
21345 Some(crate::nystrom::O1Cfg {
21346 layers: crate::nystrom::O1Layers::All,
21347 m: 4,
21348 w: 8,
21349 sink: 2,
21350 rect: crate::nystrom::O1Rect::Aggregate,
21351 })
21352 };
21353 let mut p = mimo_test_pipeline();
21354 p.set_o1(cfg());
21355 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
21356 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
21357 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
21358 q.sliding_layers = Some(vec![false, false, true]);
21359 q.swa = Some((4, usize::MAX));
21360 q.set_o1(cfg());
21361 assert_eq!(q.o1_flags, vec![true, false, false]);
21362 }
21363
21364 #[test]
21365 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21366 const B: usize = 19;
21367 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21368 p.set_o1(Some(crate::nystrom::O1Cfg {
21369 layers: crate::nystrom::O1Layers::All,
21370 m: 4,
21371 w: 8,
21372 sink: 2,
21373 rect: crate::nystrom::O1Rect::Aggregate,
21374 }));
21375 p.o1_begin_with_prefix(Some(B));
21376 let ids: Vec<u32> = (0..B as u32).collect();
21377 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21378
21379 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21380 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21381 let next = p.embed_single(B as u32);
21382 let _ = p.forward_layers(&next, B, None);
21383 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21384 }
21385
21386 #[test]
21387 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21388 const B: usize = 19;
21389 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21390 let gdn_cfg = crate::linear_core::GdnCfg {
21394 num_v_heads: 2,
21395 num_k_heads: 1,
21396 key_head_dim: 2,
21397 value_head_dim: 4,
21398 conv_kernel: 3,
21399 hidden_size: 8,
21400 rms_eps: 1e-6,
21401 output_gate_sigmoid: false,
21402 };
21403 let synth = |n: usize, salt: usize| -> Vec<f32> {
21404 (0..n)
21405 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21406 .collect()
21407 };
21408 let qt = |rows: usize, cols: usize, salt: usize| {
21409 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21410 };
21411 let c_dim = gdn_cfg.conv_dim();
21412 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21413 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21414 in_proj_qkv: qt(c_dim, 8, 1),
21415 in_proj_z: qt(vd, 8, 2),
21416 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21417 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21418 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21419 a_log: vec![0.2, 0.5],
21420 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21421 norm: vec![1.0; gdn_cfg.value_head_dim],
21422 out_proj: qt(8, vd, 7),
21423 });
21424 p.gdn_cfg = Some(gdn_cfg);
21425 p.set_o1(Some(crate::nystrom::O1Cfg {
21426 layers: crate::nystrom::O1Layers::All,
21427 m: 4,
21428 w: 8,
21429 sink: 2,
21430 rect: crate::nystrom::O1Rect::Aggregate,
21431 }));
21432 p.o1_begin_with_prefix(Some(B));
21433 for pos in 0..B - 2 {
21434 let emb = p.embed_single(pos as u32);
21435 let _ = p.forward_layers(&emb, pos, None);
21436 }
21437 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21438
21439 let e1 = p.embed_single((B - 2) as u32);
21440 let e2 = p.embed_single((B - 1) as u32);
21441 let _ = p.forward_pair(&e1, &e2, B - 2);
21442
21443 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21444 assert!(
21445 p.kv_cache
21446 .layers
21447 .iter()
21448 .enumerate()
21449 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21450 );
21451 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21452 assert_ne!(
21453 p.kv_cache.layers[0].linear_state, lane1_state,
21454 "real pair must commit GDN lane 2 before returning"
21455 );
21456 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21457 let next = p.embed_single(B as u32);
21458 let _ = p.forward_layers(&next, B, None);
21459 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21460 }
21461
21462 #[test]
21463 fn o1_error_observation_stays_terminal_until_reset() {
21464 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21465 p.set_o1(Some(crate::nystrom::O1Cfg {
21466 layers: crate::nystrom::O1Layers::All,
21467 m: 4,
21468 w: 8,
21469 sink: 2,
21470 rect: crate::nystrom::O1Rect::Aggregate,
21471 }));
21472 p.o1_begin();
21473 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21474
21475 assert!(p.o1_seal_checked().is_err());
21476 assert!(
21477 p.o1_seal_checked().is_err(),
21478 "retry must see the sticky error"
21479 );
21480 let k = vec![0.2f32; 4];
21481 let v = vec![0.3f32; 4];
21482 p.kv_cache.layers[0].append(&k, &v, &[]);
21483 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
21484
21485 p.reset_session();
21486 p.o1_begin();
21487 p.kv_cache.layers[0].append(&k, &v, &[]);
21488 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
21489 }
21490
21491 #[test]
21492 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
21493 let ids = vec![1u32, 2, 3, 4, 5, 6];
21494 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21495 p.graph_logits = Some(vec![123.0]);
21496 p.graph_want_logits = true;
21497 p.graph_failed
21498 .store(true, std::sync::atomic::Ordering::Relaxed);
21499 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21500 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
21501 assert!(err.contains("before NLL"));
21502 assert!(p.graph_logits.is_none());
21503 assert!(!p.graph_want_logits);
21504 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21505 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21506
21507 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21508 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21509 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21510 assert_eq!(actual.1, expected.1);
21511 assert!((actual.0 - expected.0).abs() < 1e-9);
21512 }
21513
21514 #[test]
21515 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
21516 let ids = vec![1u32, 2, 3, 4, 5, 6];
21517 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21518 p.nll_test_fail_at = Some(1);
21519 let err = p
21520 .nll_ids_from(&ids, 0)
21521 .expect_err("one-shot forward failure");
21522 assert!(err.contains("forward") || err.contains("score row"));
21523 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21524 assert!(!p.graph_want_logits);
21525 assert!(p.graph_logits.is_none());
21526 assert!(p.kv_history.is_empty());
21527
21528 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21529 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21530 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21531 assert_eq!(actual.1, expected.1);
21532 assert!((actual.0 - expected.0).abs() < 1e-9);
21533 }
21534
21535 #[test]
21536 fn nll_serial_failure_before_first_row_is_reported() {
21537 let ids = vec![1u32, 2, 3, 4];
21538 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21539 p.nll_test_force_serial = true;
21540 p.nll_test_fail_at = Some(0);
21541 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
21542 assert!(err.contains("serial forward"));
21543 assert!(p.kv_history.is_empty());
21544 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21545 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21546 }
21547
21548 #[test]
21549 fn ffn_probe_failure_discards_recorder_and_state() {
21550 let ids = vec![1u32, 2, 3, 4];
21551 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21552 p.nll_test_fail_at = Some(0);
21553 let err = p
21554 .probe_ffn_mass_batch(&ids)
21555 .expect_err("probe forward failure");
21556 assert!(err.contains("NLL"));
21557 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
21558 assert!(p.kv_history.is_empty());
21559 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21560 }
21561
21562 #[test]
21563 fn nll_test_controls_are_pipeline_scoped() {
21564 let ids = vec![1u32, 2, 3, 4];
21565 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21566 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21567 failing.nll_test_force_serial = true;
21568 failing.nll_test_fail_at = Some(0);
21569
21570 assert!(!failing.can_prefill_batched());
21571 assert!(unaffected.can_prefill_batched());
21572 let expected = unaffected
21573 .nll_ids_from(&ids, 0)
21574 .expect("unaffected pipeline remains usable");
21575 let err = failing
21576 .nll_ids_from(&ids, 0)
21577 .expect_err("failure injection belongs to failing pipeline");
21578 assert!(err.contains("serial forward"));
21579 assert!(failing.nll_test_fail_at.is_none());
21580 assert!(unaffected.can_prefill_batched());
21581 let actual = unaffected
21582 .nll_ids_from(&ids, 0)
21583 .expect("unaffected pipeline remains reusable");
21584 assert_eq!(actual.1, expected.1);
21585 assert!((actual.0 - expected.0).abs() < 1e-9);
21586 }
21587
21588 #[test]
21589 fn forward_ids_failure_channel_is_terminal_and_reusable() {
21590 let ids = vec![1u32, 2, 3, 4, 5, 6];
21591 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21592 p.graph_logits = Some(vec![123.0]);
21593 p.graph_want_logits = true;
21594 p.graph_failed
21595 .store(true, std::sync::atomic::Ordering::Relaxed);
21596 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21597
21598 let err = p
21599 .forward_ids(&ids, None)
21600 .expect_err("a failed forward must not become a valid head result");
21601 assert!(err.contains("forward_ids setup"));
21602 assert!(p.graph_logits.is_none());
21603 assert!(!p.graph_want_logits);
21604 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21605 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21606 assert_eq!(p.kv_cache.seq_len(), 0);
21607
21608 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
21609 .forward_ids(&ids, None)
21610 .expect("fresh forward_ids");
21611 let actual = p
21612 .forward_ids(&ids, None)
21613 .expect("pipeline remains reusable after a failed forward");
21614 assert_eq!(actual.len(), expected.len());
21615 assert!(
21616 actual
21617 .iter()
21618 .zip(expected)
21619 .all(|(a, b)| (a - b).abs() < 1e-9)
21620 );
21621 assert_eq!(p.kv_cache.seq_len(), ids.len());
21622 }
21623
21624 #[test]
21625 fn sigmoid_router_floor_is_explicit_per_architecture() {
21626 let zero = || DenseFfn {
21632 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21633 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21634 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21635 act: Act::Silu,
21636 down_t: None,
21637 segs: Vec::new(),
21638 };
21639 let m = MoeFfn {
21640 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
21641 experts: vec![zero(), zero()],
21642 top_k: 1,
21643 norm_topk_prob: true,
21644 router_sigmoid: true,
21645 expert_bias: None,
21646 routed_scaling: 2.5,
21647 route_tau: None,
21648 shared: None,
21649 stats: std::cell::RefCell::new(Vec::new()),
21650 act_sq: std::cell::RefCell::new(Vec::new()),
21651 act_rows: std::cell::RefCell::new(Vec::new()),
21652 mask: None,
21653 per_expert_scale: None,
21654 router_input_norm: false,
21655 resonance: None,
21656 grown: Vec::new(),
21657 };
21658 let logits = [-20.0f32, -20.0];
21659 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
21660 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
21661 let expected = (p[0] + 1e-20) / m.routed_scaling;
21662 assert!((glm_wsum - expected).abs() < 1e-15);
21663 assert!(generic_wsum > glm_wsum * 100.0);
21664 }
21665
21666 #[test]
21667 fn resonance_scores_match_formula_and_stable_tie() {
21668 let r = Resonance {
21669 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
21671 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
21672 k: 1,
21673 bias: vec![1.5, 0.5, 0.0],
21674 shell: Vec::new(),
21675 };
21676 let x = [1.0f32, 1.0];
21677 let mut got = vec![0.0; 3];
21678 r.scores(&x, &mut got);
21679 assert!((got[0] - 0.5).abs() < 1e-6);
21683 assert!((got[1] - 0.5).abs() < 1e-6);
21684 assert!(got[2].abs() < 1e-6);
21685 let best = got
21686 .iter()
21687 .enumerate()
21688 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
21689 .map(|(i, _)| i);
21690 assert_eq!(best, Some(0));
21691 assert!(got.iter().all(|v| v.is_finite()));
21692 }
21693
21694 #[test]
21699 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
21700 let plain = Resonance {
21706 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
21707 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
21708 k: 1,
21709 bias: vec![1.5, 0.5, 0.0, 0.0],
21710 shell: Vec::new(),
21711 };
21712 let shelled = Resonance {
21713 mu: plain.mu.clone(),
21714 u: plain.u.clone(),
21715 k: 1,
21716 bias: plain.bias.clone(),
21717 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
21718 };
21719 assert!(!plain.has_shell());
21720 assert!(shelled.has_shell());
21721 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
21722 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
21723 set_growth_shell(Some(true));
21724 assert!(growth_shell_enabled());
21725 let x = [1.0f32, 1.0];
21728 plain.scores(&x, &mut a);
21729 shelled.scores(&x, &mut b);
21730 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
21731 assert!(a[2] == 0.0 && a[3] == 0.0);
21732 let xo = [3.0f32, 0.0];
21735 plain.scores(&xo, &mut a);
21736 shelled.scores(&xo, &mut b);
21737 assert_eq!(a[2], -6.0);
21738 assert_eq!(a[3], -6.0);
21739 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
21740 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
21741 assert_eq!(shelled.effective_shell(4), shelled.shell);
21742 set_growth_shell(Some(false));
21745 assert!(!growth_shell_enabled());
21746 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
21747 shelled.scores(&xo, &mut b);
21748 assert_eq!(bits(&a), bits(&b));
21749 set_growth_shell(None);
21750 let short = Resonance {
21753 shell: vec![f32::INFINITY, f32::INFINITY],
21754 ..shelled
21755 };
21756 set_growth_shell(Some(true));
21757 short.scores(&xo, &mut b);
21758 assert_eq!(bits(&a), bits(&b));
21759 set_growth_shell(None);
21760 }
21761
21762 #[test]
21766 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
21767 let zero = || DenseFfn {
21768 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21769 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21770 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21771 act: Act::Silu,
21772 down_t: None,
21773 segs: Vec::new(),
21774 };
21775 let moe = |sigmoid: bool| MoeFfn {
21776 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
21777 experts: vec![zero(), zero(), zero(), zero()],
21778 top_k: 1,
21779 norm_topk_prob: true,
21780 router_sigmoid: sigmoid,
21781 expert_bias: None,
21782 routed_scaling: 1.0,
21783 route_tau: None,
21784 shared: None,
21785 stats: std::cell::RefCell::new(Vec::new()),
21786 act_sq: std::cell::RefCell::new(Vec::new()),
21787 act_rows: std::cell::RefCell::new(Vec::new()),
21788 mask: None,
21789 per_expert_scale: None,
21790 router_input_norm: false,
21791 resonance: None,
21792 grown: Vec::new(),
21793 };
21794 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
21795 for sigmoid in [false, true] {
21796 let m = moe(sigmoid);
21797 let (idx, p, wsum) = moe_route(&logits, &m, None);
21798 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
21799 assert_eq!(p[1], 0.0);
21800 assert_eq!(p[3], 0.0);
21801 assert!(p[2] > p[0] && p[0] > 0.0);
21802 assert!(p.iter().all(|v| v.is_finite()));
21803 let w = p[2] / wsum;
21804 if sigmoid {
21805 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
21807 } else {
21808 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
21809 }
21810 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
21813 assert_eq!(idx, vec![2]);
21814 }
21815 let m = moe(false);
21817 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
21818 assert_eq!(idx, vec![3]);
21819 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
21822 assert_eq!(idx, vec![0]);
21823 assert!(p.iter().all(|&v| v == 0.25));
21824 assert!(wsum.is_finite() && wsum > 0.0);
21825 }
21826
21827 #[test]
21832 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
21833 let zero = || DenseFfn {
21834 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21835 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21836 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21837 act: Act::Silu,
21838 down_t: None,
21839 segs: Vec::new(),
21840 };
21841 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
21842 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
21843 experts: vec![zero(), zero(), zero()],
21844 top_k: 1,
21845 norm_topk_prob: norm_topk,
21846 router_sigmoid: false,
21847 expert_bias: None,
21848 routed_scaling: 1.0,
21849 route_tau: None,
21850 shared: None,
21851 stats: std::cell::RefCell::new(Vec::new()),
21852 act_sq: std::cell::RefCell::new(Vec::new()),
21853 act_rows: std::cell::RefCell::new(Vec::new()),
21854 mask: None,
21855 per_expert_scale: None,
21856 router_input_norm: false,
21857 resonance: resonant.then(|| Resonance {
21858 mu: vec![0.0; 6],
21859 u: Vec::new(),
21860 k: 0,
21861 bias: vec![0.0; 3],
21862 shell: Vec::new(),
21863 }),
21864 grown: Vec::new(),
21865 };
21866 let lo = -0.1f32;
21869 let hi = f32::from_bits(lo.to_bits() - 1);
21870 assert!(hi > lo && hi - lo < 2f32.powi(-25));
21871 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
21872 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
21874 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
21875 for norm in [true, false] {
21878 let m = moe(true, norm);
21879 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
21880 assert_eq!(idx, vec![1], "norm_topk {norm}");
21881 assert_eq!(p, vec![0.0, 1.0, 0.0]);
21882 assert_eq!(p[1] / wsum, 1.0);
21883 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
21886 assert_eq!(idx, vec![0]);
21887 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
21889 assert_eq!(idx, vec![2]);
21890 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
21891 assert_eq!(idx, vec![0]);
21892 assert_eq!(p[0] / wsum, 1.0);
21893 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
21896 assert_eq!(idx, vec![0]);
21897 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
21898 }
21899 }
21900}