1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16use crate::kv_cache::KvCache;
17use crate::linear_core::{
18 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
19 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
20 vmf_phase_pair,
21};
22use crate::pool::Pool;
23use crate::qtensor::QTensor;
24use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
25use crate::tokenizer::Tokenizer;
26use cortiq_core::mask::TaskMask;
27use cortiq_core::types::NormStyle;
28
29pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
30 std::sync::atomic::AtomicBool::new(false);
31
32struct ForwardScratch {
36 n1: Vec<f32>,
37 n2: Vec<f32>,
38 p1: Vec<f32>,
39 p2: Vec<f32>,
40}
41
42impl ForwardScratch {
43 fn new(hidden: usize) -> Self {
44 Self {
45 n1: vec![0.0; hidden],
46 n2: vec![0.0; hidden],
47 p1: vec![0.0; hidden],
48 p2: vec![0.0; hidden],
49 }
50 }
51}
52
53pub struct Pipeline {
55 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
60 pub tokenizer: std::sync::Arc<Tokenizer>,
63 pub kv_cache: KvCache,
64 pub sampler_config: SamplerConfig,
65 pub weights: PipelineWeights,
66 pub hidden_size: usize,
67 pub intermediate_size: usize,
68 pub num_heads: usize,
69 pub num_kv_heads: usize,
70 pub head_dim: usize,
71 pub num_layers: usize,
73 pub physical_layers: usize,
75 pub loop_final_norm: bool,
77 pub vocab_size: usize,
78 pub rms_eps: f64,
79 pub rope_base: f32,
80 pub norm_style: NormStyle,
81 pub rotary_dim: usize,
83 pub attention_heads_per_layer: Option<Vec<usize>>,
85 pub kv_heads_per_layer: Option<Vec<usize>>,
90 pub v_head_dim: Option<usize>,
95 pub layer_dump: Option<std::path::PathBuf>,
108 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
111 pub(crate) mimo_moe: crate::mimo_moe::Slot,
114 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
119 pub vmf_cfg: Option<VmfPhaseCfg>,
121 pub gdn_cfg: Option<GdnCfg>,
123 pub logit_multiplier: Option<f32>,
125 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
130 graph_failed: std::sync::atomic::AtomicBool,
135 pub kv_history: Vec<u32>,
140 pub kv_history_device: bool,
143 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
145 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
148 pub dsv4: Option<
152 Box<(
153 crate::dsv4::Dsv4Globals,
154 Vec<crate::dsv4::Dsv4Layer>,
155 crate::dsv4::Dsv4Cfg,
156 crate::dsv4::Dsv4State,
157 )>,
158 >,
159 pub dsv41: Option<
163 Box<(
164 crate::dsv41::Dsv41Globals,
165 Vec<crate::dsv41::Dsv41Layer>,
166 crate::dsv41::Dsv41Cfg,
167 crate::dsv41::Dsv41State,
168 )>,
169 >,
170 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
172 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
174 pub qwen4_exp: Option<
177 Box<(
178 crate::qwen4_exp::Globals,
179 Vec<crate::qwen4_exp::Layer>,
180 crate::qwen4_exp::Cfg,
181 crate::qwen4_exp::State,
182 )>,
183 >,
184 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
188 pub dspark: Option<crate::dsv4::DsparkState>,
190 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
193 pub dspark_hist: Vec<usize>,
195 pub dspark_real: Vec<u32>,
199 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
203 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
205 pub dspark_draft_ns: u128,
209 pub short_conv_cfg: Option<ShortConvCfg>,
212 pub mtp: Option<MtpModule>,
214 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
218 verify_exact_moe: bool,
221 pub speculative: bool,
223 pub ignore_eos: bool,
229 pub draft_full_streak: u32,
235 pub spec_k_adapt: Option<usize>,
243 pub spec_acc_ewma: f32,
245 rng: SplitMix64,
246 sampler_scratch: SamplerScratch,
247 spec_forced: Option<u32>,
253 spec_q: Vec<Vec<f32>>,
254 spec_p: Vec<f32>,
255 spec_res: Vec<f32>,
256 spec_qs: Vec<sampler::Sparse>,
258 spec_ps: sampler::Sparse,
259 spec_ress: sampler::Sparse,
260 mtp_graph_mode: Option<bool>,
267 #[cfg(target_os = "macos")]
270 metal_verify: Option<MetalVerifyPending>,
271 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
275 ws: ForwardScratch,
279 pool: Option<std::sync::Arc<Pool>>,
281 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
285 pub(crate) dyn_force_f32: bool,
287 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
292 pub(crate) dyn_active: Option<usize>,
298 pub(crate) dyn_blend_loaded: bool,
302 pub(crate) dyn_phi_layer: Option<usize>,
305 dyn_phi_ema: Vec<f32>,
307 dyn_phi_seen: usize,
308 pub dyn_router: Option<crate::swarm::DynRouter>,
311 o1_cfg: Option<crate::nystrom::O1Cfg>,
314 o1_epoch: u64,
317 o1_flags: Vec<bool>,
319 trace: bool,
322 calib_temp: f32,
325 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
327 graph_kv_id: u64,
328 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
331 graph_want_logits: bool,
332 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
336 graph_head_required: bool,
337 graph_logits: Option<Vec<f32>>,
340 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
343 graph_refused: std::sync::atomic::AtomicBool,
349 pub embed_multiplier: f32,
351 pub attn_scale: f32,
354 pub swa: Option<(usize, usize)>,
357 pub swa_trim: Option<(usize, usize)>,
364 pub sliding_layers: Option<Vec<bool>>,
367 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
373 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
376 pub kv_prefix: KvPrefix,
380 pub last_prefill_tokens: usize,
383 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
386 pub rotary_dim_local: Option<usize>,
387 pub rope_scale: f32,
388 pub rope_scale_local: f32,
389 pub global_attn: Option<(usize, usize)>,
392 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
395 pub attn_v_norm: bool,
397 pub qk_norm_after_rope: bool,
399 pub proj_gate_sigmoid: bool,
402 pub final_softcap: Option<f32>,
404 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
408 pub attn_softcap: f32,
410 confidence_on: bool,
414 #[cfg(test)]
417 nll_test_fail_at: Option<usize>,
418 #[cfg(test)]
421 nll_test_force_serial: bool,
422}
423
424#[cfg(target_os = "macos")]
425impl Drop for Pipeline {
426 fn drop(&mut self) {
427 let _ = crate::gpu_metal::wait_replay();
429 crate::gpu::kv_mirror_drop(self.graph_kv_id);
430 }
431}
432
433#[cfg(not(target_os = "macos"))]
434impl Drop for Pipeline {
435 fn drop(&mut self) {
436 crate::gpu::graph_kv_reset(self.graph_kv_id);
440 }
441}
442
443pub struct PipelineWeights {
448 pub embed_tokens: QTensor,
450 pub layers: Vec<LayerWeights>,
452 pub lm_head: QTensor,
454 pub final_norm: Vec<f32>,
456}
457
458pub struct LayerWeights {
460 pub input_norm: Vec<f32>,
461 pub post_norm: Vec<f32>,
464 pub attn_out_norm: Option<Vec<f32>>,
467 pub layer_scale: Option<f32>,
469 pub ffn_out_norm: Option<Vec<f32>>,
472 pub ffn: FfnKind,
473 pub attn: AttnKind,
474}
475
476#[derive(Clone, Copy, PartialEq, Debug, Default)]
479pub enum Act {
480 #[default]
481 Silu,
482 GeluTanh,
483 Gelu,
485 Situ {
488 beta: f32,
489 linear_beta: f32,
490 },
491}
492
493impl Act {
494 pub fn from_arch(name: &str) -> Self {
495 if name == "gelu_tanh" {
496 Self::GeluTanh
497 } else if name == "gelu" {
498 Self::Gelu
499 } else {
500 Self::Silu
501 }
502 }
503
504 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
506 match arch.hidden_act.as_str() {
507 "situ" => Self::Situ {
508 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
509 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
510 },
511 other => Self::from_arch(other),
512 }
513 }
514
515 #[inline]
516 pub fn apply(self, x: f32) -> f32 {
517 match self {
518 Self::Silu => inference::silu(x),
519 Self::GeluTanh => inference::gelu_tanh(x),
520 Self::Gelu => inference::gelu_erf(x),
521 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
522 }
523 }
524
525 #[inline]
528 pub fn combine(self, g: f32, u: f32) -> f32 {
529 match self {
530 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
531 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
532 }
533 _ => self.apply(g) * u,
534 }
535 }
536
537 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
541 match self {
542 Self::Silu => Some(crate::gpu::GraphAct::Silu),
543 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
544 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
545 Self::Situ { .. } => None,
546 }
547 }
548}
549
550pub struct DenseFfn {
552 pub gate_proj: QTensor,
553 pub up_proj: QTensor,
554 pub down_proj: QTensor,
555 pub act: Act,
557 pub down_t: Option<QTensor>,
563 pub segs: Vec<FfnSeg>,
570}
571
572pub struct FfnSeg {
577 pub gate: QTensor,
578 pub up: QTensor,
579 pub down: QTensor,
580 pub start: usize,
581 pub width: usize,
582}
583
584pub enum FfnKind {
587 Dense(DenseFfn),
588 Moe(MoeFfn),
592 DenseMoe(Box<DenseMoeFfn>),
599}
600
601pub struct DenseMoeFfn {
603 pub dense: DenseFfn,
604 pub moe: MoeFfn,
605 pub post_norm_1: Vec<f32>,
607 pub pre_norm_2: Vec<f32>,
610 pub post_norm_2: Vec<f32>,
612}
613
614pub struct MoeFfn {
615 pub router: QTensor,
617 pub experts: Vec<DenseFfn>,
618 pub top_k: usize,
619 pub norm_topk_prob: bool,
620 pub router_sigmoid: bool,
623 pub expert_bias: Option<Vec<f32>>,
627 pub routed_scaling: f32,
630 pub route_tau: Option<f32>,
636 pub shared: Option<(DenseFfn, Option<QTensor>)>,
639 pub stats: std::cell::RefCell<Vec<u64>>,
643 pub act_sq: std::cell::RefCell<Vec<f64>>,
650 pub act_rows: std::cell::RefCell<Vec<f32>>,
656 pub mask: Option<Vec<bool>>,
661 pub per_expert_scale: Option<Vec<f32>>,
664 pub router_input_norm: bool,
668 pub resonance: Option<Resonance>,
672 pub grown: Vec<GrownExpert>,
678}
679
680#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct GrownExpert {
683 pub record: String,
685 pub record_index: usize,
687 pub layer: usize,
688 pub expert: usize,
692}
693
694pub struct Resonance {
696 pub mu: Vec<f32>,
698 pub u: Vec<f32>,
700 pub k: usize,
701 pub bias: Vec<f32>,
703 pub shell: Vec<f32>,
709}
710
711static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
714
715pub fn growth_shell_enabled() -> bool {
720 use std::sync::atomic::Ordering;
721 match GROWTH_SHELL.load(Ordering::Relaxed) {
722 1 => true,
723 2 => false,
724 _ => {
725 let off = std::env::var("CMF_GROWTH_SHELL")
726 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
727 .unwrap_or(false);
728 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
729 !off
730 }
731 }
732}
733
734pub fn set_growth_shell(on: Option<bool>) {
739 GROWTH_SHELL.store(
740 match on {
741 Some(true) => 1,
742 Some(false) => 2,
743 None => 0,
744 },
745 std::sync::atomic::Ordering::Relaxed,
746 );
747}
748
749impl Resonance {
750 pub fn has_shell(&self) -> bool {
752 self.shell.iter().any(|s| s.is_finite())
753 }
754
755 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
758 let mut out = vec![f32::INFINITY; ne];
759 if growth_shell_enabled() {
760 for (o, s) in out.iter_mut().zip(&self.shell) {
761 *o = *s;
762 }
763 }
764 out
765 }
766
767 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
772 let h = x.len();
773 let ne = out.len();
774 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
775 for e in 0..ne {
776 let mu = &self.mu[e * h..(e + 1) * h];
777 let mut d2 = 0.0f32;
778 for j in 0..h {
779 let d = x[j] - mu[j];
780 d2 += d * d;
781 }
782 let mut proj = 0.0f32;
783 for i in 0..self.k {
784 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
785 let mut p = 0.0f32;
786 for j in 0..h {
787 p += (x[j] - mu[j]) * u[j];
788 }
789 proj += p * p;
790 }
791 let err = d2 - proj;
792 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
793 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
794 out[e] = f32::NEG_INFINITY;
795 }
796 }
797 }
798}
799
800pub enum AttnKind {
803 Full {
805 wq: QTensor,
806 wk: QTensor,
807 wv: QTensor,
808 wo: QTensor,
809 q_norm: Option<Vec<f32>>,
810 k_norm: Option<Vec<f32>>,
811 output_gate: bool,
812 softplus_gate: Option<(QTensor, bool)>,
816 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
818 },
819 Linear(VmfPhaseWeights),
821 LinearGdn(GdnWeights),
823 ShortConv(ShortConvWeights),
826 Mla(Box<MlaWeights>),
834 Kda(Box<crate::linear_core::KdaWeights>),
838 Bounded(Box<crate::bounded::BoundedWeights>),
843}
844
845pub struct MlaWeights {
847 pub q_proj: QTensor,
851 pub q_a: Option<QTensor>,
854 pub q_a_norm: Option<Vec<f32>>,
855 pub kv_a: QTensor,
857 pub kv_a_norm: Vec<f32>,
859 pub kv_b: QTensor,
861 pub o_proj: QTensor,
863 pub nh: usize,
864 pub qk_rope: usize,
865 pub qk_nope: usize,
866 pub v_dim: usize,
867 pub lora: usize,
868 pub scale: f32,
870 pub nope: bool,
872}
873
874pub struct MtpModule {
879 pub enorm: Vec<f32>,
880 pub hnorm: Vec<f32>,
881 pub eh_proj: QTensor,
883 pub layer: LayerWeights,
884 pub final_norm: Vec<f32>,
885 pub kv: crate::kv_cache::LayerKvCache,
886}
887
888#[cfg(target_os = "macos")]
895enum MetalRowsItem<'a> {
896 Gdn {
897 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
898 first: usize,
899 },
900 Attn {
901 l: crate::gpu_metal::AttnGpuLayer<'a>,
902 li: usize,
903 q_norm: Option<&'a [f32]>,
904 k_norm: Option<&'a [f32]>,
905 output_gate: bool,
906 },
907}
908
909#[cfg(target_os = "macos")]
910struct MetalVerifyPending {
911 graph: crate::gpu_metal::VerifyGraph,
912 gdn_layers: Vec<usize>,
913 attn_layers: Vec<(usize, usize)>,
914}
915
916#[cfg(target_os = "macos")]
920struct MetalWarmPending {
921 graph: crate::gpu_metal::VerifyGraph,
922 cpu_stored: usize,
923 b: usize,
924}
925
926#[cfg(target_os = "macos")]
927enum MetalRowsRun {
928 Declined,
930 Failed,
933 Completed(MetalVerifyPending),
934}
935
936#[cfg(target_os = "macos")]
937enum MetalPrefillOutcome {
938 Declined,
939 Failed,
940 Completed(Vec<f32>),
941}
942
943#[cfg(target_os = "macos")]
944enum MetalBatchNllOutcome {
945 Declined,
946 Failed(String),
947 Completed(f64, usize),
948}
949
950#[derive(Clone, Copy)]
954enum SpecTrial {
955 Spec {
956 t0: std::time::Instant,
957 gen0: usize,
958 rounds: usize,
959 },
960 Plain {
961 t0: std::time::Instant,
962 gen0: usize,
963 },
964 Decided {
965 spec: bool,
966 recheck_at: usize,
967 },
968}
969
970pub(crate) fn spec_time_level() -> u8 {
974 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
975 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
976 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
977 Err(_) => 0,
978 })
979}
980
981struct SpecStampLog {
987 t_last: std::time::Instant,
988 items: Vec<(&'static str, f32)>,
989}
990
991static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
992
993pub(crate) fn spec_stamp(name: &'static str) {
994 if spec_time_level() == 0 {
995 return;
996 }
997 if let Ok(mut g) = SPEC_STAMPS.lock() {
998 if let Some(log) = g.as_mut() {
999 let now = std::time::Instant::now();
1000 log.items
1001 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1002 log.t_last = now;
1003 }
1004 }
1005}
1006
1007fn spec_stamps_begin() {
1008 if spec_time_level() == 0 {
1009 return;
1010 }
1011 if let Ok(mut g) = SPEC_STAMPS.lock() {
1012 *g = Some(SpecStampLog {
1013 t_last: std::time::Instant::now(),
1014 items: Vec::with_capacity(64),
1015 });
1016 }
1017}
1018
1019fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1020 SPEC_STAMPS
1021 .lock()
1022 .ok()
1023 .and_then(|mut g| g.take())
1024 .map(|l| l.items)
1025 .unwrap_or_default()
1026}
1027
1028fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1031 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1032 for &(n, ms) in items {
1033 match agg.iter_mut().find(|e| e.0 == n) {
1034 Some(e) => {
1035 e.1 += ms;
1036 e.2 += 1;
1037 }
1038 None => agg.push((n, ms, 1)),
1039 }
1040 }
1041 let mut s = String::with_capacity(agg.len() * 16);
1042 for (n, ms, k) in agg {
1043 if k > 1 {
1044 s.push_str(&format!("{n} {ms:.1}/{k} "));
1045 } else {
1046 s.push_str(&format!("{n} {ms:.1} "));
1047 }
1048 }
1049 s
1050}
1051
1052#[derive(Default, Clone, Copy)]
1074struct SpecMon {
1075 round_ms: f64,
1076 tokens: f64,
1077 plain_ms: f64,
1078 n: u32,
1079 fails: u32,
1080 metal: bool,
1081}
1082
1083const SPEC_PROXY_TOKENS: f64 = 3.5;
1086const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1089
1090impl SpecMon {
1091 fn round(&mut self, dt_ms: f64, produced: usize) {
1092 self.n += 1;
1093 if self.n == 1 {
1094 return; }
1096 let a = if self.n == 2 { 1.0 } else { 0.3 };
1097 self.round_ms += a * (dt_ms - self.round_ms);
1098 self.tokens += a * (produced as f64 - self.tokens);
1099 }
1100 fn pays(&self) -> bool {
1101 if self.plain_ms > 0.0 {
1102 self.tokens * self.plain_ms > self.round_ms * 1.03
1103 } else {
1104 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1105 }
1106 }
1107 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1109 let n = generated.saturating_sub(gen0);
1110 if n >= 8 {
1111 return true;
1112 }
1113 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1114 }
1115}
1116
1117pub const KV_PREFIX_TAIL: usize = 128;
1120
1121#[derive(Debug, Clone, Default)]
1127pub struct KvPrefix {
1128 len: usize,
1129 hash: u64,
1130 tail: Vec<u32>,
1131 device: bool,
1136}
1137
1138impl KvPrefix {
1139 #[inline]
1140 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1141 for &id in ids {
1142 h ^= id as u64;
1143 h = h.wrapping_mul(0x100000001b3);
1144 h ^= h >> 29;
1145 }
1146 h
1147 }
1148
1149 pub fn clear(&mut self) {
1150 self.len = 0;
1151 self.hash = 0xcbf29ce484222325;
1152 self.tail.clear();
1153 self.device = false;
1154 }
1155
1156 pub fn on_device(&self) -> bool {
1158 self.device
1159 }
1160
1161 pub fn set_on_device(&mut self, device: bool) {
1163 self.device = device;
1164 }
1165
1166 pub fn len(&self) -> usize {
1168 self.len
1169 }
1170
1171 pub fn is_empty(&self) -> bool {
1172 self.len == 0
1173 }
1174
1175 pub fn tail_len(&self) -> usize {
1177 self.tail.len()
1178 }
1179
1180 pub fn set(&mut self, ids: &[u32]) {
1182 self.clear();
1183 self.extend(ids);
1184 }
1185
1186 pub fn extend(&mut self, more: &[u32]) {
1188 if self.len == 0 && self.hash == 0 {
1189 self.hash = 0xcbf29ce484222325;
1190 }
1191 self.hash = Self::fold(self.hash, more);
1192 self.len += more.len();
1193 if more.len() >= KV_PREFIX_TAIL {
1194 self.tail.clear();
1195 self.tail.extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1196 } else {
1197 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1198 self.tail.drain(..drop);
1199 self.tail.extend_from_slice(more);
1200 }
1201 }
1202
1203 pub fn extension(&self, ids: &[u32]) -> usize {
1207 if self.len == 0 || ids.len() <= self.len {
1208 return 0;
1209 }
1210 let t = self.tail.len();
1211 if ids[self.len - t..self.len] != self.tail[..] {
1212 return 0;
1213 }
1214 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1215 return 0;
1216 }
1217 self.len
1218 }
1219}
1220
1221pub struct GenerateResult {
1223 pub text: String,
1224 pub token_ids: Vec<u32>,
1225 pub prompt_tokens: usize,
1226 pub tokens_generated: usize,
1227 pub finish_reason: String,
1228 pub mtp_drafted: usize,
1230 pub mtp_accepted: usize,
1231 pub token_confidence: Vec<f32>,
1236 pub traces: Vec<TokenTrace>,
1239}
1240
1241#[derive(Clone, Debug)]
1246pub struct TokenTrace {
1247 pub t: usize,
1249 pub token_id: u32,
1251 pub confidence: f32,
1253 pub active_skill: Option<String>,
1255 pub recon: Option<f32>,
1259 pub switched: bool,
1262}
1263
1264#[cfg_attr(not(test), allow(dead_code))]
1269fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1270 let t = if temp > 1e-3 { temp } else { 1.0 };
1271 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1272 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1273 if sum > 0.0 {
1274 (((logits[id as usize] - max) / t).exp()) / sum
1275 } else {
1276 0.0
1277 }
1278}
1279
1280fn prefill_batched() -> bool {
1283 std::env::var("CMF_PREFILL")
1284 .map(|v| v != "seq")
1285 .unwrap_or(true)
1286}
1287
1288#[inline]
1292fn nll_graph_policy(
1293 unmasked: bool,
1294 prefer_graph: bool,
1295 native_metal: bool,
1296) -> (bool, bool) {
1297 let graph_quality = unmasked && prefer_graph;
1298 let fused_head_quality = graph_quality && native_metal;
1299 (graph_quality, fused_head_quality)
1300}
1301
1302#[derive(Clone, Copy)]
1306enum PrefillIn<'a> {
1307 Ids(&'a [u32]),
1308 Hidden(&'a [f32]),
1309}
1310
1311impl Pipeline {
1318 fn can_prefill_batched(&self) -> bool {
1319 #[cfg(test)]
1320 let force_serial = self.nll_test_force_serial;
1321 #[cfg(not(test))]
1322 let force_serial = false;
1323 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1324 }
1325
1326 fn automatic_gpu_prefix(&self) -> Option<usize> {
1329 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1330 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1331 }
1332
1333 pub fn prefill_chunk(&self) -> usize {
1337 let env = env_prefill_chunk();
1338 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1339 return prefill_chunk_rule(env, ChunkHost::here(), false);
1340 }
1341 prefill_chunk_rule(None, ChunkHost::Other, self.chunk_stack_facts().dense_on_discrete())
1342 }
1343
1344 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1345 let plain_dense = !self.weights.layers.is_empty()
1346 && self.g3n.is_none()
1347 && self.dsv4.is_none()
1348 && self.dsv41.is_none()
1349 && self.qwen4_exp.is_none()
1350 && self.weights.layers.iter().all(|lw| {
1351 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1352 });
1353 let gpu_on = crate::gpu::enabled();
1354 ChunkStackFacts {
1355 plain_dense,
1356 discrete: gpu_on && crate::gpu::discrete(),
1357 gpu_on,
1358 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1361 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1362 multi_gpu: self.gpu_plan.is_some(),
1363 o1: self.o1_active(),
1364 }
1365 }
1366}
1367
1368pub fn prefill_chunk() -> usize {
1377 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1378}
1379
1380fn env_prefill_chunk() -> Option<usize> {
1381 std::env::var("CMF_PREFILL_CHUNK")
1382 .ok()
1383 .and_then(|v| v.parse::<usize>().ok())
1384}
1385
1386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1388enum ChunkHost {
1389 Macos,
1390 Aarch64,
1392 Other,
1394}
1395
1396impl ChunkHost {
1397 fn here() -> Self {
1398 if cfg!(target_os = "macos") {
1399 ChunkHost::Macos
1400 } else if cfg!(target_arch = "aarch64") {
1401 ChunkHost::Aarch64
1402 } else {
1403 ChunkHost::Other
1404 }
1405 }
1406}
1407
1408const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1415
1416fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1422 if let Some(n) = env {
1423 return n.max(1);
1424 }
1425 match host {
1426 ChunkHost::Macos => 512,
1427 ChunkHost::Aarch64 => 256,
1430 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1431 ChunkHost::Other => 48,
1432 }
1433}
1434
1435#[derive(Clone, Copy, Debug, Default)]
1437struct ChunkStackFacts {
1438 plain_dense: bool,
1441 discrete: bool,
1443 gpu_on: bool,
1445 capacity_split: bool,
1447 multi_gpu: bool,
1449 o1: bool,
1451}
1452
1453impl ChunkStackFacts {
1454 fn dense_on_discrete(self) -> bool {
1455 self.plain_dense
1456 && self.discrete
1457 && self.gpu_on
1458 && !self.capacity_split
1459 && !self.multi_gpu
1460 && !self.o1
1461 }
1462}
1463
1464#[inline]
1470fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1471 if end <= start || start >= input_len {
1472 return 0;
1473 }
1474 let rows = (end.min(input_len) - start).min(input_len - start);
1475 if end < input_len {
1476 rows
1477 } else {
1478 rows.saturating_sub(1)
1479 }
1480}
1481
1482pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1484
1485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1487pub(crate) struct ReuseLayer {
1488 pub full: bool,
1491 pub host_rows: usize,
1494 pub device_rows: Option<usize>,
1496 pub device_state: bool,
1498}
1499
1500#[derive(Debug, Clone, PartialEq, Eq)]
1502pub(crate) enum ReusePlan {
1503 Ready,
1505 Pull(Vec<(usize, usize, usize)>),
1508 Fresh,
1510}
1511
1512pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1522 let mut pulls = Vec::new();
1523 for (li, l) in layers.iter().enumerate() {
1524 if !l.full {
1525 if l.device_state {
1526 return ReusePlan::Fresh;
1527 }
1528 continue;
1529 }
1530 if l.host_rows == reuse_from {
1531 continue;
1532 }
1533 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1534 pulls.push((li, l.host_rows, reuse_from));
1535 continue;
1536 }
1537 return ReusePlan::Fresh;
1538 }
1539 if pulls.is_empty() {
1540 ReusePlan::Ready
1541 } else {
1542 ReusePlan::Pull(pulls)
1543 }
1544}
1545
1546impl Pipeline {
1547 fn clear_sequence_state(&mut self) {
1555 #[cfg(target_os = "macos")]
1558 let _ = crate::gpu_metal::wait_replay();
1559 self.kv_cache.clear();
1560 self.clear_history();
1563 self.graph_logits = None;
1564 if let Some(b) = &mut self.dsv41 {
1565 b.3.clear();
1566 }
1567 crate::gpu::graph_kv_reset(self.graph_kv_id);
1568 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1573 }
1574
1575 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1582 if self.graph_prefill_preferred() {
1583 return true;
1584 }
1585 let kv_id = self.graph_kv_id;
1586 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1587 .map(|li| {
1588 let full = matches!(
1589 self.weights.layers[self.phys_layer(li)].attn,
1590 AttnKind::Full { .. }
1591 );
1592 ReuseLayer {
1593 full,
1594 host_rows: self.kv_cache.layers[li].pos_len(),
1597 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1598 device_state: crate::gpu::graph_state_resident(kv_id, li),
1599 }
1600 })
1601 .collect();
1602 if layers
1606 .iter()
1607 .all(|l| l.device_rows.is_none() && !l.device_state)
1608 {
1609 return true;
1610 }
1611 let plan = kv_reuse_plan(reuse_from, &layers);
1612 let (what, rows, n) = match &plan {
1613 ReusePlan::Ready => ("host ready", 0, 0),
1614 ReusePlan::Fresh => ("fresh", 0, 0),
1615 ReusePlan::Pull(p) => (
1616 "pull",
1617 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1618 p.len(),
1619 ),
1620 };
1621 let t0 = std::time::Instant::now();
1622 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1623 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1624 eprintln!(
1625 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1626 if ok { "" } else { " (failed → fresh)" },
1627 t0.elapsed().as_secs_f64() * 1e3
1628 );
1629 }
1630 ok
1631 }
1632
1633 fn apply_kv_reuse_plan(
1634 &mut self,
1635 reuse_from: usize,
1636 plan: ReusePlan,
1637 layers: &[ReuseLayer],
1638 ) -> bool {
1639 let kv_id = self.graph_kv_id;
1640 match plan {
1641 ReusePlan::Fresh => return false,
1642 ReusePlan::Ready => {}
1643 ReusePlan::Pull(pulls) => {
1644 let (nkv, hd) = {
1649 let c = &self.kv_cache.layers[pulls[0].0];
1650 (c.num_kv_heads, c.head_dim)
1651 };
1652 let uniform = pulls.iter().all(|&(li, _, _)| {
1653 let c = &self.kv_cache.layers[li];
1654 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1655 });
1656 let batched = if uniform {
1657 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1658 } else {
1659 None
1660 };
1661 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1662 Some(rows) => rows,
1663 None => {
1664 let mut rows = Vec::with_capacity(pulls.len());
1665 for &(li, from, to) in &pulls {
1666 let (lnkv, lhd) = {
1667 let c = &self.kv_cache.layers[li];
1668 (c.num_kv_heads, c.head_dim)
1669 };
1670 let Some((k, v, first_valid)) =
1671 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1672 else {
1673 return false;
1674 };
1675 let need_from = match self.layer_window(li) {
1679 Some(w) => from.max((to + 1).saturating_sub(w)),
1680 None => from,
1681 };
1682 if first_valid > need_from {
1683 return false;
1684 }
1685 rows.push((k, v));
1686 }
1687 rows
1688 }
1689 };
1690 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1691 let cache = &mut self.kv_cache.layers[li];
1692 let row = cache.num_kv_heads * cache.head_dim;
1693 for p in 0..to - from {
1694 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1695 }
1696 if cache.pos_len() != to {
1697 return false;
1698 }
1699 }
1700 }
1701 }
1702 for (li, l) in layers.iter().enumerate() {
1706 if l.full
1707 && l.device_rows.is_some_and(|d| d > reuse_from)
1708 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1709 {
1710 return false;
1711 }
1712 }
1713 true
1714 }
1715
1716 fn finish_generation(
1722 &mut self,
1723 mtp: &mut Option<MtpModule>,
1724 router: &mut Option<crate::swarm::DynRouter>,
1725 clear_sequence: bool,
1726 ) {
1727 if router.is_some() {
1731 let _ = self.set_active_skill(None);
1732 }
1733 #[cfg(target_os = "macos")]
1740 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1741 if clear_sequence {
1742 self.clear_sequence_state();
1743 if let Some(m) = mtp.as_mut() {
1744 m.kv.clear();
1750 }
1751 if let Some(m) = self.mtp.as_mut() {
1752 m.kv.clear();
1756 }
1757 }
1758 self.graph_want_logits = false;
1759 self.graph_head_required = false;
1760 self.graph_logits = None;
1761 self.graph_failed
1762 .store(false, std::sync::atomic::Ordering::Relaxed);
1763 self.cancel
1764 .store(false, std::sync::atomic::Ordering::Relaxed);
1765 self.dyn_router = router.take().or(self.dyn_router.take());
1766 self.mtp = mtp.take().or(self.mtp.take());
1767 self.mtp_graph_mode = None;
1768 self.spec_forced = None;
1769 }
1770
1771 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1775 if self
1776 .graph_failed
1777 .swap(false, std::sync::atomic::Ordering::Relaxed)
1778 {
1779 self.cancel
1780 .store(false, std::sync::atomic::Ordering::Relaxed);
1781 self.clear_sequence_state();
1782 self.graph_logits = None;
1783 self.graph_want_logits = false;
1784 self.graph_head_required = false;
1785 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1786 }
1787 Ok(())
1788 }
1789
1790 #[cfg(target_os = "macos")]
1791 fn fail_metal_graph(&mut self, reason: &str) {
1792 crate::pipeline::METAL_GRAPH_ERRORS
1793 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1794 self.clear_sequence_state();
1795 self.graph_logits = None;
1796 self.graph_failed
1797 .store(true, std::sync::atomic::Ordering::Relaxed);
1798 self.cancel
1799 .store(true, std::sync::atomic::Ordering::Relaxed);
1800 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1801 }
1802
1803 fn nll_begin(&mut self) -> Result<(), String> {
1808 if self
1809 .graph_failed
1810 .swap(false, std::sync::atomic::Ordering::Relaxed)
1811 {
1812 self.cancel
1813 .store(false, std::sync::atomic::Ordering::Relaxed);
1814 self.clear_sequence_state();
1815 self.graph_logits = None;
1816 self.graph_want_logits = false;
1817 self.graph_head_required = false;
1818 return Err("GPU graph failed before NLL scoring".to_string());
1819 }
1820 self.clear_sequence_state();
1821 self.graph_logits = None;
1822 self.graph_want_logits = false;
1823 self.graph_head_required = false;
1824 Ok(())
1825 }
1826
1827 fn nll_end(&mut self) {
1831 self.clear_sequence_state();
1832 self.graph_logits = None;
1833 self.graph_want_logits = false;
1834 self.graph_head_required = false;
1835 self.graph_failed
1836 .store(false, std::sync::atomic::Ordering::Relaxed);
1837 }
1838
1839 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1842 #[cfg(test)]
1843 if self.nll_test_fail_at == Some(pos) {
1844 self.nll_test_fail_at = None;
1845 self.graph_failed
1846 .store(true, std::sync::atomic::Ordering::Relaxed);
1847 self.cancel
1848 .store(true, std::sync::atomic::Ordering::Relaxed);
1849 }
1850 if self
1851 .graph_failed
1852 .swap(false, std::sync::atomic::Ordering::Relaxed)
1853 {
1854 self.cancel
1855 .store(false, std::sync::atomic::Ordering::Relaxed);
1856 self.clear_sequence_state();
1857 self.graph_logits = None;
1858 self.graph_want_logits = false;
1859 return Err(format!(
1860 "GPU graph failed during NLL {phase} at position {pos}"
1861 ));
1862 }
1863 Ok(())
1864 }
1865
1866 #[inline]
1870 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1871 virtual_idx % self.physical_layers
1872 }
1873
1874 #[inline]
1877 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1878 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1879 }
1880
1881 #[allow(clippy::too_many_arguments)]
1883
1884 #[cfg(target_os = "macos")]
1903 fn graph_prefill_preferred(&self) -> bool {
1904 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1905 if !crate::gpu::enabled_here()
1906 || !graph_force
1907 || std::env::var("CMF_GPU_BLOCK")
1908 .map(|v| v == "0")
1909 .unwrap_or(false)
1910 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1913 {
1914 return false;
1915 }
1916 self.weights
1917 .layers
1918 .iter()
1919 .any(|lw| {
1920 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1921 })
1922 }
1923
1924 #[cfg(not(target_os = "macos"))]
1931 fn batch_prefix_prefill(&self) -> bool {
1932 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1933 Ok("0") => return false,
1934 Ok("1") => true,
1935 _ => false,
1936 };
1937 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1938 && crate::gpu::enabled_here()
1939 && !self.graph_refused()
1940 && (forced || self.graph_attn_decline_reason().is_some())
1941 && self.wgpu_graph_attn_decline().is_none()
1942 && self.attn_softcap == 0.0
1943 && self
1944 .weights
1945 .layers
1946 .iter()
1947 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1948 && self.automatic_gpu_prefix().is_some()
1949 }
1950
1951 fn g4_batch_prefill(&self) -> bool {
1957 #[cfg(target_os = "macos")]
1958 {
1959 false
1960 }
1961 #[cfg(not(target_os = "macos"))]
1962 {
1963 (self.global_attn.is_some() || self.attn_v_norm)
1964 && crate::gpu::g4_batch_on()
1965 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1966 && crate::gpu::enabled_here()
1967 && crate::gpu::discrete()
1968 && !self.graph_refused()
1969 && !self.o1_active()
1970 && self.attn_softcap == 0.0
1971 && self.wgpu_graph_attn_decline().is_none()
1972 && self.weights.layers.iter().all(|lw| {
1973 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1974 })
1975 && self.automatic_gpu_prefix().is_none()
1976 }
1977 }
1978
1979 #[cfg(not(target_os = "macos"))]
1986 fn g4_batch_ingest(
1987 &mut self,
1988 ids: &[u32],
1989 pos0: usize,
1990 keep_all: bool,
1991 ) -> Result<(usize, Vec<f32>), String> {
1992 let hs = self.hidden_size;
1993 let chunk = self.generation_batch_k().max(1);
1994 let end_all = pos0 + ids.len();
1995 let mut pos = pos0;
1996 let mut out = Vec::new();
1997 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1998 let end = (pos + chunk).min(end_all);
1999 let bk = end - pos;
2000 let mut hiddens = vec![0f32; bk * hs];
2001 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2002 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2003 }
2004 let positions: Vec<usize> = (pos..end).collect();
2005 let t_chunk = std::time::Instant::now();
2006 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2007 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2008 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2009 eprintln!(
2010 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2011 bk as f64 / (ms / 1000.0)
2012 );
2013 }
2014 match outcome {
2015 crate::gpu::BatchGraphOutcome::Completed => {
2016 if keep_all {
2017 out.extend_from_slice(&hiddens);
2018 } else {
2019 out = hiddens[(bk - 1) * hs..].to_vec();
2020 }
2021 pos = end;
2022 }
2023 crate::gpu::BatchGraphOutcome::Failed => {
2024 return Err("Gemma-4 batched prefill failed after admission".into());
2025 }
2026 crate::gpu::BatchGraphOutcome::Declined => {
2027 #[cfg(feature = "gpu")]
2028 if pos > pos0 {
2029 self.pull_lagging_host_kv(0, self.num_layers, pos);
2030 }
2031 break;
2032 }
2033 }
2034 }
2035 Ok((pos - pos0, out))
2036 }
2037
2038 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2043 #[cfg(not(target_os = "macos"))]
2044 {
2045 if ids.is_empty() || !self.g4_batch_prefill() {
2046 return None;
2047 }
2048 self.clear_sequence_state();
2049 match self.g4_batch_ingest(ids, 0, true) {
2050 Ok((n, rows)) if n == ids.len() => Some(rows),
2051 _ => {
2052 self.clear_sequence_state();
2053 None
2054 }
2055 }
2056 }
2057 #[cfg(target_os = "macos")]
2058 {
2059 let _ = ids;
2060 None
2061 }
2062 }
2063
2064 #[cfg(not(target_os = "macos"))]
2065 fn graph_prefill_preferred(&self) -> bool {
2066 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2074 if !graph_on || !crate::gpu::enabled_here() {
2075 return false;
2076 }
2077 if self.embryo_resident_eligible() {
2081 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2086 }
2087 if self.o1_active() {
2101 return false;
2102 }
2103 if self.wgpu_graph_attn_decline().is_some() {
2107 return false;
2108 }
2109 if self
2110 .weights
2111 .layers
2112 .iter()
2113 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2114 {
2115 return true;
2116 }
2117 self.weights
2128 .layers
2129 .iter()
2130 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2131 && self.automatic_gpu_prefix().is_none()
2132 }
2133
2134 #[cfg(target_os = "macos")]
2135 fn q1_graph_gpu(
2136 &mut self,
2137 start: usize,
2138 upto: Option<usize>,
2139 position: usize,
2140 h: &mut [f32],
2141 ) -> usize {
2142 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2144 const MOE_WORKING_SET_REASON: &str =
2145 "model weights exceed Metal working set for native MoE graph";
2146 if self
2150 .graph_declines
2151 .borrow()
2152 .iter()
2153 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2154 {
2155 return start;
2156 }
2157 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2158 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2160 || !crate::gpu::enabled_here()
2161 || !graph_force
2162 || std::env::var("CMF_GPU_BLOCK")
2163 .map(|v| v == "0")
2164 .unwrap_or(false)
2165 {
2166 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2167 eprintln!(
2168 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2169 self.attn_softcap > 0.0,
2170 crate::gpu::enabled_here(),
2171 graph_force,
2172 );
2173 }
2174 if self.graph_head_required {
2175 self.fail_metal_graph("native graph front gate refused");
2176 }
2177 return start;
2178 }
2179 let gemma4 = self.metal_gemma4();
2189 let swa_graph = self.metal_graph_swa() || gemma4;
2190 if (self.swa.is_some() && !swa_graph)
2191 || (self.global_attn.is_some() && !gemma4)
2192 || self.attention_heads_per_layer.is_some()
2193 || (self.attn_v_norm && !gemma4)
2194 || (self.graph_attn_decline_reason().is_some() && !swa_graph)
2196 || self.weights.layers.iter().any(|lw| {
2197 ((lw.attn_out_norm.is_some()
2198 || lw.ffn_out_norm.is_some()
2199 || lw.layer_scale.is_some())
2200 && !gemma4)
2201 || matches!(&lw.ffn, FfnKind::Dense(d)
2202 if !(matches!(d.act, Act::Silu | Act::Gelu)
2203 || (gemma4 && d.act == Act::GeluTanh)))
2204 })
2205 {
2206 if let Some(reason) = self.graph_attn_decline_reason().filter(|_| !swa_graph) {
2209 self.note_graph_decline("metal block graph", reason);
2210 }
2211 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2212 eprintln!(
2213 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2214 self.swa.is_some(),
2215 self.global_attn.is_some(),
2216 self.attention_heads_per_layer.is_some(),
2217 self.attn_v_norm,
2218 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2219 );
2220 }
2221 if self.graph_head_required {
2222 self.fail_metal_graph("native graph architecture gate refused");
2223 }
2224 return start;
2225 }
2226 let limit = upto
2229 .map(|u| u + 1)
2230 .unwrap_or(self.num_layers)
2231 .min(self.num_layers);
2232
2233 enum Item<'a> {
2234 Gdn {
2235 run: Vec<GdnGpuLayer<'a>>,
2236 first: usize,
2237 },
2238 Attn {
2239 l: AttnGpuLayer<'a>,
2240 li: usize,
2241 q_norm: Option<&'a [f32]>,
2242 k_norm: Option<&'a [f32]>,
2243 output_gate: bool,
2244 proj_gate: Option<(&'a QTensor, bool)>,
2248 head_gate_w: Option<&'a [f32]>,
2251 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2252 full_gpu: bool,
2255 },
2256 }
2257
2258 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2265 let attend_contract = attend_mode != "0"
2266 && attend_mode != "off"
2267 && self.head_dim % 4 == 0
2268 && self.head_dim <= 256
2269 && self.rotary_dim >= 2
2270 && self.rotary_dim <= self.head_dim
2271 && (self.rotary_dim / 2) % 32 == 0
2272 && self.num_kv_heads > 0
2273 && self.num_heads % self.num_kv_heads == 0;
2274
2275 let mut plan: Vec<Item> = Vec::new();
2276 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2277 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2279 let mut scan = start;
2280 while scan < limit {
2281 let lw = &self.weights.layers[self.phys_layer(scan)];
2282 let ffn = match &lw.ffn {
2283 FfnKind::Dense(d) if d.segs.is_empty() => {
2284 let (Some(g), Some(u), Some(dn)) = (
2285 d.gate_proj.metal_graph_parts(),
2286 d.up_proj.metal_graph_parts(),
2287 d.down_proj.metal_graph_parts(),
2288 ) else {
2289 if block_diag {
2290 eprintln!(
2291 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2292 );
2293 }
2294 break;
2295 };
2296 MetalFfn::Dense {
2297 gate: g,
2298 up: u,
2299 down: dn,
2300 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2303 }
2304 }
2305 FfnKind::Moe(m) => {
2306 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
2307 if block_diag {
2308 eprintln!(
2309 "block-graph: L{scan} MoE outside the graph contract — run ends"
2310 );
2311 }
2312 break;
2313 };
2314 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2315 model_ref.get_or_insert_with(|| model.clone());
2316 }
2317 MetalFfn::Moe(moe)
2318 }
2319 _ => {
2320 if block_diag {
2321 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2322 }
2323 break;
2324 }
2325 };
2326 match &lw.attn {
2327 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2328 let parts = (
2329 w.in_proj_qkv.metal_graph_parts(),
2330 w.in_proj_z.metal_graph_parts(),
2331 w.in_proj_a.f32_parts(),
2332 w.in_proj_b.f32_parts(),
2333 w.out_proj.metal_graph_parts(),
2334 );
2335 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2336 if block_diag {
2337 eprintln!(
2338 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2339 w.in_proj_qkv.metal_graph_parts().is_some(),
2340 w.in_proj_z.metal_graph_parts().is_some(),
2341 w.in_proj_a.f32_parts().is_some(),
2342 w.in_proj_b.f32_parts().is_some(),
2343 w.out_proj.metal_graph_parts().is_some(),
2344 );
2345 }
2346 break;
2347 };
2348 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2349 model_ref.get_or_insert_with(|| model.clone());
2350 }
2351 let gl = GdnGpuLayer {
2352 attn_norm: &lw.input_norm,
2353 post_norm: &lw.post_norm,
2354 qkv,
2355 z,
2356 a,
2357 b,
2358 out,
2359 ffn,
2360 conv1d: &w.conv1d,
2361 a_log: &w.a_log,
2362 dt_bias: &w.dt_bias,
2363 gnorm: &w.norm,
2364 };
2365 match plan.last_mut() {
2366 Some(Item::Gdn { run, .. }) => run.push(gl),
2367 _ => plan.push(Item::Gdn {
2368 run: vec![gl],
2369 first: scan,
2370 }),
2371 }
2372 }
2373 AttnKind::Full {
2374 wq,
2375 wk,
2376 wv,
2377 wo,
2378 q_norm,
2379 k_norm,
2380 output_gate,
2381 softplus_gate,
2382 bias,
2383 } if (!self.kv_cache.layers[scan].o1_sealed()
2384 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2389 && softplus_gate
2393 .as_ref()
2394 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2395 {
2396 let parts = (
2397 wq.metal_graph_parts(),
2398 wk.metal_graph_parts(),
2399 wv.metal_graph_parts(),
2400 wo.metal_graph_parts(),
2401 );
2402 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2403 break;
2404 };
2405 if let QTensor::Mapped { model, .. } = wq {
2406 model_ref.get_or_insert_with(|| model.clone());
2407 }
2408 let cache = &self.kv_cache.layers[scan];
2409 let o1_metal = cache.o1.is_some()
2413 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2414 && cache.o1_views().is_some();
2415 let head_gate_w = softplus_gate
2419 .as_ref()
2420 .and_then(|(g, _)| g.f32_parts())
2421 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2422 .map(|(d, _, _)| d);
2423 let (nkv_g, hd_g) = if gemma4 {
2426 let (a, b, _) = self.layer_geom(scan);
2427 (a, b)
2428 } else {
2429 (self.num_kv_heads, self.head_dim)
2430 };
2431 let full_gpu = attend_contract
2432 && softplus_gate.is_none() == head_gate_w.is_none()
2433 && cache.mode == crate::kv_cache::KvMode::F32
2434 && (cache.o1.is_none() || o1_metal)
2435 && bias.is_none()
2436 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2437 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2438 && pk.1 == nkv_g * hd_g
2439 && pv.1 == nkv_g * hd_g
2440 && po.2 == self.num_heads * hd_g;
2441 plan.push(Item::Attn {
2442 l: AttnGpuLayer {
2443 attn_norm: &lw.input_norm,
2444 post_norm: &lw.post_norm,
2445 wq: pq,
2446 wk: pk,
2447 wv: pv,
2448 wo: po,
2449 ffn,
2450 attn_out_norm: lw.attn_out_norm.as_deref(),
2451 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2452 layer_scale: lw.layer_scale,
2453 },
2454 li: scan,
2455 q_norm: q_norm.as_deref(),
2456 k_norm: k_norm.as_deref(),
2457 output_gate: *output_gate,
2458 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2459 head_gate_w,
2460 bias: bias
2461 .as_ref()
2462 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2463 full_gpu,
2464 });
2465 }
2466 _ => break,
2467 }
2468 scan += 1;
2469 }
2470 let Some(model) = model_ref else {
2471 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2472 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2473 }
2474 if self.graph_head_required {
2475 self.fail_metal_graph("native graph has no mapped model reference");
2476 }
2477 return start;
2478 };
2479 if plan.is_empty() {
2480 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2481 eprintln!("q1-graph: empty plan at layer {start}");
2482 }
2483 if self.graph_head_required {
2484 self.fail_metal_graph("native graph plan is empty");
2485 }
2486 return start;
2487 }
2488 let has_moe = plan.iter().any(|it| match it {
2489 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
2490 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
2491 });
2492 let metal_working_set = crate::gpu_metal::working_set_bytes();
2493 let model_bytes = model.primary_bytes().len() as u64;
2494 let pager_cap = has_moe
2503 .then(crate::gpu_metal::moe_pager_cap_bytes)
2504 .flatten();
2505 let pager_eligible = pager_cap.is_some()
2506 && !self.graph_head_required
2507 && metal_working_set != 0
2508 && model_bytes > metal_working_set;
2509 let mut pager_span: Option<(usize, usize)> = None;
2510 if pager_eligible {
2511 let cap = pager_cap.expect("checked above");
2512 let tail_span = (self.graph_want_logits
2516 && upto.is_none()
2517 && scan == self.num_layers
2518 && std::env::var("CMF_GPU_LMHEAD")
2519 .map(|v| v != "0")
2520 .unwrap_or(true))
2521 .then(|| self.weights.lm_head.metal_graph_parts())
2522 .flatten()
2523 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2524 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2525 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2526 Some(match span {
2527 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2528 None => (lo, hi),
2529 })
2530 };
2531 let page_span = |span: (usize, usize), include_tail: bool| {
2532 let (mut lo, mut hi) = span;
2533 if include_tail {
2534 if let Some((tail_lo, tail_hi)) = tail_span {
2535 lo = lo.min(tail_lo);
2536 hi = hi.max(tail_hi);
2537 }
2538 }
2539 crate::gpu_metal::page_round_span(lo, hi)
2540 };
2541 let mut selected = 0usize;
2542 let mut selected_span: Option<(usize, usize)> = None;
2543 let mut gdn_prefix: Option<(usize, usize)> = None;
2551 'items: for (i, item) in plan.iter().enumerate() {
2552 match item {
2553 Item::Attn { li, .. } => {
2554 let Some(span) = add_layer_span(selected_span, *li) else {
2555 break;
2556 };
2557 let Some((page_lo, page_hi)) =
2558 page_span(span, i + 1 == plan.len())
2559 else {
2560 break;
2561 };
2562 if page_hi - page_lo > cap {
2563 break;
2564 }
2565 selected = i + 1;
2566 selected_span = Some((page_lo, page_hi));
2567 }
2568 Item::Gdn { first, run } => {
2569 let mut whole = None;
2576 for take in 1..=run.len() {
2577 whole = add_layer_span(whole, *first + take - 1);
2578 if whole.is_none() {
2579 break;
2580 }
2581 }
2582 let whole_fits_empty_page = whole
2583 .and_then(|span| page_span(span, false))
2584 .is_some_and(|(lo, hi)| hi - lo <= cap);
2585 if whole_fits_empty_page {
2586 let mut span = selected_span;
2587 for take in 1..=run.len() {
2588 let Some(next) = add_layer_span(span, *first + take - 1) else {
2589 break 'items;
2590 };
2591 span = Some(next);
2592 }
2593 let Some(span) = span else {
2594 break;
2595 };
2596 let Some((page_lo, page_hi)) =
2597 page_span(span, i + 1 == plan.len())
2598 else {
2599 break;
2600 };
2601 if page_hi - page_lo > cap {
2602 break;
2603 }
2604 selected = i + 1;
2605 selected_span = Some((page_lo, page_hi));
2606 continue;
2607 }
2608 if selected != 0 {
2609 break;
2610 }
2611 let mut span = None;
2612 let mut best: Option<(usize, (usize, usize))> = None;
2613 for take in 1..=run.len() {
2614 let Some(next) = add_layer_span(span, *first + take - 1) else {
2615 break;
2616 };
2617 let Some((page_lo, page_hi)) = page_span(next, false) else {
2620 break;
2621 };
2622 if page_hi - page_lo > cap {
2623 break;
2624 }
2625 span = Some((page_lo, page_hi));
2626 best = Some((take, (page_lo, page_hi)));
2627 }
2628 let Some((take, page)) = best else {
2629 break;
2630 };
2631 selected = i + 1;
2632 selected_span = Some(page);
2633 if take != run.len() {
2634 gdn_prefix = Some((i, take));
2635 break 'items;
2636 }
2637 }
2638 }
2639 }
2640 if selected != 0 {
2641 if let Some((item, take)) = gdn_prefix {
2642 match &mut plan[item] {
2643 Item::Gdn { run, .. } => run.truncate(take),
2644 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2645 }
2646 }
2647 let (page_lo, page_hi) = selected_span.expect("selected page span");
2648 tracing::info!(
2649 page_start = page_lo,
2650 page_end = page_hi,
2651 page_mb = (page_hi - page_lo) / (1024 * 1024),
2652 cap_mb = cap / (1024 * 1024),
2653 graph_items = selected,
2654 "native Metal MoE graph uses transient bounded weight page"
2655 );
2656 plan.truncate(selected);
2657 pager_span = Some((page_lo, page_hi));
2658 } else {
2659 tracing::warn!(
2660 cap_mb = cap / (1024 * 1024),
2661 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2662 );
2663 }
2664 }
2665 if has_moe
2674 && metal_working_set != 0
2675 && model_bytes > metal_working_set
2676 && pager_span.is_none()
2677 {
2678 tracing::info!(
2679 model_bytes,
2680 metal_working_set,
2681 "native Metal MoE graph declined before allocation"
2682 );
2683 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2684 if self.graph_head_required {
2685 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2686 }
2687 return start;
2688 }
2689 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2690 let dev_attend = attend_contract
2691 && (self.head_dim <= 128
2692 || has_moe
2693 || (self.head_dim <= 256 && has_gdn)
2699 || (self.head_dim <= 256 && swa_graph)
2703 || attend_mode == "force"
2704 || attend_mode == "256");
2705 if !dev_attend {
2706 for it in &mut plan {
2707 if let Item::Attn { li, full_gpu, .. } = it {
2708 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2711 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2712 if !keep_o1 {
2713 *full_gpu = false;
2714 }
2715 }
2716 }
2717 }
2718 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2719 use std::sync::atomic::{AtomicBool, Ordering};
2720 static SAID: AtomicBool = AtomicBool::new(false);
2721 if !SAID.swap(true, Ordering::Relaxed) {
2722 let fg = plan
2723 .iter()
2724 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2725 .count();
2726 let att = plan
2727 .iter()
2728 .filter(|it| matches!(it, Item::Attn { .. }))
2729 .count();
2730 eprintln!(
2731 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2732 plan.len(),
2733 self.head_dim,
2734 self.rotary_dim,
2735 self.num_kv_heads,
2736 self.num_heads,
2737 );
2738 }
2739 }
2740 let dims = GraphDims {
2741 hidden: self.hidden_size,
2742 eps: self.rms_eps as f32,
2743 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2744 };
2745 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2749 crate::gpu_metal::MoePagerPageScope::new(
2750 _mt0,
2751 position,
2752 start,
2753 page_lo,
2754 page_hi,
2755 pager_cap.unwrap_or(0),
2759 )
2760 });
2761 let graph_new = match pager_span {
2762 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2763 None => TokenGraph::new(&model, dims, h),
2764 };
2765 let Some(mut graph) = graph_new else {
2766 if self.graph_head_required {
2767 self.fail_metal_graph("native TokenGraph allocation refused");
2768 }
2769 return start;
2770 };
2771 if swa_graph && self.head_dim > 128 {
2772 graph.set_attend_blk_from(64);
2778 }
2779 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2780 nv: cfg.num_v_heads,
2781 nk: cfg.num_k_heads,
2782 dk: cfg.key_head_dim,
2783 dv: cfg.value_head_dim,
2784 kk: cfg.conv_kernel,
2785 hidden: self.hidden_size,
2786 inter: self.intermediate_size,
2787 c_dim: cfg.conv_dim(),
2788 eps: cfg.rms_eps as f32,
2789 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2790 });
2791 let mut valid = 0usize;
2795 let mut end = start;
2796 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2798 static ONCE: std::sync::Once = std::sync::Once::new();
2799 ONCE.call_once(|| {
2800 for it in &plan {
2801 match it {
2802 Item::Gdn { first, run } => {
2803 eprintln!("plan: Gdn first={first} len={}", run.len())
2804 }
2805 Item::Attn { li, full_gpu, .. } => {
2806 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2807 }
2808 }
2809 }
2810 });
2811 }
2812 for item in &plan {
2813 let ok = match item {
2814 Item::Gdn { run, .. } => gcfg
2815 .as_ref()
2816 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2817 .unwrap_or(false),
2818 Item::Attn { l, .. } => graph.attn_ok(l),
2819 };
2820 if !ok {
2821 if block_diag {
2822 eprintln!(
2823 "block-graph: plan item {} ({}) failed graph preflight",
2824 valid,
2825 match item {
2826 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2827 Item::Attn { li, .. } => format!("Attn L{li}"),
2828 }
2829 );
2830 }
2831 break;
2832 }
2833 valid += 1;
2834 end += match item {
2835 Item::Gdn { run, .. } => run.len(),
2836 Item::Attn { .. } => 1,
2837 };
2838 }
2839 plan.truncate(valid);
2840 if plan.is_empty() {
2841 if self.graph_head_required {
2842 self.fail_metal_graph("native graph preflight produced no valid items");
2843 }
2844 return start;
2845 }
2846
2847 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2848 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2849 return start;
2850 }
2851
2852 let one_pass = |t: (usize, usize, usize)| {
2861 use cortiq_core::TensorDtype as D;
2862 matches!(
2863 model.tensors[t.0].dtype,
2864 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1
2865 )
2866 };
2867 let dense_fast = plan.iter().all(|it| match it {
2868 Item::Attn {
2869 l, li, full_gpu, ..
2870 } => {
2871 *full_gpu
2872 && self.kv_cache.layers[*li].o1.is_none()
2873 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2874 && match l.ffn {
2875 MetalFfn::Dense { gate, up, down, .. } => {
2876 one_pass(gate) && one_pass(up) && one_pass(down)
2877 }
2878 _ => false,
2879 }
2880 }
2881 Item::Gdn { .. } => false,
2882 });
2883 let moe_plan = !dense_fast
2887 && plan.iter().all(|it| match it {
2888 Item::Attn {
2889 l, li, full_gpu, ..
2890 } => {
2891 *full_gpu
2892 && self.kv_cache.layers[*li].o1.is_none()
2893 && matches!(l.ffn, MetalFfn::Moe(_))
2894 }
2895 Item::Gdn { .. } => false,
2896 });
2897 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
2898 if block_diag {
2899 use std::sync::atomic::{AtomicBool, Ordering};
2900 static SAID_AB: AtomicBool = AtomicBool::new(false);
2901 if !SAID_AB.swap(true, Ordering::Relaxed) {
2902 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
2903 }
2904 }
2905 let _mv_fast = match ab {
2906 Some((bits, _)) => {
2907 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2908 crate::gpu_metal::MvFastGuard::set_raw(bits)
2909 }
2910 None if moe_plan => {
2911 graph.set_dense_concurrent(crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0);
2912 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
2913 }
2914 None => {
2915 graph.set_dense_concurrent(dense_fast);
2916 let q8r4 = if swa_graph {
2919 crate::gpu_metal::DENSE_Q8R4
2920 } else {
2921 0
2922 };
2923 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2924 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2925 } else {
2926 0
2927 })
2928 }
2929 };
2930
2931 let pool = self.pool.clone();
2934 let (nh, nkv, hd, hs, eps) = (
2935 self.num_heads,
2936 self.num_kv_heads,
2937 self.head_dim,
2938 self.hidden_size,
2939 self.rms_eps,
2940 );
2941 let norm_style = self.norm_style;
2942 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2943 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2944 let kv_id = self.graph_kv_id;
2945 let mut pending: Vec<(usize, usize)> = Vec::new();
2948 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
2951 for item in &plan {
2952 let _xt0 = std::time::Instant::now();
2953 let _xkind: u32 = match item {
2954 Item::Gdn { .. } => 2,
2955 Item::Attn { .. } => 3,
2956 };
2957 if self.loop_final_norm {
2959 let item_start = match item {
2960 Item::Gdn { first, .. } => *first,
2961 Item::Attn { li, .. } => *li,
2962 };
2963 if item_start > start && self.is_loop_end(item_start - 1) {
2964 graph.encode_loop_norm(&self.weights.final_norm);
2965 }
2966 }
2967 match item {
2968 Item::Gdn { run, first } => {
2969 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2970 if l.linear_state.len() != want {
2971 l.linear_state = vec![0f32; want];
2972 }
2973 }
2974 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2975 .iter()
2976 .map(|l| l.linear_state.as_slice())
2977 .collect();
2978 let _ig = std::time::Instant::now();
2979 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2980 tracing::error!("q1 graph: GDN run refused after validation");
2982 return start;
2983 }
2984 graph.commit_kind = 2;
2987 graph.commit();
2988 crate::gpu::stageprof(0, _ig.elapsed());
2989 pending.push((*first, run.len()));
2990 }
2991 Item::Attn {
2992 l,
2993 li,
2994 q_norm,
2995 k_norm,
2996 output_gate,
2997 proj_gate,
2998 head_gate_w,
2999 bias,
3000 full_gpu,
3001 } => {
3002 let _ia = std::time::Instant::now();
3003 let inv_freq_l = self.layer_inv_freq(*li);
3009 let rd_l = self.layer_geom(*li).2;
3010 let window_l = self.layer_window(*li);
3011 let head_gate_w = *head_gate_w;
3012 let (nkv, hd) = if gemma4 {
3015 let (a, b, _) = self.layer_geom(*li);
3016 (a, b)
3017 } else {
3018 (nkv, hd)
3019 };
3020 if *full_gpu {
3022 let cache = &self.kv_cache.layers[*li];
3023 let o1p = if cache.o1.is_some() {
3024 match cache.o1_views() {
3025 Some(views) => Some(crate::gpu::O1AttnParams {
3026 views,
3027 epoch: self.o1_epoch,
3028 }),
3029 None => None,
3031 }
3032 } else {
3033 None
3034 };
3035 let o1_layer = cache.o1.is_some();
3036 if o1_layer && o1p.is_none() {
3037 }
3039 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3040 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3041 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3042 debug_assert!(
3045 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3046 );
3047 let p = crate::gpu::AttnDeviceParams {
3048 kv_id,
3049 layer: *li,
3050 nh,
3051 nkv,
3052 hd,
3053 rd: rd_l,
3054 position,
3055 scale: self.attn_scale,
3056 eps: eps as f32,
3057 gemma,
3058 late_qk_norm: self.qk_norm_after_rope,
3059 output_gate: *output_gate,
3060 q_norm: *q_norm,
3061 k_norm: *k_norm,
3062 inv_freq: &inv_freq_l,
3063 rope_scale: self.layer_rope_scale(*li),
3064 cpu_k,
3065 cpu_v,
3066 cpu_stored,
3067 cpu_gen: cache.generation(),
3068 o1: o1p,
3069 window: window_l,
3070 head_gate: head_gate_w,
3071 v_norm: self.attn_v_norm,
3072 };
3073 let o1_bad = o1_layer && p.o1.is_none();
3074 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3075 {
3076 if p.o1.is_none() {
3078 dev_attn.push((*li, nkv, hd));
3079 }
3080 graph.commit_kind = 3;
3081 graph.commit();
3082 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3086 continue;
3087 }
3088 }
3090 graph.encode_attn_prefix(l);
3091 if let Err(err) = graph.sync_checked() {
3092 self.fail_metal_graph(&err);
3093 return start;
3094 }
3095 if !pending.is_empty() {
3096 let idxs: Vec<usize> =
3097 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3098 let mut outs: Vec<&mut [f32]> = self
3099 .kv_cache
3100 .layers
3101 .iter_mut()
3102 .enumerate()
3103 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3104 .map(|(_, s)| s.linear_state.as_mut_slice())
3105 .collect();
3106 graph.read_states(&mut outs);
3107 }
3108 let mut q_raw = attention::take_buf(l.wq.1);
3109 let mut k = attention::take_buf(l.wk.1);
3110 let mut v = attention::take_buf(l.wv.1);
3111 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3112 let mut gate_raw = proj_gate.map(|(gp, _)| {
3115 let mut normed = attention::take_buf(hs);
3116 graph.read_normed(&mut normed);
3117 let mut raw = attention::take_buf(gp.rows());
3118 gp.matvec(&normed, &mut raw, pool.as_deref());
3119 attention::recycle_buf(&mut normed);
3120 raw
3121 });
3122 let cfg = QwenAttnCfg {
3123 num_heads: nh,
3124 num_kv_heads: nkv,
3125 head_dim: hd,
3126 hidden_size: hs,
3127 position,
3128 inv_freq: &inv_freq_l,
3129 rotary_dim: rd_l,
3130 scale: self.attn_scale,
3131 softcap: self.attn_softcap,
3132 window: window_l,
3133 v_norm: self.attn_v_norm,
3136 qk_norm_after_rope: self.qk_norm_after_rope,
3137 gate_sigmoid: self.proj_gate_sigmoid,
3138 q_norm: *q_norm,
3139 k_norm: *k_norm,
3140 output_gate: *output_gate,
3141 softplus_gate: None,
3142 rope_scale: self.layer_rope_scale(*li),
3143 bias: *bias,
3144 rms_eps: eps,
3145 norm_style,
3146 pool: pool.as_deref(),
3147 v_head_dim: hd,
3148 };
3149 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3152 || std::env::var("CMF_ATTN_DUMP").is_ok();
3153 let _ = full_gpu;
3154 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3155 let mut ao = attention::qwen_attention_core(
3156 q_raw,
3157 k,
3158 v,
3159 &mut self.kv_cache.layers[*li],
3160 &cfg,
3161 );
3162 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3166 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3167 let (cq, _cg, _ck, _cv) =
3168 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3169 let cache = &self.kv_cache.layers[*li];
3170 let n = cache.head_keys(0).len() / hd;
3171 let mut bytes: Vec<u8> = Vec::new();
3172 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3173 bytes.extend_from_slice(&v.to_le_bytes());
3174 }
3175 for v in &cq {
3176 bytes.extend_from_slice(&v.to_le_bytes());
3177 }
3178 for g in 0..nkv {
3179 for v in cache.head_keys(g) {
3180 bytes.extend_from_slice(&v.to_le_bytes());
3181 }
3182 }
3183 for g in 0..nkv {
3184 for v in cache.head_values(g) {
3185 bytes.extend_from_slice(&v.to_le_bytes());
3186 }
3187 }
3188 let _ =
3189 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3190 }
3191 }
3192 if let Some((qr0, k0, v0)) =
3193 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3194 {
3195 let (cq, _cg, ck, cv) =
3196 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3197 let mut h_now = vec![0f32; hs];
3198 graph.read_h(&mut h_now);
3199 let cache = &self.kv_cache.layers[*li];
3200 let n_after = cache.head_keys(0).len() / hd;
3201 let stored = n_after.saturating_sub(1);
3205 let cpu_k: Vec<&[f32]> = (0..nkv)
3206 .map(|g| &cache.head_keys(g)[..stored * hd])
3207 .collect();
3208 let cpu_v: Vec<&[f32]> = (0..nkv)
3209 .map(|g| &cache.head_values(g)[..stored * hd])
3210 .collect();
3211 let p = crate::gpu::AttnDeviceParams {
3212 kv_id,
3213 layer: *li,
3214 nh,
3215 nkv,
3216 hd,
3217 rd: rd_l,
3221 position,
3222 scale: self.attn_scale,
3223 eps: eps as f32,
3224 gemma,
3225 late_qk_norm: self.qk_norm_after_rope,
3226 output_gate: *output_gate,
3227 q_norm: *q_norm,
3228 k_norm: *k_norm,
3229 inv_freq: &inv_freq_l,
3230 rope_scale: self.layer_rope_scale(*li),
3231 cpu_k,
3232 cpu_v,
3233 cpu_stored: stored,
3234 cpu_gen: cache.generation(),
3235 o1: None,
3236 window: window_l,
3237 head_gate: None,
3238 v_norm: self.attn_v_norm,
3239 };
3240 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3241 let md = |a: &[f32], b: &[f32]| {
3242 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3243 };
3244 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3245 eprintln!(
3246 "attn-oracle L{li} pos {position}: |q| {:.2} max|dq| {:.4} | |k| {:.2} max|dk| {:.4} | |v| {:.2} max|dv| {:.4} | |ao| {:.2} max|dao| {:.4}",
3247 nn(&cq),
3248 md(&cq, &dq),
3249 nn(&ck),
3250 md(&ck, &dk),
3251 nn(&cv),
3252 md(&cv, &dv),
3253 nn(&ao),
3254 md(&ao, &dao)
3255 );
3256 } else {
3257 eprintln!("attn-oracle L{li}: device probe declined");
3258 }
3259 }
3260 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3261 attention::apply_projected_gate(
3264 &mut ao,
3265 raw,
3266 per_head,
3267 hd,
3268 self.proj_gate_sigmoid,
3269 );
3270 }
3271 if let Some(mut raw) = gate_raw.take() {
3272 attention::recycle_buf(&mut raw);
3273 }
3274 graph.encode_attn_suffix(l, &ao);
3275 graph.commit();
3278 attention::recycle_buf(&mut ao);
3279 }
3280 }
3281
3282 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3283 }
3284 let mut lm_rows = None;
3289 if self.graph_want_logits
3290 && upto.is_none()
3291 && end == self.num_layers
3292 && std::env::var("CMF_GPU_LMHEAD")
3293 .map(|v| v != "0")
3294 .unwrap_or(true)
3295 {
3296 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3297 if graph.lm_head_ok(lm) {
3298 graph.encode_lm_head(&self.weights.final_norm, lm);
3299 lm_rows = Some(lm.1);
3300 }
3301 }
3302 }
3303 if self.graph_head_required && lm_rows.is_none() {
3304 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3305 self.fail_metal_graph("fused graph head was requested but not encodable");
3306 return start;
3307 }
3308 let _sy0 = std::time::Instant::now();
3309 if let Err(err) = graph.sync_checked() {
3310 if let Some(profile) = pager_profile_page.as_mut() {
3311 profile.finish(start, graph.pager_page_stats(), "sync-error");
3312 }
3313 self.fail_metal_graph(&err);
3314 return start;
3315 }
3316 let _rs0 = std::time::Instant::now();
3317 if !pending.is_empty() {
3318 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3319 let mut outs: Vec<&mut [f32]> = self
3320 .kv_cache
3321 .layers
3322 .iter_mut()
3323 .enumerate()
3324 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3325 .map(|(_, s)| s.linear_state.as_mut_slice())
3326 .collect();
3327 graph.read_states(&mut outs);
3328 }
3329 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3330 use std::sync::atomic::{AtomicU64, Ordering};
3331 static SY: AtomicU64 = AtomicU64::new(0);
3332 static RS: AtomicU64 = AtomicU64::new(0);
3333 static N: AtomicU64 = AtomicU64::new(0);
3334 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3335 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3336 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3337 if n % 100 == 0 {
3338 eprintln!(
3339 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3340 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3341 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3342 );
3343 }
3344 }
3345 if let Some(rows) = lm_rows {
3346 crate::gpu::hostprof_encode_done(_mt0);
3347 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3348 graph.read_logits(&mut lg);
3349 crate::gpu::hostprof_total(_mt0);
3350 lg.resize(self.vocab_size, 0.0);
3351 if let Some(c) = self.final_softcap {
3352 softcap_in_place(&mut lg, c, self.pool.as_deref());
3353 }
3354 self.graph_logits = Some(lg);
3355 }
3356 graph.read_h(h);
3357 if self.graph_head_required && self.graph_logits.is_none() {
3358 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3359 self.fail_metal_graph("fused graph head completed without logits readback");
3360 return start;
3361 }
3362 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3363 METAL_GRAPH_LAYERS.fetch_add(
3364 end.saturating_sub(start) as u64,
3365 std::sync::atomic::Ordering::Relaxed,
3366 );
3367 if self.graph_head_required {
3368 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3369 }
3370 for (li, nkv, hd) in dev_attn {
3374 let mut krow = attention::take_buf(nkv * hd);
3375 let mut vrow = attention::take_buf(nkv * hd);
3376 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3377 let cache = &mut self.kv_cache.layers[li];
3378 cache.append(&krow, &vrow, &[]);
3379 let n = cache.seq_len;
3380 let mut imp = attention::take_buf(n);
3381 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3382 cache.accumulate_imp(&imp);
3383 attention::recycle_buf(&mut imp);
3384 }
3385 attention::recycle_buf(&mut krow);
3386 attention::recycle_buf(&mut vrow);
3387 }
3388 if let Some(profile) = pager_profile_page.as_mut() {
3389 profile.finish(end, graph.pager_page_stats(), "page-complete");
3390 }
3391 if let Some((_, arm)) = ab {
3392 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3393 }
3394 end
3395 }
3396
3397 pub fn new(
3398 tokenizer: Tokenizer,
3399 weights: PipelineWeights,
3400 hidden_size: usize,
3401 intermediate_size: usize,
3402 num_heads: usize,
3403 num_kv_heads: usize,
3404 head_dim: usize,
3405 num_layers: usize,
3406 physical_layers: usize,
3407 loop_final_norm: bool,
3408 vocab_size: usize,
3409 rms_eps: f64,
3410 rope_base: f32,
3411 norm_style: NormStyle,
3412 max_seq_len: usize,
3413 sampler_config: SamplerConfig,
3414 ) -> Self {
3415 let rng = match sampler_config.seed {
3416 Some(s) => SplitMix64::new(s),
3417 None => SplitMix64::from_entropy(),
3418 };
3419 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3420 let pool = Pool::from_env();
3421 if let Some(p) = &pool {
3422 tracing::info!("worker pool: {} threads", p.n_workers());
3423 if let Some(model) = weights
3425 .lm_head
3426 .model_arc()
3427 .or_else(|| weights.embed_tokens.model_arc())
3428 {
3429 let regions: Vec<&[u8]> =
3430 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3431 p.bind_numa(®ions);
3432 }
3433 }
3434 Self {
3435 gpu_plan: None,
3436 tokenizer: std::sync::Arc::new(tokenizer),
3437 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3438 sampler_config,
3439 weights,
3440 hidden_size,
3441 intermediate_size,
3442 num_heads,
3443 num_kv_heads,
3444 head_dim,
3445 num_layers,
3446 physical_layers,
3447 loop_final_norm,
3448 vocab_size,
3449 rms_eps,
3450 rope_base,
3451 norm_style,
3452 rotary_dim: head_dim,
3453 attention_heads_per_layer: None,
3454 kv_heads_per_layer: None,
3455 v_head_dim: None,
3456 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3457 .filter(|v| !v.is_empty())
3458 .map(std::path::PathBuf::from),
3459 graph_declines: std::cell::RefCell::new(Vec::new()),
3460 mimo_moe: Default::default(),
3461 minicpm_tail: Default::default(),
3462 vmf_cfg: None,
3463 gdn_cfg: None,
3464 kda_cfg: None,
3465 g3n: None,
3466 dsv4: None,
3467 dsv41: None,
3468 dsv41_vision: None,
3469 dsv41_prefill: None,
3470 qwen4_exp: None,
3471 dsv4_mtp: Vec::new(),
3472 dspark: None,
3473 dspark_pending: Vec::new(),
3474 dspark_hist: Vec::new(),
3475 dspark_real: Vec::new(),
3476 dspark_trunk_picks: Vec::new(),
3477 dspark_exp: Vec::new(),
3478 dspark_draft_ns: 0,
3479 logit_multiplier: None,
3480 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3481 graph_failed: std::sync::atomic::AtomicBool::new(false),
3482 kv_history: Vec::new(),
3483 kv_history_device: false,
3484 short_conv_cfg: None,
3485 mtp: None,
3486 mimo_mtp: None,
3487 verify_exact_moe: false,
3488 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3489 ignore_eos: false,
3490 draft_full_streak: 0,
3491 spec_k_adapt: None,
3492 spec_acc_ewma: 0.7,
3493 rng,
3494 sampler_scratch: SamplerScratch::default(),
3495 spec_forced: None,
3496 spec_q: Vec::new(),
3497 spec_p: Vec::new(),
3498 spec_res: Vec::new(),
3499 spec_qs: Vec::new(),
3500 spec_ps: Vec::new(),
3501 spec_ress: Vec::new(),
3502 mtp_graph_mode: None,
3503 #[cfg(target_os = "macos")]
3504 metal_verify: None,
3505 inv_freq,
3506 ws: ForwardScratch::new(hidden_size),
3507 pool,
3508 model: None,
3509 dyn_force_f32: false,
3510 dyn_skill_layers: Vec::new(),
3511 dyn_active: None,
3512 dyn_blend_loaded: false,
3513 dyn_phi_layer: None,
3514 dyn_phi_ema: Vec::new(),
3515 dyn_phi_seen: 0,
3516 dyn_router: None,
3517 o1_cfg: None,
3518 o1_epoch: 0,
3519 o1_flags: Vec::new(),
3520 trace: false,
3521 calib_temp: 1.0,
3522 confidence_on: true,
3523 embed_multiplier: 1.0,
3524 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3525 swa: None,
3526 swa_trim: None,
3527 sliding_layers: None,
3528 anchor_core: None,
3529 bounded_rope: None,
3530 kv_prefix: KvPrefix::default(),
3531 last_prefill_tokens: 0,
3532 inv_freq_local: None,
3533 rotary_dim_local: None,
3534 rope_scale: 1.0,
3535 rope_scale_local: 1.0,
3536 global_attn: None,
3537 inv_freq_global: None,
3538 attn_v_norm: false,
3539 qk_norm_after_rope: false,
3540 proj_gate_sigmoid: false,
3541 final_softcap: None,
3542 head_clusters: None,
3543 attn_softcap: 0.0,
3544 graph_want_logits: false,
3545 graph_head_required: false,
3546 graph_logits: None,
3547 embryo_graph: None,
3548 graph_refused: std::sync::atomic::AtomicBool::new(false),
3549 graph_kv_id: {
3550 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3551 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3552 },
3553 #[cfg(test)]
3554 nll_test_fail_at: None,
3555 #[cfg(test)]
3556 nll_test_force_serial: false,
3557 }
3558 }
3559
3560 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3568 if let Err(e) = self.try_set_o1(cfg) {
3569 tracing::error!("{e}");
3570 }
3571 }
3572
3573 pub fn bounded_native(&self) -> bool {
3577 self.anchor_core.is_some()
3578 }
3579
3580 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3583 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3584 }
3585
3586 pub fn o1_refusal(&self) -> Option<String> {
3588 self.anchor_core.as_ref().map(|ac| {
3589 format!(
3590 "--o1 / CMF_O1 refused: the anchor is native bounded \
3591 (anchor_core kind={} window={} sink={}); the file's operator \
3592 is executed as-is and no post-hoc Nyström overlay applies",
3593 ac.kind, ac.window, ac.sink
3594 )
3595 })
3596 }
3597
3598 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3600 if let Some(c) = &cfg {
3601 if let Some(why) = self.o1_refusal() {
3602 self.o1_flags = Vec::new();
3603 self.o1_cfg = None;
3604 return Err(why);
3605 }
3606 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3607 self.o1_flags.clear();
3608 self.o1_cfg = None;
3609 return Err(format!(
3610 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3611 c.w, c.sink
3612 ));
3613 }
3614 }
3615 self.o1_flags = match &cfg {
3616 Some(c) => {
3617 let mut flags = c.layer_flags(self.num_layers);
3618 for (li, f) in flags.iter_mut().enumerate() {
3619 if *f
3625 && (!matches!(
3626 self.weights.layers[self.phys_layer(li)].attn,
3627 AttnKind::Full { .. }
3628 ) || self.layer_window(li).is_some()
3629 || self.kv_cache.layers[li].sinks.is_some()
3630 || self.layer_v_dim(li) != self.layer_geom(li).1)
3631 {
3632 *f = false;
3633 }
3634 }
3635 flags
3636 }
3637 None => Vec::new(),
3638 };
3639 if let Some(c) = &cfg {
3640 let n = self.o1_flags.iter().filter(|&&f| f).count();
3641 tracing::info!(
3642 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3643 self.num_layers,
3644 c.m,
3645 c.w,
3646 c.sink,
3647 c.rect
3648 );
3649 }
3650 self.o1_cfg = cfg;
3651 Ok(())
3652 }
3653
3654 pub fn install_bounded(
3660 &mut self,
3661 cfg: &cortiq_core::AnchorCoreConfig,
3662 ) -> Result<(), String> {
3663 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3664 return Err(format!(
3665 "anchor_core kind '{}' is not executable by this runtime",
3666 cfg.kind
3667 ));
3668 }
3669 if cfg.window == 0 {
3670 return Err("anchor_core.window must be >= 1".into());
3671 }
3672 let mut n = 0usize;
3673 for li in 0..self.num_layers {
3674 let pl = self.phys_layer(li);
3675 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3676 if w.window != cfg.window || w.sink != cfg.sink {
3677 return Err(format!(
3678 "layer {li}: bounded weights (window {} sink {}) disagree with \
3679 anchor_core (window {} sink {})",
3680 w.window, w.sink, cfg.window, cfg.sink
3681 ));
3682 }
3683 self.kv_cache.layers[li].install_bounded(cfg.window);
3684 n += 1;
3685 }
3686 }
3687 if n == 0 {
3688 return Err("anchor_core is present but no layer executes it".into());
3689 }
3690 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3691 self.bounded_rope = Some(std::sync::Arc::new(rope));
3692 self.anchor_core = Some(cfg.clone());
3693 self.embryo_graph = None;
3694 tracing::info!(
3695 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3696 cfg.kind,
3697 cfg.window,
3698 cfg.sink,
3699 self.kv_cache.layers.iter().map(|l| l.bounded_state_bytes()).max().unwrap_or(0)
3700 );
3701 Ok(())
3702 }
3703
3704 pub fn install_wire_identity(&mut self, identity: u64) {
3708 for li in 0..self.kv_cache.layers.len() {
3709 let pl = self.phys_layer(li);
3710 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3711 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3712 Some(AttnKind::Linear(_))
3713 | Some(AttnKind::LinearGdn(_))
3714 | Some(AttnKind::ShortConv(_))
3715 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3716 _ => crate::kv_cache::WireKind::Full,
3717 };
3718 let l = &mut self.kv_cache.layers[li];
3719 l.wire_kind = kind;
3720 l.wire_identity = identity;
3721 l.wire_layer = li as u32;
3724 }
3725 }
3726
3727 pub fn clear_history(&mut self) {
3729 self.kv_history.clear();
3730 self.kv_history_device = false;
3731 self.kv_prefix.clear();
3732 }
3733
3734 pub fn graph_refused(&self) -> bool {
3738 self.graph_refused
3739 .load(std::sync::atomic::Ordering::Relaxed)
3740 }
3741
3742 pub fn mark_graph_refused(&self) {
3744 if !self
3745 .graph_refused
3746 .swap(true, std::sync::atomic::Ordering::Relaxed)
3747 {
3748 tracing::info!(
3749 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3750 self.graph_kv_id
3751 );
3752 }
3753 }
3754
3755 pub fn device_sequence_position(&self) -> Option<usize> {
3759 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3760 }
3761
3762 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3767 let dev = self.device_sequence_position();
3768 if recorded_on_device {
3769 dev == Some(n) && self.embryo_resident_wanted()
3770 } else {
3771 dev.is_none()
3772 }
3773 }
3774
3775 pub(crate) fn invalidate_for_weight_change(&mut self) {
3783 self.clear_sequence_state();
3784 self.embryo_graph = None;
3785 }
3786
3787 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3793 let (n, on_device) = if self.bounded_native() {
3794 (self.kv_prefix.extension(input_ids), self.kv_prefix.on_device())
3795 } else {
3796 let h = &self.kv_history;
3797 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3798 (h.len(), self.kv_history_device)
3799 } else {
3800 (0, false)
3801 }
3802 };
3803 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3806 tracing::warn!(
3807 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3808 the device now holds {:?} — re-prefilling from zero",
3809 if on_device { "resident device" } else { "host" },
3810 self.device_sequence_position()
3811 );
3812 return 0;
3813 }
3814 n
3815 }
3816
3817 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3820 self.cached_prefix_len(input_ids)
3821 }
3822
3823 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3829 let on_device = self.device_sequence_position().is_some();
3830 if self.bounded_native() {
3831 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3832 let prev_device = self.kv_prefix.on_device();
3833 self.kv_history.clear();
3834 self.kv_history_device = false;
3835 if keep && prev_device == on_device {
3836 self.kv_prefix.extend(&consumed[reused..]);
3837 } else {
3838 self.kv_prefix.set(consumed);
3839 }
3840 self.kv_prefix.set_on_device(on_device);
3841 } else {
3842 self.kv_history = consumed.to_vec();
3843 self.kv_history_device = on_device;
3844 }
3845 }
3846
3847 pub fn o1_active(&self) -> bool {
3849 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3850 }
3851
3852 pub fn generation_batch_k(&self) -> usize {
3870 if let Some(k) = std::env::var("CMF_BATCH_K")
3871 .ok()
3872 .and_then(|v| v.parse::<usize>().ok())
3873 {
3874 return k;
3875 }
3876 if self.g4_batch_prefill() {
3879 return 128;
3880 }
3881 #[cfg(not(target_os = "macos"))]
3882 if self.graph_prefill_preferred() && !self.o1_active() {
3883 let moe = self
3886 .weights
3887 .layers
3888 .iter()
3889 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
3890 let q82 = crate::gpu::moe_q82_graph_on()
3896 && self.weights.layers.iter().any(|lw| {
3897 matches!(&lw.ffn, FfnKind::Moe(m)
3898 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
3899 });
3900 return if q82 {
3901 128
3902 } else if moe {
3903 32
3904 } else {
3905 128
3906 };
3907 }
3908 0
3909 }
3910
3911 pub fn generation_graph_prefill(&self) -> bool {
3912 let graph = self.graph_prefill_preferred();
3913 #[cfg(not(target_os = "macos"))]
3924 if graph
3925 && self.generation_batch_k() > 0
3926 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3927 {
3928 return false;
3929 }
3930 graph
3931 }
3932
3933 pub fn o1_device_stats(&self) -> (usize, u64) {
3938 crate::gpu::o1_device_stats(self.graph_kv_id)
3939 }
3940
3941 pub fn o1_begin(&mut self) {
3946 self.o1_begin_with_prefix(None);
3947 }
3948
3949 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
3953 if let Some(c) = &self.o1_cfg {
3954 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
3955 let boundary = requested_prefix.map(|p| {
3956 p.max(
3957 crate::nystrom::o1_deferred_boundary(w, sink)
3958 .expect("o1 config boundary validated in set_o1"),
3959 )
3960 });
3961 for (li, &f) in self.o1_flags.iter().enumerate() {
3962 if f {
3963 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
3964 }
3965 }
3966 }
3967 }
3968
3969 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
3971 self.o1_cfg.as_ref().and_then(|c| {
3972 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
3973 .map(|floor| requested_prefix.max(floor))
3974 })
3975 }
3976
3977 fn o1_note_transition(&mut self) {
3978 let mut transitioned = false;
3982 for (li, &flagged) in self.o1_flags.iter().enumerate() {
3983 if flagged {
3984 transitioned |= self.kv_cache.layers[li].take_o1_transition();
3985 }
3986 }
3987 if transitioned {
3988 self.o1_epoch = self.o1_epoch.wrapping_add(1);
3989 }
3990 }
3991
3992 fn o1_pending(&self) -> bool {
3993 self.o1_flags.iter().enumerate().any(|(li, &f)| {
3994 f && self.kv_cache.layers[li].seq_len > 0
3995 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
3996 })
3997 }
3998
3999 fn o1_fail(&mut self, err: String) {
4000 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4001 self.clear_sequence_state();
4002 self.graph_failed
4003 .store(true, std::sync::atomic::Ordering::Relaxed);
4004 self.cancel
4005 .store(true, std::sync::atomic::Ordering::Relaxed);
4006 }
4007
4008 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4013 if self.o1_cfg.is_none() {
4014 return Ok(false);
4015 }
4016 let mut participating = false;
4017 for li in 0..self.num_layers {
4018 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4019 continue;
4020 }
4021 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4022 return Err(err);
4023 }
4024 if self.kv_cache.layers[li].seq_len == 0 {
4025 continue;
4026 }
4027 participating = true;
4028 let num_heads = self.layer_num_heads(li);
4029 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4030 }
4031 self.o1_note_transition();
4032 for li in 0..self.num_layers {
4033 if self.o1_flags.get(li).copied().unwrap_or(false) {
4034 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4035 return Err(err);
4036 }
4037 }
4038 }
4039 Ok(participating
4040 && (0..self.num_layers).all(|li| {
4041 !self.o1_flags.get(li).copied().unwrap_or(false)
4042 || self.kv_cache.layers[li].seq_len == 0
4043 || self.kv_cache.layers[li].o1_sealed()
4044 }))
4045 }
4046
4047 fn o1_progress(&mut self) {
4050 if !self.o1_active() {
4051 return;
4052 }
4053 for li in 0..self.num_layers {
4054 if self.o1_flags.get(li).copied().unwrap_or(false) {
4055 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4056 self.o1_fail(err);
4057 return;
4058 }
4059 }
4060 }
4061 self.o1_note_transition();
4065 if !self.o1_pending() {
4066 return;
4067 }
4068 if let Err(err) = self.o1_seal_checked() {
4069 self.o1_fail(err);
4070 }
4071 }
4072
4073 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4078 if self
4079 .graph_failed
4080 .swap(false, std::sync::atomic::Ordering::Relaxed)
4081 {
4082 self.cancel
4083 .store(false, std::sync::atomic::Ordering::Relaxed);
4084 self.clear_sequence_state();
4085 return Err(format!("{phase}: deferred O(1) transition failed"));
4086 }
4087 Ok(())
4088 }
4089
4090 pub fn o1_seal(&mut self) {
4094 if let Err(err) = self.o1_seal_checked() {
4095 self.o1_fail(err);
4096 }
4097 }
4098
4099 pub fn set_trace(&mut self, on: bool) {
4101 self.trace = on;
4102 }
4103
4104 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4107 self.rng = match config.seed {
4108 Some(seed) => SplitMix64::new(seed),
4109 None => SplitMix64::from_entropy(),
4110 };
4111 self.sampler_config = config;
4112 }
4113
4114 pub fn set_confidence(&mut self, on: bool) {
4119 self.confidence_on = on;
4120 }
4121
4122 pub fn set_calib_temp(&mut self, t: f32) {
4125 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4126 }
4127
4128 pub fn calib_temp(&self) -> f32 {
4130 self.calib_temp
4131 }
4132
4133 pub fn rope_inv_freq(&self) -> &[f32] {
4137 self.inv_freq.as_slice()
4138 }
4139
4140 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4143 self.rotary_dim = rotary_dim.min(self.head_dim);
4144 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4145 self.embryo_graph = None;
4149 }
4150
4151 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4152 QwenAttnCfg {
4153 num_heads: self.num_heads,
4154 num_kv_heads: self.num_kv_heads,
4155 head_dim: self.head_dim,
4156 hidden_size: self.hidden_size,
4157 position,
4158 inv_freq: &self.inv_freq,
4159 rotary_dim: self.rotary_dim,
4160 scale: self.attn_scale,
4161 softcap: self.attn_softcap,
4162 window: None,
4163 v_norm: false,
4164 qk_norm_after_rope: self.qk_norm_after_rope,
4165 gate_sigmoid: self.proj_gate_sigmoid,
4166 q_norm: None,
4167 k_norm: None,
4168 output_gate: false,
4169 softplus_gate: None,
4170 rope_scale: self.rope_scale,
4171 bias: None,
4172 rms_eps: self.rms_eps,
4173 norm_style: self.norm_style,
4174 pool: self.pool.as_deref(),
4175 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4176 }
4177 }
4178
4179 pub fn generate(
4181 &mut self,
4182 prompt: &str,
4183 max_tokens: usize,
4184 task_mask: Option<&TaskMask>,
4185 on_token: Option<TokenCallback>,
4186 ) -> Result<GenerateResult, String> {
4187 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4188 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4189 }
4190
4191 pub fn generate_from_vl(
4194 &mut self,
4195 input: &crate::dsv41_vision::PreparedVlInputs,
4196 max_tokens: usize,
4197 task_mask: Option<&TaskMask>,
4198 on_token: Option<TokenCallback>,
4199 ) -> Result<GenerateResult, String> {
4200 let Some(dsv41) = &self.dsv41 else {
4201 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4202 };
4203 if input.token_ids.is_empty() {
4204 return Err("empty V4.1 multimodal prompt".into());
4205 }
4206 if input.token_types.len() != input.token_ids.len() {
4207 return Err(format!(
4208 "V4.1 token type count {} != token count {}",
4209 input.token_types.len(),
4210 input.token_ids.len()
4211 ));
4212 }
4213 let dim = dsv41.2.dim;
4214 let mut embeddings = vec![None; input.token_ids.len()];
4215 let mut participates = vec![true; input.token_ids.len()];
4216 if !input.images.is_empty() {
4217 let vision = self
4218 .dsv41_vision
4219 .as_ref()
4220 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4221 for image in &input.images {
4222 let end = image.start.saturating_add(image.types.len());
4223 if end > input.token_ids.len() {
4224 return Err(format!(
4225 "V4.1 image span {}..{} exceeds prompt length {}",
4226 image.start,
4227 end,
4228 input.token_ids.len()
4229 ));
4230 }
4231 let mut span = vec![0.0f32; image.types.len() * dim];
4232 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4233 for (offset, &kind) in image.types.iter().enumerate() {
4234 let pos = image.start + offset;
4235 if input.token_types[pos] != kind {
4236 return Err(format!(
4237 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4238 input.token_types[pos]
4239 ));
4240 }
4241 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4242 participates[pos] = false;
4243 }
4244 }
4245 }
4246 for (pos, &kind) in input.token_types.iter().enumerate() {
4247 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4248 return Err(format!("V4.1 text position {pos} has an image embedding"));
4249 }
4250 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4251 return Err(format!("V4.1 image position {pos} has no image embedding"));
4252 }
4253 }
4254 self.dsv41_prefill = Some((embeddings, participates));
4255 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4256 self.dsv41_prefill = None;
4257 result
4258 }
4259
4260 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4262 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4263 }
4264
4265 pub fn generate_from_ids(
4273 &mut self,
4274 input_ids: &[u32],
4275 max_tokens: usize,
4276 task_mask: Option<&TaskMask>,
4277 on_token: Option<TokenCallback>,
4278 ) -> Result<GenerateResult, String> {
4279 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4280 }
4281
4282 pub fn generate_from_embeds(
4288 &mut self,
4289 input_ids: &[u32],
4290 prompt_rows: &[f32],
4291 max_tokens: usize,
4292 task_mask: Option<&TaskMask>,
4293 on_token: Option<TokenCallback>,
4294 ) -> Result<GenerateResult, String> {
4295 if input_ids.is_empty()
4296 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4297 {
4298 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4299 }
4300 if prompt_rows.iter().any(|x| !x.is_finite()) {
4301 return Err("embedded prompt contains non-finite values".into());
4302 }
4303 if !self.can_prefill_batched() || self.dyn_router.is_some()
4304 || self.o1_active() || self.mtp.is_some() || self.gpu_plan.is_some()
4305 {
4306 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4307 }
4308 self.generate_with_prompt_rows(input_ids, Some(prompt_rows), max_tokens, task_mask, on_token)
4309 }
4310
4311 fn generate_with_prompt_rows(
4312 &mut self,
4313 input_ids: &[u32],
4314 prompt_rows: Option<&[f32]>,
4315 max_tokens: usize,
4316 task_mask: Option<&TaskMask>,
4317 mut on_token: Option<TokenCallback>,
4318 ) -> Result<GenerateResult, String> {
4319 #[cfg(target_os = "macos")]
4320 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4321 if std::env::var("CMF_TRACE_H").is_ok() {
4322 eprintln!("input_ids: {input_ids:?}");
4323 }
4324 if input_ids.is_empty() {
4325 return Err("empty prompt: nothing to generate from".to_string());
4326 }
4327 self.graph_failed
4331 .store(false, std::sync::atomic::Ordering::Relaxed);
4332 let task_mask = self.drop_open_mask(task_mask);
4337
4338 let mut reuse_from = {
4346 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4347 if on
4348 && prompt_rows.is_none()
4349 && task_mask.is_none()
4350 && self.mtp.is_none()
4351 && !(self.mimo_mtp.is_some() && self.speculative)
4352 && self.o1_cfg.is_none()
4353 && self.dsv41.is_none()
4354 {
4355 self.cached_prefix_len(input_ids)
4356 } else {
4357 0
4358 }
4359 };
4360 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4363 reuse_from = 0;
4364 }
4365 self.last_prefill_tokens = input_ids.len() - reuse_from;
4366 let bounded_native = self.bounded_native();
4367 if reuse_from == 0 {
4368 self.clear_sequence_state();
4370 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4371 eprintln!(
4372 "kv-reuse: {} of {} prompt positions already cached",
4373 reuse_from,
4374 input_ids.len()
4375 );
4376 }
4377 crate::gpu::graph_race_begin_generation();
4378 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4382 std::env::var("CMF_O1_PREFILL")
4383 .ok()
4384 .and_then(|v| v.parse::<usize>().ok())
4385 .filter(|&p| p > 0)
4386 } else {
4387 None
4388 };
4389 if task_mask.is_none() {
4390 self.o1_begin_with_prefix(o1_prefill);
4391 }
4392
4393 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4399 #[cfg(target_os = "macos")]
4438 let metal_graph = crate::gpu::q1_force()
4439 && crate::gpu::enabled_here()
4440 && std::env::var("CMF_GPU_BLOCK")
4441 .map(|v| v != "0")
4442 .unwrap_or(true);
4443 #[cfg(not(target_os = "macos"))]
4444 let metal_graph = false;
4445 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4446 let spec_cheap_round = self.sampler_config.temperature < 1e-6
4450 || sampler::sparse_ok(&self.sampler_config);
4451 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4452 || match spec_sample_env.as_deref() {
4453 Some("1") => true,
4454 Some(_) => false,
4455 None => metal_graph && spec_cheap_round,
4456 };
4457 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4473 for lw in &self.weights.layers {
4474 if let FfnKind::Dense(d) = &lw.ffn {
4475 dense_n += 1;
4476 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4477 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4478 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4479 {
4480 dense_q4tp += 1;
4481 }
4482 }
4483 }
4484 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4485 let penalized = !metal_graph
4497 && (self.sampler_config.repetition_penalty != 1.0
4498 || self.sampler_config.presence_penalty != 0.0
4499 || !self.sampler_config.suppress_tokens.is_empty());
4500 #[cfg(feature = "gpu")]
4505 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4506 #[cfg(not(feature = "gpu"))]
4507 let metal_wgpu = false;
4508 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4509 let spec_wanted = match spec_env.as_deref() {
4510 Some("0") => false,
4511 Some(_) => {
4512 if metal_wgpu {
4513 tracing::warn!(
4514 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4515 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4516 );
4517 }
4518 true
4519 }
4520 None => spec_default_ok && !penalized && !metal_wgpu,
4521 };
4522 let graph_spec = self.speculative
4526 && (graph_on || metal_graph)
4527 && self.mtp.is_some()
4528 && task_mask.is_none()
4529 && !self.o1_active()
4530 && spec_sampling_ok
4531 && spec_wanted;
4532 #[cfg(target_os = "macos")]
4536 if metal_graph {
4537 static SAID: std::sync::Once = std::sync::Once::new();
4538 SAID.call_once(|| {
4539 let spec = if graph_spec {
4540 let k = std::env::var("CMF_GRAPH_SPEC_K")
4541 .ok()
4542 .and_then(|v| v.parse::<usize>().ok())
4543 .filter(|&v| (1..=8).contains(&v))
4544 .unwrap_or(7);
4545 let arm = if self.sampler_config.temperature < 1e-6 {
4546 "greedy"
4547 } else {
4548 "sampling"
4549 };
4550 format!(
4551 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4552 Self::draft_vocab_rows(usize::MAX)
4553 )
4554 } else if !self.speculative {
4555 "spec off (CMF_MTP=0)".to_string()
4556 } else if self.mtp.is_none() {
4557 "spec off (no MTP head)".to_string()
4558 } else if !spec_sampling_ok {
4559 if spec_cheap_round {
4560 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4561 } else {
4562 "spec off (sampling without a top-k: the dense chain \
4563 costs more than it saves)"
4564 .to_string()
4565 }
4566 } else if !spec_wanted {
4567 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4568 } else if task_mask.is_some() {
4569 "spec off (task mask)".to_string()
4570 } else {
4571 "spec off (O(1) attention)".to_string()
4572 };
4573 let on = |var: &str| {
4574 if std::env::var(var).as_deref() == Ok("0") {
4575 "off"
4576 } else {
4577 "on"
4578 }
4579 };
4580 tracing::info!(
4581 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4582 MTP graph {}, attend {}, probe {}",
4583 if crate::gpu_metal::state4_on() { "on" } else { "off" },
4584 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
4585 on("CMF_METAL_PREFILL"),
4586 on("CMF_MTP_GRAPH"),
4587 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4588 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
4589 );
4590 });
4591 }
4592 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4599 let spec_active = self.speculative
4600 && self.mtp.is_some()
4601 && task_mask.is_none()
4602 && !self.o1_active()
4603 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4604 let mut mtp = if spec_active { self.mtp.take() } else { None };
4607 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4608 eprintln!(
4609 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4610 mtp.is_some(),
4611 self.speculative,
4612 self.sampler_config.temperature < 1e-6,
4613 );
4614 }
4615 if let Some(m) = &mut mtp {
4616 m.kv.clear();
4617 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4619 self.mtp_graph_mode = None;
4620 }
4621 let mimo_spec = self.speculative
4625 && self.mimo_mtp.is_some()
4626 && task_mask.is_none()
4627 && !self.o1_active()
4628 && self.dyn_router.is_none()
4629 && self.sampler_config.temperature < 1e-6
4630 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4631 if let Some(st) = self.mimo_mtp.as_mut() {
4632 st.reset();
4633 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4634 Self::mimo_mtp_hist_cap(st, input_ids.len());
4635 }
4636 }
4637 let mut router = if mtp.is_none() {
4641 self.dyn_router.take()
4642 } else {
4643 None
4644 };
4645 let mut reuse_from = reuse_from;
4646 if let Some(r) = &mut router {
4647 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4650 let _ = self.set_active_skill(None);
4653 reuse_from = 0;
4654 self.last_prefill_tokens = input_ids.len();
4655 }
4656 }
4657
4658 let mut all_ids = input_ids.to_vec();
4659 let mut generated = 0usize;
4660 let mut finish_reason = "max_tokens".to_string();
4661 let mut drafted = 0usize;
4662 let mut accepted = 0usize;
4663 let mut dsv4_spec_bad = 0usize;
4670 let mut dsv4_spec_retry_at = 0usize;
4671 let mut confidence: Vec<f32> = Vec::new();
4672 let trace_on = self.trace;
4673 let calib_temp = self.calib_temp;
4674 let mut traces: Vec<TokenTrace> = Vec::new();
4675
4676 let mut hidden = vec![0.0f32; self.hidden_size];
4682 let mut pos = reuse_from;
4683 let fuse_lm = mtp.is_none()
4692 && router.is_none()
4693 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4694 self.graph_logits = None;
4695 self.graph_want_logits = false;
4696 let _tpf = std::time::Instant::now();
4697 let batch_k = self.generation_batch_k();
4698 if let Some(rows) = prompt_rows {
4699 let hs = self.hidden_size;
4700 let chunk = self.prefill_chunk().max(1);
4701 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4702 let end = (pos + chunk).min(input_ids.len());
4703 let hb = match self.prefill_input_rows(
4704 PrefillIn::Hidden(&rows[pos * hs..end * hs]), pos, task_mask,
4705 ) {
4706 Ok(hb) => hb,
4707 Err(err) => {
4708 self.finish_generation(&mut mtp, &mut router, true);
4709 return Err(err);
4710 }
4711 };
4712 if mimo_spec { self.mimo_note_rows(&hb, pos); }
4713 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4714 pos = end;
4715 }
4716 }
4717 while self.qwen4_exp.is_some()
4728 && mtp.is_none()
4729 && pos < input_ids.len()
4730 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4731 {
4732 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4735 let want_logits = end == input_ids.len();
4736 let mut lg = Vec::new();
4737 if let Some(b) = &mut self.qwen4_exp {
4738 crate::qwen4_exp::forward_tokens(
4739 &b.0,
4740 &b.1,
4741 &b.2,
4742 &mut b.3,
4743 &input_ids[pos..end],
4744 pos,
4745 &self.inv_freq,
4746 self.pool.as_deref(),
4747 &mut lg,
4748 want_logits,
4749 );
4750 }
4751 if want_logits {
4752 self.graph_logits = Some(lg);
4753 }
4754 pos = end;
4755 hidden.fill(0.0);
4756 }
4757 while self.dsv4.is_some()
4758 && mtp.is_none()
4759 && pos < input_ids.len()
4760 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4761 {
4762 let end = (pos + prefill_chunk()).min(input_ids.len());
4763 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4764 let mut lg = Vec::new();
4765 if let Some(b) = &mut self.dsv4 {
4766 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4767 crate::dsv4::forward_chunk(
4768 g,
4769 layers,
4770 &cfg,
4771 st,
4772 &ids,
4773 pos,
4774 &self.inv_freq,
4775 self.pool.as_deref(),
4776 &mut lg,
4777 end == input_ids.len(),
4778 );
4779 }
4780 if end == input_ids.len() {
4781 self.graph_logits = Some(lg);
4782 }
4783 pos = end;
4784 hidden = vec![0.0; self.hidden_size];
4785 }
4786 let dsv41_prefill = self.dsv41_prefill.take();
4787 while self.dsv41.is_some()
4788 && mtp.is_none()
4789 && pos < input_ids.len()
4790 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4791 {
4792 let end = (pos + prefill_chunk()).min(input_ids.len());
4793 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4794 let mut lg = Vec::new();
4795 if let Some(b) = &mut self.dsv41 {
4796 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4797 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4798 crate::dsv41::forward_chunk_masked_with_embeddings(
4799 g,
4800 layers,
4801 cfg,
4802 st,
4803 &ids,
4804 pos,
4805 &embeddings[pos..end],
4806 &participates[pos..end],
4807 self.pool.as_deref(),
4808 &mut lg,
4809 );
4810 } else {
4811 crate::dsv41::forward_chunk(
4812 g,
4813 layers,
4814 cfg,
4815 st,
4816 &ids,
4817 pos,
4818 self.pool.as_deref(),
4819 &mut lg,
4820 );
4821 }
4822 }
4823 if end == input_ids.len() {
4824 self.graph_logits = Some(lg);
4825 }
4826 pos = end;
4827 hidden = vec![0.0; self.hidden_size];
4828 }
4829 let dyn_prefill = router.is_some();
4834 let o1_prefill_limit = o1_prefill
4842 .and_then(|requested| self.o1_effective_boundary(requested))
4843 .map(|boundary| boundary.min(input_ids.len()));
4844 let mut o1_sealed = false;
4845 if let Some(limit) = o1_prefill_limit {
4846 if self.can_prefill_batched() && limit > 2 {
4849 let chunk = self.prefill_chunk();
4850 let hs = self.hidden_size;
4851 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4852 let end = (pos + chunk).min(limit);
4853 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4854 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4855 pos = end;
4856 }
4857 } else {
4858 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4859 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4860 pos += 1;
4861 }
4862 }
4863 if pos >= limit {
4864 o1_sealed = match self.o1_seal_checked() {
4865 Ok(sealed) => sealed,
4866 Err(err) => {
4867 self.finish_generation(&mut mtp, &mut router, true);
4868 return Err(err);
4869 }
4870 };
4871 tracing::info!(
4872 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4873 o1_prefill.unwrap_or(0),
4874 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4875 .unwrap_or(limit),
4876 limit,
4877 input_ids.len()
4878 );
4879 }
4880 }
4881 let graph_prefill = self.graph_prefill_preferred();
4887 #[cfg(target_os = "macos")]
4895 if task_mask.is_none()
4896 && !dyn_prefill
4897 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4898 && crate::gpu::enabled_here()
4899 && self.gdn_cfg.is_some()
4900 && self.g3n.is_none()
4901 && input_ids.len() > 8
4902 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4903 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4904 {
4905 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4906 .ok()
4907 .and_then(|v| v.parse().ok())
4908 .filter(|&v| (16..=512).contains(&v))
4909 .unwrap_or(256);
4910 let hs = self.hidden_size;
4911 let _tp = std::time::Instant::now();
4912 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4913 let end = (pos + chunk).min(input_ids.len());
4914 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4915 MetalPrefillOutcome::Completed(hb) => hb,
4916 MetalPrefillOutcome::Declined => break,
4917 MetalPrefillOutcome::Failed => {
4918 self.finish_generation(&mut mtp, &mut router, true);
4919 return Err("ordinary Metal prefill failed after admission".into());
4920 }
4921 };
4922 if let Some(m) = &mut mtp {
4923 let n_pairs = if end < input_ids.len() {
4924 end - pos
4925 } else {
4926 end - pos - 1
4927 };
4928 if n_pairs > 0 {
4929 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
4930 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
4931 .collect();
4932 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
4933 for (j, (h, t)) in pairs.iter().enumerate() {
4934 let h = h.to_vec();
4935 let _ = self.mtp_step(m, &h, *t, pos + j);
4936 }
4937 }
4938 }
4939 }
4940 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4941 pos = end;
4942 }
4943 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4944 eprintln!(
4945 "metal-prefill: {} of {} tokens in {:.1} ms",
4946 pos,
4947 input_ids.len(),
4948 _tp.elapsed().as_secs_f64() * 1e3
4949 );
4950 }
4951 }
4952 #[cfg(not(target_os = "macos"))]
4957 if task_mask.is_none()
4958 && !dyn_prefill
4959 && mtp.is_none()
4960 && o1_prefill.is_none()
4961 && batch_k > 0
4962 && pos + 1 < input_ids.len()
4963 && self.g4_batch_prefill()
4964 {
4965 let t_g4 = std::time::Instant::now();
4966 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
4967 Ok((n, last)) => {
4968 if n > 0 {
4969 hidden.copy_from_slice(&last);
4970 }
4971 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4972 eprintln!(
4973 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
4974 input_ids.len() - pos,
4975 t_g4.elapsed().as_secs_f64() * 1e3
4976 );
4977 }
4978 pos += n;
4979 }
4980 Err(e) => {
4981 self.finish_generation(&mut mtp, &mut router, true);
4982 return Err(e);
4983 }
4984 }
4985 }
4986 self.mimo_moe_prepare();
4987 #[cfg(not(target_os = "macos"))]
4993 if task_mask.is_none()
4994 && !dyn_prefill
4995 && !graph_prefill
4996 && mtp.is_none()
4997 && o1_prefill.is_none()
4998 && !self.o1_active()
4999 && input_ids.len() > 2
5000 && self.batch_prefix_prefill()
5001 {
5002 let chunk = self.prefill_chunk().max(1);
5003 let hs = self.hidden_size;
5004 let t_bp = std::time::Instant::now();
5005 let pos0 = pos;
5006 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5007 let end = (pos + chunk).min(input_ids.len());
5008 let bk = end - pos;
5009 let mut hiddens = vec![0f32; bk * hs];
5010 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5011 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5012 }
5013 let positions: Vec<usize> = (pos..end).collect();
5014 let mut run = 0usize;
5015 let outcome = self.try_batch_graph_wgpu_prefix(
5016 &mut hiddens,
5017 &positions,
5018 bk,
5019 None,
5020 Some(&mut run),
5021 );
5022 match outcome {
5023 crate::gpu::BatchGraphOutcome::Completed => {
5024 let hb = if run < self.num_layers {
5025 self.prefill_batch_span(
5026 PrefillIn::Hidden(&hiddens),
5027 pos,
5028 None,
5029 run,
5030 self.num_layers,
5031 )
5032 } else {
5033 hiddens
5034 };
5035 if mimo_spec {
5036 self.mimo_note_rows(&hb, pos);
5037 }
5038 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5039 pos = end;
5040 }
5041 crate::gpu::BatchGraphOutcome::Failed => {
5042 self.finish_generation(&mut mtp, &mut router, true);
5043 return Err("batched prefix prefill failed after admission".into());
5044 }
5045 crate::gpu::BatchGraphOutcome::Declined => {
5046 #[cfg(feature = "gpu")]
5049 if pos > pos0 {
5050 self.pull_lagging_host_kv(0, self.num_layers, pos);
5051 }
5052 break;
5053 }
5054 }
5055 }
5056 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5057 eprintln!(
5058 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5059 pos - pos0,
5060 input_ids.len(),
5061 t_bp.elapsed().as_secs_f64() * 1e3
5062 );
5063 }
5064 }
5065 if task_mask.is_none()
5066 && !dyn_prefill
5067 && !graph_prefill
5068 && self.can_prefill_batched()
5069 && self.g3n.is_none()
5070 && o1_prefill.is_none()
5071 && input_ids.len() > 2
5072 {
5073 let chunk = self.prefill_chunk();
5079 let hs = self.hidden_size;
5080 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5081 let end = (pos + chunk).min(input_ids.len());
5082 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5083 if mimo_spec {
5084 self.mimo_note_rows(&hb, pos);
5085 }
5086 if let Some(m) = &mut mtp {
5087 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5088 .ok()
5089 .and_then(|v| v.parse().ok())
5090 .unwrap_or(0);
5091 for p in pos..end {
5092 if p + 1 < input_ids.len() {
5093 if probe >= 1 && p + 2 < input_ids.len() {
5094 let (d1, mut hx) = self.mtp_step_h(
5098 m,
5099 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5100 input_ids[p + 1],
5101 p,
5102 );
5103 let mut ok = d1 == input_ids[p + 2];
5104 Self::chain_probe_note(0, ok);
5105 let mut d_prev = d1;
5106 let mut extra = 0usize;
5107 for j in 1..probe {
5108 if p + 2 + j >= input_ids.len() {
5109 break;
5110 }
5111 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5112 extra += 1;
5113 ok = ok && dj == input_ids[p + 2 + j];
5114 Self::chain_probe_note(j, ok);
5115 d_prev = dj;
5116 hx = hj;
5117 }
5118 m.kv.truncate_last(extra);
5119 } else {
5120 let _ = self.mtp_step(
5121 m,
5122 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5123 input_ids[p + 1],
5124 p,
5125 );
5126 }
5127 }
5128 }
5129 }
5130 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5131 pos = end;
5132 }
5133 }
5134 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5135 if task_mask.is_none()
5136 && !dyn_prefill
5137 && !graph_prefill
5138 && !pair_off
5139 && self.pair_supported()
5140 && o1_prefill.is_none()
5141 {
5142 while pos + 1 < input_ids.len()
5143 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5144 {
5145 let e1 = self.embed_single(input_ids[pos]);
5146 let e2 = self.embed_single(input_ids[pos + 1]);
5147 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5148 if mimo_spec {
5149 self.mimo_note_rows(&h1, pos);
5150 self.mimo_note_rows(&h2, pos + 1);
5151 }
5152 self.commit_linear_scratch();
5154 if let Some(m) = &mut mtp {
5155 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5156 if pos + 2 < input_ids.len() {
5157 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5158 .ok()
5159 .and_then(|v| v.parse().ok())
5160 .unwrap_or(0);
5161 if probe >= 1 && pos + 3 < input_ids.len() {
5162 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5166 let mut ok = d1 == input_ids[pos + 3];
5167 Self::chain_probe_note(0, ok);
5168 let mut d_prev = d1;
5169 let mut extra = 0usize;
5170 for j in 1..probe {
5171 if pos + 3 + j >= input_ids.len() {
5172 break;
5173 }
5174 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5175 extra += 1;
5176 ok = ok && dj == input_ids[pos + 3 + j];
5177 Self::chain_probe_note(j, ok);
5178 d_prev = dj;
5179 hx = hj;
5180 }
5181 m.kv.truncate_last(extra);
5182 } else {
5183 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5184 }
5185 }
5186 }
5187 hidden = h2;
5188 pos += 2;
5189 }
5190 }
5191 let o1_batch_ready = o1_sealed
5204 && o1_prefill.is_some()
5205 && mtp.is_none()
5206 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5207 && (0..self.num_layers).all(|li| {
5208 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5209 cache.o1.is_none() || cache.o1_views().is_some()
5210 });
5211 let mtp_batch_prefill = mtp.is_some()
5216 && graph_prefill
5217 && task_mask.is_none()
5218 && !dyn_prefill
5219 && !self.o1_active()
5220 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5221 if batch_k > 0
5222 && (graph_prefill || o1_batch_ready)
5223 && task_mask.is_none()
5224 && (!self.o1_active() || o1_batch_ready)
5225 && (mtp.is_none() || mtp_batch_prefill)
5226 && !dyn_prefill
5227 && pos + 1 < input_ids.len()
5228 {
5229 let hs = self.hidden_size;
5230 let chunk = batch_k;
5231 while pos < input_ids.len() {
5232 let end = (pos + chunk).min(input_ids.len());
5233 let bk = end - pos;
5234 let mut hiddens = vec![0f32; bk * hs];
5235 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5236 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5237 }
5238 let positions: Vec<usize> = (pos..end).collect();
5239 let t_chunk = std::time::Instant::now();
5240 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5241 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5242 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5243 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5244 eprintln!(
5245 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5246 if o1_batch_ready {
5247 "o1"
5248 } else if mtp_batch_prefill {
5249 "ordinary_mtp"
5250 } else {
5251 "ordinary"
5252 },
5253 bk as f64 / (ms / 1000.0)
5254 );
5255 }
5256 {
5257 use std::sync::atomic::{AtomicBool, Ordering};
5258 static SAID: AtomicBool = AtomicBool::new(false);
5259 if !SAID.swap(true, Ordering::Relaxed) {
5260 if ok_b {
5261 tracing::info!(
5262 "batched prefill: ACTIVE mode={} (k={bk})",
5263 if o1_batch_ready {
5264 "o1"
5265 } else if mtp_batch_prefill {
5266 "ordinary_mtp"
5267 } else {
5268 "ordinary"
5269 }
5270 );
5271 } else {
5272 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5273 }
5274 }
5275 }
5276 if ok_b {
5277 if mimo_spec {
5278 self.mimo_note_rows(&hiddens, pos);
5279 }
5280 if mtp_batch_prefill {
5281 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5282 if n_pairs > 0 {
5283 let rows: Vec<Vec<f32>> = (0..n_pairs)
5289 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5290 .collect();
5291 let pairs: Vec<(&[f32], u32)> = rows
5292 .iter()
5293 .enumerate()
5294 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5295 .collect();
5296 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5297 eprintln!(
5298 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5299 pos,
5300 n_pairs,
5301 pos + n_pairs - 1,
5302 );
5303 }
5304 let warm_error = if let Some(m) = mtp.as_mut() {
5305 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5306 } else {
5307 None
5308 };
5309 if let Some(err) = warm_error {
5310 self.finish_generation(&mut mtp, &mut router, true);
5315 return Err(err.to_string());
5316 }
5317 }
5318 }
5319 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5320 pos = end;
5321 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5322 self.finish_generation(&mut mtp, &mut router, true);
5327 return Err(if o1_batch_ready {
5328 "sealed O(1) batch graph failed after admission".to_string()
5329 } else {
5330 "ordinary recurrent batch graph failed after admission".to_string()
5331 });
5332 } else {
5333 break; }
5335 }
5336 }
5337 if graph_prefill
5341 && task_mask.is_none()
5342 && mtp.is_none()
5343 && !dyn_prefill
5344 && pos == 0
5345 && input_ids.len() > 1
5346 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5347 {
5348 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5349 self.graph_logits = Some(lg);
5350 hidden = vec![0.0; self.hidden_size];
5351 pos = input_ids.len();
5352 }
5353 }
5354 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5355 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5356 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5357 if mimo_spec {
5358 self.mimo_note_rows(&hidden, pos);
5359 }
5360 if let Some(m) = &mut mtp {
5361 if pos + 1 < input_ids.len() {
5362 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5368 .ok()
5369 .and_then(|v| v.parse().ok())
5370 .unwrap_or(0);
5371 if probe >= 1 && pos + 2 < input_ids.len() {
5372 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5373 let mut ok = d1 == input_ids[pos + 2];
5374 Self::chain_probe_note(0, ok);
5375 let mut d_prev = d1;
5376 let mut extra = 0usize;
5377 for j in 1..probe {
5378 if pos + 2 + j >= input_ids.len() {
5379 break;
5380 }
5381 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5382 extra += 1;
5383 ok = ok && dj == input_ids[pos + 2 + j];
5384 Self::chain_probe_note(j, ok);
5385 d_prev = dj;
5386 hx = hj;
5387 }
5388 m.kv.truncate_last(extra);
5391 } else {
5392 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5393 }
5394 }
5395 }
5396 pos += 1;
5397 }
5398 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5399 eprintln!(
5400 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5401 input_ids.len(),
5402 _tpf.elapsed().as_secs_f64() * 1000.0
5403 );
5404 }
5405 if self
5406 .graph_failed
5407 .swap(false, std::sync::atomic::Ordering::Relaxed)
5408 {
5409 self.finish_generation(&mut mtp, &mut router, true);
5414 return Err("GPU token graph failed during prefill".to_string());
5415 }
5416 if self
5419 .cancel
5420 .swap(false, std::sync::atomic::Ordering::Relaxed)
5421 {
5422 self.finish_generation(&mut mtp, &mut router, true);
5426 return Ok(GenerateResult {
5427 text: String::new(),
5428 token_ids: Vec::new(),
5429 prompt_tokens: input_ids.len(),
5430 tokens_generated: 0,
5431 finish_reason: "cancelled".to_string(),
5432 mtp_drafted: 0,
5433 mtp_accepted: 0,
5434 token_confidence: Vec::new(),
5435 traces: Vec::new(),
5436 });
5437 }
5438
5439 if !o1_sealed {
5442 match self.o1_seal_checked() {
5443 Ok(_) => {}
5444 Err(err) => {
5445 self.finish_generation(&mut mtp, &mut router, true);
5446 return Err(err);
5447 }
5448 }
5449 }
5450
5451 macro_rules! commit {
5453 ($id:expr) => {{
5454 all_ids.push($id);
5455 generated += 1;
5456 self.note_draft_id($id);
5457 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5458 finish_reason = "stop".to_string();
5459 false
5460 } else {
5461 let token_text = self.tokenizer.decode_token($id);
5462 let mut go = true;
5463 if let Some(ref mut cb) = on_token {
5464 if !cb(&token_text) {
5465 finish_reason = "cancelled".to_string();
5466 go = false;
5467 }
5468 }
5469 go
5470 }
5471 }};
5472 }
5473
5474 let mut spec_trial = SpecTrial::Spec {
5485 t0: std::time::Instant::now(),
5486 gen0: generated,
5487 rounds: 0,
5488 };
5489 let mut spec_mon = SpecMon {
5495 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5496 ..SpecMon::default()
5497 };
5498 let mut spec_watchdog_off = false;
5499 let mut spec_walls: Vec<f32> = Vec::new();
5502 let mut spec_round_end: Option<std::time::Instant> = None;
5505 if mimo_spec {
5506 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5507 if let Some(mut st) = self.mimo_mtp.take() {
5508 self.mimo_mtp_probe(&mut st, input_ids, &path);
5509 self.mimo_mtp = Some(st);
5510 }
5511 }
5512 }
5513 let mut next_pos = input_ids.len();
5515 'decode: while generated < max_tokens {
5516 if self
5517 .graph_failed
5518 .swap(false, std::sync::atomic::Ordering::Relaxed)
5519 {
5520 self.finish_generation(&mut mtp, &mut router, true);
5525 return Err("GPU token graph failed during decode".to_string());
5526 }
5527 if self
5528 .cancel
5529 .swap(false, std::sync::atomic::Ordering::Relaxed)
5530 {
5531 finish_reason = "cancelled".to_string();
5532 break 'decode;
5533 }
5534 if mimo_spec && next_pos > 0 {
5539 self.mimo_note_rows(&hidden, next_pos - 1);
5542 }
5543 let forced = self.spec_forced.take();
5544 let mut logits = match (forced, self.graph_logits.take()) {
5545 (Some(_), _) => Vec::new(),
5546 (None, Some(lg)) => lg,
5547 (None, None) => {
5548 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5549 inference::rms_norm_into(
5550 &hidden,
5551 &self.weights.final_norm,
5552 self.rms_eps,
5553 self.norm_style,
5554 &mut self.ws.n1,
5555 );
5556 self.lm_head_forward(&self.ws.n1)
5557 }
5558 };
5559 if generated
5562 == std::env::var("CMF_LOGIT_DUMP_STEP")
5563 .ok()
5564 .and_then(|v| v.parse().ok())
5565 .unwrap_or(0)
5566 {
5567 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5568 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5569 for v in hidden.iter().chain(logits.iter()) {
5570 bytes.extend_from_slice(&v.to_le_bytes());
5571 }
5572 if let Err(e) = std::fs::write(&path, &bytes) {
5573 eprintln!("logit dump: failed to write {path}: {e}");
5574 self.finish_generation(&mut mtp, &mut router, true);
5575 return Err(format!("logit dump write failed: {e}"));
5576 }
5577 }
5578 }
5579 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5583 if !logits.is_empty() {
5584 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5585 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5586 if let Err(e) =
5587 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5588 {
5589 eprintln!("logit dump: failed to write {}: {e}", path.display());
5590 }
5591 }
5592 }
5593 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5594 Some(c) => c,
5595 None => {
5596 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5597 sampler::sample_with_scratch_pool(
5598 &logits,
5599 &self.sampler_config,
5600 self.sampler_config.penalty_past(&all_ids, bounded_native),
5601 &mut self.rng,
5602 &mut self.sampler_scratch,
5603 self.pool.as_deref(),
5604 )
5605 }
5606 };
5607 if self.confidence_on {
5608 confidence.push(if logits.is_empty() {
5609 0.0
5610 } else {
5611 sampler::top1_prob_pool(
5612 self.pool.as_deref(),
5613 &mut self.sampler_scratch,
5614 &logits,
5615 t_next,
5616 calib_temp,
5617 )
5618 });
5619 }
5620 if !logits.is_empty() {
5621 attention::recycle_buf(&mut logits);
5622 }
5623 if trace_on {
5624 let skill = router.as_ref().and_then(|r| r.active_id());
5628 traces.push(TokenTrace {
5629 t: generated,
5630 token_id: t_next,
5631 confidence: confidence.last().copied().unwrap_or(0.0),
5632 active_skill: skill,
5633 recon: None,
5634 switched: false,
5635 });
5636 }
5637 if !commit!(t_next) {
5638 break 'decode;
5639 }
5640 if generated >= max_tokens {
5641 break 'decode;
5642 }
5643
5644 self.swa_trim_tails();
5647 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5648 static SAID: std::sync::Once = std::sync::Once::new();
5654 SAID.call_once(|| {
5655 tracing::warn!(
5656 "KV cache full at {} positions — evicting half; quality \
5657 will degrade. Raise CMF_MAX_SEQ.",
5658 self.kv_cache.max_seq_len,
5659 );
5660 });
5661 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5662 self.kv_cache.evict(keep);
5663 }
5664
5665 if graph_spec {
5668 match spec_trial {
5669 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5670 spec_mon.plain_ms =
5671 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5672 let keep = spec_mon.pays();
5673 tracing::info!(
5674 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5675 spec_mon.tokens,
5676 spec_mon.round_ms,
5677 spec_mon.plain_ms,
5678 if keep { "speculating" } else { "plain" }
5679 );
5680 spec_mon.fails = 0;
5681 spec_trial = SpecTrial::Decided {
5682 spec: keep,
5683 recheck_at: if keep { usize::MAX } else { generated + 128 },
5684 };
5685 }
5686 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5687 spec_mon.n = 0;
5688 spec_trial = SpecTrial::Spec {
5689 t0: std::time::Instant::now(),
5690 gen0: generated,
5691 rounds: 0,
5692 };
5693 }
5694 _ => {}
5695 }
5696 spec_watchdog_off = matches!(
5697 spec_trial,
5698 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5699 );
5700 }
5701 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5703 let budget = max_tokens - generated - 1;
5704 if let Some(mut st) = self.mimo_mtp.take() {
5705 let k = st.depth.min(budget);
5706 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5707 self.mimo_mtp = Some(st);
5708 let r = match r {
5709 Ok(r) => r,
5710 Err(err) => {
5711 self.finish_generation(&mut mtp, &mut router, true);
5712 return Err(err);
5713 }
5714 };
5715 if let Some(r) = r {
5716 drafted += r.drafted;
5717 accepted += r.accepted.len();
5718 let mut stopped = false;
5719 for &id in &r.accepted {
5720 if self.confidence_on {
5721 confidence.push(0.0);
5722 }
5723 if !commit!(id) {
5724 stopped = true;
5725 break;
5726 }
5727 }
5728 if stopped {
5729 break 'decode;
5730 }
5731 next_pos += r.accepted.len() + 1;
5732 hidden = r.hidden;
5733 self.graph_logits = Some(r.logits);
5736 continue 'decode;
5737 }
5738 }
5739 }
5740 #[cfg(feature = "gpu")]
5743 if self.speculative
5744 && self.qwen4_exp.is_some()
5745 && task_mask.is_none()
5746 && self.sampler_config.temperature < 1e-6
5747 && generated + 1 < max_tokens
5748 && next_pos > 0
5749 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5750 {
5751 let r = match &mut self.qwen4_exp {
5752 Some(b) => crate::qwen4_exp::spec_round(
5753 &b.0,
5754 &b.1,
5755 &b.2,
5756 &mut b.3,
5757 next_pos,
5758 &all_ids,
5759 &self.inv_freq,
5760 self.pool.as_deref(),
5761 ),
5762 None => None,
5763 };
5764 if let Some(r) = r {
5765 drafted += r.drafted;
5766 accepted += r.accepted.len();
5767 let mut stopped = false;
5768 for &id in &r.accepted {
5769 if self.confidence_on {
5770 confidence.push(0.0);
5771 }
5772 if !commit!(id) {
5773 stopped = true;
5774 break;
5775 }
5776 }
5777 if stopped {
5778 break 'decode;
5779 }
5780 next_pos += r.accepted.len() + 1;
5781 hidden.fill(0.0);
5782 self.graph_logits = Some(r.logits);
5783 continue 'decode;
5784 }
5785 }
5786 match &mut mtp {
5787 #[cfg(feature = "gpu")]
5789 Some(m)
5790 if graph_spec
5791 && !spec_watchdog_off
5792 && generated + 1 < max_tokens
5793 && next_pos > 0 =>
5794 {
5795 let t_round = std::time::Instant::now();
5796 if spec_time_level() >= 2 {
5797 if let Some(t) = spec_round_end.take() {
5798 eprintln!(
5799 "spec-gap {:.2} ms (host between rounds)",
5800 t.elapsed().as_secs_f64() * 1e3
5801 );
5802 }
5803 }
5804 spec_stamps_begin();
5805 #[cfg(target_os = "macos")]
5810 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
5811 .load(std::sync::atomic::Ordering::Relaxed);
5812 #[cfg(not(target_os = "macos"))]
5813 let allocs0 = 0u64;
5814 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5815 m,
5816 &hidden,
5817 t_next,
5818 next_pos,
5819 &mut drafted,
5820 &mut accepted,
5821 &mut all_ids,
5822 max_tokens - generated,
5823 ) {
5824 next_pos = n_pos;
5825 hidden = new_h;
5826 let level = spec_time_level();
5827 if level > 0 {
5828 let wall = t_round.elapsed().as_secs_f32() * 1e3;
5829 let stamps = spec_stamps_take();
5830 let median = if spec_walls.len() >= 3 {
5833 let mut s = spec_walls.clone();
5834 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5835 Some(s[s.len() / 2])
5836 } else {
5837 None
5838 };
5839 let outlier = median.is_some_and(|m| wall > 1.4 * m);
5840 #[cfg(target_os = "macos")]
5841 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5842 .load(std::sync::atomic::Ordering::Relaxed)
5843 - allocs0;
5844 #[cfg(not(target_os = "macos"))]
5845 let allocs = allocs0;
5846 eprintln!(
5847 "spec-round wall {wall:.1} ms → {} tokens{}{}",
5848 extra.len() + 1,
5849 if allocs > 0 {
5850 format!(" [{allocs} new device buffers]")
5851 } else {
5852 String::new()
5853 },
5854 match (outlier, median) {
5855 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5856 _ => String::new(),
5857 }
5858 );
5859 if level >= 2 || outlier {
5860 let sum: f32 = stamps.iter().map(|s| s.1).sum();
5861 eprintln!(
5862 "spec-stamps: {}| untracked {:.1}",
5863 spec_stamps_format(&stamps),
5864 wall - sum
5865 );
5866 }
5867 if spec_mon.n >= 1 {
5868 spec_walls.push(wall);
5869 }
5870 }
5871 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5875 spec_trial = Self::spec_trial_round(
5878 spec_trial,
5879 &mut spec_mon,
5880 generated + extra.len() + 1,
5881 );
5882 let mut stopped = false;
5883 for &id in &extra {
5884 if self.confidence_on {
5885 confidence.push(0.0);
5886 }
5887 if !commit!(id) {
5888 stopped = true;
5889 break;
5890 }
5891 }
5892 if stopped {
5893 break 'decode;
5894 }
5895 if spec_time_level() >= 2 {
5896 spec_round_end = Some(std::time::Instant::now());
5897 }
5898 continue 'decode;
5899 }
5900 if self
5901 .graph_failed
5902 .swap(false, std::sync::atomic::Ordering::Relaxed)
5903 {
5904 self.finish_generation(&mut mtp, &mut router, true);
5910 return Err("GPU MTP graph failed during speculative decode".to_string());
5911 }
5912 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
5923 spec_mon.tokens = 0.0;
5924 spec_mon.fails = 3;
5925 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
5926 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
5927 next_pos += 1;
5928 continue 'decode;
5929 }
5930 Some(m) if !graph_spec && generated + 1 < max_tokens => {
5932 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
5933 drafted += 1;
5934 let emb1 = self.embed_single(t_next);
5935 let emb2 = self.embed_single(draft);
5936 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
5937
5938 inference::rms_norm_into(
5939 &h1,
5940 &self.weights.final_norm,
5941 self.rms_eps,
5942 self.norm_style,
5943 &mut self.ws.n1,
5944 );
5945 let mut logits1 = self.lm_head_forward(&self.ws.n1);
5946 let t_after = sampler::sample_with_scratch_pool(
5947 &logits1,
5948 &self.sampler_config,
5949 self.sampler_config.penalty_past(&all_ids, bounded_native),
5950 &mut self.rng,
5951 &mut self.sampler_scratch,
5952 self.pool.as_deref(),
5953 );
5954 if self.confidence_on {
5955 confidence.push(sampler::top1_prob_pool(
5956 self.pool.as_deref(),
5957 &mut self.sampler_scratch,
5958 &logits1,
5959 t_after,
5960 calib_temp,
5961 ));
5962 }
5963 attention::recycle_buf(&mut logits1);
5964 if trace_on {
5965 traces.push(TokenTrace {
5968 t: generated,
5969 token_id: t_after,
5970 confidence: confidence.last().copied().unwrap_or(0.0),
5971 active_skill: None,
5972 recon: None,
5973 switched: false,
5974 });
5975 }
5976 let stop = !commit!(t_after);
5977
5978 if t_after == draft {
5979 accepted += 1;
5980 self.commit_linear_scratch();
5981 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5982 hidden = h2;
5983 next_pos += 2;
5984 } else {
5985 for layer in &mut self.kv_cache.layers {
5987 layer.truncate_last(1);
5988 }
5989 if !stop {
5990 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5991 hidden = self.forward_layers(
5992 &self.embed_single(t_after),
5993 next_pos + 1,
5994 None,
5995 );
5996 }
5997 next_pos += 2;
5998 }
5999 if stop {
6000 break 'decode;
6001 }
6002 }
6003 _ => {
6005 #[cfg(feature = "gpu")]
6010 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6011 static SAID: std::sync::Once = std::sync::Once::new();
6012 SAID.call_once(|| {
6013 eprintln!(
6014 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6015 !self.dsv4_mtp.is_empty(),
6016 task_mask.is_none(),
6017 router.is_none(),
6018 !trace_on,
6019 self.sampler_config.temperature < 1e-6,
6020 self.sampler_config.repetition_penalty == 1.0,
6021 );
6022 });
6023 }
6024 #[cfg(feature = "gpu")]
6025 if Self::dsv4_spec_on()
6026 && self.dsv4.is_some()
6027 && !self.dsv4_mtp.is_empty()
6028 && task_mask.is_none()
6029 && router.is_none()
6030 && !trace_on
6031 && self.sampler_config.temperature < 1e-6
6032 && self.sampler_config.repetition_penalty == 1.0
6033 && generated + 1 < max_tokens
6034 && all_ids.len() >= 2
6035 && generated >= dsv4_spec_retry_at
6036 {
6037 let tip_token = all_ids[all_ids.len() - 2];
6038 let drafted0 = drafted;
6039 let round = self.dsv4_spec_step(
6040 tip_token,
6041 t_next,
6042 next_pos,
6043 max_tokens.saturating_sub(generated),
6044 &mut drafted,
6045 &mut accepted,
6046 );
6047 if drafted > drafted0 {
6048 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6049 if useful {
6050 dsv4_spec_bad = 0;
6051 } else {
6052 dsv4_spec_bad += 1;
6053 if dsv4_spec_bad >= 2 {
6054 dsv4_spec_bad = 0;
6055 dsv4_spec_retry_at = generated.saturating_add(32);
6056 tracing::info!(
6057 "dsv4: draft не окупился дважды — точный walk на 32 токена"
6058 );
6059 }
6060 }
6061 }
6062 if let Some((extra, n_pos)) = round {
6063 next_pos = n_pos;
6064 let mut stopped = false;
6065 for &id in &extra {
6066 if self.confidence_on {
6067 confidence.push(0.0);
6068 }
6069 if !commit!(id) {
6070 stopped = true;
6071 break;
6072 }
6073 }
6074 if stopped {
6075 break 'decode;
6076 }
6077 continue 'decode;
6078 }
6079 }
6080 self.graph_want_logits = fuse_lm;
6081 let mut t_fwd = t_next;
6087 let pure_greedy = self.sampler_config.temperature < 1e-6
6088 && self.sampler_config.repetition_penalty == 1.0
6089 && self.sampler_config.suppress_tokens.is_empty();
6090 let burst_k = std::env::var("CMF_MULTISTEP")
6095 .ok()
6096 .and_then(|v| v.parse::<usize>().ok())
6097 .unwrap_or(0);
6098 if pure_greedy
6099 && burst_k >= 1
6100 && fuse_lm
6101 && task_mask.is_none()
6102 && router.is_none()
6103 && !trace_on
6104 && !self.confidence_on
6105 {
6106 let mut stopped = false;
6107 loop {
6108 let room = max_tokens.saturating_sub(generated);
6109 if room <= 2 {
6110 break;
6111 }
6112 let k = burst_k.min(room - 1);
6113 if k < 1 {
6114 break;
6115 }
6116 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6117 if self
6118 .graph_failed
6119 .swap(false, std::sync::atomic::Ordering::Relaxed)
6120 {
6121 self.finish_generation(&mut mtp, &mut router, true);
6122 return Err(
6123 "GPU token graph failed during greedy burst".to_string()
6124 );
6125 }
6126 break;
6127 };
6128 next_pos += k;
6129 for &id in &ids {
6130 if !commit!(id) {
6131 stopped = true;
6132 break;
6133 }
6134 }
6135 if stopped {
6136 break;
6137 }
6138 t_fwd = *ids.last().unwrap();
6139 }
6140 if stopped {
6141 break 'decode;
6142 }
6143 }
6144 #[cfg(target_os = "macos")]
6154 if graph_spec
6155 && spec_watchdog_off
6156 && next_pos > 0
6157 && self.mtp_graph_mode == Some(true)
6158 && crate::gpu::q1_force()
6159 {
6160 if let Some(m) = mtp.as_mut() {
6161 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6162 }
6163 }
6164 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6165 next_pos += 1;
6166 if let Some(r) = &mut router {
6169 let phi = self.dyn_phi_ema.clone();
6170 let decision = r.step(&phi, generated);
6171 if let Some(new_active) = decision {
6172 let _ = self.set_active_skill(new_active);
6173 }
6174 if trace_on {
6177 if let Some(last) = traces.last_mut() {
6178 let e = r.last_best_e();
6179 last.recon = e.is_finite().then_some(e);
6180 last.switched = decision.is_some();
6181 }
6182 }
6183 }
6184 }
6185 }
6186 }
6187
6188 let cancelled = finish_reason == "cancelled";
6189 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6193 if mimo_spec {
6194 if let Some(st) = self.mimo_mtp.as_ref() {
6195 let line = st.stats.line();
6196 tracing::info!("{line}");
6197 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6198 eprintln!("{line}");
6199 }
6200 }
6201 }
6202 self.finish_generation(&mut mtp, &mut router, cancelled);
6203
6204 let output_ids = &all_ids[input_ids.len()..];
6205 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6209 let consumed = std::mem::take(&mut all_ids);
6214 if dyn_switched {
6215 self.clear_sequence_state();
6216 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6217 self.clear_history();
6218 } else {
6219 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6220 }
6221 all_ids = consumed;
6222 let output_ids = &all_ids[input_ids.len()..];
6223 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6225 Ok(GenerateResult {
6226 text: self.tokenizer.decode(output_ids),
6227 token_ids: output_ids.to_vec(),
6228 prompt_tokens: input_ids.len(),
6229 tokens_generated: generated,
6230 finish_reason,
6231 mtp_drafted: drafted,
6232 mtp_accepted: accepted,
6233 token_confidence: confidence,
6234 traces,
6235 })
6236 }
6237
6238 fn mtp_step(
6242 &mut self,
6243 m: &mut MtpModule,
6244 hidden: &[f32],
6245 next_token: u32,
6246 position: usize,
6247 ) -> u32 {
6248 self.mtp_step_h(m, hidden, next_token, position).0
6249 }
6250
6251 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6255 use std::sync::Mutex;
6256 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6257 let mut t = T.lock().unwrap();
6258 if t.len() <= depth {
6259 t.resize(depth + 1, (0, 0));
6260 }
6261 t[depth].0 += 1;
6262 t[depth].1 += prefix_ok as u64;
6263 if depth == 0 && t[0].0 % 128 == 0 {
6264 let line: Vec<String> = t
6265 .iter()
6266 .enumerate()
6267 .map(|(d, (n, k))| {
6268 format!(
6269 "d{}={:.0}%({n})",
6270 d + 1,
6271 100.0 * *k as f64 / (*n).max(1) as f64
6272 )
6273 })
6274 .collect();
6275 eprintln!("mtp-chain: {}", line.join(" "));
6276 }
6277 }
6278
6279 fn mtp_step_hl(
6287 &mut self,
6288 m: &mut MtpModule,
6289 hidden: &[f32],
6290 next_token: u32,
6291 position: usize,
6292 ) -> (Vec<f32>, Vec<f32>) {
6293 #[cfg(target_os = "macos")]
6298 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6299 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6300 self.mtp_graph_mode = Some(true);
6301 return r;
6302 }
6303 if self.mtp_graph_mode == Some(true) {
6304 tracing::error!("mtp Metal graph failed after admission");
6305 self.clear_sequence_state();
6306 self.graph_failed
6307 .store(true, std::sync::atomic::Ordering::Relaxed);
6308 self.cancel
6309 .store(true, std::sync::atomic::Ordering::Relaxed);
6310 return (Vec::new(), Vec::new());
6311 }
6312 self.mtp_graph_mode = Some(false);
6313 }
6314 #[cfg(feature = "gpu")]
6315 if self.mtp_graph_mode != Some(false) {
6316 if !self.mtp_graph_ok(m) {
6317 if self.mtp_graph_mode == Some(true) {
6318 tracing::error!("mtp graph became unavailable after admission");
6323 self.clear_sequence_state();
6324 self.graph_failed
6325 .store(true, std::sync::atomic::Ordering::Relaxed);
6326 self.cancel
6327 .store(true, std::sync::atomic::Ordering::Relaxed);
6328 return (Vec::new(), Vec::new());
6329 }
6330 self.mtp_graph_mode = Some(false);
6331 } else {
6332 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6333 self.mtp_graph_mode = Some(true);
6334 return r;
6335 }
6336 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6337 return (Vec::new(), Vec::new());
6344 }
6345 tracing::error!("mtp graph failed or declined after admission");
6349 self.clear_sequence_state();
6350 self.graph_failed
6351 .store(true, std::sync::atomic::Ordering::Relaxed);
6352 self.cancel
6353 .store(true, std::sync::atomic::Ordering::Relaxed);
6354 return (Vec::new(), Vec::new());
6355 }
6356 }
6357 let e = self.embed_single(next_token);
6361 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6362 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6363 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6364 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6365 let mut x = vec![0.0f32; self.hidden_size];
6366 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6367
6368 let lw = &m.layer;
6370 inference::rms_norm_into(
6371 &x,
6372 &lw.input_norm,
6373 self.rms_eps,
6374 self.norm_style,
6375 &mut self.ws.n1,
6376 );
6377 let attn = match &lw.attn {
6378 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6380 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6381 AttnKind::Full {
6382 wq,
6383 wk,
6384 wv,
6385 wo,
6386 q_norm,
6387 k_norm,
6388 output_gate,
6389 softplus_gate,
6390 bias,
6391 } => {
6392 let mut cfg = self.attn_cfg(position);
6393 cfg.q_norm = q_norm.as_deref();
6394 cfg.k_norm = k_norm.as_deref();
6395 cfg.output_gate = *output_gate;
6396 cfg.softplus_gate = softplus_gate
6397 .as_ref()
6398 .map(|(gate, per_head)| (gate, *per_head));
6399 cfg.bias = bias
6400 .as_ref()
6401 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6402 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6403 }
6404 AttnKind::Linear(_)
6405 | AttnKind::LinearGdn(_)
6406 | AttnKind::ShortConv(_)
6407 | AttnKind::Bounded(_) => {
6408 unreachable!("MTP block is full attention")
6409 }
6410 };
6411 for (i, &a) in attn.iter().enumerate() {
6412 x[i] += a;
6413 }
6414 inference::rms_norm_into(
6415 &x,
6416 &lw.post_norm,
6417 self.rms_eps,
6418 self.norm_style,
6419 &mut self.ws.p1,
6420 );
6421 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6422 for (i, &f) in ffn.iter().enumerate() {
6423 x[i] += f;
6424 }
6425
6426 inference::rms_norm_into(
6427 &x,
6428 &m.final_norm,
6429 self.rms_eps,
6430 self.norm_style,
6431 &mut self.ws.n1,
6432 );
6433 let lg = self.lm_head_forward(&self.ws.n1);
6434 (lg, x)
6435 }
6436
6437 fn mtp_step_h(
6439 &mut self,
6440 m: &mut MtpModule,
6441 hidden: &[f32],
6442 next_token: u32,
6443 position: usize,
6444 ) -> (u32, Vec<f32>) {
6445 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6446 let draft = sampler::argmax(&lg);
6447 attention::recycle_buf(&mut lg);
6448 (draft, x)
6449 }
6450
6451 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6457 match trial {
6458 SpecTrial::Spec { t0, gen0, rounds } => {
6459 let rounds = rounds + 1;
6460 if rounds >= 5 {
6461 if mon.plain_ms > 0.0 {
6462 let keep = mon.pays();
6463 mon.fails = 0;
6464 tracing::info!(
6465 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6466 mon.tokens,
6467 mon.round_ms,
6468 mon.plain_ms,
6469 if keep { "speculating" } else { "plain" }
6470 );
6471 SpecTrial::Decided {
6472 spec: keep,
6473 recheck_at: if keep { usize::MAX } else { generated + 128 },
6474 }
6475 } else if mon.pays() {
6476 mon.fails = 0;
6481 tracing::info!(
6482 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6483 mon.tokens,
6484 mon.round_ms,
6485 );
6486 SpecTrial::Decided {
6487 spec: true,
6488 recheck_at: usize::MAX,
6489 }
6490 } else {
6491 SpecTrial::Plain {
6492 t0: std::time::Instant::now(),
6493 gen0: generated,
6494 }
6495 }
6496 } else {
6497 SpecTrial::Spec { t0, gen0, rounds }
6498 }
6499 }
6500 SpecTrial::Decided { spec: true, .. } => {
6501 if mon.pays() {
6502 mon.fails = 0;
6503 trial
6504 } else {
6505 mon.fails += 1;
6506 if mon.fails >= 4 {
6507 if mon.plain_ms <= 0.0 {
6508 tracing::info!(
6512 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6513 mon.tokens,
6514 mon.round_ms,
6515 );
6516 return SpecTrial::Plain {
6517 t0: std::time::Instant::now(),
6518 gen0: generated,
6519 };
6520 }
6521 tracing::info!(
6522 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6523 mon.tokens,
6524 mon.round_ms,
6525 mon.plain_ms
6526 );
6527 SpecTrial::Decided {
6528 spec: false,
6529 recheck_at: generated + 128,
6530 }
6531 } else {
6532 trial
6533 }
6534 }
6535 }
6536 other => other,
6537 }
6538 }
6539
6540 fn mtp_kv_id(&self) -> u64 {
6543 self.graph_kv_id | (1u64 << 40)
6544 }
6545
6546 const MTP_LAYER_BASE: usize = 0;
6551
6552 #[cfg(feature = "gpu")]
6559 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6560 self.mtp_graph_mode != Some(true)
6561 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6562 }
6563
6564 #[cfg(feature = "gpu")]
6570 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6571 let mut ok = true;
6572 let mut expected = false;
6573 for li in 0..self.num_layers {
6574 if matches!(
6575 self.weights.layers[self.phys_layer(li)].attn,
6576 AttnKind::Full { .. }
6577 ) {
6578 expected = true;
6579 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6580 }
6581 }
6582 !expected || ok
6583 }
6584
6585 fn graph_gdn_layer_count(&self) -> usize {
6589 (0..self.num_layers)
6590 .filter(|&li| {
6591 matches!(
6592 &self.weights.layers[self.phys_layer(li)].attn,
6593 AttnKind::LinearGdn(_)
6594 )
6595 })
6596 .count()
6597 }
6598
6599 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6602 let e = self.embed_single(next_token);
6603 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6604 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6605 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6606 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6607 let mut x = vec![0.0f32; self.hidden_size];
6608 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6609 x
6610 }
6611
6612 #[cfg(feature = "gpu")]
6615 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6616 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6617 return false;
6618 }
6619 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6620 || !crate::gpu::enabled_here()
6621 || self.attn_softcap > 0.0
6622 || self.attention_heads_per_layer.is_some()
6623 || self.v_head_dim.is_some()
6626 {
6627 return false;
6628 }
6629 matches!(
6630 &m.layer.attn,
6631 AttnKind::Full {
6632 softplus_gate: None,
6633 ..
6634 }
6635 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6636 }
6637
6638 #[cfg(feature = "gpu")]
6642 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6643 if !self.mtp_block_graph_ok(m) {
6644 return false;
6645 }
6646 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6647 return false;
6648 };
6649 let FfnKind::Dense(d) = &m.layer.ffn else {
6650 return false;
6651 };
6652 d.segs.is_empty()
6653 && wq.graph_weight().is_some()
6654 && wk.graph_weight().is_some()
6655 && wv.graph_weight().is_some()
6656 && wo.graph_weight().is_some()
6657 && d.gate_proj.graph_weight().is_some()
6658 && d.up_proj.graph_weight().is_some()
6659 && d.down_proj.graph_weight().is_some()
6660 && self.weights.lm_head.graph_weight().is_some()
6661 }
6662
6663 #[cfg(feature = "gpu")]
6669 fn mtp_step_graph(
6670 &mut self,
6671 m: &mut MtpModule,
6672 hidden: &[f32],
6673 next_token: u32,
6674 position: usize,
6675 ) -> Option<(Vec<f32>, Vec<f32>)> {
6676 if !self.mtp_graph_ok(m) {
6677 return None;
6678 }
6679 let lw = &m.layer;
6680 let AttnKind::Full {
6681 wq,
6682 wk,
6683 wv,
6684 wo,
6685 q_norm,
6686 k_norm,
6687 output_gate,
6688 softplus_gate,
6689 bias,
6690 } = &lw.attn
6691 else {
6692 return None;
6693 };
6694 if softplus_gate.is_some() {
6695 return None;
6696 }
6697 let FfnKind::Dense(d) = &lw.ffn else {
6698 return None;
6699 };
6700 if !d.segs.is_empty() {
6701 return None; }
6703 let mut x = self.mtp_block_input(m, hidden, next_token);
6706 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6707 let (_, i, kind, rs) = t.graph_weight()?;
6708 Some(crate::gpu::GraphW {
6709 idx: i,
6710 kind,
6711 row_scale: rs,
6712 data: &[],
6713 prism: crate::gpu::GraphPrismOp::None,
6714 affine: false,
6715 })
6716 }
6717 let (model, _, _, _) = wq.graph_weight()?;
6718 let model = model.clone();
6719 let (lm_gw, lm_rows) = {
6720 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6721 let rows = if kind == 6 {
6725 self.draft_head_rows(self.weights.lm_head.rows())
6726 } else {
6727 self.weights.lm_head.rows()
6728 };
6729 (
6730 crate::gpu::GraphW {
6731 idx: i,
6732 kind,
6733 row_scale: rs,
6734 data: &[],
6735 prism: crate::gpu::GraphPrismOp::None,
6736 affine: false,
6737 },
6738 rows,
6739 )
6740 };
6741 let layer = crate::gpu::GraphLayer {
6742 input_norm: &lw.input_norm,
6743 attn: crate::gpu::GraphAttn::Full {
6744 wq: gw(wq)?,
6745 wk: gw(wk)?,
6746 wv: gw(wv)?,
6747 wo: gw(wo)?,
6748 q_norm: q_norm.as_deref(),
6749 k_norm: k_norm.as_deref(),
6750 late_qk_norm: self.qk_norm_after_rope,
6751 bias: bias
6752 .as_ref()
6753 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6754 output_gate: *output_gate,
6755 cpu_k: m.kv.k_heads(),
6756 cpu_v: m.kv.v_heads(),
6757 cpu_base: m.kv.base(),
6758 geom: None,
6759 head_gate: None,
6760 },
6761 post_norm: &lw.post_norm,
6762 attn_out_norm: None,
6763 ffn_out_norm: None,
6764 layer_scale: None,
6765 ffn: crate::gpu::GraphFfn::Dense {
6766 gate: gw(&d.gate_proj)?,
6767 up: gw(&d.up_proj)?,
6768 down: gw(&d.down_proj)?,
6769 act: d.act.graph_act()?,
6770 },
6771 };
6772 let nh = self.num_heads;
6773 let (nkv, hd, rd) = self.layer_geom(0);
6774 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6775 let mut logits = Vec::new();
6776 let ok = crate::gpu::forward_token_graph(
6777 &model,
6778 self.mtp_kv_id(),
6779 std::slice::from_ref(&layer),
6780 &[None],
6781 self.o1_epoch,
6782 &self.inv_freq,
6783 &mut x,
6784 nh,
6785 nkv,
6786 hd,
6787 self.attn_scale,
6788 rd,
6789 self.hidden_size,
6790 self.intermediate_size,
6791 position,
6792 self.kv_cache.max_seq_len,
6793 gemma,
6794 self.rms_eps as f32,
6795 Some((&lm_gw, lm_rows)),
6796 &m.final_norm,
6797 &mut logits,
6798 &[],
6799 1,
6800 None,
6801 None,
6802 None,
6803 Self::MTP_LAYER_BASE,
6804 true,
6805 );
6806 match ok {
6807 crate::gpu::TokenGraphOutcome::Completed => {}
6808 crate::gpu::TokenGraphOutcome::Declined => return None,
6809 crate::gpu::TokenGraphOutcome::Failed => {
6810 self.clear_sequence_state();
6814 self.graph_failed
6815 .store(true, std::sync::atomic::Ordering::Relaxed);
6816 self.cancel
6817 .store(true, std::sync::atomic::Ordering::Relaxed);
6818 return None;
6819 }
6820 }
6821 logits.resize(self.vocab_size, 0.0);
6822 Some((logits, x))
6823 }
6824
6825 #[cfg(feature = "gpu")]
6833 fn mtp_warm_graph(
6834 &mut self,
6835 m: &mut MtpModule,
6836 pairs: &[(&[f32], u32)],
6837 first_pos: usize,
6838 ) -> crate::gpu::BatchGraphOutcome {
6839 if pairs.is_empty() {
6840 return crate::gpu::BatchGraphOutcome::Completed;
6841 }
6842 if !self.mtp_block_graph_ok(m) {
6843 return crate::gpu::BatchGraphOutcome::Declined;
6844 }
6845 let hs = self.hidden_size;
6846 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6849 for (h, t) in pairs {
6850 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6851 }
6852 let lw = &m.layer;
6853 let AttnKind::Full {
6854 wq,
6855 wk,
6856 wv,
6857 wo,
6858 q_norm,
6859 k_norm,
6860 output_gate,
6861 bias,
6862 ..
6863 } = &lw.attn
6864 else {
6865 return crate::gpu::BatchGraphOutcome::Declined;
6866 };
6867 let FfnKind::Dense(d) = &lw.ffn else {
6868 return crate::gpu::BatchGraphOutcome::Declined;
6869 };
6870 if !d.segs.is_empty() {
6871 return crate::gpu::BatchGraphOutcome::Declined; }
6873 let (AttnKind::Full { softplus_gate: None, .. }, Some(gact)) =
6876 (&lw.attn, d.act.graph_act())
6877 else {
6878 return crate::gpu::BatchGraphOutcome::Declined;
6879 };
6880 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6881 let (_, i, kind, rs) = t.graph_weight()?;
6882 Some(crate::gpu::GraphW {
6883 idx: i,
6884 kind,
6885 row_scale: rs,
6886 data: &[],
6887 prism: crate::gpu::GraphPrismOp::None,
6888 affine: false,
6889 })
6890 }
6891 let Some((model, _, _, _)) = wq.graph_weight() else {
6892 return crate::gpu::BatchGraphOutcome::Declined;
6893 };
6894 let model = model.clone();
6895 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6896 gw(wq),
6897 gw(wk),
6898 gw(wv),
6899 gw(wo),
6900 gw(&d.gate_proj),
6901 gw(&d.up_proj),
6902 gw(&d.down_proj),
6903 ) else {
6904 return crate::gpu::BatchGraphOutcome::Declined;
6905 };
6906 let layer = crate::gpu::GraphLayer {
6907 input_norm: &lw.input_norm,
6908 attn: crate::gpu::GraphAttn::Full {
6909 wq: gwq,
6910 wk: gwk,
6911 wv: gwv,
6912 wo: gwo,
6913 q_norm: q_norm.as_deref(),
6914 k_norm: k_norm.as_deref(),
6915 late_qk_norm: self.qk_norm_after_rope,
6916 bias: bias
6917 .as_ref()
6918 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6919 output_gate: *output_gate,
6920 cpu_k: m.kv.k_heads(),
6921 cpu_v: m.kv.v_heads(),
6922 cpu_base: m.kv.base(),
6923 geom: None,
6924 head_gate: None,
6925 },
6926 post_norm: &lw.post_norm,
6927 attn_out_norm: None,
6928 ffn_out_norm: None,
6929 layer_scale: None,
6930 ffn: crate::gpu::GraphFfn::Dense {
6931 gate: gg,
6932 up: gu,
6933 down: gd,
6934 act: gact,
6935 },
6936 };
6937 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
6938 let nh = self.num_heads;
6939 let (nkv, hd, rd) = self.layer_geom(0);
6940 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6941 crate::gpu::forward_batch_graph(
6942 &model,
6943 self.mtp_kv_id(),
6944 std::slice::from_ref(&layer),
6945 &self.inv_freq,
6946 &mut hiddens,
6947 nh,
6948 nkv,
6949 hd,
6950 rd,
6951 hs,
6952 self.intermediate_size,
6953 &positions,
6954 self.kv_cache.max_seq_len,
6955 gemma,
6956 self.rms_eps as f32,
6957 self.attn_scale,
6958 pairs.len(),
6959 &[],
6960 0,
6961 None,
6962 None,
6963 )
6964 }
6965
6966 #[cfg(feature = "gpu")]
6973 fn mtp_warm_graph_fallback(
6974 &mut self,
6975 m: &mut MtpModule,
6976 pairs: &[(&[f32], u32)],
6977 first_pos: usize,
6978 ) -> bool {
6979 if pairs.is_empty() {
6980 return true;
6981 }
6982 let graphable = self.mtp_block_graph_ok(m);
6983 if !graphable {
6984 if self.mtp_graph_mode == Some(true) {
6988 return false;
6989 }
6990 self.mtp_graph_mode = Some(false);
6991 for (j, (h, t)) in pairs.iter().enumerate() {
6992 self.mtp_warm(m, h, *t, first_pos + j);
6993 }
6994 return true;
6995 }
6996
6997 for (j, (h, t)) in pairs.iter().enumerate() {
7002 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7003 return false;
7004 }
7005 }
7006 self.mtp_graph_mode = Some(true);
7007 true
7008 }
7009
7010 #[cfg(feature = "gpu")]
7015 fn mtp_warm_prefill_pairs(
7016 &mut self,
7017 m: &mut MtpModule,
7018 pairs: &[(&[f32], u32)],
7019 first_pos: usize,
7020 ) -> Result<(), &'static str> {
7021 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7026 if self.mtp_graph_mode == Some(true) {
7027 return Err("MTP token graph became unavailable after admission");
7028 }
7029 self.mtp_graph_mode = Some(false);
7030 for (j, (h, t)) in pairs.iter().enumerate() {
7031 self.mtp_warm(m, h, *t, first_pos + j);
7032 }
7033 return Ok(());
7034 }
7035 match self.mtp_warm_graph(m, pairs, first_pos) {
7036 crate::gpu::BatchGraphOutcome::Completed => {
7037 if !pairs.is_empty() {
7038 self.mtp_graph_mode = Some(true);
7039 }
7040 Ok(())
7041 }
7042 crate::gpu::BatchGraphOutcome::Declined => {
7043 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7044 Ok(())
7045 } else {
7046 Err("MTP warm-up fallback failed after device admission")
7047 }
7048 }
7049 crate::gpu::BatchGraphOutcome::Failed => {
7050 Err("MTP warm batch graph failed after admission")
7051 }
7052 }
7053 }
7054
7055 #[cfg(not(feature = "gpu"))]
7056 fn mtp_warm_prefill_pairs(
7057 &mut self,
7058 m: &mut MtpModule,
7059 pairs: &[(&[f32], u32)],
7060 first_pos: usize,
7061 ) -> Result<(), &'static str> {
7062 for (j, (h, t)) in pairs.iter().enumerate() {
7063 self.mtp_warm(m, h, *t, first_pos + j);
7064 }
7065 Ok(())
7066 }
7067
7068 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7072 let e = self.embed_single(next_token);
7073 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7074 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7075 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7076 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7077 let mut x = vec![0.0f32; self.hidden_size];
7078 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7079 inference::rms_norm_into(
7080 &x,
7081 &m.layer.input_norm,
7082 self.rms_eps,
7083 self.norm_style,
7084 &mut self.ws.n1,
7085 );
7086 let attn = match &m.layer.attn {
7087 AttnKind::Full {
7088 wq,
7089 wk,
7090 wv,
7091 wo,
7092 q_norm,
7093 k_norm,
7094 output_gate,
7095 softplus_gate,
7096 bias,
7097 } => {
7098 let mut cfg = self.attn_cfg(position);
7099 cfg.q_norm = q_norm.as_deref();
7100 cfg.k_norm = k_norm.as_deref();
7101 cfg.output_gate = *output_gate;
7102 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7103 cfg.bias = bias
7104 .as_ref()
7105 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7106 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7107 }
7108 _ => return,
7109 };
7110 let _ = attn;
7111 }
7112
7113 #[cfg(feature = "gpu")]
7120 #[allow(clippy::too_many_arguments)]
7121 fn graph_spec_step(
7122 &mut self,
7123 m: &mut MtpModule,
7124 hidden: &[f32],
7125 t_next: u32,
7126 next_pos: usize,
7127 drafted: &mut usize,
7128 accepted: &mut usize,
7129 all_ids: &mut Vec<u32>,
7133 room: usize,
7138 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7139 #[cfg(target_os = "macos")]
7150 let metal_native = crate::gpu::q1_force();
7151 #[cfg(not(target_os = "macos"))]
7152 let metal_native = false;
7153 #[cfg(feature = "gpu")]
7154 let k_default = if metal_native {
7155 7
7158 } else if crate::gpu_wgpu::verify_i8_on() {
7159 5
7160 } else {
7161 4
7162 };
7163 #[cfg(not(feature = "gpu"))]
7164 let k_default = 4;
7165 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7166 .ok()
7167 .and_then(|v| v.parse().ok())
7168 .filter(|&v| (1..=8).contains(&v));
7169 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
7174 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7175 let k_spec = k_full.min(room).max(1);
7176 let k_capped = k_spec < k_full;
7179 if next_pos == 0 {
7180 return None;
7181 }
7182 let t_round = std::time::Instant::now();
7183 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7199 let sub0 = subs();
7200 let cfg = self.sampler_config.clone();
7205 let penalized = !(cfg.repetition_penalty == 1.0
7206 && cfg.presence_penalty == 0.0
7207 && cfg.suppress_tokens.is_empty());
7208 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7213 let sampling = cfg.temperature >= 1e-6;
7214 let sparse = sampling && sampler::sparse_ok(&cfg);
7220 let base_len = all_ids.len();
7221 if sampling && !sparse && self.spec_q.len() < k_spec {
7222 self.spec_q.resize_with(k_spec, Vec::new);
7223 }
7224 if sparse && self.spec_qs.len() < k_spec {
7225 self.spec_qs.resize_with(k_spec, Vec::new);
7226 }
7227 let mut drafts = Vec::with_capacity(k_spec);
7232 let mut hx = hidden.to_vec();
7233 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7236 spec_stamp("pro");
7237 #[cfg(target_os = "macos")]
7243 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7244 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7245 Ok(ids) => {
7246 self.mtp_graph_mode = Some(true);
7247 drafts = ids;
7248 }
7249 Err(true) => {
7250 tracing::error!("mtp Metal draft chain failed after commit");
7251 self.clear_sequence_state();
7252 self.graph_failed
7253 .store(true, std::sync::atomic::Ordering::Relaxed);
7254 self.cancel
7255 .store(true, std::sync::atomic::Ordering::Relaxed);
7256 return None;
7257 }
7258 Err(false) => {}
7259 }
7260 }
7261 for j in drafts.len()..k_spec {
7262 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7263 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7264 if spec_dbg {
7265 let saved = self.mtp_graph_mode;
7266 self.mtp_graph_mode = Some(false);
7267 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7268 self.mtp_graph_mode = saved;
7269 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7270 return None;
7271 }
7272 m.kv.truncate_last(1);
7273 dbg_ref = Some(r);
7274 }
7275 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7276 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7277 return None;
7278 }
7279 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7280 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7281 let dl = lg
7282 .iter()
7283 .zip(&lg_cpu)
7284 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7285 let dh = hj
7286 .iter()
7287 .zip(&h_cpu)
7288 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7289 eprintln!(
7290 "spec-dbg j={j} pos {} tok_in {tok_in}: per-op draft {} graph draft {} | max|dlogit| {dl:.3} | |h_cpu| {:.2} |h_graph| {:.2} max|dh| {dh:.3} | kv rows {}",
7291 next_pos - 1 + j,
7292 sampler::argmax(&lg_cpu),
7293 sampler::argmax(&lg),
7294 n(&h_cpu),
7295 n(&hj),
7296 m.kv.seq_len
7297 );
7298 }
7299 let dj = if sparse {
7300 let mut q = std::mem::take(&mut self.spec_qs[j]);
7301 let ok = sampler::sparse_distribution_into(
7302 &lg,
7303 &cfg,
7304 all_ids,
7305 &mut self.sampler_scratch,
7306 self.pool.as_deref(),
7307 &mut q,
7308 );
7309 let d = if ok {
7310 sampler::draw_sparse(&q, &mut self.rng)
7311 } else {
7312 let t = sampler::argmax(&lg);
7314 q.clear();
7315 q.push((t, 1.0));
7316 t
7317 };
7318 self.spec_qs[j] = q;
7319 all_ids.push(d);
7320 d
7321 } else if sampling {
7322 let mut q = std::mem::take(&mut self.spec_q[j]);
7323 sampler::distribution_into(
7324 &lg,
7325 &cfg,
7326 all_ids,
7327 &mut self.sampler_scratch,
7328 self.pool.as_deref(),
7329 &mut q,
7330 );
7331 let d = sampler::draw(&q, &mut self.rng);
7332 self.spec_q[j] = q;
7333 all_ids.push(d); d
7335 } else if greedy_pen {
7336 let d = sampler::argmax_penalized(
7337 &lg,
7338 &cfg,
7339 all_ids,
7340 &mut self.sampler_scratch,
7341 self.pool.as_deref(),
7342 );
7343 all_ids.push(d);
7344 d
7345 } else {
7346 sampler::argmax(&lg)
7347 };
7348 attention::recycle_buf(&mut lg);
7349 drafts.push(dj);
7350 hx = hj;
7351 spec_stamp("d.pick");
7352 }
7353 all_ids.truncate(base_len);
7354 *drafted += k_spec;
7355 let t_draft = t_round.elapsed();
7356 let sub_draft = subs();
7357 let b = k_spec + 1;
7360 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7361 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7362 let e = self.embed_single(t);
7363 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7364 }
7365 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7366 spec_stamp("v.emb");
7367 let (lm_gw, lm_rows) = {
7368 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7369 (
7370 crate::gpu::GraphW {
7371 idx: i,
7372 kind,
7373 row_scale: rs,
7374 data: &[],
7375 prism: crate::gpu::GraphPrismOp::None,
7376 affine: false,
7377 },
7378 self.weights.lm_head.rows(),
7379 )
7380 };
7381 let mut logits = Vec::new();
7382 let final_norm = self.weights.final_norm.clone();
7383 #[cfg(target_os = "macos")]
7392 let greedy_dev = metal_native
7393 && !sampling
7394 && !greedy_pen
7395 && !self.confidence_on
7396 && self.final_softcap.is_none()
7397 && self.vocab_size == lm_rows
7403 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7404 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7405 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7406 #[cfg(not(target_os = "macos"))]
7407 let greedy_dev = false;
7408 let mut dev_ids: Vec<u32> = Vec::new();
7409 #[cfg(target_os = "macos")]
7410 let verify_outcome = if metal_native {
7411 let lm = self.weights.lm_head.q1_parts()?;
7412 let n_score = self.vocab_size.min(lm_rows);
7413 self.try_batch_graph_metal(
7414 &mut hiddens,
7415 &positions,
7416 b,
7417 Some((lm, &final_norm, &mut logits)),
7418 if greedy_dev {
7419 Some((n_score, &mut dev_ids))
7420 } else {
7421 None
7422 },
7423 )
7424 } else {
7425 self.try_batch_graph_wgpu(
7426 &mut hiddens,
7427 &positions,
7428 b,
7429 Some(crate::gpu::SpecTail {
7430 lm: lm_gw,
7431 lm_rows,
7432 final_norm: &final_norm,
7433 logits_out: &mut logits,
7434 }),
7435 )
7436 };
7437 #[cfg(not(target_os = "macos"))]
7438 let verify_outcome = self.try_batch_graph_wgpu(
7439 &mut hiddens,
7440 &positions,
7441 b,
7442 Some(crate::gpu::SpecTail {
7443 lm: lm_gw,
7444 lm_rows,
7445 final_norm: &final_norm,
7446 logits_out: &mut logits,
7447 }),
7448 );
7449 match verify_outcome {
7450 crate::gpu::BatchGraphOutcome::Completed => {}
7451 crate::gpu::BatchGraphOutcome::Declined => {
7452 m.kv.truncate_last(k_spec);
7456 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7457 self.clear_sequence_state();
7458 self.graph_failed
7459 .store(true, std::sync::atomic::Ordering::Relaxed);
7460 self.cancel
7461 .store(true, std::sync::atomic::Ordering::Relaxed);
7462 tracing::error!("MTP graph mirror rewind failed after verify decline");
7463 }
7464 return None;
7465 }
7466 crate::gpu::BatchGraphOutcome::Failed => {
7467 self.clear_sequence_state();
7471 self.graph_failed
7472 .store(true, std::sync::atomic::Ordering::Relaxed);
7473 self.cancel
7474 .store(true, std::sync::atomic::Ordering::Relaxed);
7475 tracing::error!("MTP verify batch graph failed after admission");
7476 return None;
7477 }
7478 }
7479 #[cfg(target_os = "macos")]
7485 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7486 let snap: Vec<Vec<f32>> = self
7487 .kv_cache
7488 .layers
7489 .iter()
7490 .map(|l| l.linear_state.clone())
7491 .collect();
7492 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7493 let toks: Vec<u32> = std::iter::once(t_next)
7494 .chain(drafts.iter().copied())
7495 .collect();
7496 let want_save = self.graph_want_logits;
7497 self.graph_want_logits = false;
7498 for (i, &t) in toks.iter().enumerate() {
7499 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7500 let _ = self.graph_logits.take();
7501 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7505 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7506 }
7507 let ref_lg = self.logits_from_hidden(&hi);
7508 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7509 let ra = sampler::argmax(&ref_lg);
7510 let va = sampler::argmax(row);
7511 let mut md = 0f32;
7512 let mut rms = 0f64;
7513 for j in 0..lm_rows.min(ref_lg.len()) {
7514 let d = (ref_lg[j] - row[j]).abs();
7515 md = md.max(d);
7516 rms += (d as f64) * (d as f64);
7517 }
7518 let mut hd = 0f32;
7519 for j in 0..self.hidden_size {
7520 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7521 }
7522 eprintln!(
7523 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7524 next_pos + i,
7525 if ra == va { "OK" } else { "MISMATCH" },
7526 (rms / lm_rows as f64).sqrt()
7527 );
7528 }
7529 self.graph_want_logits = want_save;
7530 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7533 if l.linear_state.len() == st.len() {
7534 l.linear_state.copy_from_slice(&st);
7535 } else {
7536 l.linear_state = st;
7537 }
7538 }
7539 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7540 let extra = l.seq_len.saturating_sub(n0);
7541 if extra > 0 {
7542 l.truncate_last(extra);
7543 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7544 }
7545 }
7546 }
7547 let t_verify = t_round.elapsed();
7548 let sub_verify = subs();
7549 let mut a = 0usize;
7554 let mut forced: Option<u32> = None;
7555 let ids: Vec<u32> = if sparse {
7556 let mut p = std::mem::take(&mut self.spec_ps);
7557 let mut res = std::mem::take(&mut self.spec_ress);
7558 while a < k_spec {
7559 let ok = sampler::sparse_distribution_into(
7560 &logits[a * lm_rows..(a + 1) * lm_rows],
7561 &cfg,
7562 all_ids,
7563 &mut self.sampler_scratch,
7564 self.pool.as_deref(),
7565 &mut p,
7566 );
7567 if !ok {
7568 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7569 p.clear();
7570 p.push((t, 1.0));
7571 }
7572 match sampler::spec_accept_or_correct_sparse(
7573 &p,
7574 &self.spec_qs[a],
7575 drafts[a],
7576 &mut self.rng,
7577 &mut res,
7578 ) {
7579 None => {
7580 all_ids.push(drafts[a]);
7581 a += 1;
7582 }
7583 Some(c) => {
7584 forced = Some(c);
7585 break;
7586 }
7587 }
7588 }
7589 all_ids.truncate(base_len);
7590 self.spec_ps = p;
7591 self.spec_ress = res;
7592 drafts.clone()
7593 } else if sampling {
7594 let mut p = std::mem::take(&mut self.spec_p);
7595 let mut res = std::mem::take(&mut self.spec_res);
7596 while a < k_spec {
7597 sampler::distribution_into(
7598 &logits[a * lm_rows..(a + 1) * lm_rows],
7599 &cfg,
7600 all_ids,
7601 &mut self.sampler_scratch,
7602 self.pool.as_deref(),
7603 &mut p,
7604 );
7605 match sampler::spec_accept_or_correct(
7606 &p,
7607 &self.spec_q[a],
7608 drafts[a],
7609 &mut self.rng,
7610 &mut res,
7611 self.pool.as_deref(),
7612 ) {
7613 None => {
7614 all_ids.push(drafts[a]);
7615 a += 1;
7616 }
7617 Some(c) => {
7618 forced = Some(c);
7619 break;
7620 }
7621 }
7622 }
7623 all_ids.truncate(base_len);
7624 self.spec_p = p;
7625 self.spec_res = res;
7626 drafts.clone()
7628 } else if greedy_pen {
7629 let mut ids: Vec<u32> = Vec::with_capacity(b);
7633 for i in 0..b {
7634 let t = sampler::argmax_penalized(
7635 &logits[i * lm_rows..(i + 1) * lm_rows],
7636 &cfg,
7637 all_ids,
7638 &mut self.sampler_scratch,
7639 self.pool.as_deref(),
7640 );
7641 ids.push(t);
7642 if i < k_spec && t == drafts[i] {
7643 all_ids.push(t);
7644 } else {
7645 break;
7646 }
7647 }
7648 all_ids.truncate(base_len);
7649 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7650 a += 1;
7651 }
7652 ids
7655 } else if greedy_dev && dev_ids.len() == b {
7656 let ids = std::mem::take(&mut dev_ids);
7657 while a < k_spec && ids[a] == drafts[a] {
7658 a += 1;
7659 }
7660 ids
7661 } else {
7662 if logits.len() < b * lm_rows {
7663 self.clear_sequence_state();
7666 self.graph_failed
7667 .store(true, std::sync::atomic::Ordering::Relaxed);
7668 self.cancel
7669 .store(true, std::sync::atomic::Ordering::Relaxed);
7670 tracing::error!("Metal verify returned neither logits nor argmax ids");
7671 return None;
7672 }
7673 let ids: Vec<u32> = (0..b)
7674 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7675 .collect();
7676 while a < k_spec && ids[a] == drafts[a] {
7677 a += 1;
7678 }
7679 ids
7680 };
7681 spec_stamp("acc");
7682 if spec_dbg {
7683 eprintln!(
7684 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7685 drafts, ids
7686 );
7687 }
7688 #[cfg(target_os = "macos")]
7692 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7693 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7694 {
7695 let snap: Vec<Vec<f32>> = self
7696 .kv_cache
7697 .layers
7698 .iter()
7699 .map(|l| l.linear_state.clone())
7700 .collect();
7701 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7702 let toks: Vec<u32> = std::iter::once(t_next)
7703 .chain(drafts.iter().copied())
7704 .collect();
7705 let want_save = self.graph_want_logits;
7706 self.graph_want_logits = false;
7707 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7708 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7709 let _ = self.graph_logits.take();
7710 }
7711 self.graph_want_logits = want_save;
7712 let plain_states: Vec<Vec<f32>> = self
7713 .kv_cache
7714 .layers
7715 .iter()
7716 .map(|l| l.linear_state.clone())
7717 .collect();
7718 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7719 let mut rows = Vec::new();
7720 for (li, (l, n0)) in self
7721 .kv_cache
7722 .layers
7723 .iter_mut()
7724 .zip(attn_lens.iter())
7725 .enumerate()
7726 {
7727 let extra = l.seq_len.saturating_sub(*n0);
7728 if extra > 0 {
7729 let mut kk = Vec::new();
7730 let mut vv = Vec::new();
7731 for g in 0..nkv {
7732 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7733 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7734 }
7735 rows.push((li, kk, vv));
7736 l.truncate_last(extra);
7737 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7738 }
7739 }
7740 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7741 if l.linear_state.len() == st.len() {
7742 l.linear_state.copy_from_slice(&st);
7743 } else {
7744 l.linear_state = st;
7745 }
7746 }
7747 Some((plain_states, rows))
7748 } else {
7749 None
7750 };
7751 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7752 #[cfg(target_os = "macos")]
7761 let mut warm_pending: Option<MetalWarmPending> = None;
7762 #[cfg(target_os = "macos")]
7763 if metal_native {
7764 m.kv.truncate_last(k_spec.saturating_sub(1));
7765 if self.mtp_graph_mode == Some(true) {
7766 crate::gpu_metal::kv_mirror_set_stored(
7769 self.mtp_kv_id(),
7770 Self::MTP_LAYER_BASE,
7771 m.kv.seq_len,
7772 );
7773 if !warm_off && a > 0 {
7774 let pairs: Vec<(&[f32], u32)> = (0..a)
7775 .map(|j| {
7776 (
7777 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7778 ids[j],
7779 )
7780 })
7781 .collect();
7782 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7783 }
7784 }
7785 spec_stamp("c.wsub");
7786 }
7787 #[cfg(target_os = "macos")]
7789 if metal_native {
7790 if !self.metal_verify_commit(a) {
7793 self.clear_sequence_state();
7794 self.graph_failed
7795 .store(true, std::sync::atomic::Ordering::Relaxed);
7796 self.cancel
7797 .store(true, std::sync::atomic::Ordering::Relaxed);
7798 tracing::error!("Metal verify state/KV handoff failed after admission");
7799 return None;
7800 }
7801 if let Some((plain_states, rows)) = commit_ref {
7802 crate::gpu_metal::queue_fence();
7803 let _ = crate::gpu_metal::wait_replay();
7806 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7807 let mut worst_s = 0f32;
7808 let mut worst_li = 0usize;
7809 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7810 if l.linear_state.len() != ps.len() || ps.is_empty() {
7811 continue;
7812 }
7813 let d = l
7814 .linear_state
7815 .iter()
7816 .zip(ps)
7817 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7818 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7819 let rel = d / n.max(1e-6);
7820 if rel > worst_s {
7821 worst_s = rel;
7822 worst_li = li;
7823 }
7824 }
7825 let mut worst_k = 0f32;
7826 for (li, kk, vv) in &rows {
7827 let l = &self.kv_cache.layers[*li];
7828 let n0 = l.seq_len - (kk.len() / (nkv * hd));
7829 let mut ck = Vec::new();
7830 let mut cv = Vec::new();
7831 for g in 0..nkv {
7832 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7833 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7834 }
7835 if ck.len() == kk.len() {
7836 let dk = ck
7837 .iter()
7838 .zip(kk)
7839 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7840 let dv = cv
7841 .iter()
7842 .zip(vv)
7843 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7844 worst_k = worst_k.max(dk).max(dv);
7845 } else {
7846 eprintln!(
7847 "commit-check L{li}: kv row count mismatch {} vs {}",
7848 ck.len(),
7849 kk.len()
7850 );
7851 }
7852 }
7853 eprintln!(
7854 "commit-check a={a}: worst GDN state rel-max diff {worst_s:.2e} (L{worst_li}) | worst K/V row abs diff {worst_k:.4}"
7855 );
7856 }
7857 }
7858 if !metal_native && a + 1 < b {
7859 let expected_gdn_layers = self.graph_gdn_layer_count();
7860 if expected_gdn_layers > 0
7861 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7862 {
7863 self.clear_sequence_state();
7864 self.graph_failed
7865 .store(true, std::sync::atomic::Ordering::Relaxed);
7866 self.cancel
7867 .store(true, std::sync::atomic::Ordering::Relaxed);
7868 tracing::error!("GDN speculative restore failed after verify");
7869 return None;
7870 }
7871 }
7872 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7873 self.clear_sequence_state();
7878 self.graph_failed
7879 .store(true, std::sync::atomic::Ordering::Relaxed);
7880 self.cancel
7881 .store(true, std::sync::atomic::Ordering::Relaxed);
7882 tracing::error!("trunk graph KV rewind failed after speculative verify");
7883 return None;
7884 }
7885 *accepted += a;
7886 if !metal_native {
7897 m.kv.truncate_last(k_spec.saturating_sub(1));
7899 }
7900 spec_stamp("c.trunc");
7901 if !metal_native
7902 && self.mtp_graph_mode == Some(true)
7903 && !self.rewind_mtp_graph_mirror(next_pos)
7904 {
7905 self.clear_sequence_state();
7909 self.graph_failed
7910 .store(true, std::sync::atomic::Ordering::Relaxed);
7911 self.cancel
7912 .store(true, std::sync::atomic::Ordering::Relaxed);
7913 tracing::error!("MTP graph mirror rewind failed after verify commit");
7914 return None;
7915 }
7916 if !warm_off && a > 0 {
7917 let mut warmed = false;
7920 #[cfg(target_os = "macos")]
7921 if metal_native && self.mtp_graph_mode == Some(true) {
7922 warmed = match warm_pending.take() {
7926 Some(p) => self.mtp_warm_batch_finish(m, p),
7927 None => false,
7928 };
7929 if !warmed {
7930 warmed = true;
7931 for j in 0..a {
7932 let row =
7933 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
7934 if self
7935 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
7936 .is_none()
7937 {
7938 warmed = false;
7939 break;
7940 }
7941 }
7942 }
7943 }
7944 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
7945 let rows: Vec<Vec<f32>> = (0..a)
7946 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
7947 .collect();
7948 let pairs: Vec<(&[f32], u32)> = rows
7949 .iter()
7950 .zip(ids.iter())
7951 .map(|(r, &t)| (r.as_slice(), t))
7952 .collect();
7953 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
7954 Ok(()) => warmed = true,
7955 Err(err) => {
7956 tracing::error!("{err}");
7962 self.clear_sequence_state();
7963 self.graph_failed
7964 .store(true, std::sync::atomic::Ordering::Relaxed);
7965 self.cancel
7966 .store(true, std::sync::atomic::Ordering::Relaxed);
7967 return None;
7968 }
7969 }
7970 }
7971 if !warmed {
7972 for j in 0..a {
7973 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
7974 let row = row.to_vec();
7975 self.mtp_warm(m, &row, ids[j], next_pos + j);
7976 }
7977 }
7978 }
7979 spec_stamp("c.warm");
7983 if let Some(c) = forced {
7984 self.spec_forced = Some(c);
7985 self.graph_logits = None;
7986 } else if greedy_dev && logits.is_empty() {
7987 self.spec_forced = Some(ids[a]);
7990 self.graph_logits = None;
7991 } else {
7992 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
7993 row.resize(self.vocab_size, 0.0);
7994 if let Some(c) = self.final_softcap {
7995 for l in row.iter_mut() {
7996 *l = c * (*l / c).tanh();
7997 }
7998 }
7999 self.graph_logits = Some(row);
8000 }
8001 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8002 spec_stamp("c.row");
8003 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8009 let end = subs();
8010 eprintln!(
8011 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8012 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8013 t_draft.as_secs_f64() * 1e3,
8014 sub_draft - sub0,
8015 (t_verify - t_draft).as_secs_f64() * 1e3,
8016 sub_verify - sub_draft,
8017 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8018 end - sub_verify,
8019 self.draft_full_streak,
8020 );
8021 }
8022 if k_env.is_none() && !metal_native && !k_capped {
8027 let f = a as f32 / k_spec.max(1) as f32;
8031 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8032 let mut k_next = k_spec;
8033 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8034 k_next = k_spec + 1;
8035 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8036 k_next = k_spec - 1;
8037 }
8038 if k_next != k_spec {
8039 self.spec_acc_ewma = 0.6;
8040 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8041 eprintln!("spec-k: {k_spec} → {k_next}");
8042 }
8043 }
8044 self.spec_k_adapt = Some(k_next);
8045 }
8046 spec_stamp("end");
8047 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8048 }
8049
8050 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8059 if !self.pair_supported() {
8060 return (0.0, 0.0);
8061 }
8062 if self.gdn_cfg.is_some()
8070 && self.graph_prefill_preferred()
8071 && self.automatic_gpu_prefix().is_some()
8072 {
8073 tracing::info!(
8074 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8075 );
8076 return (0.0, 0.0);
8077 }
8078 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8085 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8086 let emb1 = self.embed_single(1);
8087 let emb2 = self.embed_single(2);
8088 let pos = self.kv_cache.seq_len();
8089
8090 let t0 = std::time::Instant::now();
8091 for _ in 0..iters {
8092 let _ = self.forward_layers(&emb1, pos, None);
8093 let _ = self.forward_layers(&emb2, pos + 1, None);
8094 for l in &mut self.kv_cache.layers {
8095 l.truncate_last(2);
8096 }
8097 }
8098 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8099
8100 let t1 = std::time::Instant::now();
8101 for _ in 0..iters {
8102 let _ = self.forward_pair(&emb1, &emb2, pos);
8103 for l in &mut self.kv_cache.layers {
8104 l.truncate_last(2);
8105 }
8106 }
8107 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8108 match graph_env {
8109 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8110 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8111 }
8112 (singles_ms, pair_ms)
8113 }
8114
8115 fn pair_supported(&self) -> bool {
8123 !self.weights.layers.is_empty()
8130 && self.g3n.is_none()
8131 && !self
8132 .weights
8133 .layers
8134 .iter()
8135 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8136 }
8137
8138 fn forward_pair(
8139 &mut self,
8140 emb1: &[f32],
8141 emb2: &[f32],
8142 position: usize,
8143 ) -> (Vec<f32>, Vec<f32>) {
8144 self.mimo_moe_prepare();
8147 let mut h1 = emb1.to_vec();
8148 let mut h2 = emb2.to_vec();
8149 let (_nkv, _hd, hs, _rd, eps) = (
8150 self.num_kv_heads,
8151 self.head_dim,
8152 self.hidden_size,
8153 self.rotary_dim,
8154 self.rms_eps,
8155 );
8156 let pool = self.pool.clone();
8157 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8164
8165 for li in 0..self.num_layers {
8166 let _capacity_tail = automatic_gpu_prefix
8167 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8168 .map(|_| crate::gpu::enter_cpu_scope());
8169 crate::gpu::set_layer(li as i64);
8172 let lw = &self.weights.layers[self.phys_layer(li)];
8173 inference::rms_norm_into(
8176 &h1,
8177 &lw.input_norm,
8178 self.rms_eps,
8179 self.norm_style,
8180 &mut self.ws.n1,
8181 );
8182 inference::rms_norm_into(
8183 &h2,
8184 &lw.input_norm,
8185 self.rms_eps,
8186 self.norm_style,
8187 &mut self.ws.n2,
8188 );
8189
8190 let (a1, a2) = match &lw.attn {
8191 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8192 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8193 AttnKind::Bounded(w) => {
8194 let rope = self
8197 .bounded_rope
8198 .clone()
8199 .expect("bounded layer without an installed rotation table");
8200 let cfg = crate::bounded::BoundedAttnCfg {
8201 num_heads: self.num_heads,
8202 num_kv_heads: self.num_kv_heads,
8203 head_dim: self.head_dim,
8204 hidden_size: hs,
8205 scale: self.attn_scale,
8206 rope: &rope,
8207 pool: pool.as_deref(),
8208 };
8209 let a1 = crate::bounded::bounded_attention(
8210 &self.ws.n1,
8211 w,
8212 &mut self.kv_cache.layers[li],
8213 &cfg,
8214 );
8215 let a2 = crate::bounded::bounded_attention(
8216 &self.ws.n2,
8217 w,
8218 &mut self.kv_cache.layers[li],
8219 &cfg,
8220 );
8221 (a1, a2)
8222 }
8223 AttnKind::Linear(w) => {
8224 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8225 let layer = &mut self.kv_cache.layers[li];
8226 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8227 vmf_phase_pair(
8228 &self.ws.n1,
8229 &self.ws.n2,
8230 w,
8231 &cfg,
8232 state,
8233 scratch,
8234 self.pool.as_deref(),
8235 )
8236 }
8237 AttnKind::LinearGdn(w) => {
8238 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8239 let layer = &mut self.kv_cache.layers[li];
8240 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8241 gdn_pair(
8242 &self.ws.n1,
8243 &self.ws.n2,
8244 w,
8245 &cfg,
8246 state,
8247 scratch,
8248 self.pool.as_deref(),
8249 )
8250 }
8251 AttnKind::ShortConv(w) => {
8252 let cfg = self
8253 .short_conv_cfg
8254 .expect("short-conv layer without short_conv_cfg");
8255 let layer = &mut self.kv_cache.layers[li];
8256 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8257 short_conv_pair(
8258 &self.ws.n1,
8259 &self.ws.n2,
8260 w,
8261 &cfg,
8262 state,
8263 scratch,
8264 self.pool.as_deref(),
8265 )
8266 }
8267 AttnKind::Full {
8268 wq,
8269 wk,
8270 wv,
8271 wo,
8272 q_norm,
8273 k_norm,
8274 output_gate,
8275 softplus_gate,
8276 bias,
8277 } => {
8278 let inv_freq_l = self.layer_inv_freq(li);
8279 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8280 let cfg = QwenAttnCfg {
8281 num_heads: self.layer_num_heads(li),
8282 num_kv_heads: nkv_l,
8283 head_dim: hd_l,
8284 hidden_size: hs,
8285 position,
8286 inv_freq: &inv_freq_l,
8287 rotary_dim: rd_l,
8288 scale: self.attn_scale,
8289 softcap: self.attn_softcap,
8290 window: self.layer_window(li),
8291 v_norm: self.attn_v_norm,
8292 qk_norm_after_rope: self.qk_norm_after_rope,
8293 gate_sigmoid: self.proj_gate_sigmoid,
8294 q_norm: q_norm.as_deref(),
8295 k_norm: k_norm.as_deref(),
8296 output_gate: *output_gate,
8297 softplus_gate: softplus_gate
8298 .as_ref()
8299 .map(|(gate, per_head)| (gate, *per_head)),
8300 rope_scale: self.layer_rope_scale(li),
8301 bias: bias
8302 .as_ref()
8303 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8304 rms_eps: eps,
8305 norm_style: self.norm_style,
8306 pool: pool.as_deref(),
8307 v_head_dim: self.layer_v_dim(li),
8308 };
8309 attention::qwen_attention_pair(
8310 &self.ws.n1,
8311 &self.ws.n2,
8312 wq,
8313 wk,
8314 wv,
8315 wo,
8316 &mut self.kv_cache.layers[li],
8317 &cfg,
8318 )
8319 }
8320 };
8321 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8322 Some(w) => (
8323 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8324 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8325 ),
8326 None => (a1, a2),
8327 };
8328 for i in 0..self.hidden_size {
8329 h1[i] += a1[i];
8330 h2[i] += a2[i];
8331 }
8332 let (mut a1, mut a2) = (a1, a2);
8333 attention::recycle_buf(&mut a1);
8334 attention::recycle_buf(&mut a2);
8335
8336 let lw = &self.weights.layers[self.phys_layer(li)];
8337 inference::rms_norm_into(
8338 &h1,
8339 &lw.post_norm,
8340 self.rms_eps,
8341 self.norm_style,
8342 &mut self.ws.p1,
8343 );
8344 inference::rms_norm_into(
8345 &h2,
8346 &lw.post_norm,
8347 self.rms_eps,
8348 self.norm_style,
8349 &mut self.ws.p2,
8350 );
8351 let (f1, f2) = match &lw.ffn {
8352 FfnKind::DenseMoe(dm) => (
8355 dense_moe_ffn(
8356 dm,
8357 &self.ws.p1,
8358 &h1,
8359 self.rms_eps,
8360 self.norm_style,
8361 self.pool.as_deref(),
8362 ),
8363 dense_moe_ffn(
8364 dm,
8365 &self.ws.p2,
8366 &h2,
8367 self.rms_eps,
8368 self.norm_style,
8369 self.pool.as_deref(),
8370 ),
8371 ),
8372 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8373 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8374 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8375 ),
8376 _ => ffn_forward_pair(
8377 &lw.ffn,
8378 &self.ws.p1,
8379 &self.ws.p2,
8380 self.pool.as_deref(),
8381 None,
8382 ),
8383 };
8384 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8385 Some(w) => (
8386 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8387 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8388 ),
8389 None => (f1, f2),
8390 };
8391 for i in 0..self.hidden_size {
8392 h1[i] += f1[i];
8393 h2[i] += f2[i];
8394 }
8395 let (mut f1, mut f2) = (f1, f2);
8396 attention::recycle_buf(&mut f1);
8397 attention::recycle_buf(&mut f2);
8398 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8399 for i in 0..self.hidden_size {
8400 h1[i] *= sc;
8401 h2[i] *= sc;
8402 }
8403 }
8404 if self.is_loop_end(li) && li + 1 < self.num_layers {
8406 h1 = inference::rms_norm(
8407 &h1,
8408 &self.weights.final_norm,
8409 self.rms_eps,
8410 self.norm_style,
8411 );
8412 h2 = inference::rms_norm(
8413 &h2,
8414 &self.weights.final_norm,
8415 self.rms_eps,
8416 self.norm_style,
8417 );
8418 }
8419 }
8420 crate::gpu::set_layer(-1); if self.o1_active() {
8427 self.commit_linear_scratch();
8428 }
8429 self.o1_progress();
8430 self.swa_trim_tails();
8431 (h1, h2)
8432 }
8433
8434 fn commit_linear_scratch(&mut self) {
8436 for layer in &mut self.kv_cache.layers {
8437 if !layer.linear_scratch.is_empty() {
8438 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8439 layer.linear_scratch.clear();
8440 }
8441 }
8442 }
8443
8444 pub fn forward_ids(
8447 &mut self,
8448 ids: &[u32],
8449 task_mask: Option<&TaskMask>,
8450 ) -> Result<Vec<f32>, String> {
8451 #[cfg(target_os = "macos")]
8452 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8453 if ids.is_empty() {
8454 return Err("empty id sequence".to_string());
8455 }
8456 self.clear_sequence_state();
8457 self.check_forward_graph("forward_ids setup", 0)?;
8458 if task_mask.is_none() {
8459 self.o1_begin();
8460 }
8461 let mut hidden = vec![0.0f32; self.hidden_size];
8462 let mut pos = 0usize;
8463 if let Some(b) = &mut self.dsv41 {
8464 let pool = self.pool.clone();
8465 let mut logits = Vec::new();
8466 crate::dsv41::forward_chunk(
8467 &b.0,
8468 &b.1,
8469 &b.2,
8470 &mut b.3,
8471 ids,
8472 0,
8473 pool.as_deref(),
8474 &mut logits,
8475 );
8476 if let Err(err) = self.o1_seal_checked() {
8477 self.clear_sequence_state();
8478 return Err(err);
8479 }
8480 return Ok(logits);
8481 }
8482 #[cfg(not(target_os = "macos"))]
8493 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8494 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8495 Ok(r) => r,
8496 Err(e) => {
8497 self.clear_sequence_state();
8498 return Err(e);
8499 }
8500 };
8501 if n > 0 {
8502 hidden.copy_from_slice(&last);
8503 pos = n;
8504 }
8505 }
8506 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8507 let chunk = self.prefill_chunk();
8511 let hs = self.hidden_size;
8512 while pos < ids.len() {
8513 let end = (pos + chunk).min(ids.len());
8514 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8515 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8516 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8517 pos = end;
8518 }
8519 }
8520 if task_mask.is_none()
8529 && !self.graph_prefill_preferred()
8530 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8531 && self.pair_supported()
8532 {
8533 while pos + 1 < ids.len() {
8534 let e1 = self.embed_single(ids[pos]);
8535 let e2 = self.embed_single(ids[pos + 1]);
8536 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8537 self.check_forward_graph("forward_ids pair", pos + 1)?;
8538 self.commit_linear_scratch();
8539 hidden = h2;
8540 pos += 2;
8541 }
8542 }
8543 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8546 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8547 self.graph_logits = Some(lg);
8548 hidden = vec![0.0; self.hidden_size];
8549 pos = ids.len();
8550 }
8551 }
8552 while pos < ids.len() {
8553 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8554 self.check_forward_graph("forward_ids", pos)?;
8555 pos += 1;
8556 }
8557 if let Some(logits) = self.graph_logits.take() {
8558 if let Err(err) = self.o1_seal_checked() {
8562 self.clear_sequence_state();
8563 return Err(err);
8564 }
8565 return Ok(logits);
8566 }
8567 if let Err(err) = self.o1_seal_checked() {
8571 self.clear_sequence_state();
8572 return Err(err);
8573 }
8574 let normed = inference::rms_norm(
8575 &hidden,
8576 &self.weights.final_norm,
8577 self.rms_eps,
8578 self.norm_style,
8579 );
8580 Ok(self.lm_head_forward(&normed))
8581 }
8582
8583 #[doc(hidden)]
8587 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8588 #[cfg(target_os = "macos")]
8589 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8590 if ids.is_empty() {
8591 return Err("empty id sequence".to_string());
8592 }
8593 self.clear_sequence_state();
8594 self.dsv41
8595 .as_ref()
8596 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8597 self.o1_begin();
8598 let rows = {
8599 let pool = self.pool.clone();
8600 let b = self
8601 .dsv41
8602 .as_mut()
8603 .expect("dsv41 checked above; state cannot change during forward");
8604 let mut rows = Vec::with_capacity(ids.len());
8605 for (position, &id) in ids.iter().enumerate() {
8606 let mut logits = Vec::new();
8607 crate::dsv41::forward_token(
8608 &b.0,
8609 &b.1,
8610 &b.2,
8611 &mut b.3,
8612 id,
8613 position,
8614 pool.as_deref(),
8615 &mut logits,
8616 );
8617 rows.push(logits);
8618 }
8619 rows
8620 };
8621 self.o1_seal();
8622 Ok(rows)
8623 }
8624
8625 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8632 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8633 Ok((nll / cnt.max(1) as f64).exp())
8634 }
8635
8636 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8641 self.clear_sequence_state();
8642 FFN_PROBE.with(|p| {
8643 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8644 });
8645 crate::gpu::cpu_scope(|| {
8646 for (pos, &id) in ids.iter().enumerate() {
8647 let emb = self.embed_single(id);
8648 let _ = self.forward_layers(&emb, pos, None);
8649 }
8650 });
8651 self.clear_sequence_state();
8652 FFN_PROBE
8653 .with(|p| p.borrow_mut().take())
8654 .unwrap_or_default()
8655 }
8656
8657 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8661 if let Err(err) = self.nll_begin() {
8662 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8666 self.nll_end();
8667 return Err(err);
8668 }
8669 FFN_PROBE.with(|p| {
8670 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8671 });
8672 let result: Result<(), String> = (|| {
8673 for chunk in ids.chunks(256) {
8674 if chunk.len() < 2 {
8675 continue;
8676 }
8677 self.nll_ids_masked(chunk, 0, None)?;
8678 }
8679 Ok(())
8680 })();
8681 self.nll_end();
8682 let probe = FFN_PROBE
8683 .with(|p| p.borrow_mut().take())
8684 .unwrap_or_default();
8685 match result {
8686 Ok(()) => Ok(probe),
8687 Err(err) => {
8688 drop(probe);
8689 Err(err)
8690 }
8691 }
8692 }
8693
8694 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8698 self.nll_begin()?;
8699 let result: Result<f64, String> = (|| {
8700 let mut nll = 0f64;
8701 let mut cnt = 0usize;
8702 let mut hidden = vec![0f32; self.hidden_size];
8703 for (pos, &id) in ids.iter().enumerate() {
8704 if pos > 0 {
8705 inference::rms_norm_into(
8706 &hidden,
8707 &self.weights.final_norm,
8708 self.rms_eps,
8709 self.norm_style,
8710 &mut self.ws.n1,
8711 );
8712 let mut logits = self.lm_head_forward(&self.ws.n1);
8713 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8714 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8715 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8716 nll -= p.max(1e-300).ln();
8717 cnt += 1;
8718 attention::recycle_buf(&mut logits);
8719 }
8720 let emb = self.embed_single(id);
8721 hidden = self.forward_layers(&emb, pos, Some(mask));
8722 self.nll_check_graph("masked serial forward", pos)?;
8723 let _ = self.graph_logits.take();
8727 }
8728 Ok((nll / cnt.max(1) as f64).exp())
8729 })();
8730 self.nll_end();
8731 result
8732 }
8733
8734 pub fn nll_ids_masked(
8753 &mut self,
8754 ids: &[u32],
8755 start: usize,
8756 task_mask: Option<&TaskMask>,
8757 ) -> Result<(f64, usize), String> {
8758 let task_mask = self.drop_open_mask(task_mask);
8759 self.nll_ids_inner(ids, start, task_mask)
8760 }
8761
8762 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
8763 self.nll_ids_inner(ids, start, None)
8764 }
8765
8766 fn nll_ids_inner(
8767 &mut self,
8768 ids: &[u32],
8769 start: usize,
8770 task_mask: Option<&TaskMask>,
8771 ) -> Result<(f64, usize), String> {
8772 self.nll_begin()?;
8773 let result: Result<(f64, usize), String> = (|| {
8774 let mut nll = 0f64;
8775 let mut cnt = 0usize;
8776 let (graph_quality, fused_head_quality) = nll_graph_policy(
8789 task_mask.is_none(),
8790 self.graph_prefill_preferred(),
8791 crate::gpu::q1_force(),
8792 );
8793 self.graph_head_required = fused_head_quality;
8794 self.graph_want_logits = fused_head_quality;
8795 #[cfg(target_os = "macos")]
8796 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
8797 match self.nll_batch_metal(ids, start) {
8798 MetalBatchNllOutcome::Completed(nll, count) => {
8799 return Ok((nll, count));
8800 }
8801 MetalBatchNllOutcome::Declined => {}
8802 MetalBatchNllOutcome::Failed(err) => return Err(err),
8803 }
8804 }
8805 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
8810 if self.can_prefill_batched() && !graph_quality && !force_serial {
8811 const CHUNK: usize = 128;
8817 const LM_SUB: usize = 32;
8818 let n = ids.len().saturating_sub(1);
8819 let hs = self.hidden_size;
8820 let rows = self.weights.lm_head.rows();
8821 let mut pos = 0usize;
8822 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
8823 while pos < n {
8824 let end = (pos + CHUNK).min(n);
8825 let bsz = end - pos;
8826 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8827 self.nll_check_graph("batched prefill", pos)?;
8828 if state_trace && end % 256 == 0 {
8829 self.trace_recurrent_state(end);
8830 }
8831 let mut k0 = 0usize;
8832 while k0 < bsz {
8833 let k1 = (k0 + LM_SUB).min(bsz);
8834 let sb = k1 - k0;
8835 if pos + k1 <= start {
8838 k0 = k1;
8839 continue;
8840 }
8841 let mut normed = vec![0.0f32; sb * hs];
8842 for k in 0..sb {
8843 let r = inference::rms_norm(
8844 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
8845 &self.weights.final_norm,
8846 self.rms_eps,
8847 self.norm_style,
8848 );
8849 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
8850 }
8851 let mut logits = vec![0.0f32; sb * rows];
8852 self.weights
8853 .lm_head
8854 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
8855 for k in 0..sb {
8856 if pos + k0 + k < start {
8857 continue;
8858 }
8859 self.nll_check_graph("batched score row", pos + k0 + k)?;
8860 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
8861 if let Some(mu) = self.logit_multiplier {
8862 for v in lg.iter_mut() {
8863 *v *= mu;
8864 }
8865 }
8866 if let Some(c) = self.final_softcap {
8870 softcap_in_place(lg, c, self.pool.as_deref());
8871 }
8872 if let Some(cm) = self.head_clusters.clone() {
8875 self.hierarchical_head_logprobs(
8876 &normed[k * hs..(k + 1) * hs],
8877 &cm,
8878 lg,
8879 );
8880 }
8881 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
8882 let target = ids[pos + k0 + k + 1] as usize;
8883 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8884 let lse: f64 = lg
8885 .iter()
8886 .map(|&v| ((v - max) as f64).exp())
8887 .sum::<f64>()
8888 .ln()
8889 + max as f64;
8890 nll += lse - lg[target] as f64;
8891 cnt += 1;
8892 if std::env::var("CMF_PPL_TRACE").is_ok() {
8893 let top = lg
8894 .iter()
8895 .enumerate()
8896 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8897 .map(|(i, _)| i)
8898 .unwrap_or(0);
8899 eprintln!(
8900 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
8901 pos + k0 + k,
8902 target,
8903 lse - lg[target] as f64,
8904 top,
8905 lg[target],
8906 lg[top]
8907 );
8908 }
8909 }
8910 k0 = k1;
8911 }
8912 pos = end;
8913 }
8914 return Ok((nll, cnt));
8915 }
8916 for pos in 0..ids.len().saturating_sub(1) {
8917 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8918 self.nll_check_graph("serial forward", pos)?;
8919 let out_of_band = self.graph_logits.take();
8927 if self.graph_head_required && out_of_band.is_none() {
8928 METAL_GRAPH_HEAD_MISS.fetch_add(
8929 1,
8930 std::sync::atomic::Ordering::Relaxed,
8931 );
8932 return Err(format!(
8933 "fused Metal graph head did not complete at NLL position {pos}"
8934 ));
8935 }
8936 if pos < start {
8937 continue;
8938 }
8939 let logits = match out_of_band {
8940 Some(lg) => lg,
8941 None => {
8942 let normed = inference::rms_norm(
8943 &hidden,
8944 &self.weights.final_norm,
8945 self.rms_eps,
8946 self.norm_style,
8947 );
8948 self.lm_head_forward(&normed)
8952 }
8953 };
8954 let target = ids[pos + 1] as usize;
8955 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8956 let lse: f64 = logits
8957 .iter()
8958 .map(|&v| ((v - max) as f64).exp())
8959 .sum::<f64>()
8960 .ln()
8961 + max as f64;
8962 let tok_nll = lse - logits[target] as f64;
8963 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8964 let top = logits
8965 .iter()
8966 .enumerate()
8967 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8968 .map(|(i, _)| i)
8969 .unwrap_or(0);
8970 eprintln!(
8971 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8972 logits[target], logits[top]
8973 );
8974 }
8975 nll += tok_nll;
8976 cnt += 1;
8977 }
8978 Ok((nll, cnt))
8979 })();
8980 self.nll_end();
8981 result
8982 }
8983
8984 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
8989 let normed = inference::rms_norm(
8990 hidden,
8991 &self.weights.final_norm,
8992 self.rms_eps,
8993 self.norm_style,
8994 );
8995 let mut logits = self.lm_head_forward(&normed);
8998 let target = target as usize;
8999 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9000 let lse: f64 = logits
9001 .iter()
9002 .map(|&v| ((v - max) as f64).exp())
9003 .sum::<f64>()
9004 .ln()
9005 + max as f64;
9006 let tok_nll = lse - logits[target] as f64;
9007 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9008 let top = logits
9009 .iter()
9010 .enumerate()
9011 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9012 .map(|(i, _)| i)
9013 .unwrap_or(0);
9014 eprintln!(
9015 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9016 logits[target], logits[top]
9017 );
9018 }
9019 attention::recycle_buf(&mut logits);
9020 tok_nll
9021 }
9022
9023 fn trace_recurrent_state(&self, pos: usize) {
9031 let stats = |v: &[f32]| -> (f64, f64) {
9032 if v.is_empty() {
9033 return (0.0, 0.0);
9034 }
9035 let (mut ss, mut mx) = (0f64, 0f64);
9036 for &x in v {
9037 ss += (x as f64) * (x as f64);
9038 mx = mx.max((x as f64).abs());
9039 }
9040 ((ss / v.len() as f64).sqrt(), mx)
9041 };
9042 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9043 let lw = &self.weights.layers[self.phys_layer(li)];
9044 let (kind, s_len) = match &lw.attn {
9045 AttnKind::Linear(_) => (
9046 "vmf",
9047 self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0),
9048 ),
9049 AttnKind::LinearGdn(_) => (
9050 "gdn",
9051 self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0),
9052 ),
9053 AttnKind::Bounded(_) => ("bounded", 0),
9054 AttnKind::Full { .. } => ("full", 0),
9055 _ => ("other", 0),
9056 };
9057 let (rms, max) = stats(&l.linear_state);
9058 let s_part = if kind == "vmf" {
9059 &l.linear_state[..s_len.min(l.linear_state.len())]
9060 } else if kind == "gdn" {
9061 let ring = l.linear_state.len().saturating_sub(s_len).min(l.linear_state.len());
9062 &l.linear_state[ring..]
9063 } else {
9064 &l.linear_state[..0]
9065 };
9066 let (s_rms, s_max) = stats(s_part);
9067 let (ring_rms, ring_len) = match &l.bounded {
9068 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9069 None => (0.0, 0),
9070 };
9071 eprintln!(
9072 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9073 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9074 l.linear_state.len(),
9075 l.seq_len
9076 );
9077 }
9078 }
9079
9080 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9098 self.nll_begin()?;
9103 let requested_prefix = (prefill > 0).then_some(prefill);
9104 self.o1_begin_with_prefix(requested_prefix);
9105 let n = ids.len().saturating_sub(1);
9106 let requested_start = prefill.min(n);
9107 let exact_end = if self.o1_active() {
9112 match requested_prefix {
9113 Some(requested) => self.o1_effective_boundary(requested),
9114 None => self
9115 .o1_cfg
9116 .as_ref()
9117 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9118 }
9119 .unwrap_or(requested_start)
9120 .min(n)
9121 } else {
9122 requested_start
9123 };
9124 let mut nll = 0f64;
9125 let mut cnt = 0usize;
9126
9127 let mut pos = 0usize;
9131 if self.can_prefill_batched() {
9132 const CHUNK: usize = 128;
9133 while pos < exact_end {
9134 let end = (pos + CHUNK).min(exact_end);
9135 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9136 if self
9137 .graph_failed
9138 .swap(false, std::sync::atomic::Ordering::Relaxed)
9139 {
9140 self.cancel
9141 .store(false, std::sync::atomic::Ordering::Relaxed);
9142 self.nll_end();
9143 return Err("GPU graph failed during O(1) NLL prefix".into());
9144 }
9145 for row in 0..end - pos {
9146 let score_pos = pos + row;
9147 if score_pos >= requested_start && score_pos < n {
9148 nll += self.nll_from_hidden(
9149 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9150 ids[score_pos + 1],
9151 score_pos,
9152 );
9153 cnt += 1;
9154 }
9155 }
9156 pos = end;
9157 }
9158 } else {
9159 while pos < exact_end {
9160 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9161 if self
9162 .graph_failed
9163 .swap(false, std::sync::atomic::Ordering::Relaxed)
9164 {
9165 self.cancel
9166 .store(false, std::sync::atomic::Ordering::Relaxed);
9167 self.nll_end();
9168 return Err("GPU graph failed during O(1) NLL prefix".into());
9169 }
9170 if pos >= requested_start && pos < n {
9171 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9172 cnt += 1;
9173 }
9174 pos += 1;
9175 }
9176 }
9177 self.o1_seal_checked().map_err(|err| {
9178 self.nll_end();
9179 err
9180 })?;
9181
9182 let batch_k = std::env::var("CMF_BATCH_K")
9191 .ok()
9192 .and_then(|v| v.parse::<usize>().ok())
9193 .unwrap_or(0);
9194 let batch_admitted = batch_k > 0
9195 && self.can_prefill_batched()
9196 && self.o1_active()
9197 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9198 && (0..self.num_layers).all(|li| {
9199 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9200 cache.o1.is_none() || cache.o1_views().is_some()
9201 });
9202 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9203 eprintln!(
9204 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9205 batch_admitted,
9206 batch_k,
9207 n.saturating_sub(exact_end),
9208 );
9209 }
9210 let mut batch_completed = false;
9211 if batch_admitted && exact_end < n {
9212 let hs = self.hidden_size;
9213 let mut batch_pos = exact_end;
9214 while batch_pos < n {
9215 let end = (batch_pos + batch_k).min(n);
9216 let bk = end - batch_pos;
9217 let mut hiddens = vec![0.0f32; bk * hs];
9218 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
9219 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
9220 }
9221 let positions: Vec<usize> = (batch_pos..end).collect();
9222 let t_batch = std::time::Instant::now();
9223 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
9224 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9225 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
9226 eprintln!(
9227 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
9228 batch_pos,
9229 end.saturating_sub(1),
9230 bk as f64 / (ms / 1000.0),
9231 );
9232 }
9233 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
9234 self.nll_end();
9235 return Err(err);
9236 }
9237 match outcome {
9238 crate::gpu::BatchGraphOutcome::Completed => {
9239 batch_completed = true;
9240 for row in 0..bk {
9241 nll += self.nll_from_hidden(
9242 &hiddens[row * hs..(row + 1) * hs],
9243 ids[batch_pos + row + 1],
9244 batch_pos + row,
9245 );
9246 cnt += 1;
9247 }
9248 batch_pos = end;
9249 }
9250 crate::gpu::BatchGraphOutcome::Declined => {
9251 if batch_completed {
9252 self.nll_end();
9253 return Err(format!(
9254 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9255 ));
9256 }
9257 break;
9258 }
9259 crate::gpu::BatchGraphOutcome::Failed => {
9260 self.nll_end();
9261 return Err(format!(
9262 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9263 ));
9264 }
9265 }
9266 }
9267 if batch_completed && cnt == n.saturating_sub(requested_start) {
9268 self.nll_end();
9269 return Ok((nll, cnt));
9270 }
9271 }
9272
9273 for pos in exact_end..n {
9278 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9279 if self
9280 .graph_failed
9281 .swap(false, std::sync::atomic::Ordering::Relaxed)
9282 {
9283 self.cancel
9284 .store(false, std::sync::atomic::Ordering::Relaxed);
9285 self.nll_end();
9286 return Err(format!(
9287 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9288 ));
9289 }
9290 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9291 cnt += 1;
9292 }
9293 self.nll_end();
9294 Ok((nll, cnt))
9295 }
9296
9297 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9305 self.clear_sequence_state();
9306 let n = ids.len().saturating_sub(1);
9307 let mut correct = Vec::with_capacity(n);
9308 let mut pmax = Vec::with_capacity(n);
9309 for pos in 0..n {
9310 let emb = self.embed_single(ids[pos]);
9311 let hidden = self.forward_layers(&emb, pos, None);
9312 let logits = if let Some(logits) = self.graph_logits.take() {
9313 logits
9314 } else {
9315 let normed = inference::rms_norm(
9316 &hidden,
9317 &self.weights.final_norm,
9318 self.rms_eps,
9319 self.norm_style,
9320 );
9321 self.lm_head_forward(&normed)
9325 };
9326 let target = ids[pos + 1] as usize;
9327 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9328 for (i, &v) in logits.iter().enumerate() {
9329 if v > mval {
9330 mval = v;
9331 amax = i;
9332 }
9333 }
9334 correct.push(amax == target);
9335 let row: Vec<f32> = temps
9336 .iter()
9337 .map(|&t| {
9338 let tt = t.max(1e-3);
9339 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9340 1.0 / s.max(1e-12) })
9342 .collect();
9343 pmax.push(row);
9344 }
9345 self.clear_sequence_state();
9346 (correct, pmax)
9347 }
9348
9349 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9356 if self.dyn_router.is_none() {
9357 return Ok((self.ppl_ids(ids)?, 0));
9358 }
9359 self.nll_begin()?;
9360 let saved_active = self.dyn_active;
9361 let mut router = self
9362 .dyn_router
9363 .take()
9364 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9365 router.reset();
9366 self.dyn_phi_seen = 0;
9367 let _ = self.set_active_skill(None);
9368
9369 let result: Result<(f64, usize), String> = (|| {
9370 let mut nll = 0f64;
9371 let mut cnt = 0usize;
9372 for pos in 0..ids.len().saturating_sub(1) {
9373 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9374 self.nll_check_graph("dynamic serial forward", pos)?;
9375 let out_of_band = self.graph_logits.take();
9376 let mut logits = match out_of_band {
9377 Some(lg) => lg,
9378 None => {
9379 let normed = inference::rms_norm(
9380 &hidden,
9381 &self.weights.final_norm,
9382 self.rms_eps,
9383 self.norm_style,
9384 );
9385 self.lm_head_forward(&normed)
9389 }
9390 };
9391 let target = ids[pos + 1] as usize;
9392 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9393 let lse: f64 = logits
9394 .iter()
9395 .map(|&v| ((v - max) as f64).exp())
9396 .sum::<f64>()
9397 .ln()
9398 + max as f64;
9399 let tok_nll = lse - logits[target] as f64;
9400 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9401 let top = logits
9402 .iter()
9403 .enumerate()
9404 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9405 .map(|(i, _)| i)
9406 .unwrap_or(0);
9407 eprintln!(
9408 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9409 logits[target], logits[top]
9410 );
9411 }
9412 nll += tok_nll;
9413 cnt += 1;
9414 attention::recycle_buf(&mut logits);
9415 let phi = self.dyn_phi_ema.clone();
9417 if let Some(new_active) = router.step(&phi, pos) {
9418 let _ = self.set_active_skill(new_active);
9419 }
9420 }
9421 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9422 })();
9423
9424 let _ = self.set_active_skill(saved_active);
9427 self.dyn_router = Some(router);
9428 self.nll_end();
9429 result
9430 }
9431
9432 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9434 self.clear_sequence_state();
9435 let mut acc = vec![0f32; self.hidden_size];
9436 for (pos, &id) in ids.iter().enumerate() {
9437 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9438 for (a, v) in acc.iter_mut().zip(&h) {
9439 *a += v;
9440 }
9441 }
9442 let n = ids.len().max(1) as f32;
9443 for a in acc.iter_mut() {
9444 *a /= n;
9445 }
9446 self.clear_sequence_state();
9447 acc
9448 }
9449
9450 pub fn probe_phi_span(
9464 &mut self,
9465 ids: &[u32],
9466 layer: usize,
9467 span: std::ops::Range<usize>,
9468 ) -> Vec<f32> {
9469 #[cfg(target_os = "macos")]
9470 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9471 let end = span.end.min(ids.len());
9472 let start = span.start.min(end);
9473 let reset = |p: &mut Self| p.clear_sequence_state();
9474 reset(self);
9475 let mut acc = vec![0f32; self.hidden_size];
9476 for (pos, &id) in ids[..end].iter().enumerate() {
9477 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9478 if pos >= start {
9479 for (a, v) in acc.iter_mut().zip(&h) {
9480 *a += v;
9481 }
9482 }
9483 }
9484 let n = end - start;
9485 if n > 0 {
9486 let n = n as f32;
9487 for a in acc.iter_mut() {
9488 *a /= n;
9489 }
9490 }
9491 reset(self);
9492 acc
9493 }
9494
9495 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9502 #[cfg(target_os = "macos")]
9503 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9504 self.graph_logits = None;
9505 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9506 if let Some(logits) = self.graph_logits.take() {
9507 return logits;
9508 }
9509 inference::rms_norm_into(
9510 &hidden,
9511 &self.weights.final_norm,
9512 self.rms_eps,
9513 self.norm_style,
9514 &mut self.ws.n1,
9515 );
9516 self.lm_head_forward(&self.ws.n1)
9517 }
9518
9519 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9525 self.prefill_batch_masked(ids, start_pos, None)
9526 }
9527
9528 fn prefill_batch_masked(
9534 &mut self,
9535 ids: &[u32],
9536 start_pos: usize,
9537 task_mask: Option<&TaskMask>,
9538 ) -> Vec<f32> {
9539 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9540 }
9541
9542 fn prefill_rows(
9550 &mut self,
9551 ids: &[u32],
9552 pos: usize,
9553 task_mask: Option<&TaskMask>,
9554 ) -> Result<Vec<f32>, String> {
9555 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9556 }
9557
9558 fn prefill_input_rows(
9559 &mut self,
9560 input: PrefillIn<'_>,
9561 pos: usize,
9562 task_mask: Option<&TaskMask>,
9563 ) -> Result<Vec<f32>, String> {
9564 self.mimo_moe_prepare();
9565 let hs = self.hidden_size;
9566 let bk = match input {
9567 PrefillIn::Ids(ids) => ids.len(),
9568 PrefillIn::Hidden(rows) => rows.len() / hs,
9569 };
9570 #[cfg(not(target_os = "macos"))]
9571 if task_mask.is_none()
9572 && !self.o1_active()
9573 && bk > 1
9574 && (self.batch_prefix_prefill()
9575 || (self.verify_exact_moe
9576 && crate::gpu::enabled_here()
9577 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9578 {
9579 let mut hiddens = match input {
9580 PrefillIn::Hidden(rows) => rows.to_vec(),
9581 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9582 };
9583 let positions: Vec<usize> = (pos..pos + bk).collect();
9584 let mut run = 0usize;
9585 match self.try_batch_graph_wgpu_prefix(
9586 &mut hiddens,
9587 &positions,
9588 bk,
9589 None,
9590 Some(&mut run),
9591 ) {
9592 crate::gpu::BatchGraphOutcome::Completed => {
9593 let out = if run < self.num_layers {
9594 self.prefill_batch_span(
9595 PrefillIn::Hidden(&hiddens),
9596 pos,
9597 None,
9598 run,
9599 self.num_layers,
9600 )
9601 } else {
9602 hiddens
9603 };
9604 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9605 Err("MiMo attention graph failed after admission".into())
9606 } else { Ok(out) };
9607 }
9608 crate::gpu::BatchGraphOutcome::Failed => {
9609 return Err("batched prefix prefill failed after admission".into());
9610 }
9611 crate::gpu::BatchGraphOutcome::Declined => {
9612 #[cfg(feature = "gpu")]
9614 self.pull_lagging_host_kv(0, self.num_layers, pos);
9615 }
9616 }
9617 }
9618 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9619 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9620 Err("batch tail graph failed after admission".into())
9621 } else { Ok(out) }
9622 }
9623
9624 fn prefill_batch_span(
9630 &mut self,
9631 input: PrefillIn<'_>,
9632 start_pos: usize,
9633 task_mask: Option<&TaskMask>,
9634 from: usize,
9635 upto_excl: usize,
9636 ) -> Vec<f32> {
9637 let hs = self.hidden_size;
9638 let b = match input {
9639 PrefillIn::Ids(ids) => ids.len(),
9640 PrefillIn::Hidden(hb) => hb.len() / hs,
9641 };
9642 let upto_excl = upto_excl.min(self.num_layers);
9643 let mut h: Vec<f32>;
9647 let mut h_ready;
9648 match input {
9649 PrefillIn::Ids(_) => {
9650 h = vec![0.0; b * hs];
9651 h_ready = false;
9652 }
9653 PrefillIn::Hidden(hb) => {
9654 h = hb.to_vec();
9655 h_ready = true;
9656 }
9657 }
9658 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9659 if let PrefillIn::Ids(ids) = input {
9660 for (bi, &id) in ids.iter().enumerate() {
9661 let e = me.embed_single(id);
9662 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9663 }
9664 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9665 if let Ok(t) = tp.parse::<usize>() {
9666 if t >= start_pos && t < start_pos + ids.len() {
9667 let bi = t - start_pos;
9668 let row = &h[bi * hs..(bi + 1) * hs];
9669 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9670 eprintln!(
9671 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9672 ids[bi],
9673 row[0],
9674 row[1],
9675 ids.len(),
9676 &ids[..ids.len().min(8)]
9677 );
9678 }
9679 }
9680 }
9681 }
9682 };
9683 let (_nkv, _hd, _rd, eps) = (
9684 self.num_kv_heads,
9685 self.head_dim,
9686 self.rotary_dim,
9687 self.rms_eps,
9688 );
9689 let pool = self.pool.clone();
9690 let norm_style = self.norm_style;
9691 self.mimo_moe_prepare();
9692 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9693
9694 #[cfg(target_os = "macos")]
9695 let mut chunk_skip_until = 0usize;
9696 for li in from..upto_excl {
9697 let _capacity_tail = automatic_gpu_prefix
9698 .filter(|&prefix| {
9699 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9700 })
9701 .map(|_| crate::gpu::enter_cpu_scope());
9702 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9707 let _fast_gemm = self
9708 .proj_gate_sigmoid
9709 .then(crate::gpu::enter_prefill_fast_gemm);
9710 #[cfg(target_os = "macos")]
9716 if task_mask.is_none() {
9717 if li < chunk_skip_until {
9718 continue;
9719 }
9720 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9726 fill_h(&mut h, self);
9727 h_ready = true;
9728 }
9729 let ids_for_embed = match input {
9730 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9731 PrefillIn::Hidden(_) => None,
9732 };
9733 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9734 if end > li {
9735 h_ready = true;
9736 chunk_skip_until = end;
9737 if self.is_loop_end(end - 1) && end < self.num_layers {
9740 for bi in 0..b {
9741 let normed = inference::rms_norm(
9742 &h[bi * hs..(bi + 1) * hs],
9743 &self.weights.final_norm,
9744 eps,
9745 norm_style,
9746 );
9747 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9748 }
9749 }
9750 continue;
9751 }
9752 }
9753 if !h_ready {
9754 fill_h(&mut h, self);
9755 h_ready = true;
9756 }
9757 if task_mask.is_none() && self.verify_exact_moe {
9758 let positions: Vec<_> = (start_pos..start_pos + b).collect();
9759 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
9760 crate::gpu::BatchGraphOutcome::Completed => continue,
9761 crate::gpu::BatchGraphOutcome::Failed => return h,
9762 crate::gpu::BatchGraphOutcome::Declined => {},
9763 }
9764 }
9765 #[cfg(feature = "gpu")]
9766 self.pull_lagging_host_kv(li, li + 1, start_pos);
9767 let lw = &self.weights.layers[self.phys_layer(li)];
9768 let t_attn = std::time::Instant::now();
9769 match &lw.attn {
9771 AttnKind::Kda(w) => {
9772 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9774 let mut normed = vec![0.0f32; b * hs];
9775 for bi in 0..b {
9776 inference::rms_norm_into(
9777 &h[bi * hs..(bi + 1) * hs],
9778 &lw.input_norm,
9779 eps,
9780 norm_style,
9781 &mut normed[bi * hs..(bi + 1) * hs],
9782 );
9783 }
9784 let attn = crate::linear_core::kda_forward_batch(
9785 &normed,
9786 b,
9787 w,
9788 &cfg,
9789 &mut self.kv_cache.layers[li].linear_state,
9790 pool.as_deref(),
9791 );
9792 for (dst, &a) in h.iter_mut().zip(&attn) {
9793 *dst += a;
9794 }
9795 }
9796 AttnKind::LinearGdn(w) => {
9797 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9799 let mut normed = vec![0.0f32; b * hs];
9800 for bi in 0..b {
9801 let r = inference::rms_norm(
9802 &h[bi * hs..(bi + 1) * hs],
9803 &lw.input_norm,
9804 eps,
9805 norm_style,
9806 );
9807 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9808 }
9809 let attn = crate::linear_core::gdn_forward_batch(
9810 &normed,
9811 b,
9812 w,
9813 &cfg,
9814 &mut self.kv_cache.layers[li].linear_state,
9815 pool.as_deref(),
9816 );
9817 for (dst, &a) in h.iter_mut().zip(&attn) {
9818 *dst += a;
9819 }
9820 }
9821 AttnKind::ShortConv(w) => {
9822 let cfg = self
9825 .short_conv_cfg
9826 .expect("short-conv layer without short_conv_cfg");
9827 let mut normed = vec![0.0f32; b * hs];
9828 for bi in 0..b {
9829 inference::rms_norm_into(
9830 &h[bi * hs..(bi + 1) * hs],
9831 &lw.input_norm,
9832 eps,
9833 norm_style,
9834 &mut normed[bi * hs..(bi + 1) * hs],
9835 );
9836 }
9837 let attn = short_conv_forward_batch(
9838 &normed,
9839 b,
9840 w,
9841 &cfg,
9842 &mut self.kv_cache.layers[li].linear_state,
9843 pool.as_deref(),
9844 );
9845 for (dst, &a) in h.iter_mut().zip(&attn) {
9846 *dst += a;
9847 }
9848 }
9849 AttnKind::Mla(w) => {
9850 let inv_freq_l = self.layer_inv_freq(li);
9853 let rs = self.layer_rope_scale(li);
9854 let mut normed = vec![0.0f32; hs];
9855 for bi in 0..b {
9856 inference::rms_norm_into(
9857 &h[bi * hs..(bi + 1) * hs],
9858 &lw.input_norm,
9859 eps,
9860 norm_style,
9861 &mut normed,
9862 );
9863 let ao = mla_attention(
9864 w,
9865 &normed,
9866 &mut self.kv_cache.layers[li],
9867 start_pos + bi,
9868 &inv_freq_l,
9869 rs,
9870 eps,
9871 pool.as_deref(),
9872 );
9873 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
9874 *dst += a;
9875 }
9876 }
9877 }
9878 AttnKind::Full {
9879 wq,
9880 wk,
9881 wv,
9882 wo,
9883 q_norm,
9884 k_norm,
9885 output_gate,
9886 softplus_gate,
9887 bias,
9888 } => {
9889 let mut normed = vec![0.0f32; b * hs];
9893 for bi in 0..b {
9894 inference::rms_norm_into(
9895 &h[bi * hs..(bi + 1) * hs],
9896 &lw.input_norm,
9897 eps,
9898 norm_style,
9899 &mut normed[bi * hs..(bi + 1) * hs],
9900 );
9901 }
9902 let inv_freq_l = self.layer_inv_freq(li);
9903 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9904 let cfg = QwenAttnCfg {
9905 num_heads: self.layer_num_heads(li),
9906 num_kv_heads: nkv_l,
9907 head_dim: hd_l,
9908 hidden_size: hs,
9909 position: start_pos,
9910 inv_freq: &inv_freq_l,
9911 rotary_dim: rd_l,
9912 scale: self.attn_scale,
9913 softcap: self.attn_softcap,
9914 window: self.layer_window(li),
9915 v_norm: self.attn_v_norm,
9916 qk_norm_after_rope: self.qk_norm_after_rope,
9917 gate_sigmoid: self.proj_gate_sigmoid,
9918 q_norm: q_norm.as_deref(),
9919 k_norm: k_norm.as_deref(),
9920 output_gate: *output_gate,
9921 softplus_gate: softplus_gate
9922 .as_ref()
9923 .map(|(gate, per_head)| (gate, *per_head)),
9924 rope_scale: self.layer_rope_scale(li),
9925 bias: bias
9926 .as_ref()
9927 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9928 rms_eps: eps,
9929 norm_style,
9930 pool: pool.as_deref(),
9931 v_head_dim: self.layer_v_dim(li),
9932 };
9933 #[cfg(feature = "gpu")]
9934 let _mirror = self
9935 .prefill_mirror_target(li)
9936 .map(crate::gpu::enter_prefill_mirror);
9937 let mut attn = attention::qwen_attention_batch(
9938 &normed,
9939 b,
9940 wq,
9941 wk,
9942 wv,
9943 wo,
9944 &mut self.kv_cache.layers[li],
9945 &cfg,
9946 );
9947 if let Some(w) = &lw.attn_out_norm {
9948 for bi in 0..b {
9949 inference::rms_norm_into(
9950 &attn[bi * hs..(bi + 1) * hs],
9951 w,
9952 eps,
9953 norm_style,
9954 &mut normed[bi * hs..(bi + 1) * hs],
9955 );
9956 }
9957 attn.copy_from_slice(&normed);
9958 }
9959 for (dst, &a) in h.iter_mut().zip(&attn) {
9960 *dst += a;
9961 }
9962 }
9963 AttnKind::Bounded(w) => {
9964 let mut normed = vec![0.0f32; b * hs];
9967 for bi in 0..b {
9968 inference::rms_norm_into(
9969 &h[bi * hs..(bi + 1) * hs],
9970 &lw.input_norm,
9971 eps,
9972 norm_style,
9973 &mut normed[bi * hs..(bi + 1) * hs],
9974 );
9975 }
9976 let rope = self
9977 .bounded_rope
9978 .clone()
9979 .expect("bounded layer without an installed rotation table");
9980 let cfg = crate::bounded::BoundedAttnCfg {
9981 num_heads: self.num_heads,
9982 num_kv_heads: self.num_kv_heads,
9983 head_dim: self.head_dim,
9984 hidden_size: hs,
9985 scale: self.attn_scale,
9986 rope: &rope,
9987 pool: pool.as_deref(),
9988 };
9989 let mut attn = crate::bounded::bounded_attention_batch(
9990 &normed,
9991 b,
9992 w,
9993 &mut self.kv_cache.layers[li],
9994 &cfg,
9995 );
9996 if let Some(wn) = &lw.attn_out_norm {
9997 for bi in 0..b {
9998 inference::rms_norm_into(
9999 &attn[bi * hs..(bi + 1) * hs],
10000 wn,
10001 eps,
10002 norm_style,
10003 &mut normed[bi * hs..(bi + 1) * hs],
10004 );
10005 }
10006 attn.copy_from_slice(&normed);
10007 }
10008 for (dst, &a) in h.iter_mut().zip(&attn) {
10009 *dst += a;
10010 }
10011 attention::recycle_buf(&mut attn);
10012 }
10013 AttnKind::Linear(w) => {
10014 for bi in 0..b {
10015 let normed = inference::rms_norm(
10016 &h[bi * hs..(bi + 1) * hs],
10017 &lw.input_norm,
10018 eps,
10019 norm_style,
10020 );
10021 vmf_phase_forward(
10022 &normed,
10023 w,
10024 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10025 &mut self.kv_cache.layers[li].linear_state,
10026 pool.as_deref(),
10027 )
10028 .iter()
10029 .enumerate()
10030 .for_each(|(i, &a)| h[bi * hs + i] += a);
10031 }
10032 }
10033 }
10034
10035 let lw = &self.weights.layers[self.phys_layer(li)];
10037 let mut post = vec![0.0f32; b * hs];
10038 for bi in 0..b {
10039 let r =
10040 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10041 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10042 }
10043 let mask_row = task_mask
10046 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10047 .and_then(|m| m.ffn_masks.get(li))
10048 .map(|v| v.as_slice());
10049 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10050 let t_ffn = std::time::Instant::now();
10051 let mut ffn = match &lw.ffn {
10052 FfnKind::Dense(d) if !d.segs.is_empty() => {
10053 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10054 }
10055 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10056 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10057 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10058 }
10059 FfnKind::Moe(m) if self.verify_exact_moe => {
10060 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10061 }
10062 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10065 let before = m.stats.borrow().clone();
10066 let out = crate::gpu::cpu_scope(|| {
10067 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10068 });
10069 self.mimo_moe.prime(li, m, &before);
10070 out
10071 }
10072 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10073 FfnKind::DenseMoe(dm) => {
10076 let mut out = vec![0.0f32; b * hs];
10077 for bi in 0..b {
10078 let r = dense_moe_ffn(
10079 dm,
10080 &post[bi * hs..(bi + 1) * hs],
10081 &h[bi * hs..(bi + 1) * hs],
10082 eps,
10083 norm_style,
10084 pool.as_deref(),
10085 );
10086 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10087 }
10088 out
10089 }
10090 };
10091 if prefill_prof_on() {
10092 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10093 PREFILL_SPLIT[1].fetch_add(t_ffn.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
10094 }
10095 if let Some(w) = &lw.ffn_out_norm {
10096 for bi in 0..b {
10097 inference::rms_norm_into(
10098 &ffn[bi * hs..(bi + 1) * hs],
10099 w,
10100 eps,
10101 norm_style,
10102 &mut post[bi * hs..(bi + 1) * hs],
10103 );
10104 }
10105 ffn.copy_from_slice(&post);
10106 }
10107 for (dst, &f) in h.iter_mut().zip(&ffn) {
10108 *dst += f;
10109 }
10110 if let Some(sc) = lw.layer_scale {
10111 for v in h.iter_mut() {
10112 *v *= sc;
10113 }
10114 }
10115 if self.layer_dump.is_some() {
10117 for bi in 0..b {
10118 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10119 }
10120 }
10121 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10122 if let Ok(t) = tp.parse::<usize>() {
10123 if t >= start_pos && t < start_pos + b {
10124 let bi = t - start_pos;
10125 let row = &h[bi * hs..(bi + 1) * hs];
10126 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10127 eprintln!(
10128 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10129 row[0], row[1]
10130 );
10131 }
10132 }
10133 }
10134 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10138 let row = &h[(b - 1) * hs..b * hs];
10139 let rms =
10140 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10141 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10142 eprintln!(
10143 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10144 match &self.weights.layers[self.phys_layer(li)].attn {
10145 AttnKind::LinearGdn(_) => "gdn",
10146 AttnKind::Linear(_) => "vmf",
10147 AttnKind::ShortConv(_) => "conv",
10148 _ => "attn",
10149 },
10150 match &lw.ffn {
10151 FfnKind::Moe(_) => "moe",
10152 FfnKind::Dense(_) => "dense",
10153 FfnKind::DenseMoe(_) => "dense+moe",
10154 },
10155 );
10156 }
10157 if self.is_loop_end(li) && li + 1 < self.num_layers {
10159 for bi in 0..b {
10160 let normed = inference::rms_norm(
10161 &h[bi * hs..(bi + 1) * hs],
10162 &self.weights.final_norm,
10163 eps,
10164 norm_style,
10165 );
10166 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10167 }
10168 }
10169 if std::env::var("CMF_TRACE_H").is_ok() {
10170 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10171 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10172 eprintln!(
10173 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10174 lw.layer_scale
10175 );
10176 }
10177 }
10178 crate::gpu::set_layer(-1); if prefill_prof_on() {
10180 let ms = |a: &std::sync::atomic::AtomicU64| {
10181 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10182 };
10183 let sp = &attention::ATTN_SPLIT;
10184 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10185 eprintln!(
10186 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10187 host gate {:.1}], host loop {:.1}, attend {:.1} \
10188 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10189 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10190 ms(&PREFILL_SPLIT[0]),
10191 ms(&sp[0]),
10192 qkv_enq,
10193 qkv_wait,
10194 ms(&sp[6]),
10195 ms(&sp[1]),
10196 ms(&sp[2]),
10197 ms(&sp[4]),
10198 ms(&sp[5]),
10199 ca_wait,
10200 ms(&sp[3]),
10201 ms(&PREFILL_SPLIT[1]),
10202 );
10203 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10204 eprintln!(
10205 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10206 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10207 fallbacks (cumulative)",
10208 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10209 );
10210 }
10211 self.o1_progress();
10216 self.swa_trim_tails();
10218 h
10219 }
10220
10221 fn embed_single(&self, id: u32) -> Vec<f32> {
10223 let mut out = vec![0.0f32; self.hidden_size];
10224 if (id as usize) < self.weights.embed_tokens.rows() {
10225 self.weights.embed_tokens.row_f32(id as usize, &mut out);
10226 }
10227 if self.embed_multiplier != 1.0 {
10228 for v in out.iter_mut() {
10229 *v *= self.embed_multiplier;
10230 }
10231 }
10232 if self.dsv4.is_some()
10236 || self.dsv41.is_some()
10237 || self.qwen4_exp.is_some()
10238 {
10239 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10240 v[0] = id as f32;
10241 return v;
10242 }
10243 if let Some(b) = &self.g3n {
10246 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10247 }
10248 out
10249 }
10250
10251 #[cfg(target_os = "macos")]
10257 fn chunk_run_gpu(
10258 &mut self,
10259 li0: usize,
10260 h: &mut [f32],
10261 b: usize,
10262 pos0: usize,
10263 embed_ids: Option<&[u32]>,
10264 cap: usize,
10265 ) -> usize {
10266 let gemma4 = self.metal_gemma4()
10273 && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10274 if !crate::gpu::enabled_here()
10275 || std::env::var("CMF_GPU_CHUNK")
10276 .map(|v| v == "0")
10277 .unwrap_or(false)
10278 || b < 32
10279 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10282 || (self.global_attn.is_some() && !gemma4)
10283 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10285 || self.o1_active()
10288 || (self.attn_v_norm && !gemma4)
10289 || !self.metal_rope_scale_ok()
10290 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10291 {
10292 return li0;
10293 }
10294 let Some(model) = self.model.clone() else {
10295 return li0;
10296 };
10297 let (nh, nkv, hd, hs) = (
10298 self.num_heads,
10299 self.num_kv_heads,
10300 self.head_dim,
10301 self.hidden_size,
10302 );
10303 let loop_end = if self.loop_final_norm {
10307 ((li0 / self.physical_layers) + 1) * self.physical_layers
10308 } else {
10309 self.num_layers
10310 };
10311 let run_end = self.num_layers.min(loop_end).min(cap);
10312 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10317 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10318 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10319 let mut stored_at: Vec<usize> = Vec::new();
10320 for li in li0..run_end {
10321 let lw = &self.weights.layers[self.phys_layer(li)];
10322 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10323 && !gemma4
10324 {
10325 break;
10326 }
10327 let AttnKind::Full {
10328 wq,
10329 wk,
10330 wv,
10331 wo,
10332 q_norm,
10333 k_norm,
10334 output_gate: false,
10335 softplus_gate,
10336 bias,
10337 } = &lw.attn
10338 else {
10339 break;
10340 };
10341 let head_gate = match softplus_gate {
10343 None => None,
10344 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10345 Some((d, r, c)) if r == nh && c == hs => Some(d),
10346 _ => break,
10347 },
10348 Some(_) => break,
10349 };
10350 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10354 FfnKind::Dense(d)
10355 if (matches!(d.act, Act::Silu | Act::Gelu)
10356 || (gemma4 && d.act == Act::GeluTanh))
10357 && d.segs.is_empty() =>
10358 {
10359 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10360 }
10361 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10362 Some((router, experts, q8)) => {
10363 let e0 = &m.experts[0];
10364 let route = Box::new(move |lg: &[f32]| {
10365 let (idx, p, wsum) = moe_route(lg, m, None);
10367 let mut st = m.stats.borrow_mut();
10368 if st.len() < m.experts.len() {
10369 st.resize(m.experts.len(), 0);
10370 }
10371 for &e in &idx {
10372 st[e] += 1;
10373 }
10374 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10375 (idx, w)
10376 });
10377 (
10378 &e0.gate_proj,
10379 &e0.up_proj,
10380 &e0.down_proj,
10381 Act::Silu,
10382 Some(crate::gpu_metal::ChunkMoe {
10383 router,
10384 experts,
10385 q8,
10386 route,
10387 }),
10388 )
10389 }
10390 None => break,
10391 },
10392 _ => break,
10393 };
10394 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10400 t.q8_row_parts()
10401 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10402 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10403 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10404 }
10405 fn cf(t: &QTensor) -> &[f32] {
10406 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10407 }
10408 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10409 if moe.is_none()
10418 && !gemma4
10419 && (col.iter().any(|c| !c.is_empty())
10420 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10421 {
10422 break;
10423 }
10424 let parts = (
10425 cw(wq),
10426 cw(wk),
10427 cw(wv),
10428 cw(wo),
10429 cw(gate_t),
10430 cw(up_t),
10431 cw(down_t),
10432 );
10433 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10434 else {
10435 break;
10436 };
10437 let layer = &self.kv_cache.layers[li];
10438 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10439 break;
10440 }
10441 debug_assert!(
10443 layer.base() == 0
10444 || self
10445 .layer_window(li)
10446 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10447 );
10448 stored_at.push(layer.head_len(0));
10449 let (nkv, hd) = if gemma4 {
10452 let (a, bb, _) = self.layer_geom(li);
10453 (a, bb)
10454 } else {
10455 (nkv, hd)
10456 };
10457 layers.push(crate::gpu_metal::ChunkLayer {
10458 model: &model,
10459 kv_id: self.graph_kv_id,
10460 layer: li,
10461 wq: pq,
10462 wk: pk,
10463 wv: pv,
10464 wo: po,
10465 gate: pg,
10466 up: pu,
10467 down: pd,
10468 input_norm: &lw.input_norm,
10469 post_norm: &lw.post_norm,
10470 bias: bias
10471 .as_ref()
10472 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10473 q_norm: q_norm.as_deref(),
10474 k_norm: k_norm.as_deref(),
10475 inv_freq: &tables[li - li0],
10476 rope_scale: self.layer_rope_scale(li),
10477 rd: self.layer_geom(li).2,
10478 nh,
10479 nkv,
10480 hd,
10481 hs,
10482 inter: gate_t.rows(),
10483 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10484 late_qk_norm: self.qk_norm_after_rope,
10485 eps: self.rms_eps as f32,
10486 window: self.layer_window(li),
10487 head_gate,
10488 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10489 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10490 v_norm: self.attn_v_norm,
10491 scale: self.attn_scale,
10492 attn_out_norm: lw.attn_out_norm.as_deref(),
10493 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10494 layer_scale: lw.layer_scale,
10495 }),
10496 col,
10497 moe,
10498 });
10499 }
10500 if layers.is_empty() {
10501 return li0;
10502 }
10503 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10505 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10506 .iter()
10507 .zip(&rows_kv)
10508 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10509 .collect();
10510 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10511 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10512 let li = layers[i].layer;
10513 let layer = &self.kv_cache.layers[li];
10514 let nkv = rows_kv[i].0;
10515 io.push(crate::gpu_metal::ChunkIo {
10516 cpu_stored: stored_at[i],
10517 cpu_gen: layer.generation(),
10518 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10519 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10520 out_k: ok,
10521 out_v: ov,
10522 imp: oi,
10523 });
10524 }
10525 let n_run = layers.len();
10526 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10527 let ep = embed_ids.and_then(|ids| {
10530 self.weights
10531 .embed_tokens
10532 .q8_row_parts()
10533 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10534 idx,
10535 rows,
10536 row_scale: rs,
10537 ids,
10538 mult: self.embed_multiplier,
10539 })
10540 });
10541 if embed_ids.is_some() && ep.is_none() {
10542 return li0;
10543 }
10544 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10545 return li0;
10546 }
10547 drop(io);
10548 drop(layers);
10549 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10552 let li = li0 + i;
10553 let row = rows_kv[i].1;
10554 let layer = &mut self.kv_cache.layers[li];
10555 for bi in 0..b {
10556 layer.append(
10557 &ok[bi * row..(bi + 1) * row],
10558 &ov[bi * row..(bi + 1) * row],
10559 &[],
10560 );
10561 }
10562 layer.accumulate_imp(oi);
10563 }
10564 last
10565 }
10566
10567 fn layer_is_local(&self, li: usize) -> bool {
10570 if let Some(layers) = &self.sliding_layers {
10571 return layers.get(li).copied().unwrap_or(false);
10572 }
10573 match self.swa {
10574 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10575 None => false,
10576 }
10577 }
10578
10579 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10582 if self.layer_is_local(li) {
10583 if let Some(f) = &self.inv_freq_local {
10584 return f.clone();
10585 }
10586 } else if let Some(f) = &self.inv_freq_global {
10587 return f.clone();
10588 }
10589 self.inv_freq.clone()
10590 }
10591
10592 fn layer_window(&self, li: usize) -> Option<usize> {
10594 self.swa
10595 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10596 }
10597
10598 fn swa_trim_tails(&mut self) {
10607 let Some((slack, align)) = self.swa_trim else {
10608 return;
10609 };
10610 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10611 return;
10612 }
10613 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10614 if let Some(w) = self.layer_window(li) {
10615 self.kv_cache.layers[li].trim_window(w, slack, align);
10616 }
10617 }
10618 }
10619
10620 fn layer_num_heads(&self, li: usize) -> usize {
10621 self.attention_heads_per_layer
10622 .as_ref()
10623 .and_then(|v| v.get(li).copied())
10624 .unwrap_or(self.num_heads)
10625 }
10626
10627 fn layer_rope_scale(&self, li: usize) -> f32 {
10628 if self.layer_is_local(li) {
10629 self.rope_scale_local
10630 } else {
10631 self.rope_scale
10632 }
10633 }
10634
10635 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10638 if !self.layer_is_local(li) {
10639 if let Some((ghd, gkv)) = self.global_attn {
10640 return (gkv, ghd, ghd);
10641 }
10642 }
10643 (
10644 self.layer_num_kv_heads(li),
10645 self.head_dim,
10646 if self.layer_is_local(li) {
10647 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10648 } else {
10649 self.rotary_dim
10650 },
10651 )
10652 }
10653
10654 fn layer_num_kv_heads(&self, li: usize) -> usize {
10657 self.kv_heads_per_layer
10658 .as_ref()
10659 .and_then(|v| v.get(self.phys_layer(li)).copied())
10660 .unwrap_or(self.num_kv_heads)
10661 }
10662
10663 fn layer_v_dim(&self, li: usize) -> usize {
10665 let (_, hd, _) = self.layer_geom(li);
10666 self.v_head_dim.unwrap_or(hd).min(hd)
10667 }
10668
10669 pub fn set_attn_geometry(
10678 &mut self,
10679 kv_heads_per_layer: Option<Vec<usize>>,
10680 v_head_dim: Option<usize>,
10681 ) -> Result<(), String> {
10682 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10683 if self.global_attn.is_some() {
10684 return Err(
10685 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10686 attention geometry"
10687 .into(),
10688 );
10689 }
10690 if self
10691 .weights
10692 .layers
10693 .iter()
10694 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10695 {
10696 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10697 }
10698 }
10699 if let Some(vd) = v_head_dim {
10700 if vd == 0 || vd > self.head_dim {
10701 return Err(format!(
10702 "v_head_dim {vd} must be in 1..={} (head_dim)",
10703 self.head_dim
10704 ));
10705 }
10706 }
10707 if let Some(v) = &kv_heads_per_layer {
10708 if v.len() != self.physical_layers {
10709 return Err(format!(
10710 "kv_heads_per_layer has {} entries, expected {} layers",
10711 v.len(),
10712 self.physical_layers
10713 ));
10714 }
10715 for (li, &nkv) in v.iter().enumerate() {
10716 let is_attn = matches!(
10717 self.weights.layers.get(li).map(|lw| &lw.attn),
10718 Some(AttnKind::Full { .. }) | None
10719 );
10720 if !is_attn {
10721 continue;
10722 }
10723 let nh = self
10724 .attention_heads_per_layer
10725 .as_ref()
10726 .and_then(|h| h.get(li).copied())
10727 .unwrap_or(self.num_heads);
10728 if nkv == 0 || nh % nkv != 0 {
10729 return Err(format!(
10730 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10731 ));
10732 }
10733 }
10734 }
10735 self.kv_heads_per_layer = kv_heads_per_layer;
10736 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10737 if self.kv_heads_per_layer.is_some() {
10738 for li in 0..self.kv_cache.layers.len() {
10739 let full = matches!(
10740 self.weights
10741 .layers
10742 .get(self.phys_layer(li))
10743 .map(|lw| &lw.attn),
10744 Some(AttnKind::Full { .. })
10745 );
10746 let nkv = self.layer_num_kv_heads(li);
10747 let cache = &self.kv_cache.layers[li];
10748 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
10749 let sinks = cache.sinks.clone();
10750 self.kv_cache.layers[li] =
10751 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
10752 self.kv_cache.layers[li].sinks = sinks;
10753 }
10754 }
10755 }
10756 Ok(())
10757 }
10758
10759 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
10763 let Some(lw) = self.weights.layers.get(phys) else {
10764 return Err(format!("sinks for layer {phys}: no such layer"));
10765 };
10766 if !matches!(lw.attn, AttnKind::Full { .. }) {
10767 return Err(format!(
10768 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
10769 ));
10770 }
10771 let nh = self
10772 .attention_heads_per_layer
10773 .as_ref()
10774 .and_then(|h| h.get(phys).copied())
10775 .unwrap_or(self.num_heads);
10776 if sinks.len() != nh {
10777 return Err(format!(
10778 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
10779 sinks.len()
10780 ));
10781 }
10782 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
10783 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
10784 }
10785 for li in 0..self.kv_cache.layers.len() {
10786 if self.phys_layer(li) == phys {
10787 self.kv_cache.layers[li].sinks = Some(sinks.clone());
10788 }
10789 }
10790 Ok(())
10791 }
10792
10793 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
10803 if self.kv_heads_per_layer.is_some() {
10804 return Some("per-layer KV head counts");
10805 }
10806 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
10807 return Some("V heads narrower than Q/K heads");
10808 }
10809 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
10810 return Some("learned attention sinks");
10811 }
10812 if self.swa.is_some() || self.sliding_layers.is_some() {
10813 return Some("sliding-window layers");
10814 }
10815 None
10816 }
10817
10818 #[cfg(target_os = "macos")]
10826 fn metal_rope_scale_ok(&self) -> bool {
10827 !(self.qk_norm_after_rope && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
10828 }
10829
10830 #[cfg(target_os = "macos")]
10844 fn metal_graph_swa(&self) -> bool {
10845 (self.swa.is_some() || self.sliding_layers.is_some())
10846 && self.attention_heads_per_layer.is_none()
10847 && !self.attn_v_norm
10848 && self.attn_softcap == 0.0
10849 && self.kv_heads_per_layer.is_none()
10850 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
10851 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10852 && self.global_attn.is_none()
10853 && self.inv_freq_global.is_none()
10854 && self.metal_rope_scale_ok()
10855 && !(0..self.num_layers).any(|li| {
10856 self.layer_is_local(li)
10857 && self.inv_freq_local.is_none()
10858 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10859 })
10860 }
10861
10862 #[cfg(target_os = "macos")]
10870 fn metal_gemma4(&self) -> bool {
10871 crate::gpu_metal::gemma4::enabled()
10872 && (self.global_attn.is_some() || self.attn_v_norm)
10873 && self.attention_heads_per_layer.is_none()
10874 && self.kv_heads_per_layer.is_none()
10875 && self.v_head_dim.is_none()
10876 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10877 && self.attn_softcap == 0.0
10878 && !self.qk_norm_after_rope
10879 && self.metal_rope_scale_ok()
10880 && self
10881 .weights
10882 .layers
10883 .iter()
10884 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
10885 && !(0..self.num_layers).any(|li| {
10886 self.layer_is_local(li)
10887 && self.inv_freq_local.is_none()
10888 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10889 })
10890 }
10891
10892 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
10899 let geometry_reason = self.graph_attn_decline_reason();
10903 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
10904 if has_scaled_rope && geometry_reason.is_none() {
10905 return Some("scaled RoPE without per-layer geometry");
10906 }
10907 if has_scaled_rope && self.qk_norm_after_rope {
10912 return Some("scaled RoPE with post-RoPE Q/K norm");
10913 }
10914 geometry_reason?;
10915 if self.attention_heads_per_layer.is_some() {
10921 return Some("per-layer Q head counts with per-layer geometry");
10922 }
10923 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
10927 if !gemma4
10928 && self.weights.layers.iter().any(|lw| {
10929 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10930 })
10931 {
10932 return Some("sandwich norms / layer scale with per-layer geometry");
10933 }
10934 if self.weights.layers.iter().any(|lw| {
10938 matches!(
10939 &lw.attn,
10940 AttnKind::Full {
10941 output_gate: true,
10942 ..
10943 }
10944 )
10945 }) && self.v_head_dim.is_some()
10946 {
10947 return Some("gated attention with V narrower than K");
10948 }
10949 if (0..self.num_layers).any(|li| {
10950 self.layer_is_local(li)
10951 && self.inv_freq_local.is_none()
10952 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10953 }) {
10954 return Some("local rotary width without a local RoPE table");
10955 }
10956 None
10957 }
10958
10959 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
10963 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
10966 return None;
10967 }
10968 if self.global_attn.is_some() {
10969 return Some("per-layer head width (Gemma-4 global layers)");
10970 }
10971 if self.attn_v_norm {
10972 return Some("V norm");
10973 }
10974 if self.weights.layers.iter().any(|lw| {
10975 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10976 }) {
10977 return Some("sandwich norms / layer scale");
10978 }
10979 None
10980 }
10981
10982 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
10987 self.graph_attn_decline_reason()?;
10988 let (nkv, hd, rd) = self.layer_geom(li);
10989 let invf: &[f32] = if self.layer_is_local(li) {
10990 match &self.inv_freq_local {
10991 Some(f) => f.as_slice(),
10992 None => self.inv_freq.as_slice(),
10993 }
10994 } else {
10995 match &self.inv_freq_global {
10996 Some(f) => f.as_slice(),
10997 None => self.inv_freq.as_slice(),
10998 }
10999 };
11000 Some(crate::gpu::GraphAttnGeom {
11001 nkv,
11002 hd,
11003 dv: self.layer_v_dim(li),
11004 rd,
11005 invf,
11006 rope_scale: self.layer_rope_scale(li),
11007 window: self.layer_window(li),
11008 sink: self.kv_cache.layers[li].sinks.as_deref(),
11009 v_norm: self.attn_v_norm,
11010 })
11011 }
11012
11013 #[cfg(feature = "gpu")]
11025 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11026 if !self.proj_gate_sigmoid
11027 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11028 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11029 || self.graph_refused()
11030 || self.o1_active()
11031 || self.attn_softcap > 0.0
11032 || self.wgpu_graph_attn_decline().is_some()
11033 {
11034 return None;
11035 }
11036 let g = self.graph_attn_geom(li)?;
11037 let (nkv, hd, _) = self.layer_geom(li);
11038 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11040 return None;
11041 }
11042 Some(crate::gpu::PrefillMirror {
11043 kv_id: self.graph_kv_id,
11044 layer: li,
11045 limit: self.kv_cache.max_seq_len,
11046 })
11047 }
11048
11049 #[cfg(feature = "gpu")]
11058 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11059 let kv_id = self.graph_kv_id;
11060 for li in from..upto.min(self.num_layers) {
11061 if !matches!(
11062 self.weights.layers[self.phys_layer(li)].attn,
11063 AttnKind::Full { .. }
11064 ) {
11065 continue;
11066 }
11067 let host = self.kv_cache.layers[li].pos_len();
11069 if host >= position {
11070 continue;
11071 }
11072 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11073 continue;
11074 };
11075 let to = dev.min(position);
11076 if to <= host {
11077 continue;
11078 }
11079 let (nkv, hd) = {
11080 let c = &self.kv_cache.layers[li];
11081 (c.num_kv_heads, c.head_dim)
11082 };
11083 let Some((k, v, first_valid)) =
11084 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11085 else {
11086 continue;
11087 };
11088 let need_from = match self.layer_window(li) {
11091 Some(w) => host.max((position + 1).saturating_sub(w)),
11092 None => host,
11093 };
11094 if first_valid > need_from {
11095 tracing::warn!(
11096 "layer {li}: device KV rows {host}..{to} no longer resident \
11097 (from {first_valid}); host attention will miss them"
11098 );
11099 }
11100 let row = nkv * hd;
11101 let cache = &mut self.kv_cache.layers[li];
11102 for p in 0..to - host {
11103 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11104 }
11105 }
11106 }
11107
11108 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11111 let mut seen = self.graph_declines.borrow_mut();
11112 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11113 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11114 seen.push((site, reason));
11115 }
11116 }
11117
11118 pub fn graph_declines(&self) -> Vec<String> {
11121 self.graph_declines
11122 .borrow()
11123 .iter()
11124 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11125 .collect()
11126 }
11127
11128 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11137 let Some(dir) = &self.layer_dump else {
11138 return;
11139 };
11140 let mut bytes = Vec::with_capacity(row.len() * 4);
11141 for v in row {
11142 bytes.extend_from_slice(&v.to_le_bytes());
11143 }
11144 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11145 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11146 use std::sync::atomic::{AtomicBool, Ordering};
11147 static SAID: AtomicBool = AtomicBool::new(false);
11148 if !SAID.swap(true, Ordering::Relaxed) {
11149 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11150 }
11151 }
11152 }
11153
11154 fn mimo_moe_prepare(&mut self) {
11157 if !self.mimo_moe.is_undecided() {
11158 return;
11159 }
11160 let slot = {
11161 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11162 .filter_map(
11163 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11164 FfnKind::Moe(m) => Some((li, m)),
11165 _ => None,
11166 },
11167 )
11168 .collect();
11169 if layers.is_empty()
11172 || self.physical_layers != self.num_layers
11173 || self.gpu_plan.is_some()
11174 {
11175 crate::mimo_moe::Slot::Off
11176 } else {
11177 let graph_prefix =
11181 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11182 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11183 }
11184 };
11185 self.mimo_moe = slot;
11186 }
11187
11188 #[cfg(test)]
11189 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11190 self.graph_kv_id
11191 }
11192
11193 pub(crate) fn mimo_graph_layer_rows(
11197 &mut self,
11198 li: usize,
11199 h: &mut [f32],
11200 positions: &[usize],
11201 ) -> crate::gpu::BatchGraphOutcome {
11202 use crate::gpu::BatchGraphOutcome as Out;
11203 let b = positions.len();
11204 if !(1..=4).contains(&b)
11205 || h.len() != b * self.hidden_size
11206 || !self.mimo_moe.is_dynamic(li, true)
11207 || !crate::gpu::enabled_here()
11208 || !crate::gpu::wgpu_active()
11209 || self.o1_active()
11210 || self.physical_layers != self.num_layers
11211 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
11215 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
11216 || self.wgpu_graph_attn_decline().is_some()
11217 {
11218 return Out::Declined;
11219 }
11220 let attn_started = std::time::Instant::now();
11221 let outcome = {
11222 let lw = &self.weights.layers[li];
11223 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11224 return Out::Declined;
11225 }
11226 let FfnKind::Moe(m) = &lw.ffn else {
11227 return Out::Declined;
11228 };
11229 let AttnKind::Full {
11230 wq,
11231 wk,
11232 wv,
11233 wo,
11234 q_norm,
11235 k_norm,
11236 output_gate,
11237 softplus_gate,
11238 bias,
11239 } = &lw.attn
11240 else {
11241 return Out::Declined;
11242 };
11243 if *output_gate || softplus_gate.is_some() {
11244 return Out::Declined;
11245 }
11246 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11247 m.experts
11248 .first()?
11249 .gate_proj
11250 .mapped_q4tp()
11251 .map(|(m, _)| m.clone())
11252 }) else {
11253 return Out::Declined;
11254 };
11255 fn gw<'a>(
11256 t: &'a QTensor,
11257 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11258 ) -> Option<crate::gpu::GraphW<'a>> {
11259 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11260 if m.uid() != owner.uid() || t.has_prism_contract() {
11261 return None;
11262 }
11263 return Some(crate::gpu::GraphW {
11264 idx,
11265 kind,
11266 row_scale: rs,
11267 data: &[],
11268 prism: crate::gpu::GraphPrismOp::None,
11269 affine: false,
11270 });
11271 }
11272 t.as_f32().map(|data| crate::gpu::GraphW {
11273 idx: 0,
11274 kind: 4,
11275 row_scale: &[],
11276 data,
11277 prism: crate::gpu::GraphPrismOp::None,
11278 affine: false,
11279 })
11280 }
11281 let (Some(q), Some(k), Some(v), Some(o)) = (
11282 gw(wq, &model),
11283 gw(wk, &model),
11284 gw(wv, &model),
11285 gw(wo, &model),
11286 ) else {
11287 return Out::Declined;
11288 };
11289 let layer = crate::gpu::GraphLayer {
11290 input_norm: &lw.input_norm,
11291 post_norm: &lw.post_norm,
11292 attn_out_norm: None,
11293 ffn_out_norm: None,
11294 layer_scale: None,
11295 ffn: crate::gpu::GraphFfn::AttentionOnly,
11296 attn: crate::gpu::GraphAttn::Full {
11297 wq: q,
11298 wk: k,
11299 wv: v,
11300 wo: o,
11301 q_norm: q_norm.as_deref(),
11302 k_norm: k_norm.as_deref(),
11303 late_qk_norm: self.qk_norm_after_rope,
11304 bias: bias
11305 .as_ref()
11306 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11307 output_gate: false,
11308 cpu_k: self.kv_cache.layers[li].k_heads(),
11309 cpu_v: self.kv_cache.layers[li].v_heads(),
11310 cpu_base: self.kv_cache.layers[li].base(),
11311 geom: self.graph_attn_geom(li),
11312 head_gate: None,
11313 },
11314 };
11315 let (nkv, hd, rd) = self.layer_geom(li);
11316 crate::gpu::forward_batch_graph_at(
11317 &model,
11318 self.graph_kv_id,
11319 li,
11320 &[layer],
11321 &self.inv_freq,
11322 h,
11323 self.layer_num_heads(li),
11324 nkv,
11325 hd,
11326 rd,
11327 self.hidden_size,
11328 1,
11329 positions,
11330 self.kv_cache.max_seq_len,
11331 self.norm_style == cortiq_core::NormStyle::Gemma,
11332 self.rms_eps as f32,
11333 self.attn_scale,
11334 b,
11335 &[],
11336 self.o1_epoch,
11337 None,
11338 None,
11339 )
11340 };
11341 match outcome {
11342 Out::Completed => {}
11343 Out::Declined => return Out::Declined,
11344 Out::Failed => {
11345 self.graph_failed
11346 .store(true, std::sync::atomic::Ordering::Relaxed);
11347 return Out::Failed;
11348 }
11349 }
11350 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11351 let hs = self.hidden_size;
11352 let lw = &self.weights.layers[li];
11353 let FfnKind::Moe(m) = &lw.ffn else {
11354 unreachable!()
11355 };
11356 let mut post = vec![0.0; h.len()];
11357 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11358 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11359 }
11360 let mut ffn = if b == 1 {
11361 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11362 } else {
11363 moe_ffn_banked_rows(
11364 &mut self.mimo_moe,
11365 li,
11366 m,
11367 &post,
11368 b,
11369 hs,
11370 self.pool.as_deref(),
11371 )
11372 };
11373 for (x, &f) in h.iter_mut().zip(&ffn) {
11374 *x += f;
11375 }
11376 attention::recycle_buf(&mut ffn);
11377 if self.layer_dump.is_some() {
11378 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11379 self.dump_layer_row(pos, li, row);
11380 }
11381 }
11382 crate::mimo_moe::note_attention_graph(b, attn_ns);
11383 Out::Completed
11384 }
11385
11386 fn layer_attn_plain(&self, li: usize) -> bool {
11387 self.kv_heads_per_layer.is_none()
11388 && self.v_head_dim.is_none()
11389 && self.global_attn.is_none()
11390 && self.layer_window(li).is_none()
11391 && self.kv_cache.layers[li].sinks.is_none()
11392 }
11393
11394 fn forward_layers(
11396 &mut self,
11397 hidden: &[f32],
11398 position: usize,
11399 task_mask: Option<&TaskMask>,
11400 ) -> Vec<f32> {
11401 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11402 self.o1_progress();
11403 self.swa_trim_tails();
11404 out
11405 }
11406
11407 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11415 self.embed_single(id)
11416 }
11417
11418 pub fn split_supported(&self) -> Result<(), String> {
11422 if self.dsv4.is_some() {
11423 return Err(
11424 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11425 );
11426 }
11427 if self.dsv41.is_some() {
11428 return Err(
11429 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11430 .into(),
11431 );
11432 }
11433 if self.qwen4_exp.is_some() {
11434 return Err(
11435 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11436 );
11437 }
11438 if self.g3n.is_some() {
11439 return Err(
11440 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11441 );
11442 }
11443 Ok(())
11444 }
11445
11446 pub fn forward_span(
11451 &mut self,
11452 hidden: &[f32],
11453 position: usize,
11454 from: usize,
11455 upto: usize,
11456 task_mask: Option<&TaskMask>,
11457 ) -> Result<Vec<f32>, String> {
11458 self.split_supported()?;
11459 if from > upto || upto >= self.num_layers {
11460 return Err(format!(
11461 "forward_span: layer range {from}..={upto} outside 0..{}",
11462 self.num_layers
11463 ));
11464 }
11465 if hidden.len() != self.hidden_size {
11466 return Err(format!(
11467 "forward_span: hidden len {} ≠ hidden_size {}",
11468 hidden.len(),
11469 self.hidden_size
11470 ));
11471 }
11472 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11473 self.o1_progress();
11474 self.swa_trim_tails();
11475 if self
11476 .graph_failed
11477 .swap(false, std::sync::atomic::Ordering::Relaxed)
11478 {
11479 self.cancel
11480 .store(false, std::sync::atomic::Ordering::Relaxed);
11481 self.clear_sequence_state();
11482 return Err("forward_span: deferred O(1) transition failed".into());
11483 }
11484 Ok(out)
11485 }
11486
11487 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11490 let normed = inference::rms_norm(
11491 hidden,
11492 &self.weights.final_norm,
11493 self.rms_eps,
11494 self.norm_style,
11495 );
11496 self.lm_head_forward(&normed)
11497 }
11498
11499 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11501 sampler::sample_with_scratch(
11502 logits,
11503 &self.sampler_config,
11504 past_tokens,
11505 &mut self.rng,
11506 &mut self.sampler_scratch,
11507 )
11508 }
11509
11510 pub fn reset_session(&mut self) {
11512 self.clear_sequence_state();
11513 }
11514
11515 pub fn prefill_span_ids(
11521 &mut self,
11522 ids: &[u32],
11523 start_pos: usize,
11524 upto: usize,
11525 task_mask: Option<&TaskMask>,
11526 ) -> Result<Vec<f32>, String> {
11527 self.split_supported()?;
11528 if upto >= self.num_layers {
11529 return Err(format!(
11530 "prefill_span_ids: upto {upto} outside 0..{}",
11531 self.num_layers
11532 ));
11533 }
11534 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11538 let out =
11539 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11540 self.check_o1_progress_failure("prefill_span_ids")?;
11541 Ok(out)
11542 } else {
11543 let hs = self.hidden_size;
11544 let mut out = Vec::with_capacity(ids.len() * hs);
11545 for (i, &id) in ids.iter().enumerate() {
11546 let emb = self.embed_id(id);
11547 out.extend_from_slice(&self.forward_span(
11548 &emb,
11549 start_pos + i,
11550 0,
11551 upto,
11552 task_mask,
11553 )?);
11554 }
11555 Ok(out)
11556 }
11557 }
11558
11559 pub fn prefill_span_hidden(
11562 &mut self,
11563 hidden: &[f32],
11564 start_pos: usize,
11565 from: usize,
11566 upto: usize,
11567 task_mask: Option<&TaskMask>,
11568 ) -> Result<Vec<f32>, String> {
11569 self.split_supported()?;
11570 let hs = self.hidden_size;
11571 if hidden.is_empty() || hidden.len() % hs != 0 {
11572 return Err(format!(
11573 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11574 hidden.len()
11575 ));
11576 }
11577 if from > upto || upto >= self.num_layers {
11578 return Err(format!(
11579 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11580 self.num_layers
11581 ));
11582 }
11583 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11584 let out = self.prefill_batch_span(
11585 PrefillIn::Hidden(hidden),
11586 start_pos,
11587 task_mask,
11588 from,
11589 upto + 1,
11590 );
11591 self.check_o1_progress_failure("prefill_span_hidden")?;
11592 Ok(out)
11593 } else {
11594 let b = hidden.len() / hs;
11595 let mut out = Vec::with_capacity(hidden.len());
11596 for i in 0..b {
11597 let h = self.forward_span(
11598 &hidden[i * hs..(i + 1) * hs],
11599 start_pos + i,
11600 from,
11601 upto,
11602 task_mask,
11603 )?;
11604 out.extend_from_slice(&h);
11605 }
11606 Ok(out)
11607 }
11608 }
11609
11610 fn try_token_graph_wgpu(
11614 &self,
11615 hidden: &[f32],
11616 position: usize,
11617 logits_out: &mut Vec<f32>,
11618 layers_run: &mut usize,
11619 ) -> Option<Result<Vec<f32>, ()>> {
11620 self.try_token_graph_wgpu_steps(
11621 hidden,
11622 position,
11623 logits_out,
11624 1,
11625 None,
11626 Some(layers_run),
11627 0,
11628 self.num_layers,
11629 )
11630 }
11631
11632 fn try_token_graph_wgpu_span(
11636 &self,
11637 hidden: &[f32],
11638 position: usize,
11639 logits_out: &mut Vec<f32>,
11640 from: usize,
11641 upto_excl: usize,
11642 layers_run: &mut usize,
11643 ) -> Option<Result<Vec<f32>, ()>> {
11644 self.try_token_graph_wgpu_steps(
11645 hidden,
11646 position,
11647 logits_out,
11648 1,
11649 None,
11650 Some(layers_run),
11651 from,
11652 upto_excl,
11653 )
11654 }
11655
11656 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11660 if self.o1_active() || self.attn_softcap > 0.0 {
11661 return None;
11662 }
11663 if let Some(reason) = self.wgpu_graph_attn_decline() {
11666 self.note_graph_decline("wgpu multi-burst", reason);
11667 return None;
11668 }
11669 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11670 if !graph_on || self.graph_refused() {
11671 return None;
11678 }
11679 let emb = self.embed_single(t_next);
11680 let mut lg = Vec::new();
11681 let mut ids = Vec::new();
11682 match self.try_token_graph_wgpu_steps(
11683 &emb,
11684 position,
11685 &mut lg,
11686 k,
11687 Some(&mut ids),
11688 None,
11689 0,
11690 self.num_layers,
11691 ) {
11692 Some(Ok(_)) => {}
11693 Some(Err(())) => {
11694 self.graph_failed
11699 .store(true, std::sync::atomic::Ordering::Relaxed);
11700 return None;
11701 }
11702 None => return None,
11703 }
11704 (ids.len() == k).then_some(ids)
11705 }
11706
11707 fn try_token_graph_wgpu_steps(
11711 &self,
11712 hidden: &[f32],
11713 position: usize,
11714 logits_out: &mut Vec<f32>,
11715 steps: usize,
11716 ids_out: Option<&mut Vec<u32>>,
11717 layers_run: Option<&mut usize>,
11718 from: usize,
11719 upto_excl: usize,
11720 ) -> Option<Result<Vec<f32>, ()>> {
11721 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11724 Some(end) if end < upto_excl => {
11725 if steps != 1 || layers_run.is_none() || from >= end {
11726 return None;
11727 }
11728 end
11729 }
11730 _ => upto_excl,
11731 };
11732 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11735 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11736 return None;
11740 }
11741 if let Some(reason) = self.wgpu_graph_attn_decline() {
11748 self.note_graph_decline("wgpu token graph", reason);
11749 return None;
11750 }
11751 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
11756 .map(|li| {
11757 if !o1_gpu {
11758 return None;
11759 }
11760 self.kv_cache.layers[self.phys_layer(li)].o1_views()
11761 })
11762 .collect();
11763 if self.o1_active() && o1_gpu {
11764 let want: usize = (from..upto_excl)
11767 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
11768 .count();
11769 let have = o1_views.iter().filter(|v| v.is_some()).count();
11770 if want == 0 || have != want {
11771 use std::sync::atomic::{AtomicUsize, Ordering};
11781 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
11782 let code = have * 1000 + want;
11783 if LAST.swap(code, Ordering::Relaxed) != code {
11784 tracing::warn!(
11785 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
11786 );
11787 }
11788 return None;
11789 }
11790 }
11791 let nh = self.num_heads;
11792 let (nkv, hd, rd) = self.layer_geom(0);
11793 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11794 let mut layers = Vec::with_capacity(upto_excl - from);
11795 let mut model = None;
11796 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
11797 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11798 if let Some((m, i, kind, rs)) = t
11799 .graph_weight()
11800 .or_else(|| t.graph_weight_descriptor())
11801 {
11802 let name = &m.tensors[i].name;
11803 let prism = if crate::prism::is_inverse_embedding(m, name) {
11804 crate::gpu::GraphPrismOp::InverseEmbedding
11805 } else if crate::prism::is_forward_weight(m, name) {
11806 crate::gpu::GraphPrismOp::Forward
11807 } else {
11808 crate::gpu::GraphPrismOp::None
11809 };
11810 return Some(crate::gpu::GraphW {
11811 idx: i,
11812 kind,
11813 row_scale: rs,
11814 data: &[],
11815 prism,
11816 affine: crate::prism::is_affine_target(m, name),
11817 });
11818 }
11819 match t.as_f32() {
11821 Some(d) => Some(crate::gpu::GraphW {
11822 idx: 0,
11823 kind: 4,
11824 row_scale: &[],
11825 data: d,
11826 prism: crate::gpu::GraphPrismOp::None,
11827 affine: false,
11828 }),
11829 None => {
11830 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11831 eprintln!("batch graph: weight has no graph/f32 representation");
11832 }
11833 None
11834 }
11835 }
11836 }
11837 for li in from..upto_excl {
11838 let lw = &self.weights.layers[self.phys_layer(li)];
11839 if dbg {
11840 let ak = match &lw.attn {
11841 AttnKind::Mla(_) => "Mla".into(),
11842 AttnKind::Full {
11843 output_gate, bias, ..
11844 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11845 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11846 AttnKind::Kda(_) => "Kda".into(),
11847 AttnKind::Linear(_) => "Linear".into(),
11848 AttnKind::ShortConv(_) => "ShortConv".into(),
11849 AttnKind::Bounded(_) => "Bounded".into(),
11850 };
11851 let fk = match &lw.ffn {
11852 FfnKind::Dense(_) => "Dense",
11853 FfnKind::Moe(_) => "Moe",
11854 FfnKind::DenseMoe(_) => "DenseMoe",
11855 };
11856 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11857 }
11858 let gffn = match &lw.ffn {
11859 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11863 FfnKind::Dense(d) => {
11864 let Some(act) = d.act.graph_act() else {
11868 self.note_graph_decline(
11869 "wgpu token graph",
11870 "dense FFN activation without a graph kernel",
11871 );
11872 return None;
11873 };
11874 crate::gpu::GraphFfn::Dense {
11875 gate: gw(&d.gate_proj)?,
11876 up: gw(&d.up_proj)?,
11877 down: gw(&d.down_proj)?,
11878 act,
11879 }
11880 }
11881 FfnKind::Moe(m) => {
11882 if m.route_tau.is_some() || m.mask.is_some() {
11890 return None;
11891 }
11892 let shared = m.shared.as_ref();
11893 let has_shared = shared.is_some();
11894 let shared_gated = matches!(shared, Some((_, Some(_))));
11895 let sgate = match shared {
11896 Some((_, Some(sg))) => gw(sg)?,
11897 _ => gw(&m.router)?,
11901 };
11902 let router = gw(&m.router)?;
11903 if router.prism != crate::gpu::GraphPrismOp::None
11909 || sgate.prism != crate::gpu::GraphPrismOp::None
11910 || router.affine
11911 || sgate.affine
11912 {
11913 tracing::warn!(
11914 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
11915 );
11916 return None;
11917 }
11918 let inter = m.experts.first()?.gate_proj.rows();
11919 let mut experts = Vec::with_capacity(m.experts.len() + 1);
11920 let mut q4tp: Option<bool> = None;
11923 let mut gu_q2: Option<bool> = None;
11926 let mut q82: Option<bool> = None;
11928 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
11929 if !matches!(e.act, Act::Silu)
11930 || e.gate_proj.rows() != inter
11931 || e.up_proj.rows() != inter
11932 {
11933 return None;
11934 }
11935 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
11940 let Some((em, ei, _, _)) = expert_weight
11941 .graph_weight()
11942 .or_else(|| expert_weight.graph_weight_descriptor())
11943 else {
11944 return None;
11945 };
11946 let name = &em.tensors[ei].name;
11947 if crate::prism::is_forward_weight(em, name)
11948 || crate::prism::is_inverse_embedding(em, name)
11949 || crate::prism::is_affine_target(em, name)
11950 {
11951 tracing::warn!(
11952 "resident MoE declined: expert Prism/affine transform is not implemented"
11953 );
11954 return None;
11955 }
11956 }
11957 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
11958 Some((mm, gi)) => (
11959 mm,
11960 gi,
11961 e.up_proj.mapped_q4t()?.1,
11962 e.down_proj.mapped_q4t()?.1,
11963 false,
11964 false,
11965 false,
11966 ),
11967 None => match e.gate_proj.mapped_q2tp() {
11968 Some((mm, gi)) => (
11969 mm,
11970 gi,
11971 e.up_proj.mapped_q2tp()?.1,
11972 e.down_proj.mapped_q4tp()?.1,
11973 true,
11974 true,
11975 false,
11976 ),
11977 None => match e.gate_proj.mapped_q4tp() {
11978 Some((mm, gi)) => (
11979 mm,
11980 gi,
11981 e.up_proj.mapped_q4tp()?.1,
11982 e.down_proj.mapped_q4tp()?.1,
11983 true,
11984 false,
11985 false,
11986 ),
11987 None => {
11990 if !crate::gpu::moe_q82_graph_on() {
11991 return None;
11992 }
11993 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
11994 (
11995 mm,
11996 gi,
11997 e.up_proj.mapped_q8_2f()?.1,
11998 e.down_proj.mapped_q8_2f()?.1,
11999 false,
12000 false,
12001 true,
12002 )
12003 }
12004 },
12005 },
12006 };
12007 if *q4tp.get_or_insert(is_p) != is_p
12008 || *gu_q2.get_or_insert(is_q2) != is_q2
12009 || *q82.get_or_insert(is_8) != is_8
12010 {
12011 tracing::warn!(
12017 "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."
12018 );
12019 return None;
12020 }
12021 model.get_or_insert_with(|| mm.clone());
12022 experts.push((gi, ui, di));
12023 }
12024 crate::gpu::GraphFfn::Moe {
12025 router,
12026 shared_gate: sgate,
12027 experts,
12028 n_exp: m.experts.len(),
12029 top_k: std::env::var("CMF_TOPK_PROBE")
12035 .ok()
12036 .and_then(|v| v.parse::<usize>().ok())
12037 .filter(|k| *k > 0 && *k <= m.top_k)
12038 .unwrap_or(m.top_k),
12039 inter,
12040 norm_topk: m.norm_topk_prob,
12041 q4tp: q4tp?,
12042 gu_q2: gu_q2.unwrap_or(false),
12043 q82: q82.unwrap_or(false),
12044 sigmoid: m.router_sigmoid,
12045 bias: m.expert_bias.as_deref(),
12046 has_shared,
12047 shared_gated,
12048 route_scale: m.routed_scaling,
12049 }
12050 }
12051 };
12052 let attn = match &lw.attn {
12053 AttnKind::Full {
12054 wq,
12055 wk,
12056 wv,
12057 wo,
12058 q_norm,
12059 k_norm,
12060 output_gate,
12061 softplus_gate,
12062 bias,
12063 } => {
12064 if self.attention_heads_per_layer.is_some() {
12065 return None;
12066 }
12067 let head_gate = match softplus_gate {
12071 None => None,
12072 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
12073 Some(gw(g)?)
12074 }
12075 Some(_) => {
12076 self.note_graph_decline(
12077 "wgpu token graph",
12078 "projected softplus / per-element output gate",
12079 );
12080 return None;
12081 }
12082 };
12083 let (m, _, _, _) = wq
12084 .graph_weight()
12085 .or_else(|| wq.graph_weight_descriptor())?;
12086 model = Some(m.clone());
12087 crate::gpu::GraphAttn::Full {
12088 wq: gw(wq)?,
12089 wk: gw(wk)?,
12090 wv: gw(wv)?,
12091 wo: gw(wo)?,
12092 q_norm: q_norm.as_deref(),
12093 k_norm: k_norm.as_deref(),
12094 late_qk_norm: self.qk_norm_after_rope,
12095 bias: bias
12096 .as_ref()
12097 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12098 output_gate: *output_gate,
12099 cpu_k: self.kv_cache.layers[li].k_heads(),
12100 cpu_v: self.kv_cache.layers[li].v_heads(),
12101 cpu_base: self.kv_cache.layers[li].base(),
12102 geom: self.graph_attn_geom(li),
12103 head_gate,
12104 }
12105 }
12106 AttnKind::LinearGdn(w) => {
12107 let cfg = self.gdn_cfg?;
12108 let (m, _, _, _) = w
12109 .in_proj_qkv
12110 .graph_weight()
12111 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
12112 model = Some(m.clone());
12113 crate::gpu::GraphAttn::Gdn {
12114 qkv: gw(&w.in_proj_qkv)?,
12115 z: gw(&w.in_proj_z)?,
12116 a: gw(&w.in_proj_a)?,
12117 b: gw(&w.in_proj_b)?,
12118 out: gw(&w.out_proj)?,
12119 conv1d: &w.conv1d,
12120 a_log: &w.a_log,
12121 dt_bias: &w.dt_bias,
12122 norm: &w.norm,
12123 nv: cfg.num_v_heads,
12124 nk: cfg.num_k_heads,
12125 dk: cfg.key_head_dim,
12126 dv: cfg.value_head_dim,
12127 kk: cfg.conv_kernel,
12128 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12129 }
12130 }
12131 AttnKind::ShortConv(w) => {
12132 let cfg = self.short_conv_cfg?;
12133 let (m, _, _, _) = w
12134 .in_proj
12135 .graph_weight()
12136 .or_else(|| w.in_proj.graph_weight_descriptor())?;
12137 model = Some(m.clone());
12138 crate::gpu::GraphAttn::ShortConv {
12139 inp: gw(&w.in_proj)?,
12140 out: gw(&w.out_proj)?,
12141 taps: &w.conv,
12142 kernel: cfg.kernel,
12143 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12144 }
12145 }
12146 _ => return None,
12147 };
12148 layers.push(crate::gpu::GraphLayer {
12149 input_norm: &lw.input_norm,
12150 attn,
12151 post_norm: &lw.post_norm,
12152 attn_out_norm: lw.attn_out_norm.as_deref(),
12153 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12154 layer_scale: lw.layer_scale,
12155 ffn: gffn,
12156 });
12157 }
12158 let model = model?;
12159 let lm_gw = if upto_excl == self.num_layers
12165 && self.graph_want_logits
12166 && std::env::var("CMF_GPU_LMHEAD")
12167 .map(|v| v != "0")
12168 .unwrap_or(true)
12169 {
12170 self.weights
12171 .lm_head
12172 .graph_weight()
12173 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
12174 .map(|(m, i, kind, rs)| {
12175 let name = &m.tensors[i].name;
12176 let prism = if crate::prism::is_inverse_embedding(m, name) {
12177 crate::gpu::GraphPrismOp::InverseEmbedding
12178 } else if crate::prism::is_forward_weight(m, name) {
12179 crate::gpu::GraphPrismOp::Forward
12180 } else {
12181 crate::gpu::GraphPrismOp::None
12182 };
12183 (
12184 crate::gpu::GraphW {
12185 idx: i,
12186 kind,
12187 row_scale: rs,
12188 data: &[],
12189 prism,
12190 affine: crate::prism::is_affine_target(m, name),
12191 },
12192 self.weights.lm_head.rows(),
12193 )
12194 })
12195 } else {
12196 None
12197 };
12198 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
12199 let emb_gw = if steps > 1 {
12201 self.weights
12202 .embed_tokens
12203 .graph_weight()
12204 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
12205 .map(|(m, i, kind, rs)| {
12206 let name = &m.tensors[i].name;
12207 let prism = if crate::prism::is_inverse_embedding(m, name) {
12208 crate::gpu::GraphPrismOp::InverseEmbedding
12209 } else if crate::prism::is_forward_weight(m, name) {
12210 crate::gpu::GraphPrismOp::Forward
12211 } else {
12212 crate::gpu::GraphPrismOp::None
12213 };
12214 (
12215 crate::gpu::GraphW {
12216 idx: i,
12217 kind,
12218 row_scale: rs,
12219 data: &[],
12220 prism,
12221 affine: crate::prism::is_affine_target(m, name),
12222 },
12223 self.weights.embed_tokens.rows(),
12224 self.embed_multiplier,
12225 )
12226 })
12227 } else {
12228 None
12229 };
12230
12231 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12237 (from..upto_excl.min(self.num_layers - 1))
12238 .filter(|&li| (li + 1) % self.physical_layers == 0)
12239 .map(|li| li - from)
12240 .collect()
12241 } else {
12242 Vec::new()
12243 };
12244 let mut h = hidden.to_vec();
12245 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12251 let outcome = crate::gpu::forward_token_graph(
12252 &model,
12253 self.graph_kv_id,
12254 &layers,
12255 &o1_views,
12256 self.o1_epoch,
12257 &self.inv_freq,
12258 &mut h,
12259 nh,
12260 nkv,
12261 hd,
12262 self.attn_scale,
12263 rd,
12264 self.hidden_size,
12265 self.intermediate_size,
12266 position,
12267 self.kv_cache.max_seq_len,
12268 gemma,
12269 self.rms_eps as f32,
12270 lm,
12271 &self.weights.final_norm,
12272 logits_out,
12273 &loop_norm_at,
12274 steps,
12275 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12276 ids_out,
12277 layers_run,
12278 from,
12279 dump_hidden,
12280 );
12281 match outcome {
12282 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12283 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12284 crate::gpu::TokenGraphOutcome::Declined => None,
12285 }
12286 }
12287
12288 #[cfg(target_os = "macos")]
12297 #[allow(clippy::type_complexity)]
12298 fn metal_rows_plan(
12299 &self,
12300 ) -> Option<(
12301 Vec<MetalRowsItem<'_>>,
12302 std::sync::Arc<cortiq_core::CmfModel>,
12303 Option<crate::gpu_metal::GdnGpuCfg>,
12304 )> {
12305 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12306 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12307 if !graph_force
12308 || !crate::gpu::enabled_here()
12309 || std::env::var("CMF_GPU_BLOCK")
12310 .map(|v| v == "0")
12311 .unwrap_or(false)
12312 || self.attn_softcap > 0.0
12313 || self.o1_active()
12314 || self.swa.is_some()
12315 || self.global_attn.is_some()
12316 || self.attention_heads_per_layer.is_some()
12317 || self.graph_attn_decline_reason().is_some()
12319 || self.attn_v_norm
12320 || self.loop_final_norm
12321 || !self.metal_rope_scale_ok()
12322 {
12323 return None;
12324 }
12325 let attend_contract = self.head_dim % 4 == 0
12326 && self.head_dim <= 256
12327 && self.rotary_dim >= 2
12328 && self.rotary_dim <= self.head_dim
12329 && (self.rotary_dim / 2) % 32 == 0
12330 && self.num_kv_heads > 0
12331 && self.num_heads % self.num_kv_heads == 0;
12332 if !attend_contract {
12333 return None;
12334 }
12335 let mut plan: Vec<MetalRowsItem> = Vec::new();
12336 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12337 for li in 0..self.num_layers {
12338 let lw = &self.weights.layers[self.phys_layer(li)];
12339 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12340 return None;
12341 }
12342 let ffn = match &lw.ffn {
12343 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12344 let (Some(g), Some(u), Some(dn)) = (
12345 d.gate_proj.metal_graph_parts(),
12346 d.up_proj.metal_graph_parts(),
12347 d.down_proj.metal_graph_parts(),
12348 ) else {
12349 return None;
12350 };
12351 MetalFfn::Dense {
12352 gate: g,
12353 up: u,
12354 down: dn,
12355 act: 0, }
12357 }
12358 _ => return None,
12359 };
12360 match &lw.attn {
12361 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12362 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12363 w.in_proj_qkv.metal_graph_parts(),
12364 w.in_proj_z.metal_graph_parts(),
12365 w.in_proj_a.f32_parts(),
12366 w.in_proj_b.f32_parts(),
12367 w.out_proj.metal_graph_parts(),
12368 ) else {
12369 return None;
12370 };
12371 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12372 model_ref.get_or_insert_with(|| model.clone());
12373 }
12374 let gl = GdnGpuLayer {
12375 attn_norm: &lw.input_norm,
12376 post_norm: &lw.post_norm,
12377 qkv,
12378 z,
12379 a,
12380 b: bb,
12381 out,
12382 ffn,
12383 conv1d: &w.conv1d,
12384 a_log: &w.a_log,
12385 dt_bias: &w.dt_bias,
12386 gnorm: &w.norm,
12387 };
12388 match plan.last_mut() {
12389 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12390 _ => plan.push(MetalRowsItem::Gdn {
12391 run: vec![gl],
12392 first: li,
12393 }),
12394 }
12395 }
12396 AttnKind::Full {
12397 wq,
12398 wk,
12399 wv,
12400 wo,
12401 q_norm,
12402 k_norm,
12403 output_gate,
12404 softplus_gate: None,
12405 bias: None,
12406 } => {
12407 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12408 (
12409 wq.metal_graph_parts(),
12410 wk.metal_graph_parts(),
12411 wv.metal_graph_parts(),
12412 wo.metal_graph_parts(),
12413 )
12414 else {
12415 return None;
12416 };
12417 if let QTensor::Mapped { model, .. } = wq {
12418 model_ref.get_or_insert_with(|| model.clone());
12419 }
12420 let cache = &self.kv_cache.layers[li];
12421 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12422 return None;
12423 }
12424 plan.push(MetalRowsItem::Attn {
12425 l: AttnGpuLayer {
12426 attn_norm: &lw.input_norm,
12427 post_norm: &lw.post_norm,
12428 wq: pq,
12429 wk: pk,
12430 wv: pv,
12431 wo: po,
12432 ffn,
12433 attn_out_norm: None,
12434 ffn_out_norm: None,
12435 layer_scale: None,
12436 },
12437 li,
12438 q_norm: q_norm.as_deref(),
12439 k_norm: k_norm.as_deref(),
12440 output_gate: *output_gate,
12441 });
12442 }
12443 _ => return None,
12444 }
12445 }
12446 let model = model_ref?;
12447 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12448 nv: cfg.num_v_heads,
12449 nk: cfg.num_k_heads,
12450 dk: cfg.key_head_dim,
12451 dv: cfg.value_head_dim,
12452 kk: cfg.conv_kernel,
12453 hidden: self.hidden_size,
12454 inter: self.intermediate_size,
12455 c_dim: cfg.conv_dim(),
12456 eps: cfg.rms_eps as f32,
12457 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12458 });
12459 Some((plan, model, gcfg))
12460 }
12461
12462 #[cfg(target_os = "macos")]
12464 #[allow(clippy::too_many_arguments)]
12465 fn metal_attn_params<'a>(
12466 li: usize,
12467 cache: &'a crate::kv_cache::LayerKvCache,
12468 q_norm: Option<&'a [f32]>,
12469 k_norm: Option<&'a [f32]>,
12470 output_gate: bool,
12471 inv_freq: &'a [f32],
12472 rope_scale: f32,
12473 geom: (usize, usize, usize, usize),
12474 pos0: usize,
12475 kv_id: u64,
12476 scale: f32,
12477 eps: f32,
12478 gemma: bool,
12479 late_qk_norm: bool,
12480 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12481 let (nh, nkv, hd, rd) = geom;
12482 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12483 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12484 let cpu_stored = cpu_k[0].len() / hd;
12485 (
12486 crate::gpu_metal::AttnDeviceParams {
12487 kv_id,
12488 layer: li,
12489 nh,
12490 nkv,
12491 hd,
12492 rd,
12493 position: pos0,
12494 scale,
12495 eps,
12496 gemma,
12497 late_qk_norm,
12498 output_gate,
12499 q_norm,
12500 k_norm,
12501 inv_freq,
12502 rope_scale,
12503 cpu_k,
12504 cpu_v,
12505 cpu_stored,
12506 cpu_gen: cache.generation(),
12507 o1: None,
12508 window: None,
12509 head_gate: None,
12510 v_norm: false,
12511 },
12512 cpu_stored,
12513 )
12514 }
12515
12516 #[cfg(target_os = "macos")]
12521 #[allow(clippy::type_complexity)]
12522 fn metal_rows_run(
12523 &mut self,
12524 hiddens: &mut [f32],
12525 pos0: usize,
12526 b: usize,
12527 prefill: bool,
12528 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12529 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12533 ) -> MetalRowsRun {
12534 use crate::gpu_metal::{GraphDims, VerifyGraph};
12535 if !crate::gpu_metal::wait_replay() {
12541 tracing::error!("Metal rows graph: the pending async replay failed");
12542 return MetalRowsRun::Failed;
12543 }
12544 spec_stamp("v.wait");
12545 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12551 if want > 0 {
12552 let phys = self.physical_layers.max(1);
12553 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12554 let is_gdn = self
12555 .weights
12556 .layers
12557 .get(li % phys)
12558 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12559 if is_gdn && l.linear_state.len() != want {
12560 l.linear_state = vec![0f32; want];
12561 }
12562 }
12563 }
12564 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12565 return MetalRowsRun::Declined;
12566 };
12567 spec_stamp("v.plan");
12568 let dims = GraphDims {
12569 hidden: self.hidden_size,
12570 eps: self.rms_eps as f32,
12571 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12572 };
12573 let Some(mut graph) = (if prefill {
12574 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12575 } else {
12576 VerifyGraph::new(&model, dims, hiddens, b)
12577 }) else {
12578 return MetalRowsRun::Declined;
12579 };
12580 let geom = (
12581 self.num_heads,
12582 self.num_kv_heads,
12583 self.head_dim,
12584 self.rotary_dim,
12585 );
12586 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12587 let eps = self.rms_eps as f32;
12588 let kv_id = self.graph_kv_id;
12589 let inv_freq = self.inv_freq.clone();
12590 for item in &plan {
12591 let ok = match item {
12592 MetalRowsItem::Gdn { run, .. } => gcfg
12593 .as_ref()
12594 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12595 .unwrap_or(false),
12596 MetalRowsItem::Attn {
12597 l,
12598 li,
12599 q_norm,
12600 k_norm,
12601 output_gate,
12602 } => {
12603 let (p, _) = Self::metal_attn_params(
12604 *li,
12605 &self.kv_cache.layers[*li],
12606 *q_norm,
12607 *k_norm,
12608 *output_gate,
12609 &inv_freq,
12610 self.layer_rope_scale(*li),
12611 geom,
12612 pos0,
12613 kv_id,
12614 self.attn_scale,
12615 eps,
12616 gemma,
12617 self.qk_norm_after_rope,
12618 );
12619 graph.attn_ok(l, &p)
12620 }
12621 };
12622 if !ok {
12623 use std::sync::atomic::{AtomicBool, Ordering};
12624 static SAID: AtomicBool = AtomicBool::new(false);
12625 if !SAID.swap(true, Ordering::Relaxed) {
12626 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12627 }
12628 return MetalRowsRun::Declined;
12629 }
12630 }
12631 let lm = match &spec {
12632 Some((lm, _, _)) => {
12633 if !graph.lm_head_ok(*lm) {
12634 return MetalRowsRun::Declined;
12635 }
12636 Some(*lm)
12637 }
12638 None => None,
12639 };
12640 let mut gdn_layers = Vec::new();
12641 let mut attn_layers = Vec::new();
12642 for item in &plan {
12643 match item {
12644 MetalRowsItem::Gdn { run, first } => {
12645 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12646 .iter()
12647 .map(|l| l.linear_state.as_slice())
12648 .collect();
12649 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12650 return MetalRowsRun::Declined;
12651 }
12652 gdn_layers.extend(*first..*first + run.len());
12653 }
12654 MetalRowsItem::Attn {
12655 l,
12656 li,
12657 q_norm,
12658 k_norm,
12659 output_gate,
12660 } => {
12661 let (p, cpu_stored) = Self::metal_attn_params(
12662 *li,
12663 &self.kv_cache.layers[*li],
12664 *q_norm,
12665 *k_norm,
12666 *output_gate,
12667 &inv_freq,
12668 self.layer_rope_scale(*li),
12669 geom,
12670 pos0,
12671 kv_id,
12672 self.attn_scale,
12673 eps,
12674 gemma,
12675 self.qk_norm_after_rope,
12676 );
12677 if !graph.encode_attn_b(l, &p) {
12678 return MetalRowsRun::Declined;
12679 }
12680 attn_layers.push((*li, cpu_stored));
12681 }
12682 }
12683 }
12684 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12685 if !graph.encode_lm_head_b(final_norm, lm) {
12686 return MetalRowsRun::Declined;
12687 }
12688 if let Some((n, _)) = argmax_out.as_ref() {
12693 if !graph.encode_argmax_b(*n) {
12694 argmax_out = None;
12695 }
12696 }
12697 }
12698 spec_stamp("v.enc");
12699 if !graph.sync() {
12700 return MetalRowsRun::Failed;
12701 }
12702 spec_stamp("v.gpu");
12703 match (spec, argmax_out) {
12704 (Some(_), Some((_, ids))) => {
12705 ids.resize(b, 0);
12706 if !graph.read_argmax(ids) {
12707 return MetalRowsRun::Failed;
12708 }
12709 spec_stamp("v.am");
12710 }
12711 (Some((lm, _, logits)), None) => {
12712 logits.resize(b * lm.1, 0.0);
12713 if !graph.read_logits(logits) {
12714 return MetalRowsRun::Failed;
12715 }
12716 spec_stamp("v.lg");
12717 }
12718 (None, _) => {}
12719 }
12720 if !graph.read_hidden(hiddens) {
12721 return MetalRowsRun::Failed;
12722 }
12723 spec_stamp("v.hid");
12724 MetalRowsRun::Completed(MetalVerifyPending {
12725 graph,
12726 gdn_layers,
12727 attn_layers,
12728 })
12729 }
12730
12731 #[cfg(target_os = "macos")]
12737 fn try_batch_graph_metal(
12738 &mut self,
12739 hiddens: &mut [f32],
12740 positions: &[usize],
12741 b: usize,
12742 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12743 argmax_out: Option<(usize, &mut Vec<u32>)>,
12744 ) -> crate::gpu::BatchGraphOutcome {
12745 let _t0 = std::time::Instant::now();
12746 if positions.len() != b
12747 || positions.windows(2).any(|w| w[1] != w[0] + 1)
12748 || hiddens.len() != b * self.hidden_size
12749 {
12750 return crate::gpu::BatchGraphOutcome::Declined;
12751 }
12752 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12753 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12754 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12755 MetalRowsRun::Completed(pending) => pending,
12756 };
12757 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12758 eprintln!(
12759 "metal-verify: {:.1} ms | b={b}",
12760 _t0.elapsed().as_secs_f64() * 1e3
12761 );
12762 }
12763 self.metal_verify = Some(pending);
12764 crate::gpu::BatchGraphOutcome::Completed
12765 }
12766
12767 #[cfg(target_os = "macos")]
12772 fn prefill_rows_metal(
12773 &mut self,
12774 ids: &[u32],
12775 start_pos: usize,
12776 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12777 ) -> MetalPrefillOutcome {
12778 let b = ids.len();
12779 if b == 0 || b > 512 {
12780 return MetalPrefillOutcome::Declined;
12781 }
12782 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12783 let with_head = spec.is_some();
12784 let hs = self.hidden_size;
12785 let mut hiddens = vec![0f32; b * hs];
12786 for (j, &id) in ids.iter().enumerate() {
12787 let e = self.embed_single(id);
12788 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12789 }
12790 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12791 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12792 MetalRowsRun::Failed => {
12793 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12794 return MetalPrefillOutcome::Failed;
12795 }
12796 MetalRowsRun::Completed(pending) => pending,
12797 };
12798 let idxs = pending.gdn_layers.clone();
12800 let mut outs: Vec<&mut [f32]> = self
12801 .kv_cache
12802 .layers
12803 .iter_mut()
12804 .enumerate()
12805 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12806 .map(|(_, l)| l.linear_state.as_mut_slice())
12807 .collect();
12808 if !pending.graph.finish_states(&mut outs) {
12809 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12810 return MetalPrefillOutcome::Failed;
12811 }
12812 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12813 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12817 for (li, cpu_stored) in &pending.attn_layers {
12818 let mut kbuf = vec![0f32; b * nkv * hd];
12819 let mut vbuf = vec![0f32; b * nkv * hd];
12820 if !crate::gpu_metal::kv_mirror_read_rows(
12821 self.graph_kv_id,
12822 *li,
12823 nkv,
12824 hd,
12825 *cpu_stored,
12826 b,
12827 &mut kbuf,
12828 &mut vbuf,
12829 ) {
12830 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12831 return MetalPrefillOutcome::Failed;
12832 }
12833 rows.push((*li, *cpu_stored, kbuf, vbuf));
12834 }
12835 for (li, cpu_stored, kbuf, vbuf) in rows {
12836 let cache = &mut self.kv_cache.layers[li];
12837 for r in 0..b {
12838 cache.append(
12839 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12840 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12841 &[],
12842 );
12843 }
12844 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12845 }
12846 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12847 if with_head {
12848 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12849 }
12850 MetalPrefillOutcome::Completed(hiddens)
12851 }
12852
12853 #[cfg(target_os = "macos")]
12854 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12855 self.prefill_rows_metal(ids, start_pos, None)
12856 }
12857
12858 #[cfg(target_os = "macos")]
12863 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12864 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12865 return MetalBatchNllOutcome::Declined;
12866 }
12867 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12868 return MetalBatchNllOutcome::Declined;
12869 };
12870 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12871 .ok()
12872 .and_then(|v| v.parse::<usize>().ok())
12873 .filter(|&v| (1..=512).contains(&v))
12874 .unwrap_or(32);
12875 let final_norm = self.weights.final_norm.clone();
12876 let mut nll = 0.0f64;
12877 let mut count = 0usize;
12878 let mut pos = 0usize;
12879 let mut completed = 0usize;
12880 while pos < ids.len() {
12881 let end = (pos + chunk).min(ids.len());
12882 let mut logits = Vec::new();
12883 let outcome = self.prefill_rows_metal(
12884 &ids[pos..end],
12885 pos,
12886 Some((lm, &final_norm, &mut logits)),
12887 );
12888 match outcome {
12889 MetalPrefillOutcome::Declined => {
12890 return if completed == 0 {
12891 MetalBatchNllOutcome::Declined
12892 } else {
12893 MetalBatchNllOutcome::Failed(format!(
12894 "ordinary Metal NLL batch declined after {completed} chunks"
12895 ))
12896 };
12897 }
12898 MetalPrefillOutcome::Failed => {
12899 return MetalBatchNllOutcome::Failed(
12900 "ordinary Metal NLL batch failed after admission".to_string(),
12901 );
12902 }
12903 MetalPrefillOutcome::Completed(_) => {}
12904 }
12905 completed += 1;
12906 let vocab = self.vocab_size.min(lm.1);
12907 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
12908 return MetalBatchNllOutcome::Failed(
12909 "ordinary Metal NLL head returned an invalid shape".to_string(),
12910 );
12911 }
12912 for row in 0..(end - pos) {
12913 let absolute = pos + row;
12914 if absolute < start || absolute + 1 >= ids.len() {
12915 continue;
12916 }
12917 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
12918 if let Some(mu) = self.logit_multiplier {
12919 for v in lg.iter_mut() {
12920 *v *= mu;
12921 }
12922 }
12923 if let Some(c) = self.final_softcap {
12924 softcap_in_place(lg, c, self.pool.as_deref());
12925 }
12926 let target = ids[absolute + 1] as usize;
12927 if target >= vocab {
12928 return MetalBatchNllOutcome::Failed(format!(
12929 "target token {target} exceeds Metal head rows {vocab}"
12930 ));
12931 }
12932 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
12933 let lse: f64 = lg
12934 .iter()
12935 .map(|&v| ((v - max) as f64).exp())
12936 .sum::<f64>()
12937 .ln()
12938 + max as f64;
12939 nll += lse - lg[target] as f64;
12940 count += 1;
12941 }
12942 pos = end;
12943 }
12944 MetalBatchNllOutcome::Completed(nll, count)
12945 }
12946
12947 #[cfg(target_os = "macos")]
12951 fn metal_verify_commit(&mut self, a: usize) -> bool {
12952 let Some(mut pending) = self.metal_verify.take() else {
12953 return false;
12954 };
12955 let n = a + 1;
12956 let idxs = pending.gdn_layers.clone();
12958 let mut outs: Vec<&mut [f32]> = self
12959 .kv_cache
12960 .layers
12961 .iter_mut()
12962 .enumerate()
12963 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12964 .map(|(_, l)| l.linear_state.as_mut_slice())
12965 .collect();
12966 if !pending.graph.commit(n, &mut outs) {
12967 return false;
12968 }
12969 spec_stamp("c.replay");
12970 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12971 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12975 for (li, cpu_stored) in &pending.attn_layers {
12976 let mut kbuf = vec![0f32; n * nkv * hd];
12977 let mut vbuf = vec![0f32; n * nkv * hd];
12978 if !crate::gpu_metal::kv_mirror_read_rows(
12979 self.graph_kv_id,
12980 *li,
12981 nkv,
12982 hd,
12983 *cpu_stored,
12984 n,
12985 &mut kbuf,
12986 &mut vbuf,
12987 ) {
12988 return false;
12989 }
12990 rows.push((*li, *cpu_stored, kbuf, vbuf));
12991 }
12992 for (li, cpu_stored, kbuf, vbuf) in rows {
12993 let cache = &mut self.kv_cache.layers[li];
12994 for r in 0..n {
12995 cache.append(
12996 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12997 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12998 &[],
12999 );
13000 }
13001 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
13002 }
13003 spec_stamp("c.kv");
13004 true
13005 }
13006
13007 #[cfg(target_os = "macos")]
13014 fn mtp_warm_batch_submit(
13015 &mut self,
13016 m: &mut MtpModule,
13017 pairs: &[(&[f32], u32)],
13018 first_pos: usize,
13019 ) -> Option<MetalWarmPending> {
13020 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
13021 let b = pairs.len();
13022 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
13023 return None;
13024 }
13025 let AttnKind::Full {
13026 wq,
13027 wk,
13028 wv,
13029 wo,
13030 q_norm,
13031 k_norm,
13032 output_gate,
13033 softplus_gate: None,
13034 bias: None,
13035 } = &m.layer.attn
13036 else {
13037 return None;
13038 };
13039 let FfnKind::Dense(d) = &m.layer.ffn else {
13040 return None;
13041 };
13042 if !d.segs.is_empty() {
13043 return None;
13044 }
13045 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13046 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13047 else {
13048 return None;
13049 };
13050 let (Some(g), Some(u), Some(dn)) = (
13051 d.gate_proj.q1_parts(),
13052 d.up_proj.q1_parts(),
13053 d.down_proj.q1_parts(),
13054 ) else {
13055 return None;
13056 };
13057 let Some(eh) = m.eh_proj.q1_parts() else {
13058 return None;
13059 };
13060 let QTensor::Mapped { model, .. } = wq else {
13061 return None;
13062 };
13063 let model = model.clone();
13064 let hs = self.hidden_size;
13065 let mut cat = vec![0f32; b * 2 * hs];
13067 for (j, (h, tok)) in pairs.iter().enumerate() {
13068 let e = self.embed_single(*tok);
13069 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
13070 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
13071 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
13072 }
13073 let dims = GraphDims {
13074 hidden: hs,
13075 eps: self.rms_eps as f32,
13076 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13077 };
13078 spec_stamp("w.cat");
13079 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
13080 return None;
13081 };
13082 spec_stamp("w.new");
13083 let l = AttnGpuLayer {
13084 attn_norm: &m.layer.input_norm,
13085 post_norm: &m.layer.post_norm,
13086 wq: pq,
13087 wk: pk,
13088 wv: pv,
13089 wo: po,
13090 ffn: MetalFfn::Dense {
13091 gate: g,
13092 up: u,
13093 down: dn,
13094 act: (d.act == Act::Gelu) as u8,
13095 },
13096 attn_out_norm: None,
13097 ffn_out_norm: None,
13098 layer_scale: None,
13099 };
13100 let (nh, nkv, hd, rd) = (
13101 self.num_heads,
13102 self.num_kv_heads,
13103 self.head_dim,
13104 self.rotary_dim,
13105 );
13106 let inv_freq = self.inv_freq.clone();
13107 let cpu_stored;
13108 {
13109 let cache = &m.kv;
13110 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13111 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13112 cpu_stored = cpu_k[0].len() / hd;
13113 if cpu_stored > first_pos {
13118 spec_stamp("w.decl");
13119 return None;
13120 }
13121 let p = AttnDeviceParams {
13122 kv_id: self.mtp_kv_id(),
13123 layer: Self::MTP_LAYER_BASE,
13124 nh,
13125 nkv,
13126 hd,
13127 rd,
13128 position: first_pos,
13129 scale: self.attn_scale,
13130 eps: self.rms_eps as f32,
13131 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13132 late_qk_norm: self.qk_norm_after_rope,
13133 output_gate: *output_gate,
13134 q_norm: q_norm.as_deref(),
13135 k_norm: k_norm.as_deref(),
13136 inv_freq: &inv_freq,
13137 rope_scale: self.rope_scale,
13138 cpu_k,
13139 cpu_v,
13140 cpu_stored,
13141 cpu_gen: cache.generation(),
13142 o1: None,
13143 window: None,
13144 head_gate: None,
13145 v_norm: false,
13146 };
13147 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
13148 return None;
13149 }
13150 }
13151 spec_stamp("w.enc");
13152 if !graph.submit() {
13153 return None;
13154 }
13155 spec_stamp("w.sub");
13156 Some(MetalWarmPending {
13157 graph,
13158 cpu_stored,
13159 b,
13160 })
13161 }
13162
13163 #[cfg(target_os = "macos")]
13166 fn mtp_warm_batch_metal(
13167 &mut self,
13168 m: &mut MtpModule,
13169 pairs: &[(&[f32], u32)],
13170 first_pos: usize,
13171 ) -> bool {
13172 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
13173 Some(p) => self.mtp_warm_batch_finish(m, p),
13174 None => false,
13175 }
13176 }
13177
13178 #[cfg(target_os = "macos")]
13183 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
13184 let MetalWarmPending {
13185 mut graph,
13186 cpu_stored,
13187 b,
13188 } = pending;
13189 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13190 if !graph.sync() {
13191 return false;
13192 }
13193 spec_stamp("w.gpu");
13194 let mut kbuf = vec![0f32; b * nkv * hd];
13195 let mut vbuf = vec![0f32; b * nkv * hd];
13196 if !crate::gpu_metal::kv_mirror_read_rows(
13197 self.mtp_kv_id(),
13198 Self::MTP_LAYER_BASE,
13199 nkv,
13200 hd,
13201 cpu_stored,
13202 b,
13203 &mut kbuf,
13204 &mut vbuf,
13205 ) {
13206 return false;
13207 }
13208 for r in 0..b {
13209 m.kv.append(
13210 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13211 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13212 &[],
13213 );
13214 }
13215 crate::gpu_metal::kv_mirror_set_stored(
13216 self.mtp_kv_id(),
13217 Self::MTP_LAYER_BASE,
13218 cpu_stored + b,
13219 );
13220 spec_stamp("w.kv");
13221 true
13222 }
13223
13224 pub(crate) fn note_draft_id(&mut self, id: u32) {
13231 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
13232 if (id as usize) >= cut {
13233 self.draft_full_streak = 16;
13234 } else {
13235 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
13236 }
13237 }
13238
13239 fn draft_head_rows(&self, head_rows: usize) -> usize {
13242 if self.draft_full_streak > 0 {
13243 head_rows
13244 } else {
13245 Self::draft_vocab_rows(head_rows)
13246 }
13247 }
13248
13249 fn draft_vocab_rows(head_rows: usize) -> usize {
13252 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13253 let n = *N.get_or_init(|| {
13254 std::env::var("CMF_DRAFT_VOCAB")
13255 .ok()
13256 .and_then(|v| v.parse().ok())
13257 .unwrap_or(65536)
13258 });
13259 if n == 0 { head_rows } else { n.min(head_rows) }
13260 }
13261
13262 #[cfg(target_os = "macos")]
13267 fn mtp_step_metal(
13268 &mut self,
13269 m: &mut MtpModule,
13270 hidden: &[f32],
13271 next_token: u32,
13272 position: usize,
13273 want_logits: bool,
13274 ) -> Option<(Vec<f32>, Vec<f32>)> {
13275 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13276 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13277 || !crate::gpu::q1_force()
13278 || !crate::gpu::enabled_here()
13279 || self.attn_softcap > 0.0
13280 || self.attention_heads_per_layer.is_some()
13281 || m.kv.mode != crate::kv_cache::KvMode::F32
13282 || m.kv.o1.is_some()
13283 {
13284 return None;
13285 }
13286 let AttnKind::Full {
13287 wq,
13288 wk,
13289 wv,
13290 wo,
13291 q_norm,
13292 k_norm,
13293 output_gate,
13294 softplus_gate: None,
13295 bias: None,
13296 } = &m.layer.attn
13297 else {
13298 return None;
13299 };
13300 let FfnKind::Dense(d) = &m.layer.ffn else {
13301 return None;
13302 };
13303 if d.act != Act::Silu || !d.segs.is_empty() {
13304 return None;
13305 }
13306 let (pq, pk, pv, po) = (
13307 wq.q1_parts()?,
13308 wk.q1_parts()?,
13309 wv.q1_parts()?,
13310 wo.q1_parts()?,
13311 );
13312 let (g, u, dn) = (
13313 d.gate_proj.q1_parts()?,
13314 d.up_proj.q1_parts()?,
13315 d.down_proj.q1_parts()?,
13316 );
13317 let QTensor::Mapped { model, .. } = wq else {
13318 return None;
13319 };
13320 let model = model.clone();
13321 let lm = if want_logits {
13322 Some(self.weights.lm_head.q1_parts()?)
13323 } else {
13324 None
13325 };
13326 let dims = GraphDims {
13327 hidden: self.hidden_size,
13328 eps: self.rms_eps as f32,
13329 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13330 };
13331 let hs = self.hidden_size;
13334 let mut x = vec![0f32; hs];
13335 let mut graph = TokenGraph::new(&model, dims, &x)?;
13336 let mut folded = false;
13337 if let Some(eh) = m.eh_proj.q1_parts() {
13338 let e = self.embed_single(next_token);
13339 let mut cat = vec![0.0f32; 2 * hs];
13340 let (cat_e, cat_h) = cat.split_at_mut(hs);
13341 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13342 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13343 folded = graph.encode_input_proj(eh, &cat);
13344 }
13345 if !folded {
13346 x = self.mtp_block_input(m, hidden, next_token);
13347 graph = TokenGraph::new(&model, dims, &x)?;
13348 }
13349 spec_stamp("d.in");
13350 let l = AttnGpuLayer {
13351 attn_norm: &m.layer.input_norm,
13352 post_norm: &m.layer.post_norm,
13353 wq: pq,
13354 wk: pk,
13355 wv: pv,
13356 wo: po,
13357 ffn: MetalFfn::Dense {
13358 gate: g,
13359 up: u,
13360 down: dn,
13361 act: (d.act == Act::Gelu) as u8,
13362 },
13363 attn_out_norm: None,
13364 ffn_out_norm: None,
13365 layer_scale: None,
13366 };
13367 let (nh, nkv, hd, rd) = (
13368 self.num_heads,
13369 self.num_kv_heads,
13370 self.head_dim,
13371 self.rotary_dim,
13372 );
13373 let inv_freq = self.inv_freq.clone();
13374 {
13375 let cache = &m.kv;
13376 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13377 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13378 let cpu_stored = cpu_k[0].len() / hd;
13379 let p = AttnDeviceParams {
13380 kv_id: self.mtp_kv_id(),
13381 layer: Self::MTP_LAYER_BASE,
13382 nh,
13383 nkv,
13384 hd,
13385 rd,
13386 position,
13387 scale: self.attn_scale,
13388 eps: self.rms_eps as f32,
13389 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13390 late_qk_norm: self.qk_norm_after_rope,
13391 output_gate: *output_gate,
13392 q_norm: q_norm.as_deref(),
13393 k_norm: k_norm.as_deref(),
13394 inv_freq: &inv_freq,
13395 rope_scale: self.rope_scale,
13396 cpu_k,
13397 cpu_v,
13398 cpu_stored,
13399 cpu_gen: cache.generation(),
13400 o1: None,
13401 window: None,
13402 head_gate: None,
13403 v_norm: false,
13404 };
13405 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13406 return None;
13407 }
13408 }
13409 let draft_rows = if let Some(lm) = lm {
13415 self.draft_head_rows(lm.1)
13416 } else {
13417 0
13418 };
13419 if let Some(lm) = lm {
13420 if !graph.lm_head_ok(lm) {
13421 return None;
13422 }
13423 if draft_rows < lm.1 {
13424 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13425 return None;
13426 }
13427 } else {
13428 graph.encode_lm_head(&m.final_norm, lm);
13429 }
13430 }
13431 spec_stamp("d.enc");
13432 if graph.sync_checked().is_err() {
13433 return None;
13434 }
13435 spec_stamp("d.gpu");
13436 let mut logits = Vec::new();
13437 if let Some(lm) = lm {
13438 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13439 logits = attention::take_buf(n_read);
13440 graph.read_logits(&mut logits);
13441 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13443 }
13444 graph.finish(&mut x);
13445 let mut krow = attention::take_buf(nkv * hd);
13446 let mut vrow = attention::take_buf(nkv * hd);
13447 if crate::gpu_metal::kv_mirror_read_last(
13448 self.mtp_kv_id(),
13449 Self::MTP_LAYER_BASE,
13450 nkv,
13451 hd,
13452 &mut krow,
13453 &mut vrow,
13454 ) {
13455 m.kv.append(&krow, &vrow, &[]);
13456 }
13457 attention::recycle_buf(&mut krow);
13458 attention::recycle_buf(&mut vrow);
13459 spec_stamp("d.rd");
13460 Some((logits, x))
13461 }
13462
13463 fn mtp_chain_on() -> bool {
13476 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13477 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13478 }
13479
13480 #[cfg(target_os = "macos")]
13492 fn mtp_draft_chain_metal(
13493 &mut self,
13494 m: &mut MtpModule,
13495 hidden: &[f32],
13496 t_next: u32,
13497 position: usize,
13498 k: usize,
13499 ) -> Result<Vec<u32>, bool> {
13500 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13501 if k == 0
13502 || k > 64
13503 || !Self::mtp_chain_on()
13504 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13505 || !crate::gpu::q1_force()
13506 || !crate::gpu::enabled_here()
13507 || self.attn_softcap > 0.0
13508 || self.attention_heads_per_layer.is_some()
13509 || m.kv.mode != crate::kv_cache::KvMode::F32
13510 || m.kv.o1.is_some()
13511 || self.dsv4.is_some()
13513 || self.dsv41.is_some()
13514 || self.qwen4_exp.is_some()
13515 || self.g3n.is_some()
13516 {
13517 return Err(false);
13518 }
13519 let AttnKind::Full {
13520 wq,
13521 wk,
13522 wv,
13523 wo,
13524 q_norm,
13525 k_norm,
13526 output_gate,
13527 softplus_gate: None,
13528 bias: None,
13529 } = &m.layer.attn
13530 else {
13531 return Err(false);
13532 };
13533 let FfnKind::Dense(d) = &m.layer.ffn else {
13534 return Err(false);
13535 };
13536 if d.act != Act::Silu || !d.segs.is_empty() {
13537 return Err(false);
13538 }
13539 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13540 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13541 else {
13542 return Err(false);
13543 };
13544 let (Some(g), Some(u), Some(dn)) = (
13545 d.gate_proj.q1_parts(),
13546 d.up_proj.q1_parts(),
13547 d.down_proj.q1_parts(),
13548 ) else {
13549 return Err(false);
13550 };
13551 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13552 return Err(false);
13553 };
13554 let QTensor::Mapped { model, .. } = wq else {
13555 return Err(false);
13556 };
13557 let model = model.clone();
13558 let QTensor::Mapped {
13561 model: em,
13562 idx: eidx,
13563 dtype: cortiq_core::TensorDtype::Q4TiledP,
13564 ..
13565 } = &self.weights.embed_tokens
13566 else {
13567 return Err(false);
13568 };
13569 if !std::sync::Arc::ptr_eq(em, &model)
13570 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13571 {
13572 return Err(false);
13573 }
13574 let embed = (
13575 *eidx,
13576 self.weights.embed_tokens.rows(),
13577 self.weights.embed_tokens.cols(),
13578 );
13579 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13580 return Err(false);
13581 }
13582 let dims = GraphDims {
13583 hidden: self.hidden_size,
13584 eps: self.rms_eps as f32,
13585 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13586 };
13587 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13588 return Err(false);
13589 };
13590 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13591 return Err(false);
13592 }
13593 let l = AttnGpuLayer {
13594 attn_norm: &m.layer.input_norm,
13595 post_norm: &m.layer.post_norm,
13596 wq: pq,
13597 wk: pk,
13598 wv: pv,
13599 wo: po,
13600 ffn: MetalFfn::Dense {
13601 gate: g,
13602 up: u,
13603 down: dn,
13604 act: (d.act == Act::Gelu) as u8,
13605 },
13606 attn_out_norm: None,
13607 ffn_out_norm: None,
13608 layer_scale: None,
13609 };
13610 let (nh, nkv, hd, rd) = (
13611 self.num_heads,
13612 self.num_kv_heads,
13613 self.head_dim,
13614 self.rotary_dim,
13615 );
13616 let inv_freq = self.inv_freq.clone();
13617 let draft_rows = self.draft_head_rows(lm.1);
13618 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13619 if n_arg == 0 {
13620 return Err(false);
13621 }
13622 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13629 let t_chain = std::time::Instant::now();
13630 graph.chain_ids_init(t_next, k);
13631 let cpu_stored;
13632 {
13633 let cache = &m.kv;
13634 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13635 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13636 cpu_stored = cpu_k[0].len() / hd;
13637 for j in 0..k {
13638 if !graph.encode_chain_input(
13639 embed,
13640 j as u32,
13641 &m.enorm,
13642 &m.hnorm,
13643 self.embed_multiplier,
13644 eh,
13645 ) {
13646 return Err(false);
13647 }
13648 let p = AttnDeviceParams {
13652 kv_id: self.mtp_kv_id(),
13653 layer: Self::MTP_LAYER_BASE,
13654 nh,
13655 nkv,
13656 hd,
13657 rd,
13658 position: position + j,
13659 scale: self.attn_scale,
13660 eps: self.rms_eps as f32,
13661 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13662 late_qk_norm: self.qk_norm_after_rope,
13663 output_gate: *output_gate,
13664 q_norm: q_norm.as_deref(),
13665 k_norm: k_norm.as_deref(),
13666 inv_freq: &inv_freq,
13667 rope_scale: self.rope_scale,
13668 cpu_k: cpu_k.clone(),
13669 cpu_v: cpu_v.clone(),
13670 cpu_stored: cpu_stored + j,
13671 cpu_gen: cache.generation(),
13672 o1: None,
13673 window: None,
13674 head_gate: None,
13675 v_norm: false,
13676 };
13677 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13678 return Err(false);
13679 }
13680 if draft_rows < lm.1 {
13681 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13682 return Err(false);
13683 }
13684 } else {
13685 graph.encode_lm_head(&m.final_norm, lm);
13686 }
13687 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13688 return Err(false);
13689 }
13690 if split {
13691 graph.commit();
13694 }
13695 }
13696 }
13697 let t_enc = t_chain.elapsed();
13698 if graph.sync_checked().is_err() {
13699 return Err(true);
13700 }
13701 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
13702 eprintln!(
13703 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
13704 t_enc.as_secs_f64() * 1e3,
13705 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
13706 if split { ", split" } else { "" }
13707 );
13708 }
13709 let mut ids = vec![0u32; k];
13710 if !graph.chain_ids_read(&mut ids) {
13711 return Err(true);
13712 }
13713 let mut kbuf = vec![0f32; k * nkv * hd];
13714 let mut vbuf = vec![0f32; k * nkv * hd];
13715 if !crate::gpu_metal::kv_mirror_read_rows(
13716 self.mtp_kv_id(),
13717 Self::MTP_LAYER_BASE,
13718 nkv,
13719 hd,
13720 cpu_stored,
13721 k,
13722 &mut kbuf,
13723 &mut vbuf,
13724 ) {
13725 return Err(true);
13726 }
13727 for r in 0..k {
13728 m.kv.append(
13729 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13730 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13731 &[],
13732 );
13733 }
13734 Ok(ids)
13735 }
13736
13737 fn try_batch_graph_wgpu(
13738 &self,
13739 hiddens: &mut [f32],
13740 positions: &[usize],
13741 k: usize,
13742 spec: Option<crate::gpu::SpecTail<'_>>,
13743 ) -> crate::gpu::BatchGraphOutcome {
13744 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
13745 }
13746
13747 fn try_batch_graph_wgpu_prefix(
13752 &self,
13753 hiddens: &mut [f32],
13754 positions: &[usize],
13755 k: usize,
13756 spec: Option<crate::gpu::SpecTail<'_>>,
13757 layers_run: Option<&mut usize>,
13758 ) -> crate::gpu::BatchGraphOutcome {
13759 let graph_end = match self.mimo_moe.graph_prefix_end() {
13760 Some(end) if end < self.num_layers => {
13761 if layers_run.is_none() || spec.is_some() || end == 0 {
13762 return crate::gpu::BatchGraphOutcome::Declined;
13763 }
13764 end
13765 }
13766 _ => self.num_layers,
13767 };
13768 let _tb = std::time::Instant::now();
13769 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13770 if self.attn_softcap > 0.0 {
13771 return crate::gpu::BatchGraphOutcome::Declined; }
13773 if let Some(reason) = self
13776 .wgpu_graph_attn_decline()
13777 .or_else(|| self.batch_graph_gemma_decline())
13778 {
13779 self.note_graph_decline("wgpu batch graph", reason);
13780 return crate::gpu::BatchGraphOutcome::Declined;
13781 }
13782 let nh = self.num_heads;
13783 let (nkv, hd, rd) = self.layer_geom(0);
13784 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13785 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13786 if let Some((m, i, kind, rs)) = t
13787 .graph_weight()
13788 .or_else(|| t.graph_weight_descriptor())
13789 {
13790 let name = &m.tensors[i].name;
13791 let prism = if crate::prism::is_inverse_embedding(m, name) {
13792 crate::gpu::GraphPrismOp::InverseEmbedding
13793 } else if crate::prism::is_forward_weight(m, name) {
13794 crate::gpu::GraphPrismOp::Forward
13795 } else {
13796 crate::gpu::GraphPrismOp::None
13797 };
13798 return Some(crate::gpu::GraphW {
13799 idx: i,
13800 kind,
13801 row_scale: rs,
13802 data: &[],
13803 prism,
13804 affine: crate::prism::is_affine_target(m, name),
13805 });
13806 }
13807 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13808 eprintln!(
13809 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13810 t.rows(),
13811 t.cols()
13812 );
13813 }
13814 t.as_f32().map(|d| crate::gpu::GraphW {
13815 idx: 0,
13816 kind: 4,
13817 row_scale: &[],
13818 data: d,
13819 prism: crate::gpu::GraphPrismOp::None,
13820 affine: false,
13821 })
13822 }
13823 let built: Option<(
13824 Vec<crate::gpu::GraphLayer<'_>>,
13825 std::sync::Arc<cortiq_core::CmfModel>,
13826 )> = (|| {
13827 let mut layers = Vec::with_capacity(graph_end);
13828 let mut model = None;
13829 for li in 0..graph_end {
13830 let lw = &self.weights.layers[self.phys_layer(li)];
13831 let gffn = match &lw.ffn {
13838 FfnKind::Dense(d) if !d.segs.is_empty() => {
13839 if batch_debug {
13840 eprintln!("batch graph: dense segmented FFN at layer {li}");
13841 }
13842 return None;
13843 }
13844 FfnKind::Dense(d) => {
13845 let Some(act) = d.act.graph_act() else {
13846 if batch_debug {
13847 eprintln!(
13848 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13849 d.act
13850 );
13851 }
13852 return None;
13853 };
13854 crate::gpu::GraphFfn::Dense {
13855 gate: gw(&d.gate_proj)?,
13856 up: gw(&d.up_proj)?,
13857 down: gw(&d.down_proj)?,
13858 act,
13859 }
13860 }
13861 FfnKind::Moe(m) => {
13862 if m.route_tau.is_some() || m.mask.is_some() {
13869 return None;
13870 }
13871 let shared = m.shared.as_ref();
13875 let has_shared = shared.is_some();
13876 let shared_gated = matches!(shared, Some((_, Some(_))));
13877 let sgate = match shared {
13878 Some((_, Some(sg))) => gw(sg)?,
13879 _ => gw(&m.router)?,
13883 };
13884 let router = gw(&m.router)?;
13885 if router.prism != crate::gpu::GraphPrismOp::None
13891 || router.affine
13892 || sgate.prism != crate::gpu::GraphPrismOp::None
13893 || sgate.affine
13894 {
13895 return None;
13896 }
13897 let inter = m.experts.first()?.gate_proj.rows();
13898 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13899 let mut q4tp: Option<bool> = None;
13900 let mut gu_q2: Option<bool> = None;
13901 let mut q82: Option<bool> = None;
13902 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
13903 if !matches!(e.act, Act::Silu)
13904 || e.gate_proj.rows() != inter
13905 || e.up_proj.rows() != inter
13906 {
13907 return None;
13908 }
13909 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
13913 {
13914 Some((mm, gi)) => (
13915 mm,
13916 gi,
13917 e.up_proj.mapped_q4t()?.1,
13918 e.down_proj.mapped_q4t()?.1,
13919 false,
13920 false,
13921 false,
13922 ),
13923 None => match e.gate_proj.mapped_q2tp() {
13924 Some((mm, gi)) => (
13925 mm,
13926 gi,
13927 e.up_proj.mapped_q2tp()?.1,
13928 e.down_proj.mapped_q4tp()?.1,
13929 true,
13930 true,
13931 false,
13932 ),
13933 None => match e.gate_proj.mapped_q4tp() {
13934 Some((mm, gi)) => (
13935 mm,
13936 gi,
13937 e.up_proj.mapped_q4tp()?.1,
13938 e.down_proj.mapped_q4tp()?.1,
13939 true,
13940 false,
13941 false,
13942 ),
13943 None => {
13944 if !crate::gpu::moe_q82_graph_on() {
13945 return None;
13946 }
13947 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13948 (
13949 mm,
13950 gi,
13951 e.up_proj.mapped_q8_2f()?.1,
13952 e.down_proj.mapped_q8_2f()?.1,
13953 false,
13954 false,
13955 true,
13956 )
13957 }
13958 },
13959 },
13960 };
13961 if *q4tp.get_or_insert(is_p) != is_p
13962 || *gu_q2.get_or_insert(is_q2) != is_q2
13963 || *q82.get_or_insert(is_8) != is_8
13964 {
13965 return None;
13966 }
13967 if [gi, ui, di].into_iter().any(|idx| {
13968 mm.tensors
13969 .get(idx)
13970 .is_some_and(|t| {
13971 crate::prism::is_forward_weight(mm, &t.name)
13972 || crate::prism::is_affine_target(mm, &t.name)
13973 })
13974 }) {
13975 return None;
13976 }
13977 model.get_or_insert_with(|| mm.clone());
13978 experts.push((gi, ui, di));
13979 }
13980 crate::gpu::GraphFfn::Moe {
13981 router,
13982 shared_gate: sgate,
13983 experts,
13984 n_exp: m.experts.len(),
13985 top_k: m.top_k,
13986 inter,
13987 norm_topk: m.norm_topk_prob,
13988 q4tp: q4tp?,
13989 gu_q2: gu_q2.unwrap_or(false),
13990 q82: q82.unwrap_or(false),
13991 sigmoid: m.router_sigmoid,
13992 bias: m.expert_bias.as_deref(),
13993 has_shared,
13994 shared_gated,
13995 route_scale: m.routed_scaling,
13996 }
13997 }
13998 _ => return None,
13999 };
14000 let attn = match &lw.attn {
14001 AttnKind::Full {
14002 wq,
14003 wk,
14004 wv,
14005 wo,
14006 q_norm,
14007 k_norm,
14008 output_gate,
14009 softplus_gate,
14010 bias,
14011 } => {
14012 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
14013 if batch_debug {
14014 eprintln!(
14015 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
14016 softplus_gate.is_some(),
14017 self.attention_heads_per_layer.is_some()
14018 );
14019 }
14020 return None;
14021 }
14022 let (m, _, _, _) = wq
14023 .graph_weight()
14024 .or_else(|| wq.graph_weight_descriptor())?;
14025 model = Some(m.clone());
14026 crate::gpu::GraphAttn::Full {
14027 wq: gw(wq)?,
14028 wk: gw(wk)?,
14029 wv: gw(wv)?,
14030 wo: gw(wo)?,
14031 q_norm: q_norm.as_deref(),
14032 k_norm: k_norm.as_deref(),
14033 late_qk_norm: self.qk_norm_after_rope,
14034 bias: bias
14035 .as_ref()
14036 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14037 output_gate: *output_gate,
14038 cpu_k: self.kv_cache.layers[li].k_heads(),
14039 cpu_v: self.kv_cache.layers[li].v_heads(),
14040 cpu_base: self.kv_cache.layers[li].base(),
14041 geom: self.graph_attn_geom(li),
14042 head_gate: None,
14045 }
14046 }
14047 AttnKind::LinearGdn(w) => {
14048 let Some(cfg) = self.gdn_cfg else {
14049 if batch_debug {
14050 eprintln!("batch graph: no GDN config at layer {li}");
14051 }
14052 return None;
14053 };
14054 let (m, _, _, _) = w
14055 .in_proj_qkv
14056 .graph_weight()
14057 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
14058 model = Some(m.clone());
14059 crate::gpu::GraphAttn::Gdn {
14060 qkv: gw(&w.in_proj_qkv)?,
14061 z: gw(&w.in_proj_z)?,
14062 a: gw(&w.in_proj_a)?,
14063 b: gw(&w.in_proj_b)?,
14064 out: gw(&w.out_proj)?,
14065 conv1d: &w.conv1d,
14066 a_log: &w.a_log,
14067 dt_bias: &w.dt_bias,
14068 norm: &w.norm,
14069 nv: cfg.num_v_heads,
14070 nk: cfg.num_k_heads,
14071 dk: cfg.key_head_dim,
14072 dv: cfg.value_head_dim,
14073 kk: cfg.conv_kernel,
14074 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14075 }
14076 }
14077 _ => return None,
14078 };
14079 let g4 = self.global_attn.is_some() || self.attn_v_norm;
14082 layers.push(crate::gpu::GraphLayer {
14083 input_norm: &lw.input_norm,
14084 attn,
14085 post_norm: &lw.post_norm,
14086 attn_out_norm: if g4 { lw.attn_out_norm.as_deref() } else { None },
14087 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
14088 layer_scale: if g4 { lw.layer_scale } else { None },
14089 ffn: gffn,
14090 });
14091 }
14092 Some((layers, model?))
14093 })();
14094 let Some((layers, model)) = built else {
14095 {
14096 use std::sync::atomic::{AtomicBool, Ordering};
14097 static SAID: AtomicBool = AtomicBool::new(false);
14098 if !SAID.swap(true, Ordering::Relaxed) {
14099 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
14100 }
14101 }
14102 return crate::gpu::BatchGraphOutcome::Declined;
14103 };
14104 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14105 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
14106 }
14107 crate::gpu::forward_batch_graph(
14108 &model,
14109 self.graph_kv_id,
14110 &layers,
14111 &self.inv_freq,
14112 hiddens,
14113 nh,
14114 nkv,
14115 hd,
14116 rd,
14117 self.hidden_size,
14118 self.intermediate_size,
14119 positions,
14120 self.kv_cache.max_seq_len,
14121 gemma,
14122 self.rms_eps as f32,
14123 self.attn_scale,
14124 k,
14125 &(0..graph_end)
14126 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
14127 .collect::<Vec<_>>(),
14128 self.o1_epoch,
14129 spec,
14130 layers_run,
14131 )
14132 }
14133
14134 fn draft_probe() -> bool {
14138 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14139 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
14140 }
14141
14142 #[cfg(feature = "gpu")]
14154 fn dsv4_spec_on() -> bool {
14155 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14156 *ON.get_or_init(|| {
14157 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
14161 return v != "0";
14162 }
14163 std::env::var("CMF_DSV4_SPEC")
14170 .map(|v| v != "0")
14171 .unwrap_or_else(|_| {
14172 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
14173 })
14174 })
14175 }
14176
14177 #[cfg(feature = "gpu")]
14184 fn dsv4_spec_step(
14185 &mut self,
14186 tip_token: u32,
14187 t_next: u32,
14188 next_pos: usize,
14189 max_extra: usize,
14190 drafted: &mut usize,
14191 accepted_ctr: &mut usize,
14192 ) -> Option<(Vec<u32>, usize)> {
14193 let t_all = std::time::Instant::now();
14194 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14195 thread_local! {
14196 static LAST: std::cell::Cell<Option<std::time::Instant>> =
14197 const { std::cell::Cell::new(None) };
14198 }
14199 LAST.with(|l| {
14200 if let Some(prev) = l.get() {
14201 eprintln!(
14202 "между раундами {:.1} мс",
14203 prev.elapsed().as_secs_f64() * 1e3
14204 );
14205 }
14206 l.set(Some(std::time::Instant::now()));
14207 });
14208 }
14209 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14210 eprintln!("spec_step: вход pos={next_pos}");
14211 }
14212 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
14213 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
14214 if self.dspark.is_none() {
14216 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14217 if t.is_empty() {
14218 return None;
14219 }
14220 crate::dsv4::dspark_arm(&t, cfg.dim);
14221 self.dspark = Some(crate::dsv4::DsparkState::new(
14222 self.dsv4_mtp.len(),
14223 &cfg,
14224 t.len(),
14225 ));
14226 }
14227 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14228 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
14229 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
14230 eprintln!("spec_step: пак не построился (targets {targets:?})");
14231 }
14232 let pack = pack?;
14233 let block = crate::dsv4::dspark_block();
14234 let b_box = self.dsv4.as_mut()?;
14235 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
14236 let ds = self.dspark.as_mut()?;
14237 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14240 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14241 if dbg {
14242 eprintln!("spec_step: нет захвата");
14243 }
14244 return None;
14245 }
14246 ds.have_hidden = true;
14247 let tip_pos = next_pos.checked_sub(1)?;
14248 let draft_started = std::time::Instant::now();
14249 let mut conf = Vec::new();
14250 let props = crate::dsv4::dspark_draft_gpu(
14251 g,
14252 &self.dsv4_mtp,
14253 &cfg,
14254 ds,
14255 pack,
14256 st.kv_id,
14257 tip_token,
14258 tip_pos,
14259 self.pool.as_deref(),
14260 &mut conf,
14261 );
14262 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14263 *drafted += block;
14264 if props.is_empty() || props[0] != t_next {
14265 if dbg {
14266 eprintln!(
14267 "spec_step: черновик {} (props0={:?} t_next={t_next})",
14268 if props.is_empty() {
14269 "пуст"
14270 } else {
14271 "мимо"
14272 },
14273 props.first()
14274 );
14275 }
14276 return None;
14277 }
14278 let mut k_verify = crate::dsv4::dspark_verify_k()
14285 .min(props.len())
14286 .min(max_extra.saturating_add(1));
14287 let conf_min = {
14293 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14294 *M.get_or_init(|| {
14295 std::env::var("CMF_DSPARK_CONF_MIN")
14296 .ok()
14297 .and_then(|v| v.parse().ok())
14298 .unwrap_or(0.0)
14299 })
14300 };
14301 if conf_min > 0.0 && conf.len() >= props.len() {
14302 let mut keep = 1usize;
14303 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14304 keep += 1;
14305 }
14306 k_verify = k_verify.min(keep.max(2));
14307 }
14308 if k_verify < 2 {
14309 return None;
14310 }
14311 let mut fed = Vec::with_capacity(k_verify);
14312 fed.push(t_next);
14313 fed.extend_from_slice(&props[1..k_verify]);
14314 let mut argmax = Vec::new();
14315 let mut logits_all = Vec::new();
14316 let mut walked = Vec::new();
14317 let txn = crate::dsv4::dsv4_verify_chunk(
14318 g,
14319 layers,
14320 &cfg,
14321 st,
14322 &fed,
14323 next_pos,
14324 &self.inv_freq,
14325 self.pool.as_deref(),
14326 &targets,
14327 &mut argmax,
14328 &mut logits_all,
14329 &mut walked,
14330 );
14331 if txn.is_none() && dbg {
14332 eprintln!("spec_step: verify отказал");
14333 }
14334 let txn = txn?;
14335 let spec_gpu_end = txn.gpu_end;
14336 let b = fed.len();
14337 let mut accepted = 1usize;
14338 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14339 accepted += 1;
14340 }
14341 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14346 accepted = 1;
14347 }
14348 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14349 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14350 }
14351 let t_fin = std::time::Instant::now();
14352 if !crate::dsv4::dsv4_spec_finish(
14353 g,
14354 layers,
14355 &cfg,
14356 st,
14357 txn,
14358 accepted,
14359 &fed,
14360 &self.inv_freq,
14361 self.pool.as_deref(),
14362 ) {
14363 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14364 return None;
14365 }
14366 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14367 eprintln!(
14368 "finish(k={accepted}): {:.1} мс",
14369 t_fin.elapsed().as_secs_f64() * 1e3
14370 );
14371 }
14372 *accepted_ctr += accepted - 1;
14373 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14378 let dev_caps: Vec<usize> = targets
14383 .iter()
14384 .copied()
14385 .filter(|&t| t < spec_gpu_end)
14386 .collect();
14387 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14388 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14389 return None;
14390 }
14391 for t in 0..accepted {
14392 let tip = t + 1 == accepted;
14393 for (slot, &tl) in targets.iter().enumerate() {
14394 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14395 let lo = (di * b + t) * hc * dim;
14396 crate::dsv4::dspark_capture(
14397 &caps_all[lo..lo + hc * dim],
14398 &cfg,
14399 slot,
14400 &mut ds.main_hidden,
14401 );
14402 } else if tip
14403 && crate::dsv4::dspark_peek_slot(slot, dim, {
14404 let lo = slot * dim;
14405 &mut ds.main_hidden[lo..lo + dim]
14406 })
14407 {
14408 } else {
14413 crate::dsv4::dspark_capture(
14417 &walked[t * hc * dim..(t + 1) * hc * dim],
14418 &cfg,
14419 slot,
14420 &mut ds.main_hidden,
14421 );
14422 }
14423 }
14424 crate::dsv4::dspark_ring_append(
14425 g,
14426 &self.dsv4_mtp,
14427 &cfg,
14428 ds,
14429 next_pos + t,
14430 self.pool.as_deref(),
14431 );
14432 }
14433 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14434 self.graph_logits = Some(row);
14435 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14440 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14441 crate::dsv4::pick_tally_arm();
14442 }
14443 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14444 eprintln!(
14445 "spec_step total {:.1} мс (k={accepted})",
14446 t_all.elapsed().as_secs_f64() * 1e3
14447 );
14448 }
14449 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14450 }
14451
14452 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14453 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14454 return;
14455 }
14456 let trunk_now = crate::dsv4::pick_tally_take();
14458 crate::dsv4::trunk_freq_note(&trunk_now);
14459 if !trunk_now.is_empty() {
14460 self.dspark_trunk_picks.push(trunk_now);
14461 let keep = crate::dsv4::dspark_block();
14462 if self.dspark_trunk_picks.len() > keep {
14463 self.dspark_trunk_picks.remove(0);
14464 }
14465 }
14466 for p in std::mem::take(&mut self.dspark_pending) {
14469 let Some(i) = position.checked_sub(p.0 + 1) else {
14470 continue;
14471 };
14472 let mut p = p;
14473 if i < p.1.len() {
14474 if p.2 && p.1[i] == token_id {
14475 p.3 = i + 1;
14476 } else {
14477 p.2 = false;
14478 }
14479 if i + 1 < p.1.len() {
14480 self.dspark_pending.push(p);
14481 continue;
14482 }
14483 }
14484 self.dspark_hist.push(p.3);
14485 self.dspark_real.push(token_id);
14486 }
14487 let Some(b) = &mut self.dsv4 else { return };
14488 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14489 let n_layers = layers.len();
14490 if self.dspark.is_none() {
14491 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14492 if t.is_empty() {
14493 return;
14494 }
14495 eprintln!(
14496 "DSpark: захват со слоёв {t:?}, блок {}",
14497 crate::dsv4::dspark_block()
14498 );
14499 crate::dsv4::dspark_arm(&t, cfg.dim);
14500 self.dspark = Some(crate::dsv4::DsparkState::new(
14501 self.dsv4_mtp.len(),
14502 &cfg,
14503 t.len(),
14504 ));
14505 }
14506 let ds = self.dspark.as_mut().unwrap();
14507 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14508 return; }
14510 let mut conf = Vec::new();
14511 crate::dsv4::pick_tally_arm();
14512 let draft_started = std::time::Instant::now();
14517 #[cfg(feature = "gpu")]
14518 let gpu_draft = crate::dsv4::dspark_gpu_on();
14519 #[cfg(not(feature = "gpu"))]
14520 let gpu_draft = false;
14521 let props = if gpu_draft {
14522 #[cfg(feature = "gpu")]
14523 {
14524 let kv_id = b.3.kv_id;
14525 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14526 Some(pk) => crate::dsv4::dspark_draft_gpu(
14527 g,
14528 &self.dsv4_mtp,
14529 &cfg,
14530 ds,
14531 pk,
14532 kv_id,
14533 token_id,
14534 position,
14535 self.pool.as_deref(),
14536 &mut conf,
14537 ),
14538 None => Vec::new(),
14539 }
14540 }
14541 #[cfg(not(feature = "gpu"))]
14542 Vec::new()
14543 } else {
14544 crate::gpu::cpu_scope(|| {
14545 crate::dsv4::dspark_draft(
14546 g,
14547 &self.dsv4_mtp,
14548 &cfg,
14549 ds,
14550 token_id,
14551 position,
14552 self.pool.as_deref(),
14553 &mut conf,
14554 )
14555 })
14556 };
14557 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14558 let draft_picks = crate::dsv4::pick_tally_take();
14559 crate::dsv4::dspark_freq_note(&draft_picks);
14560 crate::dsv4::pick_tally_arm();
14563 if !props.is_empty() {
14564 let (tu, tt) = {
14568 let flat: Vec<(usize, Vec<usize>)> = self
14569 .dspark_trunk_picks
14570 .iter()
14571 .flat_map(|v| v.iter().cloned())
14572 .collect();
14573 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14575 std::collections::HashMap::new();
14576 for (li, picks) in flat {
14577 per.entry(li).or_default().extend(picks);
14578 }
14579 let n = per.len().max(1);
14580 let mut u = 0usize;
14581 let mut t = 0usize;
14582 for (_, v) in per {
14583 t += v.len();
14584 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14585 }
14586 (u / n, t / n)
14587 };
14588 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14589 self.dspark_exp.push((tu, tt, du, dt));
14590 self.dspark_pending.push((position, props, true, 0));
14591 }
14592 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14593 let n = self.dspark_hist.len() as f32;
14594 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14595 let block = crate::dsv4::dspark_block();
14596 let mut at = vec![0usize; block + 1];
14597 for &k in &self.dspark_hist {
14598 at[k] += 1;
14599 }
14600 let mut surv = Vec::with_capacity(block);
14602 for i in 1..=block {
14603 let k = at[i..].iter().sum::<usize>() as f32 / n;
14604 surv.push(format!("{k:.2}"));
14605 }
14606 let distinct = self
14607 .dspark_real
14608 .iter()
14609 .collect::<std::collections::HashSet<_>>()
14610 .len();
14611 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14612 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14613 });
14614 let m = self.dspark_exp.len().max(1);
14615 eprintln!(
14616 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14617 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14618 self.dspark_hist.len(),
14619 mean + 1.0,
14620 surv.join(" ")
14621 );
14622 eprintln!(
14623 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14624 эксперты ствол {}/{} на слой за {block} токенов, \
14625 черновик {}/{} за блок, draft {:.2} мс/блок",
14626 self.dspark_real.len(),
14627 tu / m,
14628 tt / m,
14629 du / m,
14630 dt / m,
14631 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14632 );
14633 }
14634 }
14635
14636 fn forward_layers_upto(
14637 &mut self,
14638 hidden: &[f32],
14639 position: usize,
14640 task_mask: Option<&TaskMask>,
14641 upto: Option<usize>,
14642 ) -> Vec<f32> {
14643 if let Some(plan) = self.gpu_plan.clone() {
14649 if upto.is_none() && plan.len() > 1 {
14650 let mut h = hidden.to_vec();
14651 for &(dev, from, upto_incl) in plan.iter() {
14652 h = crate::gpu::with_device(dev, || {
14653 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14654 });
14655 }
14656 return h;
14657 }
14658 }
14659 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14660 }
14661
14662 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14667 self.set_gpu_plan_at(devices, None)
14668 }
14669
14670 pub fn set_gpu_plan_at(
14674 &mut self,
14675 devices: Option<&[usize]>,
14676 at: Option<usize>,
14677 ) -> Result<(), String> {
14678 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14679 self.gpu_plan = None;
14680 return Ok(());
14681 };
14682 self.split_supported()?;
14683 let n = self.num_layers;
14684 if devs.len() > n {
14685 return Err(format!("{} devices for {n} layers", devs.len()));
14686 }
14687 if let Some(k) = at {
14688 if k == 0 || k >= n {
14689 return Err(format!("split at {k}: the model has {n} layers"));
14690 }
14691 if devs.len() == 2 {
14692 self.gpu_plan = Some(std::sync::Arc::new(vec![
14693 (devs[0], 0, k - 1),
14694 (devs[1], k, n - 1),
14695 ]));
14696 return Ok(());
14697 }
14698 return Err(format!(
14699 "an explicit split point takes exactly 2 devices, got {}",
14700 devs.len()
14701 ));
14702 }
14703 let per = n.div_ceil(devs.len());
14704 let mut plan = Vec::with_capacity(devs.len());
14705 let mut from = 0usize;
14706 for &d in devs {
14707 if from >= n {
14708 break;
14709 }
14710 let upto = (from + per - 1).min(n - 1);
14711 plan.push((d, from, upto));
14712 from = upto + 1;
14713 }
14714 self.gpu_plan = Some(std::sync::Arc::new(plan));
14715 Ok(())
14716 }
14717
14718 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
14720 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
14721 }
14722
14723 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
14729 if let Some(x) = t.as_f32() {
14730 return x.to_vec();
14731 }
14732 let mut out = vec![0.0; t.rows() * t.cols()];
14733 for r in 0..t.rows() {
14734 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
14735 }
14736 out
14737 }
14738
14739 fn embryo_resident_eligible(&self) -> bool {
14740 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
14743 || self.num_layers != self.physical_layers
14744 || self.loop_final_norm
14745 || self.weights.layers.len() != self.num_layers
14746 || self.head_clusters.is_none()
14747 || self.final_softcap.is_some()
14748 || self.logit_multiplier.is_some()
14749 || self.attn_softcap != 0.0
14750 || self.mtp.is_some()
14751 || self.g3n.is_some()
14752 || self.dsv4.is_some()
14753 || self.dsv41.is_some()
14754 || self.qwen4_exp.is_some()
14755 || self.dyn_router.is_some()
14762 || self.dyn_phi_layer.is_some()
14763 || self.dyn_blend_loaded
14764 || self.o1_cfg.is_some()
14765 || self.swa.is_some()
14766 || self.sliding_layers.is_some()
14767 || self.global_attn.is_some()
14768 || self.attention_heads_per_layer.is_some()
14769 || self.attn_v_norm
14770 || self
14771 .kv_cache
14772 .layers
14773 .iter()
14774 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
14775 || self.rope_scale != 1.0
14776 || self.rope_scale_local != 1.0
14777 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
14778 || self.hidden_size == 0
14779 || self.hidden_size > 1024
14780 || self.intermediate_size > 1024
14781 || self.num_heads == 0
14782 || self.num_kv_heads == 0
14783 || self.num_heads % self.num_kv_heads != 0
14784 || self.num_heads.saturating_mul(self.head_dim) > 1024
14785 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
14786 || self.vocab_size == 0
14787 || self.kv_cache.max_seq_len == 0
14788 || self.rotary_dim == 0
14789 || self.rotary_dim > self.head_dim
14790 || self.rotary_dim % 2 != 0
14791 || self.inv_freq.len() < self.rotary_dim / 2
14792 {
14793 return false;
14794 }
14795 if self.weights.lm_head.as_f32().is_none()
14806 || self.weights.embed_tokens.as_f32().is_none()
14807 || self.weights.lm_head.rows() < self.vocab_size
14808 || self.weights.lm_head.cols() != self.hidden_size
14809 || self.weights.embed_tokens.rows() < self.vocab_size
14810 || self.weights.embed_tokens.cols() != self.hidden_size
14811 || self.weights.final_norm.len() != self.hidden_size
14812 {
14813 return false;
14814 }
14815 if let Some(cfg) = self.vmf_cfg {
14816 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14817 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14818 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14819 || cfg.state_len() == 0
14820 {
14821 return false;
14822 }
14823 }
14824 if let Some(g) = self.gdn_cfg {
14825 if g.num_v_heads == 0
14830 || g.num_k_heads == 0
14831 || g.num_v_heads % g.num_k_heads != 0
14832 || g.key_head_dim == 0
14833 || g.key_head_dim > 128
14834 || g.value_head_dim == 0
14835 || g.value_head_dim > 256
14836 || g.value_head_dim % 4 != 0
14837 || g.conv_kernel == 0
14838 || g.num_v_heads > 512
14839 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14840 || g.conv_dim() > 2048
14841 || g.conv_dim() % 4 != 0
14842 || g.hidden_size != self.hidden_size
14843 || g.output_gate_sigmoid
14844 || g.rms_eps != self.rms_eps
14845 || g.state_len() == 0
14846 {
14847 return false;
14848 }
14849 }
14850 let mut full_seen = false;
14851 for lw in &self.weights.layers {
14852 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14853 return false;
14854 }
14855 match &lw.attn {
14856 AttnKind::LinearGdn(w) => {
14857 let Some(g) = self.gdn_cfg else {
14858 return false;
14859 };
14860 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14861 if w.in_proj_qkv.rows() != g.conv_dim()
14862 || w.in_proj_qkv.cols() != self.hidden_size
14863 || w.in_proj_qkv.as_f32().is_none()
14864 || w.in_proj_z.rows() != nv * dv
14865 || w.in_proj_z.cols() != self.hidden_size
14866 || w.in_proj_z.as_f32().is_none()
14867 || w.in_proj_a.rows() != nv
14868 || w.in_proj_a.cols() != self.hidden_size
14869 || w.in_proj_a.as_f32().is_none()
14870 || w.in_proj_b.rows() != nv
14871 || w.in_proj_b.cols() != self.hidden_size
14872 || w.in_proj_b.as_f32().is_none()
14873 || w.conv1d.len() != g.conv_dim() * kk
14874 || w.a_log.len() != nv
14875 || w.dt_bias.len() != nv
14876 || w.norm.len() != dv
14877 || w.out_proj.rows() != self.hidden_size
14878 || w.out_proj.cols() != nv * dv
14879 || w.out_proj.as_f32().is_none()
14880 {
14881 return false;
14882 }
14883 }
14884 AttnKind::Linear(w) => {
14885 let Some(cfg) = self.vmf_cfg else {
14886 return false;
14887 };
14888 if w.thq.rows() != cfg.num_heads * cfg.nphase
14889 || w.thq.cols() != self.hidden_size
14890 || w.thq.as_f32().is_none()
14891 || w.thk.rows() != cfg.num_heads * cfg.nphase
14892 || w.thk.cols() != self.hidden_size
14893 || w.thk.as_f32().is_none()
14894 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14895 || w.v_proj.cols() != self.hidden_size
14896 || w.v_proj.as_f32().is_none()
14897 || w.out_proj.rows() != self.hidden_size
14898 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14899 || w.out_proj.as_f32().is_none()
14900 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14901 {
14902 return false;
14903 }
14904 if let Some((kg, kb)) = &w.k_gate {
14905 if kg.rows() != cfg.num_heads
14906 || kg.cols() != self.hidden_size
14907 || kg.as_f32().is_none()
14908 || kb.len() != cfg.num_heads
14909 {
14910 return false;
14911 }
14912 }
14913 if let Some(conv) = &w.conv {
14914 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
14915 return false;
14916 }
14917 }
14918 }
14919 AttnKind::Full {
14920 wq,
14921 wk,
14922 wv,
14923 wo,
14924 q_norm,
14925 k_norm,
14926 output_gate,
14927 softplus_gate,
14928 bias,
14929 } => {
14930 if full_seen
14931 || q_norm.is_some()
14932 || k_norm.is_some()
14933 || *output_gate
14934 || softplus_gate.is_some()
14935 || bias.is_some()
14936 || wq.as_f32().is_none()
14937 || wk.as_f32().is_none()
14938 || wv.as_f32().is_none()
14939 || wo.as_f32().is_none()
14940 || wq.rows() != self.num_heads * self.head_dim
14941 || wk.rows() != self.num_kv_heads * self.head_dim
14942 || wv.rows() != self.num_kv_heads * self.head_dim
14943 || wq.cols() != self.hidden_size
14944 || wk.cols() != self.hidden_size
14945 || wv.cols() != self.hidden_size
14946 || wo.rows() != self.hidden_size
14947 || wo.cols() != self.num_heads * self.head_dim
14948 {
14949 return false;
14950 }
14951 full_seen = true;
14952 }
14953 AttnKind::Bounded(w) => {
14954 let Some(ac) = self.anchor_core.as_ref() else {
14957 return false;
14958 };
14959 if self.bounded_rope.is_none()
14960 || w.window != ac.window
14961 || w.sink != ac.sink
14962 || w.window == 0
14963 || w.window + w.sink > 256
14964 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
14965 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
14966 || w.wq.as_f32().is_none()
14967 || w.wk.as_f32().is_none()
14968 || w.wv.as_f32().is_none()
14969 || w.wo.as_f32().is_none()
14970 || w.wq.rows() != self.num_heads * self.head_dim
14971 || w.wk.rows() != self.num_kv_heads * self.head_dim
14972 || w.wv.rows() != self.num_kv_heads * self.head_dim
14973 || w.wq.cols() != self.hidden_size
14974 || w.wk.cols() != self.hidden_size
14975 || w.wv.cols() != self.hidden_size
14976 || w.wo.rows() != self.hidden_size
14977 || w.wo.cols() != self.num_heads * self.head_dim
14978 {
14979 return false;
14980 }
14981 }
14982 _ => return false,
14983 }
14984 match &lw.ffn {
14985 FfnKind::Dense(d) => {
14986 if d.act != Act::Silu
14987 || !d.segs.is_empty()
14988 || d.gate_proj.as_f32().is_none()
14989 || d.up_proj.as_f32().is_none()
14990 || d.down_proj.as_f32().is_none()
14991 || d.gate_proj.rows() != self.intermediate_size
14992 || d.gate_proj.cols() != self.hidden_size
14993 || d.up_proj.rows() != self.intermediate_size
14994 || d.up_proj.cols() != self.hidden_size
14995 || d.down_proj.rows() != self.hidden_size
14996 || d.down_proj.cols() != self.intermediate_size
14997 {
14998 return false;
14999 }
15000 }
15001 FfnKind::Moe(m) => {
15002 if m.resonance.is_none()
15003 || m.top_k != 1
15004 || m.router_sigmoid
15005 || !m.norm_topk_prob
15006 || m.expert_bias.is_some()
15007 || m.routed_scaling != 1.0
15008 || m.route_tau.is_some()
15009 || m.shared.is_none()
15010 || m.mask.is_some()
15011 || m.per_expert_scale.is_some()
15012 || m.router_input_norm
15013 || m.experts.is_empty()
15014 || m.experts.len() > 8
15015 {
15016 return false;
15017 }
15018 let r = m.resonance.as_ref().unwrap();
15019 if r.mu.len() != m.experts.len() * self.hidden_size
15020 || r.bias.len() != m.experts.len()
15021 || r.u.len() != m.experts.len() * r.k * self.hidden_size
15022 || r.k > 128
15023 {
15024 return false;
15025 }
15026 let Some((shared, gate)) = &m.shared else {
15027 return false;
15028 };
15029 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
15030 return false;
15031 }
15032 if shared.gate_proj.as_f32().is_none()
15033 || shared.up_proj.as_f32().is_none()
15034 || shared.down_proj.as_f32().is_none()
15035 || shared.gate_proj.rows() != self.intermediate_size
15036 || shared.gate_proj.cols() != self.hidden_size
15037 || shared.up_proj.rows() != self.intermediate_size
15038 || shared.up_proj.cols() != self.hidden_size
15039 || shared.down_proj.rows() != self.hidden_size
15040 || shared.down_proj.cols() != self.intermediate_size
15041 {
15042 return false;
15043 }
15044 for e in &m.experts {
15045 if e.act != Act::Silu
15046 || !e.segs.is_empty()
15047 || e.gate_proj.as_f32().is_none()
15048 || e.up_proj.as_f32().is_none()
15049 || e.down_proj.as_f32().is_none()
15050 || e.gate_proj.rows() != self.intermediate_size
15051 || e.gate_proj.cols() != self.hidden_size
15052 || e.up_proj.rows() != self.intermediate_size
15053 || e.up_proj.cols() != self.hidden_size
15054 || e.down_proj.rows() != self.hidden_size
15055 || e.down_proj.cols() != self.intermediate_size
15056 {
15057 return false;
15058 }
15059 }
15060 }
15061 FfnKind::DenseMoe(_) => return false,
15062 }
15063 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
15064 return false;
15065 }
15066 }
15067 if full_seen && self.anchor_core.is_some() {
15068 return false;
15069 }
15070 full_seen || self.num_layers > 0
15071 }
15072
15073 fn embryo_resident_wanted(&self) -> bool {
15078 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15079 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15080 && matches!(
15081 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15082 Ok("1") | Ok("parallel")
15083 )
15084 && crate::gpu::enabled_here()
15085 && !self.graph_refused()
15086 && self.embryo_resident_eligible()
15087 }
15088
15089 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
15097 if ids.len() < 2
15098 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
15099 || !self.embryo_resident_wanted()
15100 {
15101 return None;
15102 }
15103 let model = self.ensure_embryo_graph()?;
15104 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
15105 .ok()
15106 .and_then(|v| v.parse::<usize>().ok())
15107 .filter(|&v| v >= 1)
15108 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
15109 .min(crate::gpu::EMBRYO_CHUNK_MAX);
15110 let hs = self.hidden_size;
15111 let n = ids.len();
15112 let mut pos = start;
15113 let mut last = None;
15114 let mut rows = Vec::with_capacity(cmax * hs);
15115 while pos < n {
15116 let end = (pos + cmax).min(n);
15117 rows.clear();
15118 for &id in &ids[pos..end] {
15119 rows.extend_from_slice(&self.embed_single(id));
15120 }
15121 let mut lg = Vec::new();
15122 if !crate::gpu::forward_embryo_graph_chunk(
15123 &model,
15124 self.graph_kv_id,
15125 &rows,
15126 pos,
15127 end - pos,
15128 &mut lg,
15129 ) {
15130 if pos == start {
15131 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15132 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
15133 }
15134 return None;
15135 }
15136 self.kv_cache.clear();
15140 self.clear_history();
15141 crate::gpu::graph_kv_reset(self.graph_kv_id);
15142 panic!(
15143 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
15144 );
15145 }
15146 last = Some(lg);
15147 pos = end;
15148 }
15149 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15150 eprintln!(
15151 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
15152 n - start,
15153 (n - start).div_ceil(cmax)
15154 );
15155 }
15156 last
15157 }
15158
15159 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
15160 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
15161 const UMAX: u32 = u32::MAX;
15162 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
15163 const REC: usize = 64;
15164 struct Pack {
15165 data: Vec<f32>,
15166 }
15167 impl Pack {
15168 fn put(&mut self, x: &[f32]) -> u32 {
15169 if x.is_empty() {
15170 return u32::MAX;
15171 }
15172 let off = self.data.len();
15173 self.data.extend_from_slice(x);
15174 off as u32
15175 }
15176 }
15177 let vmf = self.vmf_cfg;
15181 let gdn = self.gdn_cfg;
15182 let mut pack = Pack { data: Vec::new() };
15183 let mut meta = vec![0u32; HEADER];
15184 meta[0] = self.hidden_size as u32;
15185 meta[1] = self.intermediate_size as u32;
15186 meta[2] = self.vocab_size as u32;
15187 meta[3] = self.num_layers as u32;
15188 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
15189 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
15190 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
15191 if let Some(g) = gdn {
15192 meta[24] = g.num_v_heads as u32;
15193 meta[25] = g.num_k_heads as u32;
15194 meta[26] = g.key_head_dim as u32;
15195 meta[27] = g.value_head_dim as u32;
15196 meta[28] = g.conv_kernel as u32;
15197 meta[29] = g.conv_dim() as u32;
15198 }
15199 meta[7] = self.num_heads as u32;
15200 meta[8] = self.num_kv_heads as u32;
15201 meta[9] = self.head_dim as u32;
15202 meta[10] = self.kv_cache.max_seq_len as u32;
15203 let clusters = self.head_clusters.as_ref().unwrap();
15204 let cluster_count = clusters.len() / self.hidden_size;
15205 if clusters.len() % self.hidden_size != 0
15206 || cluster_count == 0
15207 || cluster_count > 1024
15208 || self.vocab_size % cluster_count != 0
15209 || self.weights.lm_head.rows() < self.vocab_size
15210 || self.weights.final_norm.len() != self.hidden_size
15211 {
15212 return None;
15213 }
15214 meta[11] = cluster_count as u32;
15215 meta[12] = (self.vocab_size / cluster_count) as u32;
15216 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
15217 meta[16] = self.rotary_dim as u32;
15218 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
15219 meta[19] = (self.rms_eps as f32).to_bits();
15220 let max_conv = self
15221 .weights
15222 .layers
15223 .iter()
15224 .filter_map(|lw| match &lw.attn {
15225 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
15226 _ => None,
15227 })
15228 .max()
15229 .unwrap_or(1);
15230 let phase_stride = vmf
15236 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15237 .unwrap_or(0);
15238 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15239 let state_stride = phase_stride.max(gdn_stride);
15240 let bounded = self.anchor_core.clone();
15244 let (anchor_window, anchor_sink) = bounded
15245 .as_ref()
15246 .map(|ac| (ac.window, ac.sink))
15247 .unwrap_or((0, 0));
15248 let kv_stride = if bounded.is_some() {
15249 2usize
15250 .saturating_mul(self.num_kv_heads)
15251 .saturating_mul(anchor_window)
15252 .saturating_mul(self.head_dim)
15253 } else {
15254 2usize
15255 .saturating_mul(self.num_kv_heads)
15256 .saturating_mul(self.kv_cache.max_seq_len)
15257 .saturating_mul(self.head_dim)
15258 };
15259 meta[14] = state_stride as u32;
15260 meta[15] = kv_stride as u32;
15261 meta[18] = anchor_window as u32;
15262 meta[20] = anchor_sink as u32;
15263 meta[21] = match &self.bounded_rope {
15264 Some(rope) => {
15265 let off = pack.put(&rope.cos);
15267 let _ = pack.put(&rope.sin);
15268 off
15269 }
15270 None => UMAX,
15271 };
15272 let mut full_seen = false;
15273 let mut bounded_seen = 0usize;
15274 let mut phase_seen = 0usize;
15278 let mut gdn_seen = 0usize;
15279 for (li, lw) in self.weights.layers.iter().enumerate() {
15280 let base = meta.len();
15281 meta.resize(base + REC, UMAX);
15282 meta[base] = match &lw.attn {
15283 AttnKind::Linear(w) if w.phase_delta => 1,
15284 AttnKind::Linear(_) => 0,
15285 AttnKind::Full { .. } => 2,
15286 AttnKind::Bounded(_) => 3,
15287 AttnKind::LinearGdn(_) => 4,
15288 _ => UMAX,
15289 };
15290 meta[base + 1] = pack.put(&lw.input_norm);
15291 meta[base + 2] = pack.put(&lw.post_norm);
15292 meta[base + 25] = match &lw.attn {
15293 AttnKind::Linear(_) => {
15294 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15295 phase_seen += 1;
15296 off
15297 }
15298 AttnKind::LinearGdn(_) => {
15299 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15300 gdn_seen += 1;
15301 off
15302 }
15303 _ => UMAX,
15304 };
15305 match &lw.attn {
15306 AttnKind::LinearGdn(w) => {
15307 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15310 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15311 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15312 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15313 meta[base + 60] = pack.put(&w.conv1d);
15314 meta[base + 61] = pack.put(&w.a_log);
15315 meta[base + 62] = pack.put(&w.dt_bias);
15316 meta[base + 63] = pack.put(&w.norm);
15317 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15318 meta[base + 24] = 0;
15319 }
15320 AttnKind::Linear(w) => {
15321 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15322 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15323 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15324 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15325 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15326 meta[base + 7] = pack.put(&decay);
15327 if let Some((kg, kb)) = &w.k_gate {
15328 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15329 meta[base + 9] = pack.put(kb);
15330 }
15331 if let Some(conv) = &w.conv {
15332 meta[base + 10] = pack.put(conv);
15333 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15334 } else {
15335 meta[base + 24] = 0;
15336 }
15337 }
15338 AttnKind::Full { wq, wk, wv, wo, .. } => {
15339 full_seen = true;
15340 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15341 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15342 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15343 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15344 meta[base + 26] = (li * kv_stride) as u32;
15345 }
15346 AttnKind::Bounded(w) => {
15347 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15348 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15349 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15350 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15351 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15353 meta[base + 27] = pack.put(&w.sink_k);
15354 meta[base + 28] = pack.put(&w.sink_v);
15355 bounded_seen += 1;
15356 }
15357 _ => return None,
15358 }
15359 match &lw.ffn {
15360 FfnKind::Dense(d) => {
15361 meta[base + 15] = 0;
15362 meta[base + 16] = 0;
15363 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15364 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15365 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15366 }
15367 FfnKind::Moe(m) => {
15368 let r = m.resonance.as_ref().unwrap();
15369 let (shared, _) = m.shared.as_ref().unwrap();
15370 meta[base + 15] = 1;
15371 meta[base + 16] = m.experts.len() as u32;
15372 meta[base + 17] = pack.put(&r.mu);
15373 meta[base + 18] = pack.put(&r.u);
15374 meta[base + 19] = pack.put(&r.bias);
15375 meta[base + 20] = r.k as u32;
15376 let mut shell = r.effective_shell(m.experts.len());
15385 shell.push(f32::NEG_INFINITY);
15386 meta[base + 30] = pack.put(&shell);
15387 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15388 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15389 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15390 for (e, ex) in m.experts.iter().enumerate() {
15391 meta[base + 32 + e * 3] =
15392 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15393 meta[base + 33 + e * 3] =
15394 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15395 meta[base + 34 + e * 3] =
15396 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15397 }
15398 }
15399 FfnKind::DenseMoe(_) => return None,
15400 }
15401 }
15402 if !full_seen && self.num_layers == 0 {
15403 return None;
15404 }
15405 let id = {
15406 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15407 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15408 };
15409 let model = crate::gpu::EmbryoGraphModel {
15410 id,
15411 hidden: self.hidden_size,
15412 intermediate: self.intermediate_size,
15413 vocab: self.vocab_size,
15414 layers: self.num_layers,
15415 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15416 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15417 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15418 anchor_q_heads: self.num_heads,
15419 anchor_kv_heads: self.num_kv_heads,
15420 anchor_head_dim: self.head_dim,
15421 rotary_dim: self.rotary_dim,
15422 max_seq: self.kv_cache.max_seq_len,
15423 cluster_count,
15424 cluster_size: self.vocab_size / cluster_count,
15425 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15426 state_stride,
15427 kv_stride,
15428 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15429 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15430 weights: pack.data,
15431 meta,
15432 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15433 clusters: clusters.as_ref().clone(),
15434 final_norm: self.weights.final_norm.clone(),
15435 inv_freq: self.inv_freq.as_ref().clone(),
15436 bounded: bounded.is_some(),
15437 kv_layers: if bounded.is_some() {
15438 bounded_seen
15439 } else {
15440 self.num_layers
15441 },
15442 state_layers: phase_seen + gdn_seen,
15443 anchor_window,
15444 anchor_sink,
15445 phase_layers: phase_seen,
15446 gdn_layers: gdn_seen,
15447 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15448 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15449 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15450 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15451 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15452 };
15453 self.embryo_graph = Some(std::sync::Arc::new(model));
15454 }
15455 self.embryo_graph.clone()
15456 }
15457
15458 fn forward_layers_span(
15459 &mut self,
15460 hidden: &[f32],
15461 position: usize,
15462 task_mask: Option<&TaskMask>,
15463 from: usize,
15464 upto: Option<usize>,
15465 ) -> Vec<f32> {
15466 #[cfg(target_os = "macos")]
15467 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
15468 position,
15469 from,
15470 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
15471 );
15472 debug_assert!(
15473 from == 0
15474 || (self.dsv4.is_none()
15475 && self.dsv41.is_none()
15476 && self.qwen4_exp.is_none()
15477 && self.g3n.is_none())
15478 );
15479 #[cfg(target_os = "macos")]
15485 if !crate::gpu_metal::wait_replay() {
15486 self.fail_metal_graph("the pending async replay failed before a plain forward");
15487 return vec![0.0; self.hidden_size];
15488 }
15489 if let Some(b) = &mut self.qwen4_exp {
15490 let _ = (task_mask, upto);
15491 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15492 let mut logits = Vec::new();
15493 crate::qwen4_exp::forward_token(
15494 &b.0,
15495 &b.1,
15496 &b.2,
15497 &mut b.3,
15498 token_id,
15499 position,
15500 &self.inv_freq,
15501 self.pool.as_deref(),
15502 &mut logits,
15503 true,
15504 );
15505 self.graph_logits = Some(logits);
15506 return vec![0.0; self.hidden_size];
15507 }
15508 if let Some(b) = &mut self.dsv4 {
15514 let _ = (task_mask, upto);
15515 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15516 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15517 st.pos = position;
15518 let mut logits = Vec::new();
15519 crate::dsv4::forward_token(
15520 g,
15521 layers,
15522 &cfg,
15523 st,
15524 token_id,
15525 &self.inv_freq,
15526 self.pool.as_deref(),
15527 &mut logits,
15528 );
15529 self.graph_logits = Some(logits);
15530 self.dspark_probe(position, token_id);
15531 return vec![0.0; self.hidden_size];
15534 }
15535 if let Some(b) = &mut self.dsv41 {
15537 let _ = (task_mask, upto);
15538 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15539 let mut logits = Vec::new();
15540 crate::dsv41::forward_token(
15541 &b.0,
15542 &b.1,
15543 &b.2,
15544 &mut b.3,
15545 token_id,
15546 position,
15547 self.pool.as_deref(),
15548 &mut logits,
15549 );
15550 self.graph_logits = Some(logits);
15551 return vec![0.0; self.hidden_size];
15552 }
15553 if let Some(b) = &self.g3n {
15556 let _ = (task_mask, upto);
15557 return crate::g3n::g3n_forward(
15558 &b.0,
15559 &b.1,
15560 hidden,
15561 position,
15562 &mut self.kv_cache.layers,
15563 self.num_heads,
15564 self.num_kv_heads,
15565 self.head_dim,
15566 self.pool.as_deref(),
15567 );
15568 }
15569 if from == 0
15576 && upto.is_none()
15577 && task_mask.is_none()
15578 && self.anchor_core.is_some()
15579 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15580 {
15581 static ONCE: std::sync::Once = std::sync::Once::new();
15582 ONCE.call_once(|| {
15583 eprintln!(
15584 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15585 unsupported={} eligible={}",
15586 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15587 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15588 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15589 crate::gpu::enabled_here(),
15590 self.graph_refused(),
15591 self.embryo_resident_eligible(),
15592 );
15593 });
15594 }
15595 if from == 0
15596 && upto.is_none()
15597 && task_mask.is_none()
15598 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15602 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15603 && matches!(
15607 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15608 Ok("1") | Ok("parallel")
15609 )
15610 && crate::gpu::enabled_here()
15611 && !self.graph_refused()
15612 && (position == 0 || self.device_sequence_position().is_some())
15618 && self.embryo_resident_eligible()
15619 && let Some(model) = self.ensure_embryo_graph()
15620 {
15621 let mut lg = Vec::new();
15622 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15623 {
15624 self.graph_logits = Some(lg);
15625 return vec![0.0; self.hidden_size];
15626 }
15627 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15628 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15629 }
15630 self.mark_graph_refused();
15636 if position != 0 {
15637 self.kv_cache.clear();
15641 self.clear_history();
15642 crate::gpu::graph_kv_reset(self.graph_kv_id);
15643 panic!(
15644 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15645 );
15646 }
15647 }
15648 let mut h = hidden.to_vec();
15649 self.mimo_moe_prepare();
15652 let _mimo_q8 = self.mimo_moe.is_on()
15653 .then(crate::qtensor::enter_full_gpu_q8_scope);
15654 let (nh, _nkv, _hd, hs, _rd, eps) = (
15657 self.num_heads,
15658 self.num_kv_heads,
15659 self.head_dim,
15660 self.hidden_size,
15661 self.rotary_dim,
15662 self.rms_eps,
15663 );
15664 let pool = self.pool.clone();
15665 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15677 let graph_on = match graph_env.as_deref() {
15678 Some("0") => false,
15679 Some("prefill") => false, Some(_) => true,
15681 None => crate::gpu::wgpu_graph_default(),
15687 };
15688 let graph_trusted =
15689 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15690 let race_eligible = graph_on
15691 && upto.is_none()
15692 && task_mask.is_none()
15693 && from == 0
15694 && !self.graph_refused();
15695 let mut tail_start = 0usize;
15696 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15697 let t_graph = std::time::Instant::now();
15698 let mut lg = Vec::new();
15699 let mut gl = 0usize;
15700 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
15701 let declined = built.is_none();
15702 let built = match built {
15703 Some(Ok(hh)) => Some(hh),
15704 Some(Err(())) => {
15705 self.clear_sequence_state();
15709 self.graph_failed
15710 .store(true, std::sync::atomic::Ordering::Relaxed);
15711 self.cancel
15712 .store(true, std::sync::atomic::Ordering::Relaxed);
15713 tracing::error!("token graph failed after admission; sequence state cleared");
15714 return vec![0.0; self.hidden_size];
15715 }
15716 None => None,
15717 };
15718 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
15723 self.mark_graph_refused();
15724 }
15725 graph_note(built.is_some(), gl, self.num_layers);
15726 if let Some(hh) = built {
15727 let dur = t_graph.elapsed();
15728 if std::env::var("CMF_GRAPH_PROF").is_ok() {
15729 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
15730 }
15731 if gl > 0 && gl < self.num_layers {
15732 h = hh;
15738 tail_start = gl;
15739 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
15740 if !graph_trusted {
15741 crate::gpu::graph_race_record(true, dur);
15742 }
15743 if !lg.is_empty() {
15744 lg.resize(self.vocab_size, 0.0);
15747 if let Some(c) = self.final_softcap {
15748 softcap_in_place(&mut lg, c, self.pool.as_deref());
15749 }
15750 self.graph_logits = Some(lg);
15751 }
15752 return hh;
15753 }
15754 }
15760 }
15761 let span = from > 0 || upto.is_some();
15785 if span && graph_on && task_mask.is_none() && graph_trusted {
15786 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
15787 let mut lg = Vec::new();
15788 let mut gl = 0usize;
15789 let span_res =
15790 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
15791 let span_res = match span_res {
15792 Some(Ok(hh)) => Some(hh),
15793 Some(Err(())) => {
15794 self.clear_sequence_state();
15795 self.graph_failed
15796 .store(true, std::sync::atomic::Ordering::Relaxed);
15797 self.cancel
15798 .store(true, std::sync::atomic::Ordering::Relaxed);
15799 tracing::error!(
15800 "span token graph failed after admission; sequence state cleared"
15801 );
15802 return vec![0.0; self.hidden_size];
15803 }
15804 None => None,
15805 };
15806 graph_note(span_res.is_some(), gl, upto_excl - from);
15807 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15808 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15812 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15813 eprintln!(
15814 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15815 upto_excl - from,
15816 span_res.is_some()
15817 );
15818 }
15819 }
15820 if let Some(hh) = span_res {
15821 if gl == upto_excl - from {
15822 if !lg.is_empty() {
15823 lg.resize(self.vocab_size, 0.0);
15824 if let Some(c) = self.final_softcap {
15825 softcap_in_place(&mut lg, c, self.pool.as_deref());
15826 }
15827 self.graph_logits = Some(lg);
15828 }
15829 crate::gpu::set_layer(-1);
15830 return hh;
15831 }
15832 h = hh;
15834 tail_start = from + gl;
15835 }
15836 }
15837 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15842
15843 let host_tail = tail_start > from;
15853 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15854 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15855
15856 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15857 #[cfg(target_os = "macos")]
15858 let mut gpu_skip_until = 0usize;
15859 for li in tail_start.max(from)..self.num_layers {
15860 let _capacity_tail = automatic_gpu_prefix
15861 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15862 .map(|_| crate::gpu::enter_cpu_scope());
15863 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15865 if li > u {
15866 break;
15867 }
15868 }
15869 if let Some(mask) = task_mask {
15870 if !mask.layer_alive(li) {
15871 continue; }
15873 }
15874 #[cfg(target_os = "macos")]
15878 {
15879 if li < gpu_skip_until {
15880 continue;
15881 }
15882 if task_mask.is_none() {
15883 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15884 if self
15885 .graph_failed
15886 .load(std::sync::atomic::Ordering::Relaxed)
15887 {
15888 return vec![0.0; self.hidden_size];
15892 }
15893 if end > li {
15894 gpu_skip_until = end;
15895 if self.is_loop_end(end - 1) && end < self.num_layers {
15898 h = inference::rms_norm(
15899 &h,
15900 &self.weights.final_norm,
15901 self.rms_eps,
15902 self.norm_style,
15903 );
15904 }
15905 continue;
15906 }
15907 }
15908 }
15909
15910 if task_mask.is_none() {
15911 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
15912 crate::gpu::BatchGraphOutcome::Completed => continue,
15913 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
15914 crate::gpu::BatchGraphOutcome::Declined => {},
15915 }
15916 }
15917 #[cfg(feature = "gpu")]
15918 self.pull_lagging_host_kv(li, li + 1, position);
15919 let lw = &self.weights.layers[self.phys_layer(li)];
15920 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15921 if tp.parse::<usize>().ok() == Some(position) {
15922 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
15923 eprintln!(
15924 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
15925 h[0], h[1]
15926 );
15927 }
15928 }
15929 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15932 inference::rms_norm_into(
15933 &h,
15934 &lw.input_norm,
15935 self.rms_eps,
15936 self.norm_style,
15937 &mut self.ws.n1,
15938 );
15939 drop(prof);
15940
15941 let attn_out = match &lw.attn {
15942 AttnKind::Mla(w) => {
15943 let inv_freq_l = self.layer_inv_freq(li);
15944 let rs = self.layer_rope_scale(li);
15945 let eps = self.rms_eps;
15946 let pool = self.pool.clone();
15947 mla_attention(
15948 w,
15949 &self.ws.n1,
15950 &mut self.kv_cache.layers[li],
15951 position,
15952 &inv_freq_l,
15953 rs,
15954 eps,
15955 pool.as_deref(),
15956 )
15957 }
15958 AttnKind::Linear(w) => {
15959 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
15960 vmf_phase_forward(
15961 &self.ws.n1,
15962 w,
15963 &cfg,
15964 &mut self.kv_cache.layers[li].linear_state,
15965 self.pool.as_deref(),
15966 )
15967 }
15968 AttnKind::Kda(w) => {
15969 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
15970 crate::linear_core::kda_forward(
15971 &self.ws.n1,
15972 w,
15973 &cfg,
15974 &mut self.kv_cache.layers[li].linear_state,
15975 self.pool.as_deref(),
15976 )
15977 }
15978 AttnKind::LinearGdn(w) => {
15979 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
15980 gdn_forward(
15981 &self.ws.n1,
15982 w,
15983 &cfg,
15984 &mut self.kv_cache.layers[li].linear_state,
15985 self.pool.as_deref(),
15986 )
15987 }
15988 AttnKind::ShortConv(w) => {
15989 let cfg = self
15990 .short_conv_cfg
15991 .expect("short-conv layer without short_conv_cfg");
15992 short_conv_forward(
15993 &self.ws.n1,
15994 w,
15995 &cfg,
15996 &mut self.kv_cache.layers[li].linear_state,
15997 self.pool.as_deref(),
15998 )
15999 }
16000 AttnKind::Bounded(w) => {
16001 let rope = self
16004 .bounded_rope
16005 .clone()
16006 .expect("bounded layer without an installed rotation table");
16007 let cfg = crate::bounded::BoundedAttnCfg {
16008 num_heads: self.num_heads,
16009 num_kv_heads: self.num_kv_heads,
16010 head_dim: self.head_dim,
16011 hidden_size: hs,
16012 scale: self.attn_scale,
16013 rope: &rope,
16014 pool: pool.as_deref(),
16015 };
16016 crate::bounded::bounded_attention(
16017 &self.ws.n1,
16018 w,
16019 &mut self.kv_cache.layers[li],
16020 &cfg,
16021 )
16022 }
16023 AttnKind::Full {
16024 wq,
16025 wk,
16026 wv,
16027 wo,
16028 q_norm,
16029 k_norm,
16030 output_gate,
16031 softplus_gate,
16032 bias,
16033 } if self.kv_cache.layers[li].o1_sealed() => {
16034 let inv_freq_l = self.layer_inv_freq(li);
16037 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16038 let cfg = QwenAttnCfg {
16039 num_heads: self.layer_num_heads(li),
16040 num_kv_heads: nkv_l,
16041 head_dim: hd_l,
16042 hidden_size: hs,
16043 position,
16044 inv_freq: &inv_freq_l,
16045 rotary_dim: rd_l,
16046 scale: self.attn_scale,
16047 softcap: self.attn_softcap,
16048 window: None,
16049 v_norm: self.attn_v_norm,
16050 qk_norm_after_rope: self.qk_norm_after_rope,
16051 gate_sigmoid: self.proj_gate_sigmoid,
16052 q_norm: q_norm.as_deref(),
16053 k_norm: k_norm.as_deref(),
16054 output_gate: *output_gate,
16055 softplus_gate: softplus_gate
16056 .as_ref()
16057 .map(|(gate, per_head)| (gate, *per_head)),
16058 rope_scale: self.layer_rope_scale(li),
16059 bias: bias
16060 .as_ref()
16061 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16062 rms_eps: eps,
16063 norm_style: self.norm_style,
16064 pool: pool.as_deref(),
16065 v_head_dim: self.layer_v_dim(li),
16066 };
16067 attention::qwen_attention_nystrom(
16068 &self.ws.n1,
16069 wq,
16070 wk,
16071 wv,
16072 wo,
16073 &mut self.kv_cache.layers[li],
16074 &cfg,
16075 )
16076 }
16077 AttnKind::Full {
16078 wq,
16079 wk,
16080 wv,
16081 wo,
16082 q_norm,
16083 k_norm,
16084 output_gate,
16085 softplus_gate,
16086 bias,
16087 } => 'attn: {
16088 let dropin_reason =
16093 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
16094 if let Some(reason) = dropin_reason {
16095 self.note_graph_decline("wgpu attn dropin", reason);
16096 }
16097 if graph_on
16098 && dropin_reason.is_none()
16099 && !*output_gate
16100 && softplus_gate.is_none()
16101 && self.attention_heads_per_layer.is_none()
16102 && bias.is_none()
16103 && task_mask.is_none()
16104 {
16105 let inv_freq_l = self.layer_inv_freq(li);
16106 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16107 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16108 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
16109 wq.mapped_q1(),
16110 wk.mapped_q1(),
16111 wv.mapped_q1(),
16112 wo.mapped_q1(),
16113 ) {
16114 let gm = gm.clone();
16115 let mut out = vec![0f32; hs];
16116 let cache = &self.kv_cache.layers[li];
16117 if crate::gpu::attn_dropin(
16118 &gm,
16119 self.graph_kv_id,
16120 li,
16121 &self.ws.n1,
16122 qi,
16123 ki,
16124 vi,
16125 oi,
16126 q_norm.as_deref(),
16127 k_norm.as_deref(),
16128 self.qk_norm_after_rope,
16129 &inv_freq_l,
16130 nh,
16131 nkv_l,
16132 hd_l,
16133 rd_l,
16134 hs,
16135 position,
16136 self.kv_cache.max_seq_len,
16137 gemma,
16138 eps as f32,
16139 cache.k_heads(),
16140 cache.v_heads(),
16141 &mut out,
16142 ) {
16143 break 'attn out;
16144 }
16145 }
16146 }
16147 let masked = task_mask
16148 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
16149 .unwrap_or(false);
16150 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
16151 let plain = self.layer_attn_plain(li);
16154 match (masked, f32_view) {
16155 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
16158 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
16159 attention::multi_head_attention(
16160 &self.ws.n1,
16161 q,
16162 k,
16163 v,
16164 o,
16165 &mut self.kv_cache.layers[li],
16166 self.num_heads,
16167 self.num_kv_heads,
16168 self.head_dim,
16169 self.hidden_size,
16170 position,
16171 &active_heads,
16172 &self.inv_freq,
16173 )
16174 }
16175 (masked, _) => {
16176 if masked {
16177 tracing::warn!(
16178 "layer {li}: head mask on quantized weights or on a \
16179 window/sink/per-layer-geometry layer not supported \
16180 yet — executing dense"
16181 );
16182 }
16183 let inv_freq_l = self.layer_inv_freq(li);
16184 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
16185 let cfg = QwenAttnCfg {
16186 num_heads: self.layer_num_heads(li),
16187 num_kv_heads: nkv_l,
16188 head_dim: hd_l,
16189 hidden_size: hs,
16190 position,
16191 inv_freq: &inv_freq_l,
16192 rotary_dim: rd_l,
16193 scale: self.attn_scale,
16194 softcap: self.attn_softcap,
16195 window: self.layer_window(li),
16196 v_norm: self.attn_v_norm,
16197 qk_norm_after_rope: self.qk_norm_after_rope,
16198 gate_sigmoid: self.proj_gate_sigmoid,
16199 q_norm: q_norm.as_deref(),
16200 k_norm: k_norm.as_deref(),
16201 output_gate: *output_gate,
16202 softplus_gate: softplus_gate
16203 .as_ref()
16204 .map(|(gate, per_head)| (gate, *per_head)),
16205 rope_scale: self.layer_rope_scale(li),
16206 bias: bias
16207 .as_ref()
16208 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16209 rms_eps: eps,
16210 norm_style: self.norm_style,
16211 pool: pool.as_deref(),
16212 v_head_dim: self.layer_v_dim(li),
16213 };
16214 attention::qwen_attention(
16215 &self.ws.n1,
16216 wq,
16217 wk,
16218 wv,
16219 wo,
16220 &mut self.kv_cache.layers[li],
16221 &cfg,
16222 )
16223 }
16224 }
16225 }
16226 };
16227 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
16230 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
16231 None => attn_out,
16232 };
16233 let lw = &self.weights.layers[self.phys_layer(li)];
16234 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
16235 inference::add_rmsnorm_fused_into(
16236 &mut h,
16237 &attn_out,
16238 &lw.post_norm,
16239 self.rms_eps,
16240 self.norm_style,
16241 &mut self.ws.p1,
16242 );
16243 drop(prof);
16244 let mut attn_out = attn_out;
16245 attention::recycle_buf(&mut attn_out);
16246 let post_normed = &self.ws.p1;
16247
16248 let ffn_masked = task_mask
16249 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16250 .unwrap_or(false);
16251 let ffn_out = match (ffn_masked, &lw.ffn) {
16263 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16267 let row = task_mask
16268 .and_then(|tm| tm.ffn_masks.get(li))
16269 .map(|v| v.as_slice());
16270 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16271 }
16272 (true, FfnKind::Dense(d)) => {
16273 let tm = task_mask.unwrap();
16274 let alive = tm.ffn_active_count(li);
16275 let deep = alive * 2 <= self.intermediate_size;
16276 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16277 let active = tm.ffn_active_indices(li);
16278 sparse_ffn_quant(
16279 d,
16280 post_normed,
16281 &active,
16282 self.hidden_size,
16283 self.pool.as_deref(),
16284 )
16285 } else if deep
16286 && let (Some(g), Some(u), Some(dn)) = (
16287 d.gate_proj.as_f32(),
16288 d.up_proj.as_f32(),
16289 d.down_proj.as_f32(),
16290 )
16291 {
16292 let active = tm.ffn_active_indices(li);
16293 inference::sparse_ffn_forward(
16294 post_normed,
16295 g,
16296 u,
16297 dn,
16298 self.hidden_size,
16299 self.intermediate_size,
16300 &active,
16301 self.pool.as_deref(),
16302 )
16303 } else {
16304 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16305 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16306 }
16307 }
16308 (true, FfnKind::Moe(m)) => {
16309 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16313 ffn_forward(
16314 &lw.ffn,
16315 post_normed,
16316 self.pool.as_deref(),
16317 allowed.as_deref(),
16318 )
16319 }
16320 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16321 dm,
16322 post_normed,
16323 &h,
16324 self.rms_eps,
16325 self.norm_style,
16326 self.pool.as_deref(),
16327 ),
16328 (false, _) => match &lw.ffn {
16329 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16330 dm,
16331 post_normed,
16332 &h,
16333 self.rms_eps,
16334 self.norm_style,
16335 self.pool.as_deref(),
16336 ),
16337 FfnKind::Moe(m)
16338 if task_mask.is_none()
16339 && host_tail
16340 && li >= tail_start
16341 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
16342 {
16343 moe_ffn_minicpm_tail(
16344 &mut self.minicpm_tail,
16345 li,
16346 self.num_layers,
16347 m,
16348 post_normed,
16349 self.pool.as_deref(),
16350 )
16351 }
16352 FfnKind::Moe(m)
16353 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16354 {
16355 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16356 }
16357 _ => {
16358 let allowed = match (&lw.ffn, task_mask) {
16359 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16360 _ => None,
16361 };
16362 ffn_forward(
16363 &lw.ffn,
16364 post_normed,
16365 self.pool.as_deref(),
16366 allowed.as_deref(),
16367 )
16368 }
16369 },
16370 };
16371 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16372 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16373 None => ffn_out,
16374 };
16375 for (i, &f) in ffn_out.iter().enumerate() {
16376 h[i] += f;
16377 }
16378 let mut ffn_out = ffn_out;
16379 attention::recycle_buf(&mut ffn_out);
16380
16381 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16383 for v in h.iter_mut() {
16384 *v *= sc;
16385 }
16386 }
16387 if self.layer_dump.is_some() {
16389 self.dump_layer_row(position, li, &h);
16390 }
16391
16392 if self.is_loop_end(li) && li + 1 < self.num_layers {
16395 h = inference::rms_norm(
16396 &h,
16397 &self.weights.final_norm,
16398 self.rms_eps,
16399 self.norm_style,
16400 );
16401 }
16402
16403 if self.dyn_phi_layer == Some(li) {
16407 self.update_dyn_phi(&h);
16408 }
16409 }
16410 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16412 crate::gpu::graph_race_record(false, t.elapsed());
16413 }
16414
16415 h
16416 }
16417
16418 fn update_dyn_phi(&mut self, h: &[f32]) {
16421 const A: f32 = 0.2;
16422 if self.dyn_phi_ema.len() != h.len() {
16423 self.dyn_phi_ema = vec![0.0; h.len()];
16424 self.dyn_phi_seen = 0;
16425 }
16426 if self.dyn_phi_seen == 0 {
16427 self.dyn_phi_ema.copy_from_slice(h);
16428 } else {
16429 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16430 *e = (1.0 - A) * *e + A * v;
16431 }
16432 }
16433 self.dyn_phi_seen += 1;
16434 }
16435
16436 pub fn dyn_phi(&self) -> &[f32] {
16438 &self.dyn_phi_ema
16439 }
16440
16441 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16443 self.dyn_phi_layer = layer;
16444 self.dyn_phi_ema.clear();
16445 self.dyn_phi_seen = 0;
16446 }
16447
16448 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16450 let Some(model) = &self.model else {
16451 return Vec::new();
16452 };
16453 model
16454 .header
16455 .skills
16456 .iter()
16457 .enumerate()
16458 .filter_map(|(i, sk)| {
16459 if sk.is_v2() {
16463 return None;
16464 }
16465 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16466 let sel = sk.selection.as_ref()?;
16467 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16468 })
16469 .collect()
16470 }
16471
16472 pub fn active_skill(&self) -> Option<usize> {
16474 self.dyn_active
16475 }
16476
16477 pub fn enable_dynamic_routing(&mut self) -> usize {
16482 use crate::swarm::{DynRouter, RoutableSkill};
16483 let Some(model) = self.model.clone() else {
16484 return 0;
16485 };
16486 if let Some(r) = &model.header.router {
16493 tracing::warn!(
16494 "dynamic routing disabled: this file declares router policy '{}' with \
16495 granularity \"{}\" — the request-level decision applies instead",
16496 r.policy,
16497 r.granularity
16498 );
16499 return 0;
16500 }
16501 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16507 || model.header.skills.iter().any(|s| s.is_v2())
16508 {
16509 tracing::warn!(
16510 "dynamic routing disabled: this file carries format-v2 skill records \
16511 (SKILLS_V2) — they route per request through a router policy only"
16512 );
16513 return 0;
16514 }
16515 if self.dyn_blend_loaded {
16518 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16519 return 0;
16520 }
16521 if let Some(a) = self.dyn_active {
16525 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16526 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16527 return 0;
16528 }
16529 }
16530 let hidden = self.hidden_size;
16531 let mut skills = Vec::new();
16532 for (idx, id, _phi) in self.dynamic_skills() {
16533 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16534 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16535 skills.push(rs);
16536 }
16537 }
16538 }
16539 if skills.is_empty() {
16540 return 0;
16541 }
16542 let phi = skills[0].phi_layer;
16544 if skills.iter().any(|s| s.phi_layer != phi) {
16545 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16546 }
16547 let n = skills.len();
16548 self.set_dyn_phi_layer(Some(phi));
16549 self.dyn_router = Some(DynRouter::new(skills));
16550 n
16551 }
16552
16553 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16555 self.dyn_router
16556 .as_ref()
16557 .map(|r| r.switches.clone())
16558 .unwrap_or_default()
16559 }
16560
16561 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16564 let _mimo_q8 = self.mimo_moe.is_on()
16565 .then(crate::qtensor::enter_full_gpu_q8_scope);
16566 let rows = self.weights.lm_head.rows();
16567 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16568 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
16572 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
16573 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
16574 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
16575 rows, self.hidden_size, &mut logits)
16576 });
16577 if !served {
16578 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
16579 }
16580 logits.resize(self.vocab_size, 0.0);
16581 if let Some(m) = self.logit_multiplier {
16582 for l in logits.iter_mut() {
16583 *l *= m;
16584 }
16585 }
16586 if let Some(c) = self.final_softcap {
16587 softcap_in_place(&mut logits, c, self.pool.as_deref());
16588 }
16589 if let Some(cm) = self.head_clusters.as_ref() {
16590 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16591 }
16592 logits
16593 }
16594
16595 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16598 let h = hidden.len();
16599 let ncl = cm.len() / h.max(1);
16600 if ncl == 0 || logits.len() % ncl != 0 {
16601 return;
16602 }
16603 let cs = logits.len() / ncl;
16604 let mut lc = vec![0.0f32; ncl];
16606 for c in 0..ncl {
16607 let row = &cm[c * h..(c + 1) * h];
16608 let mut s = 0.0f32;
16609 for j in 0..h {
16610 s += row[j] * hidden[j];
16611 }
16612 lc[c] = s;
16613 }
16614 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16615 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16616 for c in 0..ncl {
16617 let blk = &mut logits[c * cs..(c + 1) * cs];
16618 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16619 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16620 let add = lc[c] - lse - bl;
16621 for v in blk.iter_mut() {
16622 *v += add;
16623 }
16624 }
16625 }
16626
16627 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16632 #[cfg(target_os = "macos")]
16633 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16634 self.clear_sequence_state();
16635 crate::gpu::graph_race_begin_generation();
16639 if task_mask.is_none() {
16640 self.o1_begin();
16641 }
16642 let mut hidden = vec![0.0f32; self.hidden_size];
16643 for (pos, &id) in ids.iter().enumerate() {
16644 let emb = self.embed_single(id);
16645 hidden = self.forward_layers(&emb, pos, task_mask);
16646 }
16647 if let Err(err) = self.o1_seal_checked() {
16648 self.o1_fail(err);
16649 }
16650 if let Some(logits) = self.graph_logits.take() {
16653 return logits;
16654 }
16655 inference::rms_norm_into(
16656 &hidden,
16657 &self.weights.final_norm,
16658 self.rms_eps,
16659 self.norm_style,
16660 &mut self.ws.n1,
16661 );
16662 self.lm_head_forward(&self.ws.n1)
16663 }
16664}
16665
16666pub fn create_test_pipeline(
16668 hidden_size: usize,
16669 intermediate_size: usize,
16670 num_heads: usize,
16671 num_kv_heads: usize,
16672 head_dim: usize,
16673 num_layers: usize,
16674 vocab_size: usize,
16675) -> Pipeline {
16676 let synth = |n: usize, salt: usize| -> Vec<f32> {
16679 (0..n)
16680 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16681 .collect()
16682 };
16683 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16684 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16685 };
16686 let layer_weights: Vec<LayerWeights> = (0..num_layers)
16687 .map(|li| LayerWeights {
16688 input_norm: vec![1.0; hidden_size],
16689 post_norm: vec![1.0; hidden_size],
16690 attn_out_norm: None,
16691 ffn_out_norm: None,
16692 layer_scale: None,
16693 ffn: FfnKind::Dense(DenseFfn {
16694 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
16695 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
16696 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
16697 act: Act::Silu,
16698 down_t: None,
16699 segs: Vec::new(),
16700 }),
16701 attn: AttnKind::Full {
16702 bias: None,
16703 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
16704 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
16705 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
16706 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
16707 q_norm: None,
16708 k_norm: None,
16709 output_gate: false,
16710 softplus_gate: None,
16711 },
16712 })
16713 .collect();
16714
16715 Pipeline::new(
16716 Tokenizer::byte_level(),
16717 PipelineWeights {
16718 embed_tokens: qt(vocab_size, hidden_size, 100),
16719 layers: layer_weights,
16720 lm_head: qt(vocab_size, hidden_size, 200),
16721 final_norm: vec![1.0; hidden_size],
16722 },
16723 hidden_size,
16724 intermediate_size,
16725 num_heads,
16726 num_kv_heads,
16727 head_dim,
16728 num_layers,
16729 num_layers, false, vocab_size,
16732 1e-6,
16733 10_000.0,
16734 NormStyle::Qwen,
16735 4096,
16736 SamplerConfig {
16737 seed: Some(42),
16738 ..Default::default()
16739 },
16740 )
16741}
16742
16743#[inline]
16748fn mask_bit(row: &[u8], j: usize) -> bool {
16749 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
16750}
16751
16752fn mask_gain() -> f32 {
16763 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16764 *G.get_or_init(|| {
16765 std::env::var("CMF_FFN_MASK_GAIN")
16766 .ok()
16767 .and_then(|v| v.parse().ok())
16768 .unwrap_or(1.0)
16769 })
16770}
16771
16772fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
16773 let fill = meanfill().and_then(|(i, v)| {
16776 let li = crate::gpu::cur_layer();
16777 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
16778 });
16779 for r in 0..rows {
16780 let base = r * inter;
16781 for (bi, &byte) in row.iter().enumerate() {
16782 if byte == 0xFF {
16783 continue;
16784 }
16785 let j0 = bi * 8;
16786 for bit in 0..8 {
16787 let j = j0 + bit;
16788 if j < inter && byte & (1 << bit) == 0 {
16789 g[base + j] = fill.map_or(0.0, |f| f[j]);
16790 }
16791 }
16792 }
16793 }
16794 let gain = mask_gain();
16795 if gain != 1.0 {
16796 for v in g[..rows * inter].iter_mut() {
16797 *v *= gain;
16798 }
16799 }
16800}
16801
16802#[inline]
16804fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
16805 row.is_none_or(|r| mask_bit(r, i))
16806}
16807
16808fn all_bits_on(row: &[u8], n: usize) -> bool {
16811 (0..n).all(|i| mask_bit(row, i))
16812}
16813
16814fn tube_topk() -> usize {
16822 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16823 *K.get_or_init(|| {
16824 std::env::var("CMF_TUBE_TOPK")
16825 .ok()
16826 .and_then(|v| v.parse().ok())
16827 .unwrap_or(0)
16828 })
16829}
16830
16831fn tube_score_oracle() -> bool {
16832 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16833 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16834}
16835
16836fn tube_ffn_routed(
16843 d: &DenseFfn,
16844 xs: &[f32],
16845 b: usize,
16846 pool: Option<&Pool>,
16847 mask_row: Option<&[u8]>,
16848 k: usize,
16849) -> Vec<f32> {
16850 let hidden = d.down_proj.rows();
16851 let core = d.gate_proj.rows();
16852 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16853 let mut out = match (b, core_full, mask_row) {
16854 (1, true, _) => dense_ffn(d, xs, pool),
16855 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16856 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16857 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16858 };
16859 let cand: Vec<usize> = (0..d.segs.len())
16860 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16861 .collect();
16862 if cand.is_empty() {
16863 return out;
16864 }
16865 let oracle = tube_score_oracle();
16869 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16870 let mut scores = vec![0f32; b * cand.len()];
16871 for (ci, &i) in cand.iter().enumerate() {
16872 let seg = &d.segs[i];
16873 let w = seg.width;
16874 let mut g = vec![0.0f32; b * w];
16875 if b == 1 {
16876 seg.gate.matvec(xs, &mut g, pool);
16877 } else {
16878 seg.gate.matmat(xs, b, &mut g, pool);
16879 }
16880 for v in g.iter_mut() {
16881 *v = Act::Silu.combine(*v, 1.0);
16882 }
16883 if !oracle {
16884 for t in 0..b {
16885 scores[t * cand.len() + ci] =
16886 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16887 }
16888 }
16889 if oracle || b > 1 {
16890 let mut u = vec![0.0f32; b * w];
16891 if b == 1 {
16892 seg.up.matvec(xs, &mut u, pool);
16893 } else {
16894 seg.up.matmat(xs, b, &mut u, pool);
16895 }
16896 for (a, &v) in g.iter_mut().zip(u.iter()) {
16897 *a *= v;
16898 }
16899 if oracle {
16900 for t in 0..b {
16901 scores[t * cand.len() + ci] =
16902 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16903 }
16904 }
16905 }
16906 acts.push(g);
16907 }
16908 let keep = k.min(cand.len());
16910 let mut scratch: Vec<f32> = Vec::new();
16911 for t in 0..b {
16912 let mut sc: Vec<(f32, usize)> = (0..cand.len())
16913 .map(|ci| (scores[t * cand.len() + ci], ci))
16914 .collect();
16915 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
16916 let mut alive = vec![false; cand.len()];
16917 for &(_, ci) in sc.iter().take(keep) {
16918 alive[ci] = true;
16919 }
16920 if b > 1 {
16921 for (ci, a) in acts.iter_mut().enumerate() {
16922 if !alive[ci] {
16923 let w = d.segs[cand[ci]].width;
16924 a[t * w..(t + 1) * w].fill(0.0);
16925 }
16926 }
16927 } else {
16928 for (ci, &i) in cand.iter().enumerate() {
16932 if !alive[ci] {
16933 continue;
16934 }
16935 let seg = &d.segs[i];
16936 let w = seg.width;
16937 let g = &mut acts[ci];
16938 if !tube_score_oracle() {
16939 scratch.clear();
16940 scratch.resize(w, 0.0);
16941 seg.up.matvec(xs, &mut scratch, pool);
16942 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
16943 *a *= v;
16944 }
16945 }
16946 let mut acc = vec![0.0f32; hidden];
16947 seg.down.matvec(g, &mut acc, pool);
16948 for (o, a) in out.iter_mut().zip(&acc) {
16949 *o += *a;
16950 }
16951 }
16952 }
16953 }
16954 if b > 1 {
16955 for (ci, &i) in cand.iter().enumerate() {
16956 let seg = &d.segs[i];
16957 let mut acc = vec![0.0f32; b * hidden];
16958 seg.down.matmat(&acts[ci], b, &mut acc, pool);
16959 for (o, a) in out.iter_mut().zip(&acc) {
16960 *o += *a;
16961 }
16962 }
16963 }
16964 out
16965}
16966
16967fn tube_ffn(
16973 d: &DenseFfn,
16974 xs: &[f32],
16975 b: usize,
16976 pool: Option<&Pool>,
16977 mask_row: Option<&[u8]>,
16978) -> Vec<f32> {
16979 if tube_topk() > 0 {
16980 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
16981 }
16982 let hidden = d.down_proj.rows();
16983 let core = d.gate_proj.rows();
16984 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16985 let mut out = match (b, core_full, mask_row) {
16986 (1, true, _) => dense_ffn(d, xs, pool),
16987 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16988 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16989 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16990 };
16991 TUBE_SCRATCH.with(|sc| {
16992 let mut sc = sc.borrow_mut();
16993 let [g, u, acc] = &mut *sc;
16994 for seg in &d.segs {
16995 if !tube_bit(mask_row, seg.start) {
16996 continue;
16997 }
16998 let w = seg.width;
16999 g.resize(b * w, 0.0);
17000 if b == 1
17001 && d.act == Act::Silu
17002 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
17003 {
17004 } else {
17006 u.resize(b * w, 0.0);
17007 if b == 1 {
17008 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
17009 } else {
17010 seg.gate.matmat(xs, b, g, pool);
17011 seg.up.matmat(xs, b, u, pool);
17012 }
17013 for i in 0..b * w {
17014 g[i] = d.act.combine(g[i], u[i]);
17015 }
17016 }
17017 acc.resize(b * hidden, 0.0);
17018 acc.fill(0.0);
17019 if b == 1 {
17020 seg.down.matvec(g, acc, pool);
17021 } else {
17022 seg.down.matmat(g, b, acc, pool);
17023 }
17024 for (o, a) in out.iter_mut().zip(acc.iter()) {
17025 *o += *a;
17026 }
17027 }
17028 out
17029 })
17030}
17031
17032thread_local! {
17033 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
17037 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
17038}
17039
17040static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
17043 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
17044
17045fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
17049 #[cfg(feature = "gpu")]
17050 {
17051 use std::sync::atomic::Ordering::Relaxed;
17052 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
17053 (
17054 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
17055 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
17056 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
17057 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
17058 )
17059 }
17060 #[cfg(not(feature = "gpu"))]
17061 (0.0, 0.0, 0.0, 0)
17062}
17063
17064fn wgpu_mirror_counters() -> [u64; 6] {
17067 #[cfg(feature = "gpu")]
17068 {
17069 crate::gpu_wgpu::MIRROR_EVENTS
17070 .each_ref()
17071 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
17072 }
17073 #[cfg(not(feature = "gpu"))]
17074 [0; 6]
17075}
17076
17077fn prefill_prof_on() -> bool {
17078 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17079 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
17080}
17081
17082fn dense_ffn_batch(
17083 d: &DenseFfn,
17084 xs: &[f32],
17085 b: usize,
17086 pool: Option<&Pool>,
17087 mask_row: Option<&[u8]>,
17088) -> Vec<f32> {
17089 let inter = d.gate_proj.rows();
17090 let hidden = d.down_proj.rows();
17091 let fused_act = d.act.graph_act();
17101 if mask_row.is_none()
17102 && fused_act.is_some()
17103 && b >= 32
17104 && crate::gpu::enabled_here()
17105 && !crate::gpu::mm_killed()
17106 && refit_dir().is_none()
17111 && !ffn_probe_active()
17116 {
17117 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17118 d.gate_proj.mapped_q4t(),
17119 d.up_proj.mapped_q4t(),
17120 d.down_proj.mapped_q4t(),
17121 ) {
17122 let mut out = vec![0.0f32; b * hidden];
17123 let act = fused_act.expect("checked above");
17124 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
17125 {
17126 return out;
17127 }
17128 }
17129 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17134 d.gate_proj.mapped_q4tp(),
17135 d.up_proj.mapped_q4tp(),
17136 d.down_proj.mapped_q4tp(),
17137 ) {
17138 let mut out = vec![0.0f32; b * hidden];
17139 let act = fused_act.expect("checked above");
17140 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
17141 {
17142 return out;
17143 }
17144 }
17145 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
17151 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
17152 d.gate_proj.mapped_device_gemm(),
17153 d.up_proj.mapped_device_gemm(),
17154 d.down_proj.mapped_device_gemm(),
17155 ) {
17156 let mut out = vec![0.0f32; b * hidden];
17157 let act = fused_act.expect("checked above");
17158 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
17159 return out;
17160 }
17161 }
17162 }
17163 let mut g = vec![0.0f32; b * inter];
17164 d.gate_proj.matmat(xs, b, &mut g, pool);
17165 let mut u = vec![0.0f32; b * inter];
17166 d.up_proj.matmat(xs, b, &mut u, pool);
17167 if gate_topk() > 0 && d.act == Act::Silu {
17168 for t in 0..b {
17169 let row = &mut g[t * inter..(t + 1) * inter];
17170 for v in row.iter_mut() {
17171 *v = Act::Silu.combine(*v, 1.0);
17172 }
17173 keep_top_k(row, gate_topk());
17174 }
17175 for i in 0..b * inter {
17176 g[i] *= u[i];
17177 }
17178 } else {
17179 for i in 0..b * inter {
17180 g[i] = d.act.combine(g[i], u[i]);
17181 }
17182 }
17183 if let Some(row) = mask_row {
17184 zero_masked_cols(&mut g, b, inter, row);
17185 }
17186 if oracle_topk() > 0 {
17187 for t in 0..b {
17188 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
17189 }
17190 }
17191 let mut out = vec![0.0f32; b * hidden];
17192 d.down_proj.matmat(&g, b, &mut out, pool);
17193 if refit_dir().is_some() {
17194 let li = crate::gpu::cur_layer();
17195 if li >= 0 {
17196 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
17197 }
17198 }
17199 FFN_PROBE.with(|pr| {
17203 if let Some(acc) = pr.borrow_mut().as_mut() {
17204 let li = crate::gpu::cur_layer();
17205 if li < 0 {
17206 return;
17207 }
17208 let Some(row) = acc.get_mut(li as usize) else {
17209 return;
17210 };
17211 let sq = probe_sq();
17212 for t in 0..b {
17213 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
17214 *a += if sq {
17215 (v as f64) * (v as f64)
17216 } else {
17217 (v as f64).abs()
17218 };
17219 }
17220 }
17221 }
17222 });
17223 out
17224}
17225
17226fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
17231 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17232 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17233 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
17234 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
17235 if (!on && !dump) || b == 0 {
17236 return;
17237 }
17238 let hidden = xs.len() / b;
17239 if on {
17240 let mut acc = m.act_sq.borrow_mut();
17241 if acc.len() < hidden {
17242 acc.resize(hidden, 0.0);
17243 }
17244 for t in 0..b {
17245 let row = &xs[t * hidden..(t + 1) * hidden];
17246 for (a, &v) in acc.iter_mut().zip(row) {
17247 *a += (v as f64) * (v as f64);
17248 }
17249 }
17250 }
17251 if dump {
17252 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
17255 .ok()
17256 .and_then(|v| v.parse().ok())
17257 .unwrap_or(4096);
17258 let mut rows = m.act_rows.borrow_mut();
17259 if rows.len() < cap * hidden {
17260 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17261 rows.extend_from_slice(&xs[..take * hidden]);
17262 }
17263 }
17264}
17265
17266#[derive(Clone, Copy)]
17269struct SendVecs(*mut Vec<f32>);
17270unsafe impl Send for SendVecs {}
17271unsafe impl Sync for SendVecs {}
17272impl SendVecs {
17273 #[inline]
17274 fn at(self, i: usize) -> *mut Vec<f32> {
17275 unsafe { self.0.add(i) }
17276 }
17277}
17278
17279fn moe_ffn_batch(
17280 m: &MoeFfn,
17281 xs: &[f32],
17282 b: usize,
17283 hidden: usize,
17284 pool: Option<&Pool>,
17285 allowed: Option<&[bool]>,
17286) -> Vec<f32> {
17287 accumulate_act(m, xs, b);
17288 let ne = m.experts.len();
17289 let mut logits = vec![0.0f32; b * ne];
17290 match &m.resonance {
17291 Some(r) => {
17292 let hdim = xs.len() / b.max(1);
17293 for bi in 0..b {
17294 r.scores(
17295 &xs[bi * hdim..(bi + 1) * hdim],
17296 &mut logits[bi * ne..(bi + 1) * ne],
17297 );
17298 }
17299 }
17300 None => m.router.matmat(xs, b, &mut logits, pool),
17301 }
17302
17303 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17306 {
17307 let mut st = m.stats.borrow_mut();
17308 if st.len() < ne {
17309 st.resize(ne, 0);
17310 }
17311 for bi in 0..b {
17312 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17313 for &e in &idx {
17314 st[e] += 1;
17315 assign[e].push((bi, p[e] / wsum));
17316 }
17317 }
17318 }
17319
17320 let mut out = vec![0.0f32; b * hidden];
17321 let cols = m.experts[0].gate_proj.cols();
17322 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17323 let sb = list.len();
17324 let mut sub = vec![0.0f32; sb * cols];
17325 for (k, &(bi, _)) in list.iter().enumerate() {
17326 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17327 }
17328 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17329 for (k, &(bi, w)) in list.iter().enumerate() {
17330 for i in 0..hidden {
17331 out[bi * hidden + i] += w * eo[k * hidden + i];
17332 }
17333 }
17334 };
17335 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17341 if pool.is_some() && active.len() >= 8 {
17342 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17343 {
17344 let panel_ptr = SendVecs(panels.as_mut_ptr());
17345 let experts = &m.experts;
17348 let (active_r, assign_r) = (&active, &assign);
17349 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17350 let run = |start: usize, end: usize| {
17351 let _cpu_scope = inherit_cpu();
17352 #[cfg(target_os = "macos")]
17360 let _metal_cpu = crate::gpu::enter_cpu_scope();
17361 for ai in start..end {
17362 let e = active_r[ai];
17363 let list = &assign_r[e];
17364 let sb = list.len();
17365 let mut sub = vec![0.0f32; sb * cols];
17366 for (k, &(bi, _)) in list.iter().enumerate() {
17367 sub[k * cols..(k + 1) * cols]
17368 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17369 }
17370 unsafe {
17372 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17373 }
17374 }
17375 };
17376 match pool {
17377 Some(p) => p.run_rows(active.len(), &run),
17378 None => run(0, active.len()),
17379 }
17380 }
17381 for (ai, &e) in active.iter().enumerate() {
17382 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17383 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17384 for i in 0..hidden {
17385 out[bi * hidden + i] += w * eo[i];
17386 }
17387 }
17388 }
17389 } else {
17390 for &e in &active {
17391 run_expert(&m.experts[e], &assign[e], &mut out);
17392 }
17393 }
17394 if let Some((se, gate)) = &m.shared {
17395 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17396 let mut gl = vec![0.0f32; b];
17397 gate.matmat(xs, b, &mut gl, pool);
17398 (0..b)
17399 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17400 .collect()
17401 } else {
17402 (0..b).map(|bi| (bi, 1.0)).collect()
17403 };
17404 run_expert(se, &all, &mut out);
17405 }
17406 out
17407}
17408
17409fn moe_ffn_rows_exact(
17421 m: &MoeFfn,
17422 xs: &[f32],
17423 b: usize,
17424 hidden: usize,
17425 pool: Option<&Pool>,
17426) -> Vec<f32> {
17427 let mut out = vec![0.0f32; b * hidden];
17428 let per_row = |out: &mut [f32]| {
17429 for r in 0..b {
17430 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17431 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17432 }
17433 };
17434 let covered = !crate::gpu::enabled_here()
17435 && moe_batch_enabled()
17436 && m.shared.is_none()
17437 && m.resonance.is_none()
17438 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17439 && m.experts.iter().all(|d| d.act == Act::Silu);
17440 if !covered {
17441 per_row(&mut out);
17442 return out;
17443 }
17444 let ne = m.experts.len();
17445 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17447 for r in 0..b {
17448 let x = &xs[r * hidden..(r + 1) * hidden];
17449 accumulate_act(m, x, 1);
17450 let mut logits = vec![0.0f32; ne];
17451 m.router.matvec(x, &mut logits, pool);
17452 let (idx, p, wsum) = moe_route(&logits, m, None);
17453 {
17454 let mut st = m.stats.borrow_mut();
17455 if st.len() < ne {
17456 st.resize(ne, 0);
17457 }
17458 for &e in &idx {
17459 st[e] += 1;
17460 }
17461 }
17462 let w: Vec<f32> = idx
17463 .iter()
17464 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17465 .collect();
17466 routes.push((idx, w));
17467 }
17468 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17469 per_row(&mut out);
17470 return out;
17471 }
17472 let mut experts: Vec<usize> = Vec::new();
17474 let mut groups: Vec<Vec<usize>> = Vec::new();
17475 for (r, (idx, _)) in routes.iter().enumerate() {
17476 for &e in idx {
17477 match experts.iter().position(|&x| x == e) {
17478 Some(g) => groups[g].push(r),
17479 None => {
17480 experts.push(e);
17481 groups.push(vec![r]);
17482 }
17483 }
17484 }
17485 }
17486 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17487 let inter = m.experts[experts[0]].gate_proj.rows();
17488 let pairs: Vec<(&QTensor, &QTensor)> = experts
17489 .iter()
17490 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17491 .collect();
17492 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17493 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17494 per_row(&mut out);
17495 return out;
17496 }
17497 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17498 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17499 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17500 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17501 per_row(&mut out);
17502 return out;
17503 }
17504 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17506 let mut p = 0usize;
17507 for (g, &e) in experts.iter().enumerate() {
17508 for &r in &groups[g] {
17509 slot.insert((r, e), p);
17510 p += 1;
17511 }
17512 }
17513 for (r, (idx, w)) in routes.iter().enumerate() {
17514 let terms: Vec<(&[f32], f32)> = idx
17515 .iter()
17516 .zip(w)
17517 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17518 .collect();
17519 let row = &mut out[r * hidden..(r + 1) * hidden];
17520 for (i, dst) in row.iter_mut().enumerate() {
17521 let mut acc = 0f32;
17523 for (d, we) in &terms {
17524 acc += we * d[i];
17525 }
17526 *dst = acc;
17527 }
17528 }
17529 out
17530}
17531
17532pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17536 match pool {
17537 Some(p) if v.len() >= 1 << 15 => {
17538 let base = v.as_mut_ptr() as usize;
17539 p.run_rows(v.len(), &|s, e| {
17540 let sl = unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17543 for x in sl {
17544 *x = c * (*x / c).tanh();
17545 }
17546 });
17547 }
17548 _ => {
17549 for x in v.iter_mut() {
17550 *x = c * (*x / c).tanh();
17551 }
17552 }
17553 }
17554}
17555
17556thread_local! {
17557 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17561 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17562}
17563
17564fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17566 if gate_topk() > 0
17569 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17570 {
17571 return out;
17572 }
17573 let prism_body = d.gate_proj.has_prism_contract()
17589 || d.up_proj.has_prism_contract()
17590 || d.down_proj.has_prism_contract();
17591 if !prism_body
17592 && crate::gpu::enabled_here()
17593 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17594 {
17595 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17596 crate::gpu::ProbeArm::Gpu
17597 } else {
17598 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17599 };
17600 match arm {
17601 crate::gpu::ProbeArm::Gpu => {
17602 let t0 = std::time::Instant::now();
17603 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17604 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17605 return out;
17606 }
17607 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17611 }
17612 crate::gpu::ProbeArm::CpuTimed => {
17613 let t0 = std::time::Instant::now();
17614 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17615 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17616 return out;
17617 }
17618 crate::gpu::ProbeArm::Cpu => {
17619 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17620 }
17621 }
17622 }
17623 dense_ffn_cpu(d, x, pool)
17624}
17625
17626fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17628 let inter = d.gate_proj.rows();
17629 FFN_SCRATCH.with(|s| {
17630 let mut s = s.borrow_mut();
17631 let [g, u, ..] = &mut *s;
17632 g.resize(inter, 0.0);
17633 if gate_topk() > 0 {
17636 u.resize(inter, 0.0);
17640 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17641 for i in 0..inter {
17642 g[i] = Act::Silu.combine(g[i], 1.0);
17643 }
17644 keep_top_k(g, gate_topk());
17645 for i in 0..inter {
17646 g[i] *= u[i];
17647 }
17648 } else if d.act == Act::Silu && {
17649 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17650 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17651 } {
17652 } else {
17654 u.resize(inter, 0.0);
17655 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17657 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17658 for i in 0..inter {
17659 g[i] = d.act.combine(g[i], u[i]);
17660 }
17661 }
17662 FFN_PROBE.with(|pr| {
17670 if let Some(acc) = pr.borrow_mut().as_mut() {
17671 let li = crate::gpu::cur_layer();
17672 if li >= 0 {
17673 if let Some(row) = acc.get_mut(li as usize) {
17674 match probe_topk() {
17675 0 if probe_sq() => {
17676 for (a, &v) in row.iter_mut().zip(g.iter()) {
17677 *a += (v as f64) * (v as f64);
17678 }
17679 }
17680 0 if probe_signed() => {
17681 for (a, &v) in row.iter_mut().zip(g.iter()) {
17682 *a += v as f64;
17683 }
17684 }
17685 0 => {
17686 for (a, &v) in row.iter_mut().zip(g.iter()) {
17687 *a += (v as f64).abs();
17688 }
17689 }
17690 k => {
17691 let n = g.len();
17692 let k = k.min(n);
17693 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17694 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17695 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17696 });
17697 let thr = *kth;
17698 for (a, &v) in row.iter_mut().zip(g.iter()) {
17699 if v.abs() >= thr {
17700 *a += 1.0;
17701 }
17702 }
17703 }
17704 }
17705 }
17706 }
17707 }
17708 });
17709 if oracle_topk() > 0 {
17710 keep_top_k(g, oracle_topk());
17711 }
17712 {
17713 let li = crate::gpu::cur_layer();
17714 if li >= 0 {
17715 adump_row(li as usize, g);
17716 }
17717 }
17718 let mut out = attention::take_buf(d.down_proj.rows());
17719 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
17720 d.down_proj.matvec(g, &mut out, pool);
17721 out
17722 })
17723}
17724
17725pub struct RefitAcc {
17738 pub support: Vec<u32>,
17739 pub gss: Vec<f32>,
17740 pub ya: Vec<f32>,
17741 pub hidden: usize,
17742 pub tokens: u64,
17743 pub buf_g: Vec<f32>,
17749 pub buf_o: Vec<f32>,
17750 pub buf_t: usize,
17751}
17752
17753type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
17757
17758static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
17759 std::sync::OnceLock::new();
17760
17761fn ffn_probe_active() -> bool {
17764 FFN_PROBE.with(|p| p.borrow().is_some())
17765}
17766
17767fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
17768 REFIT
17769 .get_or_init(|| {
17770 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
17771 (
17772 d,
17773 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
17774 )
17775 })
17776 })
17777 .as_ref()
17778}
17779
17780fn refit_accumulate(
17782 li: usize,
17783 g: &[f32],
17784 b: usize,
17785 inter: usize,
17786 out: &[f32],
17787 hidden: usize,
17788 pool: Option<&Pool>,
17789) {
17790 let Some((dir, map)) = refit_dir() else {
17791 return;
17792 };
17793 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17794 let (from, to) = *SPAN.get_or_init(|| {
17795 let g = |k: &str, d: usize| {
17796 std::env::var(k)
17797 .ok()
17798 .and_then(|v| v.parse().ok())
17799 .unwrap_or(d)
17800 };
17801 (
17802 g("CMF_FFN_REFIT_FROM", 0),
17803 g("CMF_FFN_REFIT_TO", usize::MAX),
17804 )
17805 });
17806 if li < from || li > to {
17807 return;
17808 }
17809 let mut guard = map.lock().unwrap();
17810 let (map, shared) = &mut *guard;
17811 let acc = match map.entry(li) {
17812 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
17813 std::collections::hash_map::Entry::Vacant(e) => {
17814 let path = format!("{dir}/support.{li}.u32");
17815 let Ok(bytes) = std::fs::read(&path) else {
17816 eprintln!("refit: no {path} — layer {li} skipped");
17817 return;
17818 };
17819 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
17820 let support: Vec<u32> = bytes[4..4 + n * 4]
17821 .chunks_exact(4)
17822 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17823 .collect();
17824 eprintln!(
17825 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
17826 (n * n + hidden * n) as f64 * 4.0 / 1e6
17827 );
17828 e.insert(RefitAcc {
17829 gss: vec![0.0; n * n],
17830 ya: vec![0.0; hidden * n],
17831 buf_g: Vec::new(),
17832 buf_o: Vec::new(),
17833 buf_t: 0,
17834 support,
17835 hidden,
17836 tokens: 0,
17837 })
17838 }
17839 };
17840 let ns = acc.support.len();
17841 let cap = refit_batch();
17843 if acc.buf_g.is_empty() {
17844 acc.buf_g = vec![0.0; ns * cap];
17845 acc.buf_o = vec![0.0; hidden * cap];
17846 }
17847 let take = b.min(cap - acc.buf_t);
17848 for t in 0..take {
17849 let col = acc.buf_t + t;
17850 for (j, &n) in acc.support.iter().enumerate() {
17851 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17852 }
17853 for h in 0..hidden {
17854 acc.buf_o[h * cap + col] = out[t * hidden + h];
17855 }
17856 }
17857 acc.buf_t += take;
17858 acc.tokens += take as u64;
17859 if acc.buf_t < cap {
17860 return;
17861 }
17862 let bt = acc.buf_t;
17863 acc.buf_t = 0;
17864 let RefitAcc {
17874 gss,
17875 ya,
17876 buf_g,
17877 buf_o,
17878 ..
17879 } = acc;
17880 let need = (ns * ns).max(hidden * ns);
17881 if shared.len() < need {
17882 shared.resize(need, 0.0);
17883 }
17884 let scratch = &mut shared[..];
17885 let _ = bt;
17886 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17887 add_into(gss, &scratch[..ns * ns], pool);
17888 if crate::gpu::gemm_nt_f32_transient(
17889 buf_o,
17890 buf_g,
17891 &mut scratch[..hidden * ns],
17892 hidden,
17893 cap,
17894 ns,
17895 ) {
17896 add_into(ya, &scratch[..hidden * ns], pool);
17897 } else {
17898 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17899 }
17900 } else {
17901 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
17902 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17903 }
17904 }
17908
17909fn refit_batch() -> usize {
17911 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17912 *B.get_or_init(|| {
17913 std::env::var("CMF_FFN_REFIT_BATCH")
17914 .ok()
17915 .and_then(|v| v.parse().ok())
17916 .unwrap_or(4096)
17917 })
17918}
17919
17920fn accum_outer_t(
17923 c: &mut [f32],
17924 m: usize,
17925 n: usize,
17926 b: usize,
17927 left: &[f32],
17928 right: &[f32],
17929 pool: Option<&Pool>,
17930) {
17931 let ptr = SendMut(c.as_mut_ptr());
17932 let body = |i: usize| {
17933 let ptr = &ptr;
17934 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
17935 for t in 0..b {
17936 let a = left[i * b + t];
17937 if a == 0.0 {
17938 continue;
17939 }
17940 for (j, o) in row.iter_mut().enumerate() {
17941 *o += a * right[j * b + t];
17942 }
17943 }
17944 };
17945 match pool {
17946 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
17947 for i in s..e {
17948 body(i);
17949 }
17950 }),
17951 _ => {
17952 for i in 0..m {
17953 body(i);
17954 }
17955 }
17956 }
17957}
17958
17959fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
17962 let n = dst.len().min(src.len());
17963 match pool {
17964 Some(p) if n >= 1 << 16 => {
17965 let ptr = SendMut(dst.as_mut_ptr());
17966 let f = |s: usize, e: usize| {
17967 let ptr = &ptr;
17968 for blk in s..e {
17969 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
17970 for i in a..b {
17971 unsafe { *ptr.0.add(i) += src[i] };
17972 }
17973 }
17974 };
17975 p.run_rows(n.div_ceil(4096), &f);
17976 }
17977 _ => {
17978 for (d, v) in dst.iter_mut().zip(&src[..n]) {
17979 *d += *v;
17980 }
17981 }
17982 }
17983}
17984
17985fn accum_outer(
17990 c: &mut [f32],
17991 m: usize,
17992 n: usize,
17993 b: usize,
17994 left: &[f32],
17995 right: &[f32],
17996 pool: Option<&Pool>,
17997) {
17998 const TILE: usize = 32;
17999 let tiles = m.div_ceil(TILE);
18000 let cp = SendMut(c.as_mut_ptr());
18001 let body = |ti: usize| {
18002 let cp = &cp;
18003 let i0 = ti * TILE;
18004 let i1 = (i0 + TILE).min(m);
18005 for t in 0..b {
18006 let r = &right[t * n..t * n + n];
18007 for i in i0..i1 {
18008 let a = left[i * b + t];
18009 if a == 0.0 {
18010 continue;
18011 }
18012 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
18014 for (o, v) in row.iter_mut().zip(r) {
18015 *o += a * *v;
18016 }
18017 }
18018 }
18019 };
18020 match pool {
18021 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
18022 for ti in s..e {
18023 body(ti);
18024 }
18025 }),
18026 _ => {
18027 for ti in 0..tiles {
18028 body(ti);
18029 }
18030 }
18031 }
18032}
18033
18034pub fn refit_flush() -> usize {
18036 let Some((dir, map)) = refit_dir() else {
18037 return 0;
18038 };
18039 let guard = map.lock().unwrap();
18040 let mut n = 0;
18041 for (li, acc) in guard.0.iter() {
18042 let w = |name: &str, v: &[f32]| {
18045 let path = format!("{dir}/{name}.{li}.f32");
18046 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
18047 match std::fs::write(&path, &bytes) {
18048 Ok(()) => {}
18049 Err(e) => eprintln!(
18050 "refit: FAILED to write {path} ({} MB): {e}",
18051 bytes.len() / 1_000_000
18052 ),
18053 }
18054 };
18055 w("gss", &acc.gss);
18056 w("ya", &acc.ya);
18057 println!(
18058 "refit L{li}: {} support, {} tokens, hidden {}",
18059 acc.support.len(),
18060 acc.tokens,
18061 acc.hidden
18062 );
18063 n += 1;
18064 }
18065 n
18066}
18067
18068fn adump_row(li: usize, g: &[f32]) {
18073 use std::io::Write as _;
18074 static FILES: std::sync::OnceLock<
18075 Option<(
18076 String,
18077 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
18078 )>,
18079 > = std::sync::OnceLock::new();
18080 let Some((prefix, map)) = FILES
18081 .get_or_init(|| {
18082 std::env::var("CMF_FFN_ADUMP")
18083 .ok()
18084 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
18085 })
18086 .as_ref()
18087 else {
18088 return;
18089 };
18090 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
18093 let (from, to) = *SPAN.get_or_init(|| {
18094 let g = |k: &str, d: usize| {
18095 std::env::var(k)
18096 .ok()
18097 .and_then(|v| v.parse().ok())
18098 .unwrap_or(d)
18099 };
18100 (
18101 g("CMF_FFN_ADUMP_FROM", 0),
18102 g("CMF_FFN_ADUMP_TO", usize::MAX),
18103 )
18104 });
18105 if li < from || li > to {
18106 return;
18107 }
18108 let mut map = map.lock().unwrap();
18109 let f = map.entry(li).or_insert_with(|| {
18110 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
18111 });
18112 let mut bytes = Vec::with_capacity(g.len() * 2);
18113 for v in g {
18114 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
18115 }
18116 let _ = f.write_all(&bytes);
18117}
18118
18119fn oracle_topk() -> usize {
18125 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18126 *K.get_or_init(|| {
18127 std::env::var("CMF_FFN_ORACLE_TOPK")
18128 .ok()
18129 .and_then(|v| v.parse().ok())
18130 .unwrap_or(0)
18131 })
18132}
18133
18134fn gate_topk() -> usize {
18140 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18141 *K.get_or_init(|| {
18142 std::env::var("CMF_FFN_GATE_TOPK")
18143 .ok()
18144 .and_then(|v| v.parse().ok())
18145 .unwrap_or(0)
18146 })
18147}
18148
18149fn gate_block() -> usize {
18156 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18157 *B.get_or_init(|| {
18158 std::env::var("CMF_FFN_GATE_BLOCK")
18159 .ok()
18160 .and_then(|v| v.parse().ok())
18161 .unwrap_or(1)
18162 })
18163}
18164
18165fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
18167 let n = g.len();
18168 let nb = n.div_ceil(block);
18169 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
18170 if kb >= nb {
18171 return;
18172 }
18173 let mut score: Vec<f32> = (0..nb)
18174 .map(|b| {
18175 g[b * block..((b + 1) * block).min(n)]
18176 .iter()
18177 .map(|v| v * v)
18178 .sum::<f32>()
18179 })
18180 .collect();
18181 let mut ord = score.clone();
18182 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
18183 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18184 });
18185 let thr = *kth;
18186 for b in 0..nb {
18187 if score[b] < thr {
18188 g[b * block..((b + 1) * block).min(n)].fill(0.0);
18189 }
18190 }
18191 score.clear();
18192}
18193
18194fn keep_top_k(g: &mut [f32], k: usize) {
18196 if gate_block() > 1 {
18197 return keep_top_blocks(g, k, gate_block());
18198 }
18199 let n = g.len();
18200 if k == 0 || k >= n {
18201 return;
18202 }
18203 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
18204 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18205 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18206 });
18207 let thr = *kth;
18208 for v in g.iter_mut() {
18209 if v.abs() < thr {
18210 *v = 0.0;
18211 }
18212 }
18213}
18214
18215fn probe_sq() -> bool {
18219 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18220 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
18221}
18222
18223fn probe_signed() -> bool {
18227 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18228 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
18229}
18230
18231fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
18239 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
18240 M.get_or_init(|| {
18241 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
18242 let b = std::fs::read(&p).ok()?;
18243 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
18244 let vals: Vec<f32> = b[8..]
18245 .chunks_exact(4)
18246 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
18247 .collect();
18248 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
18249 Some((inter, vals))
18250 })
18251 .as_ref()
18252}
18253
18254fn probe_topk() -> usize {
18257 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18258 *K.get_or_init(|| {
18259 std::env::var("CMF_FFN_PROBE_TOPK")
18260 .ok()
18261 .and_then(|v| v.parse().ok())
18262 .unwrap_or(0)
18263 })
18264}
18265
18266thread_local! {
18267 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18270 const { std::cell::RefCell::new(None) };
18271}
18272
18273fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18286 if d.gate_proj.has_prism_contract()
18291 || d.up_proj.has_prism_contract()
18292 || d.down_proj.has_prism_contract()
18293 {
18294 return None;
18295 }
18296 let dt = d.down_t.as_ref()?;
18297 let inter = d.gate_proj.rows();
18298 let hidden = dt.cols();
18299 if k == 0 || k >= inter || d.act != Act::Silu {
18300 return None;
18301 }
18302 DYN_SCRATCH.with(|sc| {
18303 let mut sc = sc.borrow_mut();
18304 let DynScratch {
18305 g,
18306 mag,
18307 live,
18308 parts,
18309 } = &mut *sc;
18310 g.resize(inter, 0.0);
18311 d.gate_proj.matvec(x, g, pool);
18312 for v in g.iter_mut() {
18313 *v = inference::silu(*v);
18314 }
18315 mag.clear();
18318 mag.extend(g.iter().map(|v| v.abs()));
18319 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18320 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18321 });
18322 let thr = *kth;
18323 live.clear();
18324 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18325 let mut out = vec![0.0f32; hidden];
18326 match pool {
18327 Some(p) if live.len() >= 64 => {
18328 let nw = p.n_workers() + 1;
18329 parts.clear();
18330 parts.resize(nw * hidden, 0.0);
18331 let ptr = SendMut(parts.as_mut_ptr());
18332 let n = live.len();
18333 let live_ref: &[u32] = live;
18334 let g_ref: &[f32] = g;
18335 p.run(&|w, workers| {
18336 let chunk = n.div_ceil(workers);
18337 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18338 if s >= e {
18339 return;
18340 }
18341 WORKER_SCRATCH.with(|ws| {
18342 let mut ws = ws.borrow_mut();
18343 let [scratch, acc] = &mut *ws;
18344 scratch.resize(hidden.max(x.len()), 0.0);
18345 acc.clear();
18346 acc.resize(hidden, 0.0);
18347 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18348 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18351 d.up_proj.prefetch_row(nx as usize);
18352 dt.prefetch_row(nx as usize);
18353 }
18354 let idx = nrm as usize;
18355 let up = d.up_proj.row_dot(idx, x, scratch);
18356 let a = g_ref[idx] * up;
18357 if a != 0.0 {
18358 dt.add_row_scaled(idx, a, acc, scratch);
18359 }
18360 }
18361 for (j, v) in acc.iter().enumerate() {
18362 unsafe { *ptr.at(w * hidden + j) = *v };
18363 }
18364 });
18365 });
18366 for w in 0..nw {
18367 for (j, o) in out.iter_mut().enumerate() {
18368 *o += parts[w * hidden + j];
18369 }
18370 }
18371 }
18372 _ => {
18373 WORKER_SCRATCH.with(|ws| {
18374 let mut ws = ws.borrow_mut();
18375 let [scratch, _acc] = &mut *ws;
18376 scratch.resize(hidden.max(x.len()), 0.0);
18377 for &nrm in live.iter() {
18378 let idx = nrm as usize;
18379 let up = d.up_proj.row_dot(idx, x, scratch);
18380 let a = g[idx] * up;
18381 if a != 0.0 {
18382 dt.add_row_scaled(idx, a, &mut out, scratch);
18383 }
18384 }
18385 });
18386 }
18387 }
18388 Some(out)
18389 })
18390}
18391
18392struct DynScratch {
18395 g: Vec<f32>,
18396 mag: Vec<f32>,
18397 live: Vec<u32>,
18398 parts: Vec<f32>,
18399}
18400
18401thread_local! {
18402 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18403 std::cell::RefCell::new(DynScratch {
18404 g: Vec::new(),
18405 mag: Vec::new(),
18406 live: Vec::new(),
18407 parts: Vec::new(),
18408 })
18409 };
18410 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18412 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18413}
18414
18415fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18420 let inter = d.gate_proj.rows();
18421 FFN_SCRATCH.with(|s| {
18422 let mut s = s.borrow_mut();
18423 let [g, u, ..] = &mut *s;
18424 g.resize(inter, 0.0);
18425 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18426 } else {
18428 u.resize(inter, 0.0);
18429 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18430 for i in 0..inter {
18431 g[i] = d.act.combine(g[i], u[i]);
18432 }
18433 }
18434 zero_masked_cols(g, 1, inter, mask_row);
18435 let mut out = attention::take_buf(d.down_proj.rows());
18436 d.down_proj.matvec(g, &mut out, pool);
18437 out
18438 })
18439}
18440
18441fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18447 if d.gate_proj.has_prism_contract()
18448 || d.up_proj.has_prism_contract()
18449 || d.down_proj.has_prism_contract()
18450 {
18451 return None;
18452 }
18453 d.act.graph_act()?;
18456 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18459 return None;
18460 }
18461 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18462 let mut model_ref = None;
18463 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18464 let model = model_ref?;
18465 let hidden = jobs[0].down.1;
18466 let mut out = attention::take_buf(hidden);
18467 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18468 Some(out)
18469 } else {
18470 let mut out = out;
18471 attention::recycle_buf(&mut out);
18472 None
18473 }
18474}
18475
18476#[allow(clippy::type_complexity)]
18481#[allow(clippy::type_complexity)]
18482pub(crate) fn moe_parts(
18483 t: &QTensor,
18484) -> Option<(
18485 &std::sync::Arc<cortiq_core::CmfModel>,
18486 usize,
18487 usize,
18488 usize,
18489 &[f32],
18490 &[f32],
18491 bool,
18492 bool,
18493 bool,
18494)> {
18495 match t {
18496 QTensor::Mapped {
18497 model,
18498 idx,
18499 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18500 rows,
18501 cols,
18502 row_scale,
18503 col_field,
18504 ..
18505 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18506 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18507 )),
18508 QTensor::Mapped {
18510 model,
18511 idx,
18512 dtype: cortiq_core::TensorDtype::Q1,
18513 rows,
18514 cols,
18515 ..
18516 } => Some((
18517 model,
18518 *idx,
18519 *rows,
18520 *cols,
18521 &[][..],
18522 &[][..],
18523 true,
18524 false,
18525 false,
18526 )),
18527 QTensor::Mapped {
18529 model,
18530 idx,
18531 dtype: cortiq_core::TensorDtype::Q4Tiled,
18532 rows,
18533 cols,
18534 ..
18535 } => Some((
18536 model,
18537 *idx,
18538 *rows,
18539 *cols,
18540 &[][..],
18541 &[][..],
18542 false,
18543 true,
18544 false,
18545 )),
18546 QTensor::Mapped {
18548 model,
18549 idx,
18550 dtype: cortiq_core::TensorDtype::Q4TiledP,
18551 rows,
18552 cols,
18553 ..
18554 } => Some((
18555 model,
18556 *idx,
18557 *rows,
18558 *cols,
18559 &[][..],
18560 &[][..],
18561 false,
18562 true,
18563 false,
18564 )),
18565 QTensor::Mapped {
18569 model,
18570 idx,
18571 dtype: cortiq_core::TensorDtype::Q2TiledP,
18572 rows,
18573 cols,
18574 ..
18575 } => Some((
18576 model,
18577 *idx,
18578 *rows,
18579 *cols,
18580 &[][..],
18581 &[][..],
18582 false,
18583 true,
18584 true,
18585 )),
18586 _ => None,
18587 }
18588}
18589
18590#[cfg(target_os = "macos")]
18597#[allow(clippy::type_complexity)]
18598fn chunk_moe_parts(m: &MoeFfn, hidden: usize) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18599 if m.shared.is_some()
18600 || m.per_expert_scale.is_some()
18601 || m.resonance.is_some()
18602 || m.route_tau.is_some()
18603 || m.router_input_norm
18604 || m.experts.is_empty()
18605 || m.top_k == 0
18606 || std::env::var("CMF_RMS_TRACE").is_ok()
18607 || std::env::var("CMF_ACT_DUMP").is_ok()
18608 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18609 {
18610 return None;
18611 }
18612 let (rf, rr, rc) = m.router.f32_parts()?;
18613 if rr != m.experts.len() || rc != hidden {
18614 return None;
18615 }
18616 let inter = m.experts[0].gate_proj.rows();
18617 let q8 = crate::gpu_metal::moe_q8::enabled()
18620 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18621 let experts = m
18622 .experts
18623 .iter()
18624 .map(|e| {
18625 if e.act != Act::Silu
18626 || !e.segs.is_empty()
18627 || e.gate_proj.rows() != inter
18628 || e.up_proj.rows() != inter
18629 || e.down_proj.cols() != inter
18630 || e.gate_proj.cols() != hidden
18631 || e.down_proj.rows() != hidden
18632 {
18633 return None;
18634 }
18635 if q8 {
18636 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18637 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18638 }
18639 Some((
18640 e.gate_proj.mapped_q4tp()?.1,
18641 e.up_proj.mapped_q4tp()?.1,
18642 e.down_proj.mapped_q4tp()?.1,
18643 ))
18644 })
18645 .collect::<Option<Vec<_>>>()?;
18646 Some((rf, experts, q8))
18647}
18648
18649#[cfg(target_os = "macos")]
18657fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
18658 if m.router_input_norm
18659 || m.route_tau.is_some()
18660 || m.mask.is_some()
18661 || m.per_expert_scale.is_some()
18662 || m.experts.is_empty()
18663 || m.top_k == 0
18664 || m.resonance.is_some()
18665 {
18666 return None;
18667 }
18668 let (sh, sg) = match &m.shared {
18673 Some((sh, sg)) => (Some(sh), sg.as_ref()),
18674 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
18675 None => return None,
18676 };
18677 let (rf, rr, rc) = m.router.f32_parts()?;
18678 if rr != m.experts.len() || rc != hidden {
18679 return None;
18680 }
18681 let shared_gated = sg.is_some();
18682 let sf = match sg {
18683 Some(sg) => {
18684 let (sf, sr, sc) = sg.f32_parts()?;
18685 if sr * sc != hidden {
18686 return None;
18687 }
18688 sf
18689 }
18690 None => &rf[..hidden],
18693 };
18694 if let Some(b) = &m.expert_bias {
18695 if b.len() != m.experts.len() {
18696 return None;
18697 }
18698 }
18699 let inter = m.experts[0].gate_proj.rows();
18700 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
18703 let q8 = crate::gpu_metal::moe_q8::enabled()
18707 && m.shared.is_none()
18708 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18709 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
18710 if e.act != Act::Silu
18711 || e.gate_proj.rows() != inter
18712 || e.gate_proj.cols() != hidden
18713 || e.up_proj.rows() != inter
18714 || e.up_proj.cols() != hidden
18715 || e.down_proj.rows() != hidden
18716 || e.down_proj.cols() != inter
18717 {
18718 return None;
18719 }
18720 if q8 {
18721 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18722 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18723 }
18724 let pick = |t: &QTensor| -> Option<usize> {
18725 if gu_q2 {
18726 t.mapped_q2tp().map(|(_, i)| i)
18727 } else {
18728 t.mapped_q4tp().map(|(_, i)| i)
18729 }
18730 };
18731 Some((
18732 pick(&e.gate_proj)?,
18733 pick(&e.up_proj)?,
18734 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
18735 ))
18736 };
18737 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
18738 let shared = match sh {
18739 Some(sh) => Some(trio(sh)?),
18740 None => None,
18741 };
18742 Some(crate::gpu::GpuMoe {
18743 router: rf,
18744 sgate: sf,
18745 experts,
18746 shared,
18747 n_exp: m.experts.len(),
18748 top_k: m.top_k,
18749 inter,
18750 norm_topk: m.norm_topk_prob,
18751 route_scale: m.routed_scaling,
18752 gu_q2,
18753 q8,
18754 sigmoid: m.router_sigmoid,
18755 bias: m.expert_bias.as_deref(),
18756 shared_gated,
18757 })
18758}
18759
18760fn forced_ids_at(step: usize) -> Option<u32> {
18766 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
18767 IDS.get_or_init(|| {
18768 std::env::var("CMF_FORCE_IDS")
18769 .ok()
18770 .and_then(|p| std::fs::read_to_string(p).ok())
18771 .map(|s| s.split_whitespace().filter_map(|t| t.parse().ok()).collect())
18772 .unwrap_or_default()
18773 })
18774 .get(step)
18775 .copied()
18776}
18777
18778pub(crate) fn moe_push_job_parts<'a>(
18782 gate: &'a QTensor,
18783 up: &'a QTensor,
18784 down: &'a QTensor,
18785 x: &[f32],
18786 w: f32,
18787 swiglu_limit: f32,
18788 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18789 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18790) -> Option<()> {
18791 use crate::qtensor::prescale;
18792 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
18793 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
18794 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
18795 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18796 return None; }
18798 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
18801 return None;
18802 }
18803 if !gq2 && dq2 {
18804 return None;
18805 }
18806 model_ref.get_or_insert_with(|| gm.clone());
18807 let dt = |cf: &[f32]| {
18808 if cf.is_empty() {
18809 cortiq_core::TensorDtype::Q8Row
18810 } else {
18811 cortiq_core::TensorDtype::Q8_2f
18812 }
18813 };
18814 jobs.push(crate::gpu::MoeJob {
18815 gate: (gi, gr, gc, grs),
18816 up: (ui, ur, uc, urs),
18817 down: (di, dr, dc, drs),
18818 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
18819 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
18820 down_col: dcf,
18821 w,
18822 q1: gq1,
18823 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
18824 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
18825 gu_q2: gq2,
18826 swiglu_limit,
18827 act: 0,
18828 });
18829 Some(())
18830}
18831
18832fn moe_push_job<'a>(
18834 d: &'a DenseFfn,
18835 x: &[f32],
18836 w: f32,
18837 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18838 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18839) -> Option<()> {
18840 use crate::qtensor::prescale;
18841 let act = d.act.graph_act()?.code();
18844 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
18845 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
18846 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
18847 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18848 return None; }
18850 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
18851 return None;
18852 }
18853 if !gq2 && dq2 {
18854 return None;
18855 }
18856 model_ref.get_or_insert_with(|| gm.clone());
18857 let gdt = if gcf.is_empty() {
18858 cortiq_core::TensorDtype::Q8Row
18859 } else {
18860 cortiq_core::TensorDtype::Q8_2f
18861 };
18862 let udt = if ucf.is_empty() {
18863 cortiq_core::TensorDtype::Q8Row
18864 } else {
18865 cortiq_core::TensorDtype::Q8_2f
18866 };
18867 jobs.push(crate::gpu::MoeJob {
18868 gate: (gi, gr, gc, grs),
18869 up: (ui, ur, uc, urs),
18870 down: (di, dr, dc, drs),
18871 xs_gate: prescale(x, gcf, gdt).into_owned(),
18872 xs_up: prescale(x, ucf, udt).into_owned(),
18873 down_col: dcf,
18874 w,
18875 q1: gq1,
18876 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
18877 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
18878 gu_q2: gq2,
18879 swiglu_limit: 0.0,
18880 act,
18881 });
18882 Some(())
18883}
18884
18885fn sparse_ffn_quant(
18892 d: &DenseFfn,
18893 x: &[f32],
18894 active: &[u16],
18895 hidden: usize,
18896 pool: Option<&Pool>,
18897) -> Vec<f32> {
18898 let n = active.len();
18899 let inter = d.gate_proj.rows();
18900 let mut act = vec![0.0f32; n];
18901 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
18904 let compute = |ai: usize| -> f32 {
18905 let idx = active[ai] as usize;
18906 if idx >= inter {
18907 return 0.0; }
18909 let mut s = if need_scratch {
18910 vec![0.0f32; hidden]
18911 } else {
18912 Vec::new()
18913 };
18914 let gate = d.gate_proj.row_dot(idx, x, &mut s);
18915 let up = d.up_proj.row_dot(idx, x, &mut s);
18916 d.act.combine(gate, up)
18917 };
18918 match pool {
18919 Some(p) if n >= 256 => {
18920 let ptr = SendMut(act.as_mut_ptr());
18921 p.run(&|widx, nw| {
18922 let chunk = n.div_ceil(nw);
18923 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
18924 for ai in s..e {
18925 unsafe { *ptr.at(ai) = compute(ai) };
18926 }
18927 });
18928 }
18929 _ => {
18930 for (ai, a) in act.iter_mut().enumerate() {
18931 *a = compute(ai);
18932 }
18933 }
18934 }
18935 let mut out = vec![0.0f32; hidden];
18937 for (ai, &idx) in active.iter().enumerate() {
18938 let w = act[ai];
18939 if w.abs() >= 1e-12 && (idx as usize) < inter {
18940 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
18941 }
18942 }
18943 out
18944}
18945
18946#[doc(hidden)]
18948pub fn sparse_ffn_quant_for_test(
18949 d: &DenseFfn,
18950 x: &[f32],
18951 active: &[u16],
18952 hidden: usize,
18953) -> Vec<f32> {
18954 sparse_ffn_quant(d, x, active, hidden, None)
18955}
18956
18957fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
18961 let deq = |t: &QTensor| -> Vec<f32> {
18962 let (rows, cols) = (t.rows(), t.cols());
18963 let mut out = vec![0.0f32; rows * cols];
18964 for r in 0..rows {
18965 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
18966 }
18967 out
18968 };
18969 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
18970}
18971
18972struct SendMut(*mut f32);
18974unsafe impl Send for SendMut {}
18975unsafe impl Sync for SendMut {}
18976impl SendMut {
18977 #[inline]
18978 #[allow(clippy::mut_from_ref)]
18981 unsafe fn at(&self, i: usize) -> &mut f32 {
18982 unsafe { &mut *self.0.add(i) }
18983 }
18984}
18985
18986pub(crate) fn moe_route(
18999 logits: &[f32],
19000 m: &MoeFfn,
19001 allowed: Option<&[bool]>,
19002) -> (Vec<usize>, Vec<f32>, f32) {
19003 moe_route_with_eps(logits, m, allowed, 1e-6)
19004}
19005
19006pub(crate) fn moe_route_with_eps(
19015 logits: &[f32],
19016 m: &MoeFfn,
19017 allowed: Option<&[bool]>,
19018 sigmoid_denom_eps: f32,
19019) -> (Vec<usize>, Vec<f32>, f32) {
19020 let ne = logits.len();
19021 let admit = |e: usize| {
19027 m.mask.as_ref().is_none_or(|mk| mk[e])
19028 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
19029 };
19030 if m.resonance.is_some() && m.top_k == 1 {
19043 let mut best: Option<usize> = None;
19044 for e in (0..ne).filter(|&e| admit(e)) {
19045 let l = logits[e];
19046 if l == f32::NEG_INFINITY || l.is_nan() {
19047 continue;
19048 }
19049 if best.is_none_or(|b| l > logits[b]) {
19050 best = Some(e);
19051 }
19052 }
19053 if let Some(b) = best {
19054 let mut p = vec![0.0f32; ne];
19055 p[b] = 1.0;
19056 return (vec![b], p, 1.0 / m.routed_scaling);
19057 }
19058 }
19059 let p: Vec<f32> = if m.router_sigmoid {
19065 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
19066 } else {
19067 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19068 if mx == f32::NEG_INFINITY {
19069 vec![1.0 / ne.max(1) as f32; ne]
19070 } else {
19071 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
19072 let s: f32 = e.iter().sum();
19073 for v in &mut e {
19074 *v /= s;
19075 }
19076 e
19077 }
19078 };
19079 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
19080 match &m.expert_bias {
19082 Some(b) => idx.sort_unstable_by(|&x, &y| {
19083 (p[y] + b[y])
19084 .partial_cmp(&(p[x] + b[x]))
19085 .unwrap()
19086 .then(x.cmp(&y))
19087 }),
19088 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
19089 }
19090 idx.truncate(m.top_k);
19091 if let Some(tau) = m.route_tau {
19095 let total: f32 = idx.iter().map(|&e| p[e]).sum();
19096 if total > 0.0 {
19097 let mut acc = 0.0f32;
19098 let mut keep = idx.len();
19099 for (i, &e) in idx.iter().enumerate() {
19100 acc += p[e];
19101 if acc >= tau * total {
19102 keep = i + 1;
19103 break;
19104 }
19105 }
19106 idx.truncate(keep);
19107 }
19108 }
19109 let wsum: f32 = if m.norm_topk_prob {
19110 let s: f32 = idx.iter().map(|&e| p[e]).sum();
19111 (if m.router_sigmoid {
19115 s + sigmoid_denom_eps
19116 } else {
19117 s
19118 }) / m.routed_scaling
19119 } else {
19120 1.0 / m.routed_scaling
19121 };
19122 (idx, p, wsum)
19123}
19124
19125fn moe_trace(idx: &[usize]) {
19127 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
19128}
19129
19130pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
19133 use std::io::Write;
19134 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
19135 std::sync::OnceLock::new();
19136 let Some(f) = F.get_or_init(|| {
19137 let p = std::env::var("CMF_MOE_TRACE").ok()?;
19138 Some(std::sync::Mutex::new(
19139 std::fs::OpenOptions::new()
19140 .create(true)
19141 .append(true)
19142 .open(p)
19143 .ok()?,
19144 ))
19145 }) else {
19146 return;
19147 };
19148 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
19149 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
19150}
19151
19152pub(crate) fn moe_ffn(
19155 m: &MoeFfn,
19156 x: &[f32],
19157 pool: Option<&Pool>,
19158 allowed: Option<&[bool]>,
19159) -> Vec<f32> {
19160 let r = moe_ffn_route(m, x, pool, allowed);
19161 moe_ffn_experts(m, x, &r, pool)
19162}
19163
19164pub(crate) struct MoeRoute {
19168 pub idx: Vec<usize>,
19169 pub p: Vec<f32>,
19170 pub wsum: f32,
19171 pub logits: Vec<f32>,
19172}
19173
19174pub(crate) fn moe_ffn_route(
19180 m: &MoeFfn,
19181 x: &[f32],
19182 pool: Option<&Pool>,
19183 allowed: Option<&[bool]>,
19184) -> MoeRoute {
19185 accumulate_act(m, x, 1);
19186 let ne = m.experts.len();
19187 let mut logits = vec![0.0f32; ne];
19188 match &m.resonance {
19189 Some(r) => r.scores(x, &mut logits),
19190 None => m.router.matvec(x, &mut logits, pool),
19191 }
19192 let (idx, p, wsum) = moe_route(&logits, m, allowed);
19193 {
19194 let mut st = m.stats.borrow_mut();
19195 if st.len() < ne {
19196 st.resize(ne, 0);
19197 }
19198 for &e in &idx {
19199 st[e] += 1;
19200 }
19201 }
19202 moe_trace(&idx);
19208 MoeRoute {
19209 idx,
19210 p,
19211 wsum,
19212 logits,
19213 }
19214}
19215
19216pub(crate) fn moe_ffn_experts(
19219 m: &MoeFfn,
19220 x: &[f32],
19221 r: &MoeRoute,
19222 pool: Option<&Pool>,
19223) -> Vec<f32> {
19224 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
19225 if crate::gpu::enabled_here() {
19230 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
19231 crate::gpu::ProbeArm::Gpu => {
19232 let t0 = std::time::Instant::now();
19233 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
19234 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
19235 return out;
19236 }
19237 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
19238 }
19239 crate::gpu::ProbeArm::CpuTimed => {
19240 let t0 = std::time::Instant::now();
19241 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19242 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
19243 return out;
19244 }
19245 crate::gpu::ProbeArm::Cpu => {
19246 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
19247 }
19248 }
19249 }
19250 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
19251}
19252
19253fn moe_ffn_minicpm_tail(
19257 slot: &mut crate::minicpm_tail::MiniCpmTail,
19258 li: usize,
19259 total_layers: usize,
19260 m: &MoeFfn,
19261 x: &[f32],
19262 pool: Option<&Pool>,
19263) -> Vec<f32> {
19264 let route = moe_ffn_route(m, x, pool, None);
19265 match slot.forward(li, m, x, &route, pool, total_layers) {
19266 Some(out) => out,
19267 None => moe_ffn_experts(m, x, &route, pool),
19272 }
19273}
19274
19275fn moe_ffn_banked(
19279 slot: &mut crate::mimo_moe::Slot,
19280 li: usize,
19281 m: &MoeFfn,
19282 x: &[f32],
19283 pool: Option<&Pool>,
19284) -> Vec<f32> {
19285 let t0 = std::time::Instant::now();
19286 let r = moe_ffn_route(m, x, pool, None);
19287 slot.note_route(t0.elapsed().as_nanos() as u64);
19288 match slot.forward(li, m, x, &r, pool) {
19289 Some(out) => out,
19290 None => crate::qtensor::float_activations_scope(|| {
19291 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19292 }),
19293 }
19294}
19295
19296fn moe_ffn_banked_rows(
19298 slot: &mut crate::mimo_moe::Slot,
19299 li: usize,
19300 m: &MoeFfn,
19301 xs: &[f32],
19302 b: usize,
19303 hidden: usize,
19304 pool: Option<&Pool>,
19305) -> Vec<f32> {
19306 let t0 = std::time::Instant::now();
19307 let routes: Vec<_> = xs
19308 .chunks_exact(hidden)
19309 .map(|x| moe_ffn_route(m, x, pool, None))
19310 .collect();
19311 slot.note_route(t0.elapsed().as_nanos() as u64);
19312 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19313 return out;
19314 }
19315 let mut out = Vec::with_capacity(b * hidden);
19316 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19317 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19318 crate::qtensor::float_activations_scope(|| {
19320 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19321 })
19322 });
19323 out.extend(row);
19324 }
19325 out
19326}
19327
19328fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19333 use std::sync::atomic::{AtomicBool, Ordering};
19334 if built {
19335 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19336 if total_layers > 0 && layers_run < total_layers {
19337 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19338 } else {
19339 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19340 }
19341 } else {
19342 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19343 }
19344 static SAID: AtomicBool = AtomicBool::new(false);
19345 if !SAID.swap(true, Ordering::Relaxed) {
19346 if built {
19347 tracing::info!("wgpu whole-token graph: ACTIVE");
19348 } else {
19349 tracing::warn!("wgpu whole-token graph refused — per-op path");
19350 }
19351 }
19352}
19353
19354pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19358pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19359pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19363pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19365
19366pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
19370 std::sync::atomic::AtomicU64::new(0);
19371pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
19372 std::sync::atomic::AtomicU64::new(0);
19373pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19374 std::sync::atomic::AtomicU64::new(0);
19375pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
19376 std::sync::atomic::AtomicU64::new(0);
19377pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
19378 std::sync::atomic::AtomicU64::new(0);
19379pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19383 std::sync::atomic::AtomicU64::new(0);
19384pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
19385 std::sync::atomic::AtomicU64::new(0);
19386pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19387 std::sync::atomic::AtomicU64::new(0);
19388pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19389 std::sync::atomic::AtomicU64::new(0);
19390
19391fn moe_batch_enabled() -> bool {
19394 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19395 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19396}
19397
19398fn moe_ffn_cpu_batched(
19404 m: &MoeFfn,
19405 x: &[f32],
19406 idx: &[usize],
19407 p: &[f32],
19408 wsum: f32,
19409 pool: Option<&Pool>,
19410) -> Option<Vec<f32>> {
19411 if idx.is_empty() || !moe_batch_enabled() {
19412 return None;
19413 }
19414 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19418 return None;
19419 }
19420 let n = idx.len() + usize::from(m.shared.is_some());
19421 let mut pairs = Vec::with_capacity(n);
19422 let mut downs = Vec::with_capacity(n);
19423 let mut ws = Vec::with_capacity(n);
19424 for &e in idx {
19425 let d = &m.experts[e];
19426 if d.act != Act::Silu {
19427 return None;
19428 }
19429 pairs.push((&d.gate_proj, &d.up_proj));
19430 downs.push(&d.down_proj);
19431 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19432 }
19433 if let Some((se, gate)) = &m.shared {
19436 if se.act != Act::Silu {
19437 return None;
19438 }
19439 let g = gate.as_ref().map_or(1.0, |gate| {
19440 let mut gl = [0.0f32; 1];
19441 gate.matvec(x, &mut gl, pool);
19442 1.0 / (1.0 + (-gl[0]).exp())
19443 });
19444 pairs.push((&se.gate_proj, &se.up_proj));
19445 downs.push(&se.down_proj);
19446 ws.push(g);
19447 }
19448 let inter = pairs[0].0.rows();
19449 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19450 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19451 return None;
19452 }
19453 let mut out = attention::take_buf(x.len());
19454 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19455 attention::recycle_buf(&mut out);
19456 return None;
19457 }
19458 Some(out)
19459}
19460
19461pub(crate) fn moe_cold_experts_cpu(
19467 experts: &[(&DenseFfn, f32)],
19468 x: &[f32],
19469 pool: Option<&Pool>,
19470) -> Vec<f32> {
19471 let mut out = attention::take_buf(x.len());
19472 if experts.is_empty() {
19473 return out;
19474 }
19475 let pairs: Vec<_> = experts
19476 .iter()
19477 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19478 .collect();
19479 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19480 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19481 let inter = experts[0].0.gate_proj.rows();
19482 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19483 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19484 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19485 {
19486 return out;
19487 }
19488 out.fill(0.0);
19489 for &(expert, weight) in experts {
19490 let mut one = dense_ffn(expert, x, pool);
19491 for (o, v) in out.iter_mut().zip(&one) {
19492 *o += weight * v;
19493 }
19494 attention::recycle_buf(&mut one);
19495 }
19496 out
19497}
19498
19499pub(crate) fn moe_cold_experts_rows_cpu(
19503 jobs: &[Vec<(&DenseFfn, f32)>],
19504 xs: &[f32],
19505 hidden: usize,
19506 pool: Option<&Pool>,
19507) -> Vec<f32> {
19508 let mut out = vec![0.0; xs.len()];
19509 let mut experts: Vec<&DenseFfn> = Vec::new();
19510 let mut groups: Vec<Vec<usize>> = Vec::new();
19511 let mut terms = vec![Vec::new(); jobs.len()];
19512 for (r, row) in jobs.iter().enumerate() {
19513 for &(e, w) in row {
19514 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
19515 Some(g) => g,
19516 None => {
19517 experts.push(e);
19518 groups.push(Vec::new());
19519 groups.len() - 1
19520 }
19521 };
19522 terms[r].push((g, groups[g].len(), w));
19523 groups[g].push(r);
19524 }
19525 }
19526 if experts.is_empty() {
19527 return out;
19528 }
19529 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
19530 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
19531 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
19532 let count: usize = lens.iter().sum();
19533 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
19534 let mut ds = vec![vec![0.0; hidden]; count];
19535 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
19536 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
19537 {
19538 let mut offset = 0;
19539 let offsets: Vec<_> = lens
19540 .iter()
19541 .map(|&n| {
19542 let start = offset;
19543 offset += n;
19544 start
19545 })
19546 .collect();
19547 for (r, terms) in terms.iter().enumerate() {
19548 for &(g, slot, w) in terms {
19549 for (o, &v) in out[r * hidden..(r + 1) * hidden]
19550 .iter_mut()
19551 .zip(&ds[offsets[g] + slot])
19552 {
19553 *o += w * v;
19554 }
19555 }
19556 }
19557 } else {
19558 for (r, jobs) in jobs.iter().enumerate() {
19559 if !jobs.is_empty() {
19560 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
19561 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
19562 attention::recycle_buf(&mut row);
19563 }
19564 }
19565 }
19566 out
19567}
19568
19569fn moe_ffn_cpu(
19571 m: &MoeFfn,
19572 x: &[f32],
19573 idx: &[usize],
19574 p: &[f32],
19575 wsum: f32,
19576 pool: Option<&Pool>,
19577) -> Vec<f32> {
19578 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
19579 return out;
19580 }
19581 let mut out = attention::take_buf(x.len());
19582 for &e in idx {
19583 let mut eo = dense_ffn(&m.experts[e], x, pool);
19584 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
19585 for i in 0..out.len() {
19586 out[i] += w * eo[i];
19587 }
19588 attention::recycle_buf(&mut eo);
19589 }
19590 if let Some((se, gate)) = &m.shared {
19591 let mut so = dense_ffn(se, x, pool);
19592 let g = gate.as_ref().map_or(1.0, |gate| {
19593 let mut gl = [0.0f32; 1];
19594 gate.matvec(x, &mut gl, pool);
19595 1.0 / (1.0 + (-gl[0]).exp())
19596 });
19597 for i in 0..out.len() {
19598 out[i] += g * so[i];
19599 }
19600 attention::recycle_buf(&mut so);
19601 }
19602 out
19603}
19604
19605#[allow(clippy::too_many_arguments)]
19613pub(crate) fn mla_attention(
19614 w: &MlaWeights,
19615 normed: &[f32],
19616 cache: &mut crate::kv_cache::LayerKvCache,
19617 position: usize,
19618 inv_freq: &[f32],
19619 rope_scale: f32,
19620 eps: f64,
19621 pool: Option<&Pool>,
19622) -> Vec<f32> {
19623 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
19624 let hd = dr + dn;
19625 let mut q = vec![0.0f32; nh * hd];
19626 match (&w.q_a, &w.q_a_norm) {
19627 (Some(qa), Some(qn)) => {
19628 let mut t = vec![0.0f32; qa.rows()];
19629 qa.matvec(normed, &mut t, pool);
19630 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
19631 w.q_proj.matvec(&tn, &mut q, pool);
19632 }
19633 _ => w.q_proj.matvec(normed, &mut q, pool),
19634 }
19635 let mut ca = vec![0.0f32; lora + dr];
19636 w.kv_a.matvec(normed, &mut ca, pool);
19637 let (c_lat, k_rope) = ca.split_at_mut(lora);
19638 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
19639 let mut kvb = vec![0.0f32; nh * (dn + dv)];
19640 w.kv_b.matvec(&latn, &mut kvb, pool);
19641 if !w.nope {
19642 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
19643 }
19644 for h in 0..nh {
19645 if !w.nope {
19646 attention::rope_rotate_scaled(
19647 &mut q[h * hd..h * hd + dr],
19648 position,
19649 inv_freq,
19650 rope_scale,
19651 );
19652 }
19653 }
19654 let mut k = vec![0.0f32; nh * hd];
19655 let mut v = vec![0.0f32; nh * hd];
19656 for h in 0..nh {
19657 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
19658 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
19659 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
19660 }
19661 cache.append(&k, &v, &vec![true; nh]);
19662 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
19663 attention::recycle_buf(&mut imp);
19664 let mut ov = vec![0.0f32; nh * dv];
19665 for h in 0..nh {
19666 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
19667 }
19668 let mut out = vec![0.0f32; w.o_proj.rows()];
19669 w.o_proj.matvec(&ov, &mut out, pool);
19670 out
19671}
19672
19673fn dense_moe_ffn(
19680 dm: &DenseMoeFfn,
19681 x_normed: &[f32],
19682 h_raw: &[f32],
19683 eps: f64,
19684 norm_style: NormStyle,
19685 pool: Option<&Pool>,
19686) -> Vec<f32> {
19687 let mut d = dense_ffn(&dm.dense, x_normed, pool);
19688 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
19689 let m = &dm.moe;
19690 let ne = m.experts.len();
19691 let mut logits = vec![0.0f32; ne];
19692 if m.router_input_norm {
19693 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
19694 let inv = 1.0 / (ss + eps as f32).sqrt();
19695 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
19696 m.router.matvec(&xr, &mut logits, pool);
19697 } else {
19698 m.router.matvec(h_raw, &mut logits, pool);
19699 }
19700 let (idx, p, wsum) = moe_route(&logits, m, None);
19701 {
19702 let mut st = m.stats.borrow_mut();
19703 if st.len() < ne {
19704 st.resize(ne, 0);
19705 }
19706 for &e in &idx {
19707 st[e] += 1;
19708 }
19709 }
19710 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
19711 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
19712 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
19713 for (di, mi) in d.iter_mut().zip(&mo) {
19714 *di += mi;
19715 }
19716 d
19717}
19718
19719fn moe_gpu_refused(why: &'static str) {
19726 use std::sync::atomic::{AtomicBool, Ordering};
19727 static SAID: AtomicBool = AtomicBool::new(false);
19728 if !SAID.swap(true, Ordering::Relaxed) {
19729 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
19730 }
19731}
19732
19733fn moe_ffn_gpu(
19734 m: &MoeFfn,
19735 x: &[f32],
19736 idx: &[usize],
19737 p: &[f32],
19738 wsum: f32,
19739 pool: Option<&Pool>,
19740) -> Option<Vec<f32>> {
19741 use crate::gpu::MoeJob;
19742
19743 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
19744 let mut model_ref = None;
19745 for &e in idx {
19746 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
19747 moe_gpu_refused("push_job(expert)");
19748 return None;
19749 }
19750 }
19751 if let Some((se, gate)) = &m.shared {
19752 let g = gate.as_ref().map_or(1.0, |gate| {
19753 let mut gl = [0.0f32; 1];
19754 gate.matvec(x, &mut gl, pool);
19755 1.0 / (1.0 + (-gl[0]).exp())
19756 });
19757 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
19758 moe_gpu_refused("push_job(shared)");
19759 return None;
19760 }
19761 }
19762 let Some(model) = model_ref else {
19763 moe_gpu_refused("no model_ref");
19764 return None;
19765 };
19766 let hidden = jobs[0].down.1;
19767 let mut out = vec![0.0f32; hidden];
19768 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19769 Some(out)
19770 } else {
19771 moe_gpu_refused("gpu::moe_block");
19772 None
19773 }
19774}
19775
19776fn ffn_forward(
19778 ffn: &FfnKind,
19779 x: &[f32],
19780 pool: Option<&Pool>,
19781 experts_allowed: Option<&[bool]>,
19782) -> Vec<f32> {
19783 match ffn {
19784 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
19785 FfnKind::Dense(d) => dense_ffn(d, x, pool),
19786 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
19787 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19791 }
19792}
19793
19794fn ffn_forward_pair(
19798 ffn: &FfnKind,
19799 x1: &[f32],
19800 x2: &[f32],
19801 pool: Option<&Pool>,
19802 experts_allowed: Option<&[bool]>,
19803) -> (Vec<f32>, Vec<f32>) {
19804 let d = match ffn {
19805 FfnKind::Dense(d) if !d.segs.is_empty() => {
19808 return (
19809 tube_ffn(d, x1, 1, pool, None),
19810 tube_ffn(d, x2, 1, pool, None),
19811 );
19812 }
19813 FfnKind::Dense(d) => d,
19814 FfnKind::Moe(m) => {
19815 return (
19816 moe_ffn(m, x1, pool, experts_allowed),
19817 moe_ffn(m, x2, pool, experts_allowed),
19818 );
19819 }
19820 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19821 };
19822 let inter = d.gate_proj.rows();
19823 FFN_SCRATCH.with(|s| {
19824 let mut s = s.borrow_mut();
19825 let [g1, g2, u1, u2] = &mut *s;
19826 g1.resize(inter, 0.0);
19827 g2.resize(inter, 0.0);
19828 u1.resize(inter, 0.0);
19829 u2.resize(inter, 0.0);
19830 QTensor::matvec2_many(
19833 [&d.gate_proj, &d.up_proj],
19834 x1,
19835 x2,
19836 [g1.as_mut_slice(), u1.as_mut_slice()],
19837 [g2.as_mut_slice(), u2.as_mut_slice()],
19838 pool,
19839 );
19840 for i in 0..inter {
19841 g1[i] = d.act.combine(g1[i], u1[i]);
19842 g2[i] = d.act.combine(g2[i], u2[i]);
19843 }
19844 let mut o1 = attention::take_buf(d.down_proj.rows());
19845 let mut o2 = attention::take_buf(d.down_proj.rows());
19846 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
19847 (o1, o2)
19848 })
19849}
19850
19851#[cfg(test)]
19852mod tests {
19853
19854 #[test]
19859 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
19860 use super::{
19861 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
19862 };
19863 let dense_card = ChunkStackFacts {
19864 plain_dense: true,
19865 discrete: true,
19866 gpu_on: true,
19867 ..Default::default()
19868 };
19869 assert!(dense_card.dense_on_discrete());
19870 assert_eq!(
19872 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
19873 DISCRETE_DENSE_PREFILL_CHUNK
19874 );
19875 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
19876 for (label, facts) in [
19877 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
19878 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
19879 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
19880 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
19881 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
19882 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
19883 ] {
19884 assert!(!facts.dense_on_discrete(), "{label}");
19885 assert_eq!(
19886 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
19887 48,
19888 "{label} keeps the historical x86 chunk"
19889 );
19890 }
19891 for dense in [false, true] {
19893 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
19894 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
19895 }
19896 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
19898 for dense in [false, true] {
19899 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
19900 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
19901 }
19902 }
19903 }
19904
19905 #[test]
19906 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
19907 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
19908 let full = |host_rows, device_rows| ReuseLayer {
19909 full: true,
19910 host_rows,
19911 device_rows,
19912 device_state: false,
19913 };
19914 assert_eq!(
19917 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
19918 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
19919 );
19920 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
19922 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
19924 assert_eq!(
19926 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
19927 ReusePlan::Pull(vec![(0, 300, 339)])
19928 );
19929 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
19931 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
19932 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
19933 let conv = |device_state| ReuseLayer {
19936 full: false,
19937 host_rows: 0,
19938 device_rows: None,
19939 device_state,
19940 };
19941 assert_eq!(
19942 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
19943 ReusePlan::Fresh
19944 );
19945 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
19946 }
19947
19948 #[test]
19949 fn nll_graph_policy_scopes_only_the_fused_head() {
19950 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
19951 ("vulkan graph", true, true, false, true, false),
19953 ("native Metal graph", true, true, true, true, true),
19955 ("masked", false, true, false, false, false),
19957 ("graph disabled", true, false, true, false, false),
19958 ] {
19959 let (graph_quality, graph_head_required) =
19960 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
19961 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
19962 assert_eq!(graph_head_required, want_head, "{label}: fused head");
19963 }
19964 }
19965
19966 #[test]
19967 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
19968 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
19969 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
19970 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
19971 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
19972 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
19973 }
19974
19975 #[test]
19976 fn cancel_flag_stops_generation() {
19977 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
19978 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
19981 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
19982 assert_eq!(r.finish_reason, "cancelled");
19983 assert!(
19984 r.token_ids.is_empty(),
19985 "no tokens after cancel: {:?}",
19986 r.token_ids
19987 );
19988 assert_eq!(p.kv_cache.seq_len(), 0);
19989 assert!(p.kv_history.is_empty());
19990 assert!(!p.graph_want_logits);
19991 assert!(p.graph_logits.is_none());
19992 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
19994 assert_ne!(r2.finish_reason, "cancelled");
19995 }
19996 use super::*;
19997
19998 #[test]
19999 fn explicit_sliding_schedule_selects_local_rope_profile() {
20000 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
20005 p.swa = Some((1024, usize::MAX));
20006 p.sliding_layers = Some(vec![true, true, true, false]);
20007 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
20008 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
20009 p.rope_scale = 1.277_258_9;
20010 p.rope_scale_local = 1.0;
20011
20012 for li in 0..3 {
20013 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
20014 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
20015 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
20016 }
20017 assert_eq!(p.layer_window(3), None);
20018 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
20019 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
20020 }
20021
20022 #[test]
20030 fn dynamic_ffn_equals_the_zeroing_arm() {
20031 let (hidden, inter) = (8usize, 32usize);
20032 let synth = |n: usize, salt: usize| -> Vec<f32> {
20033 (0..n)
20034 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
20035 .collect()
20036 };
20037 let down = synth(hidden * inter, 3);
20038 let mut down_t = vec![0.0f32; inter * hidden];
20039 for r in 0..hidden {
20040 for c in 0..inter {
20041 down_t[c * hidden + r] = down[r * inter + c];
20042 }
20043 }
20044 let d = DenseFfn {
20045 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20046 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20047 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
20048 act: Act::Silu,
20049 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
20050 segs: Vec::new(),
20051 };
20052 let x = synth(hidden, 11);
20053 let k = 12usize;
20054 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
20055 let mut g = vec![0.0f32; inter];
20057 d.gate_proj.matvec(&x, &mut g, None);
20058 let mut u = vec![0.0f32; inter];
20059 d.up_proj.matvec(&x, &mut u, None);
20060 for v in g.iter_mut() {
20061 *v = inference::silu(*v);
20062 }
20063 keep_top_k(&mut g, k);
20064 for i in 0..inter {
20065 g[i] *= u[i];
20066 }
20067 let mut want = vec![0.0f32; hidden];
20068 d.down_proj.matvec(&g, &mut want, None);
20069 for (a, b) in want.iter().zip(&got) {
20070 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
20071 }
20072 }
20073
20074 #[test]
20080 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
20081 let (hidden, core, tube) = (8usize, 12usize, 8usize);
20082 let inter = core + tube;
20083 let synth = |n: usize, salt: usize| -> Vec<f32> {
20084 (0..n)
20085 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
20086 .collect()
20087 };
20088 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
20089 let d_all = synth(hidden * inter, 3);
20090 let dense = DenseFfn {
20092 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
20093 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
20094 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
20095 act: Act::Silu,
20096 down_t: None,
20097 segs: Vec::new(),
20098 };
20099 let rows =
20100 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
20101 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
20102 let mut o = Vec::with_capacity(hidden * (b - a));
20103 for r in 0..hidden {
20104 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
20105 }
20106 o
20107 };
20108 let tubed = DenseFfn {
20109 down_t: None,
20110 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
20111 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
20112 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
20113 act: Act::Silu,
20114 segs: vec![FfnSeg {
20115 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
20116 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
20117 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
20118 start: core,
20119 width: tube,
20120 }],
20121 };
20122 let x = synth(hidden, 7);
20123 let want = dense_ffn(&dense, &x, None);
20124 let got = tube_ffn(&tubed, &x, 1, None, None);
20125 for (a, b) in want.iter().zip(&got) {
20126 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
20127 }
20128 let mut bits = vec![0u8; inter.div_ceil(8)];
20130 for n in 0..core {
20131 bits[n / 8] |= 1 << (n % 8);
20132 }
20133 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20134 let masked = dense_ffn_masked(&dense, &x, None, &bits);
20135 for (a, b) in masked.iter().zip(&closed) {
20136 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
20137 }
20138 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
20140 for (a, b) in closed.iter().zip(&batch) {
20141 assert_eq!(a, b, "batch arm disagrees with decode arm");
20142 }
20143 }
20144
20145 #[test]
20147 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
20148 let (hidden, inter) = (16usize, 40usize);
20149 let synth = |n: usize, salt: usize| -> Vec<f32> {
20150 (0..n)
20151 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
20152 .collect()
20153 };
20154 let d = DenseFfn {
20155 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
20156 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
20157 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
20158 act: Act::Silu,
20159 down_t: None,
20160 segs: Vec::new(),
20161 };
20162 let x = synth(hidden, 9);
20163 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
20165
20166 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
20167
20168 let mut g = vec![0.0f32; inter];
20170 d.gate_proj.matvec(&x, &mut g, None);
20171 let mut u = vec![0.0f32; inter];
20172 d.up_proj.matvec(&x, &mut u, None);
20173 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
20174 for i in 0..inter {
20175 g[i] = if act_set.contains(&(i as u16)) {
20176 inference::silu(g[i]) * u[i]
20177 } else {
20178 0.0
20179 };
20180 }
20181 let mut reference = vec![0.0f32; hidden];
20182 d.down_proj.matvec(&g, &mut reference, None);
20183
20184 let max_d = sparse
20185 .iter()
20186 .zip(&reference)
20187 .map(|(a, b)| (a - b).abs())
20188 .fold(0.0f32, f32::max);
20189 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
20190 }
20191
20192 fn attach_test_mtp(p: &mut Pipeline) {
20194 let (h, inter, heads, kv, hd) = (
20195 p.hidden_size,
20196 p.intermediate_size,
20197 p.num_heads,
20198 p.num_kv_heads,
20199 p.head_dim,
20200 );
20201 let synth = |n: usize, salt: usize| -> Vec<f32> {
20202 (0..n)
20203 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
20204 .collect()
20205 };
20206 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
20207 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20208 };
20209 p.mtp = Some(MtpModule {
20210 enorm: vec![1.0; h],
20211 hnorm: vec![1.0; h],
20212 eh_proj: qt(h, 2 * h, 301),
20213 layer: LayerWeights {
20214 input_norm: vec![1.0; h],
20215 post_norm: vec![1.0; h],
20216 attn_out_norm: None,
20217 ffn_out_norm: None,
20218 layer_scale: None,
20219 ffn: FfnKind::Dense(DenseFfn {
20220 gate_proj: qt(inter, h, 315),
20221 up_proj: qt(inter, h, 316),
20222 down_proj: qt(h, inter, 317),
20223 act: Act::Silu,
20224 down_t: None,
20225 segs: Vec::new(),
20226 }),
20227 attn: AttnKind::Full {
20228 bias: None,
20229 wq: qt(heads * hd, h, 311),
20230 wk: qt(kv * hd, h, 312),
20231 wv: qt(kv * hd, h, 313),
20232 wo: qt(h, heads * hd, 314),
20233 q_norm: None,
20234 k_norm: None,
20235 output_gate: false,
20236 softplus_gate: None,
20237 },
20238 },
20239 final_norm: vec![1.0; h],
20240 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
20241 });
20242 }
20243
20244 #[test]
20245 fn speculative_equals_vanilla_greedy() {
20246 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20250 let run = |spec: bool| {
20251 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20252 p.sampler_config.temperature = 0.0;
20253 attach_test_mtp(&mut p);
20254 p.speculative = spec;
20255 let r = p.generate("abcdef", 12, None, None).unwrap();
20256 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
20257 };
20258 let (vanilla, d0, _) = run(false);
20259 let (spec, d1, a1) = run(true);
20260 assert_eq!(d0, 0, "vanilla path must not draft");
20261 assert!(d1 > 0, "speculative path must draft");
20262 assert_eq!(
20263 vanilla, spec,
20264 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
20265 );
20266 }
20267
20268 #[test]
20269 fn speculative_accepts_constant_oracle() {
20270 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20272 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20273 p.sampler_config.temperature = 0.0;
20274 p.sampler_config.repetition_penalty = 1.0;
20275 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
20278 attach_test_mtp(&mut p);
20279 p.speculative = true;
20280 let r = p.generate("abcd", 10, None, None).unwrap();
20281 assert!(r.mtp_drafted > 0);
20282 assert_eq!(
20283 r.mtp_accepted, r.mtp_drafted,
20284 "constant logits → every draft accepted"
20285 );
20286 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20289 }
20290
20291 #[test]
20292 fn empty_prompt_is_an_error_not_a_panic() {
20293 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20294 let r = p.generate("", 4, None, None);
20295 assert!(r.is_err(), "empty prompt must be a clean error");
20296 }
20297
20298 #[test]
20299 fn every_token_enters_kv_exactly_once() {
20300 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20301 p.sampler_config.temperature = 0.0;
20303 let r = p.generate("abc", 2, None, None).unwrap();
20304 assert_eq!(r.prompt_tokens, 3);
20305 assert_eq!(
20309 p.kv_cache.seq_len(),
20310 3 + r.tokens_generated - 1,
20311 "each token must be cached exactly once (v1 cached the last prompt token twice)"
20312 );
20313 }
20314
20315 #[test]
20316 fn generation_is_reproducible_with_seed() {
20317 let run = || {
20318 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20319 p.generate("hello", 8, None, None).unwrap().token_ids
20320 };
20321 assert_eq!(run(), run());
20322 }
20323
20324 #[test]
20325 fn resetting_sampler_restarts_the_seeded_stream() {
20326 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20327 let config = SamplerConfig {
20328 seed: Some(1234),
20329 ..SamplerConfig::default()
20330 };
20331 p.set_sampler_config(config.clone());
20332 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20333 p.set_sampler_config(config);
20334 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20335 assert_eq!(first, second);
20336 }
20337
20338 #[test]
20339 fn eviction_bounds_the_cache() {
20340 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20341 p.kv_cache.max_seq_len = 6;
20342 p.sampler_config.temperature = 0.0;
20343 let _ = p.generate("abcd", 12, None, None).unwrap();
20344 assert!(
20345 p.kv_cache.seq_len() <= 6 + 1,
20346 "cache must stay bounded by max_seq_len (got {})",
20347 p.kv_cache.seq_len()
20348 );
20349 }
20350
20351 #[test]
20352 fn confidence_matches_tokens_and_is_a_probability() {
20353 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20354 p.sampler_config.temperature = 0.0;
20355 p.sampler_config.repetition_penalty = 1.0;
20356 let r = p.generate("abcd", 10, None, None).unwrap();
20357 assert_eq!(
20358 r.token_confidence.len(),
20359 r.token_ids.len(),
20360 "one confidence per emitted token"
20361 );
20362 for &c in &r.token_confidence {
20363 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20364 }
20365 let logits = [1.0f32, 3.0, 0.5, 3.0];
20367 let p0 = top1_prob_t(&logits, 1, 1.0);
20368 let p1 = top1_prob_t(&logits, 3, 1.0);
20369 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20370 assert!(p0 > 0.0 && p0 < 1.0);
20371 let sharp = top1_prob_t(&logits, 1, 1.0);
20373 let soft = top1_prob_t(&logits, 1, 2.0);
20374 assert!(soft < sharp, "higher temperature lowers peak confidence");
20375 }
20376
20377 #[test]
20378 fn trace_is_opt_in_and_parallels_the_output() {
20379 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20381 p.sampler_config.temperature = 0.0;
20382 p.sampler_config.repetition_penalty = 1.0;
20383 let r = p.generate("abcd", 10, None, None).unwrap();
20384 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20385
20386 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20388 p.sampler_config.temperature = 0.0;
20389 p.sampler_config.repetition_penalty = 1.0;
20390 p.set_trace(true);
20391 let r = p.generate("abcd", 10, None, None).unwrap();
20392 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20393 for (i, tr) in r.traces.iter().enumerate() {
20394 assert_eq!(tr.t, i, "trace index is sequential");
20395 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20396 assert_eq!(
20397 tr.confidence, r.token_confidence[i],
20398 "trace confidence matches the confidence channel"
20399 );
20400 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20402 }
20403 }
20404
20405 #[test]
20406 fn explain_prefill_logits_match_greedy_first_token() {
20407 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20411 p.sampler_config.temperature = 0.0;
20412 p.sampler_config.repetition_penalty = 1.0;
20413 let ids = p.tokenizer.encode("abcd");
20414 let logits = p.prefill_next_logits(&ids, None);
20415 let argmax = logits
20416 .iter()
20417 .enumerate()
20418 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20419 .unwrap()
20420 .0 as u32;
20421 let r = p.generate("abcd", 1, None, None).unwrap();
20422 assert_eq!(
20423 argmax, r.token_ids[0],
20424 "explain preview must match greedy emit"
20425 );
20426 }
20427
20428 #[test]
20429 fn laguna_shared_expert_is_unconditionally_added() {
20430 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20431 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20432 let zero_dense = || DenseFfn {
20433 gate_proj: matrix(vec![0.0; 4]),
20434 up_proj: matrix(vec![0.0; 4]),
20435 down_proj: matrix(vec![0.0; 4]),
20436 act: Act::Silu,
20437 down_t: None,
20438 segs: Vec::new(),
20439 };
20440 let shared = DenseFfn {
20441 gate_proj: identity(),
20442 up_proj: identity(),
20443 down_proj: identity(),
20444 act: Act::Silu,
20445 down_t: None,
20446 segs: Vec::new(),
20447 };
20448 let x = [1.0, 2.0];
20449 let expected = dense_ffn(&shared, &x, None);
20450 let moe = MoeFfn {
20451 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20452 experts: vec![zero_dense()],
20453 top_k: 1,
20454 norm_topk_prob: true,
20455 router_sigmoid: true,
20456 expert_bias: None,
20457 routed_scaling: 1.0,
20458 route_tau: None,
20459 shared: Some((shared, None)),
20460 stats: std::cell::RefCell::new(Vec::new()),
20461 act_sq: std::cell::RefCell::new(Vec::new()),
20462 act_rows: std::cell::RefCell::new(Vec::new()),
20463 mask: None,
20464 per_expert_scale: None,
20465 router_input_norm: false,
20466 resonance: None,
20467 grown: Vec::new(),
20468 };
20469 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
20470 for (actual, expected) in actual.iter().zip(expected) {
20471 assert!((actual - expected).abs() < 1e-6);
20472 }
20473 }
20474
20475 fn mimo_test_pipeline() -> Pipeline {
20484 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
20485 let kvh = [1usize, 2, 2, 1];
20486 let synth = |n: usize, salt: usize| -> Vec<f32> {
20487 (0..n)
20488 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20489 .collect()
20490 };
20491 let qt = |rows: usize, cols: usize, salt: usize| {
20492 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20493 };
20494 let dense = |inter: usize, salt: usize| DenseFfn {
20495 gate_proj: qt(inter, hs, salt),
20496 up_proj: qt(inter, hs, salt + 1),
20497 down_proj: qt(hs, inter, salt + 2),
20498 act: Act::Silu,
20499 down_t: None,
20500 segs: Vec::new(),
20501 };
20502 let layers: Vec<LayerWeights> = (0..4)
20503 .map(|li| LayerWeights {
20504 input_norm: vec![1.0; hs],
20505 post_norm: vec![1.0; hs],
20506 attn_out_norm: None,
20507 ffn_out_norm: None,
20508 layer_scale: None,
20509 ffn: if li == 0 {
20510 FfnKind::Dense(dense(inter, 50))
20511 } else {
20512 FfnKind::Moe(MoeFfn {
20513 router: qt(4, hs, 60 + li),
20514 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
20515 top_k: 2,
20516 norm_topk_prob: true,
20517 router_sigmoid: true,
20518 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
20519 routed_scaling: 1.0,
20520 route_tau: None,
20521 shared: None,
20522 stats: std::cell::RefCell::new(Vec::new()),
20523 act_sq: std::cell::RefCell::new(Vec::new()),
20524 act_rows: std::cell::RefCell::new(Vec::new()),
20525 mask: None,
20526 per_expert_scale: None,
20527 router_input_norm: false,
20528 resonance: None,
20529 grown: Vec::new(),
20530 })
20531 },
20532 attn: AttnKind::Full {
20533 wq: qt(nh * hd, hs, li * 10 + 1),
20534 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
20535 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
20536 wo: qt(hs, nh * vd, li * 10 + 4),
20537 q_norm: None,
20538 k_norm: None,
20539 output_gate: false,
20540 softplus_gate: None,
20541 bias: None,
20542 },
20543 })
20544 .collect();
20545 let mut p = Pipeline::new(
20546 Tokenizer::byte_level(),
20547 PipelineWeights {
20548 embed_tokens: qt(vocab, hs, 100),
20549 layers,
20550 lm_head: qt(vocab, hs, 200),
20551 final_norm: vec![1.0; hs],
20552 },
20553 hs,
20554 inter,
20555 nh,
20556 1, hd,
20558 4,
20559 4,
20560 false,
20561 vocab,
20562 1e-6,
20563 1e7,
20564 NormStyle::Qwen,
20565 4096,
20566 SamplerConfig {
20567 seed: Some(7),
20568 ..Default::default()
20569 },
20570 );
20571 p.layer_dump = None;
20573 p.set_rotary(4, 1e7);
20574 p.sliding_layers = Some(vec![false, true, true, false]);
20575 p.swa = Some((3, usize::MAX));
20576 p.rotary_dim_local = Some(4);
20577 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
20578 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
20579 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
20580 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
20581 p
20582 }
20583
20584 #[test]
20585 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
20586 let mut p = mimo_test_pipeline();
20587 p.speculative = false;
20588 p.ignore_eos = true;
20589 p.sampler_config.temperature = 0.0;
20590 p.sampler_config.repetition_penalty = 1.0;
20591 let a = vec![3, 5, 7, 9, 11, 13];
20592 let b = vec![4, 8, 12, 16, 20, 24];
20593 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
20594 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
20595 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
20599 assert_eq!(actual, expected);
20600 assert!(p.kv_history.is_empty());
20601 let mut extended = a.clone();
20602 extended.push(17);
20603 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20604 p.reset_session();
20605 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20606 assert_eq!(after_media, fresh);
20607 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
20608 p.reset_session();
20612 p.generate_from_ids(&a, 1, None, None).unwrap();
20613 let mut media_ids = p.kv_history.clone();
20614 assert!(!media_ids.is_empty());
20615 media_ids.extend_from_slice(&[19, 21, 23]);
20616 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
20617 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
20618 let mut oracle = mimo_test_pipeline();
20619 oracle.speculative = false;
20620 oracle.ignore_eos = true;
20621 oracle.sampler_config.temperature = 0.0;
20622 oracle.sampler_config.repetition_penalty = 1.0;
20623 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
20624 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
20625 assert!(p.kv_history.is_empty());
20626 let mut bad = rows;
20627 bad[0] = f32::NAN;
20628 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
20629 }
20630
20631 fn f32_bits(v: &[f32]) -> Vec<u32> {
20632 v.iter().map(|x| x.to_bits()).collect()
20633 }
20634
20635 #[test]
20642 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
20643 let mut p = mimo_test_pipeline();
20644 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
20645 assert_eq!(kv, vec![1, 2, 2, 1]);
20646 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
20647 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
20648 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
20649 let hs = p.hidden_size;
20650 let mut decode = Vec::new();
20651 for (pos, &id) in ids.iter().enumerate() {
20652 let e = p.embed_single(id);
20653 let h = p.forward_layers(&e, pos, None);
20654 decode.push(p.logits_from_hidden(&h));
20655 }
20656 for l in &p.kv_cache.layers {
20657 assert_eq!(l.seq_len, 12);
20658 assert_eq!(l.head_values(0).len(), 12 * 8);
20660 }
20661 assert!(decode.iter().flatten().all(|v| v.is_finite()));
20662
20663 p.clear_sequence_state();
20664 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20665 for pos in 0..ids.len() {
20666 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
20667 assert_eq!(
20668 f32_bits(&decode[pos]),
20669 f32_bits(&lg),
20670 "whole prompt, pos {pos}"
20671 );
20672 }
20673
20674 p.clear_sequence_state();
20675 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
20676 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
20677 for pos in 0..ids.len() {
20678 let row = if pos < 5 {
20679 &a[pos * hs..(pos + 1) * hs]
20680 } else {
20681 &b[(pos - 5) * hs..(pos - 4) * hs]
20682 };
20683 let lg = p.logits_from_hidden(row);
20684 assert_eq!(
20685 f32_bits(&decode[pos]),
20686 f32_bits(&lg),
20687 "two chunks, pos {pos}"
20688 );
20689 }
20690
20691 let last = |p: &mut Pipeline| {
20694 p.clear_sequence_state();
20695 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20696 p.logits_from_hidden(&hb[11 * hs..12 * hs])
20697 };
20698 let base = last(&mut p);
20699 let mut no_sinks = mimo_test_pipeline();
20700 for l in &mut no_sinks.kv_cache.layers {
20701 l.sinks = None;
20702 }
20703 assert_ne!(
20704 f32_bits(&last(&mut no_sinks)),
20705 f32_bits(&base),
20706 "sinks are live"
20707 );
20708 let mut wide = mimo_test_pipeline();
20709 wide.swa = Some((64, usize::MAX));
20710 assert_ne!(
20711 f32_bits(&last(&mut wide)),
20712 f32_bits(&base),
20713 "window is live"
20714 );
20715
20716 p.clear_sequence_state();
20718 p.ignore_eos = true;
20719 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
20720 assert_eq!(r.token_ids.len(), 4);
20721 }
20722
20723 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
20726 let (hs, nh) = (16usize, 4usize);
20727 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
20728 p.layer_dump = None;
20729 p.swa = Some((6, 4));
20730 p.proj_gate_sigmoid = true;
20731 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
20732 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
20733 let g: Vec<f32> = (0..nh * hs)
20734 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
20735 .collect();
20736 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
20737 }
20738 }
20739 p.swa_trim = trim;
20740 p
20741 }
20742
20743 #[test]
20749 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
20750 let mut a = spark_test_pipeline(None);
20751 let mut b = spark_test_pipeline(Some((2, 4)));
20752 assert_eq!(
20753 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
20754 vec![Some(6), Some(6), Some(6), None]
20755 );
20756 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
20757 let mut pos = 0usize;
20758 for &n in [5usize, 13, 7, 16].iter().cycle() {
20759 if pos >= ids.len() {
20760 break;
20761 }
20762 let end = (pos + n).min(ids.len());
20763 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20764 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20765 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
20766 pos = end;
20767 }
20768 for _ in 0..23 {
20769 let e = a.embed_single(((pos * 7) % 64) as u32);
20770 let ha = a.forward_layers(&e, pos, None);
20771 let hb = b.forward_layers(&e, pos, None);
20772 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
20773 for li in 0..3 {
20774 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
20775 }
20776 pos += 1;
20777 }
20778 for round in 0..9 {
20779 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
20780 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
20781 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
20782 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
20783 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
20784 if round % 2 == 1 {
20785 for p in [&mut a, &mut b] {
20787 for l in &mut p.kv_cache.layers {
20788 l.truncate_last(2);
20789 }
20790 }
20791 } else {
20792 pos += 2;
20793 }
20794 }
20795 let e = a.embed_single(9);
20796 let (ha, hb) = (
20797 a.forward_layers(&e, pos, None),
20798 b.forward_layers(&e, pos, None),
20799 );
20800 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
20801 for li in 0..3 {
20802 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
20803 assert!(lb.base() > 0, "layer {li} trimmed");
20804 assert_eq!(la.base(), 0);
20805 assert_eq!(lb.pos_len(), la.seq_len);
20806 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
20807 }
20808 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
20809 assert_eq!(
20810 (gb.base(), gb.seq_len),
20811 (0, ga.seq_len),
20812 "the global layer keeps all"
20813 );
20814 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
20815 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
20816 }
20817
20818 #[test]
20823 fn swa_trim_wire_handoff_continues_bitwise() {
20824 let mut a = spark_test_pipeline(None);
20825 let mut b = spark_test_pipeline(Some((2, 4)));
20826 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
20827 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20828 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20829 assert_eq!(f32_bits(&ha), f32_bits(&hb));
20830 let mut pos = ids.len();
20831 for _ in 0..5 {
20832 let e = a.embed_single(((pos * 5) % 64) as u32);
20833 assert_eq!(
20834 f32_bits(&a.forward_layers(&e, pos, None)),
20835 f32_bits(&b.forward_layers(&e, pos, None))
20836 );
20837 pos += 1;
20838 }
20839 let mut c = spark_test_pipeline(Some((2, 4)));
20840 for li in 0..4 {
20841 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
20842 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
20843 }
20844 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
20845 assert_eq!(c.kv_cache.seq_len(), pos);
20846 for step in 0..17 {
20847 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
20848 let ha = a.forward_layers(&e, pos, None);
20849 let hc = c.forward_layers(&e, pos, None);
20850 assert_eq!(
20851 f32_bits(&ha),
20852 f32_bits(&hc),
20853 "after the hand-off, step {step}"
20854 );
20855 pos += 1;
20856 }
20857 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
20858 }
20859
20860 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
20863 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
20864 let synth = |len: usize, salt: usize| -> Vec<f32> {
20865 (0..len)
20866 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
20867 .collect()
20868 };
20869 let qt = |rows: usize, cols: usize, salt: usize| {
20870 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20871 };
20872 let layers = (0..n)
20873 .map(|k| {
20874 let s = 500 + k * 40;
20875 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
20876 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
20877 MtpModule {
20878 enorm: vec![1.0; hs],
20879 hnorm: vec![1.0; hs],
20880 eh_proj: qt(hs, 2 * hs, s),
20881 layer: LayerWeights {
20882 input_norm: vec![1.0; hs],
20883 post_norm: vec![1.0; hs],
20884 attn_out_norm: None,
20885 ffn_out_norm: None,
20886 layer_scale: None,
20887 attn: AttnKind::Full {
20888 wq: qt(nh * hd, hs, s + 1),
20889 wk: qt(nkv * hd, hs, s + 2),
20890 wv: qt(nkv * vd, hs, s + 3),
20891 wo: qt(hs, nh * vd, s + 4),
20892 q_norm: None,
20893 k_norm: None,
20894 output_gate: false,
20895 softplus_gate: None,
20896 bias: None,
20897 },
20898 ffn: FfnKind::Dense(DenseFfn {
20899 gate_proj: qt(inter, hs, s + 5),
20900 up_proj: qt(inter, hs, s + 6),
20901 down_proj: qt(hs, inter, s + 7),
20902 act: Act::Silu,
20903 down_t: None,
20904 segs: Vec::new(),
20905 }),
20906 },
20907 final_norm: vec![1.0; hs],
20908 kv,
20909 }
20910 })
20911 .collect();
20912 mimo_mtp::MimoMtp::from_layers(layers)
20913 }
20914
20915 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
20916 p.clear_sequence_state();
20917 p.speculative = spec;
20918 p.ignore_eos = true;
20919 p.sampler_config.temperature = 0.0;
20920 p.generate_from_ids(ids, n, None, None).unwrap()
20921 }
20922
20923 #[test]
20929 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
20930 for post in [false, true] {
20933 let mut p = mimo_test_pipeline();
20934 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
20936 let mut st0 = mimo_test_mtp(3, 1.0);
20937 st0.post_norm_hidden = post;
20938 p.mimo_mtp = Some(st0);
20939 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
20940 let hs = p.hidden_size;
20941 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20942 p.mimo_note_rows(&hb, 0);
20943 let mut st = p.mimo_mtp.take().unwrap();
20944 let k = 3;
20947 let mut inc = Vec::new();
20948 for t in 0..ids.len() - k - 1 {
20949 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
20950 }
20951 let s = ids.len();
20954 let mut reference = vec![vec![0u32; k]; s - k - 1];
20955 let mut fresh = mimo_test_mtp(3, 1.0);
20956 for (layer, m) in fresh.layers.iter_mut().enumerate() {
20957 let n = s - layer - 1;
20958 let mut cats = vec![0.0f32; n * 2 * hs];
20959 for j in 0..n {
20960 let e = p.embed_single(ids[j + layer + 1]);
20961 let raw = &hb[j * hs..(j + 1) * hs];
20962 let g = if post {
20963 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
20964 } else {
20965 raw.to_vec()
20966 };
20967 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
20968 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
20969 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
20970 }
20971 let mut x = vec![0.0f32; n * hs];
20972 m.eh_proj.matmat(&cats, n, &mut x, None);
20973 p.mimo_mtp_block(m, &mut x, n, 0);
20974 for (t, row) in reference.iter_mut().enumerate() {
20975 let y = inference::rms_norm(
20976 &x[t * hs..(t + 1) * hs],
20977 &m.final_norm,
20978 p.rms_eps,
20979 p.norm_style,
20980 );
20981 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
20982 }
20983 }
20984 assert_eq!(inc, reference, "post_norm_hidden = {post}");
20985 let distinct: std::collections::HashSet<u32> =
20987 inc.iter().flatten().copied().collect();
20988 assert!(distinct.len() > 3, "{inc:?}");
20989 let last_t = ids.len() - k - 2;
20991 for m in &st.layers {
20992 assert_eq!(m.kv.seq_len, last_t + 1);
20993 }
20994 }
20995 }
20996
20997 #[test]
21004 fn mimo_speculative_greedy_equals_plain_greedy() {
21005 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
21006 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
21007 let n = 24;
21008 let mut p = mimo_test_pipeline();
21009 let plain = mimo_greedy(&mut p, &ids, n, false);
21010 assert_eq!(plain.mtp_drafted, 0);
21011 assert_eq!(plain.token_ids.len(), n);
21012 let plain_kv = p.kv_cache.layers[0].seq_len;
21013
21014 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
21016 let spec = mimo_greedy(&mut p, &ids, n, true);
21017 assert!(spec.mtp_drafted > 0, "the round must draft");
21018 assert_eq!(spec.token_ids, plain.token_ids);
21019 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21020
21021 let mut truth: Vec<u32> = ids.clone();
21024 truth.extend(&plain.token_ids);
21025 let mut noisy = truth.clone();
21026 for (i, t) in noisy.iter_mut().enumerate() {
21027 if i % 5 == 0 {
21028 *t = (*t + 1) % 64;
21029 }
21030 }
21031 let mut st = mimo_test_mtp(3, 1.0);
21032 st.draft_override = Some(noisy);
21033 p.mimo_mtp = Some(st);
21034 let spec = mimo_greedy(&mut p, &ids, n, true);
21035 assert_eq!(spec.token_ids, plain.token_ids);
21036 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21037 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
21038 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
21039 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
21040 assert!(
21041 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
21042 "{:?}",
21043 stats.accept_hist
21044 );
21045 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
21046
21047 let mut st = mimo_test_mtp(3, 1.0);
21050 st.draft_override = Some(truth);
21051 p.mimo_mtp = Some(st);
21052 let spec = mimo_greedy(&mut p, &ids, n, true);
21053 assert_eq!(spec.token_ids, plain.token_ids);
21054 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
21055 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
21056
21057 let off = mimo_greedy(&mut p, &ids, n, false);
21059 assert_eq!(off.token_ids, plain.token_ids);
21060 assert_eq!(off.mtp_drafted, 0);
21061 }
21062
21063 #[test]
21072 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
21073 let p = mimo_test_pipeline();
21074 assert_eq!(
21075 p.graph_attn_decline_reason(),
21076 Some("per-layer KV head counts")
21077 );
21078 assert_eq!(p.wgpu_graph_attn_decline(), None);
21079 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
21080 assert_eq!(
21081 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
21082 (1, 4, 4, None, false)
21083 );
21084 assert_eq!(g0.invf, p.inv_freq.as_slice());
21085 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
21086 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
21087 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
21088 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
21089 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
21090 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
21091 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
21092
21093 let emb = p.embed_single(3);
21096 let mut lg = Vec::new();
21097 assert!(
21098 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
21099 .is_none()
21100 );
21101 let mut hid = emb.clone();
21102 assert_eq!(
21103 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
21104 crate::gpu::BatchGraphOutcome::Declined
21105 );
21106 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
21107 assert!(p.try_multi_burst(3, 0, 4).is_none());
21108 assert!(
21109 p.graph_declines().is_empty(),
21110 "no attention decline logged: {:?}",
21111 p.graph_declines()
21112 );
21113 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
21118 assert_eq!(plain().graph_attn_decline_reason(), None);
21119 assert_eq!(plain().wgpu_graph_attn_decline(), None);
21120 assert!(
21121 plain().graph_attn_geom(0).is_none(),
21122 "uniform models keep the historical arms"
21123 );
21124 let mut q = plain();
21125 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
21126 assert_eq!(
21127 q.graph_attn_decline_reason(),
21128 Some("learned attention sinks")
21129 );
21130 assert_eq!(
21131 q.graph_attn_geom(1).unwrap().sink,
21132 Some(&[0.25f32, -0.25][..])
21133 );
21134 let mut q = plain();
21135 q.set_attn_geometry(None, Some(2)).unwrap();
21136 assert_eq!(
21137 q.graph_attn_decline_reason(),
21138 Some("V heads narrower than Q/K heads")
21139 );
21140 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
21141 let mut q = plain();
21142 q.sliding_layers = Some(vec![true, false]);
21143 q.swa = Some((4, usize::MAX));
21144 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
21145 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
21146 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
21147
21148 let mut q = mimo_test_pipeline();
21152 q.rope_scale = 2.0;
21153 q.rope_scale_local = 1.25;
21154 assert_eq!(q.wgpu_graph_attn_decline(), None);
21155 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
21156 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
21157
21158 let mut q = plain();
21161 q.rope_scale = 2.0;
21162 assert_eq!(
21163 q.wgpu_graph_attn_decline(),
21164 Some("scaled RoPE without per-layer geometry")
21165 );
21166 }
21167
21168 #[test]
21169 fn mimo_verify_rewind_preserves_lagging_host_caches() {
21170 let mut p = mimo_test_pipeline();
21171 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
21172 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
21173 for _ in 0..if li == 0 { 2 } else { 12 } {
21174 layer.append(&row, &row, &[]);
21175 }
21176 }
21177 p.mimo_verify_rewind(9).unwrap();
21178 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
21179 for layer in &p.kv_cache.layers[1..] {
21180 assert_eq!(layer.seq_len, 9);
21181 }
21182 }
21183
21184 #[test]
21188 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
21189 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
21190 let _ = std::fs::remove_dir_all(&dir);
21191 let mut p = mimo_test_pipeline();
21192 let hs = p.hidden_size;
21193 let ids = [5u32, 9, 11, 2, 40];
21194 p.layer_dump = Some(dir.join("decode"));
21195 for (pos, &id) in ids.iter().enumerate() {
21196 let e = p.embed_single(id);
21197 let _ = p.forward_layers(&e, pos, None);
21198 }
21199 p.clear_sequence_state();
21200 p.layer_dump = Some(dir.join("prefill"));
21201 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21202 for pos in 0..ids.len() {
21203 for li in 0..p.num_layers {
21204 let name = format!("p{pos:06}_l{li:02}.f32");
21205 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
21206 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
21207 assert_eq!(a.len(), hs * 4, "{name}");
21208 assert_eq!(a, b, "{name}");
21209 }
21210 }
21211 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
21212 let vals: Vec<f32> = last
21213 .chunks(4)
21214 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
21215 .collect();
21216 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
21217 let _ = std::fs::remove_dir_all(&dir);
21218 }
21219
21220 #[test]
21221 fn attn_geometry_and_sinks_are_validated() {
21222 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
21223 assert!(
21224 p.set_attn_geometry(Some(vec![2]), None).is_err(),
21225 "one entry per layer"
21226 );
21227 assert!(
21228 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
21229 "3 does not divide 4"
21230 );
21231 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
21232 assert!(p.set_attn_geometry(None, Some(0)).is_err());
21233 assert!(
21234 p.set_attn_geometry(None, Some(5)).is_err(),
21235 "V wider than the head"
21236 );
21237 p.set_attn_geometry(None, Some(4)).unwrap();
21238 assert_eq!(
21239 p.v_head_dim, None,
21240 "v_head_dim == head_dim is the uniform case"
21241 );
21242 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
21243 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
21244 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
21245 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
21246 assert!(
21247 p.kv_cache.layers[1].sinks.is_some(),
21248 "a reshape keeps the layer's sinks"
21249 );
21250 assert_eq!(p.layer_geom(1).0, 4);
21251 assert!(
21252 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
21253 "one sink per Q head"
21254 );
21255 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
21256 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
21257 }
21258
21259 #[test]
21262 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
21263 let cfg = || {
21264 Some(crate::nystrom::O1Cfg {
21265 layers: crate::nystrom::O1Layers::All,
21266 m: 4,
21267 w: 8,
21268 sink: 2,
21269 rect: crate::nystrom::O1Rect::Aggregate,
21270 })
21271 };
21272 let mut p = mimo_test_pipeline();
21273 p.set_o1(cfg());
21274 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
21275 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
21276 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
21277 q.sliding_layers = Some(vec![false, false, true]);
21278 q.swa = Some((4, usize::MAX));
21279 q.set_o1(cfg());
21280 assert_eq!(q.o1_flags, vec![true, false, false]);
21281 }
21282
21283 #[test]
21284 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21285 const B: usize = 19;
21286 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21287 p.set_o1(Some(crate::nystrom::O1Cfg {
21288 layers: crate::nystrom::O1Layers::All,
21289 m: 4,
21290 w: 8,
21291 sink: 2,
21292 rect: crate::nystrom::O1Rect::Aggregate,
21293 }));
21294 p.o1_begin_with_prefix(Some(B));
21295 let ids: Vec<u32> = (0..B as u32).collect();
21296 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21297
21298 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21299 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21300 let next = p.embed_single(B as u32);
21301 let _ = p.forward_layers(&next, B, None);
21302 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21303 }
21304
21305 #[test]
21306 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21307 const B: usize = 19;
21308 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21309 let gdn_cfg = crate::linear_core::GdnCfg {
21313 num_v_heads: 2,
21314 num_k_heads: 1,
21315 key_head_dim: 2,
21316 value_head_dim: 4,
21317 conv_kernel: 3,
21318 hidden_size: 8,
21319 rms_eps: 1e-6,
21320 output_gate_sigmoid: false,
21321 };
21322 let synth = |n: usize, salt: usize| -> Vec<f32> {
21323 (0..n)
21324 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21325 .collect()
21326 };
21327 let qt = |rows: usize, cols: usize, salt: usize| {
21328 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21329 };
21330 let c_dim = gdn_cfg.conv_dim();
21331 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21332 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21333 in_proj_qkv: qt(c_dim, 8, 1),
21334 in_proj_z: qt(vd, 8, 2),
21335 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21336 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21337 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21338 a_log: vec![0.2, 0.5],
21339 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21340 norm: vec![1.0; gdn_cfg.value_head_dim],
21341 out_proj: qt(8, vd, 7),
21342 });
21343 p.gdn_cfg = Some(gdn_cfg);
21344 p.set_o1(Some(crate::nystrom::O1Cfg {
21345 layers: crate::nystrom::O1Layers::All,
21346 m: 4,
21347 w: 8,
21348 sink: 2,
21349 rect: crate::nystrom::O1Rect::Aggregate,
21350 }));
21351 p.o1_begin_with_prefix(Some(B));
21352 for pos in 0..B - 2 {
21353 let emb = p.embed_single(pos as u32);
21354 let _ = p.forward_layers(&emb, pos, None);
21355 }
21356 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21357
21358 let e1 = p.embed_single((B - 2) as u32);
21359 let e2 = p.embed_single((B - 1) as u32);
21360 let _ = p.forward_pair(&e1, &e2, B - 2);
21361
21362 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21363 assert!(
21364 p.kv_cache
21365 .layers
21366 .iter()
21367 .enumerate()
21368 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21369 );
21370 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21371 assert_ne!(
21372 p.kv_cache.layers[0].linear_state, lane1_state,
21373 "real pair must commit GDN lane 2 before returning"
21374 );
21375 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21376 let next = p.embed_single(B as u32);
21377 let _ = p.forward_layers(&next, B, None);
21378 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21379 }
21380
21381 #[test]
21382 fn o1_error_observation_stays_terminal_until_reset() {
21383 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21384 p.set_o1(Some(crate::nystrom::O1Cfg {
21385 layers: crate::nystrom::O1Layers::All,
21386 m: 4,
21387 w: 8,
21388 sink: 2,
21389 rect: crate::nystrom::O1Rect::Aggregate,
21390 }));
21391 p.o1_begin();
21392 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21393
21394 assert!(p.o1_seal_checked().is_err());
21395 assert!(
21396 p.o1_seal_checked().is_err(),
21397 "retry must see the sticky error"
21398 );
21399 let k = vec![0.2f32; 4];
21400 let v = vec![0.3f32; 4];
21401 p.kv_cache.layers[0].append(&k, &v, &[]);
21402 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
21403
21404 p.reset_session();
21405 p.o1_begin();
21406 p.kv_cache.layers[0].append(&k, &v, &[]);
21407 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
21408 }
21409
21410 #[test]
21411 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
21412 let ids = vec![1u32, 2, 3, 4, 5, 6];
21413 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21414 p.graph_logits = Some(vec![123.0]);
21415 p.graph_want_logits = true;
21416 p.graph_failed
21417 .store(true, std::sync::atomic::Ordering::Relaxed);
21418 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21419 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
21420 assert!(err.contains("before NLL"));
21421 assert!(p.graph_logits.is_none());
21422 assert!(!p.graph_want_logits);
21423 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21424 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21425
21426 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21427 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21428 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21429 assert_eq!(actual.1, expected.1);
21430 assert!((actual.0 - expected.0).abs() < 1e-9);
21431 }
21432
21433 #[test]
21434 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
21435 let ids = vec![1u32, 2, 3, 4, 5, 6];
21436 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21437 p.nll_test_fail_at = Some(1);
21438 let err = p
21439 .nll_ids_from(&ids, 0)
21440 .expect_err("one-shot forward failure");
21441 assert!(err.contains("forward") || err.contains("score row"));
21442 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21443 assert!(!p.graph_want_logits);
21444 assert!(p.graph_logits.is_none());
21445 assert!(p.kv_history.is_empty());
21446
21447 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21448 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21449 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21450 assert_eq!(actual.1, expected.1);
21451 assert!((actual.0 - expected.0).abs() < 1e-9);
21452 }
21453
21454 #[test]
21455 fn nll_serial_failure_before_first_row_is_reported() {
21456 let ids = vec![1u32, 2, 3, 4];
21457 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21458 p.nll_test_force_serial = true;
21459 p.nll_test_fail_at = Some(0);
21460 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
21461 assert!(err.contains("serial forward"));
21462 assert!(p.kv_history.is_empty());
21463 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21464 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21465 }
21466
21467 #[test]
21468 fn ffn_probe_failure_discards_recorder_and_state() {
21469 let ids = vec![1u32, 2, 3, 4];
21470 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21471 p.nll_test_fail_at = Some(0);
21472 let err = p
21473 .probe_ffn_mass_batch(&ids)
21474 .expect_err("probe forward failure");
21475 assert!(err.contains("NLL"));
21476 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
21477 assert!(p.kv_history.is_empty());
21478 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21479 }
21480
21481 #[test]
21482 fn nll_test_controls_are_pipeline_scoped() {
21483 let ids = vec![1u32, 2, 3, 4];
21484 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21485 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21486 failing.nll_test_force_serial = true;
21487 failing.nll_test_fail_at = Some(0);
21488
21489 assert!(!failing.can_prefill_batched());
21490 assert!(unaffected.can_prefill_batched());
21491 let expected = unaffected
21492 .nll_ids_from(&ids, 0)
21493 .expect("unaffected pipeline remains usable");
21494 let err = failing
21495 .nll_ids_from(&ids, 0)
21496 .expect_err("failure injection belongs to failing pipeline");
21497 assert!(err.contains("serial forward"));
21498 assert!(failing.nll_test_fail_at.is_none());
21499 assert!(unaffected.can_prefill_batched());
21500 let actual = unaffected
21501 .nll_ids_from(&ids, 0)
21502 .expect("unaffected pipeline remains reusable");
21503 assert_eq!(actual.1, expected.1);
21504 assert!((actual.0 - expected.0).abs() < 1e-9);
21505 }
21506
21507 #[test]
21508 fn forward_ids_failure_channel_is_terminal_and_reusable() {
21509 let ids = vec![1u32, 2, 3, 4, 5, 6];
21510 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21511 p.graph_logits = Some(vec![123.0]);
21512 p.graph_want_logits = true;
21513 p.graph_failed
21514 .store(true, std::sync::atomic::Ordering::Relaxed);
21515 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21516
21517 let err = p
21518 .forward_ids(&ids, None)
21519 .expect_err("a failed forward must not become a valid head result");
21520 assert!(err.contains("forward_ids setup"));
21521 assert!(p.graph_logits.is_none());
21522 assert!(!p.graph_want_logits);
21523 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21524 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21525 assert_eq!(p.kv_cache.seq_len(), 0);
21526
21527 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
21528 .forward_ids(&ids, None)
21529 .expect("fresh forward_ids");
21530 let actual = p
21531 .forward_ids(&ids, None)
21532 .expect("pipeline remains reusable after a failed forward");
21533 assert_eq!(actual.len(), expected.len());
21534 assert!(
21535 actual
21536 .iter()
21537 .zip(expected)
21538 .all(|(a, b)| (a - b).abs() < 1e-9)
21539 );
21540 assert_eq!(p.kv_cache.seq_len(), ids.len());
21541 }
21542
21543 #[test]
21544 fn sigmoid_router_floor_is_explicit_per_architecture() {
21545 let zero = || DenseFfn {
21551 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21552 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21553 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21554 act: Act::Silu,
21555 down_t: None,
21556 segs: Vec::new(),
21557 };
21558 let m = MoeFfn {
21559 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
21560 experts: vec![zero(), zero()],
21561 top_k: 1,
21562 norm_topk_prob: true,
21563 router_sigmoid: true,
21564 expert_bias: None,
21565 routed_scaling: 2.5,
21566 route_tau: None,
21567 shared: None,
21568 stats: std::cell::RefCell::new(Vec::new()),
21569 act_sq: std::cell::RefCell::new(Vec::new()),
21570 act_rows: std::cell::RefCell::new(Vec::new()),
21571 mask: None,
21572 per_expert_scale: None,
21573 router_input_norm: false,
21574 resonance: None,
21575 grown: Vec::new(),
21576 };
21577 let logits = [-20.0f32, -20.0];
21578 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
21579 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
21580 let expected = (p[0] + 1e-20) / m.routed_scaling;
21581 assert!((glm_wsum - expected).abs() < 1e-15);
21582 assert!(generic_wsum > glm_wsum * 100.0);
21583 }
21584
21585 #[test]
21586 fn resonance_scores_match_formula_and_stable_tie() {
21587 let r = Resonance {
21588 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
21590 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
21591 k: 1,
21592 bias: vec![1.5, 0.5, 0.0],
21593 shell: Vec::new(),
21594 };
21595 let x = [1.0f32, 1.0];
21596 let mut got = vec![0.0; 3];
21597 r.scores(&x, &mut got);
21598 assert!((got[0] - 0.5).abs() < 1e-6);
21602 assert!((got[1] - 0.5).abs() < 1e-6);
21603 assert!(got[2].abs() < 1e-6);
21604 let best = got
21605 .iter()
21606 .enumerate()
21607 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
21608 .map(|(i, _)| i);
21609 assert_eq!(best, Some(0));
21610 assert!(got.iter().all(|v| v.is_finite()));
21611 }
21612
21613 #[test]
21618 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
21619 let plain = Resonance {
21625 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
21626 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
21627 k: 1,
21628 bias: vec![1.5, 0.5, 0.0, 0.0],
21629 shell: Vec::new(),
21630 };
21631 let shelled = Resonance {
21632 mu: plain.mu.clone(),
21633 u: plain.u.clone(),
21634 k: 1,
21635 bias: plain.bias.clone(),
21636 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
21637 };
21638 assert!(!plain.has_shell());
21639 assert!(shelled.has_shell());
21640 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
21641 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
21642 set_growth_shell(Some(true));
21643 assert!(growth_shell_enabled());
21644 let x = [1.0f32, 1.0];
21647 plain.scores(&x, &mut a);
21648 shelled.scores(&x, &mut b);
21649 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
21650 assert!(a[2] == 0.0 && a[3] == 0.0);
21651 let xo = [3.0f32, 0.0];
21654 plain.scores(&xo, &mut a);
21655 shelled.scores(&xo, &mut b);
21656 assert_eq!(a[2], -6.0);
21657 assert_eq!(a[3], -6.0);
21658 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
21659 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
21660 assert_eq!(shelled.effective_shell(4), shelled.shell);
21661 set_growth_shell(Some(false));
21664 assert!(!growth_shell_enabled());
21665 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
21666 shelled.scores(&xo, &mut b);
21667 assert_eq!(bits(&a), bits(&b));
21668 set_growth_shell(None);
21669 let short = Resonance {
21672 shell: vec![f32::INFINITY, f32::INFINITY],
21673 ..shelled
21674 };
21675 set_growth_shell(Some(true));
21676 short.scores(&xo, &mut b);
21677 assert_eq!(bits(&a), bits(&b));
21678 set_growth_shell(None);
21679 }
21680
21681 #[test]
21685 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
21686 let zero = || DenseFfn {
21687 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21688 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21689 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21690 act: Act::Silu,
21691 down_t: None,
21692 segs: Vec::new(),
21693 };
21694 let moe = |sigmoid: bool| MoeFfn {
21695 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
21696 experts: vec![zero(), zero(), zero(), zero()],
21697 top_k: 1,
21698 norm_topk_prob: true,
21699 router_sigmoid: sigmoid,
21700 expert_bias: None,
21701 routed_scaling: 1.0,
21702 route_tau: None,
21703 shared: None,
21704 stats: std::cell::RefCell::new(Vec::new()),
21705 act_sq: std::cell::RefCell::new(Vec::new()),
21706 act_rows: std::cell::RefCell::new(Vec::new()),
21707 mask: None,
21708 per_expert_scale: None,
21709 router_input_norm: false,
21710 resonance: None,
21711 grown: Vec::new(),
21712 };
21713 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
21714 for sigmoid in [false, true] {
21715 let m = moe(sigmoid);
21716 let (idx, p, wsum) = moe_route(&logits, &m, None);
21717 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
21718 assert_eq!(p[1], 0.0);
21719 assert_eq!(p[3], 0.0);
21720 assert!(p[2] > p[0] && p[0] > 0.0);
21721 assert!(p.iter().all(|v| v.is_finite()));
21722 let w = p[2] / wsum;
21723 if sigmoid {
21724 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
21726 } else {
21727 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
21728 }
21729 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
21732 assert_eq!(idx, vec![2]);
21733 }
21734 let m = moe(false);
21736 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
21737 assert_eq!(idx, vec![3]);
21738 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
21741 assert_eq!(idx, vec![0]);
21742 assert!(p.iter().all(|&v| v == 0.25));
21743 assert!(wsum.is_finite() && wsum > 0.0);
21744 }
21745
21746 #[test]
21751 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
21752 let zero = || DenseFfn {
21753 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21754 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21755 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21756 act: Act::Silu,
21757 down_t: None,
21758 segs: Vec::new(),
21759 };
21760 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
21761 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
21762 experts: vec![zero(), zero(), zero()],
21763 top_k: 1,
21764 norm_topk_prob: norm_topk,
21765 router_sigmoid: false,
21766 expert_bias: None,
21767 routed_scaling: 1.0,
21768 route_tau: None,
21769 shared: None,
21770 stats: std::cell::RefCell::new(Vec::new()),
21771 act_sq: std::cell::RefCell::new(Vec::new()),
21772 act_rows: std::cell::RefCell::new(Vec::new()),
21773 mask: None,
21774 per_expert_scale: None,
21775 router_input_norm: false,
21776 resonance: resonant.then(|| Resonance {
21777 mu: vec![0.0; 6],
21778 u: Vec::new(),
21779 k: 0,
21780 bias: vec![0.0; 3],
21781 shell: Vec::new(),
21782 }),
21783 grown: Vec::new(),
21784 };
21785 let lo = -0.1f32;
21788 let hi = f32::from_bits(lo.to_bits() - 1);
21789 assert!(hi > lo && hi - lo < 2f32.powi(-25));
21790 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
21791 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
21793 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
21794 for norm in [true, false] {
21797 let m = moe(true, norm);
21798 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
21799 assert_eq!(idx, vec![1], "norm_topk {norm}");
21800 assert_eq!(p, vec![0.0, 1.0, 0.0]);
21801 assert_eq!(p[1] / wsum, 1.0);
21802 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
21805 assert_eq!(idx, vec![0]);
21806 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
21808 assert_eq!(idx, vec![2]);
21809 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
21810 assert_eq!(idx, vec![0]);
21811 assert_eq!(p[0] / wsum, 1.0);
21812 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
21815 assert_eq!(idx, vec![0]);
21816 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
21817 }
21818 }
21819}