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 g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
151 pub dsv4: Option<
155 Box<(
156 crate::dsv4::Dsv4Globals,
157 Vec<crate::dsv4::Dsv4Layer>,
158 crate::dsv4::Dsv4Cfg,
159 crate::dsv4::Dsv4State,
160 )>,
161 >,
162 pub dsv41: Option<
166 Box<(
167 crate::dsv41::Dsv41Globals,
168 Vec<crate::dsv41::Dsv41Layer>,
169 crate::dsv41::Dsv41Cfg,
170 crate::dsv41::Dsv41State,
171 )>,
172 >,
173 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
175 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
177 pub qwen4_exp: Option<
180 Box<(
181 crate::qwen4_exp::Globals,
182 Vec<crate::qwen4_exp::Layer>,
183 crate::qwen4_exp::Cfg,
184 crate::qwen4_exp::State,
185 )>,
186 >,
187 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
191 pub dspark: Option<crate::dsv4::DsparkState>,
193 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
196 pub dspark_hist: Vec<usize>,
198 pub dspark_real: Vec<u32>,
202 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
206 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
208 pub dspark_draft_ns: u128,
212 m3_winners: Vec<Vec<u32>>,
214 m3_counts: Vec<u32>,
216 m3_n_exp: usize,
217 m3_tokens: u64,
218 m3_prewarmed: bool,
219 pub short_conv_cfg: Option<ShortConvCfg>,
222 pub mtp: Option<MtpModule>,
224 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
228 verify_exact_moe: bool,
231 pub speculative: bool,
233 pub ignore_eos: bool,
239 pub draft_full_streak: u32,
245 pub spec_k_adapt: Option<usize>,
253 pub spec_acc_ewma: f32,
255 rng: SplitMix64,
256 sampler_scratch: SamplerScratch,
257 spec_forced: Option<u32>,
263 spec_q: Vec<Vec<f32>>,
264 spec_p: Vec<f32>,
265 spec_res: Vec<f32>,
266 spec_qs: Vec<sampler::Sparse>,
268 spec_ps: sampler::Sparse,
269 spec_ress: sampler::Sparse,
270 mtp_graph_mode: Option<bool>,
277 #[cfg(target_os = "macos")]
280 metal_verify: Option<MetalVerifyPending>,
281 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
285 ws: ForwardScratch,
289 pool: Option<std::sync::Arc<Pool>>,
291 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
295 pub(crate) dyn_force_f32: bool,
297 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
302 pub(crate) dyn_active: Option<usize>,
308 pub(crate) dyn_blend_loaded: bool,
312 pub(crate) dyn_phi_layer: Option<usize>,
315 dyn_phi_ema: Vec<f32>,
317 dyn_phi_seen: usize,
318 pub dyn_router: Option<crate::swarm::DynRouter>,
321 o1_cfg: Option<crate::nystrom::O1Cfg>,
324 o1_epoch: u64,
327 o1_flags: Vec<bool>,
329 trace: bool,
332 calib_temp: f32,
335 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
337 graph_kv_id: u64,
338 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
341 graph_want_logits: bool,
342 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
346 graph_head_required: bool,
347 graph_logits: Option<Vec<f32>>,
350 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
353 graph_refused: std::sync::atomic::AtomicBool,
359 pub embed_multiplier: f32,
361 pub attn_scale: f32,
364 pub swa: Option<(usize, usize)>,
367 pub swa_trim: Option<(usize, usize)>,
374 pub sliding_layers: Option<Vec<bool>>,
377 pub no_rope_layers: Option<Vec<bool>>,
382 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
388 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
391 pub kv_prefix: KvPrefix,
395 pub last_prefill_tokens: usize,
398 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
401 pub rotary_dim_local: Option<usize>,
402 pub rope_scale: f32,
403 pub rope_scale_local: f32,
404 pub global_attn: Option<(usize, usize)>,
407 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
410 pub attn_v_norm: bool,
412 pub qk_norm_after_rope: bool,
414 pub proj_gate_sigmoid: bool,
417 pub final_softcap: Option<f32>,
419 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
423 pub attn_softcap: f32,
425 confidence_on: bool,
429 #[cfg(test)]
432 nll_test_fail_at: Option<usize>,
433 #[cfg(test)]
436 nll_test_force_serial: bool,
437}
438
439#[cfg(target_os = "macos")]
440impl Drop for Pipeline {
441 fn drop(&mut self) {
442 let _ = crate::gpu_metal::wait_replay();
444 crate::gpu::kv_mirror_drop(self.graph_kv_id);
445 }
446}
447
448#[cfg(not(target_os = "macos"))]
449impl Drop for Pipeline {
450 fn drop(&mut self) {
451 crate::gpu::graph_kv_reset(self.graph_kv_id);
455 }
456}
457
458pub struct PipelineWeights {
463 pub embed_tokens: QTensor,
465 pub layers: Vec<LayerWeights>,
467 pub lm_head: QTensor,
469 pub final_norm: Vec<f32>,
471}
472
473pub struct LayerWeights {
475 pub input_norm: Vec<f32>,
476 pub post_norm: Vec<f32>,
479 pub attn_out_norm: Option<Vec<f32>>,
482 pub layer_scale: Option<f32>,
484 pub ffn_out_norm: Option<Vec<f32>>,
487 pub ffn: FfnKind,
488 pub attn: AttnKind,
489}
490
491#[derive(Clone, Copy, PartialEq, Debug, Default)]
495pub enum Act {
496 #[default]
497 Silu,
498 GeluTanh,
499 Gelu,
501 Situ {
504 beta: f32,
505 linear_beta: f32,
506 },
507 SwiGluOai {
512 alpha: f32,
513 limit: f32,
514 },
515}
516
517impl Act {
518 pub fn from_arch(name: &str) -> Self {
519 if name == "gelu_tanh" {
520 Self::GeluTanh
521 } else if name == "gelu" {
522 Self::Gelu
523 } else {
524 Self::Silu
525 }
526 }
527
528 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
530 match arch.hidden_act.as_str() {
531 "situ" => Self::Situ {
532 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
533 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
534 },
535 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
540 alpha: 1.702,
541 limit: 7.0,
542 },
543 other => Self::from_arch(other),
544 }
545 }
546
547 #[inline]
552 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
553 Self::SwiGluOai { alpha, limit }
554 }
555
556 #[inline]
557 pub fn apply(self, x: f32) -> f32 {
558 match self {
559 Self::Silu => inference::silu(x),
560 Self::GeluTanh => inference::gelu_tanh(x),
561 Self::Gelu => inference::gelu_erf(x),
562 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
563 Self::SwiGluOai { alpha, limit } => {
564 let gate = x.min(limit);
565 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
566 }
567 }
568 }
569
570 #[inline]
573 pub fn combine(self, g: f32, u: f32) -> f32 {
574 match self {
575 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
576 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
577 }
578 Self::SwiGluOai { alpha, limit } => {
579 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
580 }
581 _ => self.apply(g) * u,
582 }
583 }
584
585 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
589 match self {
590 Self::Silu => Some(crate::gpu::GraphAct::Silu),
591 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
592 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
593 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
594 }
595 }
596
597 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
603 self.moe_job_graph_contract().map(|(act, _)| act)
604 }
605
606 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
612 match self {
613 Self::SwiGluOai { alpha, limit }
614 if alpha.to_bits() == 1.702f32.to_bits()
615 && limit.to_bits() == 7.0f32.to_bits() =>
616 {
617 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
618 }
619 _ => self.graph_act().map(|act| (act, 0.0)),
620 }
621 }
622}
623
624pub struct DenseFfn {
626 pub gate_proj: QTensor,
627 pub up_proj: QTensor,
628 pub down_proj: QTensor,
629 pub act: Act,
631 pub down_t: Option<QTensor>,
637 pub segs: Vec<FfnSeg>,
644}
645
646pub struct FfnSeg {
651 pub gate: QTensor,
652 pub up: QTensor,
653 pub down: QTensor,
654 pub start: usize,
655 pub width: usize,
656}
657
658pub enum FfnKind {
661 Dense(DenseFfn),
662 Moe(MoeFfn),
666 DenseMoe(Box<DenseMoeFfn>),
673}
674
675pub struct DenseMoeFfn {
677 pub dense: DenseFfn,
678 pub moe: MoeFfn,
679 pub post_norm_1: Vec<f32>,
681 pub pre_norm_2: Vec<f32>,
684 pub post_norm_2: Vec<f32>,
686}
687
688pub struct MoeFfn {
689 pub router: QTensor,
691 pub experts: Vec<DenseFfn>,
692 pub top_k: usize,
693 pub norm_topk_prob: bool,
694 pub router_sigmoid: bool,
697 pub sigmoid_denom_eps: f32,
703 pub expert_bias: Option<Vec<f32>>,
707 pub routed_scaling: f32,
710 pub route_tau: Option<f32>,
716 pub shared: Option<(DenseFfn, Option<QTensor>)>,
719 pub stats: std::cell::RefCell<Vec<u64>>,
723 pub act_sq: std::cell::RefCell<Vec<f64>>,
730 pub act_rows: std::cell::RefCell<Vec<f32>>,
736 pub mask: Option<Vec<bool>>,
741 pub per_expert_scale: Option<Vec<f32>>,
744 pub router_input_norm: bool,
748 pub resonance: Option<Resonance>,
752 pub grown: Vec<GrownExpert>,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq)]
762pub struct GrownExpert {
763 pub record: String,
765 pub record_index: usize,
767 pub layer: usize,
768 pub expert: usize,
772}
773
774pub struct Resonance {
776 pub mu: Vec<f32>,
778 pub u: Vec<f32>,
780 pub k: usize,
781 pub bias: Vec<f32>,
783 pub shell: Vec<f32>,
789}
790
791static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
794
795pub fn growth_shell_enabled() -> bool {
800 use std::sync::atomic::Ordering;
801 match GROWTH_SHELL.load(Ordering::Relaxed) {
802 1 => true,
803 2 => false,
804 _ => {
805 let off = std::env::var("CMF_GROWTH_SHELL")
806 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
807 .unwrap_or(false);
808 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
809 !off
810 }
811 }
812}
813
814pub fn set_growth_shell(on: Option<bool>) {
819 GROWTH_SHELL.store(
820 match on {
821 Some(true) => 1,
822 Some(false) => 2,
823 None => 0,
824 },
825 std::sync::atomic::Ordering::Relaxed,
826 );
827}
828
829impl Resonance {
830 pub fn has_shell(&self) -> bool {
832 self.shell.iter().any(|s| s.is_finite())
833 }
834
835 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
838 let mut out = vec![f32::INFINITY; ne];
839 if growth_shell_enabled() {
840 for (o, s) in out.iter_mut().zip(&self.shell) {
841 *o = *s;
842 }
843 }
844 out
845 }
846
847 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
852 let h = x.len();
853 let ne = out.len();
854 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
855 for e in 0..ne {
856 let mu = &self.mu[e * h..(e + 1) * h];
857 let mut d2 = 0.0f32;
858 for j in 0..h {
859 let d = x[j] - mu[j];
860 d2 += d * d;
861 }
862 let mut proj = 0.0f32;
863 for i in 0..self.k {
864 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
865 let mut p = 0.0f32;
866 for j in 0..h {
867 p += (x[j] - mu[j]) * u[j];
868 }
869 proj += p * p;
870 }
871 let err = d2 - proj;
872 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
873 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
874 out[e] = f32::NEG_INFINITY;
875 }
876 }
877 }
878}
879
880pub enum AttnKind {
883 Full {
885 wq: QTensor,
886 wk: QTensor,
887 wv: QTensor,
888 wo: QTensor,
889 q_norm: Option<Vec<f32>>,
890 k_norm: Option<Vec<f32>>,
891 output_gate: bool,
892 softplus_gate: Option<(QTensor, bool)>,
896 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
898 },
899 Linear(VmfPhaseWeights),
901 LinearGdn(GdnWeights),
903 ShortConv(ShortConvWeights),
906 Mla(Box<MlaWeights>),
914 Kda(Box<crate::linear_core::KdaWeights>),
918 Bounded(Box<crate::bounded::BoundedWeights>),
923}
924
925pub struct MlaWeights {
927 pub q_proj: QTensor,
931 pub q_a: Option<QTensor>,
934 pub q_a_norm: Option<Vec<f32>>,
935 pub kv_a: QTensor,
937 pub kv_a_norm: Vec<f32>,
939 pub kv_b: QTensor,
941 pub o_proj: QTensor,
943 pub nh: usize,
944 pub qk_rope: usize,
945 pub qk_nope: usize,
946 pub v_dim: usize,
947 pub lora: usize,
948 pub scale: f32,
950 pub nope: bool,
952}
953
954pub struct MtpModule {
959 pub enorm: Vec<f32>,
960 pub hnorm: Vec<f32>,
961 pub eh_proj: QTensor,
963 pub layer: LayerWeights,
964 pub final_norm: Vec<f32>,
965 pub kv: crate::kv_cache::LayerKvCache,
966}
967
968#[cfg(target_os = "macos")]
975enum MetalRowsItem<'a> {
976 Gdn {
977 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
978 first: usize,
979 },
980 Attn {
981 l: crate::gpu_metal::AttnGpuLayer<'a>,
982 li: usize,
983 q_norm: Option<&'a [f32]>,
984 k_norm: Option<&'a [f32]>,
985 output_gate: bool,
986 },
987}
988
989#[cfg(target_os = "macos")]
990struct MetalVerifyPending {
991 graph: crate::gpu_metal::VerifyGraph,
992 gdn_layers: Vec<usize>,
993 attn_layers: Vec<(usize, usize)>,
994}
995
996#[cfg(target_os = "macos")]
1000struct MetalWarmPending {
1001 graph: crate::gpu_metal::VerifyGraph,
1002 cpu_stored: usize,
1003 b: usize,
1004}
1005
1006#[cfg(target_os = "macos")]
1007enum MetalRowsRun {
1008 Declined,
1010 Failed,
1013 Completed(MetalVerifyPending),
1014}
1015
1016#[cfg(target_os = "macos")]
1017enum MetalPrefillOutcome {
1018 Declined,
1019 Failed,
1020 Completed(Vec<f32>),
1021}
1022
1023#[cfg(target_os = "macos")]
1024enum MetalBatchNllOutcome {
1025 Declined,
1026 Failed(String),
1027 Completed(f64, usize),
1028}
1029
1030#[derive(Clone, Copy)]
1034enum SpecTrial {
1035 Spec {
1036 t0: std::time::Instant,
1037 gen0: usize,
1038 rounds: usize,
1039 },
1040 Plain {
1041 t0: std::time::Instant,
1042 gen0: usize,
1043 },
1044 Decided {
1045 spec: bool,
1046 recheck_at: usize,
1047 },
1048}
1049
1050pub(crate) fn spec_time_level() -> u8 {
1054 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1055 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1056 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1057 Err(_) => 0,
1058 })
1059}
1060
1061struct SpecStampLog {
1067 t_last: std::time::Instant,
1068 items: Vec<(&'static str, f32)>,
1069}
1070
1071static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1072
1073pub(crate) fn spec_stamp(name: &'static str) {
1074 if spec_time_level() == 0 {
1075 return;
1076 }
1077 if let Ok(mut g) = SPEC_STAMPS.lock() {
1078 if let Some(log) = g.as_mut() {
1079 let now = std::time::Instant::now();
1080 log.items
1081 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1082 log.t_last = now;
1083 }
1084 }
1085}
1086
1087fn spec_stamps_begin() {
1088 if spec_time_level() == 0 {
1089 return;
1090 }
1091 if let Ok(mut g) = SPEC_STAMPS.lock() {
1092 *g = Some(SpecStampLog {
1093 t_last: std::time::Instant::now(),
1094 items: Vec::with_capacity(64),
1095 });
1096 }
1097}
1098
1099fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1100 SPEC_STAMPS
1101 .lock()
1102 .ok()
1103 .and_then(|mut g| g.take())
1104 .map(|l| l.items)
1105 .unwrap_or_default()
1106}
1107
1108fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1111 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1112 for &(n, ms) in items {
1113 match agg.iter_mut().find(|e| e.0 == n) {
1114 Some(e) => {
1115 e.1 += ms;
1116 e.2 += 1;
1117 }
1118 None => agg.push((n, ms, 1)),
1119 }
1120 }
1121 let mut s = String::with_capacity(agg.len() * 16);
1122 for (n, ms, k) in agg {
1123 if k > 1 {
1124 s.push_str(&format!("{n} {ms:.1}/{k} "));
1125 } else {
1126 s.push_str(&format!("{n} {ms:.1} "));
1127 }
1128 }
1129 s
1130}
1131
1132#[derive(Default, Clone, Copy)]
1154struct SpecMon {
1155 round_ms: f64,
1156 tokens: f64,
1157 plain_ms: f64,
1158 n: u32,
1159 fails: u32,
1160 metal: bool,
1161}
1162
1163const SPEC_PROXY_TOKENS: f64 = 3.5;
1166const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1169
1170impl SpecMon {
1171 fn round(&mut self, dt_ms: f64, produced: usize) {
1172 self.n += 1;
1173 if self.n == 1 {
1174 return; }
1176 let a = if self.n == 2 { 1.0 } else { 0.3 };
1177 self.round_ms += a * (dt_ms - self.round_ms);
1178 self.tokens += a * (produced as f64 - self.tokens);
1179 }
1180 fn pays(&self) -> bool {
1181 if self.plain_ms > 0.0 {
1182 self.tokens * self.plain_ms > self.round_ms * 1.03
1183 } else {
1184 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1185 }
1186 }
1187 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1189 let n = generated.saturating_sub(gen0);
1190 if n >= 8 {
1191 return true;
1192 }
1193 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1194 }
1195}
1196
1197pub const KV_PREFIX_TAIL: usize = 128;
1200
1201#[derive(Debug, Clone, Default)]
1207pub struct KvPrefix {
1208 len: usize,
1209 hash: u64,
1210 tail: Vec<u32>,
1211 device: bool,
1216}
1217
1218impl KvPrefix {
1219 #[inline]
1220 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1221 for &id in ids {
1222 h ^= id as u64;
1223 h = h.wrapping_mul(0x100000001b3);
1224 h ^= h >> 29;
1225 }
1226 h
1227 }
1228
1229 pub fn clear(&mut self) {
1230 self.len = 0;
1231 self.hash = 0xcbf29ce484222325;
1232 self.tail.clear();
1233 self.device = false;
1234 }
1235
1236 pub fn on_device(&self) -> bool {
1238 self.device
1239 }
1240
1241 pub fn set_on_device(&mut self, device: bool) {
1243 self.device = device;
1244 }
1245
1246 pub fn len(&self) -> usize {
1248 self.len
1249 }
1250
1251 pub fn is_empty(&self) -> bool {
1252 self.len == 0
1253 }
1254
1255 pub fn tail_len(&self) -> usize {
1257 self.tail.len()
1258 }
1259
1260 pub fn set(&mut self, ids: &[u32]) {
1262 self.clear();
1263 self.extend(ids);
1264 }
1265
1266 pub fn extend(&mut self, more: &[u32]) {
1268 if self.len == 0 && self.hash == 0 {
1269 self.hash = 0xcbf29ce484222325;
1270 }
1271 self.hash = Self::fold(self.hash, more);
1272 self.len += more.len();
1273 if more.len() >= KV_PREFIX_TAIL {
1274 self.tail.clear();
1275 self.tail
1276 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1277 } else {
1278 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1279 self.tail.drain(..drop);
1280 self.tail.extend_from_slice(more);
1281 }
1282 }
1283
1284 pub fn extension(&self, ids: &[u32]) -> usize {
1288 if self.len == 0 || ids.len() <= self.len {
1289 return 0;
1290 }
1291 let t = self.tail.len();
1292 if ids[self.len - t..self.len] != self.tail[..] {
1293 return 0;
1294 }
1295 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1296 return 0;
1297 }
1298 self.len
1299 }
1300}
1301
1302pub struct GenerateResult {
1304 pub text: String,
1305 pub token_ids: Vec<u32>,
1306 pub prompt_tokens: usize,
1307 pub tokens_generated: usize,
1308 pub finish_reason: String,
1309 pub mtp_drafted: usize,
1311 pub mtp_accepted: usize,
1312 pub token_confidence: Vec<f32>,
1317 pub traces: Vec<TokenTrace>,
1320}
1321
1322#[derive(Clone, Debug)]
1327pub struct TokenTrace {
1328 pub t: usize,
1330 pub token_id: u32,
1332 pub confidence: f32,
1334 pub active_skill: Option<String>,
1336 pub recon: Option<f32>,
1340 pub switched: bool,
1343}
1344
1345#[cfg_attr(not(test), allow(dead_code))]
1350fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1351 let t = if temp > 1e-3 { temp } else { 1.0 };
1352 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1353 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1354 if sum > 0.0 {
1355 (((logits[id as usize] - max) / t).exp()) / sum
1356 } else {
1357 0.0
1358 }
1359}
1360
1361fn prefill_batched() -> bool {
1364 std::env::var("CMF_PREFILL")
1365 .map(|v| v != "seq")
1366 .unwrap_or(true)
1367}
1368
1369#[inline]
1373fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1374 let graph_quality = unmasked && prefer_graph;
1375 let fused_head_quality = graph_quality && native_metal;
1376 (graph_quality, fused_head_quality)
1377}
1378
1379#[derive(Clone, Copy)]
1383enum PrefillIn<'a> {
1384 Ids(&'a [u32]),
1385 Hidden(&'a [f32]),
1386}
1387
1388impl Pipeline {
1395 fn can_prefill_batched(&self) -> bool {
1396 #[cfg(test)]
1397 let force_serial = self.nll_test_force_serial;
1398 #[cfg(not(test))]
1399 let force_serial = false;
1400 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1401 }
1402
1403 fn automatic_gpu_prefix(&self) -> Option<usize> {
1406 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1407 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1408 }
1409
1410 pub fn prefill_chunk(&self) -> usize {
1414 let env = env_prefill_chunk();
1415 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1416 return prefill_chunk_rule(env, ChunkHost::here(), false);
1417 }
1418 prefill_chunk_rule(
1419 None,
1420 ChunkHost::Other,
1421 self.chunk_stack_facts().dense_on_discrete(),
1422 )
1423 }
1424
1425 fn per_token_stack(&self) -> bool {
1429 self.g3n.is_some() || self.g4e.is_some()
1430 }
1431
1432 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1433 let plain_dense = !self.weights.layers.is_empty()
1434 && !self.per_token_stack()
1435 && self.dsv4.is_none()
1436 && self.dsv41.is_none()
1437 && self.qwen4_exp.is_none()
1438 && self.weights.layers.iter().all(|lw| {
1439 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1440 });
1441 let gpu_on = crate::gpu::enabled();
1442 ChunkStackFacts {
1443 plain_dense,
1444 discrete: gpu_on && crate::gpu::discrete(),
1445 gpu_on,
1446 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1449 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1450 multi_gpu: self.gpu_plan.is_some(),
1451 o1: self.o1_active(),
1452 }
1453 }
1454}
1455
1456pub fn prefill_chunk() -> usize {
1465 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1466}
1467
1468fn env_prefill_chunk() -> Option<usize> {
1469 std::env::var("CMF_PREFILL_CHUNK")
1470 .ok()
1471 .and_then(|v| v.parse::<usize>().ok())
1472}
1473
1474#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1476enum ChunkHost {
1477 Macos,
1478 Aarch64,
1480 Other,
1482}
1483
1484impl ChunkHost {
1485 fn here() -> Self {
1486 if cfg!(target_os = "macos") {
1487 ChunkHost::Macos
1488 } else if cfg!(target_arch = "aarch64") {
1489 ChunkHost::Aarch64
1490 } else {
1491 ChunkHost::Other
1492 }
1493 }
1494}
1495
1496const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1503
1504fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1510 if let Some(n) = env {
1511 return n.max(1);
1512 }
1513 match host {
1514 ChunkHost::Macos => 512,
1515 ChunkHost::Aarch64 => 256,
1518 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1519 ChunkHost::Other => 48,
1520 }
1521}
1522
1523#[derive(Clone, Copy, Debug, Default)]
1525struct ChunkStackFacts {
1526 plain_dense: bool,
1529 discrete: bool,
1531 gpu_on: bool,
1533 capacity_split: bool,
1535 multi_gpu: bool,
1537 o1: bool,
1539}
1540
1541impl ChunkStackFacts {
1542 fn dense_on_discrete(self) -> bool {
1543 self.plain_dense
1544 && self.discrete
1545 && self.gpu_on
1546 && !self.capacity_split
1547 && !self.multi_gpu
1548 && !self.o1
1549 }
1550}
1551
1552#[inline]
1558fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1559 if end <= start || start >= input_len {
1560 return 0;
1561 }
1562 let rows = (end.min(input_len) - start).min(input_len - start);
1563 if end < input_len {
1564 rows
1565 } else {
1566 rows.saturating_sub(1)
1567 }
1568}
1569
1570pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1572
1573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1575pub(crate) struct ReuseLayer {
1576 pub full: bool,
1579 pub host_rows: usize,
1582 pub device_rows: Option<usize>,
1584 pub device_state: bool,
1586}
1587
1588#[derive(Debug, Clone, PartialEq, Eq)]
1590pub(crate) enum ReusePlan {
1591 Ready,
1593 Pull(Vec<(usize, usize, usize)>),
1596 Fresh,
1598}
1599
1600pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1610 let mut pulls = Vec::new();
1611 for (li, l) in layers.iter().enumerate() {
1612 if !l.full {
1613 if l.device_state {
1614 return ReusePlan::Fresh;
1615 }
1616 continue;
1617 }
1618 if l.host_rows == reuse_from {
1619 continue;
1620 }
1621 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1622 pulls.push((li, l.host_rows, reuse_from));
1623 continue;
1624 }
1625 return ReusePlan::Fresh;
1626 }
1627 if pulls.is_empty() {
1628 ReusePlan::Ready
1629 } else {
1630 ReusePlan::Pull(pulls)
1631 }
1632}
1633
1634impl Pipeline {
1635 fn clear_sequence_state(&mut self) {
1643 #[cfg(target_os = "macos")]
1646 let _ = crate::gpu_metal::wait_replay();
1647 self.kv_cache.clear();
1648 self.clear_history();
1651 self.graph_logits = None;
1652 if let Some(b) = &mut self.dsv41 {
1653 b.3.clear();
1654 }
1655 crate::gpu::graph_kv_reset(self.graph_kv_id);
1656 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1661 }
1662
1663 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1670 if self.graph_prefill_preferred() {
1671 return true;
1672 }
1673 let kv_id = self.graph_kv_id;
1674 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1675 .map(|li| {
1676 let full = matches!(
1677 self.weights.layers[self.phys_layer(li)].attn,
1678 AttnKind::Full { .. }
1679 );
1680 ReuseLayer {
1681 full,
1682 host_rows: self.kv_cache.layers[li].pos_len(),
1685 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1686 device_state: crate::gpu::graph_state_resident(kv_id, li),
1687 }
1688 })
1689 .collect();
1690 if layers
1694 .iter()
1695 .all(|l| l.device_rows.is_none() && !l.device_state)
1696 {
1697 return true;
1698 }
1699 let plan = kv_reuse_plan(reuse_from, &layers);
1700 let (what, rows, n) = match &plan {
1701 ReusePlan::Ready => ("host ready", 0, 0),
1702 ReusePlan::Fresh => ("fresh", 0, 0),
1703 ReusePlan::Pull(p) => (
1704 "pull",
1705 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1706 p.len(),
1707 ),
1708 };
1709 let t0 = std::time::Instant::now();
1710 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1711 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1712 eprintln!(
1713 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1714 if ok { "" } else { " (failed → fresh)" },
1715 t0.elapsed().as_secs_f64() * 1e3
1716 );
1717 }
1718 ok
1719 }
1720
1721 fn apply_kv_reuse_plan(
1722 &mut self,
1723 reuse_from: usize,
1724 plan: ReusePlan,
1725 layers: &[ReuseLayer],
1726 ) -> bool {
1727 let kv_id = self.graph_kv_id;
1728 match plan {
1729 ReusePlan::Fresh => return false,
1730 ReusePlan::Ready => {}
1731 ReusePlan::Pull(pulls) => {
1732 let (nkv, hd) = {
1737 let c = &self.kv_cache.layers[pulls[0].0];
1738 (c.num_kv_heads, c.head_dim)
1739 };
1740 let uniform = pulls.iter().all(|&(li, _, _)| {
1741 let c = &self.kv_cache.layers[li];
1742 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1743 });
1744 let batched = if uniform {
1745 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1746 } else {
1747 None
1748 };
1749 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1750 Some(rows) => rows,
1751 None => {
1752 let mut rows = Vec::with_capacity(pulls.len());
1753 for &(li, from, to) in &pulls {
1754 let (lnkv, lhd) = {
1755 let c = &self.kv_cache.layers[li];
1756 (c.num_kv_heads, c.head_dim)
1757 };
1758 let Some((k, v, first_valid)) =
1759 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1760 else {
1761 return false;
1762 };
1763 let need_from = match self.layer_window(li) {
1767 Some(w) => from.max((to + 1).saturating_sub(w)),
1768 None => from,
1769 };
1770 if first_valid > need_from {
1771 return false;
1772 }
1773 rows.push((k, v));
1774 }
1775 rows
1776 }
1777 };
1778 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1779 let cache = &mut self.kv_cache.layers[li];
1780 let row = cache.num_kv_heads * cache.head_dim;
1781 for p in 0..to - from {
1782 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1783 }
1784 if cache.pos_len() != to {
1785 return false;
1786 }
1787 }
1788 }
1789 }
1790 for (li, l) in layers.iter().enumerate() {
1794 if l.full
1795 && l.device_rows.is_some_and(|d| d > reuse_from)
1796 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1797 {
1798 return false;
1799 }
1800 }
1801 true
1802 }
1803
1804 fn finish_generation(
1810 &mut self,
1811 mtp: &mut Option<MtpModule>,
1812 router: &mut Option<crate::swarm::DynRouter>,
1813 clear_sequence: bool,
1814 ) {
1815 if router.is_some() {
1819 let _ = self.set_active_skill(None);
1820 }
1821 #[cfg(target_os = "macos")]
1828 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1829 if clear_sequence {
1830 self.clear_sequence_state();
1831 if let Some(m) = mtp.as_mut() {
1832 m.kv.clear();
1838 }
1839 if let Some(m) = self.mtp.as_mut() {
1840 m.kv.clear();
1844 }
1845 }
1846 self.graph_want_logits = false;
1847 self.graph_head_required = false;
1848 self.graph_logits = None;
1849 self.graph_failed
1850 .store(false, std::sync::atomic::Ordering::Relaxed);
1851 self.cancel
1852 .store(false, std::sync::atomic::Ordering::Relaxed);
1853 self.dyn_router = router.take().or(self.dyn_router.take());
1854 self.mtp = mtp.take().or(self.mtp.take());
1855 self.mtp_graph_mode = None;
1856 self.spec_forced = None;
1857 }
1858
1859 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1863 if self
1864 .graph_failed
1865 .swap(false, std::sync::atomic::Ordering::Relaxed)
1866 {
1867 self.cancel
1868 .store(false, std::sync::atomic::Ordering::Relaxed);
1869 self.clear_sequence_state();
1870 self.graph_logits = None;
1871 self.graph_want_logits = false;
1872 self.graph_head_required = false;
1873 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1874 }
1875 Ok(())
1876 }
1877
1878 #[cfg(target_os = "macos")]
1879 fn fail_metal_graph(&mut self, reason: &str) {
1880 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1881 self.clear_sequence_state();
1882 self.graph_logits = None;
1883 self.graph_failed
1884 .store(true, std::sync::atomic::Ordering::Relaxed);
1885 self.cancel
1886 .store(true, std::sync::atomic::Ordering::Relaxed);
1887 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1888 }
1889
1890 fn nll_begin(&mut self) -> Result<(), String> {
1895 if self
1896 .graph_failed
1897 .swap(false, std::sync::atomic::Ordering::Relaxed)
1898 {
1899 self.cancel
1900 .store(false, std::sync::atomic::Ordering::Relaxed);
1901 self.clear_sequence_state();
1902 self.graph_logits = None;
1903 self.graph_want_logits = false;
1904 self.graph_head_required = false;
1905 return Err("GPU graph failed before NLL scoring".to_string());
1906 }
1907 self.clear_sequence_state();
1908 self.graph_logits = None;
1909 self.graph_want_logits = false;
1910 self.graph_head_required = false;
1911 Ok(())
1912 }
1913
1914 fn nll_end(&mut self) {
1918 self.clear_sequence_state();
1919 self.graph_logits = None;
1920 self.graph_want_logits = false;
1921 self.graph_head_required = false;
1922 self.graph_failed
1923 .store(false, std::sync::atomic::Ordering::Relaxed);
1924 }
1925
1926 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1929 #[cfg(test)]
1930 if self.nll_test_fail_at == Some(pos) {
1931 self.nll_test_fail_at = None;
1932 self.graph_failed
1933 .store(true, std::sync::atomic::Ordering::Relaxed);
1934 self.cancel
1935 .store(true, std::sync::atomic::Ordering::Relaxed);
1936 }
1937 if self
1938 .graph_failed
1939 .swap(false, std::sync::atomic::Ordering::Relaxed)
1940 {
1941 self.cancel
1942 .store(false, std::sync::atomic::Ordering::Relaxed);
1943 self.clear_sequence_state();
1944 self.graph_logits = None;
1945 self.graph_want_logits = false;
1946 return Err(format!(
1947 "GPU graph failed during NLL {phase} at position {pos}"
1948 ));
1949 }
1950 Ok(())
1951 }
1952
1953 #[inline]
1957 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1958 virtual_idx % self.physical_layers
1959 }
1960
1961 #[inline]
1964 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1965 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1966 }
1967
1968 #[allow(clippy::too_many_arguments)]
1970
1971 #[cfg(target_os = "macos")]
1990 fn graph_prefill_preferred(&self) -> bool {
1991 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1992 if !crate::gpu::enabled_here()
1993 || !graph_force
1994 || std::env::var("CMF_GPU_BLOCK")
1995 .map(|v| v == "0")
1996 .unwrap_or(false)
1997 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2000 {
2001 return false;
2002 }
2003 self.weights
2004 .layers
2005 .iter()
2006 .any(|lw| {
2007 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2008 })
2009 }
2010
2011 #[cfg(not(target_os = "macos"))]
2018 fn batch_prefix_prefill(&self) -> bool {
2019 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2020 Ok("0") => return false,
2021 Ok("1") => true,
2022 _ => false,
2023 };
2024 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2025 && crate::gpu::enabled_here()
2026 && !self.graph_refused()
2027 && (forced || self.graph_attn_decline_reason().is_some())
2028 && self.wgpu_graph_attn_decline().is_none()
2029 && self.attn_softcap == 0.0
2030 && self
2031 .weights
2032 .layers
2033 .iter()
2034 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2035 && self.automatic_gpu_prefix().is_some()
2036 }
2037
2038 fn g4_batch_prefill(&self) -> bool {
2044 #[cfg(target_os = "macos")]
2045 {
2046 false
2047 }
2048 #[cfg(not(target_os = "macos"))]
2049 {
2050 !self.weights.layers.is_empty()
2054 && !self.per_token_stack()
2055 && (self.global_attn.is_some() || self.attn_v_norm)
2056 && crate::gpu::g4_batch_on()
2057 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2058 && crate::gpu::enabled_here()
2059 && crate::gpu::discrete()
2060 && !self.graph_refused()
2061 && !self.o1_active()
2062 && self.attn_softcap == 0.0
2063 && self.wgpu_graph_attn_decline().is_none()
2064 && self.weights.layers.iter().all(|lw| {
2065 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2066 })
2067 && self.automatic_gpu_prefix().is_none()
2068 }
2069 }
2070
2071 #[cfg(not(target_os = "macos"))]
2078 fn g4_batch_ingest(
2079 &mut self,
2080 ids: &[u32],
2081 pos0: usize,
2082 keep_all: bool,
2083 ) -> Result<(usize, Vec<f32>), String> {
2084 let hs = self.hidden_size;
2085 let chunk = self.generation_batch_k().max(1);
2086 let end_all = pos0 + ids.len();
2087 let mut pos = pos0;
2088 let mut out = Vec::new();
2089 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2090 let end = (pos + chunk).min(end_all);
2091 let bk = end - pos;
2092 let mut hiddens = vec![0f32; bk * hs];
2093 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2094 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2095 }
2096 let positions: Vec<usize> = (pos..end).collect();
2097 let t_chunk = std::time::Instant::now();
2098 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2099 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2100 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2101 eprintln!(
2102 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2103 bk as f64 / (ms / 1000.0)
2104 );
2105 }
2106 match outcome {
2107 crate::gpu::BatchGraphOutcome::Completed => {
2108 if keep_all {
2109 out.extend_from_slice(&hiddens);
2110 } else {
2111 out = hiddens[(bk - 1) * hs..].to_vec();
2112 }
2113 pos = end;
2114 }
2115 crate::gpu::BatchGraphOutcome::Failed => {
2116 return Err("Gemma-4 batched prefill failed after admission".into());
2117 }
2118 crate::gpu::BatchGraphOutcome::Declined => {
2119 #[cfg(feature = "gpu")]
2120 if pos > pos0 {
2121 self.pull_lagging_host_kv(0, self.num_layers, pos);
2122 }
2123 break;
2124 }
2125 }
2126 }
2127 Ok((pos - pos0, out))
2128 }
2129
2130 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2135 #[cfg(not(target_os = "macos"))]
2136 {
2137 if ids.is_empty() || !self.g4_batch_prefill() {
2138 return None;
2139 }
2140 self.clear_sequence_state();
2141 match self.g4_batch_ingest(ids, 0, true) {
2142 Ok((n, rows)) if n == ids.len() => Some(rows),
2143 _ => {
2144 self.clear_sequence_state();
2145 None
2146 }
2147 }
2148 }
2149 #[cfg(target_os = "macos")]
2150 {
2151 let _ = ids;
2152 None
2153 }
2154 }
2155
2156 #[cfg(not(target_os = "macos"))]
2157 fn graph_prefill_preferred(&self) -> bool {
2158 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2166 if !graph_on || !crate::gpu::enabled_here() {
2167 return false;
2168 }
2169 if self.embryo_resident_eligible() {
2173 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2178 }
2179 if self.o1_active() {
2193 return false;
2194 }
2195 if self.wgpu_graph_attn_decline().is_some() {
2199 return false;
2200 }
2201 if self
2202 .weights
2203 .layers
2204 .iter()
2205 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2206 {
2207 return true;
2208 }
2209 self.weights
2220 .layers
2221 .iter()
2222 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2223 && self.automatic_gpu_prefix().is_none()
2224 }
2225
2226 #[cfg(target_os = "macos")]
2227 fn q1_graph_gpu(
2228 &mut self,
2229 start: usize,
2230 upto: Option<usize>,
2231 position: usize,
2232 h: &mut [f32],
2233 ) -> usize {
2234 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2236 const MOE_WORKING_SET_REASON: &str =
2237 "model weights exceed Metal working set for native MoE graph";
2238 if self
2242 .graph_declines
2243 .borrow()
2244 .iter()
2245 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2246 {
2247 return start;
2248 }
2249 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2250 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2252 || !crate::gpu::enabled_here()
2253 || !graph_force
2254 || std::env::var("CMF_GPU_BLOCK")
2255 .map(|v| v == "0")
2256 .unwrap_or(false)
2257 {
2258 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2259 eprintln!(
2260 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2261 self.attn_softcap > 0.0,
2262 crate::gpu::enabled_here(),
2263 graph_force,
2264 );
2265 }
2266 if self.graph_head_required {
2267 self.fail_metal_graph("native graph front gate refused");
2268 }
2269 return start;
2270 }
2271 let gemma4 = self.metal_gemma4();
2281 let swa_graph = self.metal_graph_swa() || gemma4;
2282 let graph_attn_reason = self.graph_attn_decline_reason();
2289 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2290 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2291 if (self.swa.is_some() && !swa_graph)
2292 || (self.global_attn.is_some() && !gemma4)
2293 || self.attention_heads_per_layer.is_some()
2294 || (self.attn_v_norm && !gemma4)
2295 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2298 || self.weights.layers.iter().any(|lw| {
2299 ((lw.attn_out_norm.is_some()
2300 || lw.ffn_out_norm.is_some()
2301 || lw.layer_scale.is_some())
2302 && !gemma4)
2303 || matches!(&lw.ffn, FfnKind::Dense(d)
2304 if !(matches!(d.act, Act::Silu | Act::Gelu)
2305 || (gemma4 && d.act == Act::GeluTanh)))
2306 })
2307 {
2308 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2311 self.note_graph_decline("metal block graph", reason);
2312 }
2313 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2314 eprintln!(
2315 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2316 self.swa.is_some(),
2317 self.global_attn.is_some(),
2318 self.attention_heads_per_layer.is_some(),
2319 self.attn_v_norm,
2320 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2321 );
2322 }
2323 if self.graph_head_required {
2324 self.fail_metal_graph("native graph architecture gate refused");
2325 }
2326 return start;
2327 }
2328 let limit = upto
2331 .map(|u| u + 1)
2332 .unwrap_or(self.num_layers)
2333 .min(self.num_layers);
2334
2335 enum Item<'a> {
2336 Gdn {
2337 run: Vec<GdnGpuLayer<'a>>,
2338 first: usize,
2339 },
2340 Attn {
2341 l: AttnGpuLayer<'a>,
2342 li: usize,
2343 q_norm: Option<&'a [f32]>,
2344 k_norm: Option<&'a [f32]>,
2345 output_gate: bool,
2346 proj_gate: Option<(&'a QTensor, bool)>,
2350 head_gate_w: Option<&'a [f32]>,
2353 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2354 full_gpu: bool,
2357 },
2358 }
2359
2360 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2367 let attend_contract = attend_mode != "0"
2368 && attend_mode != "off"
2369 && self.head_dim % 4 == 0
2370 && self.head_dim <= 256
2371 && (self.rotary_dim == 0
2375 || (self.rotary_dim >= 2
2376 && self.rotary_dim <= self.head_dim
2377 && (self.rotary_dim / 2) % 32 == 0))
2378 && self.num_kv_heads > 0
2379 && self.num_heads % self.num_kv_heads == 0;
2380
2381 let mut plan: Vec<Item> = Vec::new();
2382 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2383 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2385 let mut scan = start;
2386 while scan < limit {
2387 let lw = &self.weights.layers[self.phys_layer(scan)];
2388 let ffn = match &lw.ffn {
2389 FfnKind::Dense(d) if d.segs.is_empty() => {
2390 let (Some(g), Some(u), Some(dn)) = (
2391 d.gate_proj.metal_graph_parts(),
2392 d.up_proj.metal_graph_parts(),
2393 d.down_proj.metal_graph_parts(),
2394 ) else {
2395 if block_diag {
2396 eprintln!(
2397 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2398 );
2399 }
2400 break;
2401 };
2402 MetalFfn::Dense {
2403 gate: g,
2404 up: u,
2405 down: dn,
2406 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2409 }
2410 }
2411 FfnKind::Moe(m) => {
2412 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2418 {
2419 MetalFfn::FlintMoe(flint)
2420 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2421 MetalFfn::Moe(moe)
2422 } else {
2423 if block_diag {
2424 eprintln!(
2425 "block-graph: L{scan} MoE outside the graph contract — run ends"
2426 );
2427 }
2428 break;
2429 };
2430 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2431 model_ref.get_or_insert_with(|| model.clone());
2432 }
2433 ffn
2434 }
2435 _ => {
2436 if block_diag {
2437 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2438 }
2439 break;
2440 }
2441 };
2442 match &lw.attn {
2443 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2444 let parts = (
2445 w.in_proj_qkv.metal_graph_parts(),
2446 w.in_proj_z.metal_graph_parts(),
2447 w.in_proj_a.f32_parts(),
2448 w.in_proj_b.f32_parts(),
2449 w.out_proj.metal_graph_parts(),
2450 );
2451 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2452 if block_diag {
2453 eprintln!(
2454 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2455 w.in_proj_qkv.metal_graph_parts().is_some(),
2456 w.in_proj_z.metal_graph_parts().is_some(),
2457 w.in_proj_a.f32_parts().is_some(),
2458 w.in_proj_b.f32_parts().is_some(),
2459 w.out_proj.metal_graph_parts().is_some(),
2460 );
2461 }
2462 break;
2463 };
2464 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2465 model_ref.get_or_insert_with(|| model.clone());
2466 }
2467 let gl = GdnGpuLayer {
2468 attn_norm: &lw.input_norm,
2469 post_norm: &lw.post_norm,
2470 qkv,
2471 z,
2472 a,
2473 b,
2474 out,
2475 ffn,
2476 conv1d: &w.conv1d,
2477 a_log: &w.a_log,
2478 dt_bias: &w.dt_bias,
2479 gnorm: &w.norm,
2480 };
2481 match plan.last_mut() {
2482 Some(Item::Gdn { run, .. }) => run.push(gl),
2483 _ => plan.push(Item::Gdn {
2484 run: vec![gl],
2485 first: scan,
2486 }),
2487 }
2488 }
2489 AttnKind::Full {
2490 wq,
2491 wk,
2492 wv,
2493 wo,
2494 q_norm,
2495 k_norm,
2496 output_gate,
2497 softplus_gate,
2498 bias,
2499 } if (!self.kv_cache.layers[scan].o1_sealed()
2500 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2505 && softplus_gate
2509 .as_ref()
2510 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2511 {
2512 let parts = (
2513 wq.metal_graph_parts(),
2514 wk.metal_graph_parts(),
2515 wv.metal_graph_parts(),
2516 wo.metal_graph_parts(),
2517 );
2518 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2519 break;
2520 };
2521 if let QTensor::Mapped { model, .. } = wq {
2522 model_ref.get_or_insert_with(|| model.clone());
2523 }
2524 let cache = &self.kv_cache.layers[scan];
2525 let o1_metal = cache.o1.is_some()
2529 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2530 && cache.o1_views().is_some();
2531 let head_gate_w = softplus_gate
2535 .as_ref()
2536 .and_then(|(g, _)| g.f32_parts())
2537 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2538 .map(|(d, _, _)| d);
2539 let (nkv_g, hd_g) = if gemma4 {
2542 let (a, b, _) = self.layer_geom(scan);
2543 (a, b)
2544 } else {
2545 (self.num_kv_heads, self.head_dim)
2546 };
2547 let full_gpu = attend_contract
2548 && softplus_gate.is_none() == head_gate_w.is_none()
2549 && cache.mode == crate::kv_cache::KvMode::F32
2550 && (cache.o1.is_none() || o1_metal)
2551 && bias.is_none()
2552 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2553 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2554 && pk.1 == nkv_g * hd_g
2555 && pv.1 == nkv_g * hd_g
2556 && po.2 == self.num_heads * hd_g;
2557 plan.push(Item::Attn {
2558 l: AttnGpuLayer {
2559 attn_norm: &lw.input_norm,
2560 post_norm: &lw.post_norm,
2561 wq: pq,
2562 wk: pk,
2563 wv: pv,
2564 wo: po,
2565 ffn,
2566 attn_out_norm: lw.attn_out_norm.as_deref(),
2567 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2568 layer_scale: lw.layer_scale,
2569 },
2570 li: scan,
2571 q_norm: q_norm.as_deref(),
2572 k_norm: k_norm.as_deref(),
2573 output_gate: *output_gate,
2574 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2575 head_gate_w,
2576 bias: bias
2577 .as_ref()
2578 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2579 full_gpu,
2580 });
2581 }
2582 _ => break,
2583 }
2584 scan += 1;
2585 }
2586 let Some(model) = model_ref else {
2587 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2588 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2589 }
2590 if self.graph_head_required {
2591 self.fail_metal_graph("native graph has no mapped model reference");
2592 }
2593 return start;
2594 };
2595 if plan.is_empty() {
2596 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2597 eprintln!("q1-graph: empty plan at layer {start}");
2598 }
2599 if self.graph_head_required {
2600 self.fail_metal_graph("native graph plan is empty");
2601 }
2602 return start;
2603 }
2604 let has_moe = plan.iter().any(|it| match it {
2605 Item::Gdn { run, .. } => run
2606 .iter()
2607 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2608 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2609 });
2610 let has_flint_moe = plan.iter().any(|it| match it {
2611 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2612 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2613 });
2614 let metal_working_set = crate::gpu_metal::working_set_bytes();
2615 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2616 let model_bytes = model.primary_bytes().len() as u64;
2617 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2623 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2624 let pager_cap = if !has_moe {
2633 None
2634 } else if has_flint_moe {
2635 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2639 } else {
2640 crate::gpu_metal::moe_pager_cap_bytes()
2641 };
2642 let pager_eligible =
2643 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2644 let mut pager_span: Option<(usize, usize)> = None;
2645 if pager_eligible {
2646 let cap = pager_cap.expect("checked above");
2647 let tail_span = (self.graph_want_logits
2651 && upto.is_none()
2652 && scan == self.num_layers
2653 && std::env::var("CMF_GPU_LMHEAD")
2654 .map(|v| v != "0")
2655 .unwrap_or(true))
2656 .then(|| self.weights.lm_head.metal_graph_parts())
2657 .flatten()
2658 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2659 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2660 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2661 Some(match span {
2662 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2663 None => (lo, hi),
2664 })
2665 };
2666 let page_span = |span: (usize, usize), include_tail: bool| {
2667 let (mut lo, mut hi) = span;
2668 if include_tail {
2669 if let Some((tail_lo, tail_hi)) = tail_span {
2670 lo = lo.min(tail_lo);
2671 hi = hi.max(tail_hi);
2672 }
2673 }
2674 crate::gpu_metal::page_round_span(lo, hi)
2675 };
2676 let mut selected = 0usize;
2677 let mut selected_span: Option<(usize, usize)> = None;
2678 let mut gdn_prefix: Option<(usize, usize)> = None;
2686 'items: for (i, item) in plan.iter().enumerate() {
2687 match item {
2688 Item::Attn { li, .. } => {
2689 let Some(span) = add_layer_span(selected_span, *li) else {
2690 break;
2691 };
2692 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2693 break;
2694 };
2695 if page_hi - page_lo > cap {
2696 break;
2697 }
2698 selected = i + 1;
2699 selected_span = Some((page_lo, page_hi));
2700 }
2701 Item::Gdn { first, run } => {
2702 let mut whole = None;
2709 for take in 1..=run.len() {
2710 whole = add_layer_span(whole, *first + take - 1);
2711 if whole.is_none() {
2712 break;
2713 }
2714 }
2715 let whole_fits_empty_page = whole
2716 .and_then(|span| page_span(span, false))
2717 .is_some_and(|(lo, hi)| hi - lo <= cap);
2718 if whole_fits_empty_page {
2719 let mut span = selected_span;
2720 for take in 1..=run.len() {
2721 let Some(next) = add_layer_span(span, *first + take - 1) else {
2722 break 'items;
2723 };
2724 span = Some(next);
2725 }
2726 let Some(span) = span else {
2727 break;
2728 };
2729 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2730 else {
2731 break;
2732 };
2733 if page_hi - page_lo > cap {
2734 break;
2735 }
2736 selected = i + 1;
2737 selected_span = Some((page_lo, page_hi));
2738 continue;
2739 }
2740 if selected != 0 {
2741 break;
2742 }
2743 let mut span = None;
2744 let mut best: Option<(usize, (usize, usize))> = None;
2745 for take in 1..=run.len() {
2746 let Some(next) = add_layer_span(span, *first + take - 1) else {
2747 break;
2748 };
2749 let Some((page_lo, page_hi)) = page_span(next, false) else {
2752 break;
2753 };
2754 if page_hi - page_lo > cap {
2755 break;
2756 }
2757 span = Some((page_lo, page_hi));
2758 best = Some((take, (page_lo, page_hi)));
2759 }
2760 let Some((take, page)) = best else {
2761 break;
2762 };
2763 selected = i + 1;
2764 selected_span = Some(page);
2765 if take != run.len() {
2766 gdn_prefix = Some((i, take));
2767 break 'items;
2768 }
2769 }
2770 }
2771 }
2772 if selected != 0 {
2773 if let Some((item, take)) = gdn_prefix {
2774 match &mut plan[item] {
2775 Item::Gdn { run, .. } => run.truncate(take),
2776 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2777 }
2778 }
2779 let (page_lo, page_hi) = selected_span.expect("selected page span");
2780 tracing::info!(
2781 page_start = page_lo,
2782 page_end = page_hi,
2783 page_mb = (page_hi - page_lo) / (1024 * 1024),
2784 cap_mb = cap / (1024 * 1024),
2785 graph_items = selected,
2786 "native Metal MoE graph uses transient bounded weight page"
2787 );
2788 plan.truncate(selected);
2789 pager_span = Some((page_lo, page_hi));
2790 } else {
2791 tracing::warn!(
2792 cap_mb = cap / (1024 * 1024),
2793 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2794 );
2795 }
2796 }
2797 if has_moe && needs_bounded_pager && pager_span.is_none() {
2806 tracing::info!(
2807 model_bytes,
2808 metal_working_set,
2809 "native Metal MoE graph declined before allocation"
2810 );
2811 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2812 if self.graph_head_required {
2813 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2814 }
2815 return start;
2816 }
2817 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2818 let dev_attend = attend_contract
2819 && (self.head_dim <= 128
2820 || has_moe
2821 || (self.head_dim <= 256 && has_gdn)
2827 || (self.head_dim <= 256 && swa_graph)
2831 || attend_mode == "force"
2832 || attend_mode == "256");
2833 if !dev_attend {
2834 for it in &mut plan {
2835 if let Item::Attn { li, full_gpu, .. } = it {
2836 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2839 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2840 if !keep_o1 {
2841 *full_gpu = false;
2842 }
2843 }
2844 }
2845 }
2846 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2847 use std::sync::atomic::{AtomicBool, Ordering};
2848 static SAID: AtomicBool = AtomicBool::new(false);
2849 if !SAID.swap(true, Ordering::Relaxed) {
2850 let fg = plan
2851 .iter()
2852 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2853 .count();
2854 let att = plan
2855 .iter()
2856 .filter(|it| matches!(it, Item::Attn { .. }))
2857 .count();
2858 eprintln!(
2859 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2860 plan.len(),
2861 self.head_dim,
2862 self.rotary_dim,
2863 self.num_kv_heads,
2864 self.num_heads,
2865 );
2866 }
2867 }
2868 let dims = GraphDims {
2869 hidden: self.hidden_size,
2870 eps: self.rms_eps as f32,
2871 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2872 };
2873 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2877 crate::gpu_metal::MoePagerPageScope::new(
2878 _mt0,
2879 position,
2880 start,
2881 page_lo,
2882 page_hi,
2883 pager_cap.unwrap_or(0),
2887 )
2888 });
2889 let graph_new = match pager_span {
2890 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2891 None => TokenGraph::new(&model, dims, h),
2892 };
2893 let Some(mut graph) = graph_new else {
2894 if self.graph_head_required {
2895 self.fail_metal_graph("native TokenGraph allocation refused");
2896 }
2897 return start;
2898 };
2899 if swa_graph && self.head_dim > 128 {
2900 graph.set_attend_blk_from(64);
2906 }
2907 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2908 nv: cfg.num_v_heads,
2909 nk: cfg.num_k_heads,
2910 dk: cfg.key_head_dim,
2911 dv: cfg.value_head_dim,
2912 kk: cfg.conv_kernel,
2913 hidden: self.hidden_size,
2914 inter: self.intermediate_size,
2915 c_dim: cfg.conv_dim(),
2916 eps: cfg.rms_eps as f32,
2917 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2918 });
2919 let mut valid = 0usize;
2923 let mut end = start;
2924 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2926 static ONCE: std::sync::Once = std::sync::Once::new();
2927 ONCE.call_once(|| {
2928 for it in &plan {
2929 match it {
2930 Item::Gdn { first, run } => {
2931 eprintln!("plan: Gdn first={first} len={}", run.len())
2932 }
2933 Item::Attn { li, full_gpu, .. } => {
2934 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2935 }
2936 }
2937 }
2938 });
2939 }
2940 for item in &plan {
2941 let ok = match item {
2942 Item::Gdn { run, .. } => gcfg
2943 .as_ref()
2944 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2945 .unwrap_or(false),
2946 Item::Attn { l, .. } => graph.attn_ok(l),
2947 };
2948 if !ok {
2949 if block_diag {
2950 eprintln!(
2951 "block-graph: plan item {} ({}) failed graph preflight",
2952 valid,
2953 match item {
2954 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2955 Item::Attn { li, .. } => format!("Attn L{li}"),
2956 }
2957 );
2958 }
2959 break;
2960 }
2961 valid += 1;
2962 end += match item {
2963 Item::Gdn { run, .. } => run.len(),
2964 Item::Attn { .. } => 1,
2965 };
2966 }
2967 plan.truncate(valid);
2968 if plan.is_empty() {
2969 if self.graph_head_required {
2970 self.fail_metal_graph("native graph preflight produced no valid items");
2971 }
2972 return start;
2973 }
2974
2975 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2976 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2977 return start;
2978 }
2979
2980 let one_pass = |t: (usize, usize, usize)| {
2989 use cortiq_core::TensorDtype as D;
2990 matches!(
2991 model.tensors[t.0].dtype,
2992 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
2993 )
2994 };
2995 let dense_fast = plan.iter().all(|it| match it {
2996 Item::Attn {
2997 l, li, full_gpu, ..
2998 } => {
2999 *full_gpu
3000 && self.kv_cache.layers[*li].o1.is_none()
3001 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3002 && match l.ffn {
3003 MetalFfn::Dense { gate, up, down, .. } => {
3004 one_pass(gate) && one_pass(up) && one_pass(down)
3005 }
3006 _ => false,
3007 }
3008 }
3009 Item::Gdn { .. } => false,
3010 });
3011 let moe_plan = !dense_fast
3015 && plan.iter().all(|it| match it {
3016 Item::Attn {
3017 l, li, full_gpu, ..
3018 } => {
3019 *full_gpu
3020 && self.kv_cache.layers[*li].o1.is_none()
3021 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3022 }
3023 Item::Gdn { .. } => false,
3024 });
3025 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3026 if block_diag {
3027 use std::sync::atomic::{AtomicBool, Ordering};
3028 static SAID_AB: AtomicBool = AtomicBool::new(false);
3029 if !SAID_AB.swap(true, Ordering::Relaxed) {
3030 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3031 }
3032 }
3033 let _mv_fast = match ab {
3034 Some((bits, _)) => {
3035 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3036 crate::gpu_metal::MvFastGuard::set_raw(bits)
3037 }
3038 None if moe_plan => {
3039 graph.set_dense_concurrent(
3040 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3041 );
3042 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3043 }
3044 None => {
3045 graph.set_dense_concurrent(dense_fast);
3046 let q8r4 = if swa_graph {
3049 crate::gpu_metal::DENSE_Q8R4
3050 } else {
3051 0
3052 };
3053 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3054 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3055 } else {
3056 0
3057 })
3058 }
3059 };
3060
3061 let pool = self.pool.clone();
3064 let (nh, nkv, hd, hs, eps) = (
3065 self.num_heads,
3066 self.num_kv_heads,
3067 self.head_dim,
3068 self.hidden_size,
3069 self.rms_eps,
3070 );
3071 let norm_style = self.norm_style;
3072 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3073 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3074 let kv_id = self.graph_kv_id;
3075 let mut pending: Vec<(usize, usize)> = Vec::new();
3078 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3081 for item in &plan {
3082 let _xt0 = std::time::Instant::now();
3083 let _xkind: u32 = match item {
3084 Item::Gdn { .. } => 2,
3085 Item::Attn { .. } => 3,
3086 };
3087 if self.loop_final_norm {
3089 let item_start = match item {
3090 Item::Gdn { first, .. } => *first,
3091 Item::Attn { li, .. } => *li,
3092 };
3093 if item_start > start && self.is_loop_end(item_start - 1) {
3094 graph.encode_loop_norm(&self.weights.final_norm);
3095 }
3096 }
3097 match item {
3098 Item::Gdn { run, first } => {
3099 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3100 if l.linear_state.len() != want {
3101 l.linear_state = vec![0f32; want];
3102 }
3103 }
3104 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3105 .iter()
3106 .map(|l| l.linear_state.as_slice())
3107 .collect();
3108 let _ig = std::time::Instant::now();
3109 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3110 tracing::error!("q1 graph: GDN run refused after validation");
3112 return start;
3113 }
3114 graph.commit_kind = 2;
3117 graph.commit();
3118 crate::gpu::stageprof(0, _ig.elapsed());
3119 pending.push((*first, run.len()));
3120 }
3121 Item::Attn {
3122 l,
3123 li,
3124 q_norm,
3125 k_norm,
3126 output_gate,
3127 proj_gate,
3128 head_gate_w,
3129 bias,
3130 full_gpu,
3131 } => {
3132 let _ia = std::time::Instant::now();
3133 let inv_freq_l = self.layer_inv_freq(*li);
3139 let rd_l = self.layer_geom(*li).2;
3140 let window_l = self.layer_window(*li);
3141 let head_gate_w = *head_gate_w;
3142 let (nkv, hd) = if gemma4 {
3145 let (a, b, _) = self.layer_geom(*li);
3146 (a, b)
3147 } else {
3148 (nkv, hd)
3149 };
3150 if *full_gpu {
3152 let cache = &self.kv_cache.layers[*li];
3153 let o1p = if cache.o1.is_some() {
3154 match cache.o1_views() {
3155 Some(views) => Some(crate::gpu::O1AttnParams {
3156 views,
3157 epoch: self.o1_epoch,
3158 }),
3159 None => None,
3161 }
3162 } else {
3163 None
3164 };
3165 let o1_layer = cache.o1.is_some();
3166 if o1_layer && o1p.is_none() {
3167 }
3169 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3170 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3171 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3172 debug_assert!(
3175 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3176 );
3177 let p = crate::gpu::AttnDeviceParams {
3178 kv_id,
3179 layer: *li,
3180 nh,
3181 nkv,
3182 hd,
3183 rd: rd_l,
3184 position,
3185 scale: self.attn_scale,
3186 eps: eps as f32,
3187 gemma,
3188 late_qk_norm: self.qk_norm_after_rope,
3189 output_gate: *output_gate,
3190 q_norm: *q_norm,
3191 k_norm: *k_norm,
3192 inv_freq: &inv_freq_l,
3193 rope_scale: self.layer_rope_scale(*li),
3194 cpu_k,
3195 cpu_v,
3196 cpu_stored,
3197 cpu_gen: cache.generation(),
3198 o1: o1p,
3199 window: window_l,
3200 softmax_sink: cache.sinks.as_deref(),
3201 head_gate: head_gate_w,
3202 v_norm: self.attn_v_norm,
3203 };
3204 let o1_bad = o1_layer && p.o1.is_none();
3205 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3206 {
3207 if p.o1.is_none() {
3209 dev_attn.push((*li, nkv, hd));
3210 }
3211 graph.commit_kind = 3;
3212 graph.commit();
3213 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3217 continue;
3218 }
3219 }
3221 graph.encode_attn_prefix(l);
3222 if let Err(err) = graph.sync_checked() {
3223 self.fail_metal_graph(&err);
3224 return start;
3225 }
3226 if !pending.is_empty() {
3227 let idxs: Vec<usize> =
3228 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3229 let mut outs: Vec<&mut [f32]> = self
3230 .kv_cache
3231 .layers
3232 .iter_mut()
3233 .enumerate()
3234 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3235 .map(|(_, s)| s.linear_state.as_mut_slice())
3236 .collect();
3237 graph.read_states(&mut outs);
3238 }
3239 let mut q_raw = attention::take_buf(l.wq.1);
3240 let mut k = attention::take_buf(l.wk.1);
3241 let mut v = attention::take_buf(l.wv.1);
3242 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3243 let mut gate_raw = proj_gate.map(|(gp, _)| {
3246 let mut normed = attention::take_buf(hs);
3247 graph.read_normed(&mut normed);
3248 let mut raw = attention::take_buf(gp.rows());
3249 gp.matvec(&normed, &mut raw, pool.as_deref());
3250 attention::recycle_buf(&mut normed);
3251 raw
3252 });
3253 let cfg = QwenAttnCfg {
3254 num_heads: nh,
3255 num_kv_heads: nkv,
3256 head_dim: hd,
3257 hidden_size: hs,
3258 position,
3259 inv_freq: &inv_freq_l,
3260 rotary_dim: rd_l,
3261 scale: self.attn_scale,
3262 softcap: self.attn_softcap,
3263 window: window_l,
3264 v_norm: self.attn_v_norm,
3267 qk_norm_after_rope: self.qk_norm_after_rope,
3268 gate_sigmoid: self.proj_gate_sigmoid,
3269 q_norm: *q_norm,
3270 k_norm: *k_norm,
3271 output_gate: *output_gate,
3272 softplus_gate: None,
3273 rope_scale: self.layer_rope_scale(*li),
3274 bias: *bias,
3275 rms_eps: eps,
3276 norm_style,
3277 pool: pool.as_deref(),
3278 v_head_dim: hd,
3279 };
3280 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3283 || std::env::var("CMF_ATTN_DUMP").is_ok();
3284 let _ = full_gpu;
3285 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3286 let mut ao = attention::qwen_attention_core(
3287 q_raw,
3288 k,
3289 v,
3290 &mut self.kv_cache.layers[*li],
3291 &cfg,
3292 );
3293 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3297 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3298 let (cq, _cg, _ck, _cv) =
3299 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3300 let cache = &self.kv_cache.layers[*li];
3301 let n = cache.head_keys(0).len() / hd;
3302 let mut bytes: Vec<u8> = Vec::new();
3303 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3304 bytes.extend_from_slice(&v.to_le_bytes());
3305 }
3306 for v in &cq {
3307 bytes.extend_from_slice(&v.to_le_bytes());
3308 }
3309 for g in 0..nkv {
3310 for v in cache.head_keys(g) {
3311 bytes.extend_from_slice(&v.to_le_bytes());
3312 }
3313 }
3314 for g in 0..nkv {
3315 for v in cache.head_values(g) {
3316 bytes.extend_from_slice(&v.to_le_bytes());
3317 }
3318 }
3319 let _ =
3320 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3321 }
3322 }
3323 if let Some((qr0, k0, v0)) =
3324 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3325 {
3326 let (cq, _cg, ck, cv) =
3327 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3328 let mut h_now = vec![0f32; hs];
3329 graph.read_h(&mut h_now);
3330 let cache = &self.kv_cache.layers[*li];
3331 let n_after = cache.head_keys(0).len() / hd;
3332 let stored = n_after.saturating_sub(1);
3336 let cpu_k: Vec<&[f32]> = (0..nkv)
3337 .map(|g| &cache.head_keys(g)[..stored * hd])
3338 .collect();
3339 let cpu_v: Vec<&[f32]> = (0..nkv)
3340 .map(|g| &cache.head_values(g)[..stored * hd])
3341 .collect();
3342 let p = crate::gpu::AttnDeviceParams {
3343 kv_id,
3344 layer: *li,
3345 nh,
3346 nkv,
3347 hd,
3348 rd: rd_l,
3352 position,
3353 scale: self.attn_scale,
3354 eps: eps as f32,
3355 gemma,
3356 late_qk_norm: self.qk_norm_after_rope,
3357 output_gate: *output_gate,
3358 q_norm: *q_norm,
3359 k_norm: *k_norm,
3360 inv_freq: &inv_freq_l,
3361 rope_scale: self.layer_rope_scale(*li),
3362 cpu_k,
3363 cpu_v,
3364 cpu_stored: stored,
3365 cpu_gen: cache.generation(),
3366 o1: None,
3367 window: window_l,
3368 softmax_sink: cache.sinks.as_deref(),
3369 head_gate: None,
3370 v_norm: self.attn_v_norm,
3371 };
3372 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3373 let md = |a: &[f32], b: &[f32]| {
3374 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3375 };
3376 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3377 eprintln!(
3378 "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}",
3379 nn(&cq),
3380 md(&cq, &dq),
3381 nn(&ck),
3382 md(&ck, &dk),
3383 nn(&cv),
3384 md(&cv, &dv),
3385 nn(&ao),
3386 md(&ao, &dao)
3387 );
3388 } else {
3389 eprintln!("attn-oracle L{li}: device probe declined");
3390 }
3391 }
3392 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3393 attention::apply_projected_gate(
3396 &mut ao,
3397 raw,
3398 per_head,
3399 hd,
3400 self.proj_gate_sigmoid,
3401 );
3402 }
3403 if let Some(mut raw) = gate_raw.take() {
3404 attention::recycle_buf(&mut raw);
3405 }
3406 graph.encode_attn_suffix(l, &ao);
3407 graph.commit();
3410 attention::recycle_buf(&mut ao);
3411 }
3412 }
3413
3414 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3415 }
3416 let mut lm_rows = None;
3421 if self.graph_want_logits
3422 && upto.is_none()
3423 && end == self.num_layers
3424 && std::env::var("CMF_GPU_LMHEAD")
3425 .map(|v| v != "0")
3426 .unwrap_or(true)
3427 {
3428 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3429 if graph.lm_head_ok(lm) {
3430 graph.encode_lm_head(&self.weights.final_norm, lm);
3431 lm_rows = Some(lm.1);
3432 }
3433 }
3434 }
3435 if self.graph_head_required && lm_rows.is_none() {
3436 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3437 self.fail_metal_graph("fused graph head was requested but not encodable");
3438 return start;
3439 }
3440 let _sy0 = std::time::Instant::now();
3441 if let Err(err) = graph.sync_checked() {
3442 if let Some(profile) = pager_profile_page.as_mut() {
3443 profile.finish(start, graph.pager_page_stats(), "sync-error");
3444 }
3445 self.fail_metal_graph(&err);
3446 return start;
3447 }
3448 let _rs0 = std::time::Instant::now();
3449 if !pending.is_empty() {
3450 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3451 let mut outs: Vec<&mut [f32]> = self
3452 .kv_cache
3453 .layers
3454 .iter_mut()
3455 .enumerate()
3456 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3457 .map(|(_, s)| s.linear_state.as_mut_slice())
3458 .collect();
3459 graph.read_states(&mut outs);
3460 }
3461 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3462 use std::sync::atomic::{AtomicU64, Ordering};
3463 static SY: AtomicU64 = AtomicU64::new(0);
3464 static RS: AtomicU64 = AtomicU64::new(0);
3465 static N: AtomicU64 = AtomicU64::new(0);
3466 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3467 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3468 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3469 if n % 100 == 0 {
3470 eprintln!(
3471 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3472 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3473 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3474 );
3475 }
3476 }
3477 if let Some(rows) = lm_rows {
3478 crate::gpu::hostprof_encode_done(_mt0);
3479 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3480 graph.read_logits(&mut lg);
3481 crate::gpu::hostprof_total(_mt0);
3482 lg.resize(self.vocab_size, 0.0);
3483 if let Some(c) = self.final_softcap {
3484 softcap_in_place(&mut lg, c, self.pool.as_deref());
3485 }
3486 self.graph_logits = Some(lg);
3487 }
3488 graph.read_h(h);
3489 if self.graph_head_required && self.graph_logits.is_none() {
3490 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3491 self.fail_metal_graph("fused graph head completed without logits readback");
3492 return start;
3493 }
3494 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3495 METAL_GRAPH_LAYERS.fetch_add(
3496 end.saturating_sub(start) as u64,
3497 std::sync::atomic::Ordering::Relaxed,
3498 );
3499 if self.graph_head_required {
3500 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3501 }
3502 for (li, nkv, hd) in dev_attn {
3506 let mut krow = attention::take_buf(nkv * hd);
3507 let mut vrow = attention::take_buf(nkv * hd);
3508 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3509 let cache = &mut self.kv_cache.layers[li];
3510 cache.append(&krow, &vrow, &[]);
3511 let n = cache.seq_len;
3512 let mut imp = attention::take_buf(n);
3513 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3514 cache.accumulate_imp(&imp);
3515 attention::recycle_buf(&mut imp);
3516 }
3517 attention::recycle_buf(&mut krow);
3518 attention::recycle_buf(&mut vrow);
3519 }
3520 if let Some(profile) = pager_profile_page.as_mut() {
3521 profile.finish(end, graph.pager_page_stats(), "page-complete");
3522 }
3523 if let Some((_, arm)) = ab {
3524 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3525 }
3526 end
3527 }
3528
3529 pub fn new(
3530 tokenizer: Tokenizer,
3531 weights: PipelineWeights,
3532 hidden_size: usize,
3533 intermediate_size: usize,
3534 num_heads: usize,
3535 num_kv_heads: usize,
3536 head_dim: usize,
3537 num_layers: usize,
3538 physical_layers: usize,
3539 loop_final_norm: bool,
3540 vocab_size: usize,
3541 rms_eps: f64,
3542 rope_base: f32,
3543 norm_style: NormStyle,
3544 max_seq_len: usize,
3545 sampler_config: SamplerConfig,
3546 ) -> Self {
3547 let rng = match sampler_config.seed {
3548 Some(s) => SplitMix64::new(s),
3549 None => SplitMix64::from_entropy(),
3550 };
3551 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3552 let pool = Pool::from_env();
3553 if let Some(p) = &pool {
3554 tracing::info!("worker pool: {} threads", p.n_workers());
3555 if let Some(model) = weights
3557 .lm_head
3558 .model_arc()
3559 .or_else(|| weights.embed_tokens.model_arc())
3560 {
3561 let regions: Vec<&[u8]> =
3562 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3563 p.bind_numa(®ions);
3564 }
3565 }
3566 Self {
3567 gpu_plan: None,
3568 tokenizer: std::sync::Arc::new(tokenizer),
3569 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3570 sampler_config,
3571 weights,
3572 hidden_size,
3573 intermediate_size,
3574 num_heads,
3575 num_kv_heads,
3576 head_dim,
3577 num_layers,
3578 physical_layers,
3579 loop_final_norm,
3580 vocab_size,
3581 rms_eps,
3582 rope_base,
3583 norm_style,
3584 rotary_dim: head_dim,
3585 attention_heads_per_layer: None,
3586 kv_heads_per_layer: None,
3587 v_head_dim: None,
3588 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3589 .filter(|v| !v.is_empty())
3590 .map(std::path::PathBuf::from),
3591 graph_declines: std::cell::RefCell::new(Vec::new()),
3592 mimo_moe: Default::default(),
3593 minicpm_tail: Default::default(),
3594 vmf_cfg: None,
3595 gdn_cfg: None,
3596 kda_cfg: None,
3597 g3n: None,
3598 g4e: None,
3599 dsv4: None,
3600 dsv41: None,
3601 dsv41_vision: None,
3602 dsv41_prefill: None,
3603 qwen4_exp: None,
3604 dsv4_mtp: Vec::new(),
3605 dspark: None,
3606 dspark_pending: Vec::new(),
3607 dspark_hist: Vec::new(),
3608 dspark_real: Vec::new(),
3609 dspark_trunk_picks: Vec::new(),
3610 dspark_exp: Vec::new(),
3611 dspark_draft_ns: 0,
3612 m3_winners: Vec::new(),
3613 m3_counts: Vec::new(),
3614 m3_n_exp: 0,
3615 m3_tokens: 0,
3616 m3_prewarmed: false,
3617 logit_multiplier: None,
3618 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3619 graph_failed: std::sync::atomic::AtomicBool::new(false),
3620 kv_history: Vec::new(),
3621 kv_history_device: false,
3622 short_conv_cfg: None,
3623 mtp: None,
3624 mimo_mtp: None,
3625 verify_exact_moe: false,
3626 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3627 ignore_eos: false,
3628 draft_full_streak: 0,
3629 spec_k_adapt: None,
3630 spec_acc_ewma: 0.7,
3631 rng,
3632 sampler_scratch: SamplerScratch::default(),
3633 spec_forced: None,
3634 spec_q: Vec::new(),
3635 spec_p: Vec::new(),
3636 spec_res: Vec::new(),
3637 spec_qs: Vec::new(),
3638 spec_ps: Vec::new(),
3639 spec_ress: Vec::new(),
3640 mtp_graph_mode: None,
3641 #[cfg(target_os = "macos")]
3642 metal_verify: None,
3643 inv_freq,
3644 ws: ForwardScratch::new(hidden_size),
3645 pool,
3646 model: None,
3647 dyn_force_f32: false,
3648 dyn_skill_layers: Vec::new(),
3649 dyn_active: None,
3650 dyn_blend_loaded: false,
3651 dyn_phi_layer: None,
3652 dyn_phi_ema: Vec::new(),
3653 dyn_phi_seen: 0,
3654 dyn_router: None,
3655 o1_cfg: None,
3656 o1_epoch: 0,
3657 o1_flags: Vec::new(),
3658 trace: false,
3659 calib_temp: 1.0,
3660 confidence_on: true,
3661 embed_multiplier: 1.0,
3662 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3663 swa: None,
3664 swa_trim: None,
3665 sliding_layers: None,
3666 no_rope_layers: None,
3667 anchor_core: None,
3668 bounded_rope: None,
3669 kv_prefix: KvPrefix::default(),
3670 last_prefill_tokens: 0,
3671 inv_freq_local: None,
3672 rotary_dim_local: None,
3673 rope_scale: 1.0,
3674 rope_scale_local: 1.0,
3675 global_attn: None,
3676 inv_freq_global: None,
3677 attn_v_norm: false,
3678 qk_norm_after_rope: false,
3679 proj_gate_sigmoid: false,
3680 final_softcap: None,
3681 head_clusters: None,
3682 attn_softcap: 0.0,
3683 graph_want_logits: false,
3684 graph_head_required: false,
3685 graph_logits: None,
3686 embryo_graph: None,
3687 graph_refused: std::sync::atomic::AtomicBool::new(false),
3688 graph_kv_id: {
3689 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3690 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3691 },
3692 #[cfg(test)]
3693 nll_test_fail_at: None,
3694 #[cfg(test)]
3695 nll_test_force_serial: false,
3696 }
3697 }
3698
3699 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3707 if let Err(e) = self.try_set_o1(cfg) {
3708 tracing::error!("{e}");
3709 }
3710 }
3711
3712 pub fn bounded_native(&self) -> bool {
3716 self.anchor_core.is_some()
3717 }
3718
3719 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3722 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3723 }
3724
3725 pub fn o1_refusal(&self) -> Option<String> {
3727 self.anchor_core.as_ref().map(|ac| {
3728 format!(
3729 "--o1 / CMF_O1 refused: the anchor is native bounded \
3730 (anchor_core kind={} window={} sink={}); the file's operator \
3731 is executed as-is and no post-hoc Nyström overlay applies",
3732 ac.kind, ac.window, ac.sink
3733 )
3734 })
3735 }
3736
3737 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3739 if let Some(c) = &cfg {
3740 if let Some(why) = self.o1_refusal() {
3741 self.o1_flags = Vec::new();
3742 self.o1_cfg = None;
3743 return Err(why);
3744 }
3745 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3746 self.o1_flags.clear();
3747 self.o1_cfg = None;
3748 return Err(format!(
3749 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3750 c.w, c.sink
3751 ));
3752 }
3753 }
3754 self.o1_flags = match &cfg {
3755 Some(c) => {
3756 let mut flags = c.layer_flags(self.num_layers);
3757 for (li, f) in flags.iter_mut().enumerate() {
3758 if *f
3764 && (!matches!(
3765 self.weights.layers[self.phys_layer(li)].attn,
3766 AttnKind::Full { .. }
3767 ) || self.layer_window(li).is_some()
3768 || self.kv_cache.layers[li].sinks.is_some()
3769 || self.layer_v_dim(li) != self.layer_geom(li).1)
3770 {
3771 *f = false;
3772 }
3773 }
3774 flags
3775 }
3776 None => Vec::new(),
3777 };
3778 if let Some(c) = &cfg {
3779 let n = self.o1_flags.iter().filter(|&&f| f).count();
3780 tracing::info!(
3781 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3782 self.num_layers,
3783 c.m,
3784 c.w,
3785 c.sink,
3786 c.rect
3787 );
3788 }
3789 self.o1_cfg = cfg;
3790 Ok(())
3791 }
3792
3793 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3799 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3800 return Err(format!(
3801 "anchor_core kind '{}' is not executable by this runtime",
3802 cfg.kind
3803 ));
3804 }
3805 if cfg.window == 0 {
3806 return Err("anchor_core.window must be >= 1".into());
3807 }
3808 let mut n = 0usize;
3809 for li in 0..self.num_layers {
3810 let pl = self.phys_layer(li);
3811 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3812 if w.window != cfg.window || w.sink != cfg.sink {
3813 return Err(format!(
3814 "layer {li}: bounded weights (window {} sink {}) disagree with \
3815 anchor_core (window {} sink {})",
3816 w.window, w.sink, cfg.window, cfg.sink
3817 ));
3818 }
3819 self.kv_cache.layers[li].install_bounded(cfg.window);
3820 n += 1;
3821 }
3822 }
3823 if n == 0 {
3824 return Err("anchor_core is present but no layer executes it".into());
3825 }
3826 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3827 self.bounded_rope = Some(std::sync::Arc::new(rope));
3828 self.anchor_core = Some(cfg.clone());
3829 self.embryo_graph = None;
3830 tracing::info!(
3831 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3832 cfg.kind,
3833 cfg.window,
3834 cfg.sink,
3835 self.kv_cache
3836 .layers
3837 .iter()
3838 .map(|l| l.bounded_state_bytes())
3839 .max()
3840 .unwrap_or(0)
3841 );
3842 Ok(())
3843 }
3844
3845 pub fn install_wire_identity(&mut self, identity: u64) {
3849 for li in 0..self.kv_cache.layers.len() {
3850 let pl = self.phys_layer(li);
3851 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3852 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3853 Some(AttnKind::Linear(_))
3854 | Some(AttnKind::LinearGdn(_))
3855 | Some(AttnKind::ShortConv(_))
3856 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3857 _ => crate::kv_cache::WireKind::Full,
3858 };
3859 let l = &mut self.kv_cache.layers[li];
3860 l.wire_kind = kind;
3861 l.wire_identity = identity;
3862 l.wire_layer = li as u32;
3865 }
3866 }
3867
3868 pub fn clear_history(&mut self) {
3870 self.kv_history.clear();
3871 self.kv_history_device = false;
3872 self.kv_prefix.clear();
3873 }
3874
3875 pub fn graph_refused(&self) -> bool {
3879 self.graph_refused
3880 .load(std::sync::atomic::Ordering::Relaxed)
3881 }
3882
3883 pub fn mark_graph_refused(&self) {
3885 if !self
3886 .graph_refused
3887 .swap(true, std::sync::atomic::Ordering::Relaxed)
3888 {
3889 tracing::info!(
3890 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3891 self.graph_kv_id
3892 );
3893 }
3894 }
3895
3896 pub fn device_sequence_position(&self) -> Option<usize> {
3900 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3901 }
3902
3903 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3908 let dev = self.device_sequence_position();
3909 if recorded_on_device {
3910 dev == Some(n) && self.embryo_resident_wanted()
3911 } else {
3912 dev.is_none()
3913 }
3914 }
3915
3916 pub(crate) fn invalidate_for_weight_change(&mut self) {
3924 self.clear_sequence_state();
3925 self.embryo_graph = None;
3926 }
3927
3928 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3934 let (n, on_device) = if self.bounded_native() {
3935 (
3936 self.kv_prefix.extension(input_ids),
3937 self.kv_prefix.on_device(),
3938 )
3939 } else {
3940 let h = &self.kv_history;
3941 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3942 (h.len(), self.kv_history_device)
3943 } else {
3944 (0, false)
3945 }
3946 };
3947 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3950 tracing::warn!(
3951 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3952 the device now holds {:?} — re-prefilling from zero",
3953 if on_device { "resident device" } else { "host" },
3954 self.device_sequence_position()
3955 );
3956 return 0;
3957 }
3958 n
3959 }
3960
3961 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3964 self.cached_prefix_len(input_ids)
3965 }
3966
3967 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3973 let on_device = self.device_sequence_position().is_some();
3974 if self.bounded_native() {
3975 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3976 let prev_device = self.kv_prefix.on_device();
3977 self.kv_history.clear();
3978 self.kv_history_device = false;
3979 if keep && prev_device == on_device {
3980 self.kv_prefix.extend(&consumed[reused..]);
3981 } else {
3982 self.kv_prefix.set(consumed);
3983 }
3984 self.kv_prefix.set_on_device(on_device);
3985 } else {
3986 self.kv_history = consumed.to_vec();
3987 self.kv_history_device = on_device;
3988 }
3989 }
3990
3991 pub fn o1_active(&self) -> bool {
3993 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3994 }
3995
3996 pub fn generation_batch_k(&self) -> usize {
4014 if let Some(k) = std::env::var("CMF_BATCH_K")
4015 .ok()
4016 .and_then(|v| v.parse::<usize>().ok())
4017 {
4018 return k;
4019 }
4020 if self.g4_batch_prefill() {
4023 return 128;
4024 }
4025 #[cfg(not(target_os = "macos"))]
4026 if self.graph_prefill_preferred() && !self.o1_active() {
4027 let moe = self
4030 .weights
4031 .layers
4032 .iter()
4033 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4034 let q82 = crate::gpu::moe_q82_graph_on()
4040 && self.weights.layers.iter().any(|lw| {
4041 matches!(&lw.ffn, FfnKind::Moe(m)
4042 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4043 });
4044 return if q82 {
4045 128
4046 } else if moe {
4047 32
4048 } else {
4049 128
4050 };
4051 }
4052 0
4053 }
4054
4055 pub fn generation_graph_prefill(&self) -> bool {
4056 let graph = self.graph_prefill_preferred();
4057 #[cfg(not(target_os = "macos"))]
4068 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4069 return false;
4070 }
4071 graph
4072 }
4073
4074 pub fn o1_device_stats(&self) -> (usize, u64) {
4079 crate::gpu::o1_device_stats(self.graph_kv_id)
4080 }
4081
4082 pub fn o1_begin(&mut self) {
4087 self.o1_begin_with_prefix(None);
4088 }
4089
4090 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4094 if let Some(c) = &self.o1_cfg {
4095 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4096 let boundary = requested_prefix.map(|p| {
4097 p.max(
4098 crate::nystrom::o1_deferred_boundary(w, sink)
4099 .expect("o1 config boundary validated in set_o1"),
4100 )
4101 });
4102 for (li, &f) in self.o1_flags.iter().enumerate() {
4103 if f {
4104 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4105 }
4106 }
4107 }
4108 }
4109
4110 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4112 self.o1_cfg.as_ref().and_then(|c| {
4113 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4114 .map(|floor| requested_prefix.max(floor))
4115 })
4116 }
4117
4118 fn o1_note_transition(&mut self) {
4119 let mut transitioned = false;
4123 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4124 if flagged {
4125 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4126 }
4127 }
4128 if transitioned {
4129 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4130 }
4131 }
4132
4133 fn o1_pending(&self) -> bool {
4134 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4135 f && self.kv_cache.layers[li].seq_len > 0
4136 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4137 })
4138 }
4139
4140 fn o1_fail(&mut self, err: String) {
4141 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4142 self.clear_sequence_state();
4143 self.graph_failed
4144 .store(true, std::sync::atomic::Ordering::Relaxed);
4145 self.cancel
4146 .store(true, std::sync::atomic::Ordering::Relaxed);
4147 }
4148
4149 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4154 if self.o1_cfg.is_none() {
4155 return Ok(false);
4156 }
4157 let mut participating = false;
4158 for li in 0..self.num_layers {
4159 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4160 continue;
4161 }
4162 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4163 return Err(err);
4164 }
4165 if self.kv_cache.layers[li].seq_len == 0 {
4166 continue;
4167 }
4168 participating = true;
4169 let num_heads = self.layer_num_heads(li);
4170 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4171 }
4172 self.o1_note_transition();
4173 for li in 0..self.num_layers {
4174 if self.o1_flags.get(li).copied().unwrap_or(false) {
4175 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4176 return Err(err);
4177 }
4178 }
4179 }
4180 Ok(participating
4181 && (0..self.num_layers).all(|li| {
4182 !self.o1_flags.get(li).copied().unwrap_or(false)
4183 || self.kv_cache.layers[li].seq_len == 0
4184 || self.kv_cache.layers[li].o1_sealed()
4185 }))
4186 }
4187
4188 fn o1_progress(&mut self) {
4191 if !self.o1_active() {
4192 return;
4193 }
4194 for li in 0..self.num_layers {
4195 if self.o1_flags.get(li).copied().unwrap_or(false) {
4196 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4197 self.o1_fail(err);
4198 return;
4199 }
4200 }
4201 }
4202 self.o1_note_transition();
4206 if !self.o1_pending() {
4207 return;
4208 }
4209 if let Err(err) = self.o1_seal_checked() {
4210 self.o1_fail(err);
4211 }
4212 }
4213
4214 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4219 if self
4220 .graph_failed
4221 .swap(false, std::sync::atomic::Ordering::Relaxed)
4222 {
4223 self.cancel
4224 .store(false, std::sync::atomic::Ordering::Relaxed);
4225 self.clear_sequence_state();
4226 return Err(format!("{phase}: deferred O(1) transition failed"));
4227 }
4228 Ok(())
4229 }
4230
4231 pub fn o1_seal(&mut self) {
4235 if let Err(err) = self.o1_seal_checked() {
4236 self.o1_fail(err);
4237 }
4238 }
4239
4240 pub fn set_trace(&mut self, on: bool) {
4242 self.trace = on;
4243 }
4244
4245 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4248 self.rng = match config.seed {
4249 Some(seed) => SplitMix64::new(seed),
4250 None => SplitMix64::from_entropy(),
4251 };
4252 self.sampler_config = config;
4253 }
4254
4255 pub fn set_confidence(&mut self, on: bool) {
4260 self.confidence_on = on;
4261 }
4262
4263 pub fn set_calib_temp(&mut self, t: f32) {
4266 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4267 }
4268
4269 pub fn calib_temp(&self) -> f32 {
4271 self.calib_temp
4272 }
4273
4274 pub fn rope_inv_freq(&self) -> &[f32] {
4278 self.inv_freq.as_slice()
4279 }
4280
4281 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4284 self.rotary_dim = rotary_dim.min(self.head_dim);
4285 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4286 self.embryo_graph = None;
4290 }
4291
4292 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4293 QwenAttnCfg {
4294 num_heads: self.num_heads,
4295 num_kv_heads: self.num_kv_heads,
4296 head_dim: self.head_dim,
4297 hidden_size: self.hidden_size,
4298 position,
4299 inv_freq: &self.inv_freq,
4300 rotary_dim: self.rotary_dim,
4301 scale: self.attn_scale,
4302 softcap: self.attn_softcap,
4303 window: None,
4304 v_norm: false,
4305 qk_norm_after_rope: self.qk_norm_after_rope,
4306 gate_sigmoid: self.proj_gate_sigmoid,
4307 q_norm: None,
4308 k_norm: None,
4309 output_gate: false,
4310 softplus_gate: None,
4311 rope_scale: self.rope_scale,
4312 bias: None,
4313 rms_eps: self.rms_eps,
4314 norm_style: self.norm_style,
4315 pool: self.pool.as_deref(),
4316 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4317 }
4318 }
4319
4320 pub fn generate(
4322 &mut self,
4323 prompt: &str,
4324 max_tokens: usize,
4325 task_mask: Option<&TaskMask>,
4326 on_token: Option<TokenCallback>,
4327 ) -> Result<GenerateResult, String> {
4328 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4329 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4330 }
4331
4332 pub fn generate_from_vl(
4335 &mut self,
4336 input: &crate::dsv41_vision::PreparedVlInputs,
4337 max_tokens: usize,
4338 task_mask: Option<&TaskMask>,
4339 on_token: Option<TokenCallback>,
4340 ) -> Result<GenerateResult, String> {
4341 let Some(dsv41) = &self.dsv41 else {
4342 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4343 };
4344 if input.token_ids.is_empty() {
4345 return Err("empty V4.1 multimodal prompt".into());
4346 }
4347 if input.token_types.len() != input.token_ids.len() {
4348 return Err(format!(
4349 "V4.1 token type count {} != token count {}",
4350 input.token_types.len(),
4351 input.token_ids.len()
4352 ));
4353 }
4354 let dim = dsv41.2.dim;
4355 let mut embeddings = vec![None; input.token_ids.len()];
4356 let mut participates = vec![true; input.token_ids.len()];
4357 if !input.images.is_empty() {
4358 let vision = self
4359 .dsv41_vision
4360 .as_ref()
4361 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4362 for image in &input.images {
4363 let end = image.start.saturating_add(image.types.len());
4364 if end > input.token_ids.len() {
4365 return Err(format!(
4366 "V4.1 image span {}..{} exceeds prompt length {}",
4367 image.start,
4368 end,
4369 input.token_ids.len()
4370 ));
4371 }
4372 let mut span = vec![0.0f32; image.types.len() * dim];
4373 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4374 for (offset, &kind) in image.types.iter().enumerate() {
4375 let pos = image.start + offset;
4376 if input.token_types[pos] != kind {
4377 return Err(format!(
4378 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4379 input.token_types[pos]
4380 ));
4381 }
4382 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4383 participates[pos] = false;
4384 }
4385 }
4386 }
4387 for (pos, &kind) in input.token_types.iter().enumerate() {
4388 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4389 return Err(format!("V4.1 text position {pos} has an image embedding"));
4390 }
4391 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4392 return Err(format!("V4.1 image position {pos} has no image embedding"));
4393 }
4394 }
4395 self.dsv41_prefill = Some((embeddings, participates));
4396 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4397 self.dsv41_prefill = None;
4398 result
4399 }
4400
4401 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4403 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4404 }
4405
4406 pub fn generate_from_ids(
4414 &mut self,
4415 input_ids: &[u32],
4416 max_tokens: usize,
4417 task_mask: Option<&TaskMask>,
4418 on_token: Option<TokenCallback>,
4419 ) -> Result<GenerateResult, String> {
4420 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4421 }
4422
4423 pub fn generate_from_embeds(
4429 &mut self,
4430 input_ids: &[u32],
4431 prompt_rows: &[f32],
4432 max_tokens: usize,
4433 task_mask: Option<&TaskMask>,
4434 on_token: Option<TokenCallback>,
4435 ) -> Result<GenerateResult, String> {
4436 if input_ids.is_empty()
4437 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4438 {
4439 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4440 }
4441 if prompt_rows.iter().any(|x| !x.is_finite()) {
4442 return Err("embedded prompt contains non-finite values".into());
4443 }
4444 if !self.can_prefill_batched()
4445 || self.dyn_router.is_some()
4446 || self.o1_active()
4447 || self.mtp.is_some()
4448 || self.gpu_plan.is_some()
4449 {
4450 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4451 }
4452 self.generate_with_prompt_rows(
4453 input_ids,
4454 Some(prompt_rows),
4455 max_tokens,
4456 task_mask,
4457 on_token,
4458 )
4459 }
4460
4461 fn generate_with_prompt_rows(
4462 &mut self,
4463 input_ids: &[u32],
4464 prompt_rows: Option<&[f32]>,
4465 max_tokens: usize,
4466 task_mask: Option<&TaskMask>,
4467 mut on_token: Option<TokenCallback>,
4468 ) -> Result<GenerateResult, String> {
4469 #[cfg(target_os = "macos")]
4470 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4471 if std::env::var("CMF_TRACE_H").is_ok() {
4472 eprintln!("input_ids: {input_ids:?}");
4473 }
4474 if input_ids.is_empty() {
4475 return Err("empty prompt: nothing to generate from".to_string());
4476 }
4477 self.graph_failed
4481 .store(false, std::sync::atomic::Ordering::Relaxed);
4482 let task_mask = self.drop_open_mask(task_mask);
4487
4488 let mut reuse_from = {
4496 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4497 if on
4498 && prompt_rows.is_none()
4499 && task_mask.is_none()
4500 && self.mtp.is_none()
4501 && !(self.mimo_mtp.is_some() && self.speculative)
4502 && self.o1_cfg.is_none()
4503 && self.dsv41.is_none()
4504 {
4505 self.cached_prefix_len(input_ids)
4506 } else {
4507 0
4508 }
4509 };
4510 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4513 reuse_from = 0;
4514 }
4515 self.last_prefill_tokens = input_ids.len() - reuse_from;
4516 let bounded_native = self.bounded_native();
4517 if reuse_from == 0 {
4518 self.clear_sequence_state();
4520 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4521 eprintln!(
4522 "kv-reuse: {} of {} prompt positions already cached",
4523 reuse_from,
4524 input_ids.len()
4525 );
4526 }
4527 crate::gpu::graph_race_begin_generation();
4528 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4532 std::env::var("CMF_O1_PREFILL")
4533 .ok()
4534 .and_then(|v| v.parse::<usize>().ok())
4535 .filter(|&p| p > 0)
4536 } else {
4537 None
4538 };
4539 if task_mask.is_none() {
4540 self.o1_begin_with_prefix(o1_prefill);
4541 }
4542
4543 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4549 #[cfg(target_os = "macos")]
4588 let metal_graph = crate::gpu::q1_force()
4589 && crate::gpu::enabled_here()
4590 && std::env::var("CMF_GPU_BLOCK")
4591 .map(|v| v != "0")
4592 .unwrap_or(true);
4593 #[cfg(not(target_os = "macos"))]
4594 let metal_graph = false;
4595 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4596 let spec_cheap_round =
4600 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4601 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4602 || match spec_sample_env.as_deref() {
4603 Some("1") => true,
4604 Some(_) => false,
4605 None => metal_graph && spec_cheap_round,
4606 };
4607 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4623 for lw in &self.weights.layers {
4624 if let FfnKind::Dense(d) = &lw.ffn {
4625 dense_n += 1;
4626 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4627 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4628 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4629 {
4630 dense_q4tp += 1;
4631 }
4632 }
4633 }
4634 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4635 let penalized = !metal_graph
4647 && (self.sampler_config.repetition_penalty != 1.0
4648 || self.sampler_config.presence_penalty != 0.0
4649 || !self.sampler_config.suppress_tokens.is_empty());
4650 #[cfg(feature = "gpu")]
4655 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4656 #[cfg(not(feature = "gpu"))]
4657 let metal_wgpu = false;
4658 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4659 let spec_wanted = match spec_env.as_deref() {
4660 Some("0") => false,
4661 Some(_) => {
4662 if metal_wgpu {
4663 tracing::warn!(
4664 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4665 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4666 );
4667 }
4668 true
4669 }
4670 None => spec_default_ok && !penalized && !metal_wgpu,
4671 };
4672 let graph_spec = self.speculative
4676 && (graph_on || metal_graph)
4677 && self.mtp.is_some()
4678 && task_mask.is_none()
4679 && !self.o1_active()
4680 && spec_sampling_ok
4681 && spec_wanted;
4682 #[cfg(target_os = "macos")]
4686 if metal_graph {
4687 static SAID: std::sync::Once = std::sync::Once::new();
4688 SAID.call_once(|| {
4689 let spec = if graph_spec {
4690 let k = std::env::var("CMF_GRAPH_SPEC_K")
4691 .ok()
4692 .and_then(|v| v.parse::<usize>().ok())
4693 .filter(|&v| (1..=8).contains(&v))
4694 .unwrap_or(7);
4695 let arm = if self.sampler_config.temperature < 1e-6 {
4696 "greedy"
4697 } else {
4698 "sampling"
4699 };
4700 format!(
4701 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4702 Self::draft_vocab_rows(usize::MAX)
4703 )
4704 } else if !self.speculative {
4705 "spec off (CMF_MTP=0)".to_string()
4706 } else if self.mtp.is_none() {
4707 "spec off (no MTP head)".to_string()
4708 } else if !spec_sampling_ok {
4709 if spec_cheap_round {
4710 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4711 } else {
4712 "spec off (sampling without a top-k: the dense chain \
4713 costs more than it saves)"
4714 .to_string()
4715 }
4716 } else if !spec_wanted {
4717 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4718 } else if task_mask.is_some() {
4719 "spec off (task mask)".to_string()
4720 } else {
4721 "spec off (O(1) attention)".to_string()
4722 };
4723 let on = |var: &str| {
4724 if std::env::var(var).as_deref() == Ok("0") {
4725 "off"
4726 } else {
4727 "on"
4728 }
4729 };
4730 tracing::info!(
4731 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4732 MTP graph {}, attend {}, probe {}",
4733 if crate::gpu_metal::state4_on() {
4734 "on"
4735 } else {
4736 "off"
4737 },
4738 if crate::gpu_metal::async_replay_on() {
4739 "on"
4740 } else {
4741 "off"
4742 },
4743 on("CMF_METAL_PREFILL"),
4744 on("CMF_MTP_GRAPH"),
4745 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4746 if crate::gpu::probe_enabled() {
4747 "bypassed (q1 force)"
4748 } else {
4749 "off"
4750 },
4751 );
4752 });
4753 }
4754 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4761 let spec_active = self.speculative
4762 && self.mtp.is_some()
4763 && task_mask.is_none()
4764 && !self.o1_active()
4765 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4766 let mut mtp = if spec_active { self.mtp.take() } else { None };
4769 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4770 eprintln!(
4771 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4772 mtp.is_some(),
4773 self.speculative,
4774 self.sampler_config.temperature < 1e-6,
4775 );
4776 }
4777 if let Some(m) = &mut mtp {
4778 m.kv.clear();
4779 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4781 self.mtp_graph_mode = None;
4782 }
4783 let mimo_spec = self.speculative
4787 && self.mimo_mtp.is_some()
4788 && task_mask.is_none()
4789 && !self.o1_active()
4790 && self.dyn_router.is_none()
4791 && self.sampler_config.temperature < 1e-6
4792 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4793 if let Some(st) = self.mimo_mtp.as_mut() {
4794 st.reset();
4795 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4796 Self::mimo_mtp_hist_cap(st, input_ids.len());
4797 }
4798 }
4799 let mut router = if mtp.is_none() {
4803 self.dyn_router.take()
4804 } else {
4805 None
4806 };
4807 let mut reuse_from = reuse_from;
4808 if let Some(r) = &mut router {
4809 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4812 let _ = self.set_active_skill(None);
4815 reuse_from = 0;
4816 self.last_prefill_tokens = input_ids.len();
4817 }
4818 }
4819
4820 let mut all_ids = input_ids.to_vec();
4821 let mut generated = 0usize;
4822 let mut finish_reason = "max_tokens".to_string();
4823 let mut drafted = 0usize;
4824 let mut accepted = 0usize;
4825 let mut dsv4_spec_bad = 0usize;
4832 let mut dsv4_spec_retry_at = 0usize;
4833 let mut confidence: Vec<f32> = Vec::new();
4834 let trace_on = self.trace;
4835 let calib_temp = self.calib_temp;
4836 let mut traces: Vec<TokenTrace> = Vec::new();
4837
4838 let mut hidden = vec![0.0f32; self.hidden_size];
4844 let mut pos = reuse_from;
4845 let fuse_lm = mtp.is_none()
4854 && router.is_none()
4855 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4856 self.graph_logits = None;
4857 self.graph_want_logits = false;
4858 let _tpf = std::time::Instant::now();
4859 let batch_k = self.generation_batch_k();
4860 if let Some(rows) = prompt_rows {
4861 let hs = self.hidden_size;
4862 let chunk = self.prefill_chunk().max(1);
4863 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4864 let end = (pos + chunk).min(input_ids.len());
4865 let hb = match self.prefill_input_rows(
4866 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
4867 pos,
4868 task_mask,
4869 ) {
4870 Ok(hb) => hb,
4871 Err(err) => {
4872 self.finish_generation(&mut mtp, &mut router, true);
4873 return Err(err);
4874 }
4875 };
4876 if mimo_spec {
4877 self.mimo_note_rows(&hb, pos);
4878 }
4879 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4880 pos = end;
4881 }
4882 }
4883 while self.qwen4_exp.is_some()
4894 && mtp.is_none()
4895 && pos < input_ids.len()
4896 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4897 {
4898 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4901 let want_logits = end == input_ids.len();
4902 let mut lg = Vec::new();
4903 if let Some(b) = &mut self.qwen4_exp {
4904 crate::qwen4_exp::forward_tokens(
4905 &b.0,
4906 &b.1,
4907 &b.2,
4908 &mut b.3,
4909 &input_ids[pos..end],
4910 pos,
4911 &self.inv_freq,
4912 self.pool.as_deref(),
4913 &mut lg,
4914 want_logits,
4915 );
4916 }
4917 if want_logits {
4918 self.graph_logits = Some(lg);
4919 }
4920 pos = end;
4921 hidden.fill(0.0);
4922 }
4923 while self.dsv4.is_some()
4924 && mtp.is_none()
4925 && pos < input_ids.len()
4926 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4927 {
4928 let end = (pos + prefill_chunk()).min(input_ids.len());
4929 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4930 let mut lg = Vec::new();
4931 if let Some(b) = &mut self.dsv4 {
4932 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4933 crate::dsv4::forward_chunk(
4934 g,
4935 layers,
4936 &cfg,
4937 st,
4938 &ids,
4939 pos,
4940 &self.inv_freq,
4941 self.pool.as_deref(),
4942 &mut lg,
4943 end == input_ids.len(),
4944 );
4945 }
4946 if end == input_ids.len() {
4947 self.graph_logits = Some(lg);
4948 }
4949 pos = end;
4950 hidden = vec![0.0; self.hidden_size];
4951 }
4952 let dsv41_prefill = self.dsv41_prefill.take();
4953 while self.dsv41.is_some()
4954 && mtp.is_none()
4955 && pos < input_ids.len()
4956 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4957 {
4958 let end = (pos + prefill_chunk()).min(input_ids.len());
4959 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4960 let mut lg = Vec::new();
4961 if let Some(b) = &mut self.dsv41 {
4962 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4963 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4964 crate::dsv41::forward_chunk_masked_with_embeddings(
4965 g,
4966 layers,
4967 cfg,
4968 st,
4969 &ids,
4970 pos,
4971 &embeddings[pos..end],
4972 &participates[pos..end],
4973 self.pool.as_deref(),
4974 &mut lg,
4975 );
4976 } else {
4977 crate::dsv41::forward_chunk(
4978 g,
4979 layers,
4980 cfg,
4981 st,
4982 &ids,
4983 pos,
4984 self.pool.as_deref(),
4985 &mut lg,
4986 );
4987 }
4988 }
4989 if end == input_ids.len() {
4990 self.graph_logits = Some(lg);
4991 }
4992 pos = end;
4993 hidden = vec![0.0; self.hidden_size];
4994 }
4995 let dyn_prefill = router.is_some();
5000 let o1_prefill_limit = o1_prefill
5008 .and_then(|requested| self.o1_effective_boundary(requested))
5009 .map(|boundary| boundary.min(input_ids.len()));
5010 let mut o1_sealed = false;
5011 if let Some(limit) = o1_prefill_limit {
5012 if self.can_prefill_batched() && limit > 2 {
5015 let chunk = self.prefill_chunk();
5016 let hs = self.hidden_size;
5017 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5018 let end = (pos + chunk).min(limit);
5019 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5020 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5021 pos = end;
5022 }
5023 } else {
5024 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5025 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5026 pos += 1;
5027 }
5028 }
5029 if pos >= limit {
5030 o1_sealed = match self.o1_seal_checked() {
5031 Ok(sealed) => sealed,
5032 Err(err) => {
5033 self.finish_generation(&mut mtp, &mut router, true);
5034 return Err(err);
5035 }
5036 };
5037 tracing::info!(
5038 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5039 o1_prefill.unwrap_or(0),
5040 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5041 .unwrap_or(limit),
5042 limit,
5043 input_ids.len()
5044 );
5045 }
5046 }
5047 let graph_prefill = self.graph_prefill_preferred();
5053 #[cfg(target_os = "macos")]
5061 if task_mask.is_none()
5062 && !dyn_prefill
5063 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5064 && crate::gpu::enabled_here()
5065 && self.gdn_cfg.is_some()
5066 && !self.per_token_stack()
5067 && input_ids.len() > 8
5068 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5069 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5070 {
5071 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5072 .ok()
5073 .and_then(|v| v.parse().ok())
5074 .filter(|&v| (16..=512).contains(&v))
5075 .unwrap_or(256);
5076 let hs = self.hidden_size;
5077 let _tp = std::time::Instant::now();
5078 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5079 let end = (pos + chunk).min(input_ids.len());
5080 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5081 MetalPrefillOutcome::Completed(hb) => hb,
5082 MetalPrefillOutcome::Declined => break,
5083 MetalPrefillOutcome::Failed => {
5084 self.finish_generation(&mut mtp, &mut router, true);
5085 return Err("ordinary Metal prefill failed after admission".into());
5086 }
5087 };
5088 if let Some(m) = &mut mtp {
5089 let n_pairs = if end < input_ids.len() {
5090 end - pos
5091 } else {
5092 end - pos - 1
5093 };
5094 if n_pairs > 0 {
5095 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5096 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5097 .collect();
5098 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5099 for (j, (h, t)) in pairs.iter().enumerate() {
5100 let h = h.to_vec();
5101 let _ = self.mtp_step(m, &h, *t, pos + j);
5102 }
5103 }
5104 }
5105 }
5106 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5107 pos = end;
5108 }
5109 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5110 eprintln!(
5111 "metal-prefill: {} of {} tokens in {:.1} ms",
5112 pos,
5113 input_ids.len(),
5114 _tp.elapsed().as_secs_f64() * 1e3
5115 );
5116 }
5117 }
5118 #[cfg(not(target_os = "macos"))]
5123 if task_mask.is_none()
5124 && !dyn_prefill
5125 && mtp.is_none()
5126 && o1_prefill.is_none()
5127 && batch_k > 0
5128 && pos + 1 < input_ids.len()
5129 && self.g4_batch_prefill()
5130 {
5131 let t_g4 = std::time::Instant::now();
5132 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5133 Ok((n, last)) => {
5134 if n > 0 {
5135 hidden.copy_from_slice(&last);
5136 }
5137 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5138 eprintln!(
5139 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5140 input_ids.len() - pos,
5141 t_g4.elapsed().as_secs_f64() * 1e3
5142 );
5143 }
5144 pos += n;
5145 }
5146 Err(e) => {
5147 self.finish_generation(&mut mtp, &mut router, true);
5148 return Err(e);
5149 }
5150 }
5151 }
5152 self.mimo_moe_prepare();
5153 #[cfg(not(target_os = "macos"))]
5159 if task_mask.is_none()
5160 && !dyn_prefill
5161 && !graph_prefill
5162 && mtp.is_none()
5163 && o1_prefill.is_none()
5164 && !self.o1_active()
5165 && input_ids.len() > 2
5166 && self.batch_prefix_prefill()
5167 {
5168 let chunk = self.prefill_chunk().max(1);
5169 let hs = self.hidden_size;
5170 let t_bp = std::time::Instant::now();
5171 let pos0 = pos;
5172 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5173 let end = (pos + chunk).min(input_ids.len());
5174 let bk = end - pos;
5175 let mut hiddens = vec![0f32; bk * hs];
5176 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5177 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5178 }
5179 let positions: Vec<usize> = (pos..end).collect();
5180 let mut run = 0usize;
5181 let outcome = self.try_batch_graph_wgpu_prefix(
5182 &mut hiddens,
5183 &positions,
5184 bk,
5185 None,
5186 Some(&mut run),
5187 );
5188 match outcome {
5189 crate::gpu::BatchGraphOutcome::Completed => {
5190 let hb = if run < self.num_layers {
5191 self.prefill_batch_span(
5192 PrefillIn::Hidden(&hiddens),
5193 pos,
5194 None,
5195 run,
5196 self.num_layers,
5197 )
5198 } else {
5199 hiddens
5200 };
5201 if mimo_spec {
5202 self.mimo_note_rows(&hb, pos);
5203 }
5204 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5205 pos = end;
5206 }
5207 crate::gpu::BatchGraphOutcome::Failed => {
5208 self.finish_generation(&mut mtp, &mut router, true);
5209 return Err("batched prefix prefill failed after admission".into());
5210 }
5211 crate::gpu::BatchGraphOutcome::Declined => {
5212 #[cfg(feature = "gpu")]
5215 if pos > pos0 {
5216 self.pull_lagging_host_kv(0, self.num_layers, pos);
5217 }
5218 break;
5219 }
5220 }
5221 }
5222 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5223 eprintln!(
5224 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5225 pos - pos0,
5226 input_ids.len(),
5227 t_bp.elapsed().as_secs_f64() * 1e3
5228 );
5229 }
5230 }
5231 if task_mask.is_none()
5232 && !dyn_prefill
5233 && !graph_prefill
5234 && self.can_prefill_batched()
5235 && !self.per_token_stack()
5236 && o1_prefill.is_none()
5237 && input_ids.len() > 2
5238 {
5239 let chunk = self.prefill_chunk();
5245 let hs = self.hidden_size;
5246 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5247 let end = (pos + chunk).min(input_ids.len());
5248 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5249 if mimo_spec {
5250 self.mimo_note_rows(&hb, pos);
5251 }
5252 if let Some(m) = &mut mtp {
5253 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5254 .ok()
5255 .and_then(|v| v.parse().ok())
5256 .unwrap_or(0);
5257 for p in pos..end {
5258 if p + 1 < input_ids.len() {
5259 if probe >= 1 && p + 2 < input_ids.len() {
5260 let (d1, mut hx) = self.mtp_step_h(
5264 m,
5265 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5266 input_ids[p + 1],
5267 p,
5268 );
5269 let mut ok = d1 == input_ids[p + 2];
5270 Self::chain_probe_note(0, ok);
5271 let mut d_prev = d1;
5272 let mut extra = 0usize;
5273 for j in 1..probe {
5274 if p + 2 + j >= input_ids.len() {
5275 break;
5276 }
5277 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5278 extra += 1;
5279 ok = ok && dj == input_ids[p + 2 + j];
5280 Self::chain_probe_note(j, ok);
5281 d_prev = dj;
5282 hx = hj;
5283 }
5284 m.kv.truncate_last(extra);
5285 } else {
5286 let _ = self.mtp_step(
5287 m,
5288 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5289 input_ids[p + 1],
5290 p,
5291 );
5292 }
5293 }
5294 }
5295 }
5296 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5297 pos = end;
5298 }
5299 }
5300 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5301 if task_mask.is_none()
5302 && !dyn_prefill
5303 && !graph_prefill
5304 && !pair_off
5305 && self.pair_supported()
5306 && o1_prefill.is_none()
5307 {
5308 while pos + 1 < input_ids.len()
5309 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5310 {
5311 let e1 = self.embed_single(input_ids[pos]);
5312 let e2 = self.embed_single(input_ids[pos + 1]);
5313 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5314 if mimo_spec {
5315 self.mimo_note_rows(&h1, pos);
5316 self.mimo_note_rows(&h2, pos + 1);
5317 }
5318 self.commit_linear_scratch();
5320 if let Some(m) = &mut mtp {
5321 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5322 if pos + 2 < input_ids.len() {
5323 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5324 .ok()
5325 .and_then(|v| v.parse().ok())
5326 .unwrap_or(0);
5327 if probe >= 1 && pos + 3 < input_ids.len() {
5328 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5332 let mut ok = d1 == input_ids[pos + 3];
5333 Self::chain_probe_note(0, ok);
5334 let mut d_prev = d1;
5335 let mut extra = 0usize;
5336 for j in 1..probe {
5337 if pos + 3 + j >= input_ids.len() {
5338 break;
5339 }
5340 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5341 extra += 1;
5342 ok = ok && dj == input_ids[pos + 3 + j];
5343 Self::chain_probe_note(j, ok);
5344 d_prev = dj;
5345 hx = hj;
5346 }
5347 m.kv.truncate_last(extra);
5348 } else {
5349 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5350 }
5351 }
5352 }
5353 hidden = h2;
5354 pos += 2;
5355 }
5356 }
5357 let o1_batch_ready = o1_sealed
5370 && o1_prefill.is_some()
5371 && mtp.is_none()
5372 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5373 && (0..self.num_layers).all(|li| {
5374 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5375 cache.o1.is_none() || cache.o1_views().is_some()
5376 });
5377 let mtp_batch_prefill = mtp.is_some()
5382 && graph_prefill
5383 && task_mask.is_none()
5384 && !dyn_prefill
5385 && !self.o1_active()
5386 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5387 if batch_k > 0
5388 && (graph_prefill || o1_batch_ready)
5389 && task_mask.is_none()
5390 && (!self.o1_active() || o1_batch_ready)
5391 && (mtp.is_none() || mtp_batch_prefill)
5392 && !dyn_prefill
5393 && pos + 1 < input_ids.len()
5394 {
5395 let hs = self.hidden_size;
5396 let chunk = batch_k;
5397 while pos < input_ids.len() {
5398 let end = (pos + chunk).min(input_ids.len());
5399 let bk = end - pos;
5400 let mut hiddens = vec![0f32; bk * hs];
5401 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5402 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5403 }
5404 let positions: Vec<usize> = (pos..end).collect();
5405 let t_chunk = std::time::Instant::now();
5406 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5407 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5408 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5409 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5410 eprintln!(
5411 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5412 if o1_batch_ready {
5413 "o1"
5414 } else if mtp_batch_prefill {
5415 "ordinary_mtp"
5416 } else {
5417 "ordinary"
5418 },
5419 bk as f64 / (ms / 1000.0)
5420 );
5421 }
5422 {
5423 use std::sync::atomic::{AtomicBool, Ordering};
5424 static SAID: AtomicBool = AtomicBool::new(false);
5425 if !SAID.swap(true, Ordering::Relaxed) {
5426 if ok_b {
5427 tracing::info!(
5428 "batched prefill: ACTIVE mode={} (k={bk})",
5429 if o1_batch_ready {
5430 "o1"
5431 } else if mtp_batch_prefill {
5432 "ordinary_mtp"
5433 } else {
5434 "ordinary"
5435 }
5436 );
5437 } else {
5438 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5439 }
5440 }
5441 }
5442 if ok_b {
5443 if mimo_spec {
5444 self.mimo_note_rows(&hiddens, pos);
5445 }
5446 if mtp_batch_prefill {
5447 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5448 if n_pairs > 0 {
5449 let rows: Vec<Vec<f32>> = (0..n_pairs)
5455 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5456 .collect();
5457 let pairs: Vec<(&[f32], u32)> = rows
5458 .iter()
5459 .enumerate()
5460 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5461 .collect();
5462 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5463 eprintln!(
5464 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5465 pos,
5466 n_pairs,
5467 pos + n_pairs - 1,
5468 );
5469 }
5470 let warm_error = if let Some(m) = mtp.as_mut() {
5471 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5472 } else {
5473 None
5474 };
5475 if let Some(err) = warm_error {
5476 self.finish_generation(&mut mtp, &mut router, true);
5481 return Err(err.to_string());
5482 }
5483 }
5484 }
5485 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5486 pos = end;
5487 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5488 self.finish_generation(&mut mtp, &mut router, true);
5493 return Err(if o1_batch_ready {
5494 "sealed O(1) batch graph failed after admission".to_string()
5495 } else {
5496 "ordinary recurrent batch graph failed after admission".to_string()
5497 });
5498 } else {
5499 break; }
5501 }
5502 }
5503 if graph_prefill
5507 && task_mask.is_none()
5508 && mtp.is_none()
5509 && !dyn_prefill
5510 && pos == 0
5511 && input_ids.len() > 1
5512 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5513 {
5514 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5515 self.graph_logits = Some(lg);
5516 hidden = vec![0.0; self.hidden_size];
5517 pos = input_ids.len();
5518 }
5519 }
5520 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5521 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5522 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5523 if mimo_spec {
5524 self.mimo_note_rows(&hidden, pos);
5525 }
5526 if let Some(m) = &mut mtp {
5527 if pos + 1 < input_ids.len() {
5528 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5534 .ok()
5535 .and_then(|v| v.parse().ok())
5536 .unwrap_or(0);
5537 if probe >= 1 && pos + 2 < input_ids.len() {
5538 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5539 let mut ok = d1 == input_ids[pos + 2];
5540 Self::chain_probe_note(0, ok);
5541 let mut d_prev = d1;
5542 let mut extra = 0usize;
5543 for j in 1..probe {
5544 if pos + 2 + j >= input_ids.len() {
5545 break;
5546 }
5547 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5548 extra += 1;
5549 ok = ok && dj == input_ids[pos + 2 + j];
5550 Self::chain_probe_note(j, ok);
5551 d_prev = dj;
5552 hx = hj;
5553 }
5554 m.kv.truncate_last(extra);
5557 } else {
5558 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5559 }
5560 }
5561 }
5562 pos += 1;
5563 }
5564 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5565 eprintln!(
5566 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5567 input_ids.len(),
5568 _tpf.elapsed().as_secs_f64() * 1000.0
5569 );
5570 }
5571 if self
5572 .graph_failed
5573 .swap(false, std::sync::atomic::Ordering::Relaxed)
5574 {
5575 self.finish_generation(&mut mtp, &mut router, true);
5580 return Err("GPU token graph failed during prefill".to_string());
5581 }
5582 if self
5585 .cancel
5586 .swap(false, std::sync::atomic::Ordering::Relaxed)
5587 {
5588 self.finish_generation(&mut mtp, &mut router, true);
5592 return Ok(GenerateResult {
5593 text: String::new(),
5594 token_ids: Vec::new(),
5595 prompt_tokens: input_ids.len(),
5596 tokens_generated: 0,
5597 finish_reason: "cancelled".to_string(),
5598 mtp_drafted: 0,
5599 mtp_accepted: 0,
5600 token_confidence: Vec::new(),
5601 traces: Vec::new(),
5602 });
5603 }
5604
5605 if !o1_sealed {
5608 match self.o1_seal_checked() {
5609 Ok(_) => {}
5610 Err(err) => {
5611 self.finish_generation(&mut mtp, &mut router, true);
5612 return Err(err);
5613 }
5614 }
5615 }
5616
5617 macro_rules! commit {
5619 ($id:expr) => {{
5620 all_ids.push($id);
5621 generated += 1;
5622 self.note_draft_id($id);
5623 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5624 finish_reason = "stop".to_string();
5625 false
5626 } else {
5627 let token_text = self.tokenizer.decode_token($id);
5628 let mut go = true;
5629 if let Some(ref mut cb) = on_token {
5630 if !cb(&token_text) {
5631 finish_reason = "cancelled".to_string();
5632 go = false;
5633 }
5634 }
5635 go
5636 }
5637 }};
5638 }
5639
5640 let mut spec_trial = SpecTrial::Spec {
5651 t0: std::time::Instant::now(),
5652 gen0: generated,
5653 rounds: 0,
5654 };
5655 let mut spec_mon = SpecMon {
5661 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5662 ..SpecMon::default()
5663 };
5664 let mut spec_watchdog_off = false;
5665 let mut spec_walls: Vec<f32> = Vec::new();
5668 let mut spec_round_end: Option<std::time::Instant> = None;
5671 if mimo_spec {
5672 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5673 if let Some(mut st) = self.mimo_mtp.take() {
5674 self.mimo_mtp_probe(&mut st, input_ids, &path);
5675 self.mimo_mtp = Some(st);
5676 }
5677 }
5678 }
5679 let mut next_pos = input_ids.len();
5681 'decode: while generated < max_tokens {
5682 if self
5683 .graph_failed
5684 .swap(false, std::sync::atomic::Ordering::Relaxed)
5685 {
5686 self.finish_generation(&mut mtp, &mut router, true);
5691 return Err("GPU token graph failed during decode".to_string());
5692 }
5693 if self
5694 .cancel
5695 .swap(false, std::sync::atomic::Ordering::Relaxed)
5696 {
5697 finish_reason = "cancelled".to_string();
5698 break 'decode;
5699 }
5700 if mimo_spec && next_pos > 0 {
5705 self.mimo_note_rows(&hidden, next_pos - 1);
5708 }
5709 let forced = self.spec_forced.take();
5710 let mut logits = match (forced, self.graph_logits.take()) {
5711 (Some(_), _) => Vec::new(),
5712 (None, Some(lg)) => lg,
5713 (None, None) => {
5714 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5715 #[cfg(feature = "gpu")]
5720 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5721 lg
5722 } else {
5723 inference::rms_norm_into(
5724 &hidden,
5725 &self.weights.final_norm,
5726 self.rms_eps,
5727 self.norm_style,
5728 &mut self.ws.n1,
5729 );
5730 self.lm_head_forward(&self.ws.n1)
5731 };
5732 #[cfg(not(feature = "gpu"))]
5733 let logits_out = {
5734 inference::rms_norm_into(
5735 &hidden,
5736 &self.weights.final_norm,
5737 self.rms_eps,
5738 self.norm_style,
5739 &mut self.ws.n1,
5740 );
5741 self.lm_head_forward(&self.ws.n1)
5742 };
5743 logits_out
5744 }
5745 };
5746 if generated
5749 == std::env::var("CMF_LOGIT_DUMP_STEP")
5750 .ok()
5751 .and_then(|v| v.parse().ok())
5752 .unwrap_or(0)
5753 {
5754 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5755 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5756 for v in hidden.iter().chain(logits.iter()) {
5757 bytes.extend_from_slice(&v.to_le_bytes());
5758 }
5759 if let Err(e) = std::fs::write(&path, &bytes) {
5760 eprintln!("logit dump: failed to write {path}: {e}");
5761 self.finish_generation(&mut mtp, &mut router, true);
5762 return Err(format!("logit dump write failed: {e}"));
5763 }
5764 }
5765 }
5766 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5770 if !logits.is_empty() {
5771 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5772 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5773 if let Err(e) =
5774 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5775 {
5776 eprintln!("logit dump: failed to write {}: {e}", path.display());
5777 }
5778 }
5779 }
5780 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5781 Some(c) => c,
5782 None => {
5783 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5784 sampler::sample_with_scratch_pool(
5785 &logits,
5786 &self.sampler_config,
5787 self.sampler_config.penalty_past(&all_ids, bounded_native),
5788 &mut self.rng,
5789 &mut self.sampler_scratch,
5790 self.pool.as_deref(),
5791 )
5792 }
5793 };
5794 if self.confidence_on {
5795 confidence.push(if logits.is_empty() {
5796 0.0
5797 } else {
5798 sampler::top1_prob_pool(
5799 self.pool.as_deref(),
5800 &mut self.sampler_scratch,
5801 &logits,
5802 t_next,
5803 calib_temp,
5804 )
5805 });
5806 }
5807 if !logits.is_empty() {
5808 attention::recycle_buf(&mut logits);
5809 }
5810 if trace_on {
5811 let skill = router.as_ref().and_then(|r| r.active_id());
5815 traces.push(TokenTrace {
5816 t: generated,
5817 token_id: t_next,
5818 confidence: confidence.last().copied().unwrap_or(0.0),
5819 active_skill: skill,
5820 recon: None,
5821 switched: false,
5822 });
5823 }
5824 if !commit!(t_next) {
5825 break 'decode;
5826 }
5827 if generated >= max_tokens {
5828 break 'decode;
5829 }
5830
5831 self.swa_trim_tails();
5834 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5835 static SAID: std::sync::Once = std::sync::Once::new();
5841 SAID.call_once(|| {
5842 tracing::warn!(
5843 "KV cache full at {} positions — evicting half; quality \
5844 will degrade. Raise CMF_MAX_SEQ.",
5845 self.kv_cache.max_seq_len,
5846 );
5847 });
5848 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5849 self.kv_cache.evict(keep);
5850 }
5851
5852 if graph_spec {
5855 match spec_trial {
5856 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5857 spec_mon.plain_ms =
5858 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5859 let keep = spec_mon.pays();
5860 tracing::info!(
5861 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5862 spec_mon.tokens,
5863 spec_mon.round_ms,
5864 spec_mon.plain_ms,
5865 if keep { "speculating" } else { "plain" }
5866 );
5867 spec_mon.fails = 0;
5868 spec_trial = SpecTrial::Decided {
5869 spec: keep,
5870 recheck_at: if keep { usize::MAX } else { generated + 128 },
5871 };
5872 }
5873 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5874 spec_mon.n = 0;
5875 spec_trial = SpecTrial::Spec {
5876 t0: std::time::Instant::now(),
5877 gen0: generated,
5878 rounds: 0,
5879 };
5880 }
5881 _ => {}
5882 }
5883 spec_watchdog_off = matches!(
5884 spec_trial,
5885 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5886 );
5887 }
5888 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5890 let budget = max_tokens - generated - 1;
5891 if let Some(mut st) = self.mimo_mtp.take() {
5892 let k = st.depth.min(budget);
5893 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5894 self.mimo_mtp = Some(st);
5895 let r = match r {
5896 Ok(r) => r,
5897 Err(err) => {
5898 self.finish_generation(&mut mtp, &mut router, true);
5899 return Err(err);
5900 }
5901 };
5902 if let Some(r) = r {
5903 drafted += r.drafted;
5904 accepted += r.accepted.len();
5905 let mut stopped = false;
5906 for &id in &r.accepted {
5907 if self.confidence_on {
5908 confidence.push(0.0);
5909 }
5910 if !commit!(id) {
5911 stopped = true;
5912 break;
5913 }
5914 }
5915 if stopped {
5916 break 'decode;
5917 }
5918 next_pos += r.accepted.len() + 1;
5919 hidden = r.hidden;
5920 self.graph_logits = Some(r.logits);
5923 continue 'decode;
5924 }
5925 }
5926 }
5927 #[cfg(feature = "gpu")]
5930 if self.speculative
5931 && self.qwen4_exp.is_some()
5932 && task_mask.is_none()
5933 && self.sampler_config.temperature < 1e-6
5934 && generated + 1 < max_tokens
5935 && next_pos > 0
5936 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5937 {
5938 let r = match &mut self.qwen4_exp {
5939 Some(b) => crate::qwen4_exp::spec_round(
5940 &b.0,
5941 &b.1,
5942 &b.2,
5943 &mut b.3,
5944 next_pos,
5945 &all_ids,
5946 &self.inv_freq,
5947 self.pool.as_deref(),
5948 ),
5949 None => None,
5950 };
5951 if let Some(r) = r {
5952 drafted += r.drafted;
5953 accepted += r.accepted.len();
5954 let mut stopped = false;
5955 for &id in &r.accepted {
5956 if self.confidence_on {
5957 confidence.push(0.0);
5958 }
5959 if !commit!(id) {
5960 stopped = true;
5961 break;
5962 }
5963 }
5964 if stopped {
5965 break 'decode;
5966 }
5967 next_pos += r.accepted.len() + 1;
5968 hidden.fill(0.0);
5969 self.graph_logits = Some(r.logits);
5970 continue 'decode;
5971 }
5972 }
5973 match &mut mtp {
5974 #[cfg(feature = "gpu")]
5976 Some(m)
5977 if graph_spec
5978 && !spec_watchdog_off
5979 && generated + 1 < max_tokens
5980 && next_pos > 0 =>
5981 {
5982 let t_round = std::time::Instant::now();
5983 if spec_time_level() >= 2 {
5984 if let Some(t) = spec_round_end.take() {
5985 eprintln!(
5986 "spec-gap {:.2} ms (host between rounds)",
5987 t.elapsed().as_secs_f64() * 1e3
5988 );
5989 }
5990 }
5991 spec_stamps_begin();
5992 #[cfg(target_os = "macos")]
5997 let allocs0 =
5998 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
5999 #[cfg(not(target_os = "macos"))]
6000 let allocs0 = 0u64;
6001 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6002 m,
6003 &hidden,
6004 t_next,
6005 next_pos,
6006 &mut drafted,
6007 &mut accepted,
6008 &mut all_ids,
6009 max_tokens - generated,
6010 ) {
6011 next_pos = n_pos;
6012 hidden = new_h;
6013 let level = spec_time_level();
6014 if level > 0 {
6015 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6016 let stamps = spec_stamps_take();
6017 let median = if spec_walls.len() >= 3 {
6020 let mut s = spec_walls.clone();
6021 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6022 Some(s[s.len() / 2])
6023 } else {
6024 None
6025 };
6026 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6027 #[cfg(target_os = "macos")]
6028 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6029 .load(std::sync::atomic::Ordering::Relaxed)
6030 - allocs0;
6031 #[cfg(not(target_os = "macos"))]
6032 let allocs = allocs0;
6033 eprintln!(
6034 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6035 extra.len() + 1,
6036 if allocs > 0 {
6037 format!(" [{allocs} new device buffers]")
6038 } else {
6039 String::new()
6040 },
6041 match (outlier, median) {
6042 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6043 _ => String::new(),
6044 }
6045 );
6046 if level >= 2 || outlier {
6047 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6048 eprintln!(
6049 "spec-stamps: {}| untracked {:.1}",
6050 spec_stamps_format(&stamps),
6051 wall - sum
6052 );
6053 }
6054 if spec_mon.n >= 1 {
6055 spec_walls.push(wall);
6056 }
6057 }
6058 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6062 spec_trial = Self::spec_trial_round(
6065 spec_trial,
6066 &mut spec_mon,
6067 generated + extra.len() + 1,
6068 );
6069 let mut stopped = false;
6070 for &id in &extra {
6071 if self.confidence_on {
6072 confidence.push(0.0);
6073 }
6074 if !commit!(id) {
6075 stopped = true;
6076 break;
6077 }
6078 }
6079 if stopped {
6080 break 'decode;
6081 }
6082 if spec_time_level() >= 2 {
6083 spec_round_end = Some(std::time::Instant::now());
6084 }
6085 continue 'decode;
6086 }
6087 if self
6088 .graph_failed
6089 .swap(false, std::sync::atomic::Ordering::Relaxed)
6090 {
6091 self.finish_generation(&mut mtp, &mut router, true);
6097 return Err("GPU MTP graph failed during speculative decode".to_string());
6098 }
6099 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6110 spec_mon.tokens = 0.0;
6111 spec_mon.fails = 3;
6112 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6113 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6114 next_pos += 1;
6115 continue 'decode;
6116 }
6117 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6119 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6120 drafted += 1;
6121 let emb1 = self.embed_single(t_next);
6122 let emb2 = self.embed_single(draft);
6123 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6124
6125 inference::rms_norm_into(
6126 &h1,
6127 &self.weights.final_norm,
6128 self.rms_eps,
6129 self.norm_style,
6130 &mut self.ws.n1,
6131 );
6132 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6133 let t_after = sampler::sample_with_scratch_pool(
6134 &logits1,
6135 &self.sampler_config,
6136 self.sampler_config.penalty_past(&all_ids, bounded_native),
6137 &mut self.rng,
6138 &mut self.sampler_scratch,
6139 self.pool.as_deref(),
6140 );
6141 if self.confidence_on {
6142 confidence.push(sampler::top1_prob_pool(
6143 self.pool.as_deref(),
6144 &mut self.sampler_scratch,
6145 &logits1,
6146 t_after,
6147 calib_temp,
6148 ));
6149 }
6150 attention::recycle_buf(&mut logits1);
6151 if trace_on {
6152 traces.push(TokenTrace {
6155 t: generated,
6156 token_id: t_after,
6157 confidence: confidence.last().copied().unwrap_or(0.0),
6158 active_skill: None,
6159 recon: None,
6160 switched: false,
6161 });
6162 }
6163 let stop = !commit!(t_after);
6164
6165 if t_after == draft {
6166 accepted += 1;
6167 self.commit_linear_scratch();
6168 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6169 hidden = h2;
6170 next_pos += 2;
6171 } else {
6172 for layer in &mut self.kv_cache.layers {
6174 layer.truncate_last(1);
6175 }
6176 if !stop {
6177 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6178 hidden = self.forward_layers(
6179 &self.embed_single(t_after),
6180 next_pos + 1,
6181 None,
6182 );
6183 }
6184 next_pos += 2;
6185 }
6186 if stop {
6187 break 'decode;
6188 }
6189 }
6190 _ => {
6192 #[cfg(feature = "gpu")]
6197 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6198 static SAID: std::sync::Once = std::sync::Once::new();
6199 SAID.call_once(|| {
6200 eprintln!(
6201 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6202 !self.dsv4_mtp.is_empty(),
6203 task_mask.is_none(),
6204 router.is_none(),
6205 !trace_on,
6206 self.sampler_config.temperature < 1e-6,
6207 self.sampler_config.repetition_penalty == 1.0,
6208 );
6209 });
6210 }
6211 #[cfg(feature = "gpu")]
6212 if Self::dsv4_spec_on()
6213 && self.dsv4.is_some()
6214 && !self.dsv4_mtp.is_empty()
6215 && task_mask.is_none()
6216 && router.is_none()
6217 && !trace_on
6218 && self.sampler_config.temperature < 1e-6
6219 && self.sampler_config.repetition_penalty == 1.0
6220 && generated + 1 < max_tokens
6221 && all_ids.len() >= 2
6222 && generated >= dsv4_spec_retry_at
6223 {
6224 let tip_token = all_ids[all_ids.len() - 2];
6225 let drafted0 = drafted;
6226 let round = self.dsv4_spec_step(
6227 tip_token,
6228 t_next,
6229 next_pos,
6230 max_tokens.saturating_sub(generated),
6231 &mut drafted,
6232 &mut accepted,
6233 );
6234 if drafted > drafted0 {
6235 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6236 if useful {
6237 dsv4_spec_bad = 0;
6238 } else {
6239 dsv4_spec_bad += 1;
6240 if dsv4_spec_bad >= 2 {
6241 dsv4_spec_bad = 0;
6242 dsv4_spec_retry_at = generated.saturating_add(32);
6243 tracing::info!(
6244 "dsv4: draft не окупился дважды — точный walk на 32 токена"
6245 );
6246 }
6247 }
6248 }
6249 if let Some((extra, n_pos)) = round {
6250 next_pos = n_pos;
6251 let mut stopped = false;
6252 for &id in &extra {
6253 if self.confidence_on {
6254 confidence.push(0.0);
6255 }
6256 if !commit!(id) {
6257 stopped = true;
6258 break;
6259 }
6260 }
6261 if stopped {
6262 break 'decode;
6263 }
6264 continue 'decode;
6265 }
6266 }
6267 self.graph_want_logits = fuse_lm;
6268 let mut t_fwd = t_next;
6274 let pure_greedy = self.sampler_config.temperature < 1e-6
6275 && self.sampler_config.repetition_penalty == 1.0
6276 && self.sampler_config.presence_penalty == 0.0
6277 && self.sampler_config.suppress_tokens.is_empty();
6278 let burst_k = std::env::var("CMF_MULTISTEP")
6283 .ok()
6284 .and_then(|v| v.parse::<usize>().ok())
6285 .unwrap_or(0);
6286 if pure_greedy
6287 && burst_k >= 1
6288 && fuse_lm
6289 && task_mask.is_none()
6290 && router.is_none()
6291 && !trace_on
6292 && !self.confidence_on
6293 && std::env::var_os("CMF_FORCE_IDS").is_none()
6298 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6299 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6300 {
6301 let mut stopped = false;
6302 loop {
6303 if self
6308 .cancel
6309 .swap(false, std::sync::atomic::Ordering::Relaxed)
6310 {
6311 finish_reason = "cancelled".to_string();
6312 stopped = true;
6313 break;
6314 }
6315 let room = max_tokens.saturating_sub(generated);
6316 if room <= 2 {
6317 break;
6318 }
6319 let k = burst_k.min(room - 1);
6320 if k < 1 {
6321 break;
6322 }
6323 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6324 if self
6325 .graph_failed
6326 .swap(false, std::sync::atomic::Ordering::Relaxed)
6327 {
6328 self.finish_generation(&mut mtp, &mut router, true);
6329 return Err(
6330 "GPU token graph failed during greedy burst".to_string()
6331 );
6332 }
6333 break;
6334 };
6335 next_pos += k;
6336 for &id in &ids {
6337 if !commit!(id) {
6338 stopped = true;
6339 break;
6340 }
6341 }
6342 if stopped {
6343 break;
6344 }
6345 t_fwd = *ids.last().unwrap();
6346 }
6347 if stopped {
6348 break 'decode;
6349 }
6350 }
6351 #[cfg(target_os = "macos")]
6361 if graph_spec
6362 && spec_watchdog_off
6363 && next_pos > 0
6364 && self.mtp_graph_mode == Some(true)
6365 && crate::gpu::q1_force()
6366 {
6367 if let Some(m) = mtp.as_mut() {
6368 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6369 }
6370 }
6371 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6372 next_pos += 1;
6373 if let Some(r) = &mut router {
6376 let phi = self.dyn_phi_ema.clone();
6377 let decision = r.step(&phi, generated);
6378 if let Some(new_active) = decision {
6379 let _ = self.set_active_skill(new_active);
6380 }
6381 if trace_on {
6384 if let Some(last) = traces.last_mut() {
6385 let e = r.last_best_e();
6386 last.recon = e.is_finite().then_some(e);
6387 last.switched = decision.is_some();
6388 }
6389 }
6390 }
6391 }
6392 }
6393 }
6394
6395 let cancelled = finish_reason == "cancelled";
6396 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6400 if mimo_spec {
6401 if let Some(st) = self.mimo_mtp.as_ref() {
6402 let line = st.stats.line();
6403 tracing::info!("{line}");
6404 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6405 eprintln!("{line}");
6406 }
6407 }
6408 }
6409 self.finish_generation(&mut mtp, &mut router, cancelled);
6410
6411 let output_ids = &all_ids[input_ids.len()..];
6412 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6416 let consumed = std::mem::take(&mut all_ids);
6421 if dyn_switched {
6422 self.clear_sequence_state();
6423 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6424 self.clear_history();
6425 } else {
6426 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6427 }
6428 all_ids = consumed;
6429 let output_ids = &all_ids[input_ids.len()..];
6430 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6432 Ok(GenerateResult {
6433 text: self.tokenizer.decode(output_ids),
6434 token_ids: output_ids.to_vec(),
6435 prompt_tokens: input_ids.len(),
6436 tokens_generated: generated,
6437 finish_reason,
6438 mtp_drafted: drafted,
6439 mtp_accepted: accepted,
6440 token_confidence: confidence,
6441 traces,
6442 })
6443 }
6444
6445 fn mtp_step(
6449 &mut self,
6450 m: &mut MtpModule,
6451 hidden: &[f32],
6452 next_token: u32,
6453 position: usize,
6454 ) -> u32 {
6455 self.mtp_step_h(m, hidden, next_token, position).0
6456 }
6457
6458 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6462 use std::sync::Mutex;
6463 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6464 let mut t = T.lock().unwrap();
6465 if t.len() <= depth {
6466 t.resize(depth + 1, (0, 0));
6467 }
6468 t[depth].0 += 1;
6469 t[depth].1 += prefix_ok as u64;
6470 if depth == 0 && t[0].0 % 128 == 0 {
6471 let line: Vec<String> = t
6472 .iter()
6473 .enumerate()
6474 .map(|(d, (n, k))| {
6475 format!(
6476 "d{}={:.0}%({n})",
6477 d + 1,
6478 100.0 * *k as f64 / (*n).max(1) as f64
6479 )
6480 })
6481 .collect();
6482 eprintln!("mtp-chain: {}", line.join(" "));
6483 }
6484 }
6485
6486 fn mtp_step_hl(
6494 &mut self,
6495 m: &mut MtpModule,
6496 hidden: &[f32],
6497 next_token: u32,
6498 position: usize,
6499 ) -> (Vec<f32>, Vec<f32>) {
6500 #[cfg(target_os = "macos")]
6505 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6506 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6507 self.mtp_graph_mode = Some(true);
6508 return r;
6509 }
6510 if self.mtp_graph_mode == Some(true) {
6511 tracing::error!("mtp Metal graph failed after admission");
6512 self.clear_sequence_state();
6513 self.graph_failed
6514 .store(true, std::sync::atomic::Ordering::Relaxed);
6515 self.cancel
6516 .store(true, std::sync::atomic::Ordering::Relaxed);
6517 return (Vec::new(), Vec::new());
6518 }
6519 self.mtp_graph_mode = Some(false);
6520 }
6521 #[cfg(feature = "gpu")]
6522 if self.mtp_graph_mode != Some(false) {
6523 if !self.mtp_graph_ok(m) {
6524 if self.mtp_graph_mode == Some(true) {
6525 tracing::error!("mtp graph became unavailable after admission");
6530 self.clear_sequence_state();
6531 self.graph_failed
6532 .store(true, std::sync::atomic::Ordering::Relaxed);
6533 self.cancel
6534 .store(true, std::sync::atomic::Ordering::Relaxed);
6535 return (Vec::new(), Vec::new());
6536 }
6537 self.mtp_graph_mode = Some(false);
6538 } else {
6539 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6540 self.mtp_graph_mode = Some(true);
6541 return r;
6542 }
6543 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6544 return (Vec::new(), Vec::new());
6551 }
6552 tracing::error!("mtp graph failed or declined after admission");
6556 self.clear_sequence_state();
6557 self.graph_failed
6558 .store(true, std::sync::atomic::Ordering::Relaxed);
6559 self.cancel
6560 .store(true, std::sync::atomic::Ordering::Relaxed);
6561 return (Vec::new(), Vec::new());
6562 }
6563 }
6564 let e = self.embed_single(next_token);
6568 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6569 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6570 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6571 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6572 let mut x = vec![0.0f32; self.hidden_size];
6573 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6574
6575 let lw = &m.layer;
6577 inference::rms_norm_into(
6578 &x,
6579 &lw.input_norm,
6580 self.rms_eps,
6581 self.norm_style,
6582 &mut self.ws.n1,
6583 );
6584 let attn = match &lw.attn {
6585 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6587 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6588 AttnKind::Full {
6589 wq,
6590 wk,
6591 wv,
6592 wo,
6593 q_norm,
6594 k_norm,
6595 output_gate,
6596 softplus_gate,
6597 bias,
6598 } => {
6599 let mut cfg = self.attn_cfg(position);
6600 cfg.q_norm = q_norm.as_deref();
6601 cfg.k_norm = k_norm.as_deref();
6602 cfg.output_gate = *output_gate;
6603 cfg.softplus_gate = softplus_gate
6604 .as_ref()
6605 .map(|(gate, per_head)| (gate, *per_head));
6606 cfg.bias = bias
6607 .as_ref()
6608 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6609 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6610 }
6611 AttnKind::Linear(_)
6612 | AttnKind::LinearGdn(_)
6613 | AttnKind::ShortConv(_)
6614 | AttnKind::Bounded(_) => {
6615 unreachable!("MTP block is full attention")
6616 }
6617 };
6618 for (i, &a) in attn.iter().enumerate() {
6619 x[i] += a;
6620 }
6621 inference::rms_norm_into(
6622 &x,
6623 &lw.post_norm,
6624 self.rms_eps,
6625 self.norm_style,
6626 &mut self.ws.p1,
6627 );
6628 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6629 for (i, &f) in ffn.iter().enumerate() {
6630 x[i] += f;
6631 }
6632
6633 inference::rms_norm_into(
6634 &x,
6635 &m.final_norm,
6636 self.rms_eps,
6637 self.norm_style,
6638 &mut self.ws.n1,
6639 );
6640 let lg = self.lm_head_forward(&self.ws.n1);
6641 (lg, x)
6642 }
6643
6644 fn mtp_step_h(
6646 &mut self,
6647 m: &mut MtpModule,
6648 hidden: &[f32],
6649 next_token: u32,
6650 position: usize,
6651 ) -> (u32, Vec<f32>) {
6652 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6653 let draft = sampler::argmax(&lg);
6654 attention::recycle_buf(&mut lg);
6655 (draft, x)
6656 }
6657
6658 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6664 match trial {
6665 SpecTrial::Spec { t0, gen0, rounds } => {
6666 let rounds = rounds + 1;
6667 if rounds >= 5 {
6668 if mon.plain_ms > 0.0 {
6669 let keep = mon.pays();
6670 mon.fails = 0;
6671 tracing::info!(
6672 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6673 mon.tokens,
6674 mon.round_ms,
6675 mon.plain_ms,
6676 if keep { "speculating" } else { "plain" }
6677 );
6678 SpecTrial::Decided {
6679 spec: keep,
6680 recheck_at: if keep { usize::MAX } else { generated + 128 },
6681 }
6682 } else if mon.pays() {
6683 mon.fails = 0;
6688 tracing::info!(
6689 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6690 mon.tokens,
6691 mon.round_ms,
6692 );
6693 SpecTrial::Decided {
6694 spec: true,
6695 recheck_at: usize::MAX,
6696 }
6697 } else {
6698 SpecTrial::Plain {
6699 t0: std::time::Instant::now(),
6700 gen0: generated,
6701 }
6702 }
6703 } else {
6704 SpecTrial::Spec { t0, gen0, rounds }
6705 }
6706 }
6707 SpecTrial::Decided { spec: true, .. } => {
6708 if mon.pays() {
6709 mon.fails = 0;
6710 trial
6711 } else {
6712 mon.fails += 1;
6713 if mon.fails >= 4 {
6714 if mon.plain_ms <= 0.0 {
6715 tracing::info!(
6719 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6720 mon.tokens,
6721 mon.round_ms,
6722 );
6723 return SpecTrial::Plain {
6724 t0: std::time::Instant::now(),
6725 gen0: generated,
6726 };
6727 }
6728 tracing::info!(
6729 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6730 mon.tokens,
6731 mon.round_ms,
6732 mon.plain_ms
6733 );
6734 SpecTrial::Decided {
6735 spec: false,
6736 recheck_at: generated + 128,
6737 }
6738 } else {
6739 trial
6740 }
6741 }
6742 }
6743 other => other,
6744 }
6745 }
6746
6747 fn mtp_kv_id(&self) -> u64 {
6750 self.graph_kv_id | (1u64 << 40)
6751 }
6752
6753 const MTP_LAYER_BASE: usize = 0;
6758
6759 #[cfg(feature = "gpu")]
6766 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6767 self.mtp_graph_mode != Some(true)
6768 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6769 }
6770
6771 #[cfg(feature = "gpu")]
6777 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6778 let mut ok = true;
6779 let mut expected = false;
6780 for li in 0..self.num_layers {
6781 if matches!(
6782 self.weights.layers[self.phys_layer(li)].attn,
6783 AttnKind::Full { .. }
6784 ) {
6785 expected = true;
6786 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6787 }
6788 }
6789 !expected || ok
6790 }
6791
6792 fn graph_gdn_layer_count(&self) -> usize {
6796 (0..self.num_layers)
6797 .filter(|&li| {
6798 matches!(
6799 &self.weights.layers[self.phys_layer(li)].attn,
6800 AttnKind::LinearGdn(_)
6801 )
6802 })
6803 .count()
6804 }
6805
6806 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6809 let e = self.embed_single(next_token);
6810 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6811 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6812 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6813 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6814 let mut x = vec![0.0f32; self.hidden_size];
6815 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6816 x
6817 }
6818
6819 #[cfg(feature = "gpu")]
6822 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6823 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6824 return false;
6825 }
6826 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6827 || !crate::gpu::enabled_here()
6828 || self.attn_softcap > 0.0
6829 || self.attention_heads_per_layer.is_some()
6830 || self.v_head_dim.is_some()
6833 {
6834 return false;
6835 }
6836 matches!(
6837 &m.layer.attn,
6838 AttnKind::Full {
6839 softplus_gate: None,
6840 ..
6841 }
6842 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6843 }
6844
6845 #[cfg(feature = "gpu")]
6849 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6850 if !self.mtp_block_graph_ok(m) {
6851 return false;
6852 }
6853 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6854 return false;
6855 };
6856 let FfnKind::Dense(d) = &m.layer.ffn else {
6857 return false;
6858 };
6859 d.segs.is_empty()
6860 && wq.graph_weight().is_some()
6861 && wk.graph_weight().is_some()
6862 && wv.graph_weight().is_some()
6863 && wo.graph_weight().is_some()
6864 && d.gate_proj.graph_weight().is_some()
6865 && d.up_proj.graph_weight().is_some()
6866 && d.down_proj.graph_weight().is_some()
6867 && self.weights.lm_head.graph_weight().is_some()
6868 }
6869
6870 #[cfg(feature = "gpu")]
6876 fn mtp_step_graph(
6877 &mut self,
6878 m: &mut MtpModule,
6879 hidden: &[f32],
6880 next_token: u32,
6881 position: usize,
6882 ) -> Option<(Vec<f32>, Vec<f32>)> {
6883 if !self.mtp_graph_ok(m) {
6884 return None;
6885 }
6886 let lw = &m.layer;
6887 let AttnKind::Full {
6888 wq,
6889 wk,
6890 wv,
6891 wo,
6892 q_norm,
6893 k_norm,
6894 output_gate,
6895 softplus_gate,
6896 bias,
6897 } = &lw.attn
6898 else {
6899 return None;
6900 };
6901 if softplus_gate.is_some() {
6902 return None;
6903 }
6904 let FfnKind::Dense(d) = &lw.ffn else {
6905 return None;
6906 };
6907 if !d.segs.is_empty() {
6908 return None; }
6910 let mut x = self.mtp_block_input(m, hidden, next_token);
6913 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6914 let (_, i, kind, rs) = t.graph_weight()?;
6915 Some(crate::gpu::GraphW {
6916 idx: i,
6917 kind,
6918 row_scale: rs,
6919 data: &[],
6920 prism: crate::gpu::GraphPrismOp::None,
6921 affine: false,
6922 })
6923 }
6924 let (model, _, _, _) = wq.graph_weight()?;
6925 let model = model.clone();
6926 let (lm_gw, lm_rows) = {
6927 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6928 let rows = if kind == 6 {
6932 self.draft_head_rows(self.weights.lm_head.rows())
6933 } else {
6934 self.weights.lm_head.rows()
6935 };
6936 (
6937 crate::gpu::GraphW {
6938 idx: i,
6939 kind,
6940 row_scale: rs,
6941 data: &[],
6942 prism: crate::gpu::GraphPrismOp::None,
6943 affine: false,
6944 },
6945 rows,
6946 )
6947 };
6948 let layer = crate::gpu::GraphLayer {
6949 input_norm: &lw.input_norm,
6950 attn: crate::gpu::GraphAttn::Full {
6951 wq: gw(wq)?,
6952 wk: gw(wk)?,
6953 wv: gw(wv)?,
6954 wo: gw(wo)?,
6955 q_norm: q_norm.as_deref(),
6956 k_norm: k_norm.as_deref(),
6957 late_qk_norm: self.qk_norm_after_rope,
6958 bias: bias
6959 .as_ref()
6960 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6961 output_gate: *output_gate,
6962 cpu_k: m.kv.k_heads(),
6963 cpu_v: m.kv.v_heads(),
6964 cpu_base: m.kv.base(),
6965 geom: None,
6966 head_gate: None,
6967 },
6968 post_norm: &lw.post_norm,
6969 attn_out_norm: None,
6970 ffn_out_norm: None,
6971 layer_scale: None,
6972 ffn: crate::gpu::GraphFfn::Dense {
6973 gate: gw(&d.gate_proj)?,
6974 up: gw(&d.up_proj)?,
6975 down: gw(&d.down_proj)?,
6976 act: d.act.graph_act()?,
6977 },
6978 };
6979 let nh = self.num_heads;
6980 let (nkv, hd, rd) = self.layer_geom(0);
6981 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6982 let mut logits = Vec::new();
6983 let ok = crate::gpu::forward_token_graph(
6984 &model,
6985 self.mtp_kv_id(),
6986 std::slice::from_ref(&layer),
6987 &[None],
6988 self.o1_epoch,
6989 &self.inv_freq,
6990 &mut x,
6991 nh,
6992 nkv,
6993 hd,
6994 self.attn_scale,
6995 rd,
6996 self.hidden_size,
6997 self.intermediate_size,
6998 position,
6999 self.kv_cache.max_seq_len,
7000 gemma,
7001 self.rms_eps as f32,
7002 Some((&lm_gw, lm_rows)),
7003 &m.final_norm,
7004 &mut logits,
7005 &[],
7006 1,
7007 None,
7008 None,
7009 None,
7010 Self::MTP_LAYER_BASE,
7011 true,
7012 );
7013 match ok {
7014 crate::gpu::TokenGraphOutcome::Completed => {}
7015 crate::gpu::TokenGraphOutcome::Declined => return None,
7016 crate::gpu::TokenGraphOutcome::Failed => {
7017 self.clear_sequence_state();
7021 self.graph_failed
7022 .store(true, std::sync::atomic::Ordering::Relaxed);
7023 self.cancel
7024 .store(true, std::sync::atomic::Ordering::Relaxed);
7025 return None;
7026 }
7027 }
7028 logits.resize(self.vocab_size, 0.0);
7029 Some((logits, x))
7030 }
7031
7032 #[cfg(feature = "gpu")]
7040 fn mtp_warm_graph(
7041 &mut self,
7042 m: &mut MtpModule,
7043 pairs: &[(&[f32], u32)],
7044 first_pos: usize,
7045 ) -> crate::gpu::BatchGraphOutcome {
7046 if pairs.is_empty() {
7047 return crate::gpu::BatchGraphOutcome::Completed;
7048 }
7049 if !self.mtp_block_graph_ok(m) {
7050 return crate::gpu::BatchGraphOutcome::Declined;
7051 }
7052 let hs = self.hidden_size;
7053 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7056 for (h, t) in pairs {
7057 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7058 }
7059 let lw = &m.layer;
7060 let AttnKind::Full {
7061 wq,
7062 wk,
7063 wv,
7064 wo,
7065 q_norm,
7066 k_norm,
7067 output_gate,
7068 bias,
7069 ..
7070 } = &lw.attn
7071 else {
7072 return crate::gpu::BatchGraphOutcome::Declined;
7073 };
7074 let FfnKind::Dense(d) = &lw.ffn else {
7075 return crate::gpu::BatchGraphOutcome::Declined;
7076 };
7077 if !d.segs.is_empty() {
7078 return crate::gpu::BatchGraphOutcome::Declined; }
7080 let (
7083 AttnKind::Full {
7084 softplus_gate: None,
7085 ..
7086 },
7087 Some(gact),
7088 ) = (&lw.attn, d.act.graph_act())
7089 else {
7090 return crate::gpu::BatchGraphOutcome::Declined;
7091 };
7092 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7093 let (_, i, kind, rs) = t.graph_weight()?;
7094 Some(crate::gpu::GraphW {
7095 idx: i,
7096 kind,
7097 row_scale: rs,
7098 data: &[],
7099 prism: crate::gpu::GraphPrismOp::None,
7100 affine: false,
7101 })
7102 }
7103 let Some((model, _, _, _)) = wq.graph_weight() else {
7104 return crate::gpu::BatchGraphOutcome::Declined;
7105 };
7106 let model = model.clone();
7107 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7108 gw(wq),
7109 gw(wk),
7110 gw(wv),
7111 gw(wo),
7112 gw(&d.gate_proj),
7113 gw(&d.up_proj),
7114 gw(&d.down_proj),
7115 ) else {
7116 return crate::gpu::BatchGraphOutcome::Declined;
7117 };
7118 let layer = crate::gpu::GraphLayer {
7119 input_norm: &lw.input_norm,
7120 attn: crate::gpu::GraphAttn::Full {
7121 wq: gwq,
7122 wk: gwk,
7123 wv: gwv,
7124 wo: gwo,
7125 q_norm: q_norm.as_deref(),
7126 k_norm: k_norm.as_deref(),
7127 late_qk_norm: self.qk_norm_after_rope,
7128 bias: bias
7129 .as_ref()
7130 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7131 output_gate: *output_gate,
7132 cpu_k: m.kv.k_heads(),
7133 cpu_v: m.kv.v_heads(),
7134 cpu_base: m.kv.base(),
7135 geom: None,
7136 head_gate: None,
7137 },
7138 post_norm: &lw.post_norm,
7139 attn_out_norm: None,
7140 ffn_out_norm: None,
7141 layer_scale: None,
7142 ffn: crate::gpu::GraphFfn::Dense {
7143 gate: gg,
7144 up: gu,
7145 down: gd,
7146 act: gact,
7147 },
7148 };
7149 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7150 let nh = self.num_heads;
7151 let (nkv, hd, rd) = self.layer_geom(0);
7152 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7153 crate::gpu::forward_batch_graph(
7154 &model,
7155 self.mtp_kv_id(),
7156 std::slice::from_ref(&layer),
7157 &self.inv_freq,
7158 &mut hiddens,
7159 nh,
7160 nkv,
7161 hd,
7162 rd,
7163 hs,
7164 self.intermediate_size,
7165 &positions,
7166 self.kv_cache.max_seq_len,
7167 gemma,
7168 self.rms_eps as f32,
7169 self.attn_scale,
7170 pairs.len(),
7171 &[],
7172 0,
7173 None,
7174 None,
7175 )
7176 }
7177
7178 #[cfg(feature = "gpu")]
7185 fn mtp_warm_graph_fallback(
7186 &mut self,
7187 m: &mut MtpModule,
7188 pairs: &[(&[f32], u32)],
7189 first_pos: usize,
7190 ) -> bool {
7191 if pairs.is_empty() {
7192 return true;
7193 }
7194 let graphable = self.mtp_block_graph_ok(m);
7195 if !graphable {
7196 if self.mtp_graph_mode == Some(true) {
7200 return false;
7201 }
7202 self.mtp_graph_mode = Some(false);
7203 for (j, (h, t)) in pairs.iter().enumerate() {
7204 self.mtp_warm(m, h, *t, first_pos + j);
7205 }
7206 return true;
7207 }
7208
7209 for (j, (h, t)) in pairs.iter().enumerate() {
7214 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7215 return false;
7216 }
7217 }
7218 self.mtp_graph_mode = Some(true);
7219 true
7220 }
7221
7222 #[cfg(feature = "gpu")]
7227 fn mtp_warm_prefill_pairs(
7228 &mut self,
7229 m: &mut MtpModule,
7230 pairs: &[(&[f32], u32)],
7231 first_pos: usize,
7232 ) -> Result<(), &'static str> {
7233 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7238 if self.mtp_graph_mode == Some(true) {
7239 return Err("MTP token graph became unavailable after admission");
7240 }
7241 self.mtp_graph_mode = Some(false);
7242 for (j, (h, t)) in pairs.iter().enumerate() {
7243 self.mtp_warm(m, h, *t, first_pos + j);
7244 }
7245 return Ok(());
7246 }
7247 match self.mtp_warm_graph(m, pairs, first_pos) {
7248 crate::gpu::BatchGraphOutcome::Completed => {
7249 if !pairs.is_empty() {
7250 self.mtp_graph_mode = Some(true);
7251 }
7252 Ok(())
7253 }
7254 crate::gpu::BatchGraphOutcome::Declined => {
7255 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7256 Ok(())
7257 } else {
7258 Err("MTP warm-up fallback failed after device admission")
7259 }
7260 }
7261 crate::gpu::BatchGraphOutcome::Failed => {
7262 Err("MTP warm batch graph failed after admission")
7263 }
7264 }
7265 }
7266
7267 #[cfg(not(feature = "gpu"))]
7268 fn mtp_warm_prefill_pairs(
7269 &mut self,
7270 m: &mut MtpModule,
7271 pairs: &[(&[f32], u32)],
7272 first_pos: usize,
7273 ) -> Result<(), &'static str> {
7274 for (j, (h, t)) in pairs.iter().enumerate() {
7275 self.mtp_warm(m, h, *t, first_pos + j);
7276 }
7277 Ok(())
7278 }
7279
7280 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7284 let e = self.embed_single(next_token);
7285 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7286 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7287 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7288 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7289 let mut x = vec![0.0f32; self.hidden_size];
7290 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7291 inference::rms_norm_into(
7292 &x,
7293 &m.layer.input_norm,
7294 self.rms_eps,
7295 self.norm_style,
7296 &mut self.ws.n1,
7297 );
7298 let attn = match &m.layer.attn {
7299 AttnKind::Full {
7300 wq,
7301 wk,
7302 wv,
7303 wo,
7304 q_norm,
7305 k_norm,
7306 output_gate,
7307 softplus_gate,
7308 bias,
7309 } => {
7310 let mut cfg = self.attn_cfg(position);
7311 cfg.q_norm = q_norm.as_deref();
7312 cfg.k_norm = k_norm.as_deref();
7313 cfg.output_gate = *output_gate;
7314 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7315 cfg.bias = bias
7316 .as_ref()
7317 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7318 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7319 }
7320 _ => return,
7321 };
7322 let _ = attn;
7323 }
7324
7325 #[cfg(feature = "gpu")]
7332 #[allow(clippy::too_many_arguments)]
7333 fn graph_spec_step(
7334 &mut self,
7335 m: &mut MtpModule,
7336 hidden: &[f32],
7337 t_next: u32,
7338 next_pos: usize,
7339 drafted: &mut usize,
7340 accepted: &mut usize,
7341 all_ids: &mut Vec<u32>,
7345 room: usize,
7350 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7351 #[cfg(target_os = "macos")]
7362 let metal_native = crate::gpu::q1_force();
7363 #[cfg(not(target_os = "macos"))]
7364 let metal_native = false;
7365 #[cfg(feature = "gpu")]
7366 let k_default = if metal_native {
7367 7
7370 } else if crate::gpu_wgpu::verify_i8_on() {
7371 5
7372 } else {
7373 4
7374 };
7375 #[cfg(not(feature = "gpu"))]
7376 let k_default = 4;
7377 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7378 .ok()
7379 .and_then(|v| v.parse().ok())
7380 .filter(|&v| (1..=8).contains(&v));
7381 let (k_start, k_max) = if metal_native {
7386 (7, 7)
7387 } else {
7388 (3, k_default.max(5))
7389 };
7390 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7391 let k_spec = k_full.min(room).max(1);
7392 let k_capped = k_spec < k_full;
7395 if next_pos == 0 {
7396 return None;
7397 }
7398 let t_round = std::time::Instant::now();
7399 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7415 let sub0 = subs();
7416 let cfg = self.sampler_config.clone();
7421 let penalized = !(cfg.repetition_penalty == 1.0
7422 && cfg.presence_penalty == 0.0
7423 && cfg.suppress_tokens.is_empty());
7424 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7429 let sampling = cfg.temperature >= 1e-6;
7430 let sparse = sampling && sampler::sparse_ok(&cfg);
7436 let base_len = all_ids.len();
7437 if sampling && !sparse && self.spec_q.len() < k_spec {
7438 self.spec_q.resize_with(k_spec, Vec::new);
7439 }
7440 if sparse && self.spec_qs.len() < k_spec {
7441 self.spec_qs.resize_with(k_spec, Vec::new);
7442 }
7443 let mut drafts = Vec::with_capacity(k_spec);
7448 let mut hx = hidden.to_vec();
7449 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7452 spec_stamp("pro");
7453 #[cfg(target_os = "macos")]
7459 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7460 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7461 Ok(ids) => {
7462 self.mtp_graph_mode = Some(true);
7463 drafts = ids;
7464 }
7465 Err(true) => {
7466 tracing::error!("mtp Metal draft chain failed after commit");
7467 self.clear_sequence_state();
7468 self.graph_failed
7469 .store(true, std::sync::atomic::Ordering::Relaxed);
7470 self.cancel
7471 .store(true, std::sync::atomic::Ordering::Relaxed);
7472 return None;
7473 }
7474 Err(false) => {}
7475 }
7476 }
7477 for j in drafts.len()..k_spec {
7478 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7479 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7480 if spec_dbg {
7481 let saved = self.mtp_graph_mode;
7482 self.mtp_graph_mode = Some(false);
7483 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7484 self.mtp_graph_mode = saved;
7485 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7486 return None;
7487 }
7488 m.kv.truncate_last(1);
7489 dbg_ref = Some(r);
7490 }
7491 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7492 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7493 return None;
7494 }
7495 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7496 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7497 let dl = lg
7498 .iter()
7499 .zip(&lg_cpu)
7500 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7501 let dh = hj
7502 .iter()
7503 .zip(&h_cpu)
7504 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7505 eprintln!(
7506 "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 {}",
7507 next_pos - 1 + j,
7508 sampler::argmax(&lg_cpu),
7509 sampler::argmax(&lg),
7510 n(&h_cpu),
7511 n(&hj),
7512 m.kv.seq_len
7513 );
7514 }
7515 let dj = if sparse {
7516 let mut q = std::mem::take(&mut self.spec_qs[j]);
7517 let ok = sampler::sparse_distribution_into(
7518 &lg,
7519 &cfg,
7520 all_ids,
7521 &mut self.sampler_scratch,
7522 self.pool.as_deref(),
7523 &mut q,
7524 );
7525 let d = if ok {
7526 sampler::draw_sparse(&q, &mut self.rng)
7527 } else {
7528 let t = sampler::argmax(&lg);
7530 q.clear();
7531 q.push((t, 1.0));
7532 t
7533 };
7534 self.spec_qs[j] = q;
7535 all_ids.push(d);
7536 d
7537 } else if sampling {
7538 let mut q = std::mem::take(&mut self.spec_q[j]);
7539 sampler::distribution_into(
7540 &lg,
7541 &cfg,
7542 all_ids,
7543 &mut self.sampler_scratch,
7544 self.pool.as_deref(),
7545 &mut q,
7546 );
7547 let d = sampler::draw(&q, &mut self.rng);
7548 self.spec_q[j] = q;
7549 all_ids.push(d); d
7551 } else if greedy_pen {
7552 let d = sampler::argmax_penalized(
7553 &lg,
7554 &cfg,
7555 all_ids,
7556 &mut self.sampler_scratch,
7557 self.pool.as_deref(),
7558 );
7559 all_ids.push(d);
7560 d
7561 } else {
7562 sampler::argmax(&lg)
7563 };
7564 attention::recycle_buf(&mut lg);
7565 drafts.push(dj);
7566 hx = hj;
7567 spec_stamp("d.pick");
7568 }
7569 all_ids.truncate(base_len);
7570 *drafted += k_spec;
7571 let t_draft = t_round.elapsed();
7572 let sub_draft = subs();
7573 let b = k_spec + 1;
7576 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7577 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7578 let e = self.embed_single(t);
7579 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7580 }
7581 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7582 spec_stamp("v.emb");
7583 let (lm_gw, lm_rows) = {
7584 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7585 (
7586 crate::gpu::GraphW {
7587 idx: i,
7588 kind,
7589 row_scale: rs,
7590 data: &[],
7591 prism: crate::gpu::GraphPrismOp::None,
7592 affine: false,
7593 },
7594 self.weights.lm_head.rows(),
7595 )
7596 };
7597 let mut logits = Vec::new();
7598 let final_norm = self.weights.final_norm.clone();
7599 #[cfg(target_os = "macos")]
7608 let greedy_dev = metal_native
7609 && !sampling
7610 && !greedy_pen
7611 && !self.confidence_on
7612 && self.final_softcap.is_none()
7613 && self.vocab_size == lm_rows
7619 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7620 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7621 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7622 #[cfg(not(target_os = "macos"))]
7623 let greedy_dev = false;
7624 let mut dev_ids: Vec<u32> = Vec::new();
7625 #[cfg(target_os = "macos")]
7626 let verify_outcome = if metal_native {
7627 let lm = self.weights.lm_head.q1_parts()?;
7628 let n_score = self.vocab_size.min(lm_rows);
7629 self.try_batch_graph_metal(
7630 &mut hiddens,
7631 &positions,
7632 b,
7633 Some((lm, &final_norm, &mut logits)),
7634 if greedy_dev {
7635 Some((n_score, &mut dev_ids))
7636 } else {
7637 None
7638 },
7639 )
7640 } else {
7641 self.try_batch_graph_wgpu(
7642 &mut hiddens,
7643 &positions,
7644 b,
7645 Some(crate::gpu::SpecTail {
7646 lm: lm_gw,
7647 lm_rows,
7648 final_norm: &final_norm,
7649 logits_out: &mut logits,
7650 }),
7651 )
7652 };
7653 #[cfg(not(target_os = "macos"))]
7654 let verify_outcome = self.try_batch_graph_wgpu(
7655 &mut hiddens,
7656 &positions,
7657 b,
7658 Some(crate::gpu::SpecTail {
7659 lm: lm_gw,
7660 lm_rows,
7661 final_norm: &final_norm,
7662 logits_out: &mut logits,
7663 }),
7664 );
7665 match verify_outcome {
7666 crate::gpu::BatchGraphOutcome::Completed => {}
7667 crate::gpu::BatchGraphOutcome::Declined => {
7668 m.kv.truncate_last(k_spec);
7672 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7673 self.clear_sequence_state();
7674 self.graph_failed
7675 .store(true, std::sync::atomic::Ordering::Relaxed);
7676 self.cancel
7677 .store(true, std::sync::atomic::Ordering::Relaxed);
7678 tracing::error!("MTP graph mirror rewind failed after verify decline");
7679 }
7680 return None;
7681 }
7682 crate::gpu::BatchGraphOutcome::Failed => {
7683 self.clear_sequence_state();
7687 self.graph_failed
7688 .store(true, std::sync::atomic::Ordering::Relaxed);
7689 self.cancel
7690 .store(true, std::sync::atomic::Ordering::Relaxed);
7691 tracing::error!("MTP verify batch graph failed after admission");
7692 return None;
7693 }
7694 }
7695 #[cfg(target_os = "macos")]
7701 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7702 let snap: Vec<Vec<f32>> = self
7703 .kv_cache
7704 .layers
7705 .iter()
7706 .map(|l| l.linear_state.clone())
7707 .collect();
7708 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7709 let toks: Vec<u32> = std::iter::once(t_next)
7710 .chain(drafts.iter().copied())
7711 .collect();
7712 let want_save = self.graph_want_logits;
7713 self.graph_want_logits = false;
7714 for (i, &t) in toks.iter().enumerate() {
7715 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7716 let _ = self.graph_logits.take();
7717 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7721 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7722 }
7723 let ref_lg = self.logits_from_hidden(&hi);
7724 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7725 let ra = sampler::argmax(&ref_lg);
7726 let va = sampler::argmax(row);
7727 let mut md = 0f32;
7728 let mut rms = 0f64;
7729 for j in 0..lm_rows.min(ref_lg.len()) {
7730 let d = (ref_lg[j] - row[j]).abs();
7731 md = md.max(d);
7732 rms += (d as f64) * (d as f64);
7733 }
7734 let mut hd = 0f32;
7735 for j in 0..self.hidden_size {
7736 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7737 }
7738 eprintln!(
7739 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7740 next_pos + i,
7741 if ra == va { "OK" } else { "MISMATCH" },
7742 (rms / lm_rows as f64).sqrt()
7743 );
7744 }
7745 self.graph_want_logits = want_save;
7746 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7749 if l.linear_state.len() == st.len() {
7750 l.linear_state.copy_from_slice(&st);
7751 } else {
7752 l.linear_state = st;
7753 }
7754 }
7755 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7756 let extra = l.seq_len.saturating_sub(n0);
7757 if extra > 0 {
7758 l.truncate_last(extra);
7759 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7760 }
7761 }
7762 }
7763 let t_verify = t_round.elapsed();
7764 let sub_verify = subs();
7765 let mut a = 0usize;
7770 let mut forced: Option<u32> = None;
7771 let ids: Vec<u32> = if sparse {
7772 let mut p = std::mem::take(&mut self.spec_ps);
7773 let mut res = std::mem::take(&mut self.spec_ress);
7774 while a < k_spec {
7775 let ok = sampler::sparse_distribution_into(
7776 &logits[a * lm_rows..(a + 1) * lm_rows],
7777 &cfg,
7778 all_ids,
7779 &mut self.sampler_scratch,
7780 self.pool.as_deref(),
7781 &mut p,
7782 );
7783 if !ok {
7784 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7785 p.clear();
7786 p.push((t, 1.0));
7787 }
7788 match sampler::spec_accept_or_correct_sparse(
7789 &p,
7790 &self.spec_qs[a],
7791 drafts[a],
7792 &mut self.rng,
7793 &mut res,
7794 ) {
7795 None => {
7796 all_ids.push(drafts[a]);
7797 a += 1;
7798 }
7799 Some(c) => {
7800 forced = Some(c);
7801 break;
7802 }
7803 }
7804 }
7805 all_ids.truncate(base_len);
7806 self.spec_ps = p;
7807 self.spec_ress = res;
7808 drafts.clone()
7809 } else if sampling {
7810 let mut p = std::mem::take(&mut self.spec_p);
7811 let mut res = std::mem::take(&mut self.spec_res);
7812 while a < k_spec {
7813 sampler::distribution_into(
7814 &logits[a * lm_rows..(a + 1) * lm_rows],
7815 &cfg,
7816 all_ids,
7817 &mut self.sampler_scratch,
7818 self.pool.as_deref(),
7819 &mut p,
7820 );
7821 match sampler::spec_accept_or_correct(
7822 &p,
7823 &self.spec_q[a],
7824 drafts[a],
7825 &mut self.rng,
7826 &mut res,
7827 self.pool.as_deref(),
7828 ) {
7829 None => {
7830 all_ids.push(drafts[a]);
7831 a += 1;
7832 }
7833 Some(c) => {
7834 forced = Some(c);
7835 break;
7836 }
7837 }
7838 }
7839 all_ids.truncate(base_len);
7840 self.spec_p = p;
7841 self.spec_res = res;
7842 drafts.clone()
7844 } else if greedy_pen {
7845 let mut ids: Vec<u32> = Vec::with_capacity(b);
7849 for i in 0..b {
7850 let t = sampler::argmax_penalized(
7851 &logits[i * lm_rows..(i + 1) * lm_rows],
7852 &cfg,
7853 all_ids,
7854 &mut self.sampler_scratch,
7855 self.pool.as_deref(),
7856 );
7857 ids.push(t);
7858 if i < k_spec && t == drafts[i] {
7859 all_ids.push(t);
7860 } else {
7861 break;
7862 }
7863 }
7864 all_ids.truncate(base_len);
7865 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7866 a += 1;
7867 }
7868 ids
7871 } else if greedy_dev && dev_ids.len() == b {
7872 let ids = std::mem::take(&mut dev_ids);
7873 while a < k_spec && ids[a] == drafts[a] {
7874 a += 1;
7875 }
7876 ids
7877 } else {
7878 if logits.len() < b * lm_rows {
7879 self.clear_sequence_state();
7882 self.graph_failed
7883 .store(true, std::sync::atomic::Ordering::Relaxed);
7884 self.cancel
7885 .store(true, std::sync::atomic::Ordering::Relaxed);
7886 tracing::error!("Metal verify returned neither logits nor argmax ids");
7887 return None;
7888 }
7889 let ids: Vec<u32> = (0..b)
7890 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7891 .collect();
7892 while a < k_spec && ids[a] == drafts[a] {
7893 a += 1;
7894 }
7895 ids
7896 };
7897 spec_stamp("acc");
7898 if spec_dbg {
7899 eprintln!(
7900 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7901 drafts, ids
7902 );
7903 }
7904 #[cfg(target_os = "macos")]
7908 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7909 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7910 {
7911 let snap: Vec<Vec<f32>> = self
7912 .kv_cache
7913 .layers
7914 .iter()
7915 .map(|l| l.linear_state.clone())
7916 .collect();
7917 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7918 let toks: Vec<u32> = std::iter::once(t_next)
7919 .chain(drafts.iter().copied())
7920 .collect();
7921 let want_save = self.graph_want_logits;
7922 self.graph_want_logits = false;
7923 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7924 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7925 let _ = self.graph_logits.take();
7926 }
7927 self.graph_want_logits = want_save;
7928 let plain_states: Vec<Vec<f32>> = self
7929 .kv_cache
7930 .layers
7931 .iter()
7932 .map(|l| l.linear_state.clone())
7933 .collect();
7934 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7935 let mut rows = Vec::new();
7936 for (li, (l, n0)) in self
7937 .kv_cache
7938 .layers
7939 .iter_mut()
7940 .zip(attn_lens.iter())
7941 .enumerate()
7942 {
7943 let extra = l.seq_len.saturating_sub(*n0);
7944 if extra > 0 {
7945 let mut kk = Vec::new();
7946 let mut vv = Vec::new();
7947 for g in 0..nkv {
7948 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7949 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7950 }
7951 rows.push((li, kk, vv));
7952 l.truncate_last(extra);
7953 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7954 }
7955 }
7956 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7957 if l.linear_state.len() == st.len() {
7958 l.linear_state.copy_from_slice(&st);
7959 } else {
7960 l.linear_state = st;
7961 }
7962 }
7963 Some((plain_states, rows))
7964 } else {
7965 None
7966 };
7967 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7968 #[cfg(target_os = "macos")]
7977 let mut warm_pending: Option<MetalWarmPending> = None;
7978 #[cfg(target_os = "macos")]
7979 if metal_native {
7980 m.kv.truncate_last(k_spec.saturating_sub(1));
7981 if self.mtp_graph_mode == Some(true) {
7982 crate::gpu_metal::kv_mirror_set_stored(
7985 self.mtp_kv_id(),
7986 Self::MTP_LAYER_BASE,
7987 m.kv.seq_len,
7988 );
7989 if !warm_off && a > 0 {
7990 let pairs: Vec<(&[f32], u32)> = (0..a)
7991 .map(|j| {
7992 (
7993 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7994 ids[j],
7995 )
7996 })
7997 .collect();
7998 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7999 }
8000 }
8001 spec_stamp("c.wsub");
8002 }
8003 #[cfg(target_os = "macos")]
8005 if metal_native {
8006 if !self.metal_verify_commit(a) {
8009 self.clear_sequence_state();
8010 self.graph_failed
8011 .store(true, std::sync::atomic::Ordering::Relaxed);
8012 self.cancel
8013 .store(true, std::sync::atomic::Ordering::Relaxed);
8014 tracing::error!("Metal verify state/KV handoff failed after admission");
8015 return None;
8016 }
8017 if let Some((plain_states, rows)) = commit_ref {
8018 crate::gpu_metal::queue_fence();
8019 let _ = crate::gpu_metal::wait_replay();
8022 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8023 let mut worst_s = 0f32;
8024 let mut worst_li = 0usize;
8025 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8026 if l.linear_state.len() != ps.len() || ps.is_empty() {
8027 continue;
8028 }
8029 let d = l
8030 .linear_state
8031 .iter()
8032 .zip(ps)
8033 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8034 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8035 let rel = d / n.max(1e-6);
8036 if rel > worst_s {
8037 worst_s = rel;
8038 worst_li = li;
8039 }
8040 }
8041 let mut worst_k = 0f32;
8042 for (li, kk, vv) in &rows {
8043 let l = &self.kv_cache.layers[*li];
8044 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8045 let mut ck = Vec::new();
8046 let mut cv = Vec::new();
8047 for g in 0..nkv {
8048 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8049 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8050 }
8051 if ck.len() == kk.len() {
8052 let dk = ck
8053 .iter()
8054 .zip(kk)
8055 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8056 let dv = cv
8057 .iter()
8058 .zip(vv)
8059 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8060 worst_k = worst_k.max(dk).max(dv);
8061 } else {
8062 eprintln!(
8063 "commit-check L{li}: kv row count mismatch {} vs {}",
8064 ck.len(),
8065 kk.len()
8066 );
8067 }
8068 }
8069 eprintln!(
8070 "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}"
8071 );
8072 }
8073 }
8074 if !metal_native && a + 1 < b {
8075 let expected_gdn_layers = self.graph_gdn_layer_count();
8076 if expected_gdn_layers > 0
8077 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8078 {
8079 self.clear_sequence_state();
8080 self.graph_failed
8081 .store(true, std::sync::atomic::Ordering::Relaxed);
8082 self.cancel
8083 .store(true, std::sync::atomic::Ordering::Relaxed);
8084 tracing::error!("GDN speculative restore failed after verify");
8085 return None;
8086 }
8087 }
8088 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8089 self.clear_sequence_state();
8094 self.graph_failed
8095 .store(true, std::sync::atomic::Ordering::Relaxed);
8096 self.cancel
8097 .store(true, std::sync::atomic::Ordering::Relaxed);
8098 tracing::error!("trunk graph KV rewind failed after speculative verify");
8099 return None;
8100 }
8101 *accepted += a;
8102 if !metal_native {
8113 m.kv.truncate_last(k_spec.saturating_sub(1));
8115 }
8116 spec_stamp("c.trunc");
8117 if !metal_native
8118 && self.mtp_graph_mode == Some(true)
8119 && !self.rewind_mtp_graph_mirror(next_pos)
8120 {
8121 self.clear_sequence_state();
8125 self.graph_failed
8126 .store(true, std::sync::atomic::Ordering::Relaxed);
8127 self.cancel
8128 .store(true, std::sync::atomic::Ordering::Relaxed);
8129 tracing::error!("MTP graph mirror rewind failed after verify commit");
8130 return None;
8131 }
8132 if !warm_off && a > 0 {
8133 let mut warmed = false;
8136 #[cfg(target_os = "macos")]
8137 if metal_native && self.mtp_graph_mode == Some(true) {
8138 warmed = match warm_pending.take() {
8142 Some(p) => self.mtp_warm_batch_finish(m, p),
8143 None => false,
8144 };
8145 if !warmed {
8146 warmed = true;
8147 for j in 0..a {
8148 let row =
8149 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8150 if self
8151 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8152 .is_none()
8153 {
8154 warmed = false;
8155 break;
8156 }
8157 }
8158 }
8159 }
8160 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8161 let rows: Vec<Vec<f32>> = (0..a)
8162 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8163 .collect();
8164 let pairs: Vec<(&[f32], u32)> = rows
8165 .iter()
8166 .zip(ids.iter())
8167 .map(|(r, &t)| (r.as_slice(), t))
8168 .collect();
8169 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8170 Ok(()) => warmed = true,
8171 Err(err) => {
8172 tracing::error!("{err}");
8178 self.clear_sequence_state();
8179 self.graph_failed
8180 .store(true, std::sync::atomic::Ordering::Relaxed);
8181 self.cancel
8182 .store(true, std::sync::atomic::Ordering::Relaxed);
8183 return None;
8184 }
8185 }
8186 }
8187 if !warmed {
8188 for j in 0..a {
8189 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8190 let row = row.to_vec();
8191 self.mtp_warm(m, &row, ids[j], next_pos + j);
8192 }
8193 }
8194 }
8195 spec_stamp("c.warm");
8199 if let Some(c) = forced {
8200 self.spec_forced = Some(c);
8201 self.graph_logits = None;
8202 } else if greedy_dev && logits.is_empty() {
8203 self.spec_forced = Some(ids[a]);
8206 self.graph_logits = None;
8207 } else {
8208 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8209 row.resize(self.vocab_size, 0.0);
8210 if let Some(c) = self.final_softcap {
8211 for l in row.iter_mut() {
8212 *l = c * (*l / c).tanh();
8213 }
8214 }
8215 self.graph_logits = Some(row);
8216 }
8217 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8218 spec_stamp("c.row");
8219 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8225 let end = subs();
8226 eprintln!(
8227 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8228 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8229 t_draft.as_secs_f64() * 1e3,
8230 sub_draft - sub0,
8231 (t_verify - t_draft).as_secs_f64() * 1e3,
8232 sub_verify - sub_draft,
8233 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8234 end - sub_verify,
8235 self.draft_full_streak,
8236 );
8237 }
8238 if k_env.is_none() && !metal_native && !k_capped {
8243 let f = a as f32 / k_spec.max(1) as f32;
8247 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8248 let mut k_next = k_spec;
8249 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8250 k_next = k_spec + 1;
8251 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8252 k_next = k_spec - 1;
8253 }
8254 if k_next != k_spec {
8255 self.spec_acc_ewma = 0.6;
8256 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8257 eprintln!("spec-k: {k_spec} → {k_next}");
8258 }
8259 }
8260 self.spec_k_adapt = Some(k_next);
8261 }
8262 spec_stamp("end");
8263 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8264 }
8265
8266 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8275 if !self.pair_supported() {
8276 return (0.0, 0.0);
8277 }
8278 if self.gdn_cfg.is_some()
8286 && self.graph_prefill_preferred()
8287 && self.automatic_gpu_prefix().is_some()
8288 {
8289 tracing::info!(
8290 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8291 );
8292 return (0.0, 0.0);
8293 }
8294 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8301 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8302 let emb1 = self.embed_single(1);
8303 let emb2 = self.embed_single(2);
8304 let pos = self.kv_cache.seq_len();
8305
8306 let t0 = std::time::Instant::now();
8307 for _ in 0..iters {
8308 let _ = self.forward_layers(&emb1, pos, None);
8309 let _ = self.forward_layers(&emb2, pos + 1, None);
8310 for l in &mut self.kv_cache.layers {
8311 l.truncate_last(2);
8312 }
8313 }
8314 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8315
8316 let t1 = std::time::Instant::now();
8317 for _ in 0..iters {
8318 let _ = self.forward_pair(&emb1, &emb2, pos);
8319 for l in &mut self.kv_cache.layers {
8320 l.truncate_last(2);
8321 }
8322 }
8323 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8324 match graph_env {
8325 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8326 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8327 }
8328 (singles_ms, pair_ms)
8329 }
8330
8331 fn pair_supported(&self) -> bool {
8339 !self.weights.layers.is_empty()
8346 && !self.per_token_stack()
8347 && !self
8348 .weights
8349 .layers
8350 .iter()
8351 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8352 }
8353
8354 fn forward_pair(
8355 &mut self,
8356 emb1: &[f32],
8357 emb2: &[f32],
8358 position: usize,
8359 ) -> (Vec<f32>, Vec<f32>) {
8360 self.mimo_moe_prepare();
8363 let mut h1 = emb1.to_vec();
8364 let mut h2 = emb2.to_vec();
8365 let (_nkv, _hd, hs, _rd, eps) = (
8366 self.num_kv_heads,
8367 self.head_dim,
8368 self.hidden_size,
8369 self.rotary_dim,
8370 self.rms_eps,
8371 );
8372 let pool = self.pool.clone();
8373 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8380
8381 for li in 0..self.num_layers {
8382 let _capacity_tail = automatic_gpu_prefix
8383 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8384 .map(|_| crate::gpu::enter_cpu_scope());
8385 crate::gpu::set_layer(li as i64);
8388 let lw = &self.weights.layers[self.phys_layer(li)];
8389 inference::rms_norm_into(
8392 &h1,
8393 &lw.input_norm,
8394 self.rms_eps,
8395 self.norm_style,
8396 &mut self.ws.n1,
8397 );
8398 inference::rms_norm_into(
8399 &h2,
8400 &lw.input_norm,
8401 self.rms_eps,
8402 self.norm_style,
8403 &mut self.ws.n2,
8404 );
8405
8406 let (a1, a2) = match &lw.attn {
8407 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8408 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8409 AttnKind::Bounded(w) => {
8410 let rope = self
8413 .bounded_rope
8414 .clone()
8415 .expect("bounded layer without an installed rotation table");
8416 let cfg = crate::bounded::BoundedAttnCfg {
8417 num_heads: self.num_heads,
8418 num_kv_heads: self.num_kv_heads,
8419 head_dim: self.head_dim,
8420 hidden_size: hs,
8421 scale: self.attn_scale,
8422 rope: &rope,
8423 pool: pool.as_deref(),
8424 };
8425 let a1 = crate::bounded::bounded_attention(
8426 &self.ws.n1,
8427 w,
8428 &mut self.kv_cache.layers[li],
8429 &cfg,
8430 );
8431 let a2 = crate::bounded::bounded_attention(
8432 &self.ws.n2,
8433 w,
8434 &mut self.kv_cache.layers[li],
8435 &cfg,
8436 );
8437 (a1, a2)
8438 }
8439 AttnKind::Linear(w) => {
8440 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8441 let layer = &mut self.kv_cache.layers[li];
8442 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8443 vmf_phase_pair(
8444 &self.ws.n1,
8445 &self.ws.n2,
8446 w,
8447 &cfg,
8448 state,
8449 scratch,
8450 self.pool.as_deref(),
8451 )
8452 }
8453 AttnKind::LinearGdn(w) => {
8454 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8455 let layer = &mut self.kv_cache.layers[li];
8456 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8457 gdn_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::ShortConv(w) => {
8468 let cfg = self
8469 .short_conv_cfg
8470 .expect("short-conv layer without short_conv_cfg");
8471 let layer = &mut self.kv_cache.layers[li];
8472 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8473 short_conv_pair(
8474 &self.ws.n1,
8475 &self.ws.n2,
8476 w,
8477 &cfg,
8478 state,
8479 scratch,
8480 self.pool.as_deref(),
8481 )
8482 }
8483 AttnKind::Full {
8484 wq,
8485 wk,
8486 wv,
8487 wo,
8488 q_norm,
8489 k_norm,
8490 output_gate,
8491 softplus_gate,
8492 bias,
8493 } => {
8494 let inv_freq_l = self.layer_inv_freq(li);
8495 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8496 let cfg = QwenAttnCfg {
8497 num_heads: self.layer_num_heads(li),
8498 num_kv_heads: nkv_l,
8499 head_dim: hd_l,
8500 hidden_size: hs,
8501 position,
8502 inv_freq: &inv_freq_l,
8503 rotary_dim: rd_l,
8504 scale: self.attn_scale,
8505 softcap: self.attn_softcap,
8506 window: self.layer_window(li),
8507 v_norm: self.attn_v_norm,
8508 qk_norm_after_rope: self.qk_norm_after_rope,
8509 gate_sigmoid: self.proj_gate_sigmoid,
8510 q_norm: q_norm.as_deref(),
8511 k_norm: k_norm.as_deref(),
8512 output_gate: *output_gate,
8513 softplus_gate: softplus_gate
8514 .as_ref()
8515 .map(|(gate, per_head)| (gate, *per_head)),
8516 rope_scale: self.layer_rope_scale(li),
8517 bias: bias
8518 .as_ref()
8519 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8520 rms_eps: eps,
8521 norm_style: self.norm_style,
8522 pool: pool.as_deref(),
8523 v_head_dim: self.layer_v_dim(li),
8524 };
8525 attention::qwen_attention_pair(
8526 &self.ws.n1,
8527 &self.ws.n2,
8528 wq,
8529 wk,
8530 wv,
8531 wo,
8532 &mut self.kv_cache.layers[li],
8533 &cfg,
8534 )
8535 }
8536 };
8537 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8538 Some(w) => (
8539 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8540 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8541 ),
8542 None => (a1, a2),
8543 };
8544 for i in 0..self.hidden_size {
8545 h1[i] += a1[i];
8546 h2[i] += a2[i];
8547 }
8548 let (mut a1, mut a2) = (a1, a2);
8549 attention::recycle_buf(&mut a1);
8550 attention::recycle_buf(&mut a2);
8551
8552 let lw = &self.weights.layers[self.phys_layer(li)];
8553 inference::rms_norm_into(
8554 &h1,
8555 &lw.post_norm,
8556 self.rms_eps,
8557 self.norm_style,
8558 &mut self.ws.p1,
8559 );
8560 inference::rms_norm_into(
8561 &h2,
8562 &lw.post_norm,
8563 self.rms_eps,
8564 self.norm_style,
8565 &mut self.ws.p2,
8566 );
8567 let (f1, f2) = match &lw.ffn {
8568 FfnKind::DenseMoe(dm) => (
8571 dense_moe_ffn(
8572 dm,
8573 &self.ws.p1,
8574 &h1,
8575 self.rms_eps,
8576 self.norm_style,
8577 self.pool.as_deref(),
8578 ),
8579 dense_moe_ffn(
8580 dm,
8581 &self.ws.p2,
8582 &h2,
8583 self.rms_eps,
8584 self.norm_style,
8585 self.pool.as_deref(),
8586 ),
8587 ),
8588 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8589 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8590 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8591 ),
8592 _ => ffn_forward_pair(
8593 &lw.ffn,
8594 &self.ws.p1,
8595 &self.ws.p2,
8596 self.pool.as_deref(),
8597 None,
8598 ),
8599 };
8600 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8601 Some(w) => (
8602 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8603 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8604 ),
8605 None => (f1, f2),
8606 };
8607 for i in 0..self.hidden_size {
8608 h1[i] += f1[i];
8609 h2[i] += f2[i];
8610 }
8611 let (mut f1, mut f2) = (f1, f2);
8612 attention::recycle_buf(&mut f1);
8613 attention::recycle_buf(&mut f2);
8614 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8615 for i in 0..self.hidden_size {
8616 h1[i] *= sc;
8617 h2[i] *= sc;
8618 }
8619 }
8620 if self.is_loop_end(li) && li + 1 < self.num_layers {
8622 h1 = inference::rms_norm(
8623 &h1,
8624 &self.weights.final_norm,
8625 self.rms_eps,
8626 self.norm_style,
8627 );
8628 h2 = inference::rms_norm(
8629 &h2,
8630 &self.weights.final_norm,
8631 self.rms_eps,
8632 self.norm_style,
8633 );
8634 }
8635 }
8636 crate::gpu::set_layer(-1); if self.o1_active() {
8643 self.commit_linear_scratch();
8644 }
8645 self.o1_progress();
8646 self.swa_trim_tails();
8647 (h1, h2)
8648 }
8649
8650 fn commit_linear_scratch(&mut self) {
8652 for layer in &mut self.kv_cache.layers {
8653 if !layer.linear_scratch.is_empty() {
8654 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8655 layer.linear_scratch.clear();
8656 }
8657 }
8658 }
8659
8660 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8668 #[cfg(target_os = "macos")]
8669 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8670 if ids.is_empty() {
8671 return Err("empty id sequence".to_string());
8672 }
8673
8674 self.nll_begin()?;
8675 let result = (|| {
8676 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8677 self.nll_check_graph("encoder prefill", 0)?;
8678 let expected = ids.len() * self.hidden_size;
8679 if hidden.len() != expected {
8680 return Err(format!(
8681 "encoder prefill returned {} values for {} rows of hidden size {}",
8682 hidden.len(),
8683 ids.len(),
8684 self.hidden_size
8685 ));
8686 }
8687
8688 let mut out = vec![0.0f32; expected];
8689 for (src, dst) in hidden
8690 .chunks_exact(self.hidden_size)
8691 .zip(out.chunks_exact_mut(self.hidden_size))
8692 {
8693 inference::rms_norm_into(
8694 src,
8695 &self.weights.final_norm,
8696 self.rms_eps,
8697 self.norm_style,
8698 dst,
8699 );
8700 }
8701 Ok(out)
8702 })();
8703 self.nll_end();
8704 result
8705 }
8706
8707 pub fn forward_ids(
8710 &mut self,
8711 ids: &[u32],
8712 task_mask: Option<&TaskMask>,
8713 ) -> Result<Vec<f32>, String> {
8714 #[cfg(target_os = "macos")]
8715 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8716 if ids.is_empty() {
8717 return Err("empty id sequence".to_string());
8718 }
8719 self.clear_sequence_state();
8720 self.check_forward_graph("forward_ids setup", 0)?;
8721 if task_mask.is_none() {
8722 self.o1_begin();
8723 }
8724 let mut hidden = vec![0.0f32; self.hidden_size];
8725 let mut pos = 0usize;
8726 if let Some(b) = &mut self.dsv41 {
8727 let pool = self.pool.clone();
8728 let mut logits = Vec::new();
8729 crate::dsv41::forward_chunk(
8730 &b.0,
8731 &b.1,
8732 &b.2,
8733 &mut b.3,
8734 ids,
8735 0,
8736 pool.as_deref(),
8737 &mut logits,
8738 );
8739 if let Err(err) = self.o1_seal_checked() {
8740 self.clear_sequence_state();
8741 return Err(err);
8742 }
8743 return Ok(logits);
8744 }
8745 #[cfg(not(target_os = "macos"))]
8756 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8757 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8758 Ok(r) => r,
8759 Err(e) => {
8760 self.clear_sequence_state();
8761 return Err(e);
8762 }
8763 };
8764 if n > 0 {
8765 hidden.copy_from_slice(&last);
8766 pos = n;
8767 }
8768 }
8769 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8770 let chunk = self.prefill_chunk();
8774 let hs = self.hidden_size;
8775 while pos < ids.len() {
8776 let end = (pos + chunk).min(ids.len());
8777 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8778 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8779 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8780 pos = end;
8781 }
8782 }
8783 if task_mask.is_none()
8792 && !self.graph_prefill_preferred()
8793 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8794 && self.pair_supported()
8795 {
8796 while pos + 1 < ids.len() {
8797 let e1 = self.embed_single(ids[pos]);
8798 let e2 = self.embed_single(ids[pos + 1]);
8799 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8800 self.check_forward_graph("forward_ids pair", pos + 1)?;
8801 self.commit_linear_scratch();
8802 hidden = h2;
8803 pos += 2;
8804 }
8805 }
8806 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8809 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8810 self.graph_logits = Some(lg);
8811 hidden = vec![0.0; self.hidden_size];
8812 pos = ids.len();
8813 }
8814 }
8815 while pos < ids.len() {
8816 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8817 self.check_forward_graph("forward_ids", pos)?;
8818 pos += 1;
8819 }
8820 if let Some(logits) = self.graph_logits.take() {
8821 if let Err(err) = self.o1_seal_checked() {
8825 self.clear_sequence_state();
8826 return Err(err);
8827 }
8828 return Ok(logits);
8829 }
8830 if let Err(err) = self.o1_seal_checked() {
8834 self.clear_sequence_state();
8835 return Err(err);
8836 }
8837 let normed = inference::rms_norm(
8838 &hidden,
8839 &self.weights.final_norm,
8840 self.rms_eps,
8841 self.norm_style,
8842 );
8843 Ok(self.lm_head_forward(&normed))
8844 }
8845
8846 #[doc(hidden)]
8850 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8851 #[cfg(target_os = "macos")]
8852 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8853 if ids.is_empty() {
8854 return Err("empty id sequence".to_string());
8855 }
8856 self.clear_sequence_state();
8857 self.dsv41
8858 .as_ref()
8859 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8860 self.o1_begin();
8861 let rows = {
8862 let pool = self.pool.clone();
8863 let b = self
8864 .dsv41
8865 .as_mut()
8866 .expect("dsv41 checked above; state cannot change during forward");
8867 let mut rows = Vec::with_capacity(ids.len());
8868 for (position, &id) in ids.iter().enumerate() {
8869 let mut logits = Vec::new();
8870 crate::dsv41::forward_token(
8871 &b.0,
8872 &b.1,
8873 &b.2,
8874 &mut b.3,
8875 id,
8876 position,
8877 pool.as_deref(),
8878 &mut logits,
8879 );
8880 rows.push(logits);
8881 }
8882 rows
8883 };
8884 self.o1_seal();
8885 Ok(rows)
8886 }
8887
8888 pub fn dsv41_moe_capture(
8892 &mut self,
8893 ids: &[u32],
8894 lo: usize,
8895 hi: usize,
8896 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
8897 #[cfg(target_os = "macos")]
8898 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8899 if ids.is_empty() {
8900 return Err("empty id sequence".to_string());
8901 }
8902 self.dsv41
8903 .as_ref()
8904 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
8905 self.clear_sequence_state();
8906 let res = {
8907 let pool = self.pool.clone();
8908 let b = self.dsv41.as_mut().expect("dsv41 checked above");
8909 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
8910 };
8911 self.clear_sequence_state();
8912 res
8913 }
8914
8915 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8922 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8923 Ok((nll / cnt.max(1) as f64).exp())
8924 }
8925
8926 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
8933 if !self.can_prefill_batched() {
8934 return self.nll_ids_from(ids, 0).map(|_| ());
8935 }
8936 self.nll_begin()?;
8937 let result: Result<(), String> = (|| {
8938 let chunk = chunk.max(1);
8939 let mut pos = 0usize;
8940 while pos < ids.len() {
8941 let end = (pos + chunk).min(ids.len());
8942 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
8943 self.nll_check_graph("calibration prefill", pos)?;
8944 pos = end;
8945 }
8946 Ok(())
8947 })();
8948 self.nll_end();
8949 result
8950 }
8951
8952 pub fn calib_window_span(
8961 &mut self,
8962 ids: &[u32],
8963 input: Option<&[f32]>,
8964 from: usize,
8965 mid: usize,
8966 upto: usize,
8967 ) -> Result<Vec<f32>, String> {
8968 if !self.can_prefill_batched() {
8969 return Err("calib_window_span: this architecture has no batched prefill".into());
8970 }
8971 self.nll_begin()?;
8972 let result: Result<Vec<f32>, String> = (|| {
8973 let h = if mid > from {
8974 let inp = match input {
8975 Some(h) => PrefillIn::Hidden(h),
8976 None => PrefillIn::Ids(ids),
8977 };
8978 let h = self.prefill_batch_span(inp, 0, None, from, mid);
8979 self.nll_check_graph("calibration span", 0)?;
8980 h
8981 } else {
8982 input.map(<[f32]>::to_vec).unwrap_or_default()
8983 };
8984 if upto > mid {
8985 let inp = if mid > from || input.is_some() {
8986 PrefillIn::Hidden(&h)
8987 } else {
8988 PrefillIn::Ids(ids)
8989 };
8990 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
8991 self.nll_check_graph("calibration span", 0)?;
8992 }
8993 Ok(h)
8994 })();
8995 self.nll_end();
8996 result
8997 }
8998
8999 #[cfg(feature = "gpu")]
9003 pub fn qwen4_capture(
9004 &mut self,
9005 ids: &[u32],
9006 lo: usize,
9007 hi: usize,
9008 ) -> Result<Vec<Vec<f32>>, String> {
9009 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9010 crate::qwen4_exp::capture_window(
9011 &b.0,
9012 &b.1,
9013 &b.2,
9014 &mut b.3,
9015 ids,
9016 lo,
9017 hi,
9018 &self.inv_freq,
9019 self.pool.as_deref(),
9020 )
9021 }
9022
9023 pub fn qwen4_expert_dirs(
9025 &self,
9026 li: usize,
9027 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9028 let b = self.qwen4_exp.as_ref()?;
9029 crate::qwen4_exp::expert_dirs(&b.1, li)
9030 }
9031
9032 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9037 self.clear_sequence_state();
9038 FFN_PROBE.with(|p| {
9039 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9040 });
9041 crate::gpu::cpu_scope(|| {
9042 for (pos, &id) in ids.iter().enumerate() {
9043 let emb = self.embed_single(id);
9044 let _ = self.forward_layers(&emb, pos, None);
9045 }
9046 });
9047 self.clear_sequence_state();
9048 FFN_PROBE
9049 .with(|p| p.borrow_mut().take())
9050 .unwrap_or_default()
9051 }
9052
9053 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9057 if let Err(err) = self.nll_begin() {
9058 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9062 self.nll_end();
9063 return Err(err);
9064 }
9065 FFN_PROBE.with(|p| {
9066 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9067 });
9068 let result: Result<(), String> = (|| {
9069 for chunk in ids.chunks(256) {
9070 if chunk.len() < 2 {
9071 continue;
9072 }
9073 self.nll_ids_masked(chunk, 0, None)?;
9074 }
9075 Ok(())
9076 })();
9077 self.nll_end();
9078 let probe = FFN_PROBE
9079 .with(|p| p.borrow_mut().take())
9080 .unwrap_or_default();
9081 match result {
9082 Ok(()) => Ok(probe),
9083 Err(err) => {
9084 drop(probe);
9085 Err(err)
9086 }
9087 }
9088 }
9089
9090 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9094 self.nll_begin()?;
9095 let result: Result<f64, String> = (|| {
9096 let mut nll = 0f64;
9097 let mut cnt = 0usize;
9098 let mut hidden = vec![0f32; self.hidden_size];
9099 for (pos, &id) in ids.iter().enumerate() {
9100 if pos > 0 {
9101 inference::rms_norm_into(
9102 &hidden,
9103 &self.weights.final_norm,
9104 self.rms_eps,
9105 self.norm_style,
9106 &mut self.ws.n1,
9107 );
9108 let mut logits = self.lm_head_forward(&self.ws.n1);
9109 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9110 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9111 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9112 nll -= p.max(1e-300).ln();
9113 cnt += 1;
9114 attention::recycle_buf(&mut logits);
9115 }
9116 let emb = self.embed_single(id);
9117 hidden = self.forward_layers(&emb, pos, Some(mask));
9118 self.nll_check_graph("masked serial forward", pos)?;
9119 let _ = self.graph_logits.take();
9123 }
9124 Ok((nll / cnt.max(1) as f64).exp())
9125 })();
9126 self.nll_end();
9127 result
9128 }
9129
9130 pub fn nll_ids_masked(
9149 &mut self,
9150 ids: &[u32],
9151 start: usize,
9152 task_mask: Option<&TaskMask>,
9153 ) -> Result<(f64, usize), String> {
9154 let task_mask = self.drop_open_mask(task_mask);
9155 self.nll_ids_inner(ids, start, task_mask)
9156 }
9157
9158 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9159 self.nll_ids_inner(ids, start, None)
9160 }
9161
9162 fn nll_ids_inner(
9163 &mut self,
9164 ids: &[u32],
9165 start: usize,
9166 task_mask: Option<&TaskMask>,
9167 ) -> Result<(f64, usize), String> {
9168 self.nll_begin()?;
9169 let result: Result<(f64, usize), String> = (|| {
9170 let mut nll = 0f64;
9171 let mut cnt = 0usize;
9172 let (graph_quality, fused_head_quality) = nll_graph_policy(
9185 task_mask.is_none(),
9186 self.graph_prefill_preferred(),
9187 crate::gpu::q1_force(),
9188 );
9189 self.graph_head_required = fused_head_quality;
9190 self.graph_want_logits = fused_head_quality;
9191 #[cfg(target_os = "macos")]
9192 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9193 match self.nll_batch_metal(ids, start) {
9194 MetalBatchNllOutcome::Completed(nll, count) => {
9195 return Ok((nll, count));
9196 }
9197 MetalBatchNllOutcome::Declined => {}
9198 MetalBatchNllOutcome::Failed(err) => return Err(err),
9199 }
9200 }
9201 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9206 if self.can_prefill_batched() && !graph_quality && !force_serial {
9207 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9216 .ok()
9217 .and_then(|v| v.parse().ok())
9218 .filter(|&c: &usize| c > 0)
9219 .unwrap_or(128);
9220 const LM_SUB: usize = 32;
9221 let n = ids.len().saturating_sub(1);
9222 let hs = self.hidden_size;
9223 let rows = self.weights.lm_head.rows();
9224 let mut pos = 0usize;
9225 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9226 while pos < n {
9227 let end = (pos + chunk_len).min(n);
9228 let bsz = end - pos;
9229 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9230 self.nll_check_graph("batched prefill", pos)?;
9231 if state_trace && end % 256 == 0 {
9232 self.trace_recurrent_state(end);
9233 }
9234 let mut k0 = 0usize;
9235 while k0 < bsz {
9236 let k1 = (k0 + LM_SUB).min(bsz);
9237 let sb = k1 - k0;
9238 if pos + k1 <= start {
9241 k0 = k1;
9242 continue;
9243 }
9244 let mut normed = vec![0.0f32; sb * hs];
9245 for k in 0..sb {
9246 let r = inference::rms_norm(
9247 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9248 &self.weights.final_norm,
9249 self.rms_eps,
9250 self.norm_style,
9251 );
9252 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9253 }
9254 let mut logits = vec![0.0f32; sb * rows];
9255 self.weights
9256 .lm_head
9257 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9258 for k in 0..sb {
9259 if pos + k0 + k < start {
9260 continue;
9261 }
9262 self.nll_check_graph("batched score row", pos + k0 + k)?;
9263 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9264 if let Some(mu) = self.logit_multiplier {
9265 for v in lg.iter_mut() {
9266 *v *= mu;
9267 }
9268 }
9269 if let Some(c) = self.final_softcap {
9273 softcap_in_place(lg, c, self.pool.as_deref());
9274 }
9275 if let Some(cm) = self.head_clusters.clone() {
9278 self.hierarchical_head_logprobs(
9279 &normed[k * hs..(k + 1) * hs],
9280 &cm,
9281 lg,
9282 );
9283 }
9284 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9285 let target = ids[pos + k0 + k + 1] as usize;
9286 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9287 let lse: f64 = lg
9288 .iter()
9289 .map(|&v| ((v - max) as f64).exp())
9290 .sum::<f64>()
9291 .ln()
9292 + max as f64;
9293 nll += lse - lg[target] as f64;
9294 cnt += 1;
9295 if std::env::var("CMF_PPL_TRACE").is_ok() {
9296 let top = lg
9297 .iter()
9298 .enumerate()
9299 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9300 .map(|(i, _)| i)
9301 .unwrap_or(0);
9302 eprintln!(
9303 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9304 pos + k0 + k,
9305 target,
9306 lse - lg[target] as f64,
9307 top,
9308 lg[target],
9309 lg[top]
9310 );
9311 }
9312 }
9313 k0 = k1;
9314 }
9315 pos = end;
9316 }
9317 return Ok((nll, cnt));
9318 }
9319 for pos in 0..ids.len().saturating_sub(1) {
9320 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9321 self.nll_check_graph("serial forward", pos)?;
9322 let out_of_band = self.graph_logits.take();
9330 if self.graph_head_required && out_of_band.is_none() {
9331 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9332 return Err(format!(
9333 "fused Metal graph head did not complete at NLL position {pos}"
9334 ));
9335 }
9336 if pos < start {
9337 continue;
9338 }
9339 let logits = match out_of_band {
9340 Some(lg) => lg,
9341 None => {
9342 let normed = inference::rms_norm(
9343 &hidden,
9344 &self.weights.final_norm,
9345 self.rms_eps,
9346 self.norm_style,
9347 );
9348 self.lm_head_forward(&normed)
9352 }
9353 };
9354 let target = ids[pos + 1] as usize;
9355 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9356 let lse: f64 = logits
9357 .iter()
9358 .map(|&v| ((v - max) as f64).exp())
9359 .sum::<f64>()
9360 .ln()
9361 + max as f64;
9362 let tok_nll = lse - logits[target] as f64;
9363 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9364 let top = logits
9365 .iter()
9366 .enumerate()
9367 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9368 .map(|(i, _)| i)
9369 .unwrap_or(0);
9370 eprintln!(
9371 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9372 logits[target], logits[top]
9373 );
9374 }
9375 nll += tok_nll;
9376 cnt += 1;
9377 }
9378 Ok((nll, cnt))
9379 })();
9380 self.nll_end();
9381 result
9382 }
9383
9384 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9389 let normed = inference::rms_norm(
9390 hidden,
9391 &self.weights.final_norm,
9392 self.rms_eps,
9393 self.norm_style,
9394 );
9395 let mut logits = self.lm_head_forward(&normed);
9398 let target = target as usize;
9399 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9400 let lse: f64 = logits
9401 .iter()
9402 .map(|&v| ((v - max) as f64).exp())
9403 .sum::<f64>()
9404 .ln()
9405 + max as f64;
9406 let tok_nll = lse - logits[target] as f64;
9407 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9408 let top = logits
9409 .iter()
9410 .enumerate()
9411 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9412 .map(|(i, _)| i)
9413 .unwrap_or(0);
9414 eprintln!(
9415 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9416 logits[target], logits[top]
9417 );
9418 }
9419 attention::recycle_buf(&mut logits);
9420 tok_nll
9421 }
9422
9423 fn trace_recurrent_state(&self, pos: usize) {
9431 let stats = |v: &[f32]| -> (f64, f64) {
9432 if v.is_empty() {
9433 return (0.0, 0.0);
9434 }
9435 let (mut ss, mut mx) = (0f64, 0f64);
9436 for &x in v {
9437 ss += (x as f64) * (x as f64);
9438 mx = mx.max((x as f64).abs());
9439 }
9440 ((ss / v.len() as f64).sqrt(), mx)
9441 };
9442 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9443 let lw = &self.weights.layers[self.phys_layer(li)];
9444 let (kind, s_len) = match &lw.attn {
9445 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9446 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9447 AttnKind::Bounded(_) => ("bounded", 0),
9448 AttnKind::Full { .. } => ("full", 0),
9449 _ => ("other", 0),
9450 };
9451 let (rms, max) = stats(&l.linear_state);
9452 let s_part = if kind == "vmf" {
9453 &l.linear_state[..s_len.min(l.linear_state.len())]
9454 } else if kind == "gdn" {
9455 let ring = l
9456 .linear_state
9457 .len()
9458 .saturating_sub(s_len)
9459 .min(l.linear_state.len());
9460 &l.linear_state[ring..]
9461 } else {
9462 &l.linear_state[..0]
9463 };
9464 let (s_rms, s_max) = stats(s_part);
9465 let (ring_rms, ring_len) = match &l.bounded {
9466 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9467 None => (0.0, 0),
9468 };
9469 eprintln!(
9470 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9471 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9472 l.linear_state.len(),
9473 l.seq_len
9474 );
9475 }
9476 }
9477
9478 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9496 self.nll_begin()?;
9501 let requested_prefix = (prefill > 0).then_some(prefill);
9502 self.o1_begin_with_prefix(requested_prefix);
9503 let n = ids.len().saturating_sub(1);
9504 let requested_start = prefill.min(n);
9505 let exact_end = if self.o1_active() {
9510 match requested_prefix {
9511 Some(requested) => self.o1_effective_boundary(requested),
9512 None => self
9513 .o1_cfg
9514 .as_ref()
9515 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9516 }
9517 .unwrap_or(requested_start)
9518 .min(n)
9519 } else {
9520 requested_start
9521 };
9522 let mut nll = 0f64;
9523 let mut cnt = 0usize;
9524
9525 let mut pos = 0usize;
9529 if self.can_prefill_batched() {
9530 const CHUNK: usize = 128;
9531 while pos < exact_end {
9532 let end = (pos + CHUNK).min(exact_end);
9533 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9534 if self
9535 .graph_failed
9536 .swap(false, std::sync::atomic::Ordering::Relaxed)
9537 {
9538 self.cancel
9539 .store(false, std::sync::atomic::Ordering::Relaxed);
9540 self.nll_end();
9541 return Err("GPU graph failed during O(1) NLL prefix".into());
9542 }
9543 for row in 0..end - pos {
9544 let score_pos = pos + row;
9545 if score_pos >= requested_start && score_pos < n {
9546 nll += self.nll_from_hidden(
9547 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9548 ids[score_pos + 1],
9549 score_pos,
9550 );
9551 cnt += 1;
9552 }
9553 }
9554 pos = end;
9555 }
9556 } else {
9557 while pos < exact_end {
9558 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9559 if self
9560 .graph_failed
9561 .swap(false, std::sync::atomic::Ordering::Relaxed)
9562 {
9563 self.cancel
9564 .store(false, std::sync::atomic::Ordering::Relaxed);
9565 self.nll_end();
9566 return Err("GPU graph failed during O(1) NLL prefix".into());
9567 }
9568 if pos >= requested_start && pos < n {
9569 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9570 cnt += 1;
9571 }
9572 pos += 1;
9573 }
9574 }
9575 self.o1_seal_checked().map_err(|err| {
9576 self.nll_end();
9577 err
9578 })?;
9579
9580 let batch_k = std::env::var("CMF_BATCH_K")
9589 .ok()
9590 .and_then(|v| v.parse::<usize>().ok())
9591 .unwrap_or(0);
9592 let batch_admitted = batch_k > 0
9593 && self.can_prefill_batched()
9594 && self.o1_active()
9595 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9596 && (0..self.num_layers).all(|li| {
9597 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9598 cache.o1.is_none() || cache.o1_views().is_some()
9599 });
9600 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9601 eprintln!(
9602 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9603 batch_admitted,
9604 batch_k,
9605 n.saturating_sub(exact_end),
9606 );
9607 }
9608 let mut batch_completed = false;
9609 if batch_admitted && exact_end < n {
9610 let hs = self.hidden_size;
9611 let mut batch_pos = exact_end;
9612 while batch_pos < n {
9613 let end = (batch_pos + batch_k).min(n);
9614 let bk = end - batch_pos;
9615 let mut hiddens = vec![0.0f32; bk * hs];
9616 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9617 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9618 }
9619 let positions: Vec<usize> = (batch_pos..end).collect();
9620 let t_batch = std::time::Instant::now();
9621 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9622 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9623 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9624 eprintln!(
9625 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9626 batch_pos,
9627 end.saturating_sub(1),
9628 bk as f64 / (ms / 1000.0),
9629 );
9630 }
9631 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9632 self.nll_end();
9633 return Err(err);
9634 }
9635 match outcome {
9636 crate::gpu::BatchGraphOutcome::Completed => {
9637 batch_completed = true;
9638 for row in 0..bk {
9639 nll += self.nll_from_hidden(
9640 &hiddens[row * hs..(row + 1) * hs],
9641 ids[batch_pos + row + 1],
9642 batch_pos + row,
9643 );
9644 cnt += 1;
9645 }
9646 batch_pos = end;
9647 }
9648 crate::gpu::BatchGraphOutcome::Declined => {
9649 if batch_completed {
9650 self.nll_end();
9651 return Err(format!(
9652 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9653 ));
9654 }
9655 break;
9656 }
9657 crate::gpu::BatchGraphOutcome::Failed => {
9658 self.nll_end();
9659 return Err(format!(
9660 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9661 ));
9662 }
9663 }
9664 }
9665 if batch_completed && cnt == n.saturating_sub(requested_start) {
9666 self.nll_end();
9667 return Ok((nll, cnt));
9668 }
9669 }
9670
9671 for pos in exact_end..n {
9676 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9677 if self
9678 .graph_failed
9679 .swap(false, std::sync::atomic::Ordering::Relaxed)
9680 {
9681 self.cancel
9682 .store(false, std::sync::atomic::Ordering::Relaxed);
9683 self.nll_end();
9684 return Err(format!(
9685 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9686 ));
9687 }
9688 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9689 cnt += 1;
9690 }
9691 self.nll_end();
9692 Ok((nll, cnt))
9693 }
9694
9695 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9703 self.clear_sequence_state();
9704 let n = ids.len().saturating_sub(1);
9705 let mut correct = Vec::with_capacity(n);
9706 let mut pmax = Vec::with_capacity(n);
9707 for pos in 0..n {
9708 let emb = self.embed_single(ids[pos]);
9709 let hidden = self.forward_layers(&emb, pos, None);
9710 let logits = if let Some(logits) = self.graph_logits.take() {
9711 logits
9712 } else {
9713 let normed = inference::rms_norm(
9714 &hidden,
9715 &self.weights.final_norm,
9716 self.rms_eps,
9717 self.norm_style,
9718 );
9719 self.lm_head_forward(&normed)
9723 };
9724 let target = ids[pos + 1] as usize;
9725 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9726 for (i, &v) in logits.iter().enumerate() {
9727 if v > mval {
9728 mval = v;
9729 amax = i;
9730 }
9731 }
9732 correct.push(amax == target);
9733 let row: Vec<f32> = temps
9734 .iter()
9735 .map(|&t| {
9736 let tt = t.max(1e-3);
9737 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9738 1.0 / s.max(1e-12) })
9740 .collect();
9741 pmax.push(row);
9742 }
9743 self.clear_sequence_state();
9744 (correct, pmax)
9745 }
9746
9747 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9754 if self.dyn_router.is_none() {
9755 return Ok((self.ppl_ids(ids)?, 0));
9756 }
9757 self.nll_begin()?;
9758 let saved_active = self.dyn_active;
9759 let mut router = self
9760 .dyn_router
9761 .take()
9762 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9763 router.reset();
9764 self.dyn_phi_seen = 0;
9765 let _ = self.set_active_skill(None);
9766
9767 let result: Result<(f64, usize), String> = (|| {
9768 let mut nll = 0f64;
9769 let mut cnt = 0usize;
9770 for pos in 0..ids.len().saturating_sub(1) {
9771 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9772 self.nll_check_graph("dynamic serial forward", pos)?;
9773 let out_of_band = self.graph_logits.take();
9774 let mut logits = match out_of_band {
9775 Some(lg) => lg,
9776 None => {
9777 let normed = inference::rms_norm(
9778 &hidden,
9779 &self.weights.final_norm,
9780 self.rms_eps,
9781 self.norm_style,
9782 );
9783 self.lm_head_forward(&normed)
9787 }
9788 };
9789 let target = ids[pos + 1] as usize;
9790 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9791 let lse: f64 = logits
9792 .iter()
9793 .map(|&v| ((v - max) as f64).exp())
9794 .sum::<f64>()
9795 .ln()
9796 + max as f64;
9797 let tok_nll = lse - logits[target] as f64;
9798 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9799 let top = logits
9800 .iter()
9801 .enumerate()
9802 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9803 .map(|(i, _)| i)
9804 .unwrap_or(0);
9805 eprintln!(
9806 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9807 logits[target], logits[top]
9808 );
9809 }
9810 nll += tok_nll;
9811 cnt += 1;
9812 attention::recycle_buf(&mut logits);
9813 let phi = self.dyn_phi_ema.clone();
9815 if let Some(new_active) = router.step(&phi, pos) {
9816 let _ = self.set_active_skill(new_active);
9817 }
9818 }
9819 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9820 })();
9821
9822 let _ = self.set_active_skill(saved_active);
9825 self.dyn_router = Some(router);
9826 self.nll_end();
9827 result
9828 }
9829
9830 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9832 self.clear_sequence_state();
9833 let mut acc = vec![0f32; self.hidden_size];
9834 for (pos, &id) in ids.iter().enumerate() {
9835 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9836 for (a, v) in acc.iter_mut().zip(&h) {
9837 *a += v;
9838 }
9839 }
9840 let n = ids.len().max(1) as f32;
9841 for a in acc.iter_mut() {
9842 *a /= n;
9843 }
9844 self.clear_sequence_state();
9845 acc
9846 }
9847
9848 pub fn probe_phi_span(
9862 &mut self,
9863 ids: &[u32],
9864 layer: usize,
9865 span: std::ops::Range<usize>,
9866 ) -> Vec<f32> {
9867 #[cfg(target_os = "macos")]
9868 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9869 let end = span.end.min(ids.len());
9870 let start = span.start.min(end);
9871 let reset = |p: &mut Self| p.clear_sequence_state();
9872 reset(self);
9873 let mut acc = vec![0f32; self.hidden_size];
9874 for (pos, &id) in ids[..end].iter().enumerate() {
9875 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9876 if pos >= start {
9877 for (a, v) in acc.iter_mut().zip(&h) {
9878 *a += v;
9879 }
9880 }
9881 }
9882 let n = end - start;
9883 if n > 0 {
9884 let n = n as f32;
9885 for a in acc.iter_mut() {
9886 *a /= n;
9887 }
9888 }
9889 reset(self);
9890 acc
9891 }
9892
9893 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9900 #[cfg(target_os = "macos")]
9901 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9902 self.graph_logits = None;
9903 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9904 if let Some(logits) = self.graph_logits.take() {
9905 return logits;
9906 }
9907 inference::rms_norm_into(
9908 &hidden,
9909 &self.weights.final_norm,
9910 self.rms_eps,
9911 self.norm_style,
9912 &mut self.ws.n1,
9913 );
9914 self.lm_head_forward(&self.ws.n1)
9915 }
9916
9917 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9923 self.prefill_batch_masked(ids, start_pos, None)
9924 }
9925
9926 fn prefill_batch_masked(
9932 &mut self,
9933 ids: &[u32],
9934 start_pos: usize,
9935 task_mask: Option<&TaskMask>,
9936 ) -> Vec<f32> {
9937 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9938 }
9939
9940 fn prefill_rows(
9948 &mut self,
9949 ids: &[u32],
9950 pos: usize,
9951 task_mask: Option<&TaskMask>,
9952 ) -> Result<Vec<f32>, String> {
9953 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9954 }
9955
9956 fn prefill_input_rows(
9957 &mut self,
9958 input: PrefillIn<'_>,
9959 pos: usize,
9960 task_mask: Option<&TaskMask>,
9961 ) -> Result<Vec<f32>, String> {
9962 self.mimo_moe_prepare();
9963 let hs = self.hidden_size;
9964 let bk = match input {
9965 PrefillIn::Ids(ids) => ids.len(),
9966 PrefillIn::Hidden(rows) => rows.len() / hs,
9967 };
9968 #[cfg(not(target_os = "macos"))]
9969 if task_mask.is_none()
9970 && !self.o1_active()
9971 && bk > 1
9972 && (self.batch_prefix_prefill()
9973 || (self.verify_exact_moe
9974 && crate::gpu::enabled_here()
9975 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9976 {
9977 let mut hiddens = match input {
9978 PrefillIn::Hidden(rows) => rows.to_vec(),
9979 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9980 };
9981 let positions: Vec<usize> = (pos..pos + bk).collect();
9982 let mut run = 0usize;
9983 match self.try_batch_graph_wgpu_prefix(
9984 &mut hiddens,
9985 &positions,
9986 bk,
9987 None,
9988 Some(&mut run),
9989 ) {
9990 crate::gpu::BatchGraphOutcome::Completed => {
9991 let out = if run < self.num_layers {
9992 self.prefill_batch_span(
9993 PrefillIn::Hidden(&hiddens),
9994 pos,
9995 None,
9996 run,
9997 self.num_layers,
9998 )
9999 } else {
10000 hiddens
10001 };
10002 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10003 Err("MiMo attention graph failed after admission".into())
10004 } else {
10005 Ok(out)
10006 };
10007 }
10008 crate::gpu::BatchGraphOutcome::Failed => {
10009 return Err("batched prefix prefill failed after admission".into());
10010 }
10011 crate::gpu::BatchGraphOutcome::Declined => {
10012 #[cfg(feature = "gpu")]
10014 self.pull_lagging_host_kv(0, self.num_layers, pos);
10015 }
10016 }
10017 }
10018 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10019 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10020 Err("batch tail graph failed after admission".into())
10021 } else {
10022 Ok(out)
10023 }
10024 }
10025
10026 fn prefill_batch_span(
10032 &mut self,
10033 input: PrefillIn<'_>,
10034 start_pos: usize,
10035 task_mask: Option<&TaskMask>,
10036 from: usize,
10037 upto_excl: usize,
10038 ) -> Vec<f32> {
10039 let hs = self.hidden_size;
10040 let b = match input {
10041 PrefillIn::Ids(ids) => ids.len(),
10042 PrefillIn::Hidden(hb) => hb.len() / hs,
10043 };
10044 let upto_excl = upto_excl.min(self.num_layers);
10045 let mut h: Vec<f32>;
10049 let mut h_ready;
10050 match input {
10051 PrefillIn::Ids(_) => {
10052 h = vec![0.0; b * hs];
10053 h_ready = false;
10054 }
10055 PrefillIn::Hidden(hb) => {
10056 h = hb.to_vec();
10057 h_ready = true;
10058 }
10059 }
10060 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10061 if let PrefillIn::Ids(ids) = input {
10062 for (bi, &id) in ids.iter().enumerate() {
10063 let e = me.embed_single(id);
10064 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10065 }
10066 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10067 if let Ok(t) = tp.parse::<usize>() {
10068 if t >= start_pos && t < start_pos + ids.len() {
10069 let bi = t - start_pos;
10070 let row = &h[bi * hs..(bi + 1) * hs];
10071 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10072 eprintln!(
10073 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10074 ids[bi],
10075 row[0],
10076 row[1],
10077 ids.len(),
10078 &ids[..ids.len().min(8)]
10079 );
10080 }
10081 }
10082 }
10083 }
10084 };
10085 let (_nkv, _hd, _rd, eps) = (
10086 self.num_kv_heads,
10087 self.head_dim,
10088 self.rotary_dim,
10089 self.rms_eps,
10090 );
10091 let pool = self.pool.clone();
10092 let norm_style = self.norm_style;
10093 self.mimo_moe_prepare();
10094 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10095
10096 #[cfg(target_os = "macos")]
10097 let mut chunk_skip_until = 0usize;
10098 for li in from..upto_excl {
10099 let _capacity_tail = automatic_gpu_prefix
10100 .filter(|&prefix| {
10101 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10102 })
10103 .map(|_| crate::gpu::enter_cpu_scope());
10104 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10109 let _fast_gemm = self
10110 .proj_gate_sigmoid
10111 .then(crate::gpu::enter_prefill_fast_gemm);
10112 #[cfg(target_os = "macos")]
10118 if task_mask.is_none() {
10119 if li < chunk_skip_until {
10120 continue;
10121 }
10122 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10128 fill_h(&mut h, self);
10129 h_ready = true;
10130 }
10131 let ids_for_embed = match input {
10132 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10133 PrefillIn::Hidden(_) => None,
10134 };
10135 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10136 if end > li {
10137 h_ready = true;
10138 chunk_skip_until = end;
10139 if self.is_loop_end(end - 1) && end < self.num_layers {
10142 for bi in 0..b {
10143 let normed = inference::rms_norm(
10144 &h[bi * hs..(bi + 1) * hs],
10145 &self.weights.final_norm,
10146 eps,
10147 norm_style,
10148 );
10149 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10150 }
10151 }
10152 continue;
10153 }
10154 }
10155 if !h_ready {
10156 fill_h(&mut h, self);
10157 h_ready = true;
10158 }
10159 if task_mask.is_none() && self.verify_exact_moe {
10160 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10161 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10162 crate::gpu::BatchGraphOutcome::Completed => continue,
10163 crate::gpu::BatchGraphOutcome::Failed => return h,
10164 crate::gpu::BatchGraphOutcome::Declined => {}
10165 }
10166 }
10167 #[cfg(feature = "gpu")]
10168 self.pull_lagging_host_kv(li, li + 1, start_pos);
10169 let lw = &self.weights.layers[self.phys_layer(li)];
10170 let t_attn = std::time::Instant::now();
10171 match &lw.attn {
10173 AttnKind::Kda(w) => {
10174 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10176 let mut normed = vec![0.0f32; b * hs];
10177 for bi in 0..b {
10178 inference::rms_norm_into(
10179 &h[bi * hs..(bi + 1) * hs],
10180 &lw.input_norm,
10181 eps,
10182 norm_style,
10183 &mut normed[bi * hs..(bi + 1) * hs],
10184 );
10185 }
10186 let attn = crate::linear_core::kda_forward_batch(
10187 &normed,
10188 b,
10189 w,
10190 &cfg,
10191 &mut self.kv_cache.layers[li].linear_state,
10192 pool.as_deref(),
10193 );
10194 for (dst, &a) in h.iter_mut().zip(&attn) {
10195 *dst += a;
10196 }
10197 }
10198 AttnKind::LinearGdn(w) => {
10199 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10201 let mut normed = vec![0.0f32; b * hs];
10202 for bi in 0..b {
10203 let r = inference::rms_norm(
10204 &h[bi * hs..(bi + 1) * hs],
10205 &lw.input_norm,
10206 eps,
10207 norm_style,
10208 );
10209 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10210 }
10211 let attn = crate::linear_core::gdn_forward_batch(
10212 &normed,
10213 b,
10214 w,
10215 &cfg,
10216 &mut self.kv_cache.layers[li].linear_state,
10217 pool.as_deref(),
10218 );
10219 for (dst, &a) in h.iter_mut().zip(&attn) {
10220 *dst += a;
10221 }
10222 }
10223 AttnKind::ShortConv(w) => {
10224 let cfg = self
10227 .short_conv_cfg
10228 .expect("short-conv layer without short_conv_cfg");
10229 let mut normed = vec![0.0f32; b * hs];
10230 for bi in 0..b {
10231 inference::rms_norm_into(
10232 &h[bi * hs..(bi + 1) * hs],
10233 &lw.input_norm,
10234 eps,
10235 norm_style,
10236 &mut normed[bi * hs..(bi + 1) * hs],
10237 );
10238 }
10239 let attn = short_conv_forward_batch(
10240 &normed,
10241 b,
10242 w,
10243 &cfg,
10244 &mut self.kv_cache.layers[li].linear_state,
10245 pool.as_deref(),
10246 );
10247 for (dst, &a) in h.iter_mut().zip(&attn) {
10248 *dst += a;
10249 }
10250 }
10251 AttnKind::Mla(w) => {
10252 let inv_freq_l = self.layer_inv_freq(li);
10255 let rs = self.layer_rope_scale(li);
10256 let mut normed = vec![0.0f32; hs];
10257 for bi in 0..b {
10258 inference::rms_norm_into(
10259 &h[bi * hs..(bi + 1) * hs],
10260 &lw.input_norm,
10261 eps,
10262 norm_style,
10263 &mut normed,
10264 );
10265 let ao = mla_attention(
10266 w,
10267 &normed,
10268 &mut self.kv_cache.layers[li],
10269 start_pos + bi,
10270 &inv_freq_l,
10271 rs,
10272 eps,
10273 pool.as_deref(),
10274 );
10275 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10276 *dst += a;
10277 }
10278 }
10279 }
10280 AttnKind::Full {
10281 wq,
10282 wk,
10283 wv,
10284 wo,
10285 q_norm,
10286 k_norm,
10287 output_gate,
10288 softplus_gate,
10289 bias,
10290 } => {
10291 let mut normed = vec![0.0f32; b * hs];
10295 for bi in 0..b {
10296 inference::rms_norm_into(
10297 &h[bi * hs..(bi + 1) * hs],
10298 &lw.input_norm,
10299 eps,
10300 norm_style,
10301 &mut normed[bi * hs..(bi + 1) * hs],
10302 );
10303 }
10304 let inv_freq_l = self.layer_inv_freq(li);
10305 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10306 let cfg = QwenAttnCfg {
10307 num_heads: self.layer_num_heads(li),
10308 num_kv_heads: nkv_l,
10309 head_dim: hd_l,
10310 hidden_size: hs,
10311 position: start_pos,
10312 inv_freq: &inv_freq_l,
10313 rotary_dim: rd_l,
10314 scale: self.attn_scale,
10315 softcap: self.attn_softcap,
10316 window: self.layer_window(li),
10317 v_norm: self.attn_v_norm,
10318 qk_norm_after_rope: self.qk_norm_after_rope,
10319 gate_sigmoid: self.proj_gate_sigmoid,
10320 q_norm: q_norm.as_deref(),
10321 k_norm: k_norm.as_deref(),
10322 output_gate: *output_gate,
10323 softplus_gate: softplus_gate
10324 .as_ref()
10325 .map(|(gate, per_head)| (gate, *per_head)),
10326 rope_scale: self.layer_rope_scale(li),
10327 bias: bias
10328 .as_ref()
10329 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10330 rms_eps: eps,
10331 norm_style,
10332 pool: pool.as_deref(),
10333 v_head_dim: self.layer_v_dim(li),
10334 };
10335 #[cfg(feature = "gpu")]
10336 let _mirror = self
10337 .prefill_mirror_target(li)
10338 .map(crate::gpu::enter_prefill_mirror);
10339 let mut attn = attention::qwen_attention_batch(
10340 &normed,
10341 b,
10342 wq,
10343 wk,
10344 wv,
10345 wo,
10346 &mut self.kv_cache.layers[li],
10347 &cfg,
10348 );
10349 if let Some(w) = &lw.attn_out_norm {
10350 for bi in 0..b {
10351 inference::rms_norm_into(
10352 &attn[bi * hs..(bi + 1) * hs],
10353 w,
10354 eps,
10355 norm_style,
10356 &mut normed[bi * hs..(bi + 1) * hs],
10357 );
10358 }
10359 attn.copy_from_slice(&normed);
10360 }
10361 for (dst, &a) in h.iter_mut().zip(&attn) {
10362 *dst += a;
10363 }
10364 }
10365 AttnKind::Bounded(w) => {
10366 let mut normed = vec![0.0f32; b * hs];
10369 for bi in 0..b {
10370 inference::rms_norm_into(
10371 &h[bi * hs..(bi + 1) * hs],
10372 &lw.input_norm,
10373 eps,
10374 norm_style,
10375 &mut normed[bi * hs..(bi + 1) * hs],
10376 );
10377 }
10378 let rope = self
10379 .bounded_rope
10380 .clone()
10381 .expect("bounded layer without an installed rotation table");
10382 let cfg = crate::bounded::BoundedAttnCfg {
10383 num_heads: self.num_heads,
10384 num_kv_heads: self.num_kv_heads,
10385 head_dim: self.head_dim,
10386 hidden_size: hs,
10387 scale: self.attn_scale,
10388 rope: &rope,
10389 pool: pool.as_deref(),
10390 };
10391 let mut attn = crate::bounded::bounded_attention_batch(
10392 &normed,
10393 b,
10394 w,
10395 &mut self.kv_cache.layers[li],
10396 &cfg,
10397 );
10398 if let Some(wn) = &lw.attn_out_norm {
10399 for bi in 0..b {
10400 inference::rms_norm_into(
10401 &attn[bi * hs..(bi + 1) * hs],
10402 wn,
10403 eps,
10404 norm_style,
10405 &mut normed[bi * hs..(bi + 1) * hs],
10406 );
10407 }
10408 attn.copy_from_slice(&normed);
10409 }
10410 for (dst, &a) in h.iter_mut().zip(&attn) {
10411 *dst += a;
10412 }
10413 attention::recycle_buf(&mut attn);
10414 }
10415 AttnKind::Linear(w) => {
10416 for bi in 0..b {
10417 let normed = inference::rms_norm(
10418 &h[bi * hs..(bi + 1) * hs],
10419 &lw.input_norm,
10420 eps,
10421 norm_style,
10422 );
10423 vmf_phase_forward(
10424 &normed,
10425 w,
10426 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10427 &mut self.kv_cache.layers[li].linear_state,
10428 pool.as_deref(),
10429 )
10430 .iter()
10431 .enumerate()
10432 .for_each(|(i, &a)| h[bi * hs + i] += a);
10433 }
10434 }
10435 }
10436
10437 let lw = &self.weights.layers[self.phys_layer(li)];
10439 let mut post = vec![0.0f32; b * hs];
10440 for bi in 0..b {
10441 let r =
10442 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10443 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10444 }
10445 let mask_row = task_mask
10448 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10449 .and_then(|m| m.ffn_masks.get(li))
10450 .map(|v| v.as_slice());
10451 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10452 let t_ffn = std::time::Instant::now();
10453 let mut ffn = match &lw.ffn {
10454 FfnKind::Dense(d) if !d.segs.is_empty() => {
10455 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10456 }
10457 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10458 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10459 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10460 }
10461 FfnKind::Moe(m) if self.verify_exact_moe => {
10462 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10463 }
10464 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10467 let before = m.stats.borrow().clone();
10468 let out = crate::gpu::cpu_scope(|| {
10469 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10470 });
10471 self.mimo_moe.prime(li, m, &before);
10472 out
10473 }
10474 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10475 FfnKind::DenseMoe(dm) => {
10478 let mut out = vec![0.0f32; b * hs];
10479 for bi in 0..b {
10480 let r = dense_moe_ffn(
10481 dm,
10482 &post[bi * hs..(bi + 1) * hs],
10483 &h[bi * hs..(bi + 1) * hs],
10484 eps,
10485 norm_style,
10486 pool.as_deref(),
10487 );
10488 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10489 }
10490 out
10491 }
10492 };
10493 if prefill_prof_on() {
10494 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10495 PREFILL_SPLIT[1].fetch_add(
10496 t_ffn.elapsed().as_nanos() as u64,
10497 std::sync::atomic::Ordering::Relaxed,
10498 );
10499 }
10500 if let Some(w) = &lw.ffn_out_norm {
10501 for bi in 0..b {
10502 inference::rms_norm_into(
10503 &ffn[bi * hs..(bi + 1) * hs],
10504 w,
10505 eps,
10506 norm_style,
10507 &mut post[bi * hs..(bi + 1) * hs],
10508 );
10509 }
10510 ffn.copy_from_slice(&post);
10511 }
10512 for (dst, &f) in h.iter_mut().zip(&ffn) {
10513 *dst += f;
10514 }
10515 if let Some(sc) = lw.layer_scale {
10516 for v in h.iter_mut() {
10517 *v *= sc;
10518 }
10519 }
10520 if self.layer_dump.is_some() {
10522 for bi in 0..b {
10523 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10524 }
10525 }
10526 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10527 if let Ok(t) = tp.parse::<usize>() {
10528 if t >= start_pos && t < start_pos + b {
10529 let bi = t - start_pos;
10530 let row = &h[bi * hs..(bi + 1) * hs];
10531 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10532 eprintln!(
10533 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10534 row[0], row[1]
10535 );
10536 }
10537 }
10538 }
10539 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10543 let row = &h[(b - 1) * hs..b * hs];
10544 let rms =
10545 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10546 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10547 eprintln!(
10548 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10549 match &self.weights.layers[self.phys_layer(li)].attn {
10550 AttnKind::LinearGdn(_) => "gdn",
10551 AttnKind::Linear(_) => "vmf",
10552 AttnKind::ShortConv(_) => "conv",
10553 _ => "attn",
10554 },
10555 match &lw.ffn {
10556 FfnKind::Moe(_) => "moe",
10557 FfnKind::Dense(_) => "dense",
10558 FfnKind::DenseMoe(_) => "dense+moe",
10559 },
10560 );
10561 }
10562 if self.is_loop_end(li) && li + 1 < self.num_layers {
10564 for bi in 0..b {
10565 let normed = inference::rms_norm(
10566 &h[bi * hs..(bi + 1) * hs],
10567 &self.weights.final_norm,
10568 eps,
10569 norm_style,
10570 );
10571 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10572 }
10573 }
10574 if std::env::var("CMF_TRACE_H").is_ok() {
10575 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10576 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10577 eprintln!(
10578 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10579 lw.layer_scale
10580 );
10581 }
10582 }
10583 crate::gpu::set_layer(-1); if prefill_prof_on() {
10585 let ms = |a: &std::sync::atomic::AtomicU64| {
10586 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10587 };
10588 let sp = &attention::ATTN_SPLIT;
10589 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10590 eprintln!(
10591 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10592 host gate {:.1}], host loop {:.1}, attend {:.1} \
10593 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10594 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10595 ms(&PREFILL_SPLIT[0]),
10596 ms(&sp[0]),
10597 qkv_enq,
10598 qkv_wait,
10599 ms(&sp[6]),
10600 ms(&sp[1]),
10601 ms(&sp[2]),
10602 ms(&sp[4]),
10603 ms(&sp[5]),
10604 ca_wait,
10605 ms(&sp[3]),
10606 ms(&PREFILL_SPLIT[1]),
10607 );
10608 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10609 eprintln!(
10610 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10611 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10612 fallbacks (cumulative)",
10613 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10614 );
10615 }
10616 self.o1_progress();
10621 self.swa_trim_tails();
10623 h
10624 }
10625
10626 fn embed_single(&self, id: u32) -> Vec<f32> {
10628 let mut out = vec![0.0f32; self.hidden_size];
10629 if (id as usize) < self.weights.embed_tokens.rows() {
10630 self.weights.embed_tokens.row_f32(id as usize, &mut out);
10631 }
10632 if self.embed_multiplier != 1.0 {
10633 for v in out.iter_mut() {
10634 *v *= self.embed_multiplier;
10635 }
10636 }
10637 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
10641 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10642 v[0] = id as f32;
10643 return v;
10644 }
10645 if let Some(b) = &self.g4e {
10649 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10650 }
10651 if let Some(b) = &self.g3n {
10652 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10653 }
10654 out
10655 }
10656
10657 #[cfg(target_os = "macos")]
10663 fn chunk_run_gpu(
10664 &mut self,
10665 li0: usize,
10666 h: &mut [f32],
10667 b: usize,
10668 pos0: usize,
10669 embed_ids: Option<&[u32]>,
10670 cap: usize,
10671 ) -> usize {
10672 let gemma4 =
10679 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10680 if !crate::gpu::enabled_here()
10681 || std::env::var("CMF_GPU_CHUNK")
10682 .map(|v| v == "0")
10683 .unwrap_or(false)
10684 || b < 32
10685 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10688 || (self.global_attn.is_some() && !gemma4)
10689 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10693 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10696 || self.o1_active()
10699 || (self.attn_v_norm && !gemma4)
10700 || !self.metal_rope_scale_ok()
10701 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10702 {
10703 return li0;
10704 }
10705 let Some(model) = self.model.clone() else {
10706 return li0;
10707 };
10708 let (nh, nkv, hd, hs) = (
10709 self.num_heads,
10710 self.num_kv_heads,
10711 self.head_dim,
10712 self.hidden_size,
10713 );
10714 let loop_end = if self.loop_final_norm {
10718 ((li0 / self.physical_layers) + 1) * self.physical_layers
10719 } else {
10720 self.num_layers
10721 };
10722 let run_end = self.num_layers.min(loop_end).min(cap);
10723 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10728 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10729 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10730 let mut stored_at: Vec<usize> = Vec::new();
10731 for li in li0..run_end {
10732 let lw = &self.weights.layers[self.phys_layer(li)];
10733 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10734 && !gemma4
10735 {
10736 break;
10737 }
10738 let AttnKind::Full {
10739 wq,
10740 wk,
10741 wv,
10742 wo,
10743 q_norm,
10744 k_norm,
10745 output_gate: false,
10746 softplus_gate,
10747 bias,
10748 } = &lw.attn
10749 else {
10750 break;
10751 };
10752 let head_gate = match softplus_gate {
10754 None => None,
10755 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10756 Some((d, r, c)) if r == nh && c == hs => Some(d),
10757 _ => break,
10758 },
10759 Some(_) => break,
10760 };
10761 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10765 FfnKind::Dense(d)
10766 if (matches!(d.act, Act::Silu | Act::Gelu)
10767 || (gemma4 && d.act == Act::GeluTanh))
10768 && d.segs.is_empty() =>
10769 {
10770 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10771 }
10772 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10773 Some((router, experts, q8)) => {
10774 let e0 = &m.experts[0];
10775 let route = Box::new(move |lg: &[f32]| {
10776 let (idx, p, wsum) = moe_route(lg, m, None);
10778 let mut st = m.stats.borrow_mut();
10779 if st.len() < m.experts.len() {
10780 st.resize(m.experts.len(), 0);
10781 }
10782 for &e in &idx {
10783 st[e] += 1;
10784 }
10785 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10786 (idx, w)
10787 });
10788 (
10789 &e0.gate_proj,
10790 &e0.up_proj,
10791 &e0.down_proj,
10792 Act::Silu,
10793 Some(crate::gpu_metal::ChunkMoe {
10794 router,
10795 experts,
10796 q8,
10797 route,
10798 }),
10799 )
10800 }
10801 None => break,
10802 },
10803 _ => break,
10804 };
10805 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10811 t.q8_row_parts()
10812 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10813 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10814 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10815 }
10816 fn cf(t: &QTensor) -> &[f32] {
10817 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10818 }
10819 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10820 if moe.is_none()
10829 && !gemma4
10830 && (col.iter().any(|c| !c.is_empty())
10831 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10832 {
10833 break;
10834 }
10835 let parts = (
10836 cw(wq),
10837 cw(wk),
10838 cw(wv),
10839 cw(wo),
10840 cw(gate_t),
10841 cw(up_t),
10842 cw(down_t),
10843 );
10844 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10845 else {
10846 break;
10847 };
10848 let layer = &self.kv_cache.layers[li];
10849 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10850 break;
10851 }
10852 debug_assert!(
10854 layer.base() == 0
10855 || self
10856 .layer_window(li)
10857 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10858 );
10859 stored_at.push(layer.head_len(0));
10860 let (nkv, hd) = if gemma4 {
10863 let (a, bb, _) = self.layer_geom(li);
10864 (a, bb)
10865 } else {
10866 (nkv, hd)
10867 };
10868 layers.push(crate::gpu_metal::ChunkLayer {
10869 model: &model,
10870 kv_id: self.graph_kv_id,
10871 layer: li,
10872 wq: pq,
10873 wk: pk,
10874 wv: pv,
10875 wo: po,
10876 gate: pg,
10877 up: pu,
10878 down: pd,
10879 input_norm: &lw.input_norm,
10880 post_norm: &lw.post_norm,
10881 bias: bias
10882 .as_ref()
10883 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10884 q_norm: q_norm.as_deref(),
10885 k_norm: k_norm.as_deref(),
10886 inv_freq: &tables[li - li0],
10887 rope_scale: self.layer_rope_scale(li),
10888 rd: self.layer_geom(li).2,
10889 nh,
10890 nkv,
10891 hd,
10892 hs,
10893 inter: gate_t.rows(),
10894 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10895 late_qk_norm: self.qk_norm_after_rope,
10896 eps: self.rms_eps as f32,
10897 window: self.layer_window(li),
10898 head_gate,
10899 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10900 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10901 v_norm: self.attn_v_norm,
10902 scale: self.attn_scale,
10903 attn_out_norm: lw.attn_out_norm.as_deref(),
10904 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10905 layer_scale: lw.layer_scale,
10906 }),
10907 col,
10908 moe,
10909 });
10910 }
10911 if layers.is_empty() {
10912 return li0;
10913 }
10914 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10916 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10917 .iter()
10918 .zip(&rows_kv)
10919 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10920 .collect();
10921 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10922 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10923 let li = layers[i].layer;
10924 let layer = &self.kv_cache.layers[li];
10925 let nkv = rows_kv[i].0;
10926 io.push(crate::gpu_metal::ChunkIo {
10927 cpu_stored: stored_at[i],
10928 cpu_gen: layer.generation(),
10929 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10930 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10931 out_k: ok,
10932 out_v: ov,
10933 imp: oi,
10934 });
10935 }
10936 let n_run = layers.len();
10937 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10938 let ep = embed_ids.and_then(|ids| {
10941 self.weights
10942 .embed_tokens
10943 .q8_row_parts()
10944 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10945 idx,
10946 rows,
10947 row_scale: rs,
10948 ids,
10949 mult: self.embed_multiplier,
10950 })
10951 });
10952 if embed_ids.is_some() && ep.is_none() {
10953 return li0;
10954 }
10955 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10956 return li0;
10957 }
10958 drop(io);
10959 drop(layers);
10960 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10963 let li = li0 + i;
10964 let row = rows_kv[i].1;
10965 let layer = &mut self.kv_cache.layers[li];
10966 for bi in 0..b {
10967 layer.append(
10968 &ok[bi * row..(bi + 1) * row],
10969 &ov[bi * row..(bi + 1) * row],
10970 &[],
10971 );
10972 }
10973 layer.accumulate_imp(oi);
10974 }
10975 last
10976 }
10977
10978 fn layer_is_local(&self, li: usize) -> bool {
10981 if let Some(layers) = &self.sliding_layers {
10982 return layers.get(li).copied().unwrap_or(false);
10983 }
10984 match self.swa {
10985 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10986 None => false,
10987 }
10988 }
10989
10990 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10993 if self.layer_is_local(li) {
10994 if let Some(f) = &self.inv_freq_local {
10995 return f.clone();
10996 }
10997 } else if let Some(f) = &self.inv_freq_global {
10998 return f.clone();
10999 }
11000 self.inv_freq.clone()
11001 }
11002
11003 fn layer_window(&self, li: usize) -> Option<usize> {
11005 self.swa
11006 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11007 }
11008
11009 fn swa_trim_tails(&mut self) {
11018 let Some((slack, align)) = self.swa_trim else {
11019 return;
11020 };
11021 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11022 return;
11023 }
11024 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11025 if let Some(w) = self.layer_window(li) {
11026 self.kv_cache.layers[li].trim_window(w, slack, align);
11027 }
11028 }
11029 }
11030
11031 fn layer_num_heads(&self, li: usize) -> usize {
11032 self.attention_heads_per_layer
11033 .as_ref()
11034 .and_then(|v| v.get(li).copied())
11035 .unwrap_or(self.num_heads)
11036 }
11037
11038 fn layer_rope_scale(&self, li: usize) -> f32 {
11039 if self.layer_is_local(li) {
11040 self.rope_scale_local
11041 } else {
11042 self.rope_scale
11043 }
11044 }
11045
11046 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11049 if !self.layer_is_local(li) {
11050 if let Some((ghd, gkv)) = self.global_attn {
11051 return (gkv, ghd, ghd);
11052 }
11053 }
11054 let rd = if self.layer_is_local(li) {
11055 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11056 } else {
11057 self.rotary_dim
11058 };
11059 let rd = if self
11061 .no_rope_layers
11062 .as_ref()
11063 .and_then(|v| v.get(li))
11064 .copied()
11065 == Some(true)
11066 {
11067 0
11068 } else {
11069 rd
11070 };
11071 (self.layer_num_kv_heads(li), self.head_dim, rd)
11072 }
11073
11074 fn layer_num_kv_heads(&self, li: usize) -> usize {
11077 self.kv_heads_per_layer
11078 .as_ref()
11079 .and_then(|v| v.get(self.phys_layer(li)).copied())
11080 .unwrap_or(self.num_kv_heads)
11081 }
11082
11083 fn layer_v_dim(&self, li: usize) -> usize {
11085 let (_, hd, _) = self.layer_geom(li);
11086 self.v_head_dim.unwrap_or(hd).min(hd)
11087 }
11088
11089 pub fn set_attn_geometry(
11098 &mut self,
11099 kv_heads_per_layer: Option<Vec<usize>>,
11100 v_head_dim: Option<usize>,
11101 ) -> Result<(), String> {
11102 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11103 if self.global_attn.is_some() {
11104 return Err(
11105 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11106 attention geometry"
11107 .into(),
11108 );
11109 }
11110 if self
11111 .weights
11112 .layers
11113 .iter()
11114 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11115 {
11116 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11117 }
11118 }
11119 if let Some(vd) = v_head_dim {
11120 if vd == 0 || vd > self.head_dim {
11121 return Err(format!(
11122 "v_head_dim {vd} must be in 1..={} (head_dim)",
11123 self.head_dim
11124 ));
11125 }
11126 }
11127 if let Some(v) = &kv_heads_per_layer {
11128 if v.len() != self.physical_layers {
11129 return Err(format!(
11130 "kv_heads_per_layer has {} entries, expected {} layers",
11131 v.len(),
11132 self.physical_layers
11133 ));
11134 }
11135 for (li, &nkv) in v.iter().enumerate() {
11136 let is_attn = matches!(
11137 self.weights.layers.get(li).map(|lw| &lw.attn),
11138 Some(AttnKind::Full { .. }) | None
11139 );
11140 if !is_attn {
11141 continue;
11142 }
11143 let nh = self
11144 .attention_heads_per_layer
11145 .as_ref()
11146 .and_then(|h| h.get(li).copied())
11147 .unwrap_or(self.num_heads);
11148 if nkv == 0 || nh % nkv != 0 {
11149 return Err(format!(
11150 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11151 ));
11152 }
11153 }
11154 }
11155 self.kv_heads_per_layer = kv_heads_per_layer;
11156 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11157 if self.kv_heads_per_layer.is_some() {
11158 for li in 0..self.kv_cache.layers.len() {
11159 let full = matches!(
11160 self.weights
11161 .layers
11162 .get(self.phys_layer(li))
11163 .map(|lw| &lw.attn),
11164 Some(AttnKind::Full { .. })
11165 );
11166 let nkv = self.layer_num_kv_heads(li);
11167 let cache = &self.kv_cache.layers[li];
11168 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11169 let sinks = cache.sinks.clone();
11170 self.kv_cache.layers[li] =
11171 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11172 self.kv_cache.layers[li].sinks = sinks;
11173 }
11174 }
11175 }
11176 Ok(())
11177 }
11178
11179 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11183 let Some(lw) = self.weights.layers.get(phys) else {
11184 return Err(format!("sinks for layer {phys}: no such layer"));
11185 };
11186 if !matches!(lw.attn, AttnKind::Full { .. }) {
11187 return Err(format!(
11188 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11189 ));
11190 }
11191 let nh = self
11192 .attention_heads_per_layer
11193 .as_ref()
11194 .and_then(|h| h.get(phys).copied())
11195 .unwrap_or(self.num_heads);
11196 if sinks.len() != nh {
11197 return Err(format!(
11198 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11199 sinks.len()
11200 ));
11201 }
11202 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11203 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11204 }
11205 for li in 0..self.kv_cache.layers.len() {
11206 if self.phys_layer(li) == phys {
11207 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11208 }
11209 }
11210 Ok(())
11211 }
11212
11213 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11223 if self.kv_heads_per_layer.is_some() {
11224 return Some("per-layer KV head counts");
11225 }
11226 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11227 return Some("V heads narrower than Q/K heads");
11228 }
11229 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11230 return Some("learned attention sinks");
11231 }
11232 if self.swa.is_some() || self.sliding_layers.is_some() {
11233 return Some("sliding-window layers");
11234 }
11235 None
11236 }
11237
11238 #[cfg(target_os = "macos")]
11246 fn metal_rope_scale_ok(&self) -> bool {
11247 !(self.qk_norm_after_rope
11248 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11249 }
11250
11251 #[cfg(target_os = "macos")]
11268 fn metal_graph_swa(&self) -> bool {
11269 (self.swa.is_some() || self.sliding_layers.is_some())
11270 && self.attention_heads_per_layer.is_none()
11271 && !self.attn_v_norm
11272 && self.attn_softcap == 0.0
11273 && self.kv_heads_per_layer.is_none()
11274 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11275 && self.global_attn.is_none()
11276 && self.inv_freq_global.is_none()
11277 && self.metal_rope_scale_ok()
11278 && !(0..self.num_layers).any(|li| {
11279 self.layer_is_local(li)
11280 && self.inv_freq_local.is_none()
11281 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11282 })
11283 }
11284
11285 #[cfg(target_os = "macos")]
11293 fn metal_gemma4(&self) -> bool {
11294 crate::gpu_metal::gemma4::enabled()
11295 && (self.global_attn.is_some() || self.attn_v_norm)
11296 && self.attention_heads_per_layer.is_none()
11297 && self.kv_heads_per_layer.is_none()
11298 && self.v_head_dim.is_none()
11299 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11300 && self.attn_softcap == 0.0
11301 && !self.qk_norm_after_rope
11302 && self.metal_rope_scale_ok()
11303 && self
11304 .weights
11305 .layers
11306 .iter()
11307 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11308 && !(0..self.num_layers).any(|li| {
11309 self.layer_is_local(li)
11310 && self.inv_freq_local.is_none()
11311 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11312 })
11313 }
11314
11315 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11322 let geometry_reason = self.graph_attn_decline_reason();
11326 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11327 if has_scaled_rope && geometry_reason.is_none() {
11328 return Some("scaled RoPE without per-layer geometry");
11329 }
11330 if has_scaled_rope && self.qk_norm_after_rope {
11335 return Some("scaled RoPE with post-RoPE Q/K norm");
11336 }
11337 geometry_reason?;
11338 if self.attention_heads_per_layer.is_some() {
11344 return Some("per-layer Q head counts with per-layer geometry");
11345 }
11346 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11350 if !gemma4
11351 && self.weights.layers.iter().any(|lw| {
11352 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11353 })
11354 {
11355 return Some("sandwich norms / layer scale with per-layer geometry");
11356 }
11357 if self.weights.layers.iter().any(|lw| {
11361 matches!(
11362 &lw.attn,
11363 AttnKind::Full {
11364 output_gate: true,
11365 ..
11366 }
11367 )
11368 }) && self.v_head_dim.is_some()
11369 {
11370 return Some("gated attention with V narrower than K");
11371 }
11372 if (0..self.num_layers).any(|li| {
11373 self.layer_is_local(li)
11374 && self.inv_freq_local.is_none()
11375 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11376 }) {
11377 return Some("local rotary width without a local RoPE table");
11378 }
11379 None
11380 }
11381
11382 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11386 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11389 return None;
11390 }
11391 if self.global_attn.is_some() {
11392 return Some("per-layer head width (Gemma-4 global layers)");
11393 }
11394 if self.attn_v_norm {
11395 return Some("V norm");
11396 }
11397 if self.weights.layers.iter().any(|lw| {
11398 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11399 }) {
11400 return Some("sandwich norms / layer scale");
11401 }
11402 None
11403 }
11404
11405 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11410 self.graph_attn_decline_reason()?;
11411 let (nkv, hd, rd) = self.layer_geom(li);
11412 let invf: &[f32] = if self.layer_is_local(li) {
11413 match &self.inv_freq_local {
11414 Some(f) => f.as_slice(),
11415 None => self.inv_freq.as_slice(),
11416 }
11417 } else {
11418 match &self.inv_freq_global {
11419 Some(f) => f.as_slice(),
11420 None => self.inv_freq.as_slice(),
11421 }
11422 };
11423 Some(crate::gpu::GraphAttnGeom {
11424 nkv,
11425 hd,
11426 dv: self.layer_v_dim(li),
11427 rd,
11428 invf,
11429 rope_scale: self.layer_rope_scale(li),
11430 window: self.layer_window(li),
11431 sink: self.kv_cache.layers[li].sinks.as_deref(),
11432 v_norm: self.attn_v_norm,
11433 })
11434 }
11435
11436 #[cfg(feature = "gpu")]
11448 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11449 if !self.proj_gate_sigmoid
11450 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11451 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11452 || self.graph_refused()
11453 || self.o1_active()
11454 || self.attn_softcap > 0.0
11455 || self.wgpu_graph_attn_decline().is_some()
11456 {
11457 return None;
11458 }
11459 let g = self.graph_attn_geom(li)?;
11460 let (nkv, hd, _) = self.layer_geom(li);
11461 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11463 return None;
11464 }
11465 Some(crate::gpu::PrefillMirror {
11466 kv_id: self.graph_kv_id,
11467 layer: li,
11468 limit: self.kv_cache.max_seq_len,
11469 })
11470 }
11471
11472 #[cfg(feature = "gpu")]
11481 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11482 let kv_id = self.graph_kv_id;
11483 for li in from..upto.min(self.num_layers) {
11484 if !matches!(
11485 self.weights.layers[self.phys_layer(li)].attn,
11486 AttnKind::Full { .. }
11487 ) {
11488 continue;
11489 }
11490 let host = self.kv_cache.layers[li].pos_len();
11492 if host >= position {
11493 continue;
11494 }
11495 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11496 continue;
11497 };
11498 let to = dev.min(position);
11499 if to <= host {
11500 continue;
11501 }
11502 let (nkv, hd) = {
11503 let c = &self.kv_cache.layers[li];
11504 (c.num_kv_heads, c.head_dim)
11505 };
11506 let Some((k, v, first_valid)) =
11507 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11508 else {
11509 continue;
11510 };
11511 let need_from = match self.layer_window(li) {
11514 Some(w) => host.max((position + 1).saturating_sub(w)),
11515 None => host,
11516 };
11517 if first_valid > need_from {
11518 tracing::warn!(
11519 "layer {li}: device KV rows {host}..{to} no longer resident \
11520 (from {first_valid}); host attention will miss them"
11521 );
11522 }
11523 let row = nkv * hd;
11524 let cache = &mut self.kv_cache.layers[li];
11525 for p in 0..to - host {
11526 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11527 }
11528 }
11529 }
11530
11531 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11534 let mut seen = self.graph_declines.borrow_mut();
11535 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11536 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11537 seen.push((site, reason));
11538 }
11539 }
11540
11541 pub fn graph_declines(&self) -> Vec<String> {
11544 self.graph_declines
11545 .borrow()
11546 .iter()
11547 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11548 .collect()
11549 }
11550
11551 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11560 let Some(dir) = &self.layer_dump else {
11561 return;
11562 };
11563 let mut bytes = Vec::with_capacity(row.len() * 4);
11564 for v in row {
11565 bytes.extend_from_slice(&v.to_le_bytes());
11566 }
11567 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11568 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11569 use std::sync::atomic::{AtomicBool, Ordering};
11570 static SAID: AtomicBool = AtomicBool::new(false);
11571 if !SAID.swap(true, Ordering::Relaxed) {
11572 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11573 }
11574 }
11575 }
11576
11577 fn mimo_moe_prepare(&mut self) {
11580 if !self.mimo_moe.is_undecided() {
11581 return;
11582 }
11583 let slot = {
11584 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11585 .filter_map(
11586 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11587 FfnKind::Moe(m) => Some((li, m)),
11588 _ => None,
11589 },
11590 )
11591 .collect();
11592 if layers.is_empty()
11595 || self.physical_layers != self.num_layers
11596 || self.gpu_plan.is_some()
11597 {
11598 crate::mimo_moe::Slot::Off
11599 } else {
11600 let graph_prefix =
11604 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11605 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11606 }
11607 };
11608 self.mimo_moe = slot;
11609 }
11610
11611 #[cfg(test)]
11612 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11613 self.graph_kv_id
11614 }
11615
11616 pub(crate) fn mimo_graph_layer_rows(
11620 &mut self,
11621 li: usize,
11622 h: &mut [f32],
11623 positions: &[usize],
11624 ) -> crate::gpu::BatchGraphOutcome {
11625 use crate::gpu::BatchGraphOutcome as Out;
11626 let b = positions.len();
11627 if !(1..=4).contains(&b)
11628 || h.len() != b * self.hidden_size
11629 || !self.mimo_moe.is_dynamic(li, true)
11630 || !crate::gpu::enabled_here()
11631 || !crate::gpu::wgpu_active()
11632 || self.o1_active()
11633 || self.physical_layers != self.num_layers
11634 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11638 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11639 || self.wgpu_graph_attn_decline().is_some()
11640 {
11641 return Out::Declined;
11642 }
11643 let attn_started = std::time::Instant::now();
11644 let outcome = {
11645 let lw = &self.weights.layers[li];
11646 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11647 return Out::Declined;
11648 }
11649 let FfnKind::Moe(m) = &lw.ffn else {
11650 return Out::Declined;
11651 };
11652 let AttnKind::Full {
11653 wq,
11654 wk,
11655 wv,
11656 wo,
11657 q_norm,
11658 k_norm,
11659 output_gate,
11660 softplus_gate,
11661 bias,
11662 } = &lw.attn
11663 else {
11664 return Out::Declined;
11665 };
11666 if *output_gate || softplus_gate.is_some() {
11667 return Out::Declined;
11668 }
11669 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11670 m.experts
11671 .first()?
11672 .gate_proj
11673 .mapped_q4tp()
11674 .map(|(m, _)| m.clone())
11675 }) else {
11676 return Out::Declined;
11677 };
11678 fn gw<'a>(
11679 t: &'a QTensor,
11680 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11681 ) -> Option<crate::gpu::GraphW<'a>> {
11682 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11683 if m.uid() != owner.uid() || t.has_prism_contract() {
11684 return None;
11685 }
11686 return Some(crate::gpu::GraphW {
11687 idx,
11688 kind,
11689 row_scale: rs,
11690 data: &[],
11691 prism: crate::gpu::GraphPrismOp::None,
11692 affine: false,
11693 });
11694 }
11695 t.as_f32().map(|data| crate::gpu::GraphW {
11696 idx: 0,
11697 kind: 4,
11698 row_scale: &[],
11699 data,
11700 prism: crate::gpu::GraphPrismOp::None,
11701 affine: false,
11702 })
11703 }
11704 let (Some(q), Some(k), Some(v), Some(o)) = (
11705 gw(wq, &model),
11706 gw(wk, &model),
11707 gw(wv, &model),
11708 gw(wo, &model),
11709 ) else {
11710 return Out::Declined;
11711 };
11712 let layer = crate::gpu::GraphLayer {
11713 input_norm: &lw.input_norm,
11714 post_norm: &lw.post_norm,
11715 attn_out_norm: None,
11716 ffn_out_norm: None,
11717 layer_scale: None,
11718 ffn: crate::gpu::GraphFfn::AttentionOnly,
11719 attn: crate::gpu::GraphAttn::Full {
11720 wq: q,
11721 wk: k,
11722 wv: v,
11723 wo: o,
11724 q_norm: q_norm.as_deref(),
11725 k_norm: k_norm.as_deref(),
11726 late_qk_norm: self.qk_norm_after_rope,
11727 bias: bias
11728 .as_ref()
11729 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11730 output_gate: false,
11731 cpu_k: self.kv_cache.layers[li].k_heads(),
11732 cpu_v: self.kv_cache.layers[li].v_heads(),
11733 cpu_base: self.kv_cache.layers[li].base(),
11734 geom: self.graph_attn_geom(li),
11735 head_gate: None,
11736 },
11737 };
11738 let (nkv, hd, rd) = self.layer_geom(li);
11739 crate::gpu::forward_batch_graph_at(
11740 &model,
11741 self.graph_kv_id,
11742 li,
11743 &[layer],
11744 &self.inv_freq,
11745 h,
11746 self.layer_num_heads(li),
11747 nkv,
11748 hd,
11749 rd,
11750 self.hidden_size,
11751 1,
11752 positions,
11753 self.kv_cache.max_seq_len,
11754 self.norm_style == cortiq_core::NormStyle::Gemma,
11755 self.rms_eps as f32,
11756 self.attn_scale,
11757 b,
11758 &[],
11759 self.o1_epoch,
11760 None,
11761 None,
11762 )
11763 };
11764 match outcome {
11765 Out::Completed => {}
11766 Out::Declined => return Out::Declined,
11767 Out::Failed => {
11768 self.graph_failed
11769 .store(true, std::sync::atomic::Ordering::Relaxed);
11770 return Out::Failed;
11771 }
11772 }
11773 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11774 let hs = self.hidden_size;
11775 let lw = &self.weights.layers[li];
11776 let FfnKind::Moe(m) = &lw.ffn else {
11777 unreachable!()
11778 };
11779 let mut post = vec![0.0; h.len()];
11780 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11781 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11782 }
11783 let mut ffn = if b == 1 {
11784 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11785 } else {
11786 moe_ffn_banked_rows(
11787 &mut self.mimo_moe,
11788 li,
11789 m,
11790 &post,
11791 b,
11792 hs,
11793 self.pool.as_deref(),
11794 )
11795 };
11796 for (x, &f) in h.iter_mut().zip(&ffn) {
11797 *x += f;
11798 }
11799 attention::recycle_buf(&mut ffn);
11800 if self.layer_dump.is_some() {
11801 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11802 self.dump_layer_row(pos, li, row);
11803 }
11804 }
11805 crate::mimo_moe::note_attention_graph(b, attn_ns);
11806 Out::Completed
11807 }
11808
11809 fn layer_attn_plain(&self, li: usize) -> bool {
11810 self.kv_heads_per_layer.is_none()
11811 && self.v_head_dim.is_none()
11812 && self.global_attn.is_none()
11813 && self.layer_window(li).is_none()
11814 && self.kv_cache.layers[li].sinks.is_none()
11815 }
11816
11817 fn forward_layers(
11819 &mut self,
11820 hidden: &[f32],
11821 position: usize,
11822 task_mask: Option<&TaskMask>,
11823 ) -> Vec<f32> {
11824 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11825 self.o1_progress();
11826 self.swa_trim_tails();
11827 out
11828 }
11829
11830 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11838 self.embed_single(id)
11839 }
11840
11841 pub fn split_supported(&self) -> Result<(), String> {
11845 if self.dsv4.is_some() {
11846 return Err(
11847 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11848 );
11849 }
11850 if self.dsv41.is_some() {
11851 return Err(
11852 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11853 .into(),
11854 );
11855 }
11856 if self.qwen4_exp.is_some() {
11857 return Err(
11858 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11859 );
11860 }
11861 if self.per_token_stack() {
11862 return Err(
11863 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
11864 );
11865 }
11866 Ok(())
11867 }
11868
11869 pub fn forward_span(
11874 &mut self,
11875 hidden: &[f32],
11876 position: usize,
11877 from: usize,
11878 upto: usize,
11879 task_mask: Option<&TaskMask>,
11880 ) -> Result<Vec<f32>, String> {
11881 self.split_supported()?;
11882 if from > upto || upto >= self.num_layers {
11883 return Err(format!(
11884 "forward_span: layer range {from}..={upto} outside 0..{}",
11885 self.num_layers
11886 ));
11887 }
11888 if hidden.len() != self.hidden_size {
11889 return Err(format!(
11890 "forward_span: hidden len {} ≠ hidden_size {}",
11891 hidden.len(),
11892 self.hidden_size
11893 ));
11894 }
11895 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11896 self.o1_progress();
11897 self.swa_trim_tails();
11898 if self
11899 .graph_failed
11900 .swap(false, std::sync::atomic::Ordering::Relaxed)
11901 {
11902 self.cancel
11903 .store(false, std::sync::atomic::Ordering::Relaxed);
11904 self.clear_sequence_state();
11905 return Err("forward_span: deferred O(1) transition failed".into());
11906 }
11907 Ok(out)
11908 }
11909
11910 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11913 let normed = inference::rms_norm(
11914 hidden,
11915 &self.weights.final_norm,
11916 self.rms_eps,
11917 self.norm_style,
11918 );
11919 self.lm_head_forward(&normed)
11920 }
11921
11922 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11924 sampler::sample_with_scratch(
11925 logits,
11926 &self.sampler_config,
11927 past_tokens,
11928 &mut self.rng,
11929 &mut self.sampler_scratch,
11930 )
11931 }
11932
11933 pub fn reset_session(&mut self) {
11935 self.clear_sequence_state();
11936 }
11937
11938 pub fn prefill_span_ids(
11944 &mut self,
11945 ids: &[u32],
11946 start_pos: usize,
11947 upto: usize,
11948 task_mask: Option<&TaskMask>,
11949 ) -> Result<Vec<f32>, String> {
11950 self.split_supported()?;
11951 if upto >= self.num_layers {
11952 return Err(format!(
11953 "prefill_span_ids: upto {upto} outside 0..{}",
11954 self.num_layers
11955 ));
11956 }
11957 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11961 let out =
11962 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11963 self.check_o1_progress_failure("prefill_span_ids")?;
11964 Ok(out)
11965 } else {
11966 let hs = self.hidden_size;
11967 let mut out = Vec::with_capacity(ids.len() * hs);
11968 for (i, &id) in ids.iter().enumerate() {
11969 let emb = self.embed_id(id);
11970 out.extend_from_slice(&self.forward_span(
11971 &emb,
11972 start_pos + i,
11973 0,
11974 upto,
11975 task_mask,
11976 )?);
11977 }
11978 Ok(out)
11979 }
11980 }
11981
11982 pub fn prefill_span_hidden(
11985 &mut self,
11986 hidden: &[f32],
11987 start_pos: usize,
11988 from: usize,
11989 upto: usize,
11990 task_mask: Option<&TaskMask>,
11991 ) -> Result<Vec<f32>, String> {
11992 self.split_supported()?;
11993 let hs = self.hidden_size;
11994 if hidden.is_empty() || hidden.len() % hs != 0 {
11995 return Err(format!(
11996 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11997 hidden.len()
11998 ));
11999 }
12000 if from > upto || upto >= self.num_layers {
12001 return Err(format!(
12002 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12003 self.num_layers
12004 ));
12005 }
12006 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12007 let out = self.prefill_batch_span(
12008 PrefillIn::Hidden(hidden),
12009 start_pos,
12010 task_mask,
12011 from,
12012 upto + 1,
12013 );
12014 self.check_o1_progress_failure("prefill_span_hidden")?;
12015 Ok(out)
12016 } else {
12017 let b = hidden.len() / hs;
12018 let mut out = Vec::with_capacity(hidden.len());
12019 for i in 0..b {
12020 let h = self.forward_span(
12021 &hidden[i * hs..(i + 1) * hs],
12022 start_pos + i,
12023 from,
12024 upto,
12025 task_mask,
12026 )?;
12027 out.extend_from_slice(&h);
12028 }
12029 Ok(out)
12030 }
12031 }
12032
12033 fn try_token_graph_wgpu(
12037 &self,
12038 hidden: &[f32],
12039 position: usize,
12040 logits_out: &mut Vec<f32>,
12041 layers_run: &mut usize,
12042 ) -> Option<Result<Vec<f32>, ()>> {
12043 self.try_token_graph_wgpu_steps(
12044 hidden,
12045 position,
12046 logits_out,
12047 1,
12048 None,
12049 Some(layers_run),
12050 0,
12051 self.num_layers,
12052 )
12053 }
12054
12055 fn try_token_graph_wgpu_span(
12059 &self,
12060 hidden: &[f32],
12061 position: usize,
12062 logits_out: &mut Vec<f32>,
12063 from: usize,
12064 upto_excl: usize,
12065 layers_run: &mut usize,
12066 ) -> Option<Result<Vec<f32>, ()>> {
12067 self.try_token_graph_wgpu_steps(
12068 hidden,
12069 position,
12070 logits_out,
12071 1,
12072 None,
12073 Some(layers_run),
12074 from,
12075 upto_excl,
12076 )
12077 }
12078
12079 #[cfg(feature = "gpu")]
12083 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12084 if !crate::minicpm_tail::minimax_pool_on()
12085 || !crate::gpu::enabled_here()
12086 || self.head_clusters.is_some()
12087 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12088 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12089 {
12090 return None;
12091 }
12092 let (model, i, kind, rs) = self
12093 .weights
12094 .lm_head
12095 .graph_weight()
12096 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12097 let name = &model.tensors[i].name;
12098 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12099 crate::gpu::GraphPrismOp::InverseEmbedding
12100 } else if crate::prism::is_forward_weight(&model, name) {
12101 crate::gpu::GraphPrismOp::Forward
12102 } else {
12103 crate::gpu::GraphPrismOp::None
12104 };
12105 let gw = crate::gpu::GraphW {
12106 idx: i,
12107 kind,
12108 row_scale: rs,
12109 data: &[],
12110 prism,
12111 affine: crate::prism::is_affine_target(&model, name),
12112 };
12113 let rows = self.weights.lm_head.rows();
12114 let mut h = hidden.to_vec();
12115 let mut lg = Vec::new();
12116 let mut layers_run = 0usize;
12117 let outcome = crate::gpu::forward_token_graph(
12118 &model,
12119 self.graph_kv_id,
12120 &[],
12121 &[],
12122 self.o1_epoch,
12123 &self.inv_freq,
12124 &mut h,
12125 self.num_heads,
12126 self.num_kv_heads,
12127 self.head_dim,
12128 self.attn_scale,
12129 self.head_dim,
12130 self.hidden_size,
12131 self.intermediate_size,
12132 0,
12133 self.kv_cache.max_seq_len,
12134 self.norm_style == cortiq_core::NormStyle::Gemma,
12135 self.rms_eps as f32,
12136 Some((&gw, rows)),
12137 &self.weights.final_norm,
12138 &mut lg,
12139 &[],
12140 1,
12141 None,
12142 None,
12143 Some(&mut layers_run),
12144 0,
12145 false,
12146 );
12147 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12148 return None;
12149 }
12150 lg.resize(self.vocab_size, 0.0);
12153 if let Some(m) = self.logit_multiplier {
12154 for l in lg.iter_mut() {
12155 *l *= m;
12156 }
12157 }
12158 if let Some(c) = self.final_softcap {
12159 softcap_in_place(&mut lg, c, self.pool.as_deref());
12160 }
12161 Some(lg)
12162 }
12163
12164 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12167 if winners.is_empty() {
12168 return;
12169 }
12170 if self.m3_n_exp == 0 {
12171 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12172 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12173 }
12174 for &e in winners {
12175 let e = e as usize;
12176 if e < self.m3_n_exp && li < self.num_layers {
12177 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12178 }
12179 }
12180 if li + 1 == self.num_layers {
12181 self.m3_tokens += 1;
12182 if self.m3_tokens % 16 == 0 {
12183 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12184 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12185 let _ = std::fs::write(&path, bytes);
12186 }
12187 }
12188 }
12189 }
12190
12191 fn m3_prewarm(&mut self) {
12196 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12197 return;
12198 };
12199 let Ok(bytes) = std::fs::read(&path) else {
12200 tracing::warn!("m3 prewarm: cannot read profile {path}");
12201 return;
12202 };
12203 let counts: Vec<u32> = bytes
12204 .chunks_exact(4)
12205 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12206 .collect();
12207 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12208 if counts.len() != self.num_layers * n_exp {
12209 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12210 return;
12211 }
12212 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12213 .ok()
12214 .and_then(|v| v.parse().ok())
12215 .unwrap_or(300);
12216 let mut ranked: Vec<(u32, usize, usize)> = counts
12217 .iter()
12218 .enumerate()
12219 .filter(|(_, c)| **c > 0)
12220 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12221 .collect();
12222 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12223 ranked.truncate(limit);
12224 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12225 for (_, li, e) in ranked {
12226 per_layer[li].push(e);
12227 }
12228 let mut admitted = 0usize;
12229 for (li, ids) in per_layer.iter().enumerate() {
12230 if ids.is_empty() {
12231 continue;
12232 }
12233 let lw = &self.weights.layers[self.phys_layer(li)];
12234 let FfnKind::Moe(m) = &lw.ffn else {
12235 continue;
12236 };
12237 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12239 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12240 admitted += ids.len();
12241 }
12242 }
12243 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12244 }
12245
12246 #[cfg(feature = "gpu")]
12257 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12258 if self.o1_active() || self.attn_softcap > 0.0 {
12259 return Ok(false);
12260 }
12261 if !self.m3_prewarmed {
12262 self.m3_prewarmed = true;
12263 self.m3_prewarm();
12264 }
12265 let frame = {
12266 let lw = &self.weights.layers[self.phys_layer(li)];
12267 let FfnKind::Moe(m) = &lw.ffn else {
12268 return Ok(false);
12269 };
12270 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12271 return Ok(false);
12272 }
12273 let t_arm = std::time::Instant::now();
12274 if self.m3_winners.len() != self.num_layers {
12275 self.m3_winners = vec![Vec::new(); self.num_layers];
12276 }
12277 let pred = std::mem::take(&mut self.m3_winners[li]);
12278 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12279 if armed.is_none() {
12280 self.m3_winners[li] = pred; }
12282 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12283 match armed {
12284 Some(frame) => frame,
12285 None => return Ok(false),
12288 }
12289 };
12290 crate::gpu_wgpu::pooled_frame_enter(frame);
12291 let want_logits = self.graph_want_logits;
12295 self.graph_want_logits = false;
12296 let mut lg = Vec::new();
12297 let mut gl = 0usize;
12298 let t_span = std::time::Instant::now();
12299 let span = crate::gpu::gpu_scope(|| {
12300 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12301 });
12302 self.graph_want_logits = want_logits;
12303 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12304 let hh = match span {
12305 Some(Ok(hh)) => hh,
12306 Some(Err(())) => return Err(()),
12307 None => {
12308 return Ok(false);
12311 }
12312 };
12313 if gl != 1 {
12314 return if gl == 0 { Ok(false) } else { Err(()) };
12317 }
12318 *h = hh;
12319 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12320 self.m3_note_route(li, &winners);
12321 if self.m3_winners.len() != self.num_layers {
12322 self.m3_winners = vec![Vec::new(); self.num_layers];
12323 }
12324 self.m3_winners[li] = winners;
12325 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12326 cold.retain(|&(_, w)| {
12329 if w.is_finite() {
12330 true
12331 } else {
12332 tracing::error!("pooled tail: non-finite cold weight from expert route");
12333 false
12334 }
12335 });
12336 if cold.is_empty() {
12337 return Ok(true);
12338 }
12339 let x = crate::gpu_wgpu::pooled_frame_take_input();
12340 if x.len() != self.hidden_size {
12341 return Err(());
12342 }
12343 let lw = &self.weights.layers[self.phys_layer(li)];
12349 let FfnKind::Moe(m) = &lw.ffn else {
12350 return Err(());
12351 };
12352 let t_cold = std::time::Instant::now();
12353 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12354 let mut out = crate::attention::take_buf(self.hidden_size);
12355 let on_device = !force_host
12356 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12357 if !on_device {
12358 crate::attention::recycle_buf(&mut out);
12359 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12360 .iter()
12361 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12362 .collect::<Result<_, ()>>()?;
12363 out = crate::gpu::cpu_scope(|| {
12364 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12365 });
12366 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12367 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12368 }
12369 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12370 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12371 if out.len() != h.len() {
12372 crate::attention::recycle_buf(&mut out);
12373 return Err(());
12374 }
12375 for (dst, src) in h.iter_mut().zip(out.iter()) {
12376 *dst += src;
12377 }
12378 crate::attention::recycle_buf(&mut out);
12379 if on_device {
12380 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12381 }
12382 Ok(true)
12383 }
12384
12385 #[cfg(not(feature = "gpu"))]
12386 fn m3_pooled_layer(
12387 &mut self,
12388 _li: usize,
12389 _position: usize,
12390 _h: &mut Vec<f32>,
12391 ) -> Result<bool, ()> {
12392 Ok(false)
12393 }
12394
12395 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12399 if self.o1_active() || self.attn_softcap > 0.0 {
12400 return None;
12401 }
12402 if let Some(reason) = self.wgpu_graph_attn_decline() {
12405 self.note_graph_decline("wgpu multi-burst", reason);
12406 return None;
12407 }
12408 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12409 if !graph_on || self.graph_refused() {
12410 return None;
12417 }
12418 let emb = self.embed_single(t_next);
12419 let mut lg = Vec::new();
12420 let mut ids = Vec::new();
12421 match self.try_token_graph_wgpu_steps(
12422 &emb,
12423 position,
12424 &mut lg,
12425 k,
12426 Some(&mut ids),
12427 None,
12428 0,
12429 self.num_layers,
12430 ) {
12431 Some(Ok(_)) => {}
12432 Some(Err(())) => {
12433 self.graph_failed
12438 .store(true, std::sync::atomic::Ordering::Relaxed);
12439 return None;
12440 }
12441 None => return None,
12442 }
12443 (ids.len() == k).then_some(ids)
12444 }
12445
12446 fn try_token_graph_wgpu_steps(
12450 &self,
12451 hidden: &[f32],
12452 position: usize,
12453 logits_out: &mut Vec<f32>,
12454 steps: usize,
12455 ids_out: Option<&mut Vec<u32>>,
12456 layers_run: Option<&mut usize>,
12457 from: usize,
12458 upto_excl: usize,
12459 ) -> Option<Result<Vec<f32>, ()>> {
12460 let upto_excl = match self.mimo_moe.graph_prefix_end() {
12463 Some(end) if end < upto_excl => {
12464 if steps != 1 || layers_run.is_none() || from >= end {
12465 return None;
12466 }
12467 end
12468 }
12469 _ => upto_excl,
12470 };
12471 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
12474 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
12475 return None;
12479 }
12480 if let Some(reason) = self.wgpu_graph_attn_decline() {
12487 self.note_graph_decline("wgpu token graph", reason);
12488 return None;
12489 }
12490 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12495 .map(|li| {
12496 if !o1_gpu {
12497 return None;
12498 }
12499 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12500 })
12501 .collect();
12502 if self.o1_active() && o1_gpu {
12503 let want: usize = (from..upto_excl)
12506 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12507 .count();
12508 let have = o1_views.iter().filter(|v| v.is_some()).count();
12509 if want == 0 || have != want {
12510 use std::sync::atomic::{AtomicUsize, Ordering};
12520 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
12521 let code = have * 1000 + want;
12522 if LAST.swap(code, Ordering::Relaxed) != code {
12523 tracing::warn!(
12524 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
12525 );
12526 }
12527 return None;
12528 }
12529 }
12530 let nh = self.num_heads;
12531 let (nkv, hd, rd) = self.layer_geom(0);
12532 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12533 let mut layers = Vec::with_capacity(upto_excl - from);
12534 let mut model = None;
12535 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
12536 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
12537 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
12539 let name = &m.tensors[i].name;
12540 let prism = if crate::prism::is_inverse_embedding(m, name) {
12541 crate::gpu::GraphPrismOp::InverseEmbedding
12542 } else if crate::prism::is_forward_weight(m, name) {
12543 crate::gpu::GraphPrismOp::Forward
12544 } else {
12545 crate::gpu::GraphPrismOp::None
12546 };
12547 return Some(crate::gpu::GraphW {
12548 idx: i,
12549 kind,
12550 row_scale: rs,
12551 data: &[],
12552 prism,
12553 affine: crate::prism::is_affine_target(m, name),
12554 });
12555 }
12556 match t.as_f32() {
12558 Some(d) => Some(crate::gpu::GraphW {
12559 idx: 0,
12560 kind: 4,
12561 row_scale: &[],
12562 data: d,
12563 prism: crate::gpu::GraphPrismOp::None,
12564 affine: false,
12565 }),
12566 None => {
12567 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
12568 eprintln!("batch graph: weight has no graph/f32 representation");
12569 }
12570 None
12571 }
12572 }
12573 }
12574 for li in from..upto_excl {
12575 let lw = &self.weights.layers[self.phys_layer(li)];
12576 if dbg {
12577 let ak = match &lw.attn {
12578 AttnKind::Mla(_) => "Mla".into(),
12579 AttnKind::Full {
12580 output_gate, bias, ..
12581 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
12582 AttnKind::LinearGdn(_) => "LinearGdn".into(),
12583 AttnKind::Kda(_) => "Kda".into(),
12584 AttnKind::Linear(_) => "Linear".into(),
12585 AttnKind::ShortConv(_) => "ShortConv".into(),
12586 AttnKind::Bounded(_) => "Bounded".into(),
12587 };
12588 let fk = match &lw.ffn {
12589 FfnKind::Dense(_) => "Dense",
12590 FfnKind::Moe(_) => "Moe",
12591 FfnKind::DenseMoe(_) => "DenseMoe",
12592 };
12593 eprintln!("graph L{li}: attn={ak} ffn={fk}");
12594 }
12595 let gffn = match &lw.ffn {
12596 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
12600 FfnKind::Dense(d) => {
12601 let Some(act) = d.act.wgpu_graph_act() else {
12605 self.note_graph_decline(
12606 "wgpu token graph",
12607 "dense FFN activation without a graph kernel",
12608 );
12609 return None;
12610 };
12611 crate::gpu::GraphFfn::Dense {
12612 gate: gw(&d.gate_proj)?,
12613 up: gw(&d.up_proj)?,
12614 down: gw(&d.down_proj)?,
12615 act,
12616 }
12617 }
12618 FfnKind::Moe(m) => {
12619 if m.route_tau.is_some() || m.mask.is_some() {
12627 return None;
12628 }
12629 let shared = m.shared.as_ref();
12630 let inter = m.experts.first()?.gate_proj.rows();
12636 let detached_shared = match shared {
12644 Some((se, None))
12645 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12646 {
12647 if !matches!(se.act, Act::Silu)
12648 || se.gate_proj.mapped_q4tp().is_none()
12649 || se.up_proj.mapped_q4tp().is_none()
12650 || se.down_proj.mapped_q4tp().is_none()
12651 {
12652 return None;
12653 }
12654 Some(crate::gpu::GraphMoeDetachedShared {
12655 gate: gw(&se.gate_proj)?,
12656 up: gw(&se.up_proj)?,
12657 down: gw(&se.down_proj)?,
12658 width: se.gate_proj.rows(),
12659 })
12660 }
12661 Some((se, Some(_)))
12665 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
12666 {
12667 return None;
12668 }
12669 _ => None,
12670 };
12671 let has_shared = shared.is_some() && detached_shared.is_none();
12672 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
12673 let sgate = match shared {
12674 Some((_, Some(sg))) if has_shared => gw(sg)?,
12675 _ => gw(&m.router)?,
12679 };
12680 let router = gw(&m.router)?;
12681 if router.prism != crate::gpu::GraphPrismOp::None
12687 || sgate.prism != crate::gpu::GraphPrismOp::None
12688 || router.affine
12689 || sgate.affine
12690 {
12691 tracing::warn!(
12692 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
12693 );
12694 return None;
12695 }
12696 let mut experts = Vec::with_capacity(m.experts.len() + 1);
12697 let mut q4tp: Option<bool> = None;
12700 let mut gu_q2: Option<bool> = None;
12703 let mut q82: Option<bool> = None;
12705 let mut moe_act = None;
12709 for e in m
12710 .experts
12711 .iter()
12712 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
12713 {
12714 let Some(e_act) = e.act.wgpu_graph_act() else {
12715 return None;
12716 };
12717 match moe_act {
12718 Some(existing) if existing != e_act => return None,
12719 None => moe_act = Some(e_act),
12720 _ => {}
12721 }
12722 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
12723 return None;
12724 }
12725 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
12730 let Some((em, ei, _, _)) = expert_weight
12731 .graph_weight()
12732 .or_else(|| expert_weight.graph_weight_descriptor())
12733 else {
12734 return None;
12735 };
12736 let name = &em.tensors[ei].name;
12737 if crate::prism::is_forward_weight(em, name)
12738 || crate::prism::is_inverse_embedding(em, name)
12739 || crate::prism::is_affine_target(em, name)
12740 {
12741 tracing::warn!(
12742 "resident MoE declined: expert Prism/affine transform is not implemented"
12743 );
12744 return None;
12745 }
12746 }
12747 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
12748 Some((mm, gi)) => (
12749 mm,
12750 gi,
12751 e.up_proj.mapped_q4t()?.1,
12752 e.down_proj.mapped_q4t()?.1,
12753 false,
12754 false,
12755 false,
12756 ),
12757 None => match e.gate_proj.mapped_q2tp() {
12758 Some((mm, gi)) => (
12759 mm,
12760 gi,
12761 e.up_proj.mapped_q2tp()?.1,
12762 e.down_proj.mapped_q4tp()?.1,
12763 true,
12764 true,
12765 false,
12766 ),
12767 None => match e.gate_proj.mapped_q4tp() {
12768 Some((mm, gi)) => (
12769 mm,
12770 gi,
12771 e.up_proj.mapped_q4tp()?.1,
12772 e.down_proj.mapped_q4tp()?.1,
12773 true,
12774 false,
12775 false,
12776 ),
12777 None => {
12780 if !crate::gpu::moe_q82_graph_on() {
12781 return None;
12782 }
12783 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
12784 (
12785 mm,
12786 gi,
12787 e.up_proj.mapped_q8_2f()?.1,
12788 e.down_proj.mapped_q8_2f()?.1,
12789 false,
12790 false,
12791 true,
12792 )
12793 }
12794 },
12795 },
12796 };
12797 if *q4tp.get_or_insert(is_p) != is_p
12798 || *gu_q2.get_or_insert(is_q2) != is_q2
12799 || *q82.get_or_insert(is_8) != is_8
12800 {
12801 tracing::warn!(
12807 "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."
12808 );
12809 return None;
12810 }
12811 model.get_or_insert_with(|| mm.clone());
12812 experts.push((gi, ui, di));
12813 }
12814 crate::gpu::GraphFfn::Moe {
12815 router,
12816 shared_gate: sgate,
12817 experts,
12818 n_exp: m.experts.len(),
12819 top_k: std::env::var("CMF_TOPK_PROBE")
12825 .ok()
12826 .and_then(|v| v.parse::<usize>().ok())
12827 .filter(|k| *k > 0 && *k <= m.top_k)
12828 .unwrap_or(m.top_k),
12829 inter,
12830 act: moe_act?,
12831 norm_topk: m.norm_topk_prob,
12832 q4tp: q4tp?,
12833 gu_q2: gu_q2.unwrap_or(false),
12834 q82: q82.unwrap_or(false),
12835 sigmoid: m.router_sigmoid,
12836 sigmoid_denom_eps: m.sigmoid_denom_eps,
12837 bias: m.expert_bias.as_deref(),
12838 has_shared,
12839 detached_shared,
12840 shared_gated,
12841 route_scale: m.routed_scaling,
12842 }
12843 }
12844 };
12845 let attn = match &lw.attn {
12846 AttnKind::Full {
12847 wq,
12848 wk,
12849 wv,
12850 wo,
12851 q_norm,
12852 k_norm,
12853 output_gate,
12854 softplus_gate,
12855 bias,
12856 } => {
12857 if self.attention_heads_per_layer.is_some() {
12858 return None;
12859 }
12860 let head_gate = match softplus_gate {
12864 None => None,
12865 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
12866 Some(_) => {
12867 self.note_graph_decline(
12868 "wgpu token graph",
12869 "projected softplus / per-element output gate",
12870 );
12871 return None;
12872 }
12873 };
12874 let (m, _, _, _) = wq.graph_weight_or_blr()?;
12875 model = Some(m.clone());
12876 crate::gpu::GraphAttn::Full {
12877 wq: gw(wq)?,
12878 wk: gw(wk)?,
12879 wv: gw(wv)?,
12880 wo: gw(wo)?,
12881 q_norm: q_norm.as_deref(),
12882 k_norm: k_norm.as_deref(),
12883 late_qk_norm: self.qk_norm_after_rope,
12884 bias: bias
12885 .as_ref()
12886 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12887 output_gate: *output_gate,
12888 cpu_k: self.kv_cache.layers[li].k_heads(),
12889 cpu_v: self.kv_cache.layers[li].v_heads(),
12890 cpu_base: self.kv_cache.layers[li].base(),
12891 geom: self.graph_attn_geom(li),
12892 head_gate,
12893 }
12894 }
12895 AttnKind::LinearGdn(w) => {
12896 let cfg = self.gdn_cfg?;
12897 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
12898 model = Some(m.clone());
12899 crate::gpu::GraphAttn::Gdn {
12900 qkv: gw(&w.in_proj_qkv)?,
12901 z: gw(&w.in_proj_z)?,
12902 a: gw(&w.in_proj_a)?,
12903 b: gw(&w.in_proj_b)?,
12904 out: gw(&w.out_proj)?,
12905 conv1d: &w.conv1d,
12906 a_log: &w.a_log,
12907 dt_bias: &w.dt_bias,
12908 norm: &w.norm,
12909 nv: cfg.num_v_heads,
12910 nk: cfg.num_k_heads,
12911 dk: cfg.key_head_dim,
12912 dv: cfg.value_head_dim,
12913 kk: cfg.conv_kernel,
12914 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12915 }
12916 }
12917 AttnKind::ShortConv(w) => {
12918 let cfg = self.short_conv_cfg?;
12919 let (m, _, _, _) = w
12920 .in_proj
12921 .graph_weight()
12922 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12923 model = Some(m.clone());
12924 crate::gpu::GraphAttn::ShortConv {
12925 inp: gw(&w.in_proj)?,
12926 out: gw(&w.out_proj)?,
12927 taps: &w.conv,
12928 kernel: cfg.kernel,
12929 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12930 }
12931 }
12932 _ => return None,
12933 };
12934 layers.push(crate::gpu::GraphLayer {
12935 input_norm: &lw.input_norm,
12936 attn,
12937 post_norm: &lw.post_norm,
12938 attn_out_norm: lw.attn_out_norm.as_deref(),
12939 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12940 layer_scale: lw.layer_scale,
12941 ffn: gffn,
12942 });
12943 }
12944 let model = model?;
12945 let lm_gw = if upto_excl == self.num_layers
12951 && self.graph_want_logits
12952 && std::env::var("CMF_GPU_LMHEAD")
12953 .map(|v| v != "0")
12954 .unwrap_or(true)
12955 {
12956 self.weights
12957 .lm_head
12958 .graph_weight()
12959 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12960 .map(|(m, i, kind, rs)| {
12961 let name = &m.tensors[i].name;
12962 let prism = if crate::prism::is_inverse_embedding(m, name) {
12963 crate::gpu::GraphPrismOp::InverseEmbedding
12964 } else if crate::prism::is_forward_weight(m, name) {
12965 crate::gpu::GraphPrismOp::Forward
12966 } else {
12967 crate::gpu::GraphPrismOp::None
12968 };
12969 (
12970 crate::gpu::GraphW {
12971 idx: i,
12972 kind,
12973 row_scale: rs,
12974 data: &[],
12975 prism,
12976 affine: crate::prism::is_affine_target(m, name),
12977 },
12978 self.weights.lm_head.rows(),
12979 )
12980 })
12981 } else {
12982 None
12983 };
12984 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12985 let emb_gw = if steps > 1 {
12987 self.weights
12988 .embed_tokens
12989 .graph_weight()
12990 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12991 .map(|(m, i, kind, rs)| {
12992 let name = &m.tensors[i].name;
12993 let prism = if crate::prism::is_inverse_embedding(m, name) {
12994 crate::gpu::GraphPrismOp::InverseEmbedding
12995 } else if crate::prism::is_forward_weight(m, name) {
12996 crate::gpu::GraphPrismOp::Forward
12997 } else {
12998 crate::gpu::GraphPrismOp::None
12999 };
13000 (
13001 crate::gpu::GraphW {
13002 idx: i,
13003 kind,
13004 row_scale: rs,
13005 data: &[],
13006 prism,
13007 affine: crate::prism::is_affine_target(m, name),
13008 },
13009 self.weights.embed_tokens.rows(),
13010 self.embed_multiplier,
13011 )
13012 })
13013 } else {
13014 None
13015 };
13016
13017 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13023 (from..upto_excl.min(self.num_layers - 1))
13024 .filter(|&li| (li + 1) % self.physical_layers == 0)
13025 .map(|li| li - from)
13026 .collect()
13027 } else {
13028 Vec::new()
13029 };
13030 let mut h = hidden.to_vec();
13031 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13037 let outcome = crate::gpu::forward_token_graph(
13038 &model,
13039 self.graph_kv_id,
13040 &layers,
13041 &o1_views,
13042 self.o1_epoch,
13043 &self.inv_freq,
13044 &mut h,
13045 nh,
13046 nkv,
13047 hd,
13048 self.attn_scale,
13049 rd,
13050 self.hidden_size,
13051 self.intermediate_size,
13052 position,
13053 self.kv_cache.max_seq_len,
13054 gemma,
13055 self.rms_eps as f32,
13056 lm,
13057 &self.weights.final_norm,
13058 logits_out,
13059 &loop_norm_at,
13060 steps,
13061 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13062 ids_out,
13063 layers_run,
13064 from,
13065 dump_hidden,
13066 );
13067 match outcome {
13068 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13069 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13070 crate::gpu::TokenGraphOutcome::Declined => None,
13071 }
13072 }
13073
13074 #[cfg(target_os = "macos")]
13083 #[allow(clippy::type_complexity)]
13084 fn metal_rows_plan(
13085 &self,
13086 ) -> Option<(
13087 Vec<MetalRowsItem<'_>>,
13088 std::sync::Arc<cortiq_core::CmfModel>,
13089 Option<crate::gpu_metal::GdnGpuCfg>,
13090 )> {
13091 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13092 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13093 if !graph_force
13094 || !crate::gpu::enabled_here()
13095 || std::env::var("CMF_GPU_BLOCK")
13096 .map(|v| v == "0")
13097 .unwrap_or(false)
13098 || self.attn_softcap > 0.0
13099 || self.o1_active()
13100 || self.swa.is_some()
13101 || self.global_attn.is_some()
13102 || self.attention_heads_per_layer.is_some()
13103 || self.graph_attn_decline_reason().is_some()
13105 || self.attn_v_norm
13106 || self.loop_final_norm
13107 || !self.metal_rope_scale_ok()
13108 {
13109 return None;
13110 }
13111 let attend_contract = self.head_dim % 4 == 0
13112 && self.head_dim <= 256
13113 && self.rotary_dim >= 2
13114 && self.rotary_dim <= self.head_dim
13115 && (self.rotary_dim / 2) % 32 == 0
13116 && self.num_kv_heads > 0
13117 && self.num_heads % self.num_kv_heads == 0;
13118 if !attend_contract {
13119 return None;
13120 }
13121 let mut plan: Vec<MetalRowsItem> = Vec::new();
13122 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13123 for li in 0..self.num_layers {
13124 let lw = &self.weights.layers[self.phys_layer(li)];
13125 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13126 return None;
13127 }
13128 let ffn = match &lw.ffn {
13129 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13130 let (Some(g), Some(u), Some(dn)) = (
13131 d.gate_proj.metal_graph_parts(),
13132 d.up_proj.metal_graph_parts(),
13133 d.down_proj.metal_graph_parts(),
13134 ) else {
13135 return None;
13136 };
13137 MetalFfn::Dense {
13138 gate: g,
13139 up: u,
13140 down: dn,
13141 act: 0, }
13143 }
13144 _ => return None,
13145 };
13146 match &lw.attn {
13147 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13148 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13149 w.in_proj_qkv.metal_graph_parts(),
13150 w.in_proj_z.metal_graph_parts(),
13151 w.in_proj_a.f32_parts(),
13152 w.in_proj_b.f32_parts(),
13153 w.out_proj.metal_graph_parts(),
13154 ) else {
13155 return None;
13156 };
13157 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13158 model_ref.get_or_insert_with(|| model.clone());
13159 }
13160 let gl = GdnGpuLayer {
13161 attn_norm: &lw.input_norm,
13162 post_norm: &lw.post_norm,
13163 qkv,
13164 z,
13165 a,
13166 b: bb,
13167 out,
13168 ffn,
13169 conv1d: &w.conv1d,
13170 a_log: &w.a_log,
13171 dt_bias: &w.dt_bias,
13172 gnorm: &w.norm,
13173 };
13174 match plan.last_mut() {
13175 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13176 _ => plan.push(MetalRowsItem::Gdn {
13177 run: vec![gl],
13178 first: li,
13179 }),
13180 }
13181 }
13182 AttnKind::Full {
13183 wq,
13184 wk,
13185 wv,
13186 wo,
13187 q_norm,
13188 k_norm,
13189 output_gate,
13190 softplus_gate: None,
13191 bias: None,
13192 } => {
13193 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13194 wq.metal_graph_parts(),
13195 wk.metal_graph_parts(),
13196 wv.metal_graph_parts(),
13197 wo.metal_graph_parts(),
13198 ) else {
13199 return None;
13200 };
13201 if let QTensor::Mapped { model, .. } = wq {
13202 model_ref.get_or_insert_with(|| model.clone());
13203 }
13204 let cache = &self.kv_cache.layers[li];
13205 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13206 return None;
13207 }
13208 plan.push(MetalRowsItem::Attn {
13209 l: AttnGpuLayer {
13210 attn_norm: &lw.input_norm,
13211 post_norm: &lw.post_norm,
13212 wq: pq,
13213 wk: pk,
13214 wv: pv,
13215 wo: po,
13216 ffn,
13217 attn_out_norm: None,
13218 ffn_out_norm: None,
13219 layer_scale: None,
13220 },
13221 li,
13222 q_norm: q_norm.as_deref(),
13223 k_norm: k_norm.as_deref(),
13224 output_gate: *output_gate,
13225 });
13226 }
13227 _ => return None,
13228 }
13229 }
13230 let model = model_ref?;
13231 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13232 nv: cfg.num_v_heads,
13233 nk: cfg.num_k_heads,
13234 dk: cfg.key_head_dim,
13235 dv: cfg.value_head_dim,
13236 kk: cfg.conv_kernel,
13237 hidden: self.hidden_size,
13238 inter: self.intermediate_size,
13239 c_dim: cfg.conv_dim(),
13240 eps: cfg.rms_eps as f32,
13241 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13242 });
13243 Some((plan, model, gcfg))
13244 }
13245
13246 #[cfg(target_os = "macos")]
13248 #[allow(clippy::too_many_arguments)]
13249 fn metal_attn_params<'a>(
13250 li: usize,
13251 cache: &'a crate::kv_cache::LayerKvCache,
13252 q_norm: Option<&'a [f32]>,
13253 k_norm: Option<&'a [f32]>,
13254 output_gate: bool,
13255 inv_freq: &'a [f32],
13256 rope_scale: f32,
13257 geom: (usize, usize, usize, usize),
13258 pos0: usize,
13259 kv_id: u64,
13260 scale: f32,
13261 eps: f32,
13262 gemma: bool,
13263 late_qk_norm: bool,
13264 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13265 let (nh, nkv, hd, rd) = geom;
13266 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13267 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13268 let cpu_stored = cpu_k[0].len() / hd;
13269 (
13270 crate::gpu_metal::AttnDeviceParams {
13271 kv_id,
13272 layer: li,
13273 nh,
13274 nkv,
13275 hd,
13276 rd,
13277 position: pos0,
13278 scale,
13279 eps,
13280 gemma,
13281 late_qk_norm,
13282 output_gate,
13283 q_norm,
13284 k_norm,
13285 inv_freq,
13286 rope_scale,
13287 cpu_k,
13288 cpu_v,
13289 cpu_stored,
13290 cpu_gen: cache.generation(),
13291 o1: None,
13292 window: None,
13293 softmax_sink: cache.sinks.as_deref(),
13294 head_gate: None,
13295 v_norm: false,
13296 },
13297 cpu_stored,
13298 )
13299 }
13300
13301 #[cfg(target_os = "macos")]
13306 #[allow(clippy::type_complexity)]
13307 fn metal_rows_run(
13308 &mut self,
13309 hiddens: &mut [f32],
13310 pos0: usize,
13311 b: usize,
13312 prefill: bool,
13313 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13314 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13318 ) -> MetalRowsRun {
13319 use crate::gpu_metal::{GraphDims, VerifyGraph};
13320 if !crate::gpu_metal::wait_replay() {
13326 tracing::error!("Metal rows graph: the pending async replay failed");
13327 return MetalRowsRun::Failed;
13328 }
13329 spec_stamp("v.wait");
13330 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13336 if want > 0 {
13337 let phys = self.physical_layers.max(1);
13338 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13339 let is_gdn = self
13340 .weights
13341 .layers
13342 .get(li % phys)
13343 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13344 if is_gdn && l.linear_state.len() != want {
13345 l.linear_state = vec![0f32; want];
13346 }
13347 }
13348 }
13349 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13350 return MetalRowsRun::Declined;
13351 };
13352 spec_stamp("v.plan");
13353 let dims = GraphDims {
13354 hidden: self.hidden_size,
13355 eps: self.rms_eps as f32,
13356 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13357 };
13358 let Some(mut graph) = (if prefill {
13359 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13360 } else {
13361 VerifyGraph::new(&model, dims, hiddens, b)
13362 }) else {
13363 return MetalRowsRun::Declined;
13364 };
13365 let geom = (
13366 self.num_heads,
13367 self.num_kv_heads,
13368 self.head_dim,
13369 self.rotary_dim,
13370 );
13371 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13372 let eps = self.rms_eps as f32;
13373 let kv_id = self.graph_kv_id;
13374 let inv_freq = self.inv_freq.clone();
13375 for item in &plan {
13376 let ok = match item {
13377 MetalRowsItem::Gdn { run, .. } => gcfg
13378 .as_ref()
13379 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13380 .unwrap_or(false),
13381 MetalRowsItem::Attn {
13382 l,
13383 li,
13384 q_norm,
13385 k_norm,
13386 output_gate,
13387 } => {
13388 let (p, _) = Self::metal_attn_params(
13389 *li,
13390 &self.kv_cache.layers[*li],
13391 *q_norm,
13392 *k_norm,
13393 *output_gate,
13394 &inv_freq,
13395 self.layer_rope_scale(*li),
13396 geom,
13397 pos0,
13398 kv_id,
13399 self.attn_scale,
13400 eps,
13401 gemma,
13402 self.qk_norm_after_rope,
13403 );
13404 graph.attn_ok(l, &p)
13405 }
13406 };
13407 if !ok {
13408 use std::sync::atomic::{AtomicBool, Ordering};
13409 static SAID: AtomicBool = AtomicBool::new(false);
13410 if !SAID.swap(true, Ordering::Relaxed) {
13411 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13412 }
13413 return MetalRowsRun::Declined;
13414 }
13415 }
13416 let lm = match &spec {
13417 Some((lm, _, _)) => {
13418 if !graph.lm_head_ok(*lm) {
13419 return MetalRowsRun::Declined;
13420 }
13421 Some(*lm)
13422 }
13423 None => None,
13424 };
13425 let mut gdn_layers = Vec::new();
13426 let mut attn_layers = Vec::new();
13427 for item in &plan {
13428 match item {
13429 MetalRowsItem::Gdn { run, first } => {
13430 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
13431 .iter()
13432 .map(|l| l.linear_state.as_slice())
13433 .collect();
13434 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
13435 return MetalRowsRun::Declined;
13436 }
13437 gdn_layers.extend(*first..*first + run.len());
13438 }
13439 MetalRowsItem::Attn {
13440 l,
13441 li,
13442 q_norm,
13443 k_norm,
13444 output_gate,
13445 } => {
13446 let (p, cpu_stored) = Self::metal_attn_params(
13447 *li,
13448 &self.kv_cache.layers[*li],
13449 *q_norm,
13450 *k_norm,
13451 *output_gate,
13452 &inv_freq,
13453 self.layer_rope_scale(*li),
13454 geom,
13455 pos0,
13456 kv_id,
13457 self.attn_scale,
13458 eps,
13459 gemma,
13460 self.qk_norm_after_rope,
13461 );
13462 if !graph.encode_attn_b(l, &p) {
13463 return MetalRowsRun::Declined;
13464 }
13465 attn_layers.push((*li, cpu_stored));
13466 }
13467 }
13468 }
13469 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
13470 if !graph.encode_lm_head_b(final_norm, lm) {
13471 return MetalRowsRun::Declined;
13472 }
13473 if let Some((n, _)) = argmax_out.as_ref() {
13478 if !graph.encode_argmax_b(*n) {
13479 argmax_out = None;
13480 }
13481 }
13482 }
13483 spec_stamp("v.enc");
13484 if !graph.sync() {
13485 return MetalRowsRun::Failed;
13486 }
13487 spec_stamp("v.gpu");
13488 match (spec, argmax_out) {
13489 (Some(_), Some((_, ids))) => {
13490 ids.resize(b, 0);
13491 if !graph.read_argmax(ids) {
13492 return MetalRowsRun::Failed;
13493 }
13494 spec_stamp("v.am");
13495 }
13496 (Some((lm, _, logits)), None) => {
13497 logits.resize(b * lm.1, 0.0);
13498 if !graph.read_logits(logits) {
13499 return MetalRowsRun::Failed;
13500 }
13501 spec_stamp("v.lg");
13502 }
13503 (None, _) => {}
13504 }
13505 if !graph.read_hidden(hiddens) {
13506 return MetalRowsRun::Failed;
13507 }
13508 spec_stamp("v.hid");
13509 MetalRowsRun::Completed(MetalVerifyPending {
13510 graph,
13511 gdn_layers,
13512 attn_layers,
13513 })
13514 }
13515
13516 #[cfg(target_os = "macos")]
13522 fn try_batch_graph_metal(
13523 &mut self,
13524 hiddens: &mut [f32],
13525 positions: &[usize],
13526 b: usize,
13527 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13528 argmax_out: Option<(usize, &mut Vec<u32>)>,
13529 ) -> crate::gpu::BatchGraphOutcome {
13530 let _t0 = std::time::Instant::now();
13531 if positions.len() != b
13532 || positions.windows(2).any(|w| w[1] != w[0] + 1)
13533 || hiddens.len() != b * self.hidden_size
13534 {
13535 return crate::gpu::BatchGraphOutcome::Declined;
13536 }
13537 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
13538 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
13539 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
13540 MetalRowsRun::Completed(pending) => pending,
13541 };
13542 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13543 eprintln!(
13544 "metal-verify: {:.1} ms | b={b}",
13545 _t0.elapsed().as_secs_f64() * 1e3
13546 );
13547 }
13548 self.metal_verify = Some(pending);
13549 crate::gpu::BatchGraphOutcome::Completed
13550 }
13551
13552 #[cfg(target_os = "macos")]
13557 fn prefill_rows_metal(
13558 &mut self,
13559 ids: &[u32],
13560 start_pos: usize,
13561 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13562 ) -> MetalPrefillOutcome {
13563 let b = ids.len();
13564 if b == 0 || b > 512 {
13565 return MetalPrefillOutcome::Declined;
13566 }
13567 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13568 let with_head = spec.is_some();
13569 let hs = self.hidden_size;
13570 let mut hiddens = vec![0f32; b * hs];
13571 for (j, &id) in ids.iter().enumerate() {
13572 let e = self.embed_single(id);
13573 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
13574 }
13575 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
13576 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
13577 MetalRowsRun::Failed => {
13578 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13579 return MetalPrefillOutcome::Failed;
13580 }
13581 MetalRowsRun::Completed(pending) => pending,
13582 };
13583 let idxs = pending.gdn_layers.clone();
13585 let mut outs: Vec<&mut [f32]> = self
13586 .kv_cache
13587 .layers
13588 .iter_mut()
13589 .enumerate()
13590 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13591 .map(|(_, l)| l.linear_state.as_mut_slice())
13592 .collect();
13593 if !pending.graph.finish_states(&mut outs) {
13594 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13595 return MetalPrefillOutcome::Failed;
13596 }
13597 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13598 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13602 for (li, cpu_stored) in &pending.attn_layers {
13603 let mut kbuf = vec![0f32; b * nkv * hd];
13604 let mut vbuf = vec![0f32; b * nkv * hd];
13605 if !crate::gpu_metal::kv_mirror_read_rows(
13606 self.graph_kv_id,
13607 *li,
13608 nkv,
13609 hd,
13610 *cpu_stored,
13611 b,
13612 &mut kbuf,
13613 &mut vbuf,
13614 ) {
13615 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13616 return MetalPrefillOutcome::Failed;
13617 }
13618 rows.push((*li, *cpu_stored, kbuf, vbuf));
13619 }
13620 for (li, cpu_stored, kbuf, vbuf) in rows {
13621 let cache = &mut self.kv_cache.layers[li];
13622 for r in 0..b {
13623 cache.append(
13624 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13625 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13626 &[],
13627 );
13628 }
13629 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
13630 }
13631 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13632 if with_head {
13633 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
13634 }
13635 MetalPrefillOutcome::Completed(hiddens)
13636 }
13637
13638 #[cfg(target_os = "macos")]
13639 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
13640 self.prefill_rows_metal(ids, start_pos, None)
13641 }
13642
13643 #[cfg(target_os = "macos")]
13648 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
13649 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
13650 return MetalBatchNllOutcome::Declined;
13651 }
13652 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
13653 return MetalBatchNllOutcome::Declined;
13654 };
13655 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
13656 .ok()
13657 .and_then(|v| v.parse::<usize>().ok())
13658 .filter(|&v| (1..=512).contains(&v))
13659 .unwrap_or(32);
13660 let final_norm = self.weights.final_norm.clone();
13661 let mut nll = 0.0f64;
13662 let mut count = 0usize;
13663 let mut pos = 0usize;
13664 let mut completed = 0usize;
13665 while pos < ids.len() {
13666 let end = (pos + chunk).min(ids.len());
13667 let mut logits = Vec::new();
13668 let outcome =
13669 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
13670 match outcome {
13671 MetalPrefillOutcome::Declined => {
13672 return if completed == 0 {
13673 MetalBatchNllOutcome::Declined
13674 } else {
13675 MetalBatchNllOutcome::Failed(format!(
13676 "ordinary Metal NLL batch declined after {completed} chunks"
13677 ))
13678 };
13679 }
13680 MetalPrefillOutcome::Failed => {
13681 return MetalBatchNllOutcome::Failed(
13682 "ordinary Metal NLL batch failed after admission".to_string(),
13683 );
13684 }
13685 MetalPrefillOutcome::Completed(_) => {}
13686 }
13687 completed += 1;
13688 let vocab = self.vocab_size.min(lm.1);
13689 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
13690 return MetalBatchNllOutcome::Failed(
13691 "ordinary Metal NLL head returned an invalid shape".to_string(),
13692 );
13693 }
13694 for row in 0..(end - pos) {
13695 let absolute = pos + row;
13696 if absolute < start || absolute + 1 >= ids.len() {
13697 continue;
13698 }
13699 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
13700 if let Some(mu) = self.logit_multiplier {
13701 for v in lg.iter_mut() {
13702 *v *= mu;
13703 }
13704 }
13705 if let Some(c) = self.final_softcap {
13706 softcap_in_place(lg, c, self.pool.as_deref());
13707 }
13708 let target = ids[absolute + 1] as usize;
13709 if target >= vocab {
13710 return MetalBatchNllOutcome::Failed(format!(
13711 "target token {target} exceeds Metal head rows {vocab}"
13712 ));
13713 }
13714 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
13715 let lse: f64 = lg
13716 .iter()
13717 .map(|&v| ((v - max) as f64).exp())
13718 .sum::<f64>()
13719 .ln()
13720 + max as f64;
13721 nll += lse - lg[target] as f64;
13722 count += 1;
13723 }
13724 pos = end;
13725 }
13726 MetalBatchNllOutcome::Completed(nll, count)
13727 }
13728
13729 #[cfg(target_os = "macos")]
13733 fn metal_verify_commit(&mut self, a: usize) -> bool {
13734 let Some(mut pending) = self.metal_verify.take() else {
13735 return false;
13736 };
13737 let n = a + 1;
13738 let idxs = pending.gdn_layers.clone();
13740 let mut outs: Vec<&mut [f32]> = self
13741 .kv_cache
13742 .layers
13743 .iter_mut()
13744 .enumerate()
13745 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13746 .map(|(_, l)| l.linear_state.as_mut_slice())
13747 .collect();
13748 if !pending.graph.commit(n, &mut outs) {
13749 return false;
13750 }
13751 spec_stamp("c.replay");
13752 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13753 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13757 for (li, cpu_stored) in &pending.attn_layers {
13758 let mut kbuf = vec![0f32; n * nkv * hd];
13759 let mut vbuf = vec![0f32; n * nkv * hd];
13760 if !crate::gpu_metal::kv_mirror_read_rows(
13761 self.graph_kv_id,
13762 *li,
13763 nkv,
13764 hd,
13765 *cpu_stored,
13766 n,
13767 &mut kbuf,
13768 &mut vbuf,
13769 ) {
13770 return false;
13771 }
13772 rows.push((*li, *cpu_stored, kbuf, vbuf));
13773 }
13774 for (li, cpu_stored, kbuf, vbuf) in rows {
13775 let cache = &mut self.kv_cache.layers[li];
13776 for r in 0..n {
13777 cache.append(
13778 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13779 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13780 &[],
13781 );
13782 }
13783 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13784 }
13785 spec_stamp("c.kv");
13786 true
13787 }
13788
13789 #[cfg(target_os = "macos")]
13796 fn mtp_warm_batch_submit(
13797 &mut self,
13798 m: &mut MtpModule,
13799 pairs: &[(&[f32], u32)],
13800 first_pos: usize,
13801 ) -> Option<MetalWarmPending> {
13802 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13803 let b = pairs.len();
13804 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13805 return None;
13806 }
13807 let AttnKind::Full {
13808 wq,
13809 wk,
13810 wv,
13811 wo,
13812 q_norm,
13813 k_norm,
13814 output_gate,
13815 softplus_gate: None,
13816 bias: None,
13817 } = &m.layer.attn
13818 else {
13819 return None;
13820 };
13821 let FfnKind::Dense(d) = &m.layer.ffn else {
13822 return None;
13823 };
13824 if !d.segs.is_empty() {
13825 return None;
13826 }
13827 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13828 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13829 else {
13830 return None;
13831 };
13832 let (Some(g), Some(u), Some(dn)) = (
13833 d.gate_proj.q1_parts(),
13834 d.up_proj.q1_parts(),
13835 d.down_proj.q1_parts(),
13836 ) else {
13837 return None;
13838 };
13839 let Some(eh) = m.eh_proj.q1_parts() else {
13840 return None;
13841 };
13842 let QTensor::Mapped { model, .. } = wq else {
13843 return None;
13844 };
13845 let model = model.clone();
13846 let hs = self.hidden_size;
13847 let mut cat = vec![0f32; b * 2 * hs];
13849 for (j, (h, tok)) in pairs.iter().enumerate() {
13850 let e = self.embed_single(*tok);
13851 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13852 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13853 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13854 }
13855 let dims = GraphDims {
13856 hidden: hs,
13857 eps: self.rms_eps as f32,
13858 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13859 };
13860 spec_stamp("w.cat");
13861 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13862 return None;
13863 };
13864 spec_stamp("w.new");
13865 let l = AttnGpuLayer {
13866 attn_norm: &m.layer.input_norm,
13867 post_norm: &m.layer.post_norm,
13868 wq: pq,
13869 wk: pk,
13870 wv: pv,
13871 wo: po,
13872 ffn: MetalFfn::Dense {
13873 gate: g,
13874 up: u,
13875 down: dn,
13876 act: (d.act == Act::Gelu) as u8,
13877 },
13878 attn_out_norm: None,
13879 ffn_out_norm: None,
13880 layer_scale: None,
13881 };
13882 let (nh, nkv, hd, rd) = (
13883 self.num_heads,
13884 self.num_kv_heads,
13885 self.head_dim,
13886 self.rotary_dim,
13887 );
13888 let inv_freq = self.inv_freq.clone();
13889 let cpu_stored;
13890 {
13891 let cache = &m.kv;
13892 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13893 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13894 cpu_stored = cpu_k[0].len() / hd;
13895 if cpu_stored > first_pos {
13900 spec_stamp("w.decl");
13901 return None;
13902 }
13903 let p = AttnDeviceParams {
13904 kv_id: self.mtp_kv_id(),
13905 layer: Self::MTP_LAYER_BASE,
13906 nh,
13907 nkv,
13908 hd,
13909 rd,
13910 position: first_pos,
13911 scale: self.attn_scale,
13912 eps: self.rms_eps as f32,
13913 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13914 late_qk_norm: self.qk_norm_after_rope,
13915 output_gate: *output_gate,
13916 q_norm: q_norm.as_deref(),
13917 k_norm: k_norm.as_deref(),
13918 inv_freq: &inv_freq,
13919 rope_scale: self.rope_scale,
13920 cpu_k,
13921 cpu_v,
13922 cpu_stored,
13923 cpu_gen: cache.generation(),
13924 o1: None,
13925 window: None,
13926 softmax_sink: None,
13927 head_gate: None,
13928 v_norm: false,
13929 };
13930 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13931 return None;
13932 }
13933 }
13934 spec_stamp("w.enc");
13935 if !graph.submit() {
13936 return None;
13937 }
13938 spec_stamp("w.sub");
13939 Some(MetalWarmPending {
13940 graph,
13941 cpu_stored,
13942 b,
13943 })
13944 }
13945
13946 #[cfg(target_os = "macos")]
13949 fn mtp_warm_batch_metal(
13950 &mut self,
13951 m: &mut MtpModule,
13952 pairs: &[(&[f32], u32)],
13953 first_pos: usize,
13954 ) -> bool {
13955 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13956 Some(p) => self.mtp_warm_batch_finish(m, p),
13957 None => false,
13958 }
13959 }
13960
13961 #[cfg(target_os = "macos")]
13966 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13967 let MetalWarmPending {
13968 mut graph,
13969 cpu_stored,
13970 b,
13971 } = pending;
13972 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13973 if !graph.sync() {
13974 return false;
13975 }
13976 spec_stamp("w.gpu");
13977 let mut kbuf = vec![0f32; b * nkv * hd];
13978 let mut vbuf = vec![0f32; b * nkv * hd];
13979 if !crate::gpu_metal::kv_mirror_read_rows(
13980 self.mtp_kv_id(),
13981 Self::MTP_LAYER_BASE,
13982 nkv,
13983 hd,
13984 cpu_stored,
13985 b,
13986 &mut kbuf,
13987 &mut vbuf,
13988 ) {
13989 return false;
13990 }
13991 for r in 0..b {
13992 m.kv.append(
13993 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13994 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13995 &[],
13996 );
13997 }
13998 crate::gpu_metal::kv_mirror_set_stored(
13999 self.mtp_kv_id(),
14000 Self::MTP_LAYER_BASE,
14001 cpu_stored + b,
14002 );
14003 spec_stamp("w.kv");
14004 true
14005 }
14006
14007 pub(crate) fn note_draft_id(&mut self, id: u32) {
14014 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14015 if (id as usize) >= cut {
14016 self.draft_full_streak = 16;
14017 } else {
14018 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14019 }
14020 }
14021
14022 fn draft_head_rows(&self, head_rows: usize) -> usize {
14025 if self.draft_full_streak > 0 {
14026 head_rows
14027 } else {
14028 Self::draft_vocab_rows(head_rows)
14029 }
14030 }
14031
14032 fn draft_vocab_rows(head_rows: usize) -> usize {
14035 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14036 let n = *N.get_or_init(|| {
14037 std::env::var("CMF_DRAFT_VOCAB")
14038 .ok()
14039 .and_then(|v| v.parse().ok())
14040 .unwrap_or(65536)
14041 });
14042 if n == 0 { head_rows } else { n.min(head_rows) }
14043 }
14044
14045 #[cfg(target_os = "macos")]
14050 fn mtp_step_metal(
14051 &mut self,
14052 m: &mut MtpModule,
14053 hidden: &[f32],
14054 next_token: u32,
14055 position: usize,
14056 want_logits: bool,
14057 ) -> Option<(Vec<f32>, Vec<f32>)> {
14058 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14059 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14060 || !crate::gpu::q1_force()
14061 || !crate::gpu::enabled_here()
14062 || self.attn_softcap > 0.0
14063 || self.attention_heads_per_layer.is_some()
14064 || m.kv.mode != crate::kv_cache::KvMode::F32
14065 || m.kv.o1.is_some()
14066 {
14067 return None;
14068 }
14069 let AttnKind::Full {
14070 wq,
14071 wk,
14072 wv,
14073 wo,
14074 q_norm,
14075 k_norm,
14076 output_gate,
14077 softplus_gate: None,
14078 bias: None,
14079 } = &m.layer.attn
14080 else {
14081 return None;
14082 };
14083 let FfnKind::Dense(d) = &m.layer.ffn else {
14084 return None;
14085 };
14086 if d.act != Act::Silu || !d.segs.is_empty() {
14087 return None;
14088 }
14089 let (pq, pk, pv, po) = (
14090 wq.q1_parts()?,
14091 wk.q1_parts()?,
14092 wv.q1_parts()?,
14093 wo.q1_parts()?,
14094 );
14095 let (g, u, dn) = (
14096 d.gate_proj.q1_parts()?,
14097 d.up_proj.q1_parts()?,
14098 d.down_proj.q1_parts()?,
14099 );
14100 let QTensor::Mapped { model, .. } = wq else {
14101 return None;
14102 };
14103 let model = model.clone();
14104 let lm = if want_logits {
14105 Some(self.weights.lm_head.q1_parts()?)
14106 } else {
14107 None
14108 };
14109 let dims = GraphDims {
14110 hidden: self.hidden_size,
14111 eps: self.rms_eps as f32,
14112 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14113 };
14114 let hs = self.hidden_size;
14117 let mut x = vec![0f32; hs];
14118 let mut graph = TokenGraph::new(&model, dims, &x)?;
14119 let mut folded = false;
14120 if let Some(eh) = m.eh_proj.q1_parts() {
14121 let e = self.embed_single(next_token);
14122 let mut cat = vec![0.0f32; 2 * hs];
14123 let (cat_e, cat_h) = cat.split_at_mut(hs);
14124 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14125 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14126 folded = graph.encode_input_proj(eh, &cat);
14127 }
14128 if !folded {
14129 x = self.mtp_block_input(m, hidden, next_token);
14130 graph = TokenGraph::new(&model, dims, &x)?;
14131 }
14132 spec_stamp("d.in");
14133 let l = AttnGpuLayer {
14134 attn_norm: &m.layer.input_norm,
14135 post_norm: &m.layer.post_norm,
14136 wq: pq,
14137 wk: pk,
14138 wv: pv,
14139 wo: po,
14140 ffn: MetalFfn::Dense {
14141 gate: g,
14142 up: u,
14143 down: dn,
14144 act: (d.act == Act::Gelu) as u8,
14145 },
14146 attn_out_norm: None,
14147 ffn_out_norm: None,
14148 layer_scale: None,
14149 };
14150 let (nh, nkv, hd, rd) = (
14151 self.num_heads,
14152 self.num_kv_heads,
14153 self.head_dim,
14154 self.rotary_dim,
14155 );
14156 let inv_freq = self.inv_freq.clone();
14157 {
14158 let cache = &m.kv;
14159 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14160 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14161 let cpu_stored = cpu_k[0].len() / hd;
14162 let p = AttnDeviceParams {
14163 kv_id: self.mtp_kv_id(),
14164 layer: Self::MTP_LAYER_BASE,
14165 nh,
14166 nkv,
14167 hd,
14168 rd,
14169 position,
14170 scale: self.attn_scale,
14171 eps: self.rms_eps as f32,
14172 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14173 late_qk_norm: self.qk_norm_after_rope,
14174 output_gate: *output_gate,
14175 q_norm: q_norm.as_deref(),
14176 k_norm: k_norm.as_deref(),
14177 inv_freq: &inv_freq,
14178 rope_scale: self.rope_scale,
14179 cpu_k,
14180 cpu_v,
14181 cpu_stored,
14182 cpu_gen: cache.generation(),
14183 o1: None,
14184 window: None,
14185 softmax_sink: None,
14186 head_gate: None,
14187 v_norm: false,
14188 };
14189 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14190 return None;
14191 }
14192 }
14193 let draft_rows = if let Some(lm) = lm {
14199 self.draft_head_rows(lm.1)
14200 } else {
14201 0
14202 };
14203 if let Some(lm) = lm {
14204 if !graph.lm_head_ok(lm) {
14205 return None;
14206 }
14207 if draft_rows < lm.1 {
14208 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14209 return None;
14210 }
14211 } else {
14212 graph.encode_lm_head(&m.final_norm, lm);
14213 }
14214 }
14215 spec_stamp("d.enc");
14216 if graph.sync_checked().is_err() {
14217 return None;
14218 }
14219 spec_stamp("d.gpu");
14220 let mut logits = Vec::new();
14221 if let Some(lm) = lm {
14222 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14223 logits = attention::take_buf(n_read);
14224 graph.read_logits(&mut logits);
14225 logits.resize(self.vocab_size, f32::NEG_INFINITY);
14227 }
14228 graph.finish(&mut x);
14229 let mut krow = attention::take_buf(nkv * hd);
14230 let mut vrow = attention::take_buf(nkv * hd);
14231 if crate::gpu_metal::kv_mirror_read_last(
14232 self.mtp_kv_id(),
14233 Self::MTP_LAYER_BASE,
14234 nkv,
14235 hd,
14236 &mut krow,
14237 &mut vrow,
14238 ) {
14239 m.kv.append(&krow, &vrow, &[]);
14240 }
14241 attention::recycle_buf(&mut krow);
14242 attention::recycle_buf(&mut vrow);
14243 spec_stamp("d.rd");
14244 Some((logits, x))
14245 }
14246
14247 fn mtp_chain_on() -> bool {
14260 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14261 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14262 }
14263
14264 #[cfg(target_os = "macos")]
14276 fn mtp_draft_chain_metal(
14277 &mut self,
14278 m: &mut MtpModule,
14279 hidden: &[f32],
14280 t_next: u32,
14281 position: usize,
14282 k: usize,
14283 ) -> Result<Vec<u32>, bool> {
14284 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14285 if k == 0
14286 || k > 64
14287 || !Self::mtp_chain_on()
14288 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14289 || !crate::gpu::q1_force()
14290 || !crate::gpu::enabled_here()
14291 || self.attn_softcap > 0.0
14292 || self.attention_heads_per_layer.is_some()
14293 || m.kv.mode != crate::kv_cache::KvMode::F32
14294 || m.kv.o1.is_some()
14295 || self.dsv4.is_some()
14297 || self.dsv41.is_some()
14298 || self.qwen4_exp.is_some()
14299 || self.per_token_stack()
14300 {
14301 return Err(false);
14302 }
14303 let AttnKind::Full {
14304 wq,
14305 wk,
14306 wv,
14307 wo,
14308 q_norm,
14309 k_norm,
14310 output_gate,
14311 softplus_gate: None,
14312 bias: None,
14313 } = &m.layer.attn
14314 else {
14315 return Err(false);
14316 };
14317 let FfnKind::Dense(d) = &m.layer.ffn else {
14318 return Err(false);
14319 };
14320 if d.act != Act::Silu || !d.segs.is_empty() {
14321 return Err(false);
14322 }
14323 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14324 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14325 else {
14326 return Err(false);
14327 };
14328 let (Some(g), Some(u), Some(dn)) = (
14329 d.gate_proj.q1_parts(),
14330 d.up_proj.q1_parts(),
14331 d.down_proj.q1_parts(),
14332 ) else {
14333 return Err(false);
14334 };
14335 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14336 return Err(false);
14337 };
14338 let QTensor::Mapped { model, .. } = wq else {
14339 return Err(false);
14340 };
14341 let model = model.clone();
14342 let QTensor::Mapped {
14345 model: em,
14346 idx: eidx,
14347 dtype: cortiq_core::TensorDtype::Q4TiledP,
14348 ..
14349 } = &self.weights.embed_tokens
14350 else {
14351 return Err(false);
14352 };
14353 if !std::sync::Arc::ptr_eq(em, &model)
14354 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14355 {
14356 return Err(false);
14357 }
14358 let embed = (
14359 *eidx,
14360 self.weights.embed_tokens.rows(),
14361 self.weights.embed_tokens.cols(),
14362 );
14363 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14364 return Err(false);
14365 }
14366 let dims = GraphDims {
14367 hidden: self.hidden_size,
14368 eps: self.rms_eps as f32,
14369 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14370 };
14371 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14372 return Err(false);
14373 };
14374 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14375 return Err(false);
14376 }
14377 let l = AttnGpuLayer {
14378 attn_norm: &m.layer.input_norm,
14379 post_norm: &m.layer.post_norm,
14380 wq: pq,
14381 wk: pk,
14382 wv: pv,
14383 wo: po,
14384 ffn: MetalFfn::Dense {
14385 gate: g,
14386 up: u,
14387 down: dn,
14388 act: (d.act == Act::Gelu) as u8,
14389 },
14390 attn_out_norm: None,
14391 ffn_out_norm: None,
14392 layer_scale: None,
14393 };
14394 let (nh, nkv, hd, rd) = (
14395 self.num_heads,
14396 self.num_kv_heads,
14397 self.head_dim,
14398 self.rotary_dim,
14399 );
14400 let inv_freq = self.inv_freq.clone();
14401 let draft_rows = self.draft_head_rows(lm.1);
14402 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
14403 if n_arg == 0 {
14404 return Err(false);
14405 }
14406 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
14413 let t_chain = std::time::Instant::now();
14414 graph.chain_ids_init(t_next, k);
14415 let cpu_stored;
14416 {
14417 let cache = &m.kv;
14418 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14419 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14420 cpu_stored = cpu_k[0].len() / hd;
14421 for j in 0..k {
14422 if !graph.encode_chain_input(
14423 embed,
14424 j as u32,
14425 &m.enorm,
14426 &m.hnorm,
14427 self.embed_multiplier,
14428 eh,
14429 ) {
14430 return Err(false);
14431 }
14432 let p = AttnDeviceParams {
14436 kv_id: self.mtp_kv_id(),
14437 layer: Self::MTP_LAYER_BASE,
14438 nh,
14439 nkv,
14440 hd,
14441 rd,
14442 position: position + j,
14443 scale: self.attn_scale,
14444 eps: self.rms_eps as f32,
14445 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14446 late_qk_norm: self.qk_norm_after_rope,
14447 output_gate: *output_gate,
14448 q_norm: q_norm.as_deref(),
14449 k_norm: k_norm.as_deref(),
14450 inv_freq: &inv_freq,
14451 rope_scale: self.rope_scale,
14452 cpu_k: cpu_k.clone(),
14453 cpu_v: cpu_v.clone(),
14454 cpu_stored: cpu_stored + j,
14455 cpu_gen: cache.generation(),
14456 o1: None,
14457 window: None,
14458 softmax_sink: None,
14459 head_gate: None,
14460 v_norm: false,
14461 };
14462 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14463 return Err(false);
14464 }
14465 if draft_rows < lm.1 {
14466 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14467 return Err(false);
14468 }
14469 } else {
14470 graph.encode_lm_head(&m.final_norm, lm);
14471 }
14472 if !graph.encode_argmax(n_arg, j as u32 + 1) {
14473 return Err(false);
14474 }
14475 if split {
14476 graph.commit();
14479 }
14480 }
14481 }
14482 let t_enc = t_chain.elapsed();
14483 if graph.sync_checked().is_err() {
14484 return Err(true);
14485 }
14486 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14487 eprintln!(
14488 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14489 t_enc.as_secs_f64() * 1e3,
14490 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14491 if split { ", split" } else { "" }
14492 );
14493 }
14494 let mut ids = vec![0u32; k];
14495 if !graph.chain_ids_read(&mut ids) {
14496 return Err(true);
14497 }
14498 let mut kbuf = vec![0f32; k * nkv * hd];
14499 let mut vbuf = vec![0f32; k * nkv * hd];
14500 if !crate::gpu_metal::kv_mirror_read_rows(
14501 self.mtp_kv_id(),
14502 Self::MTP_LAYER_BASE,
14503 nkv,
14504 hd,
14505 cpu_stored,
14506 k,
14507 &mut kbuf,
14508 &mut vbuf,
14509 ) {
14510 return Err(true);
14511 }
14512 for r in 0..k {
14513 m.kv.append(
14514 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14515 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14516 &[],
14517 );
14518 }
14519 Ok(ids)
14520 }
14521
14522 fn try_batch_graph_wgpu(
14523 &self,
14524 hiddens: &mut [f32],
14525 positions: &[usize],
14526 k: usize,
14527 spec: Option<crate::gpu::SpecTail<'_>>,
14528 ) -> crate::gpu::BatchGraphOutcome {
14529 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
14530 }
14531
14532 fn try_batch_graph_wgpu_prefix(
14537 &self,
14538 hiddens: &mut [f32],
14539 positions: &[usize],
14540 k: usize,
14541 spec: Option<crate::gpu::SpecTail<'_>>,
14542 layers_run: Option<&mut usize>,
14543 ) -> crate::gpu::BatchGraphOutcome {
14544 let graph_end = match self.mimo_moe.graph_prefix_end() {
14545 Some(end) if end < self.num_layers => {
14546 if layers_run.is_none() || spec.is_some() || end == 0 {
14547 return crate::gpu::BatchGraphOutcome::Declined;
14548 }
14549 end
14550 }
14551 _ => self.num_layers,
14552 };
14553 let _tb = std::time::Instant::now();
14554 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
14555 if self.attn_softcap > 0.0 {
14556 return crate::gpu::BatchGraphOutcome::Declined; }
14558 if let Some(reason) = self
14561 .wgpu_graph_attn_decline()
14562 .or_else(|| self.batch_graph_gemma_decline())
14563 {
14564 self.note_graph_decline("wgpu batch graph", reason);
14565 return crate::gpu::BatchGraphOutcome::Declined;
14566 }
14567 let nh = self.num_heads;
14568 let (nkv, hd, rd) = self.layer_geom(0);
14569 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14570 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14571 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
14572 {
14573 let name = &m.tensors[i].name;
14574 let prism = if crate::prism::is_inverse_embedding(m, name) {
14575 crate::gpu::GraphPrismOp::InverseEmbedding
14576 } else if crate::prism::is_forward_weight(m, name) {
14577 crate::gpu::GraphPrismOp::Forward
14578 } else {
14579 crate::gpu::GraphPrismOp::None
14580 };
14581 return Some(crate::gpu::GraphW {
14582 idx: i,
14583 kind,
14584 row_scale: rs,
14585 data: &[],
14586 prism,
14587 affine: crate::prism::is_affine_target(m, name),
14588 });
14589 }
14590 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14591 eprintln!(
14592 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
14593 t.rows(),
14594 t.cols()
14595 );
14596 }
14597 t.as_f32().map(|d| crate::gpu::GraphW {
14598 idx: 0,
14599 kind: 4,
14600 row_scale: &[],
14601 data: d,
14602 prism: crate::gpu::GraphPrismOp::None,
14603 affine: false,
14604 })
14605 }
14606 let built: Option<(
14607 Vec<crate::gpu::GraphLayer<'_>>,
14608 std::sync::Arc<cortiq_core::CmfModel>,
14609 )> = (|| {
14610 let mut layers = Vec::with_capacity(graph_end);
14611 let mut model = None;
14612 for li in 0..graph_end {
14613 let lw = &self.weights.layers[self.phys_layer(li)];
14614 let gffn = match &lw.ffn {
14621 FfnKind::Dense(d) if !d.segs.is_empty() => {
14622 if batch_debug {
14623 eprintln!("batch graph: dense segmented FFN at layer {li}");
14624 }
14625 return None;
14626 }
14627 FfnKind::Dense(d) => {
14628 let Some(act) = d.act.wgpu_graph_act() else {
14629 if batch_debug {
14630 eprintln!(
14631 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
14632 d.act
14633 );
14634 }
14635 return None;
14636 };
14637 crate::gpu::GraphFfn::Dense {
14638 gate: gw(&d.gate_proj)?,
14639 up: gw(&d.up_proj)?,
14640 down: gw(&d.down_proj)?,
14641 act,
14642 }
14643 }
14644 FfnKind::Moe(m) => {
14645 if m.route_tau.is_some() || m.mask.is_some() {
14652 return None;
14653 }
14654 let shared = m.shared.as_ref();
14658 let has_shared = shared.is_some();
14659 let shared_gated = matches!(shared, Some((_, Some(_))));
14660 let sgate = match shared {
14661 Some((_, Some(sg))) => gw(sg)?,
14662 _ => gw(&m.router)?,
14666 };
14667 let router = gw(&m.router)?;
14668 if router.prism != crate::gpu::GraphPrismOp::None
14674 || router.affine
14675 || sgate.prism != crate::gpu::GraphPrismOp::None
14676 || sgate.affine
14677 {
14678 return None;
14679 }
14680 let inter = m.experts.first()?.gate_proj.rows();
14681 let mut experts = Vec::with_capacity(m.experts.len() + 1);
14682 let mut q4tp: Option<bool> = None;
14683 let mut gu_q2: Option<bool> = None;
14684 let mut q82: Option<bool> = None;
14685 let mut moe_act = None;
14689 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
14690 let Some(e_act) = e.act.wgpu_graph_act() else {
14691 return None;
14692 };
14693 match moe_act {
14694 Some(existing) if existing != e_act => return None,
14695 None => moe_act = Some(e_act),
14696 _ => {}
14697 }
14698 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14699 return None;
14700 }
14701 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
14705 {
14706 Some((mm, gi)) => (
14707 mm,
14708 gi,
14709 e.up_proj.mapped_q4t()?.1,
14710 e.down_proj.mapped_q4t()?.1,
14711 false,
14712 false,
14713 false,
14714 ),
14715 None => match e.gate_proj.mapped_q2tp() {
14716 Some((mm, gi)) => (
14717 mm,
14718 gi,
14719 e.up_proj.mapped_q2tp()?.1,
14720 e.down_proj.mapped_q4tp()?.1,
14721 true,
14722 true,
14723 false,
14724 ),
14725 None => match e.gate_proj.mapped_q4tp() {
14726 Some((mm, gi)) => (
14727 mm,
14728 gi,
14729 e.up_proj.mapped_q4tp()?.1,
14730 e.down_proj.mapped_q4tp()?.1,
14731 true,
14732 false,
14733 false,
14734 ),
14735 None => {
14736 if !crate::gpu::moe_q82_graph_on() {
14737 return None;
14738 }
14739 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14740 (
14741 mm,
14742 gi,
14743 e.up_proj.mapped_q8_2f()?.1,
14744 e.down_proj.mapped_q8_2f()?.1,
14745 false,
14746 false,
14747 true,
14748 )
14749 }
14750 },
14751 },
14752 };
14753 if *q4tp.get_or_insert(is_p) != is_p
14754 || *gu_q2.get_or_insert(is_q2) != is_q2
14755 || *q82.get_or_insert(is_8) != is_8
14756 {
14757 return None;
14758 }
14759 if [gi, ui, di].into_iter().any(|idx| {
14760 mm.tensors.get(idx).is_some_and(|t| {
14761 crate::prism::is_forward_weight(mm, &t.name)
14762 || crate::prism::is_affine_target(mm, &t.name)
14763 })
14764 }) {
14765 return None;
14766 }
14767 model.get_or_insert_with(|| mm.clone());
14768 experts.push((gi, ui, di));
14769 }
14770 crate::gpu::GraphFfn::Moe {
14771 router,
14772 shared_gate: sgate,
14773 experts,
14774 n_exp: m.experts.len(),
14775 top_k: m.top_k,
14776 inter,
14777 act: moe_act?,
14778 norm_topk: m.norm_topk_prob,
14779 q4tp: q4tp?,
14780 gu_q2: gu_q2.unwrap_or(false),
14781 q82: q82.unwrap_or(false),
14782 sigmoid: m.router_sigmoid,
14783 sigmoid_denom_eps: m.sigmoid_denom_eps,
14784 bias: m.expert_bias.as_deref(),
14785 has_shared,
14786 detached_shared: None,
14787 shared_gated,
14788 route_scale: m.routed_scaling,
14789 }
14790 }
14791 _ => return None,
14792 };
14793 let attn = match &lw.attn {
14794 AttnKind::Full {
14795 wq,
14796 wk,
14797 wv,
14798 wo,
14799 q_norm,
14800 k_norm,
14801 output_gate,
14802 softplus_gate,
14803 bias,
14804 } => {
14805 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14806 if batch_debug {
14807 eprintln!(
14808 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14809 softplus_gate.is_some(),
14810 self.attention_heads_per_layer.is_some()
14811 );
14812 }
14813 return None;
14814 }
14815 let (m, _, _, _) =
14816 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
14817 model = Some(m.clone());
14818 crate::gpu::GraphAttn::Full {
14819 wq: gw(wq)?,
14820 wk: gw(wk)?,
14821 wv: gw(wv)?,
14822 wo: gw(wo)?,
14823 q_norm: q_norm.as_deref(),
14824 k_norm: k_norm.as_deref(),
14825 late_qk_norm: self.qk_norm_after_rope,
14826 bias: bias
14827 .as_ref()
14828 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14829 output_gate: *output_gate,
14830 cpu_k: self.kv_cache.layers[li].k_heads(),
14831 cpu_v: self.kv_cache.layers[li].v_heads(),
14832 cpu_base: self.kv_cache.layers[li].base(),
14833 geom: self.graph_attn_geom(li),
14834 head_gate: None,
14837 }
14838 }
14839 AttnKind::LinearGdn(w) => {
14840 let Some(cfg) = self.gdn_cfg else {
14841 if batch_debug {
14842 eprintln!("batch graph: no GDN config at layer {li}");
14843 }
14844 return None;
14845 };
14846 let (m, _, _, _) = w
14847 .in_proj_qkv
14848 .graph_weight()
14849 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14850 model = Some(m.clone());
14851 crate::gpu::GraphAttn::Gdn {
14852 qkv: gw(&w.in_proj_qkv)?,
14853 z: gw(&w.in_proj_z)?,
14854 a: gw(&w.in_proj_a)?,
14855 b: gw(&w.in_proj_b)?,
14856 out: gw(&w.out_proj)?,
14857 conv1d: &w.conv1d,
14858 a_log: &w.a_log,
14859 dt_bias: &w.dt_bias,
14860 norm: &w.norm,
14861 nv: cfg.num_v_heads,
14862 nk: cfg.num_k_heads,
14863 dk: cfg.key_head_dim,
14864 dv: cfg.value_head_dim,
14865 kk: cfg.conv_kernel,
14866 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14867 }
14868 }
14869 _ => return None,
14870 };
14871 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14874 layers.push(crate::gpu::GraphLayer {
14875 input_norm: &lw.input_norm,
14876 attn,
14877 post_norm: &lw.post_norm,
14878 attn_out_norm: if g4 {
14879 lw.attn_out_norm.as_deref()
14880 } else {
14881 None
14882 },
14883 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14884 layer_scale: if g4 { lw.layer_scale } else { None },
14885 ffn: gffn,
14886 });
14887 }
14888 Some((layers, model?))
14889 })();
14890 let Some((layers, model)) = built else {
14891 {
14892 use std::sync::atomic::{AtomicBool, Ordering};
14893 static SAID: AtomicBool = AtomicBool::new(false);
14894 if !SAID.swap(true, Ordering::Relaxed) {
14895 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14896 }
14897 }
14898 return crate::gpu::BatchGraphOutcome::Declined;
14899 };
14900 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14901 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14902 }
14903 crate::gpu::forward_batch_graph(
14904 &model,
14905 self.graph_kv_id,
14906 &layers,
14907 &self.inv_freq,
14908 hiddens,
14909 nh,
14910 nkv,
14911 hd,
14912 rd,
14913 self.hidden_size,
14914 self.intermediate_size,
14915 positions,
14916 self.kv_cache.max_seq_len,
14917 gemma,
14918 self.rms_eps as f32,
14919 self.attn_scale,
14920 k,
14921 &(0..graph_end)
14922 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14923 .collect::<Vec<_>>(),
14924 self.o1_epoch,
14925 spec,
14926 layers_run,
14927 )
14928 }
14929
14930 fn draft_probe() -> bool {
14934 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14935 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14936 }
14937
14938 #[cfg(feature = "gpu")]
14950 fn dsv4_spec_on() -> bool {
14951 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14952 *ON.get_or_init(|| {
14953 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14957 return v != "0";
14958 }
14959 std::env::var("CMF_DSV4_SPEC")
14966 .map(|v| v != "0")
14967 .unwrap_or_else(|_| {
14968 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14969 })
14970 })
14971 }
14972
14973 #[cfg(feature = "gpu")]
14980 fn dsv4_spec_step(
14981 &mut self,
14982 tip_token: u32,
14983 t_next: u32,
14984 next_pos: usize,
14985 max_extra: usize,
14986 drafted: &mut usize,
14987 accepted_ctr: &mut usize,
14988 ) -> Option<(Vec<u32>, usize)> {
14989 let t_all = std::time::Instant::now();
14990 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14991 thread_local! {
14992 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14993 const { std::cell::Cell::new(None) };
14994 }
14995 LAST.with(|l| {
14996 if let Some(prev) = l.get() {
14997 eprintln!(
14998 "между раундами {:.1} мс",
14999 prev.elapsed().as_secs_f64() * 1e3
15000 );
15001 }
15002 l.set(Some(std::time::Instant::now()));
15003 });
15004 }
15005 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15006 eprintln!("spec_step: вход pos={next_pos}");
15007 }
15008 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15009 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15010 if self.dspark.is_none() {
15012 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15013 if t.is_empty() {
15014 return None;
15015 }
15016 crate::dsv4::dspark_arm(&t, cfg.dim);
15017 self.dspark = Some(crate::dsv4::DsparkState::new(
15018 self.dsv4_mtp.len(),
15019 &cfg,
15020 t.len(),
15021 ));
15022 }
15023 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15024 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15025 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15026 eprintln!("spec_step: пак не построился (targets {targets:?})");
15027 }
15028 let pack = pack?;
15029 let block = crate::dsv4::dspark_block();
15030 let b_box = self.dsv4.as_mut()?;
15031 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15032 let ds = self.dspark.as_mut()?;
15033 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15036 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15037 if dbg {
15038 eprintln!("spec_step: нет захвата");
15039 }
15040 return None;
15041 }
15042 ds.have_hidden = true;
15043 let tip_pos = next_pos.checked_sub(1)?;
15044 let draft_started = std::time::Instant::now();
15045 let mut conf = Vec::new();
15046 let props = crate::dsv4::dspark_draft_gpu(
15047 g,
15048 &self.dsv4_mtp,
15049 &cfg,
15050 ds,
15051 pack,
15052 st.kv_id,
15053 tip_token,
15054 tip_pos,
15055 self.pool.as_deref(),
15056 &mut conf,
15057 );
15058 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15059 *drafted += block;
15060 if props.is_empty() || props[0] != t_next {
15061 if dbg {
15062 eprintln!(
15063 "spec_step: черновик {} (props0={:?} t_next={t_next})",
15064 if props.is_empty() {
15065 "пуст"
15066 } else {
15067 "мимо"
15068 },
15069 props.first()
15070 );
15071 }
15072 return None;
15073 }
15074 let mut k_verify = crate::dsv4::dspark_verify_k()
15081 .min(props.len())
15082 .min(max_extra.saturating_add(1));
15083 let conf_min = {
15089 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15090 *M.get_or_init(|| {
15091 std::env::var("CMF_DSPARK_CONF_MIN")
15092 .ok()
15093 .and_then(|v| v.parse().ok())
15094 .unwrap_or(0.0)
15095 })
15096 };
15097 if conf_min > 0.0 && conf.len() >= props.len() {
15098 let mut keep = 1usize;
15099 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15100 keep += 1;
15101 }
15102 k_verify = k_verify.min(keep.max(2));
15103 }
15104 if k_verify < 2 {
15105 return None;
15106 }
15107 let mut fed = Vec::with_capacity(k_verify);
15108 fed.push(t_next);
15109 fed.extend_from_slice(&props[1..k_verify]);
15110 let mut argmax = Vec::new();
15111 let mut logits_all = Vec::new();
15112 let mut walked = Vec::new();
15113 let txn = crate::dsv4::dsv4_verify_chunk(
15114 g,
15115 layers,
15116 &cfg,
15117 st,
15118 &fed,
15119 next_pos,
15120 &self.inv_freq,
15121 self.pool.as_deref(),
15122 &targets,
15123 &mut argmax,
15124 &mut logits_all,
15125 &mut walked,
15126 );
15127 if txn.is_none() && dbg {
15128 eprintln!("spec_step: verify отказал");
15129 }
15130 let txn = txn?;
15131 let spec_gpu_end = txn.gpu_end;
15132 let b = fed.len();
15133 let mut accepted = 1usize;
15134 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15135 accepted += 1;
15136 }
15137 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15142 accepted = 1;
15143 }
15144 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15145 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15146 }
15147 let t_fin = std::time::Instant::now();
15148 if !crate::dsv4::dsv4_spec_finish(
15149 g,
15150 layers,
15151 &cfg,
15152 st,
15153 txn,
15154 accepted,
15155 &fed,
15156 &self.inv_freq,
15157 self.pool.as_deref(),
15158 ) {
15159 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
15160 return None;
15161 }
15162 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15163 eprintln!(
15164 "finish(k={accepted}): {:.1} мс",
15165 t_fin.elapsed().as_secs_f64() * 1e3
15166 );
15167 }
15168 *accepted_ctr += accepted - 1;
15169 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15174 let dev_caps: Vec<usize> = targets
15179 .iter()
15180 .copied()
15181 .filter(|&t| t < spec_gpu_end)
15182 .collect();
15183 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15184 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15185 return None;
15186 }
15187 for t in 0..accepted {
15188 let tip = t + 1 == accepted;
15189 for (slot, &tl) in targets.iter().enumerate() {
15190 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15191 let lo = (di * b + t) * hc * dim;
15192 crate::dsv4::dspark_capture(
15193 &caps_all[lo..lo + hc * dim],
15194 &cfg,
15195 slot,
15196 &mut ds.main_hidden,
15197 );
15198 } else if tip
15199 && crate::dsv4::dspark_peek_slot(slot, dim, {
15200 let lo = slot * dim;
15201 &mut ds.main_hidden[lo..lo + dim]
15202 })
15203 {
15204 } else {
15209 crate::dsv4::dspark_capture(
15213 &walked[t * hc * dim..(t + 1) * hc * dim],
15214 &cfg,
15215 slot,
15216 &mut ds.main_hidden,
15217 );
15218 }
15219 }
15220 crate::dsv4::dspark_ring_append(
15221 g,
15222 &self.dsv4_mtp,
15223 &cfg,
15224 ds,
15225 next_pos + t,
15226 self.pool.as_deref(),
15227 );
15228 }
15229 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15230 self.graph_logits = Some(row);
15231 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15236 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15237 crate::dsv4::pick_tally_arm();
15238 }
15239 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15240 eprintln!(
15241 "spec_step total {:.1} мс (k={accepted})",
15242 t_all.elapsed().as_secs_f64() * 1e3
15243 );
15244 }
15245 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15246 }
15247
15248 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15249 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15250 return;
15251 }
15252 let trunk_now = crate::dsv4::pick_tally_take();
15254 crate::dsv4::trunk_freq_note(&trunk_now);
15255 if !trunk_now.is_empty() {
15256 self.dspark_trunk_picks.push(trunk_now);
15257 let keep = crate::dsv4::dspark_block();
15258 if self.dspark_trunk_picks.len() > keep {
15259 self.dspark_trunk_picks.remove(0);
15260 }
15261 }
15262 for p in std::mem::take(&mut self.dspark_pending) {
15265 let Some(i) = position.checked_sub(p.0 + 1) else {
15266 continue;
15267 };
15268 let mut p = p;
15269 if i < p.1.len() {
15270 if p.2 && p.1[i] == token_id {
15271 p.3 = i + 1;
15272 } else {
15273 p.2 = false;
15274 }
15275 if i + 1 < p.1.len() {
15276 self.dspark_pending.push(p);
15277 continue;
15278 }
15279 }
15280 self.dspark_hist.push(p.3);
15281 self.dspark_real.push(token_id);
15282 }
15283 let Some(b) = &mut self.dsv4 else { return };
15284 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15285 let n_layers = layers.len();
15286 if self.dspark.is_none() {
15287 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15288 if t.is_empty() {
15289 return;
15290 }
15291 eprintln!(
15292 "DSpark: захват со слоёв {t:?}, блок {}",
15293 crate::dsv4::dspark_block()
15294 );
15295 crate::dsv4::dspark_arm(&t, cfg.dim);
15296 self.dspark = Some(crate::dsv4::DsparkState::new(
15297 self.dsv4_mtp.len(),
15298 &cfg,
15299 t.len(),
15300 ));
15301 }
15302 let ds = self.dspark.as_mut().unwrap();
15303 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15304 return; }
15306 let mut conf = Vec::new();
15307 crate::dsv4::pick_tally_arm();
15308 let draft_started = std::time::Instant::now();
15313 #[cfg(feature = "gpu")]
15314 let gpu_draft = crate::dsv4::dspark_gpu_on();
15315 #[cfg(not(feature = "gpu"))]
15316 let gpu_draft = false;
15317 let props = if gpu_draft {
15318 #[cfg(feature = "gpu")]
15319 {
15320 let kv_id = b.3.kv_id;
15321 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15322 Some(pk) => crate::dsv4::dspark_draft_gpu(
15323 g,
15324 &self.dsv4_mtp,
15325 &cfg,
15326 ds,
15327 pk,
15328 kv_id,
15329 token_id,
15330 position,
15331 self.pool.as_deref(),
15332 &mut conf,
15333 ),
15334 None => Vec::new(),
15335 }
15336 }
15337 #[cfg(not(feature = "gpu"))]
15338 Vec::new()
15339 } else {
15340 crate::gpu::cpu_scope(|| {
15341 crate::dsv4::dspark_draft(
15342 g,
15343 &self.dsv4_mtp,
15344 &cfg,
15345 ds,
15346 token_id,
15347 position,
15348 self.pool.as_deref(),
15349 &mut conf,
15350 )
15351 })
15352 };
15353 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15354 let draft_picks = crate::dsv4::pick_tally_take();
15355 crate::dsv4::dspark_freq_note(&draft_picks);
15356 crate::dsv4::pick_tally_arm();
15359 if !props.is_empty() {
15360 let (tu, tt) = {
15364 let flat: Vec<(usize, Vec<usize>)> = self
15365 .dspark_trunk_picks
15366 .iter()
15367 .flat_map(|v| v.iter().cloned())
15368 .collect();
15369 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15371 std::collections::HashMap::new();
15372 for (li, picks) in flat {
15373 per.entry(li).or_default().extend(picks);
15374 }
15375 let n = per.len().max(1);
15376 let mut u = 0usize;
15377 let mut t = 0usize;
15378 for (_, v) in per {
15379 t += v.len();
15380 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15381 }
15382 (u / n, t / n)
15383 };
15384 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15385 self.dspark_exp.push((tu, tt, du, dt));
15386 self.dspark_pending.push((position, props, true, 0));
15387 }
15388 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15389 let n = self.dspark_hist.len() as f32;
15390 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15391 let block = crate::dsv4::dspark_block();
15392 let mut at = vec![0usize; block + 1];
15393 for &k in &self.dspark_hist {
15394 at[k] += 1;
15395 }
15396 let mut surv = Vec::with_capacity(block);
15398 for i in 1..=block {
15399 let k = at[i..].iter().sum::<usize>() as f32 / n;
15400 surv.push(format!("{k:.2}"));
15401 }
15402 let distinct = self
15403 .dspark_real
15404 .iter()
15405 .collect::<std::collections::HashSet<_>>()
15406 .len();
15407 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
15408 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
15409 });
15410 let m = self.dspark_exp.len().max(1);
15411 eprintln!(
15412 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
15413 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
15414 self.dspark_hist.len(),
15415 mean + 1.0,
15416 surv.join(" ")
15417 );
15418 eprintln!(
15419 "DSpark: разных токенов {distinct} из {} (вырожденность), \
15420 эксперты ствол {}/{} на слой за {block} токенов, \
15421 черновик {}/{} за блок, draft {:.2} мс/блок",
15422 self.dspark_real.len(),
15423 tu / m,
15424 tt / m,
15425 du / m,
15426 dt / m,
15427 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
15428 );
15429 }
15430 }
15431
15432 fn forward_layers_upto(
15433 &mut self,
15434 hidden: &[f32],
15435 position: usize,
15436 task_mask: Option<&TaskMask>,
15437 upto: Option<usize>,
15438 ) -> Vec<f32> {
15439 if let Some(plan) = self.gpu_plan.clone() {
15445 if upto.is_none() && plan.len() > 1 {
15446 let mut h = hidden.to_vec();
15447 for &(dev, from, upto_incl) in plan.iter() {
15448 h = crate::gpu::with_device(dev, || {
15449 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
15450 });
15451 }
15452 return h;
15453 }
15454 }
15455 self.forward_layers_span(hidden, position, task_mask, 0, upto)
15456 }
15457
15458 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
15463 self.set_gpu_plan_at(devices, None)
15464 }
15465
15466 pub fn set_gpu_plan_at(
15470 &mut self,
15471 devices: Option<&[usize]>,
15472 at: Option<usize>,
15473 ) -> Result<(), String> {
15474 let Some(devs) = devices.filter(|d| d.len() > 1) else {
15475 self.gpu_plan = None;
15476 return Ok(());
15477 };
15478 self.split_supported()?;
15479 let n = self.num_layers;
15480 if devs.len() > n {
15481 return Err(format!("{} devices for {n} layers", devs.len()));
15482 }
15483 if let Some(k) = at {
15484 if k == 0 || k >= n {
15485 return Err(format!("split at {k}: the model has {n} layers"));
15486 }
15487 if devs.len() == 2 {
15488 self.gpu_plan = Some(std::sync::Arc::new(vec![
15489 (devs[0], 0, k - 1),
15490 (devs[1], k, n - 1),
15491 ]));
15492 return Ok(());
15493 }
15494 return Err(format!(
15495 "an explicit split point takes exactly 2 devices, got {}",
15496 devs.len()
15497 ));
15498 }
15499 let per = n.div_ceil(devs.len());
15500 let mut plan = Vec::with_capacity(devs.len());
15501 let mut from = 0usize;
15502 for &d in devs {
15503 if from >= n {
15504 break;
15505 }
15506 let upto = (from + per - 1).min(n - 1);
15507 plan.push((d, from, upto));
15508 from = upto + 1;
15509 }
15510 self.gpu_plan = Some(std::sync::Arc::new(plan));
15511 Ok(())
15512 }
15513
15514 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
15516 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
15517 }
15518
15519 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
15525 if let Some(x) = t.as_f32() {
15526 return x.to_vec();
15527 }
15528 let mut out = vec![0.0; t.rows() * t.cols()];
15529 for r in 0..t.rows() {
15530 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
15531 }
15532 out
15533 }
15534
15535 fn embryo_resident_eligible(&self) -> bool {
15536 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
15539 || self.num_layers != self.physical_layers
15540 || self.loop_final_norm
15541 || self.weights.layers.len() != self.num_layers
15542 || self.head_clusters.is_none()
15543 || self.final_softcap.is_some()
15544 || self.logit_multiplier.is_some()
15545 || self.attn_softcap != 0.0
15546 || self.mtp.is_some()
15547 || self.per_token_stack()
15548 || self.dsv4.is_some()
15549 || self.dsv41.is_some()
15550 || self.qwen4_exp.is_some()
15551 || self.dyn_router.is_some()
15558 || self.dyn_phi_layer.is_some()
15559 || self.dyn_blend_loaded
15560 || self.o1_cfg.is_some()
15561 || self.swa.is_some()
15562 || self.sliding_layers.is_some()
15563 || self.global_attn.is_some()
15564 || self.attention_heads_per_layer.is_some()
15565 || self.attn_v_norm
15566 || self
15567 .kv_cache
15568 .layers
15569 .iter()
15570 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
15571 || self.rope_scale != 1.0
15572 || self.rope_scale_local != 1.0
15573 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
15574 || self.hidden_size == 0
15575 || self.hidden_size > 1024
15576 || self.intermediate_size > 1024
15577 || self.num_heads == 0
15578 || self.num_kv_heads == 0
15579 || self.num_heads % self.num_kv_heads != 0
15580 || self.num_heads.saturating_mul(self.head_dim) > 1024
15581 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
15582 || self.vocab_size == 0
15583 || self.kv_cache.max_seq_len == 0
15584 || self.rotary_dim == 0
15585 || self.rotary_dim > self.head_dim
15586 || self.rotary_dim % 2 != 0
15587 || self.inv_freq.len() < self.rotary_dim / 2
15588 {
15589 return false;
15590 }
15591 if self.weights.lm_head.as_f32().is_none()
15602 || self.weights.embed_tokens.as_f32().is_none()
15603 || self.weights.lm_head.rows() < self.vocab_size
15604 || self.weights.lm_head.cols() != self.hidden_size
15605 || self.weights.embed_tokens.rows() < self.vocab_size
15606 || self.weights.embed_tokens.cols() != self.hidden_size
15607 || self.weights.final_norm.len() != self.hidden_size
15608 {
15609 return false;
15610 }
15611 if let Some(cfg) = self.vmf_cfg {
15612 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
15613 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
15614 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
15615 || cfg.state_len() == 0
15616 {
15617 return false;
15618 }
15619 }
15620 if let Some(g) = self.gdn_cfg {
15621 if g.num_v_heads == 0
15626 || g.num_k_heads == 0
15627 || g.num_v_heads % g.num_k_heads != 0
15628 || g.key_head_dim == 0
15629 || g.key_head_dim > 128
15630 || g.value_head_dim == 0
15631 || g.value_head_dim > 256
15632 || g.value_head_dim % 4 != 0
15633 || g.conv_kernel == 0
15634 || g.num_v_heads > 512
15635 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
15636 || g.conv_dim() > 2048
15637 || g.conv_dim() % 4 != 0
15638 || g.hidden_size != self.hidden_size
15639 || g.output_gate_sigmoid
15640 || g.rms_eps != self.rms_eps
15641 || g.state_len() == 0
15642 {
15643 return false;
15644 }
15645 }
15646 let mut full_seen = false;
15647 for lw in &self.weights.layers {
15648 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
15649 return false;
15650 }
15651 match &lw.attn {
15652 AttnKind::LinearGdn(w) => {
15653 let Some(g) = self.gdn_cfg else {
15654 return false;
15655 };
15656 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
15657 if w.in_proj_qkv.rows() != g.conv_dim()
15658 || w.in_proj_qkv.cols() != self.hidden_size
15659 || w.in_proj_qkv.as_f32().is_none()
15660 || w.in_proj_z.rows() != nv * dv
15661 || w.in_proj_z.cols() != self.hidden_size
15662 || w.in_proj_z.as_f32().is_none()
15663 || w.in_proj_a.rows() != nv
15664 || w.in_proj_a.cols() != self.hidden_size
15665 || w.in_proj_a.as_f32().is_none()
15666 || w.in_proj_b.rows() != nv
15667 || w.in_proj_b.cols() != self.hidden_size
15668 || w.in_proj_b.as_f32().is_none()
15669 || w.conv1d.len() != g.conv_dim() * kk
15670 || w.a_log.len() != nv
15671 || w.dt_bias.len() != nv
15672 || w.norm.len() != dv
15673 || w.out_proj.rows() != self.hidden_size
15674 || w.out_proj.cols() != nv * dv
15675 || w.out_proj.as_f32().is_none()
15676 {
15677 return false;
15678 }
15679 }
15680 AttnKind::Linear(w) => {
15681 let Some(cfg) = self.vmf_cfg else {
15682 return false;
15683 };
15684 if w.thq.rows() != cfg.num_heads * cfg.nphase
15685 || w.thq.cols() != self.hidden_size
15686 || w.thq.as_f32().is_none()
15687 || w.thk.rows() != cfg.num_heads * cfg.nphase
15688 || w.thk.cols() != self.hidden_size
15689 || w.thk.as_f32().is_none()
15690 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
15691 || w.v_proj.cols() != self.hidden_size
15692 || w.v_proj.as_f32().is_none()
15693 || w.out_proj.rows() != self.hidden_size
15694 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
15695 || w.out_proj.as_f32().is_none()
15696 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
15697 {
15698 return false;
15699 }
15700 if let Some((kg, kb)) = &w.k_gate {
15701 if kg.rows() != cfg.num_heads
15702 || kg.cols() != self.hidden_size
15703 || kg.as_f32().is_none()
15704 || kb.len() != cfg.num_heads
15705 {
15706 return false;
15707 }
15708 }
15709 if let Some(conv) = &w.conv {
15710 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
15711 return false;
15712 }
15713 }
15714 }
15715 AttnKind::Full {
15716 wq,
15717 wk,
15718 wv,
15719 wo,
15720 q_norm,
15721 k_norm,
15722 output_gate,
15723 softplus_gate,
15724 bias,
15725 } => {
15726 if full_seen
15727 || q_norm.is_some()
15728 || k_norm.is_some()
15729 || *output_gate
15730 || softplus_gate.is_some()
15731 || bias.is_some()
15732 || wq.as_f32().is_none()
15733 || wk.as_f32().is_none()
15734 || wv.as_f32().is_none()
15735 || wo.as_f32().is_none()
15736 || wq.rows() != self.num_heads * self.head_dim
15737 || wk.rows() != self.num_kv_heads * self.head_dim
15738 || wv.rows() != self.num_kv_heads * self.head_dim
15739 || wq.cols() != self.hidden_size
15740 || wk.cols() != self.hidden_size
15741 || wv.cols() != self.hidden_size
15742 || wo.rows() != self.hidden_size
15743 || wo.cols() != self.num_heads * self.head_dim
15744 {
15745 return false;
15746 }
15747 full_seen = true;
15748 }
15749 AttnKind::Bounded(w) => {
15750 let Some(ac) = self.anchor_core.as_ref() else {
15753 return false;
15754 };
15755 if self.bounded_rope.is_none()
15756 || w.window != ac.window
15757 || w.sink != ac.sink
15758 || w.window == 0
15759 || w.window + w.sink > 256
15760 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
15761 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
15762 || w.wq.as_f32().is_none()
15763 || w.wk.as_f32().is_none()
15764 || w.wv.as_f32().is_none()
15765 || w.wo.as_f32().is_none()
15766 || w.wq.rows() != self.num_heads * self.head_dim
15767 || w.wk.rows() != self.num_kv_heads * self.head_dim
15768 || w.wv.rows() != self.num_kv_heads * self.head_dim
15769 || w.wq.cols() != self.hidden_size
15770 || w.wk.cols() != self.hidden_size
15771 || w.wv.cols() != self.hidden_size
15772 || w.wo.rows() != self.hidden_size
15773 || w.wo.cols() != self.num_heads * self.head_dim
15774 {
15775 return false;
15776 }
15777 }
15778 _ => return false,
15779 }
15780 match &lw.ffn {
15781 FfnKind::Dense(d) => {
15782 if d.act != Act::Silu
15783 || !d.segs.is_empty()
15784 || d.gate_proj.as_f32().is_none()
15785 || d.up_proj.as_f32().is_none()
15786 || d.down_proj.as_f32().is_none()
15787 || d.gate_proj.rows() != self.intermediate_size
15788 || d.gate_proj.cols() != self.hidden_size
15789 || d.up_proj.rows() != self.intermediate_size
15790 || d.up_proj.cols() != self.hidden_size
15791 || d.down_proj.rows() != self.hidden_size
15792 || d.down_proj.cols() != self.intermediate_size
15793 {
15794 return false;
15795 }
15796 }
15797 FfnKind::Moe(m) => {
15798 if m.resonance.is_none()
15799 || m.top_k != 1
15800 || m.router_sigmoid
15801 || !m.norm_topk_prob
15802 || m.expert_bias.is_some()
15803 || m.routed_scaling != 1.0
15804 || m.route_tau.is_some()
15805 || m.shared.is_none()
15806 || m.mask.is_some()
15807 || m.per_expert_scale.is_some()
15808 || m.router_input_norm
15809 || m.experts.is_empty()
15810 || m.experts.len() > 8
15811 {
15812 return false;
15813 }
15814 let r = m.resonance.as_ref().unwrap();
15815 if r.mu.len() != m.experts.len() * self.hidden_size
15816 || r.bias.len() != m.experts.len()
15817 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15818 || r.k > 128
15819 {
15820 return false;
15821 }
15822 let Some((shared, gate)) = &m.shared else {
15823 return false;
15824 };
15825 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15826 return false;
15827 }
15828 if shared.gate_proj.as_f32().is_none()
15829 || shared.up_proj.as_f32().is_none()
15830 || shared.down_proj.as_f32().is_none()
15831 || shared.gate_proj.rows() != self.intermediate_size
15832 || shared.gate_proj.cols() != self.hidden_size
15833 || shared.up_proj.rows() != self.intermediate_size
15834 || shared.up_proj.cols() != self.hidden_size
15835 || shared.down_proj.rows() != self.hidden_size
15836 || shared.down_proj.cols() != self.intermediate_size
15837 {
15838 return false;
15839 }
15840 for e in &m.experts {
15841 if e.act != Act::Silu
15842 || !e.segs.is_empty()
15843 || e.gate_proj.as_f32().is_none()
15844 || e.up_proj.as_f32().is_none()
15845 || e.down_proj.as_f32().is_none()
15846 || e.gate_proj.rows() != self.intermediate_size
15847 || e.gate_proj.cols() != self.hidden_size
15848 || e.up_proj.rows() != self.intermediate_size
15849 || e.up_proj.cols() != self.hidden_size
15850 || e.down_proj.rows() != self.hidden_size
15851 || e.down_proj.cols() != self.intermediate_size
15852 {
15853 return false;
15854 }
15855 }
15856 }
15857 FfnKind::DenseMoe(_) => return false,
15858 }
15859 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15860 return false;
15861 }
15862 }
15863 if full_seen && self.anchor_core.is_some() {
15864 return false;
15865 }
15866 full_seen || self.num_layers > 0
15867 }
15868
15869 fn embryo_resident_wanted(&self) -> bool {
15874 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15875 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15876 && matches!(
15877 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15878 Ok("1") | Ok("parallel")
15879 )
15880 && crate::gpu::enabled_here()
15881 && !self.graph_refused()
15882 && self.embryo_resident_eligible()
15883 }
15884
15885 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15893 if ids.len() < 2
15894 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15895 || !self.embryo_resident_wanted()
15896 {
15897 return None;
15898 }
15899 let model = self.ensure_embryo_graph()?;
15900 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15901 .ok()
15902 .and_then(|v| v.parse::<usize>().ok())
15903 .filter(|&v| v >= 1)
15904 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15905 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15906 let hs = self.hidden_size;
15907 let n = ids.len();
15908 let mut pos = start;
15909 let mut last = None;
15910 let mut rows = Vec::with_capacity(cmax * hs);
15911 while pos < n {
15912 let end = (pos + cmax).min(n);
15913 rows.clear();
15914 for &id in &ids[pos..end] {
15915 rows.extend_from_slice(&self.embed_single(id));
15916 }
15917 let mut lg = Vec::new();
15918 if !crate::gpu::forward_embryo_graph_chunk(
15919 &model,
15920 self.graph_kv_id,
15921 &rows,
15922 pos,
15923 end - pos,
15924 &mut lg,
15925 ) {
15926 if pos == start {
15927 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15928 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15929 }
15930 return None;
15931 }
15932 self.kv_cache.clear();
15936 self.clear_history();
15937 crate::gpu::graph_kv_reset(self.graph_kv_id);
15938 panic!(
15939 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15940 );
15941 }
15942 last = Some(lg);
15943 pos = end;
15944 }
15945 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15946 eprintln!(
15947 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15948 n - start,
15949 (n - start).div_ceil(cmax)
15950 );
15951 }
15952 last
15953 }
15954
15955 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15956 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15957 const UMAX: u32 = u32::MAX;
15958 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15959 const REC: usize = 64;
15960 struct Pack {
15961 data: Vec<f32>,
15962 }
15963 impl Pack {
15964 fn put(&mut self, x: &[f32]) -> u32 {
15965 if x.is_empty() {
15966 return u32::MAX;
15967 }
15968 let off = self.data.len();
15969 self.data.extend_from_slice(x);
15970 off as u32
15971 }
15972 }
15973 let vmf = self.vmf_cfg;
15977 let gdn = self.gdn_cfg;
15978 let mut pack = Pack { data: Vec::new() };
15979 let mut meta = vec![0u32; HEADER];
15980 meta[0] = self.hidden_size as u32;
15981 meta[1] = self.intermediate_size as u32;
15982 meta[2] = self.vocab_size as u32;
15983 meta[3] = self.num_layers as u32;
15984 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15985 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15986 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15987 if let Some(g) = gdn {
15988 meta[24] = g.num_v_heads as u32;
15989 meta[25] = g.num_k_heads as u32;
15990 meta[26] = g.key_head_dim as u32;
15991 meta[27] = g.value_head_dim as u32;
15992 meta[28] = g.conv_kernel as u32;
15993 meta[29] = g.conv_dim() as u32;
15994 }
15995 meta[7] = self.num_heads as u32;
15996 meta[8] = self.num_kv_heads as u32;
15997 meta[9] = self.head_dim as u32;
15998 meta[10] = self.kv_cache.max_seq_len as u32;
15999 let clusters = self.head_clusters.as_ref().unwrap();
16000 let cluster_count = clusters.len() / self.hidden_size;
16001 if clusters.len() % self.hidden_size != 0
16002 || cluster_count == 0
16003 || cluster_count > 1024
16004 || self.vocab_size % cluster_count != 0
16005 || self.weights.lm_head.rows() < self.vocab_size
16006 || self.weights.final_norm.len() != self.hidden_size
16007 {
16008 return None;
16009 }
16010 meta[11] = cluster_count as u32;
16011 meta[12] = (self.vocab_size / cluster_count) as u32;
16012 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16013 meta[16] = self.rotary_dim as u32;
16014 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16015 meta[19] = (self.rms_eps as f32).to_bits();
16016 let max_conv = self
16017 .weights
16018 .layers
16019 .iter()
16020 .filter_map(|lw| match &lw.attn {
16021 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16022 _ => None,
16023 })
16024 .max()
16025 .unwrap_or(1);
16026 let phase_stride = vmf
16032 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16033 .unwrap_or(0);
16034 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16035 let state_stride = phase_stride.max(gdn_stride);
16036 let bounded = self.anchor_core.clone();
16040 let (anchor_window, anchor_sink) = bounded
16041 .as_ref()
16042 .map(|ac| (ac.window, ac.sink))
16043 .unwrap_or((0, 0));
16044 let kv_stride = if bounded.is_some() {
16045 2usize
16046 .saturating_mul(self.num_kv_heads)
16047 .saturating_mul(anchor_window)
16048 .saturating_mul(self.head_dim)
16049 } else {
16050 2usize
16051 .saturating_mul(self.num_kv_heads)
16052 .saturating_mul(self.kv_cache.max_seq_len)
16053 .saturating_mul(self.head_dim)
16054 };
16055 meta[14] = state_stride as u32;
16056 meta[15] = kv_stride as u32;
16057 meta[18] = anchor_window as u32;
16058 meta[20] = anchor_sink as u32;
16059 meta[21] = match &self.bounded_rope {
16060 Some(rope) => {
16061 let off = pack.put(&rope.cos);
16063 let _ = pack.put(&rope.sin);
16064 off
16065 }
16066 None => UMAX,
16067 };
16068 let mut full_seen = false;
16069 let mut bounded_seen = 0usize;
16070 let mut phase_seen = 0usize;
16074 let mut gdn_seen = 0usize;
16075 for (li, lw) in self.weights.layers.iter().enumerate() {
16076 let base = meta.len();
16077 meta.resize(base + REC, UMAX);
16078 meta[base] = match &lw.attn {
16079 AttnKind::Linear(w) if w.phase_delta => 1,
16080 AttnKind::Linear(_) => 0,
16081 AttnKind::Full { .. } => 2,
16082 AttnKind::Bounded(_) => 3,
16083 AttnKind::LinearGdn(_) => 4,
16084 _ => UMAX,
16085 };
16086 meta[base + 1] = pack.put(&lw.input_norm);
16087 meta[base + 2] = pack.put(&lw.post_norm);
16088 meta[base + 25] = match &lw.attn {
16089 AttnKind::Linear(_) => {
16090 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16091 phase_seen += 1;
16092 off
16093 }
16094 AttnKind::LinearGdn(_) => {
16095 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16096 gdn_seen += 1;
16097 off
16098 }
16099 _ => UMAX,
16100 };
16101 match &lw.attn {
16102 AttnKind::LinearGdn(w) => {
16103 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16106 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16107 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16108 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16109 meta[base + 60] = pack.put(&w.conv1d);
16110 meta[base + 61] = pack.put(&w.a_log);
16111 meta[base + 62] = pack.put(&w.dt_bias);
16112 meta[base + 63] = pack.put(&w.norm);
16113 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16114 meta[base + 24] = 0;
16115 }
16116 AttnKind::Linear(w) => {
16117 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16118 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16119 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16120 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16121 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16122 meta[base + 7] = pack.put(&decay);
16123 if let Some((kg, kb)) = &w.k_gate {
16124 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16125 meta[base + 9] = pack.put(kb);
16126 }
16127 if let Some(conv) = &w.conv {
16128 meta[base + 10] = pack.put(conv);
16129 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16130 } else {
16131 meta[base + 24] = 0;
16132 }
16133 }
16134 AttnKind::Full { wq, wk, wv, wo, .. } => {
16135 full_seen = true;
16136 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16137 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16138 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16139 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16140 meta[base + 26] = (li * kv_stride) as u32;
16141 }
16142 AttnKind::Bounded(w) => {
16143 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16144 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16145 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16146 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16147 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16149 meta[base + 27] = pack.put(&w.sink_k);
16150 meta[base + 28] = pack.put(&w.sink_v);
16151 bounded_seen += 1;
16152 }
16153 _ => return None,
16154 }
16155 match &lw.ffn {
16156 FfnKind::Dense(d) => {
16157 meta[base + 15] = 0;
16158 meta[base + 16] = 0;
16159 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16160 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16161 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16162 }
16163 FfnKind::Moe(m) => {
16164 let r = m.resonance.as_ref().unwrap();
16165 let (shared, _) = m.shared.as_ref().unwrap();
16166 meta[base + 15] = 1;
16167 meta[base + 16] = m.experts.len() as u32;
16168 meta[base + 17] = pack.put(&r.mu);
16169 meta[base + 18] = pack.put(&r.u);
16170 meta[base + 19] = pack.put(&r.bias);
16171 meta[base + 20] = r.k as u32;
16172 let mut shell = r.effective_shell(m.experts.len());
16181 shell.push(f32::NEG_INFINITY);
16182 meta[base + 30] = pack.put(&shell);
16183 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16184 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16185 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16186 for (e, ex) in m.experts.iter().enumerate() {
16187 meta[base + 32 + e * 3] =
16188 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16189 meta[base + 33 + e * 3] =
16190 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16191 meta[base + 34 + e * 3] =
16192 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16193 }
16194 }
16195 FfnKind::DenseMoe(_) => return None,
16196 }
16197 }
16198 if !full_seen && self.num_layers == 0 {
16199 return None;
16200 }
16201 let id = {
16202 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16203 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16204 };
16205 let model = crate::gpu::EmbryoGraphModel {
16206 id,
16207 hidden: self.hidden_size,
16208 intermediate: self.intermediate_size,
16209 vocab: self.vocab_size,
16210 layers: self.num_layers,
16211 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16212 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16213 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16214 anchor_q_heads: self.num_heads,
16215 anchor_kv_heads: self.num_kv_heads,
16216 anchor_head_dim: self.head_dim,
16217 rotary_dim: self.rotary_dim,
16218 max_seq: self.kv_cache.max_seq_len,
16219 cluster_count,
16220 cluster_size: self.vocab_size / cluster_count,
16221 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16222 state_stride,
16223 kv_stride,
16224 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16225 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16226 weights: pack.data,
16227 meta,
16228 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16229 clusters: clusters.as_ref().clone(),
16230 final_norm: self.weights.final_norm.clone(),
16231 inv_freq: self.inv_freq.as_ref().clone(),
16232 bounded: bounded.is_some(),
16233 kv_layers: if bounded.is_some() {
16234 bounded_seen
16235 } else {
16236 self.num_layers
16237 },
16238 state_layers: phase_seen + gdn_seen,
16239 anchor_window,
16240 anchor_sink,
16241 phase_layers: phase_seen,
16242 gdn_layers: gdn_seen,
16243 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16244 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16245 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16246 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16247 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16248 };
16249 self.embryo_graph = Some(std::sync::Arc::new(model));
16250 }
16251 self.embryo_graph.clone()
16252 }
16253
16254 fn forward_layers_span(
16255 &mut self,
16256 hidden: &[f32],
16257 position: usize,
16258 task_mask: Option<&TaskMask>,
16259 from: usize,
16260 upto: Option<usize>,
16261 ) -> Vec<f32> {
16262 #[cfg(target_os = "macos")]
16263 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16264 position,
16265 from,
16266 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16267 );
16268 debug_assert!(
16269 from == 0
16270 || (self.dsv4.is_none()
16271 && self.dsv41.is_none()
16272 && self.qwen4_exp.is_none()
16273 && !self.per_token_stack())
16274 );
16275 #[cfg(target_os = "macos")]
16281 if !crate::gpu_metal::wait_replay() {
16282 self.fail_metal_graph("the pending async replay failed before a plain forward");
16283 return vec![0.0; self.hidden_size];
16284 }
16285 if let Some(b) = &mut self.qwen4_exp {
16286 let _ = (task_mask, upto);
16287 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16288 let mut logits = Vec::new();
16289 crate::qwen4_exp::forward_token(
16290 &b.0,
16291 &b.1,
16292 &b.2,
16293 &mut b.3,
16294 token_id,
16295 position,
16296 &self.inv_freq,
16297 self.pool.as_deref(),
16298 &mut logits,
16299 true,
16300 );
16301 self.graph_logits = Some(logits);
16302 return vec![0.0; self.hidden_size];
16303 }
16304 if let Some(b) = &mut self.dsv4 {
16310 let _ = (task_mask, upto);
16311 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16312 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16313 st.pos = position;
16314 let mut logits = Vec::new();
16315 crate::dsv4::forward_token(
16316 g,
16317 layers,
16318 &cfg,
16319 st,
16320 token_id,
16321 &self.inv_freq,
16322 self.pool.as_deref(),
16323 &mut logits,
16324 );
16325 self.graph_logits = Some(logits);
16326 self.dspark_probe(position, token_id);
16327 return vec![0.0; self.hidden_size];
16330 }
16331 if let Some(b) = &mut self.dsv41 {
16333 let _ = (task_mask, upto);
16334 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16335 let mut logits = Vec::new();
16336 crate::dsv41::forward_token(
16337 &b.0,
16338 &b.1,
16339 &b.2,
16340 &mut b.3,
16341 token_id,
16342 position,
16343 self.pool.as_deref(),
16344 &mut logits,
16345 );
16346 self.graph_logits = Some(logits);
16347 return vec![0.0; self.hidden_size];
16348 }
16349 if let Some(b) = &self.g4e {
16355 let _ = (task_mask, upto);
16356 return crate::g4e::g4e_forward(
16357 &b.0,
16358 &b.1,
16359 hidden,
16360 position,
16361 &mut self.kv_cache.layers,
16362 self.num_heads,
16363 self.num_kv_heads,
16364 self.pool.as_deref(),
16365 );
16366 }
16367 if let Some(b) = &self.g3n {
16368 let _ = (task_mask, upto);
16369 return crate::g3n::g3n_forward(
16370 &b.0,
16371 &b.1,
16372 hidden,
16373 position,
16374 &mut self.kv_cache.layers,
16375 self.num_heads,
16376 self.num_kv_heads,
16377 self.head_dim,
16378 self.pool.as_deref(),
16379 );
16380 }
16381 if from == 0
16388 && upto.is_none()
16389 && task_mask.is_none()
16390 && self.anchor_core.is_some()
16391 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16392 {
16393 static ONCE: std::sync::Once = std::sync::Once::new();
16394 ONCE.call_once(|| {
16395 eprintln!(
16396 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16397 unsupported={} eligible={}",
16398 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
16399 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
16400 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
16401 crate::gpu::enabled_here(),
16402 self.graph_refused(),
16403 self.embryo_resident_eligible(),
16404 );
16405 });
16406 }
16407 if from == 0
16408 && upto.is_none()
16409 && task_mask.is_none()
16410 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16414 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16415 && matches!(
16419 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16420 Ok("1") | Ok("parallel")
16421 )
16422 && crate::gpu::enabled_here()
16423 && !self.graph_refused()
16424 && (position == 0 || self.device_sequence_position().is_some())
16430 && self.embryo_resident_eligible()
16431 && let Some(model) = self.ensure_embryo_graph()
16432 {
16433 let mut lg = Vec::new();
16434 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
16435 {
16436 self.graph_logits = Some(lg);
16437 return vec![0.0; self.hidden_size];
16438 }
16439 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16440 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
16441 }
16442 self.mark_graph_refused();
16448 if position != 0 {
16449 self.kv_cache.clear();
16453 self.clear_history();
16454 crate::gpu::graph_kv_reset(self.graph_kv_id);
16455 panic!(
16456 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
16457 );
16458 }
16459 }
16460 let mut h = hidden.to_vec();
16461 self.mimo_moe_prepare();
16464 let _mimo_q8 = self
16465 .mimo_moe
16466 .is_on()
16467 .then(crate::qtensor::enter_full_gpu_q8_scope);
16468 let (nh, _nkv, _hd, hs, _rd, eps) = (
16471 self.num_heads,
16472 self.num_kv_heads,
16473 self.head_dim,
16474 self.hidden_size,
16475 self.rotary_dim,
16476 self.rms_eps,
16477 );
16478 let pool = self.pool.clone();
16479 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
16491 let graph_on = match graph_env.as_deref() {
16492 Some("0") => false,
16493 Some("prefill") => false, Some(_) => true,
16495 None => crate::gpu::wgpu_graph_default(),
16501 };
16502 let graph_trusted =
16503 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
16504 let race_eligible =
16505 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
16506 let mut tail_start = 0usize;
16507 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
16508 let t_graph = std::time::Instant::now();
16509 let mut lg = Vec::new();
16510 let mut gl = 0usize;
16511 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
16512 let declined = built.is_none();
16513 let built = match built {
16514 Some(Ok(hh)) => Some(hh),
16515 Some(Err(())) => {
16516 self.clear_sequence_state();
16520 self.graph_failed
16521 .store(true, std::sync::atomic::Ordering::Relaxed);
16522 self.cancel
16523 .store(true, std::sync::atomic::Ordering::Relaxed);
16524 tracing::error!("token graph failed after admission; sequence state cleared");
16525 return vec![0.0; self.hidden_size];
16526 }
16527 None => None,
16528 };
16529 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
16534 self.mark_graph_refused();
16535 }
16536 graph_note(built.is_some(), gl, self.num_layers);
16537 if let Some(hh) = built {
16538 let dur = t_graph.elapsed();
16539 if std::env::var("CMF_GRAPH_PROF").is_ok() {
16540 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
16541 }
16542 if gl > 0 && gl < self.num_layers {
16543 h = hh;
16549 tail_start = gl;
16550 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
16551 if !graph_trusted {
16552 crate::gpu::graph_race_record(true, dur);
16553 }
16554 if !lg.is_empty() {
16555 lg.resize(self.vocab_size, 0.0);
16558 if let Some(c) = self.final_softcap {
16559 softcap_in_place(&mut lg, c, self.pool.as_deref());
16560 }
16561 self.graph_logits = Some(lg);
16562 }
16563 return hh;
16564 }
16565 }
16571 }
16572 let span = from > 0 || upto.is_some();
16596 if span && graph_on && task_mask.is_none() && graph_trusted {
16597 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
16598 let mut lg = Vec::new();
16599 let mut gl = 0usize;
16600 let span_res =
16601 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
16602 let span_res = match span_res {
16603 Some(Ok(hh)) => Some(hh),
16604 Some(Err(())) => {
16605 self.clear_sequence_state();
16606 self.graph_failed
16607 .store(true, std::sync::atomic::Ordering::Relaxed);
16608 self.cancel
16609 .store(true, std::sync::atomic::Ordering::Relaxed);
16610 tracing::error!(
16611 "span token graph failed after admission; sequence state cleared"
16612 );
16613 return vec![0.0; self.hidden_size];
16614 }
16615 None => None,
16616 };
16617 graph_note(span_res.is_some(), gl, upto_excl - from);
16618 if std::env::var("CMF_GPU_DEBUG").is_ok() {
16619 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
16623 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
16624 eprintln!(
16625 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
16626 upto_excl - from,
16627 span_res.is_some()
16628 );
16629 }
16630 }
16631 if let Some(hh) = span_res {
16632 if gl == upto_excl - from {
16633 if !lg.is_empty() {
16634 lg.resize(self.vocab_size, 0.0);
16635 if let Some(c) = self.final_softcap {
16636 softcap_in_place(&mut lg, c, self.pool.as_deref());
16637 }
16638 self.graph_logits = Some(lg);
16639 }
16640 crate::gpu::set_layer(-1);
16641 return hh;
16642 }
16643 h = hh;
16645 tail_start = from + gl;
16646 }
16647 }
16648 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
16653
16654 let host_tail = tail_start > from;
16664 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
16665 let automatic_gpu_prefix = self.automatic_gpu_prefix();
16666
16667 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
16668 #[cfg(target_os = "macos")]
16669 let mut gpu_skip_until = 0usize;
16670 for li in tail_start.max(from)..self.num_layers {
16671 let _capacity_tail = automatic_gpu_prefix
16672 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
16673 .map(|_| crate::gpu::enter_cpu_scope());
16674 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
16676 if li > u {
16677 break;
16678 }
16679 }
16680 if let Some(mask) = task_mask {
16681 if !mask.layer_alive(li) {
16682 continue; }
16684 }
16685 if host_tail && task_mask.is_none() {
16690 match self.m3_pooled_layer(li, position, &mut h) {
16691 Ok(true) => {
16692 crate::gpu::set_layer(-1);
16693 continue;
16694 }
16695 Ok(false) => {}
16696 Err(()) => {
16697 self.clear_sequence_state();
16698 self.graph_failed
16699 .store(true, std::sync::atomic::Ordering::Relaxed);
16700 self.cancel
16701 .store(true, std::sync::atomic::Ordering::Relaxed);
16702 tracing::error!(
16703 "pooled tail token graph failed after admission; sequence state cleared"
16704 );
16705 return vec![0.0; self.hidden_size];
16706 }
16707 }
16708 }
16709 #[cfg(target_os = "macos")]
16713 {
16714 if li < gpu_skip_until {
16715 continue;
16716 }
16717 if task_mask.is_none() {
16718 let end = self.q1_graph_gpu(li, upto, position, &mut h);
16719 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
16720 return vec![0.0; self.hidden_size];
16724 }
16725 if end > li {
16726 gpu_skip_until = end;
16727 if self.is_loop_end(end - 1) && end < self.num_layers {
16730 h = inference::rms_norm(
16731 &h,
16732 &self.weights.final_norm,
16733 self.rms_eps,
16734 self.norm_style,
16735 );
16736 }
16737 continue;
16738 }
16739 }
16740 }
16741
16742 if task_mask.is_none() {
16743 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
16744 crate::gpu::BatchGraphOutcome::Completed => continue,
16745 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
16746 crate::gpu::BatchGraphOutcome::Declined => {}
16747 }
16748 }
16749 #[cfg(feature = "gpu")]
16750 self.pull_lagging_host_kv(li, li + 1, position);
16751 let lw = &self.weights.layers[self.phys_layer(li)];
16752 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
16753 if tp.parse::<usize>().ok() == Some(position) {
16754 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
16755 eprintln!(
16756 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
16757 h[0], h[1]
16758 );
16759 }
16760 }
16761 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16764 inference::rms_norm_into(
16765 &h,
16766 &lw.input_norm,
16767 self.rms_eps,
16768 self.norm_style,
16769 &mut self.ws.n1,
16770 );
16771 drop(prof);
16772
16773 let attn_out = match &lw.attn {
16774 AttnKind::Mla(w) => {
16775 let inv_freq_l = self.layer_inv_freq(li);
16776 let rs = self.layer_rope_scale(li);
16777 let eps = self.rms_eps;
16778 let pool = self.pool.clone();
16779 mla_attention(
16780 w,
16781 &self.ws.n1,
16782 &mut self.kv_cache.layers[li],
16783 position,
16784 &inv_freq_l,
16785 rs,
16786 eps,
16787 pool.as_deref(),
16788 )
16789 }
16790 AttnKind::Linear(w) => {
16791 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
16792 vmf_phase_forward(
16793 &self.ws.n1,
16794 w,
16795 &cfg,
16796 &mut self.kv_cache.layers[li].linear_state,
16797 self.pool.as_deref(),
16798 )
16799 }
16800 AttnKind::Kda(w) => {
16801 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
16802 crate::linear_core::kda_forward(
16803 &self.ws.n1,
16804 w,
16805 &cfg,
16806 &mut self.kv_cache.layers[li].linear_state,
16807 self.pool.as_deref(),
16808 )
16809 }
16810 AttnKind::LinearGdn(w) => {
16811 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
16812 gdn_forward(
16813 &self.ws.n1,
16814 w,
16815 &cfg,
16816 &mut self.kv_cache.layers[li].linear_state,
16817 self.pool.as_deref(),
16818 )
16819 }
16820 AttnKind::ShortConv(w) => {
16821 let cfg = self
16822 .short_conv_cfg
16823 .expect("short-conv layer without short_conv_cfg");
16824 short_conv_forward(
16825 &self.ws.n1,
16826 w,
16827 &cfg,
16828 &mut self.kv_cache.layers[li].linear_state,
16829 self.pool.as_deref(),
16830 )
16831 }
16832 AttnKind::Bounded(w) => {
16833 let rope = self
16836 .bounded_rope
16837 .clone()
16838 .expect("bounded layer without an installed rotation table");
16839 let cfg = crate::bounded::BoundedAttnCfg {
16840 num_heads: self.num_heads,
16841 num_kv_heads: self.num_kv_heads,
16842 head_dim: self.head_dim,
16843 hidden_size: hs,
16844 scale: self.attn_scale,
16845 rope: &rope,
16846 pool: pool.as_deref(),
16847 };
16848 crate::bounded::bounded_attention(
16849 &self.ws.n1,
16850 w,
16851 &mut self.kv_cache.layers[li],
16852 &cfg,
16853 )
16854 }
16855 AttnKind::Full {
16856 wq,
16857 wk,
16858 wv,
16859 wo,
16860 q_norm,
16861 k_norm,
16862 output_gate,
16863 softplus_gate,
16864 bias,
16865 } if self.kv_cache.layers[li].o1_sealed() => {
16866 let inv_freq_l = self.layer_inv_freq(li);
16869 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16870 let cfg = QwenAttnCfg {
16871 num_heads: self.layer_num_heads(li),
16872 num_kv_heads: nkv_l,
16873 head_dim: hd_l,
16874 hidden_size: hs,
16875 position,
16876 inv_freq: &inv_freq_l,
16877 rotary_dim: rd_l,
16878 scale: self.attn_scale,
16879 softcap: self.attn_softcap,
16880 window: None,
16881 v_norm: self.attn_v_norm,
16882 qk_norm_after_rope: self.qk_norm_after_rope,
16883 gate_sigmoid: self.proj_gate_sigmoid,
16884 q_norm: q_norm.as_deref(),
16885 k_norm: k_norm.as_deref(),
16886 output_gate: *output_gate,
16887 softplus_gate: softplus_gate
16888 .as_ref()
16889 .map(|(gate, per_head)| (gate, *per_head)),
16890 rope_scale: self.layer_rope_scale(li),
16891 bias: bias
16892 .as_ref()
16893 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16894 rms_eps: eps,
16895 norm_style: self.norm_style,
16896 pool: pool.as_deref(),
16897 v_head_dim: self.layer_v_dim(li),
16898 };
16899 attention::qwen_attention_nystrom(
16900 &self.ws.n1,
16901 wq,
16902 wk,
16903 wv,
16904 wo,
16905 &mut self.kv_cache.layers[li],
16906 &cfg,
16907 )
16908 }
16909 AttnKind::Full {
16910 wq,
16911 wk,
16912 wv,
16913 wo,
16914 q_norm,
16915 k_norm,
16916 output_gate,
16917 softplus_gate,
16918 bias,
16919 } => 'attn: {
16920 let dropin_reason =
16925 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16926 if let Some(reason) = dropin_reason {
16927 self.note_graph_decline("wgpu attn dropin", reason);
16928 }
16929 if graph_on
16930 && dropin_reason.is_none()
16931 && !*output_gate
16932 && softplus_gate.is_none()
16933 && self.attention_heads_per_layer.is_none()
16934 && bias.is_none()
16935 && task_mask.is_none()
16936 {
16937 let inv_freq_l = self.layer_inv_freq(li);
16938 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16939 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16940 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16941 wq.mapped_q1(),
16942 wk.mapped_q1(),
16943 wv.mapped_q1(),
16944 wo.mapped_q1(),
16945 ) {
16946 let gm = gm.clone();
16947 let mut out = vec![0f32; hs];
16948 let cache = &self.kv_cache.layers[li];
16949 if crate::gpu::attn_dropin(
16950 &gm,
16951 self.graph_kv_id,
16952 li,
16953 &self.ws.n1,
16954 qi,
16955 ki,
16956 vi,
16957 oi,
16958 q_norm.as_deref(),
16959 k_norm.as_deref(),
16960 self.qk_norm_after_rope,
16961 &inv_freq_l,
16962 nh,
16963 nkv_l,
16964 hd_l,
16965 rd_l,
16966 hs,
16967 position,
16968 self.kv_cache.max_seq_len,
16969 gemma,
16970 eps as f32,
16971 cache.k_heads(),
16972 cache.v_heads(),
16973 &mut out,
16974 ) {
16975 break 'attn out;
16976 }
16977 }
16978 }
16979 let masked = task_mask
16980 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16981 .unwrap_or(false);
16982 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16983 let plain = self.layer_attn_plain(li);
16986 match (masked, f32_view) {
16987 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16990 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16991 attention::multi_head_attention(
16992 &self.ws.n1,
16993 q,
16994 k,
16995 v,
16996 o,
16997 &mut self.kv_cache.layers[li],
16998 self.num_heads,
16999 self.num_kv_heads,
17000 self.head_dim,
17001 self.hidden_size,
17002 position,
17003 &active_heads,
17004 &self.inv_freq,
17005 )
17006 }
17007 (masked, _) => {
17008 if masked {
17009 tracing::warn!(
17010 "layer {li}: head mask on quantized weights or on a \
17011 window/sink/per-layer-geometry layer not supported \
17012 yet — executing dense"
17013 );
17014 }
17015 let inv_freq_l = self.layer_inv_freq(li);
17016 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17017 let cfg = QwenAttnCfg {
17018 num_heads: self.layer_num_heads(li),
17019 num_kv_heads: nkv_l,
17020 head_dim: hd_l,
17021 hidden_size: hs,
17022 position,
17023 inv_freq: &inv_freq_l,
17024 rotary_dim: rd_l,
17025 scale: self.attn_scale,
17026 softcap: self.attn_softcap,
17027 window: self.layer_window(li),
17028 v_norm: self.attn_v_norm,
17029 qk_norm_after_rope: self.qk_norm_after_rope,
17030 gate_sigmoid: self.proj_gate_sigmoid,
17031 q_norm: q_norm.as_deref(),
17032 k_norm: k_norm.as_deref(),
17033 output_gate: *output_gate,
17034 softplus_gate: softplus_gate
17035 .as_ref()
17036 .map(|(gate, per_head)| (gate, *per_head)),
17037 rope_scale: self.layer_rope_scale(li),
17038 bias: bias
17039 .as_ref()
17040 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17041 rms_eps: eps,
17042 norm_style: self.norm_style,
17043 pool: pool.as_deref(),
17044 v_head_dim: self.layer_v_dim(li),
17045 };
17046 attention::qwen_attention(
17047 &self.ws.n1,
17048 wq,
17049 wk,
17050 wv,
17051 wo,
17052 &mut self.kv_cache.layers[li],
17053 &cfg,
17054 )
17055 }
17056 }
17057 }
17058 };
17059 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17062 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17063 None => attn_out,
17064 };
17065 let lw = &self.weights.layers[self.phys_layer(li)];
17066 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17067 inference::add_rmsnorm_fused_into(
17068 &mut h,
17069 &attn_out,
17070 &lw.post_norm,
17071 self.rms_eps,
17072 self.norm_style,
17073 &mut self.ws.p1,
17074 );
17075 drop(prof);
17076 let mut attn_out = attn_out;
17077 attention::recycle_buf(&mut attn_out);
17078 let post_normed = &self.ws.p1;
17079
17080 let ffn_masked = task_mask
17081 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17082 .unwrap_or(false);
17083 let ffn_out = match (ffn_masked, &lw.ffn) {
17095 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17099 let row = task_mask
17100 .and_then(|tm| tm.ffn_masks.get(li))
17101 .map(|v| v.as_slice());
17102 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17103 }
17104 (true, FfnKind::Dense(d)) => {
17105 let tm = task_mask.unwrap();
17106 let alive = tm.ffn_active_count(li);
17107 let deep = alive * 2 <= self.intermediate_size;
17108 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17109 let active = tm.ffn_active_indices(li);
17110 sparse_ffn_quant(
17111 d,
17112 post_normed,
17113 &active,
17114 self.hidden_size,
17115 self.pool.as_deref(),
17116 )
17117 } else if deep
17118 && let (Some(g), Some(u), Some(dn)) = (
17119 d.gate_proj.as_f32(),
17120 d.up_proj.as_f32(),
17121 d.down_proj.as_f32(),
17122 )
17123 {
17124 let active = tm.ffn_active_indices(li);
17125 inference::sparse_ffn_forward(
17126 post_normed,
17127 g,
17128 u,
17129 dn,
17130 self.hidden_size,
17131 self.intermediate_size,
17132 &active,
17133 self.pool.as_deref(),
17134 )
17135 } else {
17136 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17137 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17138 }
17139 }
17140 (true, FfnKind::Moe(m)) => {
17141 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17145 ffn_forward(
17146 &lw.ffn,
17147 post_normed,
17148 self.pool.as_deref(),
17149 allowed.as_deref(),
17150 )
17151 }
17152 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17153 dm,
17154 post_normed,
17155 &h,
17156 self.rms_eps,
17157 self.norm_style,
17158 self.pool.as_deref(),
17159 ),
17160 (false, _) => match &lw.ffn {
17161 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17162 dm,
17163 post_normed,
17164 &h,
17165 self.rms_eps,
17166 self.norm_style,
17167 self.pool.as_deref(),
17168 ),
17169 FfnKind::Moe(m)
17170 if task_mask.is_none()
17171 && host_tail
17172 && li >= tail_start
17173 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17174 {
17175 moe_ffn_minicpm_tail(
17176 &mut self.minicpm_tail,
17177 li,
17178 self.num_layers,
17179 m,
17180 post_normed,
17181 self.pool.as_deref(),
17182 )
17183 }
17184 FfnKind::Moe(m)
17185 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17186 {
17187 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17188 }
17189 _ => {
17190 let allowed = match (&lw.ffn, task_mask) {
17191 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17192 _ => None,
17193 };
17194 ffn_forward(
17195 &lw.ffn,
17196 post_normed,
17197 self.pool.as_deref(),
17198 allowed.as_deref(),
17199 )
17200 }
17201 },
17202 };
17203 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17204 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17205 None => ffn_out,
17206 };
17207 for (i, &f) in ffn_out.iter().enumerate() {
17208 h[i] += f;
17209 }
17210 let mut ffn_out = ffn_out;
17211 attention::recycle_buf(&mut ffn_out);
17212
17213 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17215 for v in h.iter_mut() {
17216 *v *= sc;
17217 }
17218 }
17219 if self.layer_dump.is_some() {
17221 self.dump_layer_row(position, li, &h);
17222 }
17223
17224 if self.is_loop_end(li) && li + 1 < self.num_layers {
17227 h = inference::rms_norm(
17228 &h,
17229 &self.weights.final_norm,
17230 self.rms_eps,
17231 self.norm_style,
17232 );
17233 }
17234
17235 if self.dyn_phi_layer == Some(li) {
17239 self.update_dyn_phi(&h);
17240 }
17241 }
17242 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17244 crate::gpu::graph_race_record(false, t.elapsed());
17245 }
17246
17247 h
17248 }
17249
17250 fn update_dyn_phi(&mut self, h: &[f32]) {
17253 const A: f32 = 0.2;
17254 if self.dyn_phi_ema.len() != h.len() {
17255 self.dyn_phi_ema = vec![0.0; h.len()];
17256 self.dyn_phi_seen = 0;
17257 }
17258 if self.dyn_phi_seen == 0 {
17259 self.dyn_phi_ema.copy_from_slice(h);
17260 } else {
17261 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17262 *e = (1.0 - A) * *e + A * v;
17263 }
17264 }
17265 self.dyn_phi_seen += 1;
17266 }
17267
17268 pub fn dyn_phi(&self) -> &[f32] {
17270 &self.dyn_phi_ema
17271 }
17272
17273 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17275 self.dyn_phi_layer = layer;
17276 self.dyn_phi_ema.clear();
17277 self.dyn_phi_seen = 0;
17278 }
17279
17280 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17282 let Some(model) = &self.model else {
17283 return Vec::new();
17284 };
17285 model
17286 .header
17287 .skills
17288 .iter()
17289 .enumerate()
17290 .filter_map(|(i, sk)| {
17291 if sk.is_v2() {
17295 return None;
17296 }
17297 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17298 let sel = sk.selection.as_ref()?;
17299 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17300 })
17301 .collect()
17302 }
17303
17304 pub fn active_skill(&self) -> Option<usize> {
17306 self.dyn_active
17307 }
17308
17309 pub fn enable_dynamic_routing(&mut self) -> usize {
17314 use crate::swarm::{DynRouter, RoutableSkill};
17315 let Some(model) = self.model.clone() else {
17316 return 0;
17317 };
17318 if let Some(r) = &model.header.router {
17325 tracing::warn!(
17326 "dynamic routing disabled: this file declares router policy '{}' with \
17327 granularity \"{}\" — the request-level decision applies instead",
17328 r.policy,
17329 r.granularity
17330 );
17331 return 0;
17332 }
17333 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17339 || model.header.skills.iter().any(|s| s.is_v2())
17340 {
17341 tracing::warn!(
17342 "dynamic routing disabled: this file carries format-v2 skill records \
17343 (SKILLS_V2) — they route per request through a router policy only"
17344 );
17345 return 0;
17346 }
17347 if self.dyn_blend_loaded {
17350 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17351 return 0;
17352 }
17353 if let Some(a) = self.dyn_active {
17357 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17358 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17359 return 0;
17360 }
17361 }
17362 let hidden = self.hidden_size;
17363 let mut skills = Vec::new();
17364 for (idx, id, _phi) in self.dynamic_skills() {
17365 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17366 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17367 skills.push(rs);
17368 }
17369 }
17370 }
17371 if skills.is_empty() {
17372 return 0;
17373 }
17374 let phi = skills[0].phi_layer;
17376 if skills.iter().any(|s| s.phi_layer != phi) {
17377 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17378 }
17379 let n = skills.len();
17380 self.set_dyn_phi_layer(Some(phi));
17381 self.dyn_router = Some(DynRouter::new(skills));
17382 n
17383 }
17384
17385 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17387 self.dyn_router
17388 .as_ref()
17389 .map(|r| r.switches.clone())
17390 .unwrap_or_default()
17391 }
17392
17393 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17396 let _mimo_q8 = self
17397 .mimo_moe
17398 .is_on()
17399 .then(crate::qtensor::enter_full_gpu_q8_scope);
17400 let rows = self.weights.lm_head.rows();
17401 let mut logits = attention::take_buf(rows.min(self.vocab_size));
17402 let served = self.mimo_moe.is_on()
17406 && crate::gpu::mimo_q8_short_enabled()
17407 && rows == self.vocab_size
17408 && !self.weights.lm_head.has_prism_contract()
17409 && self
17410 .weights
17411 .lm_head
17412 .graph_weight()
17413 .is_some_and(|(model, idx, kind, _)| {
17414 kind == 7
17415 && crate::gpu::q82_short_rows(
17416 model,
17417 idx,
17418 hidden,
17419 1,
17420 rows,
17421 self.hidden_size,
17422 &mut logits,
17423 )
17424 });
17425 if !served {
17426 self.weights
17427 .lm_head
17428 .matvec(hidden, &mut logits, self.pool.as_deref());
17429 }
17430 logits.resize(self.vocab_size, 0.0);
17431 if let Some(m) = self.logit_multiplier {
17432 for l in logits.iter_mut() {
17433 *l *= m;
17434 }
17435 }
17436 if let Some(c) = self.final_softcap {
17437 softcap_in_place(&mut logits, c, self.pool.as_deref());
17438 }
17439 if let Some(cm) = self.head_clusters.as_ref() {
17440 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
17441 }
17442 logits
17443 }
17444
17445 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
17448 let h = hidden.len();
17449 let ncl = cm.len() / h.max(1);
17450 if ncl == 0 || logits.len() % ncl != 0 {
17451 return;
17452 }
17453 let cs = logits.len() / ncl;
17454 let mut lc = vec![0.0f32; ncl];
17456 for c in 0..ncl {
17457 let row = &cm[c * h..(c + 1) * h];
17458 let mut s = 0.0f32;
17459 for j in 0..h {
17460 s += row[j] * hidden[j];
17461 }
17462 lc[c] = s;
17463 }
17464 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17465 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
17466 for c in 0..ncl {
17467 let blk = &mut logits[c * cs..(c + 1) * cs];
17468 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17469 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
17470 let add = lc[c] - lse - bl;
17471 for v in blk.iter_mut() {
17472 *v += add;
17473 }
17474 }
17475 }
17476
17477 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
17482 #[cfg(target_os = "macos")]
17483 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
17484 self.clear_sequence_state();
17485 crate::gpu::graph_race_begin_generation();
17489 if task_mask.is_none() {
17490 self.o1_begin();
17491 }
17492 let mut hidden = vec![0.0f32; self.hidden_size];
17493 for (pos, &id) in ids.iter().enumerate() {
17494 let emb = self.embed_single(id);
17495 hidden = self.forward_layers(&emb, pos, task_mask);
17496 }
17497 if let Err(err) = self.o1_seal_checked() {
17498 self.o1_fail(err);
17499 }
17500 if let Some(logits) = self.graph_logits.take() {
17503 return logits;
17504 }
17505 inference::rms_norm_into(
17506 &hidden,
17507 &self.weights.final_norm,
17508 self.rms_eps,
17509 self.norm_style,
17510 &mut self.ws.n1,
17511 );
17512 self.lm_head_forward(&self.ws.n1)
17513 }
17514}
17515
17516pub fn create_test_pipeline(
17518 hidden_size: usize,
17519 intermediate_size: usize,
17520 num_heads: usize,
17521 num_kv_heads: usize,
17522 head_dim: usize,
17523 num_layers: usize,
17524 vocab_size: usize,
17525) -> Pipeline {
17526 let synth = |n: usize, salt: usize| -> Vec<f32> {
17529 (0..n)
17530 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
17531 .collect()
17532 };
17533 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
17534 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17535 };
17536 let layer_weights: Vec<LayerWeights> = (0..num_layers)
17537 .map(|li| LayerWeights {
17538 input_norm: vec![1.0; hidden_size],
17539 post_norm: vec![1.0; hidden_size],
17540 attn_out_norm: None,
17541 ffn_out_norm: None,
17542 layer_scale: None,
17543 ffn: FfnKind::Dense(DenseFfn {
17544 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
17545 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
17546 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
17547 act: Act::Silu,
17548 down_t: None,
17549 segs: Vec::new(),
17550 }),
17551 attn: AttnKind::Full {
17552 bias: None,
17553 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
17554 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
17555 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
17556 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
17557 q_norm: None,
17558 k_norm: None,
17559 output_gate: false,
17560 softplus_gate: None,
17561 },
17562 })
17563 .collect();
17564
17565 Pipeline::new(
17566 Tokenizer::byte_level(),
17567 PipelineWeights {
17568 embed_tokens: qt(vocab_size, hidden_size, 100),
17569 layers: layer_weights,
17570 lm_head: qt(vocab_size, hidden_size, 200),
17571 final_norm: vec![1.0; hidden_size],
17572 },
17573 hidden_size,
17574 intermediate_size,
17575 num_heads,
17576 num_kv_heads,
17577 head_dim,
17578 num_layers,
17579 num_layers, false, vocab_size,
17582 1e-6,
17583 10_000.0,
17584 NormStyle::Qwen,
17585 4096,
17586 SamplerConfig {
17587 seed: Some(42),
17588 ..Default::default()
17589 },
17590 )
17591}
17592
17593#[inline]
17598fn mask_bit(row: &[u8], j: usize) -> bool {
17599 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
17600}
17601
17602fn mask_gain() -> f32 {
17613 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17614 *G.get_or_init(|| {
17615 std::env::var("CMF_FFN_MASK_GAIN")
17616 .ok()
17617 .and_then(|v| v.parse().ok())
17618 .unwrap_or(1.0)
17619 })
17620}
17621
17622fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
17623 let fill = meanfill().and_then(|(i, v)| {
17626 let li = crate::gpu::cur_layer();
17627 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
17628 });
17629 for r in 0..rows {
17630 let base = r * inter;
17631 for (bi, &byte) in row.iter().enumerate() {
17632 if byte == 0xFF {
17633 continue;
17634 }
17635 let j0 = bi * 8;
17636 for bit in 0..8 {
17637 let j = j0 + bit;
17638 if j < inter && byte & (1 << bit) == 0 {
17639 g[base + j] = fill.map_or(0.0, |f| f[j]);
17640 }
17641 }
17642 }
17643 }
17644 let gain = mask_gain();
17645 if gain != 1.0 {
17646 for v in g[..rows * inter].iter_mut() {
17647 *v *= gain;
17648 }
17649 }
17650}
17651
17652#[inline]
17654fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
17655 row.is_none_or(|r| mask_bit(r, i))
17656}
17657
17658fn all_bits_on(row: &[u8], n: usize) -> bool {
17661 (0..n).all(|i| mask_bit(row, i))
17662}
17663
17664fn tube_topk() -> usize {
17672 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17673 *K.get_or_init(|| {
17674 std::env::var("CMF_TUBE_TOPK")
17675 .ok()
17676 .and_then(|v| v.parse().ok())
17677 .unwrap_or(0)
17678 })
17679}
17680
17681fn tube_score_oracle() -> bool {
17682 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17683 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
17684}
17685
17686fn tube_ffn_routed(
17693 d: &DenseFfn,
17694 xs: &[f32],
17695 b: usize,
17696 pool: Option<&Pool>,
17697 mask_row: Option<&[u8]>,
17698 k: usize,
17699) -> Vec<f32> {
17700 let hidden = d.down_proj.rows();
17701 let core = d.gate_proj.rows();
17702 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17703 let mut out = match (b, core_full, mask_row) {
17704 (1, true, _) => dense_ffn(d, xs, pool),
17705 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17706 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17707 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17708 };
17709 let cand: Vec<usize> = (0..d.segs.len())
17710 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
17711 .collect();
17712 if cand.is_empty() {
17713 return out;
17714 }
17715 let oracle = tube_score_oracle();
17719 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
17720 let mut scores = vec![0f32; b * cand.len()];
17721 for (ci, &i) in cand.iter().enumerate() {
17722 let seg = &d.segs[i];
17723 let w = seg.width;
17724 let mut g = vec![0.0f32; b * w];
17725 if b == 1 {
17726 seg.gate.matvec(xs, &mut g, pool);
17727 } else {
17728 seg.gate.matmat(xs, b, &mut g, pool);
17729 }
17730 for v in g.iter_mut() {
17731 *v = Act::Silu.combine(*v, 1.0);
17732 }
17733 if !oracle {
17734 for t in 0..b {
17735 scores[t * cand.len() + ci] =
17736 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17737 }
17738 }
17739 if oracle || b > 1 {
17740 let mut u = vec![0.0f32; b * w];
17741 if b == 1 {
17742 seg.up.matvec(xs, &mut u, pool);
17743 } else {
17744 seg.up.matmat(xs, b, &mut u, pool);
17745 }
17746 for (a, &v) in g.iter_mut().zip(u.iter()) {
17747 *a *= v;
17748 }
17749 if oracle {
17750 for t in 0..b {
17751 scores[t * cand.len() + ci] =
17752 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
17753 }
17754 }
17755 }
17756 acts.push(g);
17757 }
17758 let keep = k.min(cand.len());
17760 let mut scratch: Vec<f32> = Vec::new();
17761 for t in 0..b {
17762 let mut sc: Vec<(f32, usize)> = (0..cand.len())
17763 .map(|ci| (scores[t * cand.len() + ci], ci))
17764 .collect();
17765 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
17766 let mut alive = vec![false; cand.len()];
17767 for &(_, ci) in sc.iter().take(keep) {
17768 alive[ci] = true;
17769 }
17770 if b > 1 {
17771 for (ci, a) in acts.iter_mut().enumerate() {
17772 if !alive[ci] {
17773 let w = d.segs[cand[ci]].width;
17774 a[t * w..(t + 1) * w].fill(0.0);
17775 }
17776 }
17777 } else {
17778 for (ci, &i) in cand.iter().enumerate() {
17782 if !alive[ci] {
17783 continue;
17784 }
17785 let seg = &d.segs[i];
17786 let w = seg.width;
17787 let g = &mut acts[ci];
17788 if !tube_score_oracle() {
17789 scratch.clear();
17790 scratch.resize(w, 0.0);
17791 seg.up.matvec(xs, &mut scratch, pool);
17792 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
17793 *a *= v;
17794 }
17795 }
17796 let mut acc = vec![0.0f32; hidden];
17797 seg.down.matvec(g, &mut acc, pool);
17798 for (o, a) in out.iter_mut().zip(&acc) {
17799 *o += *a;
17800 }
17801 }
17802 }
17803 }
17804 if b > 1 {
17805 for (ci, &i) in cand.iter().enumerate() {
17806 let seg = &d.segs[i];
17807 let mut acc = vec![0.0f32; b * hidden];
17808 seg.down.matmat(&acts[ci], b, &mut acc, pool);
17809 for (o, a) in out.iter_mut().zip(&acc) {
17810 *o += *a;
17811 }
17812 }
17813 }
17814 out
17815}
17816
17817fn tube_ffn(
17823 d: &DenseFfn,
17824 xs: &[f32],
17825 b: usize,
17826 pool: Option<&Pool>,
17827 mask_row: Option<&[u8]>,
17828) -> Vec<f32> {
17829 if tube_topk() > 0 {
17830 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
17831 }
17832 let hidden = d.down_proj.rows();
17833 let core = d.gate_proj.rows();
17834 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
17835 let mut out = match (b, core_full, mask_row) {
17836 (1, true, _) => dense_ffn(d, xs, pool),
17837 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
17838 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
17839 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
17840 };
17841 TUBE_SCRATCH.with(|sc| {
17842 let mut sc = sc.borrow_mut();
17843 let [g, u, acc] = &mut *sc;
17844 for seg in &d.segs {
17845 if !tube_bit(mask_row, seg.start) {
17846 continue;
17847 }
17848 let w = seg.width;
17849 g.resize(b * w, 0.0);
17850 if b == 1
17851 && d.act == Act::Silu
17852 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17853 {
17854 } else {
17856 u.resize(b * w, 0.0);
17857 if b == 1 {
17858 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17859 } else {
17860 seg.gate.matmat(xs, b, g, pool);
17861 seg.up.matmat(xs, b, u, pool);
17862 }
17863 for i in 0..b * w {
17864 g[i] = d.act.combine(g[i], u[i]);
17865 }
17866 }
17867 acc.resize(b * hidden, 0.0);
17868 acc.fill(0.0);
17869 if b == 1 {
17870 seg.down.matvec(g, acc, pool);
17871 } else {
17872 seg.down.matmat(g, b, acc, pool);
17873 }
17874 for (o, a) in out.iter_mut().zip(acc.iter()) {
17875 *o += *a;
17876 }
17877 }
17878 out
17879 })
17880}
17881
17882thread_local! {
17883 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17887 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17888}
17889
17890static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
17893 std::sync::atomic::AtomicU64::new(0),
17894 std::sync::atomic::AtomicU64::new(0),
17895];
17896
17897fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17901 #[cfg(feature = "gpu")]
17902 {
17903 use std::sync::atomic::Ordering::Relaxed;
17904 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17905 (
17906 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17907 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17908 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17909 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17910 )
17911 }
17912 #[cfg(not(feature = "gpu"))]
17913 (0.0, 0.0, 0.0, 0)
17914}
17915
17916fn wgpu_mirror_counters() -> [u64; 6] {
17919 #[cfg(feature = "gpu")]
17920 {
17921 crate::gpu_wgpu::MIRROR_EVENTS
17922 .each_ref()
17923 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17924 }
17925 #[cfg(not(feature = "gpu"))]
17926 [0; 6]
17927}
17928
17929fn prefill_prof_on() -> bool {
17930 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17931 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17932}
17933
17934fn dense_ffn_batch(
17935 d: &DenseFfn,
17936 xs: &[f32],
17937 b: usize,
17938 pool: Option<&Pool>,
17939 mask_row: Option<&[u8]>,
17940) -> Vec<f32> {
17941 let inter = d.gate_proj.rows();
17942 let hidden = d.down_proj.rows();
17943 let fused_act = d.act.wgpu_graph_act();
17955 if mask_row.is_none()
17956 && fused_act.is_some()
17957 && b >= 32
17958 && crate::gpu::enabled_here()
17959 && !crate::gpu::mm_killed()
17960 && refit_dir().is_none()
17965 && !ffn_probe_active()
17970 && !crate::gptq_capture::capturing()
17974 {
17975 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17976 d.gate_proj.mapped_q4t(),
17977 d.up_proj.mapped_q4t(),
17978 d.down_proj.mapped_q4t(),
17979 ) {
17980 let mut out = vec![0.0f32; b * hidden];
17981 let act = fused_act.expect("checked above");
17982 if crate::gpu::q4_ffn_act(
17983 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
17984 ) {
17985 return out;
17986 }
17987 }
17988 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17993 d.gate_proj.mapped_q4tp(),
17994 d.up_proj.mapped_q4tp(),
17995 d.down_proj.mapped_q4tp(),
17996 ) {
17997 let mut out = vec![0.0f32; b * hidden];
17998 let act = fused_act.expect("checked above");
17999 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18000 {
18001 return out;
18002 }
18003 }
18004 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18010 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18011 d.gate_proj.mapped_device_gemm(),
18012 d.up_proj.mapped_device_gemm(),
18013 d.down_proj.mapped_device_gemm(),
18014 ) {
18015 let mut out = vec![0.0f32; b * hidden];
18016 let act = fused_act.expect("checked above");
18017 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18018 return out;
18019 }
18020 }
18021 }
18022 let mut g = vec![0.0f32; b * inter];
18023 d.gate_proj.matmat(xs, b, &mut g, pool);
18024 let mut u = vec![0.0f32; b * inter];
18025 d.up_proj.matmat(xs, b, &mut u, pool);
18026 if gate_topk() > 0 && d.act == Act::Silu {
18027 for t in 0..b {
18028 let row = &mut g[t * inter..(t + 1) * inter];
18029 for v in row.iter_mut() {
18030 *v = Act::Silu.combine(*v, 1.0);
18031 }
18032 keep_top_k(row, gate_topk());
18033 }
18034 for i in 0..b * inter {
18035 g[i] *= u[i];
18036 }
18037 } else {
18038 for i in 0..b * inter {
18039 g[i] = d.act.combine(g[i], u[i]);
18040 }
18041 }
18042 if let Some(row) = mask_row {
18043 zero_masked_cols(&mut g, b, inter, row);
18044 }
18045 if oracle_topk() > 0 {
18046 for t in 0..b {
18047 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18048 }
18049 }
18050 let mut out = vec![0.0f32; b * hidden];
18051 d.down_proj.matmat(&g, b, &mut out, pool);
18052 if refit_dir().is_some() {
18053 let li = crate::gpu::cur_layer();
18054 if li >= 0 {
18055 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18056 }
18057 }
18058 FFN_PROBE.with(|pr| {
18062 if let Some(acc) = pr.borrow_mut().as_mut() {
18063 let li = crate::gpu::cur_layer();
18064 if li < 0 {
18065 return;
18066 }
18067 let Some(row) = acc.get_mut(li as usize) else {
18068 return;
18069 };
18070 let sq = probe_sq();
18071 for t in 0..b {
18072 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18073 *a += if sq {
18074 (v as f64) * (v as f64)
18075 } else {
18076 (v as f64).abs()
18077 };
18078 }
18079 }
18080 }
18081 });
18082 out
18083}
18084
18085fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18090 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18091 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18092 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18093 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18094 if (!on && !dump) || b == 0 {
18095 return;
18096 }
18097 let hidden = xs.len() / b;
18098 if on {
18099 let mut acc = m.act_sq.borrow_mut();
18100 if acc.len() < hidden {
18101 acc.resize(hidden, 0.0);
18102 }
18103 for t in 0..b {
18104 let row = &xs[t * hidden..(t + 1) * hidden];
18105 for (a, &v) in acc.iter_mut().zip(row) {
18106 *a += (v as f64) * (v as f64);
18107 }
18108 }
18109 }
18110 if dump {
18111 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18114 .ok()
18115 .and_then(|v| v.parse().ok())
18116 .unwrap_or(4096);
18117 let mut rows = m.act_rows.borrow_mut();
18118 if rows.len() < cap * hidden {
18119 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18120 rows.extend_from_slice(&xs[..take * hidden]);
18121 }
18122 }
18123}
18124
18125#[derive(Clone, Copy)]
18128struct SendVecs(*mut Vec<f32>);
18129unsafe impl Send for SendVecs {}
18130unsafe impl Sync for SendVecs {}
18131impl SendVecs {
18132 #[inline]
18133 fn at(self, i: usize) -> *mut Vec<f32> {
18134 unsafe { self.0.add(i) }
18135 }
18136}
18137
18138fn moe_ffn_batch(
18139 m: &MoeFfn,
18140 xs: &[f32],
18141 b: usize,
18142 hidden: usize,
18143 pool: Option<&Pool>,
18144 allowed: Option<&[bool]>,
18145) -> Vec<f32> {
18146 accumulate_act(m, xs, b);
18147 let ne = m.experts.len();
18148 let mut logits = vec![0.0f32; b * ne];
18149 match &m.resonance {
18150 Some(r) => {
18151 let hdim = xs.len() / b.max(1);
18152 for bi in 0..b {
18153 r.scores(
18154 &xs[bi * hdim..(bi + 1) * hdim],
18155 &mut logits[bi * ne..(bi + 1) * ne],
18156 );
18157 }
18158 }
18159 None => m.router.matmat(xs, b, &mut logits, pool),
18160 }
18161
18162 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18165 {
18166 let mut st = m.stats.borrow_mut();
18167 if st.len() < ne {
18168 st.resize(ne, 0);
18169 }
18170 for bi in 0..b {
18171 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18172 for &e in &idx {
18173 st[e] += 1;
18174 assign[e].push((bi, p[e] / wsum));
18175 }
18176 }
18177 }
18178
18179 let mut out = vec![0.0f32; b * hidden];
18180 let cols = m.experts[0].gate_proj.cols();
18181 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18182 let sb = list.len();
18183 let mut sub = vec![0.0f32; sb * cols];
18184 for (k, &(bi, _)) in list.iter().enumerate() {
18185 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18186 }
18187 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18188 for (k, &(bi, w)) in list.iter().enumerate() {
18189 for i in 0..hidden {
18190 out[bi * hidden + i] += w * eo[k * hidden + i];
18191 }
18192 }
18193 };
18194 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18200 if pool.is_some() && active.len() >= 8 {
18201 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18202 {
18203 let panel_ptr = SendVecs(panels.as_mut_ptr());
18204 let experts = &m.experts;
18207 let (active_r, assign_r) = (&active, &assign);
18208 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18209 let run = |start: usize, end: usize| {
18210 let _cpu_scope = inherit_cpu();
18211 #[cfg(target_os = "macos")]
18219 let _metal_cpu = crate::gpu::enter_cpu_scope();
18220 for ai in start..end {
18221 let e = active_r[ai];
18222 let list = &assign_r[e];
18223 let sb = list.len();
18224 let mut sub = vec![0.0f32; sb * cols];
18225 for (k, &(bi, _)) in list.iter().enumerate() {
18226 sub[k * cols..(k + 1) * cols]
18227 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18228 }
18229 unsafe {
18231 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18232 }
18233 }
18234 };
18235 match pool {
18236 Some(p) => p.run_rows(active.len(), &run),
18237 None => run(0, active.len()),
18238 }
18239 }
18240 for (ai, &e) in active.iter().enumerate() {
18241 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18242 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18243 for i in 0..hidden {
18244 out[bi * hidden + i] += w * eo[i];
18245 }
18246 }
18247 }
18248 } else {
18249 for &e in &active {
18250 run_expert(&m.experts[e], &assign[e], &mut out);
18251 }
18252 }
18253 if let Some((se, gate)) = &m.shared {
18254 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18255 let mut gl = vec![0.0f32; b];
18256 gate.matmat(xs, b, &mut gl, pool);
18257 (0..b)
18258 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18259 .collect()
18260 } else {
18261 (0..b).map(|bi| (bi, 1.0)).collect()
18262 };
18263 run_expert(se, &all, &mut out);
18264 }
18265 out
18266}
18267
18268fn moe_ffn_rows_exact(
18280 m: &MoeFfn,
18281 xs: &[f32],
18282 b: usize,
18283 hidden: usize,
18284 pool: Option<&Pool>,
18285) -> Vec<f32> {
18286 let mut out = vec![0.0f32; b * hidden];
18287 let per_row = |out: &mut [f32]| {
18288 for r in 0..b {
18289 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18290 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18291 }
18292 };
18293 let covered = !crate::gpu::enabled_here()
18294 && moe_batch_enabled()
18295 && m.shared.is_none()
18296 && m.resonance.is_none()
18297 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18298 && m.experts.iter().all(|d| d.act == Act::Silu);
18299 if !covered {
18300 per_row(&mut out);
18301 return out;
18302 }
18303 let ne = m.experts.len();
18304 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18306 for r in 0..b {
18307 let x = &xs[r * hidden..(r + 1) * hidden];
18308 accumulate_act(m, x, 1);
18309 let mut logits = vec![0.0f32; ne];
18310 m.router.matvec(x, &mut logits, pool);
18311 let (idx, p, wsum) = moe_route(&logits, m, None);
18312 {
18313 let mut st = m.stats.borrow_mut();
18314 if st.len() < ne {
18315 st.resize(ne, 0);
18316 }
18317 for &e in &idx {
18318 st[e] += 1;
18319 }
18320 }
18321 let w: Vec<f32> = idx
18322 .iter()
18323 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18324 .collect();
18325 routes.push((idx, w));
18326 }
18327 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18328 per_row(&mut out);
18329 return out;
18330 }
18331 let mut experts: Vec<usize> = Vec::new();
18333 let mut groups: Vec<Vec<usize>> = Vec::new();
18334 for (r, (idx, _)) in routes.iter().enumerate() {
18335 for &e in idx {
18336 match experts.iter().position(|&x| x == e) {
18337 Some(g) => groups[g].push(r),
18338 None => {
18339 experts.push(e);
18340 groups.push(vec![r]);
18341 }
18342 }
18343 }
18344 }
18345 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
18346 let inter = m.experts[experts[0]].gate_proj.rows();
18347 let pairs: Vec<(&QTensor, &QTensor)> = experts
18348 .iter()
18349 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
18350 .collect();
18351 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
18352 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
18353 per_row(&mut out);
18354 return out;
18355 }
18356 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
18357 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
18358 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
18359 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
18360 per_row(&mut out);
18361 return out;
18362 }
18363 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
18365 let mut p = 0usize;
18366 for (g, &e) in experts.iter().enumerate() {
18367 for &r in &groups[g] {
18368 slot.insert((r, e), p);
18369 p += 1;
18370 }
18371 }
18372 for (r, (idx, w)) in routes.iter().enumerate() {
18373 let terms: Vec<(&[f32], f32)> = idx
18374 .iter()
18375 .zip(w)
18376 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
18377 .collect();
18378 let row = &mut out[r * hidden..(r + 1) * hidden];
18379 for (i, dst) in row.iter_mut().enumerate() {
18380 let mut acc = 0f32;
18382 for (d, we) in &terms {
18383 acc += we * d[i];
18384 }
18385 *dst = acc;
18386 }
18387 }
18388 out
18389}
18390
18391pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
18395 match pool {
18396 Some(p) if v.len() >= 1 << 15 => {
18397 let base = v.as_mut_ptr() as usize;
18398 p.run_rows(v.len(), &|s, e| {
18399 let sl =
18402 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
18403 for x in sl {
18404 *x = c * (*x / c).tanh();
18405 }
18406 });
18407 }
18408 _ => {
18409 for x in v.iter_mut() {
18410 *x = c * (*x / c).tanh();
18411 }
18412 }
18413 }
18414}
18415
18416thread_local! {
18417 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
18421 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
18422}
18423
18424fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18426 if gate_topk() > 0
18429 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
18430 {
18431 return out;
18432 }
18433 let prism_body = d.gate_proj.has_prism_contract()
18449 || d.up_proj.has_prism_contract()
18450 || d.down_proj.has_prism_contract();
18451 if !prism_body
18452 && crate::gpu::enabled_here()
18453 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
18454 {
18455 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
18456 crate::gpu::ProbeArm::Gpu
18457 } else {
18458 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
18459 };
18460 match arm {
18461 crate::gpu::ProbeArm::Gpu => {
18462 let t0 = std::time::Instant::now();
18463 if let Some(out) = dense_ffn_gpu(d, x, pool) {
18464 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18465 return out;
18466 }
18467 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
18471 }
18472 crate::gpu::ProbeArm::CpuTimed => {
18473 let t0 = std::time::Instant::now();
18474 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18475 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18476 return out;
18477 }
18478 crate::gpu::ProbeArm::Cpu => {
18479 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18480 }
18481 }
18482 }
18483 dense_ffn_cpu(d, x, pool)
18484}
18485
18486fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18488 let inter = d.gate_proj.rows();
18489 FFN_SCRATCH.with(|s| {
18490 let mut s = s.borrow_mut();
18491 let [g, u, ..] = &mut *s;
18492 g.resize(inter, 0.0);
18493 if gate_topk() > 0 {
18496 u.resize(inter, 0.0);
18500 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18501 for i in 0..inter {
18502 g[i] = Act::Silu.combine(g[i], 1.0);
18503 }
18504 keep_top_k(g, gate_topk());
18505 for i in 0..inter {
18506 g[i] *= u[i];
18507 }
18508 } else if d.act == Act::Silu && {
18509 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18510 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
18511 } {
18512 } else {
18514 u.resize(inter, 0.0);
18515 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18517 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18518 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
18519 for i in 0..inter {
18520 g[i] = d.act.combine(g[i], u[i]);
18521 }
18522 }
18523 FFN_PROBE.with(|pr| {
18531 if let Some(acc) = pr.borrow_mut().as_mut() {
18532 let li = crate::gpu::cur_layer();
18533 if li >= 0 {
18534 if let Some(row) = acc.get_mut(li as usize) {
18535 match probe_topk() {
18536 0 if probe_sq() => {
18537 for (a, &v) in row.iter_mut().zip(g.iter()) {
18538 *a += (v as f64) * (v as f64);
18539 }
18540 }
18541 0 if probe_signed() => {
18542 for (a, &v) in row.iter_mut().zip(g.iter()) {
18543 *a += v as f64;
18544 }
18545 }
18546 0 => {
18547 for (a, &v) in row.iter_mut().zip(g.iter()) {
18548 *a += (v as f64).abs();
18549 }
18550 }
18551 k => {
18552 let n = g.len();
18553 let k = k.min(n);
18554 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18555 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18556 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18557 });
18558 let thr = *kth;
18559 for (a, &v) in row.iter_mut().zip(g.iter()) {
18560 if v.abs() >= thr {
18561 *a += 1.0;
18562 }
18563 }
18564 }
18565 }
18566 }
18567 }
18568 }
18569 });
18570 if oracle_topk() > 0 {
18571 keep_top_k(g, oracle_topk());
18572 }
18573 {
18574 let li = crate::gpu::cur_layer();
18575 if li >= 0 {
18576 adump_row(li as usize, g);
18577 }
18578 }
18579 let mut out = attention::take_buf(d.down_proj.rows());
18580 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
18581 d.down_proj.matvec(g, &mut out, pool);
18582 out
18583 })
18584}
18585
18586pub struct RefitAcc {
18599 pub support: Vec<u32>,
18600 pub gss: Vec<f32>,
18601 pub ya: Vec<f32>,
18602 pub hidden: usize,
18603 pub tokens: u64,
18604 pub buf_g: Vec<f32>,
18610 pub buf_o: Vec<f32>,
18611 pub buf_t: usize,
18612}
18613
18614type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
18618
18619static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
18620 std::sync::OnceLock::new();
18621
18622fn ffn_probe_active() -> bool {
18625 FFN_PROBE.with(|p| p.borrow().is_some())
18626}
18627
18628fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
18629 REFIT
18630 .get_or_init(|| {
18631 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
18632 (
18633 d,
18634 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
18635 )
18636 })
18637 })
18638 .as_ref()
18639}
18640
18641fn refit_accumulate(
18643 li: usize,
18644 g: &[f32],
18645 b: usize,
18646 inter: usize,
18647 out: &[f32],
18648 hidden: usize,
18649 pool: Option<&Pool>,
18650) {
18651 let Some((dir, map)) = refit_dir() else {
18652 return;
18653 };
18654 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18655 let (from, to) = *SPAN.get_or_init(|| {
18656 let g = |k: &str, d: usize| {
18657 std::env::var(k)
18658 .ok()
18659 .and_then(|v| v.parse().ok())
18660 .unwrap_or(d)
18661 };
18662 (
18663 g("CMF_FFN_REFIT_FROM", 0),
18664 g("CMF_FFN_REFIT_TO", usize::MAX),
18665 )
18666 });
18667 if li < from || li > to {
18668 return;
18669 }
18670 let mut guard = map.lock().unwrap();
18671 let (map, shared) = &mut *guard;
18672 let acc = match map.entry(li) {
18673 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
18674 std::collections::hash_map::Entry::Vacant(e) => {
18675 let path = format!("{dir}/support.{li}.u32");
18676 let Ok(bytes) = std::fs::read(&path) else {
18677 eprintln!("refit: no {path} — layer {li} skipped");
18678 return;
18679 };
18680 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
18681 let support: Vec<u32> = bytes[4..4 + n * 4]
18682 .chunks_exact(4)
18683 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18684 .collect();
18685 eprintln!(
18686 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
18687 (n * n + hidden * n) as f64 * 4.0 / 1e6
18688 );
18689 e.insert(RefitAcc {
18690 gss: vec![0.0; n * n],
18691 ya: vec![0.0; hidden * n],
18692 buf_g: Vec::new(),
18693 buf_o: Vec::new(),
18694 buf_t: 0,
18695 support,
18696 hidden,
18697 tokens: 0,
18698 })
18699 }
18700 };
18701 let ns = acc.support.len();
18702 let cap = refit_batch();
18704 if acc.buf_g.is_empty() {
18705 acc.buf_g = vec![0.0; ns * cap];
18706 acc.buf_o = vec![0.0; hidden * cap];
18707 }
18708 let take = b.min(cap - acc.buf_t);
18709 for t in 0..take {
18710 let col = acc.buf_t + t;
18711 for (j, &n) in acc.support.iter().enumerate() {
18712 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
18713 }
18714 for h in 0..hidden {
18715 acc.buf_o[h * cap + col] = out[t * hidden + h];
18716 }
18717 }
18718 acc.buf_t += take;
18719 acc.tokens += take as u64;
18720 if acc.buf_t < cap {
18721 return;
18722 }
18723 let bt = acc.buf_t;
18724 acc.buf_t = 0;
18725 let RefitAcc {
18735 gss,
18736 ya,
18737 buf_g,
18738 buf_o,
18739 ..
18740 } = acc;
18741 let need = (ns * ns).max(hidden * ns);
18742 if shared.len() < need {
18743 shared.resize(need, 0.0);
18744 }
18745 let scratch = &mut shared[..];
18746 let _ = bt;
18747 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
18748 add_into(gss, &scratch[..ns * ns], pool);
18749 if crate::gpu::gemm_nt_f32_transient(
18750 buf_o,
18751 buf_g,
18752 &mut scratch[..hidden * ns],
18753 hidden,
18754 cap,
18755 ns,
18756 ) {
18757 add_into(ya, &scratch[..hidden * ns], pool);
18758 } else {
18759 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18760 }
18761 } else {
18762 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
18763 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
18764 }
18765 }
18769
18770fn refit_batch() -> usize {
18772 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18773 *B.get_or_init(|| {
18774 std::env::var("CMF_FFN_REFIT_BATCH")
18775 .ok()
18776 .and_then(|v| v.parse().ok())
18777 .unwrap_or(4096)
18778 })
18779}
18780
18781fn accum_outer_t(
18784 c: &mut [f32],
18785 m: usize,
18786 n: usize,
18787 b: usize,
18788 left: &[f32],
18789 right: &[f32],
18790 pool: Option<&Pool>,
18791) {
18792 let ptr = SendMut(c.as_mut_ptr());
18793 let body = |i: usize| {
18794 let ptr = &ptr;
18795 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
18796 for t in 0..b {
18797 let a = left[i * b + t];
18798 if a == 0.0 {
18799 continue;
18800 }
18801 for (j, o) in row.iter_mut().enumerate() {
18802 *o += a * right[j * b + t];
18803 }
18804 }
18805 };
18806 match pool {
18807 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
18808 for i in s..e {
18809 body(i);
18810 }
18811 }),
18812 _ => {
18813 for i in 0..m {
18814 body(i);
18815 }
18816 }
18817 }
18818}
18819
18820fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
18823 let n = dst.len().min(src.len());
18824 match pool {
18825 Some(p) if n >= 1 << 16 => {
18826 let ptr = SendMut(dst.as_mut_ptr());
18827 let f = |s: usize, e: usize| {
18828 let ptr = &ptr;
18829 for blk in s..e {
18830 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
18831 for i in a..b {
18832 unsafe { *ptr.0.add(i) += src[i] };
18833 }
18834 }
18835 };
18836 p.run_rows(n.div_ceil(4096), &f);
18837 }
18838 _ => {
18839 for (d, v) in dst.iter_mut().zip(&src[..n]) {
18840 *d += *v;
18841 }
18842 }
18843 }
18844}
18845
18846fn accum_outer(
18851 c: &mut [f32],
18852 m: usize,
18853 n: usize,
18854 b: usize,
18855 left: &[f32],
18856 right: &[f32],
18857 pool: Option<&Pool>,
18858) {
18859 const TILE: usize = 32;
18860 let tiles = m.div_ceil(TILE);
18861 let cp = SendMut(c.as_mut_ptr());
18862 let body = |ti: usize| {
18863 let cp = &cp;
18864 let i0 = ti * TILE;
18865 let i1 = (i0 + TILE).min(m);
18866 for t in 0..b {
18867 let r = &right[t * n..t * n + n];
18868 for i in i0..i1 {
18869 let a = left[i * b + t];
18870 if a == 0.0 {
18871 continue;
18872 }
18873 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18875 for (o, v) in row.iter_mut().zip(r) {
18876 *o += a * *v;
18877 }
18878 }
18879 }
18880 };
18881 match pool {
18882 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18883 for ti in s..e {
18884 body(ti);
18885 }
18886 }),
18887 _ => {
18888 for ti in 0..tiles {
18889 body(ti);
18890 }
18891 }
18892 }
18893}
18894
18895pub fn refit_flush() -> usize {
18897 let Some((dir, map)) = refit_dir() else {
18898 return 0;
18899 };
18900 let guard = map.lock().unwrap();
18901 let mut n = 0;
18902 for (li, acc) in guard.0.iter() {
18903 let w = |name: &str, v: &[f32]| {
18906 let path = format!("{dir}/{name}.{li}.f32");
18907 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18908 match std::fs::write(&path, &bytes) {
18909 Ok(()) => {}
18910 Err(e) => eprintln!(
18911 "refit: FAILED to write {path} ({} MB): {e}",
18912 bytes.len() / 1_000_000
18913 ),
18914 }
18915 };
18916 w("gss", &acc.gss);
18917 w("ya", &acc.ya);
18918 println!(
18919 "refit L{li}: {} support, {} tokens, hidden {}",
18920 acc.support.len(),
18921 acc.tokens,
18922 acc.hidden
18923 );
18924 n += 1;
18925 }
18926 n
18927}
18928
18929fn adump_row(li: usize, g: &[f32]) {
18934 use std::io::Write as _;
18935 static FILES: std::sync::OnceLock<
18936 Option<(
18937 String,
18938 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18939 )>,
18940 > = std::sync::OnceLock::new();
18941 let Some((prefix, map)) = FILES
18942 .get_or_init(|| {
18943 std::env::var("CMF_FFN_ADUMP")
18944 .ok()
18945 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18946 })
18947 .as_ref()
18948 else {
18949 return;
18950 };
18951 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18954 let (from, to) = *SPAN.get_or_init(|| {
18955 let g = |k: &str, d: usize| {
18956 std::env::var(k)
18957 .ok()
18958 .and_then(|v| v.parse().ok())
18959 .unwrap_or(d)
18960 };
18961 (
18962 g("CMF_FFN_ADUMP_FROM", 0),
18963 g("CMF_FFN_ADUMP_TO", usize::MAX),
18964 )
18965 });
18966 if li < from || li > to {
18967 return;
18968 }
18969 let mut map = map.lock().unwrap();
18970 let f = map.entry(li).or_insert_with(|| {
18971 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18972 });
18973 let mut bytes = Vec::with_capacity(g.len() * 2);
18974 for v in g {
18975 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18976 }
18977 let _ = f.write_all(&bytes);
18978}
18979
18980fn oracle_topk() -> usize {
18986 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18987 *K.get_or_init(|| {
18988 std::env::var("CMF_FFN_ORACLE_TOPK")
18989 .ok()
18990 .and_then(|v| v.parse().ok())
18991 .unwrap_or(0)
18992 })
18993}
18994
18995fn gate_topk() -> usize {
19001 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19002 *K.get_or_init(|| {
19003 std::env::var("CMF_FFN_GATE_TOPK")
19004 .ok()
19005 .and_then(|v| v.parse().ok())
19006 .unwrap_or(0)
19007 })
19008}
19009
19010fn gate_block() -> usize {
19017 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19018 *B.get_or_init(|| {
19019 std::env::var("CMF_FFN_GATE_BLOCK")
19020 .ok()
19021 .and_then(|v| v.parse().ok())
19022 .unwrap_or(1)
19023 })
19024}
19025
19026fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19028 let n = g.len();
19029 let nb = n.div_ceil(block);
19030 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19031 if kb >= nb {
19032 return;
19033 }
19034 let mut score: Vec<f32> = (0..nb)
19035 .map(|b| {
19036 g[b * block..((b + 1) * block).min(n)]
19037 .iter()
19038 .map(|v| v * v)
19039 .sum::<f32>()
19040 })
19041 .collect();
19042 let mut ord = score.clone();
19043 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19044 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19045 });
19046 let thr = *kth;
19047 for b in 0..nb {
19048 if score[b] < thr {
19049 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19050 }
19051 }
19052 score.clear();
19053}
19054
19055fn keep_top_k(g: &mut [f32], k: usize) {
19057 if gate_block() > 1 {
19058 return keep_top_blocks(g, k, gate_block());
19059 }
19060 let n = g.len();
19061 if k == 0 || k >= n {
19062 return;
19063 }
19064 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19065 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19066 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19067 });
19068 let thr = *kth;
19069 for v in g.iter_mut() {
19070 if v.abs() < thr {
19071 *v = 0.0;
19072 }
19073 }
19074}
19075
19076fn probe_sq() -> bool {
19080 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19081 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19082}
19083
19084fn probe_signed() -> bool {
19088 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19089 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19090}
19091
19092fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19100 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19101 M.get_or_init(|| {
19102 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19103 let b = std::fs::read(&p).ok()?;
19104 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19105 let vals: Vec<f32> = b[8..]
19106 .chunks_exact(4)
19107 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19108 .collect();
19109 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19110 Some((inter, vals))
19111 })
19112 .as_ref()
19113}
19114
19115fn probe_topk() -> usize {
19118 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19119 *K.get_or_init(|| {
19120 std::env::var("CMF_FFN_PROBE_TOPK")
19121 .ok()
19122 .and_then(|v| v.parse().ok())
19123 .unwrap_or(0)
19124 })
19125}
19126
19127thread_local! {
19128 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19131 const { std::cell::RefCell::new(None) };
19132}
19133
19134fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19147 if d.gate_proj.has_prism_contract()
19152 || d.up_proj.has_prism_contract()
19153 || d.down_proj.has_prism_contract()
19154 {
19155 return None;
19156 }
19157 let dt = d.down_t.as_ref()?;
19158 let inter = d.gate_proj.rows();
19159 let hidden = dt.cols();
19160 if k == 0 || k >= inter || d.act != Act::Silu {
19161 return None;
19162 }
19163 DYN_SCRATCH.with(|sc| {
19164 let mut sc = sc.borrow_mut();
19165 let DynScratch {
19166 g,
19167 mag,
19168 live,
19169 parts,
19170 } = &mut *sc;
19171 g.resize(inter, 0.0);
19172 d.gate_proj.matvec(x, g, pool);
19173 for v in g.iter_mut() {
19174 *v = inference::silu(*v);
19175 }
19176 mag.clear();
19179 mag.extend(g.iter().map(|v| v.abs()));
19180 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19181 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19182 });
19183 let thr = *kth;
19184 live.clear();
19185 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19186 let mut out = vec![0.0f32; hidden];
19187 match pool {
19188 Some(p) if live.len() >= 64 => {
19189 let nw = p.n_workers() + 1;
19190 parts.clear();
19191 parts.resize(nw * hidden, 0.0);
19192 let ptr = SendMut(parts.as_mut_ptr());
19193 let n = live.len();
19194 let live_ref: &[u32] = live;
19195 let g_ref: &[f32] = g;
19196 p.run(&|w, workers| {
19197 let chunk = n.div_ceil(workers);
19198 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19199 if s >= e {
19200 return;
19201 }
19202 WORKER_SCRATCH.with(|ws| {
19203 let mut ws = ws.borrow_mut();
19204 let [scratch, acc] = &mut *ws;
19205 scratch.resize(hidden.max(x.len()), 0.0);
19206 acc.clear();
19207 acc.resize(hidden, 0.0);
19208 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19209 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19212 d.up_proj.prefetch_row(nx as usize);
19213 dt.prefetch_row(nx as usize);
19214 }
19215 let idx = nrm as usize;
19216 let up = d.up_proj.row_dot(idx, x, scratch);
19217 let a = g_ref[idx] * up;
19218 if a != 0.0 {
19219 dt.add_row_scaled(idx, a, acc, scratch);
19220 }
19221 }
19222 for (j, v) in acc.iter().enumerate() {
19223 unsafe { *ptr.at(w * hidden + j) = *v };
19224 }
19225 });
19226 });
19227 for w in 0..nw {
19228 for (j, o) in out.iter_mut().enumerate() {
19229 *o += parts[w * hidden + j];
19230 }
19231 }
19232 }
19233 _ => {
19234 WORKER_SCRATCH.with(|ws| {
19235 let mut ws = ws.borrow_mut();
19236 let [scratch, _acc] = &mut *ws;
19237 scratch.resize(hidden.max(x.len()), 0.0);
19238 for &nrm in live.iter() {
19239 let idx = nrm as usize;
19240 let up = d.up_proj.row_dot(idx, x, scratch);
19241 let a = g[idx] * up;
19242 if a != 0.0 {
19243 dt.add_row_scaled(idx, a, &mut out, scratch);
19244 }
19245 }
19246 });
19247 }
19248 }
19249 Some(out)
19250 })
19251}
19252
19253struct DynScratch {
19256 g: Vec<f32>,
19257 mag: Vec<f32>,
19258 live: Vec<u32>,
19259 parts: Vec<f32>,
19260}
19261
19262thread_local! {
19263 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19264 std::cell::RefCell::new(DynScratch {
19265 g: Vec::new(),
19266 mag: Vec::new(),
19267 live: Vec::new(),
19268 parts: Vec::new(),
19269 })
19270 };
19271 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19273 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19274}
19275
19276fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19281 let inter = d.gate_proj.rows();
19282 FFN_SCRATCH.with(|s| {
19283 let mut s = s.borrow_mut();
19284 let [g, u, ..] = &mut *s;
19285 g.resize(inter, 0.0);
19286 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19287 } else {
19289 u.resize(inter, 0.0);
19290 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19291 for i in 0..inter {
19292 g[i] = d.act.combine(g[i], u[i]);
19293 }
19294 }
19295 zero_masked_cols(g, 1, inter, mask_row);
19296 let mut out = attention::take_buf(d.down_proj.rows());
19297 d.down_proj.matvec(g, &mut out, pool);
19298 out
19299 })
19300}
19301
19302fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19308 if d.gate_proj.has_prism_contract()
19309 || d.up_proj.has_prism_contract()
19310 || d.down_proj.has_prism_contract()
19311 {
19312 return None;
19313 }
19314 d.act.moe_job_graph_contract()?;
19317 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19320 return None;
19321 }
19322 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19323 let mut model_ref = None;
19324 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19325 let model = model_ref?;
19326 let hidden = jobs[0].down.1;
19327 let mut out = attention::take_buf(hidden);
19328 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19329 Some(out)
19330 } else {
19331 let mut out = out;
19332 attention::recycle_buf(&mut out);
19333 None
19334 }
19335}
19336
19337#[allow(clippy::type_complexity)]
19342#[allow(clippy::type_complexity)]
19343pub(crate) fn moe_parts(
19344 t: &QTensor,
19345) -> Option<(
19346 &std::sync::Arc<cortiq_core::CmfModel>,
19347 usize,
19348 usize,
19349 usize,
19350 &[f32],
19351 &[f32],
19352 bool,
19353 bool,
19354 bool,
19355)> {
19356 match t {
19357 QTensor::Mapped {
19358 model,
19359 idx,
19360 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
19361 rows,
19362 cols,
19363 row_scale,
19364 col_field,
19365 ..
19366 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
19367 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
19368 )),
19369 QTensor::Mapped {
19371 model,
19372 idx,
19373 dtype: cortiq_core::TensorDtype::Q1,
19374 rows,
19375 cols,
19376 ..
19377 } => Some((
19378 model,
19379 *idx,
19380 *rows,
19381 *cols,
19382 &[][..],
19383 &[][..],
19384 true,
19385 false,
19386 false,
19387 )),
19388 QTensor::Mapped {
19390 model,
19391 idx,
19392 dtype: cortiq_core::TensorDtype::Q4Tiled,
19393 rows,
19394 cols,
19395 ..
19396 } => Some((
19397 model,
19398 *idx,
19399 *rows,
19400 *cols,
19401 &[][..],
19402 &[][..],
19403 false,
19404 true,
19405 false,
19406 )),
19407 QTensor::Mapped {
19409 model,
19410 idx,
19411 dtype: cortiq_core::TensorDtype::Q4TiledP,
19412 rows,
19413 cols,
19414 ..
19415 } => Some((
19416 model,
19417 *idx,
19418 *rows,
19419 *cols,
19420 &[][..],
19421 &[][..],
19422 false,
19423 true,
19424 false,
19425 )),
19426 QTensor::Mapped {
19430 model,
19431 idx,
19432 dtype: cortiq_core::TensorDtype::Q2TiledP,
19433 rows,
19434 cols,
19435 ..
19436 } => Some((
19437 model,
19438 *idx,
19439 *rows,
19440 *cols,
19441 &[][..],
19442 &[][..],
19443 false,
19444 true,
19445 true,
19446 )),
19447 _ => None,
19448 }
19449}
19450
19451#[cfg(target_os = "macos")]
19458#[allow(clippy::type_complexity)]
19459fn chunk_moe_parts(
19460 m: &MoeFfn,
19461 hidden: usize,
19462) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
19463 if m.shared.is_some()
19464 || m.per_expert_scale.is_some()
19465 || m.resonance.is_some()
19466 || m.route_tau.is_some()
19467 || m.router_input_norm
19468 || m.experts.is_empty()
19469 || m.top_k == 0
19470 || std::env::var("CMF_RMS_TRACE").is_ok()
19471 || std::env::var("CMF_ACT_DUMP").is_ok()
19472 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
19473 {
19474 return None;
19475 }
19476 let (rf, rr, rc) = m.router.f32_parts()?;
19477 if rr != m.experts.len() || rc != hidden {
19478 return None;
19479 }
19480 let inter = m.experts[0].gate_proj.rows();
19481 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
19484 let experts = m
19485 .experts
19486 .iter()
19487 .map(|e| {
19488 if e.act != Act::Silu
19489 || !e.segs.is_empty()
19490 || e.gate_proj.rows() != inter
19491 || e.up_proj.rows() != inter
19492 || e.down_proj.cols() != inter
19493 || e.gate_proj.cols() != hidden
19494 || e.down_proj.rows() != hidden
19495 {
19496 return None;
19497 }
19498 if q8 {
19499 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19500 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19501 }
19502 Some((
19503 e.gate_proj.mapped_q4tp()?.1,
19504 e.up_proj.mapped_q4tp()?.1,
19505 e.down_proj.mapped_q4tp()?.1,
19506 ))
19507 })
19508 .collect::<Option<Vec<_>>>()?;
19509 Some((rf, experts, q8))
19510}
19511
19512#[cfg(target_os = "macos")]
19525fn metal_flint_moe_graph_parts(
19526 m: &MoeFfn,
19527 hidden: usize,
19528 scratch_key: usize,
19529) -> Option<crate::gpu::GpuFlintMoe<'_>> {
19530 use std::sync::Arc;
19531
19532 const HIDDEN: usize = 2_048;
19533 const ROUTED_INTER: usize = 512;
19534 const SHARED_INTER: usize = 1_024;
19535 const EXPERTS: usize = 256;
19536 const TOP_K: usize = 6;
19537
19538 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
19539 return None;
19540 }
19541 let routed0 = m.experts.first()?;
19542 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
19543 let (router, router_rows, router_cols) = m.router.f32_parts()?;
19544 if router_rows != EXPERTS || router_cols != HIDDEN {
19545 return None;
19546 }
19547 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
19548 return None;
19549 }
19550 let same_q2 = |t: &QTensor| -> Option<usize> {
19551 let (owner, idx) = t.mapped_q2tp()?;
19552 Arc::ptr_eq(&owner, &model).then_some(idx)
19553 };
19554 let same_q4 = |t: &QTensor| -> Option<usize> {
19555 let (owner, idx) = t.mapped_q4tp()?;
19556 Arc::ptr_eq(&owner, &model).then_some(idx)
19557 };
19558 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19559 if e.act != Act::Silu
19560 || !e.segs.is_empty()
19561 || e.gate_proj.rows() != ROUTED_INTER
19562 || e.gate_proj.cols() != HIDDEN
19563 || e.up_proj.rows() != ROUTED_INTER
19564 || e.up_proj.cols() != HIDDEN
19565 || e.down_proj.rows() != HIDDEN
19566 || e.down_proj.cols() != ROUTED_INTER
19567 {
19568 return None;
19569 }
19570 Some((
19571 same_q2(&e.gate_proj)?,
19572 same_q2(&e.up_proj)?,
19573 same_q4(&e.down_proj)?,
19574 ))
19575 };
19576 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19577 if e.act != Act::Silu
19578 || !e.segs.is_empty()
19579 || e.gate_proj.rows() != SHARED_INTER
19580 || e.gate_proj.cols() != HIDDEN
19581 || e.up_proj.rows() != SHARED_INTER
19582 || e.up_proj.cols() != HIDDEN
19583 || e.down_proj.rows() != HIDDEN
19584 || e.down_proj.cols() != SHARED_INTER
19585 {
19586 return None;
19587 }
19588 Some((
19589 same_q4(&e.gate_proj)?,
19590 same_q4(&e.up_proj)?,
19591 same_q4(&e.down_proj)?,
19592 ))
19593 };
19594 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
19595 if experts.len() != EXPERTS {
19596 return None;
19597 }
19598 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
19599 if shared_gate.is_some() {
19600 return None;
19601 }
19602 Some(crate::gpu::GpuFlintMoe {
19603 router,
19604 bias: m.expert_bias.as_deref(),
19605 experts,
19606 scratch_key,
19607 shared: shared(shared_ffn)?,
19608 top_k: m.top_k,
19609 norm_topk: m.norm_topk_prob,
19610 route_scale: m.routed_scaling,
19611 sigmoid: m.router_sigmoid,
19612 sigmoid_denom_eps: m.sigmoid_denom_eps,
19613 })
19614}
19615
19616#[cfg(target_os = "macos")]
19617fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
19618 if m.router_input_norm
19619 || m.route_tau.is_some()
19620 || m.mask.is_some()
19621 || m.per_expert_scale.is_some()
19622 || m.experts.is_empty()
19623 || m.top_k == 0
19624 || m.resonance.is_some()
19625 {
19626 return None;
19627 }
19628 let (sh, sg) = match &m.shared {
19633 Some((sh, sg)) => (Some(sh), sg.as_ref()),
19634 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
19635 None => return None,
19636 };
19637 let (rf, rr, rc) = m.router.f32_parts()?;
19638 if rr != m.experts.len() || rc != hidden {
19639 return None;
19640 }
19641 let shared_gated = sg.is_some();
19642 let sf = match sg {
19643 Some(sg) => {
19644 let (sf, sr, sc) = sg.f32_parts()?;
19645 if sr * sc != hidden {
19646 return None;
19647 }
19648 sf
19649 }
19650 None => &rf[..hidden],
19653 };
19654 if let Some(b) = &m.expert_bias {
19655 if b.len() != m.experts.len() {
19656 return None;
19657 }
19658 }
19659 let inter = m.experts[0].gate_proj.rows();
19660 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
19663 let q8 = crate::gpu_metal::moe_q8::enabled()
19667 && m.shared.is_none()
19668 && m.experts[0].gate_proj.q8_2f_parts().is_some();
19669 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
19670 if e.act != Act::Silu
19671 || e.gate_proj.rows() != inter
19672 || e.gate_proj.cols() != hidden
19673 || e.up_proj.rows() != inter
19674 || e.up_proj.cols() != hidden
19675 || e.down_proj.rows() != hidden
19676 || e.down_proj.cols() != inter
19677 {
19678 return None;
19679 }
19680 if q8 {
19681 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19682 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19683 }
19684 let pick = |t: &QTensor| -> Option<usize> {
19685 if gu_q2 {
19686 t.mapped_q2tp().map(|(_, i)| i)
19687 } else {
19688 t.mapped_q4tp().map(|(_, i)| i)
19689 }
19690 };
19691 Some((
19692 pick(&e.gate_proj)?,
19693 pick(&e.up_proj)?,
19694 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
19695 ))
19696 };
19697 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
19698 let shared = match sh {
19699 Some(sh) => Some(trio(sh)?),
19700 None => None,
19701 };
19702 Some(crate::gpu::GpuMoe {
19703 router: rf,
19704 sgate: sf,
19705 experts,
19706 shared,
19707 n_exp: m.experts.len(),
19708 top_k: m.top_k,
19709 inter,
19710 norm_topk: m.norm_topk_prob,
19711 route_scale: m.routed_scaling,
19712 gu_q2,
19713 q8,
19714 sigmoid: m.router_sigmoid,
19715 sigmoid_denom_eps: m.sigmoid_denom_eps,
19716 bias: m.expert_bias.as_deref(),
19717 shared_gated,
19718 })
19719}
19720
19721fn forced_ids_at(step: usize) -> Option<u32> {
19727 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
19728 IDS.get_or_init(|| {
19729 std::env::var("CMF_FORCE_IDS")
19730 .ok()
19731 .and_then(|p| std::fs::read_to_string(p).ok())
19732 .map(|s| {
19733 s.split_whitespace()
19734 .filter_map(|t| t.parse().ok())
19735 .collect()
19736 })
19737 .unwrap_or_default()
19738 })
19739 .get(step)
19740 .copied()
19741}
19742
19743pub(crate) fn moe_push_job_parts<'a>(
19747 gate: &'a QTensor,
19748 up: &'a QTensor,
19749 down: &'a QTensor,
19750 x: &[f32],
19751 w: f32,
19752 swiglu_limit: f32,
19753 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19754 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19755) -> Option<()> {
19756 use crate::qtensor::prescale;
19757 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
19758 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
19759 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
19760 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19761 return None; }
19763 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
19766 return None;
19767 }
19768 if !gq2 && dq2 {
19769 return None;
19770 }
19771 model_ref.get_or_insert_with(|| gm.clone());
19772 let dt = |cf: &[f32]| {
19773 if cf.is_empty() {
19774 cortiq_core::TensorDtype::Q8Row
19775 } else {
19776 cortiq_core::TensorDtype::Q8_2f
19777 }
19778 };
19779 jobs.push(crate::gpu::MoeJob {
19780 gate: (gi, gr, gc, grs),
19781 up: (ui, ur, uc, urs),
19782 down: (di, dr, dc, drs),
19783 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
19784 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
19785 down_col: dcf,
19786 w,
19787 q1: gq1,
19788 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
19789 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
19790 gu_q2: gq2,
19791 swiglu_limit,
19792 act: 0,
19793 });
19794 Some(())
19795}
19796
19797fn moe_push_job<'a>(
19799 d: &'a DenseFfn,
19800 x: &[f32],
19801 w: f32,
19802 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
19803 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
19804) -> Option<()> {
19805 use crate::qtensor::prescale;
19806 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
19811 let act = graph_act.code();
19812 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
19813 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
19814 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
19815 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
19816 return None; }
19818 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
19819 return None;
19820 }
19821 if !gq2 && dq2 {
19822 return None;
19823 }
19824 model_ref.get_or_insert_with(|| gm.clone());
19825 let gdt = if gcf.is_empty() {
19826 cortiq_core::TensorDtype::Q8Row
19827 } else {
19828 cortiq_core::TensorDtype::Q8_2f
19829 };
19830 let udt = if ucf.is_empty() {
19831 cortiq_core::TensorDtype::Q8Row
19832 } else {
19833 cortiq_core::TensorDtype::Q8_2f
19834 };
19835 jobs.push(crate::gpu::MoeJob {
19836 gate: (gi, gr, gc, grs),
19837 up: (ui, ur, uc, urs),
19838 down: (di, dr, dc, drs),
19839 xs_gate: prescale(x, gcf, gdt).into_owned(),
19840 xs_up: prescale(x, ucf, udt).into_owned(),
19841 down_col: dcf,
19842 w,
19843 q1: gq1,
19844 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
19845 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
19846 gu_q2: gq2,
19847 swiglu_limit,
19848 act,
19849 });
19850 Some(())
19851}
19852
19853fn sparse_ffn_quant(
19860 d: &DenseFfn,
19861 x: &[f32],
19862 active: &[u16],
19863 hidden: usize,
19864 pool: Option<&Pool>,
19865) -> Vec<f32> {
19866 let n = active.len();
19867 let inter = d.gate_proj.rows();
19868 let mut act = vec![0.0f32; n];
19869 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
19872 let compute = |ai: usize| -> f32 {
19873 let idx = active[ai] as usize;
19874 if idx >= inter {
19875 return 0.0; }
19877 let mut s = if need_scratch {
19878 vec![0.0f32; hidden]
19879 } else {
19880 Vec::new()
19881 };
19882 let gate = d.gate_proj.row_dot(idx, x, &mut s);
19883 let up = d.up_proj.row_dot(idx, x, &mut s);
19884 d.act.combine(gate, up)
19885 };
19886 match pool {
19887 Some(p) if n >= 256 => {
19888 let ptr = SendMut(act.as_mut_ptr());
19889 p.run(&|widx, nw| {
19890 let chunk = n.div_ceil(nw);
19891 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
19892 for ai in s..e {
19893 unsafe { *ptr.at(ai) = compute(ai) };
19894 }
19895 });
19896 }
19897 _ => {
19898 for (ai, a) in act.iter_mut().enumerate() {
19899 *a = compute(ai);
19900 }
19901 }
19902 }
19903 let mut out = vec![0.0f32; hidden];
19905 for (ai, &idx) in active.iter().enumerate() {
19906 let w = act[ai];
19907 if w.abs() >= 1e-12 && (idx as usize) < inter {
19908 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
19909 }
19910 }
19911 out
19912}
19913
19914#[doc(hidden)]
19916pub fn sparse_ffn_quant_for_test(
19917 d: &DenseFfn,
19918 x: &[f32],
19919 active: &[u16],
19920 hidden: usize,
19921) -> Vec<f32> {
19922 sparse_ffn_quant(d, x, active, hidden, None)
19923}
19924
19925fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
19929 let deq = |t: &QTensor| -> Vec<f32> {
19930 let (rows, cols) = (t.rows(), t.cols());
19931 let mut out = vec![0.0f32; rows * cols];
19932 for r in 0..rows {
19933 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
19934 }
19935 out
19936 };
19937 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
19938}
19939
19940struct SendMut(*mut f32);
19942unsafe impl Send for SendMut {}
19943unsafe impl Sync for SendMut {}
19944impl SendMut {
19945 #[inline]
19946 #[allow(clippy::mut_from_ref)]
19949 unsafe fn at(&self, i: usize) -> &mut f32 {
19950 unsafe { &mut *self.0.add(i) }
19951 }
19952}
19953
19954pub(crate) fn moe_route(
19966 logits: &[f32],
19967 m: &MoeFfn,
19968 allowed: Option<&[bool]>,
19969) -> (Vec<usize>, Vec<f32>, f32) {
19970 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
19971}
19972
19973pub(crate) fn moe_route_with_eps(
19983 logits: &[f32],
19984 m: &MoeFfn,
19985 allowed: Option<&[bool]>,
19986 sigmoid_denom_eps: f32,
19987) -> (Vec<usize>, Vec<f32>, f32) {
19988 let ne = logits.len();
19989 let admit = |e: usize| {
19995 m.mask.as_ref().is_none_or(|mk| mk[e])
19996 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19997 };
19998 if m.resonance.is_some() && m.top_k == 1 {
20011 let mut best: Option<usize> = None;
20012 for e in (0..ne).filter(|&e| admit(e)) {
20013 let l = logits[e];
20014 if l == f32::NEG_INFINITY || l.is_nan() {
20015 continue;
20016 }
20017 if best.is_none_or(|b| l > logits[b]) {
20018 best = Some(e);
20019 }
20020 }
20021 if let Some(b) = best {
20022 let mut p = vec![0.0f32; ne];
20023 p[b] = 1.0;
20024 return (vec![b], p, 1.0 / m.routed_scaling);
20025 }
20026 }
20027 let p: Vec<f32> = if m.router_sigmoid {
20033 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20034 } else {
20035 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20036 if mx == f32::NEG_INFINITY {
20037 vec![1.0 / ne.max(1) as f32; ne]
20038 } else {
20039 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20040 let s: f32 = e.iter().sum();
20041 for v in &mut e {
20042 *v /= s;
20043 }
20044 e
20045 }
20046 };
20047 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20048 match &m.expert_bias {
20050 Some(b) => idx.sort_unstable_by(|&x, &y| {
20051 (p[y] + b[y])
20052 .partial_cmp(&(p[x] + b[x]))
20053 .unwrap()
20054 .then(x.cmp(&y))
20055 }),
20056 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20057 }
20058 idx.truncate(m.top_k);
20059 if let Some(tau) = m.route_tau {
20063 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20064 if total > 0.0 {
20065 let mut acc = 0.0f32;
20066 let mut keep = idx.len();
20067 for (i, &e) in idx.iter().enumerate() {
20068 acc += p[e];
20069 if acc >= tau * total {
20070 keep = i + 1;
20071 break;
20072 }
20073 }
20074 idx.truncate(keep);
20075 }
20076 }
20077 let wsum: f32 = if m.norm_topk_prob {
20078 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20079 (if m.router_sigmoid {
20083 s + sigmoid_denom_eps
20084 } else {
20085 s
20086 }) / m.routed_scaling
20087 } else {
20088 1.0 / m.routed_scaling
20089 };
20090 (idx, p, wsum)
20091}
20092
20093fn moe_trace(idx: &[usize]) {
20095 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20096}
20097
20098pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20101 use std::io::Write;
20102 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20103 std::sync::OnceLock::new();
20104 let Some(f) = F.get_or_init(|| {
20105 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20106 Some(std::sync::Mutex::new(
20107 std::fs::OpenOptions::new()
20108 .create(true)
20109 .append(true)
20110 .open(p)
20111 .ok()?,
20112 ))
20113 }) else {
20114 return;
20115 };
20116 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20117 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20118}
20119
20120pub(crate) fn moe_ffn(
20123 m: &MoeFfn,
20124 x: &[f32],
20125 pool: Option<&Pool>,
20126 allowed: Option<&[bool]>,
20127) -> Vec<f32> {
20128 let r = moe_ffn_route(m, x, pool, allowed);
20129 moe_ffn_experts(m, x, &r, pool)
20130}
20131
20132pub(crate) struct MoeRoute {
20136 pub idx: Vec<usize>,
20137 pub p: Vec<f32>,
20138 pub wsum: f32,
20139 pub logits: Vec<f32>,
20140}
20141
20142pub(crate) fn moe_ffn_route(
20148 m: &MoeFfn,
20149 x: &[f32],
20150 pool: Option<&Pool>,
20151 allowed: Option<&[bool]>,
20152) -> MoeRoute {
20153 accumulate_act(m, x, 1);
20154 let ne = m.experts.len();
20155 let mut logits = vec![0.0f32; ne];
20156 match &m.resonance {
20157 Some(r) => r.scores(x, &mut logits),
20158 None => m.router.matvec(x, &mut logits, pool),
20159 }
20160 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20161 {
20162 let mut st = m.stats.borrow_mut();
20163 if st.len() < ne {
20164 st.resize(ne, 0);
20165 }
20166 for &e in &idx {
20167 st[e] += 1;
20168 }
20169 }
20170 moe_trace(&idx);
20176 MoeRoute {
20177 idx,
20178 p,
20179 wsum,
20180 logits,
20181 }
20182}
20183
20184#[cfg(target_os = "macos")]
20193fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20194 const HIDDEN: usize = 2_048;
20195 const ROUTED_INTER: usize = 512;
20196 const SHARED_INTER: usize = 1_024;
20197 const EXPERTS: usize = 256;
20198 const TOP_K: usize = 6;
20199
20200 let Some(routed) = m.experts.first() else {
20201 return false;
20202 };
20203 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20204 return false;
20205 };
20206 let Some((shared, shared_gate)) = &m.shared else {
20207 return false;
20208 };
20209 if shared_gate.is_some()
20210 || model.header.arch.arch_name != "flint"
20211 || model.header.arch.prism_hadamard.is_some()
20212 || model.header.arch.hidden_size != HIDDEN
20213 || m.experts.len() != EXPERTS
20214 || m.top_k != TOP_K
20215 || !m.router_sigmoid
20216 || !m.norm_topk_prob
20217 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20218 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20219 || m.route_tau.is_some()
20220 || m.mask.is_some()
20221 || m.per_expert_scale.is_some()
20222 || m.router_input_norm
20223 || m.resonance.is_some()
20224 || !m.grown.is_empty()
20225 {
20226 return false;
20227 }
20228
20229 let routed_q2 = routed.act == Act::Silu
20230 && routed.segs.is_empty()
20231 && routed.gate_proj.rows() == ROUTED_INTER
20232 && routed.gate_proj.cols() == HIDDEN
20233 && routed.up_proj.rows() == ROUTED_INTER
20234 && routed.up_proj.cols() == HIDDEN
20235 && routed.down_proj.rows() == HIDDEN
20236 && routed.down_proj.cols() == ROUTED_INTER
20237 && routed.up_proj.mapped_q2tp().is_some()
20238 && routed.down_proj.mapped_q4tp().is_some();
20239 let shared_q4 = shared.act == Act::Silu
20240 && shared.segs.is_empty()
20241 && shared.gate_proj.rows() == SHARED_INTER
20242 && shared.gate_proj.cols() == HIDDEN
20243 && shared.up_proj.rows() == SHARED_INTER
20244 && shared.up_proj.cols() == HIDDEN
20245 && shared.down_proj.rows() == HIDDEN
20246 && shared.down_proj.cols() == SHARED_INTER
20247 && shared.gate_proj.mapped_q4tp().is_some()
20248 && shared.up_proj.mapped_q4tp().is_some()
20249 && shared.down_proj.mapped_q4tp().is_some();
20250 routed_q2 && shared_q4
20251}
20252
20253pub(crate) fn moe_ffn_experts(
20256 m: &MoeFfn,
20257 x: &[f32],
20258 r: &MoeRoute,
20259 pool: Option<&Pool>,
20260) -> Vec<f32> {
20261 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20262 if crate::gpu::enabled_here() {
20267 #[cfg(target_os = "macos")]
20268 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20269 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20275 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20276 });
20277 }
20278 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20279 crate::gpu::ProbeArm::Gpu => {
20280 let t0 = std::time::Instant::now();
20281 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20282 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20283 return out;
20284 }
20285 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20286 }
20287 crate::gpu::ProbeArm::CpuTimed => {
20288 let t0 = std::time::Instant::now();
20289 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20290 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20291 return out;
20292 }
20293 crate::gpu::ProbeArm::Cpu => {
20294 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20295 }
20296 }
20297 }
20298 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20299}
20300
20301fn moe_ffn_minicpm_tail(
20305 slot: &mut crate::minicpm_tail::MiniCpmTail,
20306 li: usize,
20307 total_layers: usize,
20308 m: &MoeFfn,
20309 x: &[f32],
20310 pool: Option<&Pool>,
20311) -> Vec<f32> {
20312 let route = moe_ffn_route(m, x, pool, None);
20313 match slot.forward(li, m, x, &route, pool, total_layers) {
20314 Some(out) => out,
20315 None => moe_ffn_experts(m, x, &route, pool),
20320 }
20321}
20322
20323fn moe_ffn_banked(
20327 slot: &mut crate::mimo_moe::Slot,
20328 li: usize,
20329 m: &MoeFfn,
20330 x: &[f32],
20331 pool: Option<&Pool>,
20332) -> Vec<f32> {
20333 let t0 = std::time::Instant::now();
20334 let r = moe_ffn_route(m, x, pool, None);
20335 slot.note_route(t0.elapsed().as_nanos() as u64);
20336 match slot.forward(li, m, x, &r, pool) {
20337 Some(out) => out,
20338 None => crate::qtensor::float_activations_scope(|| {
20339 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
20340 }),
20341 }
20342}
20343
20344fn moe_ffn_banked_rows(
20346 slot: &mut crate::mimo_moe::Slot,
20347 li: usize,
20348 m: &MoeFfn,
20349 xs: &[f32],
20350 b: usize,
20351 hidden: usize,
20352 pool: Option<&Pool>,
20353) -> Vec<f32> {
20354 let t0 = std::time::Instant::now();
20355 let routes: Vec<_> = xs
20356 .chunks_exact(hidden)
20357 .map(|x| moe_ffn_route(m, x, pool, None))
20358 .collect();
20359 slot.note_route(t0.elapsed().as_nanos() as u64);
20360 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
20361 return out;
20362 }
20363 let mut out = Vec::with_capacity(b * hidden);
20364 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
20365 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
20366 crate::qtensor::float_activations_scope(|| {
20368 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
20369 })
20370 });
20371 out.extend(row);
20372 }
20373 out
20374}
20375
20376fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
20381 use std::sync::atomic::{AtomicBool, Ordering};
20382 if built {
20383 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
20384 if total_layers > 0 && layers_run < total_layers {
20385 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
20386 } else {
20387 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
20388 }
20389 } else {
20390 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
20391 }
20392 static SAID: AtomicBool = AtomicBool::new(false);
20393 if !SAID.swap(true, Ordering::Relaxed) {
20394 if built {
20395 tracing::info!("wgpu whole-token graph: ACTIVE");
20396 } else {
20397 tracing::warn!("wgpu whole-token graph refused — per-op path");
20398 }
20399 }
20400}
20401
20402pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20406pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20407pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20411pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20413
20414pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20418pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20419pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
20420 std::sync::atomic::AtomicU64::new(0);
20421pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20422pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20423pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
20427 std::sync::atomic::AtomicU64::new(0);
20428pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20429pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
20430 std::sync::atomic::AtomicU64::new(0);
20431pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
20432 std::sync::atomic::AtomicU64::new(0);
20433
20434fn moe_batch_enabled() -> bool {
20437 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20438 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
20439}
20440
20441fn moe_ffn_cpu_batched(
20447 m: &MoeFfn,
20448 x: &[f32],
20449 idx: &[usize],
20450 p: &[f32],
20451 wsum: f32,
20452 pool: Option<&Pool>,
20453) -> Option<Vec<f32>> {
20454 if idx.is_empty() || !moe_batch_enabled() {
20455 return None;
20456 }
20457 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
20461 return None;
20462 }
20463 let n = idx.len() + usize::from(m.shared.is_some());
20464 let mut pairs = Vec::with_capacity(n);
20465 let mut downs = Vec::with_capacity(n);
20466 let mut ws = Vec::with_capacity(n);
20467 for &e in idx {
20468 let d = &m.experts[e];
20469 if d.act != Act::Silu {
20470 return None;
20471 }
20472 pairs.push((&d.gate_proj, &d.up_proj));
20473 downs.push(&d.down_proj);
20474 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
20475 }
20476 if let Some((se, gate)) = &m.shared {
20479 if se.act != Act::Silu {
20480 return None;
20481 }
20482 let g = gate.as_ref().map_or(1.0, |gate| {
20483 let mut gl = [0.0f32; 1];
20484 gate.matvec(x, &mut gl, pool);
20485 1.0 / (1.0 + (-gl[0]).exp())
20486 });
20487 pairs.push((&se.gate_proj, &se.up_proj));
20488 downs.push(&se.down_proj);
20489 ws.push(g);
20490 }
20491 let inter = pairs[0].0.rows();
20492 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
20493 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
20494 return None;
20495 }
20496 let mut out = attention::take_buf(x.len());
20497 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
20498 attention::recycle_buf(&mut out);
20499 return None;
20500 }
20501 Some(out)
20502}
20503
20504pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20512pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20513pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20514pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20515pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20516
20517pub(crate) fn moe_cold_experts_cpu(
20518 experts: &[(&DenseFfn, f32)],
20519 x: &[f32],
20520 pool: Option<&Pool>,
20521) -> Vec<f32> {
20522 let mut out = attention::take_buf(x.len());
20523 if experts.is_empty() {
20524 return out;
20525 }
20526 let pairs: Vec<_> = experts
20527 .iter()
20528 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
20529 .collect();
20530 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
20531 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
20532 let inter = experts[0].0.gate_proj.rows();
20533 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
20534 let silu_only = experts
20542 .iter()
20543 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
20544 if silu_only
20545 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
20546 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
20547 {
20548 return out;
20549 }
20550 out.fill(0.0);
20551 for &(expert, weight) in experts {
20552 let mut one = dense_ffn(expert, x, pool);
20553 for (o, v) in out.iter_mut().zip(&one) {
20554 *o += weight * v;
20555 }
20556 attention::recycle_buf(&mut one);
20557 }
20558 out
20559}
20560
20561pub(crate) fn moe_cold_experts_rows_cpu(
20565 jobs: &[Vec<(&DenseFfn, f32)>],
20566 xs: &[f32],
20567 hidden: usize,
20568 pool: Option<&Pool>,
20569) -> Vec<f32> {
20570 let mut out = vec![0.0; xs.len()];
20571 let mut experts: Vec<&DenseFfn> = Vec::new();
20572 let mut groups: Vec<Vec<usize>> = Vec::new();
20573 let mut terms = vec![Vec::new(); jobs.len()];
20574 for (r, row) in jobs.iter().enumerate() {
20575 for &(e, w) in row {
20576 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
20577 Some(g) => g,
20578 None => {
20579 experts.push(e);
20580 groups.push(Vec::new());
20581 groups.len() - 1
20582 }
20583 };
20584 terms[r].push((g, groups[g].len(), w));
20585 groups[g].push(r);
20586 }
20587 }
20588 if experts.is_empty() {
20589 return out;
20590 }
20591 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
20592 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
20593 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
20594 let count: usize = lens.iter().sum();
20595 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
20596 let mut ds = vec![vec![0.0; hidden]; count];
20597 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
20598 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
20599 {
20600 let mut offset = 0;
20601 let offsets: Vec<_> = lens
20602 .iter()
20603 .map(|&n| {
20604 let start = offset;
20605 offset += n;
20606 start
20607 })
20608 .collect();
20609 for (r, terms) in terms.iter().enumerate() {
20610 for &(g, slot, w) in terms {
20611 for (o, &v) in out[r * hidden..(r + 1) * hidden]
20612 .iter_mut()
20613 .zip(&ds[offsets[g] + slot])
20614 {
20615 *o += w * v;
20616 }
20617 }
20618 }
20619 } else {
20620 for (r, jobs) in jobs.iter().enumerate() {
20621 if !jobs.is_empty() {
20622 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
20623 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
20624 attention::recycle_buf(&mut row);
20625 }
20626 }
20627 }
20628 out
20629}
20630
20631fn moe_ffn_cpu(
20633 m: &MoeFfn,
20634 x: &[f32],
20635 idx: &[usize],
20636 p: &[f32],
20637 wsum: f32,
20638 pool: Option<&Pool>,
20639) -> Vec<f32> {
20640 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
20641 return out;
20642 }
20643 let mut out = attention::take_buf(x.len());
20644 for &e in idx {
20645 let mut eo = dense_ffn(&m.experts[e], x, pool);
20646 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
20647 for i in 0..out.len() {
20648 out[i] += w * eo[i];
20649 }
20650 attention::recycle_buf(&mut eo);
20651 }
20652 if let Some((se, gate)) = &m.shared {
20653 let mut so = dense_ffn(se, x, pool);
20654 let g = gate.as_ref().map_or(1.0, |gate| {
20655 let mut gl = [0.0f32; 1];
20656 gate.matvec(x, &mut gl, pool);
20657 1.0 / (1.0 + (-gl[0]).exp())
20658 });
20659 for i in 0..out.len() {
20660 out[i] += g * so[i];
20661 }
20662 attention::recycle_buf(&mut so);
20663 }
20664 out
20665}
20666
20667#[allow(clippy::too_many_arguments)]
20675pub(crate) fn mla_attention(
20676 w: &MlaWeights,
20677 normed: &[f32],
20678 cache: &mut crate::kv_cache::LayerKvCache,
20679 position: usize,
20680 inv_freq: &[f32],
20681 rope_scale: f32,
20682 eps: f64,
20683 pool: Option<&Pool>,
20684) -> Vec<f32> {
20685 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
20686 let hd = dr + dn;
20687 let mut q = vec![0.0f32; nh * hd];
20688 match (&w.q_a, &w.q_a_norm) {
20689 (Some(qa), Some(qn)) => {
20690 let mut t = vec![0.0f32; qa.rows()];
20691 qa.matvec(normed, &mut t, pool);
20692 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
20693 w.q_proj.matvec(&tn, &mut q, pool);
20694 }
20695 _ => w.q_proj.matvec(normed, &mut q, pool),
20696 }
20697 let mut ca = vec![0.0f32; lora + dr];
20698 w.kv_a.matvec(normed, &mut ca, pool);
20699 let (c_lat, k_rope) = ca.split_at_mut(lora);
20700 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
20701 let mut kvb = vec![0.0f32; nh * (dn + dv)];
20702 w.kv_b.matvec(&latn, &mut kvb, pool);
20703 if !w.nope {
20704 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
20705 }
20706 for h in 0..nh {
20707 if !w.nope {
20708 attention::rope_rotate_scaled(
20709 &mut q[h * hd..h * hd + dr],
20710 position,
20711 inv_freq,
20712 rope_scale,
20713 );
20714 }
20715 }
20716 let mut k = vec![0.0f32; nh * hd];
20717 let mut v = vec![0.0f32; nh * hd];
20718 for h in 0..nh {
20719 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
20720 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
20721 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
20722 }
20723 cache.append(&k, &v, &vec![true; nh]);
20724 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
20725 attention::recycle_buf(&mut imp);
20726 let mut ov = vec![0.0f32; nh * dv];
20727 for h in 0..nh {
20728 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
20729 }
20730 let mut out = vec![0.0f32; w.o_proj.rows()];
20731 w.o_proj.matvec(&ov, &mut out, pool);
20732 out
20733}
20734
20735fn dense_moe_ffn(
20742 dm: &DenseMoeFfn,
20743 x_normed: &[f32],
20744 h_raw: &[f32],
20745 eps: f64,
20746 norm_style: NormStyle,
20747 pool: Option<&Pool>,
20748) -> Vec<f32> {
20749 let mut d = dense_ffn(&dm.dense, x_normed, pool);
20750 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
20751 let m = &dm.moe;
20752 let ne = m.experts.len();
20753 let mut logits = vec![0.0f32; ne];
20754 if m.router_input_norm {
20755 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
20756 let inv = 1.0 / (ss + eps as f32).sqrt();
20757 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
20758 m.router.matvec(&xr, &mut logits, pool);
20759 } else {
20760 m.router.matvec(h_raw, &mut logits, pool);
20761 }
20762 let (idx, p, wsum) = moe_route(&logits, m, None);
20763 {
20764 let mut st = m.stats.borrow_mut();
20765 if st.len() < ne {
20766 st.resize(ne, 0);
20767 }
20768 for &e in &idx {
20769 st[e] += 1;
20770 }
20771 }
20772 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
20773 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
20774 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
20775 for (di, mi) in d.iter_mut().zip(&mo) {
20776 *di += mi;
20777 }
20778 d
20779}
20780
20781fn moe_gpu_refused(why: &'static str) {
20788 use std::sync::atomic::{AtomicBool, Ordering};
20789 static SAID: AtomicBool = AtomicBool::new(false);
20790 if !SAID.swap(true, Ordering::Relaxed) {
20791 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
20792 }
20793}
20794
20795fn moe_ffn_gpu(
20796 m: &MoeFfn,
20797 x: &[f32],
20798 idx: &[usize],
20799 p: &[f32],
20800 wsum: f32,
20801 pool: Option<&Pool>,
20802) -> Option<Vec<f32>> {
20803 use crate::gpu::MoeJob;
20804
20805 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
20806 let mut model_ref = None;
20807 for &e in idx {
20808 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
20809 moe_gpu_refused("push_job(expert)");
20810 return None;
20811 }
20812 }
20813 if let Some((se, gate)) = &m.shared {
20814 let g = gate.as_ref().map_or(1.0, |gate| {
20815 let mut gl = [0.0f32; 1];
20816 gate.matvec(x, &mut gl, pool);
20817 1.0 / (1.0 + (-gl[0]).exp())
20818 });
20819 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
20820 moe_gpu_refused("push_job(shared)");
20821 return None;
20822 }
20823 }
20824 let Some(model) = model_ref else {
20825 moe_gpu_refused("no model_ref");
20826 return None;
20827 };
20828 let hidden = jobs[0].down.1;
20829 let mut out = vec![0.0f32; hidden];
20830 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20831 Some(out)
20832 } else {
20833 moe_gpu_refused("gpu::moe_block");
20834 None
20835 }
20836}
20837
20838fn ffn_forward(
20840 ffn: &FfnKind,
20841 x: &[f32],
20842 pool: Option<&Pool>,
20843 experts_allowed: Option<&[bool]>,
20844) -> Vec<f32> {
20845 match ffn {
20846 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
20847 FfnKind::Dense(d) => dense_ffn(d, x, pool),
20848 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
20849 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20853 }
20854}
20855
20856fn ffn_forward_pair(
20860 ffn: &FfnKind,
20861 x1: &[f32],
20862 x2: &[f32],
20863 pool: Option<&Pool>,
20864 experts_allowed: Option<&[bool]>,
20865) -> (Vec<f32>, Vec<f32>) {
20866 let d = match ffn {
20867 FfnKind::Dense(d) if !d.segs.is_empty() => {
20870 return (
20871 tube_ffn(d, x1, 1, pool, None),
20872 tube_ffn(d, x2, 1, pool, None),
20873 );
20874 }
20875 FfnKind::Dense(d) => d,
20876 FfnKind::Moe(m) => {
20877 return (
20878 moe_ffn(m, x1, pool, experts_allowed),
20879 moe_ffn(m, x2, pool, experts_allowed),
20880 );
20881 }
20882 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
20883 };
20884 let inter = d.gate_proj.rows();
20885 FFN_SCRATCH.with(|s| {
20886 let mut s = s.borrow_mut();
20887 let [g1, g2, u1, u2] = &mut *s;
20888 g1.resize(inter, 0.0);
20889 g2.resize(inter, 0.0);
20890 u1.resize(inter, 0.0);
20891 u2.resize(inter, 0.0);
20892 QTensor::matvec2_many(
20895 [&d.gate_proj, &d.up_proj],
20896 x1,
20897 x2,
20898 [g1.as_mut_slice(), u1.as_mut_slice()],
20899 [g2.as_mut_slice(), u2.as_mut_slice()],
20900 pool,
20901 );
20902 for i in 0..inter {
20903 g1[i] = d.act.combine(g1[i], u1[i]);
20904 g2[i] = d.act.combine(g2[i], u2[i]);
20905 }
20906 let mut o1 = attention::take_buf(d.down_proj.rows());
20907 let mut o2 = attention::take_buf(d.down_proj.rows());
20908 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
20909 (o1, o2)
20910 })
20911}
20912
20913#[cfg(test)]
20914mod tests {
20915
20916 #[test]
20917 fn encoder_hidden_states_are_normalized_and_reusable() {
20918 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
20919 let ids = [1, 7, 11, 19];
20920
20921 let first = p.encode_hidden_states(&ids).expect("encoder forward");
20922 let second = p
20923 .encode_hidden_states(&ids)
20924 .expect("reusable encoder forward");
20925 assert_eq!(first.len(), ids.len() * 8);
20926 assert!(first.iter().all(|v| v.is_finite()));
20927 assert_eq!(
20928 first, second,
20929 "encoder forward must start from a fresh state"
20930 );
20931
20932 assert!(p.forward_ids(&ids, None).is_ok());
20935 }
20936
20937 #[test]
20942 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
20943 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
20944 let dense_card = ChunkStackFacts {
20945 plain_dense: true,
20946 discrete: true,
20947 gpu_on: true,
20948 ..Default::default()
20949 };
20950 assert!(dense_card.dense_on_discrete());
20951 assert_eq!(
20953 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
20954 DISCRETE_DENSE_PREFILL_CHUNK
20955 );
20956 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
20957 for (label, facts) in [
20958 (
20959 "GDN hybrid / MoE / DeepSeek stack",
20960 ChunkStackFacts {
20961 plain_dense: false,
20962 ..dense_card
20963 },
20964 ),
20965 (
20966 "integrated GPU",
20967 ChunkStackFacts {
20968 discrete: false,
20969 ..dense_card
20970 },
20971 ),
20972 (
20973 "CPU only",
20974 ChunkStackFacts {
20975 gpu_on: false,
20976 discrete: false,
20977 ..dense_card
20978 },
20979 ),
20980 (
20981 "capacity split",
20982 ChunkStackFacts {
20983 capacity_split: true,
20984 ..dense_card
20985 },
20986 ),
20987 (
20988 "multi-GPU plan",
20989 ChunkStackFacts {
20990 multi_gpu: true,
20991 ..dense_card
20992 },
20993 ),
20994 (
20995 "O(1) layers",
20996 ChunkStackFacts {
20997 o1: true,
20998 ..dense_card
20999 },
21000 ),
21001 ] {
21002 assert!(!facts.dense_on_discrete(), "{label}");
21003 assert_eq!(
21004 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21005 48,
21006 "{label} keeps the historical x86 chunk"
21007 );
21008 }
21009 for dense in [false, true] {
21011 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21012 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21013 }
21014 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21016 for dense in [false, true] {
21017 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21018 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21019 }
21020 }
21021 }
21022
21023 #[test]
21024 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21025 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21026 let full = |host_rows, device_rows| ReuseLayer {
21027 full: true,
21028 host_rows,
21029 device_rows,
21030 device_state: false,
21031 };
21032 assert_eq!(
21035 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21036 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21037 );
21038 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21040 assert_eq!(
21042 kv_reuse_plan(339, &[full(339, Some(345))]),
21043 ReusePlan::Ready
21044 );
21045 assert_eq!(
21047 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21048 ReusePlan::Pull(vec![(0, 300, 339)])
21049 );
21050 assert_eq!(
21052 kv_reuse_plan(339, &[full(300, Some(320))]),
21053 ReusePlan::Fresh
21054 );
21055 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21056 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21057 let conv = |device_state| ReuseLayer {
21060 full: false,
21061 host_rows: 0,
21062 device_rows: None,
21063 device_state,
21064 };
21065 assert_eq!(
21066 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21067 ReusePlan::Fresh
21068 );
21069 assert_eq!(
21070 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21071 ReusePlan::Ready
21072 );
21073 }
21074
21075 #[test]
21076 fn nll_graph_policy_scopes_only_the_fused_head() {
21077 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21078 ("vulkan graph", true, true, false, true, false),
21080 ("native Metal graph", true, true, true, true, true),
21082 ("masked", false, true, false, false, false),
21084 ("graph disabled", true, false, true, false, false),
21085 ] {
21086 let (graph_quality, graph_head_required) =
21087 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21088 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21089 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21090 }
21091 }
21092
21093 #[test]
21094 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21095 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21096 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21097 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21098 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21099 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21100 }
21101
21102 #[test]
21103 fn cancel_flag_stops_generation() {
21104 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21105 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21108 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21109 assert_eq!(r.finish_reason, "cancelled");
21110 assert!(
21111 r.token_ids.is_empty(),
21112 "no tokens after cancel: {:?}",
21113 r.token_ids
21114 );
21115 assert_eq!(p.kv_cache.seq_len(), 0);
21116 assert!(p.kv_history.is_empty());
21117 assert!(!p.graph_want_logits);
21118 assert!(p.graph_logits.is_none());
21119 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21121 assert_ne!(r2.finish_reason, "cancelled");
21122 }
21123 use super::*;
21124
21125 #[test]
21126 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21127 let arch = |arch_name: &str, hidden_act: &str| {
21131 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21132 "arch_name": arch_name,
21133 "hidden_size": 8,
21134 "intermediate_size": 16,
21135 "num_layers": 1,
21136 "num_attention_heads": 1,
21137 "num_kv_heads": 1,
21138 "head_dim": 8,
21139 "vocab_size": 16,
21140 "layer_types": ["FullAttention"],
21141 "rms_norm_eps": 1e-6,
21142 "max_position_embeddings": 16,
21143 "hidden_act": hidden_act
21144 }))
21145 .expect("minimal architecture")
21146 };
21147 let minimax = arch("minimax_m3_vl", "swigluoai");
21148 assert_eq!(
21149 Act::from_arch_full(&minimax),
21150 Act::SwiGluOai {
21151 alpha: 1.702,
21152 limit: 7.0,
21153 }
21154 );
21155
21156 let unrelated = arch("qwen2", "swigluoai");
21159 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21160
21161 let non_oai = arch("minimax_m3_vl", "gelu");
21165 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21166 }
21167
21168 #[test]
21169 fn minimax_swiglu_oai_act_matches_scalar_contract() {
21170 let act = Act::swiglu_oai(1.702, 7.0);
21171 let got = act.combine(10.0, -10.0);
21172 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
21173 assert!((got - expected).abs() < 1e-6);
21174 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
21175 assert_eq!(
21176 act.wgpu_graph_act(),
21177 Some(crate::gpu::GraphAct::SwiGluOaiM3),
21178 "the WGPU-only arm accepts only the frozen M3 constants"
21179 );
21180 assert_eq!(
21181 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
21182 None,
21183 "nearby OAI constants must not borrow MiniMax's kernel"
21184 );
21185 assert_eq!(
21186 act.moe_job_graph_contract(),
21187 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
21188 "the direct MoE path carries the frozen clamp with the marker"
21189 );
21190 assert_eq!(
21191 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
21192 None,
21193 "a nearby clamp must not borrow MiniMax's device contract"
21194 );
21195
21196 let gate_only = act.apply(-10.0);
21199 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
21200 assert!((gate_only - expected_gate).abs() < 1e-10);
21201 }
21202
21203 #[test]
21204 fn explicit_sliding_schedule_selects_local_rope_profile() {
21205 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
21210 p.swa = Some((1024, usize::MAX));
21211 p.sliding_layers = Some(vec![true, true, true, false]);
21212 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
21213 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
21214 p.rope_scale = 1.277_258_9;
21215 p.rope_scale_local = 1.0;
21216
21217 for li in 0..3 {
21218 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
21219 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
21220 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
21221 }
21222 assert_eq!(p.layer_window(3), None);
21223 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
21224 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
21225 }
21226
21227 #[test]
21235 fn dynamic_ffn_equals_the_zeroing_arm() {
21236 let (hidden, inter) = (8usize, 32usize);
21237 let synth = |n: usize, salt: usize| -> Vec<f32> {
21238 (0..n)
21239 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
21240 .collect()
21241 };
21242 let down = synth(hidden * inter, 3);
21243 let mut down_t = vec![0.0f32; inter * hidden];
21244 for r in 0..hidden {
21245 for c in 0..inter {
21246 down_t[c * hidden + r] = down[r * inter + c];
21247 }
21248 }
21249 let d = DenseFfn {
21250 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21251 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21252 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
21253 act: Act::Silu,
21254 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
21255 segs: Vec::new(),
21256 };
21257 let x = synth(hidden, 11);
21258 let k = 12usize;
21259 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
21260 let mut g = vec![0.0f32; inter];
21262 d.gate_proj.matvec(&x, &mut g, None);
21263 let mut u = vec![0.0f32; inter];
21264 d.up_proj.matvec(&x, &mut u, None);
21265 for v in g.iter_mut() {
21266 *v = inference::silu(*v);
21267 }
21268 keep_top_k(&mut g, k);
21269 for i in 0..inter {
21270 g[i] *= u[i];
21271 }
21272 let mut want = vec![0.0f32; hidden];
21273 d.down_proj.matvec(&g, &mut want, None);
21274 for (a, b) in want.iter().zip(&got) {
21275 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
21276 }
21277 }
21278
21279 #[test]
21285 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
21286 let (hidden, core, tube) = (8usize, 12usize, 8usize);
21287 let inter = core + tube;
21288 let synth = |n: usize, salt: usize| -> Vec<f32> {
21289 (0..n)
21290 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
21291 .collect()
21292 };
21293 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
21294 let d_all = synth(hidden * inter, 3);
21295 let dense = DenseFfn {
21297 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
21298 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
21299 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
21300 act: Act::Silu,
21301 down_t: None,
21302 segs: Vec::new(),
21303 };
21304 let rows =
21305 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
21306 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
21307 let mut o = Vec::with_capacity(hidden * (b - a));
21308 for r in 0..hidden {
21309 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
21310 }
21311 o
21312 };
21313 let tubed = DenseFfn {
21314 down_t: None,
21315 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
21316 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
21317 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
21318 act: Act::Silu,
21319 segs: vec![FfnSeg {
21320 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
21321 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
21322 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
21323 start: core,
21324 width: tube,
21325 }],
21326 };
21327 let x = synth(hidden, 7);
21328 let want = dense_ffn(&dense, &x, None);
21329 let got = tube_ffn(&tubed, &x, 1, None, None);
21330 for (a, b) in want.iter().zip(&got) {
21331 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
21332 }
21333 let mut bits = vec![0u8; inter.div_ceil(8)];
21335 for n in 0..core {
21336 bits[n / 8] |= 1 << (n % 8);
21337 }
21338 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21339 let masked = dense_ffn_masked(&dense, &x, None, &bits);
21340 for (a, b) in masked.iter().zip(&closed) {
21341 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
21342 }
21343 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21345 for (a, b) in closed.iter().zip(&batch) {
21346 assert_eq!(a, b, "batch arm disagrees with decode arm");
21347 }
21348 }
21349
21350 #[test]
21352 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
21353 let (hidden, inter) = (16usize, 40usize);
21354 let synth = |n: usize, salt: usize| -> Vec<f32> {
21355 (0..n)
21356 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
21357 .collect()
21358 };
21359 let d = DenseFfn {
21360 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21361 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21362 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
21363 act: Act::Silu,
21364 down_t: None,
21365 segs: Vec::new(),
21366 };
21367 let x = synth(hidden, 9);
21368 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
21370
21371 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
21372
21373 let mut g = vec![0.0f32; inter];
21375 d.gate_proj.matvec(&x, &mut g, None);
21376 let mut u = vec![0.0f32; inter];
21377 d.up_proj.matvec(&x, &mut u, None);
21378 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
21379 for i in 0..inter {
21380 g[i] = if act_set.contains(&(i as u16)) {
21381 inference::silu(g[i]) * u[i]
21382 } else {
21383 0.0
21384 };
21385 }
21386 let mut reference = vec![0.0f32; hidden];
21387 d.down_proj.matvec(&g, &mut reference, None);
21388
21389 let max_d = sparse
21390 .iter()
21391 .zip(&reference)
21392 .map(|(a, b)| (a - b).abs())
21393 .fold(0.0f32, f32::max);
21394 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
21395 }
21396
21397 fn attach_test_mtp(p: &mut Pipeline) {
21399 let (h, inter, heads, kv, hd) = (
21400 p.hidden_size,
21401 p.intermediate_size,
21402 p.num_heads,
21403 p.num_kv_heads,
21404 p.head_dim,
21405 );
21406 let synth = |n: usize, salt: usize| -> Vec<f32> {
21407 (0..n)
21408 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
21409 .collect()
21410 };
21411 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
21412 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21413 };
21414 p.mtp = Some(MtpModule {
21415 enorm: vec![1.0; h],
21416 hnorm: vec![1.0; h],
21417 eh_proj: qt(h, 2 * h, 301),
21418 layer: LayerWeights {
21419 input_norm: vec![1.0; h],
21420 post_norm: vec![1.0; h],
21421 attn_out_norm: None,
21422 ffn_out_norm: None,
21423 layer_scale: None,
21424 ffn: FfnKind::Dense(DenseFfn {
21425 gate_proj: qt(inter, h, 315),
21426 up_proj: qt(inter, h, 316),
21427 down_proj: qt(h, inter, 317),
21428 act: Act::Silu,
21429 down_t: None,
21430 segs: Vec::new(),
21431 }),
21432 attn: AttnKind::Full {
21433 bias: None,
21434 wq: qt(heads * hd, h, 311),
21435 wk: qt(kv * hd, h, 312),
21436 wv: qt(kv * hd, h, 313),
21437 wo: qt(h, heads * hd, 314),
21438 q_norm: None,
21439 k_norm: None,
21440 output_gate: false,
21441 softplus_gate: None,
21442 },
21443 },
21444 final_norm: vec![1.0; h],
21445 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
21446 });
21447 }
21448
21449 #[test]
21450 fn speculative_equals_vanilla_greedy() {
21451 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21455 let run = |spec: bool| {
21456 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21457 p.sampler_config.temperature = 0.0;
21458 attach_test_mtp(&mut p);
21459 p.speculative = spec;
21460 let r = p.generate("abcdef", 12, None, None).unwrap();
21461 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
21462 };
21463 let (vanilla, d0, _) = run(false);
21464 let (spec, d1, a1) = run(true);
21465 assert_eq!(d0, 0, "vanilla path must not draft");
21466 assert!(d1 > 0, "speculative path must draft");
21467 assert_eq!(
21468 vanilla, spec,
21469 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
21470 );
21471 }
21472
21473 #[test]
21474 fn speculative_accepts_constant_oracle() {
21475 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21477 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21478 p.sampler_config.temperature = 0.0;
21479 p.sampler_config.repetition_penalty = 1.0;
21480 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
21483 attach_test_mtp(&mut p);
21484 p.speculative = true;
21485 let r = p.generate("abcd", 10, None, None).unwrap();
21486 assert!(r.mtp_drafted > 0);
21487 assert_eq!(
21488 r.mtp_accepted, r.mtp_drafted,
21489 "constant logits → every draft accepted"
21490 );
21491 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
21494 }
21495
21496 #[test]
21497 fn empty_prompt_is_an_error_not_a_panic() {
21498 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
21499 let r = p.generate("", 4, None, None);
21500 assert!(r.is_err(), "empty prompt must be a clean error");
21501 }
21502
21503 #[test]
21504 fn every_token_enters_kv_exactly_once() {
21505 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21506 p.sampler_config.temperature = 0.0;
21508 let r = p.generate("abc", 2, None, None).unwrap();
21509 assert_eq!(r.prompt_tokens, 3);
21510 assert_eq!(
21514 p.kv_cache.seq_len(),
21515 3 + r.tokens_generated - 1,
21516 "each token must be cached exactly once (v1 cached the last prompt token twice)"
21517 );
21518 }
21519
21520 #[test]
21521 fn generation_is_reproducible_with_seed() {
21522 let run = || {
21523 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21524 p.generate("hello", 8, None, None).unwrap().token_ids
21525 };
21526 assert_eq!(run(), run());
21527 }
21528
21529 #[test]
21530 fn resetting_sampler_restarts_the_seeded_stream() {
21531 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21532 let config = SamplerConfig {
21533 seed: Some(1234),
21534 ..SamplerConfig::default()
21535 };
21536 p.set_sampler_config(config.clone());
21537 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
21538 p.set_sampler_config(config);
21539 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
21540 assert_eq!(first, second);
21541 }
21542
21543 #[test]
21544 fn eviction_bounds_the_cache() {
21545 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
21546 p.kv_cache.max_seq_len = 6;
21547 p.sampler_config.temperature = 0.0;
21548 let _ = p.generate("abcd", 12, None, None).unwrap();
21549 assert!(
21550 p.kv_cache.seq_len() <= 6 + 1,
21551 "cache must stay bounded by max_seq_len (got {})",
21552 p.kv_cache.seq_len()
21553 );
21554 }
21555
21556 #[test]
21557 fn confidence_matches_tokens_and_is_a_probability() {
21558 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21559 p.sampler_config.temperature = 0.0;
21560 p.sampler_config.repetition_penalty = 1.0;
21561 let r = p.generate("abcd", 10, None, None).unwrap();
21562 assert_eq!(
21563 r.token_confidence.len(),
21564 r.token_ids.len(),
21565 "one confidence per emitted token"
21566 );
21567 for &c in &r.token_confidence {
21568 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
21569 }
21570 let logits = [1.0f32, 3.0, 0.5, 3.0];
21572 let p0 = top1_prob_t(&logits, 1, 1.0);
21573 let p1 = top1_prob_t(&logits, 3, 1.0);
21574 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
21575 assert!(p0 > 0.0 && p0 < 1.0);
21576 let sharp = top1_prob_t(&logits, 1, 1.0);
21578 let soft = top1_prob_t(&logits, 1, 2.0);
21579 assert!(soft < sharp, "higher temperature lowers peak confidence");
21580 }
21581
21582 #[test]
21583 fn trace_is_opt_in_and_parallels_the_output() {
21584 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21586 p.sampler_config.temperature = 0.0;
21587 p.sampler_config.repetition_penalty = 1.0;
21588 let r = p.generate("abcd", 10, None, None).unwrap();
21589 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
21590
21591 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21593 p.sampler_config.temperature = 0.0;
21594 p.sampler_config.repetition_penalty = 1.0;
21595 p.set_trace(true);
21596 let r = p.generate("abcd", 10, None, None).unwrap();
21597 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
21598 for (i, tr) in r.traces.iter().enumerate() {
21599 assert_eq!(tr.t, i, "trace index is sequential");
21600 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
21601 assert_eq!(
21602 tr.confidence, r.token_confidence[i],
21603 "trace confidence matches the confidence channel"
21604 );
21605 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
21607 }
21608 }
21609
21610 #[test]
21611 fn explain_prefill_logits_match_greedy_first_token() {
21612 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21616 p.sampler_config.temperature = 0.0;
21617 p.sampler_config.repetition_penalty = 1.0;
21618 let ids = p.tokenizer.encode("abcd");
21619 let logits = p.prefill_next_logits(&ids, None);
21620 let argmax = logits
21621 .iter()
21622 .enumerate()
21623 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
21624 .unwrap()
21625 .0 as u32;
21626 let r = p.generate("abcd", 1, None, None).unwrap();
21627 assert_eq!(
21628 argmax, r.token_ids[0],
21629 "explain preview must match greedy emit"
21630 );
21631 }
21632
21633 #[test]
21634 fn laguna_shared_expert_is_unconditionally_added() {
21635 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
21636 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
21637 let zero_dense = || DenseFfn {
21638 gate_proj: matrix(vec![0.0; 4]),
21639 up_proj: matrix(vec![0.0; 4]),
21640 down_proj: matrix(vec![0.0; 4]),
21641 act: Act::Silu,
21642 down_t: None,
21643 segs: Vec::new(),
21644 };
21645 let shared = DenseFfn {
21646 gate_proj: identity(),
21647 up_proj: identity(),
21648 down_proj: identity(),
21649 act: Act::Silu,
21650 down_t: None,
21651 segs: Vec::new(),
21652 };
21653 let x = [1.0, 2.0];
21654 let expected = dense_ffn(&shared, &x, None);
21655 let moe = MoeFfn {
21656 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
21657 experts: vec![zero_dense()],
21658 top_k: 1,
21659 norm_topk_prob: true,
21660 router_sigmoid: true,
21661 sigmoid_denom_eps: 1e-6,
21662 expert_bias: None,
21663 routed_scaling: 1.0,
21664 route_tau: None,
21665 shared: Some((shared, None)),
21666 stats: std::cell::RefCell::new(Vec::new()),
21667 act_sq: std::cell::RefCell::new(Vec::new()),
21668 act_rows: std::cell::RefCell::new(Vec::new()),
21669 mask: None,
21670 per_expert_scale: None,
21671 router_input_norm: false,
21672 resonance: None,
21673 grown: Vec::new(),
21674 };
21675 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
21676 for (actual, expected) in actual.iter().zip(expected) {
21677 assert!((actual - expected).abs() < 1e-6);
21678 }
21679 }
21680
21681 fn mimo_test_pipeline() -> Pipeline {
21690 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
21691 let kvh = [1usize, 2, 2, 1];
21692 let synth = |n: usize, salt: usize| -> Vec<f32> {
21693 (0..n)
21694 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21695 .collect()
21696 };
21697 let qt = |rows: usize, cols: usize, salt: usize| {
21698 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21699 };
21700 let dense = |inter: usize, salt: usize| DenseFfn {
21701 gate_proj: qt(inter, hs, salt),
21702 up_proj: qt(inter, hs, salt + 1),
21703 down_proj: qt(hs, inter, salt + 2),
21704 act: Act::Silu,
21705 down_t: None,
21706 segs: Vec::new(),
21707 };
21708 let layers: Vec<LayerWeights> = (0..4)
21709 .map(|li| LayerWeights {
21710 input_norm: vec![1.0; hs],
21711 post_norm: vec![1.0; hs],
21712 attn_out_norm: None,
21713 ffn_out_norm: None,
21714 layer_scale: None,
21715 ffn: if li == 0 {
21716 FfnKind::Dense(dense(inter, 50))
21717 } else {
21718 FfnKind::Moe(MoeFfn {
21719 router: qt(4, hs, 60 + li),
21720 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
21721 top_k: 2,
21722 norm_topk_prob: true,
21723 router_sigmoid: true,
21724 sigmoid_denom_eps: 1e-6,
21725 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
21726 routed_scaling: 1.0,
21727 route_tau: None,
21728 shared: None,
21729 stats: std::cell::RefCell::new(Vec::new()),
21730 act_sq: std::cell::RefCell::new(Vec::new()),
21731 act_rows: std::cell::RefCell::new(Vec::new()),
21732 mask: None,
21733 per_expert_scale: None,
21734 router_input_norm: false,
21735 resonance: None,
21736 grown: Vec::new(),
21737 })
21738 },
21739 attn: AttnKind::Full {
21740 wq: qt(nh * hd, hs, li * 10 + 1),
21741 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
21742 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
21743 wo: qt(hs, nh * vd, li * 10 + 4),
21744 q_norm: None,
21745 k_norm: None,
21746 output_gate: false,
21747 softplus_gate: None,
21748 bias: None,
21749 },
21750 })
21751 .collect();
21752 let mut p = Pipeline::new(
21753 Tokenizer::byte_level(),
21754 PipelineWeights {
21755 embed_tokens: qt(vocab, hs, 100),
21756 layers,
21757 lm_head: qt(vocab, hs, 200),
21758 final_norm: vec![1.0; hs],
21759 },
21760 hs,
21761 inter,
21762 nh,
21763 1, hd,
21765 4,
21766 4,
21767 false,
21768 vocab,
21769 1e-6,
21770 1e7,
21771 NormStyle::Qwen,
21772 4096,
21773 SamplerConfig {
21774 seed: Some(7),
21775 ..Default::default()
21776 },
21777 );
21778 p.layer_dump = None;
21780 p.set_rotary(4, 1e7);
21781 p.sliding_layers = Some(vec![false, true, true, false]);
21782 p.swa = Some((3, usize::MAX));
21783 p.rotary_dim_local = Some(4);
21784 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
21785 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
21786 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
21787 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
21788 p
21789 }
21790
21791 #[test]
21792 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
21793 let mut p = mimo_test_pipeline();
21794 p.speculative = false;
21795 p.ignore_eos = true;
21796 p.sampler_config.temperature = 0.0;
21797 p.sampler_config.repetition_penalty = 1.0;
21798 let a = vec![3, 5, 7, 9, 11, 13];
21799 let b = vec![4, 8, 12, 16, 20, 24];
21800 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
21801 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
21802 let actual = p
21806 .generate_from_embeds(&a, &rows, 8, None, None)
21807 .unwrap()
21808 .token_ids;
21809 assert_eq!(actual, expected);
21810 assert!(p.kv_history.is_empty());
21811 let mut extended = a.clone();
21812 extended.push(17);
21813 let after_media = p
21814 .generate_from_ids(&extended, 8, None, None)
21815 .unwrap()
21816 .token_ids;
21817 p.reset_session();
21818 let fresh = p
21819 .generate_from_ids(&extended, 8, None, None)
21820 .unwrap()
21821 .token_ids;
21822 assert_eq!(after_media, fresh);
21823 assert!(
21824 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
21825 .is_err()
21826 );
21827 p.reset_session();
21831 p.generate_from_ids(&a, 1, None, None).unwrap();
21832 let mut media_ids = p.kv_history.clone();
21833 assert!(!media_ids.is_empty());
21834 media_ids.extend_from_slice(&[19, 21, 23]);
21835 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
21836 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
21837 let mut oracle = mimo_test_pipeline();
21838 oracle.speculative = false;
21839 oracle.ignore_eos = true;
21840 oracle.sampler_config.temperature = 0.0;
21841 oracle.sampler_config.repetition_penalty = 1.0;
21842 let expected = oracle
21843 .generate_from_ids(&source_ids, 8, None, None)
21844 .unwrap()
21845 .token_ids;
21846 assert_eq!(
21847 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
21848 .unwrap()
21849 .token_ids,
21850 expected
21851 );
21852 assert!(p.kv_history.is_empty());
21853 let mut bad = rows;
21854 bad[0] = f32::NAN;
21855 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
21856 }
21857
21858 fn f32_bits(v: &[f32]) -> Vec<u32> {
21859 v.iter().map(|x| x.to_bits()).collect()
21860 }
21861
21862 #[test]
21869 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
21870 let mut p = mimo_test_pipeline();
21871 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
21872 assert_eq!(kv, vec![1, 2, 2, 1]);
21873 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
21874 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
21875 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
21876 let hs = p.hidden_size;
21877 let mut decode = Vec::new();
21878 for (pos, &id) in ids.iter().enumerate() {
21879 let e = p.embed_single(id);
21880 let h = p.forward_layers(&e, pos, None);
21881 decode.push(p.logits_from_hidden(&h));
21882 }
21883 for l in &p.kv_cache.layers {
21884 assert_eq!(l.seq_len, 12);
21885 assert_eq!(l.head_values(0).len(), 12 * 8);
21887 }
21888 assert!(decode.iter().flatten().all(|v| v.is_finite()));
21889
21890 p.clear_sequence_state();
21891 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21892 for pos in 0..ids.len() {
21893 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
21894 assert_eq!(
21895 f32_bits(&decode[pos]),
21896 f32_bits(&lg),
21897 "whole prompt, pos {pos}"
21898 );
21899 }
21900
21901 p.clear_sequence_state();
21902 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
21903 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
21904 for pos in 0..ids.len() {
21905 let row = if pos < 5 {
21906 &a[pos * hs..(pos + 1) * hs]
21907 } else {
21908 &b[(pos - 5) * hs..(pos - 4) * hs]
21909 };
21910 let lg = p.logits_from_hidden(row);
21911 assert_eq!(
21912 f32_bits(&decode[pos]),
21913 f32_bits(&lg),
21914 "two chunks, pos {pos}"
21915 );
21916 }
21917
21918 let last = |p: &mut Pipeline| {
21921 p.clear_sequence_state();
21922 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21923 p.logits_from_hidden(&hb[11 * hs..12 * hs])
21924 };
21925 let base = last(&mut p);
21926 let mut no_sinks = mimo_test_pipeline();
21927 for l in &mut no_sinks.kv_cache.layers {
21928 l.sinks = None;
21929 }
21930 assert_ne!(
21931 f32_bits(&last(&mut no_sinks)),
21932 f32_bits(&base),
21933 "sinks are live"
21934 );
21935 let mut wide = mimo_test_pipeline();
21936 wide.swa = Some((64, usize::MAX));
21937 assert_ne!(
21938 f32_bits(&last(&mut wide)),
21939 f32_bits(&base),
21940 "window is live"
21941 );
21942
21943 p.clear_sequence_state();
21945 p.ignore_eos = true;
21946 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
21947 assert_eq!(r.token_ids.len(), 4);
21948 }
21949
21950 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
21953 let (hs, nh) = (16usize, 4usize);
21954 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
21955 p.layer_dump = None;
21956 p.swa = Some((6, 4));
21957 p.proj_gate_sigmoid = true;
21958 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
21959 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
21960 let g: Vec<f32> = (0..nh * hs)
21961 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
21962 .collect();
21963 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
21964 }
21965 }
21966 p.swa_trim = trim;
21967 p
21968 }
21969
21970 #[test]
21976 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
21977 let mut a = spark_test_pipeline(None);
21978 let mut b = spark_test_pipeline(Some((2, 4)));
21979 assert_eq!(
21980 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
21981 vec![Some(6), Some(6), Some(6), None]
21982 );
21983 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
21984 let mut pos = 0usize;
21985 for &n in [5usize, 13, 7, 16].iter().cycle() {
21986 if pos >= ids.len() {
21987 break;
21988 }
21989 let end = (pos + n).min(ids.len());
21990 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21991 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
21992 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
21993 pos = end;
21994 }
21995 for _ in 0..23 {
21996 let e = a.embed_single(((pos * 7) % 64) as u32);
21997 let ha = a.forward_layers(&e, pos, None);
21998 let hb = b.forward_layers(&e, pos, None);
21999 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22000 for li in 0..3 {
22001 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22002 }
22003 pos += 1;
22004 }
22005 for round in 0..9 {
22006 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22007 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22008 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22009 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22010 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22011 if round % 2 == 1 {
22012 for p in [&mut a, &mut b] {
22014 for l in &mut p.kv_cache.layers {
22015 l.truncate_last(2);
22016 }
22017 }
22018 } else {
22019 pos += 2;
22020 }
22021 }
22022 let e = a.embed_single(9);
22023 let (ha, hb) = (
22024 a.forward_layers(&e, pos, None),
22025 b.forward_layers(&e, pos, None),
22026 );
22027 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22028 for li in 0..3 {
22029 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22030 assert!(lb.base() > 0, "layer {li} trimmed");
22031 assert_eq!(la.base(), 0);
22032 assert_eq!(lb.pos_len(), la.seq_len);
22033 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22034 }
22035 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22036 assert_eq!(
22037 (gb.base(), gb.seq_len),
22038 (0, ga.seq_len),
22039 "the global layer keeps all"
22040 );
22041 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22042 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22043 }
22044
22045 #[test]
22050 fn swa_trim_wire_handoff_continues_bitwise() {
22051 let mut a = spark_test_pipeline(None);
22052 let mut b = spark_test_pipeline(Some((2, 4)));
22053 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22054 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22055 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22056 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22057 let mut pos = ids.len();
22058 for _ in 0..5 {
22059 let e = a.embed_single(((pos * 5) % 64) as u32);
22060 assert_eq!(
22061 f32_bits(&a.forward_layers(&e, pos, None)),
22062 f32_bits(&b.forward_layers(&e, pos, None))
22063 );
22064 pos += 1;
22065 }
22066 let mut c = spark_test_pipeline(Some((2, 4)));
22067 for li in 0..4 {
22068 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22069 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22070 }
22071 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22072 assert_eq!(c.kv_cache.seq_len(), pos);
22073 for step in 0..17 {
22074 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22075 let ha = a.forward_layers(&e, pos, None);
22076 let hc = c.forward_layers(&e, pos, None);
22077 assert_eq!(
22078 f32_bits(&ha),
22079 f32_bits(&hc),
22080 "after the hand-off, step {step}"
22081 );
22082 pos += 1;
22083 }
22084 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
22085 }
22086
22087 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
22090 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
22091 let synth = |len: usize, salt: usize| -> Vec<f32> {
22092 (0..len)
22093 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
22094 .collect()
22095 };
22096 let qt = |rows: usize, cols: usize, salt: usize| {
22097 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22098 };
22099 let layers = (0..n)
22100 .map(|k| {
22101 let s = 500 + k * 40;
22102 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
22103 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
22104 MtpModule {
22105 enorm: vec![1.0; hs],
22106 hnorm: vec![1.0; hs],
22107 eh_proj: qt(hs, 2 * hs, s),
22108 layer: LayerWeights {
22109 input_norm: vec![1.0; hs],
22110 post_norm: vec![1.0; hs],
22111 attn_out_norm: None,
22112 ffn_out_norm: None,
22113 layer_scale: None,
22114 attn: AttnKind::Full {
22115 wq: qt(nh * hd, hs, s + 1),
22116 wk: qt(nkv * hd, hs, s + 2),
22117 wv: qt(nkv * vd, hs, s + 3),
22118 wo: qt(hs, nh * vd, s + 4),
22119 q_norm: None,
22120 k_norm: None,
22121 output_gate: false,
22122 softplus_gate: None,
22123 bias: None,
22124 },
22125 ffn: FfnKind::Dense(DenseFfn {
22126 gate_proj: qt(inter, hs, s + 5),
22127 up_proj: qt(inter, hs, s + 6),
22128 down_proj: qt(hs, inter, s + 7),
22129 act: Act::Silu,
22130 down_t: None,
22131 segs: Vec::new(),
22132 }),
22133 },
22134 final_norm: vec![1.0; hs],
22135 kv,
22136 }
22137 })
22138 .collect();
22139 mimo_mtp::MimoMtp::from_layers(layers)
22140 }
22141
22142 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
22143 p.clear_sequence_state();
22144 p.speculative = spec;
22145 p.ignore_eos = true;
22146 p.sampler_config.temperature = 0.0;
22147 p.generate_from_ids(ids, n, None, None).unwrap()
22148 }
22149
22150 #[test]
22156 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
22157 for post in [false, true] {
22160 let mut p = mimo_test_pipeline();
22161 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
22163 let mut st0 = mimo_test_mtp(3, 1.0);
22164 st0.post_norm_hidden = post;
22165 p.mimo_mtp = Some(st0);
22166 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
22167 let hs = p.hidden_size;
22168 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22169 p.mimo_note_rows(&hb, 0);
22170 let mut st = p.mimo_mtp.take().unwrap();
22171 let k = 3;
22174 let mut inc = Vec::new();
22175 for t in 0..ids.len() - k - 1 {
22176 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
22177 }
22178 let s = ids.len();
22181 let mut reference = vec![vec![0u32; k]; s - k - 1];
22182 let mut fresh = mimo_test_mtp(3, 1.0);
22183 for (layer, m) in fresh.layers.iter_mut().enumerate() {
22184 let n = s - layer - 1;
22185 let mut cats = vec![0.0f32; n * 2 * hs];
22186 for j in 0..n {
22187 let e = p.embed_single(ids[j + layer + 1]);
22188 let raw = &hb[j * hs..(j + 1) * hs];
22189 let g = if post {
22190 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
22191 } else {
22192 raw.to_vec()
22193 };
22194 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
22195 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
22196 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
22197 }
22198 let mut x = vec![0.0f32; n * hs];
22199 m.eh_proj.matmat(&cats, n, &mut x, None);
22200 p.mimo_mtp_block(m, &mut x, n, 0);
22201 for (t, row) in reference.iter_mut().enumerate() {
22202 let y = inference::rms_norm(
22203 &x[t * hs..(t + 1) * hs],
22204 &m.final_norm,
22205 p.rms_eps,
22206 p.norm_style,
22207 );
22208 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
22209 }
22210 }
22211 assert_eq!(inc, reference, "post_norm_hidden = {post}");
22212 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
22214 assert!(distinct.len() > 3, "{inc:?}");
22215 let last_t = ids.len() - k - 2;
22217 for m in &st.layers {
22218 assert_eq!(m.kv.seq_len, last_t + 1);
22219 }
22220 }
22221 }
22222
22223 #[test]
22230 fn mimo_speculative_greedy_equals_plain_greedy() {
22231 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
22232 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
22233 let n = 24;
22234 let mut p = mimo_test_pipeline();
22235 let plain = mimo_greedy(&mut p, &ids, n, false);
22236 assert_eq!(plain.mtp_drafted, 0);
22237 assert_eq!(plain.token_ids.len(), n);
22238 let plain_kv = p.kv_cache.layers[0].seq_len;
22239
22240 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
22242 let spec = mimo_greedy(&mut p, &ids, n, true);
22243 assert!(spec.mtp_drafted > 0, "the round must draft");
22244 assert_eq!(spec.token_ids, plain.token_ids);
22245 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
22246
22247 let mut truth: Vec<u32> = ids.clone();
22250 truth.extend(&plain.token_ids);
22251 let mut noisy = truth.clone();
22252 for (i, t) in noisy.iter_mut().enumerate() {
22253 if i % 5 == 0 {
22254 *t = (*t + 1) % 64;
22255 }
22256 }
22257 let mut st = mimo_test_mtp(3, 1.0);
22258 st.draft_override = Some(noisy);
22259 p.mimo_mtp = Some(st);
22260 let spec = mimo_greedy(&mut p, &ids, n, true);
22261 assert_eq!(spec.token_ids, plain.token_ids);
22262 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
22263 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
22264 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
22265 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
22266 assert!(
22267 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
22268 "{:?}",
22269 stats.accept_hist
22270 );
22271 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
22272
22273 let mut st = mimo_test_mtp(3, 1.0);
22276 st.draft_override = Some(truth);
22277 p.mimo_mtp = Some(st);
22278 let spec = mimo_greedy(&mut p, &ids, n, true);
22279 assert_eq!(spec.token_ids, plain.token_ids);
22280 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
22281 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
22282
22283 let off = mimo_greedy(&mut p, &ids, n, false);
22285 assert_eq!(off.token_ids, plain.token_ids);
22286 assert_eq!(off.mtp_drafted, 0);
22287 }
22288
22289 #[test]
22298 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
22299 let p = mimo_test_pipeline();
22300 assert_eq!(
22301 p.graph_attn_decline_reason(),
22302 Some("per-layer KV head counts")
22303 );
22304 assert_eq!(p.wgpu_graph_attn_decline(), None);
22305 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
22306 assert_eq!(
22307 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
22308 (1, 4, 4, None, false)
22309 );
22310 assert_eq!(g0.invf, p.inv_freq.as_slice());
22311 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
22312 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
22313 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
22314 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
22315 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
22316 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
22317 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
22318
22319 let emb = p.embed_single(3);
22322 let mut lg = Vec::new();
22323 assert!(
22324 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
22325 .is_none()
22326 );
22327 let mut hid = emb.clone();
22328 assert_eq!(
22329 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
22330 crate::gpu::BatchGraphOutcome::Declined
22331 );
22332 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
22333 assert!(p.try_multi_burst(3, 0, 4).is_none());
22334 assert!(
22335 p.graph_declines().is_empty(),
22336 "no attention decline logged: {:?}",
22337 p.graph_declines()
22338 );
22339 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
22344 assert_eq!(plain().graph_attn_decline_reason(), None);
22345 assert_eq!(plain().wgpu_graph_attn_decline(), None);
22346 assert!(
22347 plain().graph_attn_geom(0).is_none(),
22348 "uniform models keep the historical arms"
22349 );
22350 let mut q = plain();
22351 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
22352 assert_eq!(
22353 q.graph_attn_decline_reason(),
22354 Some("learned attention sinks")
22355 );
22356 assert_eq!(
22357 q.graph_attn_geom(1).unwrap().sink,
22358 Some(&[0.25f32, -0.25][..])
22359 );
22360 let mut q = plain();
22361 q.set_attn_geometry(None, Some(2)).unwrap();
22362 assert_eq!(
22363 q.graph_attn_decline_reason(),
22364 Some("V heads narrower than Q/K heads")
22365 );
22366 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
22367 let mut q = plain();
22368 q.sliding_layers = Some(vec![true, false]);
22369 q.swa = Some((4, usize::MAX));
22370 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
22371 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
22372 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
22373
22374 let mut q = mimo_test_pipeline();
22378 q.rope_scale = 2.0;
22379 q.rope_scale_local = 1.25;
22380 assert_eq!(q.wgpu_graph_attn_decline(), None);
22381 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
22382 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
22383
22384 let mut q = plain();
22387 q.rope_scale = 2.0;
22388 assert_eq!(
22389 q.wgpu_graph_attn_decline(),
22390 Some("scaled RoPE without per-layer geometry")
22391 );
22392 }
22393
22394 #[cfg(target_os = "macos")]
22400 #[test]
22401 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
22402 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
22406 p.swa = Some((16, usize::MAX));
22407 p.sliding_layers = Some(vec![true, true]);
22408 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
22409 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
22410
22411 assert_eq!(
22412 p.graph_attn_decline_reason(),
22413 Some("learned attention sinks")
22414 );
22415 let swa_graph = p.metal_graph_swa();
22416 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
22417 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
22418 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
22419 assert!(
22420 native_sink_attend,
22421 "the q1 sink admission must follow the SWA door"
22422 );
22423 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
22426 }
22427
22428 #[test]
22429 fn mimo_verify_rewind_preserves_lagging_host_caches() {
22430 let mut p = mimo_test_pipeline();
22431 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
22432 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
22433 for _ in 0..if li == 0 { 2 } else { 12 } {
22434 layer.append(&row, &row, &[]);
22435 }
22436 }
22437 p.mimo_verify_rewind(9).unwrap();
22438 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
22439 for layer in &p.kv_cache.layers[1..] {
22440 assert_eq!(layer.seq_len, 9);
22441 }
22442 }
22443
22444 #[test]
22448 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
22449 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
22450 let _ = std::fs::remove_dir_all(&dir);
22451 let mut p = mimo_test_pipeline();
22452 let hs = p.hidden_size;
22453 let ids = [5u32, 9, 11, 2, 40];
22454 p.layer_dump = Some(dir.join("decode"));
22455 for (pos, &id) in ids.iter().enumerate() {
22456 let e = p.embed_single(id);
22457 let _ = p.forward_layers(&e, pos, None);
22458 }
22459 p.clear_sequence_state();
22460 p.layer_dump = Some(dir.join("prefill"));
22461 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22462 for pos in 0..ids.len() {
22463 for li in 0..p.num_layers {
22464 let name = format!("p{pos:06}_l{li:02}.f32");
22465 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
22466 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
22467 assert_eq!(a.len(), hs * 4, "{name}");
22468 assert_eq!(a, b, "{name}");
22469 }
22470 }
22471 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
22472 let vals: Vec<f32> = last
22473 .chunks(4)
22474 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
22475 .collect();
22476 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
22477 let _ = std::fs::remove_dir_all(&dir);
22478 }
22479
22480 #[test]
22481 fn attn_geometry_and_sinks_are_validated() {
22482 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
22483 assert!(
22484 p.set_attn_geometry(Some(vec![2]), None).is_err(),
22485 "one entry per layer"
22486 );
22487 assert!(
22488 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
22489 "3 does not divide 4"
22490 );
22491 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
22492 assert!(p.set_attn_geometry(None, Some(0)).is_err());
22493 assert!(
22494 p.set_attn_geometry(None, Some(5)).is_err(),
22495 "V wider than the head"
22496 );
22497 p.set_attn_geometry(None, Some(4)).unwrap();
22498 assert_eq!(
22499 p.v_head_dim, None,
22500 "v_head_dim == head_dim is the uniform case"
22501 );
22502 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
22503 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
22504 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
22505 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
22506 assert!(
22507 p.kv_cache.layers[1].sinks.is_some(),
22508 "a reshape keeps the layer's sinks"
22509 );
22510 assert_eq!(p.layer_geom(1).0, 4);
22511 assert!(
22512 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
22513 "one sink per Q head"
22514 );
22515 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
22516 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
22517 }
22518
22519 #[test]
22522 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
22523 let cfg = || {
22524 Some(crate::nystrom::O1Cfg {
22525 layers: crate::nystrom::O1Layers::All,
22526 m: 4,
22527 w: 8,
22528 sink: 2,
22529 rect: crate::nystrom::O1Rect::Aggregate,
22530 })
22531 };
22532 let mut p = mimo_test_pipeline();
22533 p.set_o1(cfg());
22534 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
22535 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
22536 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
22537 q.sliding_layers = Some(vec![false, false, true]);
22538 q.swa = Some((4, usize::MAX));
22539 q.set_o1(cfg());
22540 assert_eq!(q.o1_flags, vec![true, false, false]);
22541 }
22542
22543 #[test]
22544 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
22545 const B: usize = 19;
22546 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22547 p.set_o1(Some(crate::nystrom::O1Cfg {
22548 layers: crate::nystrom::O1Layers::All,
22549 m: 4,
22550 w: 8,
22551 sink: 2,
22552 rect: crate::nystrom::O1Rect::Aggregate,
22553 }));
22554 p.o1_begin_with_prefix(Some(B));
22555 let ids: Vec<u32> = (0..B as u32).collect();
22556 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22557
22558 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
22559 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
22560 let next = p.embed_single(B as u32);
22561 let _ = p.forward_layers(&next, B, None);
22562 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
22563 }
22564
22565 #[test]
22566 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
22567 const B: usize = 19;
22568 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22569 let gdn_cfg = crate::linear_core::GdnCfg {
22573 num_v_heads: 2,
22574 num_k_heads: 1,
22575 key_head_dim: 2,
22576 value_head_dim: 4,
22577 conv_kernel: 3,
22578 hidden_size: 8,
22579 rms_eps: 1e-6,
22580 output_gate_sigmoid: false,
22581 };
22582 let synth = |n: usize, salt: usize| -> Vec<f32> {
22583 (0..n)
22584 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22585 .collect()
22586 };
22587 let qt = |rows: usize, cols: usize, salt: usize| {
22588 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22589 };
22590 let c_dim = gdn_cfg.conv_dim();
22591 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
22592 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
22593 in_proj_qkv: qt(c_dim, 8, 1),
22594 in_proj_z: qt(vd, 8, 2),
22595 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
22596 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
22597 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
22598 a_log: vec![0.2, 0.5],
22599 dt_bias: synth(gdn_cfg.num_v_heads, 6),
22600 norm: vec![1.0; gdn_cfg.value_head_dim],
22601 out_proj: qt(8, vd, 7),
22602 });
22603 p.gdn_cfg = Some(gdn_cfg);
22604 p.set_o1(Some(crate::nystrom::O1Cfg {
22605 layers: crate::nystrom::O1Layers::All,
22606 m: 4,
22607 w: 8,
22608 sink: 2,
22609 rect: crate::nystrom::O1Rect::Aggregate,
22610 }));
22611 p.o1_begin_with_prefix(Some(B));
22612 for pos in 0..B - 2 {
22613 let emb = p.embed_single(pos as u32);
22614 let _ = p.forward_layers(&emb, pos, None);
22615 }
22616 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
22617
22618 let e1 = p.embed_single((B - 2) as u32);
22619 let e2 = p.embed_single((B - 1) as u32);
22620 let _ = p.forward_pair(&e1, &e2, B - 2);
22621
22622 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
22623 assert!(
22624 p.kv_cache
22625 .layers
22626 .iter()
22627 .enumerate()
22628 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
22629 );
22630 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
22631 assert_ne!(
22632 p.kv_cache.layers[0].linear_state, lane1_state,
22633 "real pair must commit GDN lane 2 before returning"
22634 );
22635 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
22636 let next = p.embed_single(B as u32);
22637 let _ = p.forward_layers(&next, B, None);
22638 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
22639 }
22640
22641 #[test]
22642 fn o1_error_observation_stays_terminal_until_reset() {
22643 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22644 p.set_o1(Some(crate::nystrom::O1Cfg {
22645 layers: crate::nystrom::O1Layers::All,
22646 m: 4,
22647 w: 8,
22648 sink: 2,
22649 rect: crate::nystrom::O1Rect::Aggregate,
22650 }));
22651 p.o1_begin();
22652 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
22653
22654 assert!(p.o1_seal_checked().is_err());
22655 assert!(
22656 p.o1_seal_checked().is_err(),
22657 "retry must see the sticky error"
22658 );
22659 let k = vec![0.2f32; 4];
22660 let v = vec![0.3f32; 4];
22661 p.kv_cache.layers[0].append(&k, &v, &[]);
22662 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
22663
22664 p.reset_session();
22665 p.o1_begin();
22666 p.kv_cache.layers[0].append(&k, &v, &[]);
22667 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
22668 }
22669
22670 #[test]
22671 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
22672 let ids = vec![1u32, 2, 3, 4, 5, 6];
22673 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22674 p.graph_logits = Some(vec![123.0]);
22675 p.graph_want_logits = true;
22676 p.graph_failed
22677 .store(true, std::sync::atomic::Ordering::Relaxed);
22678 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22679 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
22680 assert!(err.contains("before NLL"));
22681 assert!(p.graph_logits.is_none());
22682 assert!(!p.graph_want_logits);
22683 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22684 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22685
22686 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22687 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22688 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22689 assert_eq!(actual.1, expected.1);
22690 assert!((actual.0 - expected.0).abs() < 1e-9);
22691 }
22692
22693 #[test]
22694 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
22695 let ids = vec![1u32, 2, 3, 4, 5, 6];
22696 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22697 p.nll_test_fail_at = Some(1);
22698 let err = p
22699 .nll_ids_from(&ids, 0)
22700 .expect_err("one-shot forward failure");
22701 assert!(err.contains("forward") || err.contains("score row"));
22702 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22703 assert!(!p.graph_want_logits);
22704 assert!(p.graph_logits.is_none());
22705 assert!(p.kv_history.is_empty());
22706
22707 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22708 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
22709 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
22710 assert_eq!(actual.1, expected.1);
22711 assert!((actual.0 - expected.0).abs() < 1e-9);
22712 }
22713
22714 #[test]
22715 fn nll_serial_failure_before_first_row_is_reported() {
22716 let ids = vec![1u32, 2, 3, 4];
22717 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22718 p.nll_test_force_serial = true;
22719 p.nll_test_fail_at = Some(0);
22720 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
22721 assert!(err.contains("serial forward"));
22722 assert!(p.kv_history.is_empty());
22723 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22724 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22725 }
22726
22727 #[test]
22728 fn ffn_probe_failure_discards_recorder_and_state() {
22729 let ids = vec![1u32, 2, 3, 4];
22730 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22731 p.nll_test_fail_at = Some(0);
22732 let err = p
22733 .probe_ffn_mass_batch(&ids)
22734 .expect_err("probe forward failure");
22735 assert!(err.contains("NLL"));
22736 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
22737 assert!(p.kv_history.is_empty());
22738 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22739 }
22740
22741 #[test]
22742 fn nll_test_controls_are_pipeline_scoped() {
22743 let ids = vec![1u32, 2, 3, 4];
22744 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22745 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22746 failing.nll_test_force_serial = true;
22747 failing.nll_test_fail_at = Some(0);
22748
22749 assert!(!failing.can_prefill_batched());
22750 assert!(unaffected.can_prefill_batched());
22751 let expected = unaffected
22752 .nll_ids_from(&ids, 0)
22753 .expect("unaffected pipeline remains usable");
22754 let err = failing
22755 .nll_ids_from(&ids, 0)
22756 .expect_err("failure injection belongs to failing pipeline");
22757 assert!(err.contains("serial forward"));
22758 assert!(failing.nll_test_fail_at.is_none());
22759 assert!(unaffected.can_prefill_batched());
22760 let actual = unaffected
22761 .nll_ids_from(&ids, 0)
22762 .expect("unaffected pipeline remains reusable");
22763 assert_eq!(actual.1, expected.1);
22764 assert!((actual.0 - expected.0).abs() < 1e-9);
22765 }
22766
22767 #[test]
22768 fn forward_ids_failure_channel_is_terminal_and_reusable() {
22769 let ids = vec![1u32, 2, 3, 4, 5, 6];
22770 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22771 p.graph_logits = Some(vec![123.0]);
22772 p.graph_want_logits = true;
22773 p.graph_failed
22774 .store(true, std::sync::atomic::Ordering::Relaxed);
22775 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22776
22777 let err = p
22778 .forward_ids(&ids, None)
22779 .expect_err("a failed forward must not become a valid head result");
22780 assert!(err.contains("forward_ids setup"));
22781 assert!(p.graph_logits.is_none());
22782 assert!(!p.graph_want_logits);
22783 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
22784 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
22785 assert_eq!(p.kv_cache.seq_len(), 0);
22786
22787 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
22788 .forward_ids(&ids, None)
22789 .expect("fresh forward_ids");
22790 let actual = p
22791 .forward_ids(&ids, None)
22792 .expect("pipeline remains reusable after a failed forward");
22793 assert_eq!(actual.len(), expected.len());
22794 assert!(
22795 actual
22796 .iter()
22797 .zip(expected)
22798 .all(|(a, b)| (a - b).abs() < 1e-9)
22799 );
22800 assert_eq!(p.kv_cache.seq_len(), ids.len());
22801 }
22802
22803 #[test]
22804 fn sigmoid_router_floor_is_explicit_per_architecture() {
22805 let zero = || DenseFfn {
22811 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22812 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22813 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22814 act: Act::Silu,
22815 down_t: None,
22816 segs: Vec::new(),
22817 };
22818 let m = MoeFfn {
22819 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
22820 experts: vec![zero(), zero()],
22821 top_k: 1,
22822 norm_topk_prob: true,
22823 router_sigmoid: true,
22824 sigmoid_denom_eps: 1e-20,
22825 expert_bias: None,
22826 routed_scaling: 2.5,
22827 route_tau: None,
22828 shared: None,
22829 stats: std::cell::RefCell::new(Vec::new()),
22830 act_sq: std::cell::RefCell::new(Vec::new()),
22831 act_rows: std::cell::RefCell::new(Vec::new()),
22832 mask: None,
22833 per_expert_scale: None,
22834 router_input_norm: false,
22835 resonance: None,
22836 grown: Vec::new(),
22837 };
22838 let logits = [-20.0f32, -20.0];
22839 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
22840 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
22841 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
22842 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
22843 assert!((flint_wsum - reference_expected).abs() < 1e-15);
22844 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
22845 assert!(legacy_wsum > minimax_wsum * 100.0);
22846 }
22847
22848 #[test]
22849 fn resonance_scores_match_formula_and_stable_tie() {
22850 let r = Resonance {
22851 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
22853 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
22854 k: 1,
22855 bias: vec![1.5, 0.5, 0.0],
22856 shell: Vec::new(),
22857 };
22858 let x = [1.0f32, 1.0];
22859 let mut got = vec![0.0; 3];
22860 r.scores(&x, &mut got);
22861 assert!((got[0] - 0.5).abs() < 1e-6);
22865 assert!((got[1] - 0.5).abs() < 1e-6);
22866 assert!(got[2].abs() < 1e-6);
22867 let best = got
22868 .iter()
22869 .enumerate()
22870 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
22871 .map(|(i, _)| i);
22872 assert_eq!(best, Some(0));
22873 assert!(got.iter().all(|v| v.is_finite()));
22874 }
22875
22876 #[test]
22881 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
22882 let plain = Resonance {
22888 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
22889 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
22890 k: 1,
22891 bias: vec![1.5, 0.5, 0.0, 0.0],
22892 shell: Vec::new(),
22893 };
22894 let shelled = Resonance {
22895 mu: plain.mu.clone(),
22896 u: plain.u.clone(),
22897 k: 1,
22898 bias: plain.bias.clone(),
22899 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
22900 };
22901 assert!(!plain.has_shell());
22902 assert!(shelled.has_shell());
22903 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
22904 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
22905 set_growth_shell(Some(true));
22906 assert!(growth_shell_enabled());
22907 let x = [1.0f32, 1.0];
22910 plain.scores(&x, &mut a);
22911 shelled.scores(&x, &mut b);
22912 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
22913 assert!(a[2] == 0.0 && a[3] == 0.0);
22914 let xo = [3.0f32, 0.0];
22917 plain.scores(&xo, &mut a);
22918 shelled.scores(&xo, &mut b);
22919 assert_eq!(a[2], -6.0);
22920 assert_eq!(a[3], -6.0);
22921 assert_eq!(
22922 bits(&a[..3]),
22923 bits(&b[..3]),
22924 "trunk rows + the boundary row"
22925 );
22926 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
22927 assert_eq!(shelled.effective_shell(4), shelled.shell);
22928 set_growth_shell(Some(false));
22931 assert!(!growth_shell_enabled());
22932 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
22933 shelled.scores(&xo, &mut b);
22934 assert_eq!(bits(&a), bits(&b));
22935 set_growth_shell(None);
22936 let short = Resonance {
22939 shell: vec![f32::INFINITY, f32::INFINITY],
22940 ..shelled
22941 };
22942 set_growth_shell(Some(true));
22943 short.scores(&xo, &mut b);
22944 assert_eq!(bits(&a), bits(&b));
22945 set_growth_shell(None);
22946 }
22947
22948 #[test]
22952 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
22953 let zero = || DenseFfn {
22954 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22955 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22956 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
22957 act: Act::Silu,
22958 down_t: None,
22959 segs: Vec::new(),
22960 };
22961 let moe = |sigmoid: bool| MoeFfn {
22962 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
22963 experts: vec![zero(), zero(), zero(), zero()],
22964 top_k: 1,
22965 norm_topk_prob: true,
22966 router_sigmoid: sigmoid,
22967 sigmoid_denom_eps: 1e-6,
22968 expert_bias: None,
22969 routed_scaling: 1.0,
22970 route_tau: None,
22971 shared: None,
22972 stats: std::cell::RefCell::new(Vec::new()),
22973 act_sq: std::cell::RefCell::new(Vec::new()),
22974 act_rows: std::cell::RefCell::new(Vec::new()),
22975 mask: None,
22976 per_expert_scale: None,
22977 router_input_norm: false,
22978 resonance: None,
22979 grown: Vec::new(),
22980 };
22981 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
22982 for sigmoid in [false, true] {
22983 let m = moe(sigmoid);
22984 let (idx, p, wsum) = moe_route(&logits, &m, None);
22985 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
22986 assert_eq!(p[1], 0.0);
22987 assert_eq!(p[3], 0.0);
22988 assert!(p[2] > p[0] && p[0] > 0.0);
22989 assert!(p.iter().all(|v| v.is_finite()));
22990 let w = p[2] / wsum;
22991 if sigmoid {
22992 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
22994 } else {
22995 assert_eq!(
22996 w, 1.0,
22997 "softmax: the renormalized top-1 weight is exactly 1"
22998 );
22999 }
23000 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
23003 assert_eq!(idx, vec![2]);
23004 }
23005 let m = moe(false);
23007 let (idx, _, _) = moe_route(
23008 &[
23009 f32::NEG_INFINITY,
23010 f32::NEG_INFINITY,
23011 f32::NEG_INFINITY,
23012 -9.0,
23013 ],
23014 &m,
23015 None,
23016 );
23017 assert_eq!(idx, vec![3]);
23018 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
23021 assert_eq!(idx, vec![0]);
23022 assert!(p.iter().all(|&v| v == 0.25));
23023 assert!(wsum.is_finite() && wsum > 0.0);
23024 }
23025
23026 #[cfg(feature = "gpu")]
23032 #[test]
23033 fn minimax_device_route_matches_host_sigmoid_contract() {
23034 match crate::gpu_wgpu::selected_and_up() {
23035 None => {
23036 crate::gpu_wgpu::skip_or_fail(module_path!());
23037 return;
23038 }
23039 Some(false) => {
23047 eprintln!(
23048 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
23049 module_path!()
23050 );
23051 return;
23052 }
23053 Some(true) => {}
23054 }
23055 let n = 128usize;
23056 let top_k = 4usize;
23057 let logits: Vec<f32> = (0..n)
23058 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23059 .collect();
23060 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23061 let m = MoeFfn {
23062 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23063 experts: Vec::new(),
23064 top_k,
23065 norm_topk_prob: true,
23066 router_sigmoid: true,
23067 sigmoid_denom_eps: 1e-20,
23068 expert_bias: Some(bias.clone()),
23069 routed_scaling: 2.0,
23070 route_tau: None,
23071 shared: None,
23072 stats: std::cell::RefCell::new(Vec::new()),
23073 act_sq: std::cell::RefCell::new(Vec::new()),
23074 act_rows: std::cell::RefCell::new(Vec::new()),
23075 mask: None,
23076 per_expert_scale: None,
23077 router_input_norm: false,
23078 resonance: None,
23079 grown: Vec::new(),
23080 };
23081 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23082 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
23083
23084 for shared in [Some(n as u32), None] {
23085 let mut gi = Vec::new();
23086 let mut gw = Vec::new();
23087 assert!(
23088 crate::gpu_wgpu::minimax_moe_route_for_test(
23089 &logits,
23090 Some(&bias),
23091 top_k,
23092 2.0,
23093 shared,
23094 &mut gi,
23095 &mut gw,
23096 ),
23097 "устройство не поднялось — тест не проверил ничего"
23098 );
23099 let routed = if shared.is_some() { top_k } else { gi.len() };
23100 assert_eq!(
23101 &gi[..routed],
23102 &ci[..],
23103 "разные эксперты или порядок (shared={shared:?})"
23104 );
23105 let d = cw
23106 .iter()
23107 .zip(&gw[..routed])
23108 .map(|(a, b)| (a - b).abs())
23109 .fold(0.0f32, f32::max);
23110 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
23111 assert!(d < 1e-6, "веса разошлись на {d:.3e}");
23112 if let Some(s) = shared {
23113 assert_eq!(gi[top_k], s as usize, "shared pin slot");
23114 assert_eq!(gw[top_k], 1.0, "shared weight");
23115 }
23116 }
23117 }
23118
23119 #[cfg(feature = "gpu")]
23124 #[test]
23125 fn minimax_device_cold_weights_match_host_contract() {
23126 match crate::gpu_wgpu::selected_and_up() {
23127 None => {
23128 crate::gpu_wgpu::skip_or_fail(module_path!());
23129 return;
23130 }
23131 Some(false) => {
23139 eprintln!(
23140 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
23141 module_path!()
23142 );
23143 return;
23144 }
23145 Some(true) => {}
23146 }
23147 let n = 128usize;
23148 let top_k = 4usize;
23149 let logits: Vec<f32> = (0..n)
23150 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23151 .collect();
23152 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23153 let m = MoeFfn {
23154 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23155 experts: Vec::new(),
23156 top_k,
23157 norm_topk_prob: true,
23158 router_sigmoid: true,
23159 sigmoid_denom_eps: 1e-20,
23160 expert_bias: Some(bias.clone()),
23161 routed_scaling: 2.0,
23162 route_tau: None,
23163 shared: None,
23164 stats: std::cell::RefCell::new(Vec::new()),
23165 act_sq: std::cell::RefCell::new(Vec::new()),
23166 act_rows: std::cell::RefCell::new(Vec::new()),
23167 mask: None,
23168 per_expert_scale: None,
23169 router_input_norm: false,
23170 resonance: None,
23171 grown: Vec::new(),
23172 };
23173 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23174 assert_eq!(ci.len(), top_k);
23175
23176 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
23178 let mut cold = Vec::new();
23179 let mut idx = Vec::new();
23180 let mut dw = Vec::new();
23181 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
23182 &logits,
23183 Some(&bias),
23184 top_k,
23185 2.0,
23186 &resident,
23187 n as u32,
23188 &mut cold,
23189 &mut idx,
23190 &mut dw,
23191 ));
23192 assert!(cold.is_empty(), "полностью резидентный банк: {cold:?}");
23193 for (r, &e) in ci.iter().enumerate() {
23198 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
23199 let host = p[e] / wsum;
23200 let d = (dw[r] - host).abs();
23201 assert!(
23202 d <= 1e-6 * host.abs().max(1.0),
23203 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
23204 dw[r]
23205 );
23206 }
23207 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
23208 assert_eq!(dw[top_k], 1.0, "shared weight");
23209
23210 let mut remap = resident.clone();
23212 remap[ci[0]] = u32::MAX;
23213 remap[ci[2]] = u32::MAX;
23214 let mut cold = Vec::new();
23215 let mut idx2 = Vec::new();
23216 let mut dw2 = Vec::new();
23217 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
23218 &logits,
23219 Some(&bias),
23220 top_k,
23221 2.0,
23222 &remap,
23223 n as u32,
23224 &mut cold,
23225 &mut idx2,
23226 &mut dw2,
23227 ));
23228 let mut resident_weight_checked = 0usize;
23236 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
23237 if dw2[r] == 0.0 {
23238 continue; }
23240 let e = slot as usize;
23241 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
23242 let host = p[e] / wsum;
23243 let d = (dw2[r] - host).abs();
23244 assert!(
23245 d <= 1e-6 * host.abs().max(1.0),
23246 "partial-bank resident weight expert {e}: Δ={d:.3e}"
23247 );
23248 resident_weight_checked += 1;
23249 }
23250 assert_eq!(resident_weight_checked, 2, "ровно два резидентных пика из четырёх");
23251 println!("partial bank: checked {resident_weight_checked} resident weights");
23252 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
23253 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
23254 ids.sort_unstable();
23255 let mut want = vec![ci[0], ci[2]];
23256 want.sort_unstable();
23257 assert_eq!(ids, want, "холодные эксперты разошлись: {cold:?} против {want:?}");
23258 for &(e, w) in &cold {
23259 let host = p[e] / wsum;
23260 let d = (w - host).abs();
23261 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
23262 assert!(d <= 1e-6 * host.abs().max(1.0), "cold вес эксперта {e}: Δ={d:.3e}");
23263 }
23264 }
23265
23266 #[test]
23271 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
23272 let zero = || DenseFfn {
23273 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23274 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23275 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23276 act: Act::Silu,
23277 down_t: None,
23278 segs: Vec::new(),
23279 };
23280 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
23281 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
23282 experts: vec![zero(), zero(), zero()],
23283 top_k: 1,
23284 norm_topk_prob: norm_topk,
23285 router_sigmoid: false,
23286 sigmoid_denom_eps: 1e-6,
23287 expert_bias: None,
23288 routed_scaling: 1.0,
23289 route_tau: None,
23290 shared: None,
23291 stats: std::cell::RefCell::new(Vec::new()),
23292 act_sq: std::cell::RefCell::new(Vec::new()),
23293 act_rows: std::cell::RefCell::new(Vec::new()),
23294 mask: None,
23295 per_expert_scale: None,
23296 router_input_norm: false,
23297 resonance: resonant.then(|| Resonance {
23298 mu: vec![0.0; 6],
23299 u: Vec::new(),
23300 k: 0,
23301 bias: vec![0.0; 3],
23302 shell: Vec::new(),
23303 }),
23304 grown: Vec::new(),
23305 };
23306 let lo = -0.1f32;
23309 let hi = f32::from_bits(lo.to_bits() - 1);
23310 assert!(hi > lo && hi - lo < 2f32.powi(-25));
23311 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
23312 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
23314 assert_eq!(
23315 idx,
23316 vec![0],
23317 "softmax tie → lower index (the gated contract)"
23318 );
23319 for norm in [true, false] {
23322 let m = moe(true, norm);
23323 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
23324 assert_eq!(idx, vec![1], "norm_topk {norm}");
23325 assert_eq!(p, vec![0.0, 1.0, 0.0]);
23326 assert_eq!(p[1] / wsum, 1.0);
23327 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
23330 assert_eq!(idx, vec![0]);
23331 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
23333 assert_eq!(idx, vec![2]);
23334 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
23335 assert_eq!(idx, vec![0]);
23336 assert_eq!(p[0] / wsum, 1.0);
23337 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
23340 assert_eq!(idx, vec![0]);
23341 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
23342 }
23343 }
23344}