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 m3_winners: Vec<Vec<u32>>,
211 m3_counts: Vec<u32>,
213 m3_n_exp: usize,
214 m3_tokens: u64,
215 m3_prewarmed: bool,
216 pub short_conv_cfg: Option<ShortConvCfg>,
219 pub mtp: Option<MtpModule>,
221 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
225 verify_exact_moe: bool,
228 pub speculative: bool,
230 pub ignore_eos: bool,
236 pub draft_full_streak: u32,
242 pub spec_k_adapt: Option<usize>,
250 pub spec_acc_ewma: f32,
252 rng: SplitMix64,
253 sampler_scratch: SamplerScratch,
254 spec_forced: Option<u32>,
260 spec_q: Vec<Vec<f32>>,
261 spec_p: Vec<f32>,
262 spec_res: Vec<f32>,
263 spec_qs: Vec<sampler::Sparse>,
265 spec_ps: sampler::Sparse,
266 spec_ress: sampler::Sparse,
267 mtp_graph_mode: Option<bool>,
274 #[cfg(target_os = "macos")]
277 metal_verify: Option<MetalVerifyPending>,
278 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
282 ws: ForwardScratch,
286 pool: Option<std::sync::Arc<Pool>>,
288 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
292 pub(crate) dyn_force_f32: bool,
294 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
299 pub(crate) dyn_active: Option<usize>,
305 pub(crate) dyn_blend_loaded: bool,
309 pub(crate) dyn_phi_layer: Option<usize>,
312 dyn_phi_ema: Vec<f32>,
314 dyn_phi_seen: usize,
315 pub dyn_router: Option<crate::swarm::DynRouter>,
318 o1_cfg: Option<crate::nystrom::O1Cfg>,
321 o1_epoch: u64,
324 o1_flags: Vec<bool>,
326 trace: bool,
329 calib_temp: f32,
332 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
334 graph_kv_id: u64,
335 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
338 graph_want_logits: bool,
339 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
343 graph_head_required: bool,
344 graph_logits: Option<Vec<f32>>,
347 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
350 graph_refused: std::sync::atomic::AtomicBool,
356 pub embed_multiplier: f32,
358 pub attn_scale: f32,
361 pub swa: Option<(usize, usize)>,
364 pub swa_trim: Option<(usize, usize)>,
371 pub sliding_layers: Option<Vec<bool>>,
374 pub no_rope_layers: Option<Vec<bool>>,
379 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
385 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
388 pub kv_prefix: KvPrefix,
392 pub last_prefill_tokens: usize,
395 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
398 pub rotary_dim_local: Option<usize>,
399 pub rope_scale: f32,
400 pub rope_scale_local: f32,
401 pub global_attn: Option<(usize, usize)>,
404 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
407 pub attn_v_norm: bool,
409 pub qk_norm_after_rope: bool,
411 pub proj_gate_sigmoid: bool,
414 pub final_softcap: Option<f32>,
416 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
420 pub attn_softcap: f32,
422 confidence_on: bool,
426 #[cfg(test)]
429 nll_test_fail_at: Option<usize>,
430 #[cfg(test)]
433 nll_test_force_serial: bool,
434}
435
436#[cfg(target_os = "macos")]
437impl Drop for Pipeline {
438 fn drop(&mut self) {
439 let _ = crate::gpu_metal::wait_replay();
441 crate::gpu::kv_mirror_drop(self.graph_kv_id);
442 }
443}
444
445#[cfg(not(target_os = "macos"))]
446impl Drop for Pipeline {
447 fn drop(&mut self) {
448 crate::gpu::graph_kv_reset(self.graph_kv_id);
452 }
453}
454
455pub struct PipelineWeights {
460 pub embed_tokens: QTensor,
462 pub layers: Vec<LayerWeights>,
464 pub lm_head: QTensor,
466 pub final_norm: Vec<f32>,
468}
469
470pub struct LayerWeights {
472 pub input_norm: Vec<f32>,
473 pub post_norm: Vec<f32>,
476 pub attn_out_norm: Option<Vec<f32>>,
479 pub layer_scale: Option<f32>,
481 pub ffn_out_norm: Option<Vec<f32>>,
484 pub ffn: FfnKind,
485 pub attn: AttnKind,
486}
487
488#[derive(Clone, Copy, PartialEq, Debug, Default)]
492pub enum Act {
493 #[default]
494 Silu,
495 GeluTanh,
496 Gelu,
498 Situ {
501 beta: f32,
502 linear_beta: f32,
503 },
504 SwiGluOai {
509 alpha: f32,
510 limit: f32,
511 },
512}
513
514impl Act {
515 pub fn from_arch(name: &str) -> Self {
516 if name == "gelu_tanh" {
517 Self::GeluTanh
518 } else if name == "gelu" {
519 Self::Gelu
520 } else {
521 Self::Silu
522 }
523 }
524
525 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
527 match arch.hidden_act.as_str() {
528 "situ" => Self::Situ {
529 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
530 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
531 },
532 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
537 alpha: 1.702,
538 limit: 7.0,
539 },
540 other => Self::from_arch(other),
541 }
542 }
543
544 #[inline]
549 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
550 Self::SwiGluOai { alpha, limit }
551 }
552
553 #[inline]
554 pub fn apply(self, x: f32) -> f32 {
555 match self {
556 Self::Silu => inference::silu(x),
557 Self::GeluTanh => inference::gelu_tanh(x),
558 Self::Gelu => inference::gelu_erf(x),
559 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
560 Self::SwiGluOai { alpha, limit } => {
561 let gate = x.min(limit);
562 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
563 }
564 }
565 }
566
567 #[inline]
570 pub fn combine(self, g: f32, u: f32) -> f32 {
571 match self {
572 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
573 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
574 }
575 Self::SwiGluOai { alpha, limit } => {
576 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
577 }
578 _ => self.apply(g) * u,
579 }
580 }
581
582 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
586 match self {
587 Self::Silu => Some(crate::gpu::GraphAct::Silu),
588 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
589 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
590 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
591 }
592 }
593
594 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
600 self.moe_job_graph_contract().map(|(act, _)| act)
601 }
602
603 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
609 match self {
610 Self::SwiGluOai { alpha, limit }
611 if alpha.to_bits() == 1.702f32.to_bits()
612 && limit.to_bits() == 7.0f32.to_bits() =>
613 {
614 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
615 }
616 _ => self.graph_act().map(|act| (act, 0.0)),
617 }
618 }
619}
620
621pub struct DenseFfn {
623 pub gate_proj: QTensor,
624 pub up_proj: QTensor,
625 pub down_proj: QTensor,
626 pub act: Act,
628 pub down_t: Option<QTensor>,
634 pub segs: Vec<FfnSeg>,
641}
642
643pub struct FfnSeg {
648 pub gate: QTensor,
649 pub up: QTensor,
650 pub down: QTensor,
651 pub start: usize,
652 pub width: usize,
653}
654
655pub enum FfnKind {
658 Dense(DenseFfn),
659 Moe(MoeFfn),
663 DenseMoe(Box<DenseMoeFfn>),
670}
671
672pub struct DenseMoeFfn {
674 pub dense: DenseFfn,
675 pub moe: MoeFfn,
676 pub post_norm_1: Vec<f32>,
678 pub pre_norm_2: Vec<f32>,
681 pub post_norm_2: Vec<f32>,
683}
684
685pub struct MoeFfn {
686 pub router: QTensor,
688 pub experts: Vec<DenseFfn>,
689 pub top_k: usize,
690 pub norm_topk_prob: bool,
691 pub router_sigmoid: bool,
694 pub sigmoid_denom_eps: f32,
700 pub expert_bias: Option<Vec<f32>>,
704 pub routed_scaling: f32,
707 pub route_tau: Option<f32>,
713 pub shared: Option<(DenseFfn, Option<QTensor>)>,
716 pub stats: std::cell::RefCell<Vec<u64>>,
720 pub act_sq: std::cell::RefCell<Vec<f64>>,
727 pub act_rows: std::cell::RefCell<Vec<f32>>,
733 pub mask: Option<Vec<bool>>,
738 pub per_expert_scale: Option<Vec<f32>>,
741 pub router_input_norm: bool,
745 pub resonance: Option<Resonance>,
749 pub grown: Vec<GrownExpert>,
755}
756
757#[derive(Debug, Clone, PartialEq, Eq)]
759pub struct GrownExpert {
760 pub record: String,
762 pub record_index: usize,
764 pub layer: usize,
765 pub expert: usize,
769}
770
771pub struct Resonance {
773 pub mu: Vec<f32>,
775 pub u: Vec<f32>,
777 pub k: usize,
778 pub bias: Vec<f32>,
780 pub shell: Vec<f32>,
786}
787
788static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
791
792pub fn growth_shell_enabled() -> bool {
797 use std::sync::atomic::Ordering;
798 match GROWTH_SHELL.load(Ordering::Relaxed) {
799 1 => true,
800 2 => false,
801 _ => {
802 let off = std::env::var("CMF_GROWTH_SHELL")
803 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
804 .unwrap_or(false);
805 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
806 !off
807 }
808 }
809}
810
811pub fn set_growth_shell(on: Option<bool>) {
816 GROWTH_SHELL.store(
817 match on {
818 Some(true) => 1,
819 Some(false) => 2,
820 None => 0,
821 },
822 std::sync::atomic::Ordering::Relaxed,
823 );
824}
825
826impl Resonance {
827 pub fn has_shell(&self) -> bool {
829 self.shell.iter().any(|s| s.is_finite())
830 }
831
832 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
835 let mut out = vec![f32::INFINITY; ne];
836 if growth_shell_enabled() {
837 for (o, s) in out.iter_mut().zip(&self.shell) {
838 *o = *s;
839 }
840 }
841 out
842 }
843
844 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
849 let h = x.len();
850 let ne = out.len();
851 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
852 for e in 0..ne {
853 let mu = &self.mu[e * h..(e + 1) * h];
854 let mut d2 = 0.0f32;
855 for j in 0..h {
856 let d = x[j] - mu[j];
857 d2 += d * d;
858 }
859 let mut proj = 0.0f32;
860 for i in 0..self.k {
861 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
862 let mut p = 0.0f32;
863 for j in 0..h {
864 p += (x[j] - mu[j]) * u[j];
865 }
866 proj += p * p;
867 }
868 let err = d2 - proj;
869 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
870 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
871 out[e] = f32::NEG_INFINITY;
872 }
873 }
874 }
875}
876
877pub enum AttnKind {
880 Full {
882 wq: QTensor,
883 wk: QTensor,
884 wv: QTensor,
885 wo: QTensor,
886 q_norm: Option<Vec<f32>>,
887 k_norm: Option<Vec<f32>>,
888 output_gate: bool,
889 softplus_gate: Option<(QTensor, bool)>,
893 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
895 },
896 Linear(VmfPhaseWeights),
898 LinearGdn(GdnWeights),
900 ShortConv(ShortConvWeights),
903 Mla(Box<MlaWeights>),
911 Kda(Box<crate::linear_core::KdaWeights>),
915 Bounded(Box<crate::bounded::BoundedWeights>),
920}
921
922pub struct MlaWeights {
924 pub q_proj: QTensor,
928 pub q_a: Option<QTensor>,
931 pub q_a_norm: Option<Vec<f32>>,
932 pub kv_a: QTensor,
934 pub kv_a_norm: Vec<f32>,
936 pub kv_b: QTensor,
938 pub o_proj: QTensor,
940 pub nh: usize,
941 pub qk_rope: usize,
942 pub qk_nope: usize,
943 pub v_dim: usize,
944 pub lora: usize,
945 pub scale: f32,
947 pub nope: bool,
949}
950
951pub struct MtpModule {
956 pub enorm: Vec<f32>,
957 pub hnorm: Vec<f32>,
958 pub eh_proj: QTensor,
960 pub layer: LayerWeights,
961 pub final_norm: Vec<f32>,
962 pub kv: crate::kv_cache::LayerKvCache,
963}
964
965#[cfg(target_os = "macos")]
972enum MetalRowsItem<'a> {
973 Gdn {
974 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
975 first: usize,
976 },
977 Attn {
978 l: crate::gpu_metal::AttnGpuLayer<'a>,
979 li: usize,
980 q_norm: Option<&'a [f32]>,
981 k_norm: Option<&'a [f32]>,
982 output_gate: bool,
983 },
984}
985
986#[cfg(target_os = "macos")]
987struct MetalVerifyPending {
988 graph: crate::gpu_metal::VerifyGraph,
989 gdn_layers: Vec<usize>,
990 attn_layers: Vec<(usize, usize)>,
991}
992
993#[cfg(target_os = "macos")]
997struct MetalWarmPending {
998 graph: crate::gpu_metal::VerifyGraph,
999 cpu_stored: usize,
1000 b: usize,
1001}
1002
1003#[cfg(target_os = "macos")]
1004enum MetalRowsRun {
1005 Declined,
1007 Failed,
1010 Completed(MetalVerifyPending),
1011}
1012
1013#[cfg(target_os = "macos")]
1014enum MetalPrefillOutcome {
1015 Declined,
1016 Failed,
1017 Completed(Vec<f32>),
1018}
1019
1020#[cfg(target_os = "macos")]
1021enum MetalBatchNllOutcome {
1022 Declined,
1023 Failed(String),
1024 Completed(f64, usize),
1025}
1026
1027#[derive(Clone, Copy)]
1031enum SpecTrial {
1032 Spec {
1033 t0: std::time::Instant,
1034 gen0: usize,
1035 rounds: usize,
1036 },
1037 Plain {
1038 t0: std::time::Instant,
1039 gen0: usize,
1040 },
1041 Decided {
1042 spec: bool,
1043 recheck_at: usize,
1044 },
1045}
1046
1047pub(crate) fn spec_time_level() -> u8 {
1051 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1052 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1053 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1054 Err(_) => 0,
1055 })
1056}
1057
1058struct SpecStampLog {
1064 t_last: std::time::Instant,
1065 items: Vec<(&'static str, f32)>,
1066}
1067
1068static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1069
1070pub(crate) fn spec_stamp(name: &'static str) {
1071 if spec_time_level() == 0 {
1072 return;
1073 }
1074 if let Ok(mut g) = SPEC_STAMPS.lock() {
1075 if let Some(log) = g.as_mut() {
1076 let now = std::time::Instant::now();
1077 log.items
1078 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1079 log.t_last = now;
1080 }
1081 }
1082}
1083
1084fn spec_stamps_begin() {
1085 if spec_time_level() == 0 {
1086 return;
1087 }
1088 if let Ok(mut g) = SPEC_STAMPS.lock() {
1089 *g = Some(SpecStampLog {
1090 t_last: std::time::Instant::now(),
1091 items: Vec::with_capacity(64),
1092 });
1093 }
1094}
1095
1096fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1097 SPEC_STAMPS
1098 .lock()
1099 .ok()
1100 .and_then(|mut g| g.take())
1101 .map(|l| l.items)
1102 .unwrap_or_default()
1103}
1104
1105fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1108 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1109 for &(n, ms) in items {
1110 match agg.iter_mut().find(|e| e.0 == n) {
1111 Some(e) => {
1112 e.1 += ms;
1113 e.2 += 1;
1114 }
1115 None => agg.push((n, ms, 1)),
1116 }
1117 }
1118 let mut s = String::with_capacity(agg.len() * 16);
1119 for (n, ms, k) in agg {
1120 if k > 1 {
1121 s.push_str(&format!("{n} {ms:.1}/{k} "));
1122 } else {
1123 s.push_str(&format!("{n} {ms:.1} "));
1124 }
1125 }
1126 s
1127}
1128
1129#[derive(Default, Clone, Copy)]
1151struct SpecMon {
1152 round_ms: f64,
1153 tokens: f64,
1154 plain_ms: f64,
1155 n: u32,
1156 fails: u32,
1157 metal: bool,
1158}
1159
1160const SPEC_PROXY_TOKENS: f64 = 3.5;
1163const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1166
1167impl SpecMon {
1168 fn round(&mut self, dt_ms: f64, produced: usize) {
1169 self.n += 1;
1170 if self.n == 1 {
1171 return; }
1173 let a = if self.n == 2 { 1.0 } else { 0.3 };
1174 self.round_ms += a * (dt_ms - self.round_ms);
1175 self.tokens += a * (produced as f64 - self.tokens);
1176 }
1177 fn pays(&self) -> bool {
1178 if self.plain_ms > 0.0 {
1179 self.tokens * self.plain_ms > self.round_ms * 1.03
1180 } else {
1181 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1182 }
1183 }
1184 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1186 let n = generated.saturating_sub(gen0);
1187 if n >= 8 {
1188 return true;
1189 }
1190 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1191 }
1192}
1193
1194pub const KV_PREFIX_TAIL: usize = 128;
1197
1198#[derive(Debug, Clone, Default)]
1204pub struct KvPrefix {
1205 len: usize,
1206 hash: u64,
1207 tail: Vec<u32>,
1208 device: bool,
1213}
1214
1215impl KvPrefix {
1216 #[inline]
1217 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1218 for &id in ids {
1219 h ^= id as u64;
1220 h = h.wrapping_mul(0x100000001b3);
1221 h ^= h >> 29;
1222 }
1223 h
1224 }
1225
1226 pub fn clear(&mut self) {
1227 self.len = 0;
1228 self.hash = 0xcbf29ce484222325;
1229 self.tail.clear();
1230 self.device = false;
1231 }
1232
1233 pub fn on_device(&self) -> bool {
1235 self.device
1236 }
1237
1238 pub fn set_on_device(&mut self, device: bool) {
1240 self.device = device;
1241 }
1242
1243 pub fn len(&self) -> usize {
1245 self.len
1246 }
1247
1248 pub fn is_empty(&self) -> bool {
1249 self.len == 0
1250 }
1251
1252 pub fn tail_len(&self) -> usize {
1254 self.tail.len()
1255 }
1256
1257 pub fn set(&mut self, ids: &[u32]) {
1259 self.clear();
1260 self.extend(ids);
1261 }
1262
1263 pub fn extend(&mut self, more: &[u32]) {
1265 if self.len == 0 && self.hash == 0 {
1266 self.hash = 0xcbf29ce484222325;
1267 }
1268 self.hash = Self::fold(self.hash, more);
1269 self.len += more.len();
1270 if more.len() >= KV_PREFIX_TAIL {
1271 self.tail.clear();
1272 self.tail
1273 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1274 } else {
1275 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1276 self.tail.drain(..drop);
1277 self.tail.extend_from_slice(more);
1278 }
1279 }
1280
1281 pub fn extension(&self, ids: &[u32]) -> usize {
1285 if self.len == 0 || ids.len() <= self.len {
1286 return 0;
1287 }
1288 let t = self.tail.len();
1289 if ids[self.len - t..self.len] != self.tail[..] {
1290 return 0;
1291 }
1292 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1293 return 0;
1294 }
1295 self.len
1296 }
1297}
1298
1299pub struct GenerateResult {
1301 pub text: String,
1302 pub token_ids: Vec<u32>,
1303 pub prompt_tokens: usize,
1304 pub tokens_generated: usize,
1305 pub finish_reason: String,
1306 pub mtp_drafted: usize,
1308 pub mtp_accepted: usize,
1309 pub token_confidence: Vec<f32>,
1314 pub traces: Vec<TokenTrace>,
1317}
1318
1319#[derive(Clone, Debug)]
1324pub struct TokenTrace {
1325 pub t: usize,
1327 pub token_id: u32,
1329 pub confidence: f32,
1331 pub active_skill: Option<String>,
1333 pub recon: Option<f32>,
1337 pub switched: bool,
1340}
1341
1342#[cfg_attr(not(test), allow(dead_code))]
1347fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1348 let t = if temp > 1e-3 { temp } else { 1.0 };
1349 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1350 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1351 if sum > 0.0 {
1352 (((logits[id as usize] - max) / t).exp()) / sum
1353 } else {
1354 0.0
1355 }
1356}
1357
1358fn prefill_batched() -> bool {
1361 std::env::var("CMF_PREFILL")
1362 .map(|v| v != "seq")
1363 .unwrap_or(true)
1364}
1365
1366#[inline]
1370fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1371 let graph_quality = unmasked && prefer_graph;
1372 let fused_head_quality = graph_quality && native_metal;
1373 (graph_quality, fused_head_quality)
1374}
1375
1376#[derive(Clone, Copy)]
1380enum PrefillIn<'a> {
1381 Ids(&'a [u32]),
1382 Hidden(&'a [f32]),
1383}
1384
1385impl Pipeline {
1392 fn can_prefill_batched(&self) -> bool {
1393 #[cfg(test)]
1394 let force_serial = self.nll_test_force_serial;
1395 #[cfg(not(test))]
1396 let force_serial = false;
1397 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1398 }
1399
1400 fn automatic_gpu_prefix(&self) -> Option<usize> {
1403 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1404 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1405 }
1406
1407 pub fn prefill_chunk(&self) -> usize {
1411 let env = env_prefill_chunk();
1412 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1413 return prefill_chunk_rule(env, ChunkHost::here(), false);
1414 }
1415 prefill_chunk_rule(
1416 None,
1417 ChunkHost::Other,
1418 self.chunk_stack_facts().dense_on_discrete(),
1419 )
1420 }
1421
1422 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1423 let plain_dense = !self.weights.layers.is_empty()
1424 && self.g3n.is_none()
1425 && self.dsv4.is_none()
1426 && self.dsv41.is_none()
1427 && self.qwen4_exp.is_none()
1428 && self.weights.layers.iter().all(|lw| {
1429 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1430 });
1431 let gpu_on = crate::gpu::enabled();
1432 ChunkStackFacts {
1433 plain_dense,
1434 discrete: gpu_on && crate::gpu::discrete(),
1435 gpu_on,
1436 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1439 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1440 multi_gpu: self.gpu_plan.is_some(),
1441 o1: self.o1_active(),
1442 }
1443 }
1444}
1445
1446pub fn prefill_chunk() -> usize {
1455 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1456}
1457
1458fn env_prefill_chunk() -> Option<usize> {
1459 std::env::var("CMF_PREFILL_CHUNK")
1460 .ok()
1461 .and_then(|v| v.parse::<usize>().ok())
1462}
1463
1464#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1466enum ChunkHost {
1467 Macos,
1468 Aarch64,
1470 Other,
1472}
1473
1474impl ChunkHost {
1475 fn here() -> Self {
1476 if cfg!(target_os = "macos") {
1477 ChunkHost::Macos
1478 } else if cfg!(target_arch = "aarch64") {
1479 ChunkHost::Aarch64
1480 } else {
1481 ChunkHost::Other
1482 }
1483 }
1484}
1485
1486const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1493
1494fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1500 if let Some(n) = env {
1501 return n.max(1);
1502 }
1503 match host {
1504 ChunkHost::Macos => 512,
1505 ChunkHost::Aarch64 => 256,
1508 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1509 ChunkHost::Other => 48,
1510 }
1511}
1512
1513#[derive(Clone, Copy, Debug, Default)]
1515struct ChunkStackFacts {
1516 plain_dense: bool,
1519 discrete: bool,
1521 gpu_on: bool,
1523 capacity_split: bool,
1525 multi_gpu: bool,
1527 o1: bool,
1529}
1530
1531impl ChunkStackFacts {
1532 fn dense_on_discrete(self) -> bool {
1533 self.plain_dense
1534 && self.discrete
1535 && self.gpu_on
1536 && !self.capacity_split
1537 && !self.multi_gpu
1538 && !self.o1
1539 }
1540}
1541
1542#[inline]
1548fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1549 if end <= start || start >= input_len {
1550 return 0;
1551 }
1552 let rows = (end.min(input_len) - start).min(input_len - start);
1553 if end < input_len {
1554 rows
1555 } else {
1556 rows.saturating_sub(1)
1557 }
1558}
1559
1560pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1562
1563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1565pub(crate) struct ReuseLayer {
1566 pub full: bool,
1569 pub host_rows: usize,
1572 pub device_rows: Option<usize>,
1574 pub device_state: bool,
1576}
1577
1578#[derive(Debug, Clone, PartialEq, Eq)]
1580pub(crate) enum ReusePlan {
1581 Ready,
1583 Pull(Vec<(usize, usize, usize)>),
1586 Fresh,
1588}
1589
1590pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1600 let mut pulls = Vec::new();
1601 for (li, l) in layers.iter().enumerate() {
1602 if !l.full {
1603 if l.device_state {
1604 return ReusePlan::Fresh;
1605 }
1606 continue;
1607 }
1608 if l.host_rows == reuse_from {
1609 continue;
1610 }
1611 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1612 pulls.push((li, l.host_rows, reuse_from));
1613 continue;
1614 }
1615 return ReusePlan::Fresh;
1616 }
1617 if pulls.is_empty() {
1618 ReusePlan::Ready
1619 } else {
1620 ReusePlan::Pull(pulls)
1621 }
1622}
1623
1624impl Pipeline {
1625 fn clear_sequence_state(&mut self) {
1633 #[cfg(target_os = "macos")]
1636 let _ = crate::gpu_metal::wait_replay();
1637 self.kv_cache.clear();
1638 self.clear_history();
1641 self.graph_logits = None;
1642 if let Some(b) = &mut self.dsv41 {
1643 b.3.clear();
1644 }
1645 crate::gpu::graph_kv_reset(self.graph_kv_id);
1646 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1651 }
1652
1653 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1660 if self.graph_prefill_preferred() {
1661 return true;
1662 }
1663 let kv_id = self.graph_kv_id;
1664 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1665 .map(|li| {
1666 let full = matches!(
1667 self.weights.layers[self.phys_layer(li)].attn,
1668 AttnKind::Full { .. }
1669 );
1670 ReuseLayer {
1671 full,
1672 host_rows: self.kv_cache.layers[li].pos_len(),
1675 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1676 device_state: crate::gpu::graph_state_resident(kv_id, li),
1677 }
1678 })
1679 .collect();
1680 if layers
1684 .iter()
1685 .all(|l| l.device_rows.is_none() && !l.device_state)
1686 {
1687 return true;
1688 }
1689 let plan = kv_reuse_plan(reuse_from, &layers);
1690 let (what, rows, n) = match &plan {
1691 ReusePlan::Ready => ("host ready", 0, 0),
1692 ReusePlan::Fresh => ("fresh", 0, 0),
1693 ReusePlan::Pull(p) => (
1694 "pull",
1695 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1696 p.len(),
1697 ),
1698 };
1699 let t0 = std::time::Instant::now();
1700 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1701 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1702 eprintln!(
1703 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1704 if ok { "" } else { " (failed → fresh)" },
1705 t0.elapsed().as_secs_f64() * 1e3
1706 );
1707 }
1708 ok
1709 }
1710
1711 fn apply_kv_reuse_plan(
1712 &mut self,
1713 reuse_from: usize,
1714 plan: ReusePlan,
1715 layers: &[ReuseLayer],
1716 ) -> bool {
1717 let kv_id = self.graph_kv_id;
1718 match plan {
1719 ReusePlan::Fresh => return false,
1720 ReusePlan::Ready => {}
1721 ReusePlan::Pull(pulls) => {
1722 let (nkv, hd) = {
1727 let c = &self.kv_cache.layers[pulls[0].0];
1728 (c.num_kv_heads, c.head_dim)
1729 };
1730 let uniform = pulls.iter().all(|&(li, _, _)| {
1731 let c = &self.kv_cache.layers[li];
1732 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1733 });
1734 let batched = if uniform {
1735 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1736 } else {
1737 None
1738 };
1739 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1740 Some(rows) => rows,
1741 None => {
1742 let mut rows = Vec::with_capacity(pulls.len());
1743 for &(li, from, to) in &pulls {
1744 let (lnkv, lhd) = {
1745 let c = &self.kv_cache.layers[li];
1746 (c.num_kv_heads, c.head_dim)
1747 };
1748 let Some((k, v, first_valid)) =
1749 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1750 else {
1751 return false;
1752 };
1753 let need_from = match self.layer_window(li) {
1757 Some(w) => from.max((to + 1).saturating_sub(w)),
1758 None => from,
1759 };
1760 if first_valid > need_from {
1761 return false;
1762 }
1763 rows.push((k, v));
1764 }
1765 rows
1766 }
1767 };
1768 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1769 let cache = &mut self.kv_cache.layers[li];
1770 let row = cache.num_kv_heads * cache.head_dim;
1771 for p in 0..to - from {
1772 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1773 }
1774 if cache.pos_len() != to {
1775 return false;
1776 }
1777 }
1778 }
1779 }
1780 for (li, l) in layers.iter().enumerate() {
1784 if l.full
1785 && l.device_rows.is_some_and(|d| d > reuse_from)
1786 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1787 {
1788 return false;
1789 }
1790 }
1791 true
1792 }
1793
1794 fn finish_generation(
1800 &mut self,
1801 mtp: &mut Option<MtpModule>,
1802 router: &mut Option<crate::swarm::DynRouter>,
1803 clear_sequence: bool,
1804 ) {
1805 if router.is_some() {
1809 let _ = self.set_active_skill(None);
1810 }
1811 #[cfg(target_os = "macos")]
1818 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1819 if clear_sequence {
1820 self.clear_sequence_state();
1821 if let Some(m) = mtp.as_mut() {
1822 m.kv.clear();
1828 }
1829 if let Some(m) = self.mtp.as_mut() {
1830 m.kv.clear();
1834 }
1835 }
1836 self.graph_want_logits = false;
1837 self.graph_head_required = false;
1838 self.graph_logits = None;
1839 self.graph_failed
1840 .store(false, std::sync::atomic::Ordering::Relaxed);
1841 self.cancel
1842 .store(false, std::sync::atomic::Ordering::Relaxed);
1843 self.dyn_router = router.take().or(self.dyn_router.take());
1844 self.mtp = mtp.take().or(self.mtp.take());
1845 self.mtp_graph_mode = None;
1846 self.spec_forced = None;
1847 }
1848
1849 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1853 if self
1854 .graph_failed
1855 .swap(false, std::sync::atomic::Ordering::Relaxed)
1856 {
1857 self.cancel
1858 .store(false, std::sync::atomic::Ordering::Relaxed);
1859 self.clear_sequence_state();
1860 self.graph_logits = None;
1861 self.graph_want_logits = false;
1862 self.graph_head_required = false;
1863 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1864 }
1865 Ok(())
1866 }
1867
1868 #[cfg(target_os = "macos")]
1869 fn fail_metal_graph(&mut self, reason: &str) {
1870 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1871 self.clear_sequence_state();
1872 self.graph_logits = None;
1873 self.graph_failed
1874 .store(true, std::sync::atomic::Ordering::Relaxed);
1875 self.cancel
1876 .store(true, std::sync::atomic::Ordering::Relaxed);
1877 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1878 }
1879
1880 fn nll_begin(&mut self) -> Result<(), String> {
1885 if self
1886 .graph_failed
1887 .swap(false, std::sync::atomic::Ordering::Relaxed)
1888 {
1889 self.cancel
1890 .store(false, std::sync::atomic::Ordering::Relaxed);
1891 self.clear_sequence_state();
1892 self.graph_logits = None;
1893 self.graph_want_logits = false;
1894 self.graph_head_required = false;
1895 return Err("GPU graph failed before NLL scoring".to_string());
1896 }
1897 self.clear_sequence_state();
1898 self.graph_logits = None;
1899 self.graph_want_logits = false;
1900 self.graph_head_required = false;
1901 Ok(())
1902 }
1903
1904 fn nll_end(&mut self) {
1908 self.clear_sequence_state();
1909 self.graph_logits = None;
1910 self.graph_want_logits = false;
1911 self.graph_head_required = false;
1912 self.graph_failed
1913 .store(false, std::sync::atomic::Ordering::Relaxed);
1914 }
1915
1916 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1919 #[cfg(test)]
1920 if self.nll_test_fail_at == Some(pos) {
1921 self.nll_test_fail_at = None;
1922 self.graph_failed
1923 .store(true, std::sync::atomic::Ordering::Relaxed);
1924 self.cancel
1925 .store(true, std::sync::atomic::Ordering::Relaxed);
1926 }
1927 if self
1928 .graph_failed
1929 .swap(false, std::sync::atomic::Ordering::Relaxed)
1930 {
1931 self.cancel
1932 .store(false, std::sync::atomic::Ordering::Relaxed);
1933 self.clear_sequence_state();
1934 self.graph_logits = None;
1935 self.graph_want_logits = false;
1936 return Err(format!(
1937 "GPU graph failed during NLL {phase} at position {pos}"
1938 ));
1939 }
1940 Ok(())
1941 }
1942
1943 #[inline]
1947 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1948 virtual_idx % self.physical_layers
1949 }
1950
1951 #[inline]
1954 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1955 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1956 }
1957
1958 #[allow(clippy::too_many_arguments)]
1960
1961 #[cfg(target_os = "macos")]
1980 fn graph_prefill_preferred(&self) -> bool {
1981 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1982 if !crate::gpu::enabled_here()
1983 || !graph_force
1984 || std::env::var("CMF_GPU_BLOCK")
1985 .map(|v| v == "0")
1986 .unwrap_or(false)
1987 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1990 {
1991 return false;
1992 }
1993 self.weights
1994 .layers
1995 .iter()
1996 .any(|lw| {
1997 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1998 })
1999 }
2000
2001 #[cfg(not(target_os = "macos"))]
2008 fn batch_prefix_prefill(&self) -> bool {
2009 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2010 Ok("0") => return false,
2011 Ok("1") => true,
2012 _ => false,
2013 };
2014 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2015 && crate::gpu::enabled_here()
2016 && !self.graph_refused()
2017 && (forced || self.graph_attn_decline_reason().is_some())
2018 && self.wgpu_graph_attn_decline().is_none()
2019 && self.attn_softcap == 0.0
2020 && self
2021 .weights
2022 .layers
2023 .iter()
2024 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2025 && self.automatic_gpu_prefix().is_some()
2026 }
2027
2028 fn g4_batch_prefill(&self) -> bool {
2034 #[cfg(target_os = "macos")]
2035 {
2036 false
2037 }
2038 #[cfg(not(target_os = "macos"))]
2039 {
2040 (self.global_attn.is_some() || self.attn_v_norm)
2041 && crate::gpu::g4_batch_on()
2042 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2043 && crate::gpu::enabled_here()
2044 && crate::gpu::discrete()
2045 && !self.graph_refused()
2046 && !self.o1_active()
2047 && self.attn_softcap == 0.0
2048 && self.wgpu_graph_attn_decline().is_none()
2049 && self.weights.layers.iter().all(|lw| {
2050 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2051 })
2052 && self.automatic_gpu_prefix().is_none()
2053 }
2054 }
2055
2056 #[cfg(not(target_os = "macos"))]
2063 fn g4_batch_ingest(
2064 &mut self,
2065 ids: &[u32],
2066 pos0: usize,
2067 keep_all: bool,
2068 ) -> Result<(usize, Vec<f32>), String> {
2069 let hs = self.hidden_size;
2070 let chunk = self.generation_batch_k().max(1);
2071 let end_all = pos0 + ids.len();
2072 let mut pos = pos0;
2073 let mut out = Vec::new();
2074 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2075 let end = (pos + chunk).min(end_all);
2076 let bk = end - pos;
2077 let mut hiddens = vec![0f32; bk * hs];
2078 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2079 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2080 }
2081 let positions: Vec<usize> = (pos..end).collect();
2082 let t_chunk = std::time::Instant::now();
2083 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2084 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2085 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2086 eprintln!(
2087 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2088 bk as f64 / (ms / 1000.0)
2089 );
2090 }
2091 match outcome {
2092 crate::gpu::BatchGraphOutcome::Completed => {
2093 if keep_all {
2094 out.extend_from_slice(&hiddens);
2095 } else {
2096 out = hiddens[(bk - 1) * hs..].to_vec();
2097 }
2098 pos = end;
2099 }
2100 crate::gpu::BatchGraphOutcome::Failed => {
2101 return Err("Gemma-4 batched prefill failed after admission".into());
2102 }
2103 crate::gpu::BatchGraphOutcome::Declined => {
2104 #[cfg(feature = "gpu")]
2105 if pos > pos0 {
2106 self.pull_lagging_host_kv(0, self.num_layers, pos);
2107 }
2108 break;
2109 }
2110 }
2111 }
2112 Ok((pos - pos0, out))
2113 }
2114
2115 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2120 #[cfg(not(target_os = "macos"))]
2121 {
2122 if ids.is_empty() || !self.g4_batch_prefill() {
2123 return None;
2124 }
2125 self.clear_sequence_state();
2126 match self.g4_batch_ingest(ids, 0, true) {
2127 Ok((n, rows)) if n == ids.len() => Some(rows),
2128 _ => {
2129 self.clear_sequence_state();
2130 None
2131 }
2132 }
2133 }
2134 #[cfg(target_os = "macos")]
2135 {
2136 let _ = ids;
2137 None
2138 }
2139 }
2140
2141 #[cfg(not(target_os = "macos"))]
2142 fn graph_prefill_preferred(&self) -> bool {
2143 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2151 if !graph_on || !crate::gpu::enabled_here() {
2152 return false;
2153 }
2154 if self.embryo_resident_eligible() {
2158 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2163 }
2164 if self.o1_active() {
2178 return false;
2179 }
2180 if self.wgpu_graph_attn_decline().is_some() {
2184 return false;
2185 }
2186 if self
2187 .weights
2188 .layers
2189 .iter()
2190 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2191 {
2192 return true;
2193 }
2194 self.weights
2205 .layers
2206 .iter()
2207 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2208 && self.automatic_gpu_prefix().is_none()
2209 }
2210
2211 #[cfg(target_os = "macos")]
2212 fn q1_graph_gpu(
2213 &mut self,
2214 start: usize,
2215 upto: Option<usize>,
2216 position: usize,
2217 h: &mut [f32],
2218 ) -> usize {
2219 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2221 const MOE_WORKING_SET_REASON: &str =
2222 "model weights exceed Metal working set for native MoE graph";
2223 if self
2227 .graph_declines
2228 .borrow()
2229 .iter()
2230 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2231 {
2232 return start;
2233 }
2234 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2235 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2237 || !crate::gpu::enabled_here()
2238 || !graph_force
2239 || std::env::var("CMF_GPU_BLOCK")
2240 .map(|v| v == "0")
2241 .unwrap_or(false)
2242 {
2243 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2244 eprintln!(
2245 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2246 self.attn_softcap > 0.0,
2247 crate::gpu::enabled_here(),
2248 graph_force,
2249 );
2250 }
2251 if self.graph_head_required {
2252 self.fail_metal_graph("native graph front gate refused");
2253 }
2254 return start;
2255 }
2256 let gemma4 = self.metal_gemma4();
2266 let swa_graph = self.metal_graph_swa() || gemma4;
2267 let graph_attn_reason = self.graph_attn_decline_reason();
2274 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2275 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2276 if (self.swa.is_some() && !swa_graph)
2277 || (self.global_attn.is_some() && !gemma4)
2278 || self.attention_heads_per_layer.is_some()
2279 || (self.attn_v_norm && !gemma4)
2280 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2283 || self.weights.layers.iter().any(|lw| {
2284 ((lw.attn_out_norm.is_some()
2285 || lw.ffn_out_norm.is_some()
2286 || lw.layer_scale.is_some())
2287 && !gemma4)
2288 || matches!(&lw.ffn, FfnKind::Dense(d)
2289 if !(matches!(d.act, Act::Silu | Act::Gelu)
2290 || (gemma4 && d.act == Act::GeluTanh)))
2291 })
2292 {
2293 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2296 self.note_graph_decline("metal block graph", reason);
2297 }
2298 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2299 eprintln!(
2300 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2301 self.swa.is_some(),
2302 self.global_attn.is_some(),
2303 self.attention_heads_per_layer.is_some(),
2304 self.attn_v_norm,
2305 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2306 );
2307 }
2308 if self.graph_head_required {
2309 self.fail_metal_graph("native graph architecture gate refused");
2310 }
2311 return start;
2312 }
2313 let limit = upto
2316 .map(|u| u + 1)
2317 .unwrap_or(self.num_layers)
2318 .min(self.num_layers);
2319
2320 enum Item<'a> {
2321 Gdn {
2322 run: Vec<GdnGpuLayer<'a>>,
2323 first: usize,
2324 },
2325 Attn {
2326 l: AttnGpuLayer<'a>,
2327 li: usize,
2328 q_norm: Option<&'a [f32]>,
2329 k_norm: Option<&'a [f32]>,
2330 output_gate: bool,
2331 proj_gate: Option<(&'a QTensor, bool)>,
2335 head_gate_w: Option<&'a [f32]>,
2338 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2339 full_gpu: bool,
2342 },
2343 }
2344
2345 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2352 let attend_contract = attend_mode != "0"
2353 && attend_mode != "off"
2354 && self.head_dim % 4 == 0
2355 && self.head_dim <= 256
2356 && (self.rotary_dim == 0
2360 || (self.rotary_dim >= 2
2361 && self.rotary_dim <= self.head_dim
2362 && (self.rotary_dim / 2) % 32 == 0))
2363 && self.num_kv_heads > 0
2364 && self.num_heads % self.num_kv_heads == 0;
2365
2366 let mut plan: Vec<Item> = Vec::new();
2367 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2368 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2370 let mut scan = start;
2371 while scan < limit {
2372 let lw = &self.weights.layers[self.phys_layer(scan)];
2373 let ffn = match &lw.ffn {
2374 FfnKind::Dense(d) if d.segs.is_empty() => {
2375 let (Some(g), Some(u), Some(dn)) = (
2376 d.gate_proj.metal_graph_parts(),
2377 d.up_proj.metal_graph_parts(),
2378 d.down_proj.metal_graph_parts(),
2379 ) else {
2380 if block_diag {
2381 eprintln!(
2382 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2383 );
2384 }
2385 break;
2386 };
2387 MetalFfn::Dense {
2388 gate: g,
2389 up: u,
2390 down: dn,
2391 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2394 }
2395 }
2396 FfnKind::Moe(m) => {
2397 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2403 {
2404 MetalFfn::FlintMoe(flint)
2405 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2406 MetalFfn::Moe(moe)
2407 } else {
2408 if block_diag {
2409 eprintln!(
2410 "block-graph: L{scan} MoE outside the graph contract — run ends"
2411 );
2412 }
2413 break;
2414 };
2415 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2416 model_ref.get_or_insert_with(|| model.clone());
2417 }
2418 ffn
2419 }
2420 _ => {
2421 if block_diag {
2422 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2423 }
2424 break;
2425 }
2426 };
2427 match &lw.attn {
2428 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2429 let parts = (
2430 w.in_proj_qkv.metal_graph_parts(),
2431 w.in_proj_z.metal_graph_parts(),
2432 w.in_proj_a.f32_parts(),
2433 w.in_proj_b.f32_parts(),
2434 w.out_proj.metal_graph_parts(),
2435 );
2436 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2437 if block_diag {
2438 eprintln!(
2439 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2440 w.in_proj_qkv.metal_graph_parts().is_some(),
2441 w.in_proj_z.metal_graph_parts().is_some(),
2442 w.in_proj_a.f32_parts().is_some(),
2443 w.in_proj_b.f32_parts().is_some(),
2444 w.out_proj.metal_graph_parts().is_some(),
2445 );
2446 }
2447 break;
2448 };
2449 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2450 model_ref.get_or_insert_with(|| model.clone());
2451 }
2452 let gl = GdnGpuLayer {
2453 attn_norm: &lw.input_norm,
2454 post_norm: &lw.post_norm,
2455 qkv,
2456 z,
2457 a,
2458 b,
2459 out,
2460 ffn,
2461 conv1d: &w.conv1d,
2462 a_log: &w.a_log,
2463 dt_bias: &w.dt_bias,
2464 gnorm: &w.norm,
2465 };
2466 match plan.last_mut() {
2467 Some(Item::Gdn { run, .. }) => run.push(gl),
2468 _ => plan.push(Item::Gdn {
2469 run: vec![gl],
2470 first: scan,
2471 }),
2472 }
2473 }
2474 AttnKind::Full {
2475 wq,
2476 wk,
2477 wv,
2478 wo,
2479 q_norm,
2480 k_norm,
2481 output_gate,
2482 softplus_gate,
2483 bias,
2484 } if (!self.kv_cache.layers[scan].o1_sealed()
2485 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2490 && softplus_gate
2494 .as_ref()
2495 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2496 {
2497 let parts = (
2498 wq.metal_graph_parts(),
2499 wk.metal_graph_parts(),
2500 wv.metal_graph_parts(),
2501 wo.metal_graph_parts(),
2502 );
2503 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2504 break;
2505 };
2506 if let QTensor::Mapped { model, .. } = wq {
2507 model_ref.get_or_insert_with(|| model.clone());
2508 }
2509 let cache = &self.kv_cache.layers[scan];
2510 let o1_metal = cache.o1.is_some()
2514 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2515 && cache.o1_views().is_some();
2516 let head_gate_w = softplus_gate
2520 .as_ref()
2521 .and_then(|(g, _)| g.f32_parts())
2522 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2523 .map(|(d, _, _)| d);
2524 let (nkv_g, hd_g) = if gemma4 {
2527 let (a, b, _) = self.layer_geom(scan);
2528 (a, b)
2529 } else {
2530 (self.num_kv_heads, self.head_dim)
2531 };
2532 let full_gpu = attend_contract
2533 && softplus_gate.is_none() == head_gate_w.is_none()
2534 && cache.mode == crate::kv_cache::KvMode::F32
2535 && (cache.o1.is_none() || o1_metal)
2536 && bias.is_none()
2537 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2538 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2539 && pk.1 == nkv_g * hd_g
2540 && pv.1 == nkv_g * hd_g
2541 && po.2 == self.num_heads * hd_g;
2542 plan.push(Item::Attn {
2543 l: AttnGpuLayer {
2544 attn_norm: &lw.input_norm,
2545 post_norm: &lw.post_norm,
2546 wq: pq,
2547 wk: pk,
2548 wv: pv,
2549 wo: po,
2550 ffn,
2551 attn_out_norm: lw.attn_out_norm.as_deref(),
2552 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2553 layer_scale: lw.layer_scale,
2554 },
2555 li: scan,
2556 q_norm: q_norm.as_deref(),
2557 k_norm: k_norm.as_deref(),
2558 output_gate: *output_gate,
2559 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2560 head_gate_w,
2561 bias: bias
2562 .as_ref()
2563 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2564 full_gpu,
2565 });
2566 }
2567 _ => break,
2568 }
2569 scan += 1;
2570 }
2571 let Some(model) = model_ref else {
2572 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2573 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2574 }
2575 if self.graph_head_required {
2576 self.fail_metal_graph("native graph has no mapped model reference");
2577 }
2578 return start;
2579 };
2580 if plan.is_empty() {
2581 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2582 eprintln!("q1-graph: empty plan at layer {start}");
2583 }
2584 if self.graph_head_required {
2585 self.fail_metal_graph("native graph plan is empty");
2586 }
2587 return start;
2588 }
2589 let has_moe = plan.iter().any(|it| match it {
2590 Item::Gdn { run, .. } => run
2591 .iter()
2592 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2593 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2594 });
2595 let has_flint_moe = plan.iter().any(|it| match it {
2596 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2597 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2598 });
2599 let metal_working_set = crate::gpu_metal::working_set_bytes();
2600 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2601 let model_bytes = model.primary_bytes().len() as u64;
2602 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2608 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2609 let pager_cap = if !has_moe {
2618 None
2619 } else if has_flint_moe {
2620 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2624 } else {
2625 crate::gpu_metal::moe_pager_cap_bytes()
2626 };
2627 let pager_eligible =
2628 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2629 let mut pager_span: Option<(usize, usize)> = None;
2630 if pager_eligible {
2631 let cap = pager_cap.expect("checked above");
2632 let tail_span = (self.graph_want_logits
2636 && upto.is_none()
2637 && scan == self.num_layers
2638 && std::env::var("CMF_GPU_LMHEAD")
2639 .map(|v| v != "0")
2640 .unwrap_or(true))
2641 .then(|| self.weights.lm_head.metal_graph_parts())
2642 .flatten()
2643 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2644 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2645 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2646 Some(match span {
2647 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2648 None => (lo, hi),
2649 })
2650 };
2651 let page_span = |span: (usize, usize), include_tail: bool| {
2652 let (mut lo, mut hi) = span;
2653 if include_tail {
2654 if let Some((tail_lo, tail_hi)) = tail_span {
2655 lo = lo.min(tail_lo);
2656 hi = hi.max(tail_hi);
2657 }
2658 }
2659 crate::gpu_metal::page_round_span(lo, hi)
2660 };
2661 let mut selected = 0usize;
2662 let mut selected_span: Option<(usize, usize)> = None;
2663 let mut gdn_prefix: Option<(usize, usize)> = None;
2671 'items: for (i, item) in plan.iter().enumerate() {
2672 match item {
2673 Item::Attn { li, .. } => {
2674 let Some(span) = add_layer_span(selected_span, *li) else {
2675 break;
2676 };
2677 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2678 break;
2679 };
2680 if page_hi - page_lo > cap {
2681 break;
2682 }
2683 selected = i + 1;
2684 selected_span = Some((page_lo, page_hi));
2685 }
2686 Item::Gdn { first, run } => {
2687 let mut whole = None;
2694 for take in 1..=run.len() {
2695 whole = add_layer_span(whole, *first + take - 1);
2696 if whole.is_none() {
2697 break;
2698 }
2699 }
2700 let whole_fits_empty_page = whole
2701 .and_then(|span| page_span(span, false))
2702 .is_some_and(|(lo, hi)| hi - lo <= cap);
2703 if whole_fits_empty_page {
2704 let mut span = selected_span;
2705 for take in 1..=run.len() {
2706 let Some(next) = add_layer_span(span, *first + take - 1) else {
2707 break 'items;
2708 };
2709 span = Some(next);
2710 }
2711 let Some(span) = span else {
2712 break;
2713 };
2714 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2715 else {
2716 break;
2717 };
2718 if page_hi - page_lo > cap {
2719 break;
2720 }
2721 selected = i + 1;
2722 selected_span = Some((page_lo, page_hi));
2723 continue;
2724 }
2725 if selected != 0 {
2726 break;
2727 }
2728 let mut span = None;
2729 let mut best: Option<(usize, (usize, usize))> = None;
2730 for take in 1..=run.len() {
2731 let Some(next) = add_layer_span(span, *first + take - 1) else {
2732 break;
2733 };
2734 let Some((page_lo, page_hi)) = page_span(next, false) else {
2737 break;
2738 };
2739 if page_hi - page_lo > cap {
2740 break;
2741 }
2742 span = Some((page_lo, page_hi));
2743 best = Some((take, (page_lo, page_hi)));
2744 }
2745 let Some((take, page)) = best else {
2746 break;
2747 };
2748 selected = i + 1;
2749 selected_span = Some(page);
2750 if take != run.len() {
2751 gdn_prefix = Some((i, take));
2752 break 'items;
2753 }
2754 }
2755 }
2756 }
2757 if selected != 0 {
2758 if let Some((item, take)) = gdn_prefix {
2759 match &mut plan[item] {
2760 Item::Gdn { run, .. } => run.truncate(take),
2761 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2762 }
2763 }
2764 let (page_lo, page_hi) = selected_span.expect("selected page span");
2765 tracing::info!(
2766 page_start = page_lo,
2767 page_end = page_hi,
2768 page_mb = (page_hi - page_lo) / (1024 * 1024),
2769 cap_mb = cap / (1024 * 1024),
2770 graph_items = selected,
2771 "native Metal MoE graph uses transient bounded weight page"
2772 );
2773 plan.truncate(selected);
2774 pager_span = Some((page_lo, page_hi));
2775 } else {
2776 tracing::warn!(
2777 cap_mb = cap / (1024 * 1024),
2778 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2779 );
2780 }
2781 }
2782 if has_moe && needs_bounded_pager && pager_span.is_none() {
2791 tracing::info!(
2792 model_bytes,
2793 metal_working_set,
2794 "native Metal MoE graph declined before allocation"
2795 );
2796 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2797 if self.graph_head_required {
2798 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2799 }
2800 return start;
2801 }
2802 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2803 let dev_attend = attend_contract
2804 && (self.head_dim <= 128
2805 || has_moe
2806 || (self.head_dim <= 256 && has_gdn)
2812 || (self.head_dim <= 256 && swa_graph)
2816 || attend_mode == "force"
2817 || attend_mode == "256");
2818 if !dev_attend {
2819 for it in &mut plan {
2820 if let Item::Attn { li, full_gpu, .. } = it {
2821 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2824 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2825 if !keep_o1 {
2826 *full_gpu = false;
2827 }
2828 }
2829 }
2830 }
2831 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2832 use std::sync::atomic::{AtomicBool, Ordering};
2833 static SAID: AtomicBool = AtomicBool::new(false);
2834 if !SAID.swap(true, Ordering::Relaxed) {
2835 let fg = plan
2836 .iter()
2837 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2838 .count();
2839 let att = plan
2840 .iter()
2841 .filter(|it| matches!(it, Item::Attn { .. }))
2842 .count();
2843 eprintln!(
2844 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2845 plan.len(),
2846 self.head_dim,
2847 self.rotary_dim,
2848 self.num_kv_heads,
2849 self.num_heads,
2850 );
2851 }
2852 }
2853 let dims = GraphDims {
2854 hidden: self.hidden_size,
2855 eps: self.rms_eps as f32,
2856 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2857 };
2858 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2862 crate::gpu_metal::MoePagerPageScope::new(
2863 _mt0,
2864 position,
2865 start,
2866 page_lo,
2867 page_hi,
2868 pager_cap.unwrap_or(0),
2872 )
2873 });
2874 let graph_new = match pager_span {
2875 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2876 None => TokenGraph::new(&model, dims, h),
2877 };
2878 let Some(mut graph) = graph_new else {
2879 if self.graph_head_required {
2880 self.fail_metal_graph("native TokenGraph allocation refused");
2881 }
2882 return start;
2883 };
2884 if swa_graph && self.head_dim > 128 {
2885 graph.set_attend_blk_from(64);
2891 }
2892 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2893 nv: cfg.num_v_heads,
2894 nk: cfg.num_k_heads,
2895 dk: cfg.key_head_dim,
2896 dv: cfg.value_head_dim,
2897 kk: cfg.conv_kernel,
2898 hidden: self.hidden_size,
2899 inter: self.intermediate_size,
2900 c_dim: cfg.conv_dim(),
2901 eps: cfg.rms_eps as f32,
2902 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2903 });
2904 let mut valid = 0usize;
2908 let mut end = start;
2909 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2911 static ONCE: std::sync::Once = std::sync::Once::new();
2912 ONCE.call_once(|| {
2913 for it in &plan {
2914 match it {
2915 Item::Gdn { first, run } => {
2916 eprintln!("plan: Gdn first={first} len={}", run.len())
2917 }
2918 Item::Attn { li, full_gpu, .. } => {
2919 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2920 }
2921 }
2922 }
2923 });
2924 }
2925 for item in &plan {
2926 let ok = match item {
2927 Item::Gdn { run, .. } => gcfg
2928 .as_ref()
2929 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2930 .unwrap_or(false),
2931 Item::Attn { l, .. } => graph.attn_ok(l),
2932 };
2933 if !ok {
2934 if block_diag {
2935 eprintln!(
2936 "block-graph: plan item {} ({}) failed graph preflight",
2937 valid,
2938 match item {
2939 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2940 Item::Attn { li, .. } => format!("Attn L{li}"),
2941 }
2942 );
2943 }
2944 break;
2945 }
2946 valid += 1;
2947 end += match item {
2948 Item::Gdn { run, .. } => run.len(),
2949 Item::Attn { .. } => 1,
2950 };
2951 }
2952 plan.truncate(valid);
2953 if plan.is_empty() {
2954 if self.graph_head_required {
2955 self.fail_metal_graph("native graph preflight produced no valid items");
2956 }
2957 return start;
2958 }
2959
2960 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2961 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2962 return start;
2963 }
2964
2965 let one_pass = |t: (usize, usize, usize)| {
2974 use cortiq_core::TensorDtype as D;
2975 matches!(
2976 model.tensors[t.0].dtype,
2977 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
2978 )
2979 };
2980 let dense_fast = plan.iter().all(|it| match it {
2981 Item::Attn {
2982 l, li, full_gpu, ..
2983 } => {
2984 *full_gpu
2985 && self.kv_cache.layers[*li].o1.is_none()
2986 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2987 && match l.ffn {
2988 MetalFfn::Dense { gate, up, down, .. } => {
2989 one_pass(gate) && one_pass(up) && one_pass(down)
2990 }
2991 _ => false,
2992 }
2993 }
2994 Item::Gdn { .. } => false,
2995 });
2996 let moe_plan = !dense_fast
3000 && plan.iter().all(|it| match it {
3001 Item::Attn {
3002 l, li, full_gpu, ..
3003 } => {
3004 *full_gpu
3005 && self.kv_cache.layers[*li].o1.is_none()
3006 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3007 }
3008 Item::Gdn { .. } => false,
3009 });
3010 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3011 if block_diag {
3012 use std::sync::atomic::{AtomicBool, Ordering};
3013 static SAID_AB: AtomicBool = AtomicBool::new(false);
3014 if !SAID_AB.swap(true, Ordering::Relaxed) {
3015 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3016 }
3017 }
3018 let _mv_fast = match ab {
3019 Some((bits, _)) => {
3020 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3021 crate::gpu_metal::MvFastGuard::set_raw(bits)
3022 }
3023 None if moe_plan => {
3024 graph.set_dense_concurrent(
3025 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3026 );
3027 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3028 }
3029 None => {
3030 graph.set_dense_concurrent(dense_fast);
3031 let q8r4 = if swa_graph {
3034 crate::gpu_metal::DENSE_Q8R4
3035 } else {
3036 0
3037 };
3038 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3039 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3040 } else {
3041 0
3042 })
3043 }
3044 };
3045
3046 let pool = self.pool.clone();
3049 let (nh, nkv, hd, hs, eps) = (
3050 self.num_heads,
3051 self.num_kv_heads,
3052 self.head_dim,
3053 self.hidden_size,
3054 self.rms_eps,
3055 );
3056 let norm_style = self.norm_style;
3057 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3058 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3059 let kv_id = self.graph_kv_id;
3060 let mut pending: Vec<(usize, usize)> = Vec::new();
3063 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3066 for item in &plan {
3067 let _xt0 = std::time::Instant::now();
3068 let _xkind: u32 = match item {
3069 Item::Gdn { .. } => 2,
3070 Item::Attn { .. } => 3,
3071 };
3072 if self.loop_final_norm {
3074 let item_start = match item {
3075 Item::Gdn { first, .. } => *first,
3076 Item::Attn { li, .. } => *li,
3077 };
3078 if item_start > start && self.is_loop_end(item_start - 1) {
3079 graph.encode_loop_norm(&self.weights.final_norm);
3080 }
3081 }
3082 match item {
3083 Item::Gdn { run, first } => {
3084 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3085 if l.linear_state.len() != want {
3086 l.linear_state = vec![0f32; want];
3087 }
3088 }
3089 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3090 .iter()
3091 .map(|l| l.linear_state.as_slice())
3092 .collect();
3093 let _ig = std::time::Instant::now();
3094 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3095 tracing::error!("q1 graph: GDN run refused after validation");
3097 return start;
3098 }
3099 graph.commit_kind = 2;
3102 graph.commit();
3103 crate::gpu::stageprof(0, _ig.elapsed());
3104 pending.push((*first, run.len()));
3105 }
3106 Item::Attn {
3107 l,
3108 li,
3109 q_norm,
3110 k_norm,
3111 output_gate,
3112 proj_gate,
3113 head_gate_w,
3114 bias,
3115 full_gpu,
3116 } => {
3117 let _ia = std::time::Instant::now();
3118 let inv_freq_l = self.layer_inv_freq(*li);
3124 let rd_l = self.layer_geom(*li).2;
3125 let window_l = self.layer_window(*li);
3126 let head_gate_w = *head_gate_w;
3127 let (nkv, hd) = if gemma4 {
3130 let (a, b, _) = self.layer_geom(*li);
3131 (a, b)
3132 } else {
3133 (nkv, hd)
3134 };
3135 if *full_gpu {
3137 let cache = &self.kv_cache.layers[*li];
3138 let o1p = if cache.o1.is_some() {
3139 match cache.o1_views() {
3140 Some(views) => Some(crate::gpu::O1AttnParams {
3141 views,
3142 epoch: self.o1_epoch,
3143 }),
3144 None => None,
3146 }
3147 } else {
3148 None
3149 };
3150 let o1_layer = cache.o1.is_some();
3151 if o1_layer && o1p.is_none() {
3152 }
3154 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3155 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3156 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3157 debug_assert!(
3160 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3161 );
3162 let p = crate::gpu::AttnDeviceParams {
3163 kv_id,
3164 layer: *li,
3165 nh,
3166 nkv,
3167 hd,
3168 rd: rd_l,
3169 position,
3170 scale: self.attn_scale,
3171 eps: eps as f32,
3172 gemma,
3173 late_qk_norm: self.qk_norm_after_rope,
3174 output_gate: *output_gate,
3175 q_norm: *q_norm,
3176 k_norm: *k_norm,
3177 inv_freq: &inv_freq_l,
3178 rope_scale: self.layer_rope_scale(*li),
3179 cpu_k,
3180 cpu_v,
3181 cpu_stored,
3182 cpu_gen: cache.generation(),
3183 o1: o1p,
3184 window: window_l,
3185 softmax_sink: cache.sinks.as_deref(),
3186 head_gate: head_gate_w,
3187 v_norm: self.attn_v_norm,
3188 };
3189 let o1_bad = o1_layer && p.o1.is_none();
3190 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3191 {
3192 if p.o1.is_none() {
3194 dev_attn.push((*li, nkv, hd));
3195 }
3196 graph.commit_kind = 3;
3197 graph.commit();
3198 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3202 continue;
3203 }
3204 }
3206 graph.encode_attn_prefix(l);
3207 if let Err(err) = graph.sync_checked() {
3208 self.fail_metal_graph(&err);
3209 return start;
3210 }
3211 if !pending.is_empty() {
3212 let idxs: Vec<usize> =
3213 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3214 let mut outs: Vec<&mut [f32]> = self
3215 .kv_cache
3216 .layers
3217 .iter_mut()
3218 .enumerate()
3219 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3220 .map(|(_, s)| s.linear_state.as_mut_slice())
3221 .collect();
3222 graph.read_states(&mut outs);
3223 }
3224 let mut q_raw = attention::take_buf(l.wq.1);
3225 let mut k = attention::take_buf(l.wk.1);
3226 let mut v = attention::take_buf(l.wv.1);
3227 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3228 let mut gate_raw = proj_gate.map(|(gp, _)| {
3231 let mut normed = attention::take_buf(hs);
3232 graph.read_normed(&mut normed);
3233 let mut raw = attention::take_buf(gp.rows());
3234 gp.matvec(&normed, &mut raw, pool.as_deref());
3235 attention::recycle_buf(&mut normed);
3236 raw
3237 });
3238 let cfg = QwenAttnCfg {
3239 num_heads: nh,
3240 num_kv_heads: nkv,
3241 head_dim: hd,
3242 hidden_size: hs,
3243 position,
3244 inv_freq: &inv_freq_l,
3245 rotary_dim: rd_l,
3246 scale: self.attn_scale,
3247 softcap: self.attn_softcap,
3248 window: window_l,
3249 v_norm: self.attn_v_norm,
3252 qk_norm_after_rope: self.qk_norm_after_rope,
3253 gate_sigmoid: self.proj_gate_sigmoid,
3254 q_norm: *q_norm,
3255 k_norm: *k_norm,
3256 output_gate: *output_gate,
3257 softplus_gate: None,
3258 rope_scale: self.layer_rope_scale(*li),
3259 bias: *bias,
3260 rms_eps: eps,
3261 norm_style,
3262 pool: pool.as_deref(),
3263 v_head_dim: hd,
3264 };
3265 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3268 || std::env::var("CMF_ATTN_DUMP").is_ok();
3269 let _ = full_gpu;
3270 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3271 let mut ao = attention::qwen_attention_core(
3272 q_raw,
3273 k,
3274 v,
3275 &mut self.kv_cache.layers[*li],
3276 &cfg,
3277 );
3278 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3282 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3283 let (cq, _cg, _ck, _cv) =
3284 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3285 let cache = &self.kv_cache.layers[*li];
3286 let n = cache.head_keys(0).len() / hd;
3287 let mut bytes: Vec<u8> = Vec::new();
3288 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3289 bytes.extend_from_slice(&v.to_le_bytes());
3290 }
3291 for v in &cq {
3292 bytes.extend_from_slice(&v.to_le_bytes());
3293 }
3294 for g in 0..nkv {
3295 for v in cache.head_keys(g) {
3296 bytes.extend_from_slice(&v.to_le_bytes());
3297 }
3298 }
3299 for g in 0..nkv {
3300 for v in cache.head_values(g) {
3301 bytes.extend_from_slice(&v.to_le_bytes());
3302 }
3303 }
3304 let _ =
3305 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3306 }
3307 }
3308 if let Some((qr0, k0, v0)) =
3309 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3310 {
3311 let (cq, _cg, ck, cv) =
3312 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3313 let mut h_now = vec![0f32; hs];
3314 graph.read_h(&mut h_now);
3315 let cache = &self.kv_cache.layers[*li];
3316 let n_after = cache.head_keys(0).len() / hd;
3317 let stored = n_after.saturating_sub(1);
3321 let cpu_k: Vec<&[f32]> = (0..nkv)
3322 .map(|g| &cache.head_keys(g)[..stored * hd])
3323 .collect();
3324 let cpu_v: Vec<&[f32]> = (0..nkv)
3325 .map(|g| &cache.head_values(g)[..stored * hd])
3326 .collect();
3327 let p = crate::gpu::AttnDeviceParams {
3328 kv_id,
3329 layer: *li,
3330 nh,
3331 nkv,
3332 hd,
3333 rd: rd_l,
3337 position,
3338 scale: self.attn_scale,
3339 eps: eps as f32,
3340 gemma,
3341 late_qk_norm: self.qk_norm_after_rope,
3342 output_gate: *output_gate,
3343 q_norm: *q_norm,
3344 k_norm: *k_norm,
3345 inv_freq: &inv_freq_l,
3346 rope_scale: self.layer_rope_scale(*li),
3347 cpu_k,
3348 cpu_v,
3349 cpu_stored: stored,
3350 cpu_gen: cache.generation(),
3351 o1: None,
3352 window: window_l,
3353 softmax_sink: cache.sinks.as_deref(),
3354 head_gate: None,
3355 v_norm: self.attn_v_norm,
3356 };
3357 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3358 let md = |a: &[f32], b: &[f32]| {
3359 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3360 };
3361 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3362 eprintln!(
3363 "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}",
3364 nn(&cq),
3365 md(&cq, &dq),
3366 nn(&ck),
3367 md(&ck, &dk),
3368 nn(&cv),
3369 md(&cv, &dv),
3370 nn(&ao),
3371 md(&ao, &dao)
3372 );
3373 } else {
3374 eprintln!("attn-oracle L{li}: device probe declined");
3375 }
3376 }
3377 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3378 attention::apply_projected_gate(
3381 &mut ao,
3382 raw,
3383 per_head,
3384 hd,
3385 self.proj_gate_sigmoid,
3386 );
3387 }
3388 if let Some(mut raw) = gate_raw.take() {
3389 attention::recycle_buf(&mut raw);
3390 }
3391 graph.encode_attn_suffix(l, &ao);
3392 graph.commit();
3395 attention::recycle_buf(&mut ao);
3396 }
3397 }
3398
3399 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3400 }
3401 let mut lm_rows = None;
3406 if self.graph_want_logits
3407 && upto.is_none()
3408 && end == self.num_layers
3409 && std::env::var("CMF_GPU_LMHEAD")
3410 .map(|v| v != "0")
3411 .unwrap_or(true)
3412 {
3413 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3414 if graph.lm_head_ok(lm) {
3415 graph.encode_lm_head(&self.weights.final_norm, lm);
3416 lm_rows = Some(lm.1);
3417 }
3418 }
3419 }
3420 if self.graph_head_required && lm_rows.is_none() {
3421 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3422 self.fail_metal_graph("fused graph head was requested but not encodable");
3423 return start;
3424 }
3425 let _sy0 = std::time::Instant::now();
3426 if let Err(err) = graph.sync_checked() {
3427 if let Some(profile) = pager_profile_page.as_mut() {
3428 profile.finish(start, graph.pager_page_stats(), "sync-error");
3429 }
3430 self.fail_metal_graph(&err);
3431 return start;
3432 }
3433 let _rs0 = std::time::Instant::now();
3434 if !pending.is_empty() {
3435 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3436 let mut outs: Vec<&mut [f32]> = self
3437 .kv_cache
3438 .layers
3439 .iter_mut()
3440 .enumerate()
3441 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3442 .map(|(_, s)| s.linear_state.as_mut_slice())
3443 .collect();
3444 graph.read_states(&mut outs);
3445 }
3446 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3447 use std::sync::atomic::{AtomicU64, Ordering};
3448 static SY: AtomicU64 = AtomicU64::new(0);
3449 static RS: AtomicU64 = AtomicU64::new(0);
3450 static N: AtomicU64 = AtomicU64::new(0);
3451 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3452 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3453 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3454 if n % 100 == 0 {
3455 eprintln!(
3456 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3457 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3458 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3459 );
3460 }
3461 }
3462 if let Some(rows) = lm_rows {
3463 crate::gpu::hostprof_encode_done(_mt0);
3464 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3465 graph.read_logits(&mut lg);
3466 crate::gpu::hostprof_total(_mt0);
3467 lg.resize(self.vocab_size, 0.0);
3468 if let Some(c) = self.final_softcap {
3469 softcap_in_place(&mut lg, c, self.pool.as_deref());
3470 }
3471 self.graph_logits = Some(lg);
3472 }
3473 graph.read_h(h);
3474 if self.graph_head_required && self.graph_logits.is_none() {
3475 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3476 self.fail_metal_graph("fused graph head completed without logits readback");
3477 return start;
3478 }
3479 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3480 METAL_GRAPH_LAYERS.fetch_add(
3481 end.saturating_sub(start) as u64,
3482 std::sync::atomic::Ordering::Relaxed,
3483 );
3484 if self.graph_head_required {
3485 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3486 }
3487 for (li, nkv, hd) in dev_attn {
3491 let mut krow = attention::take_buf(nkv * hd);
3492 let mut vrow = attention::take_buf(nkv * hd);
3493 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3494 let cache = &mut self.kv_cache.layers[li];
3495 cache.append(&krow, &vrow, &[]);
3496 let n = cache.seq_len;
3497 let mut imp = attention::take_buf(n);
3498 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3499 cache.accumulate_imp(&imp);
3500 attention::recycle_buf(&mut imp);
3501 }
3502 attention::recycle_buf(&mut krow);
3503 attention::recycle_buf(&mut vrow);
3504 }
3505 if let Some(profile) = pager_profile_page.as_mut() {
3506 profile.finish(end, graph.pager_page_stats(), "page-complete");
3507 }
3508 if let Some((_, arm)) = ab {
3509 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3510 }
3511 end
3512 }
3513
3514 pub fn new(
3515 tokenizer: Tokenizer,
3516 weights: PipelineWeights,
3517 hidden_size: usize,
3518 intermediate_size: usize,
3519 num_heads: usize,
3520 num_kv_heads: usize,
3521 head_dim: usize,
3522 num_layers: usize,
3523 physical_layers: usize,
3524 loop_final_norm: bool,
3525 vocab_size: usize,
3526 rms_eps: f64,
3527 rope_base: f32,
3528 norm_style: NormStyle,
3529 max_seq_len: usize,
3530 sampler_config: SamplerConfig,
3531 ) -> Self {
3532 let rng = match sampler_config.seed {
3533 Some(s) => SplitMix64::new(s),
3534 None => SplitMix64::from_entropy(),
3535 };
3536 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3537 let pool = Pool::from_env();
3538 if let Some(p) = &pool {
3539 tracing::info!("worker pool: {} threads", p.n_workers());
3540 if let Some(model) = weights
3542 .lm_head
3543 .model_arc()
3544 .or_else(|| weights.embed_tokens.model_arc())
3545 {
3546 let regions: Vec<&[u8]> =
3547 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3548 p.bind_numa(®ions);
3549 }
3550 }
3551 Self {
3552 gpu_plan: None,
3553 tokenizer: std::sync::Arc::new(tokenizer),
3554 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3555 sampler_config,
3556 weights,
3557 hidden_size,
3558 intermediate_size,
3559 num_heads,
3560 num_kv_heads,
3561 head_dim,
3562 num_layers,
3563 physical_layers,
3564 loop_final_norm,
3565 vocab_size,
3566 rms_eps,
3567 rope_base,
3568 norm_style,
3569 rotary_dim: head_dim,
3570 attention_heads_per_layer: None,
3571 kv_heads_per_layer: None,
3572 v_head_dim: None,
3573 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3574 .filter(|v| !v.is_empty())
3575 .map(std::path::PathBuf::from),
3576 graph_declines: std::cell::RefCell::new(Vec::new()),
3577 mimo_moe: Default::default(),
3578 minicpm_tail: Default::default(),
3579 vmf_cfg: None,
3580 gdn_cfg: None,
3581 kda_cfg: None,
3582 g3n: None,
3583 dsv4: None,
3584 dsv41: None,
3585 dsv41_vision: None,
3586 dsv41_prefill: None,
3587 qwen4_exp: None,
3588 dsv4_mtp: Vec::new(),
3589 dspark: None,
3590 dspark_pending: Vec::new(),
3591 dspark_hist: Vec::new(),
3592 dspark_real: Vec::new(),
3593 dspark_trunk_picks: Vec::new(),
3594 dspark_exp: Vec::new(),
3595 dspark_draft_ns: 0,
3596 m3_winners: Vec::new(),
3597 m3_counts: Vec::new(),
3598 m3_n_exp: 0,
3599 m3_tokens: 0,
3600 m3_prewarmed: false,
3601 logit_multiplier: None,
3602 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3603 graph_failed: std::sync::atomic::AtomicBool::new(false),
3604 kv_history: Vec::new(),
3605 kv_history_device: false,
3606 short_conv_cfg: None,
3607 mtp: None,
3608 mimo_mtp: None,
3609 verify_exact_moe: false,
3610 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3611 ignore_eos: false,
3612 draft_full_streak: 0,
3613 spec_k_adapt: None,
3614 spec_acc_ewma: 0.7,
3615 rng,
3616 sampler_scratch: SamplerScratch::default(),
3617 spec_forced: None,
3618 spec_q: Vec::new(),
3619 spec_p: Vec::new(),
3620 spec_res: Vec::new(),
3621 spec_qs: Vec::new(),
3622 spec_ps: Vec::new(),
3623 spec_ress: Vec::new(),
3624 mtp_graph_mode: None,
3625 #[cfg(target_os = "macos")]
3626 metal_verify: None,
3627 inv_freq,
3628 ws: ForwardScratch::new(hidden_size),
3629 pool,
3630 model: None,
3631 dyn_force_f32: false,
3632 dyn_skill_layers: Vec::new(),
3633 dyn_active: None,
3634 dyn_blend_loaded: false,
3635 dyn_phi_layer: None,
3636 dyn_phi_ema: Vec::new(),
3637 dyn_phi_seen: 0,
3638 dyn_router: None,
3639 o1_cfg: None,
3640 o1_epoch: 0,
3641 o1_flags: Vec::new(),
3642 trace: false,
3643 calib_temp: 1.0,
3644 confidence_on: true,
3645 embed_multiplier: 1.0,
3646 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3647 swa: None,
3648 swa_trim: None,
3649 sliding_layers: None,
3650 no_rope_layers: None,
3651 anchor_core: None,
3652 bounded_rope: None,
3653 kv_prefix: KvPrefix::default(),
3654 last_prefill_tokens: 0,
3655 inv_freq_local: None,
3656 rotary_dim_local: None,
3657 rope_scale: 1.0,
3658 rope_scale_local: 1.0,
3659 global_attn: None,
3660 inv_freq_global: None,
3661 attn_v_norm: false,
3662 qk_norm_after_rope: false,
3663 proj_gate_sigmoid: false,
3664 final_softcap: None,
3665 head_clusters: None,
3666 attn_softcap: 0.0,
3667 graph_want_logits: false,
3668 graph_head_required: false,
3669 graph_logits: None,
3670 embryo_graph: None,
3671 graph_refused: std::sync::atomic::AtomicBool::new(false),
3672 graph_kv_id: {
3673 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3674 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3675 },
3676 #[cfg(test)]
3677 nll_test_fail_at: None,
3678 #[cfg(test)]
3679 nll_test_force_serial: false,
3680 }
3681 }
3682
3683 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3691 if let Err(e) = self.try_set_o1(cfg) {
3692 tracing::error!("{e}");
3693 }
3694 }
3695
3696 pub fn bounded_native(&self) -> bool {
3700 self.anchor_core.is_some()
3701 }
3702
3703 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3706 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3707 }
3708
3709 pub fn o1_refusal(&self) -> Option<String> {
3711 self.anchor_core.as_ref().map(|ac| {
3712 format!(
3713 "--o1 / CMF_O1 refused: the anchor is native bounded \
3714 (anchor_core kind={} window={} sink={}); the file's operator \
3715 is executed as-is and no post-hoc Nyström overlay applies",
3716 ac.kind, ac.window, ac.sink
3717 )
3718 })
3719 }
3720
3721 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3723 if let Some(c) = &cfg {
3724 if let Some(why) = self.o1_refusal() {
3725 self.o1_flags = Vec::new();
3726 self.o1_cfg = None;
3727 return Err(why);
3728 }
3729 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3730 self.o1_flags.clear();
3731 self.o1_cfg = None;
3732 return Err(format!(
3733 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3734 c.w, c.sink
3735 ));
3736 }
3737 }
3738 self.o1_flags = match &cfg {
3739 Some(c) => {
3740 let mut flags = c.layer_flags(self.num_layers);
3741 for (li, f) in flags.iter_mut().enumerate() {
3742 if *f
3748 && (!matches!(
3749 self.weights.layers[self.phys_layer(li)].attn,
3750 AttnKind::Full { .. }
3751 ) || self.layer_window(li).is_some()
3752 || self.kv_cache.layers[li].sinks.is_some()
3753 || self.layer_v_dim(li) != self.layer_geom(li).1)
3754 {
3755 *f = false;
3756 }
3757 }
3758 flags
3759 }
3760 None => Vec::new(),
3761 };
3762 if let Some(c) = &cfg {
3763 let n = self.o1_flags.iter().filter(|&&f| f).count();
3764 tracing::info!(
3765 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3766 self.num_layers,
3767 c.m,
3768 c.w,
3769 c.sink,
3770 c.rect
3771 );
3772 }
3773 self.o1_cfg = cfg;
3774 Ok(())
3775 }
3776
3777 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3783 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3784 return Err(format!(
3785 "anchor_core kind '{}' is not executable by this runtime",
3786 cfg.kind
3787 ));
3788 }
3789 if cfg.window == 0 {
3790 return Err("anchor_core.window must be >= 1".into());
3791 }
3792 let mut n = 0usize;
3793 for li in 0..self.num_layers {
3794 let pl = self.phys_layer(li);
3795 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3796 if w.window != cfg.window || w.sink != cfg.sink {
3797 return Err(format!(
3798 "layer {li}: bounded weights (window {} sink {}) disagree with \
3799 anchor_core (window {} sink {})",
3800 w.window, w.sink, cfg.window, cfg.sink
3801 ));
3802 }
3803 self.kv_cache.layers[li].install_bounded(cfg.window);
3804 n += 1;
3805 }
3806 }
3807 if n == 0 {
3808 return Err("anchor_core is present but no layer executes it".into());
3809 }
3810 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3811 self.bounded_rope = Some(std::sync::Arc::new(rope));
3812 self.anchor_core = Some(cfg.clone());
3813 self.embryo_graph = None;
3814 tracing::info!(
3815 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3816 cfg.kind,
3817 cfg.window,
3818 cfg.sink,
3819 self.kv_cache
3820 .layers
3821 .iter()
3822 .map(|l| l.bounded_state_bytes())
3823 .max()
3824 .unwrap_or(0)
3825 );
3826 Ok(())
3827 }
3828
3829 pub fn install_wire_identity(&mut self, identity: u64) {
3833 for li in 0..self.kv_cache.layers.len() {
3834 let pl = self.phys_layer(li);
3835 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3836 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3837 Some(AttnKind::Linear(_))
3838 | Some(AttnKind::LinearGdn(_))
3839 | Some(AttnKind::ShortConv(_))
3840 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3841 _ => crate::kv_cache::WireKind::Full,
3842 };
3843 let l = &mut self.kv_cache.layers[li];
3844 l.wire_kind = kind;
3845 l.wire_identity = identity;
3846 l.wire_layer = li as u32;
3849 }
3850 }
3851
3852 pub fn clear_history(&mut self) {
3854 self.kv_history.clear();
3855 self.kv_history_device = false;
3856 self.kv_prefix.clear();
3857 }
3858
3859 pub fn graph_refused(&self) -> bool {
3863 self.graph_refused
3864 .load(std::sync::atomic::Ordering::Relaxed)
3865 }
3866
3867 pub fn mark_graph_refused(&self) {
3869 if !self
3870 .graph_refused
3871 .swap(true, std::sync::atomic::Ordering::Relaxed)
3872 {
3873 tracing::info!(
3874 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3875 self.graph_kv_id
3876 );
3877 }
3878 }
3879
3880 pub fn device_sequence_position(&self) -> Option<usize> {
3884 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3885 }
3886
3887 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3892 let dev = self.device_sequence_position();
3893 if recorded_on_device {
3894 dev == Some(n) && self.embryo_resident_wanted()
3895 } else {
3896 dev.is_none()
3897 }
3898 }
3899
3900 pub(crate) fn invalidate_for_weight_change(&mut self) {
3908 self.clear_sequence_state();
3909 self.embryo_graph = None;
3910 }
3911
3912 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3918 let (n, on_device) = if self.bounded_native() {
3919 (
3920 self.kv_prefix.extension(input_ids),
3921 self.kv_prefix.on_device(),
3922 )
3923 } else {
3924 let h = &self.kv_history;
3925 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3926 (h.len(), self.kv_history_device)
3927 } else {
3928 (0, false)
3929 }
3930 };
3931 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3934 tracing::warn!(
3935 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3936 the device now holds {:?} — re-prefilling from zero",
3937 if on_device { "resident device" } else { "host" },
3938 self.device_sequence_position()
3939 );
3940 return 0;
3941 }
3942 n
3943 }
3944
3945 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3948 self.cached_prefix_len(input_ids)
3949 }
3950
3951 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3957 let on_device = self.device_sequence_position().is_some();
3958 if self.bounded_native() {
3959 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3960 let prev_device = self.kv_prefix.on_device();
3961 self.kv_history.clear();
3962 self.kv_history_device = false;
3963 if keep && prev_device == on_device {
3964 self.kv_prefix.extend(&consumed[reused..]);
3965 } else {
3966 self.kv_prefix.set(consumed);
3967 }
3968 self.kv_prefix.set_on_device(on_device);
3969 } else {
3970 self.kv_history = consumed.to_vec();
3971 self.kv_history_device = on_device;
3972 }
3973 }
3974
3975 pub fn o1_active(&self) -> bool {
3977 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3978 }
3979
3980 pub fn generation_batch_k(&self) -> usize {
3998 if let Some(k) = std::env::var("CMF_BATCH_K")
3999 .ok()
4000 .and_then(|v| v.parse::<usize>().ok())
4001 {
4002 return k;
4003 }
4004 if self.g4_batch_prefill() {
4007 return 128;
4008 }
4009 #[cfg(not(target_os = "macos"))]
4010 if self.graph_prefill_preferred() && !self.o1_active() {
4011 let moe = self
4014 .weights
4015 .layers
4016 .iter()
4017 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4018 let q82 = crate::gpu::moe_q82_graph_on()
4024 && self.weights.layers.iter().any(|lw| {
4025 matches!(&lw.ffn, FfnKind::Moe(m)
4026 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4027 });
4028 return if q82 {
4029 128
4030 } else if moe {
4031 32
4032 } else {
4033 128
4034 };
4035 }
4036 0
4037 }
4038
4039 pub fn generation_graph_prefill(&self) -> bool {
4040 let graph = self.graph_prefill_preferred();
4041 #[cfg(not(target_os = "macos"))]
4052 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4053 return false;
4054 }
4055 graph
4056 }
4057
4058 pub fn o1_device_stats(&self) -> (usize, u64) {
4063 crate::gpu::o1_device_stats(self.graph_kv_id)
4064 }
4065
4066 pub fn o1_begin(&mut self) {
4071 self.o1_begin_with_prefix(None);
4072 }
4073
4074 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4078 if let Some(c) = &self.o1_cfg {
4079 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4080 let boundary = requested_prefix.map(|p| {
4081 p.max(
4082 crate::nystrom::o1_deferred_boundary(w, sink)
4083 .expect("o1 config boundary validated in set_o1"),
4084 )
4085 });
4086 for (li, &f) in self.o1_flags.iter().enumerate() {
4087 if f {
4088 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4089 }
4090 }
4091 }
4092 }
4093
4094 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4096 self.o1_cfg.as_ref().and_then(|c| {
4097 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4098 .map(|floor| requested_prefix.max(floor))
4099 })
4100 }
4101
4102 fn o1_note_transition(&mut self) {
4103 let mut transitioned = false;
4107 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4108 if flagged {
4109 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4110 }
4111 }
4112 if transitioned {
4113 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4114 }
4115 }
4116
4117 fn o1_pending(&self) -> bool {
4118 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4119 f && self.kv_cache.layers[li].seq_len > 0
4120 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4121 })
4122 }
4123
4124 fn o1_fail(&mut self, err: String) {
4125 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4126 self.clear_sequence_state();
4127 self.graph_failed
4128 .store(true, std::sync::atomic::Ordering::Relaxed);
4129 self.cancel
4130 .store(true, std::sync::atomic::Ordering::Relaxed);
4131 }
4132
4133 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4138 if self.o1_cfg.is_none() {
4139 return Ok(false);
4140 }
4141 let mut participating = false;
4142 for li in 0..self.num_layers {
4143 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4144 continue;
4145 }
4146 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4147 return Err(err);
4148 }
4149 if self.kv_cache.layers[li].seq_len == 0 {
4150 continue;
4151 }
4152 participating = true;
4153 let num_heads = self.layer_num_heads(li);
4154 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4155 }
4156 self.o1_note_transition();
4157 for li in 0..self.num_layers {
4158 if self.o1_flags.get(li).copied().unwrap_or(false) {
4159 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4160 return Err(err);
4161 }
4162 }
4163 }
4164 Ok(participating
4165 && (0..self.num_layers).all(|li| {
4166 !self.o1_flags.get(li).copied().unwrap_or(false)
4167 || self.kv_cache.layers[li].seq_len == 0
4168 || self.kv_cache.layers[li].o1_sealed()
4169 }))
4170 }
4171
4172 fn o1_progress(&mut self) {
4175 if !self.o1_active() {
4176 return;
4177 }
4178 for li in 0..self.num_layers {
4179 if self.o1_flags.get(li).copied().unwrap_or(false) {
4180 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4181 self.o1_fail(err);
4182 return;
4183 }
4184 }
4185 }
4186 self.o1_note_transition();
4190 if !self.o1_pending() {
4191 return;
4192 }
4193 if let Err(err) = self.o1_seal_checked() {
4194 self.o1_fail(err);
4195 }
4196 }
4197
4198 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4203 if self
4204 .graph_failed
4205 .swap(false, std::sync::atomic::Ordering::Relaxed)
4206 {
4207 self.cancel
4208 .store(false, std::sync::atomic::Ordering::Relaxed);
4209 self.clear_sequence_state();
4210 return Err(format!("{phase}: deferred O(1) transition failed"));
4211 }
4212 Ok(())
4213 }
4214
4215 pub fn o1_seal(&mut self) {
4219 if let Err(err) = self.o1_seal_checked() {
4220 self.o1_fail(err);
4221 }
4222 }
4223
4224 pub fn set_trace(&mut self, on: bool) {
4226 self.trace = on;
4227 }
4228
4229 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4232 self.rng = match config.seed {
4233 Some(seed) => SplitMix64::new(seed),
4234 None => SplitMix64::from_entropy(),
4235 };
4236 self.sampler_config = config;
4237 }
4238
4239 pub fn set_confidence(&mut self, on: bool) {
4244 self.confidence_on = on;
4245 }
4246
4247 pub fn set_calib_temp(&mut self, t: f32) {
4250 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4251 }
4252
4253 pub fn calib_temp(&self) -> f32 {
4255 self.calib_temp
4256 }
4257
4258 pub fn rope_inv_freq(&self) -> &[f32] {
4262 self.inv_freq.as_slice()
4263 }
4264
4265 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4268 self.rotary_dim = rotary_dim.min(self.head_dim);
4269 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4270 self.embryo_graph = None;
4274 }
4275
4276 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4277 QwenAttnCfg {
4278 num_heads: self.num_heads,
4279 num_kv_heads: self.num_kv_heads,
4280 head_dim: self.head_dim,
4281 hidden_size: self.hidden_size,
4282 position,
4283 inv_freq: &self.inv_freq,
4284 rotary_dim: self.rotary_dim,
4285 scale: self.attn_scale,
4286 softcap: self.attn_softcap,
4287 window: None,
4288 v_norm: false,
4289 qk_norm_after_rope: self.qk_norm_after_rope,
4290 gate_sigmoid: self.proj_gate_sigmoid,
4291 q_norm: None,
4292 k_norm: None,
4293 output_gate: false,
4294 softplus_gate: None,
4295 rope_scale: self.rope_scale,
4296 bias: None,
4297 rms_eps: self.rms_eps,
4298 norm_style: self.norm_style,
4299 pool: self.pool.as_deref(),
4300 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4301 }
4302 }
4303
4304 pub fn generate(
4306 &mut self,
4307 prompt: &str,
4308 max_tokens: usize,
4309 task_mask: Option<&TaskMask>,
4310 on_token: Option<TokenCallback>,
4311 ) -> Result<GenerateResult, String> {
4312 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4313 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4314 }
4315
4316 pub fn generate_from_vl(
4319 &mut self,
4320 input: &crate::dsv41_vision::PreparedVlInputs,
4321 max_tokens: usize,
4322 task_mask: Option<&TaskMask>,
4323 on_token: Option<TokenCallback>,
4324 ) -> Result<GenerateResult, String> {
4325 let Some(dsv41) = &self.dsv41 else {
4326 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4327 };
4328 if input.token_ids.is_empty() {
4329 return Err("empty V4.1 multimodal prompt".into());
4330 }
4331 if input.token_types.len() != input.token_ids.len() {
4332 return Err(format!(
4333 "V4.1 token type count {} != token count {}",
4334 input.token_types.len(),
4335 input.token_ids.len()
4336 ));
4337 }
4338 let dim = dsv41.2.dim;
4339 let mut embeddings = vec![None; input.token_ids.len()];
4340 let mut participates = vec![true; input.token_ids.len()];
4341 if !input.images.is_empty() {
4342 let vision = self
4343 .dsv41_vision
4344 .as_ref()
4345 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4346 for image in &input.images {
4347 let end = image.start.saturating_add(image.types.len());
4348 if end > input.token_ids.len() {
4349 return Err(format!(
4350 "V4.1 image span {}..{} exceeds prompt length {}",
4351 image.start,
4352 end,
4353 input.token_ids.len()
4354 ));
4355 }
4356 let mut span = vec![0.0f32; image.types.len() * dim];
4357 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4358 for (offset, &kind) in image.types.iter().enumerate() {
4359 let pos = image.start + offset;
4360 if input.token_types[pos] != kind {
4361 return Err(format!(
4362 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4363 input.token_types[pos]
4364 ));
4365 }
4366 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4367 participates[pos] = false;
4368 }
4369 }
4370 }
4371 for (pos, &kind) in input.token_types.iter().enumerate() {
4372 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4373 return Err(format!("V4.1 text position {pos} has an image embedding"));
4374 }
4375 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4376 return Err(format!("V4.1 image position {pos} has no image embedding"));
4377 }
4378 }
4379 self.dsv41_prefill = Some((embeddings, participates));
4380 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4381 self.dsv41_prefill = None;
4382 result
4383 }
4384
4385 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4387 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4388 }
4389
4390 pub fn generate_from_ids(
4398 &mut self,
4399 input_ids: &[u32],
4400 max_tokens: usize,
4401 task_mask: Option<&TaskMask>,
4402 on_token: Option<TokenCallback>,
4403 ) -> Result<GenerateResult, String> {
4404 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4405 }
4406
4407 pub fn generate_from_embeds(
4413 &mut self,
4414 input_ids: &[u32],
4415 prompt_rows: &[f32],
4416 max_tokens: usize,
4417 task_mask: Option<&TaskMask>,
4418 on_token: Option<TokenCallback>,
4419 ) -> Result<GenerateResult, String> {
4420 if input_ids.is_empty()
4421 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4422 {
4423 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4424 }
4425 if prompt_rows.iter().any(|x| !x.is_finite()) {
4426 return Err("embedded prompt contains non-finite values".into());
4427 }
4428 if !self.can_prefill_batched()
4429 || self.dyn_router.is_some()
4430 || self.o1_active()
4431 || self.mtp.is_some()
4432 || self.gpu_plan.is_some()
4433 {
4434 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4435 }
4436 self.generate_with_prompt_rows(
4437 input_ids,
4438 Some(prompt_rows),
4439 max_tokens,
4440 task_mask,
4441 on_token,
4442 )
4443 }
4444
4445 fn generate_with_prompt_rows(
4446 &mut self,
4447 input_ids: &[u32],
4448 prompt_rows: Option<&[f32]>,
4449 max_tokens: usize,
4450 task_mask: Option<&TaskMask>,
4451 mut on_token: Option<TokenCallback>,
4452 ) -> Result<GenerateResult, String> {
4453 #[cfg(target_os = "macos")]
4454 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4455 if std::env::var("CMF_TRACE_H").is_ok() {
4456 eprintln!("input_ids: {input_ids:?}");
4457 }
4458 if input_ids.is_empty() {
4459 return Err("empty prompt: nothing to generate from".to_string());
4460 }
4461 self.graph_failed
4465 .store(false, std::sync::atomic::Ordering::Relaxed);
4466 let task_mask = self.drop_open_mask(task_mask);
4471
4472 let mut reuse_from = {
4480 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4481 if on
4482 && prompt_rows.is_none()
4483 && task_mask.is_none()
4484 && self.mtp.is_none()
4485 && !(self.mimo_mtp.is_some() && self.speculative)
4486 && self.o1_cfg.is_none()
4487 && self.dsv41.is_none()
4488 {
4489 self.cached_prefix_len(input_ids)
4490 } else {
4491 0
4492 }
4493 };
4494 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4497 reuse_from = 0;
4498 }
4499 self.last_prefill_tokens = input_ids.len() - reuse_from;
4500 let bounded_native = self.bounded_native();
4501 if reuse_from == 0 {
4502 self.clear_sequence_state();
4504 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4505 eprintln!(
4506 "kv-reuse: {} of {} prompt positions already cached",
4507 reuse_from,
4508 input_ids.len()
4509 );
4510 }
4511 crate::gpu::graph_race_begin_generation();
4512 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4516 std::env::var("CMF_O1_PREFILL")
4517 .ok()
4518 .and_then(|v| v.parse::<usize>().ok())
4519 .filter(|&p| p > 0)
4520 } else {
4521 None
4522 };
4523 if task_mask.is_none() {
4524 self.o1_begin_with_prefix(o1_prefill);
4525 }
4526
4527 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4533 #[cfg(target_os = "macos")]
4572 let metal_graph = crate::gpu::q1_force()
4573 && crate::gpu::enabled_here()
4574 && std::env::var("CMF_GPU_BLOCK")
4575 .map(|v| v != "0")
4576 .unwrap_or(true);
4577 #[cfg(not(target_os = "macos"))]
4578 let metal_graph = false;
4579 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4580 let spec_cheap_round =
4584 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4585 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4586 || match spec_sample_env.as_deref() {
4587 Some("1") => true,
4588 Some(_) => false,
4589 None => metal_graph && spec_cheap_round,
4590 };
4591 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4607 for lw in &self.weights.layers {
4608 if let FfnKind::Dense(d) = &lw.ffn {
4609 dense_n += 1;
4610 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4611 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4612 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4613 {
4614 dense_q4tp += 1;
4615 }
4616 }
4617 }
4618 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4619 let penalized = !metal_graph
4631 && (self.sampler_config.repetition_penalty != 1.0
4632 || self.sampler_config.presence_penalty != 0.0
4633 || !self.sampler_config.suppress_tokens.is_empty());
4634 #[cfg(feature = "gpu")]
4639 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4640 #[cfg(not(feature = "gpu"))]
4641 let metal_wgpu = false;
4642 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4643 let spec_wanted = match spec_env.as_deref() {
4644 Some("0") => false,
4645 Some(_) => {
4646 if metal_wgpu {
4647 tracing::warn!(
4648 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4649 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4650 );
4651 }
4652 true
4653 }
4654 None => spec_default_ok && !penalized && !metal_wgpu,
4655 };
4656 let graph_spec = self.speculative
4660 && (graph_on || metal_graph)
4661 && self.mtp.is_some()
4662 && task_mask.is_none()
4663 && !self.o1_active()
4664 && spec_sampling_ok
4665 && spec_wanted;
4666 #[cfg(target_os = "macos")]
4670 if metal_graph {
4671 static SAID: std::sync::Once = std::sync::Once::new();
4672 SAID.call_once(|| {
4673 let spec = if graph_spec {
4674 let k = std::env::var("CMF_GRAPH_SPEC_K")
4675 .ok()
4676 .and_then(|v| v.parse::<usize>().ok())
4677 .filter(|&v| (1..=8).contains(&v))
4678 .unwrap_or(7);
4679 let arm = if self.sampler_config.temperature < 1e-6 {
4680 "greedy"
4681 } else {
4682 "sampling"
4683 };
4684 format!(
4685 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4686 Self::draft_vocab_rows(usize::MAX)
4687 )
4688 } else if !self.speculative {
4689 "spec off (CMF_MTP=0)".to_string()
4690 } else if self.mtp.is_none() {
4691 "spec off (no MTP head)".to_string()
4692 } else if !spec_sampling_ok {
4693 if spec_cheap_round {
4694 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4695 } else {
4696 "spec off (sampling without a top-k: the dense chain \
4697 costs more than it saves)"
4698 .to_string()
4699 }
4700 } else if !spec_wanted {
4701 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4702 } else if task_mask.is_some() {
4703 "spec off (task mask)".to_string()
4704 } else {
4705 "spec off (O(1) attention)".to_string()
4706 };
4707 let on = |var: &str| {
4708 if std::env::var(var).as_deref() == Ok("0") {
4709 "off"
4710 } else {
4711 "on"
4712 }
4713 };
4714 tracing::info!(
4715 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4716 MTP graph {}, attend {}, probe {}",
4717 if crate::gpu_metal::state4_on() {
4718 "on"
4719 } else {
4720 "off"
4721 },
4722 if crate::gpu_metal::async_replay_on() {
4723 "on"
4724 } else {
4725 "off"
4726 },
4727 on("CMF_METAL_PREFILL"),
4728 on("CMF_MTP_GRAPH"),
4729 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4730 if crate::gpu::probe_enabled() {
4731 "bypassed (q1 force)"
4732 } else {
4733 "off"
4734 },
4735 );
4736 });
4737 }
4738 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4745 let spec_active = self.speculative
4746 && self.mtp.is_some()
4747 && task_mask.is_none()
4748 && !self.o1_active()
4749 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4750 let mut mtp = if spec_active { self.mtp.take() } else { None };
4753 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4754 eprintln!(
4755 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4756 mtp.is_some(),
4757 self.speculative,
4758 self.sampler_config.temperature < 1e-6,
4759 );
4760 }
4761 if let Some(m) = &mut mtp {
4762 m.kv.clear();
4763 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4765 self.mtp_graph_mode = None;
4766 }
4767 let mimo_spec = self.speculative
4771 && self.mimo_mtp.is_some()
4772 && task_mask.is_none()
4773 && !self.o1_active()
4774 && self.dyn_router.is_none()
4775 && self.sampler_config.temperature < 1e-6
4776 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4777 if let Some(st) = self.mimo_mtp.as_mut() {
4778 st.reset();
4779 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4780 Self::mimo_mtp_hist_cap(st, input_ids.len());
4781 }
4782 }
4783 let mut router = if mtp.is_none() {
4787 self.dyn_router.take()
4788 } else {
4789 None
4790 };
4791 let mut reuse_from = reuse_from;
4792 if let Some(r) = &mut router {
4793 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4796 let _ = self.set_active_skill(None);
4799 reuse_from = 0;
4800 self.last_prefill_tokens = input_ids.len();
4801 }
4802 }
4803
4804 let mut all_ids = input_ids.to_vec();
4805 let mut generated = 0usize;
4806 let mut finish_reason = "max_tokens".to_string();
4807 let mut drafted = 0usize;
4808 let mut accepted = 0usize;
4809 let mut dsv4_spec_bad = 0usize;
4816 let mut dsv4_spec_retry_at = 0usize;
4817 let mut confidence: Vec<f32> = Vec::new();
4818 let trace_on = self.trace;
4819 let calib_temp = self.calib_temp;
4820 let mut traces: Vec<TokenTrace> = Vec::new();
4821
4822 let mut hidden = vec![0.0f32; self.hidden_size];
4828 let mut pos = reuse_from;
4829 let fuse_lm = mtp.is_none()
4838 && router.is_none()
4839 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4840 self.graph_logits = None;
4841 self.graph_want_logits = false;
4842 let _tpf = std::time::Instant::now();
4843 let batch_k = self.generation_batch_k();
4844 if let Some(rows) = prompt_rows {
4845 let hs = self.hidden_size;
4846 let chunk = self.prefill_chunk().max(1);
4847 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4848 let end = (pos + chunk).min(input_ids.len());
4849 let hb = match self.prefill_input_rows(
4850 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
4851 pos,
4852 task_mask,
4853 ) {
4854 Ok(hb) => hb,
4855 Err(err) => {
4856 self.finish_generation(&mut mtp, &mut router, true);
4857 return Err(err);
4858 }
4859 };
4860 if mimo_spec {
4861 self.mimo_note_rows(&hb, pos);
4862 }
4863 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4864 pos = end;
4865 }
4866 }
4867 while self.qwen4_exp.is_some()
4878 && mtp.is_none()
4879 && pos < input_ids.len()
4880 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4881 {
4882 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4885 let want_logits = end == input_ids.len();
4886 let mut lg = Vec::new();
4887 if let Some(b) = &mut self.qwen4_exp {
4888 crate::qwen4_exp::forward_tokens(
4889 &b.0,
4890 &b.1,
4891 &b.2,
4892 &mut b.3,
4893 &input_ids[pos..end],
4894 pos,
4895 &self.inv_freq,
4896 self.pool.as_deref(),
4897 &mut lg,
4898 want_logits,
4899 );
4900 }
4901 if want_logits {
4902 self.graph_logits = Some(lg);
4903 }
4904 pos = end;
4905 hidden.fill(0.0);
4906 }
4907 while self.dsv4.is_some()
4908 && mtp.is_none()
4909 && pos < input_ids.len()
4910 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4911 {
4912 let end = (pos + prefill_chunk()).min(input_ids.len());
4913 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4914 let mut lg = Vec::new();
4915 if let Some(b) = &mut self.dsv4 {
4916 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4917 crate::dsv4::forward_chunk(
4918 g,
4919 layers,
4920 &cfg,
4921 st,
4922 &ids,
4923 pos,
4924 &self.inv_freq,
4925 self.pool.as_deref(),
4926 &mut lg,
4927 end == input_ids.len(),
4928 );
4929 }
4930 if end == input_ids.len() {
4931 self.graph_logits = Some(lg);
4932 }
4933 pos = end;
4934 hidden = vec![0.0; self.hidden_size];
4935 }
4936 let dsv41_prefill = self.dsv41_prefill.take();
4937 while self.dsv41.is_some()
4938 && mtp.is_none()
4939 && pos < input_ids.len()
4940 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4941 {
4942 let end = (pos + prefill_chunk()).min(input_ids.len());
4943 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4944 let mut lg = Vec::new();
4945 if let Some(b) = &mut self.dsv41 {
4946 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4947 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4948 crate::dsv41::forward_chunk_masked_with_embeddings(
4949 g,
4950 layers,
4951 cfg,
4952 st,
4953 &ids,
4954 pos,
4955 &embeddings[pos..end],
4956 &participates[pos..end],
4957 self.pool.as_deref(),
4958 &mut lg,
4959 );
4960 } else {
4961 crate::dsv41::forward_chunk(
4962 g,
4963 layers,
4964 cfg,
4965 st,
4966 &ids,
4967 pos,
4968 self.pool.as_deref(),
4969 &mut lg,
4970 );
4971 }
4972 }
4973 if end == input_ids.len() {
4974 self.graph_logits = Some(lg);
4975 }
4976 pos = end;
4977 hidden = vec![0.0; self.hidden_size];
4978 }
4979 let dyn_prefill = router.is_some();
4984 let o1_prefill_limit = o1_prefill
4992 .and_then(|requested| self.o1_effective_boundary(requested))
4993 .map(|boundary| boundary.min(input_ids.len()));
4994 let mut o1_sealed = false;
4995 if let Some(limit) = o1_prefill_limit {
4996 if self.can_prefill_batched() && limit > 2 {
4999 let chunk = self.prefill_chunk();
5000 let hs = self.hidden_size;
5001 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5002 let end = (pos + chunk).min(limit);
5003 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5004 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5005 pos = end;
5006 }
5007 } else {
5008 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5009 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5010 pos += 1;
5011 }
5012 }
5013 if pos >= limit {
5014 o1_sealed = match self.o1_seal_checked() {
5015 Ok(sealed) => sealed,
5016 Err(err) => {
5017 self.finish_generation(&mut mtp, &mut router, true);
5018 return Err(err);
5019 }
5020 };
5021 tracing::info!(
5022 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5023 o1_prefill.unwrap_or(0),
5024 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5025 .unwrap_or(limit),
5026 limit,
5027 input_ids.len()
5028 );
5029 }
5030 }
5031 let graph_prefill = self.graph_prefill_preferred();
5037 #[cfg(target_os = "macos")]
5045 if task_mask.is_none()
5046 && !dyn_prefill
5047 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5048 && crate::gpu::enabled_here()
5049 && self.gdn_cfg.is_some()
5050 && self.g3n.is_none()
5051 && input_ids.len() > 8
5052 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5053 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5054 {
5055 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5056 .ok()
5057 .and_then(|v| v.parse().ok())
5058 .filter(|&v| (16..=512).contains(&v))
5059 .unwrap_or(256);
5060 let hs = self.hidden_size;
5061 let _tp = std::time::Instant::now();
5062 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5063 let end = (pos + chunk).min(input_ids.len());
5064 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5065 MetalPrefillOutcome::Completed(hb) => hb,
5066 MetalPrefillOutcome::Declined => break,
5067 MetalPrefillOutcome::Failed => {
5068 self.finish_generation(&mut mtp, &mut router, true);
5069 return Err("ordinary Metal prefill failed after admission".into());
5070 }
5071 };
5072 if let Some(m) = &mut mtp {
5073 let n_pairs = if end < input_ids.len() {
5074 end - pos
5075 } else {
5076 end - pos - 1
5077 };
5078 if n_pairs > 0 {
5079 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5080 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5081 .collect();
5082 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5083 for (j, (h, t)) in pairs.iter().enumerate() {
5084 let h = h.to_vec();
5085 let _ = self.mtp_step(m, &h, *t, pos + j);
5086 }
5087 }
5088 }
5089 }
5090 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5091 pos = end;
5092 }
5093 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5094 eprintln!(
5095 "metal-prefill: {} of {} tokens in {:.1} ms",
5096 pos,
5097 input_ids.len(),
5098 _tp.elapsed().as_secs_f64() * 1e3
5099 );
5100 }
5101 }
5102 #[cfg(not(target_os = "macos"))]
5107 if task_mask.is_none()
5108 && !dyn_prefill
5109 && mtp.is_none()
5110 && o1_prefill.is_none()
5111 && batch_k > 0
5112 && pos + 1 < input_ids.len()
5113 && self.g4_batch_prefill()
5114 {
5115 let t_g4 = std::time::Instant::now();
5116 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5117 Ok((n, last)) => {
5118 if n > 0 {
5119 hidden.copy_from_slice(&last);
5120 }
5121 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5122 eprintln!(
5123 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5124 input_ids.len() - pos,
5125 t_g4.elapsed().as_secs_f64() * 1e3
5126 );
5127 }
5128 pos += n;
5129 }
5130 Err(e) => {
5131 self.finish_generation(&mut mtp, &mut router, true);
5132 return Err(e);
5133 }
5134 }
5135 }
5136 self.mimo_moe_prepare();
5137 #[cfg(not(target_os = "macos"))]
5143 if task_mask.is_none()
5144 && !dyn_prefill
5145 && !graph_prefill
5146 && mtp.is_none()
5147 && o1_prefill.is_none()
5148 && !self.o1_active()
5149 && input_ids.len() > 2
5150 && self.batch_prefix_prefill()
5151 {
5152 let chunk = self.prefill_chunk().max(1);
5153 let hs = self.hidden_size;
5154 let t_bp = std::time::Instant::now();
5155 let pos0 = pos;
5156 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5157 let end = (pos + chunk).min(input_ids.len());
5158 let bk = end - pos;
5159 let mut hiddens = vec![0f32; bk * hs];
5160 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5161 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5162 }
5163 let positions: Vec<usize> = (pos..end).collect();
5164 let mut run = 0usize;
5165 let outcome = self.try_batch_graph_wgpu_prefix(
5166 &mut hiddens,
5167 &positions,
5168 bk,
5169 None,
5170 Some(&mut run),
5171 );
5172 match outcome {
5173 crate::gpu::BatchGraphOutcome::Completed => {
5174 let hb = if run < self.num_layers {
5175 self.prefill_batch_span(
5176 PrefillIn::Hidden(&hiddens),
5177 pos,
5178 None,
5179 run,
5180 self.num_layers,
5181 )
5182 } else {
5183 hiddens
5184 };
5185 if mimo_spec {
5186 self.mimo_note_rows(&hb, pos);
5187 }
5188 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5189 pos = end;
5190 }
5191 crate::gpu::BatchGraphOutcome::Failed => {
5192 self.finish_generation(&mut mtp, &mut router, true);
5193 return Err("batched prefix prefill failed after admission".into());
5194 }
5195 crate::gpu::BatchGraphOutcome::Declined => {
5196 #[cfg(feature = "gpu")]
5199 if pos > pos0 {
5200 self.pull_lagging_host_kv(0, self.num_layers, pos);
5201 }
5202 break;
5203 }
5204 }
5205 }
5206 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5207 eprintln!(
5208 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5209 pos - pos0,
5210 input_ids.len(),
5211 t_bp.elapsed().as_secs_f64() * 1e3
5212 );
5213 }
5214 }
5215 if task_mask.is_none()
5216 && !dyn_prefill
5217 && !graph_prefill
5218 && self.can_prefill_batched()
5219 && self.g3n.is_none()
5220 && o1_prefill.is_none()
5221 && input_ids.len() > 2
5222 {
5223 let chunk = self.prefill_chunk();
5229 let hs = self.hidden_size;
5230 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5231 let end = (pos + chunk).min(input_ids.len());
5232 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5233 if mimo_spec {
5234 self.mimo_note_rows(&hb, pos);
5235 }
5236 if let Some(m) = &mut mtp {
5237 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5238 .ok()
5239 .and_then(|v| v.parse().ok())
5240 .unwrap_or(0);
5241 for p in pos..end {
5242 if p + 1 < input_ids.len() {
5243 if probe >= 1 && p + 2 < input_ids.len() {
5244 let (d1, mut hx) = self.mtp_step_h(
5248 m,
5249 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5250 input_ids[p + 1],
5251 p,
5252 );
5253 let mut ok = d1 == input_ids[p + 2];
5254 Self::chain_probe_note(0, ok);
5255 let mut d_prev = d1;
5256 let mut extra = 0usize;
5257 for j in 1..probe {
5258 if p + 2 + j >= input_ids.len() {
5259 break;
5260 }
5261 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5262 extra += 1;
5263 ok = ok && dj == input_ids[p + 2 + j];
5264 Self::chain_probe_note(j, ok);
5265 d_prev = dj;
5266 hx = hj;
5267 }
5268 m.kv.truncate_last(extra);
5269 } else {
5270 let _ = self.mtp_step(
5271 m,
5272 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5273 input_ids[p + 1],
5274 p,
5275 );
5276 }
5277 }
5278 }
5279 }
5280 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5281 pos = end;
5282 }
5283 }
5284 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5285 if task_mask.is_none()
5286 && !dyn_prefill
5287 && !graph_prefill
5288 && !pair_off
5289 && self.pair_supported()
5290 && o1_prefill.is_none()
5291 {
5292 while pos + 1 < input_ids.len()
5293 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5294 {
5295 let e1 = self.embed_single(input_ids[pos]);
5296 let e2 = self.embed_single(input_ids[pos + 1]);
5297 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5298 if mimo_spec {
5299 self.mimo_note_rows(&h1, pos);
5300 self.mimo_note_rows(&h2, pos + 1);
5301 }
5302 self.commit_linear_scratch();
5304 if let Some(m) = &mut mtp {
5305 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5306 if pos + 2 < input_ids.len() {
5307 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5308 .ok()
5309 .and_then(|v| v.parse().ok())
5310 .unwrap_or(0);
5311 if probe >= 1 && pos + 3 < input_ids.len() {
5312 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5316 let mut ok = d1 == input_ids[pos + 3];
5317 Self::chain_probe_note(0, ok);
5318 let mut d_prev = d1;
5319 let mut extra = 0usize;
5320 for j in 1..probe {
5321 if pos + 3 + j >= input_ids.len() {
5322 break;
5323 }
5324 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5325 extra += 1;
5326 ok = ok && dj == input_ids[pos + 3 + j];
5327 Self::chain_probe_note(j, ok);
5328 d_prev = dj;
5329 hx = hj;
5330 }
5331 m.kv.truncate_last(extra);
5332 } else {
5333 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5334 }
5335 }
5336 }
5337 hidden = h2;
5338 pos += 2;
5339 }
5340 }
5341 let o1_batch_ready = o1_sealed
5354 && o1_prefill.is_some()
5355 && mtp.is_none()
5356 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5357 && (0..self.num_layers).all(|li| {
5358 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5359 cache.o1.is_none() || cache.o1_views().is_some()
5360 });
5361 let mtp_batch_prefill = mtp.is_some()
5366 && graph_prefill
5367 && task_mask.is_none()
5368 && !dyn_prefill
5369 && !self.o1_active()
5370 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5371 if batch_k > 0
5372 && (graph_prefill || o1_batch_ready)
5373 && task_mask.is_none()
5374 && (!self.o1_active() || o1_batch_ready)
5375 && (mtp.is_none() || mtp_batch_prefill)
5376 && !dyn_prefill
5377 && pos + 1 < input_ids.len()
5378 {
5379 let hs = self.hidden_size;
5380 let chunk = batch_k;
5381 while pos < input_ids.len() {
5382 let end = (pos + chunk).min(input_ids.len());
5383 let bk = end - pos;
5384 let mut hiddens = vec![0f32; bk * hs];
5385 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5386 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5387 }
5388 let positions: Vec<usize> = (pos..end).collect();
5389 let t_chunk = std::time::Instant::now();
5390 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5391 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5392 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5393 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5394 eprintln!(
5395 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5396 if o1_batch_ready {
5397 "o1"
5398 } else if mtp_batch_prefill {
5399 "ordinary_mtp"
5400 } else {
5401 "ordinary"
5402 },
5403 bk as f64 / (ms / 1000.0)
5404 );
5405 }
5406 {
5407 use std::sync::atomic::{AtomicBool, Ordering};
5408 static SAID: AtomicBool = AtomicBool::new(false);
5409 if !SAID.swap(true, Ordering::Relaxed) {
5410 if ok_b {
5411 tracing::info!(
5412 "batched prefill: ACTIVE mode={} (k={bk})",
5413 if o1_batch_ready {
5414 "o1"
5415 } else if mtp_batch_prefill {
5416 "ordinary_mtp"
5417 } else {
5418 "ordinary"
5419 }
5420 );
5421 } else {
5422 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5423 }
5424 }
5425 }
5426 if ok_b {
5427 if mimo_spec {
5428 self.mimo_note_rows(&hiddens, pos);
5429 }
5430 if mtp_batch_prefill {
5431 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5432 if n_pairs > 0 {
5433 let rows: Vec<Vec<f32>> = (0..n_pairs)
5439 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5440 .collect();
5441 let pairs: Vec<(&[f32], u32)> = rows
5442 .iter()
5443 .enumerate()
5444 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5445 .collect();
5446 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5447 eprintln!(
5448 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5449 pos,
5450 n_pairs,
5451 pos + n_pairs - 1,
5452 );
5453 }
5454 let warm_error = if let Some(m) = mtp.as_mut() {
5455 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5456 } else {
5457 None
5458 };
5459 if let Some(err) = warm_error {
5460 self.finish_generation(&mut mtp, &mut router, true);
5465 return Err(err.to_string());
5466 }
5467 }
5468 }
5469 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5470 pos = end;
5471 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5472 self.finish_generation(&mut mtp, &mut router, true);
5477 return Err(if o1_batch_ready {
5478 "sealed O(1) batch graph failed after admission".to_string()
5479 } else {
5480 "ordinary recurrent batch graph failed after admission".to_string()
5481 });
5482 } else {
5483 break; }
5485 }
5486 }
5487 if graph_prefill
5491 && task_mask.is_none()
5492 && mtp.is_none()
5493 && !dyn_prefill
5494 && pos == 0
5495 && input_ids.len() > 1
5496 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5497 {
5498 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5499 self.graph_logits = Some(lg);
5500 hidden = vec![0.0; self.hidden_size];
5501 pos = input_ids.len();
5502 }
5503 }
5504 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5505 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5506 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5507 if mimo_spec {
5508 self.mimo_note_rows(&hidden, pos);
5509 }
5510 if let Some(m) = &mut mtp {
5511 if pos + 1 < input_ids.len() {
5512 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5518 .ok()
5519 .and_then(|v| v.parse().ok())
5520 .unwrap_or(0);
5521 if probe >= 1 && pos + 2 < input_ids.len() {
5522 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5523 let mut ok = d1 == input_ids[pos + 2];
5524 Self::chain_probe_note(0, ok);
5525 let mut d_prev = d1;
5526 let mut extra = 0usize;
5527 for j in 1..probe {
5528 if pos + 2 + j >= input_ids.len() {
5529 break;
5530 }
5531 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5532 extra += 1;
5533 ok = ok && dj == input_ids[pos + 2 + j];
5534 Self::chain_probe_note(j, ok);
5535 d_prev = dj;
5536 hx = hj;
5537 }
5538 m.kv.truncate_last(extra);
5541 } else {
5542 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5543 }
5544 }
5545 }
5546 pos += 1;
5547 }
5548 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5549 eprintln!(
5550 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5551 input_ids.len(),
5552 _tpf.elapsed().as_secs_f64() * 1000.0
5553 );
5554 }
5555 if self
5556 .graph_failed
5557 .swap(false, std::sync::atomic::Ordering::Relaxed)
5558 {
5559 self.finish_generation(&mut mtp, &mut router, true);
5564 return Err("GPU token graph failed during prefill".to_string());
5565 }
5566 if self
5569 .cancel
5570 .swap(false, std::sync::atomic::Ordering::Relaxed)
5571 {
5572 self.finish_generation(&mut mtp, &mut router, true);
5576 return Ok(GenerateResult {
5577 text: String::new(),
5578 token_ids: Vec::new(),
5579 prompt_tokens: input_ids.len(),
5580 tokens_generated: 0,
5581 finish_reason: "cancelled".to_string(),
5582 mtp_drafted: 0,
5583 mtp_accepted: 0,
5584 token_confidence: Vec::new(),
5585 traces: Vec::new(),
5586 });
5587 }
5588
5589 if !o1_sealed {
5592 match self.o1_seal_checked() {
5593 Ok(_) => {}
5594 Err(err) => {
5595 self.finish_generation(&mut mtp, &mut router, true);
5596 return Err(err);
5597 }
5598 }
5599 }
5600
5601 macro_rules! commit {
5603 ($id:expr) => {{
5604 all_ids.push($id);
5605 generated += 1;
5606 self.note_draft_id($id);
5607 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5608 finish_reason = "stop".to_string();
5609 false
5610 } else {
5611 let token_text = self.tokenizer.decode_token($id);
5612 let mut go = true;
5613 if let Some(ref mut cb) = on_token {
5614 if !cb(&token_text) {
5615 finish_reason = "cancelled".to_string();
5616 go = false;
5617 }
5618 }
5619 go
5620 }
5621 }};
5622 }
5623
5624 let mut spec_trial = SpecTrial::Spec {
5635 t0: std::time::Instant::now(),
5636 gen0: generated,
5637 rounds: 0,
5638 };
5639 let mut spec_mon = SpecMon {
5645 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5646 ..SpecMon::default()
5647 };
5648 let mut spec_watchdog_off = false;
5649 let mut spec_walls: Vec<f32> = Vec::new();
5652 let mut spec_round_end: Option<std::time::Instant> = None;
5655 if mimo_spec {
5656 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5657 if let Some(mut st) = self.mimo_mtp.take() {
5658 self.mimo_mtp_probe(&mut st, input_ids, &path);
5659 self.mimo_mtp = Some(st);
5660 }
5661 }
5662 }
5663 let mut next_pos = input_ids.len();
5665 'decode: while generated < max_tokens {
5666 if self
5667 .graph_failed
5668 .swap(false, std::sync::atomic::Ordering::Relaxed)
5669 {
5670 self.finish_generation(&mut mtp, &mut router, true);
5675 return Err("GPU token graph failed during decode".to_string());
5676 }
5677 if self
5678 .cancel
5679 .swap(false, std::sync::atomic::Ordering::Relaxed)
5680 {
5681 finish_reason = "cancelled".to_string();
5682 break 'decode;
5683 }
5684 if mimo_spec && next_pos > 0 {
5689 self.mimo_note_rows(&hidden, next_pos - 1);
5692 }
5693 let forced = self.spec_forced.take();
5694 let mut logits = match (forced, self.graph_logits.take()) {
5695 (Some(_), _) => Vec::new(),
5696 (None, Some(lg)) => lg,
5697 (None, None) => {
5698 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5699 #[cfg(feature = "gpu")]
5704 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5705 lg
5706 } else {
5707 inference::rms_norm_into(
5708 &hidden,
5709 &self.weights.final_norm,
5710 self.rms_eps,
5711 self.norm_style,
5712 &mut self.ws.n1,
5713 );
5714 self.lm_head_forward(&self.ws.n1)
5715 };
5716 #[cfg(not(feature = "gpu"))]
5717 let logits_out = {
5718 inference::rms_norm_into(
5719 &hidden,
5720 &self.weights.final_norm,
5721 self.rms_eps,
5722 self.norm_style,
5723 &mut self.ws.n1,
5724 );
5725 self.lm_head_forward(&self.ws.n1)
5726 };
5727 logits_out
5728 }
5729 };
5730 if generated
5733 == std::env::var("CMF_LOGIT_DUMP_STEP")
5734 .ok()
5735 .and_then(|v| v.parse().ok())
5736 .unwrap_or(0)
5737 {
5738 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5739 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5740 for v in hidden.iter().chain(logits.iter()) {
5741 bytes.extend_from_slice(&v.to_le_bytes());
5742 }
5743 if let Err(e) = std::fs::write(&path, &bytes) {
5744 eprintln!("logit dump: failed to write {path}: {e}");
5745 self.finish_generation(&mut mtp, &mut router, true);
5746 return Err(format!("logit dump write failed: {e}"));
5747 }
5748 }
5749 }
5750 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5754 if !logits.is_empty() {
5755 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5756 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5757 if let Err(e) =
5758 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5759 {
5760 eprintln!("logit dump: failed to write {}: {e}", path.display());
5761 }
5762 }
5763 }
5764 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5765 Some(c) => c,
5766 None => {
5767 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5768 sampler::sample_with_scratch_pool(
5769 &logits,
5770 &self.sampler_config,
5771 self.sampler_config.penalty_past(&all_ids, bounded_native),
5772 &mut self.rng,
5773 &mut self.sampler_scratch,
5774 self.pool.as_deref(),
5775 )
5776 }
5777 };
5778 if self.confidence_on {
5779 confidence.push(if logits.is_empty() {
5780 0.0
5781 } else {
5782 sampler::top1_prob_pool(
5783 self.pool.as_deref(),
5784 &mut self.sampler_scratch,
5785 &logits,
5786 t_next,
5787 calib_temp,
5788 )
5789 });
5790 }
5791 if !logits.is_empty() {
5792 attention::recycle_buf(&mut logits);
5793 }
5794 if trace_on {
5795 let skill = router.as_ref().and_then(|r| r.active_id());
5799 traces.push(TokenTrace {
5800 t: generated,
5801 token_id: t_next,
5802 confidence: confidence.last().copied().unwrap_or(0.0),
5803 active_skill: skill,
5804 recon: None,
5805 switched: false,
5806 });
5807 }
5808 if !commit!(t_next) {
5809 break 'decode;
5810 }
5811 if generated >= max_tokens {
5812 break 'decode;
5813 }
5814
5815 self.swa_trim_tails();
5818 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5819 static SAID: std::sync::Once = std::sync::Once::new();
5825 SAID.call_once(|| {
5826 tracing::warn!(
5827 "KV cache full at {} positions — evicting half; quality \
5828 will degrade. Raise CMF_MAX_SEQ.",
5829 self.kv_cache.max_seq_len,
5830 );
5831 });
5832 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5833 self.kv_cache.evict(keep);
5834 }
5835
5836 if graph_spec {
5839 match spec_trial {
5840 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5841 spec_mon.plain_ms =
5842 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5843 let keep = spec_mon.pays();
5844 tracing::info!(
5845 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5846 spec_mon.tokens,
5847 spec_mon.round_ms,
5848 spec_mon.plain_ms,
5849 if keep { "speculating" } else { "plain" }
5850 );
5851 spec_mon.fails = 0;
5852 spec_trial = SpecTrial::Decided {
5853 spec: keep,
5854 recheck_at: if keep { usize::MAX } else { generated + 128 },
5855 };
5856 }
5857 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5858 spec_mon.n = 0;
5859 spec_trial = SpecTrial::Spec {
5860 t0: std::time::Instant::now(),
5861 gen0: generated,
5862 rounds: 0,
5863 };
5864 }
5865 _ => {}
5866 }
5867 spec_watchdog_off = matches!(
5868 spec_trial,
5869 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5870 );
5871 }
5872 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5874 let budget = max_tokens - generated - 1;
5875 if let Some(mut st) = self.mimo_mtp.take() {
5876 let k = st.depth.min(budget);
5877 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5878 self.mimo_mtp = Some(st);
5879 let r = match r {
5880 Ok(r) => r,
5881 Err(err) => {
5882 self.finish_generation(&mut mtp, &mut router, true);
5883 return Err(err);
5884 }
5885 };
5886 if let Some(r) = r {
5887 drafted += r.drafted;
5888 accepted += r.accepted.len();
5889 let mut stopped = false;
5890 for &id in &r.accepted {
5891 if self.confidence_on {
5892 confidence.push(0.0);
5893 }
5894 if !commit!(id) {
5895 stopped = true;
5896 break;
5897 }
5898 }
5899 if stopped {
5900 break 'decode;
5901 }
5902 next_pos += r.accepted.len() + 1;
5903 hidden = r.hidden;
5904 self.graph_logits = Some(r.logits);
5907 continue 'decode;
5908 }
5909 }
5910 }
5911 #[cfg(feature = "gpu")]
5914 if self.speculative
5915 && self.qwen4_exp.is_some()
5916 && task_mask.is_none()
5917 && self.sampler_config.temperature < 1e-6
5918 && generated + 1 < max_tokens
5919 && next_pos > 0
5920 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5921 {
5922 let r = match &mut self.qwen4_exp {
5923 Some(b) => crate::qwen4_exp::spec_round(
5924 &b.0,
5925 &b.1,
5926 &b.2,
5927 &mut b.3,
5928 next_pos,
5929 &all_ids,
5930 &self.inv_freq,
5931 self.pool.as_deref(),
5932 ),
5933 None => None,
5934 };
5935 if let Some(r) = r {
5936 drafted += r.drafted;
5937 accepted += r.accepted.len();
5938 let mut stopped = false;
5939 for &id in &r.accepted {
5940 if self.confidence_on {
5941 confidence.push(0.0);
5942 }
5943 if !commit!(id) {
5944 stopped = true;
5945 break;
5946 }
5947 }
5948 if stopped {
5949 break 'decode;
5950 }
5951 next_pos += r.accepted.len() + 1;
5952 hidden.fill(0.0);
5953 self.graph_logits = Some(r.logits);
5954 continue 'decode;
5955 }
5956 }
5957 match &mut mtp {
5958 #[cfg(feature = "gpu")]
5960 Some(m)
5961 if graph_spec
5962 && !spec_watchdog_off
5963 && generated + 1 < max_tokens
5964 && next_pos > 0 =>
5965 {
5966 let t_round = std::time::Instant::now();
5967 if spec_time_level() >= 2 {
5968 if let Some(t) = spec_round_end.take() {
5969 eprintln!(
5970 "spec-gap {:.2} ms (host between rounds)",
5971 t.elapsed().as_secs_f64() * 1e3
5972 );
5973 }
5974 }
5975 spec_stamps_begin();
5976 #[cfg(target_os = "macos")]
5981 let allocs0 =
5982 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
5983 #[cfg(not(target_os = "macos"))]
5984 let allocs0 = 0u64;
5985 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5986 m,
5987 &hidden,
5988 t_next,
5989 next_pos,
5990 &mut drafted,
5991 &mut accepted,
5992 &mut all_ids,
5993 max_tokens - generated,
5994 ) {
5995 next_pos = n_pos;
5996 hidden = new_h;
5997 let level = spec_time_level();
5998 if level > 0 {
5999 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6000 let stamps = spec_stamps_take();
6001 let median = if spec_walls.len() >= 3 {
6004 let mut s = spec_walls.clone();
6005 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6006 Some(s[s.len() / 2])
6007 } else {
6008 None
6009 };
6010 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6011 #[cfg(target_os = "macos")]
6012 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6013 .load(std::sync::atomic::Ordering::Relaxed)
6014 - allocs0;
6015 #[cfg(not(target_os = "macos"))]
6016 let allocs = allocs0;
6017 eprintln!(
6018 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6019 extra.len() + 1,
6020 if allocs > 0 {
6021 format!(" [{allocs} new device buffers]")
6022 } else {
6023 String::new()
6024 },
6025 match (outlier, median) {
6026 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6027 _ => String::new(),
6028 }
6029 );
6030 if level >= 2 || outlier {
6031 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6032 eprintln!(
6033 "spec-stamps: {}| untracked {:.1}",
6034 spec_stamps_format(&stamps),
6035 wall - sum
6036 );
6037 }
6038 if spec_mon.n >= 1 {
6039 spec_walls.push(wall);
6040 }
6041 }
6042 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6046 spec_trial = Self::spec_trial_round(
6049 spec_trial,
6050 &mut spec_mon,
6051 generated + extra.len() + 1,
6052 );
6053 let mut stopped = false;
6054 for &id in &extra {
6055 if self.confidence_on {
6056 confidence.push(0.0);
6057 }
6058 if !commit!(id) {
6059 stopped = true;
6060 break;
6061 }
6062 }
6063 if stopped {
6064 break 'decode;
6065 }
6066 if spec_time_level() >= 2 {
6067 spec_round_end = Some(std::time::Instant::now());
6068 }
6069 continue 'decode;
6070 }
6071 if self
6072 .graph_failed
6073 .swap(false, std::sync::atomic::Ordering::Relaxed)
6074 {
6075 self.finish_generation(&mut mtp, &mut router, true);
6081 return Err("GPU MTP graph failed during speculative decode".to_string());
6082 }
6083 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6094 spec_mon.tokens = 0.0;
6095 spec_mon.fails = 3;
6096 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6097 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6098 next_pos += 1;
6099 continue 'decode;
6100 }
6101 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6103 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6104 drafted += 1;
6105 let emb1 = self.embed_single(t_next);
6106 let emb2 = self.embed_single(draft);
6107 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6108
6109 inference::rms_norm_into(
6110 &h1,
6111 &self.weights.final_norm,
6112 self.rms_eps,
6113 self.norm_style,
6114 &mut self.ws.n1,
6115 );
6116 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6117 let t_after = sampler::sample_with_scratch_pool(
6118 &logits1,
6119 &self.sampler_config,
6120 self.sampler_config.penalty_past(&all_ids, bounded_native),
6121 &mut self.rng,
6122 &mut self.sampler_scratch,
6123 self.pool.as_deref(),
6124 );
6125 if self.confidence_on {
6126 confidence.push(sampler::top1_prob_pool(
6127 self.pool.as_deref(),
6128 &mut self.sampler_scratch,
6129 &logits1,
6130 t_after,
6131 calib_temp,
6132 ));
6133 }
6134 attention::recycle_buf(&mut logits1);
6135 if trace_on {
6136 traces.push(TokenTrace {
6139 t: generated,
6140 token_id: t_after,
6141 confidence: confidence.last().copied().unwrap_or(0.0),
6142 active_skill: None,
6143 recon: None,
6144 switched: false,
6145 });
6146 }
6147 let stop = !commit!(t_after);
6148
6149 if t_after == draft {
6150 accepted += 1;
6151 self.commit_linear_scratch();
6152 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6153 hidden = h2;
6154 next_pos += 2;
6155 } else {
6156 for layer in &mut self.kv_cache.layers {
6158 layer.truncate_last(1);
6159 }
6160 if !stop {
6161 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6162 hidden = self.forward_layers(
6163 &self.embed_single(t_after),
6164 next_pos + 1,
6165 None,
6166 );
6167 }
6168 next_pos += 2;
6169 }
6170 if stop {
6171 break 'decode;
6172 }
6173 }
6174 _ => {
6176 #[cfg(feature = "gpu")]
6181 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6182 static SAID: std::sync::Once = std::sync::Once::new();
6183 SAID.call_once(|| {
6184 eprintln!(
6185 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6186 !self.dsv4_mtp.is_empty(),
6187 task_mask.is_none(),
6188 router.is_none(),
6189 !trace_on,
6190 self.sampler_config.temperature < 1e-6,
6191 self.sampler_config.repetition_penalty == 1.0,
6192 );
6193 });
6194 }
6195 #[cfg(feature = "gpu")]
6196 if Self::dsv4_spec_on()
6197 && self.dsv4.is_some()
6198 && !self.dsv4_mtp.is_empty()
6199 && task_mask.is_none()
6200 && router.is_none()
6201 && !trace_on
6202 && self.sampler_config.temperature < 1e-6
6203 && self.sampler_config.repetition_penalty == 1.0
6204 && generated + 1 < max_tokens
6205 && all_ids.len() >= 2
6206 && generated >= dsv4_spec_retry_at
6207 {
6208 let tip_token = all_ids[all_ids.len() - 2];
6209 let drafted0 = drafted;
6210 let round = self.dsv4_spec_step(
6211 tip_token,
6212 t_next,
6213 next_pos,
6214 max_tokens.saturating_sub(generated),
6215 &mut drafted,
6216 &mut accepted,
6217 );
6218 if drafted > drafted0 {
6219 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6220 if useful {
6221 dsv4_spec_bad = 0;
6222 } else {
6223 dsv4_spec_bad += 1;
6224 if dsv4_spec_bad >= 2 {
6225 dsv4_spec_bad = 0;
6226 dsv4_spec_retry_at = generated.saturating_add(32);
6227 tracing::info!(
6228 "dsv4: draft не окупился дважды — точный walk на 32 токена"
6229 );
6230 }
6231 }
6232 }
6233 if let Some((extra, n_pos)) = round {
6234 next_pos = n_pos;
6235 let mut stopped = false;
6236 for &id in &extra {
6237 if self.confidence_on {
6238 confidence.push(0.0);
6239 }
6240 if !commit!(id) {
6241 stopped = true;
6242 break;
6243 }
6244 }
6245 if stopped {
6246 break 'decode;
6247 }
6248 continue 'decode;
6249 }
6250 }
6251 self.graph_want_logits = fuse_lm;
6252 let mut t_fwd = t_next;
6258 let pure_greedy = self.sampler_config.temperature < 1e-6
6259 && self.sampler_config.repetition_penalty == 1.0
6260 && self.sampler_config.presence_penalty == 0.0
6261 && self.sampler_config.suppress_tokens.is_empty();
6262 let burst_k = std::env::var("CMF_MULTISTEP")
6267 .ok()
6268 .and_then(|v| v.parse::<usize>().ok())
6269 .unwrap_or(0);
6270 if pure_greedy
6271 && burst_k >= 1
6272 && fuse_lm
6273 && task_mask.is_none()
6274 && router.is_none()
6275 && !trace_on
6276 && !self.confidence_on
6277 && std::env::var_os("CMF_FORCE_IDS").is_none()
6282 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6283 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6284 {
6285 let mut stopped = false;
6286 loop {
6287 if self
6292 .cancel
6293 .swap(false, std::sync::atomic::Ordering::Relaxed)
6294 {
6295 finish_reason = "cancelled".to_string();
6296 stopped = true;
6297 break;
6298 }
6299 let room = max_tokens.saturating_sub(generated);
6300 if room <= 2 {
6301 break;
6302 }
6303 let k = burst_k.min(room - 1);
6304 if k < 1 {
6305 break;
6306 }
6307 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6308 if self
6309 .graph_failed
6310 .swap(false, std::sync::atomic::Ordering::Relaxed)
6311 {
6312 self.finish_generation(&mut mtp, &mut router, true);
6313 return Err(
6314 "GPU token graph failed during greedy burst".to_string()
6315 );
6316 }
6317 break;
6318 };
6319 next_pos += k;
6320 for &id in &ids {
6321 if !commit!(id) {
6322 stopped = true;
6323 break;
6324 }
6325 }
6326 if stopped {
6327 break;
6328 }
6329 t_fwd = *ids.last().unwrap();
6330 }
6331 if stopped {
6332 break 'decode;
6333 }
6334 }
6335 #[cfg(target_os = "macos")]
6345 if graph_spec
6346 && spec_watchdog_off
6347 && next_pos > 0
6348 && self.mtp_graph_mode == Some(true)
6349 && crate::gpu::q1_force()
6350 {
6351 if let Some(m) = mtp.as_mut() {
6352 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6353 }
6354 }
6355 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6356 next_pos += 1;
6357 if let Some(r) = &mut router {
6360 let phi = self.dyn_phi_ema.clone();
6361 let decision = r.step(&phi, generated);
6362 if let Some(new_active) = decision {
6363 let _ = self.set_active_skill(new_active);
6364 }
6365 if trace_on {
6368 if let Some(last) = traces.last_mut() {
6369 let e = r.last_best_e();
6370 last.recon = e.is_finite().then_some(e);
6371 last.switched = decision.is_some();
6372 }
6373 }
6374 }
6375 }
6376 }
6377 }
6378
6379 let cancelled = finish_reason == "cancelled";
6380 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6384 if mimo_spec {
6385 if let Some(st) = self.mimo_mtp.as_ref() {
6386 let line = st.stats.line();
6387 tracing::info!("{line}");
6388 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6389 eprintln!("{line}");
6390 }
6391 }
6392 }
6393 self.finish_generation(&mut mtp, &mut router, cancelled);
6394
6395 let output_ids = &all_ids[input_ids.len()..];
6396 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6400 let consumed = std::mem::take(&mut all_ids);
6405 if dyn_switched {
6406 self.clear_sequence_state();
6407 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6408 self.clear_history();
6409 } else {
6410 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6411 }
6412 all_ids = consumed;
6413 let output_ids = &all_ids[input_ids.len()..];
6414 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6416 Ok(GenerateResult {
6417 text: self.tokenizer.decode(output_ids),
6418 token_ids: output_ids.to_vec(),
6419 prompt_tokens: input_ids.len(),
6420 tokens_generated: generated,
6421 finish_reason,
6422 mtp_drafted: drafted,
6423 mtp_accepted: accepted,
6424 token_confidence: confidence,
6425 traces,
6426 })
6427 }
6428
6429 fn mtp_step(
6433 &mut self,
6434 m: &mut MtpModule,
6435 hidden: &[f32],
6436 next_token: u32,
6437 position: usize,
6438 ) -> u32 {
6439 self.mtp_step_h(m, hidden, next_token, position).0
6440 }
6441
6442 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6446 use std::sync::Mutex;
6447 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6448 let mut t = T.lock().unwrap();
6449 if t.len() <= depth {
6450 t.resize(depth + 1, (0, 0));
6451 }
6452 t[depth].0 += 1;
6453 t[depth].1 += prefix_ok as u64;
6454 if depth == 0 && t[0].0 % 128 == 0 {
6455 let line: Vec<String> = t
6456 .iter()
6457 .enumerate()
6458 .map(|(d, (n, k))| {
6459 format!(
6460 "d{}={:.0}%({n})",
6461 d + 1,
6462 100.0 * *k as f64 / (*n).max(1) as f64
6463 )
6464 })
6465 .collect();
6466 eprintln!("mtp-chain: {}", line.join(" "));
6467 }
6468 }
6469
6470 fn mtp_step_hl(
6478 &mut self,
6479 m: &mut MtpModule,
6480 hidden: &[f32],
6481 next_token: u32,
6482 position: usize,
6483 ) -> (Vec<f32>, Vec<f32>) {
6484 #[cfg(target_os = "macos")]
6489 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6490 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6491 self.mtp_graph_mode = Some(true);
6492 return r;
6493 }
6494 if self.mtp_graph_mode == Some(true) {
6495 tracing::error!("mtp Metal graph failed after admission");
6496 self.clear_sequence_state();
6497 self.graph_failed
6498 .store(true, std::sync::atomic::Ordering::Relaxed);
6499 self.cancel
6500 .store(true, std::sync::atomic::Ordering::Relaxed);
6501 return (Vec::new(), Vec::new());
6502 }
6503 self.mtp_graph_mode = Some(false);
6504 }
6505 #[cfg(feature = "gpu")]
6506 if self.mtp_graph_mode != Some(false) {
6507 if !self.mtp_graph_ok(m) {
6508 if self.mtp_graph_mode == Some(true) {
6509 tracing::error!("mtp graph became unavailable after admission");
6514 self.clear_sequence_state();
6515 self.graph_failed
6516 .store(true, std::sync::atomic::Ordering::Relaxed);
6517 self.cancel
6518 .store(true, std::sync::atomic::Ordering::Relaxed);
6519 return (Vec::new(), Vec::new());
6520 }
6521 self.mtp_graph_mode = Some(false);
6522 } else {
6523 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6524 self.mtp_graph_mode = Some(true);
6525 return r;
6526 }
6527 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6528 return (Vec::new(), Vec::new());
6535 }
6536 tracing::error!("mtp graph failed or declined after admission");
6540 self.clear_sequence_state();
6541 self.graph_failed
6542 .store(true, std::sync::atomic::Ordering::Relaxed);
6543 self.cancel
6544 .store(true, std::sync::atomic::Ordering::Relaxed);
6545 return (Vec::new(), Vec::new());
6546 }
6547 }
6548 let e = self.embed_single(next_token);
6552 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6553 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6554 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6555 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6556 let mut x = vec![0.0f32; self.hidden_size];
6557 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6558
6559 let lw = &m.layer;
6561 inference::rms_norm_into(
6562 &x,
6563 &lw.input_norm,
6564 self.rms_eps,
6565 self.norm_style,
6566 &mut self.ws.n1,
6567 );
6568 let attn = match &lw.attn {
6569 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6571 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6572 AttnKind::Full {
6573 wq,
6574 wk,
6575 wv,
6576 wo,
6577 q_norm,
6578 k_norm,
6579 output_gate,
6580 softplus_gate,
6581 bias,
6582 } => {
6583 let mut cfg = self.attn_cfg(position);
6584 cfg.q_norm = q_norm.as_deref();
6585 cfg.k_norm = k_norm.as_deref();
6586 cfg.output_gate = *output_gate;
6587 cfg.softplus_gate = softplus_gate
6588 .as_ref()
6589 .map(|(gate, per_head)| (gate, *per_head));
6590 cfg.bias = bias
6591 .as_ref()
6592 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6593 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6594 }
6595 AttnKind::Linear(_)
6596 | AttnKind::LinearGdn(_)
6597 | AttnKind::ShortConv(_)
6598 | AttnKind::Bounded(_) => {
6599 unreachable!("MTP block is full attention")
6600 }
6601 };
6602 for (i, &a) in attn.iter().enumerate() {
6603 x[i] += a;
6604 }
6605 inference::rms_norm_into(
6606 &x,
6607 &lw.post_norm,
6608 self.rms_eps,
6609 self.norm_style,
6610 &mut self.ws.p1,
6611 );
6612 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6613 for (i, &f) in ffn.iter().enumerate() {
6614 x[i] += f;
6615 }
6616
6617 inference::rms_norm_into(
6618 &x,
6619 &m.final_norm,
6620 self.rms_eps,
6621 self.norm_style,
6622 &mut self.ws.n1,
6623 );
6624 let lg = self.lm_head_forward(&self.ws.n1);
6625 (lg, x)
6626 }
6627
6628 fn mtp_step_h(
6630 &mut self,
6631 m: &mut MtpModule,
6632 hidden: &[f32],
6633 next_token: u32,
6634 position: usize,
6635 ) -> (u32, Vec<f32>) {
6636 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6637 let draft = sampler::argmax(&lg);
6638 attention::recycle_buf(&mut lg);
6639 (draft, x)
6640 }
6641
6642 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6648 match trial {
6649 SpecTrial::Spec { t0, gen0, rounds } => {
6650 let rounds = rounds + 1;
6651 if rounds >= 5 {
6652 if mon.plain_ms > 0.0 {
6653 let keep = mon.pays();
6654 mon.fails = 0;
6655 tracing::info!(
6656 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6657 mon.tokens,
6658 mon.round_ms,
6659 mon.plain_ms,
6660 if keep { "speculating" } else { "plain" }
6661 );
6662 SpecTrial::Decided {
6663 spec: keep,
6664 recheck_at: if keep { usize::MAX } else { generated + 128 },
6665 }
6666 } else if mon.pays() {
6667 mon.fails = 0;
6672 tracing::info!(
6673 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6674 mon.tokens,
6675 mon.round_ms,
6676 );
6677 SpecTrial::Decided {
6678 spec: true,
6679 recheck_at: usize::MAX,
6680 }
6681 } else {
6682 SpecTrial::Plain {
6683 t0: std::time::Instant::now(),
6684 gen0: generated,
6685 }
6686 }
6687 } else {
6688 SpecTrial::Spec { t0, gen0, rounds }
6689 }
6690 }
6691 SpecTrial::Decided { spec: true, .. } => {
6692 if mon.pays() {
6693 mon.fails = 0;
6694 trial
6695 } else {
6696 mon.fails += 1;
6697 if mon.fails >= 4 {
6698 if mon.plain_ms <= 0.0 {
6699 tracing::info!(
6703 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6704 mon.tokens,
6705 mon.round_ms,
6706 );
6707 return SpecTrial::Plain {
6708 t0: std::time::Instant::now(),
6709 gen0: generated,
6710 };
6711 }
6712 tracing::info!(
6713 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6714 mon.tokens,
6715 mon.round_ms,
6716 mon.plain_ms
6717 );
6718 SpecTrial::Decided {
6719 spec: false,
6720 recheck_at: generated + 128,
6721 }
6722 } else {
6723 trial
6724 }
6725 }
6726 }
6727 other => other,
6728 }
6729 }
6730
6731 fn mtp_kv_id(&self) -> u64 {
6734 self.graph_kv_id | (1u64 << 40)
6735 }
6736
6737 const MTP_LAYER_BASE: usize = 0;
6742
6743 #[cfg(feature = "gpu")]
6750 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6751 self.mtp_graph_mode != Some(true)
6752 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6753 }
6754
6755 #[cfg(feature = "gpu")]
6761 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6762 let mut ok = true;
6763 let mut expected = false;
6764 for li in 0..self.num_layers {
6765 if matches!(
6766 self.weights.layers[self.phys_layer(li)].attn,
6767 AttnKind::Full { .. }
6768 ) {
6769 expected = true;
6770 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6771 }
6772 }
6773 !expected || ok
6774 }
6775
6776 fn graph_gdn_layer_count(&self) -> usize {
6780 (0..self.num_layers)
6781 .filter(|&li| {
6782 matches!(
6783 &self.weights.layers[self.phys_layer(li)].attn,
6784 AttnKind::LinearGdn(_)
6785 )
6786 })
6787 .count()
6788 }
6789
6790 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6793 let e = self.embed_single(next_token);
6794 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6795 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6796 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6797 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6798 let mut x = vec![0.0f32; self.hidden_size];
6799 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6800 x
6801 }
6802
6803 #[cfg(feature = "gpu")]
6806 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6807 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6808 return false;
6809 }
6810 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6811 || !crate::gpu::enabled_here()
6812 || self.attn_softcap > 0.0
6813 || self.attention_heads_per_layer.is_some()
6814 || self.v_head_dim.is_some()
6817 {
6818 return false;
6819 }
6820 matches!(
6821 &m.layer.attn,
6822 AttnKind::Full {
6823 softplus_gate: None,
6824 ..
6825 }
6826 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6827 }
6828
6829 #[cfg(feature = "gpu")]
6833 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6834 if !self.mtp_block_graph_ok(m) {
6835 return false;
6836 }
6837 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6838 return false;
6839 };
6840 let FfnKind::Dense(d) = &m.layer.ffn else {
6841 return false;
6842 };
6843 d.segs.is_empty()
6844 && wq.graph_weight().is_some()
6845 && wk.graph_weight().is_some()
6846 && wv.graph_weight().is_some()
6847 && wo.graph_weight().is_some()
6848 && d.gate_proj.graph_weight().is_some()
6849 && d.up_proj.graph_weight().is_some()
6850 && d.down_proj.graph_weight().is_some()
6851 && self.weights.lm_head.graph_weight().is_some()
6852 }
6853
6854 #[cfg(feature = "gpu")]
6860 fn mtp_step_graph(
6861 &mut self,
6862 m: &mut MtpModule,
6863 hidden: &[f32],
6864 next_token: u32,
6865 position: usize,
6866 ) -> Option<(Vec<f32>, Vec<f32>)> {
6867 if !self.mtp_graph_ok(m) {
6868 return None;
6869 }
6870 let lw = &m.layer;
6871 let AttnKind::Full {
6872 wq,
6873 wk,
6874 wv,
6875 wo,
6876 q_norm,
6877 k_norm,
6878 output_gate,
6879 softplus_gate,
6880 bias,
6881 } = &lw.attn
6882 else {
6883 return None;
6884 };
6885 if softplus_gate.is_some() {
6886 return None;
6887 }
6888 let FfnKind::Dense(d) = &lw.ffn else {
6889 return None;
6890 };
6891 if !d.segs.is_empty() {
6892 return None; }
6894 let mut x = self.mtp_block_input(m, hidden, next_token);
6897 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6898 let (_, i, kind, rs) = t.graph_weight()?;
6899 Some(crate::gpu::GraphW {
6900 idx: i,
6901 kind,
6902 row_scale: rs,
6903 data: &[],
6904 prism: crate::gpu::GraphPrismOp::None,
6905 affine: false,
6906 })
6907 }
6908 let (model, _, _, _) = wq.graph_weight()?;
6909 let model = model.clone();
6910 let (lm_gw, lm_rows) = {
6911 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6912 let rows = if kind == 6 {
6916 self.draft_head_rows(self.weights.lm_head.rows())
6917 } else {
6918 self.weights.lm_head.rows()
6919 };
6920 (
6921 crate::gpu::GraphW {
6922 idx: i,
6923 kind,
6924 row_scale: rs,
6925 data: &[],
6926 prism: crate::gpu::GraphPrismOp::None,
6927 affine: false,
6928 },
6929 rows,
6930 )
6931 };
6932 let layer = crate::gpu::GraphLayer {
6933 input_norm: &lw.input_norm,
6934 attn: crate::gpu::GraphAttn::Full {
6935 wq: gw(wq)?,
6936 wk: gw(wk)?,
6937 wv: gw(wv)?,
6938 wo: gw(wo)?,
6939 q_norm: q_norm.as_deref(),
6940 k_norm: k_norm.as_deref(),
6941 late_qk_norm: self.qk_norm_after_rope,
6942 bias: bias
6943 .as_ref()
6944 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6945 output_gate: *output_gate,
6946 cpu_k: m.kv.k_heads(),
6947 cpu_v: m.kv.v_heads(),
6948 cpu_base: m.kv.base(),
6949 geom: None,
6950 head_gate: None,
6951 },
6952 post_norm: &lw.post_norm,
6953 attn_out_norm: None,
6954 ffn_out_norm: None,
6955 layer_scale: None,
6956 ffn: crate::gpu::GraphFfn::Dense {
6957 gate: gw(&d.gate_proj)?,
6958 up: gw(&d.up_proj)?,
6959 down: gw(&d.down_proj)?,
6960 act: d.act.graph_act()?,
6961 },
6962 };
6963 let nh = self.num_heads;
6964 let (nkv, hd, rd) = self.layer_geom(0);
6965 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6966 let mut logits = Vec::new();
6967 let ok = crate::gpu::forward_token_graph(
6968 &model,
6969 self.mtp_kv_id(),
6970 std::slice::from_ref(&layer),
6971 &[None],
6972 self.o1_epoch,
6973 &self.inv_freq,
6974 &mut x,
6975 nh,
6976 nkv,
6977 hd,
6978 self.attn_scale,
6979 rd,
6980 self.hidden_size,
6981 self.intermediate_size,
6982 position,
6983 self.kv_cache.max_seq_len,
6984 gemma,
6985 self.rms_eps as f32,
6986 Some((&lm_gw, lm_rows)),
6987 &m.final_norm,
6988 &mut logits,
6989 &[],
6990 1,
6991 None,
6992 None,
6993 None,
6994 Self::MTP_LAYER_BASE,
6995 true,
6996 );
6997 match ok {
6998 crate::gpu::TokenGraphOutcome::Completed => {}
6999 crate::gpu::TokenGraphOutcome::Declined => return None,
7000 crate::gpu::TokenGraphOutcome::Failed => {
7001 self.clear_sequence_state();
7005 self.graph_failed
7006 .store(true, std::sync::atomic::Ordering::Relaxed);
7007 self.cancel
7008 .store(true, std::sync::atomic::Ordering::Relaxed);
7009 return None;
7010 }
7011 }
7012 logits.resize(self.vocab_size, 0.0);
7013 Some((logits, x))
7014 }
7015
7016 #[cfg(feature = "gpu")]
7024 fn mtp_warm_graph(
7025 &mut self,
7026 m: &mut MtpModule,
7027 pairs: &[(&[f32], u32)],
7028 first_pos: usize,
7029 ) -> crate::gpu::BatchGraphOutcome {
7030 if pairs.is_empty() {
7031 return crate::gpu::BatchGraphOutcome::Completed;
7032 }
7033 if !self.mtp_block_graph_ok(m) {
7034 return crate::gpu::BatchGraphOutcome::Declined;
7035 }
7036 let hs = self.hidden_size;
7037 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7040 for (h, t) in pairs {
7041 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7042 }
7043 let lw = &m.layer;
7044 let AttnKind::Full {
7045 wq,
7046 wk,
7047 wv,
7048 wo,
7049 q_norm,
7050 k_norm,
7051 output_gate,
7052 bias,
7053 ..
7054 } = &lw.attn
7055 else {
7056 return crate::gpu::BatchGraphOutcome::Declined;
7057 };
7058 let FfnKind::Dense(d) = &lw.ffn else {
7059 return crate::gpu::BatchGraphOutcome::Declined;
7060 };
7061 if !d.segs.is_empty() {
7062 return crate::gpu::BatchGraphOutcome::Declined; }
7064 let (
7067 AttnKind::Full {
7068 softplus_gate: None,
7069 ..
7070 },
7071 Some(gact),
7072 ) = (&lw.attn, d.act.graph_act())
7073 else {
7074 return crate::gpu::BatchGraphOutcome::Declined;
7075 };
7076 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7077 let (_, i, kind, rs) = t.graph_weight()?;
7078 Some(crate::gpu::GraphW {
7079 idx: i,
7080 kind,
7081 row_scale: rs,
7082 data: &[],
7083 prism: crate::gpu::GraphPrismOp::None,
7084 affine: false,
7085 })
7086 }
7087 let Some((model, _, _, _)) = wq.graph_weight() else {
7088 return crate::gpu::BatchGraphOutcome::Declined;
7089 };
7090 let model = model.clone();
7091 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7092 gw(wq),
7093 gw(wk),
7094 gw(wv),
7095 gw(wo),
7096 gw(&d.gate_proj),
7097 gw(&d.up_proj),
7098 gw(&d.down_proj),
7099 ) else {
7100 return crate::gpu::BatchGraphOutcome::Declined;
7101 };
7102 let layer = crate::gpu::GraphLayer {
7103 input_norm: &lw.input_norm,
7104 attn: crate::gpu::GraphAttn::Full {
7105 wq: gwq,
7106 wk: gwk,
7107 wv: gwv,
7108 wo: gwo,
7109 q_norm: q_norm.as_deref(),
7110 k_norm: k_norm.as_deref(),
7111 late_qk_norm: self.qk_norm_after_rope,
7112 bias: bias
7113 .as_ref()
7114 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7115 output_gate: *output_gate,
7116 cpu_k: m.kv.k_heads(),
7117 cpu_v: m.kv.v_heads(),
7118 cpu_base: m.kv.base(),
7119 geom: None,
7120 head_gate: None,
7121 },
7122 post_norm: &lw.post_norm,
7123 attn_out_norm: None,
7124 ffn_out_norm: None,
7125 layer_scale: None,
7126 ffn: crate::gpu::GraphFfn::Dense {
7127 gate: gg,
7128 up: gu,
7129 down: gd,
7130 act: gact,
7131 },
7132 };
7133 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7134 let nh = self.num_heads;
7135 let (nkv, hd, rd) = self.layer_geom(0);
7136 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7137 crate::gpu::forward_batch_graph(
7138 &model,
7139 self.mtp_kv_id(),
7140 std::slice::from_ref(&layer),
7141 &self.inv_freq,
7142 &mut hiddens,
7143 nh,
7144 nkv,
7145 hd,
7146 rd,
7147 hs,
7148 self.intermediate_size,
7149 &positions,
7150 self.kv_cache.max_seq_len,
7151 gemma,
7152 self.rms_eps as f32,
7153 self.attn_scale,
7154 pairs.len(),
7155 &[],
7156 0,
7157 None,
7158 None,
7159 )
7160 }
7161
7162 #[cfg(feature = "gpu")]
7169 fn mtp_warm_graph_fallback(
7170 &mut self,
7171 m: &mut MtpModule,
7172 pairs: &[(&[f32], u32)],
7173 first_pos: usize,
7174 ) -> bool {
7175 if pairs.is_empty() {
7176 return true;
7177 }
7178 let graphable = self.mtp_block_graph_ok(m);
7179 if !graphable {
7180 if self.mtp_graph_mode == Some(true) {
7184 return false;
7185 }
7186 self.mtp_graph_mode = Some(false);
7187 for (j, (h, t)) in pairs.iter().enumerate() {
7188 self.mtp_warm(m, h, *t, first_pos + j);
7189 }
7190 return true;
7191 }
7192
7193 for (j, (h, t)) in pairs.iter().enumerate() {
7198 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7199 return false;
7200 }
7201 }
7202 self.mtp_graph_mode = Some(true);
7203 true
7204 }
7205
7206 #[cfg(feature = "gpu")]
7211 fn mtp_warm_prefill_pairs(
7212 &mut self,
7213 m: &mut MtpModule,
7214 pairs: &[(&[f32], u32)],
7215 first_pos: usize,
7216 ) -> Result<(), &'static str> {
7217 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7222 if self.mtp_graph_mode == Some(true) {
7223 return Err("MTP token graph became unavailable after admission");
7224 }
7225 self.mtp_graph_mode = Some(false);
7226 for (j, (h, t)) in pairs.iter().enumerate() {
7227 self.mtp_warm(m, h, *t, first_pos + j);
7228 }
7229 return Ok(());
7230 }
7231 match self.mtp_warm_graph(m, pairs, first_pos) {
7232 crate::gpu::BatchGraphOutcome::Completed => {
7233 if !pairs.is_empty() {
7234 self.mtp_graph_mode = Some(true);
7235 }
7236 Ok(())
7237 }
7238 crate::gpu::BatchGraphOutcome::Declined => {
7239 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7240 Ok(())
7241 } else {
7242 Err("MTP warm-up fallback failed after device admission")
7243 }
7244 }
7245 crate::gpu::BatchGraphOutcome::Failed => {
7246 Err("MTP warm batch graph failed after admission")
7247 }
7248 }
7249 }
7250
7251 #[cfg(not(feature = "gpu"))]
7252 fn mtp_warm_prefill_pairs(
7253 &mut self,
7254 m: &mut MtpModule,
7255 pairs: &[(&[f32], u32)],
7256 first_pos: usize,
7257 ) -> Result<(), &'static str> {
7258 for (j, (h, t)) in pairs.iter().enumerate() {
7259 self.mtp_warm(m, h, *t, first_pos + j);
7260 }
7261 Ok(())
7262 }
7263
7264 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7268 let e = self.embed_single(next_token);
7269 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7270 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7271 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7272 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7273 let mut x = vec![0.0f32; self.hidden_size];
7274 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7275 inference::rms_norm_into(
7276 &x,
7277 &m.layer.input_norm,
7278 self.rms_eps,
7279 self.norm_style,
7280 &mut self.ws.n1,
7281 );
7282 let attn = match &m.layer.attn {
7283 AttnKind::Full {
7284 wq,
7285 wk,
7286 wv,
7287 wo,
7288 q_norm,
7289 k_norm,
7290 output_gate,
7291 softplus_gate,
7292 bias,
7293 } => {
7294 let mut cfg = self.attn_cfg(position);
7295 cfg.q_norm = q_norm.as_deref();
7296 cfg.k_norm = k_norm.as_deref();
7297 cfg.output_gate = *output_gate;
7298 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7299 cfg.bias = bias
7300 .as_ref()
7301 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7302 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7303 }
7304 _ => return,
7305 };
7306 let _ = attn;
7307 }
7308
7309 #[cfg(feature = "gpu")]
7316 #[allow(clippy::too_many_arguments)]
7317 fn graph_spec_step(
7318 &mut self,
7319 m: &mut MtpModule,
7320 hidden: &[f32],
7321 t_next: u32,
7322 next_pos: usize,
7323 drafted: &mut usize,
7324 accepted: &mut usize,
7325 all_ids: &mut Vec<u32>,
7329 room: usize,
7334 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7335 #[cfg(target_os = "macos")]
7346 let metal_native = crate::gpu::q1_force();
7347 #[cfg(not(target_os = "macos"))]
7348 let metal_native = false;
7349 #[cfg(feature = "gpu")]
7350 let k_default = if metal_native {
7351 7
7354 } else if crate::gpu_wgpu::verify_i8_on() {
7355 5
7356 } else {
7357 4
7358 };
7359 #[cfg(not(feature = "gpu"))]
7360 let k_default = 4;
7361 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7362 .ok()
7363 .and_then(|v| v.parse().ok())
7364 .filter(|&v| (1..=8).contains(&v));
7365 let (k_start, k_max) = if metal_native {
7370 (7, 7)
7371 } else {
7372 (3, k_default.max(5))
7373 };
7374 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7375 let k_spec = k_full.min(room).max(1);
7376 let k_capped = k_spec < k_full;
7379 if next_pos == 0 {
7380 return None;
7381 }
7382 let t_round = std::time::Instant::now();
7383 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7399 let sub0 = subs();
7400 let cfg = self.sampler_config.clone();
7405 let penalized = !(cfg.repetition_penalty == 1.0
7406 && cfg.presence_penalty == 0.0
7407 && cfg.suppress_tokens.is_empty());
7408 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7413 let sampling = cfg.temperature >= 1e-6;
7414 let sparse = sampling && sampler::sparse_ok(&cfg);
7420 let base_len = all_ids.len();
7421 if sampling && !sparse && self.spec_q.len() < k_spec {
7422 self.spec_q.resize_with(k_spec, Vec::new);
7423 }
7424 if sparse && self.spec_qs.len() < k_spec {
7425 self.spec_qs.resize_with(k_spec, Vec::new);
7426 }
7427 let mut drafts = Vec::with_capacity(k_spec);
7432 let mut hx = hidden.to_vec();
7433 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7436 spec_stamp("pro");
7437 #[cfg(target_os = "macos")]
7443 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7444 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7445 Ok(ids) => {
7446 self.mtp_graph_mode = Some(true);
7447 drafts = ids;
7448 }
7449 Err(true) => {
7450 tracing::error!("mtp Metal draft chain failed after commit");
7451 self.clear_sequence_state();
7452 self.graph_failed
7453 .store(true, std::sync::atomic::Ordering::Relaxed);
7454 self.cancel
7455 .store(true, std::sync::atomic::Ordering::Relaxed);
7456 return None;
7457 }
7458 Err(false) => {}
7459 }
7460 }
7461 for j in drafts.len()..k_spec {
7462 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7463 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7464 if spec_dbg {
7465 let saved = self.mtp_graph_mode;
7466 self.mtp_graph_mode = Some(false);
7467 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7468 self.mtp_graph_mode = saved;
7469 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7470 return None;
7471 }
7472 m.kv.truncate_last(1);
7473 dbg_ref = Some(r);
7474 }
7475 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7476 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7477 return None;
7478 }
7479 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7480 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7481 let dl = lg
7482 .iter()
7483 .zip(&lg_cpu)
7484 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7485 let dh = hj
7486 .iter()
7487 .zip(&h_cpu)
7488 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7489 eprintln!(
7490 "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 {}",
7491 next_pos - 1 + j,
7492 sampler::argmax(&lg_cpu),
7493 sampler::argmax(&lg),
7494 n(&h_cpu),
7495 n(&hj),
7496 m.kv.seq_len
7497 );
7498 }
7499 let dj = if sparse {
7500 let mut q = std::mem::take(&mut self.spec_qs[j]);
7501 let ok = sampler::sparse_distribution_into(
7502 &lg,
7503 &cfg,
7504 all_ids,
7505 &mut self.sampler_scratch,
7506 self.pool.as_deref(),
7507 &mut q,
7508 );
7509 let d = if ok {
7510 sampler::draw_sparse(&q, &mut self.rng)
7511 } else {
7512 let t = sampler::argmax(&lg);
7514 q.clear();
7515 q.push((t, 1.0));
7516 t
7517 };
7518 self.spec_qs[j] = q;
7519 all_ids.push(d);
7520 d
7521 } else if sampling {
7522 let mut q = std::mem::take(&mut self.spec_q[j]);
7523 sampler::distribution_into(
7524 &lg,
7525 &cfg,
7526 all_ids,
7527 &mut self.sampler_scratch,
7528 self.pool.as_deref(),
7529 &mut q,
7530 );
7531 let d = sampler::draw(&q, &mut self.rng);
7532 self.spec_q[j] = q;
7533 all_ids.push(d); d
7535 } else if greedy_pen {
7536 let d = sampler::argmax_penalized(
7537 &lg,
7538 &cfg,
7539 all_ids,
7540 &mut self.sampler_scratch,
7541 self.pool.as_deref(),
7542 );
7543 all_ids.push(d);
7544 d
7545 } else {
7546 sampler::argmax(&lg)
7547 };
7548 attention::recycle_buf(&mut lg);
7549 drafts.push(dj);
7550 hx = hj;
7551 spec_stamp("d.pick");
7552 }
7553 all_ids.truncate(base_len);
7554 *drafted += k_spec;
7555 let t_draft = t_round.elapsed();
7556 let sub_draft = subs();
7557 let b = k_spec + 1;
7560 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7561 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7562 let e = self.embed_single(t);
7563 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7564 }
7565 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7566 spec_stamp("v.emb");
7567 let (lm_gw, lm_rows) = {
7568 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7569 (
7570 crate::gpu::GraphW {
7571 idx: i,
7572 kind,
7573 row_scale: rs,
7574 data: &[],
7575 prism: crate::gpu::GraphPrismOp::None,
7576 affine: false,
7577 },
7578 self.weights.lm_head.rows(),
7579 )
7580 };
7581 let mut logits = Vec::new();
7582 let final_norm = self.weights.final_norm.clone();
7583 #[cfg(target_os = "macos")]
7592 let greedy_dev = metal_native
7593 && !sampling
7594 && !greedy_pen
7595 && !self.confidence_on
7596 && self.final_softcap.is_none()
7597 && self.vocab_size == lm_rows
7603 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7604 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7605 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7606 #[cfg(not(target_os = "macos"))]
7607 let greedy_dev = false;
7608 let mut dev_ids: Vec<u32> = Vec::new();
7609 #[cfg(target_os = "macos")]
7610 let verify_outcome = if metal_native {
7611 let lm = self.weights.lm_head.q1_parts()?;
7612 let n_score = self.vocab_size.min(lm_rows);
7613 self.try_batch_graph_metal(
7614 &mut hiddens,
7615 &positions,
7616 b,
7617 Some((lm, &final_norm, &mut logits)),
7618 if greedy_dev {
7619 Some((n_score, &mut dev_ids))
7620 } else {
7621 None
7622 },
7623 )
7624 } else {
7625 self.try_batch_graph_wgpu(
7626 &mut hiddens,
7627 &positions,
7628 b,
7629 Some(crate::gpu::SpecTail {
7630 lm: lm_gw,
7631 lm_rows,
7632 final_norm: &final_norm,
7633 logits_out: &mut logits,
7634 }),
7635 )
7636 };
7637 #[cfg(not(target_os = "macos"))]
7638 let verify_outcome = self.try_batch_graph_wgpu(
7639 &mut hiddens,
7640 &positions,
7641 b,
7642 Some(crate::gpu::SpecTail {
7643 lm: lm_gw,
7644 lm_rows,
7645 final_norm: &final_norm,
7646 logits_out: &mut logits,
7647 }),
7648 );
7649 match verify_outcome {
7650 crate::gpu::BatchGraphOutcome::Completed => {}
7651 crate::gpu::BatchGraphOutcome::Declined => {
7652 m.kv.truncate_last(k_spec);
7656 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7657 self.clear_sequence_state();
7658 self.graph_failed
7659 .store(true, std::sync::atomic::Ordering::Relaxed);
7660 self.cancel
7661 .store(true, std::sync::atomic::Ordering::Relaxed);
7662 tracing::error!("MTP graph mirror rewind failed after verify decline");
7663 }
7664 return None;
7665 }
7666 crate::gpu::BatchGraphOutcome::Failed => {
7667 self.clear_sequence_state();
7671 self.graph_failed
7672 .store(true, std::sync::atomic::Ordering::Relaxed);
7673 self.cancel
7674 .store(true, std::sync::atomic::Ordering::Relaxed);
7675 tracing::error!("MTP verify batch graph failed after admission");
7676 return None;
7677 }
7678 }
7679 #[cfg(target_os = "macos")]
7685 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7686 let snap: Vec<Vec<f32>> = self
7687 .kv_cache
7688 .layers
7689 .iter()
7690 .map(|l| l.linear_state.clone())
7691 .collect();
7692 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7693 let toks: Vec<u32> = std::iter::once(t_next)
7694 .chain(drafts.iter().copied())
7695 .collect();
7696 let want_save = self.graph_want_logits;
7697 self.graph_want_logits = false;
7698 for (i, &t) in toks.iter().enumerate() {
7699 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7700 let _ = self.graph_logits.take();
7701 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7705 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7706 }
7707 let ref_lg = self.logits_from_hidden(&hi);
7708 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7709 let ra = sampler::argmax(&ref_lg);
7710 let va = sampler::argmax(row);
7711 let mut md = 0f32;
7712 let mut rms = 0f64;
7713 for j in 0..lm_rows.min(ref_lg.len()) {
7714 let d = (ref_lg[j] - row[j]).abs();
7715 md = md.max(d);
7716 rms += (d as f64) * (d as f64);
7717 }
7718 let mut hd = 0f32;
7719 for j in 0..self.hidden_size {
7720 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7721 }
7722 eprintln!(
7723 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7724 next_pos + i,
7725 if ra == va { "OK" } else { "MISMATCH" },
7726 (rms / lm_rows as f64).sqrt()
7727 );
7728 }
7729 self.graph_want_logits = want_save;
7730 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7733 if l.linear_state.len() == st.len() {
7734 l.linear_state.copy_from_slice(&st);
7735 } else {
7736 l.linear_state = st;
7737 }
7738 }
7739 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7740 let extra = l.seq_len.saturating_sub(n0);
7741 if extra > 0 {
7742 l.truncate_last(extra);
7743 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7744 }
7745 }
7746 }
7747 let t_verify = t_round.elapsed();
7748 let sub_verify = subs();
7749 let mut a = 0usize;
7754 let mut forced: Option<u32> = None;
7755 let ids: Vec<u32> = if sparse {
7756 let mut p = std::mem::take(&mut self.spec_ps);
7757 let mut res = std::mem::take(&mut self.spec_ress);
7758 while a < k_spec {
7759 let ok = sampler::sparse_distribution_into(
7760 &logits[a * lm_rows..(a + 1) * lm_rows],
7761 &cfg,
7762 all_ids,
7763 &mut self.sampler_scratch,
7764 self.pool.as_deref(),
7765 &mut p,
7766 );
7767 if !ok {
7768 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7769 p.clear();
7770 p.push((t, 1.0));
7771 }
7772 match sampler::spec_accept_or_correct_sparse(
7773 &p,
7774 &self.spec_qs[a],
7775 drafts[a],
7776 &mut self.rng,
7777 &mut res,
7778 ) {
7779 None => {
7780 all_ids.push(drafts[a]);
7781 a += 1;
7782 }
7783 Some(c) => {
7784 forced = Some(c);
7785 break;
7786 }
7787 }
7788 }
7789 all_ids.truncate(base_len);
7790 self.spec_ps = p;
7791 self.spec_ress = res;
7792 drafts.clone()
7793 } else if sampling {
7794 let mut p = std::mem::take(&mut self.spec_p);
7795 let mut res = std::mem::take(&mut self.spec_res);
7796 while a < k_spec {
7797 sampler::distribution_into(
7798 &logits[a * lm_rows..(a + 1) * lm_rows],
7799 &cfg,
7800 all_ids,
7801 &mut self.sampler_scratch,
7802 self.pool.as_deref(),
7803 &mut p,
7804 );
7805 match sampler::spec_accept_or_correct(
7806 &p,
7807 &self.spec_q[a],
7808 drafts[a],
7809 &mut self.rng,
7810 &mut res,
7811 self.pool.as_deref(),
7812 ) {
7813 None => {
7814 all_ids.push(drafts[a]);
7815 a += 1;
7816 }
7817 Some(c) => {
7818 forced = Some(c);
7819 break;
7820 }
7821 }
7822 }
7823 all_ids.truncate(base_len);
7824 self.spec_p = p;
7825 self.spec_res = res;
7826 drafts.clone()
7828 } else if greedy_pen {
7829 let mut ids: Vec<u32> = Vec::with_capacity(b);
7833 for i in 0..b {
7834 let t = sampler::argmax_penalized(
7835 &logits[i * lm_rows..(i + 1) * lm_rows],
7836 &cfg,
7837 all_ids,
7838 &mut self.sampler_scratch,
7839 self.pool.as_deref(),
7840 );
7841 ids.push(t);
7842 if i < k_spec && t == drafts[i] {
7843 all_ids.push(t);
7844 } else {
7845 break;
7846 }
7847 }
7848 all_ids.truncate(base_len);
7849 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7850 a += 1;
7851 }
7852 ids
7855 } else if greedy_dev && dev_ids.len() == b {
7856 let ids = std::mem::take(&mut dev_ids);
7857 while a < k_spec && ids[a] == drafts[a] {
7858 a += 1;
7859 }
7860 ids
7861 } else {
7862 if logits.len() < b * lm_rows {
7863 self.clear_sequence_state();
7866 self.graph_failed
7867 .store(true, std::sync::atomic::Ordering::Relaxed);
7868 self.cancel
7869 .store(true, std::sync::atomic::Ordering::Relaxed);
7870 tracing::error!("Metal verify returned neither logits nor argmax ids");
7871 return None;
7872 }
7873 let ids: Vec<u32> = (0..b)
7874 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7875 .collect();
7876 while a < k_spec && ids[a] == drafts[a] {
7877 a += 1;
7878 }
7879 ids
7880 };
7881 spec_stamp("acc");
7882 if spec_dbg {
7883 eprintln!(
7884 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7885 drafts, ids
7886 );
7887 }
7888 #[cfg(target_os = "macos")]
7892 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7893 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7894 {
7895 let snap: Vec<Vec<f32>> = self
7896 .kv_cache
7897 .layers
7898 .iter()
7899 .map(|l| l.linear_state.clone())
7900 .collect();
7901 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7902 let toks: Vec<u32> = std::iter::once(t_next)
7903 .chain(drafts.iter().copied())
7904 .collect();
7905 let want_save = self.graph_want_logits;
7906 self.graph_want_logits = false;
7907 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7908 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7909 let _ = self.graph_logits.take();
7910 }
7911 self.graph_want_logits = want_save;
7912 let plain_states: Vec<Vec<f32>> = self
7913 .kv_cache
7914 .layers
7915 .iter()
7916 .map(|l| l.linear_state.clone())
7917 .collect();
7918 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7919 let mut rows = Vec::new();
7920 for (li, (l, n0)) in self
7921 .kv_cache
7922 .layers
7923 .iter_mut()
7924 .zip(attn_lens.iter())
7925 .enumerate()
7926 {
7927 let extra = l.seq_len.saturating_sub(*n0);
7928 if extra > 0 {
7929 let mut kk = Vec::new();
7930 let mut vv = Vec::new();
7931 for g in 0..nkv {
7932 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7933 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7934 }
7935 rows.push((li, kk, vv));
7936 l.truncate_last(extra);
7937 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7938 }
7939 }
7940 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7941 if l.linear_state.len() == st.len() {
7942 l.linear_state.copy_from_slice(&st);
7943 } else {
7944 l.linear_state = st;
7945 }
7946 }
7947 Some((plain_states, rows))
7948 } else {
7949 None
7950 };
7951 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7952 #[cfg(target_os = "macos")]
7961 let mut warm_pending: Option<MetalWarmPending> = None;
7962 #[cfg(target_os = "macos")]
7963 if metal_native {
7964 m.kv.truncate_last(k_spec.saturating_sub(1));
7965 if self.mtp_graph_mode == Some(true) {
7966 crate::gpu_metal::kv_mirror_set_stored(
7969 self.mtp_kv_id(),
7970 Self::MTP_LAYER_BASE,
7971 m.kv.seq_len,
7972 );
7973 if !warm_off && a > 0 {
7974 let pairs: Vec<(&[f32], u32)> = (0..a)
7975 .map(|j| {
7976 (
7977 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7978 ids[j],
7979 )
7980 })
7981 .collect();
7982 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7983 }
7984 }
7985 spec_stamp("c.wsub");
7986 }
7987 #[cfg(target_os = "macos")]
7989 if metal_native {
7990 if !self.metal_verify_commit(a) {
7993 self.clear_sequence_state();
7994 self.graph_failed
7995 .store(true, std::sync::atomic::Ordering::Relaxed);
7996 self.cancel
7997 .store(true, std::sync::atomic::Ordering::Relaxed);
7998 tracing::error!("Metal verify state/KV handoff failed after admission");
7999 return None;
8000 }
8001 if let Some((plain_states, rows)) = commit_ref {
8002 crate::gpu_metal::queue_fence();
8003 let _ = crate::gpu_metal::wait_replay();
8006 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8007 let mut worst_s = 0f32;
8008 let mut worst_li = 0usize;
8009 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8010 if l.linear_state.len() != ps.len() || ps.is_empty() {
8011 continue;
8012 }
8013 let d = l
8014 .linear_state
8015 .iter()
8016 .zip(ps)
8017 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8018 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8019 let rel = d / n.max(1e-6);
8020 if rel > worst_s {
8021 worst_s = rel;
8022 worst_li = li;
8023 }
8024 }
8025 let mut worst_k = 0f32;
8026 for (li, kk, vv) in &rows {
8027 let l = &self.kv_cache.layers[*li];
8028 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8029 let mut ck = Vec::new();
8030 let mut cv = Vec::new();
8031 for g in 0..nkv {
8032 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8033 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8034 }
8035 if ck.len() == kk.len() {
8036 let dk = ck
8037 .iter()
8038 .zip(kk)
8039 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8040 let dv = cv
8041 .iter()
8042 .zip(vv)
8043 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8044 worst_k = worst_k.max(dk).max(dv);
8045 } else {
8046 eprintln!(
8047 "commit-check L{li}: kv row count mismatch {} vs {}",
8048 ck.len(),
8049 kk.len()
8050 );
8051 }
8052 }
8053 eprintln!(
8054 "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}"
8055 );
8056 }
8057 }
8058 if !metal_native && a + 1 < b {
8059 let expected_gdn_layers = self.graph_gdn_layer_count();
8060 if expected_gdn_layers > 0
8061 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8062 {
8063 self.clear_sequence_state();
8064 self.graph_failed
8065 .store(true, std::sync::atomic::Ordering::Relaxed);
8066 self.cancel
8067 .store(true, std::sync::atomic::Ordering::Relaxed);
8068 tracing::error!("GDN speculative restore failed after verify");
8069 return None;
8070 }
8071 }
8072 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8073 self.clear_sequence_state();
8078 self.graph_failed
8079 .store(true, std::sync::atomic::Ordering::Relaxed);
8080 self.cancel
8081 .store(true, std::sync::atomic::Ordering::Relaxed);
8082 tracing::error!("trunk graph KV rewind failed after speculative verify");
8083 return None;
8084 }
8085 *accepted += a;
8086 if !metal_native {
8097 m.kv.truncate_last(k_spec.saturating_sub(1));
8099 }
8100 spec_stamp("c.trunc");
8101 if !metal_native
8102 && self.mtp_graph_mode == Some(true)
8103 && !self.rewind_mtp_graph_mirror(next_pos)
8104 {
8105 self.clear_sequence_state();
8109 self.graph_failed
8110 .store(true, std::sync::atomic::Ordering::Relaxed);
8111 self.cancel
8112 .store(true, std::sync::atomic::Ordering::Relaxed);
8113 tracing::error!("MTP graph mirror rewind failed after verify commit");
8114 return None;
8115 }
8116 if !warm_off && a > 0 {
8117 let mut warmed = false;
8120 #[cfg(target_os = "macos")]
8121 if metal_native && self.mtp_graph_mode == Some(true) {
8122 warmed = match warm_pending.take() {
8126 Some(p) => self.mtp_warm_batch_finish(m, p),
8127 None => false,
8128 };
8129 if !warmed {
8130 warmed = true;
8131 for j in 0..a {
8132 let row =
8133 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8134 if self
8135 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8136 .is_none()
8137 {
8138 warmed = false;
8139 break;
8140 }
8141 }
8142 }
8143 }
8144 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8145 let rows: Vec<Vec<f32>> = (0..a)
8146 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8147 .collect();
8148 let pairs: Vec<(&[f32], u32)> = rows
8149 .iter()
8150 .zip(ids.iter())
8151 .map(|(r, &t)| (r.as_slice(), t))
8152 .collect();
8153 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8154 Ok(()) => warmed = true,
8155 Err(err) => {
8156 tracing::error!("{err}");
8162 self.clear_sequence_state();
8163 self.graph_failed
8164 .store(true, std::sync::atomic::Ordering::Relaxed);
8165 self.cancel
8166 .store(true, std::sync::atomic::Ordering::Relaxed);
8167 return None;
8168 }
8169 }
8170 }
8171 if !warmed {
8172 for j in 0..a {
8173 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8174 let row = row.to_vec();
8175 self.mtp_warm(m, &row, ids[j], next_pos + j);
8176 }
8177 }
8178 }
8179 spec_stamp("c.warm");
8183 if let Some(c) = forced {
8184 self.spec_forced = Some(c);
8185 self.graph_logits = None;
8186 } else if greedy_dev && logits.is_empty() {
8187 self.spec_forced = Some(ids[a]);
8190 self.graph_logits = None;
8191 } else {
8192 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8193 row.resize(self.vocab_size, 0.0);
8194 if let Some(c) = self.final_softcap {
8195 for l in row.iter_mut() {
8196 *l = c * (*l / c).tanh();
8197 }
8198 }
8199 self.graph_logits = Some(row);
8200 }
8201 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8202 spec_stamp("c.row");
8203 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8209 let end = subs();
8210 eprintln!(
8211 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8212 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8213 t_draft.as_secs_f64() * 1e3,
8214 sub_draft - sub0,
8215 (t_verify - t_draft).as_secs_f64() * 1e3,
8216 sub_verify - sub_draft,
8217 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8218 end - sub_verify,
8219 self.draft_full_streak,
8220 );
8221 }
8222 if k_env.is_none() && !metal_native && !k_capped {
8227 let f = a as f32 / k_spec.max(1) as f32;
8231 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8232 let mut k_next = k_spec;
8233 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8234 k_next = k_spec + 1;
8235 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8236 k_next = k_spec - 1;
8237 }
8238 if k_next != k_spec {
8239 self.spec_acc_ewma = 0.6;
8240 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8241 eprintln!("spec-k: {k_spec} → {k_next}");
8242 }
8243 }
8244 self.spec_k_adapt = Some(k_next);
8245 }
8246 spec_stamp("end");
8247 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8248 }
8249
8250 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8259 if !self.pair_supported() {
8260 return (0.0, 0.0);
8261 }
8262 if self.gdn_cfg.is_some()
8270 && self.graph_prefill_preferred()
8271 && self.automatic_gpu_prefix().is_some()
8272 {
8273 tracing::info!(
8274 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8275 );
8276 return (0.0, 0.0);
8277 }
8278 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8285 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8286 let emb1 = self.embed_single(1);
8287 let emb2 = self.embed_single(2);
8288 let pos = self.kv_cache.seq_len();
8289
8290 let t0 = std::time::Instant::now();
8291 for _ in 0..iters {
8292 let _ = self.forward_layers(&emb1, pos, None);
8293 let _ = self.forward_layers(&emb2, pos + 1, None);
8294 for l in &mut self.kv_cache.layers {
8295 l.truncate_last(2);
8296 }
8297 }
8298 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8299
8300 let t1 = std::time::Instant::now();
8301 for _ in 0..iters {
8302 let _ = self.forward_pair(&emb1, &emb2, pos);
8303 for l in &mut self.kv_cache.layers {
8304 l.truncate_last(2);
8305 }
8306 }
8307 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8308 match graph_env {
8309 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8310 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8311 }
8312 (singles_ms, pair_ms)
8313 }
8314
8315 fn pair_supported(&self) -> bool {
8323 !self.weights.layers.is_empty()
8330 && self.g3n.is_none()
8331 && !self
8332 .weights
8333 .layers
8334 .iter()
8335 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8336 }
8337
8338 fn forward_pair(
8339 &mut self,
8340 emb1: &[f32],
8341 emb2: &[f32],
8342 position: usize,
8343 ) -> (Vec<f32>, Vec<f32>) {
8344 self.mimo_moe_prepare();
8347 let mut h1 = emb1.to_vec();
8348 let mut h2 = emb2.to_vec();
8349 let (_nkv, _hd, hs, _rd, eps) = (
8350 self.num_kv_heads,
8351 self.head_dim,
8352 self.hidden_size,
8353 self.rotary_dim,
8354 self.rms_eps,
8355 );
8356 let pool = self.pool.clone();
8357 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8364
8365 for li in 0..self.num_layers {
8366 let _capacity_tail = automatic_gpu_prefix
8367 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8368 .map(|_| crate::gpu::enter_cpu_scope());
8369 crate::gpu::set_layer(li as i64);
8372 let lw = &self.weights.layers[self.phys_layer(li)];
8373 inference::rms_norm_into(
8376 &h1,
8377 &lw.input_norm,
8378 self.rms_eps,
8379 self.norm_style,
8380 &mut self.ws.n1,
8381 );
8382 inference::rms_norm_into(
8383 &h2,
8384 &lw.input_norm,
8385 self.rms_eps,
8386 self.norm_style,
8387 &mut self.ws.n2,
8388 );
8389
8390 let (a1, a2) = match &lw.attn {
8391 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8392 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8393 AttnKind::Bounded(w) => {
8394 let rope = self
8397 .bounded_rope
8398 .clone()
8399 .expect("bounded layer without an installed rotation table");
8400 let cfg = crate::bounded::BoundedAttnCfg {
8401 num_heads: self.num_heads,
8402 num_kv_heads: self.num_kv_heads,
8403 head_dim: self.head_dim,
8404 hidden_size: hs,
8405 scale: self.attn_scale,
8406 rope: &rope,
8407 pool: pool.as_deref(),
8408 };
8409 let a1 = crate::bounded::bounded_attention(
8410 &self.ws.n1,
8411 w,
8412 &mut self.kv_cache.layers[li],
8413 &cfg,
8414 );
8415 let a2 = crate::bounded::bounded_attention(
8416 &self.ws.n2,
8417 w,
8418 &mut self.kv_cache.layers[li],
8419 &cfg,
8420 );
8421 (a1, a2)
8422 }
8423 AttnKind::Linear(w) => {
8424 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8425 let layer = &mut self.kv_cache.layers[li];
8426 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8427 vmf_phase_pair(
8428 &self.ws.n1,
8429 &self.ws.n2,
8430 w,
8431 &cfg,
8432 state,
8433 scratch,
8434 self.pool.as_deref(),
8435 )
8436 }
8437 AttnKind::LinearGdn(w) => {
8438 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8439 let layer = &mut self.kv_cache.layers[li];
8440 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8441 gdn_pair(
8442 &self.ws.n1,
8443 &self.ws.n2,
8444 w,
8445 &cfg,
8446 state,
8447 scratch,
8448 self.pool.as_deref(),
8449 )
8450 }
8451 AttnKind::ShortConv(w) => {
8452 let cfg = self
8453 .short_conv_cfg
8454 .expect("short-conv layer without short_conv_cfg");
8455 let layer = &mut self.kv_cache.layers[li];
8456 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8457 short_conv_pair(
8458 &self.ws.n1,
8459 &self.ws.n2,
8460 w,
8461 &cfg,
8462 state,
8463 scratch,
8464 self.pool.as_deref(),
8465 )
8466 }
8467 AttnKind::Full {
8468 wq,
8469 wk,
8470 wv,
8471 wo,
8472 q_norm,
8473 k_norm,
8474 output_gate,
8475 softplus_gate,
8476 bias,
8477 } => {
8478 let inv_freq_l = self.layer_inv_freq(li);
8479 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8480 let cfg = QwenAttnCfg {
8481 num_heads: self.layer_num_heads(li),
8482 num_kv_heads: nkv_l,
8483 head_dim: hd_l,
8484 hidden_size: hs,
8485 position,
8486 inv_freq: &inv_freq_l,
8487 rotary_dim: rd_l,
8488 scale: self.attn_scale,
8489 softcap: self.attn_softcap,
8490 window: self.layer_window(li),
8491 v_norm: self.attn_v_norm,
8492 qk_norm_after_rope: self.qk_norm_after_rope,
8493 gate_sigmoid: self.proj_gate_sigmoid,
8494 q_norm: q_norm.as_deref(),
8495 k_norm: k_norm.as_deref(),
8496 output_gate: *output_gate,
8497 softplus_gate: softplus_gate
8498 .as_ref()
8499 .map(|(gate, per_head)| (gate, *per_head)),
8500 rope_scale: self.layer_rope_scale(li),
8501 bias: bias
8502 .as_ref()
8503 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8504 rms_eps: eps,
8505 norm_style: self.norm_style,
8506 pool: pool.as_deref(),
8507 v_head_dim: self.layer_v_dim(li),
8508 };
8509 attention::qwen_attention_pair(
8510 &self.ws.n1,
8511 &self.ws.n2,
8512 wq,
8513 wk,
8514 wv,
8515 wo,
8516 &mut self.kv_cache.layers[li],
8517 &cfg,
8518 )
8519 }
8520 };
8521 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8522 Some(w) => (
8523 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8524 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8525 ),
8526 None => (a1, a2),
8527 };
8528 for i in 0..self.hidden_size {
8529 h1[i] += a1[i];
8530 h2[i] += a2[i];
8531 }
8532 let (mut a1, mut a2) = (a1, a2);
8533 attention::recycle_buf(&mut a1);
8534 attention::recycle_buf(&mut a2);
8535
8536 let lw = &self.weights.layers[self.phys_layer(li)];
8537 inference::rms_norm_into(
8538 &h1,
8539 &lw.post_norm,
8540 self.rms_eps,
8541 self.norm_style,
8542 &mut self.ws.p1,
8543 );
8544 inference::rms_norm_into(
8545 &h2,
8546 &lw.post_norm,
8547 self.rms_eps,
8548 self.norm_style,
8549 &mut self.ws.p2,
8550 );
8551 let (f1, f2) = match &lw.ffn {
8552 FfnKind::DenseMoe(dm) => (
8555 dense_moe_ffn(
8556 dm,
8557 &self.ws.p1,
8558 &h1,
8559 self.rms_eps,
8560 self.norm_style,
8561 self.pool.as_deref(),
8562 ),
8563 dense_moe_ffn(
8564 dm,
8565 &self.ws.p2,
8566 &h2,
8567 self.rms_eps,
8568 self.norm_style,
8569 self.pool.as_deref(),
8570 ),
8571 ),
8572 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8573 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8574 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8575 ),
8576 _ => ffn_forward_pair(
8577 &lw.ffn,
8578 &self.ws.p1,
8579 &self.ws.p2,
8580 self.pool.as_deref(),
8581 None,
8582 ),
8583 };
8584 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8585 Some(w) => (
8586 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8587 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8588 ),
8589 None => (f1, f2),
8590 };
8591 for i in 0..self.hidden_size {
8592 h1[i] += f1[i];
8593 h2[i] += f2[i];
8594 }
8595 let (mut f1, mut f2) = (f1, f2);
8596 attention::recycle_buf(&mut f1);
8597 attention::recycle_buf(&mut f2);
8598 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8599 for i in 0..self.hidden_size {
8600 h1[i] *= sc;
8601 h2[i] *= sc;
8602 }
8603 }
8604 if self.is_loop_end(li) && li + 1 < self.num_layers {
8606 h1 = inference::rms_norm(
8607 &h1,
8608 &self.weights.final_norm,
8609 self.rms_eps,
8610 self.norm_style,
8611 );
8612 h2 = inference::rms_norm(
8613 &h2,
8614 &self.weights.final_norm,
8615 self.rms_eps,
8616 self.norm_style,
8617 );
8618 }
8619 }
8620 crate::gpu::set_layer(-1); if self.o1_active() {
8627 self.commit_linear_scratch();
8628 }
8629 self.o1_progress();
8630 self.swa_trim_tails();
8631 (h1, h2)
8632 }
8633
8634 fn commit_linear_scratch(&mut self) {
8636 for layer in &mut self.kv_cache.layers {
8637 if !layer.linear_scratch.is_empty() {
8638 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8639 layer.linear_scratch.clear();
8640 }
8641 }
8642 }
8643
8644 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8652 #[cfg(target_os = "macos")]
8653 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8654 if ids.is_empty() {
8655 return Err("empty id sequence".to_string());
8656 }
8657
8658 self.nll_begin()?;
8659 let result = (|| {
8660 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8661 self.nll_check_graph("encoder prefill", 0)?;
8662 let expected = ids.len() * self.hidden_size;
8663 if hidden.len() != expected {
8664 return Err(format!(
8665 "encoder prefill returned {} values for {} rows of hidden size {}",
8666 hidden.len(),
8667 ids.len(),
8668 self.hidden_size
8669 ));
8670 }
8671
8672 let mut out = vec![0.0f32; expected];
8673 for (src, dst) in hidden
8674 .chunks_exact(self.hidden_size)
8675 .zip(out.chunks_exact_mut(self.hidden_size))
8676 {
8677 inference::rms_norm_into(
8678 src,
8679 &self.weights.final_norm,
8680 self.rms_eps,
8681 self.norm_style,
8682 dst,
8683 );
8684 }
8685 Ok(out)
8686 })();
8687 self.nll_end();
8688 result
8689 }
8690
8691 pub fn forward_ids(
8694 &mut self,
8695 ids: &[u32],
8696 task_mask: Option<&TaskMask>,
8697 ) -> Result<Vec<f32>, String> {
8698 #[cfg(target_os = "macos")]
8699 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8700 if ids.is_empty() {
8701 return Err("empty id sequence".to_string());
8702 }
8703 self.clear_sequence_state();
8704 self.check_forward_graph("forward_ids setup", 0)?;
8705 if task_mask.is_none() {
8706 self.o1_begin();
8707 }
8708 let mut hidden = vec![0.0f32; self.hidden_size];
8709 let mut pos = 0usize;
8710 if let Some(b) = &mut self.dsv41 {
8711 let pool = self.pool.clone();
8712 let mut logits = Vec::new();
8713 crate::dsv41::forward_chunk(
8714 &b.0,
8715 &b.1,
8716 &b.2,
8717 &mut b.3,
8718 ids,
8719 0,
8720 pool.as_deref(),
8721 &mut logits,
8722 );
8723 if let Err(err) = self.o1_seal_checked() {
8724 self.clear_sequence_state();
8725 return Err(err);
8726 }
8727 return Ok(logits);
8728 }
8729 #[cfg(not(target_os = "macos"))]
8740 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8741 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8742 Ok(r) => r,
8743 Err(e) => {
8744 self.clear_sequence_state();
8745 return Err(e);
8746 }
8747 };
8748 if n > 0 {
8749 hidden.copy_from_slice(&last);
8750 pos = n;
8751 }
8752 }
8753 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8754 let chunk = self.prefill_chunk();
8758 let hs = self.hidden_size;
8759 while pos < ids.len() {
8760 let end = (pos + chunk).min(ids.len());
8761 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8762 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8763 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8764 pos = end;
8765 }
8766 }
8767 if task_mask.is_none()
8776 && !self.graph_prefill_preferred()
8777 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8778 && self.pair_supported()
8779 {
8780 while pos + 1 < ids.len() {
8781 let e1 = self.embed_single(ids[pos]);
8782 let e2 = self.embed_single(ids[pos + 1]);
8783 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8784 self.check_forward_graph("forward_ids pair", pos + 1)?;
8785 self.commit_linear_scratch();
8786 hidden = h2;
8787 pos += 2;
8788 }
8789 }
8790 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8793 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8794 self.graph_logits = Some(lg);
8795 hidden = vec![0.0; self.hidden_size];
8796 pos = ids.len();
8797 }
8798 }
8799 while pos < ids.len() {
8800 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8801 self.check_forward_graph("forward_ids", pos)?;
8802 pos += 1;
8803 }
8804 if let Some(logits) = self.graph_logits.take() {
8805 if let Err(err) = self.o1_seal_checked() {
8809 self.clear_sequence_state();
8810 return Err(err);
8811 }
8812 return Ok(logits);
8813 }
8814 if let Err(err) = self.o1_seal_checked() {
8818 self.clear_sequence_state();
8819 return Err(err);
8820 }
8821 let normed = inference::rms_norm(
8822 &hidden,
8823 &self.weights.final_norm,
8824 self.rms_eps,
8825 self.norm_style,
8826 );
8827 Ok(self.lm_head_forward(&normed))
8828 }
8829
8830 #[doc(hidden)]
8834 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8835 #[cfg(target_os = "macos")]
8836 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8837 if ids.is_empty() {
8838 return Err("empty id sequence".to_string());
8839 }
8840 self.clear_sequence_state();
8841 self.dsv41
8842 .as_ref()
8843 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8844 self.o1_begin();
8845 let rows = {
8846 let pool = self.pool.clone();
8847 let b = self
8848 .dsv41
8849 .as_mut()
8850 .expect("dsv41 checked above; state cannot change during forward");
8851 let mut rows = Vec::with_capacity(ids.len());
8852 for (position, &id) in ids.iter().enumerate() {
8853 let mut logits = Vec::new();
8854 crate::dsv41::forward_token(
8855 &b.0,
8856 &b.1,
8857 &b.2,
8858 &mut b.3,
8859 id,
8860 position,
8861 pool.as_deref(),
8862 &mut logits,
8863 );
8864 rows.push(logits);
8865 }
8866 rows
8867 };
8868 self.o1_seal();
8869 Ok(rows)
8870 }
8871
8872 pub fn dsv41_moe_capture(
8876 &mut self,
8877 ids: &[u32],
8878 lo: usize,
8879 hi: usize,
8880 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
8881 #[cfg(target_os = "macos")]
8882 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8883 if ids.is_empty() {
8884 return Err("empty id sequence".to_string());
8885 }
8886 self.dsv41
8887 .as_ref()
8888 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
8889 self.clear_sequence_state();
8890 let res = {
8891 let pool = self.pool.clone();
8892 let b = self.dsv41.as_mut().expect("dsv41 checked above");
8893 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
8894 };
8895 self.clear_sequence_state();
8896 res
8897 }
8898
8899 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8906 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8907 Ok((nll / cnt.max(1) as f64).exp())
8908 }
8909
8910 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
8917 if !self.can_prefill_batched() {
8918 return self.nll_ids_from(ids, 0).map(|_| ());
8919 }
8920 self.nll_begin()?;
8921 let result: Result<(), String> = (|| {
8922 let chunk = chunk.max(1);
8923 let mut pos = 0usize;
8924 while pos < ids.len() {
8925 let end = (pos + chunk).min(ids.len());
8926 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
8927 self.nll_check_graph("calibration prefill", pos)?;
8928 pos = end;
8929 }
8930 Ok(())
8931 })();
8932 self.nll_end();
8933 result
8934 }
8935
8936 pub fn calib_window_span(
8945 &mut self,
8946 ids: &[u32],
8947 input: Option<&[f32]>,
8948 from: usize,
8949 mid: usize,
8950 upto: usize,
8951 ) -> Result<Vec<f32>, String> {
8952 if !self.can_prefill_batched() {
8953 return Err("calib_window_span: this architecture has no batched prefill".into());
8954 }
8955 self.nll_begin()?;
8956 let result: Result<Vec<f32>, String> = (|| {
8957 let h = if mid > from {
8958 let inp = match input {
8959 Some(h) => PrefillIn::Hidden(h),
8960 None => PrefillIn::Ids(ids),
8961 };
8962 let h = self.prefill_batch_span(inp, 0, None, from, mid);
8963 self.nll_check_graph("calibration span", 0)?;
8964 h
8965 } else {
8966 input.map(<[f32]>::to_vec).unwrap_or_default()
8967 };
8968 if upto > mid {
8969 let inp = if mid > from || input.is_some() {
8970 PrefillIn::Hidden(&h)
8971 } else {
8972 PrefillIn::Ids(ids)
8973 };
8974 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
8975 self.nll_check_graph("calibration span", 0)?;
8976 }
8977 Ok(h)
8978 })();
8979 self.nll_end();
8980 result
8981 }
8982
8983 #[cfg(feature = "gpu")]
8987 pub fn qwen4_capture(
8988 &mut self,
8989 ids: &[u32],
8990 lo: usize,
8991 hi: usize,
8992 ) -> Result<Vec<Vec<f32>>, String> {
8993 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
8994 crate::qwen4_exp::capture_window(
8995 &b.0,
8996 &b.1,
8997 &b.2,
8998 &mut b.3,
8999 ids,
9000 lo,
9001 hi,
9002 &self.inv_freq,
9003 self.pool.as_deref(),
9004 )
9005 }
9006
9007 pub fn qwen4_expert_dirs(
9009 &self,
9010 li: usize,
9011 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9012 let b = self.qwen4_exp.as_ref()?;
9013 crate::qwen4_exp::expert_dirs(&b.1, li)
9014 }
9015
9016 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9021 self.clear_sequence_state();
9022 FFN_PROBE.with(|p| {
9023 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9024 });
9025 crate::gpu::cpu_scope(|| {
9026 for (pos, &id) in ids.iter().enumerate() {
9027 let emb = self.embed_single(id);
9028 let _ = self.forward_layers(&emb, pos, None);
9029 }
9030 });
9031 self.clear_sequence_state();
9032 FFN_PROBE
9033 .with(|p| p.borrow_mut().take())
9034 .unwrap_or_default()
9035 }
9036
9037 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9041 if let Err(err) = self.nll_begin() {
9042 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9046 self.nll_end();
9047 return Err(err);
9048 }
9049 FFN_PROBE.with(|p| {
9050 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9051 });
9052 let result: Result<(), String> = (|| {
9053 for chunk in ids.chunks(256) {
9054 if chunk.len() < 2 {
9055 continue;
9056 }
9057 self.nll_ids_masked(chunk, 0, None)?;
9058 }
9059 Ok(())
9060 })();
9061 self.nll_end();
9062 let probe = FFN_PROBE
9063 .with(|p| p.borrow_mut().take())
9064 .unwrap_or_default();
9065 match result {
9066 Ok(()) => Ok(probe),
9067 Err(err) => {
9068 drop(probe);
9069 Err(err)
9070 }
9071 }
9072 }
9073
9074 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9078 self.nll_begin()?;
9079 let result: Result<f64, String> = (|| {
9080 let mut nll = 0f64;
9081 let mut cnt = 0usize;
9082 let mut hidden = vec![0f32; self.hidden_size];
9083 for (pos, &id) in ids.iter().enumerate() {
9084 if pos > 0 {
9085 inference::rms_norm_into(
9086 &hidden,
9087 &self.weights.final_norm,
9088 self.rms_eps,
9089 self.norm_style,
9090 &mut self.ws.n1,
9091 );
9092 let mut logits = self.lm_head_forward(&self.ws.n1);
9093 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9094 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9095 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9096 nll -= p.max(1e-300).ln();
9097 cnt += 1;
9098 attention::recycle_buf(&mut logits);
9099 }
9100 let emb = self.embed_single(id);
9101 hidden = self.forward_layers(&emb, pos, Some(mask));
9102 self.nll_check_graph("masked serial forward", pos)?;
9103 let _ = self.graph_logits.take();
9107 }
9108 Ok((nll / cnt.max(1) as f64).exp())
9109 })();
9110 self.nll_end();
9111 result
9112 }
9113
9114 pub fn nll_ids_masked(
9133 &mut self,
9134 ids: &[u32],
9135 start: usize,
9136 task_mask: Option<&TaskMask>,
9137 ) -> Result<(f64, usize), String> {
9138 let task_mask = self.drop_open_mask(task_mask);
9139 self.nll_ids_inner(ids, start, task_mask)
9140 }
9141
9142 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9143 self.nll_ids_inner(ids, start, None)
9144 }
9145
9146 fn nll_ids_inner(
9147 &mut self,
9148 ids: &[u32],
9149 start: usize,
9150 task_mask: Option<&TaskMask>,
9151 ) -> Result<(f64, usize), String> {
9152 self.nll_begin()?;
9153 let result: Result<(f64, usize), String> = (|| {
9154 let mut nll = 0f64;
9155 let mut cnt = 0usize;
9156 let (graph_quality, fused_head_quality) = nll_graph_policy(
9169 task_mask.is_none(),
9170 self.graph_prefill_preferred(),
9171 crate::gpu::q1_force(),
9172 );
9173 self.graph_head_required = fused_head_quality;
9174 self.graph_want_logits = fused_head_quality;
9175 #[cfg(target_os = "macos")]
9176 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9177 match self.nll_batch_metal(ids, start) {
9178 MetalBatchNllOutcome::Completed(nll, count) => {
9179 return Ok((nll, count));
9180 }
9181 MetalBatchNllOutcome::Declined => {}
9182 MetalBatchNllOutcome::Failed(err) => return Err(err),
9183 }
9184 }
9185 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9190 if self.can_prefill_batched() && !graph_quality && !force_serial {
9191 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9200 .ok()
9201 .and_then(|v| v.parse().ok())
9202 .filter(|&c: &usize| c > 0)
9203 .unwrap_or(128);
9204 const LM_SUB: usize = 32;
9205 let n = ids.len().saturating_sub(1);
9206 let hs = self.hidden_size;
9207 let rows = self.weights.lm_head.rows();
9208 let mut pos = 0usize;
9209 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9210 while pos < n {
9211 let end = (pos + chunk_len).min(n);
9212 let bsz = end - pos;
9213 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9214 self.nll_check_graph("batched prefill", pos)?;
9215 if state_trace && end % 256 == 0 {
9216 self.trace_recurrent_state(end);
9217 }
9218 let mut k0 = 0usize;
9219 while k0 < bsz {
9220 let k1 = (k0 + LM_SUB).min(bsz);
9221 let sb = k1 - k0;
9222 if pos + k1 <= start {
9225 k0 = k1;
9226 continue;
9227 }
9228 let mut normed = vec![0.0f32; sb * hs];
9229 for k in 0..sb {
9230 let r = inference::rms_norm(
9231 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9232 &self.weights.final_norm,
9233 self.rms_eps,
9234 self.norm_style,
9235 );
9236 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9237 }
9238 let mut logits = vec![0.0f32; sb * rows];
9239 self.weights
9240 .lm_head
9241 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9242 for k in 0..sb {
9243 if pos + k0 + k < start {
9244 continue;
9245 }
9246 self.nll_check_graph("batched score row", pos + k0 + k)?;
9247 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9248 if let Some(mu) = self.logit_multiplier {
9249 for v in lg.iter_mut() {
9250 *v *= mu;
9251 }
9252 }
9253 if let Some(c) = self.final_softcap {
9257 softcap_in_place(lg, c, self.pool.as_deref());
9258 }
9259 if let Some(cm) = self.head_clusters.clone() {
9262 self.hierarchical_head_logprobs(
9263 &normed[k * hs..(k + 1) * hs],
9264 &cm,
9265 lg,
9266 );
9267 }
9268 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9269 let target = ids[pos + k0 + k + 1] as usize;
9270 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9271 let lse: f64 = lg
9272 .iter()
9273 .map(|&v| ((v - max) as f64).exp())
9274 .sum::<f64>()
9275 .ln()
9276 + max as f64;
9277 nll += lse - lg[target] as f64;
9278 cnt += 1;
9279 if std::env::var("CMF_PPL_TRACE").is_ok() {
9280 let top = lg
9281 .iter()
9282 .enumerate()
9283 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9284 .map(|(i, _)| i)
9285 .unwrap_or(0);
9286 eprintln!(
9287 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9288 pos + k0 + k,
9289 target,
9290 lse - lg[target] as f64,
9291 top,
9292 lg[target],
9293 lg[top]
9294 );
9295 }
9296 }
9297 k0 = k1;
9298 }
9299 pos = end;
9300 }
9301 return Ok((nll, cnt));
9302 }
9303 for pos in 0..ids.len().saturating_sub(1) {
9304 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9305 self.nll_check_graph("serial forward", pos)?;
9306 let out_of_band = self.graph_logits.take();
9314 if self.graph_head_required && out_of_band.is_none() {
9315 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9316 return Err(format!(
9317 "fused Metal graph head did not complete at NLL position {pos}"
9318 ));
9319 }
9320 if pos < start {
9321 continue;
9322 }
9323 let logits = match out_of_band {
9324 Some(lg) => lg,
9325 None => {
9326 let normed = inference::rms_norm(
9327 &hidden,
9328 &self.weights.final_norm,
9329 self.rms_eps,
9330 self.norm_style,
9331 );
9332 self.lm_head_forward(&normed)
9336 }
9337 };
9338 let target = ids[pos + 1] as usize;
9339 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9340 let lse: f64 = logits
9341 .iter()
9342 .map(|&v| ((v - max) as f64).exp())
9343 .sum::<f64>()
9344 .ln()
9345 + max as f64;
9346 let tok_nll = lse - logits[target] as f64;
9347 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9348 let top = logits
9349 .iter()
9350 .enumerate()
9351 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9352 .map(|(i, _)| i)
9353 .unwrap_or(0);
9354 eprintln!(
9355 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9356 logits[target], logits[top]
9357 );
9358 }
9359 nll += tok_nll;
9360 cnt += 1;
9361 }
9362 Ok((nll, cnt))
9363 })();
9364 self.nll_end();
9365 result
9366 }
9367
9368 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9373 let normed = inference::rms_norm(
9374 hidden,
9375 &self.weights.final_norm,
9376 self.rms_eps,
9377 self.norm_style,
9378 );
9379 let mut logits = self.lm_head_forward(&normed);
9382 let target = target as usize;
9383 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9384 let lse: f64 = logits
9385 .iter()
9386 .map(|&v| ((v - max) as f64).exp())
9387 .sum::<f64>()
9388 .ln()
9389 + max as f64;
9390 let tok_nll = lse - logits[target] as f64;
9391 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9392 let top = logits
9393 .iter()
9394 .enumerate()
9395 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9396 .map(|(i, _)| i)
9397 .unwrap_or(0);
9398 eprintln!(
9399 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9400 logits[target], logits[top]
9401 );
9402 }
9403 attention::recycle_buf(&mut logits);
9404 tok_nll
9405 }
9406
9407 fn trace_recurrent_state(&self, pos: usize) {
9415 let stats = |v: &[f32]| -> (f64, f64) {
9416 if v.is_empty() {
9417 return (0.0, 0.0);
9418 }
9419 let (mut ss, mut mx) = (0f64, 0f64);
9420 for &x in v {
9421 ss += (x as f64) * (x as f64);
9422 mx = mx.max((x as f64).abs());
9423 }
9424 ((ss / v.len() as f64).sqrt(), mx)
9425 };
9426 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9427 let lw = &self.weights.layers[self.phys_layer(li)];
9428 let (kind, s_len) = match &lw.attn {
9429 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9430 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9431 AttnKind::Bounded(_) => ("bounded", 0),
9432 AttnKind::Full { .. } => ("full", 0),
9433 _ => ("other", 0),
9434 };
9435 let (rms, max) = stats(&l.linear_state);
9436 let s_part = if kind == "vmf" {
9437 &l.linear_state[..s_len.min(l.linear_state.len())]
9438 } else if kind == "gdn" {
9439 let ring = l
9440 .linear_state
9441 .len()
9442 .saturating_sub(s_len)
9443 .min(l.linear_state.len());
9444 &l.linear_state[ring..]
9445 } else {
9446 &l.linear_state[..0]
9447 };
9448 let (s_rms, s_max) = stats(s_part);
9449 let (ring_rms, ring_len) = match &l.bounded {
9450 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9451 None => (0.0, 0),
9452 };
9453 eprintln!(
9454 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9455 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9456 l.linear_state.len(),
9457 l.seq_len
9458 );
9459 }
9460 }
9461
9462 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9480 self.nll_begin()?;
9485 let requested_prefix = (prefill > 0).then_some(prefill);
9486 self.o1_begin_with_prefix(requested_prefix);
9487 let n = ids.len().saturating_sub(1);
9488 let requested_start = prefill.min(n);
9489 let exact_end = if self.o1_active() {
9494 match requested_prefix {
9495 Some(requested) => self.o1_effective_boundary(requested),
9496 None => self
9497 .o1_cfg
9498 .as_ref()
9499 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9500 }
9501 .unwrap_or(requested_start)
9502 .min(n)
9503 } else {
9504 requested_start
9505 };
9506 let mut nll = 0f64;
9507 let mut cnt = 0usize;
9508
9509 let mut pos = 0usize;
9513 if self.can_prefill_batched() {
9514 const CHUNK: usize = 128;
9515 while pos < exact_end {
9516 let end = (pos + CHUNK).min(exact_end);
9517 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9518 if self
9519 .graph_failed
9520 .swap(false, std::sync::atomic::Ordering::Relaxed)
9521 {
9522 self.cancel
9523 .store(false, std::sync::atomic::Ordering::Relaxed);
9524 self.nll_end();
9525 return Err("GPU graph failed during O(1) NLL prefix".into());
9526 }
9527 for row in 0..end - pos {
9528 let score_pos = pos + row;
9529 if score_pos >= requested_start && score_pos < n {
9530 nll += self.nll_from_hidden(
9531 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9532 ids[score_pos + 1],
9533 score_pos,
9534 );
9535 cnt += 1;
9536 }
9537 }
9538 pos = end;
9539 }
9540 } else {
9541 while pos < exact_end {
9542 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9543 if self
9544 .graph_failed
9545 .swap(false, std::sync::atomic::Ordering::Relaxed)
9546 {
9547 self.cancel
9548 .store(false, std::sync::atomic::Ordering::Relaxed);
9549 self.nll_end();
9550 return Err("GPU graph failed during O(1) NLL prefix".into());
9551 }
9552 if pos >= requested_start && pos < n {
9553 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9554 cnt += 1;
9555 }
9556 pos += 1;
9557 }
9558 }
9559 self.o1_seal_checked().map_err(|err| {
9560 self.nll_end();
9561 err
9562 })?;
9563
9564 let batch_k = std::env::var("CMF_BATCH_K")
9573 .ok()
9574 .and_then(|v| v.parse::<usize>().ok())
9575 .unwrap_or(0);
9576 let batch_admitted = batch_k > 0
9577 && self.can_prefill_batched()
9578 && self.o1_active()
9579 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9580 && (0..self.num_layers).all(|li| {
9581 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9582 cache.o1.is_none() || cache.o1_views().is_some()
9583 });
9584 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9585 eprintln!(
9586 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9587 batch_admitted,
9588 batch_k,
9589 n.saturating_sub(exact_end),
9590 );
9591 }
9592 let mut batch_completed = false;
9593 if batch_admitted && exact_end < n {
9594 let hs = self.hidden_size;
9595 let mut batch_pos = exact_end;
9596 while batch_pos < n {
9597 let end = (batch_pos + batch_k).min(n);
9598 let bk = end - batch_pos;
9599 let mut hiddens = vec![0.0f32; bk * hs];
9600 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9601 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9602 }
9603 let positions: Vec<usize> = (batch_pos..end).collect();
9604 let t_batch = std::time::Instant::now();
9605 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9606 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9607 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9608 eprintln!(
9609 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9610 batch_pos,
9611 end.saturating_sub(1),
9612 bk as f64 / (ms / 1000.0),
9613 );
9614 }
9615 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9616 self.nll_end();
9617 return Err(err);
9618 }
9619 match outcome {
9620 crate::gpu::BatchGraphOutcome::Completed => {
9621 batch_completed = true;
9622 for row in 0..bk {
9623 nll += self.nll_from_hidden(
9624 &hiddens[row * hs..(row + 1) * hs],
9625 ids[batch_pos + row + 1],
9626 batch_pos + row,
9627 );
9628 cnt += 1;
9629 }
9630 batch_pos = end;
9631 }
9632 crate::gpu::BatchGraphOutcome::Declined => {
9633 if batch_completed {
9634 self.nll_end();
9635 return Err(format!(
9636 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9637 ));
9638 }
9639 break;
9640 }
9641 crate::gpu::BatchGraphOutcome::Failed => {
9642 self.nll_end();
9643 return Err(format!(
9644 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9645 ));
9646 }
9647 }
9648 }
9649 if batch_completed && cnt == n.saturating_sub(requested_start) {
9650 self.nll_end();
9651 return Ok((nll, cnt));
9652 }
9653 }
9654
9655 for pos in exact_end..n {
9660 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9661 if self
9662 .graph_failed
9663 .swap(false, std::sync::atomic::Ordering::Relaxed)
9664 {
9665 self.cancel
9666 .store(false, std::sync::atomic::Ordering::Relaxed);
9667 self.nll_end();
9668 return Err(format!(
9669 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9670 ));
9671 }
9672 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9673 cnt += 1;
9674 }
9675 self.nll_end();
9676 Ok((nll, cnt))
9677 }
9678
9679 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9687 self.clear_sequence_state();
9688 let n = ids.len().saturating_sub(1);
9689 let mut correct = Vec::with_capacity(n);
9690 let mut pmax = Vec::with_capacity(n);
9691 for pos in 0..n {
9692 let emb = self.embed_single(ids[pos]);
9693 let hidden = self.forward_layers(&emb, pos, None);
9694 let logits = if let Some(logits) = self.graph_logits.take() {
9695 logits
9696 } else {
9697 let normed = inference::rms_norm(
9698 &hidden,
9699 &self.weights.final_norm,
9700 self.rms_eps,
9701 self.norm_style,
9702 );
9703 self.lm_head_forward(&normed)
9707 };
9708 let target = ids[pos + 1] as usize;
9709 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9710 for (i, &v) in logits.iter().enumerate() {
9711 if v > mval {
9712 mval = v;
9713 amax = i;
9714 }
9715 }
9716 correct.push(amax == target);
9717 let row: Vec<f32> = temps
9718 .iter()
9719 .map(|&t| {
9720 let tt = t.max(1e-3);
9721 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9722 1.0 / s.max(1e-12) })
9724 .collect();
9725 pmax.push(row);
9726 }
9727 self.clear_sequence_state();
9728 (correct, pmax)
9729 }
9730
9731 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9738 if self.dyn_router.is_none() {
9739 return Ok((self.ppl_ids(ids)?, 0));
9740 }
9741 self.nll_begin()?;
9742 let saved_active = self.dyn_active;
9743 let mut router = self
9744 .dyn_router
9745 .take()
9746 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9747 router.reset();
9748 self.dyn_phi_seen = 0;
9749 let _ = self.set_active_skill(None);
9750
9751 let result: Result<(f64, usize), String> = (|| {
9752 let mut nll = 0f64;
9753 let mut cnt = 0usize;
9754 for pos in 0..ids.len().saturating_sub(1) {
9755 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9756 self.nll_check_graph("dynamic serial forward", pos)?;
9757 let out_of_band = self.graph_logits.take();
9758 let mut logits = match out_of_band {
9759 Some(lg) => lg,
9760 None => {
9761 let normed = inference::rms_norm(
9762 &hidden,
9763 &self.weights.final_norm,
9764 self.rms_eps,
9765 self.norm_style,
9766 );
9767 self.lm_head_forward(&normed)
9771 }
9772 };
9773 let target = ids[pos + 1] as usize;
9774 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9775 let lse: f64 = logits
9776 .iter()
9777 .map(|&v| ((v - max) as f64).exp())
9778 .sum::<f64>()
9779 .ln()
9780 + max as f64;
9781 let tok_nll = lse - logits[target] as f64;
9782 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9783 let top = logits
9784 .iter()
9785 .enumerate()
9786 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9787 .map(|(i, _)| i)
9788 .unwrap_or(0);
9789 eprintln!(
9790 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9791 logits[target], logits[top]
9792 );
9793 }
9794 nll += tok_nll;
9795 cnt += 1;
9796 attention::recycle_buf(&mut logits);
9797 let phi = self.dyn_phi_ema.clone();
9799 if let Some(new_active) = router.step(&phi, pos) {
9800 let _ = self.set_active_skill(new_active);
9801 }
9802 }
9803 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9804 })();
9805
9806 let _ = self.set_active_skill(saved_active);
9809 self.dyn_router = Some(router);
9810 self.nll_end();
9811 result
9812 }
9813
9814 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9816 self.clear_sequence_state();
9817 let mut acc = vec![0f32; self.hidden_size];
9818 for (pos, &id) in ids.iter().enumerate() {
9819 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9820 for (a, v) in acc.iter_mut().zip(&h) {
9821 *a += v;
9822 }
9823 }
9824 let n = ids.len().max(1) as f32;
9825 for a in acc.iter_mut() {
9826 *a /= n;
9827 }
9828 self.clear_sequence_state();
9829 acc
9830 }
9831
9832 pub fn probe_phi_span(
9846 &mut self,
9847 ids: &[u32],
9848 layer: usize,
9849 span: std::ops::Range<usize>,
9850 ) -> Vec<f32> {
9851 #[cfg(target_os = "macos")]
9852 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9853 let end = span.end.min(ids.len());
9854 let start = span.start.min(end);
9855 let reset = |p: &mut Self| p.clear_sequence_state();
9856 reset(self);
9857 let mut acc = vec![0f32; self.hidden_size];
9858 for (pos, &id) in ids[..end].iter().enumerate() {
9859 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9860 if pos >= start {
9861 for (a, v) in acc.iter_mut().zip(&h) {
9862 *a += v;
9863 }
9864 }
9865 }
9866 let n = end - start;
9867 if n > 0 {
9868 let n = n as f32;
9869 for a in acc.iter_mut() {
9870 *a /= n;
9871 }
9872 }
9873 reset(self);
9874 acc
9875 }
9876
9877 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9884 #[cfg(target_os = "macos")]
9885 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9886 self.graph_logits = None;
9887 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9888 if let Some(logits) = self.graph_logits.take() {
9889 return logits;
9890 }
9891 inference::rms_norm_into(
9892 &hidden,
9893 &self.weights.final_norm,
9894 self.rms_eps,
9895 self.norm_style,
9896 &mut self.ws.n1,
9897 );
9898 self.lm_head_forward(&self.ws.n1)
9899 }
9900
9901 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9907 self.prefill_batch_masked(ids, start_pos, None)
9908 }
9909
9910 fn prefill_batch_masked(
9916 &mut self,
9917 ids: &[u32],
9918 start_pos: usize,
9919 task_mask: Option<&TaskMask>,
9920 ) -> Vec<f32> {
9921 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9922 }
9923
9924 fn prefill_rows(
9932 &mut self,
9933 ids: &[u32],
9934 pos: usize,
9935 task_mask: Option<&TaskMask>,
9936 ) -> Result<Vec<f32>, String> {
9937 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9938 }
9939
9940 fn prefill_input_rows(
9941 &mut self,
9942 input: PrefillIn<'_>,
9943 pos: usize,
9944 task_mask: Option<&TaskMask>,
9945 ) -> Result<Vec<f32>, String> {
9946 self.mimo_moe_prepare();
9947 let hs = self.hidden_size;
9948 let bk = match input {
9949 PrefillIn::Ids(ids) => ids.len(),
9950 PrefillIn::Hidden(rows) => rows.len() / hs,
9951 };
9952 #[cfg(not(target_os = "macos"))]
9953 if task_mask.is_none()
9954 && !self.o1_active()
9955 && bk > 1
9956 && (self.batch_prefix_prefill()
9957 || (self.verify_exact_moe
9958 && crate::gpu::enabled_here()
9959 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9960 {
9961 let mut hiddens = match input {
9962 PrefillIn::Hidden(rows) => rows.to_vec(),
9963 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9964 };
9965 let positions: Vec<usize> = (pos..pos + bk).collect();
9966 let mut run = 0usize;
9967 match self.try_batch_graph_wgpu_prefix(
9968 &mut hiddens,
9969 &positions,
9970 bk,
9971 None,
9972 Some(&mut run),
9973 ) {
9974 crate::gpu::BatchGraphOutcome::Completed => {
9975 let out = if run < self.num_layers {
9976 self.prefill_batch_span(
9977 PrefillIn::Hidden(&hiddens),
9978 pos,
9979 None,
9980 run,
9981 self.num_layers,
9982 )
9983 } else {
9984 hiddens
9985 };
9986 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9987 Err("MiMo attention graph failed after admission".into())
9988 } else {
9989 Ok(out)
9990 };
9991 }
9992 crate::gpu::BatchGraphOutcome::Failed => {
9993 return Err("batched prefix prefill failed after admission".into());
9994 }
9995 crate::gpu::BatchGraphOutcome::Declined => {
9996 #[cfg(feature = "gpu")]
9998 self.pull_lagging_host_kv(0, self.num_layers, pos);
9999 }
10000 }
10001 }
10002 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10003 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10004 Err("batch tail graph failed after admission".into())
10005 } else {
10006 Ok(out)
10007 }
10008 }
10009
10010 fn prefill_batch_span(
10016 &mut self,
10017 input: PrefillIn<'_>,
10018 start_pos: usize,
10019 task_mask: Option<&TaskMask>,
10020 from: usize,
10021 upto_excl: usize,
10022 ) -> Vec<f32> {
10023 let hs = self.hidden_size;
10024 let b = match input {
10025 PrefillIn::Ids(ids) => ids.len(),
10026 PrefillIn::Hidden(hb) => hb.len() / hs,
10027 };
10028 let upto_excl = upto_excl.min(self.num_layers);
10029 let mut h: Vec<f32>;
10033 let mut h_ready;
10034 match input {
10035 PrefillIn::Ids(_) => {
10036 h = vec![0.0; b * hs];
10037 h_ready = false;
10038 }
10039 PrefillIn::Hidden(hb) => {
10040 h = hb.to_vec();
10041 h_ready = true;
10042 }
10043 }
10044 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10045 if let PrefillIn::Ids(ids) = input {
10046 for (bi, &id) in ids.iter().enumerate() {
10047 let e = me.embed_single(id);
10048 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10049 }
10050 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10051 if let Ok(t) = tp.parse::<usize>() {
10052 if t >= start_pos && t < start_pos + ids.len() {
10053 let bi = t - start_pos;
10054 let row = &h[bi * hs..(bi + 1) * hs];
10055 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10056 eprintln!(
10057 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10058 ids[bi],
10059 row[0],
10060 row[1],
10061 ids.len(),
10062 &ids[..ids.len().min(8)]
10063 );
10064 }
10065 }
10066 }
10067 }
10068 };
10069 let (_nkv, _hd, _rd, eps) = (
10070 self.num_kv_heads,
10071 self.head_dim,
10072 self.rotary_dim,
10073 self.rms_eps,
10074 );
10075 let pool = self.pool.clone();
10076 let norm_style = self.norm_style;
10077 self.mimo_moe_prepare();
10078 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10079
10080 #[cfg(target_os = "macos")]
10081 let mut chunk_skip_until = 0usize;
10082 for li in from..upto_excl {
10083 let _capacity_tail = automatic_gpu_prefix
10084 .filter(|&prefix| {
10085 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10086 })
10087 .map(|_| crate::gpu::enter_cpu_scope());
10088 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10093 let _fast_gemm = self
10094 .proj_gate_sigmoid
10095 .then(crate::gpu::enter_prefill_fast_gemm);
10096 #[cfg(target_os = "macos")]
10102 if task_mask.is_none() {
10103 if li < chunk_skip_until {
10104 continue;
10105 }
10106 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10112 fill_h(&mut h, self);
10113 h_ready = true;
10114 }
10115 let ids_for_embed = match input {
10116 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10117 PrefillIn::Hidden(_) => None,
10118 };
10119 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10120 if end > li {
10121 h_ready = true;
10122 chunk_skip_until = end;
10123 if self.is_loop_end(end - 1) && end < self.num_layers {
10126 for bi in 0..b {
10127 let normed = inference::rms_norm(
10128 &h[bi * hs..(bi + 1) * hs],
10129 &self.weights.final_norm,
10130 eps,
10131 norm_style,
10132 );
10133 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10134 }
10135 }
10136 continue;
10137 }
10138 }
10139 if !h_ready {
10140 fill_h(&mut h, self);
10141 h_ready = true;
10142 }
10143 if task_mask.is_none() && self.verify_exact_moe {
10144 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10145 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10146 crate::gpu::BatchGraphOutcome::Completed => continue,
10147 crate::gpu::BatchGraphOutcome::Failed => return h,
10148 crate::gpu::BatchGraphOutcome::Declined => {}
10149 }
10150 }
10151 #[cfg(feature = "gpu")]
10152 self.pull_lagging_host_kv(li, li + 1, start_pos);
10153 let lw = &self.weights.layers[self.phys_layer(li)];
10154 let t_attn = std::time::Instant::now();
10155 match &lw.attn {
10157 AttnKind::Kda(w) => {
10158 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10160 let mut normed = vec![0.0f32; b * hs];
10161 for bi in 0..b {
10162 inference::rms_norm_into(
10163 &h[bi * hs..(bi + 1) * hs],
10164 &lw.input_norm,
10165 eps,
10166 norm_style,
10167 &mut normed[bi * hs..(bi + 1) * hs],
10168 );
10169 }
10170 let attn = crate::linear_core::kda_forward_batch(
10171 &normed,
10172 b,
10173 w,
10174 &cfg,
10175 &mut self.kv_cache.layers[li].linear_state,
10176 pool.as_deref(),
10177 );
10178 for (dst, &a) in h.iter_mut().zip(&attn) {
10179 *dst += a;
10180 }
10181 }
10182 AttnKind::LinearGdn(w) => {
10183 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10185 let mut normed = vec![0.0f32; b * hs];
10186 for bi in 0..b {
10187 let r = inference::rms_norm(
10188 &h[bi * hs..(bi + 1) * hs],
10189 &lw.input_norm,
10190 eps,
10191 norm_style,
10192 );
10193 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10194 }
10195 let attn = crate::linear_core::gdn_forward_batch(
10196 &normed,
10197 b,
10198 w,
10199 &cfg,
10200 &mut self.kv_cache.layers[li].linear_state,
10201 pool.as_deref(),
10202 );
10203 for (dst, &a) in h.iter_mut().zip(&attn) {
10204 *dst += a;
10205 }
10206 }
10207 AttnKind::ShortConv(w) => {
10208 let cfg = self
10211 .short_conv_cfg
10212 .expect("short-conv layer without short_conv_cfg");
10213 let mut normed = vec![0.0f32; b * hs];
10214 for bi in 0..b {
10215 inference::rms_norm_into(
10216 &h[bi * hs..(bi + 1) * hs],
10217 &lw.input_norm,
10218 eps,
10219 norm_style,
10220 &mut normed[bi * hs..(bi + 1) * hs],
10221 );
10222 }
10223 let attn = short_conv_forward_batch(
10224 &normed,
10225 b,
10226 w,
10227 &cfg,
10228 &mut self.kv_cache.layers[li].linear_state,
10229 pool.as_deref(),
10230 );
10231 for (dst, &a) in h.iter_mut().zip(&attn) {
10232 *dst += a;
10233 }
10234 }
10235 AttnKind::Mla(w) => {
10236 let inv_freq_l = self.layer_inv_freq(li);
10239 let rs = self.layer_rope_scale(li);
10240 let mut normed = vec![0.0f32; hs];
10241 for bi in 0..b {
10242 inference::rms_norm_into(
10243 &h[bi * hs..(bi + 1) * hs],
10244 &lw.input_norm,
10245 eps,
10246 norm_style,
10247 &mut normed,
10248 );
10249 let ao = mla_attention(
10250 w,
10251 &normed,
10252 &mut self.kv_cache.layers[li],
10253 start_pos + bi,
10254 &inv_freq_l,
10255 rs,
10256 eps,
10257 pool.as_deref(),
10258 );
10259 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10260 *dst += a;
10261 }
10262 }
10263 }
10264 AttnKind::Full {
10265 wq,
10266 wk,
10267 wv,
10268 wo,
10269 q_norm,
10270 k_norm,
10271 output_gate,
10272 softplus_gate,
10273 bias,
10274 } => {
10275 let mut normed = vec![0.0f32; b * hs];
10279 for bi in 0..b {
10280 inference::rms_norm_into(
10281 &h[bi * hs..(bi + 1) * hs],
10282 &lw.input_norm,
10283 eps,
10284 norm_style,
10285 &mut normed[bi * hs..(bi + 1) * hs],
10286 );
10287 }
10288 let inv_freq_l = self.layer_inv_freq(li);
10289 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10290 let cfg = QwenAttnCfg {
10291 num_heads: self.layer_num_heads(li),
10292 num_kv_heads: nkv_l,
10293 head_dim: hd_l,
10294 hidden_size: hs,
10295 position: start_pos,
10296 inv_freq: &inv_freq_l,
10297 rotary_dim: rd_l,
10298 scale: self.attn_scale,
10299 softcap: self.attn_softcap,
10300 window: self.layer_window(li),
10301 v_norm: self.attn_v_norm,
10302 qk_norm_after_rope: self.qk_norm_after_rope,
10303 gate_sigmoid: self.proj_gate_sigmoid,
10304 q_norm: q_norm.as_deref(),
10305 k_norm: k_norm.as_deref(),
10306 output_gate: *output_gate,
10307 softplus_gate: softplus_gate
10308 .as_ref()
10309 .map(|(gate, per_head)| (gate, *per_head)),
10310 rope_scale: self.layer_rope_scale(li),
10311 bias: bias
10312 .as_ref()
10313 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10314 rms_eps: eps,
10315 norm_style,
10316 pool: pool.as_deref(),
10317 v_head_dim: self.layer_v_dim(li),
10318 };
10319 #[cfg(feature = "gpu")]
10320 let _mirror = self
10321 .prefill_mirror_target(li)
10322 .map(crate::gpu::enter_prefill_mirror);
10323 let mut attn = attention::qwen_attention_batch(
10324 &normed,
10325 b,
10326 wq,
10327 wk,
10328 wv,
10329 wo,
10330 &mut self.kv_cache.layers[li],
10331 &cfg,
10332 );
10333 if let Some(w) = &lw.attn_out_norm {
10334 for bi in 0..b {
10335 inference::rms_norm_into(
10336 &attn[bi * hs..(bi + 1) * hs],
10337 w,
10338 eps,
10339 norm_style,
10340 &mut normed[bi * hs..(bi + 1) * hs],
10341 );
10342 }
10343 attn.copy_from_slice(&normed);
10344 }
10345 for (dst, &a) in h.iter_mut().zip(&attn) {
10346 *dst += a;
10347 }
10348 }
10349 AttnKind::Bounded(w) => {
10350 let mut normed = vec![0.0f32; b * hs];
10353 for bi in 0..b {
10354 inference::rms_norm_into(
10355 &h[bi * hs..(bi + 1) * hs],
10356 &lw.input_norm,
10357 eps,
10358 norm_style,
10359 &mut normed[bi * hs..(bi + 1) * hs],
10360 );
10361 }
10362 let rope = self
10363 .bounded_rope
10364 .clone()
10365 .expect("bounded layer without an installed rotation table");
10366 let cfg = crate::bounded::BoundedAttnCfg {
10367 num_heads: self.num_heads,
10368 num_kv_heads: self.num_kv_heads,
10369 head_dim: self.head_dim,
10370 hidden_size: hs,
10371 scale: self.attn_scale,
10372 rope: &rope,
10373 pool: pool.as_deref(),
10374 };
10375 let mut attn = crate::bounded::bounded_attention_batch(
10376 &normed,
10377 b,
10378 w,
10379 &mut self.kv_cache.layers[li],
10380 &cfg,
10381 );
10382 if let Some(wn) = &lw.attn_out_norm {
10383 for bi in 0..b {
10384 inference::rms_norm_into(
10385 &attn[bi * hs..(bi + 1) * hs],
10386 wn,
10387 eps,
10388 norm_style,
10389 &mut normed[bi * hs..(bi + 1) * hs],
10390 );
10391 }
10392 attn.copy_from_slice(&normed);
10393 }
10394 for (dst, &a) in h.iter_mut().zip(&attn) {
10395 *dst += a;
10396 }
10397 attention::recycle_buf(&mut attn);
10398 }
10399 AttnKind::Linear(w) => {
10400 for bi in 0..b {
10401 let normed = inference::rms_norm(
10402 &h[bi * hs..(bi + 1) * hs],
10403 &lw.input_norm,
10404 eps,
10405 norm_style,
10406 );
10407 vmf_phase_forward(
10408 &normed,
10409 w,
10410 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10411 &mut self.kv_cache.layers[li].linear_state,
10412 pool.as_deref(),
10413 )
10414 .iter()
10415 .enumerate()
10416 .for_each(|(i, &a)| h[bi * hs + i] += a);
10417 }
10418 }
10419 }
10420
10421 let lw = &self.weights.layers[self.phys_layer(li)];
10423 let mut post = vec![0.0f32; b * hs];
10424 for bi in 0..b {
10425 let r =
10426 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10427 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10428 }
10429 let mask_row = task_mask
10432 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10433 .and_then(|m| m.ffn_masks.get(li))
10434 .map(|v| v.as_slice());
10435 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10436 let t_ffn = std::time::Instant::now();
10437 let mut ffn = match &lw.ffn {
10438 FfnKind::Dense(d) if !d.segs.is_empty() => {
10439 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10440 }
10441 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10442 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10443 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10444 }
10445 FfnKind::Moe(m) if self.verify_exact_moe => {
10446 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10447 }
10448 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10451 let before = m.stats.borrow().clone();
10452 let out = crate::gpu::cpu_scope(|| {
10453 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10454 });
10455 self.mimo_moe.prime(li, m, &before);
10456 out
10457 }
10458 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10459 FfnKind::DenseMoe(dm) => {
10462 let mut out = vec![0.0f32; b * hs];
10463 for bi in 0..b {
10464 let r = dense_moe_ffn(
10465 dm,
10466 &post[bi * hs..(bi + 1) * hs],
10467 &h[bi * hs..(bi + 1) * hs],
10468 eps,
10469 norm_style,
10470 pool.as_deref(),
10471 );
10472 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10473 }
10474 out
10475 }
10476 };
10477 if prefill_prof_on() {
10478 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10479 PREFILL_SPLIT[1].fetch_add(
10480 t_ffn.elapsed().as_nanos() as u64,
10481 std::sync::atomic::Ordering::Relaxed,
10482 );
10483 }
10484 if let Some(w) = &lw.ffn_out_norm {
10485 for bi in 0..b {
10486 inference::rms_norm_into(
10487 &ffn[bi * hs..(bi + 1) * hs],
10488 w,
10489 eps,
10490 norm_style,
10491 &mut post[bi * hs..(bi + 1) * hs],
10492 );
10493 }
10494 ffn.copy_from_slice(&post);
10495 }
10496 for (dst, &f) in h.iter_mut().zip(&ffn) {
10497 *dst += f;
10498 }
10499 if let Some(sc) = lw.layer_scale {
10500 for v in h.iter_mut() {
10501 *v *= sc;
10502 }
10503 }
10504 if self.layer_dump.is_some() {
10506 for bi in 0..b {
10507 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10508 }
10509 }
10510 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10511 if let Ok(t) = tp.parse::<usize>() {
10512 if t >= start_pos && t < start_pos + b {
10513 let bi = t - start_pos;
10514 let row = &h[bi * hs..(bi + 1) * hs];
10515 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10516 eprintln!(
10517 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10518 row[0], row[1]
10519 );
10520 }
10521 }
10522 }
10523 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10527 let row = &h[(b - 1) * hs..b * hs];
10528 let rms =
10529 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10530 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10531 eprintln!(
10532 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10533 match &self.weights.layers[self.phys_layer(li)].attn {
10534 AttnKind::LinearGdn(_) => "gdn",
10535 AttnKind::Linear(_) => "vmf",
10536 AttnKind::ShortConv(_) => "conv",
10537 _ => "attn",
10538 },
10539 match &lw.ffn {
10540 FfnKind::Moe(_) => "moe",
10541 FfnKind::Dense(_) => "dense",
10542 FfnKind::DenseMoe(_) => "dense+moe",
10543 },
10544 );
10545 }
10546 if self.is_loop_end(li) && li + 1 < self.num_layers {
10548 for bi in 0..b {
10549 let normed = inference::rms_norm(
10550 &h[bi * hs..(bi + 1) * hs],
10551 &self.weights.final_norm,
10552 eps,
10553 norm_style,
10554 );
10555 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10556 }
10557 }
10558 if std::env::var("CMF_TRACE_H").is_ok() {
10559 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10560 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10561 eprintln!(
10562 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10563 lw.layer_scale
10564 );
10565 }
10566 }
10567 crate::gpu::set_layer(-1); if prefill_prof_on() {
10569 let ms = |a: &std::sync::atomic::AtomicU64| {
10570 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10571 };
10572 let sp = &attention::ATTN_SPLIT;
10573 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10574 eprintln!(
10575 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10576 host gate {:.1}], host loop {:.1}, attend {:.1} \
10577 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10578 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10579 ms(&PREFILL_SPLIT[0]),
10580 ms(&sp[0]),
10581 qkv_enq,
10582 qkv_wait,
10583 ms(&sp[6]),
10584 ms(&sp[1]),
10585 ms(&sp[2]),
10586 ms(&sp[4]),
10587 ms(&sp[5]),
10588 ca_wait,
10589 ms(&sp[3]),
10590 ms(&PREFILL_SPLIT[1]),
10591 );
10592 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10593 eprintln!(
10594 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10595 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10596 fallbacks (cumulative)",
10597 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10598 );
10599 }
10600 self.o1_progress();
10605 self.swa_trim_tails();
10607 h
10608 }
10609
10610 fn embed_single(&self, id: u32) -> Vec<f32> {
10612 let mut out = vec![0.0f32; self.hidden_size];
10613 if (id as usize) < self.weights.embed_tokens.rows() {
10614 self.weights.embed_tokens.row_f32(id as usize, &mut out);
10615 }
10616 if self.embed_multiplier != 1.0 {
10617 for v in out.iter_mut() {
10618 *v *= self.embed_multiplier;
10619 }
10620 }
10621 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
10625 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10626 v[0] = id as f32;
10627 return v;
10628 }
10629 if let Some(b) = &self.g3n {
10632 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10633 }
10634 out
10635 }
10636
10637 #[cfg(target_os = "macos")]
10643 fn chunk_run_gpu(
10644 &mut self,
10645 li0: usize,
10646 h: &mut [f32],
10647 b: usize,
10648 pos0: usize,
10649 embed_ids: Option<&[u32]>,
10650 cap: usize,
10651 ) -> usize {
10652 let gemma4 =
10659 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10660 if !crate::gpu::enabled_here()
10661 || std::env::var("CMF_GPU_CHUNK")
10662 .map(|v| v == "0")
10663 .unwrap_or(false)
10664 || b < 32
10665 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10668 || (self.global_attn.is_some() && !gemma4)
10669 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10673 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10676 || self.o1_active()
10679 || (self.attn_v_norm && !gemma4)
10680 || !self.metal_rope_scale_ok()
10681 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10682 {
10683 return li0;
10684 }
10685 let Some(model) = self.model.clone() else {
10686 return li0;
10687 };
10688 let (nh, nkv, hd, hs) = (
10689 self.num_heads,
10690 self.num_kv_heads,
10691 self.head_dim,
10692 self.hidden_size,
10693 );
10694 let loop_end = if self.loop_final_norm {
10698 ((li0 / self.physical_layers) + 1) * self.physical_layers
10699 } else {
10700 self.num_layers
10701 };
10702 let run_end = self.num_layers.min(loop_end).min(cap);
10703 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10708 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10709 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10710 let mut stored_at: Vec<usize> = Vec::new();
10711 for li in li0..run_end {
10712 let lw = &self.weights.layers[self.phys_layer(li)];
10713 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10714 && !gemma4
10715 {
10716 break;
10717 }
10718 let AttnKind::Full {
10719 wq,
10720 wk,
10721 wv,
10722 wo,
10723 q_norm,
10724 k_norm,
10725 output_gate: false,
10726 softplus_gate,
10727 bias,
10728 } = &lw.attn
10729 else {
10730 break;
10731 };
10732 let head_gate = match softplus_gate {
10734 None => None,
10735 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10736 Some((d, r, c)) if r == nh && c == hs => Some(d),
10737 _ => break,
10738 },
10739 Some(_) => break,
10740 };
10741 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10745 FfnKind::Dense(d)
10746 if (matches!(d.act, Act::Silu | Act::Gelu)
10747 || (gemma4 && d.act == Act::GeluTanh))
10748 && d.segs.is_empty() =>
10749 {
10750 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10751 }
10752 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10753 Some((router, experts, q8)) => {
10754 let e0 = &m.experts[0];
10755 let route = Box::new(move |lg: &[f32]| {
10756 let (idx, p, wsum) = moe_route(lg, m, None);
10758 let mut st = m.stats.borrow_mut();
10759 if st.len() < m.experts.len() {
10760 st.resize(m.experts.len(), 0);
10761 }
10762 for &e in &idx {
10763 st[e] += 1;
10764 }
10765 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10766 (idx, w)
10767 });
10768 (
10769 &e0.gate_proj,
10770 &e0.up_proj,
10771 &e0.down_proj,
10772 Act::Silu,
10773 Some(crate::gpu_metal::ChunkMoe {
10774 router,
10775 experts,
10776 q8,
10777 route,
10778 }),
10779 )
10780 }
10781 None => break,
10782 },
10783 _ => break,
10784 };
10785 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10791 t.q8_row_parts()
10792 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10793 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10794 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10795 }
10796 fn cf(t: &QTensor) -> &[f32] {
10797 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10798 }
10799 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10800 if moe.is_none()
10809 && !gemma4
10810 && (col.iter().any(|c| !c.is_empty())
10811 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10812 {
10813 break;
10814 }
10815 let parts = (
10816 cw(wq),
10817 cw(wk),
10818 cw(wv),
10819 cw(wo),
10820 cw(gate_t),
10821 cw(up_t),
10822 cw(down_t),
10823 );
10824 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10825 else {
10826 break;
10827 };
10828 let layer = &self.kv_cache.layers[li];
10829 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10830 break;
10831 }
10832 debug_assert!(
10834 layer.base() == 0
10835 || self
10836 .layer_window(li)
10837 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10838 );
10839 stored_at.push(layer.head_len(0));
10840 let (nkv, hd) = if gemma4 {
10843 let (a, bb, _) = self.layer_geom(li);
10844 (a, bb)
10845 } else {
10846 (nkv, hd)
10847 };
10848 layers.push(crate::gpu_metal::ChunkLayer {
10849 model: &model,
10850 kv_id: self.graph_kv_id,
10851 layer: li,
10852 wq: pq,
10853 wk: pk,
10854 wv: pv,
10855 wo: po,
10856 gate: pg,
10857 up: pu,
10858 down: pd,
10859 input_norm: &lw.input_norm,
10860 post_norm: &lw.post_norm,
10861 bias: bias
10862 .as_ref()
10863 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10864 q_norm: q_norm.as_deref(),
10865 k_norm: k_norm.as_deref(),
10866 inv_freq: &tables[li - li0],
10867 rope_scale: self.layer_rope_scale(li),
10868 rd: self.layer_geom(li).2,
10869 nh,
10870 nkv,
10871 hd,
10872 hs,
10873 inter: gate_t.rows(),
10874 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10875 late_qk_norm: self.qk_norm_after_rope,
10876 eps: self.rms_eps as f32,
10877 window: self.layer_window(li),
10878 head_gate,
10879 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10880 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10881 v_norm: self.attn_v_norm,
10882 scale: self.attn_scale,
10883 attn_out_norm: lw.attn_out_norm.as_deref(),
10884 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10885 layer_scale: lw.layer_scale,
10886 }),
10887 col,
10888 moe,
10889 });
10890 }
10891 if layers.is_empty() {
10892 return li0;
10893 }
10894 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10896 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10897 .iter()
10898 .zip(&rows_kv)
10899 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10900 .collect();
10901 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10902 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10903 let li = layers[i].layer;
10904 let layer = &self.kv_cache.layers[li];
10905 let nkv = rows_kv[i].0;
10906 io.push(crate::gpu_metal::ChunkIo {
10907 cpu_stored: stored_at[i],
10908 cpu_gen: layer.generation(),
10909 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10910 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10911 out_k: ok,
10912 out_v: ov,
10913 imp: oi,
10914 });
10915 }
10916 let n_run = layers.len();
10917 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10918 let ep = embed_ids.and_then(|ids| {
10921 self.weights
10922 .embed_tokens
10923 .q8_row_parts()
10924 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10925 idx,
10926 rows,
10927 row_scale: rs,
10928 ids,
10929 mult: self.embed_multiplier,
10930 })
10931 });
10932 if embed_ids.is_some() && ep.is_none() {
10933 return li0;
10934 }
10935 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10936 return li0;
10937 }
10938 drop(io);
10939 drop(layers);
10940 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10943 let li = li0 + i;
10944 let row = rows_kv[i].1;
10945 let layer = &mut self.kv_cache.layers[li];
10946 for bi in 0..b {
10947 layer.append(
10948 &ok[bi * row..(bi + 1) * row],
10949 &ov[bi * row..(bi + 1) * row],
10950 &[],
10951 );
10952 }
10953 layer.accumulate_imp(oi);
10954 }
10955 last
10956 }
10957
10958 fn layer_is_local(&self, li: usize) -> bool {
10961 if let Some(layers) = &self.sliding_layers {
10962 return layers.get(li).copied().unwrap_or(false);
10963 }
10964 match self.swa {
10965 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10966 None => false,
10967 }
10968 }
10969
10970 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10973 if self.layer_is_local(li) {
10974 if let Some(f) = &self.inv_freq_local {
10975 return f.clone();
10976 }
10977 } else if let Some(f) = &self.inv_freq_global {
10978 return f.clone();
10979 }
10980 self.inv_freq.clone()
10981 }
10982
10983 fn layer_window(&self, li: usize) -> Option<usize> {
10985 self.swa
10986 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10987 }
10988
10989 fn swa_trim_tails(&mut self) {
10998 let Some((slack, align)) = self.swa_trim else {
10999 return;
11000 };
11001 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11002 return;
11003 }
11004 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11005 if let Some(w) = self.layer_window(li) {
11006 self.kv_cache.layers[li].trim_window(w, slack, align);
11007 }
11008 }
11009 }
11010
11011 fn layer_num_heads(&self, li: usize) -> usize {
11012 self.attention_heads_per_layer
11013 .as_ref()
11014 .and_then(|v| v.get(li).copied())
11015 .unwrap_or(self.num_heads)
11016 }
11017
11018 fn layer_rope_scale(&self, li: usize) -> f32 {
11019 if self.layer_is_local(li) {
11020 self.rope_scale_local
11021 } else {
11022 self.rope_scale
11023 }
11024 }
11025
11026 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11029 if !self.layer_is_local(li) {
11030 if let Some((ghd, gkv)) = self.global_attn {
11031 return (gkv, ghd, ghd);
11032 }
11033 }
11034 let rd = if self.layer_is_local(li) {
11035 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11036 } else {
11037 self.rotary_dim
11038 };
11039 let rd = if self
11041 .no_rope_layers
11042 .as_ref()
11043 .and_then(|v| v.get(li))
11044 .copied()
11045 == Some(true)
11046 {
11047 0
11048 } else {
11049 rd
11050 };
11051 (self.layer_num_kv_heads(li), self.head_dim, rd)
11052 }
11053
11054 fn layer_num_kv_heads(&self, li: usize) -> usize {
11057 self.kv_heads_per_layer
11058 .as_ref()
11059 .and_then(|v| v.get(self.phys_layer(li)).copied())
11060 .unwrap_or(self.num_kv_heads)
11061 }
11062
11063 fn layer_v_dim(&self, li: usize) -> usize {
11065 let (_, hd, _) = self.layer_geom(li);
11066 self.v_head_dim.unwrap_or(hd).min(hd)
11067 }
11068
11069 pub fn set_attn_geometry(
11078 &mut self,
11079 kv_heads_per_layer: Option<Vec<usize>>,
11080 v_head_dim: Option<usize>,
11081 ) -> Result<(), String> {
11082 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11083 if self.global_attn.is_some() {
11084 return Err(
11085 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11086 attention geometry"
11087 .into(),
11088 );
11089 }
11090 if self
11091 .weights
11092 .layers
11093 .iter()
11094 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11095 {
11096 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11097 }
11098 }
11099 if let Some(vd) = v_head_dim {
11100 if vd == 0 || vd > self.head_dim {
11101 return Err(format!(
11102 "v_head_dim {vd} must be in 1..={} (head_dim)",
11103 self.head_dim
11104 ));
11105 }
11106 }
11107 if let Some(v) = &kv_heads_per_layer {
11108 if v.len() != self.physical_layers {
11109 return Err(format!(
11110 "kv_heads_per_layer has {} entries, expected {} layers",
11111 v.len(),
11112 self.physical_layers
11113 ));
11114 }
11115 for (li, &nkv) in v.iter().enumerate() {
11116 let is_attn = matches!(
11117 self.weights.layers.get(li).map(|lw| &lw.attn),
11118 Some(AttnKind::Full { .. }) | None
11119 );
11120 if !is_attn {
11121 continue;
11122 }
11123 let nh = self
11124 .attention_heads_per_layer
11125 .as_ref()
11126 .and_then(|h| h.get(li).copied())
11127 .unwrap_or(self.num_heads);
11128 if nkv == 0 || nh % nkv != 0 {
11129 return Err(format!(
11130 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11131 ));
11132 }
11133 }
11134 }
11135 self.kv_heads_per_layer = kv_heads_per_layer;
11136 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11137 if self.kv_heads_per_layer.is_some() {
11138 for li in 0..self.kv_cache.layers.len() {
11139 let full = matches!(
11140 self.weights
11141 .layers
11142 .get(self.phys_layer(li))
11143 .map(|lw| &lw.attn),
11144 Some(AttnKind::Full { .. })
11145 );
11146 let nkv = self.layer_num_kv_heads(li);
11147 let cache = &self.kv_cache.layers[li];
11148 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11149 let sinks = cache.sinks.clone();
11150 self.kv_cache.layers[li] =
11151 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11152 self.kv_cache.layers[li].sinks = sinks;
11153 }
11154 }
11155 }
11156 Ok(())
11157 }
11158
11159 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11163 let Some(lw) = self.weights.layers.get(phys) else {
11164 return Err(format!("sinks for layer {phys}: no such layer"));
11165 };
11166 if !matches!(lw.attn, AttnKind::Full { .. }) {
11167 return Err(format!(
11168 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11169 ));
11170 }
11171 let nh = self
11172 .attention_heads_per_layer
11173 .as_ref()
11174 .and_then(|h| h.get(phys).copied())
11175 .unwrap_or(self.num_heads);
11176 if sinks.len() != nh {
11177 return Err(format!(
11178 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11179 sinks.len()
11180 ));
11181 }
11182 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11183 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11184 }
11185 for li in 0..self.kv_cache.layers.len() {
11186 if self.phys_layer(li) == phys {
11187 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11188 }
11189 }
11190 Ok(())
11191 }
11192
11193 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11203 if self.kv_heads_per_layer.is_some() {
11204 return Some("per-layer KV head counts");
11205 }
11206 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11207 return Some("V heads narrower than Q/K heads");
11208 }
11209 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11210 return Some("learned attention sinks");
11211 }
11212 if self.swa.is_some() || self.sliding_layers.is_some() {
11213 return Some("sliding-window layers");
11214 }
11215 None
11216 }
11217
11218 #[cfg(target_os = "macos")]
11226 fn metal_rope_scale_ok(&self) -> bool {
11227 !(self.qk_norm_after_rope
11228 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11229 }
11230
11231 #[cfg(target_os = "macos")]
11248 fn metal_graph_swa(&self) -> bool {
11249 (self.swa.is_some() || self.sliding_layers.is_some())
11250 && self.attention_heads_per_layer.is_none()
11251 && !self.attn_v_norm
11252 && self.attn_softcap == 0.0
11253 && self.kv_heads_per_layer.is_none()
11254 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11255 && self.global_attn.is_none()
11256 && self.inv_freq_global.is_none()
11257 && self.metal_rope_scale_ok()
11258 && !(0..self.num_layers).any(|li| {
11259 self.layer_is_local(li)
11260 && self.inv_freq_local.is_none()
11261 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11262 })
11263 }
11264
11265 #[cfg(target_os = "macos")]
11273 fn metal_gemma4(&self) -> bool {
11274 crate::gpu_metal::gemma4::enabled()
11275 && (self.global_attn.is_some() || self.attn_v_norm)
11276 && self.attention_heads_per_layer.is_none()
11277 && self.kv_heads_per_layer.is_none()
11278 && self.v_head_dim.is_none()
11279 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11280 && self.attn_softcap == 0.0
11281 && !self.qk_norm_after_rope
11282 && self.metal_rope_scale_ok()
11283 && self
11284 .weights
11285 .layers
11286 .iter()
11287 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11288 && !(0..self.num_layers).any(|li| {
11289 self.layer_is_local(li)
11290 && self.inv_freq_local.is_none()
11291 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11292 })
11293 }
11294
11295 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11302 let geometry_reason = self.graph_attn_decline_reason();
11306 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11307 if has_scaled_rope && geometry_reason.is_none() {
11308 return Some("scaled RoPE without per-layer geometry");
11309 }
11310 if has_scaled_rope && self.qk_norm_after_rope {
11315 return Some("scaled RoPE with post-RoPE Q/K norm");
11316 }
11317 geometry_reason?;
11318 if self.attention_heads_per_layer.is_some() {
11324 return Some("per-layer Q head counts with per-layer geometry");
11325 }
11326 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11330 if !gemma4
11331 && self.weights.layers.iter().any(|lw| {
11332 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11333 })
11334 {
11335 return Some("sandwich norms / layer scale with per-layer geometry");
11336 }
11337 if self.weights.layers.iter().any(|lw| {
11341 matches!(
11342 &lw.attn,
11343 AttnKind::Full {
11344 output_gate: true,
11345 ..
11346 }
11347 )
11348 }) && self.v_head_dim.is_some()
11349 {
11350 return Some("gated attention with V narrower than K");
11351 }
11352 if (0..self.num_layers).any(|li| {
11353 self.layer_is_local(li)
11354 && self.inv_freq_local.is_none()
11355 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11356 }) {
11357 return Some("local rotary width without a local RoPE table");
11358 }
11359 None
11360 }
11361
11362 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11366 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11369 return None;
11370 }
11371 if self.global_attn.is_some() {
11372 return Some("per-layer head width (Gemma-4 global layers)");
11373 }
11374 if self.attn_v_norm {
11375 return Some("V norm");
11376 }
11377 if self.weights.layers.iter().any(|lw| {
11378 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11379 }) {
11380 return Some("sandwich norms / layer scale");
11381 }
11382 None
11383 }
11384
11385 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11390 self.graph_attn_decline_reason()?;
11391 let (nkv, hd, rd) = self.layer_geom(li);
11392 let invf: &[f32] = if self.layer_is_local(li) {
11393 match &self.inv_freq_local {
11394 Some(f) => f.as_slice(),
11395 None => self.inv_freq.as_slice(),
11396 }
11397 } else {
11398 match &self.inv_freq_global {
11399 Some(f) => f.as_slice(),
11400 None => self.inv_freq.as_slice(),
11401 }
11402 };
11403 Some(crate::gpu::GraphAttnGeom {
11404 nkv,
11405 hd,
11406 dv: self.layer_v_dim(li),
11407 rd,
11408 invf,
11409 rope_scale: self.layer_rope_scale(li),
11410 window: self.layer_window(li),
11411 sink: self.kv_cache.layers[li].sinks.as_deref(),
11412 v_norm: self.attn_v_norm,
11413 })
11414 }
11415
11416 #[cfg(feature = "gpu")]
11428 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11429 if !self.proj_gate_sigmoid
11430 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11431 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11432 || self.graph_refused()
11433 || self.o1_active()
11434 || self.attn_softcap > 0.0
11435 || self.wgpu_graph_attn_decline().is_some()
11436 {
11437 return None;
11438 }
11439 let g = self.graph_attn_geom(li)?;
11440 let (nkv, hd, _) = self.layer_geom(li);
11441 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11443 return None;
11444 }
11445 Some(crate::gpu::PrefillMirror {
11446 kv_id: self.graph_kv_id,
11447 layer: li,
11448 limit: self.kv_cache.max_seq_len,
11449 })
11450 }
11451
11452 #[cfg(feature = "gpu")]
11461 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11462 let kv_id = self.graph_kv_id;
11463 for li in from..upto.min(self.num_layers) {
11464 if !matches!(
11465 self.weights.layers[self.phys_layer(li)].attn,
11466 AttnKind::Full { .. }
11467 ) {
11468 continue;
11469 }
11470 let host = self.kv_cache.layers[li].pos_len();
11472 if host >= position {
11473 continue;
11474 }
11475 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11476 continue;
11477 };
11478 let to = dev.min(position);
11479 if to <= host {
11480 continue;
11481 }
11482 let (nkv, hd) = {
11483 let c = &self.kv_cache.layers[li];
11484 (c.num_kv_heads, c.head_dim)
11485 };
11486 let Some((k, v, first_valid)) =
11487 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11488 else {
11489 continue;
11490 };
11491 let need_from = match self.layer_window(li) {
11494 Some(w) => host.max((position + 1).saturating_sub(w)),
11495 None => host,
11496 };
11497 if first_valid > need_from {
11498 tracing::warn!(
11499 "layer {li}: device KV rows {host}..{to} no longer resident \
11500 (from {first_valid}); host attention will miss them"
11501 );
11502 }
11503 let row = nkv * hd;
11504 let cache = &mut self.kv_cache.layers[li];
11505 for p in 0..to - host {
11506 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11507 }
11508 }
11509 }
11510
11511 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11514 let mut seen = self.graph_declines.borrow_mut();
11515 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11516 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11517 seen.push((site, reason));
11518 }
11519 }
11520
11521 pub fn graph_declines(&self) -> Vec<String> {
11524 self.graph_declines
11525 .borrow()
11526 .iter()
11527 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11528 .collect()
11529 }
11530
11531 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11540 let Some(dir) = &self.layer_dump else {
11541 return;
11542 };
11543 let mut bytes = Vec::with_capacity(row.len() * 4);
11544 for v in row {
11545 bytes.extend_from_slice(&v.to_le_bytes());
11546 }
11547 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11548 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11549 use std::sync::atomic::{AtomicBool, Ordering};
11550 static SAID: AtomicBool = AtomicBool::new(false);
11551 if !SAID.swap(true, Ordering::Relaxed) {
11552 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11553 }
11554 }
11555 }
11556
11557 fn mimo_moe_prepare(&mut self) {
11560 if !self.mimo_moe.is_undecided() {
11561 return;
11562 }
11563 let slot = {
11564 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11565 .filter_map(
11566 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11567 FfnKind::Moe(m) => Some((li, m)),
11568 _ => None,
11569 },
11570 )
11571 .collect();
11572 if layers.is_empty()
11575 || self.physical_layers != self.num_layers
11576 || self.gpu_plan.is_some()
11577 {
11578 crate::mimo_moe::Slot::Off
11579 } else {
11580 let graph_prefix =
11584 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11585 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11586 }
11587 };
11588 self.mimo_moe = slot;
11589 }
11590
11591 #[cfg(test)]
11592 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11593 self.graph_kv_id
11594 }
11595
11596 pub(crate) fn mimo_graph_layer_rows(
11600 &mut self,
11601 li: usize,
11602 h: &mut [f32],
11603 positions: &[usize],
11604 ) -> crate::gpu::BatchGraphOutcome {
11605 use crate::gpu::BatchGraphOutcome as Out;
11606 let b = positions.len();
11607 if !(1..=4).contains(&b)
11608 || h.len() != b * self.hidden_size
11609 || !self.mimo_moe.is_dynamic(li, true)
11610 || !crate::gpu::enabled_here()
11611 || !crate::gpu::wgpu_active()
11612 || self.o1_active()
11613 || self.physical_layers != self.num_layers
11614 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11618 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11619 || self.wgpu_graph_attn_decline().is_some()
11620 {
11621 return Out::Declined;
11622 }
11623 let attn_started = std::time::Instant::now();
11624 let outcome = {
11625 let lw = &self.weights.layers[li];
11626 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11627 return Out::Declined;
11628 }
11629 let FfnKind::Moe(m) = &lw.ffn else {
11630 return Out::Declined;
11631 };
11632 let AttnKind::Full {
11633 wq,
11634 wk,
11635 wv,
11636 wo,
11637 q_norm,
11638 k_norm,
11639 output_gate,
11640 softplus_gate,
11641 bias,
11642 } = &lw.attn
11643 else {
11644 return Out::Declined;
11645 };
11646 if *output_gate || softplus_gate.is_some() {
11647 return Out::Declined;
11648 }
11649 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11650 m.experts
11651 .first()?
11652 .gate_proj
11653 .mapped_q4tp()
11654 .map(|(m, _)| m.clone())
11655 }) else {
11656 return Out::Declined;
11657 };
11658 fn gw<'a>(
11659 t: &'a QTensor,
11660 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11661 ) -> Option<crate::gpu::GraphW<'a>> {
11662 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11663 if m.uid() != owner.uid() || t.has_prism_contract() {
11664 return None;
11665 }
11666 return Some(crate::gpu::GraphW {
11667 idx,
11668 kind,
11669 row_scale: rs,
11670 data: &[],
11671 prism: crate::gpu::GraphPrismOp::None,
11672 affine: false,
11673 });
11674 }
11675 t.as_f32().map(|data| crate::gpu::GraphW {
11676 idx: 0,
11677 kind: 4,
11678 row_scale: &[],
11679 data,
11680 prism: crate::gpu::GraphPrismOp::None,
11681 affine: false,
11682 })
11683 }
11684 let (Some(q), Some(k), Some(v), Some(o)) = (
11685 gw(wq, &model),
11686 gw(wk, &model),
11687 gw(wv, &model),
11688 gw(wo, &model),
11689 ) else {
11690 return Out::Declined;
11691 };
11692 let layer = crate::gpu::GraphLayer {
11693 input_norm: &lw.input_norm,
11694 post_norm: &lw.post_norm,
11695 attn_out_norm: None,
11696 ffn_out_norm: None,
11697 layer_scale: None,
11698 ffn: crate::gpu::GraphFfn::AttentionOnly,
11699 attn: crate::gpu::GraphAttn::Full {
11700 wq: q,
11701 wk: k,
11702 wv: v,
11703 wo: o,
11704 q_norm: q_norm.as_deref(),
11705 k_norm: k_norm.as_deref(),
11706 late_qk_norm: self.qk_norm_after_rope,
11707 bias: bias
11708 .as_ref()
11709 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11710 output_gate: false,
11711 cpu_k: self.kv_cache.layers[li].k_heads(),
11712 cpu_v: self.kv_cache.layers[li].v_heads(),
11713 cpu_base: self.kv_cache.layers[li].base(),
11714 geom: self.graph_attn_geom(li),
11715 head_gate: None,
11716 },
11717 };
11718 let (nkv, hd, rd) = self.layer_geom(li);
11719 crate::gpu::forward_batch_graph_at(
11720 &model,
11721 self.graph_kv_id,
11722 li,
11723 &[layer],
11724 &self.inv_freq,
11725 h,
11726 self.layer_num_heads(li),
11727 nkv,
11728 hd,
11729 rd,
11730 self.hidden_size,
11731 1,
11732 positions,
11733 self.kv_cache.max_seq_len,
11734 self.norm_style == cortiq_core::NormStyle::Gemma,
11735 self.rms_eps as f32,
11736 self.attn_scale,
11737 b,
11738 &[],
11739 self.o1_epoch,
11740 None,
11741 None,
11742 )
11743 };
11744 match outcome {
11745 Out::Completed => {}
11746 Out::Declined => return Out::Declined,
11747 Out::Failed => {
11748 self.graph_failed
11749 .store(true, std::sync::atomic::Ordering::Relaxed);
11750 return Out::Failed;
11751 }
11752 }
11753 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11754 let hs = self.hidden_size;
11755 let lw = &self.weights.layers[li];
11756 let FfnKind::Moe(m) = &lw.ffn else {
11757 unreachable!()
11758 };
11759 let mut post = vec![0.0; h.len()];
11760 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11761 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11762 }
11763 let mut ffn = if b == 1 {
11764 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11765 } else {
11766 moe_ffn_banked_rows(
11767 &mut self.mimo_moe,
11768 li,
11769 m,
11770 &post,
11771 b,
11772 hs,
11773 self.pool.as_deref(),
11774 )
11775 };
11776 for (x, &f) in h.iter_mut().zip(&ffn) {
11777 *x += f;
11778 }
11779 attention::recycle_buf(&mut ffn);
11780 if self.layer_dump.is_some() {
11781 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11782 self.dump_layer_row(pos, li, row);
11783 }
11784 }
11785 crate::mimo_moe::note_attention_graph(b, attn_ns);
11786 Out::Completed
11787 }
11788
11789 fn layer_attn_plain(&self, li: usize) -> bool {
11790 self.kv_heads_per_layer.is_none()
11791 && self.v_head_dim.is_none()
11792 && self.global_attn.is_none()
11793 && self.layer_window(li).is_none()
11794 && self.kv_cache.layers[li].sinks.is_none()
11795 }
11796
11797 fn forward_layers(
11799 &mut self,
11800 hidden: &[f32],
11801 position: usize,
11802 task_mask: Option<&TaskMask>,
11803 ) -> Vec<f32> {
11804 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11805 self.o1_progress();
11806 self.swa_trim_tails();
11807 out
11808 }
11809
11810 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11818 self.embed_single(id)
11819 }
11820
11821 pub fn split_supported(&self) -> Result<(), String> {
11825 if self.dsv4.is_some() {
11826 return Err(
11827 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11828 );
11829 }
11830 if self.dsv41.is_some() {
11831 return Err(
11832 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11833 .into(),
11834 );
11835 }
11836 if self.qwen4_exp.is_some() {
11837 return Err(
11838 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11839 );
11840 }
11841 if self.g3n.is_some() {
11842 return Err(
11843 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11844 );
11845 }
11846 Ok(())
11847 }
11848
11849 pub fn forward_span(
11854 &mut self,
11855 hidden: &[f32],
11856 position: usize,
11857 from: usize,
11858 upto: usize,
11859 task_mask: Option<&TaskMask>,
11860 ) -> Result<Vec<f32>, String> {
11861 self.split_supported()?;
11862 if from > upto || upto >= self.num_layers {
11863 return Err(format!(
11864 "forward_span: layer range {from}..={upto} outside 0..{}",
11865 self.num_layers
11866 ));
11867 }
11868 if hidden.len() != self.hidden_size {
11869 return Err(format!(
11870 "forward_span: hidden len {} ≠ hidden_size {}",
11871 hidden.len(),
11872 self.hidden_size
11873 ));
11874 }
11875 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11876 self.o1_progress();
11877 self.swa_trim_tails();
11878 if self
11879 .graph_failed
11880 .swap(false, std::sync::atomic::Ordering::Relaxed)
11881 {
11882 self.cancel
11883 .store(false, std::sync::atomic::Ordering::Relaxed);
11884 self.clear_sequence_state();
11885 return Err("forward_span: deferred O(1) transition failed".into());
11886 }
11887 Ok(out)
11888 }
11889
11890 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11893 let normed = inference::rms_norm(
11894 hidden,
11895 &self.weights.final_norm,
11896 self.rms_eps,
11897 self.norm_style,
11898 );
11899 self.lm_head_forward(&normed)
11900 }
11901
11902 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11904 sampler::sample_with_scratch(
11905 logits,
11906 &self.sampler_config,
11907 past_tokens,
11908 &mut self.rng,
11909 &mut self.sampler_scratch,
11910 )
11911 }
11912
11913 pub fn reset_session(&mut self) {
11915 self.clear_sequence_state();
11916 }
11917
11918 pub fn prefill_span_ids(
11924 &mut self,
11925 ids: &[u32],
11926 start_pos: usize,
11927 upto: usize,
11928 task_mask: Option<&TaskMask>,
11929 ) -> Result<Vec<f32>, String> {
11930 self.split_supported()?;
11931 if upto >= self.num_layers {
11932 return Err(format!(
11933 "prefill_span_ids: upto {upto} outside 0..{}",
11934 self.num_layers
11935 ));
11936 }
11937 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11941 let out =
11942 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11943 self.check_o1_progress_failure("prefill_span_ids")?;
11944 Ok(out)
11945 } else {
11946 let hs = self.hidden_size;
11947 let mut out = Vec::with_capacity(ids.len() * hs);
11948 for (i, &id) in ids.iter().enumerate() {
11949 let emb = self.embed_id(id);
11950 out.extend_from_slice(&self.forward_span(
11951 &emb,
11952 start_pos + i,
11953 0,
11954 upto,
11955 task_mask,
11956 )?);
11957 }
11958 Ok(out)
11959 }
11960 }
11961
11962 pub fn prefill_span_hidden(
11965 &mut self,
11966 hidden: &[f32],
11967 start_pos: usize,
11968 from: usize,
11969 upto: usize,
11970 task_mask: Option<&TaskMask>,
11971 ) -> Result<Vec<f32>, String> {
11972 self.split_supported()?;
11973 let hs = self.hidden_size;
11974 if hidden.is_empty() || hidden.len() % hs != 0 {
11975 return Err(format!(
11976 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11977 hidden.len()
11978 ));
11979 }
11980 if from > upto || upto >= self.num_layers {
11981 return Err(format!(
11982 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11983 self.num_layers
11984 ));
11985 }
11986 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11987 let out = self.prefill_batch_span(
11988 PrefillIn::Hidden(hidden),
11989 start_pos,
11990 task_mask,
11991 from,
11992 upto + 1,
11993 );
11994 self.check_o1_progress_failure("prefill_span_hidden")?;
11995 Ok(out)
11996 } else {
11997 let b = hidden.len() / hs;
11998 let mut out = Vec::with_capacity(hidden.len());
11999 for i in 0..b {
12000 let h = self.forward_span(
12001 &hidden[i * hs..(i + 1) * hs],
12002 start_pos + i,
12003 from,
12004 upto,
12005 task_mask,
12006 )?;
12007 out.extend_from_slice(&h);
12008 }
12009 Ok(out)
12010 }
12011 }
12012
12013 fn try_token_graph_wgpu(
12017 &self,
12018 hidden: &[f32],
12019 position: usize,
12020 logits_out: &mut Vec<f32>,
12021 layers_run: &mut usize,
12022 ) -> Option<Result<Vec<f32>, ()>> {
12023 self.try_token_graph_wgpu_steps(
12024 hidden,
12025 position,
12026 logits_out,
12027 1,
12028 None,
12029 Some(layers_run),
12030 0,
12031 self.num_layers,
12032 )
12033 }
12034
12035 fn try_token_graph_wgpu_span(
12039 &self,
12040 hidden: &[f32],
12041 position: usize,
12042 logits_out: &mut Vec<f32>,
12043 from: usize,
12044 upto_excl: usize,
12045 layers_run: &mut usize,
12046 ) -> Option<Result<Vec<f32>, ()>> {
12047 self.try_token_graph_wgpu_steps(
12048 hidden,
12049 position,
12050 logits_out,
12051 1,
12052 None,
12053 Some(layers_run),
12054 from,
12055 upto_excl,
12056 )
12057 }
12058
12059 #[cfg(feature = "gpu")]
12063 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12064 if !crate::minicpm_tail::minimax_pool_on()
12065 || !crate::gpu::enabled_here()
12066 || self.head_clusters.is_some()
12067 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12068 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12069 {
12070 return None;
12071 }
12072 let (model, i, kind, rs) = self
12073 .weights
12074 .lm_head
12075 .graph_weight()
12076 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12077 let name = &model.tensors[i].name;
12078 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12079 crate::gpu::GraphPrismOp::InverseEmbedding
12080 } else if crate::prism::is_forward_weight(&model, name) {
12081 crate::gpu::GraphPrismOp::Forward
12082 } else {
12083 crate::gpu::GraphPrismOp::None
12084 };
12085 let gw = crate::gpu::GraphW {
12086 idx: i,
12087 kind,
12088 row_scale: rs,
12089 data: &[],
12090 prism,
12091 affine: crate::prism::is_affine_target(&model, name),
12092 };
12093 let rows = self.weights.lm_head.rows();
12094 let mut h = hidden.to_vec();
12095 let mut lg = Vec::new();
12096 let mut layers_run = 0usize;
12097 let outcome = crate::gpu::forward_token_graph(
12098 &model,
12099 self.graph_kv_id,
12100 &[],
12101 &[],
12102 self.o1_epoch,
12103 &self.inv_freq,
12104 &mut h,
12105 self.num_heads,
12106 self.num_kv_heads,
12107 self.head_dim,
12108 self.attn_scale,
12109 self.head_dim,
12110 self.hidden_size,
12111 self.intermediate_size,
12112 0,
12113 self.kv_cache.max_seq_len,
12114 self.norm_style == cortiq_core::NormStyle::Gemma,
12115 self.rms_eps as f32,
12116 Some((&gw, rows)),
12117 &self.weights.final_norm,
12118 &mut lg,
12119 &[],
12120 1,
12121 None,
12122 None,
12123 Some(&mut layers_run),
12124 0,
12125 false,
12126 );
12127 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12128 return None;
12129 }
12130 lg.resize(self.vocab_size, 0.0);
12133 if let Some(m) = self.logit_multiplier {
12134 for l in lg.iter_mut() {
12135 *l *= m;
12136 }
12137 }
12138 if let Some(c) = self.final_softcap {
12139 softcap_in_place(&mut lg, c, self.pool.as_deref());
12140 }
12141 Some(lg)
12142 }
12143
12144 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12147 if winners.is_empty() {
12148 return;
12149 }
12150 if self.m3_n_exp == 0 {
12151 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12152 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12153 }
12154 for &e in winners {
12155 let e = e as usize;
12156 if e < self.m3_n_exp && li < self.num_layers {
12157 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12158 }
12159 }
12160 if li + 1 == self.num_layers {
12161 self.m3_tokens += 1;
12162 if self.m3_tokens % 16 == 0 {
12163 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12164 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12165 let _ = std::fs::write(&path, bytes);
12166 }
12167 }
12168 }
12169 }
12170
12171 fn m3_prewarm(&mut self) {
12176 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12177 return;
12178 };
12179 let Ok(bytes) = std::fs::read(&path) else {
12180 tracing::warn!("m3 prewarm: cannot read profile {path}");
12181 return;
12182 };
12183 let counts: Vec<u32> = bytes
12184 .chunks_exact(4)
12185 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12186 .collect();
12187 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12188 if counts.len() != self.num_layers * n_exp {
12189 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12190 return;
12191 }
12192 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12193 .ok()
12194 .and_then(|v| v.parse().ok())
12195 .unwrap_or(300);
12196 let mut ranked: Vec<(u32, usize, usize)> = counts
12197 .iter()
12198 .enumerate()
12199 .filter(|(_, c)| **c > 0)
12200 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12201 .collect();
12202 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12203 ranked.truncate(limit);
12204 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12205 for (_, li, e) in ranked {
12206 per_layer[li].push(e);
12207 }
12208 let mut admitted = 0usize;
12209 for (li, ids) in per_layer.iter().enumerate() {
12210 if ids.is_empty() {
12211 continue;
12212 }
12213 let lw = &self.weights.layers[self.phys_layer(li)];
12214 let FfnKind::Moe(m) = &lw.ffn else {
12215 continue;
12216 };
12217 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12219 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12220 admitted += ids.len();
12221 }
12222 }
12223 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12224 }
12225
12226 #[cfg(feature = "gpu")]
12237 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12238 if self.o1_active() || self.attn_softcap > 0.0 {
12239 return Ok(false);
12240 }
12241 if !self.m3_prewarmed {
12242 self.m3_prewarmed = true;
12243 self.m3_prewarm();
12244 }
12245 let frame = {
12246 let lw = &self.weights.layers[self.phys_layer(li)];
12247 let FfnKind::Moe(m) = &lw.ffn else {
12248 return Ok(false);
12249 };
12250 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12251 return Ok(false);
12252 }
12253 let t_arm = std::time::Instant::now();
12254 if self.m3_winners.len() != self.num_layers {
12255 self.m3_winners = vec![Vec::new(); self.num_layers];
12256 }
12257 let pred = std::mem::take(&mut self.m3_winners[li]);
12258 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12259 if armed.is_none() {
12260 self.m3_winners[li] = pred; }
12262 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12263 match armed {
12264 Some(frame) => frame,
12265 None => return Ok(false),
12268 }
12269 };
12270 crate::gpu_wgpu::pooled_frame_enter(frame);
12271 let want_logits = self.graph_want_logits;
12275 self.graph_want_logits = false;
12276 let mut lg = Vec::new();
12277 let mut gl = 0usize;
12278 let t_span = std::time::Instant::now();
12279 let span = crate::gpu::gpu_scope(|| {
12280 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12281 });
12282 self.graph_want_logits = want_logits;
12283 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12284 let hh = match span {
12285 Some(Ok(hh)) => hh,
12286 Some(Err(())) => return Err(()),
12287 None => {
12288 return Ok(false);
12291 }
12292 };
12293 if gl != 1 {
12294 return if gl == 0 { Ok(false) } else { Err(()) };
12297 }
12298 *h = hh;
12299 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12300 self.m3_note_route(li, &winners);
12301 if self.m3_winners.len() != self.num_layers {
12302 self.m3_winners = vec![Vec::new(); self.num_layers];
12303 }
12304 self.m3_winners[li] = winners;
12305 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12306 cold.retain(|&(_, w)| {
12309 if w.is_finite() {
12310 true
12311 } else {
12312 tracing::error!("pooled tail: non-finite cold weight from expert route");
12313 false
12314 }
12315 });
12316 if cold.is_empty() {
12317 return Ok(true);
12318 }
12319 let x = crate::gpu_wgpu::pooled_frame_take_input();
12320 if x.len() != self.hidden_size {
12321 return Err(());
12322 }
12323 let lw = &self.weights.layers[self.phys_layer(li)];
12329 let FfnKind::Moe(m) = &lw.ffn else {
12330 return Err(());
12331 };
12332 let t_cold = std::time::Instant::now();
12333 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12334 let mut out = crate::attention::take_buf(self.hidden_size);
12335 let on_device = !force_host
12336 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12337 if !on_device {
12338 crate::attention::recycle_buf(&mut out);
12339 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12340 .iter()
12341 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12342 .collect::<Result<_, ()>>()?;
12343 out = crate::gpu::cpu_scope(|| {
12344 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12345 });
12346 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12347 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12348 }
12349 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12350 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12351 if out.len() != h.len() {
12352 crate::attention::recycle_buf(&mut out);
12353 return Err(());
12354 }
12355 for (dst, src) in h.iter_mut().zip(out.iter()) {
12356 *dst += src;
12357 }
12358 crate::attention::recycle_buf(&mut out);
12359 if on_device {
12360 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12361 }
12362 Ok(true)
12363 }
12364
12365 #[cfg(not(feature = "gpu"))]
12366 fn m3_pooled_layer(
12367 &mut self,
12368 _li: usize,
12369 _position: usize,
12370 _h: &mut Vec<f32>,
12371 ) -> Result<bool, ()> {
12372 Ok(false)
12373 }
12374
12375 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12379 if self.o1_active() || self.attn_softcap > 0.0 {
12380 return None;
12381 }
12382 if let Some(reason) = self.wgpu_graph_attn_decline() {
12385 self.note_graph_decline("wgpu multi-burst", reason);
12386 return None;
12387 }
12388 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12389 if !graph_on || self.graph_refused() {
12390 return None;
12397 }
12398 let emb = self.embed_single(t_next);
12399 let mut lg = Vec::new();
12400 let mut ids = Vec::new();
12401 match self.try_token_graph_wgpu_steps(
12402 &emb,
12403 position,
12404 &mut lg,
12405 k,
12406 Some(&mut ids),
12407 None,
12408 0,
12409 self.num_layers,
12410 ) {
12411 Some(Ok(_)) => {}
12412 Some(Err(())) => {
12413 self.graph_failed
12418 .store(true, std::sync::atomic::Ordering::Relaxed);
12419 return None;
12420 }
12421 None => return None,
12422 }
12423 (ids.len() == k).then_some(ids)
12424 }
12425
12426 fn try_token_graph_wgpu_steps(
12430 &self,
12431 hidden: &[f32],
12432 position: usize,
12433 logits_out: &mut Vec<f32>,
12434 steps: usize,
12435 ids_out: Option<&mut Vec<u32>>,
12436 layers_run: Option<&mut usize>,
12437 from: usize,
12438 upto_excl: usize,
12439 ) -> Option<Result<Vec<f32>, ()>> {
12440 let upto_excl = match self.mimo_moe.graph_prefix_end() {
12443 Some(end) if end < upto_excl => {
12444 if steps != 1 || layers_run.is_none() || from >= end {
12445 return None;
12446 }
12447 end
12448 }
12449 _ => upto_excl,
12450 };
12451 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
12454 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
12455 return None;
12459 }
12460 if let Some(reason) = self.wgpu_graph_attn_decline() {
12467 self.note_graph_decline("wgpu token graph", reason);
12468 return None;
12469 }
12470 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12475 .map(|li| {
12476 if !o1_gpu {
12477 return None;
12478 }
12479 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12480 })
12481 .collect();
12482 if self.o1_active() && o1_gpu {
12483 let want: usize = (from..upto_excl)
12486 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12487 .count();
12488 let have = o1_views.iter().filter(|v| v.is_some()).count();
12489 if want == 0 || have != want {
12490 use std::sync::atomic::{AtomicUsize, Ordering};
12500 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
12501 let code = have * 1000 + want;
12502 if LAST.swap(code, Ordering::Relaxed) != code {
12503 tracing::warn!(
12504 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
12505 );
12506 }
12507 return None;
12508 }
12509 }
12510 let nh = self.num_heads;
12511 let (nkv, hd, rd) = self.layer_geom(0);
12512 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12513 let mut layers = Vec::with_capacity(upto_excl - from);
12514 let mut model = None;
12515 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
12516 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
12517 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
12519 let name = &m.tensors[i].name;
12520 let prism = if crate::prism::is_inverse_embedding(m, name) {
12521 crate::gpu::GraphPrismOp::InverseEmbedding
12522 } else if crate::prism::is_forward_weight(m, name) {
12523 crate::gpu::GraphPrismOp::Forward
12524 } else {
12525 crate::gpu::GraphPrismOp::None
12526 };
12527 return Some(crate::gpu::GraphW {
12528 idx: i,
12529 kind,
12530 row_scale: rs,
12531 data: &[],
12532 prism,
12533 affine: crate::prism::is_affine_target(m, name),
12534 });
12535 }
12536 match t.as_f32() {
12538 Some(d) => Some(crate::gpu::GraphW {
12539 idx: 0,
12540 kind: 4,
12541 row_scale: &[],
12542 data: d,
12543 prism: crate::gpu::GraphPrismOp::None,
12544 affine: false,
12545 }),
12546 None => {
12547 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
12548 eprintln!("batch graph: weight has no graph/f32 representation");
12549 }
12550 None
12551 }
12552 }
12553 }
12554 for li in from..upto_excl {
12555 let lw = &self.weights.layers[self.phys_layer(li)];
12556 if dbg {
12557 let ak = match &lw.attn {
12558 AttnKind::Mla(_) => "Mla".into(),
12559 AttnKind::Full {
12560 output_gate, bias, ..
12561 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
12562 AttnKind::LinearGdn(_) => "LinearGdn".into(),
12563 AttnKind::Kda(_) => "Kda".into(),
12564 AttnKind::Linear(_) => "Linear".into(),
12565 AttnKind::ShortConv(_) => "ShortConv".into(),
12566 AttnKind::Bounded(_) => "Bounded".into(),
12567 };
12568 let fk = match &lw.ffn {
12569 FfnKind::Dense(_) => "Dense",
12570 FfnKind::Moe(_) => "Moe",
12571 FfnKind::DenseMoe(_) => "DenseMoe",
12572 };
12573 eprintln!("graph L{li}: attn={ak} ffn={fk}");
12574 }
12575 let gffn = match &lw.ffn {
12576 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
12580 FfnKind::Dense(d) => {
12581 let Some(act) = d.act.wgpu_graph_act() else {
12585 self.note_graph_decline(
12586 "wgpu token graph",
12587 "dense FFN activation without a graph kernel",
12588 );
12589 return None;
12590 };
12591 crate::gpu::GraphFfn::Dense {
12592 gate: gw(&d.gate_proj)?,
12593 up: gw(&d.up_proj)?,
12594 down: gw(&d.down_proj)?,
12595 act,
12596 }
12597 }
12598 FfnKind::Moe(m) => {
12599 if m.route_tau.is_some() || m.mask.is_some() {
12607 return None;
12608 }
12609 let shared = m.shared.as_ref();
12610 let inter = m.experts.first()?.gate_proj.rows();
12616 let detached_shared = match shared {
12624 Some((se, None))
12625 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12626 {
12627 if !matches!(se.act, Act::Silu)
12628 || se.gate_proj.mapped_q4tp().is_none()
12629 || se.up_proj.mapped_q4tp().is_none()
12630 || se.down_proj.mapped_q4tp().is_none()
12631 {
12632 return None;
12633 }
12634 Some(crate::gpu::GraphMoeDetachedShared {
12635 gate: gw(&se.gate_proj)?,
12636 up: gw(&se.up_proj)?,
12637 down: gw(&se.down_proj)?,
12638 width: se.gate_proj.rows(),
12639 })
12640 }
12641 Some((se, Some(_)))
12645 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12646 {
12647 return None;
12648 }
12649 _ => None,
12650 };
12651 let has_shared = shared.is_some() && detached_shared.is_none();
12652 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
12653 let sgate = match shared {
12654 Some((_, Some(sg))) if has_shared => gw(sg)?,
12655 _ => gw(&m.router)?,
12659 };
12660 let router = gw(&m.router)?;
12661 if router.prism != crate::gpu::GraphPrismOp::None
12667 || sgate.prism != crate::gpu::GraphPrismOp::None
12668 || router.affine
12669 || sgate.affine
12670 {
12671 tracing::warn!(
12672 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
12673 );
12674 return None;
12675 }
12676 let mut experts = Vec::with_capacity(m.experts.len() + 1);
12677 let mut q4tp: Option<bool> = None;
12680 let mut gu_q2: Option<bool> = None;
12683 let mut q82: Option<bool> = None;
12685 let mut moe_act = None;
12689 for e in m
12690 .experts
12691 .iter()
12692 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
12693 {
12694 let Some(e_act) = e.act.wgpu_graph_act() else {
12695 return None;
12696 };
12697 match moe_act {
12698 Some(existing) if existing != e_act => return None,
12699 None => moe_act = Some(e_act),
12700 _ => {}
12701 }
12702 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
12703 return None;
12704 }
12705 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12710 let Some((em, ei, _, _)) = expert_weight
12711 .graph_weight()
12712 .or_else(|| expert_weight.graph_weight_descriptor())
12713 else {
12714 return None;
12715 };
12716 let name = &em.tensors[ei].name;
12717 if crate::prism::is_forward_weight(em, name)
12718 || crate::prism::is_inverse_embedding(em, name)
12719 || crate::prism::is_affine_target(em, name)
12720 {
12721 tracing::warn!(
12722 "resident MoE declined: expert Prism/affine transform is not implemented"
12723 );
12724 return None;
12725 }
12726 }
12727 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12728 Some((mm, gi)) => (
12729 mm,
12730 gi,
12731 e.up_proj.mapped_q4t()?.1,
12732 e.down_proj.mapped_q4t()?.1,
12733 false,
12734 false,
12735 false,
12736 ),
12737 None => match e.gate_proj.mapped_q2tp() {
12738 Some((mm, gi)) => (
12739 mm,
12740 gi,
12741 e.up_proj.mapped_q2tp()?.1,
12742 e.down_proj.mapped_q4tp()?.1,
12743 true,
12744 true,
12745 false,
12746 ),
12747 None => match e.gate_proj.mapped_q4tp() {
12748 Some((mm, gi)) => (
12749 mm,
12750 gi,
12751 e.up_proj.mapped_q4tp()?.1,
12752 e.down_proj.mapped_q4tp()?.1,
12753 true,
12754 false,
12755 false,
12756 ),
12757 None => {
12760 if !crate::gpu::moe_q82_graph_on() {
12761 return None;
12762 }
12763 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12764 (
12765 mm,
12766 gi,
12767 e.up_proj.mapped_q8_2f()?.1,
12768 e.down_proj.mapped_q8_2f()?.1,
12769 false,
12770 false,
12771 true,
12772 )
12773 }
12774 },
12775 },
12776 };
12777 if *q4tp.get_or_insert(is_p) != is_p
12778 || *gu_q2.get_or_insert(is_q2) != is_q2
12779 || *q82.get_or_insert(is_8) != is_8
12780 {
12781 tracing::warn!(
12787 "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."
12788 );
12789 return None;
12790 }
12791 model.get_or_insert_with(|| mm.clone());
12792 experts.push((gi, ui, di));
12793 }
12794 crate::gpu::GraphFfn::Moe {
12795 router,
12796 shared_gate: sgate,
12797 experts,
12798 n_exp: m.experts.len(),
12799 top_k: std::env::var("CMF_TOPK_PROBE")
12805 .ok()
12806 .and_then(|v| v.parse::<usize>().ok())
12807 .filter(|k| *k > 0 && *k <= m.top_k)
12808 .unwrap_or(m.top_k),
12809 inter,
12810 act: moe_act?,
12811 norm_topk: m.norm_topk_prob,
12812 q4tp: q4tp?,
12813 gu_q2: gu_q2.unwrap_or(false),
12814 q82: q82.unwrap_or(false),
12815 sigmoid: m.router_sigmoid,
12816 sigmoid_denom_eps: m.sigmoid_denom_eps,
12817 bias: m.expert_bias.as_deref(),
12818 has_shared,
12819 detached_shared,
12820 shared_gated,
12821 route_scale: m.routed_scaling,
12822 }
12823 }
12824 };
12825 let attn = match &lw.attn {
12826 AttnKind::Full {
12827 wq,
12828 wk,
12829 wv,
12830 wo,
12831 q_norm,
12832 k_norm,
12833 output_gate,
12834 softplus_gate,
12835 bias,
12836 } => {
12837 if self.attention_heads_per_layer.is_some() {
12838 return None;
12839 }
12840 let head_gate = match softplus_gate {
12844 None => None,
12845 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
12846 Some(_) => {
12847 self.note_graph_decline(
12848 "wgpu token graph",
12849 "projected softplus / per-element output gate",
12850 );
12851 return None;
12852 }
12853 };
12854 let (m, _, _, _) = wq.graph_weight_or_blr()?;
12855 model = Some(m.clone());
12856 crate::gpu::GraphAttn::Full {
12857 wq: gw(wq)?,
12858 wk: gw(wk)?,
12859 wv: gw(wv)?,
12860 wo: gw(wo)?,
12861 q_norm: q_norm.as_deref(),
12862 k_norm: k_norm.as_deref(),
12863 late_qk_norm: self.qk_norm_after_rope,
12864 bias: bias
12865 .as_ref()
12866 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12867 output_gate: *output_gate,
12868 cpu_k: self.kv_cache.layers[li].k_heads(),
12869 cpu_v: self.kv_cache.layers[li].v_heads(),
12870 cpu_base: self.kv_cache.layers[li].base(),
12871 geom: self.graph_attn_geom(li),
12872 head_gate,
12873 }
12874 }
12875 AttnKind::LinearGdn(w) => {
12876 let cfg = self.gdn_cfg?;
12877 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
12878 model = Some(m.clone());
12879 crate::gpu::GraphAttn::Gdn {
12880 qkv: gw(&w.in_proj_qkv)?,
12881 z: gw(&w.in_proj_z)?,
12882 a: gw(&w.in_proj_a)?,
12883 b: gw(&w.in_proj_b)?,
12884 out: gw(&w.out_proj)?,
12885 conv1d: &w.conv1d,
12886 a_log: &w.a_log,
12887 dt_bias: &w.dt_bias,
12888 norm: &w.norm,
12889 nv: cfg.num_v_heads,
12890 nk: cfg.num_k_heads,
12891 dk: cfg.key_head_dim,
12892 dv: cfg.value_head_dim,
12893 kk: cfg.conv_kernel,
12894 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12895 }
12896 }
12897 AttnKind::ShortConv(w) => {
12898 let cfg = self.short_conv_cfg?;
12899 let (m, _, _, _) = w
12900 .in_proj
12901 .graph_weight()
12902 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12903 model = Some(m.clone());
12904 crate::gpu::GraphAttn::ShortConv {
12905 inp: gw(&w.in_proj)?,
12906 out: gw(&w.out_proj)?,
12907 taps: &w.conv,
12908 kernel: cfg.kernel,
12909 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12910 }
12911 }
12912 _ => return None,
12913 };
12914 layers.push(crate::gpu::GraphLayer {
12915 input_norm: &lw.input_norm,
12916 attn,
12917 post_norm: &lw.post_norm,
12918 attn_out_norm: lw.attn_out_norm.as_deref(),
12919 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12920 layer_scale: lw.layer_scale,
12921 ffn: gffn,
12922 });
12923 }
12924 let model = model?;
12925 let lm_gw = if upto_excl == self.num_layers
12931 && self.graph_want_logits
12932 && std::env::var("CMF_GPU_LMHEAD")
12933 .map(|v| v != "0")
12934 .unwrap_or(true)
12935 {
12936 self.weights
12937 .lm_head
12938 .graph_weight()
12939 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12940 .map(|(m, i, kind, rs)| {
12941 let name = &m.tensors[i].name;
12942 let prism = if crate::prism::is_inverse_embedding(m, name) {
12943 crate::gpu::GraphPrismOp::InverseEmbedding
12944 } else if crate::prism::is_forward_weight(m, name) {
12945 crate::gpu::GraphPrismOp::Forward
12946 } else {
12947 crate::gpu::GraphPrismOp::None
12948 };
12949 (
12950 crate::gpu::GraphW {
12951 idx: i,
12952 kind,
12953 row_scale: rs,
12954 data: &[],
12955 prism,
12956 affine: crate::prism::is_affine_target(m, name),
12957 },
12958 self.weights.lm_head.rows(),
12959 )
12960 })
12961 } else {
12962 None
12963 };
12964 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12965 let emb_gw = if steps > 1 {
12967 self.weights
12968 .embed_tokens
12969 .graph_weight()
12970 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12971 .map(|(m, i, kind, rs)| {
12972 let name = &m.tensors[i].name;
12973 let prism = if crate::prism::is_inverse_embedding(m, name) {
12974 crate::gpu::GraphPrismOp::InverseEmbedding
12975 } else if crate::prism::is_forward_weight(m, name) {
12976 crate::gpu::GraphPrismOp::Forward
12977 } else {
12978 crate::gpu::GraphPrismOp::None
12979 };
12980 (
12981 crate::gpu::GraphW {
12982 idx: i,
12983 kind,
12984 row_scale: rs,
12985 data: &[],
12986 prism,
12987 affine: crate::prism::is_affine_target(m, name),
12988 },
12989 self.weights.embed_tokens.rows(),
12990 self.embed_multiplier,
12991 )
12992 })
12993 } else {
12994 None
12995 };
12996
12997 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13003 (from..upto_excl.min(self.num_layers - 1))
13004 .filter(|&li| (li + 1) % self.physical_layers == 0)
13005 .map(|li| li - from)
13006 .collect()
13007 } else {
13008 Vec::new()
13009 };
13010 let mut h = hidden.to_vec();
13011 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13017 let outcome = crate::gpu::forward_token_graph(
13018 &model,
13019 self.graph_kv_id,
13020 &layers,
13021 &o1_views,
13022 self.o1_epoch,
13023 &self.inv_freq,
13024 &mut h,
13025 nh,
13026 nkv,
13027 hd,
13028 self.attn_scale,
13029 rd,
13030 self.hidden_size,
13031 self.intermediate_size,
13032 position,
13033 self.kv_cache.max_seq_len,
13034 gemma,
13035 self.rms_eps as f32,
13036 lm,
13037 &self.weights.final_norm,
13038 logits_out,
13039 &loop_norm_at,
13040 steps,
13041 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13042 ids_out,
13043 layers_run,
13044 from,
13045 dump_hidden,
13046 );
13047 match outcome {
13048 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13049 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13050 crate::gpu::TokenGraphOutcome::Declined => None,
13051 }
13052 }
13053
13054 #[cfg(target_os = "macos")]
13063 #[allow(clippy::type_complexity)]
13064 fn metal_rows_plan(
13065 &self,
13066 ) -> Option<(
13067 Vec<MetalRowsItem<'_>>,
13068 std::sync::Arc<cortiq_core::CmfModel>,
13069 Option<crate::gpu_metal::GdnGpuCfg>,
13070 )> {
13071 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13072 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13073 if !graph_force
13074 || !crate::gpu::enabled_here()
13075 || std::env::var("CMF_GPU_BLOCK")
13076 .map(|v| v == "0")
13077 .unwrap_or(false)
13078 || self.attn_softcap > 0.0
13079 || self.o1_active()
13080 || self.swa.is_some()
13081 || self.global_attn.is_some()
13082 || self.attention_heads_per_layer.is_some()
13083 || self.graph_attn_decline_reason().is_some()
13085 || self.attn_v_norm
13086 || self.loop_final_norm
13087 || !self.metal_rope_scale_ok()
13088 {
13089 return None;
13090 }
13091 let attend_contract = self.head_dim % 4 == 0
13092 && self.head_dim <= 256
13093 && self.rotary_dim >= 2
13094 && self.rotary_dim <= self.head_dim
13095 && (self.rotary_dim / 2) % 32 == 0
13096 && self.num_kv_heads > 0
13097 && self.num_heads % self.num_kv_heads == 0;
13098 if !attend_contract {
13099 return None;
13100 }
13101 let mut plan: Vec<MetalRowsItem> = Vec::new();
13102 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13103 for li in 0..self.num_layers {
13104 let lw = &self.weights.layers[self.phys_layer(li)];
13105 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13106 return None;
13107 }
13108 let ffn = match &lw.ffn {
13109 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13110 let (Some(g), Some(u), Some(dn)) = (
13111 d.gate_proj.metal_graph_parts(),
13112 d.up_proj.metal_graph_parts(),
13113 d.down_proj.metal_graph_parts(),
13114 ) else {
13115 return None;
13116 };
13117 MetalFfn::Dense {
13118 gate: g,
13119 up: u,
13120 down: dn,
13121 act: 0, }
13123 }
13124 _ => return None,
13125 };
13126 match &lw.attn {
13127 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13128 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13129 w.in_proj_qkv.metal_graph_parts(),
13130 w.in_proj_z.metal_graph_parts(),
13131 w.in_proj_a.f32_parts(),
13132 w.in_proj_b.f32_parts(),
13133 w.out_proj.metal_graph_parts(),
13134 ) else {
13135 return None;
13136 };
13137 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13138 model_ref.get_or_insert_with(|| model.clone());
13139 }
13140 let gl = GdnGpuLayer {
13141 attn_norm: &lw.input_norm,
13142 post_norm: &lw.post_norm,
13143 qkv,
13144 z,
13145 a,
13146 b: bb,
13147 out,
13148 ffn,
13149 conv1d: &w.conv1d,
13150 a_log: &w.a_log,
13151 dt_bias: &w.dt_bias,
13152 gnorm: &w.norm,
13153 };
13154 match plan.last_mut() {
13155 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13156 _ => plan.push(MetalRowsItem::Gdn {
13157 run: vec![gl],
13158 first: li,
13159 }),
13160 }
13161 }
13162 AttnKind::Full {
13163 wq,
13164 wk,
13165 wv,
13166 wo,
13167 q_norm,
13168 k_norm,
13169 output_gate,
13170 softplus_gate: None,
13171 bias: None,
13172 } => {
13173 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13174 wq.metal_graph_parts(),
13175 wk.metal_graph_parts(),
13176 wv.metal_graph_parts(),
13177 wo.metal_graph_parts(),
13178 ) else {
13179 return None;
13180 };
13181 if let QTensor::Mapped { model, .. } = wq {
13182 model_ref.get_or_insert_with(|| model.clone());
13183 }
13184 let cache = &self.kv_cache.layers[li];
13185 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13186 return None;
13187 }
13188 plan.push(MetalRowsItem::Attn {
13189 l: AttnGpuLayer {
13190 attn_norm: &lw.input_norm,
13191 post_norm: &lw.post_norm,
13192 wq: pq,
13193 wk: pk,
13194 wv: pv,
13195 wo: po,
13196 ffn,
13197 attn_out_norm: None,
13198 ffn_out_norm: None,
13199 layer_scale: None,
13200 },
13201 li,
13202 q_norm: q_norm.as_deref(),
13203 k_norm: k_norm.as_deref(),
13204 output_gate: *output_gate,
13205 });
13206 }
13207 _ => return None,
13208 }
13209 }
13210 let model = model_ref?;
13211 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13212 nv: cfg.num_v_heads,
13213 nk: cfg.num_k_heads,
13214 dk: cfg.key_head_dim,
13215 dv: cfg.value_head_dim,
13216 kk: cfg.conv_kernel,
13217 hidden: self.hidden_size,
13218 inter: self.intermediate_size,
13219 c_dim: cfg.conv_dim(),
13220 eps: cfg.rms_eps as f32,
13221 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13222 });
13223 Some((plan, model, gcfg))
13224 }
13225
13226 #[cfg(target_os = "macos")]
13228 #[allow(clippy::too_many_arguments)]
13229 fn metal_attn_params<'a>(
13230 li: usize,
13231 cache: &'a crate::kv_cache::LayerKvCache,
13232 q_norm: Option<&'a [f32]>,
13233 k_norm: Option<&'a [f32]>,
13234 output_gate: bool,
13235 inv_freq: &'a [f32],
13236 rope_scale: f32,
13237 geom: (usize, usize, usize, usize),
13238 pos0: usize,
13239 kv_id: u64,
13240 scale: f32,
13241 eps: f32,
13242 gemma: bool,
13243 late_qk_norm: bool,
13244 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13245 let (nh, nkv, hd, rd) = geom;
13246 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13247 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13248 let cpu_stored = cpu_k[0].len() / hd;
13249 (
13250 crate::gpu_metal::AttnDeviceParams {
13251 kv_id,
13252 layer: li,
13253 nh,
13254 nkv,
13255 hd,
13256 rd,
13257 position: pos0,
13258 scale,
13259 eps,
13260 gemma,
13261 late_qk_norm,
13262 output_gate,
13263 q_norm,
13264 k_norm,
13265 inv_freq,
13266 rope_scale,
13267 cpu_k,
13268 cpu_v,
13269 cpu_stored,
13270 cpu_gen: cache.generation(),
13271 o1: None,
13272 window: None,
13273 softmax_sink: cache.sinks.as_deref(),
13274 head_gate: None,
13275 v_norm: false,
13276 },
13277 cpu_stored,
13278 )
13279 }
13280
13281 #[cfg(target_os = "macos")]
13286 #[allow(clippy::type_complexity)]
13287 fn metal_rows_run(
13288 &mut self,
13289 hiddens: &mut [f32],
13290 pos0: usize,
13291 b: usize,
13292 prefill: bool,
13293 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13294 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13298 ) -> MetalRowsRun {
13299 use crate::gpu_metal::{GraphDims, VerifyGraph};
13300 if !crate::gpu_metal::wait_replay() {
13306 tracing::error!("Metal rows graph: the pending async replay failed");
13307 return MetalRowsRun::Failed;
13308 }
13309 spec_stamp("v.wait");
13310 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13316 if want > 0 {
13317 let phys = self.physical_layers.max(1);
13318 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13319 let is_gdn = self
13320 .weights
13321 .layers
13322 .get(li % phys)
13323 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13324 if is_gdn && l.linear_state.len() != want {
13325 l.linear_state = vec![0f32; want];
13326 }
13327 }
13328 }
13329 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13330 return MetalRowsRun::Declined;
13331 };
13332 spec_stamp("v.plan");
13333 let dims = GraphDims {
13334 hidden: self.hidden_size,
13335 eps: self.rms_eps as f32,
13336 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13337 };
13338 let Some(mut graph) = (if prefill {
13339 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13340 } else {
13341 VerifyGraph::new(&model, dims, hiddens, b)
13342 }) else {
13343 return MetalRowsRun::Declined;
13344 };
13345 let geom = (
13346 self.num_heads,
13347 self.num_kv_heads,
13348 self.head_dim,
13349 self.rotary_dim,
13350 );
13351 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13352 let eps = self.rms_eps as f32;
13353 let kv_id = self.graph_kv_id;
13354 let inv_freq = self.inv_freq.clone();
13355 for item in &plan {
13356 let ok = match item {
13357 MetalRowsItem::Gdn { run, .. } => gcfg
13358 .as_ref()
13359 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13360 .unwrap_or(false),
13361 MetalRowsItem::Attn {
13362 l,
13363 li,
13364 q_norm,
13365 k_norm,
13366 output_gate,
13367 } => {
13368 let (p, _) = Self::metal_attn_params(
13369 *li,
13370 &self.kv_cache.layers[*li],
13371 *q_norm,
13372 *k_norm,
13373 *output_gate,
13374 &inv_freq,
13375 self.layer_rope_scale(*li),
13376 geom,
13377 pos0,
13378 kv_id,
13379 self.attn_scale,
13380 eps,
13381 gemma,
13382 self.qk_norm_after_rope,
13383 );
13384 graph.attn_ok(l, &p)
13385 }
13386 };
13387 if !ok {
13388 use std::sync::atomic::{AtomicBool, Ordering};
13389 static SAID: AtomicBool = AtomicBool::new(false);
13390 if !SAID.swap(true, Ordering::Relaxed) {
13391 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13392 }
13393 return MetalRowsRun::Declined;
13394 }
13395 }
13396 let lm = match &spec {
13397 Some((lm, _, _)) => {
13398 if !graph.lm_head_ok(*lm) {
13399 return MetalRowsRun::Declined;
13400 }
13401 Some(*lm)
13402 }
13403 None => None,
13404 };
13405 let mut gdn_layers = Vec::new();
13406 let mut attn_layers = Vec::new();
13407 for item in &plan {
13408 match item {
13409 MetalRowsItem::Gdn { run, first } => {
13410 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
13411 .iter()
13412 .map(|l| l.linear_state.as_slice())
13413 .collect();
13414 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
13415 return MetalRowsRun::Declined;
13416 }
13417 gdn_layers.extend(*first..*first + run.len());
13418 }
13419 MetalRowsItem::Attn {
13420 l,
13421 li,
13422 q_norm,
13423 k_norm,
13424 output_gate,
13425 } => {
13426 let (p, cpu_stored) = Self::metal_attn_params(
13427 *li,
13428 &self.kv_cache.layers[*li],
13429 *q_norm,
13430 *k_norm,
13431 *output_gate,
13432 &inv_freq,
13433 self.layer_rope_scale(*li),
13434 geom,
13435 pos0,
13436 kv_id,
13437 self.attn_scale,
13438 eps,
13439 gemma,
13440 self.qk_norm_after_rope,
13441 );
13442 if !graph.encode_attn_b(l, &p) {
13443 return MetalRowsRun::Declined;
13444 }
13445 attn_layers.push((*li, cpu_stored));
13446 }
13447 }
13448 }
13449 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
13450 if !graph.encode_lm_head_b(final_norm, lm) {
13451 return MetalRowsRun::Declined;
13452 }
13453 if let Some((n, _)) = argmax_out.as_ref() {
13458 if !graph.encode_argmax_b(*n) {
13459 argmax_out = None;
13460 }
13461 }
13462 }
13463 spec_stamp("v.enc");
13464 if !graph.sync() {
13465 return MetalRowsRun::Failed;
13466 }
13467 spec_stamp("v.gpu");
13468 match (spec, argmax_out) {
13469 (Some(_), Some((_, ids))) => {
13470 ids.resize(b, 0);
13471 if !graph.read_argmax(ids) {
13472 return MetalRowsRun::Failed;
13473 }
13474 spec_stamp("v.am");
13475 }
13476 (Some((lm, _, logits)), None) => {
13477 logits.resize(b * lm.1, 0.0);
13478 if !graph.read_logits(logits) {
13479 return MetalRowsRun::Failed;
13480 }
13481 spec_stamp("v.lg");
13482 }
13483 (None, _) => {}
13484 }
13485 if !graph.read_hidden(hiddens) {
13486 return MetalRowsRun::Failed;
13487 }
13488 spec_stamp("v.hid");
13489 MetalRowsRun::Completed(MetalVerifyPending {
13490 graph,
13491 gdn_layers,
13492 attn_layers,
13493 })
13494 }
13495
13496 #[cfg(target_os = "macos")]
13502 fn try_batch_graph_metal(
13503 &mut self,
13504 hiddens: &mut [f32],
13505 positions: &[usize],
13506 b: usize,
13507 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13508 argmax_out: Option<(usize, &mut Vec<u32>)>,
13509 ) -> crate::gpu::BatchGraphOutcome {
13510 let _t0 = std::time::Instant::now();
13511 if positions.len() != b
13512 || positions.windows(2).any(|w| w[1] != w[0] + 1)
13513 || hiddens.len() != b * self.hidden_size
13514 {
13515 return crate::gpu::BatchGraphOutcome::Declined;
13516 }
13517 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
13518 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
13519 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
13520 MetalRowsRun::Completed(pending) => pending,
13521 };
13522 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13523 eprintln!(
13524 "metal-verify: {:.1} ms | b={b}",
13525 _t0.elapsed().as_secs_f64() * 1e3
13526 );
13527 }
13528 self.metal_verify = Some(pending);
13529 crate::gpu::BatchGraphOutcome::Completed
13530 }
13531
13532 #[cfg(target_os = "macos")]
13537 fn prefill_rows_metal(
13538 &mut self,
13539 ids: &[u32],
13540 start_pos: usize,
13541 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13542 ) -> MetalPrefillOutcome {
13543 let b = ids.len();
13544 if b == 0 || b > 512 {
13545 return MetalPrefillOutcome::Declined;
13546 }
13547 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13548 let with_head = spec.is_some();
13549 let hs = self.hidden_size;
13550 let mut hiddens = vec![0f32; b * hs];
13551 for (j, &id) in ids.iter().enumerate() {
13552 let e = self.embed_single(id);
13553 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
13554 }
13555 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
13556 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
13557 MetalRowsRun::Failed => {
13558 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13559 return MetalPrefillOutcome::Failed;
13560 }
13561 MetalRowsRun::Completed(pending) => pending,
13562 };
13563 let idxs = pending.gdn_layers.clone();
13565 let mut outs: Vec<&mut [f32]> = self
13566 .kv_cache
13567 .layers
13568 .iter_mut()
13569 .enumerate()
13570 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13571 .map(|(_, l)| l.linear_state.as_mut_slice())
13572 .collect();
13573 if !pending.graph.finish_states(&mut outs) {
13574 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13575 return MetalPrefillOutcome::Failed;
13576 }
13577 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13578 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13582 for (li, cpu_stored) in &pending.attn_layers {
13583 let mut kbuf = vec![0f32; b * nkv * hd];
13584 let mut vbuf = vec![0f32; b * nkv * hd];
13585 if !crate::gpu_metal::kv_mirror_read_rows(
13586 self.graph_kv_id,
13587 *li,
13588 nkv,
13589 hd,
13590 *cpu_stored,
13591 b,
13592 &mut kbuf,
13593 &mut vbuf,
13594 ) {
13595 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13596 return MetalPrefillOutcome::Failed;
13597 }
13598 rows.push((*li, *cpu_stored, kbuf, vbuf));
13599 }
13600 for (li, cpu_stored, kbuf, vbuf) in rows {
13601 let cache = &mut self.kv_cache.layers[li];
13602 for r in 0..b {
13603 cache.append(
13604 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13605 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13606 &[],
13607 );
13608 }
13609 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
13610 }
13611 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13612 if with_head {
13613 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13614 }
13615 MetalPrefillOutcome::Completed(hiddens)
13616 }
13617
13618 #[cfg(target_os = "macos")]
13619 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
13620 self.prefill_rows_metal(ids, start_pos, None)
13621 }
13622
13623 #[cfg(target_os = "macos")]
13628 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
13629 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
13630 return MetalBatchNllOutcome::Declined;
13631 }
13632 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
13633 return MetalBatchNllOutcome::Declined;
13634 };
13635 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
13636 .ok()
13637 .and_then(|v| v.parse::<usize>().ok())
13638 .filter(|&v| (1..=512).contains(&v))
13639 .unwrap_or(32);
13640 let final_norm = self.weights.final_norm.clone();
13641 let mut nll = 0.0f64;
13642 let mut count = 0usize;
13643 let mut pos = 0usize;
13644 let mut completed = 0usize;
13645 while pos < ids.len() {
13646 let end = (pos + chunk).min(ids.len());
13647 let mut logits = Vec::new();
13648 let outcome =
13649 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
13650 match outcome {
13651 MetalPrefillOutcome::Declined => {
13652 return if completed == 0 {
13653 MetalBatchNllOutcome::Declined
13654 } else {
13655 MetalBatchNllOutcome::Failed(format!(
13656 "ordinary Metal NLL batch declined after {completed} chunks"
13657 ))
13658 };
13659 }
13660 MetalPrefillOutcome::Failed => {
13661 return MetalBatchNllOutcome::Failed(
13662 "ordinary Metal NLL batch failed after admission".to_string(),
13663 );
13664 }
13665 MetalPrefillOutcome::Completed(_) => {}
13666 }
13667 completed += 1;
13668 let vocab = self.vocab_size.min(lm.1);
13669 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
13670 return MetalBatchNllOutcome::Failed(
13671 "ordinary Metal NLL head returned an invalid shape".to_string(),
13672 );
13673 }
13674 for row in 0..(end - pos) {
13675 let absolute = pos + row;
13676 if absolute < start || absolute + 1 >= ids.len() {
13677 continue;
13678 }
13679 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
13680 if let Some(mu) = self.logit_multiplier {
13681 for v in lg.iter_mut() {
13682 *v *= mu;
13683 }
13684 }
13685 if let Some(c) = self.final_softcap {
13686 softcap_in_place(lg, c, self.pool.as_deref());
13687 }
13688 let target = ids[absolute + 1] as usize;
13689 if target >= vocab {
13690 return MetalBatchNllOutcome::Failed(format!(
13691 "target token {target} exceeds Metal head rows {vocab}"
13692 ));
13693 }
13694 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13695 let lse: f64 = lg
13696 .iter()
13697 .map(|&v| ((v - max) as f64).exp())
13698 .sum::<f64>()
13699 .ln()
13700 + max as f64;
13701 nll += lse - lg[target] as f64;
13702 count += 1;
13703 }
13704 pos = end;
13705 }
13706 MetalBatchNllOutcome::Completed(nll, count)
13707 }
13708
13709 #[cfg(target_os = "macos")]
13713 fn metal_verify_commit(&mut self, a: usize) -> bool {
13714 let Some(mut pending) = self.metal_verify.take() else {
13715 return false;
13716 };
13717 let n = a + 1;
13718 let idxs = pending.gdn_layers.clone();
13720 let mut outs: Vec<&mut [f32]> = self
13721 .kv_cache
13722 .layers
13723 .iter_mut()
13724 .enumerate()
13725 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13726 .map(|(_, l)| l.linear_state.as_mut_slice())
13727 .collect();
13728 if !pending.graph.commit(n, &mut outs) {
13729 return false;
13730 }
13731 spec_stamp("c.replay");
13732 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13733 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13737 for (li, cpu_stored) in &pending.attn_layers {
13738 let mut kbuf = vec![0f32; n * nkv * hd];
13739 let mut vbuf = vec![0f32; n * nkv * hd];
13740 if !crate::gpu_metal::kv_mirror_read_rows(
13741 self.graph_kv_id,
13742 *li,
13743 nkv,
13744 hd,
13745 *cpu_stored,
13746 n,
13747 &mut kbuf,
13748 &mut vbuf,
13749 ) {
13750 return false;
13751 }
13752 rows.push((*li, *cpu_stored, kbuf, vbuf));
13753 }
13754 for (li, cpu_stored, kbuf, vbuf) in rows {
13755 let cache = &mut self.kv_cache.layers[li];
13756 for r in 0..n {
13757 cache.append(
13758 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13759 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13760 &[],
13761 );
13762 }
13763 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13764 }
13765 spec_stamp("c.kv");
13766 true
13767 }
13768
13769 #[cfg(target_os = "macos")]
13776 fn mtp_warm_batch_submit(
13777 &mut self,
13778 m: &mut MtpModule,
13779 pairs: &[(&[f32], u32)],
13780 first_pos: usize,
13781 ) -> Option<MetalWarmPending> {
13782 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13783 let b = pairs.len();
13784 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13785 return None;
13786 }
13787 let AttnKind::Full {
13788 wq,
13789 wk,
13790 wv,
13791 wo,
13792 q_norm,
13793 k_norm,
13794 output_gate,
13795 softplus_gate: None,
13796 bias: None,
13797 } = &m.layer.attn
13798 else {
13799 return None;
13800 };
13801 let FfnKind::Dense(d) = &m.layer.ffn else {
13802 return None;
13803 };
13804 if !d.segs.is_empty() {
13805 return None;
13806 }
13807 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13808 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13809 else {
13810 return None;
13811 };
13812 let (Some(g), Some(u), Some(dn)) = (
13813 d.gate_proj.q1_parts(),
13814 d.up_proj.q1_parts(),
13815 d.down_proj.q1_parts(),
13816 ) else {
13817 return None;
13818 };
13819 let Some(eh) = m.eh_proj.q1_parts() else {
13820 return None;
13821 };
13822 let QTensor::Mapped { model, .. } = wq else {
13823 return None;
13824 };
13825 let model = model.clone();
13826 let hs = self.hidden_size;
13827 let mut cat = vec![0f32; b * 2 * hs];
13829 for (j, (h, tok)) in pairs.iter().enumerate() {
13830 let e = self.embed_single(*tok);
13831 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13832 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13833 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13834 }
13835 let dims = GraphDims {
13836 hidden: hs,
13837 eps: self.rms_eps as f32,
13838 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13839 };
13840 spec_stamp("w.cat");
13841 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13842 return None;
13843 };
13844 spec_stamp("w.new");
13845 let l = AttnGpuLayer {
13846 attn_norm: &m.layer.input_norm,
13847 post_norm: &m.layer.post_norm,
13848 wq: pq,
13849 wk: pk,
13850 wv: pv,
13851 wo: po,
13852 ffn: MetalFfn::Dense {
13853 gate: g,
13854 up: u,
13855 down: dn,
13856 act: (d.act == Act::Gelu) as u8,
13857 },
13858 attn_out_norm: None,
13859 ffn_out_norm: None,
13860 layer_scale: None,
13861 };
13862 let (nh, nkv, hd, rd) = (
13863 self.num_heads,
13864 self.num_kv_heads,
13865 self.head_dim,
13866 self.rotary_dim,
13867 );
13868 let inv_freq = self.inv_freq.clone();
13869 let cpu_stored;
13870 {
13871 let cache = &m.kv;
13872 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13873 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13874 cpu_stored = cpu_k[0].len() / hd;
13875 if cpu_stored > first_pos {
13880 spec_stamp("w.decl");
13881 return None;
13882 }
13883 let p = AttnDeviceParams {
13884 kv_id: self.mtp_kv_id(),
13885 layer: Self::MTP_LAYER_BASE,
13886 nh,
13887 nkv,
13888 hd,
13889 rd,
13890 position: first_pos,
13891 scale: self.attn_scale,
13892 eps: self.rms_eps as f32,
13893 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13894 late_qk_norm: self.qk_norm_after_rope,
13895 output_gate: *output_gate,
13896 q_norm: q_norm.as_deref(),
13897 k_norm: k_norm.as_deref(),
13898 inv_freq: &inv_freq,
13899 rope_scale: self.rope_scale,
13900 cpu_k,
13901 cpu_v,
13902 cpu_stored,
13903 cpu_gen: cache.generation(),
13904 o1: None,
13905 window: None,
13906 softmax_sink: None,
13907 head_gate: None,
13908 v_norm: false,
13909 };
13910 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13911 return None;
13912 }
13913 }
13914 spec_stamp("w.enc");
13915 if !graph.submit() {
13916 return None;
13917 }
13918 spec_stamp("w.sub");
13919 Some(MetalWarmPending {
13920 graph,
13921 cpu_stored,
13922 b,
13923 })
13924 }
13925
13926 #[cfg(target_os = "macos")]
13929 fn mtp_warm_batch_metal(
13930 &mut self,
13931 m: &mut MtpModule,
13932 pairs: &[(&[f32], u32)],
13933 first_pos: usize,
13934 ) -> bool {
13935 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13936 Some(p) => self.mtp_warm_batch_finish(m, p),
13937 None => false,
13938 }
13939 }
13940
13941 #[cfg(target_os = "macos")]
13946 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13947 let MetalWarmPending {
13948 mut graph,
13949 cpu_stored,
13950 b,
13951 } = pending;
13952 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13953 if !graph.sync() {
13954 return false;
13955 }
13956 spec_stamp("w.gpu");
13957 let mut kbuf = vec![0f32; b * nkv * hd];
13958 let mut vbuf = vec![0f32; b * nkv * hd];
13959 if !crate::gpu_metal::kv_mirror_read_rows(
13960 self.mtp_kv_id(),
13961 Self::MTP_LAYER_BASE,
13962 nkv,
13963 hd,
13964 cpu_stored,
13965 b,
13966 &mut kbuf,
13967 &mut vbuf,
13968 ) {
13969 return false;
13970 }
13971 for r in 0..b {
13972 m.kv.append(
13973 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13974 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13975 &[],
13976 );
13977 }
13978 crate::gpu_metal::kv_mirror_set_stored(
13979 self.mtp_kv_id(),
13980 Self::MTP_LAYER_BASE,
13981 cpu_stored + b,
13982 );
13983 spec_stamp("w.kv");
13984 true
13985 }
13986
13987 pub(crate) fn note_draft_id(&mut self, id: u32) {
13994 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13995 if (id as usize) >= cut {
13996 self.draft_full_streak = 16;
13997 } else {
13998 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13999 }
14000 }
14001
14002 fn draft_head_rows(&self, head_rows: usize) -> usize {
14005 if self.draft_full_streak > 0 {
14006 head_rows
14007 } else {
14008 Self::draft_vocab_rows(head_rows)
14009 }
14010 }
14011
14012 fn draft_vocab_rows(head_rows: usize) -> usize {
14015 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14016 let n = *N.get_or_init(|| {
14017 std::env::var("CMF_DRAFT_VOCAB")
14018 .ok()
14019 .and_then(|v| v.parse().ok())
14020 .unwrap_or(65536)
14021 });
14022 if n == 0 { head_rows } else { n.min(head_rows) }
14023 }
14024
14025 #[cfg(target_os = "macos")]
14030 fn mtp_step_metal(
14031 &mut self,
14032 m: &mut MtpModule,
14033 hidden: &[f32],
14034 next_token: u32,
14035 position: usize,
14036 want_logits: bool,
14037 ) -> Option<(Vec<f32>, Vec<f32>)> {
14038 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14039 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14040 || !crate::gpu::q1_force()
14041 || !crate::gpu::enabled_here()
14042 || self.attn_softcap > 0.0
14043 || self.attention_heads_per_layer.is_some()
14044 || m.kv.mode != crate::kv_cache::KvMode::F32
14045 || m.kv.o1.is_some()
14046 {
14047 return None;
14048 }
14049 let AttnKind::Full {
14050 wq,
14051 wk,
14052 wv,
14053 wo,
14054 q_norm,
14055 k_norm,
14056 output_gate,
14057 softplus_gate: None,
14058 bias: None,
14059 } = &m.layer.attn
14060 else {
14061 return None;
14062 };
14063 let FfnKind::Dense(d) = &m.layer.ffn else {
14064 return None;
14065 };
14066 if d.act != Act::Silu || !d.segs.is_empty() {
14067 return None;
14068 }
14069 let (pq, pk, pv, po) = (
14070 wq.q1_parts()?,
14071 wk.q1_parts()?,
14072 wv.q1_parts()?,
14073 wo.q1_parts()?,
14074 );
14075 let (g, u, dn) = (
14076 d.gate_proj.q1_parts()?,
14077 d.up_proj.q1_parts()?,
14078 d.down_proj.q1_parts()?,
14079 );
14080 let QTensor::Mapped { model, .. } = wq else {
14081 return None;
14082 };
14083 let model = model.clone();
14084 let lm = if want_logits {
14085 Some(self.weights.lm_head.q1_parts()?)
14086 } else {
14087 None
14088 };
14089 let dims = GraphDims {
14090 hidden: self.hidden_size,
14091 eps: self.rms_eps as f32,
14092 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14093 };
14094 let hs = self.hidden_size;
14097 let mut x = vec![0f32; hs];
14098 let mut graph = TokenGraph::new(&model, dims, &x)?;
14099 let mut folded = false;
14100 if let Some(eh) = m.eh_proj.q1_parts() {
14101 let e = self.embed_single(next_token);
14102 let mut cat = vec![0.0f32; 2 * hs];
14103 let (cat_e, cat_h) = cat.split_at_mut(hs);
14104 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14105 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14106 folded = graph.encode_input_proj(eh, &cat);
14107 }
14108 if !folded {
14109 x = self.mtp_block_input(m, hidden, next_token);
14110 graph = TokenGraph::new(&model, dims, &x)?;
14111 }
14112 spec_stamp("d.in");
14113 let l = AttnGpuLayer {
14114 attn_norm: &m.layer.input_norm,
14115 post_norm: &m.layer.post_norm,
14116 wq: pq,
14117 wk: pk,
14118 wv: pv,
14119 wo: po,
14120 ffn: MetalFfn::Dense {
14121 gate: g,
14122 up: u,
14123 down: dn,
14124 act: (d.act == Act::Gelu) as u8,
14125 },
14126 attn_out_norm: None,
14127 ffn_out_norm: None,
14128 layer_scale: None,
14129 };
14130 let (nh, nkv, hd, rd) = (
14131 self.num_heads,
14132 self.num_kv_heads,
14133 self.head_dim,
14134 self.rotary_dim,
14135 );
14136 let inv_freq = self.inv_freq.clone();
14137 {
14138 let cache = &m.kv;
14139 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14140 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14141 let cpu_stored = cpu_k[0].len() / hd;
14142 let p = AttnDeviceParams {
14143 kv_id: self.mtp_kv_id(),
14144 layer: Self::MTP_LAYER_BASE,
14145 nh,
14146 nkv,
14147 hd,
14148 rd,
14149 position,
14150 scale: self.attn_scale,
14151 eps: self.rms_eps as f32,
14152 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14153 late_qk_norm: self.qk_norm_after_rope,
14154 output_gate: *output_gate,
14155 q_norm: q_norm.as_deref(),
14156 k_norm: k_norm.as_deref(),
14157 inv_freq: &inv_freq,
14158 rope_scale: self.rope_scale,
14159 cpu_k,
14160 cpu_v,
14161 cpu_stored,
14162 cpu_gen: cache.generation(),
14163 o1: None,
14164 window: None,
14165 softmax_sink: None,
14166 head_gate: None,
14167 v_norm: false,
14168 };
14169 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14170 return None;
14171 }
14172 }
14173 let draft_rows = if let Some(lm) = lm {
14179 self.draft_head_rows(lm.1)
14180 } else {
14181 0
14182 };
14183 if let Some(lm) = lm {
14184 if !graph.lm_head_ok(lm) {
14185 return None;
14186 }
14187 if draft_rows < lm.1 {
14188 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14189 return None;
14190 }
14191 } else {
14192 graph.encode_lm_head(&m.final_norm, lm);
14193 }
14194 }
14195 spec_stamp("d.enc");
14196 if graph.sync_checked().is_err() {
14197 return None;
14198 }
14199 spec_stamp("d.gpu");
14200 let mut logits = Vec::new();
14201 if let Some(lm) = lm {
14202 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14203 logits = attention::take_buf(n_read);
14204 graph.read_logits(&mut logits);
14205 logits.resize(self.vocab_size, f32::NEG_INFINITY);
14207 }
14208 graph.finish(&mut x);
14209 let mut krow = attention::take_buf(nkv * hd);
14210 let mut vrow = attention::take_buf(nkv * hd);
14211 if crate::gpu_metal::kv_mirror_read_last(
14212 self.mtp_kv_id(),
14213 Self::MTP_LAYER_BASE,
14214 nkv,
14215 hd,
14216 &mut krow,
14217 &mut vrow,
14218 ) {
14219 m.kv.append(&krow, &vrow, &[]);
14220 }
14221 attention::recycle_buf(&mut krow);
14222 attention::recycle_buf(&mut vrow);
14223 spec_stamp("d.rd");
14224 Some((logits, x))
14225 }
14226
14227 fn mtp_chain_on() -> bool {
14240 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14241 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14242 }
14243
14244 #[cfg(target_os = "macos")]
14256 fn mtp_draft_chain_metal(
14257 &mut self,
14258 m: &mut MtpModule,
14259 hidden: &[f32],
14260 t_next: u32,
14261 position: usize,
14262 k: usize,
14263 ) -> Result<Vec<u32>, bool> {
14264 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14265 if k == 0
14266 || k > 64
14267 || !Self::mtp_chain_on()
14268 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14269 || !crate::gpu::q1_force()
14270 || !crate::gpu::enabled_here()
14271 || self.attn_softcap > 0.0
14272 || self.attention_heads_per_layer.is_some()
14273 || m.kv.mode != crate::kv_cache::KvMode::F32
14274 || m.kv.o1.is_some()
14275 || self.dsv4.is_some()
14277 || self.dsv41.is_some()
14278 || self.qwen4_exp.is_some()
14279 || self.g3n.is_some()
14280 {
14281 return Err(false);
14282 }
14283 let AttnKind::Full {
14284 wq,
14285 wk,
14286 wv,
14287 wo,
14288 q_norm,
14289 k_norm,
14290 output_gate,
14291 softplus_gate: None,
14292 bias: None,
14293 } = &m.layer.attn
14294 else {
14295 return Err(false);
14296 };
14297 let FfnKind::Dense(d) = &m.layer.ffn else {
14298 return Err(false);
14299 };
14300 if d.act != Act::Silu || !d.segs.is_empty() {
14301 return Err(false);
14302 }
14303 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14304 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14305 else {
14306 return Err(false);
14307 };
14308 let (Some(g), Some(u), Some(dn)) = (
14309 d.gate_proj.q1_parts(),
14310 d.up_proj.q1_parts(),
14311 d.down_proj.q1_parts(),
14312 ) else {
14313 return Err(false);
14314 };
14315 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14316 return Err(false);
14317 };
14318 let QTensor::Mapped { model, .. } = wq else {
14319 return Err(false);
14320 };
14321 let model = model.clone();
14322 let QTensor::Mapped {
14325 model: em,
14326 idx: eidx,
14327 dtype: cortiq_core::TensorDtype::Q4TiledP,
14328 ..
14329 } = &self.weights.embed_tokens
14330 else {
14331 return Err(false);
14332 };
14333 if !std::sync::Arc::ptr_eq(em, &model)
14334 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14335 {
14336 return Err(false);
14337 }
14338 let embed = (
14339 *eidx,
14340 self.weights.embed_tokens.rows(),
14341 self.weights.embed_tokens.cols(),
14342 );
14343 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14344 return Err(false);
14345 }
14346 let dims = GraphDims {
14347 hidden: self.hidden_size,
14348 eps: self.rms_eps as f32,
14349 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14350 };
14351 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14352 return Err(false);
14353 };
14354 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14355 return Err(false);
14356 }
14357 let l = AttnGpuLayer {
14358 attn_norm: &m.layer.input_norm,
14359 post_norm: &m.layer.post_norm,
14360 wq: pq,
14361 wk: pk,
14362 wv: pv,
14363 wo: po,
14364 ffn: MetalFfn::Dense {
14365 gate: g,
14366 up: u,
14367 down: dn,
14368 act: (d.act == Act::Gelu) as u8,
14369 },
14370 attn_out_norm: None,
14371 ffn_out_norm: None,
14372 layer_scale: None,
14373 };
14374 let (nh, nkv, hd, rd) = (
14375 self.num_heads,
14376 self.num_kv_heads,
14377 self.head_dim,
14378 self.rotary_dim,
14379 );
14380 let inv_freq = self.inv_freq.clone();
14381 let draft_rows = self.draft_head_rows(lm.1);
14382 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
14383 if n_arg == 0 {
14384 return Err(false);
14385 }
14386 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
14393 let t_chain = std::time::Instant::now();
14394 graph.chain_ids_init(t_next, k);
14395 let cpu_stored;
14396 {
14397 let cache = &m.kv;
14398 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14399 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14400 cpu_stored = cpu_k[0].len() / hd;
14401 for j in 0..k {
14402 if !graph.encode_chain_input(
14403 embed,
14404 j as u32,
14405 &m.enorm,
14406 &m.hnorm,
14407 self.embed_multiplier,
14408 eh,
14409 ) {
14410 return Err(false);
14411 }
14412 let p = AttnDeviceParams {
14416 kv_id: self.mtp_kv_id(),
14417 layer: Self::MTP_LAYER_BASE,
14418 nh,
14419 nkv,
14420 hd,
14421 rd,
14422 position: position + j,
14423 scale: self.attn_scale,
14424 eps: self.rms_eps as f32,
14425 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14426 late_qk_norm: self.qk_norm_after_rope,
14427 output_gate: *output_gate,
14428 q_norm: q_norm.as_deref(),
14429 k_norm: k_norm.as_deref(),
14430 inv_freq: &inv_freq,
14431 rope_scale: self.rope_scale,
14432 cpu_k: cpu_k.clone(),
14433 cpu_v: cpu_v.clone(),
14434 cpu_stored: cpu_stored + j,
14435 cpu_gen: cache.generation(),
14436 o1: None,
14437 window: None,
14438 softmax_sink: None,
14439 head_gate: None,
14440 v_norm: false,
14441 };
14442 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14443 return Err(false);
14444 }
14445 if draft_rows < lm.1 {
14446 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14447 return Err(false);
14448 }
14449 } else {
14450 graph.encode_lm_head(&m.final_norm, lm);
14451 }
14452 if !graph.encode_argmax(n_arg, j as u32 + 1) {
14453 return Err(false);
14454 }
14455 if split {
14456 graph.commit();
14459 }
14460 }
14461 }
14462 let t_enc = t_chain.elapsed();
14463 if graph.sync_checked().is_err() {
14464 return Err(true);
14465 }
14466 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14467 eprintln!(
14468 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14469 t_enc.as_secs_f64() * 1e3,
14470 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14471 if split { ", split" } else { "" }
14472 );
14473 }
14474 let mut ids = vec![0u32; k];
14475 if !graph.chain_ids_read(&mut ids) {
14476 return Err(true);
14477 }
14478 let mut kbuf = vec![0f32; k * nkv * hd];
14479 let mut vbuf = vec![0f32; k * nkv * hd];
14480 if !crate::gpu_metal::kv_mirror_read_rows(
14481 self.mtp_kv_id(),
14482 Self::MTP_LAYER_BASE,
14483 nkv,
14484 hd,
14485 cpu_stored,
14486 k,
14487 &mut kbuf,
14488 &mut vbuf,
14489 ) {
14490 return Err(true);
14491 }
14492 for r in 0..k {
14493 m.kv.append(
14494 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14495 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14496 &[],
14497 );
14498 }
14499 Ok(ids)
14500 }
14501
14502 fn try_batch_graph_wgpu(
14503 &self,
14504 hiddens: &mut [f32],
14505 positions: &[usize],
14506 k: usize,
14507 spec: Option<crate::gpu::SpecTail<'_>>,
14508 ) -> crate::gpu::BatchGraphOutcome {
14509 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
14510 }
14511
14512 fn try_batch_graph_wgpu_prefix(
14517 &self,
14518 hiddens: &mut [f32],
14519 positions: &[usize],
14520 k: usize,
14521 spec: Option<crate::gpu::SpecTail<'_>>,
14522 layers_run: Option<&mut usize>,
14523 ) -> crate::gpu::BatchGraphOutcome {
14524 let graph_end = match self.mimo_moe.graph_prefix_end() {
14525 Some(end) if end < self.num_layers => {
14526 if layers_run.is_none() || spec.is_some() || end == 0 {
14527 return crate::gpu::BatchGraphOutcome::Declined;
14528 }
14529 end
14530 }
14531 _ => self.num_layers,
14532 };
14533 let _tb = std::time::Instant::now();
14534 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
14535 if self.attn_softcap > 0.0 {
14536 return crate::gpu::BatchGraphOutcome::Declined; }
14538 if let Some(reason) = self
14541 .wgpu_graph_attn_decline()
14542 .or_else(|| self.batch_graph_gemma_decline())
14543 {
14544 self.note_graph_decline("wgpu batch graph", reason);
14545 return crate::gpu::BatchGraphOutcome::Declined;
14546 }
14547 let nh = self.num_heads;
14548 let (nkv, hd, rd) = self.layer_geom(0);
14549 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14550 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14551 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
14552 {
14553 let name = &m.tensors[i].name;
14554 let prism = if crate::prism::is_inverse_embedding(m, name) {
14555 crate::gpu::GraphPrismOp::InverseEmbedding
14556 } else if crate::prism::is_forward_weight(m, name) {
14557 crate::gpu::GraphPrismOp::Forward
14558 } else {
14559 crate::gpu::GraphPrismOp::None
14560 };
14561 return Some(crate::gpu::GraphW {
14562 idx: i,
14563 kind,
14564 row_scale: rs,
14565 data: &[],
14566 prism,
14567 affine: crate::prism::is_affine_target(m, name),
14568 });
14569 }
14570 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14571 eprintln!(
14572 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
14573 t.rows(),
14574 t.cols()
14575 );
14576 }
14577 t.as_f32().map(|d| crate::gpu::GraphW {
14578 idx: 0,
14579 kind: 4,
14580 row_scale: &[],
14581 data: d,
14582 prism: crate::gpu::GraphPrismOp::None,
14583 affine: false,
14584 })
14585 }
14586 let built: Option<(
14587 Vec<crate::gpu::GraphLayer<'_>>,
14588 std::sync::Arc<cortiq_core::CmfModel>,
14589 )> = (|| {
14590 let mut layers = Vec::with_capacity(graph_end);
14591 let mut model = None;
14592 for li in 0..graph_end {
14593 let lw = &self.weights.layers[self.phys_layer(li)];
14594 let gffn = match &lw.ffn {
14601 FfnKind::Dense(d) if !d.segs.is_empty() => {
14602 if batch_debug {
14603 eprintln!("batch graph: dense segmented FFN at layer {li}");
14604 }
14605 return None;
14606 }
14607 FfnKind::Dense(d) => {
14608 let Some(act) = d.act.wgpu_graph_act() else {
14609 if batch_debug {
14610 eprintln!(
14611 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
14612 d.act
14613 );
14614 }
14615 return None;
14616 };
14617 crate::gpu::GraphFfn::Dense {
14618 gate: gw(&d.gate_proj)?,
14619 up: gw(&d.up_proj)?,
14620 down: gw(&d.down_proj)?,
14621 act,
14622 }
14623 }
14624 FfnKind::Moe(m) => {
14625 if m.route_tau.is_some() || m.mask.is_some() {
14632 return None;
14633 }
14634 let shared = m.shared.as_ref();
14638 let has_shared = shared.is_some();
14639 let shared_gated = matches!(shared, Some((_, Some(_))));
14640 let sgate = match shared {
14641 Some((_, Some(sg))) => gw(sg)?,
14642 _ => gw(&m.router)?,
14646 };
14647 let router = gw(&m.router)?;
14648 if router.prism != crate::gpu::GraphPrismOp::None
14654 || router.affine
14655 || sgate.prism != crate::gpu::GraphPrismOp::None
14656 || sgate.affine
14657 {
14658 return None;
14659 }
14660 let inter = m.experts.first()?.gate_proj.rows();
14661 let mut experts = Vec::with_capacity(m.experts.len() + 1);
14662 let mut q4tp: Option<bool> = None;
14663 let mut gu_q2: Option<bool> = None;
14664 let mut q82: Option<bool> = None;
14665 let mut moe_act = None;
14669 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
14670 let Some(e_act) = e.act.wgpu_graph_act() else {
14671 return None;
14672 };
14673 match moe_act {
14674 Some(existing) if existing != e_act => return None,
14675 None => moe_act = Some(e_act),
14676 _ => {}
14677 }
14678 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14679 return None;
14680 }
14681 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
14685 {
14686 Some((mm, gi)) => (
14687 mm,
14688 gi,
14689 e.up_proj.mapped_q4t()?.1,
14690 e.down_proj.mapped_q4t()?.1,
14691 false,
14692 false,
14693 false,
14694 ),
14695 None => match e.gate_proj.mapped_q2tp() {
14696 Some((mm, gi)) => (
14697 mm,
14698 gi,
14699 e.up_proj.mapped_q2tp()?.1,
14700 e.down_proj.mapped_q4tp()?.1,
14701 true,
14702 true,
14703 false,
14704 ),
14705 None => match e.gate_proj.mapped_q4tp() {
14706 Some((mm, gi)) => (
14707 mm,
14708 gi,
14709 e.up_proj.mapped_q4tp()?.1,
14710 e.down_proj.mapped_q4tp()?.1,
14711 true,
14712 false,
14713 false,
14714 ),
14715 None => {
14716 if !crate::gpu::moe_q82_graph_on() {
14717 return None;
14718 }
14719 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14720 (
14721 mm,
14722 gi,
14723 e.up_proj.mapped_q8_2f()?.1,
14724 e.down_proj.mapped_q8_2f()?.1,
14725 false,
14726 false,
14727 true,
14728 )
14729 }
14730 },
14731 },
14732 };
14733 if *q4tp.get_or_insert(is_p) != is_p
14734 || *gu_q2.get_or_insert(is_q2) != is_q2
14735 || *q82.get_or_insert(is_8) != is_8
14736 {
14737 return None;
14738 }
14739 if [gi, ui, di].into_iter().any(|idx| {
14740 mm.tensors.get(idx).is_some_and(|t| {
14741 crate::prism::is_forward_weight(mm, &t.name)
14742 || crate::prism::is_affine_target(mm, &t.name)
14743 })
14744 }) {
14745 return None;
14746 }
14747 model.get_or_insert_with(|| mm.clone());
14748 experts.push((gi, ui, di));
14749 }
14750 crate::gpu::GraphFfn::Moe {
14751 router,
14752 shared_gate: sgate,
14753 experts,
14754 n_exp: m.experts.len(),
14755 top_k: m.top_k,
14756 inter,
14757 act: moe_act?,
14758 norm_topk: m.norm_topk_prob,
14759 q4tp: q4tp?,
14760 gu_q2: gu_q2.unwrap_or(false),
14761 q82: q82.unwrap_or(false),
14762 sigmoid: m.router_sigmoid,
14763 sigmoid_denom_eps: m.sigmoid_denom_eps,
14764 bias: m.expert_bias.as_deref(),
14765 has_shared,
14766 detached_shared: None,
14767 shared_gated,
14768 route_scale: m.routed_scaling,
14769 }
14770 }
14771 _ => return None,
14772 };
14773 let attn = match &lw.attn {
14774 AttnKind::Full {
14775 wq,
14776 wk,
14777 wv,
14778 wo,
14779 q_norm,
14780 k_norm,
14781 output_gate,
14782 softplus_gate,
14783 bias,
14784 } => {
14785 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14786 if batch_debug {
14787 eprintln!(
14788 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14789 softplus_gate.is_some(),
14790 self.attention_heads_per_layer.is_some()
14791 );
14792 }
14793 return None;
14794 }
14795 let (m, _, _, _) =
14796 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
14797 model = Some(m.clone());
14798 crate::gpu::GraphAttn::Full {
14799 wq: gw(wq)?,
14800 wk: gw(wk)?,
14801 wv: gw(wv)?,
14802 wo: gw(wo)?,
14803 q_norm: q_norm.as_deref(),
14804 k_norm: k_norm.as_deref(),
14805 late_qk_norm: self.qk_norm_after_rope,
14806 bias: bias
14807 .as_ref()
14808 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14809 output_gate: *output_gate,
14810 cpu_k: self.kv_cache.layers[li].k_heads(),
14811 cpu_v: self.kv_cache.layers[li].v_heads(),
14812 cpu_base: self.kv_cache.layers[li].base(),
14813 geom: self.graph_attn_geom(li),
14814 head_gate: None,
14817 }
14818 }
14819 AttnKind::LinearGdn(w) => {
14820 let Some(cfg) = self.gdn_cfg else {
14821 if batch_debug {
14822 eprintln!("batch graph: no GDN config at layer {li}");
14823 }
14824 return None;
14825 };
14826 let (m, _, _, _) = w
14827 .in_proj_qkv
14828 .graph_weight()
14829 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14830 model = Some(m.clone());
14831 crate::gpu::GraphAttn::Gdn {
14832 qkv: gw(&w.in_proj_qkv)?,
14833 z: gw(&w.in_proj_z)?,
14834 a: gw(&w.in_proj_a)?,
14835 b: gw(&w.in_proj_b)?,
14836 out: gw(&w.out_proj)?,
14837 conv1d: &w.conv1d,
14838 a_log: &w.a_log,
14839 dt_bias: &w.dt_bias,
14840 norm: &w.norm,
14841 nv: cfg.num_v_heads,
14842 nk: cfg.num_k_heads,
14843 dk: cfg.key_head_dim,
14844 dv: cfg.value_head_dim,
14845 kk: cfg.conv_kernel,
14846 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14847 }
14848 }
14849 _ => return None,
14850 };
14851 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14854 layers.push(crate::gpu::GraphLayer {
14855 input_norm: &lw.input_norm,
14856 attn,
14857 post_norm: &lw.post_norm,
14858 attn_out_norm: if g4 {
14859 lw.attn_out_norm.as_deref()
14860 } else {
14861 None
14862 },
14863 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14864 layer_scale: if g4 { lw.layer_scale } else { None },
14865 ffn: gffn,
14866 });
14867 }
14868 Some((layers, model?))
14869 })();
14870 let Some((layers, model)) = built else {
14871 {
14872 use std::sync::atomic::{AtomicBool, Ordering};
14873 static SAID: AtomicBool = AtomicBool::new(false);
14874 if !SAID.swap(true, Ordering::Relaxed) {
14875 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14876 }
14877 }
14878 return crate::gpu::BatchGraphOutcome::Declined;
14879 };
14880 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14881 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14882 }
14883 crate::gpu::forward_batch_graph(
14884 &model,
14885 self.graph_kv_id,
14886 &layers,
14887 &self.inv_freq,
14888 hiddens,
14889 nh,
14890 nkv,
14891 hd,
14892 rd,
14893 self.hidden_size,
14894 self.intermediate_size,
14895 positions,
14896 self.kv_cache.max_seq_len,
14897 gemma,
14898 self.rms_eps as f32,
14899 self.attn_scale,
14900 k,
14901 &(0..graph_end)
14902 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14903 .collect::<Vec<_>>(),
14904 self.o1_epoch,
14905 spec,
14906 layers_run,
14907 )
14908 }
14909
14910 fn draft_probe() -> bool {
14914 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14915 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14916 }
14917
14918 #[cfg(feature = "gpu")]
14930 fn dsv4_spec_on() -> bool {
14931 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14932 *ON.get_or_init(|| {
14933 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14937 return v != "0";
14938 }
14939 std::env::var("CMF_DSV4_SPEC")
14946 .map(|v| v != "0")
14947 .unwrap_or_else(|_| {
14948 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14949 })
14950 })
14951 }
14952
14953 #[cfg(feature = "gpu")]
14960 fn dsv4_spec_step(
14961 &mut self,
14962 tip_token: u32,
14963 t_next: u32,
14964 next_pos: usize,
14965 max_extra: usize,
14966 drafted: &mut usize,
14967 accepted_ctr: &mut usize,
14968 ) -> Option<(Vec<u32>, usize)> {
14969 let t_all = std::time::Instant::now();
14970 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14971 thread_local! {
14972 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14973 const { std::cell::Cell::new(None) };
14974 }
14975 LAST.with(|l| {
14976 if let Some(prev) = l.get() {
14977 eprintln!(
14978 "между раундами {:.1} мс",
14979 prev.elapsed().as_secs_f64() * 1e3
14980 );
14981 }
14982 l.set(Some(std::time::Instant::now()));
14983 });
14984 }
14985 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14986 eprintln!("spec_step: вход pos={next_pos}");
14987 }
14988 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14989 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14990 if self.dspark.is_none() {
14992 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14993 if t.is_empty() {
14994 return None;
14995 }
14996 crate::dsv4::dspark_arm(&t, cfg.dim);
14997 self.dspark = Some(crate::dsv4::DsparkState::new(
14998 self.dsv4_mtp.len(),
14999 &cfg,
15000 t.len(),
15001 ));
15002 }
15003 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15004 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15005 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15006 eprintln!("spec_step: пак не построился (targets {targets:?})");
15007 }
15008 let pack = pack?;
15009 let block = crate::dsv4::dspark_block();
15010 let b_box = self.dsv4.as_mut()?;
15011 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15012 let ds = self.dspark.as_mut()?;
15013 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15016 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15017 if dbg {
15018 eprintln!("spec_step: нет захвата");
15019 }
15020 return None;
15021 }
15022 ds.have_hidden = true;
15023 let tip_pos = next_pos.checked_sub(1)?;
15024 let draft_started = std::time::Instant::now();
15025 let mut conf = Vec::new();
15026 let props = crate::dsv4::dspark_draft_gpu(
15027 g,
15028 &self.dsv4_mtp,
15029 &cfg,
15030 ds,
15031 pack,
15032 st.kv_id,
15033 tip_token,
15034 tip_pos,
15035 self.pool.as_deref(),
15036 &mut conf,
15037 );
15038 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15039 *drafted += block;
15040 if props.is_empty() || props[0] != t_next {
15041 if dbg {
15042 eprintln!(
15043 "spec_step: черновик {} (props0={:?} t_next={t_next})",
15044 if props.is_empty() {
15045 "пуст"
15046 } else {
15047 "мимо"
15048 },
15049 props.first()
15050 );
15051 }
15052 return None;
15053 }
15054 let mut k_verify = crate::dsv4::dspark_verify_k()
15061 .min(props.len())
15062 .min(max_extra.saturating_add(1));
15063 let conf_min = {
15069 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15070 *M.get_or_init(|| {
15071 std::env::var("CMF_DSPARK_CONF_MIN")
15072 .ok()
15073 .and_then(|v| v.parse().ok())
15074 .unwrap_or(0.0)
15075 })
15076 };
15077 if conf_min > 0.0 && conf.len() >= props.len() {
15078 let mut keep = 1usize;
15079 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15080 keep += 1;
15081 }
15082 k_verify = k_verify.min(keep.max(2));
15083 }
15084 if k_verify < 2 {
15085 return None;
15086 }
15087 let mut fed = Vec::with_capacity(k_verify);
15088 fed.push(t_next);
15089 fed.extend_from_slice(&props[1..k_verify]);
15090 let mut argmax = Vec::new();
15091 let mut logits_all = Vec::new();
15092 let mut walked = Vec::new();
15093 let txn = crate::dsv4::dsv4_verify_chunk(
15094 g,
15095 layers,
15096 &cfg,
15097 st,
15098 &fed,
15099 next_pos,
15100 &self.inv_freq,
15101 self.pool.as_deref(),
15102 &targets,
15103 &mut argmax,
15104 &mut logits_all,
15105 &mut walked,
15106 );
15107 if txn.is_none() && dbg {
15108 eprintln!("spec_step: verify отказал");
15109 }
15110 let txn = txn?;
15111 let spec_gpu_end = txn.gpu_end;
15112 let b = fed.len();
15113 let mut accepted = 1usize;
15114 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15115 accepted += 1;
15116 }
15117 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15122 accepted = 1;
15123 }
15124 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15125 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15126 }
15127 let t_fin = std::time::Instant::now();
15128 if !crate::dsv4::dsv4_spec_finish(
15129 g,
15130 layers,
15131 &cfg,
15132 st,
15133 txn,
15134 accepted,
15135 &fed,
15136 &self.inv_freq,
15137 self.pool.as_deref(),
15138 ) {
15139 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
15140 return None;
15141 }
15142 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15143 eprintln!(
15144 "finish(k={accepted}): {:.1} мс",
15145 t_fin.elapsed().as_secs_f64() * 1e3
15146 );
15147 }
15148 *accepted_ctr += accepted - 1;
15149 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15154 let dev_caps: Vec<usize> = targets
15159 .iter()
15160 .copied()
15161 .filter(|&t| t < spec_gpu_end)
15162 .collect();
15163 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15164 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15165 return None;
15166 }
15167 for t in 0..accepted {
15168 let tip = t + 1 == accepted;
15169 for (slot, &tl) in targets.iter().enumerate() {
15170 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15171 let lo = (di * b + t) * hc * dim;
15172 crate::dsv4::dspark_capture(
15173 &caps_all[lo..lo + hc * dim],
15174 &cfg,
15175 slot,
15176 &mut ds.main_hidden,
15177 );
15178 } else if tip
15179 && crate::dsv4::dspark_peek_slot(slot, dim, {
15180 let lo = slot * dim;
15181 &mut ds.main_hidden[lo..lo + dim]
15182 })
15183 {
15184 } else {
15189 crate::dsv4::dspark_capture(
15193 &walked[t * hc * dim..(t + 1) * hc * dim],
15194 &cfg,
15195 slot,
15196 &mut ds.main_hidden,
15197 );
15198 }
15199 }
15200 crate::dsv4::dspark_ring_append(
15201 g,
15202 &self.dsv4_mtp,
15203 &cfg,
15204 ds,
15205 next_pos + t,
15206 self.pool.as_deref(),
15207 );
15208 }
15209 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15210 self.graph_logits = Some(row);
15211 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15216 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15217 crate::dsv4::pick_tally_arm();
15218 }
15219 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15220 eprintln!(
15221 "spec_step total {:.1} мс (k={accepted})",
15222 t_all.elapsed().as_secs_f64() * 1e3
15223 );
15224 }
15225 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15226 }
15227
15228 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15229 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15230 return;
15231 }
15232 let trunk_now = crate::dsv4::pick_tally_take();
15234 crate::dsv4::trunk_freq_note(&trunk_now);
15235 if !trunk_now.is_empty() {
15236 self.dspark_trunk_picks.push(trunk_now);
15237 let keep = crate::dsv4::dspark_block();
15238 if self.dspark_trunk_picks.len() > keep {
15239 self.dspark_trunk_picks.remove(0);
15240 }
15241 }
15242 for p in std::mem::take(&mut self.dspark_pending) {
15245 let Some(i) = position.checked_sub(p.0 + 1) else {
15246 continue;
15247 };
15248 let mut p = p;
15249 if i < p.1.len() {
15250 if p.2 && p.1[i] == token_id {
15251 p.3 = i + 1;
15252 } else {
15253 p.2 = false;
15254 }
15255 if i + 1 < p.1.len() {
15256 self.dspark_pending.push(p);
15257 continue;
15258 }
15259 }
15260 self.dspark_hist.push(p.3);
15261 self.dspark_real.push(token_id);
15262 }
15263 let Some(b) = &mut self.dsv4 else { return };
15264 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15265 let n_layers = layers.len();
15266 if self.dspark.is_none() {
15267 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15268 if t.is_empty() {
15269 return;
15270 }
15271 eprintln!(
15272 "DSpark: захват со слоёв {t:?}, блок {}",
15273 crate::dsv4::dspark_block()
15274 );
15275 crate::dsv4::dspark_arm(&t, cfg.dim);
15276 self.dspark = Some(crate::dsv4::DsparkState::new(
15277 self.dsv4_mtp.len(),
15278 &cfg,
15279 t.len(),
15280 ));
15281 }
15282 let ds = self.dspark.as_mut().unwrap();
15283 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15284 return; }
15286 let mut conf = Vec::new();
15287 crate::dsv4::pick_tally_arm();
15288 let draft_started = std::time::Instant::now();
15293 #[cfg(feature = "gpu")]
15294 let gpu_draft = crate::dsv4::dspark_gpu_on();
15295 #[cfg(not(feature = "gpu"))]
15296 let gpu_draft = false;
15297 let props = if gpu_draft {
15298 #[cfg(feature = "gpu")]
15299 {
15300 let kv_id = b.3.kv_id;
15301 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15302 Some(pk) => crate::dsv4::dspark_draft_gpu(
15303 g,
15304 &self.dsv4_mtp,
15305 &cfg,
15306 ds,
15307 pk,
15308 kv_id,
15309 token_id,
15310 position,
15311 self.pool.as_deref(),
15312 &mut conf,
15313 ),
15314 None => Vec::new(),
15315 }
15316 }
15317 #[cfg(not(feature = "gpu"))]
15318 Vec::new()
15319 } else {
15320 crate::gpu::cpu_scope(|| {
15321 crate::dsv4::dspark_draft(
15322 g,
15323 &self.dsv4_mtp,
15324 &cfg,
15325 ds,
15326 token_id,
15327 position,
15328 self.pool.as_deref(),
15329 &mut conf,
15330 )
15331 })
15332 };
15333 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15334 let draft_picks = crate::dsv4::pick_tally_take();
15335 crate::dsv4::dspark_freq_note(&draft_picks);
15336 crate::dsv4::pick_tally_arm();
15339 if !props.is_empty() {
15340 let (tu, tt) = {
15344 let flat: Vec<(usize, Vec<usize>)> = self
15345 .dspark_trunk_picks
15346 .iter()
15347 .flat_map(|v| v.iter().cloned())
15348 .collect();
15349 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15351 std::collections::HashMap::new();
15352 for (li, picks) in flat {
15353 per.entry(li).or_default().extend(picks);
15354 }
15355 let n = per.len().max(1);
15356 let mut u = 0usize;
15357 let mut t = 0usize;
15358 for (_, v) in per {
15359 t += v.len();
15360 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15361 }
15362 (u / n, t / n)
15363 };
15364 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15365 self.dspark_exp.push((tu, tt, du, dt));
15366 self.dspark_pending.push((position, props, true, 0));
15367 }
15368 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15369 let n = self.dspark_hist.len() as f32;
15370 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15371 let block = crate::dsv4::dspark_block();
15372 let mut at = vec![0usize; block + 1];
15373 for &k in &self.dspark_hist {
15374 at[k] += 1;
15375 }
15376 let mut surv = Vec::with_capacity(block);
15378 for i in 1..=block {
15379 let k = at[i..].iter().sum::<usize>() as f32 / n;
15380 surv.push(format!("{k:.2}"));
15381 }
15382 let distinct = self
15383 .dspark_real
15384 .iter()
15385 .collect::<std::collections::HashSet<_>>()
15386 .len();
15387 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
15388 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
15389 });
15390 let m = self.dspark_exp.len().max(1);
15391 eprintln!(
15392 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
15393 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
15394 self.dspark_hist.len(),
15395 mean + 1.0,
15396 surv.join(" ")
15397 );
15398 eprintln!(
15399 "DSpark: разных токенов {distinct} из {} (вырожденность), \
15400 эксперты ствол {}/{} на слой за {block} токенов, \
15401 черновик {}/{} за блок, draft {:.2} мс/блок",
15402 self.dspark_real.len(),
15403 tu / m,
15404 tt / m,
15405 du / m,
15406 dt / m,
15407 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
15408 );
15409 }
15410 }
15411
15412 fn forward_layers_upto(
15413 &mut self,
15414 hidden: &[f32],
15415 position: usize,
15416 task_mask: Option<&TaskMask>,
15417 upto: Option<usize>,
15418 ) -> Vec<f32> {
15419 if let Some(plan) = self.gpu_plan.clone() {
15425 if upto.is_none() && plan.len() > 1 {
15426 let mut h = hidden.to_vec();
15427 for &(dev, from, upto_incl) in plan.iter() {
15428 h = crate::gpu::with_device(dev, || {
15429 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
15430 });
15431 }
15432 return h;
15433 }
15434 }
15435 self.forward_layers_span(hidden, position, task_mask, 0, upto)
15436 }
15437
15438 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
15443 self.set_gpu_plan_at(devices, None)
15444 }
15445
15446 pub fn set_gpu_plan_at(
15450 &mut self,
15451 devices: Option<&[usize]>,
15452 at: Option<usize>,
15453 ) -> Result<(), String> {
15454 let Some(devs) = devices.filter(|d| d.len() > 1) else {
15455 self.gpu_plan = None;
15456 return Ok(());
15457 };
15458 self.split_supported()?;
15459 let n = self.num_layers;
15460 if devs.len() > n {
15461 return Err(format!("{} devices for {n} layers", devs.len()));
15462 }
15463 if let Some(k) = at {
15464 if k == 0 || k >= n {
15465 return Err(format!("split at {k}: the model has {n} layers"));
15466 }
15467 if devs.len() == 2 {
15468 self.gpu_plan = Some(std::sync::Arc::new(vec![
15469 (devs[0], 0, k - 1),
15470 (devs[1], k, n - 1),
15471 ]));
15472 return Ok(());
15473 }
15474 return Err(format!(
15475 "an explicit split point takes exactly 2 devices, got {}",
15476 devs.len()
15477 ));
15478 }
15479 let per = n.div_ceil(devs.len());
15480 let mut plan = Vec::with_capacity(devs.len());
15481 let mut from = 0usize;
15482 for &d in devs {
15483 if from >= n {
15484 break;
15485 }
15486 let upto = (from + per - 1).min(n - 1);
15487 plan.push((d, from, upto));
15488 from = upto + 1;
15489 }
15490 self.gpu_plan = Some(std::sync::Arc::new(plan));
15491 Ok(())
15492 }
15493
15494 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
15496 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
15497 }
15498
15499 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
15505 if let Some(x) = t.as_f32() {
15506 return x.to_vec();
15507 }
15508 let mut out = vec![0.0; t.rows() * t.cols()];
15509 for r in 0..t.rows() {
15510 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
15511 }
15512 out
15513 }
15514
15515 fn embryo_resident_eligible(&self) -> bool {
15516 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
15519 || self.num_layers != self.physical_layers
15520 || self.loop_final_norm
15521 || self.weights.layers.len() != self.num_layers
15522 || self.head_clusters.is_none()
15523 || self.final_softcap.is_some()
15524 || self.logit_multiplier.is_some()
15525 || self.attn_softcap != 0.0
15526 || self.mtp.is_some()
15527 || self.g3n.is_some()
15528 || self.dsv4.is_some()
15529 || self.dsv41.is_some()
15530 || self.qwen4_exp.is_some()
15531 || self.dyn_router.is_some()
15538 || self.dyn_phi_layer.is_some()
15539 || self.dyn_blend_loaded
15540 || self.o1_cfg.is_some()
15541 || self.swa.is_some()
15542 || self.sliding_layers.is_some()
15543 || self.global_attn.is_some()
15544 || self.attention_heads_per_layer.is_some()
15545 || self.attn_v_norm
15546 || self
15547 .kv_cache
15548 .layers
15549 .iter()
15550 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
15551 || self.rope_scale != 1.0
15552 || self.rope_scale_local != 1.0
15553 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
15554 || self.hidden_size == 0
15555 || self.hidden_size > 1024
15556 || self.intermediate_size > 1024
15557 || self.num_heads == 0
15558 || self.num_kv_heads == 0
15559 || self.num_heads % self.num_kv_heads != 0
15560 || self.num_heads.saturating_mul(self.head_dim) > 1024
15561 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
15562 || self.vocab_size == 0
15563 || self.kv_cache.max_seq_len == 0
15564 || self.rotary_dim == 0
15565 || self.rotary_dim > self.head_dim
15566 || self.rotary_dim % 2 != 0
15567 || self.inv_freq.len() < self.rotary_dim / 2
15568 {
15569 return false;
15570 }
15571 if self.weights.lm_head.as_f32().is_none()
15582 || self.weights.embed_tokens.as_f32().is_none()
15583 || self.weights.lm_head.rows() < self.vocab_size
15584 || self.weights.lm_head.cols() != self.hidden_size
15585 || self.weights.embed_tokens.rows() < self.vocab_size
15586 || self.weights.embed_tokens.cols() != self.hidden_size
15587 || self.weights.final_norm.len() != self.hidden_size
15588 {
15589 return false;
15590 }
15591 if let Some(cfg) = self.vmf_cfg {
15592 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
15593 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
15594 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
15595 || cfg.state_len() == 0
15596 {
15597 return false;
15598 }
15599 }
15600 if let Some(g) = self.gdn_cfg {
15601 if g.num_v_heads == 0
15606 || g.num_k_heads == 0
15607 || g.num_v_heads % g.num_k_heads != 0
15608 || g.key_head_dim == 0
15609 || g.key_head_dim > 128
15610 || g.value_head_dim == 0
15611 || g.value_head_dim > 256
15612 || g.value_head_dim % 4 != 0
15613 || g.conv_kernel == 0
15614 || g.num_v_heads > 512
15615 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
15616 || g.conv_dim() > 2048
15617 || g.conv_dim() % 4 != 0
15618 || g.hidden_size != self.hidden_size
15619 || g.output_gate_sigmoid
15620 || g.rms_eps != self.rms_eps
15621 || g.state_len() == 0
15622 {
15623 return false;
15624 }
15625 }
15626 let mut full_seen = false;
15627 for lw in &self.weights.layers {
15628 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
15629 return false;
15630 }
15631 match &lw.attn {
15632 AttnKind::LinearGdn(w) => {
15633 let Some(g) = self.gdn_cfg else {
15634 return false;
15635 };
15636 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
15637 if w.in_proj_qkv.rows() != g.conv_dim()
15638 || w.in_proj_qkv.cols() != self.hidden_size
15639 || w.in_proj_qkv.as_f32().is_none()
15640 || w.in_proj_z.rows() != nv * dv
15641 || w.in_proj_z.cols() != self.hidden_size
15642 || w.in_proj_z.as_f32().is_none()
15643 || w.in_proj_a.rows() != nv
15644 || w.in_proj_a.cols() != self.hidden_size
15645 || w.in_proj_a.as_f32().is_none()
15646 || w.in_proj_b.rows() != nv
15647 || w.in_proj_b.cols() != self.hidden_size
15648 || w.in_proj_b.as_f32().is_none()
15649 || w.conv1d.len() != g.conv_dim() * kk
15650 || w.a_log.len() != nv
15651 || w.dt_bias.len() != nv
15652 || w.norm.len() != dv
15653 || w.out_proj.rows() != self.hidden_size
15654 || w.out_proj.cols() != nv * dv
15655 || w.out_proj.as_f32().is_none()
15656 {
15657 return false;
15658 }
15659 }
15660 AttnKind::Linear(w) => {
15661 let Some(cfg) = self.vmf_cfg else {
15662 return false;
15663 };
15664 if w.thq.rows() != cfg.num_heads * cfg.nphase
15665 || w.thq.cols() != self.hidden_size
15666 || w.thq.as_f32().is_none()
15667 || w.thk.rows() != cfg.num_heads * cfg.nphase
15668 || w.thk.cols() != self.hidden_size
15669 || w.thk.as_f32().is_none()
15670 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
15671 || w.v_proj.cols() != self.hidden_size
15672 || w.v_proj.as_f32().is_none()
15673 || w.out_proj.rows() != self.hidden_size
15674 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
15675 || w.out_proj.as_f32().is_none()
15676 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
15677 {
15678 return false;
15679 }
15680 if let Some((kg, kb)) = &w.k_gate {
15681 if kg.rows() != cfg.num_heads
15682 || kg.cols() != self.hidden_size
15683 || kg.as_f32().is_none()
15684 || kb.len() != cfg.num_heads
15685 {
15686 return false;
15687 }
15688 }
15689 if let Some(conv) = &w.conv {
15690 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
15691 return false;
15692 }
15693 }
15694 }
15695 AttnKind::Full {
15696 wq,
15697 wk,
15698 wv,
15699 wo,
15700 q_norm,
15701 k_norm,
15702 output_gate,
15703 softplus_gate,
15704 bias,
15705 } => {
15706 if full_seen
15707 || q_norm.is_some()
15708 || k_norm.is_some()
15709 || *output_gate
15710 || softplus_gate.is_some()
15711 || bias.is_some()
15712 || wq.as_f32().is_none()
15713 || wk.as_f32().is_none()
15714 || wv.as_f32().is_none()
15715 || wo.as_f32().is_none()
15716 || wq.rows() != self.num_heads * self.head_dim
15717 || wk.rows() != self.num_kv_heads * self.head_dim
15718 || wv.rows() != self.num_kv_heads * self.head_dim
15719 || wq.cols() != self.hidden_size
15720 || wk.cols() != self.hidden_size
15721 || wv.cols() != self.hidden_size
15722 || wo.rows() != self.hidden_size
15723 || wo.cols() != self.num_heads * self.head_dim
15724 {
15725 return false;
15726 }
15727 full_seen = true;
15728 }
15729 AttnKind::Bounded(w) => {
15730 let Some(ac) = self.anchor_core.as_ref() else {
15733 return false;
15734 };
15735 if self.bounded_rope.is_none()
15736 || w.window != ac.window
15737 || w.sink != ac.sink
15738 || w.window == 0
15739 || w.window + w.sink > 256
15740 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15741 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15742 || w.wq.as_f32().is_none()
15743 || w.wk.as_f32().is_none()
15744 || w.wv.as_f32().is_none()
15745 || w.wo.as_f32().is_none()
15746 || w.wq.rows() != self.num_heads * self.head_dim
15747 || w.wk.rows() != self.num_kv_heads * self.head_dim
15748 || w.wv.rows() != self.num_kv_heads * self.head_dim
15749 || w.wq.cols() != self.hidden_size
15750 || w.wk.cols() != self.hidden_size
15751 || w.wv.cols() != self.hidden_size
15752 || w.wo.rows() != self.hidden_size
15753 || w.wo.cols() != self.num_heads * self.head_dim
15754 {
15755 return false;
15756 }
15757 }
15758 _ => return false,
15759 }
15760 match &lw.ffn {
15761 FfnKind::Dense(d) => {
15762 if d.act != Act::Silu
15763 || !d.segs.is_empty()
15764 || d.gate_proj.as_f32().is_none()
15765 || d.up_proj.as_f32().is_none()
15766 || d.down_proj.as_f32().is_none()
15767 || d.gate_proj.rows() != self.intermediate_size
15768 || d.gate_proj.cols() != self.hidden_size
15769 || d.up_proj.rows() != self.intermediate_size
15770 || d.up_proj.cols() != self.hidden_size
15771 || d.down_proj.rows() != self.hidden_size
15772 || d.down_proj.cols() != self.intermediate_size
15773 {
15774 return false;
15775 }
15776 }
15777 FfnKind::Moe(m) => {
15778 if m.resonance.is_none()
15779 || m.top_k != 1
15780 || m.router_sigmoid
15781 || !m.norm_topk_prob
15782 || m.expert_bias.is_some()
15783 || m.routed_scaling != 1.0
15784 || m.route_tau.is_some()
15785 || m.shared.is_none()
15786 || m.mask.is_some()
15787 || m.per_expert_scale.is_some()
15788 || m.router_input_norm
15789 || m.experts.is_empty()
15790 || m.experts.len() > 8
15791 {
15792 return false;
15793 }
15794 let r = m.resonance.as_ref().unwrap();
15795 if r.mu.len() != m.experts.len() * self.hidden_size
15796 || r.bias.len() != m.experts.len()
15797 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15798 || r.k > 128
15799 {
15800 return false;
15801 }
15802 let Some((shared, gate)) = &m.shared else {
15803 return false;
15804 };
15805 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15806 return false;
15807 }
15808 if shared.gate_proj.as_f32().is_none()
15809 || shared.up_proj.as_f32().is_none()
15810 || shared.down_proj.as_f32().is_none()
15811 || shared.gate_proj.rows() != self.intermediate_size
15812 || shared.gate_proj.cols() != self.hidden_size
15813 || shared.up_proj.rows() != self.intermediate_size
15814 || shared.up_proj.cols() != self.hidden_size
15815 || shared.down_proj.rows() != self.hidden_size
15816 || shared.down_proj.cols() != self.intermediate_size
15817 {
15818 return false;
15819 }
15820 for e in &m.experts {
15821 if e.act != Act::Silu
15822 || !e.segs.is_empty()
15823 || e.gate_proj.as_f32().is_none()
15824 || e.up_proj.as_f32().is_none()
15825 || e.down_proj.as_f32().is_none()
15826 || e.gate_proj.rows() != self.intermediate_size
15827 || e.gate_proj.cols() != self.hidden_size
15828 || e.up_proj.rows() != self.intermediate_size
15829 || e.up_proj.cols() != self.hidden_size
15830 || e.down_proj.rows() != self.hidden_size
15831 || e.down_proj.cols() != self.intermediate_size
15832 {
15833 return false;
15834 }
15835 }
15836 }
15837 FfnKind::DenseMoe(_) => return false,
15838 }
15839 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15840 return false;
15841 }
15842 }
15843 if full_seen && self.anchor_core.is_some() {
15844 return false;
15845 }
15846 full_seen || self.num_layers > 0
15847 }
15848
15849 fn embryo_resident_wanted(&self) -> bool {
15854 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15855 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15856 && matches!(
15857 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15858 Ok("1") | Ok("parallel")
15859 )
15860 && crate::gpu::enabled_here()
15861 && !self.graph_refused()
15862 && self.embryo_resident_eligible()
15863 }
15864
15865 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15873 if ids.len() < 2
15874 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15875 || !self.embryo_resident_wanted()
15876 {
15877 return None;
15878 }
15879 let model = self.ensure_embryo_graph()?;
15880 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15881 .ok()
15882 .and_then(|v| v.parse::<usize>().ok())
15883 .filter(|&v| v >= 1)
15884 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15885 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15886 let hs = self.hidden_size;
15887 let n = ids.len();
15888 let mut pos = start;
15889 let mut last = None;
15890 let mut rows = Vec::with_capacity(cmax * hs);
15891 while pos < n {
15892 let end = (pos + cmax).min(n);
15893 rows.clear();
15894 for &id in &ids[pos..end] {
15895 rows.extend_from_slice(&self.embed_single(id));
15896 }
15897 let mut lg = Vec::new();
15898 if !crate::gpu::forward_embryo_graph_chunk(
15899 &model,
15900 self.graph_kv_id,
15901 &rows,
15902 pos,
15903 end - pos,
15904 &mut lg,
15905 ) {
15906 if pos == start {
15907 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15908 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15909 }
15910 return None;
15911 }
15912 self.kv_cache.clear();
15916 self.clear_history();
15917 crate::gpu::graph_kv_reset(self.graph_kv_id);
15918 panic!(
15919 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15920 );
15921 }
15922 last = Some(lg);
15923 pos = end;
15924 }
15925 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15926 eprintln!(
15927 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15928 n - start,
15929 (n - start).div_ceil(cmax)
15930 );
15931 }
15932 last
15933 }
15934
15935 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15936 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15937 const UMAX: u32 = u32::MAX;
15938 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15939 const REC: usize = 64;
15940 struct Pack {
15941 data: Vec<f32>,
15942 }
15943 impl Pack {
15944 fn put(&mut self, x: &[f32]) -> u32 {
15945 if x.is_empty() {
15946 return u32::MAX;
15947 }
15948 let off = self.data.len();
15949 self.data.extend_from_slice(x);
15950 off as u32
15951 }
15952 }
15953 let vmf = self.vmf_cfg;
15957 let gdn = self.gdn_cfg;
15958 let mut pack = Pack { data: Vec::new() };
15959 let mut meta = vec![0u32; HEADER];
15960 meta[0] = self.hidden_size as u32;
15961 meta[1] = self.intermediate_size as u32;
15962 meta[2] = self.vocab_size as u32;
15963 meta[3] = self.num_layers as u32;
15964 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15965 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15966 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15967 if let Some(g) = gdn {
15968 meta[24] = g.num_v_heads as u32;
15969 meta[25] = g.num_k_heads as u32;
15970 meta[26] = g.key_head_dim as u32;
15971 meta[27] = g.value_head_dim as u32;
15972 meta[28] = g.conv_kernel as u32;
15973 meta[29] = g.conv_dim() as u32;
15974 }
15975 meta[7] = self.num_heads as u32;
15976 meta[8] = self.num_kv_heads as u32;
15977 meta[9] = self.head_dim as u32;
15978 meta[10] = self.kv_cache.max_seq_len as u32;
15979 let clusters = self.head_clusters.as_ref().unwrap();
15980 let cluster_count = clusters.len() / self.hidden_size;
15981 if clusters.len() % self.hidden_size != 0
15982 || cluster_count == 0
15983 || cluster_count > 1024
15984 || self.vocab_size % cluster_count != 0
15985 || self.weights.lm_head.rows() < self.vocab_size
15986 || self.weights.final_norm.len() != self.hidden_size
15987 {
15988 return None;
15989 }
15990 meta[11] = cluster_count as u32;
15991 meta[12] = (self.vocab_size / cluster_count) as u32;
15992 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15993 meta[16] = self.rotary_dim as u32;
15994 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15995 meta[19] = (self.rms_eps as f32).to_bits();
15996 let max_conv = self
15997 .weights
15998 .layers
15999 .iter()
16000 .filter_map(|lw| match &lw.attn {
16001 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16002 _ => None,
16003 })
16004 .max()
16005 .unwrap_or(1);
16006 let phase_stride = vmf
16012 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16013 .unwrap_or(0);
16014 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16015 let state_stride = phase_stride.max(gdn_stride);
16016 let bounded = self.anchor_core.clone();
16020 let (anchor_window, anchor_sink) = bounded
16021 .as_ref()
16022 .map(|ac| (ac.window, ac.sink))
16023 .unwrap_or((0, 0));
16024 let kv_stride = if bounded.is_some() {
16025 2usize
16026 .saturating_mul(self.num_kv_heads)
16027 .saturating_mul(anchor_window)
16028 .saturating_mul(self.head_dim)
16029 } else {
16030 2usize
16031 .saturating_mul(self.num_kv_heads)
16032 .saturating_mul(self.kv_cache.max_seq_len)
16033 .saturating_mul(self.head_dim)
16034 };
16035 meta[14] = state_stride as u32;
16036 meta[15] = kv_stride as u32;
16037 meta[18] = anchor_window as u32;
16038 meta[20] = anchor_sink as u32;
16039 meta[21] = match &self.bounded_rope {
16040 Some(rope) => {
16041 let off = pack.put(&rope.cos);
16043 let _ = pack.put(&rope.sin);
16044 off
16045 }
16046 None => UMAX,
16047 };
16048 let mut full_seen = false;
16049 let mut bounded_seen = 0usize;
16050 let mut phase_seen = 0usize;
16054 let mut gdn_seen = 0usize;
16055 for (li, lw) in self.weights.layers.iter().enumerate() {
16056 let base = meta.len();
16057 meta.resize(base + REC, UMAX);
16058 meta[base] = match &lw.attn {
16059 AttnKind::Linear(w) if w.phase_delta => 1,
16060 AttnKind::Linear(_) => 0,
16061 AttnKind::Full { .. } => 2,
16062 AttnKind::Bounded(_) => 3,
16063 AttnKind::LinearGdn(_) => 4,
16064 _ => UMAX,
16065 };
16066 meta[base + 1] = pack.put(&lw.input_norm);
16067 meta[base + 2] = pack.put(&lw.post_norm);
16068 meta[base + 25] = match &lw.attn {
16069 AttnKind::Linear(_) => {
16070 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16071 phase_seen += 1;
16072 off
16073 }
16074 AttnKind::LinearGdn(_) => {
16075 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16076 gdn_seen += 1;
16077 off
16078 }
16079 _ => UMAX,
16080 };
16081 match &lw.attn {
16082 AttnKind::LinearGdn(w) => {
16083 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16086 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16087 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16088 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16089 meta[base + 60] = pack.put(&w.conv1d);
16090 meta[base + 61] = pack.put(&w.a_log);
16091 meta[base + 62] = pack.put(&w.dt_bias);
16092 meta[base + 63] = pack.put(&w.norm);
16093 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16094 meta[base + 24] = 0;
16095 }
16096 AttnKind::Linear(w) => {
16097 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16098 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16099 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16100 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16101 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16102 meta[base + 7] = pack.put(&decay);
16103 if let Some((kg, kb)) = &w.k_gate {
16104 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16105 meta[base + 9] = pack.put(kb);
16106 }
16107 if let Some(conv) = &w.conv {
16108 meta[base + 10] = pack.put(conv);
16109 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16110 } else {
16111 meta[base + 24] = 0;
16112 }
16113 }
16114 AttnKind::Full { wq, wk, wv, wo, .. } => {
16115 full_seen = true;
16116 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16117 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16118 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16119 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16120 meta[base + 26] = (li * kv_stride) as u32;
16121 }
16122 AttnKind::Bounded(w) => {
16123 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16124 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16125 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16126 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16127 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16129 meta[base + 27] = pack.put(&w.sink_k);
16130 meta[base + 28] = pack.put(&w.sink_v);
16131 bounded_seen += 1;
16132 }
16133 _ => return None,
16134 }
16135 match &lw.ffn {
16136 FfnKind::Dense(d) => {
16137 meta[base + 15] = 0;
16138 meta[base + 16] = 0;
16139 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16140 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16141 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16142 }
16143 FfnKind::Moe(m) => {
16144 let r = m.resonance.as_ref().unwrap();
16145 let (shared, _) = m.shared.as_ref().unwrap();
16146 meta[base + 15] = 1;
16147 meta[base + 16] = m.experts.len() as u32;
16148 meta[base + 17] = pack.put(&r.mu);
16149 meta[base + 18] = pack.put(&r.u);
16150 meta[base + 19] = pack.put(&r.bias);
16151 meta[base + 20] = r.k as u32;
16152 let mut shell = r.effective_shell(m.experts.len());
16161 shell.push(f32::NEG_INFINITY);
16162 meta[base + 30] = pack.put(&shell);
16163 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16164 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16165 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16166 for (e, ex) in m.experts.iter().enumerate() {
16167 meta[base + 32 + e * 3] =
16168 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16169 meta[base + 33 + e * 3] =
16170 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16171 meta[base + 34 + e * 3] =
16172 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16173 }
16174 }
16175 FfnKind::DenseMoe(_) => return None,
16176 }
16177 }
16178 if !full_seen && self.num_layers == 0 {
16179 return None;
16180 }
16181 let id = {
16182 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16183 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16184 };
16185 let model = crate::gpu::EmbryoGraphModel {
16186 id,
16187 hidden: self.hidden_size,
16188 intermediate: self.intermediate_size,
16189 vocab: self.vocab_size,
16190 layers: self.num_layers,
16191 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16192 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16193 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16194 anchor_q_heads: self.num_heads,
16195 anchor_kv_heads: self.num_kv_heads,
16196 anchor_head_dim: self.head_dim,
16197 rotary_dim: self.rotary_dim,
16198 max_seq: self.kv_cache.max_seq_len,
16199 cluster_count,
16200 cluster_size: self.vocab_size / cluster_count,
16201 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16202 state_stride,
16203 kv_stride,
16204 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16205 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16206 weights: pack.data,
16207 meta,
16208 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16209 clusters: clusters.as_ref().clone(),
16210 final_norm: self.weights.final_norm.clone(),
16211 inv_freq: self.inv_freq.as_ref().clone(),
16212 bounded: bounded.is_some(),
16213 kv_layers: if bounded.is_some() {
16214 bounded_seen
16215 } else {
16216 self.num_layers
16217 },
16218 state_layers: phase_seen + gdn_seen,
16219 anchor_window,
16220 anchor_sink,
16221 phase_layers: phase_seen,
16222 gdn_layers: gdn_seen,
16223 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16224 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16225 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16226 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16227 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16228 };
16229 self.embryo_graph = Some(std::sync::Arc::new(model));
16230 }
16231 self.embryo_graph.clone()
16232 }
16233
16234 fn forward_layers_span(
16235 &mut self,
16236 hidden: &[f32],
16237 position: usize,
16238 task_mask: Option<&TaskMask>,
16239 from: usize,
16240 upto: Option<usize>,
16241 ) -> Vec<f32> {
16242 #[cfg(target_os = "macos")]
16243 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16244 position,
16245 from,
16246 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16247 );
16248 debug_assert!(
16249 from == 0
16250 || (self.dsv4.is_none()
16251 && self.dsv41.is_none()
16252 && self.qwen4_exp.is_none()
16253 && self.g3n.is_none())
16254 );
16255 #[cfg(target_os = "macos")]
16261 if !crate::gpu_metal::wait_replay() {
16262 self.fail_metal_graph("the pending async replay failed before a plain forward");
16263 return vec![0.0; self.hidden_size];
16264 }
16265 if let Some(b) = &mut self.qwen4_exp {
16266 let _ = (task_mask, upto);
16267 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16268 let mut logits = Vec::new();
16269 crate::qwen4_exp::forward_token(
16270 &b.0,
16271 &b.1,
16272 &b.2,
16273 &mut b.3,
16274 token_id,
16275 position,
16276 &self.inv_freq,
16277 self.pool.as_deref(),
16278 &mut logits,
16279 true,
16280 );
16281 self.graph_logits = Some(logits);
16282 return vec![0.0; self.hidden_size];
16283 }
16284 if let Some(b) = &mut self.dsv4 {
16290 let _ = (task_mask, upto);
16291 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16292 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16293 st.pos = position;
16294 let mut logits = Vec::new();
16295 crate::dsv4::forward_token(
16296 g,
16297 layers,
16298 &cfg,
16299 st,
16300 token_id,
16301 &self.inv_freq,
16302 self.pool.as_deref(),
16303 &mut logits,
16304 );
16305 self.graph_logits = Some(logits);
16306 self.dspark_probe(position, token_id);
16307 return vec![0.0; self.hidden_size];
16310 }
16311 if let Some(b) = &mut self.dsv41 {
16313 let _ = (task_mask, upto);
16314 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16315 let mut logits = Vec::new();
16316 crate::dsv41::forward_token(
16317 &b.0,
16318 &b.1,
16319 &b.2,
16320 &mut b.3,
16321 token_id,
16322 position,
16323 self.pool.as_deref(),
16324 &mut logits,
16325 );
16326 self.graph_logits = Some(logits);
16327 return vec![0.0; self.hidden_size];
16328 }
16329 if let Some(b) = &self.g3n {
16332 let _ = (task_mask, upto);
16333 return crate::g3n::g3n_forward(
16334 &b.0,
16335 &b.1,
16336 hidden,
16337 position,
16338 &mut self.kv_cache.layers,
16339 self.num_heads,
16340 self.num_kv_heads,
16341 self.head_dim,
16342 self.pool.as_deref(),
16343 );
16344 }
16345 if from == 0
16352 && upto.is_none()
16353 && task_mask.is_none()
16354 && self.anchor_core.is_some()
16355 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16356 {
16357 static ONCE: std::sync::Once = std::sync::Once::new();
16358 ONCE.call_once(|| {
16359 eprintln!(
16360 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16361 unsupported={} eligible={}",
16362 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
16363 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
16364 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
16365 crate::gpu::enabled_here(),
16366 self.graph_refused(),
16367 self.embryo_resident_eligible(),
16368 );
16369 });
16370 }
16371 if from == 0
16372 && upto.is_none()
16373 && task_mask.is_none()
16374 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16378 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16379 && matches!(
16383 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16384 Ok("1") | Ok("parallel")
16385 )
16386 && crate::gpu::enabled_here()
16387 && !self.graph_refused()
16388 && (position == 0 || self.device_sequence_position().is_some())
16394 && self.embryo_resident_eligible()
16395 && let Some(model) = self.ensure_embryo_graph()
16396 {
16397 let mut lg = Vec::new();
16398 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
16399 {
16400 self.graph_logits = Some(lg);
16401 return vec![0.0; self.hidden_size];
16402 }
16403 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16404 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
16405 }
16406 self.mark_graph_refused();
16412 if position != 0 {
16413 self.kv_cache.clear();
16417 self.clear_history();
16418 crate::gpu::graph_kv_reset(self.graph_kv_id);
16419 panic!(
16420 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
16421 );
16422 }
16423 }
16424 let mut h = hidden.to_vec();
16425 self.mimo_moe_prepare();
16428 let _mimo_q8 = self
16429 .mimo_moe
16430 .is_on()
16431 .then(crate::qtensor::enter_full_gpu_q8_scope);
16432 let (nh, _nkv, _hd, hs, _rd, eps) = (
16435 self.num_heads,
16436 self.num_kv_heads,
16437 self.head_dim,
16438 self.hidden_size,
16439 self.rotary_dim,
16440 self.rms_eps,
16441 );
16442 let pool = self.pool.clone();
16443 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
16455 let graph_on = match graph_env.as_deref() {
16456 Some("0") => false,
16457 Some("prefill") => false, Some(_) => true,
16459 None => crate::gpu::wgpu_graph_default(),
16465 };
16466 let graph_trusted =
16467 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
16468 let race_eligible =
16469 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
16470 let mut tail_start = 0usize;
16471 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
16472 let t_graph = std::time::Instant::now();
16473 let mut lg = Vec::new();
16474 let mut gl = 0usize;
16475 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
16476 let declined = built.is_none();
16477 let built = match built {
16478 Some(Ok(hh)) => Some(hh),
16479 Some(Err(())) => {
16480 self.clear_sequence_state();
16484 self.graph_failed
16485 .store(true, std::sync::atomic::Ordering::Relaxed);
16486 self.cancel
16487 .store(true, std::sync::atomic::Ordering::Relaxed);
16488 tracing::error!("token graph failed after admission; sequence state cleared");
16489 return vec![0.0; self.hidden_size];
16490 }
16491 None => None,
16492 };
16493 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
16498 self.mark_graph_refused();
16499 }
16500 graph_note(built.is_some(), gl, self.num_layers);
16501 if let Some(hh) = built {
16502 let dur = t_graph.elapsed();
16503 if std::env::var("CMF_GRAPH_PROF").is_ok() {
16504 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
16505 }
16506 if gl > 0 && gl < self.num_layers {
16507 h = hh;
16513 tail_start = gl;
16514 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
16515 if !graph_trusted {
16516 crate::gpu::graph_race_record(true, dur);
16517 }
16518 if !lg.is_empty() {
16519 lg.resize(self.vocab_size, 0.0);
16522 if let Some(c) = self.final_softcap {
16523 softcap_in_place(&mut lg, c, self.pool.as_deref());
16524 }
16525 self.graph_logits = Some(lg);
16526 }
16527 return hh;
16528 }
16529 }
16535 }
16536 let span = from > 0 || upto.is_some();
16560 if span && graph_on && task_mask.is_none() && graph_trusted {
16561 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
16562 let mut lg = Vec::new();
16563 let mut gl = 0usize;
16564 let span_res =
16565 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
16566 let span_res = match span_res {
16567 Some(Ok(hh)) => Some(hh),
16568 Some(Err(())) => {
16569 self.clear_sequence_state();
16570 self.graph_failed
16571 .store(true, std::sync::atomic::Ordering::Relaxed);
16572 self.cancel
16573 .store(true, std::sync::atomic::Ordering::Relaxed);
16574 tracing::error!(
16575 "span token graph failed after admission; sequence state cleared"
16576 );
16577 return vec![0.0; self.hidden_size];
16578 }
16579 None => None,
16580 };
16581 graph_note(span_res.is_some(), gl, upto_excl - from);
16582 if std::env::var("CMF_GPU_DEBUG").is_ok() {
16583 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
16587 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
16588 eprintln!(
16589 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
16590 upto_excl - from,
16591 span_res.is_some()
16592 );
16593 }
16594 }
16595 if let Some(hh) = span_res {
16596 if gl == upto_excl - from {
16597 if !lg.is_empty() {
16598 lg.resize(self.vocab_size, 0.0);
16599 if let Some(c) = self.final_softcap {
16600 softcap_in_place(&mut lg, c, self.pool.as_deref());
16601 }
16602 self.graph_logits = Some(lg);
16603 }
16604 crate::gpu::set_layer(-1);
16605 return hh;
16606 }
16607 h = hh;
16609 tail_start = from + gl;
16610 }
16611 }
16612 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
16617
16618 let host_tail = tail_start > from;
16628 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
16629 let automatic_gpu_prefix = self.automatic_gpu_prefix();
16630
16631 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
16632 #[cfg(target_os = "macos")]
16633 let mut gpu_skip_until = 0usize;
16634 for li in tail_start.max(from)..self.num_layers {
16635 let _capacity_tail = automatic_gpu_prefix
16636 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
16637 .map(|_| crate::gpu::enter_cpu_scope());
16638 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
16640 if li > u {
16641 break;
16642 }
16643 }
16644 if let Some(mask) = task_mask {
16645 if !mask.layer_alive(li) {
16646 continue; }
16648 }
16649 if host_tail && task_mask.is_none() {
16654 match self.m3_pooled_layer(li, position, &mut h) {
16655 Ok(true) => {
16656 crate::gpu::set_layer(-1);
16657 continue;
16658 }
16659 Ok(false) => {}
16660 Err(()) => {
16661 self.clear_sequence_state();
16662 self.graph_failed
16663 .store(true, std::sync::atomic::Ordering::Relaxed);
16664 self.cancel
16665 .store(true, std::sync::atomic::Ordering::Relaxed);
16666 tracing::error!(
16667 "pooled tail token graph failed after admission; sequence state cleared"
16668 );
16669 return vec![0.0; self.hidden_size];
16670 }
16671 }
16672 }
16673 #[cfg(target_os = "macos")]
16677 {
16678 if li < gpu_skip_until {
16679 continue;
16680 }
16681 if task_mask.is_none() {
16682 let end = self.q1_graph_gpu(li, upto, position, &mut h);
16683 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
16684 return vec![0.0; self.hidden_size];
16688 }
16689 if end > li {
16690 gpu_skip_until = end;
16691 if self.is_loop_end(end - 1) && end < self.num_layers {
16694 h = inference::rms_norm(
16695 &h,
16696 &self.weights.final_norm,
16697 self.rms_eps,
16698 self.norm_style,
16699 );
16700 }
16701 continue;
16702 }
16703 }
16704 }
16705
16706 if task_mask.is_none() {
16707 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
16708 crate::gpu::BatchGraphOutcome::Completed => continue,
16709 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
16710 crate::gpu::BatchGraphOutcome::Declined => {}
16711 }
16712 }
16713 #[cfg(feature = "gpu")]
16714 self.pull_lagging_host_kv(li, li + 1, position);
16715 let lw = &self.weights.layers[self.phys_layer(li)];
16716 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
16717 if tp.parse::<usize>().ok() == Some(position) {
16718 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16719 eprintln!(
16720 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16721 h[0], h[1]
16722 );
16723 }
16724 }
16725 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16728 inference::rms_norm_into(
16729 &h,
16730 &lw.input_norm,
16731 self.rms_eps,
16732 self.norm_style,
16733 &mut self.ws.n1,
16734 );
16735 drop(prof);
16736
16737 let attn_out = match &lw.attn {
16738 AttnKind::Mla(w) => {
16739 let inv_freq_l = self.layer_inv_freq(li);
16740 let rs = self.layer_rope_scale(li);
16741 let eps = self.rms_eps;
16742 let pool = self.pool.clone();
16743 mla_attention(
16744 w,
16745 &self.ws.n1,
16746 &mut self.kv_cache.layers[li],
16747 position,
16748 &inv_freq_l,
16749 rs,
16750 eps,
16751 pool.as_deref(),
16752 )
16753 }
16754 AttnKind::Linear(w) => {
16755 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16756 vmf_phase_forward(
16757 &self.ws.n1,
16758 w,
16759 &cfg,
16760 &mut self.kv_cache.layers[li].linear_state,
16761 self.pool.as_deref(),
16762 )
16763 }
16764 AttnKind::Kda(w) => {
16765 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16766 crate::linear_core::kda_forward(
16767 &self.ws.n1,
16768 w,
16769 &cfg,
16770 &mut self.kv_cache.layers[li].linear_state,
16771 self.pool.as_deref(),
16772 )
16773 }
16774 AttnKind::LinearGdn(w) => {
16775 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16776 gdn_forward(
16777 &self.ws.n1,
16778 w,
16779 &cfg,
16780 &mut self.kv_cache.layers[li].linear_state,
16781 self.pool.as_deref(),
16782 )
16783 }
16784 AttnKind::ShortConv(w) => {
16785 let cfg = self
16786 .short_conv_cfg
16787 .expect("short-conv layer without short_conv_cfg");
16788 short_conv_forward(
16789 &self.ws.n1,
16790 w,
16791 &cfg,
16792 &mut self.kv_cache.layers[li].linear_state,
16793 self.pool.as_deref(),
16794 )
16795 }
16796 AttnKind::Bounded(w) => {
16797 let rope = self
16800 .bounded_rope
16801 .clone()
16802 .expect("bounded layer without an installed rotation table");
16803 let cfg = crate::bounded::BoundedAttnCfg {
16804 num_heads: self.num_heads,
16805 num_kv_heads: self.num_kv_heads,
16806 head_dim: self.head_dim,
16807 hidden_size: hs,
16808 scale: self.attn_scale,
16809 rope: &rope,
16810 pool: pool.as_deref(),
16811 };
16812 crate::bounded::bounded_attention(
16813 &self.ws.n1,
16814 w,
16815 &mut self.kv_cache.layers[li],
16816 &cfg,
16817 )
16818 }
16819 AttnKind::Full {
16820 wq,
16821 wk,
16822 wv,
16823 wo,
16824 q_norm,
16825 k_norm,
16826 output_gate,
16827 softplus_gate,
16828 bias,
16829 } if self.kv_cache.layers[li].o1_sealed() => {
16830 let inv_freq_l = self.layer_inv_freq(li);
16833 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16834 let cfg = QwenAttnCfg {
16835 num_heads: self.layer_num_heads(li),
16836 num_kv_heads: nkv_l,
16837 head_dim: hd_l,
16838 hidden_size: hs,
16839 position,
16840 inv_freq: &inv_freq_l,
16841 rotary_dim: rd_l,
16842 scale: self.attn_scale,
16843 softcap: self.attn_softcap,
16844 window: None,
16845 v_norm: self.attn_v_norm,
16846 qk_norm_after_rope: self.qk_norm_after_rope,
16847 gate_sigmoid: self.proj_gate_sigmoid,
16848 q_norm: q_norm.as_deref(),
16849 k_norm: k_norm.as_deref(),
16850 output_gate: *output_gate,
16851 softplus_gate: softplus_gate
16852 .as_ref()
16853 .map(|(gate, per_head)| (gate, *per_head)),
16854 rope_scale: self.layer_rope_scale(li),
16855 bias: bias
16856 .as_ref()
16857 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16858 rms_eps: eps,
16859 norm_style: self.norm_style,
16860 pool: pool.as_deref(),
16861 v_head_dim: self.layer_v_dim(li),
16862 };
16863 attention::qwen_attention_nystrom(
16864 &self.ws.n1,
16865 wq,
16866 wk,
16867 wv,
16868 wo,
16869 &mut self.kv_cache.layers[li],
16870 &cfg,
16871 )
16872 }
16873 AttnKind::Full {
16874 wq,
16875 wk,
16876 wv,
16877 wo,
16878 q_norm,
16879 k_norm,
16880 output_gate,
16881 softplus_gate,
16882 bias,
16883 } => 'attn: {
16884 let dropin_reason =
16889 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16890 if let Some(reason) = dropin_reason {
16891 self.note_graph_decline("wgpu attn dropin", reason);
16892 }
16893 if graph_on
16894 && dropin_reason.is_none()
16895 && !*output_gate
16896 && softplus_gate.is_none()
16897 && self.attention_heads_per_layer.is_none()
16898 && bias.is_none()
16899 && task_mask.is_none()
16900 {
16901 let inv_freq_l = self.layer_inv_freq(li);
16902 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16903 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16904 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16905 wq.mapped_q1(),
16906 wk.mapped_q1(),
16907 wv.mapped_q1(),
16908 wo.mapped_q1(),
16909 ) {
16910 let gm = gm.clone();
16911 let mut out = vec![0f32; hs];
16912 let cache = &self.kv_cache.layers[li];
16913 if crate::gpu::attn_dropin(
16914 &gm,
16915 self.graph_kv_id,
16916 li,
16917 &self.ws.n1,
16918 qi,
16919 ki,
16920 vi,
16921 oi,
16922 q_norm.as_deref(),
16923 k_norm.as_deref(),
16924 self.qk_norm_after_rope,
16925 &inv_freq_l,
16926 nh,
16927 nkv_l,
16928 hd_l,
16929 rd_l,
16930 hs,
16931 position,
16932 self.kv_cache.max_seq_len,
16933 gemma,
16934 eps as f32,
16935 cache.k_heads(),
16936 cache.v_heads(),
16937 &mut out,
16938 ) {
16939 break 'attn out;
16940 }
16941 }
16942 }
16943 let masked = task_mask
16944 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16945 .unwrap_or(false);
16946 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16947 let plain = self.layer_attn_plain(li);
16950 match (masked, f32_view) {
16951 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16954 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16955 attention::multi_head_attention(
16956 &self.ws.n1,
16957 q,
16958 k,
16959 v,
16960 o,
16961 &mut self.kv_cache.layers[li],
16962 self.num_heads,
16963 self.num_kv_heads,
16964 self.head_dim,
16965 self.hidden_size,
16966 position,
16967 &active_heads,
16968 &self.inv_freq,
16969 )
16970 }
16971 (masked, _) => {
16972 if masked {
16973 tracing::warn!(
16974 "layer {li}: head mask on quantized weights or on a \
16975 window/sink/per-layer-geometry layer not supported \
16976 yet — executing dense"
16977 );
16978 }
16979 let inv_freq_l = self.layer_inv_freq(li);
16980 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16981 let cfg = QwenAttnCfg {
16982 num_heads: self.layer_num_heads(li),
16983 num_kv_heads: nkv_l,
16984 head_dim: hd_l,
16985 hidden_size: hs,
16986 position,
16987 inv_freq: &inv_freq_l,
16988 rotary_dim: rd_l,
16989 scale: self.attn_scale,
16990 softcap: self.attn_softcap,
16991 window: self.layer_window(li),
16992 v_norm: self.attn_v_norm,
16993 qk_norm_after_rope: self.qk_norm_after_rope,
16994 gate_sigmoid: self.proj_gate_sigmoid,
16995 q_norm: q_norm.as_deref(),
16996 k_norm: k_norm.as_deref(),
16997 output_gate: *output_gate,
16998 softplus_gate: softplus_gate
16999 .as_ref()
17000 .map(|(gate, per_head)| (gate, *per_head)),
17001 rope_scale: self.layer_rope_scale(li),
17002 bias: bias
17003 .as_ref()
17004 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17005 rms_eps: eps,
17006 norm_style: self.norm_style,
17007 pool: pool.as_deref(),
17008 v_head_dim: self.layer_v_dim(li),
17009 };
17010 attention::qwen_attention(
17011 &self.ws.n1,
17012 wq,
17013 wk,
17014 wv,
17015 wo,
17016 &mut self.kv_cache.layers[li],
17017 &cfg,
17018 )
17019 }
17020 }
17021 }
17022 };
17023 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17026 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17027 None => attn_out,
17028 };
17029 let lw = &self.weights.layers[self.phys_layer(li)];
17030 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17031 inference::add_rmsnorm_fused_into(
17032 &mut h,
17033 &attn_out,
17034 &lw.post_norm,
17035 self.rms_eps,
17036 self.norm_style,
17037 &mut self.ws.p1,
17038 );
17039 drop(prof);
17040 let mut attn_out = attn_out;
17041 attention::recycle_buf(&mut attn_out);
17042 let post_normed = &self.ws.p1;
17043
17044 let ffn_masked = task_mask
17045 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17046 .unwrap_or(false);
17047 let ffn_out = match (ffn_masked, &lw.ffn) {
17059 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17063 let row = task_mask
17064 .and_then(|tm| tm.ffn_masks.get(li))
17065 .map(|v| v.as_slice());
17066 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17067 }
17068 (true, FfnKind::Dense(d)) => {
17069 let tm = task_mask.unwrap();
17070 let alive = tm.ffn_active_count(li);
17071 let deep = alive * 2 <= self.intermediate_size;
17072 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17073 let active = tm.ffn_active_indices(li);
17074 sparse_ffn_quant(
17075 d,
17076 post_normed,
17077 &active,
17078 self.hidden_size,
17079 self.pool.as_deref(),
17080 )
17081 } else if deep
17082 && let (Some(g), Some(u), Some(dn)) = (
17083 d.gate_proj.as_f32(),
17084 d.up_proj.as_f32(),
17085 d.down_proj.as_f32(),
17086 )
17087 {
17088 let active = tm.ffn_active_indices(li);
17089 inference::sparse_ffn_forward(
17090 post_normed,
17091 g,
17092 u,
17093 dn,
17094 self.hidden_size,
17095 self.intermediate_size,
17096 &active,
17097 self.pool.as_deref(),
17098 )
17099 } else {
17100 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17101 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17102 }
17103 }
17104 (true, FfnKind::Moe(m)) => {
17105 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17109 ffn_forward(
17110 &lw.ffn,
17111 post_normed,
17112 self.pool.as_deref(),
17113 allowed.as_deref(),
17114 )
17115 }
17116 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17117 dm,
17118 post_normed,
17119 &h,
17120 self.rms_eps,
17121 self.norm_style,
17122 self.pool.as_deref(),
17123 ),
17124 (false, _) => match &lw.ffn {
17125 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17126 dm,
17127 post_normed,
17128 &h,
17129 self.rms_eps,
17130 self.norm_style,
17131 self.pool.as_deref(),
17132 ),
17133 FfnKind::Moe(m)
17134 if task_mask.is_none()
17135 && host_tail
17136 && li >= tail_start
17137 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17138 {
17139 moe_ffn_minicpm_tail(
17140 &mut self.minicpm_tail,
17141 li,
17142 self.num_layers,
17143 m,
17144 post_normed,
17145 self.pool.as_deref(),
17146 )
17147 }
17148 FfnKind::Moe(m)
17149 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17150 {
17151 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17152 }
17153 _ => {
17154 let allowed = match (&lw.ffn, task_mask) {
17155 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17156 _ => None,
17157 };
17158 ffn_forward(
17159 &lw.ffn,
17160 post_normed,
17161 self.pool.as_deref(),
17162 allowed.as_deref(),
17163 )
17164 }
17165 },
17166 };
17167 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17168 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17169 None => ffn_out,
17170 };
17171 for (i, &f) in ffn_out.iter().enumerate() {
17172 h[i] += f;
17173 }
17174 let mut ffn_out = ffn_out;
17175 attention::recycle_buf(&mut ffn_out);
17176
17177 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17179 for v in h.iter_mut() {
17180 *v *= sc;
17181 }
17182 }
17183 if self.layer_dump.is_some() {
17185 self.dump_layer_row(position, li, &h);
17186 }
17187
17188 if self.is_loop_end(li) && li + 1 < self.num_layers {
17191 h = inference::rms_norm(
17192 &h,
17193 &self.weights.final_norm,
17194 self.rms_eps,
17195 self.norm_style,
17196 );
17197 }
17198
17199 if self.dyn_phi_layer == Some(li) {
17203 self.update_dyn_phi(&h);
17204 }
17205 }
17206 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17208 crate::gpu::graph_race_record(false, t.elapsed());
17209 }
17210
17211 h
17212 }
17213
17214 fn update_dyn_phi(&mut self, h: &[f32]) {
17217 const A: f32 = 0.2;
17218 if self.dyn_phi_ema.len() != h.len() {
17219 self.dyn_phi_ema = vec![0.0; h.len()];
17220 self.dyn_phi_seen = 0;
17221 }
17222 if self.dyn_phi_seen == 0 {
17223 self.dyn_phi_ema.copy_from_slice(h);
17224 } else {
17225 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17226 *e = (1.0 - A) * *e + A * v;
17227 }
17228 }
17229 self.dyn_phi_seen += 1;
17230 }
17231
17232 pub fn dyn_phi(&self) -> &[f32] {
17234 &self.dyn_phi_ema
17235 }
17236
17237 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17239 self.dyn_phi_layer = layer;
17240 self.dyn_phi_ema.clear();
17241 self.dyn_phi_seen = 0;
17242 }
17243
17244 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17246 let Some(model) = &self.model else {
17247 return Vec::new();
17248 };
17249 model
17250 .header
17251 .skills
17252 .iter()
17253 .enumerate()
17254 .filter_map(|(i, sk)| {
17255 if sk.is_v2() {
17259 return None;
17260 }
17261 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17262 let sel = sk.selection.as_ref()?;
17263 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17264 })
17265 .collect()
17266 }
17267
17268 pub fn active_skill(&self) -> Option<usize> {
17270 self.dyn_active
17271 }
17272
17273 pub fn enable_dynamic_routing(&mut self) -> usize {
17278 use crate::swarm::{DynRouter, RoutableSkill};
17279 let Some(model) = self.model.clone() else {
17280 return 0;
17281 };
17282 if let Some(r) = &model.header.router {
17289 tracing::warn!(
17290 "dynamic routing disabled: this file declares router policy '{}' with \
17291 granularity \"{}\" — the request-level decision applies instead",
17292 r.policy,
17293 r.granularity
17294 );
17295 return 0;
17296 }
17297 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17303 || model.header.skills.iter().any(|s| s.is_v2())
17304 {
17305 tracing::warn!(
17306 "dynamic routing disabled: this file carries format-v2 skill records \
17307 (SKILLS_V2) — they route per request through a router policy only"
17308 );
17309 return 0;
17310 }
17311 if self.dyn_blend_loaded {
17314 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17315 return 0;
17316 }
17317 if let Some(a) = self.dyn_active {
17321 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17322 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17323 return 0;
17324 }
17325 }
17326 let hidden = self.hidden_size;
17327 let mut skills = Vec::new();
17328 for (idx, id, _phi) in self.dynamic_skills() {
17329 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17330 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17331 skills.push(rs);
17332 }
17333 }
17334 }
17335 if skills.is_empty() {
17336 return 0;
17337 }
17338 let phi = skills[0].phi_layer;
17340 if skills.iter().any(|s| s.phi_layer != phi) {
17341 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17342 }
17343 let n = skills.len();
17344 self.set_dyn_phi_layer(Some(phi));
17345 self.dyn_router = Some(DynRouter::new(skills));
17346 n
17347 }
17348
17349 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17351 self.dyn_router
17352 .as_ref()
17353 .map(|r| r.switches.clone())
17354 .unwrap_or_default()
17355 }
17356
17357 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17360 let _mimo_q8 = self
17361 .mimo_moe
17362 .is_on()
17363 .then(crate::qtensor::enter_full_gpu_q8_scope);
17364 let rows = self.weights.lm_head.rows();
17365 let mut logits = attention::take_buf(rows.min(self.vocab_size));
17366 let served = self.mimo_moe.is_on()
17370 && crate::gpu::mimo_q8_short_enabled()
17371 && rows == self.vocab_size
17372 && !self.weights.lm_head.has_prism_contract()
17373 && self
17374 .weights
17375 .lm_head
17376 .graph_weight()
17377 .is_some_and(|(model, idx, kind, _)| {
17378 kind == 7
17379 && crate::gpu::q82_short_rows(
17380 model,
17381 idx,
17382 hidden,
17383 1,
17384 rows,
17385 self.hidden_size,
17386 &mut logits,
17387 )
17388 });
17389 if !served {
17390 self.weights
17391 .lm_head
17392 .matvec(hidden, &mut logits, self.pool.as_deref());
17393 }
17394 logits.resize(self.vocab_size, 0.0);
17395 if let Some(m) = self.logit_multiplier {
17396 for l in logits.iter_mut() {
17397 *l *= m;
17398 }
17399 }
17400 if let Some(c) = self.final_softcap {
17401 softcap_in_place(&mut logits, c, self.pool.as_deref());
17402 }
17403 if let Some(cm) = self.head_clusters.as_ref() {
17404 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
17405 }
17406 logits
17407 }
17408
17409 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
17412 let h = hidden.len();
17413 let ncl = cm.len() / h.max(1);
17414 if ncl == 0 || logits.len() % ncl != 0 {
17415 return;
17416 }
17417 let cs = logits.len() / ncl;
17418 let mut lc = vec![0.0f32; ncl];
17420 for c in 0..ncl {
17421 let row = &cm[c * h..(c + 1) * h];
17422 let mut s = 0.0f32;
17423 for j in 0..h {
17424 s += row[j] * hidden[j];
17425 }
17426 lc[c] = s;
17427 }
17428 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17429 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
17430 for c in 0..ncl {
17431 let blk = &mut logits[c * cs..(c + 1) * cs];
17432 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17433 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
17434 let add = lc[c] - lse - bl;
17435 for v in blk.iter_mut() {
17436 *v += add;
17437 }
17438 }
17439 }
17440
17441 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
17446 #[cfg(target_os = "macos")]
17447 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
17448 self.clear_sequence_state();
17449 crate::gpu::graph_race_begin_generation();
17453 if task_mask.is_none() {
17454 self.o1_begin();
17455 }
17456 let mut hidden = vec![0.0f32; self.hidden_size];
17457 for (pos, &id) in ids.iter().enumerate() {
17458 let emb = self.embed_single(id);
17459 hidden = self.forward_layers(&emb, pos, task_mask);
17460 }
17461 if let Err(err) = self.o1_seal_checked() {
17462 self.o1_fail(err);
17463 }
17464 if let Some(logits) = self.graph_logits.take() {
17467 return logits;
17468 }
17469 inference::rms_norm_into(
17470 &hidden,
17471 &self.weights.final_norm,
17472 self.rms_eps,
17473 self.norm_style,
17474 &mut self.ws.n1,
17475 );
17476 self.lm_head_forward(&self.ws.n1)
17477 }
17478}
17479
17480pub fn create_test_pipeline(
17482 hidden_size: usize,
17483 intermediate_size: usize,
17484 num_heads: usize,
17485 num_kv_heads: usize,
17486 head_dim: usize,
17487 num_layers: usize,
17488 vocab_size: usize,
17489) -> Pipeline {
17490 let synth = |n: usize, salt: usize| -> Vec<f32> {
17493 (0..n)
17494 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
17495 .collect()
17496 };
17497 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
17498 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17499 };
17500 let layer_weights: Vec<LayerWeights> = (0..num_layers)
17501 .map(|li| LayerWeights {
17502 input_norm: vec![1.0; hidden_size],
17503 post_norm: vec![1.0; hidden_size],
17504 attn_out_norm: None,
17505 ffn_out_norm: None,
17506 layer_scale: None,
17507 ffn: FfnKind::Dense(DenseFfn {
17508 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
17509 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
17510 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
17511 act: Act::Silu,
17512 down_t: None,
17513 segs: Vec::new(),
17514 }),
17515 attn: AttnKind::Full {
17516 bias: None,
17517 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
17518 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
17519 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
17520 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
17521 q_norm: None,
17522 k_norm: None,
17523 output_gate: false,
17524 softplus_gate: None,
17525 },
17526 })
17527 .collect();
17528
17529 Pipeline::new(
17530 Tokenizer::byte_level(),
17531 PipelineWeights {
17532 embed_tokens: qt(vocab_size, hidden_size, 100),
17533 layers: layer_weights,
17534 lm_head: qt(vocab_size, hidden_size, 200),
17535 final_norm: vec![1.0; hidden_size],
17536 },
17537 hidden_size,
17538 intermediate_size,
17539 num_heads,
17540 num_kv_heads,
17541 head_dim,
17542 num_layers,
17543 num_layers, false, vocab_size,
17546 1e-6,
17547 10_000.0,
17548 NormStyle::Qwen,
17549 4096,
17550 SamplerConfig {
17551 seed: Some(42),
17552 ..Default::default()
17553 },
17554 )
17555}
17556
17557#[inline]
17562fn mask_bit(row: &[u8], j: usize) -> bool {
17563 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
17564}
17565
17566fn mask_gain() -> f32 {
17577 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17578 *G.get_or_init(|| {
17579 std::env::var("CMF_FFN_MASK_GAIN")
17580 .ok()
17581 .and_then(|v| v.parse().ok())
17582 .unwrap_or(1.0)
17583 })
17584}
17585
17586fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
17587 let fill = meanfill().and_then(|(i, v)| {
17590 let li = crate::gpu::cur_layer();
17591 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
17592 });
17593 for r in 0..rows {
17594 let base = r * inter;
17595 for (bi, &byte) in row.iter().enumerate() {
17596 if byte == 0xFF {
17597 continue;
17598 }
17599 let j0 = bi * 8;
17600 for bit in 0..8 {
17601 let j = j0 + bit;
17602 if j < inter && byte & (1 << bit) == 0 {
17603 g[base + j] = fill.map_or(0.0, |f| f[j]);
17604 }
17605 }
17606 }
17607 }
17608 let gain = mask_gain();
17609 if gain != 1.0 {
17610 for v in g[..rows * inter].iter_mut() {
17611 *v *= gain;
17612 }
17613 }
17614}
17615
17616#[inline]
17618fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
17619 row.is_none_or(|r| mask_bit(r, i))
17620}
17621
17622fn all_bits_on(row: &[u8], n: usize) -> bool {
17625 (0..n).all(|i| mask_bit(row, i))
17626}
17627
17628fn tube_topk() -> usize {
17636 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17637 *K.get_or_init(|| {
17638 std::env::var("CMF_TUBE_TOPK")
17639 .ok()
17640 .and_then(|v| v.parse().ok())
17641 .unwrap_or(0)
17642 })
17643}
17644
17645fn tube_score_oracle() -> bool {
17646 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17647 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
17648}
17649
17650fn tube_ffn_routed(
17657 d: &DenseFfn,
17658 xs: &[f32],
17659 b: usize,
17660 pool: Option<&Pool>,
17661 mask_row: Option<&[u8]>,
17662 k: usize,
17663) -> Vec<f32> {
17664 let hidden = d.down_proj.rows();
17665 let core = d.gate_proj.rows();
17666 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17667 let mut out = match (b, core_full, mask_row) {
17668 (1, true, _) => dense_ffn(d, xs, pool),
17669 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17670 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17671 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17672 };
17673 let cand: Vec<usize> = (0..d.segs.len())
17674 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
17675 .collect();
17676 if cand.is_empty() {
17677 return out;
17678 }
17679 let oracle = tube_score_oracle();
17683 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
17684 let mut scores = vec![0f32; b * cand.len()];
17685 for (ci, &i) in cand.iter().enumerate() {
17686 let seg = &d.segs[i];
17687 let w = seg.width;
17688 let mut g = vec![0.0f32; b * w];
17689 if b == 1 {
17690 seg.gate.matvec(xs, &mut g, pool);
17691 } else {
17692 seg.gate.matmat(xs, b, &mut g, pool);
17693 }
17694 for v in g.iter_mut() {
17695 *v = Act::Silu.combine(*v, 1.0);
17696 }
17697 if !oracle {
17698 for t in 0..b {
17699 scores[t * cand.len() + ci] =
17700 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17701 }
17702 }
17703 if oracle || b > 1 {
17704 let mut u = vec![0.0f32; b * w];
17705 if b == 1 {
17706 seg.up.matvec(xs, &mut u, pool);
17707 } else {
17708 seg.up.matmat(xs, b, &mut u, pool);
17709 }
17710 for (a, &v) in g.iter_mut().zip(u.iter()) {
17711 *a *= v;
17712 }
17713 if oracle {
17714 for t in 0..b {
17715 scores[t * cand.len() + ci] =
17716 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17717 }
17718 }
17719 }
17720 acts.push(g);
17721 }
17722 let keep = k.min(cand.len());
17724 let mut scratch: Vec<f32> = Vec::new();
17725 for t in 0..b {
17726 let mut sc: Vec<(f32, usize)> = (0..cand.len())
17727 .map(|ci| (scores[t * cand.len() + ci], ci))
17728 .collect();
17729 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
17730 let mut alive = vec![false; cand.len()];
17731 for &(_, ci) in sc.iter().take(keep) {
17732 alive[ci] = true;
17733 }
17734 if b > 1 {
17735 for (ci, a) in acts.iter_mut().enumerate() {
17736 if !alive[ci] {
17737 let w = d.segs[cand[ci]].width;
17738 a[t * w..(t + 1) * w].fill(0.0);
17739 }
17740 }
17741 } else {
17742 for (ci, &i) in cand.iter().enumerate() {
17746 if !alive[ci] {
17747 continue;
17748 }
17749 let seg = &d.segs[i];
17750 let w = seg.width;
17751 let g = &mut acts[ci];
17752 if !tube_score_oracle() {
17753 scratch.clear();
17754 scratch.resize(w, 0.0);
17755 seg.up.matvec(xs, &mut scratch, pool);
17756 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17757 *a *= v;
17758 }
17759 }
17760 let mut acc = vec![0.0f32; hidden];
17761 seg.down.matvec(g, &mut acc, pool);
17762 for (o, a) in out.iter_mut().zip(&acc) {
17763 *o += *a;
17764 }
17765 }
17766 }
17767 }
17768 if b > 1 {
17769 for (ci, &i) in cand.iter().enumerate() {
17770 let seg = &d.segs[i];
17771 let mut acc = vec![0.0f32; b * hidden];
17772 seg.down.matmat(&acts[ci], b, &mut acc, pool);
17773 for (o, a) in out.iter_mut().zip(&acc) {
17774 *o += *a;
17775 }
17776 }
17777 }
17778 out
17779}
17780
17781fn tube_ffn(
17787 d: &DenseFfn,
17788 xs: &[f32],
17789 b: usize,
17790 pool: Option<&Pool>,
17791 mask_row: Option<&[u8]>,
17792) -> Vec<f32> {
17793 if tube_topk() > 0 {
17794 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17795 }
17796 let hidden = d.down_proj.rows();
17797 let core = d.gate_proj.rows();
17798 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17799 let mut out = match (b, core_full, mask_row) {
17800 (1, true, _) => dense_ffn(d, xs, pool),
17801 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17802 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17803 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17804 };
17805 TUBE_SCRATCH.with(|sc| {
17806 let mut sc = sc.borrow_mut();
17807 let [g, u, acc] = &mut *sc;
17808 for seg in &d.segs {
17809 if !tube_bit(mask_row, seg.start) {
17810 continue;
17811 }
17812 let w = seg.width;
17813 g.resize(b * w, 0.0);
17814 if b == 1
17815 && d.act == Act::Silu
17816 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17817 {
17818 } else {
17820 u.resize(b * w, 0.0);
17821 if b == 1 {
17822 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17823 } else {
17824 seg.gate.matmat(xs, b, g, pool);
17825 seg.up.matmat(xs, b, u, pool);
17826 }
17827 for i in 0..b * w {
17828 g[i] = d.act.combine(g[i], u[i]);
17829 }
17830 }
17831 acc.resize(b * hidden, 0.0);
17832 acc.fill(0.0);
17833 if b == 1 {
17834 seg.down.matvec(g, acc, pool);
17835 } else {
17836 seg.down.matmat(g, b, acc, pool);
17837 }
17838 for (o, a) in out.iter_mut().zip(acc.iter()) {
17839 *o += *a;
17840 }
17841 }
17842 out
17843 })
17844}
17845
17846thread_local! {
17847 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17851 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17852}
17853
17854static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
17857 std::sync::atomic::AtomicU64::new(0),
17858 std::sync::atomic::AtomicU64::new(0),
17859];
17860
17861fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17865 #[cfg(feature = "gpu")]
17866 {
17867 use std::sync::atomic::Ordering::Relaxed;
17868 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17869 (
17870 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17871 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17872 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17873 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17874 )
17875 }
17876 #[cfg(not(feature = "gpu"))]
17877 (0.0, 0.0, 0.0, 0)
17878}
17879
17880fn wgpu_mirror_counters() -> [u64; 6] {
17883 #[cfg(feature = "gpu")]
17884 {
17885 crate::gpu_wgpu::MIRROR_EVENTS
17886 .each_ref()
17887 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17888 }
17889 #[cfg(not(feature = "gpu"))]
17890 [0; 6]
17891}
17892
17893fn prefill_prof_on() -> bool {
17894 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17895 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17896}
17897
17898fn dense_ffn_batch(
17899 d: &DenseFfn,
17900 xs: &[f32],
17901 b: usize,
17902 pool: Option<&Pool>,
17903 mask_row: Option<&[u8]>,
17904) -> Vec<f32> {
17905 let inter = d.gate_proj.rows();
17906 let hidden = d.down_proj.rows();
17907 let fused_act = d.act.wgpu_graph_act();
17919 if mask_row.is_none()
17920 && fused_act.is_some()
17921 && b >= 32
17922 && crate::gpu::enabled_here()
17923 && !crate::gpu::mm_killed()
17924 && refit_dir().is_none()
17929 && !ffn_probe_active()
17934 && !crate::gptq_capture::capturing()
17938 {
17939 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17940 d.gate_proj.mapped_q4t(),
17941 d.up_proj.mapped_q4t(),
17942 d.down_proj.mapped_q4t(),
17943 ) {
17944 let mut out = vec![0.0f32; b * hidden];
17945 let act = fused_act.expect("checked above");
17946 if crate::gpu::q4_ffn_act(
17947 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
17948 ) {
17949 return out;
17950 }
17951 }
17952 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17957 d.gate_proj.mapped_q4tp(),
17958 d.up_proj.mapped_q4tp(),
17959 d.down_proj.mapped_q4tp(),
17960 ) {
17961 let mut out = vec![0.0f32; b * hidden];
17962 let act = fused_act.expect("checked above");
17963 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17964 {
17965 return out;
17966 }
17967 }
17968 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17974 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17975 d.gate_proj.mapped_device_gemm(),
17976 d.up_proj.mapped_device_gemm(),
17977 d.down_proj.mapped_device_gemm(),
17978 ) {
17979 let mut out = vec![0.0f32; b * hidden];
17980 let act = fused_act.expect("checked above");
17981 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17982 return out;
17983 }
17984 }
17985 }
17986 let mut g = vec![0.0f32; b * inter];
17987 d.gate_proj.matmat(xs, b, &mut g, pool);
17988 let mut u = vec![0.0f32; b * inter];
17989 d.up_proj.matmat(xs, b, &mut u, pool);
17990 if gate_topk() > 0 && d.act == Act::Silu {
17991 for t in 0..b {
17992 let row = &mut g[t * inter..(t + 1) * inter];
17993 for v in row.iter_mut() {
17994 *v = Act::Silu.combine(*v, 1.0);
17995 }
17996 keep_top_k(row, gate_topk());
17997 }
17998 for i in 0..b * inter {
17999 g[i] *= u[i];
18000 }
18001 } else {
18002 for i in 0..b * inter {
18003 g[i] = d.act.combine(g[i], u[i]);
18004 }
18005 }
18006 if let Some(row) = mask_row {
18007 zero_masked_cols(&mut g, b, inter, row);
18008 }
18009 if oracle_topk() > 0 {
18010 for t in 0..b {
18011 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18012 }
18013 }
18014 let mut out = vec![0.0f32; b * hidden];
18015 d.down_proj.matmat(&g, b, &mut out, pool);
18016 if refit_dir().is_some() {
18017 let li = crate::gpu::cur_layer();
18018 if li >= 0 {
18019 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18020 }
18021 }
18022 FFN_PROBE.with(|pr| {
18026 if let Some(acc) = pr.borrow_mut().as_mut() {
18027 let li = crate::gpu::cur_layer();
18028 if li < 0 {
18029 return;
18030 }
18031 let Some(row) = acc.get_mut(li as usize) else {
18032 return;
18033 };
18034 let sq = probe_sq();
18035 for t in 0..b {
18036 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18037 *a += if sq {
18038 (v as f64) * (v as f64)
18039 } else {
18040 (v as f64).abs()
18041 };
18042 }
18043 }
18044 }
18045 });
18046 out
18047}
18048
18049fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18054 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18055 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18056 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18057 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18058 if (!on && !dump) || b == 0 {
18059 return;
18060 }
18061 let hidden = xs.len() / b;
18062 if on {
18063 let mut acc = m.act_sq.borrow_mut();
18064 if acc.len() < hidden {
18065 acc.resize(hidden, 0.0);
18066 }
18067 for t in 0..b {
18068 let row = &xs[t * hidden..(t + 1) * hidden];
18069 for (a, &v) in acc.iter_mut().zip(row) {
18070 *a += (v as f64) * (v as f64);
18071 }
18072 }
18073 }
18074 if dump {
18075 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18078 .ok()
18079 .and_then(|v| v.parse().ok())
18080 .unwrap_or(4096);
18081 let mut rows = m.act_rows.borrow_mut();
18082 if rows.len() < cap * hidden {
18083 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18084 rows.extend_from_slice(&xs[..take * hidden]);
18085 }
18086 }
18087}
18088
18089#[derive(Clone, Copy)]
18092struct SendVecs(*mut Vec<f32>);
18093unsafe impl Send for SendVecs {}
18094unsafe impl Sync for SendVecs {}
18095impl SendVecs {
18096 #[inline]
18097 fn at(self, i: usize) -> *mut Vec<f32> {
18098 unsafe { self.0.add(i) }
18099 }
18100}
18101
18102fn moe_ffn_batch(
18103 m: &MoeFfn,
18104 xs: &[f32],
18105 b: usize,
18106 hidden: usize,
18107 pool: Option<&Pool>,
18108 allowed: Option<&[bool]>,
18109) -> Vec<f32> {
18110 accumulate_act(m, xs, b);
18111 let ne = m.experts.len();
18112 let mut logits = vec![0.0f32; b * ne];
18113 match &m.resonance {
18114 Some(r) => {
18115 let hdim = xs.len() / b.max(1);
18116 for bi in 0..b {
18117 r.scores(
18118 &xs[bi * hdim..(bi + 1) * hdim],
18119 &mut logits[bi * ne..(bi + 1) * ne],
18120 );
18121 }
18122 }
18123 None => m.router.matmat(xs, b, &mut logits, pool),
18124 }
18125
18126 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18129 {
18130 let mut st = m.stats.borrow_mut();
18131 if st.len() < ne {
18132 st.resize(ne, 0);
18133 }
18134 for bi in 0..b {
18135 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18136 for &e in &idx {
18137 st[e] += 1;
18138 assign[e].push((bi, p[e] / wsum));
18139 }
18140 }
18141 }
18142
18143 let mut out = vec![0.0f32; b * hidden];
18144 let cols = m.experts[0].gate_proj.cols();
18145 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18146 let sb = list.len();
18147 let mut sub = vec![0.0f32; sb * cols];
18148 for (k, &(bi, _)) in list.iter().enumerate() {
18149 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18150 }
18151 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18152 for (k, &(bi, w)) in list.iter().enumerate() {
18153 for i in 0..hidden {
18154 out[bi * hidden + i] += w * eo[k * hidden + i];
18155 }
18156 }
18157 };
18158 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18164 if pool.is_some() && active.len() >= 8 {
18165 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18166 {
18167 let panel_ptr = SendVecs(panels.as_mut_ptr());
18168 let experts = &m.experts;
18171 let (active_r, assign_r) = (&active, &assign);
18172 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18173 let run = |start: usize, end: usize| {
18174 let _cpu_scope = inherit_cpu();
18175 #[cfg(target_os = "macos")]
18183 let _metal_cpu = crate::gpu::enter_cpu_scope();
18184 for ai in start..end {
18185 let e = active_r[ai];
18186 let list = &assign_r[e];
18187 let sb = list.len();
18188 let mut sub = vec![0.0f32; sb * cols];
18189 for (k, &(bi, _)) in list.iter().enumerate() {
18190 sub[k * cols..(k + 1) * cols]
18191 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18192 }
18193 unsafe {
18195 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18196 }
18197 }
18198 };
18199 match pool {
18200 Some(p) => p.run_rows(active.len(), &run),
18201 None => run(0, active.len()),
18202 }
18203 }
18204 for (ai, &e) in active.iter().enumerate() {
18205 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18206 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18207 for i in 0..hidden {
18208 out[bi * hidden + i] += w * eo[i];
18209 }
18210 }
18211 }
18212 } else {
18213 for &e in &active {
18214 run_expert(&m.experts[e], &assign[e], &mut out);
18215 }
18216 }
18217 if let Some((se, gate)) = &m.shared {
18218 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18219 let mut gl = vec![0.0f32; b];
18220 gate.matmat(xs, b, &mut gl, pool);
18221 (0..b)
18222 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18223 .collect()
18224 } else {
18225 (0..b).map(|bi| (bi, 1.0)).collect()
18226 };
18227 run_expert(se, &all, &mut out);
18228 }
18229 out
18230}
18231
18232fn moe_ffn_rows_exact(
18244 m: &MoeFfn,
18245 xs: &[f32],
18246 b: usize,
18247 hidden: usize,
18248 pool: Option<&Pool>,
18249) -> Vec<f32> {
18250 let mut out = vec![0.0f32; b * hidden];
18251 let per_row = |out: &mut [f32]| {
18252 for r in 0..b {
18253 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18254 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18255 }
18256 };
18257 let covered = !crate::gpu::enabled_here()
18258 && moe_batch_enabled()
18259 && m.shared.is_none()
18260 && m.resonance.is_none()
18261 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18262 && m.experts.iter().all(|d| d.act == Act::Silu);
18263 if !covered {
18264 per_row(&mut out);
18265 return out;
18266 }
18267 let ne = m.experts.len();
18268 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18270 for r in 0..b {
18271 let x = &xs[r * hidden..(r + 1) * hidden];
18272 accumulate_act(m, x, 1);
18273 let mut logits = vec![0.0f32; ne];
18274 m.router.matvec(x, &mut logits, pool);
18275 let (idx, p, wsum) = moe_route(&logits, m, None);
18276 {
18277 let mut st = m.stats.borrow_mut();
18278 if st.len() < ne {
18279 st.resize(ne, 0);
18280 }
18281 for &e in &idx {
18282 st[e] += 1;
18283 }
18284 }
18285 let w: Vec<f32> = idx
18286 .iter()
18287 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18288 .collect();
18289 routes.push((idx, w));
18290 }
18291 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18292 per_row(&mut out);
18293 return out;
18294 }
18295 let mut experts: Vec<usize> = Vec::new();
18297 let mut groups: Vec<Vec<usize>> = Vec::new();
18298 for (r, (idx, _)) in routes.iter().enumerate() {
18299 for &e in idx {
18300 match experts.iter().position(|&x| x == e) {
18301 Some(g) => groups[g].push(r),
18302 None => {
18303 experts.push(e);
18304 groups.push(vec![r]);
18305 }
18306 }
18307 }
18308 }
18309 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
18310 let inter = m.experts[experts[0]].gate_proj.rows();
18311 let pairs: Vec<(&QTensor, &QTensor)> = experts
18312 .iter()
18313 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
18314 .collect();
18315 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
18316 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
18317 per_row(&mut out);
18318 return out;
18319 }
18320 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
18321 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
18322 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
18323 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
18324 per_row(&mut out);
18325 return out;
18326 }
18327 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
18329 let mut p = 0usize;
18330 for (g, &e) in experts.iter().enumerate() {
18331 for &r in &groups[g] {
18332 slot.insert((r, e), p);
18333 p += 1;
18334 }
18335 }
18336 for (r, (idx, w)) in routes.iter().enumerate() {
18337 let terms: Vec<(&[f32], f32)> = idx
18338 .iter()
18339 .zip(w)
18340 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
18341 .collect();
18342 let row = &mut out[r * hidden..(r + 1) * hidden];
18343 for (i, dst) in row.iter_mut().enumerate() {
18344 let mut acc = 0f32;
18346 for (d, we) in &terms {
18347 acc += we * d[i];
18348 }
18349 *dst = acc;
18350 }
18351 }
18352 out
18353}
18354
18355pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
18359 match pool {
18360 Some(p) if v.len() >= 1 << 15 => {
18361 let base = v.as_mut_ptr() as usize;
18362 p.run_rows(v.len(), &|s, e| {
18363 let sl =
18366 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
18367 for x in sl {
18368 *x = c * (*x / c).tanh();
18369 }
18370 });
18371 }
18372 _ => {
18373 for x in v.iter_mut() {
18374 *x = c * (*x / c).tanh();
18375 }
18376 }
18377 }
18378}
18379
18380thread_local! {
18381 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
18385 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
18386}
18387
18388fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18390 if gate_topk() > 0
18393 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
18394 {
18395 return out;
18396 }
18397 let prism_body = d.gate_proj.has_prism_contract()
18413 || d.up_proj.has_prism_contract()
18414 || d.down_proj.has_prism_contract();
18415 if !prism_body
18416 && crate::gpu::enabled_here()
18417 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
18418 {
18419 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
18420 crate::gpu::ProbeArm::Gpu
18421 } else {
18422 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
18423 };
18424 match arm {
18425 crate::gpu::ProbeArm::Gpu => {
18426 let t0 = std::time::Instant::now();
18427 if let Some(out) = dense_ffn_gpu(d, x, pool) {
18428 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18429 return out;
18430 }
18431 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
18435 }
18436 crate::gpu::ProbeArm::CpuTimed => {
18437 let t0 = std::time::Instant::now();
18438 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18439 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18440 return out;
18441 }
18442 crate::gpu::ProbeArm::Cpu => {
18443 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18444 }
18445 }
18446 }
18447 dense_ffn_cpu(d, x, pool)
18448}
18449
18450fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18452 let inter = d.gate_proj.rows();
18453 FFN_SCRATCH.with(|s| {
18454 let mut s = s.borrow_mut();
18455 let [g, u, ..] = &mut *s;
18456 g.resize(inter, 0.0);
18457 if gate_topk() > 0 {
18460 u.resize(inter, 0.0);
18464 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18465 for i in 0..inter {
18466 g[i] = Act::Silu.combine(g[i], 1.0);
18467 }
18468 keep_top_k(g, gate_topk());
18469 for i in 0..inter {
18470 g[i] *= u[i];
18471 }
18472 } else if d.act == Act::Silu && {
18473 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18474 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
18475 } {
18476 } else {
18478 u.resize(inter, 0.0);
18479 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18481 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18482 for i in 0..inter {
18483 g[i] = d.act.combine(g[i], u[i]);
18484 }
18485 }
18486 FFN_PROBE.with(|pr| {
18494 if let Some(acc) = pr.borrow_mut().as_mut() {
18495 let li = crate::gpu::cur_layer();
18496 if li >= 0 {
18497 if let Some(row) = acc.get_mut(li as usize) {
18498 match probe_topk() {
18499 0 if probe_sq() => {
18500 for (a, &v) in row.iter_mut().zip(g.iter()) {
18501 *a += (v as f64) * (v as f64);
18502 }
18503 }
18504 0 if probe_signed() => {
18505 for (a, &v) in row.iter_mut().zip(g.iter()) {
18506 *a += v as f64;
18507 }
18508 }
18509 0 => {
18510 for (a, &v) in row.iter_mut().zip(g.iter()) {
18511 *a += (v as f64).abs();
18512 }
18513 }
18514 k => {
18515 let n = g.len();
18516 let k = k.min(n);
18517 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18518 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18519 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18520 });
18521 let thr = *kth;
18522 for (a, &v) in row.iter_mut().zip(g.iter()) {
18523 if v.abs() >= thr {
18524 *a += 1.0;
18525 }
18526 }
18527 }
18528 }
18529 }
18530 }
18531 }
18532 });
18533 if oracle_topk() > 0 {
18534 keep_top_k(g, oracle_topk());
18535 }
18536 {
18537 let li = crate::gpu::cur_layer();
18538 if li >= 0 {
18539 adump_row(li as usize, g);
18540 }
18541 }
18542 let mut out = attention::take_buf(d.down_proj.rows());
18543 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
18544 d.down_proj.matvec(g, &mut out, pool);
18545 out
18546 })
18547}
18548
18549pub struct RefitAcc {
18562 pub support: Vec<u32>,
18563 pub gss: Vec<f32>,
18564 pub ya: Vec<f32>,
18565 pub hidden: usize,
18566 pub tokens: u64,
18567 pub buf_g: Vec<f32>,
18573 pub buf_o: Vec<f32>,
18574 pub buf_t: usize,
18575}
18576
18577type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
18581
18582static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
18583 std::sync::OnceLock::new();
18584
18585fn ffn_probe_active() -> bool {
18588 FFN_PROBE.with(|p| p.borrow().is_some())
18589}
18590
18591fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
18592 REFIT
18593 .get_or_init(|| {
18594 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
18595 (
18596 d,
18597 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
18598 )
18599 })
18600 })
18601 .as_ref()
18602}
18603
18604fn refit_accumulate(
18606 li: usize,
18607 g: &[f32],
18608 b: usize,
18609 inter: usize,
18610 out: &[f32],
18611 hidden: usize,
18612 pool: Option<&Pool>,
18613) {
18614 let Some((dir, map)) = refit_dir() else {
18615 return;
18616 };
18617 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18618 let (from, to) = *SPAN.get_or_init(|| {
18619 let g = |k: &str, d: usize| {
18620 std::env::var(k)
18621 .ok()
18622 .and_then(|v| v.parse().ok())
18623 .unwrap_or(d)
18624 };
18625 (
18626 g("CMF_FFN_REFIT_FROM", 0),
18627 g("CMF_FFN_REFIT_TO", usize::MAX),
18628 )
18629 });
18630 if li < from || li > to {
18631 return;
18632 }
18633 let mut guard = map.lock().unwrap();
18634 let (map, shared) = &mut *guard;
18635 let acc = match map.entry(li) {
18636 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
18637 std::collections::hash_map::Entry::Vacant(e) => {
18638 let path = format!("{dir}/support.{li}.u32");
18639 let Ok(bytes) = std::fs::read(&path) else {
18640 eprintln!("refit: no {path} — layer {li} skipped");
18641 return;
18642 };
18643 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
18644 let support: Vec<u32> = bytes[4..4 + n * 4]
18645 .chunks_exact(4)
18646 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18647 .collect();
18648 eprintln!(
18649 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
18650 (n * n + hidden * n) as f64 * 4.0 / 1e6
18651 );
18652 e.insert(RefitAcc {
18653 gss: vec![0.0; n * n],
18654 ya: vec![0.0; hidden * n],
18655 buf_g: Vec::new(),
18656 buf_o: Vec::new(),
18657 buf_t: 0,
18658 support,
18659 hidden,
18660 tokens: 0,
18661 })
18662 }
18663 };
18664 let ns = acc.support.len();
18665 let cap = refit_batch();
18667 if acc.buf_g.is_empty() {
18668 acc.buf_g = vec![0.0; ns * cap];
18669 acc.buf_o = vec![0.0; hidden * cap];
18670 }
18671 let take = b.min(cap - acc.buf_t);
18672 for t in 0..take {
18673 let col = acc.buf_t + t;
18674 for (j, &n) in acc.support.iter().enumerate() {
18675 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
18676 }
18677 for h in 0..hidden {
18678 acc.buf_o[h * cap + col] = out[t * hidden + h];
18679 }
18680 }
18681 acc.buf_t += take;
18682 acc.tokens += take as u64;
18683 if acc.buf_t < cap {
18684 return;
18685 }
18686 let bt = acc.buf_t;
18687 acc.buf_t = 0;
18688 let RefitAcc {
18698 gss,
18699 ya,
18700 buf_g,
18701 buf_o,
18702 ..
18703 } = acc;
18704 let need = (ns * ns).max(hidden * ns);
18705 if shared.len() < need {
18706 shared.resize(need, 0.0);
18707 }
18708 let scratch = &mut shared[..];
18709 let _ = bt;
18710 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
18711 add_into(gss, &scratch[..ns * ns], pool);
18712 if crate::gpu::gemm_nt_f32_transient(
18713 buf_o,
18714 buf_g,
18715 &mut scratch[..hidden * ns],
18716 hidden,
18717 cap,
18718 ns,
18719 ) {
18720 add_into(ya, &scratch[..hidden * ns], pool);
18721 } else {
18722 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18723 }
18724 } else {
18725 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
18726 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18727 }
18728 }
18732
18733fn refit_batch() -> usize {
18735 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18736 *B.get_or_init(|| {
18737 std::env::var("CMF_FFN_REFIT_BATCH")
18738 .ok()
18739 .and_then(|v| v.parse().ok())
18740 .unwrap_or(4096)
18741 })
18742}
18743
18744fn accum_outer_t(
18747 c: &mut [f32],
18748 m: usize,
18749 n: usize,
18750 b: usize,
18751 left: &[f32],
18752 right: &[f32],
18753 pool: Option<&Pool>,
18754) {
18755 let ptr = SendMut(c.as_mut_ptr());
18756 let body = |i: usize| {
18757 let ptr = &ptr;
18758 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18759 for t in 0..b {
18760 let a = left[i * b + t];
18761 if a == 0.0 {
18762 continue;
18763 }
18764 for (j, o) in row.iter_mut().enumerate() {
18765 *o += a * right[j * b + t];
18766 }
18767 }
18768 };
18769 match pool {
18770 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18771 for i in s..e {
18772 body(i);
18773 }
18774 }),
18775 _ => {
18776 for i in 0..m {
18777 body(i);
18778 }
18779 }
18780 }
18781}
18782
18783fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18786 let n = dst.len().min(src.len());
18787 match pool {
18788 Some(p) if n >= 1 << 16 => {
18789 let ptr = SendMut(dst.as_mut_ptr());
18790 let f = |s: usize, e: usize| {
18791 let ptr = &ptr;
18792 for blk in s..e {
18793 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18794 for i in a..b {
18795 unsafe { *ptr.0.add(i) += src[i] };
18796 }
18797 }
18798 };
18799 p.run_rows(n.div_ceil(4096), &f);
18800 }
18801 _ => {
18802 for (d, v) in dst.iter_mut().zip(&src[..n]) {
18803 *d += *v;
18804 }
18805 }
18806 }
18807}
18808
18809fn accum_outer(
18814 c: &mut [f32],
18815 m: usize,
18816 n: usize,
18817 b: usize,
18818 left: &[f32],
18819 right: &[f32],
18820 pool: Option<&Pool>,
18821) {
18822 const TILE: usize = 32;
18823 let tiles = m.div_ceil(TILE);
18824 let cp = SendMut(c.as_mut_ptr());
18825 let body = |ti: usize| {
18826 let cp = &cp;
18827 let i0 = ti * TILE;
18828 let i1 = (i0 + TILE).min(m);
18829 for t in 0..b {
18830 let r = &right[t * n..t * n + n];
18831 for i in i0..i1 {
18832 let a = left[i * b + t];
18833 if a == 0.0 {
18834 continue;
18835 }
18836 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18838 for (o, v) in row.iter_mut().zip(r) {
18839 *o += a * *v;
18840 }
18841 }
18842 }
18843 };
18844 match pool {
18845 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18846 for ti in s..e {
18847 body(ti);
18848 }
18849 }),
18850 _ => {
18851 for ti in 0..tiles {
18852 body(ti);
18853 }
18854 }
18855 }
18856}
18857
18858pub fn refit_flush() -> usize {
18860 let Some((dir, map)) = refit_dir() else {
18861 return 0;
18862 };
18863 let guard = map.lock().unwrap();
18864 let mut n = 0;
18865 for (li, acc) in guard.0.iter() {
18866 let w = |name: &str, v: &[f32]| {
18869 let path = format!("{dir}/{name}.{li}.f32");
18870 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18871 match std::fs::write(&path, &bytes) {
18872 Ok(()) => {}
18873 Err(e) => eprintln!(
18874 "refit: FAILED to write {path} ({} MB): {e}",
18875 bytes.len() / 1_000_000
18876 ),
18877 }
18878 };
18879 w("gss", &acc.gss);
18880 w("ya", &acc.ya);
18881 println!(
18882 "refit L{li}: {} support, {} tokens, hidden {}",
18883 acc.support.len(),
18884 acc.tokens,
18885 acc.hidden
18886 );
18887 n += 1;
18888 }
18889 n
18890}
18891
18892fn adump_row(li: usize, g: &[f32]) {
18897 use std::io::Write as _;
18898 static FILES: std::sync::OnceLock<
18899 Option<(
18900 String,
18901 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18902 )>,
18903 > = std::sync::OnceLock::new();
18904 let Some((prefix, map)) = FILES
18905 .get_or_init(|| {
18906 std::env::var("CMF_FFN_ADUMP")
18907 .ok()
18908 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18909 })
18910 .as_ref()
18911 else {
18912 return;
18913 };
18914 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18917 let (from, to) = *SPAN.get_or_init(|| {
18918 let g = |k: &str, d: usize| {
18919 std::env::var(k)
18920 .ok()
18921 .and_then(|v| v.parse().ok())
18922 .unwrap_or(d)
18923 };
18924 (
18925 g("CMF_FFN_ADUMP_FROM", 0),
18926 g("CMF_FFN_ADUMP_TO", usize::MAX),
18927 )
18928 });
18929 if li < from || li > to {
18930 return;
18931 }
18932 let mut map = map.lock().unwrap();
18933 let f = map.entry(li).or_insert_with(|| {
18934 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18935 });
18936 let mut bytes = Vec::with_capacity(g.len() * 2);
18937 for v in g {
18938 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18939 }
18940 let _ = f.write_all(&bytes);
18941}
18942
18943fn oracle_topk() -> usize {
18949 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18950 *K.get_or_init(|| {
18951 std::env::var("CMF_FFN_ORACLE_TOPK")
18952 .ok()
18953 .and_then(|v| v.parse().ok())
18954 .unwrap_or(0)
18955 })
18956}
18957
18958fn gate_topk() -> usize {
18964 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18965 *K.get_or_init(|| {
18966 std::env::var("CMF_FFN_GATE_TOPK")
18967 .ok()
18968 .and_then(|v| v.parse().ok())
18969 .unwrap_or(0)
18970 })
18971}
18972
18973fn gate_block() -> usize {
18980 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18981 *B.get_or_init(|| {
18982 std::env::var("CMF_FFN_GATE_BLOCK")
18983 .ok()
18984 .and_then(|v| v.parse().ok())
18985 .unwrap_or(1)
18986 })
18987}
18988
18989fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18991 let n = g.len();
18992 let nb = n.div_ceil(block);
18993 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18994 if kb >= nb {
18995 return;
18996 }
18997 let mut score: Vec<f32> = (0..nb)
18998 .map(|b| {
18999 g[b * block..((b + 1) * block).min(n)]
19000 .iter()
19001 .map(|v| v * v)
19002 .sum::<f32>()
19003 })
19004 .collect();
19005 let mut ord = score.clone();
19006 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19007 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19008 });
19009 let thr = *kth;
19010 for b in 0..nb {
19011 if score[b] < thr {
19012 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19013 }
19014 }
19015 score.clear();
19016}
19017
19018fn keep_top_k(g: &mut [f32], k: usize) {
19020 if gate_block() > 1 {
19021 return keep_top_blocks(g, k, gate_block());
19022 }
19023 let n = g.len();
19024 if k == 0 || k >= n {
19025 return;
19026 }
19027 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19028 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19029 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19030 });
19031 let thr = *kth;
19032 for v in g.iter_mut() {
19033 if v.abs() < thr {
19034 *v = 0.0;
19035 }
19036 }
19037}
19038
19039fn probe_sq() -> bool {
19043 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19044 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19045}
19046
19047fn probe_signed() -> bool {
19051 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19052 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19053}
19054
19055fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19063 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19064 M.get_or_init(|| {
19065 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19066 let b = std::fs::read(&p).ok()?;
19067 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19068 let vals: Vec<f32> = b[8..]
19069 .chunks_exact(4)
19070 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19071 .collect();
19072 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19073 Some((inter, vals))
19074 })
19075 .as_ref()
19076}
19077
19078fn probe_topk() -> usize {
19081 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19082 *K.get_or_init(|| {
19083 std::env::var("CMF_FFN_PROBE_TOPK")
19084 .ok()
19085 .and_then(|v| v.parse().ok())
19086 .unwrap_or(0)
19087 })
19088}
19089
19090thread_local! {
19091 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19094 const { std::cell::RefCell::new(None) };
19095}
19096
19097fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19110 if d.gate_proj.has_prism_contract()
19115 || d.up_proj.has_prism_contract()
19116 || d.down_proj.has_prism_contract()
19117 {
19118 return None;
19119 }
19120 let dt = d.down_t.as_ref()?;
19121 let inter = d.gate_proj.rows();
19122 let hidden = dt.cols();
19123 if k == 0 || k >= inter || d.act != Act::Silu {
19124 return None;
19125 }
19126 DYN_SCRATCH.with(|sc| {
19127 let mut sc = sc.borrow_mut();
19128 let DynScratch {
19129 g,
19130 mag,
19131 live,
19132 parts,
19133 } = &mut *sc;
19134 g.resize(inter, 0.0);
19135 d.gate_proj.matvec(x, g, pool);
19136 for v in g.iter_mut() {
19137 *v = inference::silu(*v);
19138 }
19139 mag.clear();
19142 mag.extend(g.iter().map(|v| v.abs()));
19143 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19144 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19145 });
19146 let thr = *kth;
19147 live.clear();
19148 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19149 let mut out = vec![0.0f32; hidden];
19150 match pool {
19151 Some(p) if live.len() >= 64 => {
19152 let nw = p.n_workers() + 1;
19153 parts.clear();
19154 parts.resize(nw * hidden, 0.0);
19155 let ptr = SendMut(parts.as_mut_ptr());
19156 let n = live.len();
19157 let live_ref: &[u32] = live;
19158 let g_ref: &[f32] = g;
19159 p.run(&|w, workers| {
19160 let chunk = n.div_ceil(workers);
19161 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19162 if s >= e {
19163 return;
19164 }
19165 WORKER_SCRATCH.with(|ws| {
19166 let mut ws = ws.borrow_mut();
19167 let [scratch, acc] = &mut *ws;
19168 scratch.resize(hidden.max(x.len()), 0.0);
19169 acc.clear();
19170 acc.resize(hidden, 0.0);
19171 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19172 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19175 d.up_proj.prefetch_row(nx as usize);
19176 dt.prefetch_row(nx as usize);
19177 }
19178 let idx = nrm as usize;
19179 let up = d.up_proj.row_dot(idx, x, scratch);
19180 let a = g_ref[idx] * up;
19181 if a != 0.0 {
19182 dt.add_row_scaled(idx, a, acc, scratch);
19183 }
19184 }
19185 for (j, v) in acc.iter().enumerate() {
19186 unsafe { *ptr.at(w * hidden + j) = *v };
19187 }
19188 });
19189 });
19190 for w in 0..nw {
19191 for (j, o) in out.iter_mut().enumerate() {
19192 *o += parts[w * hidden + j];
19193 }
19194 }
19195 }
19196 _ => {
19197 WORKER_SCRATCH.with(|ws| {
19198 let mut ws = ws.borrow_mut();
19199 let [scratch, _acc] = &mut *ws;
19200 scratch.resize(hidden.max(x.len()), 0.0);
19201 for &nrm in live.iter() {
19202 let idx = nrm as usize;
19203 let up = d.up_proj.row_dot(idx, x, scratch);
19204 let a = g[idx] * up;
19205 if a != 0.0 {
19206 dt.add_row_scaled(idx, a, &mut out, scratch);
19207 }
19208 }
19209 });
19210 }
19211 }
19212 Some(out)
19213 })
19214}
19215
19216struct DynScratch {
19219 g: Vec<f32>,
19220 mag: Vec<f32>,
19221 live: Vec<u32>,
19222 parts: Vec<f32>,
19223}
19224
19225thread_local! {
19226 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19227 std::cell::RefCell::new(DynScratch {
19228 g: Vec::new(),
19229 mag: Vec::new(),
19230 live: Vec::new(),
19231 parts: Vec::new(),
19232 })
19233 };
19234 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19236 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19237}
19238
19239fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19244 let inter = d.gate_proj.rows();
19245 FFN_SCRATCH.with(|s| {
19246 let mut s = s.borrow_mut();
19247 let [g, u, ..] = &mut *s;
19248 g.resize(inter, 0.0);
19249 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19250 } else {
19252 u.resize(inter, 0.0);
19253 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19254 for i in 0..inter {
19255 g[i] = d.act.combine(g[i], u[i]);
19256 }
19257 }
19258 zero_masked_cols(g, 1, inter, mask_row);
19259 let mut out = attention::take_buf(d.down_proj.rows());
19260 d.down_proj.matvec(g, &mut out, pool);
19261 out
19262 })
19263}
19264
19265fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19271 if d.gate_proj.has_prism_contract()
19272 || d.up_proj.has_prism_contract()
19273 || d.down_proj.has_prism_contract()
19274 {
19275 return None;
19276 }
19277 d.act.moe_job_graph_contract()?;
19280 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19283 return None;
19284 }
19285 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19286 let mut model_ref = None;
19287 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19288 let model = model_ref?;
19289 let hidden = jobs[0].down.1;
19290 let mut out = attention::take_buf(hidden);
19291 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19292 Some(out)
19293 } else {
19294 let mut out = out;
19295 attention::recycle_buf(&mut out);
19296 None
19297 }
19298}
19299
19300#[allow(clippy::type_complexity)]
19305#[allow(clippy::type_complexity)]
19306pub(crate) fn moe_parts(
19307 t: &QTensor,
19308) -> Option<(
19309 &std::sync::Arc<cortiq_core::CmfModel>,
19310 usize,
19311 usize,
19312 usize,
19313 &[f32],
19314 &[f32],
19315 bool,
19316 bool,
19317 bool,
19318)> {
19319 match t {
19320 QTensor::Mapped {
19321 model,
19322 idx,
19323 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
19324 rows,
19325 cols,
19326 row_scale,
19327 col_field,
19328 ..
19329 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
19330 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
19331 )),
19332 QTensor::Mapped {
19334 model,
19335 idx,
19336 dtype: cortiq_core::TensorDtype::Q1,
19337 rows,
19338 cols,
19339 ..
19340 } => Some((
19341 model,
19342 *idx,
19343 *rows,
19344 *cols,
19345 &[][..],
19346 &[][..],
19347 true,
19348 false,
19349 false,
19350 )),
19351 QTensor::Mapped {
19353 model,
19354 idx,
19355 dtype: cortiq_core::TensorDtype::Q4Tiled,
19356 rows,
19357 cols,
19358 ..
19359 } => Some((
19360 model,
19361 *idx,
19362 *rows,
19363 *cols,
19364 &[][..],
19365 &[][..],
19366 false,
19367 true,
19368 false,
19369 )),
19370 QTensor::Mapped {
19372 model,
19373 idx,
19374 dtype: cortiq_core::TensorDtype::Q4TiledP,
19375 rows,
19376 cols,
19377 ..
19378 } => Some((
19379 model,
19380 *idx,
19381 *rows,
19382 *cols,
19383 &[][..],
19384 &[][..],
19385 false,
19386 true,
19387 false,
19388 )),
19389 QTensor::Mapped {
19393 model,
19394 idx,
19395 dtype: cortiq_core::TensorDtype::Q2TiledP,
19396 rows,
19397 cols,
19398 ..
19399 } => Some((
19400 model,
19401 *idx,
19402 *rows,
19403 *cols,
19404 &[][..],
19405 &[][..],
19406 false,
19407 true,
19408 true,
19409 )),
19410 _ => None,
19411 }
19412}
19413
19414#[cfg(target_os = "macos")]
19421#[allow(clippy::type_complexity)]
19422fn chunk_moe_parts(
19423 m: &MoeFfn,
19424 hidden: usize,
19425) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
19426 if m.shared.is_some()
19427 || m.per_expert_scale.is_some()
19428 || m.resonance.is_some()
19429 || m.route_tau.is_some()
19430 || m.router_input_norm
19431 || m.experts.is_empty()
19432 || m.top_k == 0
19433 || std::env::var("CMF_RMS_TRACE").is_ok()
19434 || std::env::var("CMF_ACT_DUMP").is_ok()
19435 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
19436 {
19437 return None;
19438 }
19439 let (rf, rr, rc) = m.router.f32_parts()?;
19440 if rr != m.experts.len() || rc != hidden {
19441 return None;
19442 }
19443 let inter = m.experts[0].gate_proj.rows();
19444 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
19447 let experts = m
19448 .experts
19449 .iter()
19450 .map(|e| {
19451 if e.act != Act::Silu
19452 || !e.segs.is_empty()
19453 || e.gate_proj.rows() != inter
19454 || e.up_proj.rows() != inter
19455 || e.down_proj.cols() != inter
19456 || e.gate_proj.cols() != hidden
19457 || e.down_proj.rows() != hidden
19458 {
19459 return None;
19460 }
19461 if q8 {
19462 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19463 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19464 }
19465 Some((
19466 e.gate_proj.mapped_q4tp()?.1,
19467 e.up_proj.mapped_q4tp()?.1,
19468 e.down_proj.mapped_q4tp()?.1,
19469 ))
19470 })
19471 .collect::<Option<Vec<_>>>()?;
19472 Some((rf, experts, q8))
19473}
19474
19475#[cfg(target_os = "macos")]
19488fn metal_flint_moe_graph_parts(
19489 m: &MoeFfn,
19490 hidden: usize,
19491 scratch_key: usize,
19492) -> Option<crate::gpu::GpuFlintMoe<'_>> {
19493 use std::sync::Arc;
19494
19495 const HIDDEN: usize = 2_048;
19496 const ROUTED_INTER: usize = 512;
19497 const SHARED_INTER: usize = 1_024;
19498 const EXPERTS: usize = 256;
19499 const TOP_K: usize = 6;
19500
19501 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
19502 return None;
19503 }
19504 let routed0 = m.experts.first()?;
19505 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
19506 let (router, router_rows, router_cols) = m.router.f32_parts()?;
19507 if router_rows != EXPERTS || router_cols != HIDDEN {
19508 return None;
19509 }
19510 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
19511 return None;
19512 }
19513 let same_q2 = |t: &QTensor| -> Option<usize> {
19514 let (owner, idx) = t.mapped_q2tp()?;
19515 Arc::ptr_eq(&owner, &model).then_some(idx)
19516 };
19517 let same_q4 = |t: &QTensor| -> Option<usize> {
19518 let (owner, idx) = t.mapped_q4tp()?;
19519 Arc::ptr_eq(&owner, &model).then_some(idx)
19520 };
19521 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19522 if e.act != Act::Silu
19523 || !e.segs.is_empty()
19524 || e.gate_proj.rows() != ROUTED_INTER
19525 || e.gate_proj.cols() != HIDDEN
19526 || e.up_proj.rows() != ROUTED_INTER
19527 || e.up_proj.cols() != HIDDEN
19528 || e.down_proj.rows() != HIDDEN
19529 || e.down_proj.cols() != ROUTED_INTER
19530 {
19531 return None;
19532 }
19533 Some((
19534 same_q2(&e.gate_proj)?,
19535 same_q2(&e.up_proj)?,
19536 same_q4(&e.down_proj)?,
19537 ))
19538 };
19539 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19540 if e.act != Act::Silu
19541 || !e.segs.is_empty()
19542 || e.gate_proj.rows() != SHARED_INTER
19543 || e.gate_proj.cols() != HIDDEN
19544 || e.up_proj.rows() != SHARED_INTER
19545 || e.up_proj.cols() != HIDDEN
19546 || e.down_proj.rows() != HIDDEN
19547 || e.down_proj.cols() != SHARED_INTER
19548 {
19549 return None;
19550 }
19551 Some((
19552 same_q4(&e.gate_proj)?,
19553 same_q4(&e.up_proj)?,
19554 same_q4(&e.down_proj)?,
19555 ))
19556 };
19557 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
19558 if experts.len() != EXPERTS {
19559 return None;
19560 }
19561 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
19562 if shared_gate.is_some() {
19563 return None;
19564 }
19565 Some(crate::gpu::GpuFlintMoe {
19566 router,
19567 bias: m.expert_bias.as_deref(),
19568 experts,
19569 scratch_key,
19570 shared: shared(shared_ffn)?,
19571 top_k: m.top_k,
19572 norm_topk: m.norm_topk_prob,
19573 route_scale: m.routed_scaling,
19574 sigmoid: m.router_sigmoid,
19575 sigmoid_denom_eps: m.sigmoid_denom_eps,
19576 })
19577}
19578
19579#[cfg(target_os = "macos")]
19580fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
19581 if m.router_input_norm
19582 || m.route_tau.is_some()
19583 || m.mask.is_some()
19584 || m.per_expert_scale.is_some()
19585 || m.experts.is_empty()
19586 || m.top_k == 0
19587 || m.resonance.is_some()
19588 {
19589 return None;
19590 }
19591 let (sh, sg) = match &m.shared {
19596 Some((sh, sg)) => (Some(sh), sg.as_ref()),
19597 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
19598 None => return None,
19599 };
19600 let (rf, rr, rc) = m.router.f32_parts()?;
19601 if rr != m.experts.len() || rc != hidden {
19602 return None;
19603 }
19604 let shared_gated = sg.is_some();
19605 let sf = match sg {
19606 Some(sg) => {
19607 let (sf, sr, sc) = sg.f32_parts()?;
19608 if sr * sc != hidden {
19609 return None;
19610 }
19611 sf
19612 }
19613 None => &rf[..hidden],
19616 };
19617 if let Some(b) = &m.expert_bias {
19618 if b.len() != m.experts.len() {
19619 return None;
19620 }
19621 }
19622 let inter = m.experts[0].gate_proj.rows();
19623 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
19626 let q8 = crate::gpu_metal::moe_q8::enabled()
19630 && m.shared.is_none()
19631 && m.experts[0].gate_proj.q8_2f_parts().is_some();
19632 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19633 if e.act != Act::Silu
19634 || e.gate_proj.rows() != inter
19635 || e.gate_proj.cols() != hidden
19636 || e.up_proj.rows() != inter
19637 || e.up_proj.cols() != hidden
19638 || e.down_proj.rows() != hidden
19639 || e.down_proj.cols() != inter
19640 {
19641 return None;
19642 }
19643 if q8 {
19644 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19645 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19646 }
19647 let pick = |t: &QTensor| -> Option<usize> {
19648 if gu_q2 {
19649 t.mapped_q2tp().map(|(_, i)| i)
19650 } else {
19651 t.mapped_q4tp().map(|(_, i)| i)
19652 }
19653 };
19654 Some((
19655 pick(&e.gate_proj)?,
19656 pick(&e.up_proj)?,
19657 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
19658 ))
19659 };
19660 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
19661 let shared = match sh {
19662 Some(sh) => Some(trio(sh)?),
19663 None => None,
19664 };
19665 Some(crate::gpu::GpuMoe {
19666 router: rf,
19667 sgate: sf,
19668 experts,
19669 shared,
19670 n_exp: m.experts.len(),
19671 top_k: m.top_k,
19672 inter,
19673 norm_topk: m.norm_topk_prob,
19674 route_scale: m.routed_scaling,
19675 gu_q2,
19676 q8,
19677 sigmoid: m.router_sigmoid,
19678 sigmoid_denom_eps: m.sigmoid_denom_eps,
19679 bias: m.expert_bias.as_deref(),
19680 shared_gated,
19681 })
19682}
19683
19684fn forced_ids_at(step: usize) -> Option<u32> {
19690 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
19691 IDS.get_or_init(|| {
19692 std::env::var("CMF_FORCE_IDS")
19693 .ok()
19694 .and_then(|p| std::fs::read_to_string(p).ok())
19695 .map(|s| {
19696 s.split_whitespace()
19697 .filter_map(|t| t.parse().ok())
19698 .collect()
19699 })
19700 .unwrap_or_default()
19701 })
19702 .get(step)
19703 .copied()
19704}
19705
19706pub(crate) fn moe_push_job_parts<'a>(
19710 gate: &'a QTensor,
19711 up: &'a QTensor,
19712 down: &'a QTensor,
19713 x: &[f32],
19714 w: f32,
19715 swiglu_limit: f32,
19716 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19717 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19718) -> Option<()> {
19719 use crate::qtensor::prescale;
19720 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
19721 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
19722 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
19723 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19724 return None; }
19726 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
19729 return None;
19730 }
19731 if !gq2 && dq2 {
19732 return None;
19733 }
19734 model_ref.get_or_insert_with(|| gm.clone());
19735 let dt = |cf: &[f32]| {
19736 if cf.is_empty() {
19737 cortiq_core::TensorDtype::Q8Row
19738 } else {
19739 cortiq_core::TensorDtype::Q8_2f
19740 }
19741 };
19742 jobs.push(crate::gpu::MoeJob {
19743 gate: (gi, gr, gc, grs),
19744 up: (ui, ur, uc, urs),
19745 down: (di, dr, dc, drs),
19746 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
19747 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
19748 down_col: dcf,
19749 w,
19750 q1: gq1,
19751 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
19752 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
19753 gu_q2: gq2,
19754 swiglu_limit,
19755 act: 0,
19756 });
19757 Some(())
19758}
19759
19760fn moe_push_job<'a>(
19762 d: &'a DenseFfn,
19763 x: &[f32],
19764 w: f32,
19765 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19766 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19767) -> Option<()> {
19768 use crate::qtensor::prescale;
19769 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
19774 let act = graph_act.code();
19775 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
19776 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
19777 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
19778 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19779 return None; }
19781 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
19782 return None;
19783 }
19784 if !gq2 && dq2 {
19785 return None;
19786 }
19787 model_ref.get_or_insert_with(|| gm.clone());
19788 let gdt = if gcf.is_empty() {
19789 cortiq_core::TensorDtype::Q8Row
19790 } else {
19791 cortiq_core::TensorDtype::Q8_2f
19792 };
19793 let udt = if ucf.is_empty() {
19794 cortiq_core::TensorDtype::Q8Row
19795 } else {
19796 cortiq_core::TensorDtype::Q8_2f
19797 };
19798 jobs.push(crate::gpu::MoeJob {
19799 gate: (gi, gr, gc, grs),
19800 up: (ui, ur, uc, urs),
19801 down: (di, dr, dc, drs),
19802 xs_gate: prescale(x, gcf, gdt).into_owned(),
19803 xs_up: prescale(x, ucf, udt).into_owned(),
19804 down_col: dcf,
19805 w,
19806 q1: gq1,
19807 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
19808 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
19809 gu_q2: gq2,
19810 swiglu_limit,
19811 act,
19812 });
19813 Some(())
19814}
19815
19816fn sparse_ffn_quant(
19823 d: &DenseFfn,
19824 x: &[f32],
19825 active: &[u16],
19826 hidden: usize,
19827 pool: Option<&Pool>,
19828) -> Vec<f32> {
19829 let n = active.len();
19830 let inter = d.gate_proj.rows();
19831 let mut act = vec![0.0f32; n];
19832 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
19835 let compute = |ai: usize| -> f32 {
19836 let idx = active[ai] as usize;
19837 if idx >= inter {
19838 return 0.0; }
19840 let mut s = if need_scratch {
19841 vec![0.0f32; hidden]
19842 } else {
19843 Vec::new()
19844 };
19845 let gate = d.gate_proj.row_dot(idx, x, &mut s);
19846 let up = d.up_proj.row_dot(idx, x, &mut s);
19847 d.act.combine(gate, up)
19848 };
19849 match pool {
19850 Some(p) if n >= 256 => {
19851 let ptr = SendMut(act.as_mut_ptr());
19852 p.run(&|widx, nw| {
19853 let chunk = n.div_ceil(nw);
19854 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19855 for ai in s..e {
19856 unsafe { *ptr.at(ai) = compute(ai) };
19857 }
19858 });
19859 }
19860 _ => {
19861 for (ai, a) in act.iter_mut().enumerate() {
19862 *a = compute(ai);
19863 }
19864 }
19865 }
19866 let mut out = vec![0.0f32; hidden];
19868 for (ai, &idx) in active.iter().enumerate() {
19869 let w = act[ai];
19870 if w.abs() >= 1e-12 && (idx as usize) < inter {
19871 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19872 }
19873 }
19874 out
19875}
19876
19877#[doc(hidden)]
19879pub fn sparse_ffn_quant_for_test(
19880 d: &DenseFfn,
19881 x: &[f32],
19882 active: &[u16],
19883 hidden: usize,
19884) -> Vec<f32> {
19885 sparse_ffn_quant(d, x, active, hidden, None)
19886}
19887
19888fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19892 let deq = |t: &QTensor| -> Vec<f32> {
19893 let (rows, cols) = (t.rows(), t.cols());
19894 let mut out = vec![0.0f32; rows * cols];
19895 for r in 0..rows {
19896 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19897 }
19898 out
19899 };
19900 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19901}
19902
19903struct SendMut(*mut f32);
19905unsafe impl Send for SendMut {}
19906unsafe impl Sync for SendMut {}
19907impl SendMut {
19908 #[inline]
19909 #[allow(clippy::mut_from_ref)]
19912 unsafe fn at(&self, i: usize) -> &mut f32 {
19913 unsafe { &mut *self.0.add(i) }
19914 }
19915}
19916
19917pub(crate) fn moe_route(
19929 logits: &[f32],
19930 m: &MoeFfn,
19931 allowed: Option<&[bool]>,
19932) -> (Vec<usize>, Vec<f32>, f32) {
19933 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
19934}
19935
19936pub(crate) fn moe_route_with_eps(
19946 logits: &[f32],
19947 m: &MoeFfn,
19948 allowed: Option<&[bool]>,
19949 sigmoid_denom_eps: f32,
19950) -> (Vec<usize>, Vec<f32>, f32) {
19951 let ne = logits.len();
19952 let admit = |e: usize| {
19958 m.mask.as_ref().is_none_or(|mk| mk[e])
19959 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19960 };
19961 if m.resonance.is_some() && m.top_k == 1 {
19974 let mut best: Option<usize> = None;
19975 for e in (0..ne).filter(|&e| admit(e)) {
19976 let l = logits[e];
19977 if l == f32::NEG_INFINITY || l.is_nan() {
19978 continue;
19979 }
19980 if best.is_none_or(|b| l > logits[b]) {
19981 best = Some(e);
19982 }
19983 }
19984 if let Some(b) = best {
19985 let mut p = vec![0.0f32; ne];
19986 p[b] = 1.0;
19987 return (vec![b], p, 1.0 / m.routed_scaling);
19988 }
19989 }
19990 let p: Vec<f32> = if m.router_sigmoid {
19996 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19997 } else {
19998 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19999 if mx == f32::NEG_INFINITY {
20000 vec![1.0 / ne.max(1) as f32; ne]
20001 } else {
20002 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20003 let s: f32 = e.iter().sum();
20004 for v in &mut e {
20005 *v /= s;
20006 }
20007 e
20008 }
20009 };
20010 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20011 match &m.expert_bias {
20013 Some(b) => idx.sort_unstable_by(|&x, &y| {
20014 (p[y] + b[y])
20015 .partial_cmp(&(p[x] + b[x]))
20016 .unwrap()
20017 .then(x.cmp(&y))
20018 }),
20019 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20020 }
20021 idx.truncate(m.top_k);
20022 if let Some(tau) = m.route_tau {
20026 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20027 if total > 0.0 {
20028 let mut acc = 0.0f32;
20029 let mut keep = idx.len();
20030 for (i, &e) in idx.iter().enumerate() {
20031 acc += p[e];
20032 if acc >= tau * total {
20033 keep = i + 1;
20034 break;
20035 }
20036 }
20037 idx.truncate(keep);
20038 }
20039 }
20040 let wsum: f32 = if m.norm_topk_prob {
20041 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20042 (if m.router_sigmoid {
20046 s + sigmoid_denom_eps
20047 } else {
20048 s
20049 }) / m.routed_scaling
20050 } else {
20051 1.0 / m.routed_scaling
20052 };
20053 (idx, p, wsum)
20054}
20055
20056fn moe_trace(idx: &[usize]) {
20058 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20059}
20060
20061pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20064 use std::io::Write;
20065 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20066 std::sync::OnceLock::new();
20067 let Some(f) = F.get_or_init(|| {
20068 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20069 Some(std::sync::Mutex::new(
20070 std::fs::OpenOptions::new()
20071 .create(true)
20072 .append(true)
20073 .open(p)
20074 .ok()?,
20075 ))
20076 }) else {
20077 return;
20078 };
20079 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20080 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20081}
20082
20083pub(crate) fn moe_ffn(
20086 m: &MoeFfn,
20087 x: &[f32],
20088 pool: Option<&Pool>,
20089 allowed: Option<&[bool]>,
20090) -> Vec<f32> {
20091 let r = moe_ffn_route(m, x, pool, allowed);
20092 moe_ffn_experts(m, x, &r, pool)
20093}
20094
20095pub(crate) struct MoeRoute {
20099 pub idx: Vec<usize>,
20100 pub p: Vec<f32>,
20101 pub wsum: f32,
20102 pub logits: Vec<f32>,
20103}
20104
20105pub(crate) fn moe_ffn_route(
20111 m: &MoeFfn,
20112 x: &[f32],
20113 pool: Option<&Pool>,
20114 allowed: Option<&[bool]>,
20115) -> MoeRoute {
20116 accumulate_act(m, x, 1);
20117 let ne = m.experts.len();
20118 let mut logits = vec![0.0f32; ne];
20119 match &m.resonance {
20120 Some(r) => r.scores(x, &mut logits),
20121 None => m.router.matvec(x, &mut logits, pool),
20122 }
20123 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20124 {
20125 let mut st = m.stats.borrow_mut();
20126 if st.len() < ne {
20127 st.resize(ne, 0);
20128 }
20129 for &e in &idx {
20130 st[e] += 1;
20131 }
20132 }
20133 moe_trace(&idx);
20139 MoeRoute {
20140 idx,
20141 p,
20142 wsum,
20143 logits,
20144 }
20145}
20146
20147#[cfg(target_os = "macos")]
20156fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20157 const HIDDEN: usize = 2_048;
20158 const ROUTED_INTER: usize = 512;
20159 const SHARED_INTER: usize = 1_024;
20160 const EXPERTS: usize = 256;
20161 const TOP_K: usize = 6;
20162
20163 let Some(routed) = m.experts.first() else {
20164 return false;
20165 };
20166 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20167 return false;
20168 };
20169 let Some((shared, shared_gate)) = &m.shared else {
20170 return false;
20171 };
20172 if shared_gate.is_some()
20173 || model.header.arch.arch_name != "flint"
20174 || model.header.arch.prism_hadamard.is_some()
20175 || model.header.arch.hidden_size != HIDDEN
20176 || m.experts.len() != EXPERTS
20177 || m.top_k != TOP_K
20178 || !m.router_sigmoid
20179 || !m.norm_topk_prob
20180 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20181 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20182 || m.route_tau.is_some()
20183 || m.mask.is_some()
20184 || m.per_expert_scale.is_some()
20185 || m.router_input_norm
20186 || m.resonance.is_some()
20187 || !m.grown.is_empty()
20188 {
20189 return false;
20190 }
20191
20192 let routed_q2 = routed.act == Act::Silu
20193 && routed.segs.is_empty()
20194 && routed.gate_proj.rows() == ROUTED_INTER
20195 && routed.gate_proj.cols() == HIDDEN
20196 && routed.up_proj.rows() == ROUTED_INTER
20197 && routed.up_proj.cols() == HIDDEN
20198 && routed.down_proj.rows() == HIDDEN
20199 && routed.down_proj.cols() == ROUTED_INTER
20200 && routed.up_proj.mapped_q2tp().is_some()
20201 && routed.down_proj.mapped_q4tp().is_some();
20202 let shared_q4 = shared.act == Act::Silu
20203 && shared.segs.is_empty()
20204 && shared.gate_proj.rows() == SHARED_INTER
20205 && shared.gate_proj.cols() == HIDDEN
20206 && shared.up_proj.rows() == SHARED_INTER
20207 && shared.up_proj.cols() == HIDDEN
20208 && shared.down_proj.rows() == HIDDEN
20209 && shared.down_proj.cols() == SHARED_INTER
20210 && shared.gate_proj.mapped_q4tp().is_some()
20211 && shared.up_proj.mapped_q4tp().is_some()
20212 && shared.down_proj.mapped_q4tp().is_some();
20213 routed_q2 && shared_q4
20214}
20215
20216pub(crate) fn moe_ffn_experts(
20219 m: &MoeFfn,
20220 x: &[f32],
20221 r: &MoeRoute,
20222 pool: Option<&Pool>,
20223) -> Vec<f32> {
20224 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20225 if crate::gpu::enabled_here() {
20230 #[cfg(target_os = "macos")]
20231 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20232 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20238 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20239 });
20240 }
20241 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20242 crate::gpu::ProbeArm::Gpu => {
20243 let t0 = std::time::Instant::now();
20244 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20245 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20246 return out;
20247 }
20248 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20249 }
20250 crate::gpu::ProbeArm::CpuTimed => {
20251 let t0 = std::time::Instant::now();
20252 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20253 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20254 return out;
20255 }
20256 crate::gpu::ProbeArm::Cpu => {
20257 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20258 }
20259 }
20260 }
20261 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20262}
20263
20264fn moe_ffn_minicpm_tail(
20268 slot: &mut crate::minicpm_tail::MiniCpmTail,
20269 li: usize,
20270 total_layers: usize,
20271 m: &MoeFfn,
20272 x: &[f32],
20273 pool: Option<&Pool>,
20274) -> Vec<f32> {
20275 let route = moe_ffn_route(m, x, pool, None);
20276 match slot.forward(li, m, x, &route, pool, total_layers) {
20277 Some(out) => out,
20278 None => moe_ffn_experts(m, x, &route, pool),
20283 }
20284}
20285
20286fn moe_ffn_banked(
20290 slot: &mut crate::mimo_moe::Slot,
20291 li: usize,
20292 m: &MoeFfn,
20293 x: &[f32],
20294 pool: Option<&Pool>,
20295) -> Vec<f32> {
20296 let t0 = std::time::Instant::now();
20297 let r = moe_ffn_route(m, x, pool, None);
20298 slot.note_route(t0.elapsed().as_nanos() as u64);
20299 match slot.forward(li, m, x, &r, pool) {
20300 Some(out) => out,
20301 None => crate::qtensor::float_activations_scope(|| {
20302 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
20303 }),
20304 }
20305}
20306
20307fn moe_ffn_banked_rows(
20309 slot: &mut crate::mimo_moe::Slot,
20310 li: usize,
20311 m: &MoeFfn,
20312 xs: &[f32],
20313 b: usize,
20314 hidden: usize,
20315 pool: Option<&Pool>,
20316) -> Vec<f32> {
20317 let t0 = std::time::Instant::now();
20318 let routes: Vec<_> = xs
20319 .chunks_exact(hidden)
20320 .map(|x| moe_ffn_route(m, x, pool, None))
20321 .collect();
20322 slot.note_route(t0.elapsed().as_nanos() as u64);
20323 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
20324 return out;
20325 }
20326 let mut out = Vec::with_capacity(b * hidden);
20327 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
20328 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
20329 crate::qtensor::float_activations_scope(|| {
20331 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
20332 })
20333 });
20334 out.extend(row);
20335 }
20336 out
20337}
20338
20339fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
20344 use std::sync::atomic::{AtomicBool, Ordering};
20345 if built {
20346 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
20347 if total_layers > 0 && layers_run < total_layers {
20348 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
20349 } else {
20350 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
20351 }
20352 } else {
20353 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
20354 }
20355 static SAID: AtomicBool = AtomicBool::new(false);
20356 if !SAID.swap(true, Ordering::Relaxed) {
20357 if built {
20358 tracing::info!("wgpu whole-token graph: ACTIVE");
20359 } else {
20360 tracing::warn!("wgpu whole-token graph refused — per-op path");
20361 }
20362 }
20363}
20364
20365pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20369pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20370pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20374pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20376
20377pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20381pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20382pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
20383 std::sync::atomic::AtomicU64::new(0);
20384pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20385pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20386pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
20390 std::sync::atomic::AtomicU64::new(0);
20391pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20392pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
20393 std::sync::atomic::AtomicU64::new(0);
20394pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
20395 std::sync::atomic::AtomicU64::new(0);
20396
20397fn moe_batch_enabled() -> bool {
20400 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20401 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
20402}
20403
20404fn moe_ffn_cpu_batched(
20410 m: &MoeFfn,
20411 x: &[f32],
20412 idx: &[usize],
20413 p: &[f32],
20414 wsum: f32,
20415 pool: Option<&Pool>,
20416) -> Option<Vec<f32>> {
20417 if idx.is_empty() || !moe_batch_enabled() {
20418 return None;
20419 }
20420 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
20424 return None;
20425 }
20426 let n = idx.len() + usize::from(m.shared.is_some());
20427 let mut pairs = Vec::with_capacity(n);
20428 let mut downs = Vec::with_capacity(n);
20429 let mut ws = Vec::with_capacity(n);
20430 for &e in idx {
20431 let d = &m.experts[e];
20432 if d.act != Act::Silu {
20433 return None;
20434 }
20435 pairs.push((&d.gate_proj, &d.up_proj));
20436 downs.push(&d.down_proj);
20437 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
20438 }
20439 if let Some((se, gate)) = &m.shared {
20442 if se.act != Act::Silu {
20443 return None;
20444 }
20445 let g = gate.as_ref().map_or(1.0, |gate| {
20446 let mut gl = [0.0f32; 1];
20447 gate.matvec(x, &mut gl, pool);
20448 1.0 / (1.0 + (-gl[0]).exp())
20449 });
20450 pairs.push((&se.gate_proj, &se.up_proj));
20451 downs.push(&se.down_proj);
20452 ws.push(g);
20453 }
20454 let inter = pairs[0].0.rows();
20455 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
20456 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
20457 return None;
20458 }
20459 let mut out = attention::take_buf(x.len());
20460 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
20461 attention::recycle_buf(&mut out);
20462 return None;
20463 }
20464 Some(out)
20465}
20466
20467pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20475pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20476pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20477pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20478pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20479
20480pub(crate) fn moe_cold_experts_cpu(
20481 experts: &[(&DenseFfn, f32)],
20482 x: &[f32],
20483 pool: Option<&Pool>,
20484) -> Vec<f32> {
20485 let mut out = attention::take_buf(x.len());
20486 if experts.is_empty() {
20487 return out;
20488 }
20489 let pairs: Vec<_> = experts
20490 .iter()
20491 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
20492 .collect();
20493 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
20494 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
20495 let inter = experts[0].0.gate_proj.rows();
20496 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
20497 let silu_only = experts
20505 .iter()
20506 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
20507 if silu_only
20508 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
20509 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
20510 {
20511 return out;
20512 }
20513 out.fill(0.0);
20514 for &(expert, weight) in experts {
20515 let mut one = dense_ffn(expert, x, pool);
20516 for (o, v) in out.iter_mut().zip(&one) {
20517 *o += weight * v;
20518 }
20519 attention::recycle_buf(&mut one);
20520 }
20521 out
20522}
20523
20524pub(crate) fn moe_cold_experts_rows_cpu(
20528 jobs: &[Vec<(&DenseFfn, f32)>],
20529 xs: &[f32],
20530 hidden: usize,
20531 pool: Option<&Pool>,
20532) -> Vec<f32> {
20533 let mut out = vec![0.0; xs.len()];
20534 let mut experts: Vec<&DenseFfn> = Vec::new();
20535 let mut groups: Vec<Vec<usize>> = Vec::new();
20536 let mut terms = vec![Vec::new(); jobs.len()];
20537 for (r, row) in jobs.iter().enumerate() {
20538 for &(e, w) in row {
20539 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
20540 Some(g) => g,
20541 None => {
20542 experts.push(e);
20543 groups.push(Vec::new());
20544 groups.len() - 1
20545 }
20546 };
20547 terms[r].push((g, groups[g].len(), w));
20548 groups[g].push(r);
20549 }
20550 }
20551 if experts.is_empty() {
20552 return out;
20553 }
20554 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
20555 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
20556 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
20557 let count: usize = lens.iter().sum();
20558 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
20559 let mut ds = vec![vec![0.0; hidden]; count];
20560 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
20561 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
20562 {
20563 let mut offset = 0;
20564 let offsets: Vec<_> = lens
20565 .iter()
20566 .map(|&n| {
20567 let start = offset;
20568 offset += n;
20569 start
20570 })
20571 .collect();
20572 for (r, terms) in terms.iter().enumerate() {
20573 for &(g, slot, w) in terms {
20574 for (o, &v) in out[r * hidden..(r + 1) * hidden]
20575 .iter_mut()
20576 .zip(&ds[offsets[g] + slot])
20577 {
20578 *o += w * v;
20579 }
20580 }
20581 }
20582 } else {
20583 for (r, jobs) in jobs.iter().enumerate() {
20584 if !jobs.is_empty() {
20585 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
20586 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
20587 attention::recycle_buf(&mut row);
20588 }
20589 }
20590 }
20591 out
20592}
20593
20594fn moe_ffn_cpu(
20596 m: &MoeFfn,
20597 x: &[f32],
20598 idx: &[usize],
20599 p: &[f32],
20600 wsum: f32,
20601 pool: Option<&Pool>,
20602) -> Vec<f32> {
20603 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
20604 return out;
20605 }
20606 let mut out = attention::take_buf(x.len());
20607 for &e in idx {
20608 let mut eo = dense_ffn(&m.experts[e], x, pool);
20609 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
20610 for i in 0..out.len() {
20611 out[i] += w * eo[i];
20612 }
20613 attention::recycle_buf(&mut eo);
20614 }
20615 if let Some((se, gate)) = &m.shared {
20616 let mut so = dense_ffn(se, x, pool);
20617 let g = gate.as_ref().map_or(1.0, |gate| {
20618 let mut gl = [0.0f32; 1];
20619 gate.matvec(x, &mut gl, pool);
20620 1.0 / (1.0 + (-gl[0]).exp())
20621 });
20622 for i in 0..out.len() {
20623 out[i] += g * so[i];
20624 }
20625 attention::recycle_buf(&mut so);
20626 }
20627 out
20628}
20629
20630#[allow(clippy::too_many_arguments)]
20638pub(crate) fn mla_attention(
20639 w: &MlaWeights,
20640 normed: &[f32],
20641 cache: &mut crate::kv_cache::LayerKvCache,
20642 position: usize,
20643 inv_freq: &[f32],
20644 rope_scale: f32,
20645 eps: f64,
20646 pool: Option<&Pool>,
20647) -> Vec<f32> {
20648 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
20649 let hd = dr + dn;
20650 let mut q = vec![0.0f32; nh * hd];
20651 match (&w.q_a, &w.q_a_norm) {
20652 (Some(qa), Some(qn)) => {
20653 let mut t = vec![0.0f32; qa.rows()];
20654 qa.matvec(normed, &mut t, pool);
20655 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
20656 w.q_proj.matvec(&tn, &mut q, pool);
20657 }
20658 _ => w.q_proj.matvec(normed, &mut q, pool),
20659 }
20660 let mut ca = vec![0.0f32; lora + dr];
20661 w.kv_a.matvec(normed, &mut ca, pool);
20662 let (c_lat, k_rope) = ca.split_at_mut(lora);
20663 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
20664 let mut kvb = vec![0.0f32; nh * (dn + dv)];
20665 w.kv_b.matvec(&latn, &mut kvb, pool);
20666 if !w.nope {
20667 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
20668 }
20669 for h in 0..nh {
20670 if !w.nope {
20671 attention::rope_rotate_scaled(
20672 &mut q[h * hd..h * hd + dr],
20673 position,
20674 inv_freq,
20675 rope_scale,
20676 );
20677 }
20678 }
20679 let mut k = vec![0.0f32; nh * hd];
20680 let mut v = vec![0.0f32; nh * hd];
20681 for h in 0..nh {
20682 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
20683 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
20684 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
20685 }
20686 cache.append(&k, &v, &vec![true; nh]);
20687 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
20688 attention::recycle_buf(&mut imp);
20689 let mut ov = vec![0.0f32; nh * dv];
20690 for h in 0..nh {
20691 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
20692 }
20693 let mut out = vec![0.0f32; w.o_proj.rows()];
20694 w.o_proj.matvec(&ov, &mut out, pool);
20695 out
20696}
20697
20698fn dense_moe_ffn(
20705 dm: &DenseMoeFfn,
20706 x_normed: &[f32],
20707 h_raw: &[f32],
20708 eps: f64,
20709 norm_style: NormStyle,
20710 pool: Option<&Pool>,
20711) -> Vec<f32> {
20712 let mut d = dense_ffn(&dm.dense, x_normed, pool);
20713 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
20714 let m = &dm.moe;
20715 let ne = m.experts.len();
20716 let mut logits = vec![0.0f32; ne];
20717 if m.router_input_norm {
20718 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
20719 let inv = 1.0 / (ss + eps as f32).sqrt();
20720 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
20721 m.router.matvec(&xr, &mut logits, pool);
20722 } else {
20723 m.router.matvec(h_raw, &mut logits, pool);
20724 }
20725 let (idx, p, wsum) = moe_route(&logits, m, None);
20726 {
20727 let mut st = m.stats.borrow_mut();
20728 if st.len() < ne {
20729 st.resize(ne, 0);
20730 }
20731 for &e in &idx {
20732 st[e] += 1;
20733 }
20734 }
20735 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
20736 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
20737 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
20738 for (di, mi) in d.iter_mut().zip(&mo) {
20739 *di += mi;
20740 }
20741 d
20742}
20743
20744fn moe_gpu_refused(why: &'static str) {
20751 use std::sync::atomic::{AtomicBool, Ordering};
20752 static SAID: AtomicBool = AtomicBool::new(false);
20753 if !SAID.swap(true, Ordering::Relaxed) {
20754 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
20755 }
20756}
20757
20758fn moe_ffn_gpu(
20759 m: &MoeFfn,
20760 x: &[f32],
20761 idx: &[usize],
20762 p: &[f32],
20763 wsum: f32,
20764 pool: Option<&Pool>,
20765) -> Option<Vec<f32>> {
20766 use crate::gpu::MoeJob;
20767
20768 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
20769 let mut model_ref = None;
20770 for &e in idx {
20771 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
20772 moe_gpu_refused("push_job(expert)");
20773 return None;
20774 }
20775 }
20776 if let Some((se, gate)) = &m.shared {
20777 let g = gate.as_ref().map_or(1.0, |gate| {
20778 let mut gl = [0.0f32; 1];
20779 gate.matvec(x, &mut gl, pool);
20780 1.0 / (1.0 + (-gl[0]).exp())
20781 });
20782 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
20783 moe_gpu_refused("push_job(shared)");
20784 return None;
20785 }
20786 }
20787 let Some(model) = model_ref else {
20788 moe_gpu_refused("no model_ref");
20789 return None;
20790 };
20791 let hidden = jobs[0].down.1;
20792 let mut out = vec![0.0f32; hidden];
20793 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20794 Some(out)
20795 } else {
20796 moe_gpu_refused("gpu::moe_block");
20797 None
20798 }
20799}
20800
20801fn ffn_forward(
20803 ffn: &FfnKind,
20804 x: &[f32],
20805 pool: Option<&Pool>,
20806 experts_allowed: Option<&[bool]>,
20807) -> Vec<f32> {
20808 match ffn {
20809 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
20810 FfnKind::Dense(d) => dense_ffn(d, x, pool),
20811 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
20812 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20816 }
20817}
20818
20819fn ffn_forward_pair(
20823 ffn: &FfnKind,
20824 x1: &[f32],
20825 x2: &[f32],
20826 pool: Option<&Pool>,
20827 experts_allowed: Option<&[bool]>,
20828) -> (Vec<f32>, Vec<f32>) {
20829 let d = match ffn {
20830 FfnKind::Dense(d) if !d.segs.is_empty() => {
20833 return (
20834 tube_ffn(d, x1, 1, pool, None),
20835 tube_ffn(d, x2, 1, pool, None),
20836 );
20837 }
20838 FfnKind::Dense(d) => d,
20839 FfnKind::Moe(m) => {
20840 return (
20841 moe_ffn(m, x1, pool, experts_allowed),
20842 moe_ffn(m, x2, pool, experts_allowed),
20843 );
20844 }
20845 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20846 };
20847 let inter = d.gate_proj.rows();
20848 FFN_SCRATCH.with(|s| {
20849 let mut s = s.borrow_mut();
20850 let [g1, g2, u1, u2] = &mut *s;
20851 g1.resize(inter, 0.0);
20852 g2.resize(inter, 0.0);
20853 u1.resize(inter, 0.0);
20854 u2.resize(inter, 0.0);
20855 QTensor::matvec2_many(
20858 [&d.gate_proj, &d.up_proj],
20859 x1,
20860 x2,
20861 [g1.as_mut_slice(), u1.as_mut_slice()],
20862 [g2.as_mut_slice(), u2.as_mut_slice()],
20863 pool,
20864 );
20865 for i in 0..inter {
20866 g1[i] = d.act.combine(g1[i], u1[i]);
20867 g2[i] = d.act.combine(g2[i], u2[i]);
20868 }
20869 let mut o1 = attention::take_buf(d.down_proj.rows());
20870 let mut o2 = attention::take_buf(d.down_proj.rows());
20871 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
20872 (o1, o2)
20873 })
20874}
20875
20876#[cfg(test)]
20877mod tests {
20878
20879 #[test]
20880 fn encoder_hidden_states_are_normalized_and_reusable() {
20881 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
20882 let ids = [1, 7, 11, 19];
20883
20884 let first = p.encode_hidden_states(&ids).expect("encoder forward");
20885 let second = p
20886 .encode_hidden_states(&ids)
20887 .expect("reusable encoder forward");
20888 assert_eq!(first.len(), ids.len() * 8);
20889 assert!(first.iter().all(|v| v.is_finite()));
20890 assert_eq!(
20891 first, second,
20892 "encoder forward must start from a fresh state"
20893 );
20894
20895 assert!(p.forward_ids(&ids, None).is_ok());
20898 }
20899
20900 #[test]
20905 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
20906 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
20907 let dense_card = ChunkStackFacts {
20908 plain_dense: true,
20909 discrete: true,
20910 gpu_on: true,
20911 ..Default::default()
20912 };
20913 assert!(dense_card.dense_on_discrete());
20914 assert_eq!(
20916 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
20917 DISCRETE_DENSE_PREFILL_CHUNK
20918 );
20919 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
20920 for (label, facts) in [
20921 (
20922 "GDN hybrid / MoE / DeepSeek stack",
20923 ChunkStackFacts {
20924 plain_dense: false,
20925 ..dense_card
20926 },
20927 ),
20928 (
20929 "integrated GPU",
20930 ChunkStackFacts {
20931 discrete: false,
20932 ..dense_card
20933 },
20934 ),
20935 (
20936 "CPU only",
20937 ChunkStackFacts {
20938 gpu_on: false,
20939 discrete: false,
20940 ..dense_card
20941 },
20942 ),
20943 (
20944 "capacity split",
20945 ChunkStackFacts {
20946 capacity_split: true,
20947 ..dense_card
20948 },
20949 ),
20950 (
20951 "multi-GPU plan",
20952 ChunkStackFacts {
20953 multi_gpu: true,
20954 ..dense_card
20955 },
20956 ),
20957 (
20958 "O(1) layers",
20959 ChunkStackFacts {
20960 o1: true,
20961 ..dense_card
20962 },
20963 ),
20964 ] {
20965 assert!(!facts.dense_on_discrete(), "{label}");
20966 assert_eq!(
20967 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
20968 48,
20969 "{label} keeps the historical x86 chunk"
20970 );
20971 }
20972 for dense in [false, true] {
20974 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
20975 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
20976 }
20977 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
20979 for dense in [false, true] {
20980 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
20981 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
20982 }
20983 }
20984 }
20985
20986 #[test]
20987 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
20988 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
20989 let full = |host_rows, device_rows| ReuseLayer {
20990 full: true,
20991 host_rows,
20992 device_rows,
20993 device_state: false,
20994 };
20995 assert_eq!(
20998 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
20999 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21000 );
21001 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21003 assert_eq!(
21005 kv_reuse_plan(339, &[full(339, Some(345))]),
21006 ReusePlan::Ready
21007 );
21008 assert_eq!(
21010 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21011 ReusePlan::Pull(vec![(0, 300, 339)])
21012 );
21013 assert_eq!(
21015 kv_reuse_plan(339, &[full(300, Some(320))]),
21016 ReusePlan::Fresh
21017 );
21018 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21019 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21020 let conv = |device_state| ReuseLayer {
21023 full: false,
21024 host_rows: 0,
21025 device_rows: None,
21026 device_state,
21027 };
21028 assert_eq!(
21029 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21030 ReusePlan::Fresh
21031 );
21032 assert_eq!(
21033 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21034 ReusePlan::Ready
21035 );
21036 }
21037
21038 #[test]
21039 fn nll_graph_policy_scopes_only_the_fused_head() {
21040 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21041 ("vulkan graph", true, true, false, true, false),
21043 ("native Metal graph", true, true, true, true, true),
21045 ("masked", false, true, false, false, false),
21047 ("graph disabled", true, false, true, false, false),
21048 ] {
21049 let (graph_quality, graph_head_required) =
21050 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21051 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21052 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21053 }
21054 }
21055
21056 #[test]
21057 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21058 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21059 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21060 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21061 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21062 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21063 }
21064
21065 #[test]
21066 fn cancel_flag_stops_generation() {
21067 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21068 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21071 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21072 assert_eq!(r.finish_reason, "cancelled");
21073 assert!(
21074 r.token_ids.is_empty(),
21075 "no tokens after cancel: {:?}",
21076 r.token_ids
21077 );
21078 assert_eq!(p.kv_cache.seq_len(), 0);
21079 assert!(p.kv_history.is_empty());
21080 assert!(!p.graph_want_logits);
21081 assert!(p.graph_logits.is_none());
21082 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21084 assert_ne!(r2.finish_reason, "cancelled");
21085 }
21086 use super::*;
21087
21088 #[test]
21089 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21090 let arch = |arch_name: &str, hidden_act: &str| {
21094 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21095 "arch_name": arch_name,
21096 "hidden_size": 8,
21097 "intermediate_size": 16,
21098 "num_layers": 1,
21099 "num_attention_heads": 1,
21100 "num_kv_heads": 1,
21101 "head_dim": 8,
21102 "vocab_size": 16,
21103 "layer_types": ["FullAttention"],
21104 "rms_norm_eps": 1e-6,
21105 "max_position_embeddings": 16,
21106 "hidden_act": hidden_act
21107 }))
21108 .expect("minimal architecture")
21109 };
21110 let minimax = arch("minimax_m3_vl", "swigluoai");
21111 assert_eq!(
21112 Act::from_arch_full(&minimax),
21113 Act::SwiGluOai {
21114 alpha: 1.702,
21115 limit: 7.0,
21116 }
21117 );
21118
21119 let unrelated = arch("qwen2", "swigluoai");
21122 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21123
21124 let non_oai = arch("minimax_m3_vl", "gelu");
21128 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21129 }
21130
21131 #[test]
21132 fn minimax_swiglu_oai_act_matches_scalar_contract() {
21133 let act = Act::swiglu_oai(1.702, 7.0);
21134 let got = act.combine(10.0, -10.0);
21135 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
21136 assert!((got - expected).abs() < 1e-6);
21137 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
21138 assert_eq!(
21139 act.wgpu_graph_act(),
21140 Some(crate::gpu::GraphAct::SwiGluOaiM3),
21141 "the WGPU-only arm accepts only the frozen M3 constants"
21142 );
21143 assert_eq!(
21144 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
21145 None,
21146 "nearby OAI constants must not borrow MiniMax's kernel"
21147 );
21148 assert_eq!(
21149 act.moe_job_graph_contract(),
21150 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
21151 "the direct MoE path carries the frozen clamp with the marker"
21152 );
21153 assert_eq!(
21154 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
21155 None,
21156 "a nearby clamp must not borrow MiniMax's device contract"
21157 );
21158
21159 let gate_only = act.apply(-10.0);
21162 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
21163 assert!((gate_only - expected_gate).abs() < 1e-10);
21164 }
21165
21166 #[test]
21167 fn explicit_sliding_schedule_selects_local_rope_profile() {
21168 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
21173 p.swa = Some((1024, usize::MAX));
21174 p.sliding_layers = Some(vec![true, true, true, false]);
21175 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
21176 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
21177 p.rope_scale = 1.277_258_9;
21178 p.rope_scale_local = 1.0;
21179
21180 for li in 0..3 {
21181 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
21182 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
21183 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
21184 }
21185 assert_eq!(p.layer_window(3), None);
21186 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
21187 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
21188 }
21189
21190 #[test]
21198 fn dynamic_ffn_equals_the_zeroing_arm() {
21199 let (hidden, inter) = (8usize, 32usize);
21200 let synth = |n: usize, salt: usize| -> Vec<f32> {
21201 (0..n)
21202 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
21203 .collect()
21204 };
21205 let down = synth(hidden * inter, 3);
21206 let mut down_t = vec![0.0f32; inter * hidden];
21207 for r in 0..hidden {
21208 for c in 0..inter {
21209 down_t[c * hidden + r] = down[r * inter + c];
21210 }
21211 }
21212 let d = DenseFfn {
21213 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21214 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21215 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
21216 act: Act::Silu,
21217 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
21218 segs: Vec::new(),
21219 };
21220 let x = synth(hidden, 11);
21221 let k = 12usize;
21222 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
21223 let mut g = vec![0.0f32; inter];
21225 d.gate_proj.matvec(&x, &mut g, None);
21226 let mut u = vec![0.0f32; inter];
21227 d.up_proj.matvec(&x, &mut u, None);
21228 for v in g.iter_mut() {
21229 *v = inference::silu(*v);
21230 }
21231 keep_top_k(&mut g, k);
21232 for i in 0..inter {
21233 g[i] *= u[i];
21234 }
21235 let mut want = vec![0.0f32; hidden];
21236 d.down_proj.matvec(&g, &mut want, None);
21237 for (a, b) in want.iter().zip(&got) {
21238 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
21239 }
21240 }
21241
21242 #[test]
21248 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
21249 let (hidden, core, tube) = (8usize, 12usize, 8usize);
21250 let inter = core + tube;
21251 let synth = |n: usize, salt: usize| -> Vec<f32> {
21252 (0..n)
21253 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
21254 .collect()
21255 };
21256 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
21257 let d_all = synth(hidden * inter, 3);
21258 let dense = DenseFfn {
21260 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
21261 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
21262 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
21263 act: Act::Silu,
21264 down_t: None,
21265 segs: Vec::new(),
21266 };
21267 let rows =
21268 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
21269 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
21270 let mut o = Vec::with_capacity(hidden * (b - a));
21271 for r in 0..hidden {
21272 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
21273 }
21274 o
21275 };
21276 let tubed = DenseFfn {
21277 down_t: None,
21278 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
21279 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
21280 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
21281 act: Act::Silu,
21282 segs: vec![FfnSeg {
21283 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
21284 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
21285 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
21286 start: core,
21287 width: tube,
21288 }],
21289 };
21290 let x = synth(hidden, 7);
21291 let want = dense_ffn(&dense, &x, None);
21292 let got = tube_ffn(&tubed, &x, 1, None, None);
21293 for (a, b) in want.iter().zip(&got) {
21294 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
21295 }
21296 let mut bits = vec![0u8; inter.div_ceil(8)];
21298 for n in 0..core {
21299 bits[n / 8] |= 1 << (n % 8);
21300 }
21301 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21302 let masked = dense_ffn_masked(&dense, &x, None, &bits);
21303 for (a, b) in masked.iter().zip(&closed) {
21304 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
21305 }
21306 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21308 for (a, b) in closed.iter().zip(&batch) {
21309 assert_eq!(a, b, "batch arm disagrees with decode arm");
21310 }
21311 }
21312
21313 #[test]
21315 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
21316 let (hidden, inter) = (16usize, 40usize);
21317 let synth = |n: usize, salt: usize| -> Vec<f32> {
21318 (0..n)
21319 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
21320 .collect()
21321 };
21322 let d = DenseFfn {
21323 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21324 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21325 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
21326 act: Act::Silu,
21327 down_t: None,
21328 segs: Vec::new(),
21329 };
21330 let x = synth(hidden, 9);
21331 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
21333
21334 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
21335
21336 let mut g = vec![0.0f32; inter];
21338 d.gate_proj.matvec(&x, &mut g, None);
21339 let mut u = vec![0.0f32; inter];
21340 d.up_proj.matvec(&x, &mut u, None);
21341 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
21342 for i in 0..inter {
21343 g[i] = if act_set.contains(&(i as u16)) {
21344 inference::silu(g[i]) * u[i]
21345 } else {
21346 0.0
21347 };
21348 }
21349 let mut reference = vec![0.0f32; hidden];
21350 d.down_proj.matvec(&g, &mut reference, None);
21351
21352 let max_d = sparse
21353 .iter()
21354 .zip(&reference)
21355 .map(|(a, b)| (a - b).abs())
21356 .fold(0.0f32, f32::max);
21357 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
21358 }
21359
21360 fn attach_test_mtp(p: &mut Pipeline) {
21362 let (h, inter, heads, kv, hd) = (
21363 p.hidden_size,
21364 p.intermediate_size,
21365 p.num_heads,
21366 p.num_kv_heads,
21367 p.head_dim,
21368 );
21369 let synth = |n: usize, salt: usize| -> Vec<f32> {
21370 (0..n)
21371 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
21372 .collect()
21373 };
21374 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
21375 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21376 };
21377 p.mtp = Some(MtpModule {
21378 enorm: vec![1.0; h],
21379 hnorm: vec![1.0; h],
21380 eh_proj: qt(h, 2 * h, 301),
21381 layer: LayerWeights {
21382 input_norm: vec![1.0; h],
21383 post_norm: vec![1.0; h],
21384 attn_out_norm: None,
21385 ffn_out_norm: None,
21386 layer_scale: None,
21387 ffn: FfnKind::Dense(DenseFfn {
21388 gate_proj: qt(inter, h, 315),
21389 up_proj: qt(inter, h, 316),
21390 down_proj: qt(h, inter, 317),
21391 act: Act::Silu,
21392 down_t: None,
21393 segs: Vec::new(),
21394 }),
21395 attn: AttnKind::Full {
21396 bias: None,
21397 wq: qt(heads * hd, h, 311),
21398 wk: qt(kv * hd, h, 312),
21399 wv: qt(kv * hd, h, 313),
21400 wo: qt(h, heads * hd, 314),
21401 q_norm: None,
21402 k_norm: None,
21403 output_gate: false,
21404 softplus_gate: None,
21405 },
21406 },
21407 final_norm: vec![1.0; h],
21408 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
21409 });
21410 }
21411
21412 #[test]
21413 fn speculative_equals_vanilla_greedy() {
21414 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21418 let run = |spec: bool| {
21419 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21420 p.sampler_config.temperature = 0.0;
21421 attach_test_mtp(&mut p);
21422 p.speculative = spec;
21423 let r = p.generate("abcdef", 12, None, None).unwrap();
21424 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
21425 };
21426 let (vanilla, d0, _) = run(false);
21427 let (spec, d1, a1) = run(true);
21428 assert_eq!(d0, 0, "vanilla path must not draft");
21429 assert!(d1 > 0, "speculative path must draft");
21430 assert_eq!(
21431 vanilla, spec,
21432 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
21433 );
21434 }
21435
21436 #[test]
21437 fn speculative_accepts_constant_oracle() {
21438 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21440 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21441 p.sampler_config.temperature = 0.0;
21442 p.sampler_config.repetition_penalty = 1.0;
21443 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
21446 attach_test_mtp(&mut p);
21447 p.speculative = true;
21448 let r = p.generate("abcd", 10, None, None).unwrap();
21449 assert!(r.mtp_drafted > 0);
21450 assert_eq!(
21451 r.mtp_accepted, r.mtp_drafted,
21452 "constant logits → every draft accepted"
21453 );
21454 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
21457 }
21458
21459 #[test]
21460 fn empty_prompt_is_an_error_not_a_panic() {
21461 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
21462 let r = p.generate("", 4, None, None);
21463 assert!(r.is_err(), "empty prompt must be a clean error");
21464 }
21465
21466 #[test]
21467 fn every_token_enters_kv_exactly_once() {
21468 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21469 p.sampler_config.temperature = 0.0;
21471 let r = p.generate("abc", 2, None, None).unwrap();
21472 assert_eq!(r.prompt_tokens, 3);
21473 assert_eq!(
21477 p.kv_cache.seq_len(),
21478 3 + r.tokens_generated - 1,
21479 "each token must be cached exactly once (v1 cached the last prompt token twice)"
21480 );
21481 }
21482
21483 #[test]
21484 fn generation_is_reproducible_with_seed() {
21485 let run = || {
21486 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21487 p.generate("hello", 8, None, None).unwrap().token_ids
21488 };
21489 assert_eq!(run(), run());
21490 }
21491
21492 #[test]
21493 fn resetting_sampler_restarts_the_seeded_stream() {
21494 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21495 let config = SamplerConfig {
21496 seed: Some(1234),
21497 ..SamplerConfig::default()
21498 };
21499 p.set_sampler_config(config.clone());
21500 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
21501 p.set_sampler_config(config);
21502 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
21503 assert_eq!(first, second);
21504 }
21505
21506 #[test]
21507 fn eviction_bounds_the_cache() {
21508 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
21509 p.kv_cache.max_seq_len = 6;
21510 p.sampler_config.temperature = 0.0;
21511 let _ = p.generate("abcd", 12, None, None).unwrap();
21512 assert!(
21513 p.kv_cache.seq_len() <= 6 + 1,
21514 "cache must stay bounded by max_seq_len (got {})",
21515 p.kv_cache.seq_len()
21516 );
21517 }
21518
21519 #[test]
21520 fn confidence_matches_tokens_and_is_a_probability() {
21521 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21522 p.sampler_config.temperature = 0.0;
21523 p.sampler_config.repetition_penalty = 1.0;
21524 let r = p.generate("abcd", 10, None, None).unwrap();
21525 assert_eq!(
21526 r.token_confidence.len(),
21527 r.token_ids.len(),
21528 "one confidence per emitted token"
21529 );
21530 for &c in &r.token_confidence {
21531 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
21532 }
21533 let logits = [1.0f32, 3.0, 0.5, 3.0];
21535 let p0 = top1_prob_t(&logits, 1, 1.0);
21536 let p1 = top1_prob_t(&logits, 3, 1.0);
21537 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
21538 assert!(p0 > 0.0 && p0 < 1.0);
21539 let sharp = top1_prob_t(&logits, 1, 1.0);
21541 let soft = top1_prob_t(&logits, 1, 2.0);
21542 assert!(soft < sharp, "higher temperature lowers peak confidence");
21543 }
21544
21545 #[test]
21546 fn trace_is_opt_in_and_parallels_the_output() {
21547 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21549 p.sampler_config.temperature = 0.0;
21550 p.sampler_config.repetition_penalty = 1.0;
21551 let r = p.generate("abcd", 10, None, None).unwrap();
21552 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
21553
21554 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21556 p.sampler_config.temperature = 0.0;
21557 p.sampler_config.repetition_penalty = 1.0;
21558 p.set_trace(true);
21559 let r = p.generate("abcd", 10, None, None).unwrap();
21560 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
21561 for (i, tr) in r.traces.iter().enumerate() {
21562 assert_eq!(tr.t, i, "trace index is sequential");
21563 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
21564 assert_eq!(
21565 tr.confidence, r.token_confidence[i],
21566 "trace confidence matches the confidence channel"
21567 );
21568 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
21570 }
21571 }
21572
21573 #[test]
21574 fn explain_prefill_logits_match_greedy_first_token() {
21575 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21579 p.sampler_config.temperature = 0.0;
21580 p.sampler_config.repetition_penalty = 1.0;
21581 let ids = p.tokenizer.encode("abcd");
21582 let logits = p.prefill_next_logits(&ids, None);
21583 let argmax = logits
21584 .iter()
21585 .enumerate()
21586 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
21587 .unwrap()
21588 .0 as u32;
21589 let r = p.generate("abcd", 1, None, None).unwrap();
21590 assert_eq!(
21591 argmax, r.token_ids[0],
21592 "explain preview must match greedy emit"
21593 );
21594 }
21595
21596 #[test]
21597 fn laguna_shared_expert_is_unconditionally_added() {
21598 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
21599 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
21600 let zero_dense = || DenseFfn {
21601 gate_proj: matrix(vec![0.0; 4]),
21602 up_proj: matrix(vec![0.0; 4]),
21603 down_proj: matrix(vec![0.0; 4]),
21604 act: Act::Silu,
21605 down_t: None,
21606 segs: Vec::new(),
21607 };
21608 let shared = DenseFfn {
21609 gate_proj: identity(),
21610 up_proj: identity(),
21611 down_proj: identity(),
21612 act: Act::Silu,
21613 down_t: None,
21614 segs: Vec::new(),
21615 };
21616 let x = [1.0, 2.0];
21617 let expected = dense_ffn(&shared, &x, None);
21618 let moe = MoeFfn {
21619 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
21620 experts: vec![zero_dense()],
21621 top_k: 1,
21622 norm_topk_prob: true,
21623 router_sigmoid: true,
21624 sigmoid_denom_eps: 1e-6,
21625 expert_bias: None,
21626 routed_scaling: 1.0,
21627 route_tau: None,
21628 shared: Some((shared, None)),
21629 stats: std::cell::RefCell::new(Vec::new()),
21630 act_sq: std::cell::RefCell::new(Vec::new()),
21631 act_rows: std::cell::RefCell::new(Vec::new()),
21632 mask: None,
21633 per_expert_scale: None,
21634 router_input_norm: false,
21635 resonance: None,
21636 grown: Vec::new(),
21637 };
21638 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
21639 for (actual, expected) in actual.iter().zip(expected) {
21640 assert!((actual - expected).abs() < 1e-6);
21641 }
21642 }
21643
21644 fn mimo_test_pipeline() -> Pipeline {
21653 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
21654 let kvh = [1usize, 2, 2, 1];
21655 let synth = |n: usize, salt: usize| -> Vec<f32> {
21656 (0..n)
21657 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21658 .collect()
21659 };
21660 let qt = |rows: usize, cols: usize, salt: usize| {
21661 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21662 };
21663 let dense = |inter: usize, salt: usize| DenseFfn {
21664 gate_proj: qt(inter, hs, salt),
21665 up_proj: qt(inter, hs, salt + 1),
21666 down_proj: qt(hs, inter, salt + 2),
21667 act: Act::Silu,
21668 down_t: None,
21669 segs: Vec::new(),
21670 };
21671 let layers: Vec<LayerWeights> = (0..4)
21672 .map(|li| LayerWeights {
21673 input_norm: vec![1.0; hs],
21674 post_norm: vec![1.0; hs],
21675 attn_out_norm: None,
21676 ffn_out_norm: None,
21677 layer_scale: None,
21678 ffn: if li == 0 {
21679 FfnKind::Dense(dense(inter, 50))
21680 } else {
21681 FfnKind::Moe(MoeFfn {
21682 router: qt(4, hs, 60 + li),
21683 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
21684 top_k: 2,
21685 norm_topk_prob: true,
21686 router_sigmoid: true,
21687 sigmoid_denom_eps: 1e-6,
21688 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
21689 routed_scaling: 1.0,
21690 route_tau: None,
21691 shared: None,
21692 stats: std::cell::RefCell::new(Vec::new()),
21693 act_sq: std::cell::RefCell::new(Vec::new()),
21694 act_rows: std::cell::RefCell::new(Vec::new()),
21695 mask: None,
21696 per_expert_scale: None,
21697 router_input_norm: false,
21698 resonance: None,
21699 grown: Vec::new(),
21700 })
21701 },
21702 attn: AttnKind::Full {
21703 wq: qt(nh * hd, hs, li * 10 + 1),
21704 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
21705 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
21706 wo: qt(hs, nh * vd, li * 10 + 4),
21707 q_norm: None,
21708 k_norm: None,
21709 output_gate: false,
21710 softplus_gate: None,
21711 bias: None,
21712 },
21713 })
21714 .collect();
21715 let mut p = Pipeline::new(
21716 Tokenizer::byte_level(),
21717 PipelineWeights {
21718 embed_tokens: qt(vocab, hs, 100),
21719 layers,
21720 lm_head: qt(vocab, hs, 200),
21721 final_norm: vec![1.0; hs],
21722 },
21723 hs,
21724 inter,
21725 nh,
21726 1, hd,
21728 4,
21729 4,
21730 false,
21731 vocab,
21732 1e-6,
21733 1e7,
21734 NormStyle::Qwen,
21735 4096,
21736 SamplerConfig {
21737 seed: Some(7),
21738 ..Default::default()
21739 },
21740 );
21741 p.layer_dump = None;
21743 p.set_rotary(4, 1e7);
21744 p.sliding_layers = Some(vec![false, true, true, false]);
21745 p.swa = Some((3, usize::MAX));
21746 p.rotary_dim_local = Some(4);
21747 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
21748 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
21749 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
21750 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
21751 p
21752 }
21753
21754 #[test]
21755 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
21756 let mut p = mimo_test_pipeline();
21757 p.speculative = false;
21758 p.ignore_eos = true;
21759 p.sampler_config.temperature = 0.0;
21760 p.sampler_config.repetition_penalty = 1.0;
21761 let a = vec![3, 5, 7, 9, 11, 13];
21762 let b = vec![4, 8, 12, 16, 20, 24];
21763 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
21764 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
21765 let actual = p
21769 .generate_from_embeds(&a, &rows, 8, None, None)
21770 .unwrap()
21771 .token_ids;
21772 assert_eq!(actual, expected);
21773 assert!(p.kv_history.is_empty());
21774 let mut extended = a.clone();
21775 extended.push(17);
21776 let after_media = p
21777 .generate_from_ids(&extended, 8, None, None)
21778 .unwrap()
21779 .token_ids;
21780 p.reset_session();
21781 let fresh = p
21782 .generate_from_ids(&extended, 8, None, None)
21783 .unwrap()
21784 .token_ids;
21785 assert_eq!(after_media, fresh);
21786 assert!(
21787 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
21788 .is_err()
21789 );
21790 p.reset_session();
21794 p.generate_from_ids(&a, 1, None, None).unwrap();
21795 let mut media_ids = p.kv_history.clone();
21796 assert!(!media_ids.is_empty());
21797 media_ids.extend_from_slice(&[19, 21, 23]);
21798 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
21799 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
21800 let mut oracle = mimo_test_pipeline();
21801 oracle.speculative = false;
21802 oracle.ignore_eos = true;
21803 oracle.sampler_config.temperature = 0.0;
21804 oracle.sampler_config.repetition_penalty = 1.0;
21805 let expected = oracle
21806 .generate_from_ids(&source_ids, 8, None, None)
21807 .unwrap()
21808 .token_ids;
21809 assert_eq!(
21810 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
21811 .unwrap()
21812 .token_ids,
21813 expected
21814 );
21815 assert!(p.kv_history.is_empty());
21816 let mut bad = rows;
21817 bad[0] = f32::NAN;
21818 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
21819 }
21820
21821 fn f32_bits(v: &[f32]) -> Vec<u32> {
21822 v.iter().map(|x| x.to_bits()).collect()
21823 }
21824
21825 #[test]
21832 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
21833 let mut p = mimo_test_pipeline();
21834 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
21835 assert_eq!(kv, vec![1, 2, 2, 1]);
21836 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
21837 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
21838 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
21839 let hs = p.hidden_size;
21840 let mut decode = Vec::new();
21841 for (pos, &id) in ids.iter().enumerate() {
21842 let e = p.embed_single(id);
21843 let h = p.forward_layers(&e, pos, None);
21844 decode.push(p.logits_from_hidden(&h));
21845 }
21846 for l in &p.kv_cache.layers {
21847 assert_eq!(l.seq_len, 12);
21848 assert_eq!(l.head_values(0).len(), 12 * 8);
21850 }
21851 assert!(decode.iter().flatten().all(|v| v.is_finite()));
21852
21853 p.clear_sequence_state();
21854 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21855 for pos in 0..ids.len() {
21856 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
21857 assert_eq!(
21858 f32_bits(&decode[pos]),
21859 f32_bits(&lg),
21860 "whole prompt, pos {pos}"
21861 );
21862 }
21863
21864 p.clear_sequence_state();
21865 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
21866 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
21867 for pos in 0..ids.len() {
21868 let row = if pos < 5 {
21869 &a[pos * hs..(pos + 1) * hs]
21870 } else {
21871 &b[(pos - 5) * hs..(pos - 4) * hs]
21872 };
21873 let lg = p.logits_from_hidden(row);
21874 assert_eq!(
21875 f32_bits(&decode[pos]),
21876 f32_bits(&lg),
21877 "two chunks, pos {pos}"
21878 );
21879 }
21880
21881 let last = |p: &mut Pipeline| {
21884 p.clear_sequence_state();
21885 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21886 p.logits_from_hidden(&hb[11 * hs..12 * hs])
21887 };
21888 let base = last(&mut p);
21889 let mut no_sinks = mimo_test_pipeline();
21890 for l in &mut no_sinks.kv_cache.layers {
21891 l.sinks = None;
21892 }
21893 assert_ne!(
21894 f32_bits(&last(&mut no_sinks)),
21895 f32_bits(&base),
21896 "sinks are live"
21897 );
21898 let mut wide = mimo_test_pipeline();
21899 wide.swa = Some((64, usize::MAX));
21900 assert_ne!(
21901 f32_bits(&last(&mut wide)),
21902 f32_bits(&base),
21903 "window is live"
21904 );
21905
21906 p.clear_sequence_state();
21908 p.ignore_eos = true;
21909 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
21910 assert_eq!(r.token_ids.len(), 4);
21911 }
21912
21913 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
21916 let (hs, nh) = (16usize, 4usize);
21917 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
21918 p.layer_dump = None;
21919 p.swa = Some((6, 4));
21920 p.proj_gate_sigmoid = true;
21921 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
21922 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
21923 let g: Vec<f32> = (0..nh * hs)
21924 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
21925 .collect();
21926 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
21927 }
21928 }
21929 p.swa_trim = trim;
21930 p
21931 }
21932
21933 #[test]
21939 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
21940 let mut a = spark_test_pipeline(None);
21941 let mut b = spark_test_pipeline(Some((2, 4)));
21942 assert_eq!(
21943 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
21944 vec![Some(6), Some(6), Some(6), None]
21945 );
21946 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
21947 let mut pos = 0usize;
21948 for &n in [5usize, 13, 7, 16].iter().cycle() {
21949 if pos >= ids.len() {
21950 break;
21951 }
21952 let end = (pos + n).min(ids.len());
21953 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21954 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21955 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
21956 pos = end;
21957 }
21958 for _ in 0..23 {
21959 let e = a.embed_single(((pos * 7) % 64) as u32);
21960 let ha = a.forward_layers(&e, pos, None);
21961 let hb = b.forward_layers(&e, pos, None);
21962 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
21963 for li in 0..3 {
21964 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
21965 }
21966 pos += 1;
21967 }
21968 for round in 0..9 {
21969 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
21970 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
21971 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
21972 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
21973 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
21974 if round % 2 == 1 {
21975 for p in [&mut a, &mut b] {
21977 for l in &mut p.kv_cache.layers {
21978 l.truncate_last(2);
21979 }
21980 }
21981 } else {
21982 pos += 2;
21983 }
21984 }
21985 let e = a.embed_single(9);
21986 let (ha, hb) = (
21987 a.forward_layers(&e, pos, None),
21988 b.forward_layers(&e, pos, None),
21989 );
21990 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
21991 for li in 0..3 {
21992 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
21993 assert!(lb.base() > 0, "layer {li} trimmed");
21994 assert_eq!(la.base(), 0);
21995 assert_eq!(lb.pos_len(), la.seq_len);
21996 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
21997 }
21998 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
21999 assert_eq!(
22000 (gb.base(), gb.seq_len),
22001 (0, ga.seq_len),
22002 "the global layer keeps all"
22003 );
22004 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22005 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22006 }
22007
22008 #[test]
22013 fn swa_trim_wire_handoff_continues_bitwise() {
22014 let mut a = spark_test_pipeline(None);
22015 let mut b = spark_test_pipeline(Some((2, 4)));
22016 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22017 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22018 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22019 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22020 let mut pos = ids.len();
22021 for _ in 0..5 {
22022 let e = a.embed_single(((pos * 5) % 64) as u32);
22023 assert_eq!(
22024 f32_bits(&a.forward_layers(&e, pos, None)),
22025 f32_bits(&b.forward_layers(&e, pos, None))
22026 );
22027 pos += 1;
22028 }
22029 let mut c = spark_test_pipeline(Some((2, 4)));
22030 for li in 0..4 {
22031 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22032 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22033 }
22034 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22035 assert_eq!(c.kv_cache.seq_len(), pos);
22036 for step in 0..17 {
22037 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22038 let ha = a.forward_layers(&e, pos, None);
22039 let hc = c.forward_layers(&e, pos, None);
22040 assert_eq!(
22041 f32_bits(&ha),
22042 f32_bits(&hc),
22043 "after the hand-off, step {step}"
22044 );
22045 pos += 1;
22046 }
22047 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
22048 }
22049
22050 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
22053 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
22054 let synth = |len: usize, salt: usize| -> Vec<f32> {
22055 (0..len)
22056 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
22057 .collect()
22058 };
22059 let qt = |rows: usize, cols: usize, salt: usize| {
22060 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22061 };
22062 let layers = (0..n)
22063 .map(|k| {
22064 let s = 500 + k * 40;
22065 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
22066 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
22067 MtpModule {
22068 enorm: vec![1.0; hs],
22069 hnorm: vec![1.0; hs],
22070 eh_proj: qt(hs, 2 * hs, s),
22071 layer: LayerWeights {
22072 input_norm: vec![1.0; hs],
22073 post_norm: vec![1.0; hs],
22074 attn_out_norm: None,
22075 ffn_out_norm: None,
22076 layer_scale: None,
22077 attn: AttnKind::Full {
22078 wq: qt(nh * hd, hs, s + 1),
22079 wk: qt(nkv * hd, hs, s + 2),
22080 wv: qt(nkv * vd, hs, s + 3),
22081 wo: qt(hs, nh * vd, s + 4),
22082 q_norm: None,
22083 k_norm: None,
22084 output_gate: false,
22085 softplus_gate: None,
22086 bias: None,
22087 },
22088 ffn: FfnKind::Dense(DenseFfn {
22089 gate_proj: qt(inter, hs, s + 5),
22090 up_proj: qt(inter, hs, s + 6),
22091 down_proj: qt(hs, inter, s + 7),
22092 act: Act::Silu,
22093 down_t: None,
22094 segs: Vec::new(),
22095 }),
22096 },
22097 final_norm: vec![1.0; hs],
22098 kv,
22099 }
22100 })
22101 .collect();
22102 mimo_mtp::MimoMtp::from_layers(layers)
22103 }
22104
22105 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
22106 p.clear_sequence_state();
22107 p.speculative = spec;
22108 p.ignore_eos = true;
22109 p.sampler_config.temperature = 0.0;
22110 p.generate_from_ids(ids, n, None, None).unwrap()
22111 }
22112
22113 #[test]
22119 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
22120 for post in [false, true] {
22123 let mut p = mimo_test_pipeline();
22124 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
22126 let mut st0 = mimo_test_mtp(3, 1.0);
22127 st0.post_norm_hidden = post;
22128 p.mimo_mtp = Some(st0);
22129 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
22130 let hs = p.hidden_size;
22131 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22132 p.mimo_note_rows(&hb, 0);
22133 let mut st = p.mimo_mtp.take().unwrap();
22134 let k = 3;
22137 let mut inc = Vec::new();
22138 for t in 0..ids.len() - k - 1 {
22139 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
22140 }
22141 let s = ids.len();
22144 let mut reference = vec![vec![0u32; k]; s - k - 1];
22145 let mut fresh = mimo_test_mtp(3, 1.0);
22146 for (layer, m) in fresh.layers.iter_mut().enumerate() {
22147 let n = s - layer - 1;
22148 let mut cats = vec![0.0f32; n * 2 * hs];
22149 for j in 0..n {
22150 let e = p.embed_single(ids[j + layer + 1]);
22151 let raw = &hb[j * hs..(j + 1) * hs];
22152 let g = if post {
22153 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
22154 } else {
22155 raw.to_vec()
22156 };
22157 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
22158 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
22159 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
22160 }
22161 let mut x = vec![0.0f32; n * hs];
22162 m.eh_proj.matmat(&cats, n, &mut x, None);
22163 p.mimo_mtp_block(m, &mut x, n, 0);
22164 for (t, row) in reference.iter_mut().enumerate() {
22165 let y = inference::rms_norm(
22166 &x[t * hs..(t + 1) * hs],
22167 &m.final_norm,
22168 p.rms_eps,
22169 p.norm_style,
22170 );
22171 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
22172 }
22173 }
22174 assert_eq!(inc, reference, "post_norm_hidden = {post}");
22175 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
22177 assert!(distinct.len() > 3, "{inc:?}");
22178 let last_t = ids.len() - k - 2;
22180 for m in &st.layers {
22181 assert_eq!(m.kv.seq_len, last_t + 1);
22182 }
22183 }
22184 }
22185
22186 #[test]
22193 fn mimo_speculative_greedy_equals_plain_greedy() {
22194 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
22195 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
22196 let n = 24;
22197 let mut p = mimo_test_pipeline();
22198 let plain = mimo_greedy(&mut p, &ids, n, false);
22199 assert_eq!(plain.mtp_drafted, 0);
22200 assert_eq!(plain.token_ids.len(), n);
22201 let plain_kv = p.kv_cache.layers[0].seq_len;
22202
22203 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
22205 let spec = mimo_greedy(&mut p, &ids, n, true);
22206 assert!(spec.mtp_drafted > 0, "the round must draft");
22207 assert_eq!(spec.token_ids, plain.token_ids);
22208 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
22209
22210 let mut truth: Vec<u32> = ids.clone();
22213 truth.extend(&plain.token_ids);
22214 let mut noisy = truth.clone();
22215 for (i, t) in noisy.iter_mut().enumerate() {
22216 if i % 5 == 0 {
22217 *t = (*t + 1) % 64;
22218 }
22219 }
22220 let mut st = mimo_test_mtp(3, 1.0);
22221 st.draft_override = Some(noisy);
22222 p.mimo_mtp = Some(st);
22223 let spec = mimo_greedy(&mut p, &ids, n, true);
22224 assert_eq!(spec.token_ids, plain.token_ids);
22225 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
22226 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
22227 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
22228 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
22229 assert!(
22230 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
22231 "{:?}",
22232 stats.accept_hist
22233 );
22234 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
22235
22236 let mut st = mimo_test_mtp(3, 1.0);
22239 st.draft_override = Some(truth);
22240 p.mimo_mtp = Some(st);
22241 let spec = mimo_greedy(&mut p, &ids, n, true);
22242 assert_eq!(spec.token_ids, plain.token_ids);
22243 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
22244 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
22245
22246 let off = mimo_greedy(&mut p, &ids, n, false);
22248 assert_eq!(off.token_ids, plain.token_ids);
22249 assert_eq!(off.mtp_drafted, 0);
22250 }
22251
22252 #[test]
22261 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
22262 let p = mimo_test_pipeline();
22263 assert_eq!(
22264 p.graph_attn_decline_reason(),
22265 Some("per-layer KV head counts")
22266 );
22267 assert_eq!(p.wgpu_graph_attn_decline(), None);
22268 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
22269 assert_eq!(
22270 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
22271 (1, 4, 4, None, false)
22272 );
22273 assert_eq!(g0.invf, p.inv_freq.as_slice());
22274 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
22275 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
22276 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
22277 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
22278 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
22279 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
22280 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
22281
22282 let emb = p.embed_single(3);
22285 let mut lg = Vec::new();
22286 assert!(
22287 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
22288 .is_none()
22289 );
22290 let mut hid = emb.clone();
22291 assert_eq!(
22292 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
22293 crate::gpu::BatchGraphOutcome::Declined
22294 );
22295 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
22296 assert!(p.try_multi_burst(3, 0, 4).is_none());
22297 assert!(
22298 p.graph_declines().is_empty(),
22299 "no attention decline logged: {:?}",
22300 p.graph_declines()
22301 );
22302 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
22307 assert_eq!(plain().graph_attn_decline_reason(), None);
22308 assert_eq!(plain().wgpu_graph_attn_decline(), None);
22309 assert!(
22310 plain().graph_attn_geom(0).is_none(),
22311 "uniform models keep the historical arms"
22312 );
22313 let mut q = plain();
22314 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
22315 assert_eq!(
22316 q.graph_attn_decline_reason(),
22317 Some("learned attention sinks")
22318 );
22319 assert_eq!(
22320 q.graph_attn_geom(1).unwrap().sink,
22321 Some(&[0.25f32, -0.25][..])
22322 );
22323 let mut q = plain();
22324 q.set_attn_geometry(None, Some(2)).unwrap();
22325 assert_eq!(
22326 q.graph_attn_decline_reason(),
22327 Some("V heads narrower than Q/K heads")
22328 );
22329 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
22330 let mut q = plain();
22331 q.sliding_layers = Some(vec![true, false]);
22332 q.swa = Some((4, usize::MAX));
22333 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
22334 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
22335 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
22336
22337 let mut q = mimo_test_pipeline();
22341 q.rope_scale = 2.0;
22342 q.rope_scale_local = 1.25;
22343 assert_eq!(q.wgpu_graph_attn_decline(), None);
22344 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
22345 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
22346
22347 let mut q = plain();
22350 q.rope_scale = 2.0;
22351 assert_eq!(
22352 q.wgpu_graph_attn_decline(),
22353 Some("scaled RoPE without per-layer geometry")
22354 );
22355 }
22356
22357 #[cfg(target_os = "macos")]
22363 #[test]
22364 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
22365 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
22369 p.swa = Some((16, usize::MAX));
22370 p.sliding_layers = Some(vec![true, true]);
22371 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
22372 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
22373
22374 assert_eq!(
22375 p.graph_attn_decline_reason(),
22376 Some("learned attention sinks")
22377 );
22378 let swa_graph = p.metal_graph_swa();
22379 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
22380 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
22381 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
22382 assert!(
22383 native_sink_attend,
22384 "the q1 sink admission must follow the SWA door"
22385 );
22386 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
22389 }
22390
22391 #[test]
22392 fn mimo_verify_rewind_preserves_lagging_host_caches() {
22393 let mut p = mimo_test_pipeline();
22394 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
22395 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
22396 for _ in 0..if li == 0 { 2 } else { 12 } {
22397 layer.append(&row, &row, &[]);
22398 }
22399 }
22400 p.mimo_verify_rewind(9).unwrap();
22401 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
22402 for layer in &p.kv_cache.layers[1..] {
22403 assert_eq!(layer.seq_len, 9);
22404 }
22405 }
22406
22407 #[test]
22411 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
22412 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
22413 let _ = std::fs::remove_dir_all(&dir);
22414 let mut p = mimo_test_pipeline();
22415 let hs = p.hidden_size;
22416 let ids = [5u32, 9, 11, 2, 40];
22417 p.layer_dump = Some(dir.join("decode"));
22418 for (pos, &id) in ids.iter().enumerate() {
22419 let e = p.embed_single(id);
22420 let _ = p.forward_layers(&e, pos, None);
22421 }
22422 p.clear_sequence_state();
22423 p.layer_dump = Some(dir.join("prefill"));
22424 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22425 for pos in 0..ids.len() {
22426 for li in 0..p.num_layers {
22427 let name = format!("p{pos:06}_l{li:02}.f32");
22428 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
22429 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
22430 assert_eq!(a.len(), hs * 4, "{name}");
22431 assert_eq!(a, b, "{name}");
22432 }
22433 }
22434 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
22435 let vals: Vec<f32> = last
22436 .chunks(4)
22437 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
22438 .collect();
22439 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
22440 let _ = std::fs::remove_dir_all(&dir);
22441 }
22442
22443 #[test]
22444 fn attn_geometry_and_sinks_are_validated() {
22445 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
22446 assert!(
22447 p.set_attn_geometry(Some(vec![2]), None).is_err(),
22448 "one entry per layer"
22449 );
22450 assert!(
22451 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
22452 "3 does not divide 4"
22453 );
22454 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
22455 assert!(p.set_attn_geometry(None, Some(0)).is_err());
22456 assert!(
22457 p.set_attn_geometry(None, Some(5)).is_err(),
22458 "V wider than the head"
22459 );
22460 p.set_attn_geometry(None, Some(4)).unwrap();
22461 assert_eq!(
22462 p.v_head_dim, None,
22463 "v_head_dim == head_dim is the uniform case"
22464 );
22465 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
22466 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
22467 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
22468 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
22469 assert!(
22470 p.kv_cache.layers[1].sinks.is_some(),
22471 "a reshape keeps the layer's sinks"
22472 );
22473 assert_eq!(p.layer_geom(1).0, 4);
22474 assert!(
22475 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
22476 "one sink per Q head"
22477 );
22478 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
22479 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
22480 }
22481
22482 #[test]
22485 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
22486 let cfg = || {
22487 Some(crate::nystrom::O1Cfg {
22488 layers: crate::nystrom::O1Layers::All,
22489 m: 4,
22490 w: 8,
22491 sink: 2,
22492 rect: crate::nystrom::O1Rect::Aggregate,
22493 })
22494 };
22495 let mut p = mimo_test_pipeline();
22496 p.set_o1(cfg());
22497 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
22498 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
22499 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
22500 q.sliding_layers = Some(vec![false, false, true]);
22501 q.swa = Some((4, usize::MAX));
22502 q.set_o1(cfg());
22503 assert_eq!(q.o1_flags, vec![true, false, false]);
22504 }
22505
22506 #[test]
22507 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
22508 const B: usize = 19;
22509 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22510 p.set_o1(Some(crate::nystrom::O1Cfg {
22511 layers: crate::nystrom::O1Layers::All,
22512 m: 4,
22513 w: 8,
22514 sink: 2,
22515 rect: crate::nystrom::O1Rect::Aggregate,
22516 }));
22517 p.o1_begin_with_prefix(Some(B));
22518 let ids: Vec<u32> = (0..B as u32).collect();
22519 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22520
22521 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
22522 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
22523 let next = p.embed_single(B as u32);
22524 let _ = p.forward_layers(&next, B, None);
22525 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
22526 }
22527
22528 #[test]
22529 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
22530 const B: usize = 19;
22531 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22532 let gdn_cfg = crate::linear_core::GdnCfg {
22536 num_v_heads: 2,
22537 num_k_heads: 1,
22538 key_head_dim: 2,
22539 value_head_dim: 4,
22540 conv_kernel: 3,
22541 hidden_size: 8,
22542 rms_eps: 1e-6,
22543 output_gate_sigmoid: false,
22544 };
22545 let synth = |n: usize, salt: usize| -> Vec<f32> {
22546 (0..n)
22547 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22548 .collect()
22549 };
22550 let qt = |rows: usize, cols: usize, salt: usize| {
22551 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22552 };
22553 let c_dim = gdn_cfg.conv_dim();
22554 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
22555 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
22556 in_proj_qkv: qt(c_dim, 8, 1),
22557 in_proj_z: qt(vd, 8, 2),
22558 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
22559 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
22560 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
22561 a_log: vec![0.2, 0.5],
22562 dt_bias: synth(gdn_cfg.num_v_heads, 6),
22563 norm: vec![1.0; gdn_cfg.value_head_dim],
22564 out_proj: qt(8, vd, 7),
22565 });
22566 p.gdn_cfg = Some(gdn_cfg);
22567 p.set_o1(Some(crate::nystrom::O1Cfg {
22568 layers: crate::nystrom::O1Layers::All,
22569 m: 4,
22570 w: 8,
22571 sink: 2,
22572 rect: crate::nystrom::O1Rect::Aggregate,
22573 }));
22574 p.o1_begin_with_prefix(Some(B));
22575 for pos in 0..B - 2 {
22576 let emb = p.embed_single(pos as u32);
22577 let _ = p.forward_layers(&emb, pos, None);
22578 }
22579 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
22580
22581 let e1 = p.embed_single((B - 2) as u32);
22582 let e2 = p.embed_single((B - 1) as u32);
22583 let _ = p.forward_pair(&e1, &e2, B - 2);
22584
22585 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
22586 assert!(
22587 p.kv_cache
22588 .layers
22589 .iter()
22590 .enumerate()
22591 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
22592 );
22593 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
22594 assert_ne!(
22595 p.kv_cache.layers[0].linear_state, lane1_state,
22596 "real pair must commit GDN lane 2 before returning"
22597 );
22598 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
22599 let next = p.embed_single(B as u32);
22600 let _ = p.forward_layers(&next, B, None);
22601 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
22602 }
22603
22604 #[test]
22605 fn o1_error_observation_stays_terminal_until_reset() {
22606 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22607 p.set_o1(Some(crate::nystrom::O1Cfg {
22608 layers: crate::nystrom::O1Layers::All,
22609 m: 4,
22610 w: 8,
22611 sink: 2,
22612 rect: crate::nystrom::O1Rect::Aggregate,
22613 }));
22614 p.o1_begin();
22615 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
22616
22617 assert!(p.o1_seal_checked().is_err());
22618 assert!(
22619 p.o1_seal_checked().is_err(),
22620 "retry must see the sticky error"
22621 );
22622 let k = vec![0.2f32; 4];
22623 let v = vec![0.3f32; 4];
22624 p.kv_cache.layers[0].append(&k, &v, &[]);
22625 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
22626
22627 p.reset_session();
22628 p.o1_begin();
22629 p.kv_cache.layers[0].append(&k, &v, &[]);
22630 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
22631 }
22632
22633 #[test]
22634 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
22635 let ids = vec![1u32, 2, 3, 4, 5, 6];
22636 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22637 p.graph_logits = Some(vec![123.0]);
22638 p.graph_want_logits = true;
22639 p.graph_failed
22640 .store(true, std::sync::atomic::Ordering::Relaxed);
22641 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22642 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
22643 assert!(err.contains("before NLL"));
22644 assert!(p.graph_logits.is_none());
22645 assert!(!p.graph_want_logits);
22646 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22647 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22648
22649 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22650 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22651 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22652 assert_eq!(actual.1, expected.1);
22653 assert!((actual.0 - expected.0).abs() < 1e-9);
22654 }
22655
22656 #[test]
22657 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
22658 let ids = vec![1u32, 2, 3, 4, 5, 6];
22659 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22660 p.nll_test_fail_at = Some(1);
22661 let err = p
22662 .nll_ids_from(&ids, 0)
22663 .expect_err("one-shot forward failure");
22664 assert!(err.contains("forward") || err.contains("score row"));
22665 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22666 assert!(!p.graph_want_logits);
22667 assert!(p.graph_logits.is_none());
22668 assert!(p.kv_history.is_empty());
22669
22670 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22671 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22672 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22673 assert_eq!(actual.1, expected.1);
22674 assert!((actual.0 - expected.0).abs() < 1e-9);
22675 }
22676
22677 #[test]
22678 fn nll_serial_failure_before_first_row_is_reported() {
22679 let ids = vec![1u32, 2, 3, 4];
22680 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22681 p.nll_test_force_serial = true;
22682 p.nll_test_fail_at = Some(0);
22683 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
22684 assert!(err.contains("serial forward"));
22685 assert!(p.kv_history.is_empty());
22686 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22687 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22688 }
22689
22690 #[test]
22691 fn ffn_probe_failure_discards_recorder_and_state() {
22692 let ids = vec![1u32, 2, 3, 4];
22693 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22694 p.nll_test_fail_at = Some(0);
22695 let err = p
22696 .probe_ffn_mass_batch(&ids)
22697 .expect_err("probe forward failure");
22698 assert!(err.contains("NLL"));
22699 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
22700 assert!(p.kv_history.is_empty());
22701 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22702 }
22703
22704 #[test]
22705 fn nll_test_controls_are_pipeline_scoped() {
22706 let ids = vec![1u32, 2, 3, 4];
22707 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22708 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22709 failing.nll_test_force_serial = true;
22710 failing.nll_test_fail_at = Some(0);
22711
22712 assert!(!failing.can_prefill_batched());
22713 assert!(unaffected.can_prefill_batched());
22714 let expected = unaffected
22715 .nll_ids_from(&ids, 0)
22716 .expect("unaffected pipeline remains usable");
22717 let err = failing
22718 .nll_ids_from(&ids, 0)
22719 .expect_err("failure injection belongs to failing pipeline");
22720 assert!(err.contains("serial forward"));
22721 assert!(failing.nll_test_fail_at.is_none());
22722 assert!(unaffected.can_prefill_batched());
22723 let actual = unaffected
22724 .nll_ids_from(&ids, 0)
22725 .expect("unaffected pipeline remains reusable");
22726 assert_eq!(actual.1, expected.1);
22727 assert!((actual.0 - expected.0).abs() < 1e-9);
22728 }
22729
22730 #[test]
22731 fn forward_ids_failure_channel_is_terminal_and_reusable() {
22732 let ids = vec![1u32, 2, 3, 4, 5, 6];
22733 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22734 p.graph_logits = Some(vec![123.0]);
22735 p.graph_want_logits = true;
22736 p.graph_failed
22737 .store(true, std::sync::atomic::Ordering::Relaxed);
22738 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22739
22740 let err = p
22741 .forward_ids(&ids, None)
22742 .expect_err("a failed forward must not become a valid head result");
22743 assert!(err.contains("forward_ids setup"));
22744 assert!(p.graph_logits.is_none());
22745 assert!(!p.graph_want_logits);
22746 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22747 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22748 assert_eq!(p.kv_cache.seq_len(), 0);
22749
22750 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
22751 .forward_ids(&ids, None)
22752 .expect("fresh forward_ids");
22753 let actual = p
22754 .forward_ids(&ids, None)
22755 .expect("pipeline remains reusable after a failed forward");
22756 assert_eq!(actual.len(), expected.len());
22757 assert!(
22758 actual
22759 .iter()
22760 .zip(expected)
22761 .all(|(a, b)| (a - b).abs() < 1e-9)
22762 );
22763 assert_eq!(p.kv_cache.seq_len(), ids.len());
22764 }
22765
22766 #[test]
22767 fn sigmoid_router_floor_is_explicit_per_architecture() {
22768 let zero = || DenseFfn {
22774 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22775 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22776 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22777 act: Act::Silu,
22778 down_t: None,
22779 segs: Vec::new(),
22780 };
22781 let m = MoeFfn {
22782 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
22783 experts: vec![zero(), zero()],
22784 top_k: 1,
22785 norm_topk_prob: true,
22786 router_sigmoid: true,
22787 sigmoid_denom_eps: 1e-20,
22788 expert_bias: None,
22789 routed_scaling: 2.5,
22790 route_tau: None,
22791 shared: None,
22792 stats: std::cell::RefCell::new(Vec::new()),
22793 act_sq: std::cell::RefCell::new(Vec::new()),
22794 act_rows: std::cell::RefCell::new(Vec::new()),
22795 mask: None,
22796 per_expert_scale: None,
22797 router_input_norm: false,
22798 resonance: None,
22799 grown: Vec::new(),
22800 };
22801 let logits = [-20.0f32, -20.0];
22802 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
22803 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
22804 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
22805 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
22806 assert!((flint_wsum - reference_expected).abs() < 1e-15);
22807 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
22808 assert!(legacy_wsum > minimax_wsum * 100.0);
22809 }
22810
22811 #[test]
22812 fn resonance_scores_match_formula_and_stable_tie() {
22813 let r = Resonance {
22814 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
22816 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
22817 k: 1,
22818 bias: vec![1.5, 0.5, 0.0],
22819 shell: Vec::new(),
22820 };
22821 let x = [1.0f32, 1.0];
22822 let mut got = vec![0.0; 3];
22823 r.scores(&x, &mut got);
22824 assert!((got[0] - 0.5).abs() < 1e-6);
22828 assert!((got[1] - 0.5).abs() < 1e-6);
22829 assert!(got[2].abs() < 1e-6);
22830 let best = got
22831 .iter()
22832 .enumerate()
22833 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
22834 .map(|(i, _)| i);
22835 assert_eq!(best, Some(0));
22836 assert!(got.iter().all(|v| v.is_finite()));
22837 }
22838
22839 #[test]
22844 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
22845 let plain = Resonance {
22851 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
22852 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
22853 k: 1,
22854 bias: vec![1.5, 0.5, 0.0, 0.0],
22855 shell: Vec::new(),
22856 };
22857 let shelled = Resonance {
22858 mu: plain.mu.clone(),
22859 u: plain.u.clone(),
22860 k: 1,
22861 bias: plain.bias.clone(),
22862 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
22863 };
22864 assert!(!plain.has_shell());
22865 assert!(shelled.has_shell());
22866 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
22867 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
22868 set_growth_shell(Some(true));
22869 assert!(growth_shell_enabled());
22870 let x = [1.0f32, 1.0];
22873 plain.scores(&x, &mut a);
22874 shelled.scores(&x, &mut b);
22875 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
22876 assert!(a[2] == 0.0 && a[3] == 0.0);
22877 let xo = [3.0f32, 0.0];
22880 plain.scores(&xo, &mut a);
22881 shelled.scores(&xo, &mut b);
22882 assert_eq!(a[2], -6.0);
22883 assert_eq!(a[3], -6.0);
22884 assert_eq!(
22885 bits(&a[..3]),
22886 bits(&b[..3]),
22887 "trunk rows + the boundary row"
22888 );
22889 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
22890 assert_eq!(shelled.effective_shell(4), shelled.shell);
22891 set_growth_shell(Some(false));
22894 assert!(!growth_shell_enabled());
22895 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
22896 shelled.scores(&xo, &mut b);
22897 assert_eq!(bits(&a), bits(&b));
22898 set_growth_shell(None);
22899 let short = Resonance {
22902 shell: vec![f32::INFINITY, f32::INFINITY],
22903 ..shelled
22904 };
22905 set_growth_shell(Some(true));
22906 short.scores(&xo, &mut b);
22907 assert_eq!(bits(&a), bits(&b));
22908 set_growth_shell(None);
22909 }
22910
22911 #[test]
22915 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
22916 let zero = || DenseFfn {
22917 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22918 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22919 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22920 act: Act::Silu,
22921 down_t: None,
22922 segs: Vec::new(),
22923 };
22924 let moe = |sigmoid: bool| MoeFfn {
22925 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
22926 experts: vec![zero(), zero(), zero(), zero()],
22927 top_k: 1,
22928 norm_topk_prob: true,
22929 router_sigmoid: sigmoid,
22930 sigmoid_denom_eps: 1e-6,
22931 expert_bias: None,
22932 routed_scaling: 1.0,
22933 route_tau: None,
22934 shared: None,
22935 stats: std::cell::RefCell::new(Vec::new()),
22936 act_sq: std::cell::RefCell::new(Vec::new()),
22937 act_rows: std::cell::RefCell::new(Vec::new()),
22938 mask: None,
22939 per_expert_scale: None,
22940 router_input_norm: false,
22941 resonance: None,
22942 grown: Vec::new(),
22943 };
22944 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
22945 for sigmoid in [false, true] {
22946 let m = moe(sigmoid);
22947 let (idx, p, wsum) = moe_route(&logits, &m, None);
22948 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
22949 assert_eq!(p[1], 0.0);
22950 assert_eq!(p[3], 0.0);
22951 assert!(p[2] > p[0] && p[0] > 0.0);
22952 assert!(p.iter().all(|v| v.is_finite()));
22953 let w = p[2] / wsum;
22954 if sigmoid {
22955 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
22957 } else {
22958 assert_eq!(
22959 w, 1.0,
22960 "softmax: the renormalized top-1 weight is exactly 1"
22961 );
22962 }
22963 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
22966 assert_eq!(idx, vec![2]);
22967 }
22968 let m = moe(false);
22970 let (idx, _, _) = moe_route(
22971 &[
22972 f32::NEG_INFINITY,
22973 f32::NEG_INFINITY,
22974 f32::NEG_INFINITY,
22975 -9.0,
22976 ],
22977 &m,
22978 None,
22979 );
22980 assert_eq!(idx, vec![3]);
22981 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
22984 assert_eq!(idx, vec![0]);
22985 assert!(p.iter().all(|&v| v == 0.25));
22986 assert!(wsum.is_finite() && wsum > 0.0);
22987 }
22988
22989 #[cfg(feature = "gpu")]
22995 #[test]
22996 fn minimax_device_route_matches_host_sigmoid_contract() {
22997 match crate::gpu_wgpu::selected_and_up() {
22998 None => {
22999 crate::gpu_wgpu::skip_or_fail(module_path!());
23000 return;
23001 }
23002 Some(false) => panic!("wgpu requested but the context did not come up"),
23003 Some(true) => {}
23004 }
23005 let n = 128usize;
23006 let top_k = 4usize;
23007 let logits: Vec<f32> = (0..n)
23008 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23009 .collect();
23010 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23011 let m = MoeFfn {
23012 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23013 experts: Vec::new(),
23014 top_k,
23015 norm_topk_prob: true,
23016 router_sigmoid: true,
23017 sigmoid_denom_eps: 1e-20,
23018 expert_bias: Some(bias.clone()),
23019 routed_scaling: 2.0,
23020 route_tau: None,
23021 shared: None,
23022 stats: std::cell::RefCell::new(Vec::new()),
23023 act_sq: std::cell::RefCell::new(Vec::new()),
23024 act_rows: std::cell::RefCell::new(Vec::new()),
23025 mask: None,
23026 per_expert_scale: None,
23027 router_input_norm: false,
23028 resonance: None,
23029 grown: Vec::new(),
23030 };
23031 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23032 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
23033
23034 for shared in [Some(n as u32), None] {
23035 let mut gi = Vec::new();
23036 let mut gw = Vec::new();
23037 assert!(
23038 crate::gpu_wgpu::minimax_moe_route_for_test(
23039 &logits,
23040 Some(&bias),
23041 top_k,
23042 2.0,
23043 shared,
23044 &mut gi,
23045 &mut gw,
23046 ),
23047 "устройство не поднялось — тест не проверил ничего"
23048 );
23049 let routed = if shared.is_some() { top_k } else { gi.len() };
23050 assert_eq!(
23051 &gi[..routed],
23052 &ci[..],
23053 "разные эксперты или порядок (shared={shared:?})"
23054 );
23055 let d = cw
23056 .iter()
23057 .zip(&gw[..routed])
23058 .map(|(a, b)| (a - b).abs())
23059 .fold(0.0f32, f32::max);
23060 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
23061 assert!(d < 1e-6, "веса разошлись на {d:.3e}");
23062 if let Some(s) = shared {
23063 assert_eq!(gi[top_k], s as usize, "shared pin slot");
23064 assert_eq!(gw[top_k], 1.0, "shared weight");
23065 }
23066 }
23067 }
23068
23069 #[cfg(feature = "gpu")]
23074 #[test]
23075 fn minimax_device_cold_weights_match_host_contract() {
23076 match crate::gpu_wgpu::selected_and_up() {
23077 None => {
23078 crate::gpu_wgpu::skip_or_fail(module_path!());
23079 return;
23080 }
23081 Some(false) => panic!("wgpu requested but the context did not come up"),
23082 Some(true) => {}
23083 }
23084 let n = 128usize;
23085 let top_k = 4usize;
23086 let logits: Vec<f32> = (0..n)
23087 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23088 .collect();
23089 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23090 let m = MoeFfn {
23091 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23092 experts: Vec::new(),
23093 top_k,
23094 norm_topk_prob: true,
23095 router_sigmoid: true,
23096 sigmoid_denom_eps: 1e-20,
23097 expert_bias: Some(bias.clone()),
23098 routed_scaling: 2.0,
23099 route_tau: None,
23100 shared: None,
23101 stats: std::cell::RefCell::new(Vec::new()),
23102 act_sq: std::cell::RefCell::new(Vec::new()),
23103 act_rows: std::cell::RefCell::new(Vec::new()),
23104 mask: None,
23105 per_expert_scale: None,
23106 router_input_norm: false,
23107 resonance: None,
23108 grown: Vec::new(),
23109 };
23110 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23111 assert_eq!(ci.len(), top_k);
23112
23113 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
23115 let mut cold = Vec::new();
23116 let mut idx = Vec::new();
23117 let mut dw = Vec::new();
23118 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
23119 &logits,
23120 Some(&bias),
23121 top_k,
23122 2.0,
23123 &resident,
23124 n as u32,
23125 &mut cold,
23126 &mut idx,
23127 &mut dw,
23128 ));
23129 assert!(cold.is_empty(), "полностью резидентный банк: {cold:?}");
23130 for (r, &e) in ci.iter().enumerate() {
23135 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
23136 let host = p[e] / wsum;
23137 let d = (dw[r] - host).abs();
23138 assert!(
23139 d <= 1e-6 * host.abs().max(1.0),
23140 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
23141 dw[r]
23142 );
23143 }
23144 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
23145 assert_eq!(dw[top_k], 1.0, "shared weight");
23146
23147 let mut remap = resident.clone();
23149 remap[ci[0]] = u32::MAX;
23150 remap[ci[2]] = u32::MAX;
23151 let mut cold = Vec::new();
23152 let mut idx2 = Vec::new();
23153 let mut dw2 = Vec::new();
23154 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
23155 &logits,
23156 Some(&bias),
23157 top_k,
23158 2.0,
23159 &remap,
23160 n as u32,
23161 &mut cold,
23162 &mut idx2,
23163 &mut dw2,
23164 ));
23165 let mut resident_weight_checked = 0usize;
23173 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
23174 if dw2[r] == 0.0 {
23175 continue; }
23177 let e = slot as usize;
23178 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
23179 let host = p[e] / wsum;
23180 let d = (dw2[r] - host).abs();
23181 assert!(
23182 d <= 1e-6 * host.abs().max(1.0),
23183 "partial-bank resident weight expert {e}: Δ={d:.3e}"
23184 );
23185 resident_weight_checked += 1;
23186 }
23187 assert_eq!(resident_weight_checked, 2, "ровно два резидентных пика из четырёх");
23188 println!("partial bank: checked {resident_weight_checked} resident weights");
23189 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
23190 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
23191 ids.sort_unstable();
23192 let mut want = vec![ci[0], ci[2]];
23193 want.sort_unstable();
23194 assert_eq!(ids, want, "холодные эксперты разошлись: {cold:?} против {want:?}");
23195 for &(e, w) in &cold {
23196 let host = p[e] / wsum;
23197 let d = (w - host).abs();
23198 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
23199 assert!(d <= 1e-6 * host.abs().max(1.0), "cold вес эксперта {e}: Δ={d:.3e}");
23200 }
23201 }
23202
23203 #[test]
23208 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
23209 let zero = || DenseFfn {
23210 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23211 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23212 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23213 act: Act::Silu,
23214 down_t: None,
23215 segs: Vec::new(),
23216 };
23217 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
23218 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
23219 experts: vec![zero(), zero(), zero()],
23220 top_k: 1,
23221 norm_topk_prob: norm_topk,
23222 router_sigmoid: false,
23223 sigmoid_denom_eps: 1e-6,
23224 expert_bias: None,
23225 routed_scaling: 1.0,
23226 route_tau: None,
23227 shared: None,
23228 stats: std::cell::RefCell::new(Vec::new()),
23229 act_sq: std::cell::RefCell::new(Vec::new()),
23230 act_rows: std::cell::RefCell::new(Vec::new()),
23231 mask: None,
23232 per_expert_scale: None,
23233 router_input_norm: false,
23234 resonance: resonant.then(|| Resonance {
23235 mu: vec![0.0; 6],
23236 u: Vec::new(),
23237 k: 0,
23238 bias: vec![0.0; 3],
23239 shell: Vec::new(),
23240 }),
23241 grown: Vec::new(),
23242 };
23243 let lo = -0.1f32;
23246 let hi = f32::from_bits(lo.to_bits() - 1);
23247 assert!(hi > lo && hi - lo < 2f32.powi(-25));
23248 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
23249 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
23251 assert_eq!(
23252 idx,
23253 vec![0],
23254 "softmax tie → lower index (the gated contract)"
23255 );
23256 for norm in [true, false] {
23259 let m = moe(true, norm);
23260 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
23261 assert_eq!(idx, vec![1], "norm_topk {norm}");
23262 assert_eq!(p, vec![0.0, 1.0, 0.0]);
23263 assert_eq!(p[1] / wsum, 1.0);
23264 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
23267 assert_eq!(idx, vec![0]);
23268 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
23270 assert_eq!(idx, vec![2]);
23271 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
23272 assert_eq!(idx, vec![0]);
23273 assert_eq!(p[0] / wsum, 1.0);
23274 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
23277 assert_eq!(idx, vec![0]);
23278 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
23279 }
23280 }
23281}