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
11883 .graph_weight()
11884 .or_else(|| t.graph_weight_descriptor())
11885 {
11886 let name = &m.tensors[i].name;
11887 let prism = if crate::prism::is_inverse_embedding(m, name) {
11888 crate::gpu::GraphPrismOp::InverseEmbedding
11889 } else if crate::prism::is_forward_weight(m, name) {
11890 crate::gpu::GraphPrismOp::Forward
11891 } else {
11892 crate::gpu::GraphPrismOp::None
11893 };
11894 return Some(crate::gpu::GraphW {
11895 idx: i,
11896 kind,
11897 row_scale: rs,
11898 data: &[],
11899 prism,
11900 affine: crate::prism::is_affine_target(m, name),
11901 });
11902 }
11903 match t.as_f32() {
11905 Some(d) => Some(crate::gpu::GraphW {
11906 idx: 0,
11907 kind: 4,
11908 row_scale: &[],
11909 data: d,
11910 prism: crate::gpu::GraphPrismOp::None,
11911 affine: false,
11912 }),
11913 None => {
11914 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11915 eprintln!("batch graph: weight has no graph/f32 representation");
11916 }
11917 None
11918 }
11919 }
11920 }
11921 for li in from..upto_excl {
11922 let lw = &self.weights.layers[self.phys_layer(li)];
11923 if dbg {
11924 let ak = match &lw.attn {
11925 AttnKind::Mla(_) => "Mla".into(),
11926 AttnKind::Full {
11927 output_gate, bias, ..
11928 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11929 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11930 AttnKind::Kda(_) => "Kda".into(),
11931 AttnKind::Linear(_) => "Linear".into(),
11932 AttnKind::ShortConv(_) => "ShortConv".into(),
11933 AttnKind::Bounded(_) => "Bounded".into(),
11934 };
11935 let fk = match &lw.ffn {
11936 FfnKind::Dense(_) => "Dense",
11937 FfnKind::Moe(_) => "Moe",
11938 FfnKind::DenseMoe(_) => "DenseMoe",
11939 };
11940 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11941 }
11942 let gffn = match &lw.ffn {
11943 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11947 FfnKind::Dense(d) => {
11948 let Some(act) = d.act.graph_act() else {
11952 self.note_graph_decline(
11953 "wgpu token graph",
11954 "dense FFN activation without a graph kernel",
11955 );
11956 return None;
11957 };
11958 crate::gpu::GraphFfn::Dense {
11959 gate: gw(&d.gate_proj)?,
11960 up: gw(&d.up_proj)?,
11961 down: gw(&d.down_proj)?,
11962 act,
11963 }
11964 }
11965 FfnKind::Moe(m) => {
11966 if m.route_tau.is_some() || m.mask.is_some() {
11974 return None;
11975 }
11976 let shared = m.shared.as_ref();
11977 let has_shared = shared.is_some();
11978 let shared_gated = matches!(shared, Some((_, Some(_))));
11979 let sgate = match shared {
11980 Some((_, Some(sg))) => gw(sg)?,
11981 _ => gw(&m.router)?,
11985 };
11986 let router = gw(&m.router)?;
11987 if router.prism != crate::gpu::GraphPrismOp::None
11993 || sgate.prism != crate::gpu::GraphPrismOp::None
11994 || router.affine
11995 || sgate.affine
11996 {
11997 tracing::warn!(
11998 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
11999 );
12000 return None;
12001 }
12002 let inter = m.experts.first()?.gate_proj.rows();
12003 let mut experts = Vec::with_capacity(m.experts.len() + 1);
12004 let mut q4tp: Option<bool> = None;
12007 let mut gu_q2: Option<bool> = None;
12010 let mut q82: Option<bool> = None;
12012 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
12013 if !matches!(e.act, Act::Silu)
12014 || e.gate_proj.rows() != inter
12015 || e.up_proj.rows() != inter
12016 {
12017 return None;
12018 }
12019 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12024 let Some((em, ei, _, _)) = expert_weight
12025 .graph_weight()
12026 .or_else(|| expert_weight.graph_weight_descriptor())
12027 else {
12028 return None;
12029 };
12030 let name = &em.tensors[ei].name;
12031 if crate::prism::is_forward_weight(em, name)
12032 || crate::prism::is_inverse_embedding(em, name)
12033 || crate::prism::is_affine_target(em, name)
12034 {
12035 tracing::warn!(
12036 "resident MoE declined: expert Prism/affine transform is not implemented"
12037 );
12038 return None;
12039 }
12040 }
12041 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12042 Some((mm, gi)) => (
12043 mm,
12044 gi,
12045 e.up_proj.mapped_q4t()?.1,
12046 e.down_proj.mapped_q4t()?.1,
12047 false,
12048 false,
12049 false,
12050 ),
12051 None => match e.gate_proj.mapped_q2tp() {
12052 Some((mm, gi)) => (
12053 mm,
12054 gi,
12055 e.up_proj.mapped_q2tp()?.1,
12056 e.down_proj.mapped_q4tp()?.1,
12057 true,
12058 true,
12059 false,
12060 ),
12061 None => match e.gate_proj.mapped_q4tp() {
12062 Some((mm, gi)) => (
12063 mm,
12064 gi,
12065 e.up_proj.mapped_q4tp()?.1,
12066 e.down_proj.mapped_q4tp()?.1,
12067 true,
12068 false,
12069 false,
12070 ),
12071 None => {
12074 if !crate::gpu::moe_q82_graph_on() {
12075 return None;
12076 }
12077 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12078 (
12079 mm,
12080 gi,
12081 e.up_proj.mapped_q8_2f()?.1,
12082 e.down_proj.mapped_q8_2f()?.1,
12083 false,
12084 false,
12085 true,
12086 )
12087 }
12088 },
12089 },
12090 };
12091 if *q4tp.get_or_insert(is_p) != is_p
12092 || *gu_q2.get_or_insert(is_q2) != is_q2
12093 || *q82.get_or_insert(is_8) != is_8
12094 {
12095 tracing::warn!(
12101 "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."
12102 );
12103 return None;
12104 }
12105 model.get_or_insert_with(|| mm.clone());
12106 experts.push((gi, ui, di));
12107 }
12108 crate::gpu::GraphFfn::Moe {
12109 router,
12110 shared_gate: sgate,
12111 experts,
12112 n_exp: m.experts.len(),
12113 top_k: std::env::var("CMF_TOPK_PROBE")
12119 .ok()
12120 .and_then(|v| v.parse::<usize>().ok())
12121 .filter(|k| *k > 0 && *k <= m.top_k)
12122 .unwrap_or(m.top_k),
12123 inter,
12124 norm_topk: m.norm_topk_prob,
12125 q4tp: q4tp?,
12126 gu_q2: gu_q2.unwrap_or(false),
12127 q82: q82.unwrap_or(false),
12128 sigmoid: m.router_sigmoid,
12129 bias: m.expert_bias.as_deref(),
12130 has_shared,
12131 shared_gated,
12132 route_scale: m.routed_scaling,
12133 }
12134 }
12135 };
12136 let attn = match &lw.attn {
12137 AttnKind::Full {
12138 wq,
12139 wk,
12140 wv,
12141 wo,
12142 q_norm,
12143 k_norm,
12144 output_gate,
12145 softplus_gate,
12146 bias,
12147 } => {
12148 if self.attention_heads_per_layer.is_some() {
12149 return None;
12150 }
12151 let head_gate = match softplus_gate {
12155 None => None,
12156 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
12157 Some(gw(g)?)
12158 }
12159 Some(_) => {
12160 self.note_graph_decline(
12161 "wgpu token graph",
12162 "projected softplus / per-element output gate",
12163 );
12164 return None;
12165 }
12166 };
12167 let (m, _, _, _) = wq
12168 .graph_weight()
12169 .or_else(|| wq.graph_weight_descriptor())?;
12170 model = Some(m.clone());
12171 crate::gpu::GraphAttn::Full {
12172 wq: gw(wq)?,
12173 wk: gw(wk)?,
12174 wv: gw(wv)?,
12175 wo: gw(wo)?,
12176 q_norm: q_norm.as_deref(),
12177 k_norm: k_norm.as_deref(),
12178 late_qk_norm: self.qk_norm_after_rope,
12179 bias: bias
12180 .as_ref()
12181 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12182 output_gate: *output_gate,
12183 cpu_k: self.kv_cache.layers[li].k_heads(),
12184 cpu_v: self.kv_cache.layers[li].v_heads(),
12185 cpu_base: self.kv_cache.layers[li].base(),
12186 geom: self.graph_attn_geom(li),
12187 head_gate,
12188 }
12189 }
12190 AttnKind::LinearGdn(w) => {
12191 let cfg = self.gdn_cfg?;
12192 let (m, _, _, _) = w
12193 .in_proj_qkv
12194 .graph_weight()
12195 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
12196 model = Some(m.clone());
12197 crate::gpu::GraphAttn::Gdn {
12198 qkv: gw(&w.in_proj_qkv)?,
12199 z: gw(&w.in_proj_z)?,
12200 a: gw(&w.in_proj_a)?,
12201 b: gw(&w.in_proj_b)?,
12202 out: gw(&w.out_proj)?,
12203 conv1d: &w.conv1d,
12204 a_log: &w.a_log,
12205 dt_bias: &w.dt_bias,
12206 norm: &w.norm,
12207 nv: cfg.num_v_heads,
12208 nk: cfg.num_k_heads,
12209 dk: cfg.key_head_dim,
12210 dv: cfg.value_head_dim,
12211 kk: cfg.conv_kernel,
12212 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12213 }
12214 }
12215 AttnKind::ShortConv(w) => {
12216 let cfg = self.short_conv_cfg?;
12217 let (m, _, _, _) = w
12218 .in_proj
12219 .graph_weight()
12220 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12221 model = Some(m.clone());
12222 crate::gpu::GraphAttn::ShortConv {
12223 inp: gw(&w.in_proj)?,
12224 out: gw(&w.out_proj)?,
12225 taps: &w.conv,
12226 kernel: cfg.kernel,
12227 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12228 }
12229 }
12230 _ => return None,
12231 };
12232 layers.push(crate::gpu::GraphLayer {
12233 input_norm: &lw.input_norm,
12234 attn,
12235 post_norm: &lw.post_norm,
12236 attn_out_norm: lw.attn_out_norm.as_deref(),
12237 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12238 layer_scale: lw.layer_scale,
12239 ffn: gffn,
12240 });
12241 }
12242 let model = model?;
12243 let lm_gw = if upto_excl == self.num_layers
12249 && self.graph_want_logits
12250 && std::env::var("CMF_GPU_LMHEAD")
12251 .map(|v| v != "0")
12252 .unwrap_or(true)
12253 {
12254 self.weights
12255 .lm_head
12256 .graph_weight()
12257 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12258 .map(|(m, i, kind, rs)| {
12259 let name = &m.tensors[i].name;
12260 let prism = if crate::prism::is_inverse_embedding(m, name) {
12261 crate::gpu::GraphPrismOp::InverseEmbedding
12262 } else if crate::prism::is_forward_weight(m, name) {
12263 crate::gpu::GraphPrismOp::Forward
12264 } else {
12265 crate::gpu::GraphPrismOp::None
12266 };
12267 (
12268 crate::gpu::GraphW {
12269 idx: i,
12270 kind,
12271 row_scale: rs,
12272 data: &[],
12273 prism,
12274 affine: crate::prism::is_affine_target(m, name),
12275 },
12276 self.weights.lm_head.rows(),
12277 )
12278 })
12279 } else {
12280 None
12281 };
12282 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12283 let emb_gw = if steps > 1 {
12285 self.weights
12286 .embed_tokens
12287 .graph_weight()
12288 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12289 .map(|(m, i, kind, rs)| {
12290 let name = &m.tensors[i].name;
12291 let prism = if crate::prism::is_inverse_embedding(m, name) {
12292 crate::gpu::GraphPrismOp::InverseEmbedding
12293 } else if crate::prism::is_forward_weight(m, name) {
12294 crate::gpu::GraphPrismOp::Forward
12295 } else {
12296 crate::gpu::GraphPrismOp::None
12297 };
12298 (
12299 crate::gpu::GraphW {
12300 idx: i,
12301 kind,
12302 row_scale: rs,
12303 data: &[],
12304 prism,
12305 affine: crate::prism::is_affine_target(m, name),
12306 },
12307 self.weights.embed_tokens.rows(),
12308 self.embed_multiplier,
12309 )
12310 })
12311 } else {
12312 None
12313 };
12314
12315 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12321 (from..upto_excl.min(self.num_layers - 1))
12322 .filter(|&li| (li + 1) % self.physical_layers == 0)
12323 .map(|li| li - from)
12324 .collect()
12325 } else {
12326 Vec::new()
12327 };
12328 let mut h = hidden.to_vec();
12329 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12335 let outcome = crate::gpu::forward_token_graph(
12336 &model,
12337 self.graph_kv_id,
12338 &layers,
12339 &o1_views,
12340 self.o1_epoch,
12341 &self.inv_freq,
12342 &mut h,
12343 nh,
12344 nkv,
12345 hd,
12346 self.attn_scale,
12347 rd,
12348 self.hidden_size,
12349 self.intermediate_size,
12350 position,
12351 self.kv_cache.max_seq_len,
12352 gemma,
12353 self.rms_eps as f32,
12354 lm,
12355 &self.weights.final_norm,
12356 logits_out,
12357 &loop_norm_at,
12358 steps,
12359 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12360 ids_out,
12361 layers_run,
12362 from,
12363 dump_hidden,
12364 );
12365 match outcome {
12366 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12367 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12368 crate::gpu::TokenGraphOutcome::Declined => None,
12369 }
12370 }
12371
12372 #[cfg(target_os = "macos")]
12381 #[allow(clippy::type_complexity)]
12382 fn metal_rows_plan(
12383 &self,
12384 ) -> Option<(
12385 Vec<MetalRowsItem<'_>>,
12386 std::sync::Arc<cortiq_core::CmfModel>,
12387 Option<crate::gpu_metal::GdnGpuCfg>,
12388 )> {
12389 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12390 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12391 if !graph_force
12392 || !crate::gpu::enabled_here()
12393 || std::env::var("CMF_GPU_BLOCK")
12394 .map(|v| v == "0")
12395 .unwrap_or(false)
12396 || self.attn_softcap > 0.0
12397 || self.o1_active()
12398 || self.swa.is_some()
12399 || self.global_attn.is_some()
12400 || self.attention_heads_per_layer.is_some()
12401 || self.graph_attn_decline_reason().is_some()
12403 || self.attn_v_norm
12404 || self.loop_final_norm
12405 || !self.metal_rope_scale_ok()
12406 {
12407 return None;
12408 }
12409 let attend_contract = self.head_dim % 4 == 0
12410 && self.head_dim <= 256
12411 && self.rotary_dim >= 2
12412 && self.rotary_dim <= self.head_dim
12413 && (self.rotary_dim / 2) % 32 == 0
12414 && self.num_kv_heads > 0
12415 && self.num_heads % self.num_kv_heads == 0;
12416 if !attend_contract {
12417 return None;
12418 }
12419 let mut plan: Vec<MetalRowsItem> = Vec::new();
12420 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12421 for li in 0..self.num_layers {
12422 let lw = &self.weights.layers[self.phys_layer(li)];
12423 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12424 return None;
12425 }
12426 let ffn = match &lw.ffn {
12427 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12428 let (Some(g), Some(u), Some(dn)) = (
12429 d.gate_proj.metal_graph_parts(),
12430 d.up_proj.metal_graph_parts(),
12431 d.down_proj.metal_graph_parts(),
12432 ) else {
12433 return None;
12434 };
12435 MetalFfn::Dense {
12436 gate: g,
12437 up: u,
12438 down: dn,
12439 act: 0, }
12441 }
12442 _ => return None,
12443 };
12444 match &lw.attn {
12445 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12446 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12447 w.in_proj_qkv.metal_graph_parts(),
12448 w.in_proj_z.metal_graph_parts(),
12449 w.in_proj_a.f32_parts(),
12450 w.in_proj_b.f32_parts(),
12451 w.out_proj.metal_graph_parts(),
12452 ) else {
12453 return None;
12454 };
12455 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12456 model_ref.get_or_insert_with(|| model.clone());
12457 }
12458 let gl = GdnGpuLayer {
12459 attn_norm: &lw.input_norm,
12460 post_norm: &lw.post_norm,
12461 qkv,
12462 z,
12463 a,
12464 b: bb,
12465 out,
12466 ffn,
12467 conv1d: &w.conv1d,
12468 a_log: &w.a_log,
12469 dt_bias: &w.dt_bias,
12470 gnorm: &w.norm,
12471 };
12472 match plan.last_mut() {
12473 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12474 _ => plan.push(MetalRowsItem::Gdn {
12475 run: vec![gl],
12476 first: li,
12477 }),
12478 }
12479 }
12480 AttnKind::Full {
12481 wq,
12482 wk,
12483 wv,
12484 wo,
12485 q_norm,
12486 k_norm,
12487 output_gate,
12488 softplus_gate: None,
12489 bias: None,
12490 } => {
12491 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12492 (
12493 wq.metal_graph_parts(),
12494 wk.metal_graph_parts(),
12495 wv.metal_graph_parts(),
12496 wo.metal_graph_parts(),
12497 )
12498 else {
12499 return None;
12500 };
12501 if let QTensor::Mapped { model, .. } = wq {
12502 model_ref.get_or_insert_with(|| model.clone());
12503 }
12504 let cache = &self.kv_cache.layers[li];
12505 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12506 return None;
12507 }
12508 plan.push(MetalRowsItem::Attn {
12509 l: AttnGpuLayer {
12510 attn_norm: &lw.input_norm,
12511 post_norm: &lw.post_norm,
12512 wq: pq,
12513 wk: pk,
12514 wv: pv,
12515 wo: po,
12516 ffn,
12517 attn_out_norm: None,
12518 ffn_out_norm: None,
12519 layer_scale: None,
12520 },
12521 li,
12522 q_norm: q_norm.as_deref(),
12523 k_norm: k_norm.as_deref(),
12524 output_gate: *output_gate,
12525 });
12526 }
12527 _ => return None,
12528 }
12529 }
12530 let model = model_ref?;
12531 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12532 nv: cfg.num_v_heads,
12533 nk: cfg.num_k_heads,
12534 dk: cfg.key_head_dim,
12535 dv: cfg.value_head_dim,
12536 kk: cfg.conv_kernel,
12537 hidden: self.hidden_size,
12538 inter: self.intermediate_size,
12539 c_dim: cfg.conv_dim(),
12540 eps: cfg.rms_eps as f32,
12541 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12542 });
12543 Some((plan, model, gcfg))
12544 }
12545
12546 #[cfg(target_os = "macos")]
12548 #[allow(clippy::too_many_arguments)]
12549 fn metal_attn_params<'a>(
12550 li: usize,
12551 cache: &'a crate::kv_cache::LayerKvCache,
12552 q_norm: Option<&'a [f32]>,
12553 k_norm: Option<&'a [f32]>,
12554 output_gate: bool,
12555 inv_freq: &'a [f32],
12556 rope_scale: f32,
12557 geom: (usize, usize, usize, usize),
12558 pos0: usize,
12559 kv_id: u64,
12560 scale: f32,
12561 eps: f32,
12562 gemma: bool,
12563 late_qk_norm: bool,
12564 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12565 let (nh, nkv, hd, rd) = geom;
12566 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12567 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12568 let cpu_stored = cpu_k[0].len() / hd;
12569 (
12570 crate::gpu_metal::AttnDeviceParams {
12571 kv_id,
12572 layer: li,
12573 nh,
12574 nkv,
12575 hd,
12576 rd,
12577 position: pos0,
12578 scale,
12579 eps,
12580 gemma,
12581 late_qk_norm,
12582 output_gate,
12583 q_norm,
12584 k_norm,
12585 inv_freq,
12586 rope_scale,
12587 cpu_k,
12588 cpu_v,
12589 cpu_stored,
12590 cpu_gen: cache.generation(),
12591 o1: None,
12592 window: None,
12593 head_gate: None,
12594 v_norm: false,
12595 },
12596 cpu_stored,
12597 )
12598 }
12599
12600 #[cfg(target_os = "macos")]
12605 #[allow(clippy::type_complexity)]
12606 fn metal_rows_run(
12607 &mut self,
12608 hiddens: &mut [f32],
12609 pos0: usize,
12610 b: usize,
12611 prefill: bool,
12612 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12613 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12617 ) -> MetalRowsRun {
12618 use crate::gpu_metal::{GraphDims, VerifyGraph};
12619 if !crate::gpu_metal::wait_replay() {
12625 tracing::error!("Metal rows graph: the pending async replay failed");
12626 return MetalRowsRun::Failed;
12627 }
12628 spec_stamp("v.wait");
12629 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12635 if want > 0 {
12636 let phys = self.physical_layers.max(1);
12637 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12638 let is_gdn = self
12639 .weights
12640 .layers
12641 .get(li % phys)
12642 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12643 if is_gdn && l.linear_state.len() != want {
12644 l.linear_state = vec![0f32; want];
12645 }
12646 }
12647 }
12648 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12649 return MetalRowsRun::Declined;
12650 };
12651 spec_stamp("v.plan");
12652 let dims = GraphDims {
12653 hidden: self.hidden_size,
12654 eps: self.rms_eps as f32,
12655 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12656 };
12657 let Some(mut graph) = (if prefill {
12658 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12659 } else {
12660 VerifyGraph::new(&model, dims, hiddens, b)
12661 }) else {
12662 return MetalRowsRun::Declined;
12663 };
12664 let geom = (
12665 self.num_heads,
12666 self.num_kv_heads,
12667 self.head_dim,
12668 self.rotary_dim,
12669 );
12670 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12671 let eps = self.rms_eps as f32;
12672 let kv_id = self.graph_kv_id;
12673 let inv_freq = self.inv_freq.clone();
12674 for item in &plan {
12675 let ok = match item {
12676 MetalRowsItem::Gdn { run, .. } => gcfg
12677 .as_ref()
12678 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12679 .unwrap_or(false),
12680 MetalRowsItem::Attn {
12681 l,
12682 li,
12683 q_norm,
12684 k_norm,
12685 output_gate,
12686 } => {
12687 let (p, _) = Self::metal_attn_params(
12688 *li,
12689 &self.kv_cache.layers[*li],
12690 *q_norm,
12691 *k_norm,
12692 *output_gate,
12693 &inv_freq,
12694 self.layer_rope_scale(*li),
12695 geom,
12696 pos0,
12697 kv_id,
12698 self.attn_scale,
12699 eps,
12700 gemma,
12701 self.qk_norm_after_rope,
12702 );
12703 graph.attn_ok(l, &p)
12704 }
12705 };
12706 if !ok {
12707 use std::sync::atomic::{AtomicBool, Ordering};
12708 static SAID: AtomicBool = AtomicBool::new(false);
12709 if !SAID.swap(true, Ordering::Relaxed) {
12710 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12711 }
12712 return MetalRowsRun::Declined;
12713 }
12714 }
12715 let lm = match &spec {
12716 Some((lm, _, _)) => {
12717 if !graph.lm_head_ok(*lm) {
12718 return MetalRowsRun::Declined;
12719 }
12720 Some(*lm)
12721 }
12722 None => None,
12723 };
12724 let mut gdn_layers = Vec::new();
12725 let mut attn_layers = Vec::new();
12726 for item in &plan {
12727 match item {
12728 MetalRowsItem::Gdn { run, first } => {
12729 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12730 .iter()
12731 .map(|l| l.linear_state.as_slice())
12732 .collect();
12733 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12734 return MetalRowsRun::Declined;
12735 }
12736 gdn_layers.extend(*first..*first + run.len());
12737 }
12738 MetalRowsItem::Attn {
12739 l,
12740 li,
12741 q_norm,
12742 k_norm,
12743 output_gate,
12744 } => {
12745 let (p, cpu_stored) = Self::metal_attn_params(
12746 *li,
12747 &self.kv_cache.layers[*li],
12748 *q_norm,
12749 *k_norm,
12750 *output_gate,
12751 &inv_freq,
12752 self.layer_rope_scale(*li),
12753 geom,
12754 pos0,
12755 kv_id,
12756 self.attn_scale,
12757 eps,
12758 gemma,
12759 self.qk_norm_after_rope,
12760 );
12761 if !graph.encode_attn_b(l, &p) {
12762 return MetalRowsRun::Declined;
12763 }
12764 attn_layers.push((*li, cpu_stored));
12765 }
12766 }
12767 }
12768 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12769 if !graph.encode_lm_head_b(final_norm, lm) {
12770 return MetalRowsRun::Declined;
12771 }
12772 if let Some((n, _)) = argmax_out.as_ref() {
12777 if !graph.encode_argmax_b(*n) {
12778 argmax_out = None;
12779 }
12780 }
12781 }
12782 spec_stamp("v.enc");
12783 if !graph.sync() {
12784 return MetalRowsRun::Failed;
12785 }
12786 spec_stamp("v.gpu");
12787 match (spec, argmax_out) {
12788 (Some(_), Some((_, ids))) => {
12789 ids.resize(b, 0);
12790 if !graph.read_argmax(ids) {
12791 return MetalRowsRun::Failed;
12792 }
12793 spec_stamp("v.am");
12794 }
12795 (Some((lm, _, logits)), None) => {
12796 logits.resize(b * lm.1, 0.0);
12797 if !graph.read_logits(logits) {
12798 return MetalRowsRun::Failed;
12799 }
12800 spec_stamp("v.lg");
12801 }
12802 (None, _) => {}
12803 }
12804 if !graph.read_hidden(hiddens) {
12805 return MetalRowsRun::Failed;
12806 }
12807 spec_stamp("v.hid");
12808 MetalRowsRun::Completed(MetalVerifyPending {
12809 graph,
12810 gdn_layers,
12811 attn_layers,
12812 })
12813 }
12814
12815 #[cfg(target_os = "macos")]
12821 fn try_batch_graph_metal(
12822 &mut self,
12823 hiddens: &mut [f32],
12824 positions: &[usize],
12825 b: usize,
12826 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12827 argmax_out: Option<(usize, &mut Vec<u32>)>,
12828 ) -> crate::gpu::BatchGraphOutcome {
12829 let _t0 = std::time::Instant::now();
12830 if positions.len() != b
12831 || positions.windows(2).any(|w| w[1] != w[0] + 1)
12832 || hiddens.len() != b * self.hidden_size
12833 {
12834 return crate::gpu::BatchGraphOutcome::Declined;
12835 }
12836 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12837 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12838 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12839 MetalRowsRun::Completed(pending) => pending,
12840 };
12841 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12842 eprintln!(
12843 "metal-verify: {:.1} ms | b={b}",
12844 _t0.elapsed().as_secs_f64() * 1e3
12845 );
12846 }
12847 self.metal_verify = Some(pending);
12848 crate::gpu::BatchGraphOutcome::Completed
12849 }
12850
12851 #[cfg(target_os = "macos")]
12856 fn prefill_rows_metal(
12857 &mut self,
12858 ids: &[u32],
12859 start_pos: usize,
12860 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12861 ) -> MetalPrefillOutcome {
12862 let b = ids.len();
12863 if b == 0 || b > 512 {
12864 return MetalPrefillOutcome::Declined;
12865 }
12866 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12867 let with_head = spec.is_some();
12868 let hs = self.hidden_size;
12869 let mut hiddens = vec![0f32; b * hs];
12870 for (j, &id) in ids.iter().enumerate() {
12871 let e = self.embed_single(id);
12872 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12873 }
12874 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12875 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12876 MetalRowsRun::Failed => {
12877 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12878 return MetalPrefillOutcome::Failed;
12879 }
12880 MetalRowsRun::Completed(pending) => pending,
12881 };
12882 let idxs = pending.gdn_layers.clone();
12884 let mut outs: Vec<&mut [f32]> = self
12885 .kv_cache
12886 .layers
12887 .iter_mut()
12888 .enumerate()
12889 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12890 .map(|(_, l)| l.linear_state.as_mut_slice())
12891 .collect();
12892 if !pending.graph.finish_states(&mut outs) {
12893 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12894 return MetalPrefillOutcome::Failed;
12895 }
12896 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12897 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12901 for (li, cpu_stored) in &pending.attn_layers {
12902 let mut kbuf = vec![0f32; b * nkv * hd];
12903 let mut vbuf = vec![0f32; b * nkv * hd];
12904 if !crate::gpu_metal::kv_mirror_read_rows(
12905 self.graph_kv_id,
12906 *li,
12907 nkv,
12908 hd,
12909 *cpu_stored,
12910 b,
12911 &mut kbuf,
12912 &mut vbuf,
12913 ) {
12914 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12915 return MetalPrefillOutcome::Failed;
12916 }
12917 rows.push((*li, *cpu_stored, kbuf, vbuf));
12918 }
12919 for (li, cpu_stored, kbuf, vbuf) in rows {
12920 let cache = &mut self.kv_cache.layers[li];
12921 for r in 0..b {
12922 cache.append(
12923 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12924 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12925 &[],
12926 );
12927 }
12928 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12929 }
12930 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12931 if with_head {
12932 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12933 }
12934 MetalPrefillOutcome::Completed(hiddens)
12935 }
12936
12937 #[cfg(target_os = "macos")]
12938 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12939 self.prefill_rows_metal(ids, start_pos, None)
12940 }
12941
12942 #[cfg(target_os = "macos")]
12947 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12948 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12949 return MetalBatchNllOutcome::Declined;
12950 }
12951 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12952 return MetalBatchNllOutcome::Declined;
12953 };
12954 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12955 .ok()
12956 .and_then(|v| v.parse::<usize>().ok())
12957 .filter(|&v| (1..=512).contains(&v))
12958 .unwrap_or(32);
12959 let final_norm = self.weights.final_norm.clone();
12960 let mut nll = 0.0f64;
12961 let mut count = 0usize;
12962 let mut pos = 0usize;
12963 let mut completed = 0usize;
12964 while pos < ids.len() {
12965 let end = (pos + chunk).min(ids.len());
12966 let mut logits = Vec::new();
12967 let outcome = self.prefill_rows_metal(
12968 &ids[pos..end],
12969 pos,
12970 Some((lm, &final_norm, &mut logits)),
12971 );
12972 match outcome {
12973 MetalPrefillOutcome::Declined => {
12974 return if completed == 0 {
12975 MetalBatchNllOutcome::Declined
12976 } else {
12977 MetalBatchNllOutcome::Failed(format!(
12978 "ordinary Metal NLL batch declined after {completed} chunks"
12979 ))
12980 };
12981 }
12982 MetalPrefillOutcome::Failed => {
12983 return MetalBatchNllOutcome::Failed(
12984 "ordinary Metal NLL batch failed after admission".to_string(),
12985 );
12986 }
12987 MetalPrefillOutcome::Completed(_) => {}
12988 }
12989 completed += 1;
12990 let vocab = self.vocab_size.min(lm.1);
12991 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
12992 return MetalBatchNllOutcome::Failed(
12993 "ordinary Metal NLL head returned an invalid shape".to_string(),
12994 );
12995 }
12996 for row in 0..(end - pos) {
12997 let absolute = pos + row;
12998 if absolute < start || absolute + 1 >= ids.len() {
12999 continue;
13000 }
13001 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
13002 if let Some(mu) = self.logit_multiplier {
13003 for v in lg.iter_mut() {
13004 *v *= mu;
13005 }
13006 }
13007 if let Some(c) = self.final_softcap {
13008 softcap_in_place(lg, c, self.pool.as_deref());
13009 }
13010 let target = ids[absolute + 1] as usize;
13011 if target >= vocab {
13012 return MetalBatchNllOutcome::Failed(format!(
13013 "target token {target} exceeds Metal head rows {vocab}"
13014 ));
13015 }
13016 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13017 let lse: f64 = lg
13018 .iter()
13019 .map(|&v| ((v - max) as f64).exp())
13020 .sum::<f64>()
13021 .ln()
13022 + max as f64;
13023 nll += lse - lg[target] as f64;
13024 count += 1;
13025 }
13026 pos = end;
13027 }
13028 MetalBatchNllOutcome::Completed(nll, count)
13029 }
13030
13031 #[cfg(target_os = "macos")]
13035 fn metal_verify_commit(&mut self, a: usize) -> bool {
13036 let Some(mut pending) = self.metal_verify.take() else {
13037 return false;
13038 };
13039 let n = a + 1;
13040 let idxs = pending.gdn_layers.clone();
13042 let mut outs: Vec<&mut [f32]> = self
13043 .kv_cache
13044 .layers
13045 .iter_mut()
13046 .enumerate()
13047 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13048 .map(|(_, l)| l.linear_state.as_mut_slice())
13049 .collect();
13050 if !pending.graph.commit(n, &mut outs) {
13051 return false;
13052 }
13053 spec_stamp("c.replay");
13054 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13055 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13059 for (li, cpu_stored) in &pending.attn_layers {
13060 let mut kbuf = vec![0f32; n * nkv * hd];
13061 let mut vbuf = vec![0f32; n * nkv * hd];
13062 if !crate::gpu_metal::kv_mirror_read_rows(
13063 self.graph_kv_id,
13064 *li,
13065 nkv,
13066 hd,
13067 *cpu_stored,
13068 n,
13069 &mut kbuf,
13070 &mut vbuf,
13071 ) {
13072 return false;
13073 }
13074 rows.push((*li, *cpu_stored, kbuf, vbuf));
13075 }
13076 for (li, cpu_stored, kbuf, vbuf) in rows {
13077 let cache = &mut self.kv_cache.layers[li];
13078 for r in 0..n {
13079 cache.append(
13080 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13081 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13082 &[],
13083 );
13084 }
13085 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13086 }
13087 spec_stamp("c.kv");
13088 true
13089 }
13090
13091 #[cfg(target_os = "macos")]
13098 fn mtp_warm_batch_submit(
13099 &mut self,
13100 m: &mut MtpModule,
13101 pairs: &[(&[f32], u32)],
13102 first_pos: usize,
13103 ) -> Option<MetalWarmPending> {
13104 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13105 let b = pairs.len();
13106 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13107 return None;
13108 }
13109 let AttnKind::Full {
13110 wq,
13111 wk,
13112 wv,
13113 wo,
13114 q_norm,
13115 k_norm,
13116 output_gate,
13117 softplus_gate: None,
13118 bias: None,
13119 } = &m.layer.attn
13120 else {
13121 return None;
13122 };
13123 let FfnKind::Dense(d) = &m.layer.ffn else {
13124 return None;
13125 };
13126 if !d.segs.is_empty() {
13127 return None;
13128 }
13129 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13130 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13131 else {
13132 return None;
13133 };
13134 let (Some(g), Some(u), Some(dn)) = (
13135 d.gate_proj.q1_parts(),
13136 d.up_proj.q1_parts(),
13137 d.down_proj.q1_parts(),
13138 ) else {
13139 return None;
13140 };
13141 let Some(eh) = m.eh_proj.q1_parts() else {
13142 return None;
13143 };
13144 let QTensor::Mapped { model, .. } = wq else {
13145 return None;
13146 };
13147 let model = model.clone();
13148 let hs = self.hidden_size;
13149 let mut cat = vec![0f32; b * 2 * hs];
13151 for (j, (h, tok)) in pairs.iter().enumerate() {
13152 let e = self.embed_single(*tok);
13153 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13154 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13155 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13156 }
13157 let dims = GraphDims {
13158 hidden: hs,
13159 eps: self.rms_eps as f32,
13160 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13161 };
13162 spec_stamp("w.cat");
13163 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13164 return None;
13165 };
13166 spec_stamp("w.new");
13167 let l = AttnGpuLayer {
13168 attn_norm: &m.layer.input_norm,
13169 post_norm: &m.layer.post_norm,
13170 wq: pq,
13171 wk: pk,
13172 wv: pv,
13173 wo: po,
13174 ffn: MetalFfn::Dense {
13175 gate: g,
13176 up: u,
13177 down: dn,
13178 act: (d.act == Act::Gelu) as u8,
13179 },
13180 attn_out_norm: None,
13181 ffn_out_norm: None,
13182 layer_scale: None,
13183 };
13184 let (nh, nkv, hd, rd) = (
13185 self.num_heads,
13186 self.num_kv_heads,
13187 self.head_dim,
13188 self.rotary_dim,
13189 );
13190 let inv_freq = self.inv_freq.clone();
13191 let cpu_stored;
13192 {
13193 let cache = &m.kv;
13194 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13195 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13196 cpu_stored = cpu_k[0].len() / hd;
13197 if cpu_stored > first_pos {
13202 spec_stamp("w.decl");
13203 return None;
13204 }
13205 let p = AttnDeviceParams {
13206 kv_id: self.mtp_kv_id(),
13207 layer: Self::MTP_LAYER_BASE,
13208 nh,
13209 nkv,
13210 hd,
13211 rd,
13212 position: first_pos,
13213 scale: self.attn_scale,
13214 eps: self.rms_eps as f32,
13215 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13216 late_qk_norm: self.qk_norm_after_rope,
13217 output_gate: *output_gate,
13218 q_norm: q_norm.as_deref(),
13219 k_norm: k_norm.as_deref(),
13220 inv_freq: &inv_freq,
13221 rope_scale: self.rope_scale,
13222 cpu_k,
13223 cpu_v,
13224 cpu_stored,
13225 cpu_gen: cache.generation(),
13226 o1: None,
13227 window: None,
13228 head_gate: None,
13229 v_norm: false,
13230 };
13231 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13232 return None;
13233 }
13234 }
13235 spec_stamp("w.enc");
13236 if !graph.submit() {
13237 return None;
13238 }
13239 spec_stamp("w.sub");
13240 Some(MetalWarmPending {
13241 graph,
13242 cpu_stored,
13243 b,
13244 })
13245 }
13246
13247 #[cfg(target_os = "macos")]
13250 fn mtp_warm_batch_metal(
13251 &mut self,
13252 m: &mut MtpModule,
13253 pairs: &[(&[f32], u32)],
13254 first_pos: usize,
13255 ) -> bool {
13256 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13257 Some(p) => self.mtp_warm_batch_finish(m, p),
13258 None => false,
13259 }
13260 }
13261
13262 #[cfg(target_os = "macos")]
13267 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13268 let MetalWarmPending {
13269 mut graph,
13270 cpu_stored,
13271 b,
13272 } = pending;
13273 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13274 if !graph.sync() {
13275 return false;
13276 }
13277 spec_stamp("w.gpu");
13278 let mut kbuf = vec![0f32; b * nkv * hd];
13279 let mut vbuf = vec![0f32; b * nkv * hd];
13280 if !crate::gpu_metal::kv_mirror_read_rows(
13281 self.mtp_kv_id(),
13282 Self::MTP_LAYER_BASE,
13283 nkv,
13284 hd,
13285 cpu_stored,
13286 b,
13287 &mut kbuf,
13288 &mut vbuf,
13289 ) {
13290 return false;
13291 }
13292 for r in 0..b {
13293 m.kv.append(
13294 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13295 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13296 &[],
13297 );
13298 }
13299 crate::gpu_metal::kv_mirror_set_stored(
13300 self.mtp_kv_id(),
13301 Self::MTP_LAYER_BASE,
13302 cpu_stored + b,
13303 );
13304 spec_stamp("w.kv");
13305 true
13306 }
13307
13308 pub(crate) fn note_draft_id(&mut self, id: u32) {
13315 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13316 if (id as usize) >= cut {
13317 self.draft_full_streak = 16;
13318 } else {
13319 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13320 }
13321 }
13322
13323 fn draft_head_rows(&self, head_rows: usize) -> usize {
13326 if self.draft_full_streak > 0 {
13327 head_rows
13328 } else {
13329 Self::draft_vocab_rows(head_rows)
13330 }
13331 }
13332
13333 fn draft_vocab_rows(head_rows: usize) -> usize {
13336 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13337 let n = *N.get_or_init(|| {
13338 std::env::var("CMF_DRAFT_VOCAB")
13339 .ok()
13340 .and_then(|v| v.parse().ok())
13341 .unwrap_or(65536)
13342 });
13343 if n == 0 { head_rows } else { n.min(head_rows) }
13344 }
13345
13346 #[cfg(target_os = "macos")]
13351 fn mtp_step_metal(
13352 &mut self,
13353 m: &mut MtpModule,
13354 hidden: &[f32],
13355 next_token: u32,
13356 position: usize,
13357 want_logits: bool,
13358 ) -> Option<(Vec<f32>, Vec<f32>)> {
13359 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13360 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13361 || !crate::gpu::q1_force()
13362 || !crate::gpu::enabled_here()
13363 || self.attn_softcap > 0.0
13364 || self.attention_heads_per_layer.is_some()
13365 || m.kv.mode != crate::kv_cache::KvMode::F32
13366 || m.kv.o1.is_some()
13367 {
13368 return None;
13369 }
13370 let AttnKind::Full {
13371 wq,
13372 wk,
13373 wv,
13374 wo,
13375 q_norm,
13376 k_norm,
13377 output_gate,
13378 softplus_gate: None,
13379 bias: None,
13380 } = &m.layer.attn
13381 else {
13382 return None;
13383 };
13384 let FfnKind::Dense(d) = &m.layer.ffn else {
13385 return None;
13386 };
13387 if d.act != Act::Silu || !d.segs.is_empty() {
13388 return None;
13389 }
13390 let (pq, pk, pv, po) = (
13391 wq.q1_parts()?,
13392 wk.q1_parts()?,
13393 wv.q1_parts()?,
13394 wo.q1_parts()?,
13395 );
13396 let (g, u, dn) = (
13397 d.gate_proj.q1_parts()?,
13398 d.up_proj.q1_parts()?,
13399 d.down_proj.q1_parts()?,
13400 );
13401 let QTensor::Mapped { model, .. } = wq else {
13402 return None;
13403 };
13404 let model = model.clone();
13405 let lm = if want_logits {
13406 Some(self.weights.lm_head.q1_parts()?)
13407 } else {
13408 None
13409 };
13410 let dims = GraphDims {
13411 hidden: self.hidden_size,
13412 eps: self.rms_eps as f32,
13413 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13414 };
13415 let hs = self.hidden_size;
13418 let mut x = vec![0f32; hs];
13419 let mut graph = TokenGraph::new(&model, dims, &x)?;
13420 let mut folded = false;
13421 if let Some(eh) = m.eh_proj.q1_parts() {
13422 let e = self.embed_single(next_token);
13423 let mut cat = vec![0.0f32; 2 * hs];
13424 let (cat_e, cat_h) = cat.split_at_mut(hs);
13425 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13426 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13427 folded = graph.encode_input_proj(eh, &cat);
13428 }
13429 if !folded {
13430 x = self.mtp_block_input(m, hidden, next_token);
13431 graph = TokenGraph::new(&model, dims, &x)?;
13432 }
13433 spec_stamp("d.in");
13434 let l = AttnGpuLayer {
13435 attn_norm: &m.layer.input_norm,
13436 post_norm: &m.layer.post_norm,
13437 wq: pq,
13438 wk: pk,
13439 wv: pv,
13440 wo: po,
13441 ffn: MetalFfn::Dense {
13442 gate: g,
13443 up: u,
13444 down: dn,
13445 act: (d.act == Act::Gelu) as u8,
13446 },
13447 attn_out_norm: None,
13448 ffn_out_norm: None,
13449 layer_scale: None,
13450 };
13451 let (nh, nkv, hd, rd) = (
13452 self.num_heads,
13453 self.num_kv_heads,
13454 self.head_dim,
13455 self.rotary_dim,
13456 );
13457 let inv_freq = self.inv_freq.clone();
13458 {
13459 let cache = &m.kv;
13460 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13461 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13462 let cpu_stored = cpu_k[0].len() / hd;
13463 let p = AttnDeviceParams {
13464 kv_id: self.mtp_kv_id(),
13465 layer: Self::MTP_LAYER_BASE,
13466 nh,
13467 nkv,
13468 hd,
13469 rd,
13470 position,
13471 scale: self.attn_scale,
13472 eps: self.rms_eps as f32,
13473 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13474 late_qk_norm: self.qk_norm_after_rope,
13475 output_gate: *output_gate,
13476 q_norm: q_norm.as_deref(),
13477 k_norm: k_norm.as_deref(),
13478 inv_freq: &inv_freq,
13479 rope_scale: self.rope_scale,
13480 cpu_k,
13481 cpu_v,
13482 cpu_stored,
13483 cpu_gen: cache.generation(),
13484 o1: None,
13485 window: None,
13486 head_gate: None,
13487 v_norm: false,
13488 };
13489 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13490 return None;
13491 }
13492 }
13493 let draft_rows = if let Some(lm) = lm {
13499 self.draft_head_rows(lm.1)
13500 } else {
13501 0
13502 };
13503 if let Some(lm) = lm {
13504 if !graph.lm_head_ok(lm) {
13505 return None;
13506 }
13507 if draft_rows < lm.1 {
13508 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13509 return None;
13510 }
13511 } else {
13512 graph.encode_lm_head(&m.final_norm, lm);
13513 }
13514 }
13515 spec_stamp("d.enc");
13516 if graph.sync_checked().is_err() {
13517 return None;
13518 }
13519 spec_stamp("d.gpu");
13520 let mut logits = Vec::new();
13521 if let Some(lm) = lm {
13522 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13523 logits = attention::take_buf(n_read);
13524 graph.read_logits(&mut logits);
13525 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13527 }
13528 graph.finish(&mut x);
13529 let mut krow = attention::take_buf(nkv * hd);
13530 let mut vrow = attention::take_buf(nkv * hd);
13531 if crate::gpu_metal::kv_mirror_read_last(
13532 self.mtp_kv_id(),
13533 Self::MTP_LAYER_BASE,
13534 nkv,
13535 hd,
13536 &mut krow,
13537 &mut vrow,
13538 ) {
13539 m.kv.append(&krow, &vrow, &[]);
13540 }
13541 attention::recycle_buf(&mut krow);
13542 attention::recycle_buf(&mut vrow);
13543 spec_stamp("d.rd");
13544 Some((logits, x))
13545 }
13546
13547 fn mtp_chain_on() -> bool {
13560 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13561 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13562 }
13563
13564 #[cfg(target_os = "macos")]
13576 fn mtp_draft_chain_metal(
13577 &mut self,
13578 m: &mut MtpModule,
13579 hidden: &[f32],
13580 t_next: u32,
13581 position: usize,
13582 k: usize,
13583 ) -> Result<Vec<u32>, bool> {
13584 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13585 if k == 0
13586 || k > 64
13587 || !Self::mtp_chain_on()
13588 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13589 || !crate::gpu::q1_force()
13590 || !crate::gpu::enabled_here()
13591 || self.attn_softcap > 0.0
13592 || self.attention_heads_per_layer.is_some()
13593 || m.kv.mode != crate::kv_cache::KvMode::F32
13594 || m.kv.o1.is_some()
13595 || self.dsv4.is_some()
13597 || self.dsv41.is_some()
13598 || self.qwen4_exp.is_some()
13599 || self.g3n.is_some()
13600 {
13601 return Err(false);
13602 }
13603 let AttnKind::Full {
13604 wq,
13605 wk,
13606 wv,
13607 wo,
13608 q_norm,
13609 k_norm,
13610 output_gate,
13611 softplus_gate: None,
13612 bias: None,
13613 } = &m.layer.attn
13614 else {
13615 return Err(false);
13616 };
13617 let FfnKind::Dense(d) = &m.layer.ffn else {
13618 return Err(false);
13619 };
13620 if d.act != Act::Silu || !d.segs.is_empty() {
13621 return Err(false);
13622 }
13623 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13624 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13625 else {
13626 return Err(false);
13627 };
13628 let (Some(g), Some(u), Some(dn)) = (
13629 d.gate_proj.q1_parts(),
13630 d.up_proj.q1_parts(),
13631 d.down_proj.q1_parts(),
13632 ) else {
13633 return Err(false);
13634 };
13635 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13636 return Err(false);
13637 };
13638 let QTensor::Mapped { model, .. } = wq else {
13639 return Err(false);
13640 };
13641 let model = model.clone();
13642 let QTensor::Mapped {
13645 model: em,
13646 idx: eidx,
13647 dtype: cortiq_core::TensorDtype::Q4TiledP,
13648 ..
13649 } = &self.weights.embed_tokens
13650 else {
13651 return Err(false);
13652 };
13653 if !std::sync::Arc::ptr_eq(em, &model)
13654 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13655 {
13656 return Err(false);
13657 }
13658 let embed = (
13659 *eidx,
13660 self.weights.embed_tokens.rows(),
13661 self.weights.embed_tokens.cols(),
13662 );
13663 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13664 return Err(false);
13665 }
13666 let dims = GraphDims {
13667 hidden: self.hidden_size,
13668 eps: self.rms_eps as f32,
13669 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13670 };
13671 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13672 return Err(false);
13673 };
13674 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13675 return Err(false);
13676 }
13677 let l = AttnGpuLayer {
13678 attn_norm: &m.layer.input_norm,
13679 post_norm: &m.layer.post_norm,
13680 wq: pq,
13681 wk: pk,
13682 wv: pv,
13683 wo: po,
13684 ffn: MetalFfn::Dense {
13685 gate: g,
13686 up: u,
13687 down: dn,
13688 act: (d.act == Act::Gelu) as u8,
13689 },
13690 attn_out_norm: None,
13691 ffn_out_norm: None,
13692 layer_scale: None,
13693 };
13694 let (nh, nkv, hd, rd) = (
13695 self.num_heads,
13696 self.num_kv_heads,
13697 self.head_dim,
13698 self.rotary_dim,
13699 );
13700 let inv_freq = self.inv_freq.clone();
13701 let draft_rows = self.draft_head_rows(lm.1);
13702 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13703 if n_arg == 0 {
13704 return Err(false);
13705 }
13706 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13713 let t_chain = std::time::Instant::now();
13714 graph.chain_ids_init(t_next, k);
13715 let cpu_stored;
13716 {
13717 let cache = &m.kv;
13718 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13719 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13720 cpu_stored = cpu_k[0].len() / hd;
13721 for j in 0..k {
13722 if !graph.encode_chain_input(
13723 embed,
13724 j as u32,
13725 &m.enorm,
13726 &m.hnorm,
13727 self.embed_multiplier,
13728 eh,
13729 ) {
13730 return Err(false);
13731 }
13732 let p = AttnDeviceParams {
13736 kv_id: self.mtp_kv_id(),
13737 layer: Self::MTP_LAYER_BASE,
13738 nh,
13739 nkv,
13740 hd,
13741 rd,
13742 position: position + j,
13743 scale: self.attn_scale,
13744 eps: self.rms_eps as f32,
13745 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13746 late_qk_norm: self.qk_norm_after_rope,
13747 output_gate: *output_gate,
13748 q_norm: q_norm.as_deref(),
13749 k_norm: k_norm.as_deref(),
13750 inv_freq: &inv_freq,
13751 rope_scale: self.rope_scale,
13752 cpu_k: cpu_k.clone(),
13753 cpu_v: cpu_v.clone(),
13754 cpu_stored: cpu_stored + j,
13755 cpu_gen: cache.generation(),
13756 o1: None,
13757 window: None,
13758 head_gate: None,
13759 v_norm: false,
13760 };
13761 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13762 return Err(false);
13763 }
13764 if draft_rows < lm.1 {
13765 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13766 return Err(false);
13767 }
13768 } else {
13769 graph.encode_lm_head(&m.final_norm, lm);
13770 }
13771 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13772 return Err(false);
13773 }
13774 if split {
13775 graph.commit();
13778 }
13779 }
13780 }
13781 let t_enc = t_chain.elapsed();
13782 if graph.sync_checked().is_err() {
13783 return Err(true);
13784 }
13785 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
13786 eprintln!(
13787 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
13788 t_enc.as_secs_f64() * 1e3,
13789 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
13790 if split { ", split" } else { "" }
13791 );
13792 }
13793 let mut ids = vec![0u32; k];
13794 if !graph.chain_ids_read(&mut ids) {
13795 return Err(true);
13796 }
13797 let mut kbuf = vec![0f32; k * nkv * hd];
13798 let mut vbuf = vec![0f32; k * nkv * hd];
13799 if !crate::gpu_metal::kv_mirror_read_rows(
13800 self.mtp_kv_id(),
13801 Self::MTP_LAYER_BASE,
13802 nkv,
13803 hd,
13804 cpu_stored,
13805 k,
13806 &mut kbuf,
13807 &mut vbuf,
13808 ) {
13809 return Err(true);
13810 }
13811 for r in 0..k {
13812 m.kv.append(
13813 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13814 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13815 &[],
13816 );
13817 }
13818 Ok(ids)
13819 }
13820
13821 fn try_batch_graph_wgpu(
13822 &self,
13823 hiddens: &mut [f32],
13824 positions: &[usize],
13825 k: usize,
13826 spec: Option<crate::gpu::SpecTail<'_>>,
13827 ) -> crate::gpu::BatchGraphOutcome {
13828 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
13829 }
13830
13831 fn try_batch_graph_wgpu_prefix(
13836 &self,
13837 hiddens: &mut [f32],
13838 positions: &[usize],
13839 k: usize,
13840 spec: Option<crate::gpu::SpecTail<'_>>,
13841 layers_run: Option<&mut usize>,
13842 ) -> crate::gpu::BatchGraphOutcome {
13843 let graph_end = match self.mimo_moe.graph_prefix_end() {
13844 Some(end) if end < self.num_layers => {
13845 if layers_run.is_none() || spec.is_some() || end == 0 {
13846 return crate::gpu::BatchGraphOutcome::Declined;
13847 }
13848 end
13849 }
13850 _ => self.num_layers,
13851 };
13852 let _tb = std::time::Instant::now();
13853 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13854 if self.attn_softcap > 0.0 {
13855 return crate::gpu::BatchGraphOutcome::Declined; }
13857 if let Some(reason) = self
13860 .wgpu_graph_attn_decline()
13861 .or_else(|| self.batch_graph_gemma_decline())
13862 {
13863 self.note_graph_decline("wgpu batch graph", reason);
13864 return crate::gpu::BatchGraphOutcome::Declined;
13865 }
13866 let nh = self.num_heads;
13867 let (nkv, hd, rd) = self.layer_geom(0);
13868 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13869 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13870 if let Some((m, i, kind, rs)) = t
13871 .graph_weight()
13872 .or_else(|| t.graph_weight_descriptor())
13873 {
13874 let name = &m.tensors[i].name;
13875 let prism = if crate::prism::is_inverse_embedding(m, name) {
13876 crate::gpu::GraphPrismOp::InverseEmbedding
13877 } else if crate::prism::is_forward_weight(m, name) {
13878 crate::gpu::GraphPrismOp::Forward
13879 } else {
13880 crate::gpu::GraphPrismOp::None
13881 };
13882 return Some(crate::gpu::GraphW {
13883 idx: i,
13884 kind,
13885 row_scale: rs,
13886 data: &[],
13887 prism,
13888 affine: crate::prism::is_affine_target(m, name),
13889 });
13890 }
13891 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13892 eprintln!(
13893 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13894 t.rows(),
13895 t.cols()
13896 );
13897 }
13898 t.as_f32().map(|d| crate::gpu::GraphW {
13899 idx: 0,
13900 kind: 4,
13901 row_scale: &[],
13902 data: d,
13903 prism: crate::gpu::GraphPrismOp::None,
13904 affine: false,
13905 })
13906 }
13907 let built: Option<(
13908 Vec<crate::gpu::GraphLayer<'_>>,
13909 std::sync::Arc<cortiq_core::CmfModel>,
13910 )> = (|| {
13911 let mut layers = Vec::with_capacity(graph_end);
13912 let mut model = None;
13913 for li in 0..graph_end {
13914 let lw = &self.weights.layers[self.phys_layer(li)];
13915 let gffn = match &lw.ffn {
13922 FfnKind::Dense(d) if !d.segs.is_empty() => {
13923 if batch_debug {
13924 eprintln!("batch graph: dense segmented FFN at layer {li}");
13925 }
13926 return None;
13927 }
13928 FfnKind::Dense(d) => {
13929 let Some(act) = d.act.graph_act() else {
13930 if batch_debug {
13931 eprintln!(
13932 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13933 d.act
13934 );
13935 }
13936 return None;
13937 };
13938 crate::gpu::GraphFfn::Dense {
13939 gate: gw(&d.gate_proj)?,
13940 up: gw(&d.up_proj)?,
13941 down: gw(&d.down_proj)?,
13942 act,
13943 }
13944 }
13945 FfnKind::Moe(m) => {
13946 if m.route_tau.is_some() || m.mask.is_some() {
13953 return None;
13954 }
13955 let shared = m.shared.as_ref();
13959 let has_shared = shared.is_some();
13960 let shared_gated = matches!(shared, Some((_, Some(_))));
13961 let sgate = match shared {
13962 Some((_, Some(sg))) => gw(sg)?,
13963 _ => gw(&m.router)?,
13967 };
13968 let router = gw(&m.router)?;
13969 if router.prism != crate::gpu::GraphPrismOp::None
13975 || router.affine
13976 || sgate.prism != crate::gpu::GraphPrismOp::None
13977 || sgate.affine
13978 {
13979 return None;
13980 }
13981 let inter = m.experts.first()?.gate_proj.rows();
13982 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13983 let mut q4tp: Option<bool> = None;
13984 let mut gu_q2: Option<bool> = None;
13985 let mut q82: Option<bool> = None;
13986 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
13987 if !matches!(e.act, Act::Silu)
13988 || e.gate_proj.rows() != inter
13989 || e.up_proj.rows() != inter
13990 {
13991 return None;
13992 }
13993 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
13997 {
13998 Some((mm, gi)) => (
13999 mm,
14000 gi,
14001 e.up_proj.mapped_q4t()?.1,
14002 e.down_proj.mapped_q4t()?.1,
14003 false,
14004 false,
14005 false,
14006 ),
14007 None => match e.gate_proj.mapped_q2tp() {
14008 Some((mm, gi)) => (
14009 mm,
14010 gi,
14011 e.up_proj.mapped_q2tp()?.1,
14012 e.down_proj.mapped_q4tp()?.1,
14013 true,
14014 true,
14015 false,
14016 ),
14017 None => match e.gate_proj.mapped_q4tp() {
14018 Some((mm, gi)) => (
14019 mm,
14020 gi,
14021 e.up_proj.mapped_q4tp()?.1,
14022 e.down_proj.mapped_q4tp()?.1,
14023 true,
14024 false,
14025 false,
14026 ),
14027 None => {
14028 if !crate::gpu::moe_q82_graph_on() {
14029 return None;
14030 }
14031 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14032 (
14033 mm,
14034 gi,
14035 e.up_proj.mapped_q8_2f()?.1,
14036 e.down_proj.mapped_q8_2f()?.1,
14037 false,
14038 false,
14039 true,
14040 )
14041 }
14042 },
14043 },
14044 };
14045 if *q4tp.get_or_insert(is_p) != is_p
14046 || *gu_q2.get_or_insert(is_q2) != is_q2
14047 || *q82.get_or_insert(is_8) != is_8
14048 {
14049 return None;
14050 }
14051 if [gi, ui, di].into_iter().any(|idx| {
14052 mm.tensors
14053 .get(idx)
14054 .is_some_and(|t| {
14055 crate::prism::is_forward_weight(mm, &t.name)
14056 || crate::prism::is_affine_target(mm, &t.name)
14057 })
14058 }) {
14059 return None;
14060 }
14061 model.get_or_insert_with(|| mm.clone());
14062 experts.push((gi, ui, di));
14063 }
14064 crate::gpu::GraphFfn::Moe {
14065 router,
14066 shared_gate: sgate,
14067 experts,
14068 n_exp: m.experts.len(),
14069 top_k: m.top_k,
14070 inter,
14071 norm_topk: m.norm_topk_prob,
14072 q4tp: q4tp?,
14073 gu_q2: gu_q2.unwrap_or(false),
14074 q82: q82.unwrap_or(false),
14075 sigmoid: m.router_sigmoid,
14076 bias: m.expert_bias.as_deref(),
14077 has_shared,
14078 shared_gated,
14079 route_scale: m.routed_scaling,
14080 }
14081 }
14082 _ => return None,
14083 };
14084 let attn = match &lw.attn {
14085 AttnKind::Full {
14086 wq,
14087 wk,
14088 wv,
14089 wo,
14090 q_norm,
14091 k_norm,
14092 output_gate,
14093 softplus_gate,
14094 bias,
14095 } => {
14096 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14097 if batch_debug {
14098 eprintln!(
14099 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14100 softplus_gate.is_some(),
14101 self.attention_heads_per_layer.is_some()
14102 );
14103 }
14104 return None;
14105 }
14106 let (m, _, _, _) = wq
14107 .graph_weight()
14108 .or_else(|| wq.graph_weight_descriptor())?;
14109 model = Some(m.clone());
14110 crate::gpu::GraphAttn::Full {
14111 wq: gw(wq)?,
14112 wk: gw(wk)?,
14113 wv: gw(wv)?,
14114 wo: gw(wo)?,
14115 q_norm: q_norm.as_deref(),
14116 k_norm: k_norm.as_deref(),
14117 late_qk_norm: self.qk_norm_after_rope,
14118 bias: bias
14119 .as_ref()
14120 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14121 output_gate: *output_gate,
14122 cpu_k: self.kv_cache.layers[li].k_heads(),
14123 cpu_v: self.kv_cache.layers[li].v_heads(),
14124 cpu_base: self.kv_cache.layers[li].base(),
14125 geom: self.graph_attn_geom(li),
14126 head_gate: None,
14129 }
14130 }
14131 AttnKind::LinearGdn(w) => {
14132 let Some(cfg) = self.gdn_cfg else {
14133 if batch_debug {
14134 eprintln!("batch graph: no GDN config at layer {li}");
14135 }
14136 return None;
14137 };
14138 let (m, _, _, _) = w
14139 .in_proj_qkv
14140 .graph_weight()
14141 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14142 model = Some(m.clone());
14143 crate::gpu::GraphAttn::Gdn {
14144 qkv: gw(&w.in_proj_qkv)?,
14145 z: gw(&w.in_proj_z)?,
14146 a: gw(&w.in_proj_a)?,
14147 b: gw(&w.in_proj_b)?,
14148 out: gw(&w.out_proj)?,
14149 conv1d: &w.conv1d,
14150 a_log: &w.a_log,
14151 dt_bias: &w.dt_bias,
14152 norm: &w.norm,
14153 nv: cfg.num_v_heads,
14154 nk: cfg.num_k_heads,
14155 dk: cfg.key_head_dim,
14156 dv: cfg.value_head_dim,
14157 kk: cfg.conv_kernel,
14158 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14159 }
14160 }
14161 _ => return None,
14162 };
14163 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14166 layers.push(crate::gpu::GraphLayer {
14167 input_norm: &lw.input_norm,
14168 attn,
14169 post_norm: &lw.post_norm,
14170 attn_out_norm: if g4 { lw.attn_out_norm.as_deref() } else { None },
14171 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14172 layer_scale: if g4 { lw.layer_scale } else { None },
14173 ffn: gffn,
14174 });
14175 }
14176 Some((layers, model?))
14177 })();
14178 let Some((layers, model)) = built else {
14179 {
14180 use std::sync::atomic::{AtomicBool, Ordering};
14181 static SAID: AtomicBool = AtomicBool::new(false);
14182 if !SAID.swap(true, Ordering::Relaxed) {
14183 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14184 }
14185 }
14186 return crate::gpu::BatchGraphOutcome::Declined;
14187 };
14188 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14189 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14190 }
14191 crate::gpu::forward_batch_graph(
14192 &model,
14193 self.graph_kv_id,
14194 &layers,
14195 &self.inv_freq,
14196 hiddens,
14197 nh,
14198 nkv,
14199 hd,
14200 rd,
14201 self.hidden_size,
14202 self.intermediate_size,
14203 positions,
14204 self.kv_cache.max_seq_len,
14205 gemma,
14206 self.rms_eps as f32,
14207 self.attn_scale,
14208 k,
14209 &(0..graph_end)
14210 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14211 .collect::<Vec<_>>(),
14212 self.o1_epoch,
14213 spec,
14214 layers_run,
14215 )
14216 }
14217
14218 fn draft_probe() -> bool {
14222 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14223 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14224 }
14225
14226 #[cfg(feature = "gpu")]
14238 fn dsv4_spec_on() -> bool {
14239 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14240 *ON.get_or_init(|| {
14241 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14245 return v != "0";
14246 }
14247 std::env::var("CMF_DSV4_SPEC")
14254 .map(|v| v != "0")
14255 .unwrap_or_else(|_| {
14256 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14257 })
14258 })
14259 }
14260
14261 #[cfg(feature = "gpu")]
14268 fn dsv4_spec_step(
14269 &mut self,
14270 tip_token: u32,
14271 t_next: u32,
14272 next_pos: usize,
14273 max_extra: usize,
14274 drafted: &mut usize,
14275 accepted_ctr: &mut usize,
14276 ) -> Option<(Vec<u32>, usize)> {
14277 let t_all = std::time::Instant::now();
14278 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14279 thread_local! {
14280 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14281 const { std::cell::Cell::new(None) };
14282 }
14283 LAST.with(|l| {
14284 if let Some(prev) = l.get() {
14285 eprintln!(
14286 "между раундами {:.1} мс",
14287 prev.elapsed().as_secs_f64() * 1e3
14288 );
14289 }
14290 l.set(Some(std::time::Instant::now()));
14291 });
14292 }
14293 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14294 eprintln!("spec_step: вход pos={next_pos}");
14295 }
14296 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14297 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14298 if self.dspark.is_none() {
14300 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14301 if t.is_empty() {
14302 return None;
14303 }
14304 crate::dsv4::dspark_arm(&t, cfg.dim);
14305 self.dspark = Some(crate::dsv4::DsparkState::new(
14306 self.dsv4_mtp.len(),
14307 &cfg,
14308 t.len(),
14309 ));
14310 }
14311 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14312 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
14313 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14314 eprintln!("spec_step: пак не построился (targets {targets:?})");
14315 }
14316 let pack = pack?;
14317 let block = crate::dsv4::dspark_block();
14318 let b_box = self.dsv4.as_mut()?;
14319 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
14320 let ds = self.dspark.as_mut()?;
14321 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14324 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14325 if dbg {
14326 eprintln!("spec_step: нет захвата");
14327 }
14328 return None;
14329 }
14330 ds.have_hidden = true;
14331 let tip_pos = next_pos.checked_sub(1)?;
14332 let draft_started = std::time::Instant::now();
14333 let mut conf = Vec::new();
14334 let props = crate::dsv4::dspark_draft_gpu(
14335 g,
14336 &self.dsv4_mtp,
14337 &cfg,
14338 ds,
14339 pack,
14340 st.kv_id,
14341 tip_token,
14342 tip_pos,
14343 self.pool.as_deref(),
14344 &mut conf,
14345 );
14346 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14347 *drafted += block;
14348 if props.is_empty() || props[0] != t_next {
14349 if dbg {
14350 eprintln!(
14351 "spec_step: черновик {} (props0={:?} t_next={t_next})",
14352 if props.is_empty() {
14353 "пуст"
14354 } else {
14355 "мимо"
14356 },
14357 props.first()
14358 );
14359 }
14360 return None;
14361 }
14362 let mut k_verify = crate::dsv4::dspark_verify_k()
14369 .min(props.len())
14370 .min(max_extra.saturating_add(1));
14371 let conf_min = {
14377 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14378 *M.get_or_init(|| {
14379 std::env::var("CMF_DSPARK_CONF_MIN")
14380 .ok()
14381 .and_then(|v| v.parse().ok())
14382 .unwrap_or(0.0)
14383 })
14384 };
14385 if conf_min > 0.0 && conf.len() >= props.len() {
14386 let mut keep = 1usize;
14387 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14388 keep += 1;
14389 }
14390 k_verify = k_verify.min(keep.max(2));
14391 }
14392 if k_verify < 2 {
14393 return None;
14394 }
14395 let mut fed = Vec::with_capacity(k_verify);
14396 fed.push(t_next);
14397 fed.extend_from_slice(&props[1..k_verify]);
14398 let mut argmax = Vec::new();
14399 let mut logits_all = Vec::new();
14400 let mut walked = Vec::new();
14401 let txn = crate::dsv4::dsv4_verify_chunk(
14402 g,
14403 layers,
14404 &cfg,
14405 st,
14406 &fed,
14407 next_pos,
14408 &self.inv_freq,
14409 self.pool.as_deref(),
14410 &targets,
14411 &mut argmax,
14412 &mut logits_all,
14413 &mut walked,
14414 );
14415 if txn.is_none() && dbg {
14416 eprintln!("spec_step: verify отказал");
14417 }
14418 let txn = txn?;
14419 let spec_gpu_end = txn.gpu_end;
14420 let b = fed.len();
14421 let mut accepted = 1usize;
14422 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14423 accepted += 1;
14424 }
14425 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14430 accepted = 1;
14431 }
14432 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14433 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14434 }
14435 let t_fin = std::time::Instant::now();
14436 if !crate::dsv4::dsv4_spec_finish(
14437 g,
14438 layers,
14439 &cfg,
14440 st,
14441 txn,
14442 accepted,
14443 &fed,
14444 &self.inv_freq,
14445 self.pool.as_deref(),
14446 ) {
14447 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14448 return None;
14449 }
14450 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14451 eprintln!(
14452 "finish(k={accepted}): {:.1} мс",
14453 t_fin.elapsed().as_secs_f64() * 1e3
14454 );
14455 }
14456 *accepted_ctr += accepted - 1;
14457 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14462 let dev_caps: Vec<usize> = targets
14467 .iter()
14468 .copied()
14469 .filter(|&t| t < spec_gpu_end)
14470 .collect();
14471 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14472 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14473 return None;
14474 }
14475 for t in 0..accepted {
14476 let tip = t + 1 == accepted;
14477 for (slot, &tl) in targets.iter().enumerate() {
14478 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14479 let lo = (di * b + t) * hc * dim;
14480 crate::dsv4::dspark_capture(
14481 &caps_all[lo..lo + hc * dim],
14482 &cfg,
14483 slot,
14484 &mut ds.main_hidden,
14485 );
14486 } else if tip
14487 && crate::dsv4::dspark_peek_slot(slot, dim, {
14488 let lo = slot * dim;
14489 &mut ds.main_hidden[lo..lo + dim]
14490 })
14491 {
14492 } else {
14497 crate::dsv4::dspark_capture(
14501 &walked[t * hc * dim..(t + 1) * hc * dim],
14502 &cfg,
14503 slot,
14504 &mut ds.main_hidden,
14505 );
14506 }
14507 }
14508 crate::dsv4::dspark_ring_append(
14509 g,
14510 &self.dsv4_mtp,
14511 &cfg,
14512 ds,
14513 next_pos + t,
14514 self.pool.as_deref(),
14515 );
14516 }
14517 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14518 self.graph_logits = Some(row);
14519 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14524 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14525 crate::dsv4::pick_tally_arm();
14526 }
14527 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14528 eprintln!(
14529 "spec_step total {:.1} мс (k={accepted})",
14530 t_all.elapsed().as_secs_f64() * 1e3
14531 );
14532 }
14533 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14534 }
14535
14536 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14537 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14538 return;
14539 }
14540 let trunk_now = crate::dsv4::pick_tally_take();
14542 crate::dsv4::trunk_freq_note(&trunk_now);
14543 if !trunk_now.is_empty() {
14544 self.dspark_trunk_picks.push(trunk_now);
14545 let keep = crate::dsv4::dspark_block();
14546 if self.dspark_trunk_picks.len() > keep {
14547 self.dspark_trunk_picks.remove(0);
14548 }
14549 }
14550 for p in std::mem::take(&mut self.dspark_pending) {
14553 let Some(i) = position.checked_sub(p.0 + 1) else {
14554 continue;
14555 };
14556 let mut p = p;
14557 if i < p.1.len() {
14558 if p.2 && p.1[i] == token_id {
14559 p.3 = i + 1;
14560 } else {
14561 p.2 = false;
14562 }
14563 if i + 1 < p.1.len() {
14564 self.dspark_pending.push(p);
14565 continue;
14566 }
14567 }
14568 self.dspark_hist.push(p.3);
14569 self.dspark_real.push(token_id);
14570 }
14571 let Some(b) = &mut self.dsv4 else { return };
14572 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14573 let n_layers = layers.len();
14574 if self.dspark.is_none() {
14575 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14576 if t.is_empty() {
14577 return;
14578 }
14579 eprintln!(
14580 "DSpark: захват со слоёв {t:?}, блок {}",
14581 crate::dsv4::dspark_block()
14582 );
14583 crate::dsv4::dspark_arm(&t, cfg.dim);
14584 self.dspark = Some(crate::dsv4::DsparkState::new(
14585 self.dsv4_mtp.len(),
14586 &cfg,
14587 t.len(),
14588 ));
14589 }
14590 let ds = self.dspark.as_mut().unwrap();
14591 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14592 return; }
14594 let mut conf = Vec::new();
14595 crate::dsv4::pick_tally_arm();
14596 let draft_started = std::time::Instant::now();
14601 #[cfg(feature = "gpu")]
14602 let gpu_draft = crate::dsv4::dspark_gpu_on();
14603 #[cfg(not(feature = "gpu"))]
14604 let gpu_draft = false;
14605 let props = if gpu_draft {
14606 #[cfg(feature = "gpu")]
14607 {
14608 let kv_id = b.3.kv_id;
14609 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14610 Some(pk) => crate::dsv4::dspark_draft_gpu(
14611 g,
14612 &self.dsv4_mtp,
14613 &cfg,
14614 ds,
14615 pk,
14616 kv_id,
14617 token_id,
14618 position,
14619 self.pool.as_deref(),
14620 &mut conf,
14621 ),
14622 None => Vec::new(),
14623 }
14624 }
14625 #[cfg(not(feature = "gpu"))]
14626 Vec::new()
14627 } else {
14628 crate::gpu::cpu_scope(|| {
14629 crate::dsv4::dspark_draft(
14630 g,
14631 &self.dsv4_mtp,
14632 &cfg,
14633 ds,
14634 token_id,
14635 position,
14636 self.pool.as_deref(),
14637 &mut conf,
14638 )
14639 })
14640 };
14641 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14642 let draft_picks = crate::dsv4::pick_tally_take();
14643 crate::dsv4::dspark_freq_note(&draft_picks);
14644 crate::dsv4::pick_tally_arm();
14647 if !props.is_empty() {
14648 let (tu, tt) = {
14652 let flat: Vec<(usize, Vec<usize>)> = self
14653 .dspark_trunk_picks
14654 .iter()
14655 .flat_map(|v| v.iter().cloned())
14656 .collect();
14657 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14659 std::collections::HashMap::new();
14660 for (li, picks) in flat {
14661 per.entry(li).or_default().extend(picks);
14662 }
14663 let n = per.len().max(1);
14664 let mut u = 0usize;
14665 let mut t = 0usize;
14666 for (_, v) in per {
14667 t += v.len();
14668 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14669 }
14670 (u / n, t / n)
14671 };
14672 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14673 self.dspark_exp.push((tu, tt, du, dt));
14674 self.dspark_pending.push((position, props, true, 0));
14675 }
14676 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14677 let n = self.dspark_hist.len() as f32;
14678 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14679 let block = crate::dsv4::dspark_block();
14680 let mut at = vec![0usize; block + 1];
14681 for &k in &self.dspark_hist {
14682 at[k] += 1;
14683 }
14684 let mut surv = Vec::with_capacity(block);
14686 for i in 1..=block {
14687 let k = at[i..].iter().sum::<usize>() as f32 / n;
14688 surv.push(format!("{k:.2}"));
14689 }
14690 let distinct = self
14691 .dspark_real
14692 .iter()
14693 .collect::<std::collections::HashSet<_>>()
14694 .len();
14695 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14696 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14697 });
14698 let m = self.dspark_exp.len().max(1);
14699 eprintln!(
14700 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14701 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14702 self.dspark_hist.len(),
14703 mean + 1.0,
14704 surv.join(" ")
14705 );
14706 eprintln!(
14707 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14708 эксперты ствол {}/{} на слой за {block} токенов, \
14709 черновик {}/{} за блок, draft {:.2} мс/блок",
14710 self.dspark_real.len(),
14711 tu / m,
14712 tt / m,
14713 du / m,
14714 dt / m,
14715 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14716 );
14717 }
14718 }
14719
14720 fn forward_layers_upto(
14721 &mut self,
14722 hidden: &[f32],
14723 position: usize,
14724 task_mask: Option<&TaskMask>,
14725 upto: Option<usize>,
14726 ) -> Vec<f32> {
14727 if let Some(plan) = self.gpu_plan.clone() {
14733 if upto.is_none() && plan.len() > 1 {
14734 let mut h = hidden.to_vec();
14735 for &(dev, from, upto_incl) in plan.iter() {
14736 h = crate::gpu::with_device(dev, || {
14737 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14738 });
14739 }
14740 return h;
14741 }
14742 }
14743 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14744 }
14745
14746 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14751 self.set_gpu_plan_at(devices, None)
14752 }
14753
14754 pub fn set_gpu_plan_at(
14758 &mut self,
14759 devices: Option<&[usize]>,
14760 at: Option<usize>,
14761 ) -> Result<(), String> {
14762 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14763 self.gpu_plan = None;
14764 return Ok(());
14765 };
14766 self.split_supported()?;
14767 let n = self.num_layers;
14768 if devs.len() > n {
14769 return Err(format!("{} devices for {n} layers", devs.len()));
14770 }
14771 if let Some(k) = at {
14772 if k == 0 || k >= n {
14773 return Err(format!("split at {k}: the model has {n} layers"));
14774 }
14775 if devs.len() == 2 {
14776 self.gpu_plan = Some(std::sync::Arc::new(vec![
14777 (devs[0], 0, k - 1),
14778 (devs[1], k, n - 1),
14779 ]));
14780 return Ok(());
14781 }
14782 return Err(format!(
14783 "an explicit split point takes exactly 2 devices, got {}",
14784 devs.len()
14785 ));
14786 }
14787 let per = n.div_ceil(devs.len());
14788 let mut plan = Vec::with_capacity(devs.len());
14789 let mut from = 0usize;
14790 for &d in devs {
14791 if from >= n {
14792 break;
14793 }
14794 let upto = (from + per - 1).min(n - 1);
14795 plan.push((d, from, upto));
14796 from = upto + 1;
14797 }
14798 self.gpu_plan = Some(std::sync::Arc::new(plan));
14799 Ok(())
14800 }
14801
14802 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
14804 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
14805 }
14806
14807 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
14813 if let Some(x) = t.as_f32() {
14814 return x.to_vec();
14815 }
14816 let mut out = vec![0.0; t.rows() * t.cols()];
14817 for r in 0..t.rows() {
14818 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
14819 }
14820 out
14821 }
14822
14823 fn embryo_resident_eligible(&self) -> bool {
14824 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
14827 || self.num_layers != self.physical_layers
14828 || self.loop_final_norm
14829 || self.weights.layers.len() != self.num_layers
14830 || self.head_clusters.is_none()
14831 || self.final_softcap.is_some()
14832 || self.logit_multiplier.is_some()
14833 || self.attn_softcap != 0.0
14834 || self.mtp.is_some()
14835 || self.g3n.is_some()
14836 || self.dsv4.is_some()
14837 || self.dsv41.is_some()
14838 || self.qwen4_exp.is_some()
14839 || self.dyn_router.is_some()
14846 || self.dyn_phi_layer.is_some()
14847 || self.dyn_blend_loaded
14848 || self.o1_cfg.is_some()
14849 || self.swa.is_some()
14850 || self.sliding_layers.is_some()
14851 || self.global_attn.is_some()
14852 || self.attention_heads_per_layer.is_some()
14853 || self.attn_v_norm
14854 || self
14855 .kv_cache
14856 .layers
14857 .iter()
14858 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
14859 || self.rope_scale != 1.0
14860 || self.rope_scale_local != 1.0
14861 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
14862 || self.hidden_size == 0
14863 || self.hidden_size > 1024
14864 || self.intermediate_size > 1024
14865 || self.num_heads == 0
14866 || self.num_kv_heads == 0
14867 || self.num_heads % self.num_kv_heads != 0
14868 || self.num_heads.saturating_mul(self.head_dim) > 1024
14869 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
14870 || self.vocab_size == 0
14871 || self.kv_cache.max_seq_len == 0
14872 || self.rotary_dim == 0
14873 || self.rotary_dim > self.head_dim
14874 || self.rotary_dim % 2 != 0
14875 || self.inv_freq.len() < self.rotary_dim / 2
14876 {
14877 return false;
14878 }
14879 if self.weights.lm_head.as_f32().is_none()
14890 || self.weights.embed_tokens.as_f32().is_none()
14891 || self.weights.lm_head.rows() < self.vocab_size
14892 || self.weights.lm_head.cols() != self.hidden_size
14893 || self.weights.embed_tokens.rows() < self.vocab_size
14894 || self.weights.embed_tokens.cols() != self.hidden_size
14895 || self.weights.final_norm.len() != self.hidden_size
14896 {
14897 return false;
14898 }
14899 if let Some(cfg) = self.vmf_cfg {
14900 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14901 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14902 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14903 || cfg.state_len() == 0
14904 {
14905 return false;
14906 }
14907 }
14908 if let Some(g) = self.gdn_cfg {
14909 if g.num_v_heads == 0
14914 || g.num_k_heads == 0
14915 || g.num_v_heads % g.num_k_heads != 0
14916 || g.key_head_dim == 0
14917 || g.key_head_dim > 128
14918 || g.value_head_dim == 0
14919 || g.value_head_dim > 256
14920 || g.value_head_dim % 4 != 0
14921 || g.conv_kernel == 0
14922 || g.num_v_heads > 512
14923 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14924 || g.conv_dim() > 2048
14925 || g.conv_dim() % 4 != 0
14926 || g.hidden_size != self.hidden_size
14927 || g.output_gate_sigmoid
14928 || g.rms_eps != self.rms_eps
14929 || g.state_len() == 0
14930 {
14931 return false;
14932 }
14933 }
14934 let mut full_seen = false;
14935 for lw in &self.weights.layers {
14936 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14937 return false;
14938 }
14939 match &lw.attn {
14940 AttnKind::LinearGdn(w) => {
14941 let Some(g) = self.gdn_cfg else {
14942 return false;
14943 };
14944 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14945 if w.in_proj_qkv.rows() != g.conv_dim()
14946 || w.in_proj_qkv.cols() != self.hidden_size
14947 || w.in_proj_qkv.as_f32().is_none()
14948 || w.in_proj_z.rows() != nv * dv
14949 || w.in_proj_z.cols() != self.hidden_size
14950 || w.in_proj_z.as_f32().is_none()
14951 || w.in_proj_a.rows() != nv
14952 || w.in_proj_a.cols() != self.hidden_size
14953 || w.in_proj_a.as_f32().is_none()
14954 || w.in_proj_b.rows() != nv
14955 || w.in_proj_b.cols() != self.hidden_size
14956 || w.in_proj_b.as_f32().is_none()
14957 || w.conv1d.len() != g.conv_dim() * kk
14958 || w.a_log.len() != nv
14959 || w.dt_bias.len() != nv
14960 || w.norm.len() != dv
14961 || w.out_proj.rows() != self.hidden_size
14962 || w.out_proj.cols() != nv * dv
14963 || w.out_proj.as_f32().is_none()
14964 {
14965 return false;
14966 }
14967 }
14968 AttnKind::Linear(w) => {
14969 let Some(cfg) = self.vmf_cfg else {
14970 return false;
14971 };
14972 if w.thq.rows() != cfg.num_heads * cfg.nphase
14973 || w.thq.cols() != self.hidden_size
14974 || w.thq.as_f32().is_none()
14975 || w.thk.rows() != cfg.num_heads * cfg.nphase
14976 || w.thk.cols() != self.hidden_size
14977 || w.thk.as_f32().is_none()
14978 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14979 || w.v_proj.cols() != self.hidden_size
14980 || w.v_proj.as_f32().is_none()
14981 || w.out_proj.rows() != self.hidden_size
14982 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14983 || w.out_proj.as_f32().is_none()
14984 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14985 {
14986 return false;
14987 }
14988 if let Some((kg, kb)) = &w.k_gate {
14989 if kg.rows() != cfg.num_heads
14990 || kg.cols() != self.hidden_size
14991 || kg.as_f32().is_none()
14992 || kb.len() != cfg.num_heads
14993 {
14994 return false;
14995 }
14996 }
14997 if let Some(conv) = &w.conv {
14998 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
14999 return false;
15000 }
15001 }
15002 }
15003 AttnKind::Full {
15004 wq,
15005 wk,
15006 wv,
15007 wo,
15008 q_norm,
15009 k_norm,
15010 output_gate,
15011 softplus_gate,
15012 bias,
15013 } => {
15014 if full_seen
15015 || q_norm.is_some()
15016 || k_norm.is_some()
15017 || *output_gate
15018 || softplus_gate.is_some()
15019 || bias.is_some()
15020 || wq.as_f32().is_none()
15021 || wk.as_f32().is_none()
15022 || wv.as_f32().is_none()
15023 || wo.as_f32().is_none()
15024 || wq.rows() != self.num_heads * self.head_dim
15025 || wk.rows() != self.num_kv_heads * self.head_dim
15026 || wv.rows() != self.num_kv_heads * self.head_dim
15027 || wq.cols() != self.hidden_size
15028 || wk.cols() != self.hidden_size
15029 || wv.cols() != self.hidden_size
15030 || wo.rows() != self.hidden_size
15031 || wo.cols() != self.num_heads * self.head_dim
15032 {
15033 return false;
15034 }
15035 full_seen = true;
15036 }
15037 AttnKind::Bounded(w) => {
15038 let Some(ac) = self.anchor_core.as_ref() else {
15041 return false;
15042 };
15043 if self.bounded_rope.is_none()
15044 || w.window != ac.window
15045 || w.sink != ac.sink
15046 || w.window == 0
15047 || w.window + w.sink > 256
15048 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15049 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15050 || w.wq.as_f32().is_none()
15051 || w.wk.as_f32().is_none()
15052 || w.wv.as_f32().is_none()
15053 || w.wo.as_f32().is_none()
15054 || w.wq.rows() != self.num_heads * self.head_dim
15055 || w.wk.rows() != self.num_kv_heads * self.head_dim
15056 || w.wv.rows() != self.num_kv_heads * self.head_dim
15057 || w.wq.cols() != self.hidden_size
15058 || w.wk.cols() != self.hidden_size
15059 || w.wv.cols() != self.hidden_size
15060 || w.wo.rows() != self.hidden_size
15061 || w.wo.cols() != self.num_heads * self.head_dim
15062 {
15063 return false;
15064 }
15065 }
15066 _ => return false,
15067 }
15068 match &lw.ffn {
15069 FfnKind::Dense(d) => {
15070 if d.act != Act::Silu
15071 || !d.segs.is_empty()
15072 || d.gate_proj.as_f32().is_none()
15073 || d.up_proj.as_f32().is_none()
15074 || d.down_proj.as_f32().is_none()
15075 || d.gate_proj.rows() != self.intermediate_size
15076 || d.gate_proj.cols() != self.hidden_size
15077 || d.up_proj.rows() != self.intermediate_size
15078 || d.up_proj.cols() != self.hidden_size
15079 || d.down_proj.rows() != self.hidden_size
15080 || d.down_proj.cols() != self.intermediate_size
15081 {
15082 return false;
15083 }
15084 }
15085 FfnKind::Moe(m) => {
15086 if m.resonance.is_none()
15087 || m.top_k != 1
15088 || m.router_sigmoid
15089 || !m.norm_topk_prob
15090 || m.expert_bias.is_some()
15091 || m.routed_scaling != 1.0
15092 || m.route_tau.is_some()
15093 || m.shared.is_none()
15094 || m.mask.is_some()
15095 || m.per_expert_scale.is_some()
15096 || m.router_input_norm
15097 || m.experts.is_empty()
15098 || m.experts.len() > 8
15099 {
15100 return false;
15101 }
15102 let r = m.resonance.as_ref().unwrap();
15103 if r.mu.len() != m.experts.len() * self.hidden_size
15104 || r.bias.len() != m.experts.len()
15105 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15106 || r.k > 128
15107 {
15108 return false;
15109 }
15110 let Some((shared, gate)) = &m.shared else {
15111 return false;
15112 };
15113 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15114 return false;
15115 }
15116 if shared.gate_proj.as_f32().is_none()
15117 || shared.up_proj.as_f32().is_none()
15118 || shared.down_proj.as_f32().is_none()
15119 || shared.gate_proj.rows() != self.intermediate_size
15120 || shared.gate_proj.cols() != self.hidden_size
15121 || shared.up_proj.rows() != self.intermediate_size
15122 || shared.up_proj.cols() != self.hidden_size
15123 || shared.down_proj.rows() != self.hidden_size
15124 || shared.down_proj.cols() != self.intermediate_size
15125 {
15126 return false;
15127 }
15128 for e in &m.experts {
15129 if e.act != Act::Silu
15130 || !e.segs.is_empty()
15131 || e.gate_proj.as_f32().is_none()
15132 || e.up_proj.as_f32().is_none()
15133 || e.down_proj.as_f32().is_none()
15134 || e.gate_proj.rows() != self.intermediate_size
15135 || e.gate_proj.cols() != self.hidden_size
15136 || e.up_proj.rows() != self.intermediate_size
15137 || e.up_proj.cols() != self.hidden_size
15138 || e.down_proj.rows() != self.hidden_size
15139 || e.down_proj.cols() != self.intermediate_size
15140 {
15141 return false;
15142 }
15143 }
15144 }
15145 FfnKind::DenseMoe(_) => return false,
15146 }
15147 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15148 return false;
15149 }
15150 }
15151 if full_seen && self.anchor_core.is_some() {
15152 return false;
15153 }
15154 full_seen || self.num_layers > 0
15155 }
15156
15157 fn embryo_resident_wanted(&self) -> bool {
15162 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15163 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15164 && matches!(
15165 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15166 Ok("1") | Ok("parallel")
15167 )
15168 && crate::gpu::enabled_here()
15169 && !self.graph_refused()
15170 && self.embryo_resident_eligible()
15171 }
15172
15173 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15181 if ids.len() < 2
15182 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15183 || !self.embryo_resident_wanted()
15184 {
15185 return None;
15186 }
15187 let model = self.ensure_embryo_graph()?;
15188 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15189 .ok()
15190 .and_then(|v| v.parse::<usize>().ok())
15191 .filter(|&v| v >= 1)
15192 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15193 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15194 let hs = self.hidden_size;
15195 let n = ids.len();
15196 let mut pos = start;
15197 let mut last = None;
15198 let mut rows = Vec::with_capacity(cmax * hs);
15199 while pos < n {
15200 let end = (pos + cmax).min(n);
15201 rows.clear();
15202 for &id in &ids[pos..end] {
15203 rows.extend_from_slice(&self.embed_single(id));
15204 }
15205 let mut lg = Vec::new();
15206 if !crate::gpu::forward_embryo_graph_chunk(
15207 &model,
15208 self.graph_kv_id,
15209 &rows,
15210 pos,
15211 end - pos,
15212 &mut lg,
15213 ) {
15214 if pos == start {
15215 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15216 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15217 }
15218 return None;
15219 }
15220 self.kv_cache.clear();
15224 self.clear_history();
15225 crate::gpu::graph_kv_reset(self.graph_kv_id);
15226 panic!(
15227 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15228 );
15229 }
15230 last = Some(lg);
15231 pos = end;
15232 }
15233 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15234 eprintln!(
15235 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15236 n - start,
15237 (n - start).div_ceil(cmax)
15238 );
15239 }
15240 last
15241 }
15242
15243 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15244 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15245 const UMAX: u32 = u32::MAX;
15246 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15247 const REC: usize = 64;
15248 struct Pack {
15249 data: Vec<f32>,
15250 }
15251 impl Pack {
15252 fn put(&mut self, x: &[f32]) -> u32 {
15253 if x.is_empty() {
15254 return u32::MAX;
15255 }
15256 let off = self.data.len();
15257 self.data.extend_from_slice(x);
15258 off as u32
15259 }
15260 }
15261 let vmf = self.vmf_cfg;
15265 let gdn = self.gdn_cfg;
15266 let mut pack = Pack { data: Vec::new() };
15267 let mut meta = vec![0u32; HEADER];
15268 meta[0] = self.hidden_size as u32;
15269 meta[1] = self.intermediate_size as u32;
15270 meta[2] = self.vocab_size as u32;
15271 meta[3] = self.num_layers as u32;
15272 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15273 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15274 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15275 if let Some(g) = gdn {
15276 meta[24] = g.num_v_heads as u32;
15277 meta[25] = g.num_k_heads as u32;
15278 meta[26] = g.key_head_dim as u32;
15279 meta[27] = g.value_head_dim as u32;
15280 meta[28] = g.conv_kernel as u32;
15281 meta[29] = g.conv_dim() as u32;
15282 }
15283 meta[7] = self.num_heads as u32;
15284 meta[8] = self.num_kv_heads as u32;
15285 meta[9] = self.head_dim as u32;
15286 meta[10] = self.kv_cache.max_seq_len as u32;
15287 let clusters = self.head_clusters.as_ref().unwrap();
15288 let cluster_count = clusters.len() / self.hidden_size;
15289 if clusters.len() % self.hidden_size != 0
15290 || cluster_count == 0
15291 || cluster_count > 1024
15292 || self.vocab_size % cluster_count != 0
15293 || self.weights.lm_head.rows() < self.vocab_size
15294 || self.weights.final_norm.len() != self.hidden_size
15295 {
15296 return None;
15297 }
15298 meta[11] = cluster_count as u32;
15299 meta[12] = (self.vocab_size / cluster_count) as u32;
15300 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15301 meta[16] = self.rotary_dim as u32;
15302 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15303 meta[19] = (self.rms_eps as f32).to_bits();
15304 let max_conv = self
15305 .weights
15306 .layers
15307 .iter()
15308 .filter_map(|lw| match &lw.attn {
15309 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
15310 _ => None,
15311 })
15312 .max()
15313 .unwrap_or(1);
15314 let phase_stride = vmf
15320 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15321 .unwrap_or(0);
15322 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15323 let state_stride = phase_stride.max(gdn_stride);
15324 let bounded = self.anchor_core.clone();
15328 let (anchor_window, anchor_sink) = bounded
15329 .as_ref()
15330 .map(|ac| (ac.window, ac.sink))
15331 .unwrap_or((0, 0));
15332 let kv_stride = if bounded.is_some() {
15333 2usize
15334 .saturating_mul(self.num_kv_heads)
15335 .saturating_mul(anchor_window)
15336 .saturating_mul(self.head_dim)
15337 } else {
15338 2usize
15339 .saturating_mul(self.num_kv_heads)
15340 .saturating_mul(self.kv_cache.max_seq_len)
15341 .saturating_mul(self.head_dim)
15342 };
15343 meta[14] = state_stride as u32;
15344 meta[15] = kv_stride as u32;
15345 meta[18] = anchor_window as u32;
15346 meta[20] = anchor_sink as u32;
15347 meta[21] = match &self.bounded_rope {
15348 Some(rope) => {
15349 let off = pack.put(&rope.cos);
15351 let _ = pack.put(&rope.sin);
15352 off
15353 }
15354 None => UMAX,
15355 };
15356 let mut full_seen = false;
15357 let mut bounded_seen = 0usize;
15358 let mut phase_seen = 0usize;
15362 let mut gdn_seen = 0usize;
15363 for (li, lw) in self.weights.layers.iter().enumerate() {
15364 let base = meta.len();
15365 meta.resize(base + REC, UMAX);
15366 meta[base] = match &lw.attn {
15367 AttnKind::Linear(w) if w.phase_delta => 1,
15368 AttnKind::Linear(_) => 0,
15369 AttnKind::Full { .. } => 2,
15370 AttnKind::Bounded(_) => 3,
15371 AttnKind::LinearGdn(_) => 4,
15372 _ => UMAX,
15373 };
15374 meta[base + 1] = pack.put(&lw.input_norm);
15375 meta[base + 2] = pack.put(&lw.post_norm);
15376 meta[base + 25] = match &lw.attn {
15377 AttnKind::Linear(_) => {
15378 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15379 phase_seen += 1;
15380 off
15381 }
15382 AttnKind::LinearGdn(_) => {
15383 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15384 gdn_seen += 1;
15385 off
15386 }
15387 _ => UMAX,
15388 };
15389 match &lw.attn {
15390 AttnKind::LinearGdn(w) => {
15391 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15394 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15395 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15396 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15397 meta[base + 60] = pack.put(&w.conv1d);
15398 meta[base + 61] = pack.put(&w.a_log);
15399 meta[base + 62] = pack.put(&w.dt_bias);
15400 meta[base + 63] = pack.put(&w.norm);
15401 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15402 meta[base + 24] = 0;
15403 }
15404 AttnKind::Linear(w) => {
15405 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15406 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15407 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15408 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15409 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15410 meta[base + 7] = pack.put(&decay);
15411 if let Some((kg, kb)) = &w.k_gate {
15412 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15413 meta[base + 9] = pack.put(kb);
15414 }
15415 if let Some(conv) = &w.conv {
15416 meta[base + 10] = pack.put(conv);
15417 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15418 } else {
15419 meta[base + 24] = 0;
15420 }
15421 }
15422 AttnKind::Full { wq, wk, wv, wo, .. } => {
15423 full_seen = true;
15424 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15425 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15426 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15427 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15428 meta[base + 26] = (li * kv_stride) as u32;
15429 }
15430 AttnKind::Bounded(w) => {
15431 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15432 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15433 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15434 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15435 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15437 meta[base + 27] = pack.put(&w.sink_k);
15438 meta[base + 28] = pack.put(&w.sink_v);
15439 bounded_seen += 1;
15440 }
15441 _ => return None,
15442 }
15443 match &lw.ffn {
15444 FfnKind::Dense(d) => {
15445 meta[base + 15] = 0;
15446 meta[base + 16] = 0;
15447 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15448 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15449 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15450 }
15451 FfnKind::Moe(m) => {
15452 let r = m.resonance.as_ref().unwrap();
15453 let (shared, _) = m.shared.as_ref().unwrap();
15454 meta[base + 15] = 1;
15455 meta[base + 16] = m.experts.len() as u32;
15456 meta[base + 17] = pack.put(&r.mu);
15457 meta[base + 18] = pack.put(&r.u);
15458 meta[base + 19] = pack.put(&r.bias);
15459 meta[base + 20] = r.k as u32;
15460 let mut shell = r.effective_shell(m.experts.len());
15469 shell.push(f32::NEG_INFINITY);
15470 meta[base + 30] = pack.put(&shell);
15471 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15472 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15473 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15474 for (e, ex) in m.experts.iter().enumerate() {
15475 meta[base + 32 + e * 3] =
15476 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15477 meta[base + 33 + e * 3] =
15478 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15479 meta[base + 34 + e * 3] =
15480 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15481 }
15482 }
15483 FfnKind::DenseMoe(_) => return None,
15484 }
15485 }
15486 if !full_seen && self.num_layers == 0 {
15487 return None;
15488 }
15489 let id = {
15490 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15491 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15492 };
15493 let model = crate::gpu::EmbryoGraphModel {
15494 id,
15495 hidden: self.hidden_size,
15496 intermediate: self.intermediate_size,
15497 vocab: self.vocab_size,
15498 layers: self.num_layers,
15499 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15500 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15501 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15502 anchor_q_heads: self.num_heads,
15503 anchor_kv_heads: self.num_kv_heads,
15504 anchor_head_dim: self.head_dim,
15505 rotary_dim: self.rotary_dim,
15506 max_seq: self.kv_cache.max_seq_len,
15507 cluster_count,
15508 cluster_size: self.vocab_size / cluster_count,
15509 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15510 state_stride,
15511 kv_stride,
15512 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15513 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15514 weights: pack.data,
15515 meta,
15516 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15517 clusters: clusters.as_ref().clone(),
15518 final_norm: self.weights.final_norm.clone(),
15519 inv_freq: self.inv_freq.as_ref().clone(),
15520 bounded: bounded.is_some(),
15521 kv_layers: if bounded.is_some() {
15522 bounded_seen
15523 } else {
15524 self.num_layers
15525 },
15526 state_layers: phase_seen + gdn_seen,
15527 anchor_window,
15528 anchor_sink,
15529 phase_layers: phase_seen,
15530 gdn_layers: gdn_seen,
15531 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15532 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15533 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15534 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15535 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15536 };
15537 self.embryo_graph = Some(std::sync::Arc::new(model));
15538 }
15539 self.embryo_graph.clone()
15540 }
15541
15542 fn forward_layers_span(
15543 &mut self,
15544 hidden: &[f32],
15545 position: usize,
15546 task_mask: Option<&TaskMask>,
15547 from: usize,
15548 upto: Option<usize>,
15549 ) -> Vec<f32> {
15550 #[cfg(target_os = "macos")]
15551 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
15552 position,
15553 from,
15554 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
15555 );
15556 debug_assert!(
15557 from == 0
15558 || (self.dsv4.is_none()
15559 && self.dsv41.is_none()
15560 && self.qwen4_exp.is_none()
15561 && self.g3n.is_none())
15562 );
15563 #[cfg(target_os = "macos")]
15569 if !crate::gpu_metal::wait_replay() {
15570 self.fail_metal_graph("the pending async replay failed before a plain forward");
15571 return vec![0.0; self.hidden_size];
15572 }
15573 if let Some(b) = &mut self.qwen4_exp {
15574 let _ = (task_mask, upto);
15575 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15576 let mut logits = Vec::new();
15577 crate::qwen4_exp::forward_token(
15578 &b.0,
15579 &b.1,
15580 &b.2,
15581 &mut b.3,
15582 token_id,
15583 position,
15584 &self.inv_freq,
15585 self.pool.as_deref(),
15586 &mut logits,
15587 true,
15588 );
15589 self.graph_logits = Some(logits);
15590 return vec![0.0; self.hidden_size];
15591 }
15592 if let Some(b) = &mut self.dsv4 {
15598 let _ = (task_mask, upto);
15599 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15600 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15601 st.pos = position;
15602 let mut logits = Vec::new();
15603 crate::dsv4::forward_token(
15604 g,
15605 layers,
15606 &cfg,
15607 st,
15608 token_id,
15609 &self.inv_freq,
15610 self.pool.as_deref(),
15611 &mut logits,
15612 );
15613 self.graph_logits = Some(logits);
15614 self.dspark_probe(position, token_id);
15615 return vec![0.0; self.hidden_size];
15618 }
15619 if let Some(b) = &mut self.dsv41 {
15621 let _ = (task_mask, upto);
15622 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15623 let mut logits = Vec::new();
15624 crate::dsv41::forward_token(
15625 &b.0,
15626 &b.1,
15627 &b.2,
15628 &mut b.3,
15629 token_id,
15630 position,
15631 self.pool.as_deref(),
15632 &mut logits,
15633 );
15634 self.graph_logits = Some(logits);
15635 return vec![0.0; self.hidden_size];
15636 }
15637 if let Some(b) = &self.g3n {
15640 let _ = (task_mask, upto);
15641 return crate::g3n::g3n_forward(
15642 &b.0,
15643 &b.1,
15644 hidden,
15645 position,
15646 &mut self.kv_cache.layers,
15647 self.num_heads,
15648 self.num_kv_heads,
15649 self.head_dim,
15650 self.pool.as_deref(),
15651 );
15652 }
15653 if from == 0
15660 && upto.is_none()
15661 && task_mask.is_none()
15662 && self.anchor_core.is_some()
15663 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15664 {
15665 static ONCE: std::sync::Once = std::sync::Once::new();
15666 ONCE.call_once(|| {
15667 eprintln!(
15668 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15669 unsupported={} eligible={}",
15670 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15671 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15672 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15673 crate::gpu::enabled_here(),
15674 self.graph_refused(),
15675 self.embryo_resident_eligible(),
15676 );
15677 });
15678 }
15679 if from == 0
15680 && upto.is_none()
15681 && task_mask.is_none()
15682 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15686 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15687 && matches!(
15691 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15692 Ok("1") | Ok("parallel")
15693 )
15694 && crate::gpu::enabled_here()
15695 && !self.graph_refused()
15696 && (position == 0 || self.device_sequence_position().is_some())
15702 && self.embryo_resident_eligible()
15703 && let Some(model) = self.ensure_embryo_graph()
15704 {
15705 let mut lg = Vec::new();
15706 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15707 {
15708 self.graph_logits = Some(lg);
15709 return vec![0.0; self.hidden_size];
15710 }
15711 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15712 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15713 }
15714 self.mark_graph_refused();
15720 if position != 0 {
15721 self.kv_cache.clear();
15725 self.clear_history();
15726 crate::gpu::graph_kv_reset(self.graph_kv_id);
15727 panic!(
15728 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15729 );
15730 }
15731 }
15732 let mut h = hidden.to_vec();
15733 self.mimo_moe_prepare();
15736 let _mimo_q8 = self.mimo_moe.is_on()
15737 .then(crate::qtensor::enter_full_gpu_q8_scope);
15738 let (nh, _nkv, _hd, hs, _rd, eps) = (
15741 self.num_heads,
15742 self.num_kv_heads,
15743 self.head_dim,
15744 self.hidden_size,
15745 self.rotary_dim,
15746 self.rms_eps,
15747 );
15748 let pool = self.pool.clone();
15749 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15761 let graph_on = match graph_env.as_deref() {
15762 Some("0") => false,
15763 Some("prefill") => false, Some(_) => true,
15765 None => crate::gpu::wgpu_graph_default(),
15771 };
15772 let graph_trusted =
15773 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15774 let race_eligible = graph_on
15775 && upto.is_none()
15776 && task_mask.is_none()
15777 && from == 0
15778 && !self.graph_refused();
15779 let mut tail_start = 0usize;
15780 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15781 let t_graph = std::time::Instant::now();
15782 let mut lg = Vec::new();
15783 let mut gl = 0usize;
15784 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
15785 let declined = built.is_none();
15786 let built = match built {
15787 Some(Ok(hh)) => Some(hh),
15788 Some(Err(())) => {
15789 self.clear_sequence_state();
15793 self.graph_failed
15794 .store(true, std::sync::atomic::Ordering::Relaxed);
15795 self.cancel
15796 .store(true, std::sync::atomic::Ordering::Relaxed);
15797 tracing::error!("token graph failed after admission; sequence state cleared");
15798 return vec![0.0; self.hidden_size];
15799 }
15800 None => None,
15801 };
15802 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
15807 self.mark_graph_refused();
15808 }
15809 graph_note(built.is_some(), gl, self.num_layers);
15810 if let Some(hh) = built {
15811 let dur = t_graph.elapsed();
15812 if std::env::var("CMF_GRAPH_PROF").is_ok() {
15813 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
15814 }
15815 if gl > 0 && gl < self.num_layers {
15816 h = hh;
15822 tail_start = gl;
15823 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
15824 if !graph_trusted {
15825 crate::gpu::graph_race_record(true, dur);
15826 }
15827 if !lg.is_empty() {
15828 lg.resize(self.vocab_size, 0.0);
15831 if let Some(c) = self.final_softcap {
15832 softcap_in_place(&mut lg, c, self.pool.as_deref());
15833 }
15834 self.graph_logits = Some(lg);
15835 }
15836 return hh;
15837 }
15838 }
15844 }
15845 let span = from > 0 || upto.is_some();
15869 if span && graph_on && task_mask.is_none() && graph_trusted {
15870 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
15871 let mut lg = Vec::new();
15872 let mut gl = 0usize;
15873 let span_res =
15874 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
15875 let span_res = match span_res {
15876 Some(Ok(hh)) => Some(hh),
15877 Some(Err(())) => {
15878 self.clear_sequence_state();
15879 self.graph_failed
15880 .store(true, std::sync::atomic::Ordering::Relaxed);
15881 self.cancel
15882 .store(true, std::sync::atomic::Ordering::Relaxed);
15883 tracing::error!(
15884 "span token graph failed after admission; sequence state cleared"
15885 );
15886 return vec![0.0; self.hidden_size];
15887 }
15888 None => None,
15889 };
15890 graph_note(span_res.is_some(), gl, upto_excl - from);
15891 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15892 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15896 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15897 eprintln!(
15898 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15899 upto_excl - from,
15900 span_res.is_some()
15901 );
15902 }
15903 }
15904 if let Some(hh) = span_res {
15905 if gl == upto_excl - from {
15906 if !lg.is_empty() {
15907 lg.resize(self.vocab_size, 0.0);
15908 if let Some(c) = self.final_softcap {
15909 softcap_in_place(&mut lg, c, self.pool.as_deref());
15910 }
15911 self.graph_logits = Some(lg);
15912 }
15913 crate::gpu::set_layer(-1);
15914 return hh;
15915 }
15916 h = hh;
15918 tail_start = from + gl;
15919 }
15920 }
15921 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15926
15927 let host_tail = tail_start > from;
15937 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15938 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15939
15940 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15941 #[cfg(target_os = "macos")]
15942 let mut gpu_skip_until = 0usize;
15943 for li in tail_start.max(from)..self.num_layers {
15944 let _capacity_tail = automatic_gpu_prefix
15945 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15946 .map(|_| crate::gpu::enter_cpu_scope());
15947 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15949 if li > u {
15950 break;
15951 }
15952 }
15953 if let Some(mask) = task_mask {
15954 if !mask.layer_alive(li) {
15955 continue; }
15957 }
15958 #[cfg(target_os = "macos")]
15962 {
15963 if li < gpu_skip_until {
15964 continue;
15965 }
15966 if task_mask.is_none() {
15967 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15968 if self
15969 .graph_failed
15970 .load(std::sync::atomic::Ordering::Relaxed)
15971 {
15972 return vec![0.0; self.hidden_size];
15976 }
15977 if end > li {
15978 gpu_skip_until = end;
15979 if self.is_loop_end(end - 1) && end < self.num_layers {
15982 h = inference::rms_norm(
15983 &h,
15984 &self.weights.final_norm,
15985 self.rms_eps,
15986 self.norm_style,
15987 );
15988 }
15989 continue;
15990 }
15991 }
15992 }
15993
15994 if task_mask.is_none() {
15995 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
15996 crate::gpu::BatchGraphOutcome::Completed => continue,
15997 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
15998 crate::gpu::BatchGraphOutcome::Declined => {},
15999 }
16000 }
16001 #[cfg(feature = "gpu")]
16002 self.pull_lagging_host_kv(li, li + 1, position);
16003 let lw = &self.weights.layers[self.phys_layer(li)];
16004 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
16005 if tp.parse::<usize>().ok() == Some(position) {
16006 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16007 eprintln!(
16008 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16009 h[0], h[1]
16010 );
16011 }
16012 }
16013 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16016 inference::rms_norm_into(
16017 &h,
16018 &lw.input_norm,
16019 self.rms_eps,
16020 self.norm_style,
16021 &mut self.ws.n1,
16022 );
16023 drop(prof);
16024
16025 let attn_out = match &lw.attn {
16026 AttnKind::Mla(w) => {
16027 let inv_freq_l = self.layer_inv_freq(li);
16028 let rs = self.layer_rope_scale(li);
16029 let eps = self.rms_eps;
16030 let pool = self.pool.clone();
16031 mla_attention(
16032 w,
16033 &self.ws.n1,
16034 &mut self.kv_cache.layers[li],
16035 position,
16036 &inv_freq_l,
16037 rs,
16038 eps,
16039 pool.as_deref(),
16040 )
16041 }
16042 AttnKind::Linear(w) => {
16043 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16044 vmf_phase_forward(
16045 &self.ws.n1,
16046 w,
16047 &cfg,
16048 &mut self.kv_cache.layers[li].linear_state,
16049 self.pool.as_deref(),
16050 )
16051 }
16052 AttnKind::Kda(w) => {
16053 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16054 crate::linear_core::kda_forward(
16055 &self.ws.n1,
16056 w,
16057 &cfg,
16058 &mut self.kv_cache.layers[li].linear_state,
16059 self.pool.as_deref(),
16060 )
16061 }
16062 AttnKind::LinearGdn(w) => {
16063 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16064 gdn_forward(
16065 &self.ws.n1,
16066 w,
16067 &cfg,
16068 &mut self.kv_cache.layers[li].linear_state,
16069 self.pool.as_deref(),
16070 )
16071 }
16072 AttnKind::ShortConv(w) => {
16073 let cfg = self
16074 .short_conv_cfg
16075 .expect("short-conv layer without short_conv_cfg");
16076 short_conv_forward(
16077 &self.ws.n1,
16078 w,
16079 &cfg,
16080 &mut self.kv_cache.layers[li].linear_state,
16081 self.pool.as_deref(),
16082 )
16083 }
16084 AttnKind::Bounded(w) => {
16085 let rope = self
16088 .bounded_rope
16089 .clone()
16090 .expect("bounded layer without an installed rotation table");
16091 let cfg = crate::bounded::BoundedAttnCfg {
16092 num_heads: self.num_heads,
16093 num_kv_heads: self.num_kv_heads,
16094 head_dim: self.head_dim,
16095 hidden_size: hs,
16096 scale: self.attn_scale,
16097 rope: &rope,
16098 pool: pool.as_deref(),
16099 };
16100 crate::bounded::bounded_attention(
16101 &self.ws.n1,
16102 w,
16103 &mut self.kv_cache.layers[li],
16104 &cfg,
16105 )
16106 }
16107 AttnKind::Full {
16108 wq,
16109 wk,
16110 wv,
16111 wo,
16112 q_norm,
16113 k_norm,
16114 output_gate,
16115 softplus_gate,
16116 bias,
16117 } if self.kv_cache.layers[li].o1_sealed() => {
16118 let inv_freq_l = self.layer_inv_freq(li);
16121 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16122 let cfg = QwenAttnCfg {
16123 num_heads: self.layer_num_heads(li),
16124 num_kv_heads: nkv_l,
16125 head_dim: hd_l,
16126 hidden_size: hs,
16127 position,
16128 inv_freq: &inv_freq_l,
16129 rotary_dim: rd_l,
16130 scale: self.attn_scale,
16131 softcap: self.attn_softcap,
16132 window: None,
16133 v_norm: self.attn_v_norm,
16134 qk_norm_after_rope: self.qk_norm_after_rope,
16135 gate_sigmoid: self.proj_gate_sigmoid,
16136 q_norm: q_norm.as_deref(),
16137 k_norm: k_norm.as_deref(),
16138 output_gate: *output_gate,
16139 softplus_gate: softplus_gate
16140 .as_ref()
16141 .map(|(gate, per_head)| (gate, *per_head)),
16142 rope_scale: self.layer_rope_scale(li),
16143 bias: bias
16144 .as_ref()
16145 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16146 rms_eps: eps,
16147 norm_style: self.norm_style,
16148 pool: pool.as_deref(),
16149 v_head_dim: self.layer_v_dim(li),
16150 };
16151 attention::qwen_attention_nystrom(
16152 &self.ws.n1,
16153 wq,
16154 wk,
16155 wv,
16156 wo,
16157 &mut self.kv_cache.layers[li],
16158 &cfg,
16159 )
16160 }
16161 AttnKind::Full {
16162 wq,
16163 wk,
16164 wv,
16165 wo,
16166 q_norm,
16167 k_norm,
16168 output_gate,
16169 softplus_gate,
16170 bias,
16171 } => 'attn: {
16172 let dropin_reason =
16177 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16178 if let Some(reason) = dropin_reason {
16179 self.note_graph_decline("wgpu attn dropin", reason);
16180 }
16181 if graph_on
16182 && dropin_reason.is_none()
16183 && !*output_gate
16184 && softplus_gate.is_none()
16185 && self.attention_heads_per_layer.is_none()
16186 && bias.is_none()
16187 && task_mask.is_none()
16188 {
16189 let inv_freq_l = self.layer_inv_freq(li);
16190 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16191 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16192 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16193 wq.mapped_q1(),
16194 wk.mapped_q1(),
16195 wv.mapped_q1(),
16196 wo.mapped_q1(),
16197 ) {
16198 let gm = gm.clone();
16199 let mut out = vec![0f32; hs];
16200 let cache = &self.kv_cache.layers[li];
16201 if crate::gpu::attn_dropin(
16202 &gm,
16203 self.graph_kv_id,
16204 li,
16205 &self.ws.n1,
16206 qi,
16207 ki,
16208 vi,
16209 oi,
16210 q_norm.as_deref(),
16211 k_norm.as_deref(),
16212 self.qk_norm_after_rope,
16213 &inv_freq_l,
16214 nh,
16215 nkv_l,
16216 hd_l,
16217 rd_l,
16218 hs,
16219 position,
16220 self.kv_cache.max_seq_len,
16221 gemma,
16222 eps as f32,
16223 cache.k_heads(),
16224 cache.v_heads(),
16225 &mut out,
16226 ) {
16227 break 'attn out;
16228 }
16229 }
16230 }
16231 let masked = task_mask
16232 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16233 .unwrap_or(false);
16234 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16235 let plain = self.layer_attn_plain(li);
16238 match (masked, f32_view) {
16239 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16242 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16243 attention::multi_head_attention(
16244 &self.ws.n1,
16245 q,
16246 k,
16247 v,
16248 o,
16249 &mut self.kv_cache.layers[li],
16250 self.num_heads,
16251 self.num_kv_heads,
16252 self.head_dim,
16253 self.hidden_size,
16254 position,
16255 &active_heads,
16256 &self.inv_freq,
16257 )
16258 }
16259 (masked, _) => {
16260 if masked {
16261 tracing::warn!(
16262 "layer {li}: head mask on quantized weights or on a \
16263 window/sink/per-layer-geometry layer not supported \
16264 yet — executing dense"
16265 );
16266 }
16267 let inv_freq_l = self.layer_inv_freq(li);
16268 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16269 let cfg = QwenAttnCfg {
16270 num_heads: self.layer_num_heads(li),
16271 num_kv_heads: nkv_l,
16272 head_dim: hd_l,
16273 hidden_size: hs,
16274 position,
16275 inv_freq: &inv_freq_l,
16276 rotary_dim: rd_l,
16277 scale: self.attn_scale,
16278 softcap: self.attn_softcap,
16279 window: self.layer_window(li),
16280 v_norm: self.attn_v_norm,
16281 qk_norm_after_rope: self.qk_norm_after_rope,
16282 gate_sigmoid: self.proj_gate_sigmoid,
16283 q_norm: q_norm.as_deref(),
16284 k_norm: k_norm.as_deref(),
16285 output_gate: *output_gate,
16286 softplus_gate: softplus_gate
16287 .as_ref()
16288 .map(|(gate, per_head)| (gate, *per_head)),
16289 rope_scale: self.layer_rope_scale(li),
16290 bias: bias
16291 .as_ref()
16292 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16293 rms_eps: eps,
16294 norm_style: self.norm_style,
16295 pool: pool.as_deref(),
16296 v_head_dim: self.layer_v_dim(li),
16297 };
16298 attention::qwen_attention(
16299 &self.ws.n1,
16300 wq,
16301 wk,
16302 wv,
16303 wo,
16304 &mut self.kv_cache.layers[li],
16305 &cfg,
16306 )
16307 }
16308 }
16309 }
16310 };
16311 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
16314 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
16315 None => attn_out,
16316 };
16317 let lw = &self.weights.layers[self.phys_layer(li)];
16318 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16319 inference::add_rmsnorm_fused_into(
16320 &mut h,
16321 &attn_out,
16322 &lw.post_norm,
16323 self.rms_eps,
16324 self.norm_style,
16325 &mut self.ws.p1,
16326 );
16327 drop(prof);
16328 let mut attn_out = attn_out;
16329 attention::recycle_buf(&mut attn_out);
16330 let post_normed = &self.ws.p1;
16331
16332 let ffn_masked = task_mask
16333 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16334 .unwrap_or(false);
16335 let ffn_out = match (ffn_masked, &lw.ffn) {
16347 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16351 let row = task_mask
16352 .and_then(|tm| tm.ffn_masks.get(li))
16353 .map(|v| v.as_slice());
16354 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16355 }
16356 (true, FfnKind::Dense(d)) => {
16357 let tm = task_mask.unwrap();
16358 let alive = tm.ffn_active_count(li);
16359 let deep = alive * 2 <= self.intermediate_size;
16360 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16361 let active = tm.ffn_active_indices(li);
16362 sparse_ffn_quant(
16363 d,
16364 post_normed,
16365 &active,
16366 self.hidden_size,
16367 self.pool.as_deref(),
16368 )
16369 } else if deep
16370 && let (Some(g), Some(u), Some(dn)) = (
16371 d.gate_proj.as_f32(),
16372 d.up_proj.as_f32(),
16373 d.down_proj.as_f32(),
16374 )
16375 {
16376 let active = tm.ffn_active_indices(li);
16377 inference::sparse_ffn_forward(
16378 post_normed,
16379 g,
16380 u,
16381 dn,
16382 self.hidden_size,
16383 self.intermediate_size,
16384 &active,
16385 self.pool.as_deref(),
16386 )
16387 } else {
16388 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16389 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16390 }
16391 }
16392 (true, FfnKind::Moe(m)) => {
16393 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16397 ffn_forward(
16398 &lw.ffn,
16399 post_normed,
16400 self.pool.as_deref(),
16401 allowed.as_deref(),
16402 )
16403 }
16404 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16405 dm,
16406 post_normed,
16407 &h,
16408 self.rms_eps,
16409 self.norm_style,
16410 self.pool.as_deref(),
16411 ),
16412 (false, _) => match &lw.ffn {
16413 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16414 dm,
16415 post_normed,
16416 &h,
16417 self.rms_eps,
16418 self.norm_style,
16419 self.pool.as_deref(),
16420 ),
16421 FfnKind::Moe(m)
16422 if task_mask.is_none()
16423 && host_tail
16424 && li >= tail_start
16425 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
16426 {
16427 moe_ffn_minicpm_tail(
16428 &mut self.minicpm_tail,
16429 li,
16430 self.num_layers,
16431 m,
16432 post_normed,
16433 self.pool.as_deref(),
16434 )
16435 }
16436 FfnKind::Moe(m)
16437 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16438 {
16439 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16440 }
16441 _ => {
16442 let allowed = match (&lw.ffn, task_mask) {
16443 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16444 _ => None,
16445 };
16446 ffn_forward(
16447 &lw.ffn,
16448 post_normed,
16449 self.pool.as_deref(),
16450 allowed.as_deref(),
16451 )
16452 }
16453 },
16454 };
16455 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16456 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16457 None => ffn_out,
16458 };
16459 for (i, &f) in ffn_out.iter().enumerate() {
16460 h[i] += f;
16461 }
16462 let mut ffn_out = ffn_out;
16463 attention::recycle_buf(&mut ffn_out);
16464
16465 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16467 for v in h.iter_mut() {
16468 *v *= sc;
16469 }
16470 }
16471 if self.layer_dump.is_some() {
16473 self.dump_layer_row(position, li, &h);
16474 }
16475
16476 if self.is_loop_end(li) && li + 1 < self.num_layers {
16479 h = inference::rms_norm(
16480 &h,
16481 &self.weights.final_norm,
16482 self.rms_eps,
16483 self.norm_style,
16484 );
16485 }
16486
16487 if self.dyn_phi_layer == Some(li) {
16491 self.update_dyn_phi(&h);
16492 }
16493 }
16494 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16496 crate::gpu::graph_race_record(false, t.elapsed());
16497 }
16498
16499 h
16500 }
16501
16502 fn update_dyn_phi(&mut self, h: &[f32]) {
16505 const A: f32 = 0.2;
16506 if self.dyn_phi_ema.len() != h.len() {
16507 self.dyn_phi_ema = vec![0.0; h.len()];
16508 self.dyn_phi_seen = 0;
16509 }
16510 if self.dyn_phi_seen == 0 {
16511 self.dyn_phi_ema.copy_from_slice(h);
16512 } else {
16513 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16514 *e = (1.0 - A) * *e + A * v;
16515 }
16516 }
16517 self.dyn_phi_seen += 1;
16518 }
16519
16520 pub fn dyn_phi(&self) -> &[f32] {
16522 &self.dyn_phi_ema
16523 }
16524
16525 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16527 self.dyn_phi_layer = layer;
16528 self.dyn_phi_ema.clear();
16529 self.dyn_phi_seen = 0;
16530 }
16531
16532 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16534 let Some(model) = &self.model else {
16535 return Vec::new();
16536 };
16537 model
16538 .header
16539 .skills
16540 .iter()
16541 .enumerate()
16542 .filter_map(|(i, sk)| {
16543 if sk.is_v2() {
16547 return None;
16548 }
16549 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16550 let sel = sk.selection.as_ref()?;
16551 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16552 })
16553 .collect()
16554 }
16555
16556 pub fn active_skill(&self) -> Option<usize> {
16558 self.dyn_active
16559 }
16560
16561 pub fn enable_dynamic_routing(&mut self) -> usize {
16566 use crate::swarm::{DynRouter, RoutableSkill};
16567 let Some(model) = self.model.clone() else {
16568 return 0;
16569 };
16570 if let Some(r) = &model.header.router {
16577 tracing::warn!(
16578 "dynamic routing disabled: this file declares router policy '{}' with \
16579 granularity \"{}\" — the request-level decision applies instead",
16580 r.policy,
16581 r.granularity
16582 );
16583 return 0;
16584 }
16585 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16591 || model.header.skills.iter().any(|s| s.is_v2())
16592 {
16593 tracing::warn!(
16594 "dynamic routing disabled: this file carries format-v2 skill records \
16595 (SKILLS_V2) — they route per request through a router policy only"
16596 );
16597 return 0;
16598 }
16599 if self.dyn_blend_loaded {
16602 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16603 return 0;
16604 }
16605 if let Some(a) = self.dyn_active {
16609 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16610 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16611 return 0;
16612 }
16613 }
16614 let hidden = self.hidden_size;
16615 let mut skills = Vec::new();
16616 for (idx, id, _phi) in self.dynamic_skills() {
16617 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16618 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16619 skills.push(rs);
16620 }
16621 }
16622 }
16623 if skills.is_empty() {
16624 return 0;
16625 }
16626 let phi = skills[0].phi_layer;
16628 if skills.iter().any(|s| s.phi_layer != phi) {
16629 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16630 }
16631 let n = skills.len();
16632 self.set_dyn_phi_layer(Some(phi));
16633 self.dyn_router = Some(DynRouter::new(skills));
16634 n
16635 }
16636
16637 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16639 self.dyn_router
16640 .as_ref()
16641 .map(|r| r.switches.clone())
16642 .unwrap_or_default()
16643 }
16644
16645 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16648 let _mimo_q8 = self.mimo_moe.is_on()
16649 .then(crate::qtensor::enter_full_gpu_q8_scope);
16650 let rows = self.weights.lm_head.rows();
16651 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16652 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
16656 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
16657 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
16658 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
16659 rows, self.hidden_size, &mut logits)
16660 });
16661 if !served {
16662 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
16663 }
16664 logits.resize(self.vocab_size, 0.0);
16665 if let Some(m) = self.logit_multiplier {
16666 for l in logits.iter_mut() {
16667 *l *= m;
16668 }
16669 }
16670 if let Some(c) = self.final_softcap {
16671 softcap_in_place(&mut logits, c, self.pool.as_deref());
16672 }
16673 if let Some(cm) = self.head_clusters.as_ref() {
16674 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16675 }
16676 logits
16677 }
16678
16679 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16682 let h = hidden.len();
16683 let ncl = cm.len() / h.max(1);
16684 if ncl == 0 || logits.len() % ncl != 0 {
16685 return;
16686 }
16687 let cs = logits.len() / ncl;
16688 let mut lc = vec![0.0f32; ncl];
16690 for c in 0..ncl {
16691 let row = &cm[c * h..(c + 1) * h];
16692 let mut s = 0.0f32;
16693 for j in 0..h {
16694 s += row[j] * hidden[j];
16695 }
16696 lc[c] = s;
16697 }
16698 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16699 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16700 for c in 0..ncl {
16701 let blk = &mut logits[c * cs..(c + 1) * cs];
16702 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16703 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16704 let add = lc[c] - lse - bl;
16705 for v in blk.iter_mut() {
16706 *v += add;
16707 }
16708 }
16709 }
16710
16711 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16716 #[cfg(target_os = "macos")]
16717 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16718 self.clear_sequence_state();
16719 crate::gpu::graph_race_begin_generation();
16723 if task_mask.is_none() {
16724 self.o1_begin();
16725 }
16726 let mut hidden = vec![0.0f32; self.hidden_size];
16727 for (pos, &id) in ids.iter().enumerate() {
16728 let emb = self.embed_single(id);
16729 hidden = self.forward_layers(&emb, pos, task_mask);
16730 }
16731 if let Err(err) = self.o1_seal_checked() {
16732 self.o1_fail(err);
16733 }
16734 if let Some(logits) = self.graph_logits.take() {
16737 return logits;
16738 }
16739 inference::rms_norm_into(
16740 &hidden,
16741 &self.weights.final_norm,
16742 self.rms_eps,
16743 self.norm_style,
16744 &mut self.ws.n1,
16745 );
16746 self.lm_head_forward(&self.ws.n1)
16747 }
16748}
16749
16750pub fn create_test_pipeline(
16752 hidden_size: usize,
16753 intermediate_size: usize,
16754 num_heads: usize,
16755 num_kv_heads: usize,
16756 head_dim: usize,
16757 num_layers: usize,
16758 vocab_size: usize,
16759) -> Pipeline {
16760 let synth = |n: usize, salt: usize| -> Vec<f32> {
16763 (0..n)
16764 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16765 .collect()
16766 };
16767 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16768 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16769 };
16770 let layer_weights: Vec<LayerWeights> = (0..num_layers)
16771 .map(|li| LayerWeights {
16772 input_norm: vec![1.0; hidden_size],
16773 post_norm: vec![1.0; hidden_size],
16774 attn_out_norm: None,
16775 ffn_out_norm: None,
16776 layer_scale: None,
16777 ffn: FfnKind::Dense(DenseFfn {
16778 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
16779 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
16780 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
16781 act: Act::Silu,
16782 down_t: None,
16783 segs: Vec::new(),
16784 }),
16785 attn: AttnKind::Full {
16786 bias: None,
16787 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
16788 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
16789 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
16790 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
16791 q_norm: None,
16792 k_norm: None,
16793 output_gate: false,
16794 softplus_gate: None,
16795 },
16796 })
16797 .collect();
16798
16799 Pipeline::new(
16800 Tokenizer::byte_level(),
16801 PipelineWeights {
16802 embed_tokens: qt(vocab_size, hidden_size, 100),
16803 layers: layer_weights,
16804 lm_head: qt(vocab_size, hidden_size, 200),
16805 final_norm: vec![1.0; hidden_size],
16806 },
16807 hidden_size,
16808 intermediate_size,
16809 num_heads,
16810 num_kv_heads,
16811 head_dim,
16812 num_layers,
16813 num_layers, false, vocab_size,
16816 1e-6,
16817 10_000.0,
16818 NormStyle::Qwen,
16819 4096,
16820 SamplerConfig {
16821 seed: Some(42),
16822 ..Default::default()
16823 },
16824 )
16825}
16826
16827#[inline]
16832fn mask_bit(row: &[u8], j: usize) -> bool {
16833 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
16834}
16835
16836fn mask_gain() -> f32 {
16847 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16848 *G.get_or_init(|| {
16849 std::env::var("CMF_FFN_MASK_GAIN")
16850 .ok()
16851 .and_then(|v| v.parse().ok())
16852 .unwrap_or(1.0)
16853 })
16854}
16855
16856fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
16857 let fill = meanfill().and_then(|(i, v)| {
16860 let li = crate::gpu::cur_layer();
16861 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
16862 });
16863 for r in 0..rows {
16864 let base = r * inter;
16865 for (bi, &byte) in row.iter().enumerate() {
16866 if byte == 0xFF {
16867 continue;
16868 }
16869 let j0 = bi * 8;
16870 for bit in 0..8 {
16871 let j = j0 + bit;
16872 if j < inter && byte & (1 << bit) == 0 {
16873 g[base + j] = fill.map_or(0.0, |f| f[j]);
16874 }
16875 }
16876 }
16877 }
16878 let gain = mask_gain();
16879 if gain != 1.0 {
16880 for v in g[..rows * inter].iter_mut() {
16881 *v *= gain;
16882 }
16883 }
16884}
16885
16886#[inline]
16888fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
16889 row.is_none_or(|r| mask_bit(r, i))
16890}
16891
16892fn all_bits_on(row: &[u8], n: usize) -> bool {
16895 (0..n).all(|i| mask_bit(row, i))
16896}
16897
16898fn tube_topk() -> usize {
16906 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16907 *K.get_or_init(|| {
16908 std::env::var("CMF_TUBE_TOPK")
16909 .ok()
16910 .and_then(|v| v.parse().ok())
16911 .unwrap_or(0)
16912 })
16913}
16914
16915fn tube_score_oracle() -> bool {
16916 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16917 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16918}
16919
16920fn tube_ffn_routed(
16927 d: &DenseFfn,
16928 xs: &[f32],
16929 b: usize,
16930 pool: Option<&Pool>,
16931 mask_row: Option<&[u8]>,
16932 k: usize,
16933) -> Vec<f32> {
16934 let hidden = d.down_proj.rows();
16935 let core = d.gate_proj.rows();
16936 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16937 let mut out = match (b, core_full, mask_row) {
16938 (1, true, _) => dense_ffn(d, xs, pool),
16939 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16940 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16941 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16942 };
16943 let cand: Vec<usize> = (0..d.segs.len())
16944 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16945 .collect();
16946 if cand.is_empty() {
16947 return out;
16948 }
16949 let oracle = tube_score_oracle();
16953 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16954 let mut scores = vec![0f32; b * cand.len()];
16955 for (ci, &i) in cand.iter().enumerate() {
16956 let seg = &d.segs[i];
16957 let w = seg.width;
16958 let mut g = vec![0.0f32; b * w];
16959 if b == 1 {
16960 seg.gate.matvec(xs, &mut g, pool);
16961 } else {
16962 seg.gate.matmat(xs, b, &mut g, pool);
16963 }
16964 for v in g.iter_mut() {
16965 *v = Act::Silu.combine(*v, 1.0);
16966 }
16967 if !oracle {
16968 for t in 0..b {
16969 scores[t * cand.len() + ci] =
16970 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16971 }
16972 }
16973 if oracle || b > 1 {
16974 let mut u = vec![0.0f32; b * w];
16975 if b == 1 {
16976 seg.up.matvec(xs, &mut u, pool);
16977 } else {
16978 seg.up.matmat(xs, b, &mut u, pool);
16979 }
16980 for (a, &v) in g.iter_mut().zip(u.iter()) {
16981 *a *= v;
16982 }
16983 if oracle {
16984 for t in 0..b {
16985 scores[t * cand.len() + ci] =
16986 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16987 }
16988 }
16989 }
16990 acts.push(g);
16991 }
16992 let keep = k.min(cand.len());
16994 let mut scratch: Vec<f32> = Vec::new();
16995 for t in 0..b {
16996 let mut sc: Vec<(f32, usize)> = (0..cand.len())
16997 .map(|ci| (scores[t * cand.len() + ci], ci))
16998 .collect();
16999 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
17000 let mut alive = vec![false; cand.len()];
17001 for &(_, ci) in sc.iter().take(keep) {
17002 alive[ci] = true;
17003 }
17004 if b > 1 {
17005 for (ci, a) in acts.iter_mut().enumerate() {
17006 if !alive[ci] {
17007 let w = d.segs[cand[ci]].width;
17008 a[t * w..(t + 1) * w].fill(0.0);
17009 }
17010 }
17011 } else {
17012 for (ci, &i) in cand.iter().enumerate() {
17016 if !alive[ci] {
17017 continue;
17018 }
17019 let seg = &d.segs[i];
17020 let w = seg.width;
17021 let g = &mut acts[ci];
17022 if !tube_score_oracle() {
17023 scratch.clear();
17024 scratch.resize(w, 0.0);
17025 seg.up.matvec(xs, &mut scratch, pool);
17026 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17027 *a *= v;
17028 }
17029 }
17030 let mut acc = vec![0.0f32; hidden];
17031 seg.down.matvec(g, &mut acc, pool);
17032 for (o, a) in out.iter_mut().zip(&acc) {
17033 *o += *a;
17034 }
17035 }
17036 }
17037 }
17038 if b > 1 {
17039 for (ci, &i) in cand.iter().enumerate() {
17040 let seg = &d.segs[i];
17041 let mut acc = vec![0.0f32; b * hidden];
17042 seg.down.matmat(&acts[ci], b, &mut acc, pool);
17043 for (o, a) in out.iter_mut().zip(&acc) {
17044 *o += *a;
17045 }
17046 }
17047 }
17048 out
17049}
17050
17051fn tube_ffn(
17057 d: &DenseFfn,
17058 xs: &[f32],
17059 b: usize,
17060 pool: Option<&Pool>,
17061 mask_row: Option<&[u8]>,
17062) -> Vec<f32> {
17063 if tube_topk() > 0 {
17064 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17065 }
17066 let hidden = d.down_proj.rows();
17067 let core = d.gate_proj.rows();
17068 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17069 let mut out = match (b, core_full, mask_row) {
17070 (1, true, _) => dense_ffn(d, xs, pool),
17071 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17072 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17073 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17074 };
17075 TUBE_SCRATCH.with(|sc| {
17076 let mut sc = sc.borrow_mut();
17077 let [g, u, acc] = &mut *sc;
17078 for seg in &d.segs {
17079 if !tube_bit(mask_row, seg.start) {
17080 continue;
17081 }
17082 let w = seg.width;
17083 g.resize(b * w, 0.0);
17084 if b == 1
17085 && d.act == Act::Silu
17086 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17087 {
17088 } else {
17090 u.resize(b * w, 0.0);
17091 if b == 1 {
17092 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17093 } else {
17094 seg.gate.matmat(xs, b, g, pool);
17095 seg.up.matmat(xs, b, u, pool);
17096 }
17097 for i in 0..b * w {
17098 g[i] = d.act.combine(g[i], u[i]);
17099 }
17100 }
17101 acc.resize(b * hidden, 0.0);
17102 acc.fill(0.0);
17103 if b == 1 {
17104 seg.down.matvec(g, acc, pool);
17105 } else {
17106 seg.down.matmat(g, b, acc, pool);
17107 }
17108 for (o, a) in out.iter_mut().zip(acc.iter()) {
17109 *o += *a;
17110 }
17111 }
17112 out
17113 })
17114}
17115
17116thread_local! {
17117 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17121 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17122}
17123
17124static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
17127 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
17128
17129fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17133 #[cfg(feature = "gpu")]
17134 {
17135 use std::sync::atomic::Ordering::Relaxed;
17136 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17137 (
17138 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17139 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17140 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17141 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17142 )
17143 }
17144 #[cfg(not(feature = "gpu"))]
17145 (0.0, 0.0, 0.0, 0)
17146}
17147
17148fn wgpu_mirror_counters() -> [u64; 6] {
17151 #[cfg(feature = "gpu")]
17152 {
17153 crate::gpu_wgpu::MIRROR_EVENTS
17154 .each_ref()
17155 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17156 }
17157 #[cfg(not(feature = "gpu"))]
17158 [0; 6]
17159}
17160
17161fn prefill_prof_on() -> bool {
17162 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17163 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17164}
17165
17166fn dense_ffn_batch(
17167 d: &DenseFfn,
17168 xs: &[f32],
17169 b: usize,
17170 pool: Option<&Pool>,
17171 mask_row: Option<&[u8]>,
17172) -> Vec<f32> {
17173 let inter = d.gate_proj.rows();
17174 let hidden = d.down_proj.rows();
17175 let fused_act = d.act.graph_act();
17185 if mask_row.is_none()
17186 && fused_act.is_some()
17187 && b >= 32
17188 && crate::gpu::enabled_here()
17189 && !crate::gpu::mm_killed()
17190 && refit_dir().is_none()
17195 && !ffn_probe_active()
17200 && !crate::gptq_capture::capturing()
17204 {
17205 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17206 d.gate_proj.mapped_q4t(),
17207 d.up_proj.mapped_q4t(),
17208 d.down_proj.mapped_q4t(),
17209 ) {
17210 let mut out = vec![0.0f32; b * hidden];
17211 let act = fused_act.expect("checked above");
17212 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
17213 {
17214 return out;
17215 }
17216 }
17217 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17222 d.gate_proj.mapped_q4tp(),
17223 d.up_proj.mapped_q4tp(),
17224 d.down_proj.mapped_q4tp(),
17225 ) {
17226 let mut out = vec![0.0f32; b * hidden];
17227 let act = fused_act.expect("checked above");
17228 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17229 {
17230 return out;
17231 }
17232 }
17233 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17239 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17240 d.gate_proj.mapped_device_gemm(),
17241 d.up_proj.mapped_device_gemm(),
17242 d.down_proj.mapped_device_gemm(),
17243 ) {
17244 let mut out = vec![0.0f32; b * hidden];
17245 let act = fused_act.expect("checked above");
17246 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17247 return out;
17248 }
17249 }
17250 }
17251 let mut g = vec![0.0f32; b * inter];
17252 d.gate_proj.matmat(xs, b, &mut g, pool);
17253 let mut u = vec![0.0f32; b * inter];
17254 d.up_proj.matmat(xs, b, &mut u, pool);
17255 if gate_topk() > 0 && d.act == Act::Silu {
17256 for t in 0..b {
17257 let row = &mut g[t * inter..(t + 1) * inter];
17258 for v in row.iter_mut() {
17259 *v = Act::Silu.combine(*v, 1.0);
17260 }
17261 keep_top_k(row, gate_topk());
17262 }
17263 for i in 0..b * inter {
17264 g[i] *= u[i];
17265 }
17266 } else {
17267 for i in 0..b * inter {
17268 g[i] = d.act.combine(g[i], u[i]);
17269 }
17270 }
17271 if let Some(row) = mask_row {
17272 zero_masked_cols(&mut g, b, inter, row);
17273 }
17274 if oracle_topk() > 0 {
17275 for t in 0..b {
17276 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
17277 }
17278 }
17279 let mut out = vec![0.0f32; b * hidden];
17280 d.down_proj.matmat(&g, b, &mut out, pool);
17281 if refit_dir().is_some() {
17282 let li = crate::gpu::cur_layer();
17283 if li >= 0 {
17284 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
17285 }
17286 }
17287 FFN_PROBE.with(|pr| {
17291 if let Some(acc) = pr.borrow_mut().as_mut() {
17292 let li = crate::gpu::cur_layer();
17293 if li < 0 {
17294 return;
17295 }
17296 let Some(row) = acc.get_mut(li as usize) else {
17297 return;
17298 };
17299 let sq = probe_sq();
17300 for t in 0..b {
17301 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
17302 *a += if sq {
17303 (v as f64) * (v as f64)
17304 } else {
17305 (v as f64).abs()
17306 };
17307 }
17308 }
17309 }
17310 });
17311 out
17312}
17313
17314fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
17319 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17320 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17321 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
17322 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
17323 if (!on && !dump) || b == 0 {
17324 return;
17325 }
17326 let hidden = xs.len() / b;
17327 if on {
17328 let mut acc = m.act_sq.borrow_mut();
17329 if acc.len() < hidden {
17330 acc.resize(hidden, 0.0);
17331 }
17332 for t in 0..b {
17333 let row = &xs[t * hidden..(t + 1) * hidden];
17334 for (a, &v) in acc.iter_mut().zip(row) {
17335 *a += (v as f64) * (v as f64);
17336 }
17337 }
17338 }
17339 if dump {
17340 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
17343 .ok()
17344 .and_then(|v| v.parse().ok())
17345 .unwrap_or(4096);
17346 let mut rows = m.act_rows.borrow_mut();
17347 if rows.len() < cap * hidden {
17348 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17349 rows.extend_from_slice(&xs[..take * hidden]);
17350 }
17351 }
17352}
17353
17354#[derive(Clone, Copy)]
17357struct SendVecs(*mut Vec<f32>);
17358unsafe impl Send for SendVecs {}
17359unsafe impl Sync for SendVecs {}
17360impl SendVecs {
17361 #[inline]
17362 fn at(self, i: usize) -> *mut Vec<f32> {
17363 unsafe { self.0.add(i) }
17364 }
17365}
17366
17367fn moe_ffn_batch(
17368 m: &MoeFfn,
17369 xs: &[f32],
17370 b: usize,
17371 hidden: usize,
17372 pool: Option<&Pool>,
17373 allowed: Option<&[bool]>,
17374) -> Vec<f32> {
17375 accumulate_act(m, xs, b);
17376 let ne = m.experts.len();
17377 let mut logits = vec![0.0f32; b * ne];
17378 match &m.resonance {
17379 Some(r) => {
17380 let hdim = xs.len() / b.max(1);
17381 for bi in 0..b {
17382 r.scores(
17383 &xs[bi * hdim..(bi + 1) * hdim],
17384 &mut logits[bi * ne..(bi + 1) * ne],
17385 );
17386 }
17387 }
17388 None => m.router.matmat(xs, b, &mut logits, pool),
17389 }
17390
17391 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17394 {
17395 let mut st = m.stats.borrow_mut();
17396 if st.len() < ne {
17397 st.resize(ne, 0);
17398 }
17399 for bi in 0..b {
17400 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17401 for &e in &idx {
17402 st[e] += 1;
17403 assign[e].push((bi, p[e] / wsum));
17404 }
17405 }
17406 }
17407
17408 let mut out = vec![0.0f32; b * hidden];
17409 let cols = m.experts[0].gate_proj.cols();
17410 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17411 let sb = list.len();
17412 let mut sub = vec![0.0f32; sb * cols];
17413 for (k, &(bi, _)) in list.iter().enumerate() {
17414 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17415 }
17416 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17417 for (k, &(bi, w)) in list.iter().enumerate() {
17418 for i in 0..hidden {
17419 out[bi * hidden + i] += w * eo[k * hidden + i];
17420 }
17421 }
17422 };
17423 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17429 if pool.is_some() && active.len() >= 8 {
17430 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17431 {
17432 let panel_ptr = SendVecs(panels.as_mut_ptr());
17433 let experts = &m.experts;
17436 let (active_r, assign_r) = (&active, &assign);
17437 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17438 let run = |start: usize, end: usize| {
17439 let _cpu_scope = inherit_cpu();
17440 #[cfg(target_os = "macos")]
17448 let _metal_cpu = crate::gpu::enter_cpu_scope();
17449 for ai in start..end {
17450 let e = active_r[ai];
17451 let list = &assign_r[e];
17452 let sb = list.len();
17453 let mut sub = vec![0.0f32; sb * cols];
17454 for (k, &(bi, _)) in list.iter().enumerate() {
17455 sub[k * cols..(k + 1) * cols]
17456 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17457 }
17458 unsafe {
17460 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17461 }
17462 }
17463 };
17464 match pool {
17465 Some(p) => p.run_rows(active.len(), &run),
17466 None => run(0, active.len()),
17467 }
17468 }
17469 for (ai, &e) in active.iter().enumerate() {
17470 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17471 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17472 for i in 0..hidden {
17473 out[bi * hidden + i] += w * eo[i];
17474 }
17475 }
17476 }
17477 } else {
17478 for &e in &active {
17479 run_expert(&m.experts[e], &assign[e], &mut out);
17480 }
17481 }
17482 if let Some((se, gate)) = &m.shared {
17483 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17484 let mut gl = vec![0.0f32; b];
17485 gate.matmat(xs, b, &mut gl, pool);
17486 (0..b)
17487 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17488 .collect()
17489 } else {
17490 (0..b).map(|bi| (bi, 1.0)).collect()
17491 };
17492 run_expert(se, &all, &mut out);
17493 }
17494 out
17495}
17496
17497fn moe_ffn_rows_exact(
17509 m: &MoeFfn,
17510 xs: &[f32],
17511 b: usize,
17512 hidden: usize,
17513 pool: Option<&Pool>,
17514) -> Vec<f32> {
17515 let mut out = vec![0.0f32; b * hidden];
17516 let per_row = |out: &mut [f32]| {
17517 for r in 0..b {
17518 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17519 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17520 }
17521 };
17522 let covered = !crate::gpu::enabled_here()
17523 && moe_batch_enabled()
17524 && m.shared.is_none()
17525 && m.resonance.is_none()
17526 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17527 && m.experts.iter().all(|d| d.act == Act::Silu);
17528 if !covered {
17529 per_row(&mut out);
17530 return out;
17531 }
17532 let ne = m.experts.len();
17533 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17535 for r in 0..b {
17536 let x = &xs[r * hidden..(r + 1) * hidden];
17537 accumulate_act(m, x, 1);
17538 let mut logits = vec![0.0f32; ne];
17539 m.router.matvec(x, &mut logits, pool);
17540 let (idx, p, wsum) = moe_route(&logits, m, None);
17541 {
17542 let mut st = m.stats.borrow_mut();
17543 if st.len() < ne {
17544 st.resize(ne, 0);
17545 }
17546 for &e in &idx {
17547 st[e] += 1;
17548 }
17549 }
17550 let w: Vec<f32> = idx
17551 .iter()
17552 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17553 .collect();
17554 routes.push((idx, w));
17555 }
17556 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17557 per_row(&mut out);
17558 return out;
17559 }
17560 let mut experts: Vec<usize> = Vec::new();
17562 let mut groups: Vec<Vec<usize>> = Vec::new();
17563 for (r, (idx, _)) in routes.iter().enumerate() {
17564 for &e in idx {
17565 match experts.iter().position(|&x| x == e) {
17566 Some(g) => groups[g].push(r),
17567 None => {
17568 experts.push(e);
17569 groups.push(vec![r]);
17570 }
17571 }
17572 }
17573 }
17574 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17575 let inter = m.experts[experts[0]].gate_proj.rows();
17576 let pairs: Vec<(&QTensor, &QTensor)> = experts
17577 .iter()
17578 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17579 .collect();
17580 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17581 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17582 per_row(&mut out);
17583 return out;
17584 }
17585 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17586 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17587 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17588 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17589 per_row(&mut out);
17590 return out;
17591 }
17592 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17594 let mut p = 0usize;
17595 for (g, &e) in experts.iter().enumerate() {
17596 for &r in &groups[g] {
17597 slot.insert((r, e), p);
17598 p += 1;
17599 }
17600 }
17601 for (r, (idx, w)) in routes.iter().enumerate() {
17602 let terms: Vec<(&[f32], f32)> = idx
17603 .iter()
17604 .zip(w)
17605 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17606 .collect();
17607 let row = &mut out[r * hidden..(r + 1) * hidden];
17608 for (i, dst) in row.iter_mut().enumerate() {
17609 let mut acc = 0f32;
17611 for (d, we) in &terms {
17612 acc += we * d[i];
17613 }
17614 *dst = acc;
17615 }
17616 }
17617 out
17618}
17619
17620pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17624 match pool {
17625 Some(p) if v.len() >= 1 << 15 => {
17626 let base = v.as_mut_ptr() as usize;
17627 p.run_rows(v.len(), &|s, e| {
17628 let sl = unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17631 for x in sl {
17632 *x = c * (*x / c).tanh();
17633 }
17634 });
17635 }
17636 _ => {
17637 for x in v.iter_mut() {
17638 *x = c * (*x / c).tanh();
17639 }
17640 }
17641 }
17642}
17643
17644thread_local! {
17645 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17649 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17650}
17651
17652fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17654 if gate_topk() > 0
17657 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17658 {
17659 return out;
17660 }
17661 let prism_body = d.gate_proj.has_prism_contract()
17677 || d.up_proj.has_prism_contract()
17678 || d.down_proj.has_prism_contract();
17679 if !prism_body
17680 && crate::gpu::enabled_here()
17681 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17682 {
17683 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17684 crate::gpu::ProbeArm::Gpu
17685 } else {
17686 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17687 };
17688 match arm {
17689 crate::gpu::ProbeArm::Gpu => {
17690 let t0 = std::time::Instant::now();
17691 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17692 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17693 return out;
17694 }
17695 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17699 }
17700 crate::gpu::ProbeArm::CpuTimed => {
17701 let t0 = std::time::Instant::now();
17702 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17703 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17704 return out;
17705 }
17706 crate::gpu::ProbeArm::Cpu => {
17707 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17708 }
17709 }
17710 }
17711 dense_ffn_cpu(d, x, pool)
17712}
17713
17714fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17716 let inter = d.gate_proj.rows();
17717 FFN_SCRATCH.with(|s| {
17718 let mut s = s.borrow_mut();
17719 let [g, u, ..] = &mut *s;
17720 g.resize(inter, 0.0);
17721 if gate_topk() > 0 {
17724 u.resize(inter, 0.0);
17728 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17729 for i in 0..inter {
17730 g[i] = Act::Silu.combine(g[i], 1.0);
17731 }
17732 keep_top_k(g, gate_topk());
17733 for i in 0..inter {
17734 g[i] *= u[i];
17735 }
17736 } else if d.act == Act::Silu && {
17737 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17738 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17739 } {
17740 } else {
17742 u.resize(inter, 0.0);
17743 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17745 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17746 for i in 0..inter {
17747 g[i] = d.act.combine(g[i], u[i]);
17748 }
17749 }
17750 FFN_PROBE.with(|pr| {
17758 if let Some(acc) = pr.borrow_mut().as_mut() {
17759 let li = crate::gpu::cur_layer();
17760 if li >= 0 {
17761 if let Some(row) = acc.get_mut(li as usize) {
17762 match probe_topk() {
17763 0 if probe_sq() => {
17764 for (a, &v) in row.iter_mut().zip(g.iter()) {
17765 *a += (v as f64) * (v as f64);
17766 }
17767 }
17768 0 if probe_signed() => {
17769 for (a, &v) in row.iter_mut().zip(g.iter()) {
17770 *a += v as f64;
17771 }
17772 }
17773 0 => {
17774 for (a, &v) in row.iter_mut().zip(g.iter()) {
17775 *a += (v as f64).abs();
17776 }
17777 }
17778 k => {
17779 let n = g.len();
17780 let k = k.min(n);
17781 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17782 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17783 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17784 });
17785 let thr = *kth;
17786 for (a, &v) in row.iter_mut().zip(g.iter()) {
17787 if v.abs() >= thr {
17788 *a += 1.0;
17789 }
17790 }
17791 }
17792 }
17793 }
17794 }
17795 }
17796 });
17797 if oracle_topk() > 0 {
17798 keep_top_k(g, oracle_topk());
17799 }
17800 {
17801 let li = crate::gpu::cur_layer();
17802 if li >= 0 {
17803 adump_row(li as usize, g);
17804 }
17805 }
17806 let mut out = attention::take_buf(d.down_proj.rows());
17807 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
17808 d.down_proj.matvec(g, &mut out, pool);
17809 out
17810 })
17811}
17812
17813pub struct RefitAcc {
17826 pub support: Vec<u32>,
17827 pub gss: Vec<f32>,
17828 pub ya: Vec<f32>,
17829 pub hidden: usize,
17830 pub tokens: u64,
17831 pub buf_g: Vec<f32>,
17837 pub buf_o: Vec<f32>,
17838 pub buf_t: usize,
17839}
17840
17841type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
17845
17846static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
17847 std::sync::OnceLock::new();
17848
17849fn ffn_probe_active() -> bool {
17852 FFN_PROBE.with(|p| p.borrow().is_some())
17853}
17854
17855fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
17856 REFIT
17857 .get_or_init(|| {
17858 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
17859 (
17860 d,
17861 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
17862 )
17863 })
17864 })
17865 .as_ref()
17866}
17867
17868fn refit_accumulate(
17870 li: usize,
17871 g: &[f32],
17872 b: usize,
17873 inter: usize,
17874 out: &[f32],
17875 hidden: usize,
17876 pool: Option<&Pool>,
17877) {
17878 let Some((dir, map)) = refit_dir() else {
17879 return;
17880 };
17881 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17882 let (from, to) = *SPAN.get_or_init(|| {
17883 let g = |k: &str, d: usize| {
17884 std::env::var(k)
17885 .ok()
17886 .and_then(|v| v.parse().ok())
17887 .unwrap_or(d)
17888 };
17889 (
17890 g("CMF_FFN_REFIT_FROM", 0),
17891 g("CMF_FFN_REFIT_TO", usize::MAX),
17892 )
17893 });
17894 if li < from || li > to {
17895 return;
17896 }
17897 let mut guard = map.lock().unwrap();
17898 let (map, shared) = &mut *guard;
17899 let acc = match map.entry(li) {
17900 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
17901 std::collections::hash_map::Entry::Vacant(e) => {
17902 let path = format!("{dir}/support.{li}.u32");
17903 let Ok(bytes) = std::fs::read(&path) else {
17904 eprintln!("refit: no {path} — layer {li} skipped");
17905 return;
17906 };
17907 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
17908 let support: Vec<u32> = bytes[4..4 + n * 4]
17909 .chunks_exact(4)
17910 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17911 .collect();
17912 eprintln!(
17913 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
17914 (n * n + hidden * n) as f64 * 4.0 / 1e6
17915 );
17916 e.insert(RefitAcc {
17917 gss: vec![0.0; n * n],
17918 ya: vec![0.0; hidden * n],
17919 buf_g: Vec::new(),
17920 buf_o: Vec::new(),
17921 buf_t: 0,
17922 support,
17923 hidden,
17924 tokens: 0,
17925 })
17926 }
17927 };
17928 let ns = acc.support.len();
17929 let cap = refit_batch();
17931 if acc.buf_g.is_empty() {
17932 acc.buf_g = vec![0.0; ns * cap];
17933 acc.buf_o = vec![0.0; hidden * cap];
17934 }
17935 let take = b.min(cap - acc.buf_t);
17936 for t in 0..take {
17937 let col = acc.buf_t + t;
17938 for (j, &n) in acc.support.iter().enumerate() {
17939 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17940 }
17941 for h in 0..hidden {
17942 acc.buf_o[h * cap + col] = out[t * hidden + h];
17943 }
17944 }
17945 acc.buf_t += take;
17946 acc.tokens += take as u64;
17947 if acc.buf_t < cap {
17948 return;
17949 }
17950 let bt = acc.buf_t;
17951 acc.buf_t = 0;
17952 let RefitAcc {
17962 gss,
17963 ya,
17964 buf_g,
17965 buf_o,
17966 ..
17967 } = acc;
17968 let need = (ns * ns).max(hidden * ns);
17969 if shared.len() < need {
17970 shared.resize(need, 0.0);
17971 }
17972 let scratch = &mut shared[..];
17973 let _ = bt;
17974 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17975 add_into(gss, &scratch[..ns * ns], pool);
17976 if crate::gpu::gemm_nt_f32_transient(
17977 buf_o,
17978 buf_g,
17979 &mut scratch[..hidden * ns],
17980 hidden,
17981 cap,
17982 ns,
17983 ) {
17984 add_into(ya, &scratch[..hidden * ns], pool);
17985 } else {
17986 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17987 }
17988 } else {
17989 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
17990 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17991 }
17992 }
17996
17997fn refit_batch() -> usize {
17999 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18000 *B.get_or_init(|| {
18001 std::env::var("CMF_FFN_REFIT_BATCH")
18002 .ok()
18003 .and_then(|v| v.parse().ok())
18004 .unwrap_or(4096)
18005 })
18006}
18007
18008fn accum_outer_t(
18011 c: &mut [f32],
18012 m: usize,
18013 n: usize,
18014 b: usize,
18015 left: &[f32],
18016 right: &[f32],
18017 pool: Option<&Pool>,
18018) {
18019 let ptr = SendMut(c.as_mut_ptr());
18020 let body = |i: usize| {
18021 let ptr = &ptr;
18022 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18023 for t in 0..b {
18024 let a = left[i * b + t];
18025 if a == 0.0 {
18026 continue;
18027 }
18028 for (j, o) in row.iter_mut().enumerate() {
18029 *o += a * right[j * b + t];
18030 }
18031 }
18032 };
18033 match pool {
18034 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18035 for i in s..e {
18036 body(i);
18037 }
18038 }),
18039 _ => {
18040 for i in 0..m {
18041 body(i);
18042 }
18043 }
18044 }
18045}
18046
18047fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18050 let n = dst.len().min(src.len());
18051 match pool {
18052 Some(p) if n >= 1 << 16 => {
18053 let ptr = SendMut(dst.as_mut_ptr());
18054 let f = |s: usize, e: usize| {
18055 let ptr = &ptr;
18056 for blk in s..e {
18057 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18058 for i in a..b {
18059 unsafe { *ptr.0.add(i) += src[i] };
18060 }
18061 }
18062 };
18063 p.run_rows(n.div_ceil(4096), &f);
18064 }
18065 _ => {
18066 for (d, v) in dst.iter_mut().zip(&src[..n]) {
18067 *d += *v;
18068 }
18069 }
18070 }
18071}
18072
18073fn accum_outer(
18078 c: &mut [f32],
18079 m: usize,
18080 n: usize,
18081 b: usize,
18082 left: &[f32],
18083 right: &[f32],
18084 pool: Option<&Pool>,
18085) {
18086 const TILE: usize = 32;
18087 let tiles = m.div_ceil(TILE);
18088 let cp = SendMut(c.as_mut_ptr());
18089 let body = |ti: usize| {
18090 let cp = &cp;
18091 let i0 = ti * TILE;
18092 let i1 = (i0 + TILE).min(m);
18093 for t in 0..b {
18094 let r = &right[t * n..t * n + n];
18095 for i in i0..i1 {
18096 let a = left[i * b + t];
18097 if a == 0.0 {
18098 continue;
18099 }
18100 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18102 for (o, v) in row.iter_mut().zip(r) {
18103 *o += a * *v;
18104 }
18105 }
18106 }
18107 };
18108 match pool {
18109 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18110 for ti in s..e {
18111 body(ti);
18112 }
18113 }),
18114 _ => {
18115 for ti in 0..tiles {
18116 body(ti);
18117 }
18118 }
18119 }
18120}
18121
18122pub fn refit_flush() -> usize {
18124 let Some((dir, map)) = refit_dir() else {
18125 return 0;
18126 };
18127 let guard = map.lock().unwrap();
18128 let mut n = 0;
18129 for (li, acc) in guard.0.iter() {
18130 let w = |name: &str, v: &[f32]| {
18133 let path = format!("{dir}/{name}.{li}.f32");
18134 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18135 match std::fs::write(&path, &bytes) {
18136 Ok(()) => {}
18137 Err(e) => eprintln!(
18138 "refit: FAILED to write {path} ({} MB): {e}",
18139 bytes.len() / 1_000_000
18140 ),
18141 }
18142 };
18143 w("gss", &acc.gss);
18144 w("ya", &acc.ya);
18145 println!(
18146 "refit L{li}: {} support, {} tokens, hidden {}",
18147 acc.support.len(),
18148 acc.tokens,
18149 acc.hidden
18150 );
18151 n += 1;
18152 }
18153 n
18154}
18155
18156fn adump_row(li: usize, g: &[f32]) {
18161 use std::io::Write as _;
18162 static FILES: std::sync::OnceLock<
18163 Option<(
18164 String,
18165 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18166 )>,
18167 > = std::sync::OnceLock::new();
18168 let Some((prefix, map)) = FILES
18169 .get_or_init(|| {
18170 std::env::var("CMF_FFN_ADUMP")
18171 .ok()
18172 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18173 })
18174 .as_ref()
18175 else {
18176 return;
18177 };
18178 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18181 let (from, to) = *SPAN.get_or_init(|| {
18182 let g = |k: &str, d: usize| {
18183 std::env::var(k)
18184 .ok()
18185 .and_then(|v| v.parse().ok())
18186 .unwrap_or(d)
18187 };
18188 (
18189 g("CMF_FFN_ADUMP_FROM", 0),
18190 g("CMF_FFN_ADUMP_TO", usize::MAX),
18191 )
18192 });
18193 if li < from || li > to {
18194 return;
18195 }
18196 let mut map = map.lock().unwrap();
18197 let f = map.entry(li).or_insert_with(|| {
18198 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18199 });
18200 let mut bytes = Vec::with_capacity(g.len() * 2);
18201 for v in g {
18202 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18203 }
18204 let _ = f.write_all(&bytes);
18205}
18206
18207fn oracle_topk() -> usize {
18213 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18214 *K.get_or_init(|| {
18215 std::env::var("CMF_FFN_ORACLE_TOPK")
18216 .ok()
18217 .and_then(|v| v.parse().ok())
18218 .unwrap_or(0)
18219 })
18220}
18221
18222fn gate_topk() -> usize {
18228 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18229 *K.get_or_init(|| {
18230 std::env::var("CMF_FFN_GATE_TOPK")
18231 .ok()
18232 .and_then(|v| v.parse().ok())
18233 .unwrap_or(0)
18234 })
18235}
18236
18237fn gate_block() -> usize {
18244 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18245 *B.get_or_init(|| {
18246 std::env::var("CMF_FFN_GATE_BLOCK")
18247 .ok()
18248 .and_then(|v| v.parse().ok())
18249 .unwrap_or(1)
18250 })
18251}
18252
18253fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18255 let n = g.len();
18256 let nb = n.div_ceil(block);
18257 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18258 if kb >= nb {
18259 return;
18260 }
18261 let mut score: Vec<f32> = (0..nb)
18262 .map(|b| {
18263 g[b * block..((b + 1) * block).min(n)]
18264 .iter()
18265 .map(|v| v * v)
18266 .sum::<f32>()
18267 })
18268 .collect();
18269 let mut ord = score.clone();
18270 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
18271 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18272 });
18273 let thr = *kth;
18274 for b in 0..nb {
18275 if score[b] < thr {
18276 g[b * block..((b + 1) * block).min(n)].fill(0.0);
18277 }
18278 }
18279 score.clear();
18280}
18281
18282fn keep_top_k(g: &mut [f32], k: usize) {
18284 if gate_block() > 1 {
18285 return keep_top_blocks(g, k, gate_block());
18286 }
18287 let n = g.len();
18288 if k == 0 || k >= n {
18289 return;
18290 }
18291 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18292 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18293 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18294 });
18295 let thr = *kth;
18296 for v in g.iter_mut() {
18297 if v.abs() < thr {
18298 *v = 0.0;
18299 }
18300 }
18301}
18302
18303fn probe_sq() -> bool {
18307 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18308 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
18309}
18310
18311fn probe_signed() -> bool {
18315 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18316 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
18317}
18318
18319fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
18327 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
18328 M.get_or_init(|| {
18329 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
18330 let b = std::fs::read(&p).ok()?;
18331 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
18332 let vals: Vec<f32> = b[8..]
18333 .chunks_exact(4)
18334 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18335 .collect();
18336 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
18337 Some((inter, vals))
18338 })
18339 .as_ref()
18340}
18341
18342fn probe_topk() -> usize {
18345 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18346 *K.get_or_init(|| {
18347 std::env::var("CMF_FFN_PROBE_TOPK")
18348 .ok()
18349 .and_then(|v| v.parse().ok())
18350 .unwrap_or(0)
18351 })
18352}
18353
18354thread_local! {
18355 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18358 const { std::cell::RefCell::new(None) };
18359}
18360
18361fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18374 if d.gate_proj.has_prism_contract()
18379 || d.up_proj.has_prism_contract()
18380 || d.down_proj.has_prism_contract()
18381 {
18382 return None;
18383 }
18384 let dt = d.down_t.as_ref()?;
18385 let inter = d.gate_proj.rows();
18386 let hidden = dt.cols();
18387 if k == 0 || k >= inter || d.act != Act::Silu {
18388 return None;
18389 }
18390 DYN_SCRATCH.with(|sc| {
18391 let mut sc = sc.borrow_mut();
18392 let DynScratch {
18393 g,
18394 mag,
18395 live,
18396 parts,
18397 } = &mut *sc;
18398 g.resize(inter, 0.0);
18399 d.gate_proj.matvec(x, g, pool);
18400 for v in g.iter_mut() {
18401 *v = inference::silu(*v);
18402 }
18403 mag.clear();
18406 mag.extend(g.iter().map(|v| v.abs()));
18407 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18408 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18409 });
18410 let thr = *kth;
18411 live.clear();
18412 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18413 let mut out = vec![0.0f32; hidden];
18414 match pool {
18415 Some(p) if live.len() >= 64 => {
18416 let nw = p.n_workers() + 1;
18417 parts.clear();
18418 parts.resize(nw * hidden, 0.0);
18419 let ptr = SendMut(parts.as_mut_ptr());
18420 let n = live.len();
18421 let live_ref: &[u32] = live;
18422 let g_ref: &[f32] = g;
18423 p.run(&|w, workers| {
18424 let chunk = n.div_ceil(workers);
18425 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18426 if s >= e {
18427 return;
18428 }
18429 WORKER_SCRATCH.with(|ws| {
18430 let mut ws = ws.borrow_mut();
18431 let [scratch, acc] = &mut *ws;
18432 scratch.resize(hidden.max(x.len()), 0.0);
18433 acc.clear();
18434 acc.resize(hidden, 0.0);
18435 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18436 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18439 d.up_proj.prefetch_row(nx as usize);
18440 dt.prefetch_row(nx as usize);
18441 }
18442 let idx = nrm as usize;
18443 let up = d.up_proj.row_dot(idx, x, scratch);
18444 let a = g_ref[idx] * up;
18445 if a != 0.0 {
18446 dt.add_row_scaled(idx, a, acc, scratch);
18447 }
18448 }
18449 for (j, v) in acc.iter().enumerate() {
18450 unsafe { *ptr.at(w * hidden + j) = *v };
18451 }
18452 });
18453 });
18454 for w in 0..nw {
18455 for (j, o) in out.iter_mut().enumerate() {
18456 *o += parts[w * hidden + j];
18457 }
18458 }
18459 }
18460 _ => {
18461 WORKER_SCRATCH.with(|ws| {
18462 let mut ws = ws.borrow_mut();
18463 let [scratch, _acc] = &mut *ws;
18464 scratch.resize(hidden.max(x.len()), 0.0);
18465 for &nrm in live.iter() {
18466 let idx = nrm as usize;
18467 let up = d.up_proj.row_dot(idx, x, scratch);
18468 let a = g[idx] * up;
18469 if a != 0.0 {
18470 dt.add_row_scaled(idx, a, &mut out, scratch);
18471 }
18472 }
18473 });
18474 }
18475 }
18476 Some(out)
18477 })
18478}
18479
18480struct DynScratch {
18483 g: Vec<f32>,
18484 mag: Vec<f32>,
18485 live: Vec<u32>,
18486 parts: Vec<f32>,
18487}
18488
18489thread_local! {
18490 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18491 std::cell::RefCell::new(DynScratch {
18492 g: Vec::new(),
18493 mag: Vec::new(),
18494 live: Vec::new(),
18495 parts: Vec::new(),
18496 })
18497 };
18498 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18500 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18501}
18502
18503fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18508 let inter = d.gate_proj.rows();
18509 FFN_SCRATCH.with(|s| {
18510 let mut s = s.borrow_mut();
18511 let [g, u, ..] = &mut *s;
18512 g.resize(inter, 0.0);
18513 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18514 } else {
18516 u.resize(inter, 0.0);
18517 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18518 for i in 0..inter {
18519 g[i] = d.act.combine(g[i], u[i]);
18520 }
18521 }
18522 zero_masked_cols(g, 1, inter, mask_row);
18523 let mut out = attention::take_buf(d.down_proj.rows());
18524 d.down_proj.matvec(g, &mut out, pool);
18525 out
18526 })
18527}
18528
18529fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18535 if d.gate_proj.has_prism_contract()
18536 || d.up_proj.has_prism_contract()
18537 || d.down_proj.has_prism_contract()
18538 {
18539 return None;
18540 }
18541 d.act.graph_act()?;
18544 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18547 return None;
18548 }
18549 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18550 let mut model_ref = None;
18551 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18552 let model = model_ref?;
18553 let hidden = jobs[0].down.1;
18554 let mut out = attention::take_buf(hidden);
18555 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18556 Some(out)
18557 } else {
18558 let mut out = out;
18559 attention::recycle_buf(&mut out);
18560 None
18561 }
18562}
18563
18564#[allow(clippy::type_complexity)]
18569#[allow(clippy::type_complexity)]
18570pub(crate) fn moe_parts(
18571 t: &QTensor,
18572) -> Option<(
18573 &std::sync::Arc<cortiq_core::CmfModel>,
18574 usize,
18575 usize,
18576 usize,
18577 &[f32],
18578 &[f32],
18579 bool,
18580 bool,
18581 bool,
18582)> {
18583 match t {
18584 QTensor::Mapped {
18585 model,
18586 idx,
18587 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18588 rows,
18589 cols,
18590 row_scale,
18591 col_field,
18592 ..
18593 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18594 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18595 )),
18596 QTensor::Mapped {
18598 model,
18599 idx,
18600 dtype: cortiq_core::TensorDtype::Q1,
18601 rows,
18602 cols,
18603 ..
18604 } => Some((
18605 model,
18606 *idx,
18607 *rows,
18608 *cols,
18609 &[][..],
18610 &[][..],
18611 true,
18612 false,
18613 false,
18614 )),
18615 QTensor::Mapped {
18617 model,
18618 idx,
18619 dtype: cortiq_core::TensorDtype::Q4Tiled,
18620 rows,
18621 cols,
18622 ..
18623 } => Some((
18624 model,
18625 *idx,
18626 *rows,
18627 *cols,
18628 &[][..],
18629 &[][..],
18630 false,
18631 true,
18632 false,
18633 )),
18634 QTensor::Mapped {
18636 model,
18637 idx,
18638 dtype: cortiq_core::TensorDtype::Q4TiledP,
18639 rows,
18640 cols,
18641 ..
18642 } => Some((
18643 model,
18644 *idx,
18645 *rows,
18646 *cols,
18647 &[][..],
18648 &[][..],
18649 false,
18650 true,
18651 false,
18652 )),
18653 QTensor::Mapped {
18657 model,
18658 idx,
18659 dtype: cortiq_core::TensorDtype::Q2TiledP,
18660 rows,
18661 cols,
18662 ..
18663 } => Some((
18664 model,
18665 *idx,
18666 *rows,
18667 *cols,
18668 &[][..],
18669 &[][..],
18670 false,
18671 true,
18672 true,
18673 )),
18674 _ => None,
18675 }
18676}
18677
18678#[cfg(target_os = "macos")]
18685#[allow(clippy::type_complexity)]
18686fn chunk_moe_parts(m: &MoeFfn, hidden: usize) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18687 if m.shared.is_some()
18688 || m.per_expert_scale.is_some()
18689 || m.resonance.is_some()
18690 || m.route_tau.is_some()
18691 || m.router_input_norm
18692 || m.experts.is_empty()
18693 || m.top_k == 0
18694 || std::env::var("CMF_RMS_TRACE").is_ok()
18695 || std::env::var("CMF_ACT_DUMP").is_ok()
18696 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18697 {
18698 return None;
18699 }
18700 let (rf, rr, rc) = m.router.f32_parts()?;
18701 if rr != m.experts.len() || rc != hidden {
18702 return None;
18703 }
18704 let inter = m.experts[0].gate_proj.rows();
18705 let q8 = crate::gpu_metal::moe_q8::enabled()
18708 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18709 let experts = m
18710 .experts
18711 .iter()
18712 .map(|e| {
18713 if e.act != Act::Silu
18714 || !e.segs.is_empty()
18715 || e.gate_proj.rows() != inter
18716 || e.up_proj.rows() != inter
18717 || e.down_proj.cols() != inter
18718 || e.gate_proj.cols() != hidden
18719 || e.down_proj.rows() != hidden
18720 {
18721 return None;
18722 }
18723 if q8 {
18724 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18725 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18726 }
18727 Some((
18728 e.gate_proj.mapped_q4tp()?.1,
18729 e.up_proj.mapped_q4tp()?.1,
18730 e.down_proj.mapped_q4tp()?.1,
18731 ))
18732 })
18733 .collect::<Option<Vec<_>>>()?;
18734 Some((rf, experts, q8))
18735}
18736
18737#[cfg(target_os = "macos")]
18745fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
18746 if m.router_input_norm
18747 || m.route_tau.is_some()
18748 || m.mask.is_some()
18749 || m.per_expert_scale.is_some()
18750 || m.experts.is_empty()
18751 || m.top_k == 0
18752 || m.resonance.is_some()
18753 {
18754 return None;
18755 }
18756 let (sh, sg) = match &m.shared {
18761 Some((sh, sg)) => (Some(sh), sg.as_ref()),
18762 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
18763 None => return None,
18764 };
18765 let (rf, rr, rc) = m.router.f32_parts()?;
18766 if rr != m.experts.len() || rc != hidden {
18767 return None;
18768 }
18769 let shared_gated = sg.is_some();
18770 let sf = match sg {
18771 Some(sg) => {
18772 let (sf, sr, sc) = sg.f32_parts()?;
18773 if sr * sc != hidden {
18774 return None;
18775 }
18776 sf
18777 }
18778 None => &rf[..hidden],
18781 };
18782 if let Some(b) = &m.expert_bias {
18783 if b.len() != m.experts.len() {
18784 return None;
18785 }
18786 }
18787 let inter = m.experts[0].gate_proj.rows();
18788 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
18791 let q8 = crate::gpu_metal::moe_q8::enabled()
18795 && m.shared.is_none()
18796 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18797 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
18798 if e.act != Act::Silu
18799 || e.gate_proj.rows() != inter
18800 || e.gate_proj.cols() != hidden
18801 || e.up_proj.rows() != inter
18802 || e.up_proj.cols() != hidden
18803 || e.down_proj.rows() != hidden
18804 || e.down_proj.cols() != inter
18805 {
18806 return None;
18807 }
18808 if q8 {
18809 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18810 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18811 }
18812 let pick = |t: &QTensor| -> Option<usize> {
18813 if gu_q2 {
18814 t.mapped_q2tp().map(|(_, i)| i)
18815 } else {
18816 t.mapped_q4tp().map(|(_, i)| i)
18817 }
18818 };
18819 Some((
18820 pick(&e.gate_proj)?,
18821 pick(&e.up_proj)?,
18822 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
18823 ))
18824 };
18825 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
18826 let shared = match sh {
18827 Some(sh) => Some(trio(sh)?),
18828 None => None,
18829 };
18830 Some(crate::gpu::GpuMoe {
18831 router: rf,
18832 sgate: sf,
18833 experts,
18834 shared,
18835 n_exp: m.experts.len(),
18836 top_k: m.top_k,
18837 inter,
18838 norm_topk: m.norm_topk_prob,
18839 route_scale: m.routed_scaling,
18840 gu_q2,
18841 q8,
18842 sigmoid: m.router_sigmoid,
18843 bias: m.expert_bias.as_deref(),
18844 shared_gated,
18845 })
18846}
18847
18848fn forced_ids_at(step: usize) -> Option<u32> {
18854 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
18855 IDS.get_or_init(|| {
18856 std::env::var("CMF_FORCE_IDS")
18857 .ok()
18858 .and_then(|p| std::fs::read_to_string(p).ok())
18859 .map(|s| s.split_whitespace().filter_map(|t| t.parse().ok()).collect())
18860 .unwrap_or_default()
18861 })
18862 .get(step)
18863 .copied()
18864}
18865
18866pub(crate) fn moe_push_job_parts<'a>(
18870 gate: &'a QTensor,
18871 up: &'a QTensor,
18872 down: &'a QTensor,
18873 x: &[f32],
18874 w: f32,
18875 swiglu_limit: f32,
18876 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18877 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18878) -> Option<()> {
18879 use crate::qtensor::prescale;
18880 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
18881 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
18882 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
18883 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18884 return None; }
18886 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
18889 return None;
18890 }
18891 if !gq2 && dq2 {
18892 return None;
18893 }
18894 model_ref.get_or_insert_with(|| gm.clone());
18895 let dt = |cf: &[f32]| {
18896 if cf.is_empty() {
18897 cortiq_core::TensorDtype::Q8Row
18898 } else {
18899 cortiq_core::TensorDtype::Q8_2f
18900 }
18901 };
18902 jobs.push(crate::gpu::MoeJob {
18903 gate: (gi, gr, gc, grs),
18904 up: (ui, ur, uc, urs),
18905 down: (di, dr, dc, drs),
18906 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
18907 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
18908 down_col: dcf,
18909 w,
18910 q1: gq1,
18911 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
18912 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
18913 gu_q2: gq2,
18914 swiglu_limit,
18915 act: 0,
18916 });
18917 Some(())
18918}
18919
18920fn moe_push_job<'a>(
18922 d: &'a DenseFfn,
18923 x: &[f32],
18924 w: f32,
18925 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18926 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18927) -> Option<()> {
18928 use crate::qtensor::prescale;
18929 let act = d.act.graph_act()?.code();
18932 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
18933 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
18934 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
18935 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18936 return None; }
18938 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
18939 return None;
18940 }
18941 if !gq2 && dq2 {
18942 return None;
18943 }
18944 model_ref.get_or_insert_with(|| gm.clone());
18945 let gdt = if gcf.is_empty() {
18946 cortiq_core::TensorDtype::Q8Row
18947 } else {
18948 cortiq_core::TensorDtype::Q8_2f
18949 };
18950 let udt = if ucf.is_empty() {
18951 cortiq_core::TensorDtype::Q8Row
18952 } else {
18953 cortiq_core::TensorDtype::Q8_2f
18954 };
18955 jobs.push(crate::gpu::MoeJob {
18956 gate: (gi, gr, gc, grs),
18957 up: (ui, ur, uc, urs),
18958 down: (di, dr, dc, drs),
18959 xs_gate: prescale(x, gcf, gdt).into_owned(),
18960 xs_up: prescale(x, ucf, udt).into_owned(),
18961 down_col: dcf,
18962 w,
18963 q1: gq1,
18964 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
18965 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
18966 gu_q2: gq2,
18967 swiglu_limit: 0.0,
18968 act,
18969 });
18970 Some(())
18971}
18972
18973fn sparse_ffn_quant(
18980 d: &DenseFfn,
18981 x: &[f32],
18982 active: &[u16],
18983 hidden: usize,
18984 pool: Option<&Pool>,
18985) -> Vec<f32> {
18986 let n = active.len();
18987 let inter = d.gate_proj.rows();
18988 let mut act = vec![0.0f32; n];
18989 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
18992 let compute = |ai: usize| -> f32 {
18993 let idx = active[ai] as usize;
18994 if idx >= inter {
18995 return 0.0; }
18997 let mut s = if need_scratch {
18998 vec![0.0f32; hidden]
18999 } else {
19000 Vec::new()
19001 };
19002 let gate = d.gate_proj.row_dot(idx, x, &mut s);
19003 let up = d.up_proj.row_dot(idx, x, &mut s);
19004 d.act.combine(gate, up)
19005 };
19006 match pool {
19007 Some(p) if n >= 256 => {
19008 let ptr = SendMut(act.as_mut_ptr());
19009 p.run(&|widx, nw| {
19010 let chunk = n.div_ceil(nw);
19011 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19012 for ai in s..e {
19013 unsafe { *ptr.at(ai) = compute(ai) };
19014 }
19015 });
19016 }
19017 _ => {
19018 for (ai, a) in act.iter_mut().enumerate() {
19019 *a = compute(ai);
19020 }
19021 }
19022 }
19023 let mut out = vec![0.0f32; hidden];
19025 for (ai, &idx) in active.iter().enumerate() {
19026 let w = act[ai];
19027 if w.abs() >= 1e-12 && (idx as usize) < inter {
19028 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19029 }
19030 }
19031 out
19032}
19033
19034#[doc(hidden)]
19036pub fn sparse_ffn_quant_for_test(
19037 d: &DenseFfn,
19038 x: &[f32],
19039 active: &[u16],
19040 hidden: usize,
19041) -> Vec<f32> {
19042 sparse_ffn_quant(d, x, active, hidden, None)
19043}
19044
19045fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19049 let deq = |t: &QTensor| -> Vec<f32> {
19050 let (rows, cols) = (t.rows(), t.cols());
19051 let mut out = vec![0.0f32; rows * cols];
19052 for r in 0..rows {
19053 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19054 }
19055 out
19056 };
19057 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19058}
19059
19060struct SendMut(*mut f32);
19062unsafe impl Send for SendMut {}
19063unsafe impl Sync for SendMut {}
19064impl SendMut {
19065 #[inline]
19066 #[allow(clippy::mut_from_ref)]
19069 unsafe fn at(&self, i: usize) -> &mut f32 {
19070 unsafe { &mut *self.0.add(i) }
19071 }
19072}
19073
19074pub(crate) fn moe_route(
19087 logits: &[f32],
19088 m: &MoeFfn,
19089 allowed: Option<&[bool]>,
19090) -> (Vec<usize>, Vec<f32>, f32) {
19091 moe_route_with_eps(logits, m, allowed, 1e-6)
19092}
19093
19094pub(crate) fn moe_route_with_eps(
19103 logits: &[f32],
19104 m: &MoeFfn,
19105 allowed: Option<&[bool]>,
19106 sigmoid_denom_eps: f32,
19107) -> (Vec<usize>, Vec<f32>, f32) {
19108 let ne = logits.len();
19109 let admit = |e: usize| {
19115 m.mask.as_ref().is_none_or(|mk| mk[e])
19116 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19117 };
19118 if m.resonance.is_some() && m.top_k == 1 {
19131 let mut best: Option<usize> = None;
19132 for e in (0..ne).filter(|&e| admit(e)) {
19133 let l = logits[e];
19134 if l == f32::NEG_INFINITY || l.is_nan() {
19135 continue;
19136 }
19137 if best.is_none_or(|b| l > logits[b]) {
19138 best = Some(e);
19139 }
19140 }
19141 if let Some(b) = best {
19142 let mut p = vec![0.0f32; ne];
19143 p[b] = 1.0;
19144 return (vec![b], p, 1.0 / m.routed_scaling);
19145 }
19146 }
19147 let p: Vec<f32> = if m.router_sigmoid {
19153 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19154 } else {
19155 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19156 if mx == f32::NEG_INFINITY {
19157 vec![1.0 / ne.max(1) as f32; ne]
19158 } else {
19159 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
19160 let s: f32 = e.iter().sum();
19161 for v in &mut e {
19162 *v /= s;
19163 }
19164 e
19165 }
19166 };
19167 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
19168 match &m.expert_bias {
19170 Some(b) => idx.sort_unstable_by(|&x, &y| {
19171 (p[y] + b[y])
19172 .partial_cmp(&(p[x] + b[x]))
19173 .unwrap()
19174 .then(x.cmp(&y))
19175 }),
19176 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
19177 }
19178 idx.truncate(m.top_k);
19179 if let Some(tau) = m.route_tau {
19183 let total: f32 = idx.iter().map(|&e| p[e]).sum();
19184 if total > 0.0 {
19185 let mut acc = 0.0f32;
19186 let mut keep = idx.len();
19187 for (i, &e) in idx.iter().enumerate() {
19188 acc += p[e];
19189 if acc >= tau * total {
19190 keep = i + 1;
19191 break;
19192 }
19193 }
19194 idx.truncate(keep);
19195 }
19196 }
19197 let wsum: f32 = if m.norm_topk_prob {
19198 let s: f32 = idx.iter().map(|&e| p[e]).sum();
19199 (if m.router_sigmoid {
19203 s + sigmoid_denom_eps
19204 } else {
19205 s
19206 }) / m.routed_scaling
19207 } else {
19208 1.0 / m.routed_scaling
19209 };
19210 (idx, p, wsum)
19211}
19212
19213fn moe_trace(idx: &[usize]) {
19215 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
19216}
19217
19218pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
19221 use std::io::Write;
19222 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
19223 std::sync::OnceLock::new();
19224 let Some(f) = F.get_or_init(|| {
19225 let p = std::env::var("CMF_MOE_TRACE").ok()?;
19226 Some(std::sync::Mutex::new(
19227 std::fs::OpenOptions::new()
19228 .create(true)
19229 .append(true)
19230 .open(p)
19231 .ok()?,
19232 ))
19233 }) else {
19234 return;
19235 };
19236 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
19237 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
19238}
19239
19240pub(crate) fn moe_ffn(
19243 m: &MoeFfn,
19244 x: &[f32],
19245 pool: Option<&Pool>,
19246 allowed: Option<&[bool]>,
19247) -> Vec<f32> {
19248 let r = moe_ffn_route(m, x, pool, allowed);
19249 moe_ffn_experts(m, x, &r, pool)
19250}
19251
19252pub(crate) struct MoeRoute {
19256 pub idx: Vec<usize>,
19257 pub p: Vec<f32>,
19258 pub wsum: f32,
19259 pub logits: Vec<f32>,
19260}
19261
19262pub(crate) fn moe_ffn_route(
19268 m: &MoeFfn,
19269 x: &[f32],
19270 pool: Option<&Pool>,
19271 allowed: Option<&[bool]>,
19272) -> MoeRoute {
19273 accumulate_act(m, x, 1);
19274 let ne = m.experts.len();
19275 let mut logits = vec![0.0f32; ne];
19276 match &m.resonance {
19277 Some(r) => r.scores(x, &mut logits),
19278 None => m.router.matvec(x, &mut logits, pool),
19279 }
19280 let (idx, p, wsum) = moe_route(&logits, m, allowed);
19281 {
19282 let mut st = m.stats.borrow_mut();
19283 if st.len() < ne {
19284 st.resize(ne, 0);
19285 }
19286 for &e in &idx {
19287 st[e] += 1;
19288 }
19289 }
19290 moe_trace(&idx);
19296 MoeRoute {
19297 idx,
19298 p,
19299 wsum,
19300 logits,
19301 }
19302}
19303
19304pub(crate) fn moe_ffn_experts(
19307 m: &MoeFfn,
19308 x: &[f32],
19309 r: &MoeRoute,
19310 pool: Option<&Pool>,
19311) -> Vec<f32> {
19312 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
19313 if crate::gpu::enabled_here() {
19318 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
19319 crate::gpu::ProbeArm::Gpu => {
19320 let t0 = std::time::Instant::now();
19321 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
19322 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
19323 return out;
19324 }
19325 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
19326 }
19327 crate::gpu::ProbeArm::CpuTimed => {
19328 let t0 = std::time::Instant::now();
19329 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19330 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
19331 return out;
19332 }
19333 crate::gpu::ProbeArm::Cpu => {
19334 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19335 }
19336 }
19337 }
19338 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
19339}
19340
19341fn moe_ffn_minicpm_tail(
19345 slot: &mut crate::minicpm_tail::MiniCpmTail,
19346 li: usize,
19347 total_layers: usize,
19348 m: &MoeFfn,
19349 x: &[f32],
19350 pool: Option<&Pool>,
19351) -> Vec<f32> {
19352 let route = moe_ffn_route(m, x, pool, None);
19353 match slot.forward(li, m, x, &route, pool, total_layers) {
19354 Some(out) => out,
19355 None => moe_ffn_experts(m, x, &route, pool),
19360 }
19361}
19362
19363fn moe_ffn_banked(
19367 slot: &mut crate::mimo_moe::Slot,
19368 li: usize,
19369 m: &MoeFfn,
19370 x: &[f32],
19371 pool: Option<&Pool>,
19372) -> Vec<f32> {
19373 let t0 = std::time::Instant::now();
19374 let r = moe_ffn_route(m, x, pool, None);
19375 slot.note_route(t0.elapsed().as_nanos() as u64);
19376 match slot.forward(li, m, x, &r, pool) {
19377 Some(out) => out,
19378 None => crate::qtensor::float_activations_scope(|| {
19379 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19380 }),
19381 }
19382}
19383
19384fn moe_ffn_banked_rows(
19386 slot: &mut crate::mimo_moe::Slot,
19387 li: usize,
19388 m: &MoeFfn,
19389 xs: &[f32],
19390 b: usize,
19391 hidden: usize,
19392 pool: Option<&Pool>,
19393) -> Vec<f32> {
19394 let t0 = std::time::Instant::now();
19395 let routes: Vec<_> = xs
19396 .chunks_exact(hidden)
19397 .map(|x| moe_ffn_route(m, x, pool, None))
19398 .collect();
19399 slot.note_route(t0.elapsed().as_nanos() as u64);
19400 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19401 return out;
19402 }
19403 let mut out = Vec::with_capacity(b * hidden);
19404 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19405 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19406 crate::qtensor::float_activations_scope(|| {
19408 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19409 })
19410 });
19411 out.extend(row);
19412 }
19413 out
19414}
19415
19416fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19421 use std::sync::atomic::{AtomicBool, Ordering};
19422 if built {
19423 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19424 if total_layers > 0 && layers_run < total_layers {
19425 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19426 } else {
19427 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19428 }
19429 } else {
19430 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19431 }
19432 static SAID: AtomicBool = AtomicBool::new(false);
19433 if !SAID.swap(true, Ordering::Relaxed) {
19434 if built {
19435 tracing::info!("wgpu whole-token graph: ACTIVE");
19436 } else {
19437 tracing::warn!("wgpu whole-token graph refused — per-op path");
19438 }
19439 }
19440}
19441
19442pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19446pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19447pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19451pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19453
19454pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
19458 std::sync::atomic::AtomicU64::new(0);
19459pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
19460 std::sync::atomic::AtomicU64::new(0);
19461pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19462 std::sync::atomic::AtomicU64::new(0);
19463pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
19464 std::sync::atomic::AtomicU64::new(0);
19465pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
19466 std::sync::atomic::AtomicU64::new(0);
19467pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19471 std::sync::atomic::AtomicU64::new(0);
19472pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
19473 std::sync::atomic::AtomicU64::new(0);
19474pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19475 std::sync::atomic::AtomicU64::new(0);
19476pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19477 std::sync::atomic::AtomicU64::new(0);
19478
19479fn moe_batch_enabled() -> bool {
19482 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19483 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19484}
19485
19486fn moe_ffn_cpu_batched(
19492 m: &MoeFfn,
19493 x: &[f32],
19494 idx: &[usize],
19495 p: &[f32],
19496 wsum: f32,
19497 pool: Option<&Pool>,
19498) -> Option<Vec<f32>> {
19499 if idx.is_empty() || !moe_batch_enabled() {
19500 return None;
19501 }
19502 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19506 return None;
19507 }
19508 let n = idx.len() + usize::from(m.shared.is_some());
19509 let mut pairs = Vec::with_capacity(n);
19510 let mut downs = Vec::with_capacity(n);
19511 let mut ws = Vec::with_capacity(n);
19512 for &e in idx {
19513 let d = &m.experts[e];
19514 if d.act != Act::Silu {
19515 return None;
19516 }
19517 pairs.push((&d.gate_proj, &d.up_proj));
19518 downs.push(&d.down_proj);
19519 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19520 }
19521 if let Some((se, gate)) = &m.shared {
19524 if se.act != Act::Silu {
19525 return None;
19526 }
19527 let g = gate.as_ref().map_or(1.0, |gate| {
19528 let mut gl = [0.0f32; 1];
19529 gate.matvec(x, &mut gl, pool);
19530 1.0 / (1.0 + (-gl[0]).exp())
19531 });
19532 pairs.push((&se.gate_proj, &se.up_proj));
19533 downs.push(&se.down_proj);
19534 ws.push(g);
19535 }
19536 let inter = pairs[0].0.rows();
19537 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19538 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19539 return None;
19540 }
19541 let mut out = attention::take_buf(x.len());
19542 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19543 attention::recycle_buf(&mut out);
19544 return None;
19545 }
19546 Some(out)
19547}
19548
19549pub(crate) fn moe_cold_experts_cpu(
19555 experts: &[(&DenseFfn, f32)],
19556 x: &[f32],
19557 pool: Option<&Pool>,
19558) -> Vec<f32> {
19559 let mut out = attention::take_buf(x.len());
19560 if experts.is_empty() {
19561 return out;
19562 }
19563 let pairs: Vec<_> = experts
19564 .iter()
19565 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19566 .collect();
19567 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19568 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19569 let inter = experts[0].0.gate_proj.rows();
19570 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19571 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19572 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19573 {
19574 return out;
19575 }
19576 out.fill(0.0);
19577 for &(expert, weight) in experts {
19578 let mut one = dense_ffn(expert, x, pool);
19579 for (o, v) in out.iter_mut().zip(&one) {
19580 *o += weight * v;
19581 }
19582 attention::recycle_buf(&mut one);
19583 }
19584 out
19585}
19586
19587pub(crate) fn moe_cold_experts_rows_cpu(
19591 jobs: &[Vec<(&DenseFfn, f32)>],
19592 xs: &[f32],
19593 hidden: usize,
19594 pool: Option<&Pool>,
19595) -> Vec<f32> {
19596 let mut out = vec![0.0; xs.len()];
19597 let mut experts: Vec<&DenseFfn> = Vec::new();
19598 let mut groups: Vec<Vec<usize>> = Vec::new();
19599 let mut terms = vec![Vec::new(); jobs.len()];
19600 for (r, row) in jobs.iter().enumerate() {
19601 for &(e, w) in row {
19602 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
19603 Some(g) => g,
19604 None => {
19605 experts.push(e);
19606 groups.push(Vec::new());
19607 groups.len() - 1
19608 }
19609 };
19610 terms[r].push((g, groups[g].len(), w));
19611 groups[g].push(r);
19612 }
19613 }
19614 if experts.is_empty() {
19615 return out;
19616 }
19617 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
19618 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
19619 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
19620 let count: usize = lens.iter().sum();
19621 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
19622 let mut ds = vec![vec![0.0; hidden]; count];
19623 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
19624 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
19625 {
19626 let mut offset = 0;
19627 let offsets: Vec<_> = lens
19628 .iter()
19629 .map(|&n| {
19630 let start = offset;
19631 offset += n;
19632 start
19633 })
19634 .collect();
19635 for (r, terms) in terms.iter().enumerate() {
19636 for &(g, slot, w) in terms {
19637 for (o, &v) in out[r * hidden..(r + 1) * hidden]
19638 .iter_mut()
19639 .zip(&ds[offsets[g] + slot])
19640 {
19641 *o += w * v;
19642 }
19643 }
19644 }
19645 } else {
19646 for (r, jobs) in jobs.iter().enumerate() {
19647 if !jobs.is_empty() {
19648 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
19649 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
19650 attention::recycle_buf(&mut row);
19651 }
19652 }
19653 }
19654 out
19655}
19656
19657fn moe_ffn_cpu(
19659 m: &MoeFfn,
19660 x: &[f32],
19661 idx: &[usize],
19662 p: &[f32],
19663 wsum: f32,
19664 pool: Option<&Pool>,
19665) -> Vec<f32> {
19666 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
19667 return out;
19668 }
19669 let mut out = attention::take_buf(x.len());
19670 for &e in idx {
19671 let mut eo = dense_ffn(&m.experts[e], x, pool);
19672 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
19673 for i in 0..out.len() {
19674 out[i] += w * eo[i];
19675 }
19676 attention::recycle_buf(&mut eo);
19677 }
19678 if let Some((se, gate)) = &m.shared {
19679 let mut so = dense_ffn(se, x, pool);
19680 let g = gate.as_ref().map_or(1.0, |gate| {
19681 let mut gl = [0.0f32; 1];
19682 gate.matvec(x, &mut gl, pool);
19683 1.0 / (1.0 + (-gl[0]).exp())
19684 });
19685 for i in 0..out.len() {
19686 out[i] += g * so[i];
19687 }
19688 attention::recycle_buf(&mut so);
19689 }
19690 out
19691}
19692
19693#[allow(clippy::too_many_arguments)]
19701pub(crate) fn mla_attention(
19702 w: &MlaWeights,
19703 normed: &[f32],
19704 cache: &mut crate::kv_cache::LayerKvCache,
19705 position: usize,
19706 inv_freq: &[f32],
19707 rope_scale: f32,
19708 eps: f64,
19709 pool: Option<&Pool>,
19710) -> Vec<f32> {
19711 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
19712 let hd = dr + dn;
19713 let mut q = vec![0.0f32; nh * hd];
19714 match (&w.q_a, &w.q_a_norm) {
19715 (Some(qa), Some(qn)) => {
19716 let mut t = vec![0.0f32; qa.rows()];
19717 qa.matvec(normed, &mut t, pool);
19718 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
19719 w.q_proj.matvec(&tn, &mut q, pool);
19720 }
19721 _ => w.q_proj.matvec(normed, &mut q, pool),
19722 }
19723 let mut ca = vec![0.0f32; lora + dr];
19724 w.kv_a.matvec(normed, &mut ca, pool);
19725 let (c_lat, k_rope) = ca.split_at_mut(lora);
19726 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
19727 let mut kvb = vec![0.0f32; nh * (dn + dv)];
19728 w.kv_b.matvec(&latn, &mut kvb, pool);
19729 if !w.nope {
19730 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
19731 }
19732 for h in 0..nh {
19733 if !w.nope {
19734 attention::rope_rotate_scaled(
19735 &mut q[h * hd..h * hd + dr],
19736 position,
19737 inv_freq,
19738 rope_scale,
19739 );
19740 }
19741 }
19742 let mut k = vec![0.0f32; nh * hd];
19743 let mut v = vec![0.0f32; nh * hd];
19744 for h in 0..nh {
19745 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
19746 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
19747 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
19748 }
19749 cache.append(&k, &v, &vec![true; nh]);
19750 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
19751 attention::recycle_buf(&mut imp);
19752 let mut ov = vec![0.0f32; nh * dv];
19753 for h in 0..nh {
19754 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
19755 }
19756 let mut out = vec![0.0f32; w.o_proj.rows()];
19757 w.o_proj.matvec(&ov, &mut out, pool);
19758 out
19759}
19760
19761fn dense_moe_ffn(
19768 dm: &DenseMoeFfn,
19769 x_normed: &[f32],
19770 h_raw: &[f32],
19771 eps: f64,
19772 norm_style: NormStyle,
19773 pool: Option<&Pool>,
19774) -> Vec<f32> {
19775 let mut d = dense_ffn(&dm.dense, x_normed, pool);
19776 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
19777 let m = &dm.moe;
19778 let ne = m.experts.len();
19779 let mut logits = vec![0.0f32; ne];
19780 if m.router_input_norm {
19781 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
19782 let inv = 1.0 / (ss + eps as f32).sqrt();
19783 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
19784 m.router.matvec(&xr, &mut logits, pool);
19785 } else {
19786 m.router.matvec(h_raw, &mut logits, pool);
19787 }
19788 let (idx, p, wsum) = moe_route(&logits, m, None);
19789 {
19790 let mut st = m.stats.borrow_mut();
19791 if st.len() < ne {
19792 st.resize(ne, 0);
19793 }
19794 for &e in &idx {
19795 st[e] += 1;
19796 }
19797 }
19798 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
19799 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
19800 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
19801 for (di, mi) in d.iter_mut().zip(&mo) {
19802 *di += mi;
19803 }
19804 d
19805}
19806
19807fn moe_gpu_refused(why: &'static str) {
19814 use std::sync::atomic::{AtomicBool, Ordering};
19815 static SAID: AtomicBool = AtomicBool::new(false);
19816 if !SAID.swap(true, Ordering::Relaxed) {
19817 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
19818 }
19819}
19820
19821fn moe_ffn_gpu(
19822 m: &MoeFfn,
19823 x: &[f32],
19824 idx: &[usize],
19825 p: &[f32],
19826 wsum: f32,
19827 pool: Option<&Pool>,
19828) -> Option<Vec<f32>> {
19829 use crate::gpu::MoeJob;
19830
19831 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
19832 let mut model_ref = None;
19833 for &e in idx {
19834 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
19835 moe_gpu_refused("push_job(expert)");
19836 return None;
19837 }
19838 }
19839 if let Some((se, gate)) = &m.shared {
19840 let g = gate.as_ref().map_or(1.0, |gate| {
19841 let mut gl = [0.0f32; 1];
19842 gate.matvec(x, &mut gl, pool);
19843 1.0 / (1.0 + (-gl[0]).exp())
19844 });
19845 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
19846 moe_gpu_refused("push_job(shared)");
19847 return None;
19848 }
19849 }
19850 let Some(model) = model_ref else {
19851 moe_gpu_refused("no model_ref");
19852 return None;
19853 };
19854 let hidden = jobs[0].down.1;
19855 let mut out = vec![0.0f32; hidden];
19856 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19857 Some(out)
19858 } else {
19859 moe_gpu_refused("gpu::moe_block");
19860 None
19861 }
19862}
19863
19864fn ffn_forward(
19866 ffn: &FfnKind,
19867 x: &[f32],
19868 pool: Option<&Pool>,
19869 experts_allowed: Option<&[bool]>,
19870) -> Vec<f32> {
19871 match ffn {
19872 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
19873 FfnKind::Dense(d) => dense_ffn(d, x, pool),
19874 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
19875 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19879 }
19880}
19881
19882fn ffn_forward_pair(
19886 ffn: &FfnKind,
19887 x1: &[f32],
19888 x2: &[f32],
19889 pool: Option<&Pool>,
19890 experts_allowed: Option<&[bool]>,
19891) -> (Vec<f32>, Vec<f32>) {
19892 let d = match ffn {
19893 FfnKind::Dense(d) if !d.segs.is_empty() => {
19896 return (
19897 tube_ffn(d, x1, 1, pool, None),
19898 tube_ffn(d, x2, 1, pool, None),
19899 );
19900 }
19901 FfnKind::Dense(d) => d,
19902 FfnKind::Moe(m) => {
19903 return (
19904 moe_ffn(m, x1, pool, experts_allowed),
19905 moe_ffn(m, x2, pool, experts_allowed),
19906 );
19907 }
19908 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19909 };
19910 let inter = d.gate_proj.rows();
19911 FFN_SCRATCH.with(|s| {
19912 let mut s = s.borrow_mut();
19913 let [g1, g2, u1, u2] = &mut *s;
19914 g1.resize(inter, 0.0);
19915 g2.resize(inter, 0.0);
19916 u1.resize(inter, 0.0);
19917 u2.resize(inter, 0.0);
19918 QTensor::matvec2_many(
19921 [&d.gate_proj, &d.up_proj],
19922 x1,
19923 x2,
19924 [g1.as_mut_slice(), u1.as_mut_slice()],
19925 [g2.as_mut_slice(), u2.as_mut_slice()],
19926 pool,
19927 );
19928 for i in 0..inter {
19929 g1[i] = d.act.combine(g1[i], u1[i]);
19930 g2[i] = d.act.combine(g2[i], u2[i]);
19931 }
19932 let mut o1 = attention::take_buf(d.down_proj.rows());
19933 let mut o2 = attention::take_buf(d.down_proj.rows());
19934 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
19935 (o1, o2)
19936 })
19937}
19938
19939#[cfg(test)]
19940mod tests {
19941
19942 #[test]
19947 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
19948 use super::{
19949 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
19950 };
19951 let dense_card = ChunkStackFacts {
19952 plain_dense: true,
19953 discrete: true,
19954 gpu_on: true,
19955 ..Default::default()
19956 };
19957 assert!(dense_card.dense_on_discrete());
19958 assert_eq!(
19960 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
19961 DISCRETE_DENSE_PREFILL_CHUNK
19962 );
19963 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
19964 for (label, facts) in [
19965 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
19966 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
19967 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
19968 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
19969 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
19970 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
19971 ] {
19972 assert!(!facts.dense_on_discrete(), "{label}");
19973 assert_eq!(
19974 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
19975 48,
19976 "{label} keeps the historical x86 chunk"
19977 );
19978 }
19979 for dense in [false, true] {
19981 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
19982 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
19983 }
19984 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
19986 for dense in [false, true] {
19987 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
19988 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
19989 }
19990 }
19991 }
19992
19993 #[test]
19994 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
19995 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
19996 let full = |host_rows, device_rows| ReuseLayer {
19997 full: true,
19998 host_rows,
19999 device_rows,
20000 device_state: false,
20001 };
20002 assert_eq!(
20005 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
20006 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
20007 );
20008 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
20010 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
20012 assert_eq!(
20014 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
20015 ReusePlan::Pull(vec![(0, 300, 339)])
20016 );
20017 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
20019 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
20020 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
20021 let conv = |device_state| ReuseLayer {
20024 full: false,
20025 host_rows: 0,
20026 device_rows: None,
20027 device_state,
20028 };
20029 assert_eq!(
20030 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
20031 ReusePlan::Fresh
20032 );
20033 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
20034 }
20035
20036 #[test]
20037 fn nll_graph_policy_scopes_only_the_fused_head() {
20038 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
20039 ("vulkan graph", true, true, false, true, false),
20041 ("native Metal graph", true, true, true, true, true),
20043 ("masked", false, true, false, false, false),
20045 ("graph disabled", true, false, true, false, false),
20046 ] {
20047 let (graph_quality, graph_head_required) =
20048 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
20049 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
20050 assert_eq!(graph_head_required, want_head, "{label}: fused head");
20051 }
20052 }
20053
20054 #[test]
20055 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
20056 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
20057 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
20058 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
20059 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
20060 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
20061 }
20062
20063 #[test]
20064 fn cancel_flag_stops_generation() {
20065 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
20066 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20069 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
20070 assert_eq!(r.finish_reason, "cancelled");
20071 assert!(
20072 r.token_ids.is_empty(),
20073 "no tokens after cancel: {:?}",
20074 r.token_ids
20075 );
20076 assert_eq!(p.kv_cache.seq_len(), 0);
20077 assert!(p.kv_history.is_empty());
20078 assert!(!p.graph_want_logits);
20079 assert!(p.graph_logits.is_none());
20080 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
20082 assert_ne!(r2.finish_reason, "cancelled");
20083 }
20084 use super::*;
20085
20086 #[test]
20087 fn explicit_sliding_schedule_selects_local_rope_profile() {
20088 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
20093 p.swa = Some((1024, usize::MAX));
20094 p.sliding_layers = Some(vec![true, true, true, false]);
20095 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
20096 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
20097 p.rope_scale = 1.277_258_9;
20098 p.rope_scale_local = 1.0;
20099
20100 for li in 0..3 {
20101 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
20102 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
20103 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
20104 }
20105 assert_eq!(p.layer_window(3), None);
20106 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
20107 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
20108 }
20109
20110 #[test]
20118 fn dynamic_ffn_equals_the_zeroing_arm() {
20119 let (hidden, inter) = (8usize, 32usize);
20120 let synth = |n: usize, salt: usize| -> Vec<f32> {
20121 (0..n)
20122 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
20123 .collect()
20124 };
20125 let down = synth(hidden * inter, 3);
20126 let mut down_t = vec![0.0f32; inter * hidden];
20127 for r in 0..hidden {
20128 for c in 0..inter {
20129 down_t[c * hidden + r] = down[r * inter + c];
20130 }
20131 }
20132 let d = DenseFfn {
20133 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20134 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20135 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
20136 act: Act::Silu,
20137 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
20138 segs: Vec::new(),
20139 };
20140 let x = synth(hidden, 11);
20141 let k = 12usize;
20142 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
20143 let mut g = vec![0.0f32; inter];
20145 d.gate_proj.matvec(&x, &mut g, None);
20146 let mut u = vec![0.0f32; inter];
20147 d.up_proj.matvec(&x, &mut u, None);
20148 for v in g.iter_mut() {
20149 *v = inference::silu(*v);
20150 }
20151 keep_top_k(&mut g, k);
20152 for i in 0..inter {
20153 g[i] *= u[i];
20154 }
20155 let mut want = vec![0.0f32; hidden];
20156 d.down_proj.matvec(&g, &mut want, None);
20157 for (a, b) in want.iter().zip(&got) {
20158 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
20159 }
20160 }
20161
20162 #[test]
20168 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
20169 let (hidden, core, tube) = (8usize, 12usize, 8usize);
20170 let inter = core + tube;
20171 let synth = |n: usize, salt: usize| -> Vec<f32> {
20172 (0..n)
20173 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
20174 .collect()
20175 };
20176 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
20177 let d_all = synth(hidden * inter, 3);
20178 let dense = DenseFfn {
20180 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
20181 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
20182 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
20183 act: Act::Silu,
20184 down_t: None,
20185 segs: Vec::new(),
20186 };
20187 let rows =
20188 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
20189 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
20190 let mut o = Vec::with_capacity(hidden * (b - a));
20191 for r in 0..hidden {
20192 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
20193 }
20194 o
20195 };
20196 let tubed = DenseFfn {
20197 down_t: None,
20198 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
20199 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
20200 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
20201 act: Act::Silu,
20202 segs: vec![FfnSeg {
20203 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
20204 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
20205 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
20206 start: core,
20207 width: tube,
20208 }],
20209 };
20210 let x = synth(hidden, 7);
20211 let want = dense_ffn(&dense, &x, None);
20212 let got = tube_ffn(&tubed, &x, 1, None, None);
20213 for (a, b) in want.iter().zip(&got) {
20214 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
20215 }
20216 let mut bits = vec![0u8; inter.div_ceil(8)];
20218 for n in 0..core {
20219 bits[n / 8] |= 1 << (n % 8);
20220 }
20221 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20222 let masked = dense_ffn_masked(&dense, &x, None, &bits);
20223 for (a, b) in masked.iter().zip(&closed) {
20224 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
20225 }
20226 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20228 for (a, b) in closed.iter().zip(&batch) {
20229 assert_eq!(a, b, "batch arm disagrees with decode arm");
20230 }
20231 }
20232
20233 #[test]
20235 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
20236 let (hidden, inter) = (16usize, 40usize);
20237 let synth = |n: usize, salt: usize| -> Vec<f32> {
20238 (0..n)
20239 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
20240 .collect()
20241 };
20242 let d = DenseFfn {
20243 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20244 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20245 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
20246 act: Act::Silu,
20247 down_t: None,
20248 segs: Vec::new(),
20249 };
20250 let x = synth(hidden, 9);
20251 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
20253
20254 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
20255
20256 let mut g = vec![0.0f32; inter];
20258 d.gate_proj.matvec(&x, &mut g, None);
20259 let mut u = vec![0.0f32; inter];
20260 d.up_proj.matvec(&x, &mut u, None);
20261 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
20262 for i in 0..inter {
20263 g[i] = if act_set.contains(&(i as u16)) {
20264 inference::silu(g[i]) * u[i]
20265 } else {
20266 0.0
20267 };
20268 }
20269 let mut reference = vec![0.0f32; hidden];
20270 d.down_proj.matvec(&g, &mut reference, None);
20271
20272 let max_d = sparse
20273 .iter()
20274 .zip(&reference)
20275 .map(|(a, b)| (a - b).abs())
20276 .fold(0.0f32, f32::max);
20277 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
20278 }
20279
20280 fn attach_test_mtp(p: &mut Pipeline) {
20282 let (h, inter, heads, kv, hd) = (
20283 p.hidden_size,
20284 p.intermediate_size,
20285 p.num_heads,
20286 p.num_kv_heads,
20287 p.head_dim,
20288 );
20289 let synth = |n: usize, salt: usize| -> Vec<f32> {
20290 (0..n)
20291 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
20292 .collect()
20293 };
20294 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
20295 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20296 };
20297 p.mtp = Some(MtpModule {
20298 enorm: vec![1.0; h],
20299 hnorm: vec![1.0; h],
20300 eh_proj: qt(h, 2 * h, 301),
20301 layer: LayerWeights {
20302 input_norm: vec![1.0; h],
20303 post_norm: vec![1.0; h],
20304 attn_out_norm: None,
20305 ffn_out_norm: None,
20306 layer_scale: None,
20307 ffn: FfnKind::Dense(DenseFfn {
20308 gate_proj: qt(inter, h, 315),
20309 up_proj: qt(inter, h, 316),
20310 down_proj: qt(h, inter, 317),
20311 act: Act::Silu,
20312 down_t: None,
20313 segs: Vec::new(),
20314 }),
20315 attn: AttnKind::Full {
20316 bias: None,
20317 wq: qt(heads * hd, h, 311),
20318 wk: qt(kv * hd, h, 312),
20319 wv: qt(kv * hd, h, 313),
20320 wo: qt(h, heads * hd, 314),
20321 q_norm: None,
20322 k_norm: None,
20323 output_gate: false,
20324 softplus_gate: None,
20325 },
20326 },
20327 final_norm: vec![1.0; h],
20328 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
20329 });
20330 }
20331
20332 #[test]
20333 fn speculative_equals_vanilla_greedy() {
20334 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20338 let run = |spec: bool| {
20339 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20340 p.sampler_config.temperature = 0.0;
20341 attach_test_mtp(&mut p);
20342 p.speculative = spec;
20343 let r = p.generate("abcdef", 12, None, None).unwrap();
20344 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
20345 };
20346 let (vanilla, d0, _) = run(false);
20347 let (spec, d1, a1) = run(true);
20348 assert_eq!(d0, 0, "vanilla path must not draft");
20349 assert!(d1 > 0, "speculative path must draft");
20350 assert_eq!(
20351 vanilla, spec,
20352 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
20353 );
20354 }
20355
20356 #[test]
20357 fn speculative_accepts_constant_oracle() {
20358 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20360 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20361 p.sampler_config.temperature = 0.0;
20362 p.sampler_config.repetition_penalty = 1.0;
20363 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
20366 attach_test_mtp(&mut p);
20367 p.speculative = true;
20368 let r = p.generate("abcd", 10, None, None).unwrap();
20369 assert!(r.mtp_drafted > 0);
20370 assert_eq!(
20371 r.mtp_accepted, r.mtp_drafted,
20372 "constant logits → every draft accepted"
20373 );
20374 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20377 }
20378
20379 #[test]
20380 fn empty_prompt_is_an_error_not_a_panic() {
20381 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20382 let r = p.generate("", 4, None, None);
20383 assert!(r.is_err(), "empty prompt must be a clean error");
20384 }
20385
20386 #[test]
20387 fn every_token_enters_kv_exactly_once() {
20388 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20389 p.sampler_config.temperature = 0.0;
20391 let r = p.generate("abc", 2, None, None).unwrap();
20392 assert_eq!(r.prompt_tokens, 3);
20393 assert_eq!(
20397 p.kv_cache.seq_len(),
20398 3 + r.tokens_generated - 1,
20399 "each token must be cached exactly once (v1 cached the last prompt token twice)"
20400 );
20401 }
20402
20403 #[test]
20404 fn generation_is_reproducible_with_seed() {
20405 let run = || {
20406 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20407 p.generate("hello", 8, None, None).unwrap().token_ids
20408 };
20409 assert_eq!(run(), run());
20410 }
20411
20412 #[test]
20413 fn resetting_sampler_restarts_the_seeded_stream() {
20414 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20415 let config = SamplerConfig {
20416 seed: Some(1234),
20417 ..SamplerConfig::default()
20418 };
20419 p.set_sampler_config(config.clone());
20420 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20421 p.set_sampler_config(config);
20422 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20423 assert_eq!(first, second);
20424 }
20425
20426 #[test]
20427 fn eviction_bounds_the_cache() {
20428 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20429 p.kv_cache.max_seq_len = 6;
20430 p.sampler_config.temperature = 0.0;
20431 let _ = p.generate("abcd", 12, None, None).unwrap();
20432 assert!(
20433 p.kv_cache.seq_len() <= 6 + 1,
20434 "cache must stay bounded by max_seq_len (got {})",
20435 p.kv_cache.seq_len()
20436 );
20437 }
20438
20439 #[test]
20440 fn confidence_matches_tokens_and_is_a_probability() {
20441 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20442 p.sampler_config.temperature = 0.0;
20443 p.sampler_config.repetition_penalty = 1.0;
20444 let r = p.generate("abcd", 10, None, None).unwrap();
20445 assert_eq!(
20446 r.token_confidence.len(),
20447 r.token_ids.len(),
20448 "one confidence per emitted token"
20449 );
20450 for &c in &r.token_confidence {
20451 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20452 }
20453 let logits = [1.0f32, 3.0, 0.5, 3.0];
20455 let p0 = top1_prob_t(&logits, 1, 1.0);
20456 let p1 = top1_prob_t(&logits, 3, 1.0);
20457 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20458 assert!(p0 > 0.0 && p0 < 1.0);
20459 let sharp = top1_prob_t(&logits, 1, 1.0);
20461 let soft = top1_prob_t(&logits, 1, 2.0);
20462 assert!(soft < sharp, "higher temperature lowers peak confidence");
20463 }
20464
20465 #[test]
20466 fn trace_is_opt_in_and_parallels_the_output() {
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 let r = p.generate("abcd", 10, None, None).unwrap();
20472 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20473
20474 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20476 p.sampler_config.temperature = 0.0;
20477 p.sampler_config.repetition_penalty = 1.0;
20478 p.set_trace(true);
20479 let r = p.generate("abcd", 10, None, None).unwrap();
20480 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20481 for (i, tr) in r.traces.iter().enumerate() {
20482 assert_eq!(tr.t, i, "trace index is sequential");
20483 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20484 assert_eq!(
20485 tr.confidence, r.token_confidence[i],
20486 "trace confidence matches the confidence channel"
20487 );
20488 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20490 }
20491 }
20492
20493 #[test]
20494 fn explain_prefill_logits_match_greedy_first_token() {
20495 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20499 p.sampler_config.temperature = 0.0;
20500 p.sampler_config.repetition_penalty = 1.0;
20501 let ids = p.tokenizer.encode("abcd");
20502 let logits = p.prefill_next_logits(&ids, None);
20503 let argmax = logits
20504 .iter()
20505 .enumerate()
20506 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20507 .unwrap()
20508 .0 as u32;
20509 let r = p.generate("abcd", 1, None, None).unwrap();
20510 assert_eq!(
20511 argmax, r.token_ids[0],
20512 "explain preview must match greedy emit"
20513 );
20514 }
20515
20516 #[test]
20517 fn laguna_shared_expert_is_unconditionally_added() {
20518 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20519 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20520 let zero_dense = || DenseFfn {
20521 gate_proj: matrix(vec![0.0; 4]),
20522 up_proj: matrix(vec![0.0; 4]),
20523 down_proj: matrix(vec![0.0; 4]),
20524 act: Act::Silu,
20525 down_t: None,
20526 segs: Vec::new(),
20527 };
20528 let shared = DenseFfn {
20529 gate_proj: identity(),
20530 up_proj: identity(),
20531 down_proj: identity(),
20532 act: Act::Silu,
20533 down_t: None,
20534 segs: Vec::new(),
20535 };
20536 let x = [1.0, 2.0];
20537 let expected = dense_ffn(&shared, &x, None);
20538 let moe = MoeFfn {
20539 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20540 experts: vec![zero_dense()],
20541 top_k: 1,
20542 norm_topk_prob: true,
20543 router_sigmoid: true,
20544 expert_bias: None,
20545 routed_scaling: 1.0,
20546 route_tau: None,
20547 shared: Some((shared, None)),
20548 stats: std::cell::RefCell::new(Vec::new()),
20549 act_sq: std::cell::RefCell::new(Vec::new()),
20550 act_rows: std::cell::RefCell::new(Vec::new()),
20551 mask: None,
20552 per_expert_scale: None,
20553 router_input_norm: false,
20554 resonance: None,
20555 grown: Vec::new(),
20556 };
20557 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
20558 for (actual, expected) in actual.iter().zip(expected) {
20559 assert!((actual - expected).abs() < 1e-6);
20560 }
20561 }
20562
20563 fn mimo_test_pipeline() -> Pipeline {
20572 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
20573 let kvh = [1usize, 2, 2, 1];
20574 let synth = |n: usize, salt: usize| -> Vec<f32> {
20575 (0..n)
20576 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20577 .collect()
20578 };
20579 let qt = |rows: usize, cols: usize, salt: usize| {
20580 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20581 };
20582 let dense = |inter: usize, salt: usize| DenseFfn {
20583 gate_proj: qt(inter, hs, salt),
20584 up_proj: qt(inter, hs, salt + 1),
20585 down_proj: qt(hs, inter, salt + 2),
20586 act: Act::Silu,
20587 down_t: None,
20588 segs: Vec::new(),
20589 };
20590 let layers: Vec<LayerWeights> = (0..4)
20591 .map(|li| LayerWeights {
20592 input_norm: vec![1.0; hs],
20593 post_norm: vec![1.0; hs],
20594 attn_out_norm: None,
20595 ffn_out_norm: None,
20596 layer_scale: None,
20597 ffn: if li == 0 {
20598 FfnKind::Dense(dense(inter, 50))
20599 } else {
20600 FfnKind::Moe(MoeFfn {
20601 router: qt(4, hs, 60 + li),
20602 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
20603 top_k: 2,
20604 norm_topk_prob: true,
20605 router_sigmoid: true,
20606 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
20607 routed_scaling: 1.0,
20608 route_tau: None,
20609 shared: None,
20610 stats: std::cell::RefCell::new(Vec::new()),
20611 act_sq: std::cell::RefCell::new(Vec::new()),
20612 act_rows: std::cell::RefCell::new(Vec::new()),
20613 mask: None,
20614 per_expert_scale: None,
20615 router_input_norm: false,
20616 resonance: None,
20617 grown: Vec::new(),
20618 })
20619 },
20620 attn: AttnKind::Full {
20621 wq: qt(nh * hd, hs, li * 10 + 1),
20622 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
20623 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
20624 wo: qt(hs, nh * vd, li * 10 + 4),
20625 q_norm: None,
20626 k_norm: None,
20627 output_gate: false,
20628 softplus_gate: None,
20629 bias: None,
20630 },
20631 })
20632 .collect();
20633 let mut p = Pipeline::new(
20634 Tokenizer::byte_level(),
20635 PipelineWeights {
20636 embed_tokens: qt(vocab, hs, 100),
20637 layers,
20638 lm_head: qt(vocab, hs, 200),
20639 final_norm: vec![1.0; hs],
20640 },
20641 hs,
20642 inter,
20643 nh,
20644 1, hd,
20646 4,
20647 4,
20648 false,
20649 vocab,
20650 1e-6,
20651 1e7,
20652 NormStyle::Qwen,
20653 4096,
20654 SamplerConfig {
20655 seed: Some(7),
20656 ..Default::default()
20657 },
20658 );
20659 p.layer_dump = None;
20661 p.set_rotary(4, 1e7);
20662 p.sliding_layers = Some(vec![false, true, true, false]);
20663 p.swa = Some((3, usize::MAX));
20664 p.rotary_dim_local = Some(4);
20665 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
20666 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
20667 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
20668 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
20669 p
20670 }
20671
20672 #[test]
20673 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
20674 let mut p = mimo_test_pipeline();
20675 p.speculative = false;
20676 p.ignore_eos = true;
20677 p.sampler_config.temperature = 0.0;
20678 p.sampler_config.repetition_penalty = 1.0;
20679 let a = vec![3, 5, 7, 9, 11, 13];
20680 let b = vec![4, 8, 12, 16, 20, 24];
20681 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
20682 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
20683 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
20687 assert_eq!(actual, expected);
20688 assert!(p.kv_history.is_empty());
20689 let mut extended = a.clone();
20690 extended.push(17);
20691 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20692 p.reset_session();
20693 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20694 assert_eq!(after_media, fresh);
20695 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
20696 p.reset_session();
20700 p.generate_from_ids(&a, 1, None, None).unwrap();
20701 let mut media_ids = p.kv_history.clone();
20702 assert!(!media_ids.is_empty());
20703 media_ids.extend_from_slice(&[19, 21, 23]);
20704 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
20705 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
20706 let mut oracle = mimo_test_pipeline();
20707 oracle.speculative = false;
20708 oracle.ignore_eos = true;
20709 oracle.sampler_config.temperature = 0.0;
20710 oracle.sampler_config.repetition_penalty = 1.0;
20711 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
20712 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
20713 assert!(p.kv_history.is_empty());
20714 let mut bad = rows;
20715 bad[0] = f32::NAN;
20716 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
20717 }
20718
20719 fn f32_bits(v: &[f32]) -> Vec<u32> {
20720 v.iter().map(|x| x.to_bits()).collect()
20721 }
20722
20723 #[test]
20730 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
20731 let mut p = mimo_test_pipeline();
20732 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
20733 assert_eq!(kv, vec![1, 2, 2, 1]);
20734 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
20735 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
20736 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
20737 let hs = p.hidden_size;
20738 let mut decode = Vec::new();
20739 for (pos, &id) in ids.iter().enumerate() {
20740 let e = p.embed_single(id);
20741 let h = p.forward_layers(&e, pos, None);
20742 decode.push(p.logits_from_hidden(&h));
20743 }
20744 for l in &p.kv_cache.layers {
20745 assert_eq!(l.seq_len, 12);
20746 assert_eq!(l.head_values(0).len(), 12 * 8);
20748 }
20749 assert!(decode.iter().flatten().all(|v| v.is_finite()));
20750
20751 p.clear_sequence_state();
20752 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20753 for pos in 0..ids.len() {
20754 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
20755 assert_eq!(
20756 f32_bits(&decode[pos]),
20757 f32_bits(&lg),
20758 "whole prompt, pos {pos}"
20759 );
20760 }
20761
20762 p.clear_sequence_state();
20763 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
20764 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
20765 for pos in 0..ids.len() {
20766 let row = if pos < 5 {
20767 &a[pos * hs..(pos + 1) * hs]
20768 } else {
20769 &b[(pos - 5) * hs..(pos - 4) * hs]
20770 };
20771 let lg = p.logits_from_hidden(row);
20772 assert_eq!(
20773 f32_bits(&decode[pos]),
20774 f32_bits(&lg),
20775 "two chunks, pos {pos}"
20776 );
20777 }
20778
20779 let last = |p: &mut Pipeline| {
20782 p.clear_sequence_state();
20783 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20784 p.logits_from_hidden(&hb[11 * hs..12 * hs])
20785 };
20786 let base = last(&mut p);
20787 let mut no_sinks = mimo_test_pipeline();
20788 for l in &mut no_sinks.kv_cache.layers {
20789 l.sinks = None;
20790 }
20791 assert_ne!(
20792 f32_bits(&last(&mut no_sinks)),
20793 f32_bits(&base),
20794 "sinks are live"
20795 );
20796 let mut wide = mimo_test_pipeline();
20797 wide.swa = Some((64, usize::MAX));
20798 assert_ne!(
20799 f32_bits(&last(&mut wide)),
20800 f32_bits(&base),
20801 "window is live"
20802 );
20803
20804 p.clear_sequence_state();
20806 p.ignore_eos = true;
20807 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
20808 assert_eq!(r.token_ids.len(), 4);
20809 }
20810
20811 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
20814 let (hs, nh) = (16usize, 4usize);
20815 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
20816 p.layer_dump = None;
20817 p.swa = Some((6, 4));
20818 p.proj_gate_sigmoid = true;
20819 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
20820 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
20821 let g: Vec<f32> = (0..nh * hs)
20822 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
20823 .collect();
20824 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
20825 }
20826 }
20827 p.swa_trim = trim;
20828 p
20829 }
20830
20831 #[test]
20837 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
20838 let mut a = spark_test_pipeline(None);
20839 let mut b = spark_test_pipeline(Some((2, 4)));
20840 assert_eq!(
20841 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
20842 vec![Some(6), Some(6), Some(6), None]
20843 );
20844 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
20845 let mut pos = 0usize;
20846 for &n in [5usize, 13, 7, 16].iter().cycle() {
20847 if pos >= ids.len() {
20848 break;
20849 }
20850 let end = (pos + n).min(ids.len());
20851 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20852 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20853 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
20854 pos = end;
20855 }
20856 for _ in 0..23 {
20857 let e = a.embed_single(((pos * 7) % 64) as u32);
20858 let ha = a.forward_layers(&e, pos, None);
20859 let hb = b.forward_layers(&e, pos, None);
20860 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
20861 for li in 0..3 {
20862 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
20863 }
20864 pos += 1;
20865 }
20866 for round in 0..9 {
20867 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
20868 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
20869 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
20870 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
20871 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
20872 if round % 2 == 1 {
20873 for p in [&mut a, &mut b] {
20875 for l in &mut p.kv_cache.layers {
20876 l.truncate_last(2);
20877 }
20878 }
20879 } else {
20880 pos += 2;
20881 }
20882 }
20883 let e = a.embed_single(9);
20884 let (ha, hb) = (
20885 a.forward_layers(&e, pos, None),
20886 b.forward_layers(&e, pos, None),
20887 );
20888 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
20889 for li in 0..3 {
20890 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
20891 assert!(lb.base() > 0, "layer {li} trimmed");
20892 assert_eq!(la.base(), 0);
20893 assert_eq!(lb.pos_len(), la.seq_len);
20894 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
20895 }
20896 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
20897 assert_eq!(
20898 (gb.base(), gb.seq_len),
20899 (0, ga.seq_len),
20900 "the global layer keeps all"
20901 );
20902 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
20903 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
20904 }
20905
20906 #[test]
20911 fn swa_trim_wire_handoff_continues_bitwise() {
20912 let mut a = spark_test_pipeline(None);
20913 let mut b = spark_test_pipeline(Some((2, 4)));
20914 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
20915 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20916 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20917 assert_eq!(f32_bits(&ha), f32_bits(&hb));
20918 let mut pos = ids.len();
20919 for _ in 0..5 {
20920 let e = a.embed_single(((pos * 5) % 64) as u32);
20921 assert_eq!(
20922 f32_bits(&a.forward_layers(&e, pos, None)),
20923 f32_bits(&b.forward_layers(&e, pos, None))
20924 );
20925 pos += 1;
20926 }
20927 let mut c = spark_test_pipeline(Some((2, 4)));
20928 for li in 0..4 {
20929 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
20930 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
20931 }
20932 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
20933 assert_eq!(c.kv_cache.seq_len(), pos);
20934 for step in 0..17 {
20935 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
20936 let ha = a.forward_layers(&e, pos, None);
20937 let hc = c.forward_layers(&e, pos, None);
20938 assert_eq!(
20939 f32_bits(&ha),
20940 f32_bits(&hc),
20941 "after the hand-off, step {step}"
20942 );
20943 pos += 1;
20944 }
20945 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
20946 }
20947
20948 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
20951 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
20952 let synth = |len: usize, salt: usize| -> Vec<f32> {
20953 (0..len)
20954 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
20955 .collect()
20956 };
20957 let qt = |rows: usize, cols: usize, salt: usize| {
20958 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20959 };
20960 let layers = (0..n)
20961 .map(|k| {
20962 let s = 500 + k * 40;
20963 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
20964 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
20965 MtpModule {
20966 enorm: vec![1.0; hs],
20967 hnorm: vec![1.0; hs],
20968 eh_proj: qt(hs, 2 * hs, s),
20969 layer: LayerWeights {
20970 input_norm: vec![1.0; hs],
20971 post_norm: vec![1.0; hs],
20972 attn_out_norm: None,
20973 ffn_out_norm: None,
20974 layer_scale: None,
20975 attn: AttnKind::Full {
20976 wq: qt(nh * hd, hs, s + 1),
20977 wk: qt(nkv * hd, hs, s + 2),
20978 wv: qt(nkv * vd, hs, s + 3),
20979 wo: qt(hs, nh * vd, s + 4),
20980 q_norm: None,
20981 k_norm: None,
20982 output_gate: false,
20983 softplus_gate: None,
20984 bias: None,
20985 },
20986 ffn: FfnKind::Dense(DenseFfn {
20987 gate_proj: qt(inter, hs, s + 5),
20988 up_proj: qt(inter, hs, s + 6),
20989 down_proj: qt(hs, inter, s + 7),
20990 act: Act::Silu,
20991 down_t: None,
20992 segs: Vec::new(),
20993 }),
20994 },
20995 final_norm: vec![1.0; hs],
20996 kv,
20997 }
20998 })
20999 .collect();
21000 mimo_mtp::MimoMtp::from_layers(layers)
21001 }
21002
21003 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
21004 p.clear_sequence_state();
21005 p.speculative = spec;
21006 p.ignore_eos = true;
21007 p.sampler_config.temperature = 0.0;
21008 p.generate_from_ids(ids, n, None, None).unwrap()
21009 }
21010
21011 #[test]
21017 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
21018 for post in [false, true] {
21021 let mut p = mimo_test_pipeline();
21022 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
21024 let mut st0 = mimo_test_mtp(3, 1.0);
21025 st0.post_norm_hidden = post;
21026 p.mimo_mtp = Some(st0);
21027 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
21028 let hs = p.hidden_size;
21029 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21030 p.mimo_note_rows(&hb, 0);
21031 let mut st = p.mimo_mtp.take().unwrap();
21032 let k = 3;
21035 let mut inc = Vec::new();
21036 for t in 0..ids.len() - k - 1 {
21037 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
21038 }
21039 let s = ids.len();
21042 let mut reference = vec![vec![0u32; k]; s - k - 1];
21043 let mut fresh = mimo_test_mtp(3, 1.0);
21044 for (layer, m) in fresh.layers.iter_mut().enumerate() {
21045 let n = s - layer - 1;
21046 let mut cats = vec![0.0f32; n * 2 * hs];
21047 for j in 0..n {
21048 let e = p.embed_single(ids[j + layer + 1]);
21049 let raw = &hb[j * hs..(j + 1) * hs];
21050 let g = if post {
21051 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
21052 } else {
21053 raw.to_vec()
21054 };
21055 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
21056 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
21057 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
21058 }
21059 let mut x = vec![0.0f32; n * hs];
21060 m.eh_proj.matmat(&cats, n, &mut x, None);
21061 p.mimo_mtp_block(m, &mut x, n, 0);
21062 for (t, row) in reference.iter_mut().enumerate() {
21063 let y = inference::rms_norm(
21064 &x[t * hs..(t + 1) * hs],
21065 &m.final_norm,
21066 p.rms_eps,
21067 p.norm_style,
21068 );
21069 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
21070 }
21071 }
21072 assert_eq!(inc, reference, "post_norm_hidden = {post}");
21073 let distinct: std::collections::HashSet<u32> =
21075 inc.iter().flatten().copied().collect();
21076 assert!(distinct.len() > 3, "{inc:?}");
21077 let last_t = ids.len() - k - 2;
21079 for m in &st.layers {
21080 assert_eq!(m.kv.seq_len, last_t + 1);
21081 }
21082 }
21083 }
21084
21085 #[test]
21092 fn mimo_speculative_greedy_equals_plain_greedy() {
21093 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21094 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
21095 let n = 24;
21096 let mut p = mimo_test_pipeline();
21097 let plain = mimo_greedy(&mut p, &ids, n, false);
21098 assert_eq!(plain.mtp_drafted, 0);
21099 assert_eq!(plain.token_ids.len(), n);
21100 let plain_kv = p.kv_cache.layers[0].seq_len;
21101
21102 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
21104 let spec = mimo_greedy(&mut p, &ids, n, true);
21105 assert!(spec.mtp_drafted > 0, "the round must draft");
21106 assert_eq!(spec.token_ids, plain.token_ids);
21107 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21108
21109 let mut truth: Vec<u32> = ids.clone();
21112 truth.extend(&plain.token_ids);
21113 let mut noisy = truth.clone();
21114 for (i, t) in noisy.iter_mut().enumerate() {
21115 if i % 5 == 0 {
21116 *t = (*t + 1) % 64;
21117 }
21118 }
21119 let mut st = mimo_test_mtp(3, 1.0);
21120 st.draft_override = Some(noisy);
21121 p.mimo_mtp = Some(st);
21122 let spec = mimo_greedy(&mut p, &ids, n, true);
21123 assert_eq!(spec.token_ids, plain.token_ids);
21124 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21125 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
21126 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
21127 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
21128 assert!(
21129 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
21130 "{:?}",
21131 stats.accept_hist
21132 );
21133 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
21134
21135 let mut st = mimo_test_mtp(3, 1.0);
21138 st.draft_override = Some(truth);
21139 p.mimo_mtp = Some(st);
21140 let spec = mimo_greedy(&mut p, &ids, n, true);
21141 assert_eq!(spec.token_ids, plain.token_ids);
21142 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
21143 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21144
21145 let off = mimo_greedy(&mut p, &ids, n, false);
21147 assert_eq!(off.token_ids, plain.token_ids);
21148 assert_eq!(off.mtp_drafted, 0);
21149 }
21150
21151 #[test]
21160 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
21161 let p = mimo_test_pipeline();
21162 assert_eq!(
21163 p.graph_attn_decline_reason(),
21164 Some("per-layer KV head counts")
21165 );
21166 assert_eq!(p.wgpu_graph_attn_decline(), None);
21167 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
21168 assert_eq!(
21169 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
21170 (1, 4, 4, None, false)
21171 );
21172 assert_eq!(g0.invf, p.inv_freq.as_slice());
21173 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
21174 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
21175 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
21176 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
21177 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
21178 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
21179 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
21180
21181 let emb = p.embed_single(3);
21184 let mut lg = Vec::new();
21185 assert!(
21186 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
21187 .is_none()
21188 );
21189 let mut hid = emb.clone();
21190 assert_eq!(
21191 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
21192 crate::gpu::BatchGraphOutcome::Declined
21193 );
21194 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
21195 assert!(p.try_multi_burst(3, 0, 4).is_none());
21196 assert!(
21197 p.graph_declines().is_empty(),
21198 "no attention decline logged: {:?}",
21199 p.graph_declines()
21200 );
21201 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
21206 assert_eq!(plain().graph_attn_decline_reason(), None);
21207 assert_eq!(plain().wgpu_graph_attn_decline(), None);
21208 assert!(
21209 plain().graph_attn_geom(0).is_none(),
21210 "uniform models keep the historical arms"
21211 );
21212 let mut q = plain();
21213 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
21214 assert_eq!(
21215 q.graph_attn_decline_reason(),
21216 Some("learned attention sinks")
21217 );
21218 assert_eq!(
21219 q.graph_attn_geom(1).unwrap().sink,
21220 Some(&[0.25f32, -0.25][..])
21221 );
21222 let mut q = plain();
21223 q.set_attn_geometry(None, Some(2)).unwrap();
21224 assert_eq!(
21225 q.graph_attn_decline_reason(),
21226 Some("V heads narrower than Q/K heads")
21227 );
21228 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
21229 let mut q = plain();
21230 q.sliding_layers = Some(vec![true, false]);
21231 q.swa = Some((4, usize::MAX));
21232 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
21233 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
21234 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
21235
21236 let mut q = mimo_test_pipeline();
21240 q.rope_scale = 2.0;
21241 q.rope_scale_local = 1.25;
21242 assert_eq!(q.wgpu_graph_attn_decline(), None);
21243 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
21244 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
21245
21246 let mut q = plain();
21249 q.rope_scale = 2.0;
21250 assert_eq!(
21251 q.wgpu_graph_attn_decline(),
21252 Some("scaled RoPE without per-layer geometry")
21253 );
21254 }
21255
21256 #[test]
21257 fn mimo_verify_rewind_preserves_lagging_host_caches() {
21258 let mut p = mimo_test_pipeline();
21259 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
21260 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
21261 for _ in 0..if li == 0 { 2 } else { 12 } {
21262 layer.append(&row, &row, &[]);
21263 }
21264 }
21265 p.mimo_verify_rewind(9).unwrap();
21266 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
21267 for layer in &p.kv_cache.layers[1..] {
21268 assert_eq!(layer.seq_len, 9);
21269 }
21270 }
21271
21272 #[test]
21276 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
21277 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
21278 let _ = std::fs::remove_dir_all(&dir);
21279 let mut p = mimo_test_pipeline();
21280 let hs = p.hidden_size;
21281 let ids = [5u32, 9, 11, 2, 40];
21282 p.layer_dump = Some(dir.join("decode"));
21283 for (pos, &id) in ids.iter().enumerate() {
21284 let e = p.embed_single(id);
21285 let _ = p.forward_layers(&e, pos, None);
21286 }
21287 p.clear_sequence_state();
21288 p.layer_dump = Some(dir.join("prefill"));
21289 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21290 for pos in 0..ids.len() {
21291 for li in 0..p.num_layers {
21292 let name = format!("p{pos:06}_l{li:02}.f32");
21293 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
21294 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
21295 assert_eq!(a.len(), hs * 4, "{name}");
21296 assert_eq!(a, b, "{name}");
21297 }
21298 }
21299 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
21300 let vals: Vec<f32> = last
21301 .chunks(4)
21302 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
21303 .collect();
21304 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
21305 let _ = std::fs::remove_dir_all(&dir);
21306 }
21307
21308 #[test]
21309 fn attn_geometry_and_sinks_are_validated() {
21310 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
21311 assert!(
21312 p.set_attn_geometry(Some(vec![2]), None).is_err(),
21313 "one entry per layer"
21314 );
21315 assert!(
21316 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
21317 "3 does not divide 4"
21318 );
21319 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
21320 assert!(p.set_attn_geometry(None, Some(0)).is_err());
21321 assert!(
21322 p.set_attn_geometry(None, Some(5)).is_err(),
21323 "V wider than the head"
21324 );
21325 p.set_attn_geometry(None, Some(4)).unwrap();
21326 assert_eq!(
21327 p.v_head_dim, None,
21328 "v_head_dim == head_dim is the uniform case"
21329 );
21330 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
21331 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
21332 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
21333 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
21334 assert!(
21335 p.kv_cache.layers[1].sinks.is_some(),
21336 "a reshape keeps the layer's sinks"
21337 );
21338 assert_eq!(p.layer_geom(1).0, 4);
21339 assert!(
21340 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
21341 "one sink per Q head"
21342 );
21343 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
21344 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
21345 }
21346
21347 #[test]
21350 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
21351 let cfg = || {
21352 Some(crate::nystrom::O1Cfg {
21353 layers: crate::nystrom::O1Layers::All,
21354 m: 4,
21355 w: 8,
21356 sink: 2,
21357 rect: crate::nystrom::O1Rect::Aggregate,
21358 })
21359 };
21360 let mut p = mimo_test_pipeline();
21361 p.set_o1(cfg());
21362 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
21363 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
21364 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
21365 q.sliding_layers = Some(vec![false, false, true]);
21366 q.swa = Some((4, usize::MAX));
21367 q.set_o1(cfg());
21368 assert_eq!(q.o1_flags, vec![true, false, false]);
21369 }
21370
21371 #[test]
21372 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21373 const B: usize = 19;
21374 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21375 p.set_o1(Some(crate::nystrom::O1Cfg {
21376 layers: crate::nystrom::O1Layers::All,
21377 m: 4,
21378 w: 8,
21379 sink: 2,
21380 rect: crate::nystrom::O1Rect::Aggregate,
21381 }));
21382 p.o1_begin_with_prefix(Some(B));
21383 let ids: Vec<u32> = (0..B as u32).collect();
21384 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21385
21386 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21387 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21388 let next = p.embed_single(B as u32);
21389 let _ = p.forward_layers(&next, B, None);
21390 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21391 }
21392
21393 #[test]
21394 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21395 const B: usize = 19;
21396 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21397 let gdn_cfg = crate::linear_core::GdnCfg {
21401 num_v_heads: 2,
21402 num_k_heads: 1,
21403 key_head_dim: 2,
21404 value_head_dim: 4,
21405 conv_kernel: 3,
21406 hidden_size: 8,
21407 rms_eps: 1e-6,
21408 output_gate_sigmoid: false,
21409 };
21410 let synth = |n: usize, salt: usize| -> Vec<f32> {
21411 (0..n)
21412 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21413 .collect()
21414 };
21415 let qt = |rows: usize, cols: usize, salt: usize| {
21416 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21417 };
21418 let c_dim = gdn_cfg.conv_dim();
21419 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21420 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21421 in_proj_qkv: qt(c_dim, 8, 1),
21422 in_proj_z: qt(vd, 8, 2),
21423 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21424 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21425 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21426 a_log: vec![0.2, 0.5],
21427 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21428 norm: vec![1.0; gdn_cfg.value_head_dim],
21429 out_proj: qt(8, vd, 7),
21430 });
21431 p.gdn_cfg = Some(gdn_cfg);
21432 p.set_o1(Some(crate::nystrom::O1Cfg {
21433 layers: crate::nystrom::O1Layers::All,
21434 m: 4,
21435 w: 8,
21436 sink: 2,
21437 rect: crate::nystrom::O1Rect::Aggregate,
21438 }));
21439 p.o1_begin_with_prefix(Some(B));
21440 for pos in 0..B - 2 {
21441 let emb = p.embed_single(pos as u32);
21442 let _ = p.forward_layers(&emb, pos, None);
21443 }
21444 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21445
21446 let e1 = p.embed_single((B - 2) as u32);
21447 let e2 = p.embed_single((B - 1) as u32);
21448 let _ = p.forward_pair(&e1, &e2, B - 2);
21449
21450 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21451 assert!(
21452 p.kv_cache
21453 .layers
21454 .iter()
21455 .enumerate()
21456 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21457 );
21458 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21459 assert_ne!(
21460 p.kv_cache.layers[0].linear_state, lane1_state,
21461 "real pair must commit GDN lane 2 before returning"
21462 );
21463 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21464 let next = p.embed_single(B as u32);
21465 let _ = p.forward_layers(&next, B, None);
21466 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21467 }
21468
21469 #[test]
21470 fn o1_error_observation_stays_terminal_until_reset() {
21471 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21472 p.set_o1(Some(crate::nystrom::O1Cfg {
21473 layers: crate::nystrom::O1Layers::All,
21474 m: 4,
21475 w: 8,
21476 sink: 2,
21477 rect: crate::nystrom::O1Rect::Aggregate,
21478 }));
21479 p.o1_begin();
21480 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21481
21482 assert!(p.o1_seal_checked().is_err());
21483 assert!(
21484 p.o1_seal_checked().is_err(),
21485 "retry must see the sticky error"
21486 );
21487 let k = vec![0.2f32; 4];
21488 let v = vec![0.3f32; 4];
21489 p.kv_cache.layers[0].append(&k, &v, &[]);
21490 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
21491
21492 p.reset_session();
21493 p.o1_begin();
21494 p.kv_cache.layers[0].append(&k, &v, &[]);
21495 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
21496 }
21497
21498 #[test]
21499 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
21500 let ids = vec![1u32, 2, 3, 4, 5, 6];
21501 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21502 p.graph_logits = Some(vec![123.0]);
21503 p.graph_want_logits = true;
21504 p.graph_failed
21505 .store(true, std::sync::atomic::Ordering::Relaxed);
21506 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21507 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
21508 assert!(err.contains("before NLL"));
21509 assert!(p.graph_logits.is_none());
21510 assert!(!p.graph_want_logits);
21511 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21512 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21513
21514 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21515 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21516 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21517 assert_eq!(actual.1, expected.1);
21518 assert!((actual.0 - expected.0).abs() < 1e-9);
21519 }
21520
21521 #[test]
21522 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
21523 let ids = vec![1u32, 2, 3, 4, 5, 6];
21524 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21525 p.nll_test_fail_at = Some(1);
21526 let err = p
21527 .nll_ids_from(&ids, 0)
21528 .expect_err("one-shot forward failure");
21529 assert!(err.contains("forward") || err.contains("score row"));
21530 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21531 assert!(!p.graph_want_logits);
21532 assert!(p.graph_logits.is_none());
21533 assert!(p.kv_history.is_empty());
21534
21535 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21536 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21537 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21538 assert_eq!(actual.1, expected.1);
21539 assert!((actual.0 - expected.0).abs() < 1e-9);
21540 }
21541
21542 #[test]
21543 fn nll_serial_failure_before_first_row_is_reported() {
21544 let ids = vec![1u32, 2, 3, 4];
21545 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21546 p.nll_test_force_serial = true;
21547 p.nll_test_fail_at = Some(0);
21548 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
21549 assert!(err.contains("serial forward"));
21550 assert!(p.kv_history.is_empty());
21551 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21552 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21553 }
21554
21555 #[test]
21556 fn ffn_probe_failure_discards_recorder_and_state() {
21557 let ids = vec![1u32, 2, 3, 4];
21558 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21559 p.nll_test_fail_at = Some(0);
21560 let err = p
21561 .probe_ffn_mass_batch(&ids)
21562 .expect_err("probe forward failure");
21563 assert!(err.contains("NLL"));
21564 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
21565 assert!(p.kv_history.is_empty());
21566 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21567 }
21568
21569 #[test]
21570 fn nll_test_controls_are_pipeline_scoped() {
21571 let ids = vec![1u32, 2, 3, 4];
21572 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21573 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21574 failing.nll_test_force_serial = true;
21575 failing.nll_test_fail_at = Some(0);
21576
21577 assert!(!failing.can_prefill_batched());
21578 assert!(unaffected.can_prefill_batched());
21579 let expected = unaffected
21580 .nll_ids_from(&ids, 0)
21581 .expect("unaffected pipeline remains usable");
21582 let err = failing
21583 .nll_ids_from(&ids, 0)
21584 .expect_err("failure injection belongs to failing pipeline");
21585 assert!(err.contains("serial forward"));
21586 assert!(failing.nll_test_fail_at.is_none());
21587 assert!(unaffected.can_prefill_batched());
21588 let actual = unaffected
21589 .nll_ids_from(&ids, 0)
21590 .expect("unaffected pipeline remains reusable");
21591 assert_eq!(actual.1, expected.1);
21592 assert!((actual.0 - expected.0).abs() < 1e-9);
21593 }
21594
21595 #[test]
21596 fn forward_ids_failure_channel_is_terminal_and_reusable() {
21597 let ids = vec![1u32, 2, 3, 4, 5, 6];
21598 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21599 p.graph_logits = Some(vec![123.0]);
21600 p.graph_want_logits = true;
21601 p.graph_failed
21602 .store(true, std::sync::atomic::Ordering::Relaxed);
21603 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21604
21605 let err = p
21606 .forward_ids(&ids, None)
21607 .expect_err("a failed forward must not become a valid head result");
21608 assert!(err.contains("forward_ids setup"));
21609 assert!(p.graph_logits.is_none());
21610 assert!(!p.graph_want_logits);
21611 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21612 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21613 assert_eq!(p.kv_cache.seq_len(), 0);
21614
21615 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
21616 .forward_ids(&ids, None)
21617 .expect("fresh forward_ids");
21618 let actual = p
21619 .forward_ids(&ids, None)
21620 .expect("pipeline remains reusable after a failed forward");
21621 assert_eq!(actual.len(), expected.len());
21622 assert!(
21623 actual
21624 .iter()
21625 .zip(expected)
21626 .all(|(a, b)| (a - b).abs() < 1e-9)
21627 );
21628 assert_eq!(p.kv_cache.seq_len(), ids.len());
21629 }
21630
21631 #[test]
21632 fn sigmoid_router_floor_is_explicit_per_architecture() {
21633 let zero = || DenseFfn {
21639 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21640 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21641 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21642 act: Act::Silu,
21643 down_t: None,
21644 segs: Vec::new(),
21645 };
21646 let m = MoeFfn {
21647 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
21648 experts: vec![zero(), zero()],
21649 top_k: 1,
21650 norm_topk_prob: true,
21651 router_sigmoid: true,
21652 expert_bias: None,
21653 routed_scaling: 2.5,
21654 route_tau: None,
21655 shared: None,
21656 stats: std::cell::RefCell::new(Vec::new()),
21657 act_sq: std::cell::RefCell::new(Vec::new()),
21658 act_rows: std::cell::RefCell::new(Vec::new()),
21659 mask: None,
21660 per_expert_scale: None,
21661 router_input_norm: false,
21662 resonance: None,
21663 grown: Vec::new(),
21664 };
21665 let logits = [-20.0f32, -20.0];
21666 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
21667 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
21668 let expected = (p[0] + 1e-20) / m.routed_scaling;
21669 assert!((glm_wsum - expected).abs() < 1e-15);
21670 assert!(generic_wsum > glm_wsum * 100.0);
21671 }
21672
21673 #[test]
21674 fn resonance_scores_match_formula_and_stable_tie() {
21675 let r = Resonance {
21676 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
21678 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
21679 k: 1,
21680 bias: vec![1.5, 0.5, 0.0],
21681 shell: Vec::new(),
21682 };
21683 let x = [1.0f32, 1.0];
21684 let mut got = vec![0.0; 3];
21685 r.scores(&x, &mut got);
21686 assert!((got[0] - 0.5).abs() < 1e-6);
21690 assert!((got[1] - 0.5).abs() < 1e-6);
21691 assert!(got[2].abs() < 1e-6);
21692 let best = got
21693 .iter()
21694 .enumerate()
21695 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
21696 .map(|(i, _)| i);
21697 assert_eq!(best, Some(0));
21698 assert!(got.iter().all(|v| v.is_finite()));
21699 }
21700
21701 #[test]
21706 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
21707 let plain = Resonance {
21713 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
21714 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
21715 k: 1,
21716 bias: vec![1.5, 0.5, 0.0, 0.0],
21717 shell: Vec::new(),
21718 };
21719 let shelled = Resonance {
21720 mu: plain.mu.clone(),
21721 u: plain.u.clone(),
21722 k: 1,
21723 bias: plain.bias.clone(),
21724 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
21725 };
21726 assert!(!plain.has_shell());
21727 assert!(shelled.has_shell());
21728 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
21729 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
21730 set_growth_shell(Some(true));
21731 assert!(growth_shell_enabled());
21732 let x = [1.0f32, 1.0];
21735 plain.scores(&x, &mut a);
21736 shelled.scores(&x, &mut b);
21737 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
21738 assert!(a[2] == 0.0 && a[3] == 0.0);
21739 let xo = [3.0f32, 0.0];
21742 plain.scores(&xo, &mut a);
21743 shelled.scores(&xo, &mut b);
21744 assert_eq!(a[2], -6.0);
21745 assert_eq!(a[3], -6.0);
21746 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
21747 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
21748 assert_eq!(shelled.effective_shell(4), shelled.shell);
21749 set_growth_shell(Some(false));
21752 assert!(!growth_shell_enabled());
21753 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
21754 shelled.scores(&xo, &mut b);
21755 assert_eq!(bits(&a), bits(&b));
21756 set_growth_shell(None);
21757 let short = Resonance {
21760 shell: vec![f32::INFINITY, f32::INFINITY],
21761 ..shelled
21762 };
21763 set_growth_shell(Some(true));
21764 short.scores(&xo, &mut b);
21765 assert_eq!(bits(&a), bits(&b));
21766 set_growth_shell(None);
21767 }
21768
21769 #[test]
21773 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
21774 let zero = || DenseFfn {
21775 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21776 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21777 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21778 act: Act::Silu,
21779 down_t: None,
21780 segs: Vec::new(),
21781 };
21782 let moe = |sigmoid: bool| MoeFfn {
21783 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
21784 experts: vec![zero(), zero(), zero(), zero()],
21785 top_k: 1,
21786 norm_topk_prob: true,
21787 router_sigmoid: sigmoid,
21788 expert_bias: None,
21789 routed_scaling: 1.0,
21790 route_tau: None,
21791 shared: None,
21792 stats: std::cell::RefCell::new(Vec::new()),
21793 act_sq: std::cell::RefCell::new(Vec::new()),
21794 act_rows: std::cell::RefCell::new(Vec::new()),
21795 mask: None,
21796 per_expert_scale: None,
21797 router_input_norm: false,
21798 resonance: None,
21799 grown: Vec::new(),
21800 };
21801 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
21802 for sigmoid in [false, true] {
21803 let m = moe(sigmoid);
21804 let (idx, p, wsum) = moe_route(&logits, &m, None);
21805 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
21806 assert_eq!(p[1], 0.0);
21807 assert_eq!(p[3], 0.0);
21808 assert!(p[2] > p[0] && p[0] > 0.0);
21809 assert!(p.iter().all(|v| v.is_finite()));
21810 let w = p[2] / wsum;
21811 if sigmoid {
21812 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
21814 } else {
21815 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
21816 }
21817 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
21820 assert_eq!(idx, vec![2]);
21821 }
21822 let m = moe(false);
21824 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
21825 assert_eq!(idx, vec![3]);
21826 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
21829 assert_eq!(idx, vec![0]);
21830 assert!(p.iter().all(|&v| v == 0.25));
21831 assert!(wsum.is_finite() && wsum > 0.0);
21832 }
21833
21834 #[test]
21839 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
21840 let zero = || DenseFfn {
21841 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21842 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21843 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21844 act: Act::Silu,
21845 down_t: None,
21846 segs: Vec::new(),
21847 };
21848 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
21849 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
21850 experts: vec![zero(), zero(), zero()],
21851 top_k: 1,
21852 norm_topk_prob: norm_topk,
21853 router_sigmoid: false,
21854 expert_bias: None,
21855 routed_scaling: 1.0,
21856 route_tau: None,
21857 shared: None,
21858 stats: std::cell::RefCell::new(Vec::new()),
21859 act_sq: std::cell::RefCell::new(Vec::new()),
21860 act_rows: std::cell::RefCell::new(Vec::new()),
21861 mask: None,
21862 per_expert_scale: None,
21863 router_input_norm: false,
21864 resonance: resonant.then(|| Resonance {
21865 mu: vec![0.0; 6],
21866 u: Vec::new(),
21867 k: 0,
21868 bias: vec![0.0; 3],
21869 shell: Vec::new(),
21870 }),
21871 grown: Vec::new(),
21872 };
21873 let lo = -0.1f32;
21876 let hi = f32::from_bits(lo.to_bits() - 1);
21877 assert!(hi > lo && hi - lo < 2f32.powi(-25));
21878 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
21879 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
21881 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
21882 for norm in [true, false] {
21885 let m = moe(true, norm);
21886 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
21887 assert_eq!(idx, vec![1], "norm_topk {norm}");
21888 assert_eq!(p, vec![0.0, 1.0, 0.0]);
21889 assert_eq!(p[1] / wsum, 1.0);
21890 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
21893 assert_eq!(idx, vec![0]);
21894 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
21896 assert_eq!(idx, vec![2]);
21897 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
21898 assert_eq!(idx, vec![0]);
21899 assert_eq!(p[0] / wsum, 1.0);
21900 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
21903 assert_eq!(idx, vec![0]);
21904 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
21905 }
21906 }
21907}