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 vmf_cfg: Option<VmfPhaseCfg>,
116 pub gdn_cfg: Option<GdnCfg>,
118 pub logit_multiplier: Option<f32>,
120 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
125 graph_failed: std::sync::atomic::AtomicBool,
130 pub kv_history: Vec<u32>,
135 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
137 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
140 pub dsv4: Option<
144 Box<(
145 crate::dsv4::Dsv4Globals,
146 Vec<crate::dsv4::Dsv4Layer>,
147 crate::dsv4::Dsv4Cfg,
148 crate::dsv4::Dsv4State,
149 )>,
150 >,
151 pub dsv41: Option<
155 Box<(
156 crate::dsv41::Dsv41Globals,
157 Vec<crate::dsv41::Dsv41Layer>,
158 crate::dsv41::Dsv41Cfg,
159 crate::dsv41::Dsv41State,
160 )>,
161 >,
162 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
164 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
166 pub qwen4_exp: Option<
169 Box<(
170 crate::qwen4_exp::Globals,
171 Vec<crate::qwen4_exp::Layer>,
172 crate::qwen4_exp::Cfg,
173 crate::qwen4_exp::State,
174 )>,
175 >,
176 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
180 pub dspark: Option<crate::dsv4::DsparkState>,
182 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
185 pub dspark_hist: Vec<usize>,
187 pub dspark_real: Vec<u32>,
191 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
195 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
197 pub dspark_draft_ns: u128,
201 pub short_conv_cfg: Option<ShortConvCfg>,
204 pub mtp: Option<MtpModule>,
206 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
210 verify_exact_moe: bool,
213 pub speculative: bool,
215 pub ignore_eos: bool,
221 pub draft_full_streak: u32,
227 pub spec_k_adapt: Option<usize>,
235 pub spec_acc_ewma: f32,
237 rng: SplitMix64,
238 sampler_scratch: SamplerScratch,
239 spec_forced: Option<u32>,
245 spec_q: Vec<Vec<f32>>,
246 spec_p: Vec<f32>,
247 spec_res: Vec<f32>,
248 spec_qs: Vec<sampler::Sparse>,
250 spec_ps: sampler::Sparse,
251 spec_ress: sampler::Sparse,
252 mtp_graph_mode: Option<bool>,
259 #[cfg(target_os = "macos")]
262 metal_verify: Option<MetalVerifyPending>,
263 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
267 ws: ForwardScratch,
271 pool: Option<std::sync::Arc<Pool>>,
273 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
277 pub(crate) dyn_force_f32: bool,
279 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
284 pub(crate) dyn_active: Option<usize>,
290 pub(crate) dyn_blend_loaded: bool,
294 pub(crate) dyn_phi_layer: Option<usize>,
297 dyn_phi_ema: Vec<f32>,
299 dyn_phi_seen: usize,
300 pub dyn_router: Option<crate::swarm::DynRouter>,
303 o1_cfg: Option<crate::nystrom::O1Cfg>,
306 o1_epoch: u64,
309 o1_flags: Vec<bool>,
311 trace: bool,
314 calib_temp: f32,
317 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
319 graph_kv_id: u64,
320 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
323 graph_want_logits: bool,
324 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
328 graph_head_required: bool,
329 graph_logits: Option<Vec<f32>>,
332 pub embed_multiplier: f32,
334 pub attn_scale: f32,
337 pub swa: Option<(usize, usize)>,
340 pub sliding_layers: Option<Vec<bool>>,
343 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
346 pub rotary_dim_local: Option<usize>,
347 pub rope_scale: f32,
348 pub rope_scale_local: f32,
349 pub global_attn: Option<(usize, usize)>,
352 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
355 pub attn_v_norm: bool,
357 pub qk_norm_after_rope: bool,
359 pub final_softcap: Option<f32>,
361 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
365 pub attn_softcap: f32,
367 confidence_on: bool,
371 #[cfg(test)]
374 nll_test_fail_at: Option<usize>,
375 #[cfg(test)]
378 nll_test_force_serial: bool,
379}
380
381#[cfg(target_os = "macos")]
382impl Drop for Pipeline {
383 fn drop(&mut self) {
384 let _ = crate::gpu_metal::wait_replay();
386 crate::gpu::kv_mirror_drop(self.graph_kv_id);
387 }
388}
389
390pub struct PipelineWeights {
395 pub embed_tokens: QTensor,
397 pub layers: Vec<LayerWeights>,
399 pub lm_head: QTensor,
401 pub final_norm: Vec<f32>,
403}
404
405pub struct LayerWeights {
407 pub input_norm: Vec<f32>,
408 pub post_norm: Vec<f32>,
411 pub attn_out_norm: Option<Vec<f32>>,
414 pub layer_scale: Option<f32>,
416 pub ffn_out_norm: Option<Vec<f32>>,
419 pub ffn: FfnKind,
420 pub attn: AttnKind,
421}
422
423#[derive(Clone, Copy, PartialEq, Debug, Default)]
426pub enum Act {
427 #[default]
428 Silu,
429 GeluTanh,
430 Situ {
433 beta: f32,
434 linear_beta: f32,
435 },
436}
437
438impl Act {
439 pub fn from_arch(name: &str) -> Self {
440 if name == "gelu_tanh" {
441 Self::GeluTanh
442 } else {
443 Self::Silu
444 }
445 }
446
447 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
449 match arch.hidden_act.as_str() {
450 "situ" => Self::Situ {
451 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
452 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
453 },
454 other => Self::from_arch(other),
455 }
456 }
457
458 #[inline]
459 pub fn apply(self, x: f32) -> f32 {
460 match self {
461 Self::Silu => inference::silu(x),
462 Self::GeluTanh => inference::gelu_tanh(x),
463 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
464 }
465 }
466
467 #[inline]
470 pub fn combine(self, g: f32, u: f32) -> f32 {
471 match self {
472 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
473 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
474 }
475 _ => self.apply(g) * u,
476 }
477 }
478}
479
480pub struct DenseFfn {
482 pub gate_proj: QTensor,
483 pub up_proj: QTensor,
484 pub down_proj: QTensor,
485 pub act: Act,
487 pub down_t: Option<QTensor>,
493 pub segs: Vec<FfnSeg>,
500}
501
502pub struct FfnSeg {
507 pub gate: QTensor,
508 pub up: QTensor,
509 pub down: QTensor,
510 pub start: usize,
511 pub width: usize,
512}
513
514pub enum FfnKind {
517 Dense(DenseFfn),
518 Moe(MoeFfn),
522 DenseMoe(Box<DenseMoeFfn>),
529}
530
531pub struct DenseMoeFfn {
533 pub dense: DenseFfn,
534 pub moe: MoeFfn,
535 pub post_norm_1: Vec<f32>,
537 pub pre_norm_2: Vec<f32>,
540 pub post_norm_2: Vec<f32>,
542}
543
544pub struct MoeFfn {
545 pub router: QTensor,
547 pub experts: Vec<DenseFfn>,
548 pub top_k: usize,
549 pub norm_topk_prob: bool,
550 pub router_sigmoid: bool,
553 pub expert_bias: Option<Vec<f32>>,
557 pub routed_scaling: f32,
560 pub route_tau: Option<f32>,
566 pub shared: Option<(DenseFfn, Option<QTensor>)>,
569 pub stats: std::cell::RefCell<Vec<u64>>,
573 pub act_sq: std::cell::RefCell<Vec<f64>>,
580 pub act_rows: std::cell::RefCell<Vec<f32>>,
586 pub mask: Option<Vec<bool>>,
591 pub per_expert_scale: Option<Vec<f32>>,
594 pub router_input_norm: bool,
598 pub resonance: Option<Resonance>,
602}
603
604pub struct Resonance {
606 pub mu: Vec<f32>,
608 pub u: Vec<f32>,
610 pub k: usize,
611 pub bias: Vec<f32>,
613}
614
615impl Resonance {
616 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
618 let h = x.len();
619 let ne = out.len();
620 for e in 0..ne {
621 let mu = &self.mu[e * h..(e + 1) * h];
622 let mut d2 = 0.0f32;
623 for j in 0..h {
624 let d = x[j] - mu[j];
625 d2 += d * d;
626 }
627 let mut proj = 0.0f32;
628 for i in 0..self.k {
629 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
630 let mut p = 0.0f32;
631 for j in 0..h {
632 p += (x[j] - mu[j]) * u[j];
633 }
634 proj += p * p;
635 }
636 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
637 }
638 }
639}
640
641pub enum AttnKind {
644 Full {
646 wq: QTensor,
647 wk: QTensor,
648 wv: QTensor,
649 wo: QTensor,
650 q_norm: Option<Vec<f32>>,
651 k_norm: Option<Vec<f32>>,
652 output_gate: bool,
653 softplus_gate: Option<(QTensor, bool)>,
657 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
659 },
660 Linear(VmfPhaseWeights),
662 LinearGdn(GdnWeights),
664 ShortConv(ShortConvWeights),
667 Mla(Box<MlaWeights>),
675 Kda(Box<crate::linear_core::KdaWeights>),
679}
680
681pub struct MlaWeights {
683 pub q_proj: QTensor,
687 pub q_a: Option<QTensor>,
690 pub q_a_norm: Option<Vec<f32>>,
691 pub kv_a: QTensor,
693 pub kv_a_norm: Vec<f32>,
695 pub kv_b: QTensor,
697 pub o_proj: QTensor,
699 pub nh: usize,
700 pub qk_rope: usize,
701 pub qk_nope: usize,
702 pub v_dim: usize,
703 pub lora: usize,
704 pub scale: f32,
706 pub nope: bool,
708}
709
710pub struct MtpModule {
715 pub enorm: Vec<f32>,
716 pub hnorm: Vec<f32>,
717 pub eh_proj: QTensor,
719 pub layer: LayerWeights,
720 pub final_norm: Vec<f32>,
721 pub kv: crate::kv_cache::LayerKvCache,
722}
723
724#[cfg(target_os = "macos")]
731enum MetalRowsItem<'a> {
732 Gdn {
733 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
734 first: usize,
735 },
736 Attn {
737 l: crate::gpu_metal::AttnGpuLayer<'a>,
738 li: usize,
739 q_norm: Option<&'a [f32]>,
740 k_norm: Option<&'a [f32]>,
741 output_gate: bool,
742 },
743}
744
745#[cfg(target_os = "macos")]
746struct MetalVerifyPending {
747 graph: crate::gpu_metal::VerifyGraph,
748 gdn_layers: Vec<usize>,
749 attn_layers: Vec<(usize, usize)>,
750}
751
752#[cfg(target_os = "macos")]
756struct MetalWarmPending {
757 graph: crate::gpu_metal::VerifyGraph,
758 cpu_stored: usize,
759 b: usize,
760}
761
762#[cfg(target_os = "macos")]
763enum MetalRowsRun {
764 Declined,
766 Failed,
769 Completed(MetalVerifyPending),
770}
771
772#[cfg(target_os = "macos")]
773enum MetalPrefillOutcome {
774 Declined,
775 Failed,
776 Completed(Vec<f32>),
777}
778
779#[cfg(target_os = "macos")]
780enum MetalBatchNllOutcome {
781 Declined,
782 Failed(String),
783 Completed(f64, usize),
784}
785
786#[derive(Clone, Copy)]
790enum SpecTrial {
791 Spec {
792 t0: std::time::Instant,
793 gen0: usize,
794 rounds: usize,
795 },
796 Plain {
797 t0: std::time::Instant,
798 gen0: usize,
799 },
800 Decided {
801 spec: bool,
802 recheck_at: usize,
803 },
804}
805
806pub(crate) fn spec_time_level() -> u8 {
810 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
811 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
812 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
813 Err(_) => 0,
814 })
815}
816
817struct SpecStampLog {
823 t_last: std::time::Instant,
824 items: Vec<(&'static str, f32)>,
825}
826
827static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
828
829pub(crate) fn spec_stamp(name: &'static str) {
830 if spec_time_level() == 0 {
831 return;
832 }
833 if let Ok(mut g) = SPEC_STAMPS.lock() {
834 if let Some(log) = g.as_mut() {
835 let now = std::time::Instant::now();
836 log.items
837 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
838 log.t_last = now;
839 }
840 }
841}
842
843fn spec_stamps_begin() {
844 if spec_time_level() == 0 {
845 return;
846 }
847 if let Ok(mut g) = SPEC_STAMPS.lock() {
848 *g = Some(SpecStampLog {
849 t_last: std::time::Instant::now(),
850 items: Vec::with_capacity(64),
851 });
852 }
853}
854
855fn spec_stamps_take() -> Vec<(&'static str, f32)> {
856 SPEC_STAMPS
857 .lock()
858 .ok()
859 .and_then(|mut g| g.take())
860 .map(|l| l.items)
861 .unwrap_or_default()
862}
863
864fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
867 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
868 for &(n, ms) in items {
869 match agg.iter_mut().find(|e| e.0 == n) {
870 Some(e) => {
871 e.1 += ms;
872 e.2 += 1;
873 }
874 None => agg.push((n, ms, 1)),
875 }
876 }
877 let mut s = String::with_capacity(agg.len() * 16);
878 for (n, ms, k) in agg {
879 if k > 1 {
880 s.push_str(&format!("{n} {ms:.1}/{k} "));
881 } else {
882 s.push_str(&format!("{n} {ms:.1} "));
883 }
884 }
885 s
886}
887
888#[derive(Default, Clone, Copy)]
910struct SpecMon {
911 round_ms: f64,
912 tokens: f64,
913 plain_ms: f64,
914 n: u32,
915 fails: u32,
916 metal: bool,
917}
918
919const SPEC_PROXY_TOKENS: f64 = 3.5;
922const SPEC_PLAIN_MIN_MS: f64 = 200.0;
925
926impl SpecMon {
927 fn round(&mut self, dt_ms: f64, produced: usize) {
928 self.n += 1;
929 if self.n == 1 {
930 return; }
932 let a = if self.n == 2 { 1.0 } else { 0.3 };
933 self.round_ms += a * (dt_ms - self.round_ms);
934 self.tokens += a * (produced as f64 - self.tokens);
935 }
936 fn pays(&self) -> bool {
937 if self.plain_ms > 0.0 {
938 self.tokens * self.plain_ms > self.round_ms * 1.03
939 } else {
940 self.metal && self.tokens >= SPEC_PROXY_TOKENS
941 }
942 }
943 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
945 let n = generated.saturating_sub(gen0);
946 if n >= 8 {
947 return true;
948 }
949 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
950 }
951}
952
953pub struct GenerateResult {
955 pub text: String,
956 pub token_ids: Vec<u32>,
957 pub prompt_tokens: usize,
958 pub tokens_generated: usize,
959 pub finish_reason: String,
960 pub mtp_drafted: usize,
962 pub mtp_accepted: usize,
963 pub token_confidence: Vec<f32>,
968 pub traces: Vec<TokenTrace>,
971}
972
973#[derive(Clone, Debug)]
978pub struct TokenTrace {
979 pub t: usize,
981 pub token_id: u32,
983 pub confidence: f32,
985 pub active_skill: Option<String>,
987 pub recon: Option<f32>,
991 pub switched: bool,
994}
995
996#[cfg_attr(not(test), allow(dead_code))]
1001fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1002 let t = if temp > 1e-3 { temp } else { 1.0 };
1003 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1004 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1005 if sum > 0.0 {
1006 (((logits[id as usize] - max) / t).exp()) / sum
1007 } else {
1008 0.0
1009 }
1010}
1011
1012fn prefill_batched() -> bool {
1015 std::env::var("CMF_PREFILL")
1016 .map(|v| v != "seq")
1017 .unwrap_or(true)
1018}
1019
1020#[inline]
1024fn nll_graph_policy(
1025 unmasked: bool,
1026 prefer_graph: bool,
1027 native_metal: bool,
1028) -> (bool, bool) {
1029 let graph_quality = unmasked && prefer_graph;
1030 let fused_head_quality = graph_quality && native_metal;
1031 (graph_quality, fused_head_quality)
1032}
1033
1034#[derive(Clone, Copy)]
1038enum PrefillIn<'a> {
1039 Ids(&'a [u32]),
1040 Hidden(&'a [f32]),
1041}
1042
1043impl Pipeline {
1050 fn can_prefill_batched(&self) -> bool {
1051 #[cfg(test)]
1052 let force_serial = self.nll_test_force_serial;
1053 #[cfg(not(test))]
1054 let force_serial = false;
1055 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1056 }
1057
1058 fn automatic_gpu_prefix(&self) -> Option<usize> {
1061 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1062 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1063 }
1064
1065 pub fn prefill_chunk(&self) -> usize {
1069 let env = env_prefill_chunk();
1070 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1071 return prefill_chunk_rule(env, ChunkHost::here(), false);
1072 }
1073 prefill_chunk_rule(None, ChunkHost::Other, self.chunk_stack_facts().dense_on_discrete())
1074 }
1075
1076 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1077 let plain_dense = !self.weights.layers.is_empty()
1078 && self.g3n.is_none()
1079 && self.dsv4.is_none()
1080 && self.dsv41.is_none()
1081 && self.qwen4_exp.is_none()
1082 && self.weights.layers.iter().all(|lw| {
1083 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1084 });
1085 let gpu_on = crate::gpu::enabled();
1086 ChunkStackFacts {
1087 plain_dense,
1088 discrete: gpu_on && crate::gpu::discrete(),
1089 gpu_on,
1090 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1093 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1094 multi_gpu: self.gpu_plan.is_some(),
1095 o1: self.o1_active(),
1096 }
1097 }
1098}
1099
1100pub fn prefill_chunk() -> usize {
1109 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1110}
1111
1112fn env_prefill_chunk() -> Option<usize> {
1113 std::env::var("CMF_PREFILL_CHUNK")
1114 .ok()
1115 .and_then(|v| v.parse::<usize>().ok())
1116}
1117
1118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1120enum ChunkHost {
1121 Macos,
1122 Aarch64,
1124 Other,
1126}
1127
1128impl ChunkHost {
1129 fn here() -> Self {
1130 if cfg!(target_os = "macos") {
1131 ChunkHost::Macos
1132 } else if cfg!(target_arch = "aarch64") {
1133 ChunkHost::Aarch64
1134 } else {
1135 ChunkHost::Other
1136 }
1137 }
1138}
1139
1140const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1147
1148fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1154 if let Some(n) = env {
1155 return n.max(1);
1156 }
1157 match host {
1158 ChunkHost::Macos => 512,
1159 ChunkHost::Aarch64 => 256,
1162 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1163 ChunkHost::Other => 48,
1164 }
1165}
1166
1167#[derive(Clone, Copy, Debug, Default)]
1169struct ChunkStackFacts {
1170 plain_dense: bool,
1173 discrete: bool,
1175 gpu_on: bool,
1177 capacity_split: bool,
1179 multi_gpu: bool,
1181 o1: bool,
1183}
1184
1185impl ChunkStackFacts {
1186 fn dense_on_discrete(self) -> bool {
1187 self.plain_dense
1188 && self.discrete
1189 && self.gpu_on
1190 && !self.capacity_split
1191 && !self.multi_gpu
1192 && !self.o1
1193 }
1194}
1195
1196#[inline]
1202fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1203 if end <= start || start >= input_len {
1204 return 0;
1205 }
1206 let rows = (end.min(input_len) - start).min(input_len - start);
1207 if end < input_len {
1208 rows
1209 } else {
1210 rows.saturating_sub(1)
1211 }
1212}
1213
1214pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1216
1217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1219pub(crate) struct ReuseLayer {
1220 pub full: bool,
1223 pub host_rows: usize,
1225 pub device_rows: Option<usize>,
1227 pub device_state: bool,
1229}
1230
1231#[derive(Debug, Clone, PartialEq, Eq)]
1233pub(crate) enum ReusePlan {
1234 Ready,
1236 Pull(Vec<(usize, usize, usize)>),
1239 Fresh,
1241}
1242
1243pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1253 let mut pulls = Vec::new();
1254 for (li, l) in layers.iter().enumerate() {
1255 if !l.full {
1256 if l.device_state {
1257 return ReusePlan::Fresh;
1258 }
1259 continue;
1260 }
1261 if l.host_rows == reuse_from {
1262 continue;
1263 }
1264 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1265 pulls.push((li, l.host_rows, reuse_from));
1266 continue;
1267 }
1268 return ReusePlan::Fresh;
1269 }
1270 if pulls.is_empty() {
1271 ReusePlan::Ready
1272 } else {
1273 ReusePlan::Pull(pulls)
1274 }
1275}
1276
1277impl Pipeline {
1278 fn clear_sequence_state(&mut self) {
1286 #[cfg(target_os = "macos")]
1289 let _ = crate::gpu_metal::wait_replay();
1290 self.kv_cache.clear();
1291 self.kv_history.clear();
1292 if let Some(b) = &mut self.dsv41 {
1293 b.3.clear();
1294 }
1295 crate::gpu::graph_kv_reset(self.graph_kv_id);
1296 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1301 }
1302
1303 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1310 if self.graph_prefill_preferred() {
1311 return true;
1312 }
1313 let kv_id = self.graph_kv_id;
1314 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1315 .map(|li| {
1316 let full = matches!(
1317 self.weights.layers[self.phys_layer(li)].attn,
1318 AttnKind::Full { .. }
1319 );
1320 ReuseLayer {
1321 full,
1322 host_rows: self.kv_cache.layers[li].seq_len,
1323 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1324 device_state: crate::gpu::graph_state_resident(kv_id, li),
1325 }
1326 })
1327 .collect();
1328 if layers
1332 .iter()
1333 .all(|l| l.device_rows.is_none() && !l.device_state)
1334 {
1335 return true;
1336 }
1337 let plan = kv_reuse_plan(reuse_from, &layers);
1338 let (what, rows, n) = match &plan {
1339 ReusePlan::Ready => ("host ready", 0, 0),
1340 ReusePlan::Fresh => ("fresh", 0, 0),
1341 ReusePlan::Pull(p) => (
1342 "pull",
1343 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1344 p.len(),
1345 ),
1346 };
1347 let t0 = std::time::Instant::now();
1348 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1349 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1350 eprintln!(
1351 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1352 if ok { "" } else { " (failed → fresh)" },
1353 t0.elapsed().as_secs_f64() * 1e3
1354 );
1355 }
1356 ok
1357 }
1358
1359 fn apply_kv_reuse_plan(
1360 &mut self,
1361 reuse_from: usize,
1362 plan: ReusePlan,
1363 layers: &[ReuseLayer],
1364 ) -> bool {
1365 let kv_id = self.graph_kv_id;
1366 match plan {
1367 ReusePlan::Fresh => return false,
1368 ReusePlan::Ready => {}
1369 ReusePlan::Pull(pulls) => {
1370 let (nkv, hd) = {
1375 let c = &self.kv_cache.layers[pulls[0].0];
1376 (c.num_kv_heads, c.head_dim)
1377 };
1378 let uniform = pulls.iter().all(|&(li, _, _)| {
1379 let c = &self.kv_cache.layers[li];
1380 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1381 });
1382 let batched = if uniform {
1383 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1384 } else {
1385 None
1386 };
1387 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1388 Some(rows) => rows,
1389 None => {
1390 let mut rows = Vec::with_capacity(pulls.len());
1391 for &(li, from, to) in &pulls {
1392 let (lnkv, lhd) = {
1393 let c = &self.kv_cache.layers[li];
1394 (c.num_kv_heads, c.head_dim)
1395 };
1396 let Some((k, v, first_valid)) =
1397 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1398 else {
1399 return false;
1400 };
1401 let need_from = match self.layer_window(li) {
1405 Some(w) => from.max((to + 1).saturating_sub(w)),
1406 None => from,
1407 };
1408 if first_valid > need_from {
1409 return false;
1410 }
1411 rows.push((k, v));
1412 }
1413 rows
1414 }
1415 };
1416 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1417 let cache = &mut self.kv_cache.layers[li];
1418 let row = cache.num_kv_heads * cache.head_dim;
1419 for p in 0..to - from {
1420 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1421 }
1422 if cache.seq_len != to {
1423 return false;
1424 }
1425 }
1426 }
1427 }
1428 for (li, l) in layers.iter().enumerate() {
1432 if l.full
1433 && l.device_rows.is_some_and(|d| d > reuse_from)
1434 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1435 {
1436 return false;
1437 }
1438 }
1439 true
1440 }
1441
1442 fn finish_generation(
1448 &mut self,
1449 mtp: &mut Option<MtpModule>,
1450 router: &mut Option<crate::swarm::DynRouter>,
1451 clear_sequence: bool,
1452 ) {
1453 if router.is_some() {
1457 let _ = self.set_active_skill(None);
1458 }
1459 #[cfg(target_os = "macos")]
1466 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1467 if clear_sequence {
1468 self.clear_sequence_state();
1469 if let Some(m) = mtp.as_mut() {
1470 m.kv.clear();
1476 }
1477 if let Some(m) = self.mtp.as_mut() {
1478 m.kv.clear();
1482 }
1483 }
1484 self.graph_want_logits = false;
1485 self.graph_head_required = false;
1486 self.graph_logits = None;
1487 self.graph_failed
1488 .store(false, std::sync::atomic::Ordering::Relaxed);
1489 self.cancel
1490 .store(false, std::sync::atomic::Ordering::Relaxed);
1491 self.dyn_router = router.take().or(self.dyn_router.take());
1492 self.mtp = mtp.take().or(self.mtp.take());
1493 self.mtp_graph_mode = None;
1494 self.spec_forced = None;
1495 }
1496
1497 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1501 if self
1502 .graph_failed
1503 .swap(false, std::sync::atomic::Ordering::Relaxed)
1504 {
1505 self.cancel
1506 .store(false, std::sync::atomic::Ordering::Relaxed);
1507 self.clear_sequence_state();
1508 self.graph_logits = None;
1509 self.graph_want_logits = false;
1510 self.graph_head_required = false;
1511 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1512 }
1513 Ok(())
1514 }
1515
1516 #[cfg(target_os = "macos")]
1517 fn fail_metal_graph(&mut self, reason: &str) {
1518 crate::pipeline::METAL_GRAPH_ERRORS
1519 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1520 self.clear_sequence_state();
1521 self.graph_logits = None;
1522 self.graph_failed
1523 .store(true, std::sync::atomic::Ordering::Relaxed);
1524 self.cancel
1525 .store(true, std::sync::atomic::Ordering::Relaxed);
1526 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1527 }
1528
1529 fn nll_begin(&mut self) -> Result<(), String> {
1534 if self
1535 .graph_failed
1536 .swap(false, std::sync::atomic::Ordering::Relaxed)
1537 {
1538 self.cancel
1539 .store(false, std::sync::atomic::Ordering::Relaxed);
1540 self.clear_sequence_state();
1541 self.graph_logits = None;
1542 self.graph_want_logits = false;
1543 self.graph_head_required = false;
1544 return Err("GPU graph failed before NLL scoring".to_string());
1545 }
1546 self.clear_sequence_state();
1547 self.graph_logits = None;
1548 self.graph_want_logits = false;
1549 self.graph_head_required = false;
1550 Ok(())
1551 }
1552
1553 fn nll_end(&mut self) {
1557 self.clear_sequence_state();
1558 self.graph_logits = None;
1559 self.graph_want_logits = false;
1560 self.graph_head_required = false;
1561 self.graph_failed
1562 .store(false, std::sync::atomic::Ordering::Relaxed);
1563 }
1564
1565 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1568 #[cfg(test)]
1569 if self.nll_test_fail_at == Some(pos) {
1570 self.nll_test_fail_at = None;
1571 self.graph_failed
1572 .store(true, std::sync::atomic::Ordering::Relaxed);
1573 self.cancel
1574 .store(true, std::sync::atomic::Ordering::Relaxed);
1575 }
1576 if self
1577 .graph_failed
1578 .swap(false, std::sync::atomic::Ordering::Relaxed)
1579 {
1580 self.cancel
1581 .store(false, std::sync::atomic::Ordering::Relaxed);
1582 self.clear_sequence_state();
1583 self.graph_logits = None;
1584 self.graph_want_logits = false;
1585 return Err(format!(
1586 "GPU graph failed during NLL {phase} at position {pos}"
1587 ));
1588 }
1589 Ok(())
1590 }
1591
1592 #[inline]
1596 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1597 virtual_idx % self.physical_layers
1598 }
1599
1600 #[inline]
1603 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1604 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1605 }
1606
1607 #[allow(clippy::too_many_arguments)]
1609
1610 #[cfg(target_os = "macos")]
1629 fn graph_prefill_preferred(&self) -> bool {
1630 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1631 if !crate::gpu::enabled_here()
1632 || !graph_force
1633 || std::env::var("CMF_GPU_BLOCK")
1634 .map(|v| v == "0")
1635 .unwrap_or(false)
1636 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1639 {
1640 return false;
1641 }
1642 self.weights
1643 .layers
1644 .iter()
1645 .any(|lw| {
1646 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1647 })
1648 }
1649
1650 #[cfg(not(target_os = "macos"))]
1657 fn batch_prefix_prefill(&self) -> bool {
1658 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1659 Ok("0") => return false,
1660 Ok("1") => true,
1661 _ => false,
1662 };
1663 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1664 && crate::gpu::enabled_here()
1665 && !crate::gpu::graph_unsupported()
1666 && (forced || self.graph_attn_decline_reason().is_some())
1667 && self.wgpu_graph_attn_decline().is_none()
1668 && self.attn_softcap == 0.0
1669 && self
1670 .weights
1671 .layers
1672 .iter()
1673 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1674 && self.automatic_gpu_prefix().is_some()
1675 }
1676
1677 #[cfg(not(target_os = "macos"))]
1678 fn graph_prefill_preferred(&self) -> bool {
1679 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1687 if !graph_on || !crate::gpu::enabled_here() {
1688 return false;
1689 }
1690 if self.o1_active() {
1704 return false;
1705 }
1706 if self.wgpu_graph_attn_decline().is_some() {
1710 return false;
1711 }
1712 if self
1713 .weights
1714 .layers
1715 .iter()
1716 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1717 {
1718 return true;
1719 }
1720 self.weights
1731 .layers
1732 .iter()
1733 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1734 && self.automatic_gpu_prefix().is_none()
1735 }
1736
1737 #[cfg(target_os = "macos")]
1738 fn q1_graph_gpu(
1739 &mut self,
1740 start: usize,
1741 upto: Option<usize>,
1742 position: usize,
1743 h: &mut [f32],
1744 ) -> usize {
1745 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1747 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1748 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1750 || !graph_force
1751 || std::env::var("CMF_GPU_BLOCK")
1752 .map(|v| v == "0")
1753 .unwrap_or(false)
1754 {
1755 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1756 eprintln!(
1757 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1758 self.attn_softcap > 0.0,
1759 crate::gpu::enabled_here(),
1760 graph_force,
1761 );
1762 }
1763 if self.graph_head_required {
1764 self.fail_metal_graph("native graph front gate refused");
1765 }
1766 return start;
1767 }
1768 if self.swa.is_some()
1772 || self.global_attn.is_some()
1773 || self.attention_heads_per_layer.is_some()
1774 || self.attn_v_norm
1775 || self.graph_attn_decline_reason().is_some()
1777 || self.weights.layers.iter().any(|lw| {
1778 lw.attn_out_norm.is_some()
1779 || lw.ffn_out_norm.is_some()
1780 || lw.layer_scale.is_some()
1781 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1782 })
1783 {
1784 if let Some(reason) = self.graph_attn_decline_reason() {
1787 self.note_graph_decline("metal block graph", reason);
1788 }
1789 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1790 eprintln!(
1791 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1792 self.swa.is_some(),
1793 self.global_attn.is_some(),
1794 self.attention_heads_per_layer.is_some(),
1795 self.attn_v_norm,
1796 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1797 );
1798 }
1799 if self.graph_head_required {
1800 self.fail_metal_graph("native graph architecture gate refused");
1801 }
1802 return start;
1803 }
1804 let limit = upto
1807 .map(|u| u + 1)
1808 .unwrap_or(self.num_layers)
1809 .min(self.num_layers);
1810
1811 enum Item<'a> {
1812 Gdn {
1813 run: Vec<GdnGpuLayer<'a>>,
1814 first: usize,
1815 },
1816 Attn {
1817 l: AttnGpuLayer<'a>,
1818 li: usize,
1819 q_norm: Option<&'a [f32]>,
1820 k_norm: Option<&'a [f32]>,
1821 output_gate: bool,
1822 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1823 full_gpu: bool,
1826 },
1827 }
1828
1829 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1836 let attend_contract = attend_mode != "0"
1837 && attend_mode != "off"
1838 && self.head_dim % 4 == 0
1839 && self.head_dim <= 256
1840 && self.rotary_dim >= 2
1841 && self.rotary_dim <= self.head_dim
1842 && (self.rotary_dim / 2) % 32 == 0
1843 && self.num_kv_heads > 0
1844 && self.num_heads % self.num_kv_heads == 0;
1845
1846 let mut plan: Vec<Item> = Vec::new();
1847 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1848 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1850 let mut scan = start;
1851 while scan < limit {
1852 let lw = &self.weights.layers[self.phys_layer(scan)];
1853 let ffn = match &lw.ffn {
1854 FfnKind::Dense(d) if d.segs.is_empty() => {
1855 let (Some(g), Some(u), Some(dn)) = (
1856 d.gate_proj.metal_graph_parts(),
1857 d.up_proj.metal_graph_parts(),
1858 d.down_proj.metal_graph_parts(),
1859 ) else {
1860 if block_diag {
1861 eprintln!(
1862 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1863 );
1864 }
1865 break;
1866 };
1867 MetalFfn::Dense {
1868 gate: g,
1869 up: u,
1870 down: dn,
1871 }
1872 }
1873 FfnKind::Moe(m) => {
1874 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1875 if block_diag {
1876 eprintln!(
1877 "block-graph: L{scan} MoE outside the graph contract — run ends"
1878 );
1879 }
1880 break;
1881 };
1882 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1883 model_ref.get_or_insert_with(|| model.clone());
1884 }
1885 MetalFfn::Moe(moe)
1886 }
1887 _ => {
1888 if block_diag {
1889 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1890 }
1891 break;
1892 }
1893 };
1894 match &lw.attn {
1895 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1896 let parts = (
1897 w.in_proj_qkv.metal_graph_parts(),
1898 w.in_proj_z.metal_graph_parts(),
1899 w.in_proj_a.f32_parts(),
1900 w.in_proj_b.f32_parts(),
1901 w.out_proj.metal_graph_parts(),
1902 );
1903 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1904 if block_diag {
1905 eprintln!(
1906 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1907 w.in_proj_qkv.metal_graph_parts().is_some(),
1908 w.in_proj_z.metal_graph_parts().is_some(),
1909 w.in_proj_a.f32_parts().is_some(),
1910 w.in_proj_b.f32_parts().is_some(),
1911 w.out_proj.metal_graph_parts().is_some(),
1912 );
1913 }
1914 break;
1915 };
1916 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1917 model_ref.get_or_insert_with(|| model.clone());
1918 }
1919 let gl = GdnGpuLayer {
1920 attn_norm: &lw.input_norm,
1921 post_norm: &lw.post_norm,
1922 qkv,
1923 z,
1924 a,
1925 b,
1926 out,
1927 ffn,
1928 conv1d: &w.conv1d,
1929 a_log: &w.a_log,
1930 dt_bias: &w.dt_bias,
1931 gnorm: &w.norm,
1932 };
1933 match plan.last_mut() {
1934 Some(Item::Gdn { run, .. }) => run.push(gl),
1935 _ => plan.push(Item::Gdn {
1936 run: vec![gl],
1937 first: scan,
1938 }),
1939 }
1940 }
1941 AttnKind::Full {
1942 wq,
1943 wk,
1944 wv,
1945 wo,
1946 q_norm,
1947 k_norm,
1948 output_gate,
1949 softplus_gate: None,
1950 bias,
1951 } if !self.kv_cache.layers[scan].o1_sealed()
1952 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1957 {
1958 let parts = (
1959 wq.metal_graph_parts(),
1960 wk.metal_graph_parts(),
1961 wv.metal_graph_parts(),
1962 wo.metal_graph_parts(),
1963 );
1964 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1965 break;
1966 };
1967 if let QTensor::Mapped { model, .. } = wq {
1968 model_ref.get_or_insert_with(|| model.clone());
1969 }
1970 let cache = &self.kv_cache.layers[scan];
1971 let o1_metal = cache.o1.is_some()
1975 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1976 && cache.o1_views().is_some();
1977 let full_gpu = attend_contract
1978 && cache.mode == crate::kv_cache::KvMode::F32
1979 && (cache.o1.is_none() || o1_metal)
1980 && bias.is_none()
1981 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1982 && pk.1 == self.num_kv_heads * self.head_dim
1983 && pv.1 == self.num_kv_heads * self.head_dim
1984 && po.2 == self.num_heads * self.head_dim;
1985 plan.push(Item::Attn {
1986 l: AttnGpuLayer {
1987 attn_norm: &lw.input_norm,
1988 post_norm: &lw.post_norm,
1989 wq: pq,
1990 wk: pk,
1991 wv: pv,
1992 wo: po,
1993 ffn,
1994 },
1995 li: scan,
1996 q_norm: q_norm.as_deref(),
1997 k_norm: k_norm.as_deref(),
1998 output_gate: *output_gate,
1999 bias: bias
2000 .as_ref()
2001 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2002 full_gpu,
2003 });
2004 }
2005 _ => break,
2006 }
2007 scan += 1;
2008 }
2009 let Some(model) = model_ref else {
2010 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2011 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2012 }
2013 if self.graph_head_required {
2014 self.fail_metal_graph("native graph has no mapped model reference");
2015 }
2016 return start;
2017 };
2018 if plan.is_empty() {
2019 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2020 eprintln!("q1-graph: empty plan at layer {start}");
2021 }
2022 if self.graph_head_required {
2023 self.fail_metal_graph("native graph plan is empty");
2024 }
2025 return start;
2026 }
2027 let has_moe = plan.iter().any(|it| match it {
2028 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
2029 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
2030 });
2031 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2032 let dev_attend = attend_contract
2033 && (self.head_dim <= 128
2034 || has_moe
2035 || (self.head_dim <= 256 && has_gdn)
2041 || attend_mode == "force"
2042 || attend_mode == "256");
2043 if !dev_attend {
2044 for it in &mut plan {
2045 if let Item::Attn { li, full_gpu, .. } = it {
2046 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2049 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2050 if !keep_o1 {
2051 *full_gpu = false;
2052 }
2053 }
2054 }
2055 }
2056 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2057 use std::sync::atomic::{AtomicBool, Ordering};
2058 static SAID: AtomicBool = AtomicBool::new(false);
2059 if !SAID.swap(true, Ordering::Relaxed) {
2060 let fg = plan
2061 .iter()
2062 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2063 .count();
2064 let att = plan
2065 .iter()
2066 .filter(|it| matches!(it, Item::Attn { .. }))
2067 .count();
2068 eprintln!(
2069 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2070 plan.len(),
2071 self.head_dim,
2072 self.rotary_dim,
2073 self.num_kv_heads,
2074 self.num_heads,
2075 );
2076 }
2077 }
2078 let dims = GraphDims {
2079 hidden: self.hidden_size,
2080 eps: self.rms_eps as f32,
2081 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2082 };
2083 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
2084 if self.graph_head_required {
2085 self.fail_metal_graph("native TokenGraph allocation refused");
2086 }
2087 return start;
2088 };
2089 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2090 nv: cfg.num_v_heads,
2091 nk: cfg.num_k_heads,
2092 dk: cfg.key_head_dim,
2093 dv: cfg.value_head_dim,
2094 kk: cfg.conv_kernel,
2095 hidden: self.hidden_size,
2096 inter: self.intermediate_size,
2097 c_dim: cfg.conv_dim(),
2098 eps: cfg.rms_eps as f32,
2099 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2100 });
2101 let mut valid = 0usize;
2105 let mut end = start;
2106 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2108 static ONCE: std::sync::Once = std::sync::Once::new();
2109 ONCE.call_once(|| {
2110 for it in &plan {
2111 match it {
2112 Item::Gdn { first, run } => {
2113 eprintln!("plan: Gdn first={first} len={}", run.len())
2114 }
2115 Item::Attn { li, full_gpu, .. } => {
2116 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2117 }
2118 }
2119 }
2120 });
2121 }
2122 for item in &plan {
2123 let ok = match item {
2124 Item::Gdn { run, .. } => gcfg
2125 .as_ref()
2126 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2127 .unwrap_or(false),
2128 Item::Attn { l, .. } => graph.attn_ok(l),
2129 };
2130 if !ok {
2131 if block_diag {
2132 eprintln!(
2133 "block-graph: plan item {} ({}) failed graph preflight",
2134 valid,
2135 match item {
2136 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2137 Item::Attn { li, .. } => format!("Attn L{li}"),
2138 }
2139 );
2140 }
2141 break;
2142 }
2143 valid += 1;
2144 end += match item {
2145 Item::Gdn { run, .. } => run.len(),
2146 Item::Attn { .. } => 1,
2147 };
2148 }
2149 plan.truncate(valid);
2150 if plan.is_empty() {
2151 if self.graph_head_required {
2152 self.fail_metal_graph("native graph preflight produced no valid items");
2153 }
2154 return start;
2155 }
2156
2157 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2158 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2159 return start;
2160 }
2161
2162 let one_pass = |t: (usize, usize, usize)| {
2171 use cortiq_core::TensorDtype as D;
2172 matches!(
2173 model.tensors[t.0].dtype,
2174 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1
2175 )
2176 };
2177 let dense_fast = plan.iter().all(|it| match it {
2178 Item::Attn {
2179 l, li, full_gpu, ..
2180 } => {
2181 *full_gpu
2182 && self.kv_cache.layers[*li].o1.is_none()
2183 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2184 && match l.ffn {
2185 MetalFfn::Dense { gate, up, down } => {
2186 one_pass(gate) && one_pass(up) && one_pass(down)
2187 }
2188 _ => false,
2189 }
2190 }
2191 Item::Gdn { .. } => false,
2192 });
2193 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast);
2194 let _mv_fast = match ab {
2195 Some((bits, _)) => {
2196 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2197 crate::gpu_metal::MvFastGuard::set_raw(bits)
2198 }
2199 None => {
2200 graph.set_dense_concurrent(dense_fast);
2201 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2202 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE
2203 } else {
2204 0
2205 })
2206 }
2207 };
2208
2209 let inv_freq = self.inv_freq.clone();
2210 let pool = self.pool.clone();
2211 let (nh, nkv, hd, hs, rd, eps) = (
2212 self.num_heads,
2213 self.num_kv_heads,
2214 self.head_dim,
2215 self.hidden_size,
2216 self.rotary_dim,
2217 self.rms_eps,
2218 );
2219 let norm_style = self.norm_style;
2220 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2221 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2222 let kv_id = self.graph_kv_id;
2223 let mut pending: Vec<(usize, usize)> = Vec::new();
2226 let mut dev_attn: Vec<usize> = Vec::new();
2229 for item in &plan {
2230 let _xt0 = std::time::Instant::now();
2231 let _xkind: u32 = match item {
2232 Item::Gdn { .. } => 2,
2233 Item::Attn { .. } => 3,
2234 };
2235 if self.loop_final_norm {
2237 let item_start = match item {
2238 Item::Gdn { first, .. } => *first,
2239 Item::Attn { li, .. } => *li,
2240 };
2241 if item_start > start && self.is_loop_end(item_start - 1) {
2242 graph.encode_loop_norm(&self.weights.final_norm);
2243 }
2244 }
2245 match item {
2246 Item::Gdn { run, first } => {
2247 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2248 if l.linear_state.len() != want {
2249 l.linear_state = vec![0f32; want];
2250 }
2251 }
2252 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2253 .iter()
2254 .map(|l| l.linear_state.as_slice())
2255 .collect();
2256 let _ig = std::time::Instant::now();
2257 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2258 tracing::error!("q1 graph: GDN run refused after validation");
2260 return start;
2261 }
2262 graph.commit_kind = 2;
2265 graph.commit();
2266 crate::gpu::stageprof(0, _ig.elapsed());
2267 pending.push((*first, run.len()));
2268 }
2269 Item::Attn {
2270 l,
2271 li,
2272 q_norm,
2273 k_norm,
2274 output_gate,
2275 bias,
2276 full_gpu,
2277 } => {
2278 let _ia = std::time::Instant::now();
2279 if *full_gpu {
2281 let cache = &self.kv_cache.layers[*li];
2282 let o1p = if cache.o1.is_some() {
2283 match cache.o1_views() {
2284 Some(views) => Some(crate::gpu::O1AttnParams {
2285 views,
2286 epoch: self.o1_epoch,
2287 }),
2288 None => None,
2290 }
2291 } else {
2292 None
2293 };
2294 let o1_layer = cache.o1.is_some();
2295 if o1_layer && o1p.is_none() {
2296 }
2298 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
2299 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
2300 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
2301 let p = crate::gpu::AttnDeviceParams {
2302 kv_id,
2303 layer: *li,
2304 nh,
2305 nkv,
2306 hd,
2307 rd,
2308 position,
2309 scale: self.attn_scale,
2310 eps: eps as f32,
2311 gemma,
2312 late_qk_norm: self.qk_norm_after_rope,
2313 output_gate: *output_gate,
2314 q_norm: *q_norm,
2315 k_norm: *k_norm,
2316 inv_freq: &inv_freq,
2317 cpu_k,
2318 cpu_v,
2319 cpu_stored,
2320 o1: o1p,
2321 };
2322 let o1_bad = o1_layer && p.o1.is_none();
2323 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
2324 {
2325 if p.o1.is_none() {
2327 dev_attn.push(*li);
2328 }
2329 graph.commit_kind = 3;
2330 graph.commit();
2331 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2335 continue;
2336 }
2337 }
2339 graph.encode_attn_prefix(l);
2340 if let Err(err) = graph.sync_checked() {
2341 self.fail_metal_graph(&err);
2342 return start;
2343 }
2344 if !pending.is_empty() {
2345 let idxs: Vec<usize> =
2346 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2347 let mut outs: Vec<&mut [f32]> = self
2348 .kv_cache
2349 .layers
2350 .iter_mut()
2351 .enumerate()
2352 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2353 .map(|(_, s)| s.linear_state.as_mut_slice())
2354 .collect();
2355 graph.read_states(&mut outs);
2356 }
2357 let mut q_raw = attention::take_buf(l.wq.1);
2358 let mut k = attention::take_buf(l.wk.1);
2359 let mut v = attention::take_buf(l.wv.1);
2360 graph.read_qkv(&mut q_raw, &mut k, &mut v);
2361 let cfg = QwenAttnCfg {
2362 num_heads: nh,
2363 num_kv_heads: nkv,
2364 head_dim: hd,
2365 hidden_size: hs,
2366 position,
2367 inv_freq: &inv_freq,
2368 rotary_dim: rd,
2369 scale: self.attn_scale,
2370 softcap: self.attn_softcap,
2371 window: None,
2372 v_norm: false,
2373 qk_norm_after_rope: self.qk_norm_after_rope,
2374 q_norm: *q_norm,
2375 k_norm: *k_norm,
2376 output_gate: *output_gate,
2377 softplus_gate: None,
2378 rope_scale: 1.0,
2379 bias: *bias,
2380 rms_eps: eps,
2381 norm_style,
2382 pool: pool.as_deref(),
2383 v_head_dim: hd,
2384 };
2385 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
2388 || std::env::var("CMF_ATTN_DUMP").is_ok();
2389 let _ = full_gpu;
2390 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
2391 let mut ao = attention::qwen_attention_core(
2392 q_raw,
2393 k,
2394 v,
2395 &mut self.kv_cache.layers[*li],
2396 &cfg,
2397 );
2398 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
2402 if let Some((qr0, k0, v0)) = oracle_in.clone() {
2403 let (cq, _cg, _ck, _cv) =
2404 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2405 let cache = &self.kv_cache.layers[*li];
2406 let n = cache.head_keys(0).len() / hd;
2407 let mut bytes: Vec<u8> = Vec::new();
2408 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
2409 bytes.extend_from_slice(&v.to_le_bytes());
2410 }
2411 for v in &cq {
2412 bytes.extend_from_slice(&v.to_le_bytes());
2413 }
2414 for g in 0..nkv {
2415 for v in cache.head_keys(g) {
2416 bytes.extend_from_slice(&v.to_le_bytes());
2417 }
2418 }
2419 for g in 0..nkv {
2420 for v in cache.head_values(g) {
2421 bytes.extend_from_slice(&v.to_le_bytes());
2422 }
2423 }
2424 let _ =
2425 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
2426 }
2427 }
2428 if let Some((qr0, k0, v0)) =
2429 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
2430 {
2431 let (cq, _cg, ck, cv) =
2432 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2433 let mut h_now = vec![0f32; hs];
2434 graph.read_h(&mut h_now);
2435 let cache = &self.kv_cache.layers[*li];
2436 let n_after = cache.head_keys(0).len() / hd;
2437 let stored = n_after.saturating_sub(1);
2441 let cpu_k: Vec<&[f32]> = (0..nkv)
2442 .map(|g| &cache.head_keys(g)[..stored * hd])
2443 .collect();
2444 let cpu_v: Vec<&[f32]> = (0..nkv)
2445 .map(|g| &cache.head_values(g)[..stored * hd])
2446 .collect();
2447 let p = crate::gpu::AttnDeviceParams {
2448 kv_id,
2449 layer: *li,
2450 nh,
2451 nkv,
2452 hd,
2453 rd,
2454 position,
2455 scale: self.attn_scale,
2456 eps: eps as f32,
2457 gemma,
2458 late_qk_norm: self.qk_norm_after_rope,
2459 output_gate: *output_gate,
2460 q_norm: *q_norm,
2461 k_norm: *k_norm,
2462 inv_freq: &inv_freq,
2463 cpu_k,
2464 cpu_v,
2465 cpu_stored: stored,
2466 o1: None,
2467 };
2468 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
2469 let md = |a: &[f32], b: &[f32]| {
2470 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
2471 };
2472 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
2473 eprintln!(
2474 "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}",
2475 nn(&cq),
2476 md(&cq, &dq),
2477 nn(&ck),
2478 md(&ck, &dk),
2479 nn(&cv),
2480 md(&cv, &dv),
2481 nn(&ao),
2482 md(&ao, &dao)
2483 );
2484 } else {
2485 eprintln!("attn-oracle L{li}: device probe declined");
2486 }
2487 }
2488 graph.encode_attn_suffix(l, &ao);
2489 graph.commit();
2492 attention::recycle_buf(&mut ao);
2493 }
2494 }
2495
2496 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2497 }
2498 let mut lm_rows = None;
2503 if self.graph_want_logits
2504 && upto.is_none()
2505 && end == self.num_layers
2506 && std::env::var("CMF_GPU_LMHEAD")
2507 .map(|v| v != "0")
2508 .unwrap_or(true)
2509 {
2510 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
2511 if graph.lm_head_ok(lm) {
2512 graph.encode_lm_head(&self.weights.final_norm, lm);
2513 lm_rows = Some(lm.1);
2514 }
2515 }
2516 }
2517 if self.graph_head_required && lm_rows.is_none() {
2518 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2519 self.fail_metal_graph("fused graph head was requested but not encodable");
2520 return start;
2521 }
2522 let _sy0 = std::time::Instant::now();
2523 if let Err(err) = graph.sync_checked() {
2524 self.fail_metal_graph(&err);
2525 return start;
2526 }
2527 let _rs0 = std::time::Instant::now();
2528 if !pending.is_empty() {
2529 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2530 let mut outs: Vec<&mut [f32]> = self
2531 .kv_cache
2532 .layers
2533 .iter_mut()
2534 .enumerate()
2535 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2536 .map(|(_, s)| s.linear_state.as_mut_slice())
2537 .collect();
2538 graph.read_states(&mut outs);
2539 }
2540 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
2541 use std::sync::atomic::{AtomicU64, Ordering};
2542 static SY: AtomicU64 = AtomicU64::new(0);
2543 static RS: AtomicU64 = AtomicU64::new(0);
2544 static N: AtomicU64 = AtomicU64::new(0);
2545 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
2546 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
2547 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2548 if n % 100 == 0 {
2549 eprintln!(
2550 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
2551 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
2552 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2553 );
2554 }
2555 }
2556 if let Some(rows) = lm_rows {
2557 crate::gpu::hostprof_encode_done(_mt0);
2558 let mut lg = attention::take_buf(rows.min(self.vocab_size));
2559 graph.read_logits(&mut lg);
2560 crate::gpu::hostprof_total(_mt0);
2561 lg.resize(self.vocab_size, 0.0);
2562 if let Some(c) = self.final_softcap {
2563 for l in lg.iter_mut() {
2564 *l = c * (*l / c).tanh();
2565 }
2566 }
2567 self.graph_logits = Some(lg);
2568 }
2569 graph.read_h(h);
2570 if self.graph_head_required && self.graph_logits.is_none() {
2571 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2572 self.fail_metal_graph("fused graph head completed without logits readback");
2573 return start;
2574 }
2575 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2576 METAL_GRAPH_LAYERS.fetch_add(
2577 end.saturating_sub(start) as u64,
2578 std::sync::atomic::Ordering::Relaxed,
2579 );
2580 if self.graph_head_required {
2581 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2582 }
2583 for li in dev_attn {
2587 let mut krow = attention::take_buf(nkv * hd);
2588 let mut vrow = attention::take_buf(nkv * hd);
2589 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
2590 let cache = &mut self.kv_cache.layers[li];
2591 cache.append(&krow, &vrow, &[]);
2592 let n = cache.seq_len;
2593 let mut imp = attention::take_buf(n);
2594 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2595 cache.accumulate_imp(&imp);
2596 attention::recycle_buf(&mut imp);
2597 }
2598 attention::recycle_buf(&mut krow);
2599 attention::recycle_buf(&mut vrow);
2600 }
2601 if let Some((_, arm)) = ab {
2602 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
2603 }
2604 end
2605 }
2606
2607 pub fn new(
2608 tokenizer: Tokenizer,
2609 weights: PipelineWeights,
2610 hidden_size: usize,
2611 intermediate_size: usize,
2612 num_heads: usize,
2613 num_kv_heads: usize,
2614 head_dim: usize,
2615 num_layers: usize,
2616 physical_layers: usize,
2617 loop_final_norm: bool,
2618 vocab_size: usize,
2619 rms_eps: f64,
2620 rope_base: f32,
2621 norm_style: NormStyle,
2622 max_seq_len: usize,
2623 sampler_config: SamplerConfig,
2624 ) -> Self {
2625 let rng = match sampler_config.seed {
2626 Some(s) => SplitMix64::new(s),
2627 None => SplitMix64::from_entropy(),
2628 };
2629 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2630 let pool = Pool::from_env();
2631 if let Some(p) = &pool {
2632 tracing::info!("worker pool: {} threads", p.n_workers());
2633 if let Some(model) = weights
2635 .lm_head
2636 .model_arc()
2637 .or_else(|| weights.embed_tokens.model_arc())
2638 {
2639 let regions: Vec<&[u8]> =
2640 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
2641 p.bind_numa(®ions);
2642 }
2643 }
2644 Self {
2645 gpu_plan: None,
2646 tokenizer: std::sync::Arc::new(tokenizer),
2647 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2648 sampler_config,
2649 weights,
2650 hidden_size,
2651 intermediate_size,
2652 num_heads,
2653 num_kv_heads,
2654 head_dim,
2655 num_layers,
2656 physical_layers,
2657 loop_final_norm,
2658 vocab_size,
2659 rms_eps,
2660 rope_base,
2661 norm_style,
2662 rotary_dim: head_dim,
2663 attention_heads_per_layer: None,
2664 kv_heads_per_layer: None,
2665 v_head_dim: None,
2666 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
2667 .filter(|v| !v.is_empty())
2668 .map(std::path::PathBuf::from),
2669 graph_declines: std::cell::RefCell::new(Vec::new()),
2670 mimo_moe: Default::default(),
2671 vmf_cfg: None,
2672 gdn_cfg: None,
2673 kda_cfg: None,
2674 g3n: None,
2675 dsv4: None,
2676 dsv41: None,
2677 dsv41_vision: None,
2678 dsv41_prefill: None,
2679 qwen4_exp: None,
2680 dsv4_mtp: Vec::new(),
2681 dspark: None,
2682 dspark_pending: Vec::new(),
2683 dspark_hist: Vec::new(),
2684 dspark_real: Vec::new(),
2685 dspark_trunk_picks: Vec::new(),
2686 dspark_exp: Vec::new(),
2687 dspark_draft_ns: 0,
2688 logit_multiplier: None,
2689 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2690 graph_failed: std::sync::atomic::AtomicBool::new(false),
2691 kv_history: Vec::new(),
2692 short_conv_cfg: None,
2693 mtp: None,
2694 mimo_mtp: None,
2695 verify_exact_moe: false,
2696 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2697 ignore_eos: false,
2698 draft_full_streak: 0,
2699 spec_k_adapt: None,
2700 spec_acc_ewma: 0.7,
2701 rng,
2702 sampler_scratch: SamplerScratch::default(),
2703 spec_forced: None,
2704 spec_q: Vec::new(),
2705 spec_p: Vec::new(),
2706 spec_res: Vec::new(),
2707 spec_qs: Vec::new(),
2708 spec_ps: Vec::new(),
2709 spec_ress: Vec::new(),
2710 mtp_graph_mode: None,
2711 #[cfg(target_os = "macos")]
2712 metal_verify: None,
2713 inv_freq,
2714 ws: ForwardScratch::new(hidden_size),
2715 pool,
2716 model: None,
2717 dyn_force_f32: false,
2718 dyn_skill_layers: Vec::new(),
2719 dyn_active: None,
2720 dyn_blend_loaded: false,
2721 dyn_phi_layer: None,
2722 dyn_phi_ema: Vec::new(),
2723 dyn_phi_seen: 0,
2724 dyn_router: None,
2725 o1_cfg: None,
2726 o1_epoch: 0,
2727 o1_flags: Vec::new(),
2728 trace: false,
2729 calib_temp: 1.0,
2730 confidence_on: true,
2731 embed_multiplier: 1.0,
2732 attn_scale: 1.0 / (head_dim as f32).sqrt(),
2733 swa: None,
2734 sliding_layers: None,
2735 inv_freq_local: None,
2736 rotary_dim_local: None,
2737 rope_scale: 1.0,
2738 rope_scale_local: 1.0,
2739 global_attn: None,
2740 inv_freq_global: None,
2741 attn_v_norm: false,
2742 qk_norm_after_rope: false,
2743 final_softcap: None,
2744 head_clusters: None,
2745 attn_softcap: 0.0,
2746 graph_want_logits: false,
2747 graph_head_required: false,
2748 graph_logits: None,
2749 graph_kv_id: {
2750 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2751 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2752 },
2753 #[cfg(test)]
2754 nll_test_fail_at: None,
2755 #[cfg(test)]
2756 nll_test_force_serial: false,
2757 }
2758 }
2759
2760 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2768 if let Some(c) = &cfg {
2769 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2770 tracing::error!(
2771 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2772 c.w,
2773 c.sink
2774 );
2775 self.o1_flags.clear();
2776 self.o1_cfg = None;
2777 return;
2778 }
2779 }
2780 self.o1_flags = match &cfg {
2781 Some(c) => {
2782 let mut flags = c.layer_flags(self.num_layers);
2783 for (li, f) in flags.iter_mut().enumerate() {
2784 if *f
2790 && (!matches!(
2791 self.weights.layers[self.phys_layer(li)].attn,
2792 AttnKind::Full { .. }
2793 ) || self.layer_window(li).is_some()
2794 || self.kv_cache.layers[li].sinks.is_some()
2795 || self.layer_v_dim(li) != self.layer_geom(li).1)
2796 {
2797 *f = false;
2798 }
2799 }
2800 flags
2801 }
2802 None => Vec::new(),
2803 };
2804 if let Some(c) = &cfg {
2805 let n = self.o1_flags.iter().filter(|&&f| f).count();
2806 tracing::info!(
2807 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2808 self.num_layers,
2809 c.m,
2810 c.w,
2811 c.sink,
2812 c.rect
2813 );
2814 }
2815 self.o1_cfg = cfg;
2816 }
2817
2818 pub fn o1_active(&self) -> bool {
2820 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2821 }
2822
2823 pub fn generation_batch_k(&self) -> usize {
2835 if let Some(k) = std::env::var("CMF_BATCH_K")
2836 .ok()
2837 .and_then(|v| v.parse::<usize>().ok())
2838 {
2839 return k;
2840 }
2841 #[cfg(not(target_os = "macos"))]
2842 if self.graph_prefill_preferred() && !self.o1_active() {
2843 return 32;
2844 }
2845 0
2846 }
2847
2848 pub fn generation_graph_prefill(&self) -> bool {
2849 let graph = self.graph_prefill_preferred();
2850 #[cfg(not(target_os = "macos"))]
2861 if graph
2862 && self.generation_batch_k() > 0
2863 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2864 {
2865 return false;
2866 }
2867 graph
2868 }
2869
2870 pub fn o1_device_stats(&self) -> (usize, u64) {
2875 crate::gpu::o1_device_stats(self.graph_kv_id)
2876 }
2877
2878 pub fn o1_begin(&mut self) {
2883 self.o1_begin_with_prefix(None);
2884 }
2885
2886 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2890 if let Some(c) = &self.o1_cfg {
2891 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2892 let boundary = requested_prefix.map(|p| {
2893 p.max(
2894 crate::nystrom::o1_deferred_boundary(w, sink)
2895 .expect("o1 config boundary validated in set_o1"),
2896 )
2897 });
2898 for (li, &f) in self.o1_flags.iter().enumerate() {
2899 if f {
2900 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2901 }
2902 }
2903 }
2904 }
2905
2906 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2908 self.o1_cfg.as_ref().and_then(|c| {
2909 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2910 .map(|floor| requested_prefix.max(floor))
2911 })
2912 }
2913
2914 fn o1_note_transition(&mut self) {
2915 let mut transitioned = false;
2919 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2920 if flagged {
2921 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2922 }
2923 }
2924 if transitioned {
2925 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2926 }
2927 }
2928
2929 fn o1_pending(&self) -> bool {
2930 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2931 f && self.kv_cache.layers[li].seq_len > 0
2932 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2933 })
2934 }
2935
2936 fn o1_fail(&mut self, err: String) {
2937 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2938 self.clear_sequence_state();
2939 self.graph_failed
2940 .store(true, std::sync::atomic::Ordering::Relaxed);
2941 self.cancel
2942 .store(true, std::sync::atomic::Ordering::Relaxed);
2943 }
2944
2945 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2950 if self.o1_cfg.is_none() {
2951 return Ok(false);
2952 }
2953 let mut participating = false;
2954 for li in 0..self.num_layers {
2955 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2956 continue;
2957 }
2958 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2959 return Err(err);
2960 }
2961 if self.kv_cache.layers[li].seq_len == 0 {
2962 continue;
2963 }
2964 participating = true;
2965 let num_heads = self.layer_num_heads(li);
2966 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2967 }
2968 self.o1_note_transition();
2969 for li in 0..self.num_layers {
2970 if self.o1_flags.get(li).copied().unwrap_or(false) {
2971 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2972 return Err(err);
2973 }
2974 }
2975 }
2976 Ok(participating
2977 && (0..self.num_layers).all(|li| {
2978 !self.o1_flags.get(li).copied().unwrap_or(false)
2979 || self.kv_cache.layers[li].seq_len == 0
2980 || self.kv_cache.layers[li].o1_sealed()
2981 }))
2982 }
2983
2984 fn o1_progress(&mut self) {
2987 if !self.o1_active() {
2988 return;
2989 }
2990 for li in 0..self.num_layers {
2991 if self.o1_flags.get(li).copied().unwrap_or(false) {
2992 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2993 self.o1_fail(err);
2994 return;
2995 }
2996 }
2997 }
2998 self.o1_note_transition();
3002 if !self.o1_pending() {
3003 return;
3004 }
3005 if let Err(err) = self.o1_seal_checked() {
3006 self.o1_fail(err);
3007 }
3008 }
3009
3010 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
3015 if self
3016 .graph_failed
3017 .swap(false, std::sync::atomic::Ordering::Relaxed)
3018 {
3019 self.cancel
3020 .store(false, std::sync::atomic::Ordering::Relaxed);
3021 self.clear_sequence_state();
3022 return Err(format!("{phase}: deferred O(1) transition failed"));
3023 }
3024 Ok(())
3025 }
3026
3027 pub fn o1_seal(&mut self) {
3031 if let Err(err) = self.o1_seal_checked() {
3032 self.o1_fail(err);
3033 }
3034 }
3035
3036 pub fn set_trace(&mut self, on: bool) {
3038 self.trace = on;
3039 }
3040
3041 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
3044 self.rng = match config.seed {
3045 Some(seed) => SplitMix64::new(seed),
3046 None => SplitMix64::from_entropy(),
3047 };
3048 self.sampler_config = config;
3049 }
3050
3051 pub fn set_confidence(&mut self, on: bool) {
3056 self.confidence_on = on;
3057 }
3058
3059 pub fn set_calib_temp(&mut self, t: f32) {
3062 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
3063 }
3064
3065 pub fn calib_temp(&self) -> f32 {
3067 self.calib_temp
3068 }
3069
3070 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
3073 self.rotary_dim = rotary_dim.min(self.head_dim);
3074 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
3075 }
3076
3077 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
3078 QwenAttnCfg {
3079 num_heads: self.num_heads,
3080 num_kv_heads: self.num_kv_heads,
3081 head_dim: self.head_dim,
3082 hidden_size: self.hidden_size,
3083 position,
3084 inv_freq: &self.inv_freq,
3085 rotary_dim: self.rotary_dim,
3086 scale: self.attn_scale,
3087 softcap: self.attn_softcap,
3088 window: None,
3089 v_norm: false,
3090 qk_norm_after_rope: self.qk_norm_after_rope,
3091 q_norm: None,
3092 k_norm: None,
3093 output_gate: false,
3094 softplus_gate: None,
3095 rope_scale: self.rope_scale,
3096 bias: None,
3097 rms_eps: self.rms_eps,
3098 norm_style: self.norm_style,
3099 pool: self.pool.as_deref(),
3100 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
3101 }
3102 }
3103
3104 pub fn generate(
3106 &mut self,
3107 prompt: &str,
3108 max_tokens: usize,
3109 task_mask: Option<&TaskMask>,
3110 on_token: Option<TokenCallback>,
3111 ) -> Result<GenerateResult, String> {
3112 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
3113 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
3114 }
3115
3116 pub fn generate_from_vl(
3119 &mut self,
3120 input: &crate::dsv41_vision::PreparedVlInputs,
3121 max_tokens: usize,
3122 task_mask: Option<&TaskMask>,
3123 on_token: Option<TokenCallback>,
3124 ) -> Result<GenerateResult, String> {
3125 let Some(dsv41) = &self.dsv41 else {
3126 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
3127 };
3128 if input.token_ids.is_empty() {
3129 return Err("empty V4.1 multimodal prompt".into());
3130 }
3131 if input.token_types.len() != input.token_ids.len() {
3132 return Err(format!(
3133 "V4.1 token type count {} != token count {}",
3134 input.token_types.len(),
3135 input.token_ids.len()
3136 ));
3137 }
3138 let dim = dsv41.2.dim;
3139 let mut embeddings = vec![None; input.token_ids.len()];
3140 let mut participates = vec![true; input.token_ids.len()];
3141 if !input.images.is_empty() {
3142 let vision = self
3143 .dsv41_vision
3144 .as_ref()
3145 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
3146 for image in &input.images {
3147 let end = image.start.saturating_add(image.types.len());
3148 if end > input.token_ids.len() {
3149 return Err(format!(
3150 "V4.1 image span {}..{} exceeds prompt length {}",
3151 image.start,
3152 end,
3153 input.token_ids.len()
3154 ));
3155 }
3156 let mut span = vec![0.0f32; image.types.len() * dim];
3157 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
3158 for (offset, &kind) in image.types.iter().enumerate() {
3159 let pos = image.start + offset;
3160 if input.token_types[pos] != kind {
3161 return Err(format!(
3162 "V4.1 image type mismatch at position {pos}: {} != {kind}",
3163 input.token_types[pos]
3164 ));
3165 }
3166 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
3167 participates[pos] = false;
3168 }
3169 }
3170 }
3171 for (pos, &kind) in input.token_types.iter().enumerate() {
3172 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
3173 return Err(format!("V4.1 text position {pos} has an image embedding"));
3174 }
3175 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
3176 return Err(format!("V4.1 image position {pos} has no image embedding"));
3177 }
3178 }
3179 self.dsv41_prefill = Some((embeddings, participates));
3180 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
3181 self.dsv41_prefill = None;
3182 result
3183 }
3184
3185 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
3187 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
3188 }
3189
3190 pub fn generate_from_ids(
3198 &mut self,
3199 input_ids: &[u32],
3200 max_tokens: usize,
3201 task_mask: Option<&TaskMask>,
3202 on_token: Option<TokenCallback>,
3203 ) -> Result<GenerateResult, String> {
3204 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
3205 }
3206
3207 pub fn generate_from_embeds(
3213 &mut self,
3214 input_ids: &[u32],
3215 prompt_rows: &[f32],
3216 max_tokens: usize,
3217 task_mask: Option<&TaskMask>,
3218 on_token: Option<TokenCallback>,
3219 ) -> Result<GenerateResult, String> {
3220 if input_ids.is_empty()
3221 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
3222 {
3223 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
3224 }
3225 if prompt_rows.iter().any(|x| !x.is_finite()) {
3226 return Err("embedded prompt contains non-finite values".into());
3227 }
3228 if !self.can_prefill_batched() || self.dyn_router.is_some()
3229 || self.o1_active() || self.mtp.is_some() || self.gpu_plan.is_some()
3230 {
3231 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
3232 }
3233 self.generate_with_prompt_rows(input_ids, Some(prompt_rows), max_tokens, task_mask, on_token)
3234 }
3235
3236 fn generate_with_prompt_rows(
3237 &mut self,
3238 input_ids: &[u32],
3239 prompt_rows: Option<&[f32]>,
3240 max_tokens: usize,
3241 task_mask: Option<&TaskMask>,
3242 mut on_token: Option<TokenCallback>,
3243 ) -> Result<GenerateResult, String> {
3244 if std::env::var("CMF_TRACE_H").is_ok() {
3245 eprintln!("input_ids: {input_ids:?}");
3246 }
3247 if input_ids.is_empty() {
3248 return Err("empty prompt: nothing to generate from".to_string());
3249 }
3250 self.graph_failed
3254 .store(false, std::sync::atomic::Ordering::Relaxed);
3255 let task_mask = self.drop_open_mask(task_mask);
3260
3261 let mut reuse_from = {
3269 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
3270 let h = &self.kv_history;
3271 if on
3272 && prompt_rows.is_none()
3273 && task_mask.is_none()
3274 && self.mtp.is_none()
3275 && !(self.mimo_mtp.is_some() && self.speculative)
3276 && self.o1_cfg.is_none()
3277 && self.dsv41.is_none()
3278 && !h.is_empty()
3279 && h.len() < input_ids.len()
3280 && input_ids[..h.len()] == h[..]
3281 {
3282 h.len()
3283 } else {
3284 0
3285 }
3286 };
3287 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
3290 reuse_from = 0;
3291 }
3292 if reuse_from == 0 {
3293 self.clear_sequence_state();
3295 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
3296 eprintln!(
3297 "kv-reuse: {} of {} prompt positions already cached",
3298 reuse_from,
3299 input_ids.len()
3300 );
3301 }
3302 crate::gpu::graph_race_begin_generation();
3303 let o1_prefill = if self.o1_active() && task_mask.is_none() {
3307 std::env::var("CMF_O1_PREFILL")
3308 .ok()
3309 .and_then(|v| v.parse::<usize>().ok())
3310 .filter(|&p| p > 0)
3311 } else {
3312 None
3313 };
3314 if task_mask.is_none() {
3315 self.o1_begin_with_prefix(o1_prefill);
3316 }
3317
3318 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
3324 #[cfg(target_os = "macos")]
3363 let metal_graph = crate::gpu::q1_force()
3364 && crate::gpu::enabled_here()
3365 && std::env::var("CMF_GPU_BLOCK")
3366 .map(|v| v != "0")
3367 .unwrap_or(true);
3368 #[cfg(not(target_os = "macos"))]
3369 let metal_graph = false;
3370 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
3371 let spec_cheap_round = self.sampler_config.temperature < 1e-6
3375 || sampler::sparse_ok(&self.sampler_config);
3376 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
3377 || match spec_sample_env.as_deref() {
3378 Some("1") => true,
3379 Some(_) => false,
3380 None => metal_graph && spec_cheap_round,
3381 };
3382 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
3398 for lw in &self.weights.layers {
3399 if let FfnKind::Dense(d) = &lw.ffn {
3400 dense_n += 1;
3401 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
3402 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
3403 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
3404 {
3405 dense_q4tp += 1;
3406 }
3407 }
3408 }
3409 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
3410 let penalized = !metal_graph
3422 && (self.sampler_config.repetition_penalty != 1.0
3423 || self.sampler_config.presence_penalty != 0.0
3424 || !self.sampler_config.suppress_tokens.is_empty());
3425 #[cfg(feature = "gpu")]
3430 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
3431 #[cfg(not(feature = "gpu"))]
3432 let metal_wgpu = false;
3433 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
3434 let spec_wanted = match spec_env.as_deref() {
3435 Some("0") => false,
3436 Some(_) => {
3437 if metal_wgpu {
3438 tracing::warn!(
3439 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
3440 verified on this backend (garbage measured on Qwen3.5-0.8B)"
3441 );
3442 }
3443 true
3444 }
3445 None => spec_default_ok && !penalized && !metal_wgpu,
3446 };
3447 let graph_spec = self.speculative
3451 && (graph_on || metal_graph)
3452 && self.mtp.is_some()
3453 && task_mask.is_none()
3454 && !self.o1_active()
3455 && spec_sampling_ok
3456 && spec_wanted;
3457 #[cfg(target_os = "macos")]
3461 if metal_graph {
3462 static SAID: std::sync::Once = std::sync::Once::new();
3463 SAID.call_once(|| {
3464 let spec = if graph_spec {
3465 let k = std::env::var("CMF_GRAPH_SPEC_K")
3466 .ok()
3467 .and_then(|v| v.parse::<usize>().ok())
3468 .filter(|&v| (1..=8).contains(&v))
3469 .unwrap_or(7);
3470 let arm = if self.sampler_config.temperature < 1e-6 {
3471 "greedy"
3472 } else {
3473 "sampling"
3474 };
3475 format!(
3476 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
3477 Self::draft_vocab_rows(usize::MAX)
3478 )
3479 } else if !self.speculative {
3480 "spec off (CMF_MTP=0)".to_string()
3481 } else if self.mtp.is_none() {
3482 "spec off (no MTP head)".to_string()
3483 } else if !spec_sampling_ok {
3484 if spec_cheap_round {
3485 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
3486 } else {
3487 "spec off (sampling without a top-k: the dense chain \
3488 costs more than it saves)"
3489 .to_string()
3490 }
3491 } else if !spec_wanted {
3492 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
3493 } else if task_mask.is_some() {
3494 "spec off (task mask)".to_string()
3495 } else {
3496 "spec off (O(1) attention)".to_string()
3497 };
3498 let on = |var: &str| {
3499 if std::env::var(var).as_deref() == Ok("0") {
3500 "off"
3501 } else {
3502 "on"
3503 }
3504 };
3505 tracing::info!(
3506 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
3507 MTP graph {}, attend {}, probe {}",
3508 if crate::gpu_metal::state4_on() { "on" } else { "off" },
3509 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
3510 on("CMF_METAL_PREFILL"),
3511 on("CMF_MTP_GRAPH"),
3512 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
3513 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
3514 );
3515 });
3516 }
3517 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
3524 let spec_active = self.speculative
3525 && self.mtp.is_some()
3526 && task_mask.is_none()
3527 && !self.o1_active()
3528 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
3529 let mut mtp = if spec_active { self.mtp.take() } else { None };
3532 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
3533 eprintln!(
3534 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
3535 mtp.is_some(),
3536 self.speculative,
3537 self.sampler_config.temperature < 1e-6,
3538 );
3539 }
3540 if let Some(m) = &mut mtp {
3541 m.kv.clear();
3542 crate::gpu::graph_kv_reset(self.mtp_kv_id());
3544 self.mtp_graph_mode = None;
3545 }
3546 let mimo_spec = self.speculative
3550 && self.mimo_mtp.is_some()
3551 && task_mask.is_none()
3552 && !self.o1_active()
3553 && self.dyn_router.is_none()
3554 && self.sampler_config.temperature < 1e-6
3555 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
3556 if let Some(st) = self.mimo_mtp.as_mut() {
3557 st.reset();
3558 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
3559 Self::mimo_mtp_hist_cap(st, input_ids.len());
3560 }
3561 }
3562 let mut router = if mtp.is_none() {
3566 self.dyn_router.take()
3567 } else {
3568 None
3569 };
3570 if let Some(r) = &mut router {
3571 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
3574 }
3575
3576 let mut all_ids = input_ids.to_vec();
3577 let mut generated = 0usize;
3578 let mut finish_reason = "max_tokens".to_string();
3579 let mut drafted = 0usize;
3580 let mut accepted = 0usize;
3581 let mut dsv4_spec_bad = 0usize;
3588 let mut dsv4_spec_retry_at = 0usize;
3589 let mut confidence: Vec<f32> = Vec::new();
3590 let trace_on = self.trace;
3591 let calib_temp = self.calib_temp;
3592 let mut traces: Vec<TokenTrace> = Vec::new();
3593
3594 let mut hidden = vec![0.0f32; self.hidden_size];
3600 let mut pos = reuse_from;
3601 let fuse_lm = mtp.is_none()
3610 && router.is_none()
3611 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
3612 self.graph_logits = None;
3613 self.graph_want_logits = false;
3614 let _tpf = std::time::Instant::now();
3615 let batch_k = self.generation_batch_k();
3616 if let Some(rows) = prompt_rows {
3617 let hs = self.hidden_size;
3618 let chunk = self.prefill_chunk().max(1);
3619 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3620 let end = (pos + chunk).min(input_ids.len());
3621 let hb = match self.prefill_input_rows(
3622 PrefillIn::Hidden(&rows[pos * hs..end * hs]), pos, task_mask,
3623 ) {
3624 Ok(hb) => hb,
3625 Err(err) => {
3626 self.finish_generation(&mut mtp, &mut router, true);
3627 return Err(err);
3628 }
3629 };
3630 if mimo_spec { self.mimo_note_rows(&hb, pos); }
3631 hidden.copy_from_slice(&hb[hb.len() - hs..]);
3632 pos = end;
3633 }
3634 }
3635 while self.qwen4_exp.is_some()
3646 && mtp.is_none()
3647 && pos < input_ids.len()
3648 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3649 {
3650 let token_id = input_ids[pos];
3651 let want_logits = pos + 1 == input_ids.len();
3652 let mut lg = Vec::new();
3653 if let Some(b) = &mut self.qwen4_exp {
3654 crate::qwen4_exp::forward_token(
3655 &b.0,
3656 &b.1,
3657 &b.2,
3658 &mut b.3,
3659 token_id,
3660 pos,
3661 &self.inv_freq,
3662 self.pool.as_deref(),
3663 &mut lg,
3664 want_logits,
3665 );
3666 }
3667 if want_logits {
3668 self.graph_logits = Some(lg);
3669 }
3670 pos += 1;
3671 hidden.fill(0.0);
3672 }
3673 while self.dsv4.is_some()
3674 && mtp.is_none()
3675 && pos < input_ids.len()
3676 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3677 {
3678 let end = (pos + prefill_chunk()).min(input_ids.len());
3679 let ids: Vec<u32> = input_ids[pos..end].to_vec();
3680 let mut lg = Vec::new();
3681 if let Some(b) = &mut self.dsv4 {
3682 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
3683 crate::dsv4::forward_chunk(
3684 g,
3685 layers,
3686 &cfg,
3687 st,
3688 &ids,
3689 pos,
3690 &self.inv_freq,
3691 self.pool.as_deref(),
3692 &mut lg,
3693 end == input_ids.len(),
3694 );
3695 }
3696 if end == input_ids.len() {
3697 self.graph_logits = Some(lg);
3698 }
3699 pos = end;
3700 hidden = vec![0.0; self.hidden_size];
3701 }
3702 let dsv41_prefill = self.dsv41_prefill.take();
3703 while self.dsv41.is_some()
3704 && mtp.is_none()
3705 && pos < input_ids.len()
3706 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3707 {
3708 let end = (pos + prefill_chunk()).min(input_ids.len());
3709 let ids: Vec<u32> = input_ids[pos..end].to_vec();
3710 let mut lg = Vec::new();
3711 if let Some(b) = &mut self.dsv41 {
3712 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
3713 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
3714 crate::dsv41::forward_chunk_masked_with_embeddings(
3715 g,
3716 layers,
3717 cfg,
3718 st,
3719 &ids,
3720 pos,
3721 &embeddings[pos..end],
3722 &participates[pos..end],
3723 self.pool.as_deref(),
3724 &mut lg,
3725 );
3726 } else {
3727 crate::dsv41::forward_chunk(
3728 g,
3729 layers,
3730 cfg,
3731 st,
3732 &ids,
3733 pos,
3734 self.pool.as_deref(),
3735 &mut lg,
3736 );
3737 }
3738 }
3739 if end == input_ids.len() {
3740 self.graph_logits = Some(lg);
3741 }
3742 pos = end;
3743 hidden = vec![0.0; self.hidden_size];
3744 }
3745 let dyn_prefill = router.is_some();
3750 let o1_prefill_limit = o1_prefill
3758 .and_then(|requested| self.o1_effective_boundary(requested))
3759 .map(|boundary| boundary.min(input_ids.len()));
3760 let mut o1_sealed = false;
3761 if let Some(limit) = o1_prefill_limit {
3762 if self.can_prefill_batched() && limit > 2 {
3765 let chunk = self.prefill_chunk();
3766 let hs = self.hidden_size;
3767 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3768 let end = (pos + chunk).min(limit);
3769 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3770 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3771 pos = end;
3772 }
3773 } else {
3774 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3775 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
3776 pos += 1;
3777 }
3778 }
3779 if pos >= limit {
3780 o1_sealed = match self.o1_seal_checked() {
3781 Ok(sealed) => sealed,
3782 Err(err) => {
3783 self.finish_generation(&mut mtp, &mut router, true);
3784 return Err(err);
3785 }
3786 };
3787 tracing::info!(
3788 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
3789 o1_prefill.unwrap_or(0),
3790 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
3791 .unwrap_or(limit),
3792 limit,
3793 input_ids.len()
3794 );
3795 }
3796 }
3797 let graph_prefill = self.graph_prefill_preferred();
3803 #[cfg(target_os = "macos")]
3811 if task_mask.is_none()
3812 && !dyn_prefill
3813 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
3814 && crate::gpu::enabled_here()
3815 && self.gdn_cfg.is_some()
3816 && self.g3n.is_none()
3817 && input_ids.len() > 8
3818 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3819 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
3820 {
3821 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
3822 .ok()
3823 .and_then(|v| v.parse().ok())
3824 .filter(|&v| (16..=512).contains(&v))
3825 .unwrap_or(256);
3826 let hs = self.hidden_size;
3827 let _tp = std::time::Instant::now();
3828 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3829 let end = (pos + chunk).min(input_ids.len());
3830 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
3831 MetalPrefillOutcome::Completed(hb) => hb,
3832 MetalPrefillOutcome::Declined => break,
3833 MetalPrefillOutcome::Failed => {
3834 self.finish_generation(&mut mtp, &mut router, true);
3835 return Err("ordinary Metal prefill failed after admission".into());
3836 }
3837 };
3838 if let Some(m) = &mut mtp {
3839 let n_pairs = if end < input_ids.len() {
3840 end - pos
3841 } else {
3842 end - pos - 1
3843 };
3844 if n_pairs > 0 {
3845 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3846 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3847 .collect();
3848 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3849 for (j, (h, t)) in pairs.iter().enumerate() {
3850 let h = h.to_vec();
3851 let _ = self.mtp_step(m, &h, *t, pos + j);
3852 }
3853 }
3854 }
3855 }
3856 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3857 pos = end;
3858 }
3859 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3860 eprintln!(
3861 "metal-prefill: {} of {} tokens in {:.1} ms",
3862 pos,
3863 input_ids.len(),
3864 _tp.elapsed().as_secs_f64() * 1e3
3865 );
3866 }
3867 }
3868 self.mimo_moe_prepare();
3869 #[cfg(not(target_os = "macos"))]
3875 if task_mask.is_none()
3876 && !dyn_prefill
3877 && !graph_prefill
3878 && mtp.is_none()
3879 && o1_prefill.is_none()
3880 && !self.o1_active()
3881 && input_ids.len() > 2
3882 && self.batch_prefix_prefill()
3883 {
3884 let chunk = self.prefill_chunk().max(1);
3885 let hs = self.hidden_size;
3886 let t_bp = std::time::Instant::now();
3887 let pos0 = pos;
3888 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3889 let end = (pos + chunk).min(input_ids.len());
3890 let bk = end - pos;
3891 let mut hiddens = vec![0f32; bk * hs];
3892 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3893 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3894 }
3895 let positions: Vec<usize> = (pos..end).collect();
3896 let mut run = 0usize;
3897 let outcome = self.try_batch_graph_wgpu_prefix(
3898 &mut hiddens,
3899 &positions,
3900 bk,
3901 None,
3902 Some(&mut run),
3903 );
3904 match outcome {
3905 crate::gpu::BatchGraphOutcome::Completed => {
3906 let hb = if run < self.num_layers {
3907 self.prefill_batch_span(
3908 PrefillIn::Hidden(&hiddens),
3909 pos,
3910 None,
3911 run,
3912 self.num_layers,
3913 )
3914 } else {
3915 hiddens
3916 };
3917 if mimo_spec {
3918 self.mimo_note_rows(&hb, pos);
3919 }
3920 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
3921 pos = end;
3922 }
3923 crate::gpu::BatchGraphOutcome::Failed => {
3924 self.finish_generation(&mut mtp, &mut router, true);
3925 return Err("batched prefix prefill failed after admission".into());
3926 }
3927 crate::gpu::BatchGraphOutcome::Declined => {
3928 #[cfg(feature = "gpu")]
3931 if pos > pos0 {
3932 self.pull_lagging_host_kv(0, self.num_layers, pos);
3933 }
3934 break;
3935 }
3936 }
3937 }
3938 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3939 eprintln!(
3940 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
3941 pos - pos0,
3942 input_ids.len(),
3943 t_bp.elapsed().as_secs_f64() * 1e3
3944 );
3945 }
3946 }
3947 if task_mask.is_none()
3948 && !dyn_prefill
3949 && !graph_prefill
3950 && self.can_prefill_batched()
3951 && self.g3n.is_none()
3952 && o1_prefill.is_none()
3953 && input_ids.len() > 2
3954 {
3955 let chunk = self.prefill_chunk();
3961 let hs = self.hidden_size;
3962 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3963 let end = (pos + chunk).min(input_ids.len());
3964 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3965 if mimo_spec {
3966 self.mimo_note_rows(&hb, pos);
3967 }
3968 if let Some(m) = &mut mtp {
3969 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3970 .ok()
3971 .and_then(|v| v.parse().ok())
3972 .unwrap_or(0);
3973 for p in pos..end {
3974 if p + 1 < input_ids.len() {
3975 if probe >= 1 && p + 2 < input_ids.len() {
3976 let (d1, mut hx) = self.mtp_step_h(
3980 m,
3981 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3982 input_ids[p + 1],
3983 p,
3984 );
3985 let mut ok = d1 == input_ids[p + 2];
3986 Self::chain_probe_note(0, ok);
3987 let mut d_prev = d1;
3988 let mut extra = 0usize;
3989 for j in 1..probe {
3990 if p + 2 + j >= input_ids.len() {
3991 break;
3992 }
3993 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3994 extra += 1;
3995 ok = ok && dj == input_ids[p + 2 + j];
3996 Self::chain_probe_note(j, ok);
3997 d_prev = dj;
3998 hx = hj;
3999 }
4000 m.kv.truncate_last(extra);
4001 } else {
4002 let _ = self.mtp_step(
4003 m,
4004 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4005 input_ids[p + 1],
4006 p,
4007 );
4008 }
4009 }
4010 }
4011 }
4012 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4013 pos = end;
4014 }
4015 }
4016 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
4017 if task_mask.is_none()
4018 && !dyn_prefill
4019 && !graph_prefill
4020 && !pair_off
4021 && self.pair_supported()
4022 && o1_prefill.is_none()
4023 {
4024 while pos + 1 < input_ids.len()
4025 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4026 {
4027 let e1 = self.embed_single(input_ids[pos]);
4028 let e2 = self.embed_single(input_ids[pos + 1]);
4029 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
4030 if mimo_spec {
4031 self.mimo_note_rows(&h1, pos);
4032 self.mimo_note_rows(&h2, pos + 1);
4033 }
4034 self.commit_linear_scratch();
4036 if let Some(m) = &mut mtp {
4037 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
4038 if pos + 2 < input_ids.len() {
4039 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4040 .ok()
4041 .and_then(|v| v.parse().ok())
4042 .unwrap_or(0);
4043 if probe >= 1 && pos + 3 < input_ids.len() {
4044 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
4048 let mut ok = d1 == input_ids[pos + 3];
4049 Self::chain_probe_note(0, ok);
4050 let mut d_prev = d1;
4051 let mut extra = 0usize;
4052 for j in 1..probe {
4053 if pos + 3 + j >= input_ids.len() {
4054 break;
4055 }
4056 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
4057 extra += 1;
4058 ok = ok && dj == input_ids[pos + 3 + j];
4059 Self::chain_probe_note(j, ok);
4060 d_prev = dj;
4061 hx = hj;
4062 }
4063 m.kv.truncate_last(extra);
4064 } else {
4065 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
4066 }
4067 }
4068 }
4069 hidden = h2;
4070 pos += 2;
4071 }
4072 }
4073 let o1_batch_ready = o1_sealed
4086 && o1_prefill.is_some()
4087 && mtp.is_none()
4088 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4089 && (0..self.num_layers).all(|li| {
4090 let cache = &self.kv_cache.layers[self.phys_layer(li)];
4091 cache.o1.is_none() || cache.o1_views().is_some()
4092 });
4093 let mtp_batch_prefill = mtp.is_some()
4098 && graph_prefill
4099 && task_mask.is_none()
4100 && !dyn_prefill
4101 && !self.o1_active()
4102 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
4103 if batch_k > 0
4104 && (graph_prefill || o1_batch_ready)
4105 && task_mask.is_none()
4106 && (!self.o1_active() || o1_batch_ready)
4107 && (mtp.is_none() || mtp_batch_prefill)
4108 && !dyn_prefill
4109 && pos + 1 < input_ids.len()
4110 {
4111 let hs = self.hidden_size;
4112 let chunk = batch_k;
4113 while pos < input_ids.len() {
4114 let end = (pos + chunk).min(input_ids.len());
4115 let bk = end - pos;
4116 let mut hiddens = vec![0f32; bk * hs];
4117 for (j, &id) in input_ids[pos..end].iter().enumerate() {
4118 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4119 }
4120 let positions: Vec<usize> = (pos..end).collect();
4121 let t_chunk = std::time::Instant::now();
4122 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
4123 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
4124 if std::env::var("CMF_GRAPH_PROF").is_ok() {
4125 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
4126 eprintln!(
4127 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
4128 if o1_batch_ready {
4129 "o1"
4130 } else if mtp_batch_prefill {
4131 "ordinary_mtp"
4132 } else {
4133 "ordinary"
4134 },
4135 bk as f64 / (ms / 1000.0)
4136 );
4137 }
4138 {
4139 use std::sync::atomic::{AtomicBool, Ordering};
4140 static SAID: AtomicBool = AtomicBool::new(false);
4141 if !SAID.swap(true, Ordering::Relaxed) {
4142 if ok_b {
4143 tracing::info!(
4144 "batched prefill: ACTIVE mode={} (k={bk})",
4145 if o1_batch_ready {
4146 "o1"
4147 } else if mtp_batch_prefill {
4148 "ordinary_mtp"
4149 } else {
4150 "ordinary"
4151 }
4152 );
4153 } else {
4154 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
4155 }
4156 }
4157 }
4158 if ok_b {
4159 if mimo_spec {
4160 self.mimo_note_rows(&hiddens, pos);
4161 }
4162 if mtp_batch_prefill {
4163 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
4164 if n_pairs > 0 {
4165 let rows: Vec<Vec<f32>> = (0..n_pairs)
4171 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
4172 .collect();
4173 let pairs: Vec<(&[f32], u32)> = rows
4174 .iter()
4175 .enumerate()
4176 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
4177 .collect();
4178 if std::env::var("CMF_GRAPH_PROF").is_ok() {
4179 eprintln!(
4180 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
4181 pos,
4182 n_pairs,
4183 pos + n_pairs - 1,
4184 );
4185 }
4186 let warm_error = if let Some(m) = mtp.as_mut() {
4187 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
4188 } else {
4189 None
4190 };
4191 if let Some(err) = warm_error {
4192 self.finish_generation(&mut mtp, &mut router, true);
4197 return Err(err.to_string());
4198 }
4199 }
4200 }
4201 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
4202 pos = end;
4203 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
4204 self.finish_generation(&mut mtp, &mut router, true);
4209 return Err(if o1_batch_ready {
4210 "sealed O(1) batch graph failed after admission".to_string()
4211 } else {
4212 "ordinary recurrent batch graph failed after admission".to_string()
4213 });
4214 } else {
4215 break; }
4217 }
4218 }
4219 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4220 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
4221 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
4222 if mimo_spec {
4223 self.mimo_note_rows(&hidden, pos);
4224 }
4225 if let Some(m) = &mut mtp {
4226 if pos + 1 < input_ids.len() {
4227 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4233 .ok()
4234 .and_then(|v| v.parse().ok())
4235 .unwrap_or(0);
4236 if probe >= 1 && pos + 2 < input_ids.len() {
4237 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
4238 let mut ok = d1 == input_ids[pos + 2];
4239 Self::chain_probe_note(0, ok);
4240 let mut d_prev = d1;
4241 let mut extra = 0usize;
4242 for j in 1..probe {
4243 if pos + 2 + j >= input_ids.len() {
4244 break;
4245 }
4246 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
4247 extra += 1;
4248 ok = ok && dj == input_ids[pos + 2 + j];
4249 Self::chain_probe_note(j, ok);
4250 d_prev = dj;
4251 hx = hj;
4252 }
4253 m.kv.truncate_last(extra);
4256 } else {
4257 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
4258 }
4259 }
4260 }
4261 pos += 1;
4262 }
4263 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4264 eprintln!(
4265 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
4266 input_ids.len(),
4267 _tpf.elapsed().as_secs_f64() * 1000.0
4268 );
4269 }
4270 if self
4271 .graph_failed
4272 .swap(false, std::sync::atomic::Ordering::Relaxed)
4273 {
4274 self.finish_generation(&mut mtp, &mut router, true);
4279 return Err("GPU token graph failed during prefill".to_string());
4280 }
4281 if self
4284 .cancel
4285 .swap(false, std::sync::atomic::Ordering::Relaxed)
4286 {
4287 self.finish_generation(&mut mtp, &mut router, true);
4291 return Ok(GenerateResult {
4292 text: String::new(),
4293 token_ids: Vec::new(),
4294 prompt_tokens: input_ids.len(),
4295 tokens_generated: 0,
4296 finish_reason: "cancelled".to_string(),
4297 mtp_drafted: 0,
4298 mtp_accepted: 0,
4299 token_confidence: Vec::new(),
4300 traces: Vec::new(),
4301 });
4302 }
4303
4304 if !o1_sealed {
4307 match self.o1_seal_checked() {
4308 Ok(_) => {}
4309 Err(err) => {
4310 self.finish_generation(&mut mtp, &mut router, true);
4311 return Err(err);
4312 }
4313 }
4314 }
4315
4316 macro_rules! commit {
4318 ($id:expr) => {{
4319 all_ids.push($id);
4320 generated += 1;
4321 self.note_draft_id($id);
4322 if self.tokenizer.is_eos($id) && !self.ignore_eos {
4323 finish_reason = "stop".to_string();
4324 false
4325 } else {
4326 let token_text = self.tokenizer.decode_token($id);
4327 let mut go = true;
4328 if let Some(ref mut cb) = on_token {
4329 if !cb(&token_text) {
4330 finish_reason = "cancelled".to_string();
4331 go = false;
4332 }
4333 }
4334 go
4335 }
4336 }};
4337 }
4338
4339 let mut spec_trial = SpecTrial::Spec {
4350 t0: std::time::Instant::now(),
4351 gen0: generated,
4352 rounds: 0,
4353 };
4354 let mut spec_mon = SpecMon {
4360 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
4361 ..SpecMon::default()
4362 };
4363 let mut spec_watchdog_off = false;
4364 let mut spec_walls: Vec<f32> = Vec::new();
4367 let mut spec_round_end: Option<std::time::Instant> = None;
4370 if mimo_spec {
4371 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
4372 if let Some(mut st) = self.mimo_mtp.take() {
4373 self.mimo_mtp_probe(&mut st, input_ids, &path);
4374 self.mimo_mtp = Some(st);
4375 }
4376 }
4377 }
4378 let mut next_pos = input_ids.len();
4380 'decode: while generated < max_tokens {
4381 if self
4382 .graph_failed
4383 .swap(false, std::sync::atomic::Ordering::Relaxed)
4384 {
4385 self.finish_generation(&mut mtp, &mut router, true);
4390 return Err("GPU token graph failed during decode".to_string());
4391 }
4392 if self
4393 .cancel
4394 .swap(false, std::sync::atomic::Ordering::Relaxed)
4395 {
4396 finish_reason = "cancelled".to_string();
4397 break 'decode;
4398 }
4399 if mimo_spec && next_pos > 0 {
4404 self.mimo_note_rows(&hidden, next_pos - 1);
4407 }
4408 let forced = self.spec_forced.take();
4409 let mut logits = match (forced, self.graph_logits.take()) {
4410 (Some(_), _) => Vec::new(),
4411 (None, Some(lg)) => lg,
4412 (None, None) => {
4413 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
4414 inference::rms_norm_into(
4415 &hidden,
4416 &self.weights.final_norm,
4417 self.rms_eps,
4418 self.norm_style,
4419 &mut self.ws.n1,
4420 );
4421 self.lm_head_forward(&self.ws.n1)
4422 }
4423 };
4424 if generated
4427 == std::env::var("CMF_LOGIT_DUMP_STEP")
4428 .ok()
4429 .and_then(|v| v.parse().ok())
4430 .unwrap_or(0)
4431 {
4432 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
4433 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
4434 for v in hidden.iter().chain(logits.iter()) {
4435 bytes.extend_from_slice(&v.to_le_bytes());
4436 }
4437 if let Err(e) = std::fs::write(&path, &bytes) {
4438 eprintln!("logit dump: failed to write {path}: {e}");
4439 self.finish_generation(&mut mtp, &mut router, true);
4440 return Err(format!("logit dump write failed: {e}"));
4441 }
4442 }
4443 }
4444 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
4448 if !logits.is_empty() {
4449 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
4450 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
4451 if let Err(e) =
4452 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
4453 {
4454 eprintln!("logit dump: failed to write {}: {e}", path.display());
4455 }
4456 }
4457 }
4458 let t_next = match forced {
4459 Some(c) => c,
4460 None => {
4461 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
4462 sampler::sample_with_scratch_pool(
4463 &logits,
4464 &self.sampler_config,
4465 &all_ids,
4466 &mut self.rng,
4467 &mut self.sampler_scratch,
4468 self.pool.as_deref(),
4469 )
4470 }
4471 };
4472 if self.confidence_on {
4473 confidence.push(if logits.is_empty() {
4474 0.0
4475 } else {
4476 sampler::top1_prob_pool(
4477 self.pool.as_deref(),
4478 &mut self.sampler_scratch,
4479 &logits,
4480 t_next,
4481 calib_temp,
4482 )
4483 });
4484 }
4485 if !logits.is_empty() {
4486 attention::recycle_buf(&mut logits);
4487 }
4488 if trace_on {
4489 let skill = router.as_ref().and_then(|r| r.active_id());
4493 traces.push(TokenTrace {
4494 t: generated,
4495 token_id: t_next,
4496 confidence: confidence.last().copied().unwrap_or(0.0),
4497 active_skill: skill,
4498 recon: None,
4499 switched: false,
4500 });
4501 }
4502 if !commit!(t_next) {
4503 break 'decode;
4504 }
4505 if generated >= max_tokens {
4506 break 'decode;
4507 }
4508
4509 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
4510 static SAID: std::sync::Once = std::sync::Once::new();
4516 SAID.call_once(|| {
4517 tracing::warn!(
4518 "KV cache full at {} positions — evicting half; quality \
4519 will degrade. Raise CMF_MAX_SEQ.",
4520 self.kv_cache.max_seq_len,
4521 );
4522 });
4523 let keep = (self.kv_cache.max_seq_len / 2).max(1);
4524 self.kv_cache.evict(keep);
4525 }
4526
4527 if graph_spec {
4530 match spec_trial {
4531 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
4532 spec_mon.plain_ms =
4533 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
4534 let keep = spec_mon.pays();
4535 tracing::info!(
4536 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4537 spec_mon.tokens,
4538 spec_mon.round_ms,
4539 spec_mon.plain_ms,
4540 if keep { "speculating" } else { "plain" }
4541 );
4542 spec_mon.fails = 0;
4543 spec_trial = SpecTrial::Decided {
4544 spec: keep,
4545 recheck_at: if keep { usize::MAX } else { generated + 128 },
4546 };
4547 }
4548 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
4549 spec_mon.n = 0;
4550 spec_trial = SpecTrial::Spec {
4551 t0: std::time::Instant::now(),
4552 gen0: generated,
4553 rounds: 0,
4554 };
4555 }
4556 _ => {}
4557 }
4558 spec_watchdog_off = matches!(
4559 spec_trial,
4560 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
4561 );
4562 }
4563 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
4565 let budget = max_tokens - generated - 1;
4566 if let Some(mut st) = self.mimo_mtp.take() {
4567 let k = st.depth.min(budget);
4568 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
4569 self.mimo_mtp = Some(st);
4570 let r = match r {
4571 Ok(r) => r,
4572 Err(err) => {
4573 self.finish_generation(&mut mtp, &mut router, true);
4574 return Err(err);
4575 }
4576 };
4577 if let Some(r) = r {
4578 drafted += r.drafted;
4579 accepted += r.accepted.len();
4580 let mut stopped = false;
4581 for &id in &r.accepted {
4582 if self.confidence_on {
4583 confidence.push(0.0);
4584 }
4585 if !commit!(id) {
4586 stopped = true;
4587 break;
4588 }
4589 }
4590 if stopped {
4591 break 'decode;
4592 }
4593 next_pos += r.accepted.len() + 1;
4594 hidden = r.hidden;
4595 self.graph_logits = Some(r.logits);
4598 continue 'decode;
4599 }
4600 }
4601 }
4602 match &mut mtp {
4603 #[cfg(feature = "gpu")]
4605 Some(m)
4606 if graph_spec
4607 && !spec_watchdog_off
4608 && generated + 1 < max_tokens
4609 && next_pos > 0 =>
4610 {
4611 let t_round = std::time::Instant::now();
4612 if spec_time_level() >= 2 {
4613 if let Some(t) = spec_round_end.take() {
4614 eprintln!(
4615 "spec-gap {:.2} ms (host between rounds)",
4616 t.elapsed().as_secs_f64() * 1e3
4617 );
4618 }
4619 }
4620 spec_stamps_begin();
4621 #[cfg(target_os = "macos")]
4626 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
4627 .load(std::sync::atomic::Ordering::Relaxed);
4628 #[cfg(not(target_os = "macos"))]
4629 let allocs0 = 0u64;
4630 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
4631 m,
4632 &hidden,
4633 t_next,
4634 next_pos,
4635 &mut drafted,
4636 &mut accepted,
4637 &mut all_ids,
4638 max_tokens - generated,
4639 ) {
4640 next_pos = n_pos;
4641 hidden = new_h;
4642 let level = spec_time_level();
4643 if level > 0 {
4644 let wall = t_round.elapsed().as_secs_f32() * 1e3;
4645 let stamps = spec_stamps_take();
4646 let median = if spec_walls.len() >= 3 {
4649 let mut s = spec_walls.clone();
4650 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
4651 Some(s[s.len() / 2])
4652 } else {
4653 None
4654 };
4655 let outlier = median.is_some_and(|m| wall > 1.4 * m);
4656 #[cfg(target_os = "macos")]
4657 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
4658 .load(std::sync::atomic::Ordering::Relaxed)
4659 - allocs0;
4660 #[cfg(not(target_os = "macos"))]
4661 let allocs = allocs0;
4662 eprintln!(
4663 "spec-round wall {wall:.1} ms → {} tokens{}{}",
4664 extra.len() + 1,
4665 if allocs > 0 {
4666 format!(" [{allocs} new device buffers]")
4667 } else {
4668 String::new()
4669 },
4670 match (outlier, median) {
4671 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
4672 _ => String::new(),
4673 }
4674 );
4675 if level >= 2 || outlier {
4676 let sum: f32 = stamps.iter().map(|s| s.1).sum();
4677 eprintln!(
4678 "spec-stamps: {}| untracked {:.1}",
4679 spec_stamps_format(&stamps),
4680 wall - sum
4681 );
4682 }
4683 if spec_mon.n >= 1 {
4684 spec_walls.push(wall);
4685 }
4686 }
4687 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
4691 spec_trial = Self::spec_trial_round(
4694 spec_trial,
4695 &mut spec_mon,
4696 generated + extra.len() + 1,
4697 );
4698 let mut stopped = false;
4699 for &id in &extra {
4700 if self.confidence_on {
4701 confidence.push(0.0);
4702 }
4703 if !commit!(id) {
4704 stopped = true;
4705 break;
4706 }
4707 }
4708 if stopped {
4709 break 'decode;
4710 }
4711 if spec_time_level() >= 2 {
4712 spec_round_end = Some(std::time::Instant::now());
4713 }
4714 continue 'decode;
4715 }
4716 if self
4717 .graph_failed
4718 .swap(false, std::sync::atomic::Ordering::Relaxed)
4719 {
4720 self.finish_generation(&mut mtp, &mut router, true);
4726 return Err("GPU MTP graph failed during speculative decode".to_string());
4727 }
4728 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
4739 spec_mon.tokens = 0.0;
4740 spec_mon.fails = 3;
4741 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
4742 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
4743 next_pos += 1;
4744 continue 'decode;
4745 }
4746 Some(m) if !graph_spec && generated + 1 < max_tokens => {
4748 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
4749 drafted += 1;
4750 let emb1 = self.embed_single(t_next);
4751 let emb2 = self.embed_single(draft);
4752 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
4753
4754 inference::rms_norm_into(
4755 &h1,
4756 &self.weights.final_norm,
4757 self.rms_eps,
4758 self.norm_style,
4759 &mut self.ws.n1,
4760 );
4761 let mut logits1 = self.lm_head_forward(&self.ws.n1);
4762 let t_after = sampler::sample_with_scratch_pool(
4763 &logits1,
4764 &self.sampler_config,
4765 &all_ids,
4766 &mut self.rng,
4767 &mut self.sampler_scratch,
4768 self.pool.as_deref(),
4769 );
4770 if self.confidence_on {
4771 confidence.push(sampler::top1_prob_pool(
4772 self.pool.as_deref(),
4773 &mut self.sampler_scratch,
4774 &logits1,
4775 t_after,
4776 calib_temp,
4777 ));
4778 }
4779 attention::recycle_buf(&mut logits1);
4780 if trace_on {
4781 traces.push(TokenTrace {
4784 t: generated,
4785 token_id: t_after,
4786 confidence: confidence.last().copied().unwrap_or(0.0),
4787 active_skill: None,
4788 recon: None,
4789 switched: false,
4790 });
4791 }
4792 let stop = !commit!(t_after);
4793
4794 if t_after == draft {
4795 accepted += 1;
4796 self.commit_linear_scratch();
4797 let _ = self.mtp_step(m, &h1, t_after, next_pos);
4798 hidden = h2;
4799 next_pos += 2;
4800 } else {
4801 for layer in &mut self.kv_cache.layers {
4803 layer.truncate_last(1);
4804 }
4805 if !stop {
4806 let _ = self.mtp_step(m, &h1, t_after, next_pos);
4807 hidden = self.forward_layers(
4808 &self.embed_single(t_after),
4809 next_pos + 1,
4810 None,
4811 );
4812 }
4813 next_pos += 2;
4814 }
4815 if stop {
4816 break 'decode;
4817 }
4818 }
4819 _ => {
4821 #[cfg(feature = "gpu")]
4826 if Self::dsv4_spec_on() && self.dsv4.is_some() {
4827 static SAID: std::sync::Once = std::sync::Once::new();
4828 SAID.call_once(|| {
4829 eprintln!(
4830 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
4831 !self.dsv4_mtp.is_empty(),
4832 task_mask.is_none(),
4833 router.is_none(),
4834 !trace_on,
4835 self.sampler_config.temperature < 1e-6,
4836 self.sampler_config.repetition_penalty == 1.0,
4837 );
4838 });
4839 }
4840 #[cfg(feature = "gpu")]
4841 if Self::dsv4_spec_on()
4842 && self.dsv4.is_some()
4843 && !self.dsv4_mtp.is_empty()
4844 && task_mask.is_none()
4845 && router.is_none()
4846 && !trace_on
4847 && self.sampler_config.temperature < 1e-6
4848 && self.sampler_config.repetition_penalty == 1.0
4849 && generated + 1 < max_tokens
4850 && all_ids.len() >= 2
4851 && generated >= dsv4_spec_retry_at
4852 {
4853 let tip_token = all_ids[all_ids.len() - 2];
4854 let drafted0 = drafted;
4855 let round = self.dsv4_spec_step(
4856 tip_token,
4857 t_next,
4858 next_pos,
4859 max_tokens.saturating_sub(generated),
4860 &mut drafted,
4861 &mut accepted,
4862 );
4863 if drafted > drafted0 {
4864 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
4865 if useful {
4866 dsv4_spec_bad = 0;
4867 } else {
4868 dsv4_spec_bad += 1;
4869 if dsv4_spec_bad >= 2 {
4870 dsv4_spec_bad = 0;
4871 dsv4_spec_retry_at = generated.saturating_add(32);
4872 tracing::info!(
4873 "dsv4: draft не окупился дважды — точный walk на 32 токена"
4874 );
4875 }
4876 }
4877 }
4878 if let Some((extra, n_pos)) = round {
4879 next_pos = n_pos;
4880 let mut stopped = false;
4881 for &id in &extra {
4882 if self.confidence_on {
4883 confidence.push(0.0);
4884 }
4885 if !commit!(id) {
4886 stopped = true;
4887 break;
4888 }
4889 }
4890 if stopped {
4891 break 'decode;
4892 }
4893 continue 'decode;
4894 }
4895 }
4896 self.graph_want_logits = fuse_lm;
4897 let mut t_fwd = t_next;
4903 let pure_greedy = self.sampler_config.temperature < 1e-6
4904 && self.sampler_config.repetition_penalty == 1.0
4905 && self.sampler_config.suppress_tokens.is_empty();
4906 let burst_k = std::env::var("CMF_MULTISTEP")
4911 .ok()
4912 .and_then(|v| v.parse::<usize>().ok())
4913 .unwrap_or(0);
4914 if pure_greedy
4915 && burst_k >= 1
4916 && fuse_lm
4917 && task_mask.is_none()
4918 && router.is_none()
4919 && !trace_on
4920 && !self.confidence_on
4921 {
4922 let mut stopped = false;
4923 loop {
4924 let room = max_tokens.saturating_sub(generated);
4925 if room <= 2 {
4926 break;
4927 }
4928 let k = burst_k.min(room - 1);
4929 if k < 1 {
4930 break;
4931 }
4932 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
4933 if self
4934 .graph_failed
4935 .swap(false, std::sync::atomic::Ordering::Relaxed)
4936 {
4937 self.finish_generation(&mut mtp, &mut router, true);
4938 return Err(
4939 "GPU token graph failed during greedy burst".to_string()
4940 );
4941 }
4942 break;
4943 };
4944 next_pos += k;
4945 for &id in &ids {
4946 if !commit!(id) {
4947 stopped = true;
4948 break;
4949 }
4950 }
4951 if stopped {
4952 break;
4953 }
4954 t_fwd = *ids.last().unwrap();
4955 }
4956 if stopped {
4957 break 'decode;
4958 }
4959 }
4960 #[cfg(target_os = "macos")]
4970 if graph_spec
4971 && spec_watchdog_off
4972 && next_pos > 0
4973 && self.mtp_graph_mode == Some(true)
4974 && crate::gpu::q1_force()
4975 {
4976 if let Some(m) = mtp.as_mut() {
4977 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
4978 }
4979 }
4980 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
4981 next_pos += 1;
4982 if let Some(r) = &mut router {
4985 let phi = self.dyn_phi_ema.clone();
4986 let decision = r.step(&phi, generated);
4987 if let Some(new_active) = decision {
4988 let _ = self.set_active_skill(new_active);
4989 }
4990 if trace_on {
4993 if let Some(last) = traces.last_mut() {
4994 let e = r.last_best_e();
4995 last.recon = e.is_finite().then_some(e);
4996 last.switched = decision.is_some();
4997 }
4998 }
4999 }
5000 }
5001 }
5002 }
5003
5004 let cancelled = finish_reason == "cancelled";
5005 if mimo_spec {
5006 if let Some(st) = self.mimo_mtp.as_ref() {
5007 let line = st.stats.line();
5008 tracing::info!("{line}");
5009 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
5010 eprintln!("{line}");
5011 }
5012 }
5013 }
5014 self.finish_generation(&mut mtp, &mut router, cancelled);
5015
5016 let output_ids = &all_ids[input_ids.len()..];
5017 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
5021 if cancelled || mimo_spec || prompt_rows.is_some() {
5025 self.kv_history.clear();
5026 } else {
5027 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
5028 }
5029 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
5031 Ok(GenerateResult {
5032 text: self.tokenizer.decode(output_ids),
5033 token_ids: output_ids.to_vec(),
5034 prompt_tokens: input_ids.len(),
5035 tokens_generated: generated,
5036 finish_reason,
5037 mtp_drafted: drafted,
5038 mtp_accepted: accepted,
5039 token_confidence: confidence,
5040 traces,
5041 })
5042 }
5043
5044 fn mtp_step(
5048 &mut self,
5049 m: &mut MtpModule,
5050 hidden: &[f32],
5051 next_token: u32,
5052 position: usize,
5053 ) -> u32 {
5054 self.mtp_step_h(m, hidden, next_token, position).0
5055 }
5056
5057 fn chain_probe_note(depth: usize, prefix_ok: bool) {
5061 use std::sync::Mutex;
5062 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
5063 let mut t = T.lock().unwrap();
5064 if t.len() <= depth {
5065 t.resize(depth + 1, (0, 0));
5066 }
5067 t[depth].0 += 1;
5068 t[depth].1 += prefix_ok as u64;
5069 if depth == 0 && t[0].0 % 128 == 0 {
5070 let line: Vec<String> = t
5071 .iter()
5072 .enumerate()
5073 .map(|(d, (n, k))| {
5074 format!(
5075 "d{}={:.0}%({n})",
5076 d + 1,
5077 100.0 * *k as f64 / (*n).max(1) as f64
5078 )
5079 })
5080 .collect();
5081 eprintln!("mtp-chain: {}", line.join(" "));
5082 }
5083 }
5084
5085 fn mtp_step_hl(
5093 &mut self,
5094 m: &mut MtpModule,
5095 hidden: &[f32],
5096 next_token: u32,
5097 position: usize,
5098 ) -> (Vec<f32>, Vec<f32>) {
5099 #[cfg(target_os = "macos")]
5104 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
5105 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
5106 self.mtp_graph_mode = Some(true);
5107 return r;
5108 }
5109 if self.mtp_graph_mode == Some(true) {
5110 tracing::error!("mtp Metal graph failed after admission");
5111 self.clear_sequence_state();
5112 self.graph_failed
5113 .store(true, std::sync::atomic::Ordering::Relaxed);
5114 self.cancel
5115 .store(true, std::sync::atomic::Ordering::Relaxed);
5116 return (Vec::new(), Vec::new());
5117 }
5118 self.mtp_graph_mode = Some(false);
5119 }
5120 #[cfg(feature = "gpu")]
5121 if self.mtp_graph_mode != Some(false) {
5122 if !self.mtp_graph_ok(m) {
5123 if self.mtp_graph_mode == Some(true) {
5124 tracing::error!("mtp graph became unavailable after admission");
5129 self.clear_sequence_state();
5130 self.graph_failed
5131 .store(true, std::sync::atomic::Ordering::Relaxed);
5132 self.cancel
5133 .store(true, std::sync::atomic::Ordering::Relaxed);
5134 return (Vec::new(), Vec::new());
5135 }
5136 self.mtp_graph_mode = Some(false);
5137 } else {
5138 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
5139 self.mtp_graph_mode = Some(true);
5140 return r;
5141 }
5142 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
5143 return (Vec::new(), Vec::new());
5150 }
5151 tracing::error!("mtp graph failed or declined after admission");
5155 self.clear_sequence_state();
5156 self.graph_failed
5157 .store(true, std::sync::atomic::Ordering::Relaxed);
5158 self.cancel
5159 .store(true, std::sync::atomic::Ordering::Relaxed);
5160 return (Vec::new(), Vec::new());
5161 }
5162 }
5163 let e = self.embed_single(next_token);
5167 let mut cat = vec![0.0f32; 2 * self.hidden_size];
5168 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5169 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5170 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5171 let mut x = vec![0.0f32; self.hidden_size];
5172 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5173
5174 let lw = &m.layer;
5176 inference::rms_norm_into(
5177 &x,
5178 &lw.input_norm,
5179 self.rms_eps,
5180 self.norm_style,
5181 &mut self.ws.n1,
5182 );
5183 let attn = match &lw.attn {
5184 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5186 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5187 AttnKind::Full {
5188 wq,
5189 wk,
5190 wv,
5191 wo,
5192 q_norm,
5193 k_norm,
5194 output_gate,
5195 softplus_gate,
5196 bias,
5197 } => {
5198 let mut cfg = self.attn_cfg(position);
5199 cfg.q_norm = q_norm.as_deref();
5200 cfg.k_norm = k_norm.as_deref();
5201 cfg.output_gate = *output_gate;
5202 cfg.softplus_gate = softplus_gate
5203 .as_ref()
5204 .map(|(gate, per_head)| (gate, *per_head));
5205 cfg.bias = bias
5206 .as_ref()
5207 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
5208 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
5209 }
5210 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
5211 unreachable!("MTP block is full attention")
5212 }
5213 };
5214 for (i, &a) in attn.iter().enumerate() {
5215 x[i] += a;
5216 }
5217 inference::rms_norm_into(
5218 &x,
5219 &lw.post_norm,
5220 self.rms_eps,
5221 self.norm_style,
5222 &mut self.ws.p1,
5223 );
5224 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
5225 for (i, &f) in ffn.iter().enumerate() {
5226 x[i] += f;
5227 }
5228
5229 inference::rms_norm_into(
5230 &x,
5231 &m.final_norm,
5232 self.rms_eps,
5233 self.norm_style,
5234 &mut self.ws.n1,
5235 );
5236 let lg = self.lm_head_forward(&self.ws.n1);
5237 (lg, x)
5238 }
5239
5240 fn mtp_step_h(
5242 &mut self,
5243 m: &mut MtpModule,
5244 hidden: &[f32],
5245 next_token: u32,
5246 position: usize,
5247 ) -> (u32, Vec<f32>) {
5248 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
5249 let draft = sampler::argmax(&lg);
5250 attention::recycle_buf(&mut lg);
5251 (draft, x)
5252 }
5253
5254 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
5260 match trial {
5261 SpecTrial::Spec { t0, gen0, rounds } => {
5262 let rounds = rounds + 1;
5263 if rounds >= 5 {
5264 if mon.plain_ms > 0.0 {
5265 let keep = mon.pays();
5266 mon.fails = 0;
5267 tracing::info!(
5268 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5269 mon.tokens,
5270 mon.round_ms,
5271 mon.plain_ms,
5272 if keep { "speculating" } else { "plain" }
5273 );
5274 SpecTrial::Decided {
5275 spec: keep,
5276 recheck_at: if keep { usize::MAX } else { generated + 128 },
5277 }
5278 } else if mon.pays() {
5279 mon.fails = 0;
5284 tracing::info!(
5285 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
5286 mon.tokens,
5287 mon.round_ms,
5288 );
5289 SpecTrial::Decided {
5290 spec: true,
5291 recheck_at: usize::MAX,
5292 }
5293 } else {
5294 SpecTrial::Plain {
5295 t0: std::time::Instant::now(),
5296 gen0: generated,
5297 }
5298 }
5299 } else {
5300 SpecTrial::Spec { t0, gen0, rounds }
5301 }
5302 }
5303 SpecTrial::Decided { spec: true, .. } => {
5304 if mon.pays() {
5305 mon.fails = 0;
5306 trial
5307 } else {
5308 mon.fails += 1;
5309 if mon.fails >= 4 {
5310 if mon.plain_ms <= 0.0 {
5311 tracing::info!(
5315 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
5316 mon.tokens,
5317 mon.round_ms,
5318 );
5319 return SpecTrial::Plain {
5320 t0: std::time::Instant::now(),
5321 gen0: generated,
5322 };
5323 }
5324 tracing::info!(
5325 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
5326 mon.tokens,
5327 mon.round_ms,
5328 mon.plain_ms
5329 );
5330 SpecTrial::Decided {
5331 spec: false,
5332 recheck_at: generated + 128,
5333 }
5334 } else {
5335 trial
5336 }
5337 }
5338 }
5339 other => other,
5340 }
5341 }
5342
5343 fn mtp_kv_id(&self) -> u64 {
5346 self.graph_kv_id | (1u64 << 40)
5347 }
5348
5349 const MTP_LAYER_BASE: usize = 0;
5354
5355 #[cfg(feature = "gpu")]
5362 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
5363 self.mtp_graph_mode != Some(true)
5364 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
5365 }
5366
5367 #[cfg(feature = "gpu")]
5373 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
5374 let mut ok = true;
5375 let mut expected = false;
5376 for li in 0..self.num_layers {
5377 if matches!(
5378 self.weights.layers[self.phys_layer(li)].attn,
5379 AttnKind::Full { .. }
5380 ) {
5381 expected = true;
5382 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
5383 }
5384 }
5385 !expected || ok
5386 }
5387
5388 fn graph_gdn_layer_count(&self) -> usize {
5392 (0..self.num_layers)
5393 .filter(|&li| {
5394 matches!(
5395 &self.weights.layers[self.phys_layer(li)].attn,
5396 AttnKind::LinearGdn(_)
5397 )
5398 })
5399 .count()
5400 }
5401
5402 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
5405 let e = self.embed_single(next_token);
5406 let mut cat = vec![0.0f32; 2 * self.hidden_size];
5407 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5408 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5409 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5410 let mut x = vec![0.0f32; self.hidden_size];
5411 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5412 x
5413 }
5414
5415 #[cfg(feature = "gpu")]
5418 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
5419 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
5420 return false;
5421 }
5422 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
5423 || !crate::gpu::enabled_here()
5424 || self.attn_softcap > 0.0
5425 || self.attention_heads_per_layer.is_some()
5426 || self.v_head_dim.is_some()
5429 {
5430 return false;
5431 }
5432 matches!(
5433 &m.layer.attn,
5434 AttnKind::Full {
5435 softplus_gate: None,
5436 ..
5437 }
5438 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
5439 }
5440
5441 #[cfg(feature = "gpu")]
5445 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
5446 if !self.mtp_block_graph_ok(m) {
5447 return false;
5448 }
5449 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
5450 return false;
5451 };
5452 let FfnKind::Dense(d) = &m.layer.ffn else {
5453 return false;
5454 };
5455 d.segs.is_empty()
5456 && wq.graph_weight().is_some()
5457 && wk.graph_weight().is_some()
5458 && wv.graph_weight().is_some()
5459 && wo.graph_weight().is_some()
5460 && d.gate_proj.graph_weight().is_some()
5461 && d.up_proj.graph_weight().is_some()
5462 && d.down_proj.graph_weight().is_some()
5463 && self.weights.lm_head.graph_weight().is_some()
5464 }
5465
5466 #[cfg(feature = "gpu")]
5472 fn mtp_step_graph(
5473 &mut self,
5474 m: &mut MtpModule,
5475 hidden: &[f32],
5476 next_token: u32,
5477 position: usize,
5478 ) -> Option<(Vec<f32>, Vec<f32>)> {
5479 if !self.mtp_graph_ok(m) {
5480 return None;
5481 }
5482 let lw = &m.layer;
5483 let AttnKind::Full {
5484 wq,
5485 wk,
5486 wv,
5487 wo,
5488 q_norm,
5489 k_norm,
5490 output_gate,
5491 softplus_gate,
5492 bias,
5493 } = &lw.attn
5494 else {
5495 return None;
5496 };
5497 if softplus_gate.is_some() {
5498 return None;
5499 }
5500 let FfnKind::Dense(d) = &lw.ffn else {
5501 return None;
5502 };
5503 if !d.segs.is_empty() {
5504 return None; }
5506 let mut x = self.mtp_block_input(m, hidden, next_token);
5509 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5510 let (_, i, kind, rs) = t.graph_weight()?;
5511 Some(crate::gpu::GraphW {
5512 idx: i,
5513 kind,
5514 row_scale: rs,
5515 data: &[],
5516 prism: crate::gpu::GraphPrismOp::None,
5517 affine: false,
5518 })
5519 }
5520 let (model, _, _, _) = wq.graph_weight()?;
5521 let model = model.clone();
5522 let (lm_gw, lm_rows) = {
5523 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
5524 let rows = if kind == 6 {
5528 self.draft_head_rows(self.weights.lm_head.rows())
5529 } else {
5530 self.weights.lm_head.rows()
5531 };
5532 (
5533 crate::gpu::GraphW {
5534 idx: i,
5535 kind,
5536 row_scale: rs,
5537 data: &[],
5538 prism: crate::gpu::GraphPrismOp::None,
5539 affine: false,
5540 },
5541 rows,
5542 )
5543 };
5544 let layer = crate::gpu::GraphLayer {
5545 input_norm: &lw.input_norm,
5546 attn: crate::gpu::GraphAttn::Full {
5547 wq: gw(wq)?,
5548 wk: gw(wk)?,
5549 wv: gw(wv)?,
5550 wo: gw(wo)?,
5551 q_norm: q_norm.as_deref(),
5552 k_norm: k_norm.as_deref(),
5553 late_qk_norm: self.qk_norm_after_rope,
5554 bias: bias
5555 .as_ref()
5556 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5557 output_gate: *output_gate,
5558 cpu_k: m.kv.k_heads(),
5559 cpu_v: m.kv.v_heads(),
5560 geom: None,
5561 },
5562 post_norm: &lw.post_norm,
5563 ffn: crate::gpu::GraphFfn::Dense {
5564 gate: gw(&d.gate_proj)?,
5565 up: gw(&d.up_proj)?,
5566 down: gw(&d.down_proj)?,
5567 },
5568 };
5569 let nh = self.num_heads;
5570 let (nkv, hd, rd) = self.layer_geom(0);
5571 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5572 let mut logits = Vec::new();
5573 let ok = crate::gpu::forward_token_graph(
5574 &model,
5575 self.mtp_kv_id(),
5576 std::slice::from_ref(&layer),
5577 &[None],
5578 self.o1_epoch,
5579 &self.inv_freq,
5580 &mut x,
5581 nh,
5582 nkv,
5583 hd,
5584 self.attn_scale,
5585 rd,
5586 self.hidden_size,
5587 self.intermediate_size,
5588 position,
5589 self.kv_cache.max_seq_len,
5590 gemma,
5591 self.rms_eps as f32,
5592 Some((&lm_gw, lm_rows)),
5593 &m.final_norm,
5594 &mut logits,
5595 &[],
5596 1,
5597 None,
5598 None,
5599 None,
5600 Self::MTP_LAYER_BASE,
5601 true,
5602 );
5603 match ok {
5604 crate::gpu::TokenGraphOutcome::Completed => {}
5605 crate::gpu::TokenGraphOutcome::Declined => return None,
5606 crate::gpu::TokenGraphOutcome::Failed => {
5607 self.clear_sequence_state();
5611 self.graph_failed
5612 .store(true, std::sync::atomic::Ordering::Relaxed);
5613 self.cancel
5614 .store(true, std::sync::atomic::Ordering::Relaxed);
5615 return None;
5616 }
5617 }
5618 logits.resize(self.vocab_size, 0.0);
5619 Some((logits, x))
5620 }
5621
5622 #[cfg(feature = "gpu")]
5630 fn mtp_warm_graph(
5631 &mut self,
5632 m: &mut MtpModule,
5633 pairs: &[(&[f32], u32)],
5634 first_pos: usize,
5635 ) -> crate::gpu::BatchGraphOutcome {
5636 if pairs.is_empty() {
5637 return crate::gpu::BatchGraphOutcome::Completed;
5638 }
5639 if !self.mtp_block_graph_ok(m) {
5640 return crate::gpu::BatchGraphOutcome::Declined;
5641 }
5642 let hs = self.hidden_size;
5643 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
5646 for (h, t) in pairs {
5647 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
5648 }
5649 let lw = &m.layer;
5650 let AttnKind::Full {
5651 wq,
5652 wk,
5653 wv,
5654 wo,
5655 q_norm,
5656 k_norm,
5657 output_gate,
5658 bias,
5659 ..
5660 } = &lw.attn
5661 else {
5662 return crate::gpu::BatchGraphOutcome::Declined;
5663 };
5664 let FfnKind::Dense(d) = &lw.ffn else {
5665 return crate::gpu::BatchGraphOutcome::Declined;
5666 };
5667 if !d.segs.is_empty() {
5668 return crate::gpu::BatchGraphOutcome::Declined; }
5670 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5671 let (_, i, kind, rs) = t.graph_weight()?;
5672 Some(crate::gpu::GraphW {
5673 idx: i,
5674 kind,
5675 row_scale: rs,
5676 data: &[],
5677 prism: crate::gpu::GraphPrismOp::None,
5678 affine: false,
5679 })
5680 }
5681 let Some((model, _, _, _)) = wq.graph_weight() else {
5682 return crate::gpu::BatchGraphOutcome::Declined;
5683 };
5684 let model = model.clone();
5685 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
5686 gw(wq),
5687 gw(wk),
5688 gw(wv),
5689 gw(wo),
5690 gw(&d.gate_proj),
5691 gw(&d.up_proj),
5692 gw(&d.down_proj),
5693 ) else {
5694 return crate::gpu::BatchGraphOutcome::Declined;
5695 };
5696 let layer = crate::gpu::GraphLayer {
5697 input_norm: &lw.input_norm,
5698 attn: crate::gpu::GraphAttn::Full {
5699 wq: gwq,
5700 wk: gwk,
5701 wv: gwv,
5702 wo: gwo,
5703 q_norm: q_norm.as_deref(),
5704 k_norm: k_norm.as_deref(),
5705 late_qk_norm: self.qk_norm_after_rope,
5706 bias: bias
5707 .as_ref()
5708 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5709 output_gate: *output_gate,
5710 cpu_k: m.kv.k_heads(),
5711 cpu_v: m.kv.v_heads(),
5712 geom: None,
5713 },
5714 post_norm: &lw.post_norm,
5715 ffn: crate::gpu::GraphFfn::Dense {
5716 gate: gg,
5717 up: gu,
5718 down: gd,
5719 },
5720 };
5721 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
5722 let nh = self.num_heads;
5723 let (nkv, hd, rd) = self.layer_geom(0);
5724 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5725 crate::gpu::forward_batch_graph(
5726 &model,
5727 self.mtp_kv_id(),
5728 std::slice::from_ref(&layer),
5729 &self.inv_freq,
5730 &mut hiddens,
5731 nh,
5732 nkv,
5733 hd,
5734 rd,
5735 hs,
5736 self.intermediate_size,
5737 &positions,
5738 self.kv_cache.max_seq_len,
5739 gemma,
5740 self.rms_eps as f32,
5741 self.attn_scale,
5742 pairs.len(),
5743 &[],
5744 0,
5745 None,
5746 None,
5747 )
5748 }
5749
5750 #[cfg(feature = "gpu")]
5757 fn mtp_warm_graph_fallback(
5758 &mut self,
5759 m: &mut MtpModule,
5760 pairs: &[(&[f32], u32)],
5761 first_pos: usize,
5762 ) -> bool {
5763 if pairs.is_empty() {
5764 return true;
5765 }
5766 let graphable = self.mtp_block_graph_ok(m);
5767 if !graphable {
5768 if self.mtp_graph_mode == Some(true) {
5772 return false;
5773 }
5774 self.mtp_graph_mode = Some(false);
5775 for (j, (h, t)) in pairs.iter().enumerate() {
5776 self.mtp_warm(m, h, *t, first_pos + j);
5777 }
5778 return true;
5779 }
5780
5781 for (j, (h, t)) in pairs.iter().enumerate() {
5786 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
5787 return false;
5788 }
5789 }
5790 self.mtp_graph_mode = Some(true);
5791 true
5792 }
5793
5794 #[cfg(feature = "gpu")]
5799 fn mtp_warm_prefill_pairs(
5800 &mut self,
5801 m: &mut MtpModule,
5802 pairs: &[(&[f32], u32)],
5803 first_pos: usize,
5804 ) -> Result<(), &'static str> {
5805 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
5810 if self.mtp_graph_mode == Some(true) {
5811 return Err("MTP token graph became unavailable after admission");
5812 }
5813 self.mtp_graph_mode = Some(false);
5814 for (j, (h, t)) in pairs.iter().enumerate() {
5815 self.mtp_warm(m, h, *t, first_pos + j);
5816 }
5817 return Ok(());
5818 }
5819 match self.mtp_warm_graph(m, pairs, first_pos) {
5820 crate::gpu::BatchGraphOutcome::Completed => {
5821 if !pairs.is_empty() {
5822 self.mtp_graph_mode = Some(true);
5823 }
5824 Ok(())
5825 }
5826 crate::gpu::BatchGraphOutcome::Declined => {
5827 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
5828 Ok(())
5829 } else {
5830 Err("MTP warm-up fallback failed after device admission")
5831 }
5832 }
5833 crate::gpu::BatchGraphOutcome::Failed => {
5834 Err("MTP warm batch graph failed after admission")
5835 }
5836 }
5837 }
5838
5839 #[cfg(not(feature = "gpu"))]
5840 fn mtp_warm_prefill_pairs(
5841 &mut self,
5842 m: &mut MtpModule,
5843 pairs: &[(&[f32], u32)],
5844 first_pos: usize,
5845 ) -> Result<(), &'static str> {
5846 for (j, (h, t)) in pairs.iter().enumerate() {
5847 self.mtp_warm(m, h, *t, first_pos + j);
5848 }
5849 Ok(())
5850 }
5851
5852 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
5856 let e = self.embed_single(next_token);
5857 let mut cat = vec![0.0f32; 2 * self.hidden_size];
5858 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5859 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5860 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5861 let mut x = vec![0.0f32; self.hidden_size];
5862 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5863 inference::rms_norm_into(
5864 &x,
5865 &m.layer.input_norm,
5866 self.rms_eps,
5867 self.norm_style,
5868 &mut self.ws.n1,
5869 );
5870 let attn = match &m.layer.attn {
5871 AttnKind::Full {
5872 wq,
5873 wk,
5874 wv,
5875 wo,
5876 q_norm,
5877 k_norm,
5878 output_gate,
5879 softplus_gate,
5880 bias,
5881 } => {
5882 let mut cfg = self.attn_cfg(position);
5883 cfg.q_norm = q_norm.as_deref();
5884 cfg.k_norm = k_norm.as_deref();
5885 cfg.output_gate = *output_gate;
5886 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
5887 cfg.bias = bias
5888 .as_ref()
5889 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
5890 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
5891 }
5892 _ => return,
5893 };
5894 let _ = attn;
5895 }
5896
5897 #[cfg(feature = "gpu")]
5904 #[allow(clippy::too_many_arguments)]
5905 fn graph_spec_step(
5906 &mut self,
5907 m: &mut MtpModule,
5908 hidden: &[f32],
5909 t_next: u32,
5910 next_pos: usize,
5911 drafted: &mut usize,
5912 accepted: &mut usize,
5913 all_ids: &mut Vec<u32>,
5917 room: usize,
5922 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
5923 #[cfg(target_os = "macos")]
5934 let metal_native = crate::gpu::q1_force();
5935 #[cfg(not(target_os = "macos"))]
5936 let metal_native = false;
5937 #[cfg(feature = "gpu")]
5938 let k_default = if metal_native {
5939 7
5942 } else if crate::gpu_wgpu::verify_i8_on() {
5943 5
5944 } else {
5945 4
5946 };
5947 #[cfg(not(feature = "gpu"))]
5948 let k_default = 4;
5949 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
5950 .ok()
5951 .and_then(|v| v.parse().ok())
5952 .filter(|&v| (1..=8).contains(&v));
5953 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
5958 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
5959 let k_spec = k_full.min(room).max(1);
5960 let k_capped = k_spec < k_full;
5963 if next_pos == 0 {
5964 return None;
5965 }
5966 let t_round = std::time::Instant::now();
5967 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
5983 let sub0 = subs();
5984 let cfg = self.sampler_config.clone();
5989 let penalized = !(cfg.repetition_penalty == 1.0
5990 && cfg.presence_penalty == 0.0
5991 && cfg.suppress_tokens.is_empty());
5992 let greedy_pen = cfg.temperature < 1e-6 && penalized;
5997 let sampling = cfg.temperature >= 1e-6;
5998 let sparse = sampling && sampler::sparse_ok(&cfg);
6004 let base_len = all_ids.len();
6005 if sampling && !sparse && self.spec_q.len() < k_spec {
6006 self.spec_q.resize_with(k_spec, Vec::new);
6007 }
6008 if sparse && self.spec_qs.len() < k_spec {
6009 self.spec_qs.resize_with(k_spec, Vec::new);
6010 }
6011 let mut drafts = Vec::with_capacity(k_spec);
6016 let mut hx = hidden.to_vec();
6017 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
6020 spec_stamp("pro");
6021 #[cfg(target_os = "macos")]
6027 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
6028 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
6029 Ok(ids) => {
6030 self.mtp_graph_mode = Some(true);
6031 drafts = ids;
6032 }
6033 Err(true) => {
6034 tracing::error!("mtp Metal draft chain failed after commit");
6035 self.clear_sequence_state();
6036 self.graph_failed
6037 .store(true, std::sync::atomic::Ordering::Relaxed);
6038 self.cancel
6039 .store(true, std::sync::atomic::Ordering::Relaxed);
6040 return None;
6041 }
6042 Err(false) => {}
6043 }
6044 }
6045 for j in drafts.len()..k_spec {
6046 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
6047 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
6048 if spec_dbg {
6049 let saved = self.mtp_graph_mode;
6050 self.mtp_graph_mode = Some(false);
6051 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
6052 self.mtp_graph_mode = saved;
6053 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6054 return None;
6055 }
6056 m.kv.truncate_last(1);
6057 dbg_ref = Some(r);
6058 }
6059 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
6060 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6061 return None;
6062 }
6063 if let Some((lg_cpu, h_cpu)) = dbg_ref {
6064 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
6065 let dl = lg
6066 .iter()
6067 .zip(&lg_cpu)
6068 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
6069 let dh = hj
6070 .iter()
6071 .zip(&h_cpu)
6072 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
6073 eprintln!(
6074 "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 {}",
6075 next_pos - 1 + j,
6076 sampler::argmax(&lg_cpu),
6077 sampler::argmax(&lg),
6078 n(&h_cpu),
6079 n(&hj),
6080 m.kv.seq_len
6081 );
6082 }
6083 let dj = if sparse {
6084 let mut q = std::mem::take(&mut self.spec_qs[j]);
6085 let ok = sampler::sparse_distribution_into(
6086 &lg,
6087 &cfg,
6088 all_ids,
6089 &mut self.sampler_scratch,
6090 self.pool.as_deref(),
6091 &mut q,
6092 );
6093 let d = if ok {
6094 sampler::draw_sparse(&q, &mut self.rng)
6095 } else {
6096 let t = sampler::argmax(&lg);
6098 q.clear();
6099 q.push((t, 1.0));
6100 t
6101 };
6102 self.spec_qs[j] = q;
6103 all_ids.push(d);
6104 d
6105 } else if sampling {
6106 let mut q = std::mem::take(&mut self.spec_q[j]);
6107 sampler::distribution_into(
6108 &lg,
6109 &cfg,
6110 all_ids,
6111 &mut self.sampler_scratch,
6112 self.pool.as_deref(),
6113 &mut q,
6114 );
6115 let d = sampler::draw(&q, &mut self.rng);
6116 self.spec_q[j] = q;
6117 all_ids.push(d); d
6119 } else if greedy_pen {
6120 let d = sampler::argmax_penalized(
6121 &lg,
6122 &cfg,
6123 all_ids,
6124 &mut self.sampler_scratch,
6125 self.pool.as_deref(),
6126 );
6127 all_ids.push(d);
6128 d
6129 } else {
6130 sampler::argmax(&lg)
6131 };
6132 attention::recycle_buf(&mut lg);
6133 drafts.push(dj);
6134 hx = hj;
6135 spec_stamp("d.pick");
6136 }
6137 all_ids.truncate(base_len);
6138 *drafted += k_spec;
6139 let t_draft = t_round.elapsed();
6140 let sub_draft = subs();
6141 let b = k_spec + 1;
6144 let mut hiddens = vec![0.0f32; b * self.hidden_size];
6145 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
6146 let e = self.embed_single(t);
6147 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
6148 }
6149 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
6150 spec_stamp("v.emb");
6151 let (lm_gw, lm_rows) = {
6152 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6153 (
6154 crate::gpu::GraphW {
6155 idx: i,
6156 kind,
6157 row_scale: rs,
6158 data: &[],
6159 prism: crate::gpu::GraphPrismOp::None,
6160 affine: false,
6161 },
6162 self.weights.lm_head.rows(),
6163 )
6164 };
6165 let mut logits = Vec::new();
6166 let final_norm = self.weights.final_norm.clone();
6167 #[cfg(target_os = "macos")]
6176 let greedy_dev = metal_native
6177 && !sampling
6178 && !greedy_pen
6179 && !self.confidence_on
6180 && self.final_softcap.is_none()
6181 && self.vocab_size == lm_rows
6187 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
6188 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6189 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
6190 #[cfg(not(target_os = "macos"))]
6191 let greedy_dev = false;
6192 let mut dev_ids: Vec<u32> = Vec::new();
6193 #[cfg(target_os = "macos")]
6194 let verify_outcome = if metal_native {
6195 let lm = self.weights.lm_head.q1_parts()?;
6196 let n_score = self.vocab_size.min(lm_rows);
6197 self.try_batch_graph_metal(
6198 &mut hiddens,
6199 &positions,
6200 b,
6201 Some((lm, &final_norm, &mut logits)),
6202 if greedy_dev {
6203 Some((n_score, &mut dev_ids))
6204 } else {
6205 None
6206 },
6207 )
6208 } else {
6209 self.try_batch_graph_wgpu(
6210 &mut hiddens,
6211 &positions,
6212 b,
6213 Some(crate::gpu::SpecTail {
6214 lm: lm_gw,
6215 lm_rows,
6216 final_norm: &final_norm,
6217 logits_out: &mut logits,
6218 }),
6219 )
6220 };
6221 #[cfg(not(target_os = "macos"))]
6222 let verify_outcome = self.try_batch_graph_wgpu(
6223 &mut hiddens,
6224 &positions,
6225 b,
6226 Some(crate::gpu::SpecTail {
6227 lm: lm_gw,
6228 lm_rows,
6229 final_norm: &final_norm,
6230 logits_out: &mut logits,
6231 }),
6232 );
6233 match verify_outcome {
6234 crate::gpu::BatchGraphOutcome::Completed => {}
6235 crate::gpu::BatchGraphOutcome::Declined => {
6236 m.kv.truncate_last(k_spec);
6240 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
6241 self.clear_sequence_state();
6242 self.graph_failed
6243 .store(true, std::sync::atomic::Ordering::Relaxed);
6244 self.cancel
6245 .store(true, std::sync::atomic::Ordering::Relaxed);
6246 tracing::error!("MTP graph mirror rewind failed after verify decline");
6247 }
6248 return None;
6249 }
6250 crate::gpu::BatchGraphOutcome::Failed => {
6251 self.clear_sequence_state();
6255 self.graph_failed
6256 .store(true, std::sync::atomic::Ordering::Relaxed);
6257 self.cancel
6258 .store(true, std::sync::atomic::Ordering::Relaxed);
6259 tracing::error!("MTP verify batch graph failed after admission");
6260 return None;
6261 }
6262 }
6263 #[cfg(target_os = "macos")]
6269 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
6270 let snap: Vec<Vec<f32>> = self
6271 .kv_cache
6272 .layers
6273 .iter()
6274 .map(|l| l.linear_state.clone())
6275 .collect();
6276 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
6277 let toks: Vec<u32> = std::iter::once(t_next)
6278 .chain(drafts.iter().copied())
6279 .collect();
6280 let want_save = self.graph_want_logits;
6281 self.graph_want_logits = false;
6282 for (i, &t) in toks.iter().enumerate() {
6283 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
6284 let _ = self.graph_logits.take();
6285 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
6289 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
6290 }
6291 let ref_lg = self.logits_from_hidden(&hi);
6292 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
6293 let ra = sampler::argmax(&ref_lg);
6294 let va = sampler::argmax(row);
6295 let mut md = 0f32;
6296 let mut rms = 0f64;
6297 for j in 0..lm_rows.min(ref_lg.len()) {
6298 let d = (ref_lg[j] - row[j]).abs();
6299 md = md.max(d);
6300 rms += (d as f64) * (d as f64);
6301 }
6302 let mut hd = 0f32;
6303 for j in 0..self.hidden_size {
6304 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
6305 }
6306 eprintln!(
6307 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
6308 next_pos + i,
6309 if ra == va { "OK" } else { "MISMATCH" },
6310 (rms / lm_rows as f64).sqrt()
6311 );
6312 }
6313 self.graph_want_logits = want_save;
6314 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
6317 if l.linear_state.len() == st.len() {
6318 l.linear_state.copy_from_slice(&st);
6319 } else {
6320 l.linear_state = st;
6321 }
6322 }
6323 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
6324 let extra = l.seq_len.saturating_sub(n0);
6325 if extra > 0 {
6326 l.truncate_last(extra);
6327 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
6328 }
6329 }
6330 }
6331 let t_verify = t_round.elapsed();
6332 let sub_verify = subs();
6333 let mut a = 0usize;
6338 let mut forced: Option<u32> = None;
6339 let ids: Vec<u32> = if sparse {
6340 let mut p = std::mem::take(&mut self.spec_ps);
6341 let mut res = std::mem::take(&mut self.spec_ress);
6342 while a < k_spec {
6343 let ok = sampler::sparse_distribution_into(
6344 &logits[a * lm_rows..(a + 1) * lm_rows],
6345 &cfg,
6346 all_ids,
6347 &mut self.sampler_scratch,
6348 self.pool.as_deref(),
6349 &mut p,
6350 );
6351 if !ok {
6352 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
6353 p.clear();
6354 p.push((t, 1.0));
6355 }
6356 match sampler::spec_accept_or_correct_sparse(
6357 &p,
6358 &self.spec_qs[a],
6359 drafts[a],
6360 &mut self.rng,
6361 &mut res,
6362 ) {
6363 None => {
6364 all_ids.push(drafts[a]);
6365 a += 1;
6366 }
6367 Some(c) => {
6368 forced = Some(c);
6369 break;
6370 }
6371 }
6372 }
6373 all_ids.truncate(base_len);
6374 self.spec_ps = p;
6375 self.spec_ress = res;
6376 drafts.clone()
6377 } else if sampling {
6378 let mut p = std::mem::take(&mut self.spec_p);
6379 let mut res = std::mem::take(&mut self.spec_res);
6380 while a < k_spec {
6381 sampler::distribution_into(
6382 &logits[a * lm_rows..(a + 1) * lm_rows],
6383 &cfg,
6384 all_ids,
6385 &mut self.sampler_scratch,
6386 self.pool.as_deref(),
6387 &mut p,
6388 );
6389 match sampler::spec_accept_or_correct(
6390 &p,
6391 &self.spec_q[a],
6392 drafts[a],
6393 &mut self.rng,
6394 &mut res,
6395 self.pool.as_deref(),
6396 ) {
6397 None => {
6398 all_ids.push(drafts[a]);
6399 a += 1;
6400 }
6401 Some(c) => {
6402 forced = Some(c);
6403 break;
6404 }
6405 }
6406 }
6407 all_ids.truncate(base_len);
6408 self.spec_p = p;
6409 self.spec_res = res;
6410 drafts.clone()
6412 } else if greedy_pen {
6413 let mut ids: Vec<u32> = Vec::with_capacity(b);
6417 for i in 0..b {
6418 let t = sampler::argmax_penalized(
6419 &logits[i * lm_rows..(i + 1) * lm_rows],
6420 &cfg,
6421 all_ids,
6422 &mut self.sampler_scratch,
6423 self.pool.as_deref(),
6424 );
6425 ids.push(t);
6426 if i < k_spec && t == drafts[i] {
6427 all_ids.push(t);
6428 } else {
6429 break;
6430 }
6431 }
6432 all_ids.truncate(base_len);
6433 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
6434 a += 1;
6435 }
6436 ids
6439 } else if greedy_dev && dev_ids.len() == b {
6440 let ids = std::mem::take(&mut dev_ids);
6441 while a < k_spec && ids[a] == drafts[a] {
6442 a += 1;
6443 }
6444 ids
6445 } else {
6446 if logits.len() < b * lm_rows {
6447 self.clear_sequence_state();
6450 self.graph_failed
6451 .store(true, std::sync::atomic::Ordering::Relaxed);
6452 self.cancel
6453 .store(true, std::sync::atomic::Ordering::Relaxed);
6454 tracing::error!("Metal verify returned neither logits nor argmax ids");
6455 return None;
6456 }
6457 let ids: Vec<u32> = (0..b)
6458 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
6459 .collect();
6460 while a < k_spec && ids[a] == drafts[a] {
6461 a += 1;
6462 }
6463 ids
6464 };
6465 spec_stamp("acc");
6466 if spec_dbg {
6467 eprintln!(
6468 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
6469 drafts, ids
6470 );
6471 }
6472 #[cfg(target_os = "macos")]
6476 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
6477 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
6478 {
6479 let snap: Vec<Vec<f32>> = self
6480 .kv_cache
6481 .layers
6482 .iter()
6483 .map(|l| l.linear_state.clone())
6484 .collect();
6485 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
6486 let toks: Vec<u32> = std::iter::once(t_next)
6487 .chain(drafts.iter().copied())
6488 .collect();
6489 let want_save = self.graph_want_logits;
6490 self.graph_want_logits = false;
6491 for (i, &t) in toks.iter().take(a + 1).enumerate() {
6492 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
6493 let _ = self.graph_logits.take();
6494 }
6495 self.graph_want_logits = want_save;
6496 let plain_states: Vec<Vec<f32>> = self
6497 .kv_cache
6498 .layers
6499 .iter()
6500 .map(|l| l.linear_state.clone())
6501 .collect();
6502 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6503 let mut rows = Vec::new();
6504 for (li, (l, n0)) in self
6505 .kv_cache
6506 .layers
6507 .iter_mut()
6508 .zip(attn_lens.iter())
6509 .enumerate()
6510 {
6511 let extra = l.seq_len.saturating_sub(*n0);
6512 if extra > 0 {
6513 let mut kk = Vec::new();
6514 let mut vv = Vec::new();
6515 for g in 0..nkv {
6516 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
6517 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
6518 }
6519 rows.push((li, kk, vv));
6520 l.truncate_last(extra);
6521 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
6522 }
6523 }
6524 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
6525 if l.linear_state.len() == st.len() {
6526 l.linear_state.copy_from_slice(&st);
6527 } else {
6528 l.linear_state = st;
6529 }
6530 }
6531 Some((plain_states, rows))
6532 } else {
6533 None
6534 };
6535 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
6536 #[cfg(target_os = "macos")]
6545 let mut warm_pending: Option<MetalWarmPending> = None;
6546 #[cfg(target_os = "macos")]
6547 if metal_native {
6548 m.kv.truncate_last(k_spec.saturating_sub(1));
6549 if self.mtp_graph_mode == Some(true) {
6550 crate::gpu_metal::kv_mirror_set_stored(
6553 self.mtp_kv_id(),
6554 Self::MTP_LAYER_BASE,
6555 m.kv.seq_len,
6556 );
6557 if !warm_off && a > 0 {
6558 let pairs: Vec<(&[f32], u32)> = (0..a)
6559 .map(|j| {
6560 (
6561 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
6562 ids[j],
6563 )
6564 })
6565 .collect();
6566 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
6567 }
6568 }
6569 spec_stamp("c.wsub");
6570 }
6571 #[cfg(target_os = "macos")]
6573 if metal_native {
6574 if !self.metal_verify_commit(a) {
6577 self.clear_sequence_state();
6578 self.graph_failed
6579 .store(true, std::sync::atomic::Ordering::Relaxed);
6580 self.cancel
6581 .store(true, std::sync::atomic::Ordering::Relaxed);
6582 tracing::error!("Metal verify state/KV handoff failed after admission");
6583 return None;
6584 }
6585 if let Some((plain_states, rows)) = commit_ref {
6586 crate::gpu_metal::queue_fence();
6587 let _ = crate::gpu_metal::wait_replay();
6590 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6591 let mut worst_s = 0f32;
6592 let mut worst_li = 0usize;
6593 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
6594 if l.linear_state.len() != ps.len() || ps.is_empty() {
6595 continue;
6596 }
6597 let d = l
6598 .linear_state
6599 .iter()
6600 .zip(ps)
6601 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
6602 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
6603 let rel = d / n.max(1e-6);
6604 if rel > worst_s {
6605 worst_s = rel;
6606 worst_li = li;
6607 }
6608 }
6609 let mut worst_k = 0f32;
6610 for (li, kk, vv) in &rows {
6611 let l = &self.kv_cache.layers[*li];
6612 let n0 = l.seq_len - (kk.len() / (nkv * hd));
6613 let mut ck = Vec::new();
6614 let mut cv = Vec::new();
6615 for g in 0..nkv {
6616 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
6617 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
6618 }
6619 if ck.len() == kk.len() {
6620 let dk = ck
6621 .iter()
6622 .zip(kk)
6623 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
6624 let dv = cv
6625 .iter()
6626 .zip(vv)
6627 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
6628 worst_k = worst_k.max(dk).max(dv);
6629 } else {
6630 eprintln!(
6631 "commit-check L{li}: kv row count mismatch {} vs {}",
6632 ck.len(),
6633 kk.len()
6634 );
6635 }
6636 }
6637 eprintln!(
6638 "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}"
6639 );
6640 }
6641 }
6642 if !metal_native && a + 1 < b {
6643 let expected_gdn_layers = self.graph_gdn_layer_count();
6644 if expected_gdn_layers > 0
6645 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
6646 {
6647 self.clear_sequence_state();
6648 self.graph_failed
6649 .store(true, std::sync::atomic::Ordering::Relaxed);
6650 self.cancel
6651 .store(true, std::sync::atomic::Ordering::Relaxed);
6652 tracing::error!("GDN speculative restore failed after verify");
6653 return None;
6654 }
6655 }
6656 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
6657 self.clear_sequence_state();
6662 self.graph_failed
6663 .store(true, std::sync::atomic::Ordering::Relaxed);
6664 self.cancel
6665 .store(true, std::sync::atomic::Ordering::Relaxed);
6666 tracing::error!("trunk graph KV rewind failed after speculative verify");
6667 return None;
6668 }
6669 *accepted += a;
6670 if !metal_native {
6681 m.kv.truncate_last(k_spec.saturating_sub(1));
6683 }
6684 spec_stamp("c.trunc");
6685 if !metal_native
6686 && self.mtp_graph_mode == Some(true)
6687 && !self.rewind_mtp_graph_mirror(next_pos)
6688 {
6689 self.clear_sequence_state();
6693 self.graph_failed
6694 .store(true, std::sync::atomic::Ordering::Relaxed);
6695 self.cancel
6696 .store(true, std::sync::atomic::Ordering::Relaxed);
6697 tracing::error!("MTP graph mirror rewind failed after verify commit");
6698 return None;
6699 }
6700 if !warm_off && a > 0 {
6701 let mut warmed = false;
6704 #[cfg(target_os = "macos")]
6705 if metal_native && self.mtp_graph_mode == Some(true) {
6706 warmed = match warm_pending.take() {
6710 Some(p) => self.mtp_warm_batch_finish(m, p),
6711 None => false,
6712 };
6713 if !warmed {
6714 warmed = true;
6715 for j in 0..a {
6716 let row =
6717 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
6718 if self
6719 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
6720 .is_none()
6721 {
6722 warmed = false;
6723 break;
6724 }
6725 }
6726 }
6727 }
6728 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
6729 let rows: Vec<Vec<f32>> = (0..a)
6730 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
6731 .collect();
6732 let pairs: Vec<(&[f32], u32)> = rows
6733 .iter()
6734 .zip(ids.iter())
6735 .map(|(r, &t)| (r.as_slice(), t))
6736 .collect();
6737 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
6738 Ok(()) => warmed = true,
6739 Err(err) => {
6740 tracing::error!("{err}");
6746 self.clear_sequence_state();
6747 self.graph_failed
6748 .store(true, std::sync::atomic::Ordering::Relaxed);
6749 self.cancel
6750 .store(true, std::sync::atomic::Ordering::Relaxed);
6751 return None;
6752 }
6753 }
6754 }
6755 if !warmed {
6756 for j in 0..a {
6757 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
6758 let row = row.to_vec();
6759 self.mtp_warm(m, &row, ids[j], next_pos + j);
6760 }
6761 }
6762 }
6763 spec_stamp("c.warm");
6767 if let Some(c) = forced {
6768 self.spec_forced = Some(c);
6769 self.graph_logits = None;
6770 } else if greedy_dev && logits.is_empty() {
6771 self.spec_forced = Some(ids[a]);
6774 self.graph_logits = None;
6775 } else {
6776 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
6777 row.resize(self.vocab_size, 0.0);
6778 if let Some(c) = self.final_softcap {
6779 for l in row.iter_mut() {
6780 *l = c * (*l / c).tanh();
6781 }
6782 }
6783 self.graph_logits = Some(row);
6784 }
6785 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
6786 spec_stamp("c.row");
6787 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6793 let end = subs();
6794 eprintln!(
6795 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
6796 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
6797 t_draft.as_secs_f64() * 1e3,
6798 sub_draft - sub0,
6799 (t_verify - t_draft).as_secs_f64() * 1e3,
6800 sub_verify - sub_draft,
6801 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
6802 end - sub_verify,
6803 self.draft_full_streak,
6804 );
6805 }
6806 if k_env.is_none() && !metal_native && !k_capped {
6811 let f = a as f32 / k_spec.max(1) as f32;
6815 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
6816 let mut k_next = k_spec;
6817 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
6818 k_next = k_spec + 1;
6819 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
6820 k_next = k_spec - 1;
6821 }
6822 if k_next != k_spec {
6823 self.spec_acc_ewma = 0.6;
6824 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6825 eprintln!("spec-k: {k_spec} → {k_next}");
6826 }
6827 }
6828 self.spec_k_adapt = Some(k_next);
6829 }
6830 spec_stamp("end");
6831 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
6832 }
6833
6834 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
6843 if !self.pair_supported() {
6844 return (0.0, 0.0);
6845 }
6846 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
6853 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
6854 let emb1 = self.embed_single(1);
6855 let emb2 = self.embed_single(2);
6856 let pos = self.kv_cache.seq_len();
6857
6858 let t0 = std::time::Instant::now();
6859 for _ in 0..iters {
6860 let _ = self.forward_layers(&emb1, pos, None);
6861 let _ = self.forward_layers(&emb2, pos + 1, None);
6862 for l in &mut self.kv_cache.layers {
6863 l.truncate_last(2);
6864 }
6865 }
6866 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
6867
6868 let t1 = std::time::Instant::now();
6869 for _ in 0..iters {
6870 let _ = self.forward_pair(&emb1, &emb2, pos);
6871 for l in &mut self.kv_cache.layers {
6872 l.truncate_last(2);
6873 }
6874 }
6875 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
6876 match graph_env {
6877 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
6878 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
6879 }
6880 (singles_ms, pair_ms)
6881 }
6882
6883 fn pair_supported(&self) -> bool {
6891 !self.weights.layers.is_empty()
6898 && self.g3n.is_none()
6899 && !self
6900 .weights
6901 .layers
6902 .iter()
6903 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
6904 }
6905
6906 fn forward_pair(
6907 &mut self,
6908 emb1: &[f32],
6909 emb2: &[f32],
6910 position: usize,
6911 ) -> (Vec<f32>, Vec<f32>) {
6912 self.mimo_moe_prepare();
6915 let mut h1 = emb1.to_vec();
6916 let mut h2 = emb2.to_vec();
6917 let (_nkv, _hd, hs, _rd, eps) = (
6918 self.num_kv_heads,
6919 self.head_dim,
6920 self.hidden_size,
6921 self.rotary_dim,
6922 self.rms_eps,
6923 );
6924 let pool = self.pool.clone();
6925
6926 for li in 0..self.num_layers {
6927 let lw = &self.weights.layers[self.phys_layer(li)];
6928 inference::rms_norm_into(
6931 &h1,
6932 &lw.input_norm,
6933 self.rms_eps,
6934 self.norm_style,
6935 &mut self.ws.n1,
6936 );
6937 inference::rms_norm_into(
6938 &h2,
6939 &lw.input_norm,
6940 self.rms_eps,
6941 self.norm_style,
6942 &mut self.ws.n2,
6943 );
6944
6945 let (a1, a2) = match &lw.attn {
6946 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6947 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6948 AttnKind::Linear(w) => {
6949 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
6950 let layer = &mut self.kv_cache.layers[li];
6951 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6952 vmf_phase_pair(
6953 &self.ws.n1,
6954 &self.ws.n2,
6955 w,
6956 &cfg,
6957 state,
6958 scratch,
6959 self.pool.as_deref(),
6960 )
6961 }
6962 AttnKind::LinearGdn(w) => {
6963 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6964 let layer = &mut self.kv_cache.layers[li];
6965 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6966 gdn_pair(
6967 &self.ws.n1,
6968 &self.ws.n2,
6969 w,
6970 &cfg,
6971 state,
6972 scratch,
6973 self.pool.as_deref(),
6974 )
6975 }
6976 AttnKind::ShortConv(w) => {
6977 let cfg = self
6978 .short_conv_cfg
6979 .expect("short-conv layer without short_conv_cfg");
6980 let layer = &mut self.kv_cache.layers[li];
6981 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
6982 short_conv_pair(
6983 &self.ws.n1,
6984 &self.ws.n2,
6985 w,
6986 &cfg,
6987 state,
6988 scratch,
6989 self.pool.as_deref(),
6990 )
6991 }
6992 AttnKind::Full {
6993 wq,
6994 wk,
6995 wv,
6996 wo,
6997 q_norm,
6998 k_norm,
6999 output_gate,
7000 softplus_gate,
7001 bias,
7002 } => {
7003 let inv_freq_l = self.layer_inv_freq(li);
7004 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7005 let cfg = QwenAttnCfg {
7006 num_heads: self.layer_num_heads(li),
7007 num_kv_heads: nkv_l,
7008 head_dim: hd_l,
7009 hidden_size: hs,
7010 position,
7011 inv_freq: &inv_freq_l,
7012 rotary_dim: rd_l,
7013 scale: self.attn_scale,
7014 softcap: self.attn_softcap,
7015 window: self.layer_window(li),
7016 v_norm: self.attn_v_norm,
7017 qk_norm_after_rope: self.qk_norm_after_rope,
7018 q_norm: q_norm.as_deref(),
7019 k_norm: k_norm.as_deref(),
7020 output_gate: *output_gate,
7021 softplus_gate: softplus_gate
7022 .as_ref()
7023 .map(|(gate, per_head)| (gate, *per_head)),
7024 rope_scale: self.layer_rope_scale(li),
7025 bias: bias
7026 .as_ref()
7027 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7028 rms_eps: eps,
7029 norm_style: self.norm_style,
7030 pool: pool.as_deref(),
7031 v_head_dim: self.layer_v_dim(li),
7032 };
7033 attention::qwen_attention_pair(
7034 &self.ws.n1,
7035 &self.ws.n2,
7036 wq,
7037 wk,
7038 wv,
7039 wo,
7040 &mut self.kv_cache.layers[li],
7041 &cfg,
7042 )
7043 }
7044 };
7045 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
7046 Some(w) => (
7047 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
7048 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
7049 ),
7050 None => (a1, a2),
7051 };
7052 for i in 0..self.hidden_size {
7053 h1[i] += a1[i];
7054 h2[i] += a2[i];
7055 }
7056 let (mut a1, mut a2) = (a1, a2);
7057 attention::recycle_buf(&mut a1);
7058 attention::recycle_buf(&mut a2);
7059
7060 let lw = &self.weights.layers[self.phys_layer(li)];
7061 inference::rms_norm_into(
7062 &h1,
7063 &lw.post_norm,
7064 self.rms_eps,
7065 self.norm_style,
7066 &mut self.ws.p1,
7067 );
7068 inference::rms_norm_into(
7069 &h2,
7070 &lw.post_norm,
7071 self.rms_eps,
7072 self.norm_style,
7073 &mut self.ws.p2,
7074 );
7075 let (f1, f2) = match &lw.ffn {
7076 FfnKind::DenseMoe(dm) => (
7079 dense_moe_ffn(
7080 dm,
7081 &self.ws.p1,
7082 &h1,
7083 self.rms_eps,
7084 self.norm_style,
7085 self.pool.as_deref(),
7086 ),
7087 dense_moe_ffn(
7088 dm,
7089 &self.ws.p2,
7090 &h2,
7091 self.rms_eps,
7092 self.norm_style,
7093 self.pool.as_deref(),
7094 ),
7095 ),
7096 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
7097 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
7098 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
7099 ),
7100 _ => ffn_forward_pair(
7101 &lw.ffn,
7102 &self.ws.p1,
7103 &self.ws.p2,
7104 self.pool.as_deref(),
7105 None,
7106 ),
7107 };
7108 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
7109 Some(w) => (
7110 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
7111 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
7112 ),
7113 None => (f1, f2),
7114 };
7115 for i in 0..self.hidden_size {
7116 h1[i] += f1[i];
7117 h2[i] += f2[i];
7118 }
7119 let (mut f1, mut f2) = (f1, f2);
7120 attention::recycle_buf(&mut f1);
7121 attention::recycle_buf(&mut f2);
7122 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
7123 for i in 0..self.hidden_size {
7124 h1[i] *= sc;
7125 h2[i] *= sc;
7126 }
7127 }
7128 if self.is_loop_end(li) && li + 1 < self.num_layers {
7130 h1 = inference::rms_norm(
7131 &h1,
7132 &self.weights.final_norm,
7133 self.rms_eps,
7134 self.norm_style,
7135 );
7136 h2 = inference::rms_norm(
7137 &h2,
7138 &self.weights.final_norm,
7139 self.rms_eps,
7140 self.norm_style,
7141 );
7142 }
7143 }
7144 if self.o1_active() {
7150 self.commit_linear_scratch();
7151 }
7152 self.o1_progress();
7153 (h1, h2)
7154 }
7155
7156 fn commit_linear_scratch(&mut self) {
7158 for layer in &mut self.kv_cache.layers {
7159 if !layer.linear_scratch.is_empty() {
7160 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
7161 layer.linear_scratch.clear();
7162 }
7163 }
7164 }
7165
7166 pub fn forward_ids(
7169 &mut self,
7170 ids: &[u32],
7171 task_mask: Option<&TaskMask>,
7172 ) -> Result<Vec<f32>, String> {
7173 if ids.is_empty() {
7174 return Err("empty id sequence".to_string());
7175 }
7176 self.clear_sequence_state();
7177 self.check_forward_graph("forward_ids setup", 0)?;
7178 if task_mask.is_none() {
7179 self.o1_begin();
7180 }
7181 let mut hidden = vec![0.0f32; self.hidden_size];
7182 let mut pos = 0usize;
7183 if let Some(b) = &mut self.dsv41 {
7184 let pool = self.pool.clone();
7185 let mut logits = Vec::new();
7186 crate::dsv41::forward_chunk(
7187 &b.0,
7188 &b.1,
7189 &b.2,
7190 &mut b.3,
7191 ids,
7192 0,
7193 pool.as_deref(),
7194 &mut logits,
7195 );
7196 if let Err(err) = self.o1_seal_checked() {
7197 self.clear_sequence_state();
7198 return Err(err);
7199 }
7200 return Ok(logits);
7201 }
7202 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
7210 let chunk = self.prefill_chunk();
7214 let hs = self.hidden_size;
7215 while pos < ids.len() {
7216 let end = (pos + chunk).min(ids.len());
7217 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
7218 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
7219 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7220 pos = end;
7221 }
7222 }
7223 if task_mask.is_none()
7232 && !self.graph_prefill_preferred()
7233 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
7234 && self.pair_supported()
7235 {
7236 while pos + 1 < ids.len() {
7237 let e1 = self.embed_single(ids[pos]);
7238 let e2 = self.embed_single(ids[pos + 1]);
7239 let (_, h2) = self.forward_pair(&e1, &e2, pos);
7240 self.check_forward_graph("forward_ids pair", pos + 1)?;
7241 self.commit_linear_scratch();
7242 hidden = h2;
7243 pos += 2;
7244 }
7245 }
7246 while pos < ids.len() {
7247 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
7248 self.check_forward_graph("forward_ids", pos)?;
7249 pos += 1;
7250 }
7251 if let Err(err) = self.o1_seal_checked() {
7255 self.clear_sequence_state();
7256 return Err(err);
7257 }
7258 let normed = inference::rms_norm(
7259 &hidden,
7260 &self.weights.final_norm,
7261 self.rms_eps,
7262 self.norm_style,
7263 );
7264 Ok(self.lm_head_forward(&normed))
7265 }
7266
7267 #[doc(hidden)]
7271 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
7272 #[cfg(target_os = "macos")]
7273 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
7274 if ids.is_empty() {
7275 return Err("empty id sequence".to_string());
7276 }
7277 self.clear_sequence_state();
7278 self.dsv41
7279 .as_ref()
7280 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
7281 self.o1_begin();
7282 let rows = {
7283 let pool = self.pool.clone();
7284 let b = self
7285 .dsv41
7286 .as_mut()
7287 .expect("dsv41 checked above; state cannot change during forward");
7288 let mut rows = Vec::with_capacity(ids.len());
7289 for (position, &id) in ids.iter().enumerate() {
7290 let mut logits = Vec::new();
7291 crate::dsv41::forward_token(
7292 &b.0,
7293 &b.1,
7294 &b.2,
7295 &mut b.3,
7296 id,
7297 position,
7298 pool.as_deref(),
7299 &mut logits,
7300 );
7301 rows.push(logits);
7302 }
7303 rows
7304 };
7305 self.o1_seal();
7306 Ok(rows)
7307 }
7308
7309 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
7316 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
7317 Ok((nll / cnt.max(1) as f64).exp())
7318 }
7319
7320 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
7325 self.clear_sequence_state();
7326 FFN_PROBE.with(|p| {
7327 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
7328 });
7329 crate::gpu::cpu_scope(|| {
7330 for (pos, &id) in ids.iter().enumerate() {
7331 let emb = self.embed_single(id);
7332 let _ = self.forward_layers(&emb, pos, None);
7333 }
7334 });
7335 self.clear_sequence_state();
7336 FFN_PROBE
7337 .with(|p| p.borrow_mut().take())
7338 .unwrap_or_default()
7339 }
7340
7341 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
7345 if let Err(err) = self.nll_begin() {
7346 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
7350 self.nll_end();
7351 return Err(err);
7352 }
7353 FFN_PROBE.with(|p| {
7354 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
7355 });
7356 let result: Result<(), String> = (|| {
7357 for chunk in ids.chunks(256) {
7358 if chunk.len() < 2 {
7359 continue;
7360 }
7361 self.nll_ids_masked(chunk, 0, None)?;
7362 }
7363 Ok(())
7364 })();
7365 self.nll_end();
7366 let probe = FFN_PROBE
7367 .with(|p| p.borrow_mut().take())
7368 .unwrap_or_default();
7369 match result {
7370 Ok(()) => Ok(probe),
7371 Err(err) => {
7372 drop(probe);
7373 Err(err)
7374 }
7375 }
7376 }
7377
7378 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
7382 self.nll_begin()?;
7383 let result: Result<f64, String> = (|| {
7384 let mut nll = 0f64;
7385 let mut cnt = 0usize;
7386 let mut hidden = vec![0f32; self.hidden_size];
7387 for (pos, &id) in ids.iter().enumerate() {
7388 if pos > 0 {
7389 inference::rms_norm_into(
7390 &hidden,
7391 &self.weights.final_norm,
7392 self.rms_eps,
7393 self.norm_style,
7394 &mut self.ws.n1,
7395 );
7396 let mut logits = self.lm_head_forward(&self.ws.n1);
7397 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
7398 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
7399 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
7400 nll -= p.max(1e-300).ln();
7401 cnt += 1;
7402 attention::recycle_buf(&mut logits);
7403 }
7404 let emb = self.embed_single(id);
7405 hidden = self.forward_layers(&emb, pos, Some(mask));
7406 self.nll_check_graph("masked serial forward", pos)?;
7407 let _ = self.graph_logits.take();
7411 }
7412 Ok((nll / cnt.max(1) as f64).exp())
7413 })();
7414 self.nll_end();
7415 result
7416 }
7417
7418 pub fn nll_ids_masked(
7437 &mut self,
7438 ids: &[u32],
7439 start: usize,
7440 task_mask: Option<&TaskMask>,
7441 ) -> Result<(f64, usize), String> {
7442 let task_mask = self.drop_open_mask(task_mask);
7443 self.nll_ids_inner(ids, start, task_mask)
7444 }
7445
7446 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
7447 self.nll_ids_inner(ids, start, None)
7448 }
7449
7450 fn nll_ids_inner(
7451 &mut self,
7452 ids: &[u32],
7453 start: usize,
7454 task_mask: Option<&TaskMask>,
7455 ) -> Result<(f64, usize), String> {
7456 self.nll_begin()?;
7457 let result: Result<(f64, usize), String> = (|| {
7458 let mut nll = 0f64;
7459 let mut cnt = 0usize;
7460 let (graph_quality, fused_head_quality) = nll_graph_policy(
7473 task_mask.is_none(),
7474 self.graph_prefill_preferred(),
7475 crate::gpu::q1_force(),
7476 );
7477 self.graph_head_required = fused_head_quality;
7478 self.graph_want_logits = fused_head_quality;
7479 #[cfg(target_os = "macos")]
7480 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
7481 match self.nll_batch_metal(ids, start) {
7482 MetalBatchNllOutcome::Completed(nll, count) => {
7483 return Ok((nll, count));
7484 }
7485 MetalBatchNllOutcome::Declined => {}
7486 MetalBatchNllOutcome::Failed(err) => return Err(err),
7487 }
7488 }
7489 if self.can_prefill_batched() && !graph_quality {
7490 const CHUNK: usize = 128;
7496 const LM_SUB: usize = 32;
7497 let n = ids.len().saturating_sub(1);
7498 let hs = self.hidden_size;
7499 let rows = self.weights.lm_head.rows();
7500 let mut pos = 0usize;
7501 while pos < n {
7502 let end = (pos + CHUNK).min(n);
7503 let bsz = end - pos;
7504 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
7505 self.nll_check_graph("batched prefill", pos)?;
7506 let mut k0 = 0usize;
7507 while k0 < bsz {
7508 let k1 = (k0 + LM_SUB).min(bsz);
7509 let sb = k1 - k0;
7510 if pos + k1 <= start {
7513 k0 = k1;
7514 continue;
7515 }
7516 let mut normed = vec![0.0f32; sb * hs];
7517 for k in 0..sb {
7518 let r = inference::rms_norm(
7519 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
7520 &self.weights.final_norm,
7521 self.rms_eps,
7522 self.norm_style,
7523 );
7524 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
7525 }
7526 let mut logits = vec![0.0f32; sb * rows];
7527 self.weights
7528 .lm_head
7529 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
7530 for k in 0..sb {
7531 if pos + k0 + k < start {
7532 continue;
7533 }
7534 self.nll_check_graph("batched score row", pos + k0 + k)?;
7535 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
7536 if let Some(mu) = self.logit_multiplier {
7537 for v in lg.iter_mut() {
7538 *v *= mu;
7539 }
7540 }
7541 if let Some(c) = self.final_softcap {
7545 for v in lg.iter_mut() {
7546 *v = c * (*v / c).tanh();
7547 }
7548 }
7549 if let Some(cm) = self.head_clusters.clone() {
7552 self.hierarchical_head_logprobs(
7553 &normed[k * hs..(k + 1) * hs],
7554 &cm,
7555 lg,
7556 );
7557 }
7558 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
7559 let target = ids[pos + k0 + k + 1] as usize;
7560 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7561 let lse: f64 = lg
7562 .iter()
7563 .map(|&v| ((v - max) as f64).exp())
7564 .sum::<f64>()
7565 .ln()
7566 + max as f64;
7567 nll += lse - lg[target] as f64;
7568 cnt += 1;
7569 if std::env::var("CMF_PPL_TRACE").is_ok() {
7570 let top = lg
7571 .iter()
7572 .enumerate()
7573 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7574 .map(|(i, _)| i)
7575 .unwrap_or(0);
7576 eprintln!(
7577 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
7578 pos + k0 + k,
7579 target,
7580 lse - lg[target] as f64,
7581 top,
7582 lg[target],
7583 lg[top]
7584 );
7585 }
7586 }
7587 k0 = k1;
7588 }
7589 pos = end;
7590 }
7591 return Ok((nll, cnt));
7592 }
7593 for pos in 0..ids.len().saturating_sub(1) {
7594 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
7595 self.nll_check_graph("serial forward", pos)?;
7596 let out_of_band = self.graph_logits.take();
7604 if self.graph_head_required && out_of_band.is_none() {
7605 METAL_GRAPH_HEAD_MISS.fetch_add(
7606 1,
7607 std::sync::atomic::Ordering::Relaxed,
7608 );
7609 return Err(format!(
7610 "fused Metal graph head did not complete at NLL position {pos}"
7611 ));
7612 }
7613 if pos < start {
7614 continue;
7615 }
7616 let logits = match out_of_band {
7617 Some(lg) => lg,
7618 None => {
7619 let normed = inference::rms_norm(
7620 &hidden,
7621 &self.weights.final_norm,
7622 self.rms_eps,
7623 self.norm_style,
7624 );
7625 self.lm_head_forward(&normed)
7629 }
7630 };
7631 let target = ids[pos + 1] as usize;
7632 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7633 let lse: f64 = logits
7634 .iter()
7635 .map(|&v| ((v - max) as f64).exp())
7636 .sum::<f64>()
7637 .ln()
7638 + max as f64;
7639 let tok_nll = lse - logits[target] as f64;
7640 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
7641 let top = logits
7642 .iter()
7643 .enumerate()
7644 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7645 .map(|(i, _)| i)
7646 .unwrap_or(0);
7647 eprintln!(
7648 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
7649 logits[target], logits[top]
7650 );
7651 }
7652 nll += tok_nll;
7653 cnt += 1;
7654 }
7655 Ok((nll, cnt))
7656 })();
7657 self.nll_end();
7658 result
7659 }
7660
7661 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
7666 let normed = inference::rms_norm(
7667 hidden,
7668 &self.weights.final_norm,
7669 self.rms_eps,
7670 self.norm_style,
7671 );
7672 let mut logits = self.lm_head_forward(&normed);
7675 let target = target as usize;
7676 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
7677 let lse: f64 = logits
7678 .iter()
7679 .map(|&v| ((v - max) as f64).exp())
7680 .sum::<f64>()
7681 .ln()
7682 + max as f64;
7683 let tok_nll = lse - logits[target] as f64;
7684 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
7685 let top = logits
7686 .iter()
7687 .enumerate()
7688 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7689 .map(|(i, _)| i)
7690 .unwrap_or(0);
7691 eprintln!(
7692 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
7693 logits[target], logits[top]
7694 );
7695 }
7696 attention::recycle_buf(&mut logits);
7697 tok_nll
7698 }
7699
7700 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
7718 self.nll_begin()?;
7723 let requested_prefix = (prefill > 0).then_some(prefill);
7724 self.o1_begin_with_prefix(requested_prefix);
7725 let n = ids.len().saturating_sub(1);
7726 let requested_start = prefill.min(n);
7727 let exact_end = if self.o1_active() {
7732 match requested_prefix {
7733 Some(requested) => self.o1_effective_boundary(requested),
7734 None => self
7735 .o1_cfg
7736 .as_ref()
7737 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
7738 }
7739 .unwrap_or(requested_start)
7740 .min(n)
7741 } else {
7742 requested_start
7743 };
7744 let mut nll = 0f64;
7745 let mut cnt = 0usize;
7746
7747 let mut pos = 0usize;
7751 if self.can_prefill_batched() {
7752 const CHUNK: usize = 128;
7753 while pos < exact_end {
7754 let end = (pos + CHUNK).min(exact_end);
7755 let hiddens = self.prefill_batch(&ids[pos..end], pos);
7756 if self
7757 .graph_failed
7758 .swap(false, std::sync::atomic::Ordering::Relaxed)
7759 {
7760 self.cancel
7761 .store(false, std::sync::atomic::Ordering::Relaxed);
7762 self.nll_end();
7763 return Err("GPU graph failed during O(1) NLL prefix".into());
7764 }
7765 for row in 0..end - pos {
7766 let score_pos = pos + row;
7767 if score_pos >= requested_start && score_pos < n {
7768 nll += self.nll_from_hidden(
7769 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
7770 ids[score_pos + 1],
7771 score_pos,
7772 );
7773 cnt += 1;
7774 }
7775 }
7776 pos = end;
7777 }
7778 } else {
7779 while pos < exact_end {
7780 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7781 if self
7782 .graph_failed
7783 .swap(false, std::sync::atomic::Ordering::Relaxed)
7784 {
7785 self.cancel
7786 .store(false, std::sync::atomic::Ordering::Relaxed);
7787 self.nll_end();
7788 return Err("GPU graph failed during O(1) NLL prefix".into());
7789 }
7790 if pos >= requested_start && pos < n {
7791 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
7792 cnt += 1;
7793 }
7794 pos += 1;
7795 }
7796 }
7797 self.o1_seal_checked().map_err(|err| {
7798 self.nll_end();
7799 err
7800 })?;
7801
7802 let batch_k = std::env::var("CMF_BATCH_K")
7811 .ok()
7812 .and_then(|v| v.parse::<usize>().ok())
7813 .unwrap_or(0);
7814 let batch_admitted = batch_k > 0
7815 && self.can_prefill_batched()
7816 && self.o1_active()
7817 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
7818 && (0..self.num_layers).all(|li| {
7819 let cache = &self.kv_cache.layers[self.phys_layer(li)];
7820 cache.o1.is_none() || cache.o1_views().is_some()
7821 });
7822 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7823 eprintln!(
7824 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
7825 batch_admitted,
7826 batch_k,
7827 n.saturating_sub(exact_end),
7828 );
7829 }
7830 let mut batch_completed = false;
7831 if batch_admitted && exact_end < n {
7832 let hs = self.hidden_size;
7833 let mut batch_pos = exact_end;
7834 while batch_pos < n {
7835 let end = (batch_pos + batch_k).min(n);
7836 let bk = end - batch_pos;
7837 let mut hiddens = vec![0.0f32; bk * hs];
7838 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
7839 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
7840 }
7841 let positions: Vec<usize> = (batch_pos..end).collect();
7842 let t_batch = std::time::Instant::now();
7843 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
7844 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7845 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
7846 eprintln!(
7847 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
7848 batch_pos,
7849 end.saturating_sub(1),
7850 bk as f64 / (ms / 1000.0),
7851 );
7852 }
7853 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
7854 self.nll_end();
7855 return Err(err);
7856 }
7857 match outcome {
7858 crate::gpu::BatchGraphOutcome::Completed => {
7859 batch_completed = true;
7860 for row in 0..bk {
7861 nll += self.nll_from_hidden(
7862 &hiddens[row * hs..(row + 1) * hs],
7863 ids[batch_pos + row + 1],
7864 batch_pos + row,
7865 );
7866 cnt += 1;
7867 }
7868 batch_pos = end;
7869 }
7870 crate::gpu::BatchGraphOutcome::Declined => {
7871 if batch_completed {
7872 self.nll_end();
7873 return Err(format!(
7874 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
7875 ));
7876 }
7877 break;
7878 }
7879 crate::gpu::BatchGraphOutcome::Failed => {
7880 self.nll_end();
7881 return Err(format!(
7882 "O(1) NLL batch graph failed after admission at position {batch_pos}"
7883 ));
7884 }
7885 }
7886 }
7887 if batch_completed && cnt == n.saturating_sub(requested_start) {
7888 self.nll_end();
7889 return Ok((nll, cnt));
7890 }
7891 }
7892
7893 for pos in exact_end..n {
7898 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7899 if self
7900 .graph_failed
7901 .swap(false, std::sync::atomic::Ordering::Relaxed)
7902 {
7903 self.cancel
7904 .store(false, std::sync::atomic::Ordering::Relaxed);
7905 self.nll_end();
7906 return Err(format!(
7907 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
7908 ));
7909 }
7910 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
7911 cnt += 1;
7912 }
7913 self.nll_end();
7914 Ok((nll, cnt))
7915 }
7916
7917 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
7925 self.clear_sequence_state();
7926 let n = ids.len().saturating_sub(1);
7927 let mut correct = Vec::with_capacity(n);
7928 let mut pmax = Vec::with_capacity(n);
7929 for pos in 0..n {
7930 let emb = self.embed_single(ids[pos]);
7931 let hidden = self.forward_layers(&emb, pos, None);
7932 let normed = inference::rms_norm(
7933 &hidden,
7934 &self.weights.final_norm,
7935 self.rms_eps,
7936 self.norm_style,
7937 );
7938 let logits = self.lm_head_forward(&normed);
7942 let target = ids[pos + 1] as usize;
7943 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
7944 for (i, &v) in logits.iter().enumerate() {
7945 if v > mval {
7946 mval = v;
7947 amax = i;
7948 }
7949 }
7950 correct.push(amax == target);
7951 let row: Vec<f32> = temps
7952 .iter()
7953 .map(|&t| {
7954 let tt = t.max(1e-3);
7955 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
7956 1.0 / s.max(1e-12) })
7958 .collect();
7959 pmax.push(row);
7960 }
7961 self.clear_sequence_state();
7962 (correct, pmax)
7963 }
7964
7965 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
7972 if self.dyn_router.is_none() {
7973 return Ok((self.ppl_ids(ids)?, 0));
7974 }
7975 self.nll_begin()?;
7976 let saved_active = self.dyn_active;
7977 let mut router = self
7978 .dyn_router
7979 .take()
7980 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
7981 router.reset();
7982 self.dyn_phi_seen = 0;
7983 let _ = self.set_active_skill(None);
7984
7985 let result: Result<(f64, usize), String> = (|| {
7986 let mut nll = 0f64;
7987 let mut cnt = 0usize;
7988 for pos in 0..ids.len().saturating_sub(1) {
7989 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
7990 self.nll_check_graph("dynamic serial forward", pos)?;
7991 let out_of_band = self.graph_logits.take();
7992 let mut logits = match out_of_band {
7993 Some(lg) => lg,
7994 None => {
7995 let normed = inference::rms_norm(
7996 &hidden,
7997 &self.weights.final_norm,
7998 self.rms_eps,
7999 self.norm_style,
8000 );
8001 self.lm_head_forward(&normed)
8005 }
8006 };
8007 let target = ids[pos + 1] as usize;
8008 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8009 let lse: f64 = logits
8010 .iter()
8011 .map(|&v| ((v - max) as f64).exp())
8012 .sum::<f64>()
8013 .ln()
8014 + max as f64;
8015 let tok_nll = lse - logits[target] as f64;
8016 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8017 let top = logits
8018 .iter()
8019 .enumerate()
8020 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8021 .map(|(i, _)| i)
8022 .unwrap_or(0);
8023 eprintln!(
8024 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8025 logits[target], logits[top]
8026 );
8027 }
8028 nll += tok_nll;
8029 cnt += 1;
8030 attention::recycle_buf(&mut logits);
8031 let phi = self.dyn_phi_ema.clone();
8033 if let Some(new_active) = router.step(&phi, pos) {
8034 let _ = self.set_active_skill(new_active);
8035 }
8036 }
8037 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
8038 })();
8039
8040 let _ = self.set_active_skill(saved_active);
8043 self.dyn_router = Some(router);
8044 self.nll_end();
8045 result
8046 }
8047
8048 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
8050 self.clear_sequence_state();
8051 let mut acc = vec![0f32; self.hidden_size];
8052 for (pos, &id) in ids.iter().enumerate() {
8053 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
8054 for (a, v) in acc.iter_mut().zip(&h) {
8055 *a += v;
8056 }
8057 }
8058 let n = ids.len().max(1) as f32;
8059 for a in acc.iter_mut() {
8060 *a /= n;
8061 }
8062 self.clear_sequence_state();
8063 acc
8064 }
8065
8066 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
8072 self.prefill_batch_masked(ids, start_pos, None)
8073 }
8074
8075 fn prefill_batch_masked(
8081 &mut self,
8082 ids: &[u32],
8083 start_pos: usize,
8084 task_mask: Option<&TaskMask>,
8085 ) -> Vec<f32> {
8086 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
8087 }
8088
8089 fn prefill_rows(
8097 &mut self,
8098 ids: &[u32],
8099 pos: usize,
8100 task_mask: Option<&TaskMask>,
8101 ) -> Result<Vec<f32>, String> {
8102 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
8103 }
8104
8105 fn prefill_input_rows(
8106 &mut self,
8107 input: PrefillIn<'_>,
8108 pos: usize,
8109 task_mask: Option<&TaskMask>,
8110 ) -> Result<Vec<f32>, String> {
8111 self.mimo_moe_prepare();
8112 let hs = self.hidden_size;
8113 let bk = match input {
8114 PrefillIn::Ids(ids) => ids.len(),
8115 PrefillIn::Hidden(rows) => rows.len() / hs,
8116 };
8117 #[cfg(not(target_os = "macos"))]
8118 if task_mask.is_none()
8119 && !self.o1_active()
8120 && bk > 1
8121 && (self.batch_prefix_prefill()
8122 || (self.verify_exact_moe
8123 && crate::gpu::enabled_here()
8124 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
8125 {
8126 let mut hiddens = match input {
8127 PrefillIn::Hidden(rows) => rows.to_vec(),
8128 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
8129 };
8130 let positions: Vec<usize> = (pos..pos + bk).collect();
8131 let mut run = 0usize;
8132 match self.try_batch_graph_wgpu_prefix(
8133 &mut hiddens,
8134 &positions,
8135 bk,
8136 None,
8137 Some(&mut run),
8138 ) {
8139 crate::gpu::BatchGraphOutcome::Completed => {
8140 let out = if run < self.num_layers {
8141 self.prefill_batch_span(
8142 PrefillIn::Hidden(&hiddens),
8143 pos,
8144 None,
8145 run,
8146 self.num_layers,
8147 )
8148 } else {
8149 hiddens
8150 };
8151 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8152 Err("MiMo attention graph failed after admission".into())
8153 } else { Ok(out) };
8154 }
8155 crate::gpu::BatchGraphOutcome::Failed => {
8156 return Err("batched prefix prefill failed after admission".into());
8157 }
8158 crate::gpu::BatchGraphOutcome::Declined => {
8159 #[cfg(feature = "gpu")]
8161 self.pull_lagging_host_kv(0, self.num_layers, pos);
8162 }
8163 }
8164 }
8165 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
8166 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8167 Err("batch tail graph failed after admission".into())
8168 } else { Ok(out) }
8169 }
8170
8171 fn prefill_batch_span(
8177 &mut self,
8178 input: PrefillIn<'_>,
8179 start_pos: usize,
8180 task_mask: Option<&TaskMask>,
8181 from: usize,
8182 upto_excl: usize,
8183 ) -> Vec<f32> {
8184 let hs = self.hidden_size;
8185 let b = match input {
8186 PrefillIn::Ids(ids) => ids.len(),
8187 PrefillIn::Hidden(hb) => hb.len() / hs,
8188 };
8189 let upto_excl = upto_excl.min(self.num_layers);
8190 let mut h: Vec<f32>;
8194 let mut h_ready;
8195 match input {
8196 PrefillIn::Ids(_) => {
8197 h = vec![0.0; b * hs];
8198 h_ready = false;
8199 }
8200 PrefillIn::Hidden(hb) => {
8201 h = hb.to_vec();
8202 h_ready = true;
8203 }
8204 }
8205 let fill_h = |h: &mut Vec<f32>, me: &Self| {
8206 if let PrefillIn::Ids(ids) = input {
8207 for (bi, &id) in ids.iter().enumerate() {
8208 let e = me.embed_single(id);
8209 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
8210 }
8211 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8212 if let Ok(t) = tp.parse::<usize>() {
8213 if t >= start_pos && t < start_pos + ids.len() {
8214 let bi = t - start_pos;
8215 let row = &h[bi * hs..(bi + 1) * hs];
8216 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
8217 eprintln!(
8218 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
8219 ids[bi],
8220 row[0],
8221 row[1],
8222 ids.len(),
8223 &ids[..ids.len().min(8)]
8224 );
8225 }
8226 }
8227 }
8228 }
8229 };
8230 let (_nkv, _hd, _rd, eps) = (
8231 self.num_kv_heads,
8232 self.head_dim,
8233 self.rotary_dim,
8234 self.rms_eps,
8235 );
8236 let pool = self.pool.clone();
8237 let norm_style = self.norm_style;
8238 self.mimo_moe_prepare();
8239 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8240
8241 #[cfg(target_os = "macos")]
8242 let mut chunk_skip_until = 0usize;
8243 for li in from..upto_excl {
8244 let _capacity_tail = automatic_gpu_prefix
8245 .filter(|&prefix| {
8246 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
8247 })
8248 .map(|_| crate::gpu::enter_cpu_scope());
8249 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
8256 if task_mask.is_none() {
8257 if li < chunk_skip_until {
8258 continue;
8259 }
8260 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
8266 fill_h(&mut h, self);
8267 h_ready = true;
8268 }
8269 let ids_for_embed = match input {
8270 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
8271 PrefillIn::Hidden(_) => None,
8272 };
8273 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
8274 if end > li {
8275 h_ready = true;
8276 chunk_skip_until = end;
8277 if self.is_loop_end(end - 1) && end < self.num_layers {
8280 for bi in 0..b {
8281 let normed = inference::rms_norm(
8282 &h[bi * hs..(bi + 1) * hs],
8283 &self.weights.final_norm,
8284 eps,
8285 norm_style,
8286 );
8287 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
8288 }
8289 }
8290 continue;
8291 }
8292 }
8293 if !h_ready {
8294 fill_h(&mut h, self);
8295 h_ready = true;
8296 }
8297 if task_mask.is_none() && self.verify_exact_moe {
8298 let positions: Vec<_> = (start_pos..start_pos + b).collect();
8299 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
8300 crate::gpu::BatchGraphOutcome::Completed => continue,
8301 crate::gpu::BatchGraphOutcome::Failed => return h,
8302 crate::gpu::BatchGraphOutcome::Declined => {},
8303 }
8304 }
8305 #[cfg(feature = "gpu")]
8306 self.pull_lagging_host_kv(li, li + 1, start_pos);
8307 let lw = &self.weights.layers[self.phys_layer(li)];
8308 match &lw.attn {
8310 AttnKind::Kda(w) => {
8311 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
8313 let mut normed = vec![0.0f32; b * hs];
8314 for bi in 0..b {
8315 inference::rms_norm_into(
8316 &h[bi * hs..(bi + 1) * hs],
8317 &lw.input_norm,
8318 eps,
8319 norm_style,
8320 &mut normed[bi * hs..(bi + 1) * hs],
8321 );
8322 }
8323 let attn = crate::linear_core::kda_forward_batch(
8324 &normed,
8325 b,
8326 w,
8327 &cfg,
8328 &mut self.kv_cache.layers[li].linear_state,
8329 pool.as_deref(),
8330 );
8331 for (dst, &a) in h.iter_mut().zip(&attn) {
8332 *dst += a;
8333 }
8334 }
8335 AttnKind::LinearGdn(w) => {
8336 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8338 let mut normed = vec![0.0f32; b * hs];
8339 for bi in 0..b {
8340 let r = inference::rms_norm(
8341 &h[bi * hs..(bi + 1) * hs],
8342 &lw.input_norm,
8343 eps,
8344 norm_style,
8345 );
8346 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
8347 }
8348 let attn = crate::linear_core::gdn_forward_batch(
8349 &normed,
8350 b,
8351 w,
8352 &cfg,
8353 &mut self.kv_cache.layers[li].linear_state,
8354 pool.as_deref(),
8355 );
8356 for (dst, &a) in h.iter_mut().zip(&attn) {
8357 *dst += a;
8358 }
8359 }
8360 AttnKind::ShortConv(w) => {
8361 let cfg = self
8364 .short_conv_cfg
8365 .expect("short-conv layer without short_conv_cfg");
8366 let mut normed = vec![0.0f32; b * hs];
8367 for bi in 0..b {
8368 inference::rms_norm_into(
8369 &h[bi * hs..(bi + 1) * hs],
8370 &lw.input_norm,
8371 eps,
8372 norm_style,
8373 &mut normed[bi * hs..(bi + 1) * hs],
8374 );
8375 }
8376 let attn = short_conv_forward_batch(
8377 &normed,
8378 b,
8379 w,
8380 &cfg,
8381 &mut self.kv_cache.layers[li].linear_state,
8382 pool.as_deref(),
8383 );
8384 for (dst, &a) in h.iter_mut().zip(&attn) {
8385 *dst += a;
8386 }
8387 }
8388 AttnKind::Mla(w) => {
8389 let inv_freq_l = self.layer_inv_freq(li);
8392 let rs = self.layer_rope_scale(li);
8393 let mut normed = vec![0.0f32; hs];
8394 for bi in 0..b {
8395 inference::rms_norm_into(
8396 &h[bi * hs..(bi + 1) * hs],
8397 &lw.input_norm,
8398 eps,
8399 norm_style,
8400 &mut normed,
8401 );
8402 let ao = mla_attention(
8403 w,
8404 &normed,
8405 &mut self.kv_cache.layers[li],
8406 start_pos + bi,
8407 &inv_freq_l,
8408 rs,
8409 eps,
8410 pool.as_deref(),
8411 );
8412 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
8413 *dst += a;
8414 }
8415 }
8416 }
8417 AttnKind::Full {
8418 wq,
8419 wk,
8420 wv,
8421 wo,
8422 q_norm,
8423 k_norm,
8424 output_gate,
8425 softplus_gate,
8426 bias,
8427 } => {
8428 let mut normed = vec![0.0f32; b * hs];
8432 for bi in 0..b {
8433 inference::rms_norm_into(
8434 &h[bi * hs..(bi + 1) * hs],
8435 &lw.input_norm,
8436 eps,
8437 norm_style,
8438 &mut normed[bi * hs..(bi + 1) * hs],
8439 );
8440 }
8441 let inv_freq_l = self.layer_inv_freq(li);
8442 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8443 let cfg = QwenAttnCfg {
8444 num_heads: self.layer_num_heads(li),
8445 num_kv_heads: nkv_l,
8446 head_dim: hd_l,
8447 hidden_size: hs,
8448 position: start_pos,
8449 inv_freq: &inv_freq_l,
8450 rotary_dim: rd_l,
8451 scale: self.attn_scale,
8452 softcap: self.attn_softcap,
8453 window: self.layer_window(li),
8454 v_norm: self.attn_v_norm,
8455 qk_norm_after_rope: self.qk_norm_after_rope,
8456 q_norm: q_norm.as_deref(),
8457 k_norm: k_norm.as_deref(),
8458 output_gate: *output_gate,
8459 softplus_gate: softplus_gate
8460 .as_ref()
8461 .map(|(gate, per_head)| (gate, *per_head)),
8462 rope_scale: self.layer_rope_scale(li),
8463 bias: bias
8464 .as_ref()
8465 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8466 rms_eps: eps,
8467 norm_style,
8468 pool: pool.as_deref(),
8469 v_head_dim: self.layer_v_dim(li),
8470 };
8471 let mut attn = attention::qwen_attention_batch(
8472 &normed,
8473 b,
8474 wq,
8475 wk,
8476 wv,
8477 wo,
8478 &mut self.kv_cache.layers[li],
8479 &cfg,
8480 );
8481 if let Some(w) = &lw.attn_out_norm {
8482 for bi in 0..b {
8483 inference::rms_norm_into(
8484 &attn[bi * hs..(bi + 1) * hs],
8485 w,
8486 eps,
8487 norm_style,
8488 &mut normed[bi * hs..(bi + 1) * hs],
8489 );
8490 }
8491 attn.copy_from_slice(&normed);
8492 }
8493 for (dst, &a) in h.iter_mut().zip(&attn) {
8494 *dst += a;
8495 }
8496 }
8497 AttnKind::Linear(w) => {
8498 for bi in 0..b {
8499 let normed = inference::rms_norm(
8500 &h[bi * hs..(bi + 1) * hs],
8501 &lw.input_norm,
8502 eps,
8503 norm_style,
8504 );
8505 vmf_phase_forward(
8506 &normed,
8507 w,
8508 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
8509 &mut self.kv_cache.layers[li].linear_state,
8510 pool.as_deref(),
8511 )
8512 .iter()
8513 .enumerate()
8514 .for_each(|(i, &a)| h[bi * hs + i] += a);
8515 }
8516 }
8517 }
8518
8519 let lw = &self.weights.layers[self.phys_layer(li)];
8521 let mut post = vec![0.0f32; b * hs];
8522 for bi in 0..b {
8523 let r =
8524 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
8525 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
8526 }
8527 let mask_row = task_mask
8530 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
8531 .and_then(|m| m.ffn_masks.get(li))
8532 .map(|v| v.as_slice());
8533 let mut ffn = match &lw.ffn {
8534 FfnKind::Dense(d) if !d.segs.is_empty() => {
8535 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
8536 }
8537 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
8538 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
8539 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
8540 }
8541 FfnKind::Moe(m) if self.verify_exact_moe => {
8542 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
8543 }
8544 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
8547 let before = m.stats.borrow().clone();
8548 let out = crate::gpu::cpu_scope(|| {
8549 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
8550 });
8551 self.mimo_moe.prime(li, m, &before);
8552 out
8553 }
8554 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
8555 FfnKind::DenseMoe(dm) => {
8558 let mut out = vec![0.0f32; b * hs];
8559 for bi in 0..b {
8560 let r = dense_moe_ffn(
8561 dm,
8562 &post[bi * hs..(bi + 1) * hs],
8563 &h[bi * hs..(bi + 1) * hs],
8564 eps,
8565 norm_style,
8566 pool.as_deref(),
8567 );
8568 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
8569 }
8570 out
8571 }
8572 };
8573 if let Some(w) = &lw.ffn_out_norm {
8574 for bi in 0..b {
8575 inference::rms_norm_into(
8576 &ffn[bi * hs..(bi + 1) * hs],
8577 w,
8578 eps,
8579 norm_style,
8580 &mut post[bi * hs..(bi + 1) * hs],
8581 );
8582 }
8583 ffn.copy_from_slice(&post);
8584 }
8585 for (dst, &f) in h.iter_mut().zip(&ffn) {
8586 *dst += f;
8587 }
8588 if let Some(sc) = lw.layer_scale {
8589 for v in h.iter_mut() {
8590 *v *= sc;
8591 }
8592 }
8593 if self.layer_dump.is_some() {
8595 for bi in 0..b {
8596 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
8597 }
8598 }
8599 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8600 if let Ok(t) = tp.parse::<usize>() {
8601 if t >= start_pos && t < start_pos + b {
8602 let bi = t - start_pos;
8603 let row = &h[bi * hs..(bi + 1) * hs];
8604 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
8605 eprintln!(
8606 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
8607 row[0], row[1]
8608 );
8609 }
8610 }
8611 }
8612 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
8616 let row = &h[(b - 1) * hs..b * hs];
8617 let rms =
8618 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
8619 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
8620 eprintln!(
8621 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
8622 match &self.weights.layers[self.phys_layer(li)].attn {
8623 AttnKind::LinearGdn(_) => "gdn",
8624 AttnKind::Linear(_) => "vmf",
8625 AttnKind::ShortConv(_) => "conv",
8626 _ => "attn",
8627 },
8628 match &lw.ffn {
8629 FfnKind::Moe(_) => "moe",
8630 FfnKind::Dense(_) => "dense",
8631 FfnKind::DenseMoe(_) => "dense+moe",
8632 },
8633 );
8634 }
8635 if self.is_loop_end(li) && li + 1 < self.num_layers {
8637 for bi in 0..b {
8638 let normed = inference::rms_norm(
8639 &h[bi * hs..(bi + 1) * hs],
8640 &self.weights.final_norm,
8641 eps,
8642 norm_style,
8643 );
8644 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
8645 }
8646 }
8647 if std::env::var("CMF_TRACE_H").is_ok() {
8648 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
8649 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
8650 eprintln!(
8651 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
8652 lw.layer_scale
8653 );
8654 }
8655 }
8656 crate::gpu::set_layer(-1); self.o1_progress();
8662 h
8663 }
8664
8665 fn embed_single(&self, id: u32) -> Vec<f32> {
8667 let mut out = vec![0.0f32; self.hidden_size];
8668 if (id as usize) < self.weights.embed_tokens.rows() {
8669 self.weights.embed_tokens.row_f32(id as usize, &mut out);
8670 }
8671 if self.embed_multiplier != 1.0 {
8672 for v in out.iter_mut() {
8673 *v *= self.embed_multiplier;
8674 }
8675 }
8676 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
8680 let mut v = vec![0.0f32; self.hidden_size.max(1)];
8681 v[0] = id as f32;
8682 return v;
8683 }
8684 if let Some(b) = &self.g3n {
8687 return b.0.extend_embedding(id, &out, self.pool.as_deref());
8688 }
8689 out
8690 }
8691
8692 #[cfg(target_os = "macos")]
8698 fn chunk_run_gpu(
8699 &mut self,
8700 li0: usize,
8701 h: &mut [f32],
8702 b: usize,
8703 pos0: usize,
8704 embed_ids: Option<&[u32]>,
8705 cap: usize,
8706 ) -> usize {
8707 if !crate::gpu::enabled_here()
8711 || std::env::var("CMF_GPU_CHUNK")
8712 .map(|v| v == "0")
8713 .unwrap_or(false)
8714 || b < 32
8715 || self.swa.is_some()
8716 || self.global_attn.is_some()
8717 || self.graph_attn_decline_reason().is_some()
8719 || self.o1_active()
8722 || self.attn_v_norm
8723 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
8724 {
8725 return li0;
8726 }
8727 let Some(model) = self.model.clone() else {
8728 return li0;
8729 };
8730 let inv_freq = self.inv_freq.clone();
8731 let (nh, nkv, hd, hs) = (
8732 self.num_heads,
8733 self.num_kv_heads,
8734 self.head_dim,
8735 self.hidden_size,
8736 );
8737 let loop_end = if self.loop_final_norm {
8741 ((li0 / self.physical_layers) + 1) * self.physical_layers
8742 } else {
8743 self.num_layers
8744 };
8745 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
8746 let mut stored_at: Vec<usize> = Vec::new();
8747 for li in li0..self.num_layers.min(loop_end).min(cap) {
8748 let lw = &self.weights.layers[self.phys_layer(li)];
8749 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8750 break;
8751 }
8752 let AttnKind::Full {
8753 wq,
8754 wk,
8755 wv,
8756 wo,
8757 q_norm,
8758 k_norm,
8759 output_gate: false,
8760 softplus_gate: None,
8761 bias,
8762 } = &lw.attn
8763 else {
8764 break;
8765 };
8766 let FfnKind::Dense(d) = &lw.ffn else { break };
8767 if d.act != Act::Silu || !d.segs.is_empty() {
8768 break;
8769 }
8770 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
8775 t.q8_row_parts()
8776 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
8777 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
8778 }
8779 let parts = (
8780 cw(wq),
8781 cw(wk),
8782 cw(wv),
8783 cw(wo),
8784 cw(&d.gate_proj),
8785 cw(&d.up_proj),
8786 cw(&d.down_proj),
8787 );
8788 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
8789 else {
8790 break;
8791 };
8792 let layer = &self.kv_cache.layers[li];
8793 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
8794 break;
8795 }
8796 stored_at.push(layer.head_len(0));
8797 layers.push(crate::gpu_metal::ChunkLayer {
8798 model: &model,
8799 kv_id: self.graph_kv_id,
8800 layer: li,
8801 wq: pq,
8802 wk: pk,
8803 wv: pv,
8804 wo: po,
8805 gate: pg,
8806 up: pu,
8807 down: pd,
8808 input_norm: &lw.input_norm,
8809 post_norm: &lw.post_norm,
8810 bias: bias
8811 .as_ref()
8812 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
8813 q_norm: q_norm.as_deref(),
8814 k_norm: k_norm.as_deref(),
8815 inv_freq: &inv_freq,
8816 rd: self.rotary_dim,
8817 nh,
8818 nkv,
8819 hd,
8820 hs,
8821 inter: d.gate_proj.rows(),
8822 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
8823 late_qk_norm: self.qk_norm_after_rope,
8824 eps: self.rms_eps as f32,
8825 });
8826 }
8827 if layers.is_empty() {
8828 return li0;
8829 }
8830 let row = nkv * hd;
8831 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
8832 .iter()
8833 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
8834 .collect();
8835 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
8836 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
8837 let li = layers[i].layer;
8838 let layer = &self.kv_cache.layers[li];
8839 io.push(crate::gpu_metal::ChunkIo {
8840 cpu_stored: stored_at[i],
8841 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
8842 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
8843 out_k: ok,
8844 out_v: ov,
8845 imp: oi,
8846 });
8847 }
8848 let n_run = layers.len();
8849 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
8850 let ep = embed_ids.and_then(|ids| {
8853 self.weights
8854 .embed_tokens
8855 .q8_row_parts()
8856 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
8857 idx,
8858 rows,
8859 row_scale: rs,
8860 ids,
8861 mult: self.embed_multiplier,
8862 })
8863 });
8864 if embed_ids.is_some() && ep.is_none() {
8865 return li0;
8866 }
8867 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
8868 return li0;
8869 }
8870 drop(io);
8871 drop(layers);
8872 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
8875 let li = li0 + i;
8876 let layer = &mut self.kv_cache.layers[li];
8877 for bi in 0..b {
8878 layer.append(
8879 &ok[bi * row..(bi + 1) * row],
8880 &ov[bi * row..(bi + 1) * row],
8881 &[],
8882 );
8883 }
8884 layer.accumulate_imp(oi);
8885 }
8886 last
8887 }
8888
8889 fn layer_is_local(&self, li: usize) -> bool {
8892 if let Some(layers) = &self.sliding_layers {
8893 return layers.get(li).copied().unwrap_or(false);
8894 }
8895 match self.swa {
8896 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
8897 None => false,
8898 }
8899 }
8900
8901 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
8904 if self.layer_is_local(li) {
8905 if let Some(f) = &self.inv_freq_local {
8906 return f.clone();
8907 }
8908 } else if let Some(f) = &self.inv_freq_global {
8909 return f.clone();
8910 }
8911 self.inv_freq.clone()
8912 }
8913
8914 fn layer_window(&self, li: usize) -> Option<usize> {
8916 self.swa
8917 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
8918 }
8919
8920 fn layer_num_heads(&self, li: usize) -> usize {
8921 self.attention_heads_per_layer
8922 .as_ref()
8923 .and_then(|v| v.get(li).copied())
8924 .unwrap_or(self.num_heads)
8925 }
8926
8927 fn layer_rope_scale(&self, li: usize) -> f32 {
8928 if self.layer_is_local(li) {
8929 self.rope_scale_local
8930 } else {
8931 self.rope_scale
8932 }
8933 }
8934
8935 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
8938 if !self.layer_is_local(li) {
8939 if let Some((ghd, gkv)) = self.global_attn {
8940 return (gkv, ghd, ghd);
8941 }
8942 }
8943 (
8944 self.layer_num_kv_heads(li),
8945 self.head_dim,
8946 if self.layer_is_local(li) {
8947 self.rotary_dim_local.unwrap_or(self.rotary_dim)
8948 } else {
8949 self.rotary_dim
8950 },
8951 )
8952 }
8953
8954 fn layer_num_kv_heads(&self, li: usize) -> usize {
8957 self.kv_heads_per_layer
8958 .as_ref()
8959 .and_then(|v| v.get(self.phys_layer(li)).copied())
8960 .unwrap_or(self.num_kv_heads)
8961 }
8962
8963 fn layer_v_dim(&self, li: usize) -> usize {
8965 let (_, hd, _) = self.layer_geom(li);
8966 self.v_head_dim.unwrap_or(hd).min(hd)
8967 }
8968
8969 pub fn set_attn_geometry(
8978 &mut self,
8979 kv_heads_per_layer: Option<Vec<usize>>,
8980 v_head_dim: Option<usize>,
8981 ) -> Result<(), String> {
8982 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
8983 if self.global_attn.is_some() {
8984 return Err(
8985 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
8986 attention geometry"
8987 .into(),
8988 );
8989 }
8990 if self
8991 .weights
8992 .layers
8993 .iter()
8994 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
8995 {
8996 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
8997 }
8998 }
8999 if let Some(vd) = v_head_dim {
9000 if vd == 0 || vd > self.head_dim {
9001 return Err(format!(
9002 "v_head_dim {vd} must be in 1..={} (head_dim)",
9003 self.head_dim
9004 ));
9005 }
9006 }
9007 if let Some(v) = &kv_heads_per_layer {
9008 if v.len() != self.physical_layers {
9009 return Err(format!(
9010 "kv_heads_per_layer has {} entries, expected {} layers",
9011 v.len(),
9012 self.physical_layers
9013 ));
9014 }
9015 for (li, &nkv) in v.iter().enumerate() {
9016 let is_attn = matches!(
9017 self.weights.layers.get(li).map(|lw| &lw.attn),
9018 Some(AttnKind::Full { .. }) | None
9019 );
9020 if !is_attn {
9021 continue;
9022 }
9023 let nh = self
9024 .attention_heads_per_layer
9025 .as_ref()
9026 .and_then(|h| h.get(li).copied())
9027 .unwrap_or(self.num_heads);
9028 if nkv == 0 || nh % nkv != 0 {
9029 return Err(format!(
9030 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
9031 ));
9032 }
9033 }
9034 }
9035 self.kv_heads_per_layer = kv_heads_per_layer;
9036 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
9037 if self.kv_heads_per_layer.is_some() {
9038 for li in 0..self.kv_cache.layers.len() {
9039 let full = matches!(
9040 self.weights
9041 .layers
9042 .get(self.phys_layer(li))
9043 .map(|lw| &lw.attn),
9044 Some(AttnKind::Full { .. })
9045 );
9046 let nkv = self.layer_num_kv_heads(li);
9047 let cache = &self.kv_cache.layers[li];
9048 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
9049 let sinks = cache.sinks.clone();
9050 self.kv_cache.layers[li] =
9051 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
9052 self.kv_cache.layers[li].sinks = sinks;
9053 }
9054 }
9055 }
9056 Ok(())
9057 }
9058
9059 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
9063 let Some(lw) = self.weights.layers.get(phys) else {
9064 return Err(format!("sinks for layer {phys}: no such layer"));
9065 };
9066 if !matches!(lw.attn, AttnKind::Full { .. }) {
9067 return Err(format!(
9068 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
9069 ));
9070 }
9071 let nh = self
9072 .attention_heads_per_layer
9073 .as_ref()
9074 .and_then(|h| h.get(phys).copied())
9075 .unwrap_or(self.num_heads);
9076 if sinks.len() != nh {
9077 return Err(format!(
9078 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
9079 sinks.len()
9080 ));
9081 }
9082 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
9083 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
9084 }
9085 for li in 0..self.kv_cache.layers.len() {
9086 if self.phys_layer(li) == phys {
9087 self.kv_cache.layers[li].sinks = Some(sinks.clone());
9088 }
9089 }
9090 Ok(())
9091 }
9092
9093 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
9103 if self.kv_heads_per_layer.is_some() {
9104 return Some("per-layer KV head counts");
9105 }
9106 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
9107 return Some("V heads narrower than Q/K heads");
9108 }
9109 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
9110 return Some("learned attention sinks");
9111 }
9112 if self.swa.is_some() || self.sliding_layers.is_some() {
9113 return Some("sliding-window layers");
9114 }
9115 None
9116 }
9117
9118 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
9125 self.graph_attn_decline_reason()?;
9126 if self.global_attn.is_some() {
9127 return Some("per-layer head width (Gemma-4 global layers) with per-layer geometry");
9128 }
9129 if self.attention_heads_per_layer.is_some() {
9130 return Some("per-layer Q head counts with per-layer geometry");
9131 }
9132 if self.attn_v_norm {
9133 return Some("V norm with per-layer geometry");
9134 }
9135 if (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0) {
9136 return Some("scaled RoPE positions with per-layer geometry");
9137 }
9138 if self.weights.layers.iter().any(|lw| {
9139 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
9140 }) {
9141 return Some("sandwich norms / layer scale with per-layer geometry");
9142 }
9143 if self.weights.layers.iter().any(|lw| {
9144 matches!(
9145 &lw.attn,
9146 AttnKind::Full {
9147 output_gate: true,
9148 ..
9149 }
9150 )
9151 }) && self.v_head_dim.is_some()
9152 {
9153 return Some("gated attention with V narrower than K");
9154 }
9155 if (0..self.num_layers).any(|li| {
9156 self.layer_is_local(li)
9157 && self.inv_freq_local.is_none()
9158 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
9159 }) {
9160 return Some("local rotary width without a local RoPE table");
9161 }
9162 None
9163 }
9164
9165 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
9170 self.graph_attn_decline_reason()?;
9171 let (nkv, _hd, rd) = self.layer_geom(li);
9172 let invf: &[f32] = if self.layer_is_local(li) {
9173 match &self.inv_freq_local {
9174 Some(f) => f.as_slice(),
9175 None => self.inv_freq.as_slice(),
9176 }
9177 } else {
9178 match &self.inv_freq_global {
9179 Some(f) => f.as_slice(),
9180 None => self.inv_freq.as_slice(),
9181 }
9182 };
9183 Some(crate::gpu::GraphAttnGeom {
9184 nkv,
9185 dv: self.layer_v_dim(li),
9186 rd,
9187 invf,
9188 window: self.layer_window(li),
9189 sink: self.kv_cache.layers[li].sinks.as_deref(),
9190 })
9191 }
9192
9193 #[cfg(feature = "gpu")]
9202 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
9203 let kv_id = self.graph_kv_id;
9204 for li in from..upto.min(self.num_layers) {
9205 if !matches!(
9206 self.weights.layers[self.phys_layer(li)].attn,
9207 AttnKind::Full { .. }
9208 ) {
9209 continue;
9210 }
9211 let host = self.kv_cache.layers[li].seq_len;
9212 if host >= position {
9213 continue;
9214 }
9215 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
9216 continue;
9217 };
9218 let to = dev.min(position);
9219 if to <= host {
9220 continue;
9221 }
9222 let (nkv, hd) = {
9223 let c = &self.kv_cache.layers[li];
9224 (c.num_kv_heads, c.head_dim)
9225 };
9226 let Some((k, v, first_valid)) =
9227 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
9228 else {
9229 continue;
9230 };
9231 let need_from = match self.layer_window(li) {
9234 Some(w) => host.max((position + 1).saturating_sub(w)),
9235 None => host,
9236 };
9237 if first_valid > need_from {
9238 tracing::warn!(
9239 "layer {li}: device KV rows {host}..{to} no longer resident \
9240 (from {first_valid}); host attention will miss them"
9241 );
9242 }
9243 let row = nkv * hd;
9244 let cache = &mut self.kv_cache.layers[li];
9245 for p in 0..to - host {
9246 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
9247 }
9248 }
9249 }
9250
9251 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
9254 let mut seen = self.graph_declines.borrow_mut();
9255 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
9256 tracing::warn!("{site} declined: {reason} (CPU attention path)");
9257 seen.push((site, reason));
9258 }
9259 }
9260
9261 pub fn graph_declines(&self) -> Vec<String> {
9264 self.graph_declines
9265 .borrow()
9266 .iter()
9267 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
9268 .collect()
9269 }
9270
9271 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
9280 let Some(dir) = &self.layer_dump else {
9281 return;
9282 };
9283 let mut bytes = Vec::with_capacity(row.len() * 4);
9284 for v in row {
9285 bytes.extend_from_slice(&v.to_le_bytes());
9286 }
9287 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
9288 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
9289 use std::sync::atomic::{AtomicBool, Ordering};
9290 static SAID: AtomicBool = AtomicBool::new(false);
9291 if !SAID.swap(true, Ordering::Relaxed) {
9292 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
9293 }
9294 }
9295 }
9296
9297 fn mimo_moe_prepare(&mut self) {
9300 if !self.mimo_moe.is_undecided() {
9301 return;
9302 }
9303 let slot = {
9304 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
9305 .filter_map(
9306 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
9307 FfnKind::Moe(m) => Some((li, m)),
9308 _ => None,
9309 },
9310 )
9311 .collect();
9312 if layers.is_empty()
9315 || self.physical_layers != self.num_layers
9316 || self.gpu_plan.is_some()
9317 {
9318 crate::mimo_moe::Slot::Off
9319 } else {
9320 let graph_prefix =
9324 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
9325 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
9326 }
9327 };
9328 self.mimo_moe = slot;
9329 }
9330
9331 #[cfg(test)]
9332 pub(crate) fn test_graph_kv_id(&self) -> u64 {
9333 self.graph_kv_id
9334 }
9335
9336 pub(crate) fn mimo_graph_layer_rows(
9340 &mut self,
9341 li: usize,
9342 h: &mut [f32],
9343 positions: &[usize],
9344 ) -> crate::gpu::BatchGraphOutcome {
9345 use crate::gpu::BatchGraphOutcome as Out;
9346 let b = positions.len();
9347 if !(1..=4).contains(&b)
9348 || h.len() != b * self.hidden_size
9349 || !self.mimo_moe.is_dynamic(li, true)
9350 || !crate::gpu::enabled_here()
9351 || !crate::gpu::wgpu_active()
9352 || self.o1_active()
9353 || self.physical_layers != self.num_layers
9354 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
9358 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
9359 || self.wgpu_graph_attn_decline().is_some()
9360 {
9361 return Out::Declined;
9362 }
9363 let attn_started = std::time::Instant::now();
9364 let outcome = {
9365 let lw = &self.weights.layers[li];
9366 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
9367 return Out::Declined;
9368 }
9369 let FfnKind::Moe(m) = &lw.ffn else {
9370 return Out::Declined;
9371 };
9372 let AttnKind::Full {
9373 wq,
9374 wk,
9375 wv,
9376 wo,
9377 q_norm,
9378 k_norm,
9379 output_gate,
9380 softplus_gate,
9381 bias,
9382 } = &lw.attn
9383 else {
9384 return Out::Declined;
9385 };
9386 if *output_gate || softplus_gate.is_some() {
9387 return Out::Declined;
9388 }
9389 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
9390 m.experts
9391 .first()?
9392 .gate_proj
9393 .mapped_q4tp()
9394 .map(|(m, _)| m.clone())
9395 }) else {
9396 return Out::Declined;
9397 };
9398 fn gw<'a>(
9399 t: &'a QTensor,
9400 owner: &std::sync::Arc<cortiq_core::CmfModel>,
9401 ) -> Option<crate::gpu::GraphW<'a>> {
9402 if let Some((m, idx, kind, rs)) = t.graph_weight() {
9403 if m.uid() != owner.uid() || t.has_prism_contract() {
9404 return None;
9405 }
9406 return Some(crate::gpu::GraphW {
9407 idx,
9408 kind,
9409 row_scale: rs,
9410 data: &[],
9411 prism: crate::gpu::GraphPrismOp::None,
9412 affine: false,
9413 });
9414 }
9415 t.as_f32().map(|data| crate::gpu::GraphW {
9416 idx: 0,
9417 kind: 4,
9418 row_scale: &[],
9419 data,
9420 prism: crate::gpu::GraphPrismOp::None,
9421 affine: false,
9422 })
9423 }
9424 let (Some(q), Some(k), Some(v), Some(o)) = (
9425 gw(wq, &model),
9426 gw(wk, &model),
9427 gw(wv, &model),
9428 gw(wo, &model),
9429 ) else {
9430 return Out::Declined;
9431 };
9432 let layer = crate::gpu::GraphLayer {
9433 input_norm: &lw.input_norm,
9434 post_norm: &lw.post_norm,
9435 ffn: crate::gpu::GraphFfn::AttentionOnly,
9436 attn: crate::gpu::GraphAttn::Full {
9437 wq: q,
9438 wk: k,
9439 wv: v,
9440 wo: o,
9441 q_norm: q_norm.as_deref(),
9442 k_norm: k_norm.as_deref(),
9443 late_qk_norm: self.qk_norm_after_rope,
9444 bias: bias
9445 .as_ref()
9446 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
9447 output_gate: false,
9448 cpu_k: self.kv_cache.layers[li].k_heads(),
9449 cpu_v: self.kv_cache.layers[li].v_heads(),
9450 geom: self.graph_attn_geom(li),
9451 },
9452 };
9453 let (nkv, hd, rd) = self.layer_geom(li);
9454 crate::gpu::forward_batch_graph_at(
9455 &model,
9456 self.graph_kv_id,
9457 li,
9458 &[layer],
9459 &self.inv_freq,
9460 h,
9461 self.layer_num_heads(li),
9462 nkv,
9463 hd,
9464 rd,
9465 self.hidden_size,
9466 1,
9467 positions,
9468 self.kv_cache.max_seq_len,
9469 self.norm_style == cortiq_core::NormStyle::Gemma,
9470 self.rms_eps as f32,
9471 self.attn_scale,
9472 b,
9473 &[],
9474 self.o1_epoch,
9475 None,
9476 None,
9477 )
9478 };
9479 match outcome {
9480 Out::Completed => {}
9481 Out::Declined => return Out::Declined,
9482 Out::Failed => {
9483 self.graph_failed
9484 .store(true, std::sync::atomic::Ordering::Relaxed);
9485 return Out::Failed;
9486 }
9487 }
9488 let attn_ns = attn_started.elapsed().as_nanos() as u64;
9489 let hs = self.hidden_size;
9490 let lw = &self.weights.layers[li];
9491 let FfnKind::Moe(m) = &lw.ffn else {
9492 unreachable!()
9493 };
9494 let mut post = vec![0.0; h.len()];
9495 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
9496 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
9497 }
9498 let mut ffn = if b == 1 {
9499 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
9500 } else {
9501 moe_ffn_banked_rows(
9502 &mut self.mimo_moe,
9503 li,
9504 m,
9505 &post,
9506 b,
9507 hs,
9508 self.pool.as_deref(),
9509 )
9510 };
9511 for (x, &f) in h.iter_mut().zip(&ffn) {
9512 *x += f;
9513 }
9514 attention::recycle_buf(&mut ffn);
9515 if self.layer_dump.is_some() {
9516 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
9517 self.dump_layer_row(pos, li, row);
9518 }
9519 }
9520 crate::mimo_moe::note_attention_graph(b, attn_ns);
9521 Out::Completed
9522 }
9523
9524 fn layer_attn_plain(&self, li: usize) -> bool {
9525 self.kv_heads_per_layer.is_none()
9526 && self.v_head_dim.is_none()
9527 && self.global_attn.is_none()
9528 && self.layer_window(li).is_none()
9529 && self.kv_cache.layers[li].sinks.is_none()
9530 }
9531
9532 fn forward_layers(
9534 &mut self,
9535 hidden: &[f32],
9536 position: usize,
9537 task_mask: Option<&TaskMask>,
9538 ) -> Vec<f32> {
9539 let out = self.forward_layers_upto(hidden, position, task_mask, None);
9540 self.o1_progress();
9541 out
9542 }
9543
9544 pub fn embed_id(&self, id: u32) -> Vec<f32> {
9552 self.embed_single(id)
9553 }
9554
9555 pub fn split_supported(&self) -> Result<(), String> {
9559 if self.dsv4.is_some() {
9560 return Err(
9561 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
9562 );
9563 }
9564 if self.dsv41.is_some() {
9565 return Err(
9566 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
9567 .into(),
9568 );
9569 }
9570 if self.qwen4_exp.is_some() {
9571 return Err(
9572 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
9573 );
9574 }
9575 if self.g3n.is_some() {
9576 return Err(
9577 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
9578 );
9579 }
9580 Ok(())
9581 }
9582
9583 pub fn forward_span(
9588 &mut self,
9589 hidden: &[f32],
9590 position: usize,
9591 from: usize,
9592 upto: usize,
9593 task_mask: Option<&TaskMask>,
9594 ) -> Result<Vec<f32>, String> {
9595 self.split_supported()?;
9596 if from > upto || upto >= self.num_layers {
9597 return Err(format!(
9598 "forward_span: layer range {from}..={upto} outside 0..{}",
9599 self.num_layers
9600 ));
9601 }
9602 if hidden.len() != self.hidden_size {
9603 return Err(format!(
9604 "forward_span: hidden len {} ≠ hidden_size {}",
9605 hidden.len(),
9606 self.hidden_size
9607 ));
9608 }
9609 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
9610 self.o1_progress();
9611 if self
9612 .graph_failed
9613 .swap(false, std::sync::atomic::Ordering::Relaxed)
9614 {
9615 self.cancel
9616 .store(false, std::sync::atomic::Ordering::Relaxed);
9617 self.clear_sequence_state();
9618 return Err("forward_span: deferred O(1) transition failed".into());
9619 }
9620 Ok(out)
9621 }
9622
9623 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
9626 let normed = inference::rms_norm(
9627 hidden,
9628 &self.weights.final_norm,
9629 self.rms_eps,
9630 self.norm_style,
9631 );
9632 self.lm_head_forward(&normed)
9633 }
9634
9635 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
9637 sampler::sample_with_scratch(
9638 logits,
9639 &self.sampler_config,
9640 past_tokens,
9641 &mut self.rng,
9642 &mut self.sampler_scratch,
9643 )
9644 }
9645
9646 pub fn reset_session(&mut self) {
9648 self.clear_sequence_state();
9649 }
9650
9651 pub fn prefill_span_ids(
9657 &mut self,
9658 ids: &[u32],
9659 start_pos: usize,
9660 upto: usize,
9661 task_mask: Option<&TaskMask>,
9662 ) -> Result<Vec<f32>, String> {
9663 self.split_supported()?;
9664 if upto >= self.num_layers {
9665 return Err(format!(
9666 "prefill_span_ids: upto {upto} outside 0..{}",
9667 self.num_layers
9668 ));
9669 }
9670 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
9674 let out =
9675 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
9676 self.check_o1_progress_failure("prefill_span_ids")?;
9677 Ok(out)
9678 } else {
9679 let hs = self.hidden_size;
9680 let mut out = Vec::with_capacity(ids.len() * hs);
9681 for (i, &id) in ids.iter().enumerate() {
9682 let emb = self.embed_id(id);
9683 out.extend_from_slice(&self.forward_span(
9684 &emb,
9685 start_pos + i,
9686 0,
9687 upto,
9688 task_mask,
9689 )?);
9690 }
9691 Ok(out)
9692 }
9693 }
9694
9695 pub fn prefill_span_hidden(
9698 &mut self,
9699 hidden: &[f32],
9700 start_pos: usize,
9701 from: usize,
9702 upto: usize,
9703 task_mask: Option<&TaskMask>,
9704 ) -> Result<Vec<f32>, String> {
9705 self.split_supported()?;
9706 let hs = self.hidden_size;
9707 if hidden.is_empty() || hidden.len() % hs != 0 {
9708 return Err(format!(
9709 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
9710 hidden.len()
9711 ));
9712 }
9713 if from > upto || upto >= self.num_layers {
9714 return Err(format!(
9715 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
9716 self.num_layers
9717 ));
9718 }
9719 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
9720 let out = self.prefill_batch_span(
9721 PrefillIn::Hidden(hidden),
9722 start_pos,
9723 task_mask,
9724 from,
9725 upto + 1,
9726 );
9727 self.check_o1_progress_failure("prefill_span_hidden")?;
9728 Ok(out)
9729 } else {
9730 let b = hidden.len() / hs;
9731 let mut out = Vec::with_capacity(hidden.len());
9732 for i in 0..b {
9733 let h = self.forward_span(
9734 &hidden[i * hs..(i + 1) * hs],
9735 start_pos + i,
9736 from,
9737 upto,
9738 task_mask,
9739 )?;
9740 out.extend_from_slice(&h);
9741 }
9742 Ok(out)
9743 }
9744 }
9745
9746 fn try_token_graph_wgpu(
9750 &self,
9751 hidden: &[f32],
9752 position: usize,
9753 logits_out: &mut Vec<f32>,
9754 layers_run: &mut usize,
9755 ) -> Option<Result<Vec<f32>, ()>> {
9756 self.try_token_graph_wgpu_steps(
9757 hidden,
9758 position,
9759 logits_out,
9760 1,
9761 None,
9762 Some(layers_run),
9763 0,
9764 self.num_layers,
9765 )
9766 }
9767
9768 fn try_token_graph_wgpu_span(
9772 &self,
9773 hidden: &[f32],
9774 position: usize,
9775 logits_out: &mut Vec<f32>,
9776 from: usize,
9777 upto_excl: usize,
9778 layers_run: &mut usize,
9779 ) -> Option<Result<Vec<f32>, ()>> {
9780 self.try_token_graph_wgpu_steps(
9781 hidden,
9782 position,
9783 logits_out,
9784 1,
9785 None,
9786 Some(layers_run),
9787 from,
9788 upto_excl,
9789 )
9790 }
9791
9792 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
9796 if self.o1_active() || self.attn_softcap > 0.0 {
9797 return None;
9798 }
9799 if let Some(reason) = self.wgpu_graph_attn_decline() {
9802 self.note_graph_decline("wgpu multi-burst", reason);
9803 return None;
9804 }
9805 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
9806 if !graph_on || crate::gpu::graph_unsupported() {
9807 return None;
9814 }
9815 let emb = self.embed_single(t_next);
9816 let mut lg = Vec::new();
9817 let mut ids = Vec::new();
9818 match self.try_token_graph_wgpu_steps(
9819 &emb,
9820 position,
9821 &mut lg,
9822 k,
9823 Some(&mut ids),
9824 None,
9825 0,
9826 self.num_layers,
9827 ) {
9828 Some(Ok(_)) => {}
9829 Some(Err(())) => {
9830 self.graph_failed
9835 .store(true, std::sync::atomic::Ordering::Relaxed);
9836 return None;
9837 }
9838 None => return None,
9839 }
9840 (ids.len() == k).then_some(ids)
9841 }
9842
9843 fn try_token_graph_wgpu_steps(
9847 &self,
9848 hidden: &[f32],
9849 position: usize,
9850 logits_out: &mut Vec<f32>,
9851 steps: usize,
9852 ids_out: Option<&mut Vec<u32>>,
9853 layers_run: Option<&mut usize>,
9854 from: usize,
9855 upto_excl: usize,
9856 ) -> Option<Result<Vec<f32>, ()>> {
9857 let upto_excl = match self.mimo_moe.graph_prefix_end() {
9860 Some(end) if end < upto_excl => {
9861 if steps != 1 || layers_run.is_none() || from >= end {
9862 return None;
9863 }
9864 end
9865 }
9866 _ => upto_excl,
9867 };
9868 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
9871 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
9872 return None;
9876 }
9877 if let Some(reason) = self.wgpu_graph_attn_decline() {
9884 self.note_graph_decline("wgpu token graph", reason);
9885 return None;
9886 }
9887 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
9892 .map(|li| {
9893 if !o1_gpu {
9894 return None;
9895 }
9896 self.kv_cache.layers[self.phys_layer(li)].o1_views()
9897 })
9898 .collect();
9899 if self.o1_active() && o1_gpu {
9900 let want: usize = (from..upto_excl)
9903 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
9904 .count();
9905 let have = o1_views.iter().filter(|v| v.is_some()).count();
9906 if want == 0 || have != want {
9907 use std::sync::atomic::{AtomicUsize, Ordering};
9917 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
9918 let code = have * 1000 + want;
9919 if LAST.swap(code, Ordering::Relaxed) != code {
9920 tracing::warn!(
9921 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
9922 );
9923 }
9924 return None;
9925 }
9926 }
9927 let nh = self.num_heads;
9928 let (nkv, hd, rd) = self.layer_geom(0);
9929 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9930 let mut layers = Vec::with_capacity(upto_excl - from);
9931 let mut model = None;
9932 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
9933 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9934 if let Some((m, i, kind, rs)) = t
9935 .graph_weight()
9936 .or_else(|| t.graph_weight_descriptor())
9937 {
9938 let name = &m.tensors[i].name;
9939 let prism = if crate::prism::is_inverse_embedding(m, name) {
9940 crate::gpu::GraphPrismOp::InverseEmbedding
9941 } else if crate::prism::is_forward_weight(m, name) {
9942 crate::gpu::GraphPrismOp::Forward
9943 } else {
9944 crate::gpu::GraphPrismOp::None
9945 };
9946 return Some(crate::gpu::GraphW {
9947 idx: i,
9948 kind,
9949 row_scale: rs,
9950 data: &[],
9951 prism,
9952 affine: crate::prism::is_affine_target(m, name),
9953 });
9954 }
9955 match t.as_f32() {
9957 Some(d) => Some(crate::gpu::GraphW {
9958 idx: 0,
9959 kind: 4,
9960 row_scale: &[],
9961 data: d,
9962 prism: crate::gpu::GraphPrismOp::None,
9963 affine: false,
9964 }),
9965 None => {
9966 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9967 eprintln!("batch graph: weight has no graph/f32 representation");
9968 }
9969 None
9970 }
9971 }
9972 }
9973 for li in from..upto_excl {
9974 let lw = &self.weights.layers[self.phys_layer(li)];
9975 if dbg {
9976 let ak = match &lw.attn {
9977 AttnKind::Mla(_) => "Mla".into(),
9978 AttnKind::Full {
9979 output_gate, bias, ..
9980 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
9981 AttnKind::LinearGdn(_) => "LinearGdn".into(),
9982 AttnKind::Kda(_) => "Kda".into(),
9983 AttnKind::Linear(_) => "Linear".into(),
9984 AttnKind::ShortConv(_) => "ShortConv".into(),
9985 };
9986 let fk = match &lw.ffn {
9987 FfnKind::Dense(_) => "Dense",
9988 FfnKind::Moe(_) => "Moe",
9989 FfnKind::DenseMoe(_) => "DenseMoe",
9990 };
9991 eprintln!("graph L{li}: attn={ak} ffn={fk}");
9992 }
9993 let gffn = match &lw.ffn {
9994 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
9998 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9999 gate: gw(&d.gate_proj)?,
10000 up: gw(&d.up_proj)?,
10001 down: gw(&d.down_proj)?,
10002 },
10003 FfnKind::Moe(m) => {
10004 if m.route_tau.is_some() || m.mask.is_some() {
10012 return None;
10013 }
10014 let shared = m.shared.as_ref();
10015 let has_shared = shared.is_some();
10016 let shared_gated = matches!(shared, Some((_, Some(_))));
10017 let sgate = match shared {
10018 Some((_, Some(sg))) => gw(sg)?,
10019 _ => gw(&m.router)?,
10023 };
10024 let router = gw(&m.router)?;
10025 if router.prism != crate::gpu::GraphPrismOp::None
10031 || sgate.prism != crate::gpu::GraphPrismOp::None
10032 || router.affine
10033 || sgate.affine
10034 {
10035 tracing::warn!(
10036 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
10037 );
10038 return None;
10039 }
10040 let inter = m.experts.first()?.gate_proj.rows();
10041 let mut experts = Vec::with_capacity(m.experts.len() + 1);
10042 let mut q4tp: Option<bool> = None;
10045 let mut gu_q2: Option<bool> = None;
10048 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
10049 if !matches!(e.act, Act::Silu)
10050 || e.gate_proj.rows() != inter
10051 || e.up_proj.rows() != inter
10052 {
10053 return None;
10054 }
10055 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
10060 let Some((em, ei, _, _)) = expert_weight
10061 .graph_weight()
10062 .or_else(|| expert_weight.graph_weight_descriptor())
10063 else {
10064 return None;
10065 };
10066 let name = &em.tensors[ei].name;
10067 if crate::prism::is_forward_weight(em, name)
10068 || crate::prism::is_inverse_embedding(em, name)
10069 || crate::prism::is_affine_target(em, name)
10070 {
10071 tracing::warn!(
10072 "resident MoE declined: expert Prism/affine transform is not implemented"
10073 );
10074 return None;
10075 }
10076 }
10077 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
10078 Some((mm, gi)) => (
10079 mm,
10080 gi,
10081 e.up_proj.mapped_q4t()?.1,
10082 e.down_proj.mapped_q4t()?.1,
10083 false,
10084 false,
10085 ),
10086 None => match e.gate_proj.mapped_q2tp() {
10087 Some((mm, gi)) => (
10088 mm,
10089 gi,
10090 e.up_proj.mapped_q2tp()?.1,
10091 e.down_proj.mapped_q4tp()?.1,
10092 true,
10093 true,
10094 ),
10095 None => {
10096 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
10097 (
10098 mm,
10099 gi,
10100 e.up_proj.mapped_q4tp()?.1,
10101 e.down_proj.mapped_q4tp()?.1,
10102 true,
10103 false,
10104 )
10105 }
10106 },
10107 };
10108 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
10109 {
10110 tracing::warn!(
10116 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
10117 );
10118 return None;
10119 }
10120 model.get_or_insert_with(|| mm.clone());
10121 experts.push((gi, ui, di));
10122 }
10123 crate::gpu::GraphFfn::Moe {
10124 router,
10125 shared_gate: sgate,
10126 experts,
10127 n_exp: m.experts.len(),
10128 top_k: std::env::var("CMF_TOPK_PROBE")
10134 .ok()
10135 .and_then(|v| v.parse::<usize>().ok())
10136 .filter(|k| *k > 0 && *k <= m.top_k)
10137 .unwrap_or(m.top_k),
10138 inter,
10139 norm_topk: m.norm_topk_prob,
10140 q4tp: q4tp?,
10141 gu_q2: gu_q2.unwrap_or(false),
10142 sigmoid: m.router_sigmoid,
10143 bias: m.expert_bias.as_deref(),
10144 has_shared,
10145 shared_gated,
10146 route_scale: m.routed_scaling,
10147 }
10148 }
10149 };
10150 let attn = match &lw.attn {
10151 AttnKind::Full {
10152 wq,
10153 wk,
10154 wv,
10155 wo,
10156 q_norm,
10157 k_norm,
10158 output_gate,
10159 softplus_gate,
10160 bias,
10161 } => {
10162 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
10163 return None;
10164 }
10165 let (m, _, _, _) = wq
10166 .graph_weight()
10167 .or_else(|| wq.graph_weight_descriptor())?;
10168 model = Some(m.clone());
10169 crate::gpu::GraphAttn::Full {
10170 wq: gw(wq)?,
10171 wk: gw(wk)?,
10172 wv: gw(wv)?,
10173 wo: gw(wo)?,
10174 q_norm: q_norm.as_deref(),
10175 k_norm: k_norm.as_deref(),
10176 late_qk_norm: self.qk_norm_after_rope,
10177 bias: bias
10178 .as_ref()
10179 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10180 output_gate: *output_gate,
10181 cpu_k: self.kv_cache.layers[li].k_heads(),
10182 cpu_v: self.kv_cache.layers[li].v_heads(),
10183 geom: self.graph_attn_geom(li),
10184 }
10185 }
10186 AttnKind::LinearGdn(w) => {
10187 let cfg = self.gdn_cfg?;
10188 let (m, _, _, _) = w
10189 .in_proj_qkv
10190 .graph_weight()
10191 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
10192 model = Some(m.clone());
10193 crate::gpu::GraphAttn::Gdn {
10194 qkv: gw(&w.in_proj_qkv)?,
10195 z: gw(&w.in_proj_z)?,
10196 a: gw(&w.in_proj_a)?,
10197 b: gw(&w.in_proj_b)?,
10198 out: gw(&w.out_proj)?,
10199 conv1d: &w.conv1d,
10200 a_log: &w.a_log,
10201 dt_bias: &w.dt_bias,
10202 norm: &w.norm,
10203 nv: cfg.num_v_heads,
10204 nk: cfg.num_k_heads,
10205 dk: cfg.key_head_dim,
10206 dv: cfg.value_head_dim,
10207 kk: cfg.conv_kernel,
10208 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
10209 }
10210 }
10211 AttnKind::ShortConv(w) => {
10212 let cfg = self.short_conv_cfg?;
10213 let (m, _, _, _) = w
10214 .in_proj
10215 .graph_weight()
10216 .or_else(|| w.in_proj.graph_weight_descriptor())?;
10217 model = Some(m.clone());
10218 crate::gpu::GraphAttn::ShortConv {
10219 inp: gw(&w.in_proj)?,
10220 out: gw(&w.out_proj)?,
10221 taps: &w.conv,
10222 kernel: cfg.kernel,
10223 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
10224 }
10225 }
10226 _ => return None,
10227 };
10228 layers.push(crate::gpu::GraphLayer {
10229 input_norm: &lw.input_norm,
10230 attn,
10231 post_norm: &lw.post_norm,
10232 ffn: gffn,
10233 });
10234 }
10235 let model = model?;
10236 let lm_gw = if upto_excl == self.num_layers
10242 && self.graph_want_logits
10243 && std::env::var("CMF_GPU_LMHEAD")
10244 .map(|v| v != "0")
10245 .unwrap_or(true)
10246 {
10247 self.weights
10248 .lm_head
10249 .graph_weight()
10250 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
10251 .map(|(m, i, kind, rs)| {
10252 let name = &m.tensors[i].name;
10253 let prism = if crate::prism::is_inverse_embedding(m, name) {
10254 crate::gpu::GraphPrismOp::InverseEmbedding
10255 } else if crate::prism::is_forward_weight(m, name) {
10256 crate::gpu::GraphPrismOp::Forward
10257 } else {
10258 crate::gpu::GraphPrismOp::None
10259 };
10260 (
10261 crate::gpu::GraphW {
10262 idx: i,
10263 kind,
10264 row_scale: rs,
10265 data: &[],
10266 prism,
10267 affine: crate::prism::is_affine_target(m, name),
10268 },
10269 self.weights.lm_head.rows(),
10270 )
10271 })
10272 } else {
10273 None
10274 };
10275 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
10276 let emb_gw = if steps > 1 {
10278 self.weights
10279 .embed_tokens
10280 .graph_weight()
10281 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
10282 .map(|(m, i, kind, rs)| {
10283 let name = &m.tensors[i].name;
10284 let prism = if crate::prism::is_inverse_embedding(m, name) {
10285 crate::gpu::GraphPrismOp::InverseEmbedding
10286 } else if crate::prism::is_forward_weight(m, name) {
10287 crate::gpu::GraphPrismOp::Forward
10288 } else {
10289 crate::gpu::GraphPrismOp::None
10290 };
10291 (
10292 crate::gpu::GraphW {
10293 idx: i,
10294 kind,
10295 row_scale: rs,
10296 data: &[],
10297 prism,
10298 affine: crate::prism::is_affine_target(m, name),
10299 },
10300 self.weights.embed_tokens.rows(),
10301 self.embed_multiplier,
10302 )
10303 })
10304 } else {
10305 None
10306 };
10307
10308 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
10314 (from..upto_excl.min(self.num_layers - 1))
10315 .filter(|&li| (li + 1) % self.physical_layers == 0)
10316 .map(|li| li - from)
10317 .collect()
10318 } else {
10319 Vec::new()
10320 };
10321 let mut h = hidden.to_vec();
10322 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
10328 let outcome = crate::gpu::forward_token_graph(
10329 &model,
10330 self.graph_kv_id,
10331 &layers,
10332 &o1_views,
10333 self.o1_epoch,
10334 &self.inv_freq,
10335 &mut h,
10336 nh,
10337 nkv,
10338 hd,
10339 self.attn_scale,
10340 rd,
10341 self.hidden_size,
10342 self.intermediate_size,
10343 position,
10344 self.kv_cache.max_seq_len,
10345 gemma,
10346 self.rms_eps as f32,
10347 lm,
10348 &self.weights.final_norm,
10349 logits_out,
10350 &loop_norm_at,
10351 steps,
10352 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
10353 ids_out,
10354 layers_run,
10355 from,
10356 dump_hidden,
10357 );
10358 match outcome {
10359 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
10360 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
10361 crate::gpu::TokenGraphOutcome::Declined => None,
10362 }
10363 }
10364
10365 #[cfg(target_os = "macos")]
10374 #[allow(clippy::type_complexity)]
10375 fn metal_rows_plan(
10376 &self,
10377 ) -> Option<(
10378 Vec<MetalRowsItem<'_>>,
10379 std::sync::Arc<cortiq_core::CmfModel>,
10380 Option<crate::gpu_metal::GdnGpuCfg>,
10381 )> {
10382 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
10383 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
10384 if !graph_force
10385 || !crate::gpu::enabled_here()
10386 || std::env::var("CMF_GPU_BLOCK")
10387 .map(|v| v == "0")
10388 .unwrap_or(false)
10389 || self.attn_softcap > 0.0
10390 || self.o1_active()
10391 || self.swa.is_some()
10392 || self.global_attn.is_some()
10393 || self.attention_heads_per_layer.is_some()
10394 || self.graph_attn_decline_reason().is_some()
10396 || self.attn_v_norm
10397 || self.loop_final_norm
10398 {
10399 return None;
10400 }
10401 let attend_contract = self.head_dim % 4 == 0
10402 && self.head_dim <= 256
10403 && self.rotary_dim >= 2
10404 && self.rotary_dim <= self.head_dim
10405 && (self.rotary_dim / 2) % 32 == 0
10406 && self.num_kv_heads > 0
10407 && self.num_heads % self.num_kv_heads == 0;
10408 if !attend_contract {
10409 return None;
10410 }
10411 let mut plan: Vec<MetalRowsItem> = Vec::new();
10412 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
10413 for li in 0..self.num_layers {
10414 let lw = &self.weights.layers[self.phys_layer(li)];
10415 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
10416 return None;
10417 }
10418 let ffn = match &lw.ffn {
10419 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
10420 let (Some(g), Some(u), Some(dn)) = (
10421 d.gate_proj.metal_graph_parts(),
10422 d.up_proj.metal_graph_parts(),
10423 d.down_proj.metal_graph_parts(),
10424 ) else {
10425 return None;
10426 };
10427 MetalFfn::Dense {
10428 gate: g,
10429 up: u,
10430 down: dn,
10431 }
10432 }
10433 _ => return None,
10434 };
10435 match &lw.attn {
10436 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
10437 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
10438 w.in_proj_qkv.metal_graph_parts(),
10439 w.in_proj_z.metal_graph_parts(),
10440 w.in_proj_a.f32_parts(),
10441 w.in_proj_b.f32_parts(),
10442 w.out_proj.metal_graph_parts(),
10443 ) else {
10444 return None;
10445 };
10446 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
10447 model_ref.get_or_insert_with(|| model.clone());
10448 }
10449 let gl = GdnGpuLayer {
10450 attn_norm: &lw.input_norm,
10451 post_norm: &lw.post_norm,
10452 qkv,
10453 z,
10454 a,
10455 b: bb,
10456 out,
10457 ffn,
10458 conv1d: &w.conv1d,
10459 a_log: &w.a_log,
10460 dt_bias: &w.dt_bias,
10461 gnorm: &w.norm,
10462 };
10463 match plan.last_mut() {
10464 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
10465 _ => plan.push(MetalRowsItem::Gdn {
10466 run: vec![gl],
10467 first: li,
10468 }),
10469 }
10470 }
10471 AttnKind::Full {
10472 wq,
10473 wk,
10474 wv,
10475 wo,
10476 q_norm,
10477 k_norm,
10478 output_gate,
10479 softplus_gate: None,
10480 bias: None,
10481 } => {
10482 let (Some(pq), Some(pk), Some(pv), Some(po)) =
10483 (
10484 wq.metal_graph_parts(),
10485 wk.metal_graph_parts(),
10486 wv.metal_graph_parts(),
10487 wo.metal_graph_parts(),
10488 )
10489 else {
10490 return None;
10491 };
10492 if let QTensor::Mapped { model, .. } = wq {
10493 model_ref.get_or_insert_with(|| model.clone());
10494 }
10495 let cache = &self.kv_cache.layers[li];
10496 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
10497 return None;
10498 }
10499 plan.push(MetalRowsItem::Attn {
10500 l: AttnGpuLayer {
10501 attn_norm: &lw.input_norm,
10502 post_norm: &lw.post_norm,
10503 wq: pq,
10504 wk: pk,
10505 wv: pv,
10506 wo: po,
10507 ffn,
10508 },
10509 li,
10510 q_norm: q_norm.as_deref(),
10511 k_norm: k_norm.as_deref(),
10512 output_gate: *output_gate,
10513 });
10514 }
10515 _ => return None,
10516 }
10517 }
10518 let model = model_ref?;
10519 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
10520 nv: cfg.num_v_heads,
10521 nk: cfg.num_k_heads,
10522 dk: cfg.key_head_dim,
10523 dv: cfg.value_head_dim,
10524 kk: cfg.conv_kernel,
10525 hidden: self.hidden_size,
10526 inter: self.intermediate_size,
10527 c_dim: cfg.conv_dim(),
10528 eps: cfg.rms_eps as f32,
10529 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10530 });
10531 Some((plan, model, gcfg))
10532 }
10533
10534 #[cfg(target_os = "macos")]
10536 #[allow(clippy::too_many_arguments)]
10537 fn metal_attn_params<'a>(
10538 li: usize,
10539 cache: &'a crate::kv_cache::LayerKvCache,
10540 q_norm: Option<&'a [f32]>,
10541 k_norm: Option<&'a [f32]>,
10542 output_gate: bool,
10543 inv_freq: &'a [f32],
10544 geom: (usize, usize, usize, usize),
10545 pos0: usize,
10546 kv_id: u64,
10547 scale: f32,
10548 eps: f32,
10549 gemma: bool,
10550 late_qk_norm: bool,
10551 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
10552 let (nh, nkv, hd, rd) = geom;
10553 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
10554 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
10555 let cpu_stored = cpu_k[0].len() / hd;
10556 (
10557 crate::gpu_metal::AttnDeviceParams {
10558 kv_id,
10559 layer: li,
10560 nh,
10561 nkv,
10562 hd,
10563 rd,
10564 position: pos0,
10565 scale,
10566 eps,
10567 gemma,
10568 late_qk_norm,
10569 output_gate,
10570 q_norm,
10571 k_norm,
10572 inv_freq,
10573 cpu_k,
10574 cpu_v,
10575 cpu_stored,
10576 o1: None,
10577 },
10578 cpu_stored,
10579 )
10580 }
10581
10582 #[cfg(target_os = "macos")]
10587 #[allow(clippy::type_complexity)]
10588 fn metal_rows_run(
10589 &mut self,
10590 hiddens: &mut [f32],
10591 pos0: usize,
10592 b: usize,
10593 prefill: bool,
10594 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
10595 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
10599 ) -> MetalRowsRun {
10600 use crate::gpu_metal::{GraphDims, VerifyGraph};
10601 if !crate::gpu_metal::wait_replay() {
10607 tracing::error!("Metal rows graph: the pending async replay failed");
10608 return MetalRowsRun::Failed;
10609 }
10610 spec_stamp("v.wait");
10611 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
10612 for l in &mut self.kv_cache.layers {
10613 if l.linear_state.len() != want && want > 0 {
10614 l.linear_state = vec![0f32; want];
10615 }
10616 }
10617 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
10618 return MetalRowsRun::Declined;
10619 };
10620 spec_stamp("v.plan");
10621 let dims = GraphDims {
10622 hidden: self.hidden_size,
10623 eps: self.rms_eps as f32,
10624 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
10625 };
10626 let Some(mut graph) = (if prefill {
10627 VerifyGraph::new_prefill(&model, dims, hiddens, b)
10628 } else {
10629 VerifyGraph::new(&model, dims, hiddens, b)
10630 }) else {
10631 return MetalRowsRun::Declined;
10632 };
10633 let geom = (
10634 self.num_heads,
10635 self.num_kv_heads,
10636 self.head_dim,
10637 self.rotary_dim,
10638 );
10639 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10640 let eps = self.rms_eps as f32;
10641 let kv_id = self.graph_kv_id;
10642 let inv_freq = self.inv_freq.clone();
10643 for item in &plan {
10644 let ok = match item {
10645 MetalRowsItem::Gdn { run, .. } => gcfg
10646 .as_ref()
10647 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
10648 .unwrap_or(false),
10649 MetalRowsItem::Attn {
10650 l,
10651 li,
10652 q_norm,
10653 k_norm,
10654 output_gate,
10655 } => {
10656 let (p, _) = Self::metal_attn_params(
10657 *li,
10658 &self.kv_cache.layers[*li],
10659 *q_norm,
10660 *k_norm,
10661 *output_gate,
10662 &inv_freq,
10663 geom,
10664 pos0,
10665 kv_id,
10666 self.attn_scale,
10667 eps,
10668 gemma,
10669 self.qk_norm_after_rope,
10670 );
10671 graph.attn_ok(l, &p)
10672 }
10673 };
10674 if !ok {
10675 use std::sync::atomic::{AtomicBool, Ordering};
10676 static SAID: AtomicBool = AtomicBool::new(false);
10677 if !SAID.swap(true, Ordering::Relaxed) {
10678 tracing::warn!("metal rows graph: a layer failed preflight — declining");
10679 }
10680 return MetalRowsRun::Declined;
10681 }
10682 }
10683 let lm = match &spec {
10684 Some((lm, _, _)) => {
10685 if !graph.lm_head_ok(*lm) {
10686 return MetalRowsRun::Declined;
10687 }
10688 Some(*lm)
10689 }
10690 None => None,
10691 };
10692 let mut gdn_layers = Vec::new();
10693 let mut attn_layers = Vec::new();
10694 for item in &plan {
10695 match item {
10696 MetalRowsItem::Gdn { run, first } => {
10697 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
10698 .iter()
10699 .map(|l| l.linear_state.as_slice())
10700 .collect();
10701 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
10702 return MetalRowsRun::Declined;
10703 }
10704 gdn_layers.extend(*first..*first + run.len());
10705 }
10706 MetalRowsItem::Attn {
10707 l,
10708 li,
10709 q_norm,
10710 k_norm,
10711 output_gate,
10712 } => {
10713 let (p, cpu_stored) = Self::metal_attn_params(
10714 *li,
10715 &self.kv_cache.layers[*li],
10716 *q_norm,
10717 *k_norm,
10718 *output_gate,
10719 &inv_freq,
10720 geom,
10721 pos0,
10722 kv_id,
10723 self.attn_scale,
10724 eps,
10725 gemma,
10726 self.qk_norm_after_rope,
10727 );
10728 if !graph.encode_attn_b(l, &p) {
10729 return MetalRowsRun::Declined;
10730 }
10731 attn_layers.push((*li, cpu_stored));
10732 }
10733 }
10734 }
10735 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
10736 if !graph.encode_lm_head_b(final_norm, lm) {
10737 return MetalRowsRun::Declined;
10738 }
10739 if let Some((n, _)) = argmax_out.as_ref() {
10744 if !graph.encode_argmax_b(*n) {
10745 argmax_out = None;
10746 }
10747 }
10748 }
10749 spec_stamp("v.enc");
10750 if !graph.sync() {
10751 return MetalRowsRun::Failed;
10752 }
10753 spec_stamp("v.gpu");
10754 match (spec, argmax_out) {
10755 (Some(_), Some((_, ids))) => {
10756 ids.resize(b, 0);
10757 if !graph.read_argmax(ids) {
10758 return MetalRowsRun::Failed;
10759 }
10760 spec_stamp("v.am");
10761 }
10762 (Some((lm, _, logits)), None) => {
10763 logits.resize(b * lm.1, 0.0);
10764 if !graph.read_logits(logits) {
10765 return MetalRowsRun::Failed;
10766 }
10767 spec_stamp("v.lg");
10768 }
10769 (None, _) => {}
10770 }
10771 if !graph.read_hidden(hiddens) {
10772 return MetalRowsRun::Failed;
10773 }
10774 spec_stamp("v.hid");
10775 MetalRowsRun::Completed(MetalVerifyPending {
10776 graph,
10777 gdn_layers,
10778 attn_layers,
10779 })
10780 }
10781
10782 #[cfg(target_os = "macos")]
10788 fn try_batch_graph_metal(
10789 &mut self,
10790 hiddens: &mut [f32],
10791 positions: &[usize],
10792 b: usize,
10793 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
10794 argmax_out: Option<(usize, &mut Vec<u32>)>,
10795 ) -> crate::gpu::BatchGraphOutcome {
10796 let _t0 = std::time::Instant::now();
10797 if positions.len() != b
10798 || positions.windows(2).any(|w| w[1] != w[0] + 1)
10799 || hiddens.len() != b * self.hidden_size
10800 {
10801 return crate::gpu::BatchGraphOutcome::Declined;
10802 }
10803 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
10804 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
10805 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
10806 MetalRowsRun::Completed(pending) => pending,
10807 };
10808 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
10809 eprintln!(
10810 "metal-verify: {:.1} ms | b={b}",
10811 _t0.elapsed().as_secs_f64() * 1e3
10812 );
10813 }
10814 self.metal_verify = Some(pending);
10815 crate::gpu::BatchGraphOutcome::Completed
10816 }
10817
10818 #[cfg(target_os = "macos")]
10823 fn prefill_rows_metal(
10824 &mut self,
10825 ids: &[u32],
10826 start_pos: usize,
10827 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
10828 ) -> MetalPrefillOutcome {
10829 let b = ids.len();
10830 if b == 0 || b > 512 {
10831 return MetalPrefillOutcome::Declined;
10832 }
10833 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
10834 let with_head = spec.is_some();
10835 let hs = self.hidden_size;
10836 let mut hiddens = vec![0f32; b * hs];
10837 for (j, &id) in ids.iter().enumerate() {
10838 let e = self.embed_single(id);
10839 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
10840 }
10841 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
10842 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
10843 MetalRowsRun::Failed => {
10844 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
10845 return MetalPrefillOutcome::Failed;
10846 }
10847 MetalRowsRun::Completed(pending) => pending,
10848 };
10849 let idxs = pending.gdn_layers.clone();
10851 let mut outs: Vec<&mut [f32]> = self
10852 .kv_cache
10853 .layers
10854 .iter_mut()
10855 .enumerate()
10856 .filter(|(i, _)| idxs.binary_search(i).is_ok())
10857 .map(|(_, l)| l.linear_state.as_mut_slice())
10858 .collect();
10859 if !pending.graph.finish_states(&mut outs) {
10860 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
10861 return MetalPrefillOutcome::Failed;
10862 }
10863 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
10864 let mut rows = Vec::with_capacity(pending.attn_layers.len());
10868 for (li, cpu_stored) in &pending.attn_layers {
10869 let mut kbuf = vec![0f32; b * nkv * hd];
10870 let mut vbuf = vec![0f32; b * nkv * hd];
10871 if !crate::gpu_metal::kv_mirror_read_rows(
10872 self.graph_kv_id,
10873 *li,
10874 nkv,
10875 hd,
10876 *cpu_stored,
10877 b,
10878 &mut kbuf,
10879 &mut vbuf,
10880 ) {
10881 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
10882 return MetalPrefillOutcome::Failed;
10883 }
10884 rows.push((*li, *cpu_stored, kbuf, vbuf));
10885 }
10886 for (li, cpu_stored, kbuf, vbuf) in rows {
10887 let cache = &mut self.kv_cache.layers[li];
10888 for r in 0..b {
10889 cache.append(
10890 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
10891 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
10892 &[],
10893 );
10894 }
10895 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
10896 }
10897 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
10898 if with_head {
10899 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
10900 }
10901 MetalPrefillOutcome::Completed(hiddens)
10902 }
10903
10904 #[cfg(target_os = "macos")]
10905 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
10906 self.prefill_rows_metal(ids, start_pos, None)
10907 }
10908
10909 #[cfg(target_os = "macos")]
10914 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
10915 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
10916 return MetalBatchNllOutcome::Declined;
10917 }
10918 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
10919 return MetalBatchNllOutcome::Declined;
10920 };
10921 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
10922 .ok()
10923 .and_then(|v| v.parse::<usize>().ok())
10924 .filter(|&v| (1..=512).contains(&v))
10925 .unwrap_or(32);
10926 let final_norm = self.weights.final_norm.clone();
10927 let mut nll = 0.0f64;
10928 let mut count = 0usize;
10929 let mut pos = 0usize;
10930 let mut completed = 0usize;
10931 while pos < ids.len() {
10932 let end = (pos + chunk).min(ids.len());
10933 let mut logits = Vec::new();
10934 let outcome = self.prefill_rows_metal(
10935 &ids[pos..end],
10936 pos,
10937 Some((lm, &final_norm, &mut logits)),
10938 );
10939 match outcome {
10940 MetalPrefillOutcome::Declined => {
10941 return if completed == 0 {
10942 MetalBatchNllOutcome::Declined
10943 } else {
10944 MetalBatchNllOutcome::Failed(format!(
10945 "ordinary Metal NLL batch declined after {completed} chunks"
10946 ))
10947 };
10948 }
10949 MetalPrefillOutcome::Failed => {
10950 return MetalBatchNllOutcome::Failed(
10951 "ordinary Metal NLL batch failed after admission".to_string(),
10952 );
10953 }
10954 MetalPrefillOutcome::Completed(_) => {}
10955 }
10956 completed += 1;
10957 let vocab = self.vocab_size.min(lm.1);
10958 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
10959 return MetalBatchNllOutcome::Failed(
10960 "ordinary Metal NLL head returned an invalid shape".to_string(),
10961 );
10962 }
10963 for row in 0..(end - pos) {
10964 let absolute = pos + row;
10965 if absolute < start || absolute + 1 >= ids.len() {
10966 continue;
10967 }
10968 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
10969 if let Some(mu) = self.logit_multiplier {
10970 for v in lg.iter_mut() {
10971 *v *= mu;
10972 }
10973 }
10974 if let Some(c) = self.final_softcap {
10975 for v in lg.iter_mut() {
10976 *v = c * (*v / c).tanh();
10977 }
10978 }
10979 let target = ids[absolute + 1] as usize;
10980 if target >= vocab {
10981 return MetalBatchNllOutcome::Failed(format!(
10982 "target token {target} exceeds Metal head rows {vocab}"
10983 ));
10984 }
10985 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10986 let lse: f64 = lg
10987 .iter()
10988 .map(|&v| ((v - max) as f64).exp())
10989 .sum::<f64>()
10990 .ln()
10991 + max as f64;
10992 nll += lse - lg[target] as f64;
10993 count += 1;
10994 }
10995 pos = end;
10996 }
10997 MetalBatchNllOutcome::Completed(nll, count)
10998 }
10999
11000 #[cfg(target_os = "macos")]
11004 fn metal_verify_commit(&mut self, a: usize) -> bool {
11005 let Some(mut pending) = self.metal_verify.take() else {
11006 return false;
11007 };
11008 let n = a + 1;
11009 let idxs = pending.gdn_layers.clone();
11011 let mut outs: Vec<&mut [f32]> = self
11012 .kv_cache
11013 .layers
11014 .iter_mut()
11015 .enumerate()
11016 .filter(|(i, _)| idxs.binary_search(i).is_ok())
11017 .map(|(_, l)| l.linear_state.as_mut_slice())
11018 .collect();
11019 if !pending.graph.commit(n, &mut outs) {
11020 return false;
11021 }
11022 spec_stamp("c.replay");
11023 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
11024 let mut rows = Vec::with_capacity(pending.attn_layers.len());
11028 for (li, cpu_stored) in &pending.attn_layers {
11029 let mut kbuf = vec![0f32; n * nkv * hd];
11030 let mut vbuf = vec![0f32; n * nkv * hd];
11031 if !crate::gpu_metal::kv_mirror_read_rows(
11032 self.graph_kv_id,
11033 *li,
11034 nkv,
11035 hd,
11036 *cpu_stored,
11037 n,
11038 &mut kbuf,
11039 &mut vbuf,
11040 ) {
11041 return false;
11042 }
11043 rows.push((*li, *cpu_stored, kbuf, vbuf));
11044 }
11045 for (li, cpu_stored, kbuf, vbuf) in rows {
11046 let cache = &mut self.kv_cache.layers[li];
11047 for r in 0..n {
11048 cache.append(
11049 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
11050 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
11051 &[],
11052 );
11053 }
11054 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
11055 }
11056 spec_stamp("c.kv");
11057 true
11058 }
11059
11060 #[cfg(target_os = "macos")]
11067 fn mtp_warm_batch_submit(
11068 &mut self,
11069 m: &mut MtpModule,
11070 pairs: &[(&[f32], u32)],
11071 first_pos: usize,
11072 ) -> Option<MetalWarmPending> {
11073 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
11074 let b = pairs.len();
11075 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
11076 return None;
11077 }
11078 let AttnKind::Full {
11079 wq,
11080 wk,
11081 wv,
11082 wo,
11083 q_norm,
11084 k_norm,
11085 output_gate,
11086 softplus_gate: None,
11087 bias: None,
11088 } = &m.layer.attn
11089 else {
11090 return None;
11091 };
11092 let FfnKind::Dense(d) = &m.layer.ffn else {
11093 return None;
11094 };
11095 if !d.segs.is_empty() {
11096 return None;
11097 }
11098 let (Some(pq), Some(pk), Some(pv), Some(po)) =
11099 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
11100 else {
11101 return None;
11102 };
11103 let (Some(g), Some(u), Some(dn)) = (
11104 d.gate_proj.q1_parts(),
11105 d.up_proj.q1_parts(),
11106 d.down_proj.q1_parts(),
11107 ) else {
11108 return None;
11109 };
11110 let Some(eh) = m.eh_proj.q1_parts() else {
11111 return None;
11112 };
11113 let QTensor::Mapped { model, .. } = wq else {
11114 return None;
11115 };
11116 let model = model.clone();
11117 let hs = self.hidden_size;
11118 let mut cat = vec![0f32; b * 2 * hs];
11120 for (j, (h, tok)) in pairs.iter().enumerate() {
11121 let e = self.embed_single(*tok);
11122 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
11123 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
11124 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
11125 }
11126 let dims = GraphDims {
11127 hidden: hs,
11128 eps: self.rms_eps as f32,
11129 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11130 };
11131 spec_stamp("w.cat");
11132 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
11133 return None;
11134 };
11135 spec_stamp("w.new");
11136 let l = AttnGpuLayer {
11137 attn_norm: &m.layer.input_norm,
11138 post_norm: &m.layer.post_norm,
11139 wq: pq,
11140 wk: pk,
11141 wv: pv,
11142 wo: po,
11143 ffn: MetalFfn::Dense {
11144 gate: g,
11145 up: u,
11146 down: dn,
11147 },
11148 };
11149 let (nh, nkv, hd, rd) = (
11150 self.num_heads,
11151 self.num_kv_heads,
11152 self.head_dim,
11153 self.rotary_dim,
11154 );
11155 let inv_freq = self.inv_freq.clone();
11156 let cpu_stored;
11157 {
11158 let cache = &m.kv;
11159 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
11160 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
11161 cpu_stored = cpu_k[0].len() / hd;
11162 if cpu_stored > first_pos {
11167 spec_stamp("w.decl");
11168 return None;
11169 }
11170 let p = AttnDeviceParams {
11171 kv_id: self.mtp_kv_id(),
11172 layer: Self::MTP_LAYER_BASE,
11173 nh,
11174 nkv,
11175 hd,
11176 rd,
11177 position: first_pos,
11178 scale: self.attn_scale,
11179 eps: self.rms_eps as f32,
11180 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11181 late_qk_norm: self.qk_norm_after_rope,
11182 output_gate: *output_gate,
11183 q_norm: q_norm.as_deref(),
11184 k_norm: k_norm.as_deref(),
11185 inv_freq: &inv_freq,
11186 cpu_k,
11187 cpu_v,
11188 cpu_stored,
11189 o1: None,
11190 };
11191 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
11192 return None;
11193 }
11194 }
11195 spec_stamp("w.enc");
11196 if !graph.submit() {
11197 return None;
11198 }
11199 spec_stamp("w.sub");
11200 Some(MetalWarmPending {
11201 graph,
11202 cpu_stored,
11203 b,
11204 })
11205 }
11206
11207 #[cfg(target_os = "macos")]
11210 fn mtp_warm_batch_metal(
11211 &mut self,
11212 m: &mut MtpModule,
11213 pairs: &[(&[f32], u32)],
11214 first_pos: usize,
11215 ) -> bool {
11216 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
11217 Some(p) => self.mtp_warm_batch_finish(m, p),
11218 None => false,
11219 }
11220 }
11221
11222 #[cfg(target_os = "macos")]
11227 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
11228 let MetalWarmPending {
11229 mut graph,
11230 cpu_stored,
11231 b,
11232 } = pending;
11233 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
11234 if !graph.sync() {
11235 return false;
11236 }
11237 spec_stamp("w.gpu");
11238 let mut kbuf = vec![0f32; b * nkv * hd];
11239 let mut vbuf = vec![0f32; b * nkv * hd];
11240 if !crate::gpu_metal::kv_mirror_read_rows(
11241 self.mtp_kv_id(),
11242 Self::MTP_LAYER_BASE,
11243 nkv,
11244 hd,
11245 cpu_stored,
11246 b,
11247 &mut kbuf,
11248 &mut vbuf,
11249 ) {
11250 return false;
11251 }
11252 for r in 0..b {
11253 m.kv.append(
11254 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
11255 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
11256 &[],
11257 );
11258 }
11259 crate::gpu_metal::kv_mirror_set_stored(
11260 self.mtp_kv_id(),
11261 Self::MTP_LAYER_BASE,
11262 cpu_stored + b,
11263 );
11264 spec_stamp("w.kv");
11265 true
11266 }
11267
11268 pub(crate) fn note_draft_id(&mut self, id: u32) {
11275 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
11276 if (id as usize) >= cut {
11277 self.draft_full_streak = 16;
11278 } else {
11279 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
11280 }
11281 }
11282
11283 fn draft_head_rows(&self, head_rows: usize) -> usize {
11286 if self.draft_full_streak > 0 {
11287 head_rows
11288 } else {
11289 Self::draft_vocab_rows(head_rows)
11290 }
11291 }
11292
11293 fn draft_vocab_rows(head_rows: usize) -> usize {
11296 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11297 let n = *N.get_or_init(|| {
11298 std::env::var("CMF_DRAFT_VOCAB")
11299 .ok()
11300 .and_then(|v| v.parse().ok())
11301 .unwrap_or(65536)
11302 });
11303 if n == 0 { head_rows } else { n.min(head_rows) }
11304 }
11305
11306 #[cfg(target_os = "macos")]
11311 fn mtp_step_metal(
11312 &mut self,
11313 m: &mut MtpModule,
11314 hidden: &[f32],
11315 next_token: u32,
11316 position: usize,
11317 want_logits: bool,
11318 ) -> Option<(Vec<f32>, Vec<f32>)> {
11319 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
11320 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
11321 || !crate::gpu::q1_force()
11322 || !crate::gpu::enabled_here()
11323 || self.attn_softcap > 0.0
11324 || self.attention_heads_per_layer.is_some()
11325 || m.kv.mode != crate::kv_cache::KvMode::F32
11326 || m.kv.o1.is_some()
11327 {
11328 return None;
11329 }
11330 let AttnKind::Full {
11331 wq,
11332 wk,
11333 wv,
11334 wo,
11335 q_norm,
11336 k_norm,
11337 output_gate,
11338 softplus_gate: None,
11339 bias: None,
11340 } = &m.layer.attn
11341 else {
11342 return None;
11343 };
11344 let FfnKind::Dense(d) = &m.layer.ffn else {
11345 return None;
11346 };
11347 if d.act != Act::Silu || !d.segs.is_empty() {
11348 return None;
11349 }
11350 let (pq, pk, pv, po) = (
11351 wq.q1_parts()?,
11352 wk.q1_parts()?,
11353 wv.q1_parts()?,
11354 wo.q1_parts()?,
11355 );
11356 let (g, u, dn) = (
11357 d.gate_proj.q1_parts()?,
11358 d.up_proj.q1_parts()?,
11359 d.down_proj.q1_parts()?,
11360 );
11361 let QTensor::Mapped { model, .. } = wq else {
11362 return None;
11363 };
11364 let model = model.clone();
11365 let lm = if want_logits {
11366 Some(self.weights.lm_head.q1_parts()?)
11367 } else {
11368 None
11369 };
11370 let dims = GraphDims {
11371 hidden: self.hidden_size,
11372 eps: self.rms_eps as f32,
11373 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11374 };
11375 let hs = self.hidden_size;
11378 let mut x = vec![0f32; hs];
11379 let mut graph = TokenGraph::new(&model, dims, &x)?;
11380 let mut folded = false;
11381 if let Some(eh) = m.eh_proj.q1_parts() {
11382 let e = self.embed_single(next_token);
11383 let mut cat = vec![0.0f32; 2 * hs];
11384 let (cat_e, cat_h) = cat.split_at_mut(hs);
11385 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
11386 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
11387 folded = graph.encode_input_proj(eh, &cat);
11388 }
11389 if !folded {
11390 x = self.mtp_block_input(m, hidden, next_token);
11391 graph = TokenGraph::new(&model, dims, &x)?;
11392 }
11393 spec_stamp("d.in");
11394 let l = AttnGpuLayer {
11395 attn_norm: &m.layer.input_norm,
11396 post_norm: &m.layer.post_norm,
11397 wq: pq,
11398 wk: pk,
11399 wv: pv,
11400 wo: po,
11401 ffn: MetalFfn::Dense {
11402 gate: g,
11403 up: u,
11404 down: dn,
11405 },
11406 };
11407 let (nh, nkv, hd, rd) = (
11408 self.num_heads,
11409 self.num_kv_heads,
11410 self.head_dim,
11411 self.rotary_dim,
11412 );
11413 let inv_freq = self.inv_freq.clone();
11414 {
11415 let cache = &m.kv;
11416 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
11417 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
11418 let cpu_stored = cpu_k[0].len() / hd;
11419 let p = AttnDeviceParams {
11420 kv_id: self.mtp_kv_id(),
11421 layer: Self::MTP_LAYER_BASE,
11422 nh,
11423 nkv,
11424 hd,
11425 rd,
11426 position,
11427 scale: self.attn_scale,
11428 eps: self.rms_eps as f32,
11429 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11430 late_qk_norm: self.qk_norm_after_rope,
11431 output_gate: *output_gate,
11432 q_norm: q_norm.as_deref(),
11433 k_norm: k_norm.as_deref(),
11434 inv_freq: &inv_freq,
11435 cpu_k,
11436 cpu_v,
11437 cpu_stored,
11438 o1: None,
11439 };
11440 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
11441 return None;
11442 }
11443 }
11444 let draft_rows = if let Some(lm) = lm {
11450 self.draft_head_rows(lm.1)
11451 } else {
11452 0
11453 };
11454 if let Some(lm) = lm {
11455 if !graph.lm_head_ok(lm) {
11456 return None;
11457 }
11458 if draft_rows < lm.1 {
11459 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
11460 return None;
11461 }
11462 } else {
11463 graph.encode_lm_head(&m.final_norm, lm);
11464 }
11465 }
11466 spec_stamp("d.enc");
11467 if graph.sync_checked().is_err() {
11468 return None;
11469 }
11470 spec_stamp("d.gpu");
11471 let mut logits = Vec::new();
11472 if let Some(lm) = lm {
11473 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
11474 logits = attention::take_buf(n_read);
11475 graph.read_logits(&mut logits);
11476 logits.resize(self.vocab_size, f32::NEG_INFINITY);
11478 }
11479 graph.finish(&mut x);
11480 let mut krow = attention::take_buf(nkv * hd);
11481 let mut vrow = attention::take_buf(nkv * hd);
11482 if crate::gpu_metal::kv_mirror_read_last(
11483 self.mtp_kv_id(),
11484 Self::MTP_LAYER_BASE,
11485 nkv,
11486 hd,
11487 &mut krow,
11488 &mut vrow,
11489 ) {
11490 m.kv.append(&krow, &vrow, &[]);
11491 }
11492 attention::recycle_buf(&mut krow);
11493 attention::recycle_buf(&mut vrow);
11494 spec_stamp("d.rd");
11495 Some((logits, x))
11496 }
11497
11498 fn mtp_chain_on() -> bool {
11511 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11512 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
11513 }
11514
11515 #[cfg(target_os = "macos")]
11527 fn mtp_draft_chain_metal(
11528 &mut self,
11529 m: &mut MtpModule,
11530 hidden: &[f32],
11531 t_next: u32,
11532 position: usize,
11533 k: usize,
11534 ) -> Result<Vec<u32>, bool> {
11535 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
11536 if k == 0
11537 || k > 64
11538 || !Self::mtp_chain_on()
11539 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
11540 || !crate::gpu::q1_force()
11541 || !crate::gpu::enabled_here()
11542 || self.attn_softcap > 0.0
11543 || self.attention_heads_per_layer.is_some()
11544 || m.kv.mode != crate::kv_cache::KvMode::F32
11545 || m.kv.o1.is_some()
11546 || self.dsv4.is_some()
11548 || self.dsv41.is_some()
11549 || self.qwen4_exp.is_some()
11550 || self.g3n.is_some()
11551 {
11552 return Err(false);
11553 }
11554 let AttnKind::Full {
11555 wq,
11556 wk,
11557 wv,
11558 wo,
11559 q_norm,
11560 k_norm,
11561 output_gate,
11562 softplus_gate: None,
11563 bias: None,
11564 } = &m.layer.attn
11565 else {
11566 return Err(false);
11567 };
11568 let FfnKind::Dense(d) = &m.layer.ffn else {
11569 return Err(false);
11570 };
11571 if d.act != Act::Silu || !d.segs.is_empty() {
11572 return Err(false);
11573 }
11574 let (Some(pq), Some(pk), Some(pv), Some(po)) =
11575 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
11576 else {
11577 return Err(false);
11578 };
11579 let (Some(g), Some(u), Some(dn)) = (
11580 d.gate_proj.q1_parts(),
11581 d.up_proj.q1_parts(),
11582 d.down_proj.q1_parts(),
11583 ) else {
11584 return Err(false);
11585 };
11586 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
11587 return Err(false);
11588 };
11589 let QTensor::Mapped { model, .. } = wq else {
11590 return Err(false);
11591 };
11592 let model = model.clone();
11593 let QTensor::Mapped {
11596 model: em,
11597 idx: eidx,
11598 dtype: cortiq_core::TensorDtype::Q4TiledP,
11599 ..
11600 } = &self.weights.embed_tokens
11601 else {
11602 return Err(false);
11603 };
11604 if !std::sync::Arc::ptr_eq(em, &model)
11605 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
11606 {
11607 return Err(false);
11608 }
11609 let embed = (
11610 *eidx,
11611 self.weights.embed_tokens.rows(),
11612 self.weights.embed_tokens.cols(),
11613 );
11614 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
11615 return Err(false);
11616 }
11617 let dims = GraphDims {
11618 hidden: self.hidden_size,
11619 eps: self.rms_eps as f32,
11620 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11621 };
11622 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
11623 return Err(false);
11624 };
11625 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
11626 return Err(false);
11627 }
11628 let l = AttnGpuLayer {
11629 attn_norm: &m.layer.input_norm,
11630 post_norm: &m.layer.post_norm,
11631 wq: pq,
11632 wk: pk,
11633 wv: pv,
11634 wo: po,
11635 ffn: MetalFfn::Dense {
11636 gate: g,
11637 up: u,
11638 down: dn,
11639 },
11640 };
11641 let (nh, nkv, hd, rd) = (
11642 self.num_heads,
11643 self.num_kv_heads,
11644 self.head_dim,
11645 self.rotary_dim,
11646 );
11647 let inv_freq = self.inv_freq.clone();
11648 let draft_rows = self.draft_head_rows(lm.1);
11649 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
11650 if n_arg == 0 {
11651 return Err(false);
11652 }
11653 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
11660 let t_chain = std::time::Instant::now();
11661 graph.chain_ids_init(t_next, k);
11662 let cpu_stored;
11663 {
11664 let cache = &m.kv;
11665 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
11666 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
11667 cpu_stored = cpu_k[0].len() / hd;
11668 for j in 0..k {
11669 if !graph.encode_chain_input(
11670 embed,
11671 j as u32,
11672 &m.enorm,
11673 &m.hnorm,
11674 self.embed_multiplier,
11675 eh,
11676 ) {
11677 return Err(false);
11678 }
11679 let p = AttnDeviceParams {
11683 kv_id: self.mtp_kv_id(),
11684 layer: Self::MTP_LAYER_BASE,
11685 nh,
11686 nkv,
11687 hd,
11688 rd,
11689 position: position + j,
11690 scale: self.attn_scale,
11691 eps: self.rms_eps as f32,
11692 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11693 late_qk_norm: self.qk_norm_after_rope,
11694 output_gate: *output_gate,
11695 q_norm: q_norm.as_deref(),
11696 k_norm: k_norm.as_deref(),
11697 inv_freq: &inv_freq,
11698 cpu_k: cpu_k.clone(),
11699 cpu_v: cpu_v.clone(),
11700 cpu_stored: cpu_stored + j,
11701 o1: None,
11702 };
11703 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
11704 return Err(false);
11705 }
11706 if draft_rows < lm.1 {
11707 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
11708 return Err(false);
11709 }
11710 } else {
11711 graph.encode_lm_head(&m.final_norm, lm);
11712 }
11713 if !graph.encode_argmax(n_arg, j as u32 + 1) {
11714 return Err(false);
11715 }
11716 if split {
11717 graph.commit();
11720 }
11721 }
11722 }
11723 let t_enc = t_chain.elapsed();
11724 if graph.sync_checked().is_err() {
11725 return Err(true);
11726 }
11727 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
11728 eprintln!(
11729 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
11730 t_enc.as_secs_f64() * 1e3,
11731 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
11732 if split { ", split" } else { "" }
11733 );
11734 }
11735 let mut ids = vec![0u32; k];
11736 if !graph.chain_ids_read(&mut ids) {
11737 return Err(true);
11738 }
11739 let mut kbuf = vec![0f32; k * nkv * hd];
11740 let mut vbuf = vec![0f32; k * nkv * hd];
11741 if !crate::gpu_metal::kv_mirror_read_rows(
11742 self.mtp_kv_id(),
11743 Self::MTP_LAYER_BASE,
11744 nkv,
11745 hd,
11746 cpu_stored,
11747 k,
11748 &mut kbuf,
11749 &mut vbuf,
11750 ) {
11751 return Err(true);
11752 }
11753 for r in 0..k {
11754 m.kv.append(
11755 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
11756 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
11757 &[],
11758 );
11759 }
11760 Ok(ids)
11761 }
11762
11763 fn try_batch_graph_wgpu(
11764 &self,
11765 hiddens: &mut [f32],
11766 positions: &[usize],
11767 k: usize,
11768 spec: Option<crate::gpu::SpecTail<'_>>,
11769 ) -> crate::gpu::BatchGraphOutcome {
11770 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
11771 }
11772
11773 fn try_batch_graph_wgpu_prefix(
11778 &self,
11779 hiddens: &mut [f32],
11780 positions: &[usize],
11781 k: usize,
11782 spec: Option<crate::gpu::SpecTail<'_>>,
11783 layers_run: Option<&mut usize>,
11784 ) -> crate::gpu::BatchGraphOutcome {
11785 let graph_end = match self.mimo_moe.graph_prefix_end() {
11786 Some(end) if end < self.num_layers => {
11787 if layers_run.is_none() || spec.is_some() || end == 0 {
11788 return crate::gpu::BatchGraphOutcome::Declined;
11789 }
11790 end
11791 }
11792 _ => self.num_layers,
11793 };
11794 let _tb = std::time::Instant::now();
11795 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
11796 if self.attn_softcap > 0.0 {
11797 return crate::gpu::BatchGraphOutcome::Declined; }
11799 if let Some(reason) = self.wgpu_graph_attn_decline() {
11802 self.note_graph_decline("wgpu batch graph", reason);
11803 return crate::gpu::BatchGraphOutcome::Declined;
11804 }
11805 let nh = self.num_heads;
11806 let (nkv, hd, rd) = self.layer_geom(0);
11807 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11808 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11809 if let Some((m, i, kind, rs)) = t
11810 .graph_weight()
11811 .or_else(|| t.graph_weight_descriptor())
11812 {
11813 let name = &m.tensors[i].name;
11814 let prism = if crate::prism::is_inverse_embedding(m, name) {
11815 crate::gpu::GraphPrismOp::InverseEmbedding
11816 } else if crate::prism::is_forward_weight(m, name) {
11817 crate::gpu::GraphPrismOp::Forward
11818 } else {
11819 crate::gpu::GraphPrismOp::None
11820 };
11821 return Some(crate::gpu::GraphW {
11822 idx: i,
11823 kind,
11824 row_scale: rs,
11825 data: &[],
11826 prism,
11827 affine: crate::prism::is_affine_target(m, name),
11828 });
11829 }
11830 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11831 eprintln!(
11832 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
11833 t.rows(),
11834 t.cols()
11835 );
11836 }
11837 t.as_f32().map(|d| crate::gpu::GraphW {
11838 idx: 0,
11839 kind: 4,
11840 row_scale: &[],
11841 data: d,
11842 prism: crate::gpu::GraphPrismOp::None,
11843 affine: false,
11844 })
11845 }
11846 let built: Option<(
11847 Vec<crate::gpu::GraphLayer<'_>>,
11848 std::sync::Arc<cortiq_core::CmfModel>,
11849 )> = (|| {
11850 let mut layers = Vec::with_capacity(graph_end);
11851 let mut model = None;
11852 for li in 0..graph_end {
11853 let lw = &self.weights.layers[self.phys_layer(li)];
11854 let gffn = match &lw.ffn {
11861 FfnKind::Dense(d) if !d.segs.is_empty() => {
11862 if batch_debug {
11863 eprintln!("batch graph: dense segmented FFN at layer {li}");
11864 }
11865 return None;
11866 }
11867 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
11868 gate: gw(&d.gate_proj)?,
11869 up: gw(&d.up_proj)?,
11870 down: gw(&d.down_proj)?,
11871 },
11872 FfnKind::Moe(m) => {
11873 if m.route_tau.is_some() || m.mask.is_some() {
11880 return None;
11881 }
11882 let shared = m.shared.as_ref();
11886 let has_shared = shared.is_some();
11887 let shared_gated = matches!(shared, Some((_, Some(_))));
11888 let sgate = match shared {
11889 Some((_, Some(sg))) => gw(sg)?,
11890 _ => gw(&m.router)?,
11894 };
11895 let router = gw(&m.router)?;
11896 if router.prism != crate::gpu::GraphPrismOp::None
11902 || router.affine
11903 || sgate.prism != crate::gpu::GraphPrismOp::None
11904 || sgate.affine
11905 {
11906 return None;
11907 }
11908 let inter = m.experts.first()?.gate_proj.rows();
11909 let mut experts = Vec::with_capacity(m.experts.len() + 1);
11910 let mut q4tp: Option<bool> = None;
11911 let mut gu_q2: Option<bool> = None;
11912 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
11913 if !matches!(e.act, Act::Silu)
11914 || e.gate_proj.rows() != inter
11915 || e.up_proj.rows() != inter
11916 {
11917 return None;
11918 }
11919 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
11923 Some((mm, gi)) => (
11924 mm,
11925 gi,
11926 e.up_proj.mapped_q4t()?.1,
11927 e.down_proj.mapped_q4t()?.1,
11928 false,
11929 false,
11930 ),
11931 None => match e.gate_proj.mapped_q2tp() {
11932 Some((mm, gi)) => (
11933 mm,
11934 gi,
11935 e.up_proj.mapped_q2tp()?.1,
11936 e.down_proj.mapped_q4tp()?.1,
11937 true,
11938 true,
11939 ),
11940 None => {
11941 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
11942 (
11943 mm,
11944 gi,
11945 e.up_proj.mapped_q4tp()?.1,
11946 e.down_proj.mapped_q4tp()?.1,
11947 true,
11948 false,
11949 )
11950 }
11951 },
11952 };
11953 if *q4tp.get_or_insert(is_p) != is_p
11954 || *gu_q2.get_or_insert(is_q2) != is_q2
11955 {
11956 return None;
11957 }
11958 if [gi, ui, di].into_iter().any(|idx| {
11959 mm.tensors
11960 .get(idx)
11961 .is_some_and(|t| {
11962 crate::prism::is_forward_weight(mm, &t.name)
11963 || crate::prism::is_affine_target(mm, &t.name)
11964 })
11965 }) {
11966 return None;
11967 }
11968 model.get_or_insert_with(|| mm.clone());
11969 experts.push((gi, ui, di));
11970 }
11971 crate::gpu::GraphFfn::Moe {
11972 router,
11973 shared_gate: sgate,
11974 experts,
11975 n_exp: m.experts.len(),
11976 top_k: m.top_k,
11977 inter,
11978 norm_topk: m.norm_topk_prob,
11979 q4tp: q4tp?,
11980 gu_q2: gu_q2.unwrap_or(false),
11981 sigmoid: m.router_sigmoid,
11982 bias: m.expert_bias.as_deref(),
11983 has_shared,
11984 shared_gated,
11985 route_scale: m.routed_scaling,
11986 }
11987 }
11988 _ => return None,
11989 };
11990 let attn = match &lw.attn {
11991 AttnKind::Full {
11992 wq,
11993 wk,
11994 wv,
11995 wo,
11996 q_norm,
11997 k_norm,
11998 output_gate,
11999 softplus_gate,
12000 bias,
12001 } => {
12002 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
12003 if batch_debug {
12004 eprintln!(
12005 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
12006 softplus_gate.is_some(),
12007 self.attention_heads_per_layer.is_some()
12008 );
12009 }
12010 return None;
12011 }
12012 let (m, _, _, _) = wq
12013 .graph_weight()
12014 .or_else(|| wq.graph_weight_descriptor())?;
12015 model = Some(m.clone());
12016 crate::gpu::GraphAttn::Full {
12017 wq: gw(wq)?,
12018 wk: gw(wk)?,
12019 wv: gw(wv)?,
12020 wo: gw(wo)?,
12021 q_norm: q_norm.as_deref(),
12022 k_norm: k_norm.as_deref(),
12023 late_qk_norm: self.qk_norm_after_rope,
12024 bias: bias
12025 .as_ref()
12026 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12027 output_gate: *output_gate,
12028 cpu_k: self.kv_cache.layers[li].k_heads(),
12029 cpu_v: self.kv_cache.layers[li].v_heads(),
12030 geom: self.graph_attn_geom(li),
12031 }
12032 }
12033 AttnKind::LinearGdn(w) => {
12034 let Some(cfg) = self.gdn_cfg else {
12035 if batch_debug {
12036 eprintln!("batch graph: no GDN config at layer {li}");
12037 }
12038 return None;
12039 };
12040 let (m, _, _, _) = w
12041 .in_proj_qkv
12042 .graph_weight()
12043 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
12044 model = Some(m.clone());
12045 crate::gpu::GraphAttn::Gdn {
12046 qkv: gw(&w.in_proj_qkv)?,
12047 z: gw(&w.in_proj_z)?,
12048 a: gw(&w.in_proj_a)?,
12049 b: gw(&w.in_proj_b)?,
12050 out: gw(&w.out_proj)?,
12051 conv1d: &w.conv1d,
12052 a_log: &w.a_log,
12053 dt_bias: &w.dt_bias,
12054 norm: &w.norm,
12055 nv: cfg.num_v_heads,
12056 nk: cfg.num_k_heads,
12057 dk: cfg.key_head_dim,
12058 dv: cfg.value_head_dim,
12059 kk: cfg.conv_kernel,
12060 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
12061 }
12062 }
12063 _ => return None,
12064 };
12065 layers.push(crate::gpu::GraphLayer {
12066 input_norm: &lw.input_norm,
12067 attn,
12068 post_norm: &lw.post_norm,
12069 ffn: gffn,
12070 });
12071 }
12072 Some((layers, model?))
12073 })();
12074 let Some((layers, model)) = built else {
12075 {
12076 use std::sync::atomic::{AtomicBool, Ordering};
12077 static SAID: AtomicBool = AtomicBool::new(false);
12078 if !SAID.swap(true, Ordering::Relaxed) {
12079 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
12080 }
12081 }
12082 return crate::gpu::BatchGraphOutcome::Declined;
12083 };
12084 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12085 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
12086 }
12087 crate::gpu::forward_batch_graph(
12088 &model,
12089 self.graph_kv_id,
12090 &layers,
12091 &self.inv_freq,
12092 hiddens,
12093 nh,
12094 nkv,
12095 hd,
12096 rd,
12097 self.hidden_size,
12098 self.intermediate_size,
12099 positions,
12100 self.kv_cache.max_seq_len,
12101 gemma,
12102 self.rms_eps as f32,
12103 self.attn_scale,
12104 k,
12105 &(0..graph_end)
12106 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
12107 .collect::<Vec<_>>(),
12108 self.o1_epoch,
12109 spec,
12110 layers_run,
12111 )
12112 }
12113
12114 fn draft_probe() -> bool {
12118 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12119 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
12120 }
12121
12122 #[cfg(feature = "gpu")]
12134 fn dsv4_spec_on() -> bool {
12135 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12136 *ON.get_or_init(|| {
12137 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
12141 return v != "0";
12142 }
12143 std::env::var("CMF_DSV4_SPEC")
12150 .map(|v| v != "0")
12151 .unwrap_or_else(|_| {
12152 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
12153 })
12154 })
12155 }
12156
12157 #[cfg(feature = "gpu")]
12164 fn dsv4_spec_step(
12165 &mut self,
12166 tip_token: u32,
12167 t_next: u32,
12168 next_pos: usize,
12169 max_extra: usize,
12170 drafted: &mut usize,
12171 accepted_ctr: &mut usize,
12172 ) -> Option<(Vec<u32>, usize)> {
12173 let t_all = std::time::Instant::now();
12174 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
12175 thread_local! {
12176 static LAST: std::cell::Cell<Option<std::time::Instant>> =
12177 const { std::cell::Cell::new(None) };
12178 }
12179 LAST.with(|l| {
12180 if let Some(prev) = l.get() {
12181 eprintln!(
12182 "между раундами {:.1} мс",
12183 prev.elapsed().as_secs_f64() * 1e3
12184 );
12185 }
12186 l.set(Some(std::time::Instant::now()));
12187 });
12188 }
12189 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
12190 eprintln!("spec_step: вход pos={next_pos}");
12191 }
12192 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
12193 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
12194 if self.dspark.is_none() {
12196 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
12197 if t.is_empty() {
12198 return None;
12199 }
12200 crate::dsv4::dspark_arm(&t, cfg.dim);
12201 self.dspark = Some(crate::dsv4::DsparkState::new(
12202 self.dsv4_mtp.len(),
12203 &cfg,
12204 t.len(),
12205 ));
12206 }
12207 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
12208 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
12209 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
12210 eprintln!("spec_step: пак не построился (targets {targets:?})");
12211 }
12212 let pack = pack?;
12213 let block = crate::dsv4::dspark_block();
12214 let b_box = self.dsv4.as_mut()?;
12215 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
12216 let ds = self.dspark.as_mut()?;
12217 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
12220 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
12221 if dbg {
12222 eprintln!("spec_step: нет захвата");
12223 }
12224 return None;
12225 }
12226 ds.have_hidden = true;
12227 let tip_pos = next_pos.checked_sub(1)?;
12228 let draft_started = std::time::Instant::now();
12229 let mut conf = Vec::new();
12230 let props = crate::dsv4::dspark_draft_gpu(
12231 g,
12232 &self.dsv4_mtp,
12233 &cfg,
12234 ds,
12235 pack,
12236 st.kv_id,
12237 tip_token,
12238 tip_pos,
12239 self.pool.as_deref(),
12240 &mut conf,
12241 );
12242 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
12243 *drafted += block;
12244 if props.is_empty() || props[0] != t_next {
12245 if dbg {
12246 eprintln!(
12247 "spec_step: черновик {} (props0={:?} t_next={t_next})",
12248 if props.is_empty() {
12249 "пуст"
12250 } else {
12251 "мимо"
12252 },
12253 props.first()
12254 );
12255 }
12256 return None;
12257 }
12258 let mut k_verify = crate::dsv4::dspark_verify_k()
12265 .min(props.len())
12266 .min(max_extra.saturating_add(1));
12267 let conf_min = {
12273 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
12274 *M.get_or_init(|| {
12275 std::env::var("CMF_DSPARK_CONF_MIN")
12276 .ok()
12277 .and_then(|v| v.parse().ok())
12278 .unwrap_or(0.0)
12279 })
12280 };
12281 if conf_min > 0.0 && conf.len() >= props.len() {
12282 let mut keep = 1usize;
12283 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
12284 keep += 1;
12285 }
12286 k_verify = k_verify.min(keep.max(2));
12287 }
12288 if k_verify < 2 {
12289 return None;
12290 }
12291 let mut fed = Vec::with_capacity(k_verify);
12292 fed.push(t_next);
12293 fed.extend_from_slice(&props[1..k_verify]);
12294 let mut argmax = Vec::new();
12295 let mut logits_all = Vec::new();
12296 let mut walked = Vec::new();
12297 let txn = crate::dsv4::dsv4_verify_chunk(
12298 g,
12299 layers,
12300 &cfg,
12301 st,
12302 &fed,
12303 next_pos,
12304 &self.inv_freq,
12305 self.pool.as_deref(),
12306 &targets,
12307 &mut argmax,
12308 &mut logits_all,
12309 &mut walked,
12310 );
12311 if txn.is_none() && dbg {
12312 eprintln!("spec_step: verify отказал");
12313 }
12314 let txn = txn?;
12315 let spec_gpu_end = txn.gpu_end;
12316 let b = fed.len();
12317 let mut accepted = 1usize;
12318 while accepted < b && fed[accepted] == argmax[accepted - 1] {
12319 accepted += 1;
12320 }
12321 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
12326 accepted = 1;
12327 }
12328 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
12329 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
12330 }
12331 let t_fin = std::time::Instant::now();
12332 if !crate::dsv4::dsv4_spec_finish(
12333 g,
12334 layers,
12335 &cfg,
12336 st,
12337 txn,
12338 accepted,
12339 &fed,
12340 &self.inv_freq,
12341 self.pool.as_deref(),
12342 ) {
12343 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
12344 return None;
12345 }
12346 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
12347 eprintln!(
12348 "finish(k={accepted}): {:.1} мс",
12349 t_fin.elapsed().as_secs_f64() * 1e3
12350 );
12351 }
12352 *accepted_ctr += accepted - 1;
12353 let (hc, dim) = (cfg.hc_mult, cfg.dim);
12358 let dev_caps: Vec<usize> = targets
12363 .iter()
12364 .copied()
12365 .filter(|&t| t < spec_gpu_end)
12366 .collect();
12367 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
12368 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
12369 return None;
12370 }
12371 for t in 0..accepted {
12372 let tip = t + 1 == accepted;
12373 for (slot, &tl) in targets.iter().enumerate() {
12374 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
12375 let lo = (di * b + t) * hc * dim;
12376 crate::dsv4::dspark_capture(
12377 &caps_all[lo..lo + hc * dim],
12378 &cfg,
12379 slot,
12380 &mut ds.main_hidden,
12381 );
12382 } else if tip
12383 && crate::dsv4::dspark_peek_slot(slot, dim, {
12384 let lo = slot * dim;
12385 &mut ds.main_hidden[lo..lo + dim]
12386 })
12387 {
12388 } else {
12393 crate::dsv4::dspark_capture(
12397 &walked[t * hc * dim..(t + 1) * hc * dim],
12398 &cfg,
12399 slot,
12400 &mut ds.main_hidden,
12401 );
12402 }
12403 }
12404 crate::dsv4::dspark_ring_append(
12405 g,
12406 &self.dsv4_mtp,
12407 &cfg,
12408 ds,
12409 next_pos + t,
12410 self.pool.as_deref(),
12411 );
12412 }
12413 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
12414 self.graph_logits = Some(row);
12415 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
12420 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
12421 crate::dsv4::pick_tally_arm();
12422 }
12423 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
12424 eprintln!(
12425 "spec_step total {:.1} мс (k={accepted})",
12426 t_all.elapsed().as_secs_f64() * 1e3
12427 );
12428 }
12429 Some((fed[1..accepted].to_vec(), next_pos + accepted))
12430 }
12431
12432 fn dspark_probe(&mut self, position: usize, token_id: u32) {
12433 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
12434 return;
12435 }
12436 let trunk_now = crate::dsv4::pick_tally_take();
12438 crate::dsv4::trunk_freq_note(&trunk_now);
12439 if !trunk_now.is_empty() {
12440 self.dspark_trunk_picks.push(trunk_now);
12441 let keep = crate::dsv4::dspark_block();
12442 if self.dspark_trunk_picks.len() > keep {
12443 self.dspark_trunk_picks.remove(0);
12444 }
12445 }
12446 for p in std::mem::take(&mut self.dspark_pending) {
12449 let Some(i) = position.checked_sub(p.0 + 1) else {
12450 continue;
12451 };
12452 let mut p = p;
12453 if i < p.1.len() {
12454 if p.2 && p.1[i] == token_id {
12455 p.3 = i + 1;
12456 } else {
12457 p.2 = false;
12458 }
12459 if i + 1 < p.1.len() {
12460 self.dspark_pending.push(p);
12461 continue;
12462 }
12463 }
12464 self.dspark_hist.push(p.3);
12465 self.dspark_real.push(token_id);
12466 }
12467 let Some(b) = &mut self.dsv4 else { return };
12468 let (g, layers, cfg) = (&b.0, &b.1, b.2);
12469 let n_layers = layers.len();
12470 if self.dspark.is_none() {
12471 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
12472 if t.is_empty() {
12473 return;
12474 }
12475 eprintln!(
12476 "DSpark: захват со слоёв {t:?}, блок {}",
12477 crate::dsv4::dspark_block()
12478 );
12479 crate::dsv4::dspark_arm(&t, cfg.dim);
12480 self.dspark = Some(crate::dsv4::DsparkState::new(
12481 self.dsv4_mtp.len(),
12482 &cfg,
12483 t.len(),
12484 ));
12485 }
12486 let ds = self.dspark.as_mut().unwrap();
12487 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
12488 return; }
12490 let mut conf = Vec::new();
12491 crate::dsv4::pick_tally_arm();
12492 let draft_started = std::time::Instant::now();
12497 #[cfg(feature = "gpu")]
12498 let gpu_draft = crate::dsv4::dspark_gpu_on();
12499 #[cfg(not(feature = "gpu"))]
12500 let gpu_draft = false;
12501 let props = if gpu_draft {
12502 #[cfg(feature = "gpu")]
12503 {
12504 let kv_id = b.3.kv_id;
12505 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
12506 Some(pk) => crate::dsv4::dspark_draft_gpu(
12507 g,
12508 &self.dsv4_mtp,
12509 &cfg,
12510 ds,
12511 pk,
12512 kv_id,
12513 token_id,
12514 position,
12515 self.pool.as_deref(),
12516 &mut conf,
12517 ),
12518 None => Vec::new(),
12519 }
12520 }
12521 #[cfg(not(feature = "gpu"))]
12522 Vec::new()
12523 } else {
12524 crate::gpu::cpu_scope(|| {
12525 crate::dsv4::dspark_draft(
12526 g,
12527 &self.dsv4_mtp,
12528 &cfg,
12529 ds,
12530 token_id,
12531 position,
12532 self.pool.as_deref(),
12533 &mut conf,
12534 )
12535 })
12536 };
12537 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
12538 let draft_picks = crate::dsv4::pick_tally_take();
12539 crate::dsv4::dspark_freq_note(&draft_picks);
12540 crate::dsv4::pick_tally_arm();
12543 if !props.is_empty() {
12544 let (tu, tt) = {
12548 let flat: Vec<(usize, Vec<usize>)> = self
12549 .dspark_trunk_picks
12550 .iter()
12551 .flat_map(|v| v.iter().cloned())
12552 .collect();
12553 let mut per: std::collections::HashMap<usize, Vec<usize>> =
12555 std::collections::HashMap::new();
12556 for (li, picks) in flat {
12557 per.entry(li).or_default().extend(picks);
12558 }
12559 let n = per.len().max(1);
12560 let mut u = 0usize;
12561 let mut t = 0usize;
12562 for (_, v) in per {
12563 t += v.len();
12564 u += v.iter().collect::<std::collections::HashSet<_>>().len();
12565 }
12566 (u / n, t / n)
12567 };
12568 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
12569 self.dspark_exp.push((tu, tt, du, dt));
12570 self.dspark_pending.push((position, props, true, 0));
12571 }
12572 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
12573 let n = self.dspark_hist.len() as f32;
12574 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
12575 let block = crate::dsv4::dspark_block();
12576 let mut at = vec![0usize; block + 1];
12577 for &k in &self.dspark_hist {
12578 at[k] += 1;
12579 }
12580 let mut surv = Vec::with_capacity(block);
12582 for i in 1..=block {
12583 let k = at[i..].iter().sum::<usize>() as f32 / n;
12584 surv.push(format!("{k:.2}"));
12585 }
12586 let distinct = self
12587 .dspark_real
12588 .iter()
12589 .collect::<std::collections::HashSet<_>>()
12590 .len();
12591 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
12592 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
12593 });
12594 let m = self.dspark_exp.len().max(1);
12595 eprintln!(
12596 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
12597 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
12598 self.dspark_hist.len(),
12599 mean + 1.0,
12600 surv.join(" ")
12601 );
12602 eprintln!(
12603 "DSpark: разных токенов {distinct} из {} (вырожденность), \
12604 эксперты ствол {}/{} на слой за {block} токенов, \
12605 черновик {}/{} за блок, draft {:.2} мс/блок",
12606 self.dspark_real.len(),
12607 tu / m,
12608 tt / m,
12609 du / m,
12610 dt / m,
12611 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
12612 );
12613 }
12614 }
12615
12616 fn forward_layers_upto(
12617 &mut self,
12618 hidden: &[f32],
12619 position: usize,
12620 task_mask: Option<&TaskMask>,
12621 upto: Option<usize>,
12622 ) -> Vec<f32> {
12623 if let Some(plan) = self.gpu_plan.clone() {
12629 if upto.is_none() && plan.len() > 1 {
12630 let mut h = hidden.to_vec();
12631 for &(dev, from, upto_incl) in plan.iter() {
12632 h = crate::gpu::with_device(dev, || {
12633 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
12634 });
12635 }
12636 return h;
12637 }
12638 }
12639 self.forward_layers_span(hidden, position, task_mask, 0, upto)
12640 }
12641
12642 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
12647 self.set_gpu_plan_at(devices, None)
12648 }
12649
12650 pub fn set_gpu_plan_at(
12654 &mut self,
12655 devices: Option<&[usize]>,
12656 at: Option<usize>,
12657 ) -> Result<(), String> {
12658 let Some(devs) = devices.filter(|d| d.len() > 1) else {
12659 self.gpu_plan = None;
12660 return Ok(());
12661 };
12662 self.split_supported()?;
12663 let n = self.num_layers;
12664 if devs.len() > n {
12665 return Err(format!("{} devices for {n} layers", devs.len()));
12666 }
12667 if let Some(k) = at {
12668 if k == 0 || k >= n {
12669 return Err(format!("split at {k}: the model has {n} layers"));
12670 }
12671 if devs.len() == 2 {
12672 self.gpu_plan = Some(std::sync::Arc::new(vec![
12673 (devs[0], 0, k - 1),
12674 (devs[1], k, n - 1),
12675 ]));
12676 return Ok(());
12677 }
12678 return Err(format!(
12679 "an explicit split point takes exactly 2 devices, got {}",
12680 devs.len()
12681 ));
12682 }
12683 let per = n.div_ceil(devs.len());
12684 let mut plan = Vec::with_capacity(devs.len());
12685 let mut from = 0usize;
12686 for &d in devs {
12687 if from >= n {
12688 break;
12689 }
12690 let upto = (from + per - 1).min(n - 1);
12691 plan.push((d, from, upto));
12692 from = upto + 1;
12693 }
12694 self.gpu_plan = Some(std::sync::Arc::new(plan));
12695 Ok(())
12696 }
12697
12698 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
12700 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
12701 }
12702
12703 fn forward_layers_span(
12709 &mut self,
12710 hidden: &[f32],
12711 position: usize,
12712 task_mask: Option<&TaskMask>,
12713 from: usize,
12714 upto: Option<usize>,
12715 ) -> Vec<f32> {
12716 debug_assert!(
12717 from == 0
12718 || (self.dsv4.is_none()
12719 && self.dsv41.is_none()
12720 && self.qwen4_exp.is_none()
12721 && self.g3n.is_none())
12722 );
12723 #[cfg(target_os = "macos")]
12729 if !crate::gpu_metal::wait_replay() {
12730 self.fail_metal_graph("the pending async replay failed before a plain forward");
12731 return vec![0.0; self.hidden_size];
12732 }
12733 if let Some(b) = &mut self.qwen4_exp {
12734 let _ = (task_mask, upto);
12735 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
12736 let mut logits = Vec::new();
12737 crate::qwen4_exp::forward_token(
12738 &b.0,
12739 &b.1,
12740 &b.2,
12741 &mut b.3,
12742 token_id,
12743 position,
12744 &self.inv_freq,
12745 self.pool.as_deref(),
12746 &mut logits,
12747 true,
12748 );
12749 self.graph_logits = Some(logits);
12750 return vec![0.0; self.hidden_size];
12751 }
12752 if let Some(b) = &mut self.dsv4 {
12758 let _ = (task_mask, upto);
12759 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
12760 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
12761 st.pos = position;
12762 let mut logits = Vec::new();
12763 crate::dsv4::forward_token(
12764 g,
12765 layers,
12766 &cfg,
12767 st,
12768 token_id,
12769 &self.inv_freq,
12770 self.pool.as_deref(),
12771 &mut logits,
12772 );
12773 self.graph_logits = Some(logits);
12774 self.dspark_probe(position, token_id);
12775 return vec![0.0; self.hidden_size];
12778 }
12779 if let Some(b) = &mut self.dsv41 {
12781 let _ = (task_mask, upto);
12782 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
12783 let mut logits = Vec::new();
12784 crate::dsv41::forward_token(
12785 &b.0,
12786 &b.1,
12787 &b.2,
12788 &mut b.3,
12789 token_id,
12790 position,
12791 self.pool.as_deref(),
12792 &mut logits,
12793 );
12794 self.graph_logits = Some(logits);
12795 return vec![0.0; self.hidden_size];
12796 }
12797 if let Some(b) = &self.g3n {
12800 let _ = (task_mask, upto);
12801 return crate::g3n::g3n_forward(
12802 &b.0,
12803 &b.1,
12804 hidden,
12805 position,
12806 &mut self.kv_cache.layers,
12807 self.num_heads,
12808 self.num_kv_heads,
12809 self.head_dim,
12810 self.pool.as_deref(),
12811 );
12812 }
12813 let mut h = hidden.to_vec();
12814 self.mimo_moe_prepare();
12817 let _mimo_q8 = self.mimo_moe.is_on()
12818 .then(crate::qtensor::enter_full_gpu_q8_scope);
12819 let (nh, _nkv, _hd, hs, _rd, eps) = (
12822 self.num_heads,
12823 self.num_kv_heads,
12824 self.head_dim,
12825 self.hidden_size,
12826 self.rotary_dim,
12827 self.rms_eps,
12828 );
12829 let pool = self.pool.clone();
12830 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
12842 let graph_on = match graph_env.as_deref() {
12843 Some("0") => false,
12844 Some("prefill") => false, Some(_) => true,
12846 None => crate::gpu::wgpu_graph_default(),
12852 };
12853 let graph_trusted =
12854 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
12855 let race_eligible = graph_on
12856 && upto.is_none()
12857 && task_mask.is_none()
12858 && from == 0
12859 && !crate::gpu::graph_unsupported();
12860 let mut tail_start = 0usize;
12861 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
12862 let t_graph = std::time::Instant::now();
12863 let mut lg = Vec::new();
12864 let mut gl = 0usize;
12865 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
12866 let declined = built.is_none();
12867 let built = match built {
12868 Some(Ok(hh)) => Some(hh),
12869 Some(Err(())) => {
12870 self.clear_sequence_state();
12874 self.graph_failed
12875 .store(true, std::sync::atomic::Ordering::Relaxed);
12876 self.cancel
12877 .store(true, std::sync::atomic::Ordering::Relaxed);
12878 tracing::error!("token graph failed after admission; sequence state cleared");
12879 return vec![0.0; self.hidden_size];
12880 }
12881 None => None,
12882 };
12883 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
12888 crate::gpu::graph_mark_unsupported();
12889 }
12890 graph_note(built.is_some(), gl, self.num_layers);
12891 if let Some(hh) = built {
12892 let dur = t_graph.elapsed();
12893 if std::env::var("CMF_GRAPH_PROF").is_ok() {
12894 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
12895 }
12896 if gl > 0 && gl < self.num_layers {
12897 h = hh;
12903 tail_start = gl;
12904 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
12905 if !graph_trusted {
12906 crate::gpu::graph_race_record(true, dur);
12907 }
12908 if !lg.is_empty() {
12909 lg.resize(self.vocab_size, 0.0);
12912 if let Some(c) = self.final_softcap {
12913 for l in lg.iter_mut() {
12914 *l = c * (*l / c).tanh();
12915 }
12916 }
12917 self.graph_logits = Some(lg);
12918 }
12919 return hh;
12920 }
12921 }
12927 }
12928 let span = from > 0 || upto.is_some();
12952 if span && graph_on && task_mask.is_none() && graph_trusted {
12953 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
12954 let mut lg = Vec::new();
12955 let mut gl = 0usize;
12956 let span_res =
12957 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
12958 let span_res = match span_res {
12959 Some(Ok(hh)) => Some(hh),
12960 Some(Err(())) => {
12961 self.clear_sequence_state();
12962 self.graph_failed
12963 .store(true, std::sync::atomic::Ordering::Relaxed);
12964 self.cancel
12965 .store(true, std::sync::atomic::Ordering::Relaxed);
12966 tracing::error!(
12967 "span token graph failed after admission; sequence state cleared"
12968 );
12969 return vec![0.0; self.hidden_size];
12970 }
12971 None => None,
12972 };
12973 graph_note(span_res.is_some(), gl, upto_excl - from);
12974 if std::env::var("CMF_GPU_DEBUG").is_ok() {
12975 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
12979 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
12980 eprintln!(
12981 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
12982 upto_excl - from,
12983 span_res.is_some()
12984 );
12985 }
12986 }
12987 if let Some(hh) = span_res {
12988 if gl == upto_excl - from {
12989 if !lg.is_empty() {
12990 lg.resize(self.vocab_size, 0.0);
12991 if let Some(c) = self.final_softcap {
12992 for l in lg.iter_mut() {
12993 *l = c * (*l / c).tanh();
12994 }
12995 }
12996 self.graph_logits = Some(lg);
12997 }
12998 crate::gpu::set_layer(-1);
12999 return hh;
13000 }
13001 h = hh;
13003 tail_start = from + gl;
13004 }
13005 }
13006 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
13011
13012 let host_tail = tail_start > from;
13022 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
13023 let automatic_gpu_prefix = self.automatic_gpu_prefix();
13024
13025 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
13026 #[cfg(target_os = "macos")]
13027 let mut gpu_skip_until = 0usize;
13028 for li in tail_start.max(from)..self.num_layers {
13029 let _capacity_tail = automatic_gpu_prefix
13030 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
13031 .map(|_| crate::gpu::enter_cpu_scope());
13032 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
13034 if li > u {
13035 break;
13036 }
13037 }
13038 if let Some(mask) = task_mask {
13039 if !mask.layer_alive(li) {
13040 continue; }
13042 }
13043 #[cfg(target_os = "macos")]
13047 {
13048 if li < gpu_skip_until {
13049 continue;
13050 }
13051 if task_mask.is_none() {
13052 let end = self.q1_graph_gpu(li, upto, position, &mut h);
13053 if self
13054 .graph_failed
13055 .load(std::sync::atomic::Ordering::Relaxed)
13056 {
13057 return vec![0.0; self.hidden_size];
13061 }
13062 if end > li {
13063 gpu_skip_until = end;
13064 if self.is_loop_end(end - 1) && end < self.num_layers {
13067 h = inference::rms_norm(
13068 &h,
13069 &self.weights.final_norm,
13070 self.rms_eps,
13071 self.norm_style,
13072 );
13073 }
13074 continue;
13075 }
13076 }
13077 }
13078
13079 if task_mask.is_none() {
13080 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
13081 crate::gpu::BatchGraphOutcome::Completed => continue,
13082 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
13083 crate::gpu::BatchGraphOutcome::Declined => {},
13084 }
13085 }
13086 #[cfg(feature = "gpu")]
13087 self.pull_lagging_host_kv(li, li + 1, position);
13088 let lw = &self.weights.layers[self.phys_layer(li)];
13089 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
13090 if tp.parse::<usize>().ok() == Some(position) {
13091 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
13092 eprintln!(
13093 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
13094 h[0], h[1]
13095 );
13096 }
13097 }
13098 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
13101 inference::rms_norm_into(
13102 &h,
13103 &lw.input_norm,
13104 self.rms_eps,
13105 self.norm_style,
13106 &mut self.ws.n1,
13107 );
13108 drop(prof);
13109
13110 let attn_out = match &lw.attn {
13111 AttnKind::Mla(w) => {
13112 let inv_freq_l = self.layer_inv_freq(li);
13113 let rs = self.layer_rope_scale(li);
13114 let eps = self.rms_eps;
13115 let pool = self.pool.clone();
13116 mla_attention(
13117 w,
13118 &self.ws.n1,
13119 &mut self.kv_cache.layers[li],
13120 position,
13121 &inv_freq_l,
13122 rs,
13123 eps,
13124 pool.as_deref(),
13125 )
13126 }
13127 AttnKind::Linear(w) => {
13128 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
13129 vmf_phase_forward(
13130 &self.ws.n1,
13131 w,
13132 &cfg,
13133 &mut self.kv_cache.layers[li].linear_state,
13134 self.pool.as_deref(),
13135 )
13136 }
13137 AttnKind::Kda(w) => {
13138 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
13139 crate::linear_core::kda_forward(
13140 &self.ws.n1,
13141 w,
13142 &cfg,
13143 &mut self.kv_cache.layers[li].linear_state,
13144 self.pool.as_deref(),
13145 )
13146 }
13147 AttnKind::LinearGdn(w) => {
13148 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
13149 gdn_forward(
13150 &self.ws.n1,
13151 w,
13152 &cfg,
13153 &mut self.kv_cache.layers[li].linear_state,
13154 self.pool.as_deref(),
13155 )
13156 }
13157 AttnKind::ShortConv(w) => {
13158 let cfg = self
13159 .short_conv_cfg
13160 .expect("short-conv layer without short_conv_cfg");
13161 short_conv_forward(
13162 &self.ws.n1,
13163 w,
13164 &cfg,
13165 &mut self.kv_cache.layers[li].linear_state,
13166 self.pool.as_deref(),
13167 )
13168 }
13169 AttnKind::Full {
13170 wq,
13171 wk,
13172 wv,
13173 wo,
13174 q_norm,
13175 k_norm,
13176 output_gate,
13177 softplus_gate,
13178 bias,
13179 } if self.kv_cache.layers[li].o1_sealed() => {
13180 let inv_freq_l = self.layer_inv_freq(li);
13183 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
13184 let cfg = QwenAttnCfg {
13185 num_heads: self.layer_num_heads(li),
13186 num_kv_heads: nkv_l,
13187 head_dim: hd_l,
13188 hidden_size: hs,
13189 position,
13190 inv_freq: &inv_freq_l,
13191 rotary_dim: rd_l,
13192 scale: self.attn_scale,
13193 softcap: self.attn_softcap,
13194 window: None,
13195 v_norm: self.attn_v_norm,
13196 qk_norm_after_rope: self.qk_norm_after_rope,
13197 q_norm: q_norm.as_deref(),
13198 k_norm: k_norm.as_deref(),
13199 output_gate: *output_gate,
13200 softplus_gate: softplus_gate
13201 .as_ref()
13202 .map(|(gate, per_head)| (gate, *per_head)),
13203 rope_scale: self.layer_rope_scale(li),
13204 bias: bias
13205 .as_ref()
13206 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13207 rms_eps: eps,
13208 norm_style: self.norm_style,
13209 pool: pool.as_deref(),
13210 v_head_dim: self.layer_v_dim(li),
13211 };
13212 attention::qwen_attention_nystrom(
13213 &self.ws.n1,
13214 wq,
13215 wk,
13216 wv,
13217 wo,
13218 &mut self.kv_cache.layers[li],
13219 &cfg,
13220 )
13221 }
13222 AttnKind::Full {
13223 wq,
13224 wk,
13225 wv,
13226 wo,
13227 q_norm,
13228 k_norm,
13229 output_gate,
13230 softplus_gate,
13231 bias,
13232 } => 'attn: {
13233 let dropin_reason =
13238 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
13239 if let Some(reason) = dropin_reason {
13240 self.note_graph_decline("wgpu attn dropin", reason);
13241 }
13242 if graph_on
13243 && dropin_reason.is_none()
13244 && !*output_gate
13245 && softplus_gate.is_none()
13246 && self.attention_heads_per_layer.is_none()
13247 && bias.is_none()
13248 && task_mask.is_none()
13249 {
13250 let inv_freq_l = self.layer_inv_freq(li);
13251 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
13252 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13253 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
13254 wq.mapped_q1(),
13255 wk.mapped_q1(),
13256 wv.mapped_q1(),
13257 wo.mapped_q1(),
13258 ) {
13259 let gm = gm.clone();
13260 let mut out = vec![0f32; hs];
13261 let cache = &self.kv_cache.layers[li];
13262 if crate::gpu::attn_dropin(
13263 &gm,
13264 self.graph_kv_id,
13265 li,
13266 &self.ws.n1,
13267 qi,
13268 ki,
13269 vi,
13270 oi,
13271 q_norm.as_deref(),
13272 k_norm.as_deref(),
13273 self.qk_norm_after_rope,
13274 &inv_freq_l,
13275 nh,
13276 nkv_l,
13277 hd_l,
13278 rd_l,
13279 hs,
13280 position,
13281 self.kv_cache.max_seq_len,
13282 gemma,
13283 eps as f32,
13284 cache.k_heads(),
13285 cache.v_heads(),
13286 &mut out,
13287 ) {
13288 break 'attn out;
13289 }
13290 }
13291 }
13292 let masked = task_mask
13293 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
13294 .unwrap_or(false);
13295 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
13296 let plain = self.layer_attn_plain(li);
13299 match (masked, f32_view) {
13300 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
13303 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
13304 attention::multi_head_attention(
13305 &self.ws.n1,
13306 q,
13307 k,
13308 v,
13309 o,
13310 &mut self.kv_cache.layers[li],
13311 self.num_heads,
13312 self.num_kv_heads,
13313 self.head_dim,
13314 self.hidden_size,
13315 position,
13316 &active_heads,
13317 &self.inv_freq,
13318 )
13319 }
13320 (masked, _) => {
13321 if masked {
13322 tracing::warn!(
13323 "layer {li}: head mask on quantized weights or on a \
13324 window/sink/per-layer-geometry layer not supported \
13325 yet — executing dense"
13326 );
13327 }
13328 let inv_freq_l = self.layer_inv_freq(li);
13329 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
13330 let cfg = QwenAttnCfg {
13331 num_heads: self.layer_num_heads(li),
13332 num_kv_heads: nkv_l,
13333 head_dim: hd_l,
13334 hidden_size: hs,
13335 position,
13336 inv_freq: &inv_freq_l,
13337 rotary_dim: rd_l,
13338 scale: self.attn_scale,
13339 softcap: self.attn_softcap,
13340 window: self.layer_window(li),
13341 v_norm: self.attn_v_norm,
13342 qk_norm_after_rope: self.qk_norm_after_rope,
13343 q_norm: q_norm.as_deref(),
13344 k_norm: k_norm.as_deref(),
13345 output_gate: *output_gate,
13346 softplus_gate: softplus_gate
13347 .as_ref()
13348 .map(|(gate, per_head)| (gate, *per_head)),
13349 rope_scale: self.layer_rope_scale(li),
13350 bias: bias
13351 .as_ref()
13352 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13353 rms_eps: eps,
13354 norm_style: self.norm_style,
13355 pool: pool.as_deref(),
13356 v_head_dim: self.layer_v_dim(li),
13357 };
13358 attention::qwen_attention(
13359 &self.ws.n1,
13360 wq,
13361 wk,
13362 wv,
13363 wo,
13364 &mut self.kv_cache.layers[li],
13365 &cfg,
13366 )
13367 }
13368 }
13369 }
13370 };
13371 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
13374 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
13375 None => attn_out,
13376 };
13377 let lw = &self.weights.layers[self.phys_layer(li)];
13378 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
13379 inference::add_rmsnorm_fused_into(
13380 &mut h,
13381 &attn_out,
13382 &lw.post_norm,
13383 self.rms_eps,
13384 self.norm_style,
13385 &mut self.ws.p1,
13386 );
13387 drop(prof);
13388 let mut attn_out = attn_out;
13389 attention::recycle_buf(&mut attn_out);
13390 let post_normed = &self.ws.p1;
13391
13392 let ffn_masked = task_mask
13393 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
13394 .unwrap_or(false);
13395 let ffn_out = match (ffn_masked, &lw.ffn) {
13407 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
13411 let row = task_mask
13412 .and_then(|tm| tm.ffn_masks.get(li))
13413 .map(|v| v.as_slice());
13414 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
13415 }
13416 (true, FfnKind::Dense(d)) => {
13417 let tm = task_mask.unwrap();
13418 let alive = tm.ffn_active_count(li);
13419 let deep = alive * 2 <= self.intermediate_size;
13420 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
13421 let active = tm.ffn_active_indices(li);
13422 sparse_ffn_quant(
13423 d,
13424 post_normed,
13425 &active,
13426 self.hidden_size,
13427 self.pool.as_deref(),
13428 )
13429 } else if deep
13430 && let (Some(g), Some(u), Some(dn)) = (
13431 d.gate_proj.as_f32(),
13432 d.up_proj.as_f32(),
13433 d.down_proj.as_f32(),
13434 )
13435 {
13436 let active = tm.ffn_active_indices(li);
13437 inference::sparse_ffn_forward(
13438 post_normed,
13439 g,
13440 u,
13441 dn,
13442 self.hidden_size,
13443 self.intermediate_size,
13444 &active,
13445 self.pool.as_deref(),
13446 )
13447 } else {
13448 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
13449 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
13450 }
13451 }
13452 (true, FfnKind::Moe(m)) => {
13453 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
13457 ffn_forward(
13458 &lw.ffn,
13459 post_normed,
13460 self.pool.as_deref(),
13461 allowed.as_deref(),
13462 )
13463 }
13464 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
13465 dm,
13466 post_normed,
13467 &h,
13468 self.rms_eps,
13469 self.norm_style,
13470 self.pool.as_deref(),
13471 ),
13472 (false, _) => match &lw.ffn {
13473 FfnKind::DenseMoe(dm) => dense_moe_ffn(
13474 dm,
13475 post_normed,
13476 &h,
13477 self.rms_eps,
13478 self.norm_style,
13479 self.pool.as_deref(),
13480 ),
13481 FfnKind::Moe(m)
13482 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
13483 {
13484 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
13485 }
13486 _ => {
13487 let allowed = match (&lw.ffn, task_mask) {
13488 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
13489 _ => None,
13490 };
13491 ffn_forward(
13492 &lw.ffn,
13493 post_normed,
13494 self.pool.as_deref(),
13495 allowed.as_deref(),
13496 )
13497 }
13498 },
13499 };
13500 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
13501 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
13502 None => ffn_out,
13503 };
13504 for (i, &f) in ffn_out.iter().enumerate() {
13505 h[i] += f;
13506 }
13507 let mut ffn_out = ffn_out;
13508 attention::recycle_buf(&mut ffn_out);
13509
13510 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
13512 for v in h.iter_mut() {
13513 *v *= sc;
13514 }
13515 }
13516 if self.layer_dump.is_some() {
13518 self.dump_layer_row(position, li, &h);
13519 }
13520
13521 if self.is_loop_end(li) && li + 1 < self.num_layers {
13524 h = inference::rms_norm(
13525 &h,
13526 &self.weights.final_norm,
13527 self.rms_eps,
13528 self.norm_style,
13529 );
13530 }
13531
13532 if self.dyn_phi_layer == Some(li) {
13536 self.update_dyn_phi(&h);
13537 }
13538 }
13539 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
13541 crate::gpu::graph_race_record(false, t.elapsed());
13542 }
13543
13544 h
13545 }
13546
13547 fn update_dyn_phi(&mut self, h: &[f32]) {
13550 const A: f32 = 0.2;
13551 if self.dyn_phi_ema.len() != h.len() {
13552 self.dyn_phi_ema = vec![0.0; h.len()];
13553 self.dyn_phi_seen = 0;
13554 }
13555 if self.dyn_phi_seen == 0 {
13556 self.dyn_phi_ema.copy_from_slice(h);
13557 } else {
13558 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
13559 *e = (1.0 - A) * *e + A * v;
13560 }
13561 }
13562 self.dyn_phi_seen += 1;
13563 }
13564
13565 pub fn dyn_phi(&self) -> &[f32] {
13567 &self.dyn_phi_ema
13568 }
13569
13570 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
13572 self.dyn_phi_layer = layer;
13573 self.dyn_phi_ema.clear();
13574 self.dyn_phi_seen = 0;
13575 }
13576
13577 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
13579 let Some(model) = &self.model else {
13580 return Vec::new();
13581 };
13582 model
13583 .header
13584 .skills
13585 .iter()
13586 .enumerate()
13587 .filter_map(|(i, sk)| {
13588 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
13589 let sel = sk.selection.as_ref()?;
13590 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
13591 })
13592 .collect()
13593 }
13594
13595 pub fn active_skill(&self) -> Option<usize> {
13597 self.dyn_active
13598 }
13599
13600 pub fn enable_dynamic_routing(&mut self) -> usize {
13605 use crate::swarm::{DynRouter, RoutableSkill};
13606 let Some(model) = self.model.clone() else {
13607 return 0;
13608 };
13609 if self.dyn_blend_loaded {
13612 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
13613 return 0;
13614 }
13615 if let Some(a) = self.dyn_active {
13619 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
13620 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
13621 return 0;
13622 }
13623 }
13624 let hidden = self.hidden_size;
13625 let mut skills = Vec::new();
13626 for (idx, id, _phi) in self.dynamic_skills() {
13627 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
13628 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
13629 skills.push(rs);
13630 }
13631 }
13632 }
13633 if skills.is_empty() {
13634 return 0;
13635 }
13636 let phi = skills[0].phi_layer;
13638 if skills.iter().any(|s| s.phi_layer != phi) {
13639 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
13640 }
13641 let n = skills.len();
13642 self.set_dyn_phi_layer(Some(phi));
13643 self.dyn_router = Some(DynRouter::new(skills));
13644 n
13645 }
13646
13647 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
13649 self.dyn_router
13650 .as_ref()
13651 .map(|r| r.switches.clone())
13652 .unwrap_or_default()
13653 }
13654
13655 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
13658 let _mimo_q8 = self.mimo_moe.is_on()
13659 .then(crate::qtensor::enter_full_gpu_q8_scope);
13660 let rows = self.weights.lm_head.rows();
13661 let mut logits = attention::take_buf(rows.min(self.vocab_size));
13662 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
13666 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
13667 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
13668 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
13669 rows, self.hidden_size, &mut logits)
13670 });
13671 if !served {
13672 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
13673 }
13674 logits.resize(self.vocab_size, 0.0);
13675 if let Some(m) = self.logit_multiplier {
13676 for l in logits.iter_mut() {
13677 *l *= m;
13678 }
13679 }
13680 if let Some(c) = self.final_softcap {
13681 for l in logits.iter_mut() {
13682 *l = c * (*l / c).tanh();
13683 }
13684 }
13685 if let Some(cm) = self.head_clusters.as_ref() {
13686 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
13687 }
13688 logits
13689 }
13690
13691 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
13694 let h = hidden.len();
13695 let ncl = cm.len() / h.max(1);
13696 if ncl == 0 || logits.len() % ncl != 0 {
13697 return;
13698 }
13699 let cs = logits.len() / ncl;
13700 let mut lc = vec![0.0f32; ncl];
13702 for c in 0..ncl {
13703 let row = &cm[c * h..(c + 1) * h];
13704 let mut s = 0.0f32;
13705 for j in 0..h {
13706 s += row[j] * hidden[j];
13707 }
13708 lc[c] = s;
13709 }
13710 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13711 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
13712 for c in 0..ncl {
13713 let blk = &mut logits[c * cs..(c + 1) * cs];
13714 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13715 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
13716 let add = lc[c] - lse - bl;
13717 for v in blk.iter_mut() {
13718 *v += add;
13719 }
13720 }
13721 }
13722
13723 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
13728 self.clear_sequence_state();
13729 crate::gpu::graph_race_begin_generation();
13733 if task_mask.is_none() {
13734 self.o1_begin();
13735 }
13736 let mut hidden = vec![0.0f32; self.hidden_size];
13737 for (pos, &id) in ids.iter().enumerate() {
13738 let emb = self.embed_single(id);
13739 hidden = self.forward_layers(&emb, pos, task_mask);
13740 }
13741 if let Err(err) = self.o1_seal_checked() {
13742 self.o1_fail(err);
13743 }
13744 inference::rms_norm_into(
13745 &hidden,
13746 &self.weights.final_norm,
13747 self.rms_eps,
13748 self.norm_style,
13749 &mut self.ws.n1,
13750 );
13751 self.lm_head_forward(&self.ws.n1)
13752 }
13753}
13754
13755pub fn create_test_pipeline(
13757 hidden_size: usize,
13758 intermediate_size: usize,
13759 num_heads: usize,
13760 num_kv_heads: usize,
13761 head_dim: usize,
13762 num_layers: usize,
13763 vocab_size: usize,
13764) -> Pipeline {
13765 let synth = |n: usize, salt: usize| -> Vec<f32> {
13768 (0..n)
13769 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
13770 .collect()
13771 };
13772 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
13773 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
13774 };
13775 let layer_weights: Vec<LayerWeights> = (0..num_layers)
13776 .map(|li| LayerWeights {
13777 input_norm: vec![1.0; hidden_size],
13778 post_norm: vec![1.0; hidden_size],
13779 attn_out_norm: None,
13780 ffn_out_norm: None,
13781 layer_scale: None,
13782 ffn: FfnKind::Dense(DenseFfn {
13783 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
13784 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
13785 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
13786 act: Act::Silu,
13787 down_t: None,
13788 segs: Vec::new(),
13789 }),
13790 attn: AttnKind::Full {
13791 bias: None,
13792 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
13793 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
13794 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
13795 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
13796 q_norm: None,
13797 k_norm: None,
13798 output_gate: false,
13799 softplus_gate: None,
13800 },
13801 })
13802 .collect();
13803
13804 Pipeline::new(
13805 Tokenizer::byte_level(),
13806 PipelineWeights {
13807 embed_tokens: qt(vocab_size, hidden_size, 100),
13808 layers: layer_weights,
13809 lm_head: qt(vocab_size, hidden_size, 200),
13810 final_norm: vec![1.0; hidden_size],
13811 },
13812 hidden_size,
13813 intermediate_size,
13814 num_heads,
13815 num_kv_heads,
13816 head_dim,
13817 num_layers,
13818 num_layers, false, vocab_size,
13821 1e-6,
13822 10_000.0,
13823 NormStyle::Qwen,
13824 4096,
13825 SamplerConfig {
13826 seed: Some(42),
13827 ..Default::default()
13828 },
13829 )
13830}
13831
13832#[inline]
13837fn mask_bit(row: &[u8], j: usize) -> bool {
13838 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
13839}
13840
13841fn mask_gain() -> f32 {
13852 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
13853 *G.get_or_init(|| {
13854 std::env::var("CMF_FFN_MASK_GAIN")
13855 .ok()
13856 .and_then(|v| v.parse().ok())
13857 .unwrap_or(1.0)
13858 })
13859}
13860
13861fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
13862 let fill = meanfill().and_then(|(i, v)| {
13865 let li = crate::gpu::cur_layer();
13866 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
13867 });
13868 for r in 0..rows {
13869 let base = r * inter;
13870 for (bi, &byte) in row.iter().enumerate() {
13871 if byte == 0xFF {
13872 continue;
13873 }
13874 let j0 = bi * 8;
13875 for bit in 0..8 {
13876 let j = j0 + bit;
13877 if j < inter && byte & (1 << bit) == 0 {
13878 g[base + j] = fill.map_or(0.0, |f| f[j]);
13879 }
13880 }
13881 }
13882 }
13883 let gain = mask_gain();
13884 if gain != 1.0 {
13885 for v in g[..rows * inter].iter_mut() {
13886 *v *= gain;
13887 }
13888 }
13889}
13890
13891#[inline]
13893fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
13894 row.is_none_or(|r| mask_bit(r, i))
13895}
13896
13897fn all_bits_on(row: &[u8], n: usize) -> bool {
13900 (0..n).all(|i| mask_bit(row, i))
13901}
13902
13903fn tube_topk() -> usize {
13911 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13912 *K.get_or_init(|| {
13913 std::env::var("CMF_TUBE_TOPK")
13914 .ok()
13915 .and_then(|v| v.parse().ok())
13916 .unwrap_or(0)
13917 })
13918}
13919
13920fn tube_score_oracle() -> bool {
13921 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13922 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
13923}
13924
13925fn tube_ffn_routed(
13932 d: &DenseFfn,
13933 xs: &[f32],
13934 b: usize,
13935 pool: Option<&Pool>,
13936 mask_row: Option<&[u8]>,
13937 k: usize,
13938) -> Vec<f32> {
13939 let hidden = d.down_proj.rows();
13940 let core = d.gate_proj.rows();
13941 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
13942 let mut out = match (b, core_full, mask_row) {
13943 (1, true, _) => dense_ffn(d, xs, pool),
13944 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
13945 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
13946 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
13947 };
13948 let cand: Vec<usize> = (0..d.segs.len())
13949 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
13950 .collect();
13951 if cand.is_empty() {
13952 return out;
13953 }
13954 let oracle = tube_score_oracle();
13958 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
13959 let mut scores = vec![0f32; b * cand.len()];
13960 for (ci, &i) in cand.iter().enumerate() {
13961 let seg = &d.segs[i];
13962 let w = seg.width;
13963 let mut g = vec![0.0f32; b * w];
13964 if b == 1 {
13965 seg.gate.matvec(xs, &mut g, pool);
13966 } else {
13967 seg.gate.matmat(xs, b, &mut g, pool);
13968 }
13969 for v in g.iter_mut() {
13970 *v = Act::Silu.combine(*v, 1.0);
13971 }
13972 if !oracle {
13973 for t in 0..b {
13974 scores[t * cand.len() + ci] =
13975 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
13976 }
13977 }
13978 if oracle || b > 1 {
13979 let mut u = vec![0.0f32; b * w];
13980 if b == 1 {
13981 seg.up.matvec(xs, &mut u, pool);
13982 } else {
13983 seg.up.matmat(xs, b, &mut u, pool);
13984 }
13985 for (a, &v) in g.iter_mut().zip(u.iter()) {
13986 *a *= v;
13987 }
13988 if oracle {
13989 for t in 0..b {
13990 scores[t * cand.len() + ci] =
13991 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
13992 }
13993 }
13994 }
13995 acts.push(g);
13996 }
13997 let keep = k.min(cand.len());
13999 let mut scratch: Vec<f32> = Vec::new();
14000 for t in 0..b {
14001 let mut sc: Vec<(f32, usize)> = (0..cand.len())
14002 .map(|ci| (scores[t * cand.len() + ci], ci))
14003 .collect();
14004 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
14005 let mut alive = vec![false; cand.len()];
14006 for &(_, ci) in sc.iter().take(keep) {
14007 alive[ci] = true;
14008 }
14009 if b > 1 {
14010 for (ci, a) in acts.iter_mut().enumerate() {
14011 if !alive[ci] {
14012 let w = d.segs[cand[ci]].width;
14013 a[t * w..(t + 1) * w].fill(0.0);
14014 }
14015 }
14016 } else {
14017 for (ci, &i) in cand.iter().enumerate() {
14021 if !alive[ci] {
14022 continue;
14023 }
14024 let seg = &d.segs[i];
14025 let w = seg.width;
14026 let g = &mut acts[ci];
14027 if !tube_score_oracle() {
14028 scratch.clear();
14029 scratch.resize(w, 0.0);
14030 seg.up.matvec(xs, &mut scratch, pool);
14031 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
14032 *a *= v;
14033 }
14034 }
14035 let mut acc = vec![0.0f32; hidden];
14036 seg.down.matvec(g, &mut acc, pool);
14037 for (o, a) in out.iter_mut().zip(&acc) {
14038 *o += *a;
14039 }
14040 }
14041 }
14042 }
14043 if b > 1 {
14044 for (ci, &i) in cand.iter().enumerate() {
14045 let seg = &d.segs[i];
14046 let mut acc = vec![0.0f32; b * hidden];
14047 seg.down.matmat(&acts[ci], b, &mut acc, pool);
14048 for (o, a) in out.iter_mut().zip(&acc) {
14049 *o += *a;
14050 }
14051 }
14052 }
14053 out
14054}
14055
14056fn tube_ffn(
14062 d: &DenseFfn,
14063 xs: &[f32],
14064 b: usize,
14065 pool: Option<&Pool>,
14066 mask_row: Option<&[u8]>,
14067) -> Vec<f32> {
14068 if tube_topk() > 0 {
14069 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
14070 }
14071 let hidden = d.down_proj.rows();
14072 let core = d.gate_proj.rows();
14073 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
14074 let mut out = match (b, core_full, mask_row) {
14075 (1, true, _) => dense_ffn(d, xs, pool),
14076 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
14077 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
14078 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
14079 };
14080 TUBE_SCRATCH.with(|sc| {
14081 let mut sc = sc.borrow_mut();
14082 let [g, u, acc] = &mut *sc;
14083 for seg in &d.segs {
14084 if !tube_bit(mask_row, seg.start) {
14085 continue;
14086 }
14087 let w = seg.width;
14088 g.resize(b * w, 0.0);
14089 if b == 1
14090 && d.act == Act::Silu
14091 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
14092 {
14093 } else {
14095 u.resize(b * w, 0.0);
14096 if b == 1 {
14097 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
14098 } else {
14099 seg.gate.matmat(xs, b, g, pool);
14100 seg.up.matmat(xs, b, u, pool);
14101 }
14102 for i in 0..b * w {
14103 g[i] = d.act.combine(g[i], u[i]);
14104 }
14105 }
14106 acc.resize(b * hidden, 0.0);
14107 acc.fill(0.0);
14108 if b == 1 {
14109 seg.down.matvec(g, acc, pool);
14110 } else {
14111 seg.down.matmat(g, b, acc, pool);
14112 }
14113 for (o, a) in out.iter_mut().zip(acc.iter()) {
14114 *o += *a;
14115 }
14116 }
14117 out
14118 })
14119}
14120
14121thread_local! {
14122 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
14126 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
14127}
14128
14129fn dense_ffn_batch(
14130 d: &DenseFfn,
14131 xs: &[f32],
14132 b: usize,
14133 pool: Option<&Pool>,
14134 mask_row: Option<&[u8]>,
14135) -> Vec<f32> {
14136 let inter = d.gate_proj.rows();
14137 let hidden = d.down_proj.rows();
14138 if mask_row.is_none()
14146 && d.act == Act::Silu
14147 && b >= 32
14148 && crate::gpu::enabled_here()
14149 && !crate::gpu::mm_killed()
14150 && refit_dir().is_none()
14155 && !ffn_probe_active()
14160 {
14161 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
14162 d.gate_proj.mapped_q4t(),
14163 d.up_proj.mapped_q4t(),
14164 d.down_proj.mapped_q4t(),
14165 ) {
14166 let mut out = vec![0.0f32; b * hidden];
14167 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
14168 return out;
14169 }
14170 }
14171 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
14176 d.gate_proj.mapped_q4tp(),
14177 d.up_proj.mapped_q4tp(),
14178 d.down_proj.mapped_q4tp(),
14179 ) {
14180 let mut out = vec![0.0f32; b * hidden];
14181 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
14182 return out;
14183 }
14184 }
14185 }
14186 let mut g = vec![0.0f32; b * inter];
14187 d.gate_proj.matmat(xs, b, &mut g, pool);
14188 let mut u = vec![0.0f32; b * inter];
14189 d.up_proj.matmat(xs, b, &mut u, pool);
14190 if gate_topk() > 0 && d.act == Act::Silu {
14191 for t in 0..b {
14192 let row = &mut g[t * inter..(t + 1) * inter];
14193 for v in row.iter_mut() {
14194 *v = Act::Silu.combine(*v, 1.0);
14195 }
14196 keep_top_k(row, gate_topk());
14197 }
14198 for i in 0..b * inter {
14199 g[i] *= u[i];
14200 }
14201 } else {
14202 for i in 0..b * inter {
14203 g[i] = d.act.combine(g[i], u[i]);
14204 }
14205 }
14206 if let Some(row) = mask_row {
14207 zero_masked_cols(&mut g, b, inter, row);
14208 }
14209 if oracle_topk() > 0 {
14210 for t in 0..b {
14211 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
14212 }
14213 }
14214 let mut out = vec![0.0f32; b * hidden];
14215 d.down_proj.matmat(&g, b, &mut out, pool);
14216 if refit_dir().is_some() {
14217 let li = crate::gpu::cur_layer();
14218 if li >= 0 {
14219 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
14220 }
14221 }
14222 FFN_PROBE.with(|pr| {
14226 if let Some(acc) = pr.borrow_mut().as_mut() {
14227 let li = crate::gpu::cur_layer();
14228 if li < 0 {
14229 return;
14230 }
14231 let Some(row) = acc.get_mut(li as usize) else {
14232 return;
14233 };
14234 let sq = probe_sq();
14235 for t in 0..b {
14236 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
14237 *a += if sq {
14238 (v as f64) * (v as f64)
14239 } else {
14240 (v as f64).abs()
14241 };
14242 }
14243 }
14244 }
14245 });
14246 out
14247}
14248
14249fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
14254 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14255 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14256 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
14257 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
14258 if (!on && !dump) || b == 0 {
14259 return;
14260 }
14261 let hidden = xs.len() / b;
14262 if on {
14263 let mut acc = m.act_sq.borrow_mut();
14264 if acc.len() < hidden {
14265 acc.resize(hidden, 0.0);
14266 }
14267 for t in 0..b {
14268 let row = &xs[t * hidden..(t + 1) * hidden];
14269 for (a, &v) in acc.iter_mut().zip(row) {
14270 *a += (v as f64) * (v as f64);
14271 }
14272 }
14273 }
14274 if dump {
14275 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
14278 .ok()
14279 .and_then(|v| v.parse().ok())
14280 .unwrap_or(4096);
14281 let mut rows = m.act_rows.borrow_mut();
14282 if rows.len() < cap * hidden {
14283 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
14284 rows.extend_from_slice(&xs[..take * hidden]);
14285 }
14286 }
14287}
14288
14289#[derive(Clone, Copy)]
14292struct SendVecs(*mut Vec<f32>);
14293unsafe impl Send for SendVecs {}
14294unsafe impl Sync for SendVecs {}
14295impl SendVecs {
14296 #[inline]
14297 fn at(self, i: usize) -> *mut Vec<f32> {
14298 unsafe { self.0.add(i) }
14299 }
14300}
14301
14302fn moe_ffn_batch(
14303 m: &MoeFfn,
14304 xs: &[f32],
14305 b: usize,
14306 hidden: usize,
14307 pool: Option<&Pool>,
14308 allowed: Option<&[bool]>,
14309) -> Vec<f32> {
14310 accumulate_act(m, xs, b);
14311 let ne = m.experts.len();
14312 let mut logits = vec![0.0f32; b * ne];
14313 match &m.resonance {
14314 Some(r) => {
14315 let hdim = xs.len() / b.max(1);
14316 for bi in 0..b {
14317 r.scores(
14318 &xs[bi * hdim..(bi + 1) * hdim],
14319 &mut logits[bi * ne..(bi + 1) * ne],
14320 );
14321 }
14322 }
14323 None => m.router.matmat(xs, b, &mut logits, pool),
14324 }
14325
14326 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
14329 {
14330 let mut st = m.stats.borrow_mut();
14331 if st.len() < ne {
14332 st.resize(ne, 0);
14333 }
14334 for bi in 0..b {
14335 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
14336 for &e in &idx {
14337 st[e] += 1;
14338 assign[e].push((bi, p[e] / wsum));
14339 }
14340 }
14341 }
14342
14343 let mut out = vec![0.0f32; b * hidden];
14344 let cols = m.experts[0].gate_proj.cols();
14345 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
14346 let sb = list.len();
14347 let mut sub = vec![0.0f32; sb * cols];
14348 for (k, &(bi, _)) in list.iter().enumerate() {
14349 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
14350 }
14351 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
14352 for (k, &(bi, w)) in list.iter().enumerate() {
14353 for i in 0..hidden {
14354 out[bi * hidden + i] += w * eo[k * hidden + i];
14355 }
14356 }
14357 };
14358 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
14364 if pool.is_some() && active.len() >= 8 {
14365 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
14366 {
14367 let panel_ptr = SendVecs(panels.as_mut_ptr());
14368 let experts = &m.experts;
14371 let (active_r, assign_r) = (&active, &assign);
14372 let inherit_cpu = crate::gpu::inherit_cpu_scope();
14373 let run = |start: usize, end: usize| {
14374 let _cpu_scope = inherit_cpu();
14375 for ai in start..end {
14376 let e = active_r[ai];
14377 let list = &assign_r[e];
14378 let sb = list.len();
14379 let mut sub = vec![0.0f32; sb * cols];
14380 for (k, &(bi, _)) in list.iter().enumerate() {
14381 sub[k * cols..(k + 1) * cols]
14382 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
14383 }
14384 unsafe {
14386 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
14387 }
14388 }
14389 };
14390 match pool {
14391 Some(p) => p.run_rows(active.len(), &run),
14392 None => run(0, active.len()),
14393 }
14394 }
14395 for (ai, &e) in active.iter().enumerate() {
14396 for (k, &(bi, w)) in assign[e].iter().enumerate() {
14397 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
14398 for i in 0..hidden {
14399 out[bi * hidden + i] += w * eo[i];
14400 }
14401 }
14402 }
14403 } else {
14404 for &e in &active {
14405 run_expert(&m.experts[e], &assign[e], &mut out);
14406 }
14407 }
14408 if let Some((se, gate)) = &m.shared {
14409 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
14410 let mut gl = vec![0.0f32; b];
14411 gate.matmat(xs, b, &mut gl, pool);
14412 (0..b)
14413 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
14414 .collect()
14415 } else {
14416 (0..b).map(|bi| (bi, 1.0)).collect()
14417 };
14418 run_expert(se, &all, &mut out);
14419 }
14420 out
14421}
14422
14423fn moe_ffn_rows_exact(
14435 m: &MoeFfn,
14436 xs: &[f32],
14437 b: usize,
14438 hidden: usize,
14439 pool: Option<&Pool>,
14440) -> Vec<f32> {
14441 let mut out = vec![0.0f32; b * hidden];
14442 let per_row = |out: &mut [f32]| {
14443 for r in 0..b {
14444 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
14445 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
14446 }
14447 };
14448 let covered = !crate::gpu::enabled_here()
14449 && moe_batch_enabled()
14450 && m.shared.is_none()
14451 && m.resonance.is_none()
14452 && FFN_PROBE.with(|pr| pr.borrow().is_none())
14453 && m.experts.iter().all(|d| d.act == Act::Silu);
14454 if !covered {
14455 per_row(&mut out);
14456 return out;
14457 }
14458 let ne = m.experts.len();
14459 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
14461 for r in 0..b {
14462 let x = &xs[r * hidden..(r + 1) * hidden];
14463 accumulate_act(m, x, 1);
14464 let mut logits = vec![0.0f32; ne];
14465 m.router.matvec(x, &mut logits, pool);
14466 let (idx, p, wsum) = moe_route(&logits, m, None);
14467 {
14468 let mut st = m.stats.borrow_mut();
14469 if st.len() < ne {
14470 st.resize(ne, 0);
14471 }
14472 for &e in &idx {
14473 st[e] += 1;
14474 }
14475 }
14476 let w: Vec<f32> = idx
14477 .iter()
14478 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
14479 .collect();
14480 routes.push((idx, w));
14481 }
14482 if routes.iter().any(|(idx, _)| idx.is_empty()) {
14483 per_row(&mut out);
14484 return out;
14485 }
14486 let mut experts: Vec<usize> = Vec::new();
14488 let mut groups: Vec<Vec<usize>> = Vec::new();
14489 for (r, (idx, _)) in routes.iter().enumerate() {
14490 for &e in idx {
14491 match experts.iter().position(|&x| x == e) {
14492 Some(g) => groups[g].push(r),
14493 None => {
14494 experts.push(e);
14495 groups.push(vec![r]);
14496 }
14497 }
14498 }
14499 }
14500 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
14501 let inter = m.experts[experts[0]].gate_proj.rows();
14502 let pairs: Vec<(&QTensor, &QTensor)> = experts
14503 .iter()
14504 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
14505 .collect();
14506 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
14507 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
14508 per_row(&mut out);
14509 return out;
14510 }
14511 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
14512 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
14513 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
14514 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
14515 per_row(&mut out);
14516 return out;
14517 }
14518 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
14520 let mut p = 0usize;
14521 for (g, &e) in experts.iter().enumerate() {
14522 for &r in &groups[g] {
14523 slot.insert((r, e), p);
14524 p += 1;
14525 }
14526 }
14527 for (r, (idx, w)) in routes.iter().enumerate() {
14528 let terms: Vec<(&[f32], f32)> = idx
14529 .iter()
14530 .zip(w)
14531 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
14532 .collect();
14533 let row = &mut out[r * hidden..(r + 1) * hidden];
14534 for (i, dst) in row.iter_mut().enumerate() {
14535 let mut acc = 0f32;
14537 for (d, we) in &terms {
14538 acc += we * d[i];
14539 }
14540 *dst = acc;
14541 }
14542 }
14543 out
14544}
14545
14546thread_local! {
14547 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
14551 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
14552}
14553
14554fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
14556 if gate_topk() > 0
14559 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
14560 {
14561 return out;
14562 }
14563 let prism_body = d.gate_proj.has_prism_contract()
14579 || d.up_proj.has_prism_contract()
14580 || d.down_proj.has_prism_contract();
14581 if !prism_body
14582 && crate::gpu::enabled_here()
14583 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
14584 {
14585 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
14586 crate::gpu::ProbeArm::Gpu
14587 } else {
14588 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
14589 };
14590 match arm {
14591 crate::gpu::ProbeArm::Gpu => {
14592 let t0 = std::time::Instant::now();
14593 if let Some(out) = dense_ffn_gpu(d, x, pool) {
14594 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
14595 return out;
14596 }
14597 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
14601 }
14602 crate::gpu::ProbeArm::CpuTimed => {
14603 let t0 = std::time::Instant::now();
14604 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
14605 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
14606 return out;
14607 }
14608 crate::gpu::ProbeArm::Cpu => {
14609 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
14610 }
14611 }
14612 }
14613 dense_ffn_cpu(d, x, pool)
14614}
14615
14616fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
14618 let inter = d.gate_proj.rows();
14619 FFN_SCRATCH.with(|s| {
14620 let mut s = s.borrow_mut();
14621 let [g, u, ..] = &mut *s;
14622 g.resize(inter, 0.0);
14623 if gate_topk() > 0 {
14626 u.resize(inter, 0.0);
14630 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
14631 for i in 0..inter {
14632 g[i] = Act::Silu.combine(g[i], 1.0);
14633 }
14634 keep_top_k(g, gate_topk());
14635 for i in 0..inter {
14636 g[i] *= u[i];
14637 }
14638 } else if d.act == Act::Silu && {
14639 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
14640 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
14641 } {
14642 } else {
14644 u.resize(inter, 0.0);
14645 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
14647 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
14648 for i in 0..inter {
14649 g[i] = d.act.combine(g[i], u[i]);
14650 }
14651 }
14652 FFN_PROBE.with(|pr| {
14660 if let Some(acc) = pr.borrow_mut().as_mut() {
14661 let li = crate::gpu::cur_layer();
14662 if li >= 0 {
14663 if let Some(row) = acc.get_mut(li as usize) {
14664 match probe_topk() {
14665 0 if probe_sq() => {
14666 for (a, &v) in row.iter_mut().zip(g.iter()) {
14667 *a += (v as f64) * (v as f64);
14668 }
14669 }
14670 0 if probe_signed() => {
14671 for (a, &v) in row.iter_mut().zip(g.iter()) {
14672 *a += v as f64;
14673 }
14674 }
14675 0 => {
14676 for (a, &v) in row.iter_mut().zip(g.iter()) {
14677 *a += (v as f64).abs();
14678 }
14679 }
14680 k => {
14681 let n = g.len();
14682 let k = k.min(n);
14683 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
14684 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
14685 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
14686 });
14687 let thr = *kth;
14688 for (a, &v) in row.iter_mut().zip(g.iter()) {
14689 if v.abs() >= thr {
14690 *a += 1.0;
14691 }
14692 }
14693 }
14694 }
14695 }
14696 }
14697 }
14698 });
14699 if oracle_topk() > 0 {
14700 keep_top_k(g, oracle_topk());
14701 }
14702 {
14703 let li = crate::gpu::cur_layer();
14704 if li >= 0 {
14705 adump_row(li as usize, g);
14706 }
14707 }
14708 let mut out = attention::take_buf(d.down_proj.rows());
14709 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
14710 d.down_proj.matvec(g, &mut out, pool);
14711 out
14712 })
14713}
14714
14715pub struct RefitAcc {
14728 pub support: Vec<u32>,
14729 pub gss: Vec<f32>,
14730 pub ya: Vec<f32>,
14731 pub hidden: usize,
14732 pub tokens: u64,
14733 pub buf_g: Vec<f32>,
14739 pub buf_o: Vec<f32>,
14740 pub buf_t: usize,
14741}
14742
14743type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
14747
14748static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
14749 std::sync::OnceLock::new();
14750
14751fn ffn_probe_active() -> bool {
14754 FFN_PROBE.with(|p| p.borrow().is_some())
14755}
14756
14757fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
14758 REFIT
14759 .get_or_init(|| {
14760 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
14761 (
14762 d,
14763 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
14764 )
14765 })
14766 })
14767 .as_ref()
14768}
14769
14770fn refit_accumulate(
14772 li: usize,
14773 g: &[f32],
14774 b: usize,
14775 inter: usize,
14776 out: &[f32],
14777 hidden: usize,
14778 pool: Option<&Pool>,
14779) {
14780 let Some((dir, map)) = refit_dir() else {
14781 return;
14782 };
14783 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
14784 let (from, to) = *SPAN.get_or_init(|| {
14785 let g = |k: &str, d: usize| {
14786 std::env::var(k)
14787 .ok()
14788 .and_then(|v| v.parse().ok())
14789 .unwrap_or(d)
14790 };
14791 (
14792 g("CMF_FFN_REFIT_FROM", 0),
14793 g("CMF_FFN_REFIT_TO", usize::MAX),
14794 )
14795 });
14796 if li < from || li > to {
14797 return;
14798 }
14799 let mut guard = map.lock().unwrap();
14800 let (map, shared) = &mut *guard;
14801 let acc = match map.entry(li) {
14802 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
14803 std::collections::hash_map::Entry::Vacant(e) => {
14804 let path = format!("{dir}/support.{li}.u32");
14805 let Ok(bytes) = std::fs::read(&path) else {
14806 eprintln!("refit: no {path} — layer {li} skipped");
14807 return;
14808 };
14809 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
14810 let support: Vec<u32> = bytes[4..4 + n * 4]
14811 .chunks_exact(4)
14812 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
14813 .collect();
14814 eprintln!(
14815 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
14816 (n * n + hidden * n) as f64 * 4.0 / 1e6
14817 );
14818 e.insert(RefitAcc {
14819 gss: vec![0.0; n * n],
14820 ya: vec![0.0; hidden * n],
14821 buf_g: Vec::new(),
14822 buf_o: Vec::new(),
14823 buf_t: 0,
14824 support,
14825 hidden,
14826 tokens: 0,
14827 })
14828 }
14829 };
14830 let ns = acc.support.len();
14831 let cap = refit_batch();
14833 if acc.buf_g.is_empty() {
14834 acc.buf_g = vec![0.0; ns * cap];
14835 acc.buf_o = vec![0.0; hidden * cap];
14836 }
14837 let take = b.min(cap - acc.buf_t);
14838 for t in 0..take {
14839 let col = acc.buf_t + t;
14840 for (j, &n) in acc.support.iter().enumerate() {
14841 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
14842 }
14843 for h in 0..hidden {
14844 acc.buf_o[h * cap + col] = out[t * hidden + h];
14845 }
14846 }
14847 acc.buf_t += take;
14848 acc.tokens += take as u64;
14849 if acc.buf_t < cap {
14850 return;
14851 }
14852 let bt = acc.buf_t;
14853 acc.buf_t = 0;
14854 let RefitAcc {
14864 gss,
14865 ya,
14866 buf_g,
14867 buf_o,
14868 ..
14869 } = acc;
14870 let need = (ns * ns).max(hidden * ns);
14871 if shared.len() < need {
14872 shared.resize(need, 0.0);
14873 }
14874 let scratch = &mut shared[..];
14875 let _ = bt;
14876 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
14877 add_into(gss, &scratch[..ns * ns], pool);
14878 if crate::gpu::gemm_nt_f32_transient(
14879 buf_o,
14880 buf_g,
14881 &mut scratch[..hidden * ns],
14882 hidden,
14883 cap,
14884 ns,
14885 ) {
14886 add_into(ya, &scratch[..hidden * ns], pool);
14887 } else {
14888 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
14889 }
14890 } else {
14891 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
14892 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
14893 }
14894 }
14898
14899fn refit_batch() -> usize {
14901 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14902 *B.get_or_init(|| {
14903 std::env::var("CMF_FFN_REFIT_BATCH")
14904 .ok()
14905 .and_then(|v| v.parse().ok())
14906 .unwrap_or(4096)
14907 })
14908}
14909
14910fn accum_outer_t(
14913 c: &mut [f32],
14914 m: usize,
14915 n: usize,
14916 b: usize,
14917 left: &[f32],
14918 right: &[f32],
14919 pool: Option<&Pool>,
14920) {
14921 let ptr = SendMut(c.as_mut_ptr());
14922 let body = |i: usize| {
14923 let ptr = &ptr;
14924 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
14925 for t in 0..b {
14926 let a = left[i * b + t];
14927 if a == 0.0 {
14928 continue;
14929 }
14930 for (j, o) in row.iter_mut().enumerate() {
14931 *o += a * right[j * b + t];
14932 }
14933 }
14934 };
14935 match pool {
14936 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
14937 for i in s..e {
14938 body(i);
14939 }
14940 }),
14941 _ => {
14942 for i in 0..m {
14943 body(i);
14944 }
14945 }
14946 }
14947}
14948
14949fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
14952 let n = dst.len().min(src.len());
14953 match pool {
14954 Some(p) if n >= 1 << 16 => {
14955 let ptr = SendMut(dst.as_mut_ptr());
14956 let f = |s: usize, e: usize| {
14957 let ptr = &ptr;
14958 for blk in s..e {
14959 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
14960 for i in a..b {
14961 unsafe { *ptr.0.add(i) += src[i] };
14962 }
14963 }
14964 };
14965 p.run_rows(n.div_ceil(4096), &f);
14966 }
14967 _ => {
14968 for (d, v) in dst.iter_mut().zip(&src[..n]) {
14969 *d += *v;
14970 }
14971 }
14972 }
14973}
14974
14975fn accum_outer(
14980 c: &mut [f32],
14981 m: usize,
14982 n: usize,
14983 b: usize,
14984 left: &[f32],
14985 right: &[f32],
14986 pool: Option<&Pool>,
14987) {
14988 const TILE: usize = 32;
14989 let tiles = m.div_ceil(TILE);
14990 let cp = SendMut(c.as_mut_ptr());
14991 let body = |ti: usize| {
14992 let cp = &cp;
14993 let i0 = ti * TILE;
14994 let i1 = (i0 + TILE).min(m);
14995 for t in 0..b {
14996 let r = &right[t * n..t * n + n];
14997 for i in i0..i1 {
14998 let a = left[i * b + t];
14999 if a == 0.0 {
15000 continue;
15001 }
15002 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
15004 for (o, v) in row.iter_mut().zip(r) {
15005 *o += a * *v;
15006 }
15007 }
15008 }
15009 };
15010 match pool {
15011 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
15012 for ti in s..e {
15013 body(ti);
15014 }
15015 }),
15016 _ => {
15017 for ti in 0..tiles {
15018 body(ti);
15019 }
15020 }
15021 }
15022}
15023
15024pub fn refit_flush() -> usize {
15026 let Some((dir, map)) = refit_dir() else {
15027 return 0;
15028 };
15029 let guard = map.lock().unwrap();
15030 let mut n = 0;
15031 for (li, acc) in guard.0.iter() {
15032 let w = |name: &str, v: &[f32]| {
15035 let path = format!("{dir}/{name}.{li}.f32");
15036 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
15037 match std::fs::write(&path, &bytes) {
15038 Ok(()) => {}
15039 Err(e) => eprintln!(
15040 "refit: FAILED to write {path} ({} MB): {e}",
15041 bytes.len() / 1_000_000
15042 ),
15043 }
15044 };
15045 w("gss", &acc.gss);
15046 w("ya", &acc.ya);
15047 println!(
15048 "refit L{li}: {} support, {} tokens, hidden {}",
15049 acc.support.len(),
15050 acc.tokens,
15051 acc.hidden
15052 );
15053 n += 1;
15054 }
15055 n
15056}
15057
15058fn adump_row(li: usize, g: &[f32]) {
15063 use std::io::Write as _;
15064 static FILES: std::sync::OnceLock<
15065 Option<(
15066 String,
15067 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
15068 )>,
15069 > = std::sync::OnceLock::new();
15070 let Some((prefix, map)) = FILES
15071 .get_or_init(|| {
15072 std::env::var("CMF_FFN_ADUMP")
15073 .ok()
15074 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
15075 })
15076 .as_ref()
15077 else {
15078 return;
15079 };
15080 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
15083 let (from, to) = *SPAN.get_or_init(|| {
15084 let g = |k: &str, d: usize| {
15085 std::env::var(k)
15086 .ok()
15087 .and_then(|v| v.parse().ok())
15088 .unwrap_or(d)
15089 };
15090 (
15091 g("CMF_FFN_ADUMP_FROM", 0),
15092 g("CMF_FFN_ADUMP_TO", usize::MAX),
15093 )
15094 });
15095 if li < from || li > to {
15096 return;
15097 }
15098 let mut map = map.lock().unwrap();
15099 let f = map.entry(li).or_insert_with(|| {
15100 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
15101 });
15102 let mut bytes = Vec::with_capacity(g.len() * 2);
15103 for v in g {
15104 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
15105 }
15106 let _ = f.write_all(&bytes);
15107}
15108
15109fn oracle_topk() -> usize {
15115 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15116 *K.get_or_init(|| {
15117 std::env::var("CMF_FFN_ORACLE_TOPK")
15118 .ok()
15119 .and_then(|v| v.parse().ok())
15120 .unwrap_or(0)
15121 })
15122}
15123
15124fn gate_topk() -> usize {
15130 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15131 *K.get_or_init(|| {
15132 std::env::var("CMF_FFN_GATE_TOPK")
15133 .ok()
15134 .and_then(|v| v.parse().ok())
15135 .unwrap_or(0)
15136 })
15137}
15138
15139fn gate_block() -> usize {
15146 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15147 *B.get_or_init(|| {
15148 std::env::var("CMF_FFN_GATE_BLOCK")
15149 .ok()
15150 .and_then(|v| v.parse().ok())
15151 .unwrap_or(1)
15152 })
15153}
15154
15155fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
15157 let n = g.len();
15158 let nb = n.div_ceil(block);
15159 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
15160 if kb >= nb {
15161 return;
15162 }
15163 let mut score: Vec<f32> = (0..nb)
15164 .map(|b| {
15165 g[b * block..((b + 1) * block).min(n)]
15166 .iter()
15167 .map(|v| v * v)
15168 .sum::<f32>()
15169 })
15170 .collect();
15171 let mut ord = score.clone();
15172 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
15173 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
15174 });
15175 let thr = *kth;
15176 for b in 0..nb {
15177 if score[b] < thr {
15178 g[b * block..((b + 1) * block).min(n)].fill(0.0);
15179 }
15180 }
15181 score.clear();
15182}
15183
15184fn keep_top_k(g: &mut [f32], k: usize) {
15186 if gate_block() > 1 {
15187 return keep_top_blocks(g, k, gate_block());
15188 }
15189 let n = g.len();
15190 if k == 0 || k >= n {
15191 return;
15192 }
15193 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
15194 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
15195 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
15196 });
15197 let thr = *kth;
15198 for v in g.iter_mut() {
15199 if v.abs() < thr {
15200 *v = 0.0;
15201 }
15202 }
15203}
15204
15205fn probe_sq() -> bool {
15209 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15210 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
15211}
15212
15213fn probe_signed() -> bool {
15217 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15218 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
15219}
15220
15221fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
15229 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
15230 M.get_or_init(|| {
15231 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
15232 let b = std::fs::read(&p).ok()?;
15233 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
15234 let vals: Vec<f32> = b[8..]
15235 .chunks_exact(4)
15236 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
15237 .collect();
15238 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
15239 Some((inter, vals))
15240 })
15241 .as_ref()
15242}
15243
15244fn probe_topk() -> usize {
15247 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15248 *K.get_or_init(|| {
15249 std::env::var("CMF_FFN_PROBE_TOPK")
15250 .ok()
15251 .and_then(|v| v.parse().ok())
15252 .unwrap_or(0)
15253 })
15254}
15255
15256thread_local! {
15257 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
15260 const { std::cell::RefCell::new(None) };
15261}
15262
15263fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
15276 if d.gate_proj.has_prism_contract()
15281 || d.up_proj.has_prism_contract()
15282 || d.down_proj.has_prism_contract()
15283 {
15284 return None;
15285 }
15286 let dt = d.down_t.as_ref()?;
15287 let inter = d.gate_proj.rows();
15288 let hidden = dt.cols();
15289 if k == 0 || k >= inter || d.act != Act::Silu {
15290 return None;
15291 }
15292 DYN_SCRATCH.with(|sc| {
15293 let mut sc = sc.borrow_mut();
15294 let DynScratch {
15295 g,
15296 mag,
15297 live,
15298 parts,
15299 } = &mut *sc;
15300 g.resize(inter, 0.0);
15301 d.gate_proj.matvec(x, g, pool);
15302 for v in g.iter_mut() {
15303 *v = inference::silu(*v);
15304 }
15305 mag.clear();
15308 mag.extend(g.iter().map(|v| v.abs()));
15309 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
15310 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
15311 });
15312 let thr = *kth;
15313 live.clear();
15314 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
15315 let mut out = vec![0.0f32; hidden];
15316 match pool {
15317 Some(p) if live.len() >= 64 => {
15318 let nw = p.n_workers() + 1;
15319 parts.clear();
15320 parts.resize(nw * hidden, 0.0);
15321 let ptr = SendMut(parts.as_mut_ptr());
15322 let n = live.len();
15323 let live_ref: &[u32] = live;
15324 let g_ref: &[f32] = g;
15325 p.run(&|w, workers| {
15326 let chunk = n.div_ceil(workers);
15327 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
15328 if s >= e {
15329 return;
15330 }
15331 WORKER_SCRATCH.with(|ws| {
15332 let mut ws = ws.borrow_mut();
15333 let [scratch, acc] = &mut *ws;
15334 scratch.resize(hidden.max(x.len()), 0.0);
15335 acc.clear();
15336 acc.resize(hidden, 0.0);
15337 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
15338 if let Some(&nx) = live_ref[s..e].get(o + 1) {
15341 d.up_proj.prefetch_row(nx as usize);
15342 dt.prefetch_row(nx as usize);
15343 }
15344 let idx = nrm as usize;
15345 let up = d.up_proj.row_dot(idx, x, scratch);
15346 let a = g_ref[idx] * up;
15347 if a != 0.0 {
15348 dt.add_row_scaled(idx, a, acc, scratch);
15349 }
15350 }
15351 for (j, v) in acc.iter().enumerate() {
15352 unsafe { *ptr.at(w * hidden + j) = *v };
15353 }
15354 });
15355 });
15356 for w in 0..nw {
15357 for (j, o) in out.iter_mut().enumerate() {
15358 *o += parts[w * hidden + j];
15359 }
15360 }
15361 }
15362 _ => {
15363 WORKER_SCRATCH.with(|ws| {
15364 let mut ws = ws.borrow_mut();
15365 let [scratch, _acc] = &mut *ws;
15366 scratch.resize(hidden.max(x.len()), 0.0);
15367 for &nrm in live.iter() {
15368 let idx = nrm as usize;
15369 let up = d.up_proj.row_dot(idx, x, scratch);
15370 let a = g[idx] * up;
15371 if a != 0.0 {
15372 dt.add_row_scaled(idx, a, &mut out, scratch);
15373 }
15374 }
15375 });
15376 }
15377 }
15378 Some(out)
15379 })
15380}
15381
15382struct DynScratch {
15385 g: Vec<f32>,
15386 mag: Vec<f32>,
15387 live: Vec<u32>,
15388 parts: Vec<f32>,
15389}
15390
15391thread_local! {
15392 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
15393 std::cell::RefCell::new(DynScratch {
15394 g: Vec::new(),
15395 mag: Vec::new(),
15396 live: Vec::new(),
15397 parts: Vec::new(),
15398 })
15399 };
15400 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
15402 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
15403}
15404
15405fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
15410 let inter = d.gate_proj.rows();
15411 FFN_SCRATCH.with(|s| {
15412 let mut s = s.borrow_mut();
15413 let [g, u, ..] = &mut *s;
15414 g.resize(inter, 0.0);
15415 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
15416 } else {
15418 u.resize(inter, 0.0);
15419 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
15420 for i in 0..inter {
15421 g[i] = d.act.combine(g[i], u[i]);
15422 }
15423 }
15424 zero_masked_cols(g, 1, inter, mask_row);
15425 let mut out = attention::take_buf(d.down_proj.rows());
15426 d.down_proj.matvec(g, &mut out, pool);
15427 out
15428 })
15429}
15430
15431fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
15437 if d.gate_proj.has_prism_contract()
15438 || d.up_proj.has_prism_contract()
15439 || d.down_proj.has_prism_contract()
15440 {
15441 return None;
15442 }
15443 if d.act != Act::Silu {
15445 return None;
15446 }
15447 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
15450 return None;
15451 }
15452 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
15453 let mut model_ref = None;
15454 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
15455 let model = model_ref?;
15456 let hidden = jobs[0].down.1;
15457 let mut out = attention::take_buf(hidden);
15458 if crate::gpu::moe_block(&model, &jobs, &mut out) {
15459 Some(out)
15460 } else {
15461 let mut out = out;
15462 attention::recycle_buf(&mut out);
15463 None
15464 }
15465}
15466
15467#[allow(clippy::type_complexity)]
15472#[allow(clippy::type_complexity)]
15473pub(crate) fn moe_parts(
15474 t: &QTensor,
15475) -> Option<(
15476 &std::sync::Arc<cortiq_core::CmfModel>,
15477 usize,
15478 usize,
15479 usize,
15480 &[f32],
15481 &[f32],
15482 bool,
15483 bool,
15484 bool,
15485)> {
15486 match t {
15487 QTensor::Mapped {
15488 model,
15489 idx,
15490 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
15491 rows,
15492 cols,
15493 row_scale,
15494 col_field,
15495 ..
15496 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
15497 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
15498 )),
15499 QTensor::Mapped {
15501 model,
15502 idx,
15503 dtype: cortiq_core::TensorDtype::Q1,
15504 rows,
15505 cols,
15506 ..
15507 } => Some((
15508 model,
15509 *idx,
15510 *rows,
15511 *cols,
15512 &[][..],
15513 &[][..],
15514 true,
15515 false,
15516 false,
15517 )),
15518 QTensor::Mapped {
15520 model,
15521 idx,
15522 dtype: cortiq_core::TensorDtype::Q4Tiled,
15523 rows,
15524 cols,
15525 ..
15526 } => Some((
15527 model,
15528 *idx,
15529 *rows,
15530 *cols,
15531 &[][..],
15532 &[][..],
15533 false,
15534 true,
15535 false,
15536 )),
15537 QTensor::Mapped {
15539 model,
15540 idx,
15541 dtype: cortiq_core::TensorDtype::Q4TiledP,
15542 rows,
15543 cols,
15544 ..
15545 } => Some((
15546 model,
15547 *idx,
15548 *rows,
15549 *cols,
15550 &[][..],
15551 &[][..],
15552 false,
15553 true,
15554 false,
15555 )),
15556 QTensor::Mapped {
15560 model,
15561 idx,
15562 dtype: cortiq_core::TensorDtype::Q2TiledP,
15563 rows,
15564 cols,
15565 ..
15566 } => Some((
15567 model,
15568 *idx,
15569 *rows,
15570 *cols,
15571 &[][..],
15572 &[][..],
15573 false,
15574 true,
15575 true,
15576 )),
15577 _ => None,
15578 }
15579}
15580
15581#[cfg(target_os = "macos")]
15589fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
15590 if m.router_input_norm
15591 || m.route_tau.is_some()
15592 || m.mask.is_some()
15593 || m.per_expert_scale.is_some()
15594 || m.experts.is_empty()
15595 || m.top_k == 0
15596 || m.resonance.is_some()
15597 {
15598 return None;
15599 }
15600 let (sh, sg) = match &m.shared {
15603 Some((sh, sg)) => (sh, sg.as_ref()),
15604 None => return None,
15605 };
15606 let (rf, rr, rc) = m.router.f32_parts()?;
15607 if rr != m.experts.len() || rc != hidden {
15608 return None;
15609 }
15610 let shared_gated = sg.is_some();
15611 let sf = match sg {
15612 Some(sg) => {
15613 let (sf, sr, sc) = sg.f32_parts()?;
15614 if sr * sc != hidden {
15615 return None;
15616 }
15617 sf
15618 }
15619 None => &rf[..hidden],
15622 };
15623 if let Some(b) = &m.expert_bias {
15624 if b.len() != m.experts.len() {
15625 return None;
15626 }
15627 }
15628 let inter = m.experts[0].gate_proj.rows();
15629 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
15632 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
15633 if e.act != Act::Silu
15634 || e.gate_proj.rows() != inter
15635 || e.gate_proj.cols() != hidden
15636 || e.up_proj.rows() != inter
15637 || e.up_proj.cols() != hidden
15638 || e.down_proj.rows() != hidden
15639 || e.down_proj.cols() != inter
15640 {
15641 return None;
15642 }
15643 let pick = |t: &QTensor| -> Option<usize> {
15644 if gu_q2 {
15645 t.mapped_q2tp().map(|(_, i)| i)
15646 } else {
15647 t.mapped_q4tp().map(|(_, i)| i)
15648 }
15649 };
15650 Some((
15651 pick(&e.gate_proj)?,
15652 pick(&e.up_proj)?,
15653 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
15654 ))
15655 };
15656 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
15657 let shared = trio(sh)?;
15658 Some(crate::gpu::GpuMoe {
15659 router: rf,
15660 sgate: sf,
15661 experts,
15662 shared,
15663 n_exp: m.experts.len(),
15664 top_k: m.top_k,
15665 inter,
15666 norm_topk: m.norm_topk_prob,
15667 route_scale: m.routed_scaling,
15668 gu_q2,
15669 sigmoid: m.router_sigmoid,
15670 bias: m.expert_bias.as_deref(),
15671 shared_gated,
15672 })
15673}
15674
15675pub(crate) fn moe_push_job_parts<'a>(
15679 gate: &'a QTensor,
15680 up: &'a QTensor,
15681 down: &'a QTensor,
15682 x: &[f32],
15683 w: f32,
15684 swiglu_limit: f32,
15685 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
15686 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
15687) -> Option<()> {
15688 use crate::qtensor::prescale;
15689 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
15690 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
15691 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
15692 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
15693 return None; }
15695 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
15698 return None;
15699 }
15700 if !gq2 && dq2 {
15701 return None;
15702 }
15703 model_ref.get_or_insert_with(|| gm.clone());
15704 let dt = |cf: &[f32]| {
15705 if cf.is_empty() {
15706 cortiq_core::TensorDtype::Q8Row
15707 } else {
15708 cortiq_core::TensorDtype::Q8_2f
15709 }
15710 };
15711 jobs.push(crate::gpu::MoeJob {
15712 gate: (gi, gr, gc, grs),
15713 up: (ui, ur, uc, urs),
15714 down: (di, dr, dc, drs),
15715 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
15716 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
15717 down_col: dcf,
15718 w,
15719 q1: gq1,
15720 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
15721 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
15722 gu_q2: gq2,
15723 swiglu_limit,
15724 });
15725 Some(())
15726}
15727
15728fn moe_push_job<'a>(
15730 d: &'a DenseFfn,
15731 x: &[f32],
15732 w: f32,
15733 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
15734 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
15735) -> Option<()> {
15736 use crate::qtensor::prescale;
15737 if d.act != Act::Silu {
15738 return None; }
15740 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
15741 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
15742 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
15743 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
15744 return None; }
15746 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
15747 return None;
15748 }
15749 if !gq2 && dq2 {
15750 return None;
15751 }
15752 model_ref.get_or_insert_with(|| gm.clone());
15753 let gdt = if gcf.is_empty() {
15754 cortiq_core::TensorDtype::Q8Row
15755 } else {
15756 cortiq_core::TensorDtype::Q8_2f
15757 };
15758 let udt = if ucf.is_empty() {
15759 cortiq_core::TensorDtype::Q8Row
15760 } else {
15761 cortiq_core::TensorDtype::Q8_2f
15762 };
15763 jobs.push(crate::gpu::MoeJob {
15764 gate: (gi, gr, gc, grs),
15765 up: (ui, ur, uc, urs),
15766 down: (di, dr, dc, drs),
15767 xs_gate: prescale(x, gcf, gdt).into_owned(),
15768 xs_up: prescale(x, ucf, udt).into_owned(),
15769 down_col: dcf,
15770 w,
15771 q1: gq1,
15772 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
15773 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
15774 gu_q2: gq2,
15775 swiglu_limit: 0.0,
15776 });
15777 Some(())
15778}
15779
15780fn sparse_ffn_quant(
15787 d: &DenseFfn,
15788 x: &[f32],
15789 active: &[u16],
15790 hidden: usize,
15791 pool: Option<&Pool>,
15792) -> Vec<f32> {
15793 let n = active.len();
15794 let inter = d.gate_proj.rows();
15795 let mut act = vec![0.0f32; n];
15796 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
15799 let compute = |ai: usize| -> f32 {
15800 let idx = active[ai] as usize;
15801 if idx >= inter {
15802 return 0.0; }
15804 let mut s = if need_scratch {
15805 vec![0.0f32; hidden]
15806 } else {
15807 Vec::new()
15808 };
15809 let gate = d.gate_proj.row_dot(idx, x, &mut s);
15810 let up = d.up_proj.row_dot(idx, x, &mut s);
15811 d.act.combine(gate, up)
15812 };
15813 match pool {
15814 Some(p) if n >= 256 => {
15815 let ptr = SendMut(act.as_mut_ptr());
15816 p.run(&|widx, nw| {
15817 let chunk = n.div_ceil(nw);
15818 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
15819 for ai in s..e {
15820 unsafe { *ptr.at(ai) = compute(ai) };
15821 }
15822 });
15823 }
15824 _ => {
15825 for (ai, a) in act.iter_mut().enumerate() {
15826 *a = compute(ai);
15827 }
15828 }
15829 }
15830 let mut out = vec![0.0f32; hidden];
15832 for (ai, &idx) in active.iter().enumerate() {
15833 let w = act[ai];
15834 if w.abs() >= 1e-12 && (idx as usize) < inter {
15835 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
15836 }
15837 }
15838 out
15839}
15840
15841#[doc(hidden)]
15843pub fn sparse_ffn_quant_for_test(
15844 d: &DenseFfn,
15845 x: &[f32],
15846 active: &[u16],
15847 hidden: usize,
15848) -> Vec<f32> {
15849 sparse_ffn_quant(d, x, active, hidden, None)
15850}
15851
15852fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
15856 let deq = |t: &QTensor| -> Vec<f32> {
15857 let (rows, cols) = (t.rows(), t.cols());
15858 let mut out = vec![0.0f32; rows * cols];
15859 for r in 0..rows {
15860 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
15861 }
15862 out
15863 };
15864 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
15865}
15866
15867struct SendMut(*mut f32);
15869unsafe impl Send for SendMut {}
15870unsafe impl Sync for SendMut {}
15871impl SendMut {
15872 #[inline]
15873 #[allow(clippy::mut_from_ref)]
15876 unsafe fn at(&self, i: usize) -> &mut f32 {
15877 unsafe { &mut *self.0.add(i) }
15878 }
15879}
15880
15881pub(crate) fn moe_route(
15891 logits: &[f32],
15892 m: &MoeFfn,
15893 allowed: Option<&[bool]>,
15894) -> (Vec<usize>, Vec<f32>, f32) {
15895 let ne = logits.len();
15896 let p: Vec<f32> = if m.router_sigmoid {
15897 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
15898 } else {
15899 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
15900 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
15901 let s: f32 = e.iter().sum();
15902 for v in &mut e {
15903 *v /= s;
15904 }
15905 e
15906 };
15907 let admit = |e: usize| {
15913 m.mask.as_ref().is_none_or(|mk| mk[e])
15914 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
15915 };
15916 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
15917 match &m.expert_bias {
15919 Some(b) => idx.sort_unstable_by(|&x, &y| {
15920 (p[y] + b[y])
15921 .partial_cmp(&(p[x] + b[x]))
15922 .unwrap()
15923 .then(x.cmp(&y))
15924 }),
15925 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
15926 }
15927 idx.truncate(m.top_k);
15928 if let Some(tau) = m.route_tau {
15932 let total: f32 = idx.iter().map(|&e| p[e]).sum();
15933 if total > 0.0 {
15934 let mut acc = 0.0f32;
15935 let mut keep = idx.len();
15936 for (i, &e) in idx.iter().enumerate() {
15937 acc += p[e];
15938 if acc >= tau * total {
15939 keep = i + 1;
15940 break;
15941 }
15942 }
15943 idx.truncate(keep);
15944 }
15945 }
15946 let wsum: f32 = if m.norm_topk_prob {
15947 let s: f32 = idx.iter().map(|&e| p[e]).sum();
15948 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
15951 } else {
15952 1.0 / m.routed_scaling
15953 };
15954 (idx, p, wsum)
15955}
15956
15957fn moe_trace(idx: &[usize]) {
15959 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
15960}
15961
15962pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
15965 use std::io::Write;
15966 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
15967 std::sync::OnceLock::new();
15968 let Some(f) = F.get_or_init(|| {
15969 let p = std::env::var("CMF_MOE_TRACE").ok()?;
15970 Some(std::sync::Mutex::new(
15971 std::fs::OpenOptions::new()
15972 .create(true)
15973 .append(true)
15974 .open(p)
15975 .ok()?,
15976 ))
15977 }) else {
15978 return;
15979 };
15980 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
15981 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
15982}
15983
15984pub(crate) fn moe_ffn(
15987 m: &MoeFfn,
15988 x: &[f32],
15989 pool: Option<&Pool>,
15990 allowed: Option<&[bool]>,
15991) -> Vec<f32> {
15992 let r = moe_ffn_route(m, x, pool, allowed);
15993 moe_ffn_experts(m, x, &r, pool)
15994}
15995
15996pub(crate) struct MoeRoute {
16000 pub idx: Vec<usize>,
16001 pub p: Vec<f32>,
16002 pub wsum: f32,
16003 pub logits: Vec<f32>,
16004}
16005
16006pub(crate) fn moe_ffn_route(
16012 m: &MoeFfn,
16013 x: &[f32],
16014 pool: Option<&Pool>,
16015 allowed: Option<&[bool]>,
16016) -> MoeRoute {
16017 accumulate_act(m, x, 1);
16018 let ne = m.experts.len();
16019 let mut logits = vec![0.0f32; ne];
16020 match &m.resonance {
16021 Some(r) => r.scores(x, &mut logits),
16022 None => m.router.matvec(x, &mut logits, pool),
16023 }
16024 let (idx, p, wsum) = moe_route(&logits, m, allowed);
16025 {
16026 let mut st = m.stats.borrow_mut();
16027 if st.len() < ne {
16028 st.resize(ne, 0);
16029 }
16030 for &e in &idx {
16031 st[e] += 1;
16032 }
16033 }
16034 moe_trace(&idx);
16040 MoeRoute {
16041 idx,
16042 p,
16043 wsum,
16044 logits,
16045 }
16046}
16047
16048pub(crate) fn moe_ffn_experts(
16051 m: &MoeFfn,
16052 x: &[f32],
16053 r: &MoeRoute,
16054 pool: Option<&Pool>,
16055) -> Vec<f32> {
16056 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
16057 if crate::gpu::enabled_here() {
16062 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
16063 crate::gpu::ProbeArm::Gpu => {
16064 let t0 = std::time::Instant::now();
16065 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
16066 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
16067 return out;
16068 }
16069 }
16070 crate::gpu::ProbeArm::CpuTimed => {
16071 let t0 = std::time::Instant::now();
16072 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
16073 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
16074 return out;
16075 }
16076 crate::gpu::ProbeArm::Cpu => {
16077 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
16078 }
16079 }
16080 }
16081 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
16082}
16083
16084fn moe_ffn_banked(
16088 slot: &mut crate::mimo_moe::Slot,
16089 li: usize,
16090 m: &MoeFfn,
16091 x: &[f32],
16092 pool: Option<&Pool>,
16093) -> Vec<f32> {
16094 let t0 = std::time::Instant::now();
16095 let r = moe_ffn_route(m, x, pool, None);
16096 slot.note_route(t0.elapsed().as_nanos() as u64);
16097 match slot.forward(li, m, x, &r, pool) {
16098 Some(out) => out,
16099 None => crate::qtensor::float_activations_scope(|| {
16100 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
16101 }),
16102 }
16103}
16104
16105fn moe_ffn_banked_rows(
16107 slot: &mut crate::mimo_moe::Slot,
16108 li: usize,
16109 m: &MoeFfn,
16110 xs: &[f32],
16111 b: usize,
16112 hidden: usize,
16113 pool: Option<&Pool>,
16114) -> Vec<f32> {
16115 let t0 = std::time::Instant::now();
16116 let routes: Vec<_> = xs
16117 .chunks_exact(hidden)
16118 .map(|x| moe_ffn_route(m, x, pool, None))
16119 .collect();
16120 slot.note_route(t0.elapsed().as_nanos() as u64);
16121 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
16122 return out;
16123 }
16124 let mut out = Vec::with_capacity(b * hidden);
16125 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
16126 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
16127 crate::qtensor::float_activations_scope(|| {
16129 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
16130 })
16131 });
16132 out.extend(row);
16133 }
16134 out
16135}
16136
16137fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
16142 use std::sync::atomic::{AtomicBool, Ordering};
16143 if built {
16144 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
16145 if total_layers > 0 && layers_run < total_layers {
16146 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
16147 } else {
16148 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
16149 }
16150 } else {
16151 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
16152 }
16153 static SAID: AtomicBool = AtomicBool::new(false);
16154 if !SAID.swap(true, Ordering::Relaxed) {
16155 if built {
16156 tracing::info!("wgpu whole-token graph: ACTIVE");
16157 } else {
16158 tracing::warn!("wgpu whole-token graph refused — per-op path");
16159 }
16160 }
16161}
16162
16163pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
16167pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
16168pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
16172pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
16174
16175pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
16179 std::sync::atomic::AtomicU64::new(0);
16180pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
16181 std::sync::atomic::AtomicU64::new(0);
16182pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
16183 std::sync::atomic::AtomicU64::new(0);
16184pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
16185 std::sync::atomic::AtomicU64::new(0);
16186pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
16187 std::sync::atomic::AtomicU64::new(0);
16188pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
16192 std::sync::atomic::AtomicU64::new(0);
16193pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
16194 std::sync::atomic::AtomicU64::new(0);
16195pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
16196 std::sync::atomic::AtomicU64::new(0);
16197pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
16198 std::sync::atomic::AtomicU64::new(0);
16199
16200fn moe_batch_enabled() -> bool {
16203 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16204 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
16205}
16206
16207fn moe_ffn_cpu_batched(
16213 m: &MoeFfn,
16214 x: &[f32],
16215 idx: &[usize],
16216 p: &[f32],
16217 wsum: f32,
16218 pool: Option<&Pool>,
16219) -> Option<Vec<f32>> {
16220 if idx.is_empty() || !moe_batch_enabled() {
16221 return None;
16222 }
16223 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
16227 return None;
16228 }
16229 let n = idx.len() + usize::from(m.shared.is_some());
16230 let mut pairs = Vec::with_capacity(n);
16231 let mut downs = Vec::with_capacity(n);
16232 let mut ws = Vec::with_capacity(n);
16233 for &e in idx {
16234 let d = &m.experts[e];
16235 if d.act != Act::Silu {
16236 return None;
16237 }
16238 pairs.push((&d.gate_proj, &d.up_proj));
16239 downs.push(&d.down_proj);
16240 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
16241 }
16242 if let Some((se, gate)) = &m.shared {
16245 if se.act != Act::Silu {
16246 return None;
16247 }
16248 let g = gate.as_ref().map_or(1.0, |gate| {
16249 let mut gl = [0.0f32; 1];
16250 gate.matvec(x, &mut gl, pool);
16251 1.0 / (1.0 + (-gl[0]).exp())
16252 });
16253 pairs.push((&se.gate_proj, &se.up_proj));
16254 downs.push(&se.down_proj);
16255 ws.push(g);
16256 }
16257 let inter = pairs[0].0.rows();
16258 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
16259 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
16260 return None;
16261 }
16262 let mut out = attention::take_buf(x.len());
16263 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
16264 attention::recycle_buf(&mut out);
16265 return None;
16266 }
16267 Some(out)
16268}
16269
16270pub(crate) fn moe_cold_experts_cpu(
16276 experts: &[(&DenseFfn, f32)],
16277 x: &[f32],
16278 pool: Option<&Pool>,
16279) -> Vec<f32> {
16280 let mut out = attention::take_buf(x.len());
16281 if experts.is_empty() {
16282 return out;
16283 }
16284 let pairs: Vec<_> = experts
16285 .iter()
16286 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
16287 .collect();
16288 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
16289 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
16290 let inter = experts[0].0.gate_proj.rows();
16291 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
16292 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
16293 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
16294 {
16295 return out;
16296 }
16297 out.fill(0.0);
16298 for &(expert, weight) in experts {
16299 let mut one = dense_ffn(expert, x, pool);
16300 for (o, v) in out.iter_mut().zip(&one) {
16301 *o += weight * v;
16302 }
16303 attention::recycle_buf(&mut one);
16304 }
16305 out
16306}
16307
16308pub(crate) fn moe_cold_experts_rows_cpu(
16312 jobs: &[Vec<(&DenseFfn, f32)>],
16313 xs: &[f32],
16314 hidden: usize,
16315 pool: Option<&Pool>,
16316) -> Vec<f32> {
16317 let mut out = vec![0.0; xs.len()];
16318 let mut experts: Vec<&DenseFfn> = Vec::new();
16319 let mut groups: Vec<Vec<usize>> = Vec::new();
16320 let mut terms = vec![Vec::new(); jobs.len()];
16321 for (r, row) in jobs.iter().enumerate() {
16322 for &(e, w) in row {
16323 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
16324 Some(g) => g,
16325 None => {
16326 experts.push(e);
16327 groups.push(Vec::new());
16328 groups.len() - 1
16329 }
16330 };
16331 terms[r].push((g, groups[g].len(), w));
16332 groups[g].push(r);
16333 }
16334 }
16335 if experts.is_empty() {
16336 return out;
16337 }
16338 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
16339 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
16340 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
16341 let count: usize = lens.iter().sum();
16342 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
16343 let mut ds = vec![vec![0.0; hidden]; count];
16344 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
16345 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
16346 {
16347 let mut offset = 0;
16348 let offsets: Vec<_> = lens
16349 .iter()
16350 .map(|&n| {
16351 let start = offset;
16352 offset += n;
16353 start
16354 })
16355 .collect();
16356 for (r, terms) in terms.iter().enumerate() {
16357 for &(g, slot, w) in terms {
16358 for (o, &v) in out[r * hidden..(r + 1) * hidden]
16359 .iter_mut()
16360 .zip(&ds[offsets[g] + slot])
16361 {
16362 *o += w * v;
16363 }
16364 }
16365 }
16366 } else {
16367 for (r, jobs) in jobs.iter().enumerate() {
16368 if !jobs.is_empty() {
16369 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
16370 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
16371 attention::recycle_buf(&mut row);
16372 }
16373 }
16374 }
16375 out
16376}
16377
16378fn moe_ffn_cpu(
16380 m: &MoeFfn,
16381 x: &[f32],
16382 idx: &[usize],
16383 p: &[f32],
16384 wsum: f32,
16385 pool: Option<&Pool>,
16386) -> Vec<f32> {
16387 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
16388 return out;
16389 }
16390 let mut out = attention::take_buf(x.len());
16391 for &e in idx {
16392 let mut eo = dense_ffn(&m.experts[e], x, pool);
16393 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
16394 for i in 0..out.len() {
16395 out[i] += w * eo[i];
16396 }
16397 attention::recycle_buf(&mut eo);
16398 }
16399 if let Some((se, gate)) = &m.shared {
16400 let mut so = dense_ffn(se, x, pool);
16401 let g = gate.as_ref().map_or(1.0, |gate| {
16402 let mut gl = [0.0f32; 1];
16403 gate.matvec(x, &mut gl, pool);
16404 1.0 / (1.0 + (-gl[0]).exp())
16405 });
16406 for i in 0..out.len() {
16407 out[i] += g * so[i];
16408 }
16409 attention::recycle_buf(&mut so);
16410 }
16411 out
16412}
16413
16414#[allow(clippy::too_many_arguments)]
16422fn mla_attention(
16423 w: &MlaWeights,
16424 normed: &[f32],
16425 cache: &mut crate::kv_cache::LayerKvCache,
16426 position: usize,
16427 inv_freq: &[f32],
16428 rope_scale: f32,
16429 eps: f64,
16430 pool: Option<&Pool>,
16431) -> Vec<f32> {
16432 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
16433 let hd = dr + dn;
16434 let mut q = vec![0.0f32; nh * hd];
16435 match (&w.q_a, &w.q_a_norm) {
16436 (Some(qa), Some(qn)) => {
16437 let mut t = vec![0.0f32; qa.rows()];
16438 qa.matvec(normed, &mut t, pool);
16439 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
16440 w.q_proj.matvec(&tn, &mut q, pool);
16441 }
16442 _ => w.q_proj.matvec(normed, &mut q, pool),
16443 }
16444 let mut ca = vec![0.0f32; lora + dr];
16445 w.kv_a.matvec(normed, &mut ca, pool);
16446 let (c_lat, k_rope) = ca.split_at_mut(lora);
16447 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
16448 let mut kvb = vec![0.0f32; nh * (dn + dv)];
16449 w.kv_b.matvec(&latn, &mut kvb, pool);
16450 if !w.nope {
16451 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
16452 }
16453 for h in 0..nh {
16454 if !w.nope {
16455 attention::rope_rotate_scaled(
16456 &mut q[h * hd..h * hd + dr],
16457 position,
16458 inv_freq,
16459 rope_scale,
16460 );
16461 }
16462 }
16463 let mut k = vec![0.0f32; nh * hd];
16464 let mut v = vec![0.0f32; nh * hd];
16465 for h in 0..nh {
16466 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
16467 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
16468 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
16469 }
16470 cache.append(&k, &v, &vec![true; nh]);
16471 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
16472 attention::recycle_buf(&mut imp);
16473 let mut ov = vec![0.0f32; nh * dv];
16474 for h in 0..nh {
16475 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
16476 }
16477 let mut out = vec![0.0f32; w.o_proj.rows()];
16478 w.o_proj.matvec(&ov, &mut out, pool);
16479 out
16480}
16481
16482fn dense_moe_ffn(
16489 dm: &DenseMoeFfn,
16490 x_normed: &[f32],
16491 h_raw: &[f32],
16492 eps: f64,
16493 norm_style: NormStyle,
16494 pool: Option<&Pool>,
16495) -> Vec<f32> {
16496 let mut d = dense_ffn(&dm.dense, x_normed, pool);
16497 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
16498 let m = &dm.moe;
16499 let ne = m.experts.len();
16500 let mut logits = vec![0.0f32; ne];
16501 if m.router_input_norm {
16502 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
16503 let inv = 1.0 / (ss + eps as f32).sqrt();
16504 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
16505 m.router.matvec(&xr, &mut logits, pool);
16506 } else {
16507 m.router.matvec(h_raw, &mut logits, pool);
16508 }
16509 let (idx, p, wsum) = moe_route(&logits, m, None);
16510 {
16511 let mut st = m.stats.borrow_mut();
16512 if st.len() < ne {
16513 st.resize(ne, 0);
16514 }
16515 for &e in &idx {
16516 st[e] += 1;
16517 }
16518 }
16519 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
16520 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
16521 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
16522 for (di, mi) in d.iter_mut().zip(&mo) {
16523 *di += mi;
16524 }
16525 d
16526}
16527
16528fn moe_gpu_refused(why: &'static str) {
16535 use std::sync::atomic::{AtomicBool, Ordering};
16536 static SAID: AtomicBool = AtomicBool::new(false);
16537 if !SAID.swap(true, Ordering::Relaxed) {
16538 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
16539 }
16540}
16541
16542fn moe_ffn_gpu(
16543 m: &MoeFfn,
16544 x: &[f32],
16545 idx: &[usize],
16546 p: &[f32],
16547 wsum: f32,
16548 pool: Option<&Pool>,
16549) -> Option<Vec<f32>> {
16550 use crate::gpu::MoeJob;
16551
16552 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
16553 let mut model_ref = None;
16554 for &e in idx {
16555 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
16556 moe_gpu_refused("push_job(expert)");
16557 return None;
16558 }
16559 }
16560 if let Some((se, gate)) = &m.shared {
16561 let g = gate.as_ref().map_or(1.0, |gate| {
16562 let mut gl = [0.0f32; 1];
16563 gate.matvec(x, &mut gl, pool);
16564 1.0 / (1.0 + (-gl[0]).exp())
16565 });
16566 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
16567 moe_gpu_refused("push_job(shared)");
16568 return None;
16569 }
16570 }
16571 let Some(model) = model_ref else {
16572 moe_gpu_refused("no model_ref");
16573 return None;
16574 };
16575 let hidden = jobs[0].down.1;
16576 let mut out = vec![0.0f32; hidden];
16577 if crate::gpu::moe_block(&model, &jobs, &mut out) {
16578 Some(out)
16579 } else {
16580 moe_gpu_refused("gpu::moe_block");
16581 None
16582 }
16583}
16584
16585fn ffn_forward(
16587 ffn: &FfnKind,
16588 x: &[f32],
16589 pool: Option<&Pool>,
16590 experts_allowed: Option<&[bool]>,
16591) -> Vec<f32> {
16592 match ffn {
16593 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
16594 FfnKind::Dense(d) => dense_ffn(d, x, pool),
16595 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
16596 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
16600 }
16601}
16602
16603fn ffn_forward_pair(
16607 ffn: &FfnKind,
16608 x1: &[f32],
16609 x2: &[f32],
16610 pool: Option<&Pool>,
16611 experts_allowed: Option<&[bool]>,
16612) -> (Vec<f32>, Vec<f32>) {
16613 let d = match ffn {
16614 FfnKind::Dense(d) if !d.segs.is_empty() => {
16617 return (
16618 tube_ffn(d, x1, 1, pool, None),
16619 tube_ffn(d, x2, 1, pool, None),
16620 );
16621 }
16622 FfnKind::Dense(d) => d,
16623 FfnKind::Moe(m) => {
16624 return (
16625 moe_ffn(m, x1, pool, experts_allowed),
16626 moe_ffn(m, x2, pool, experts_allowed),
16627 );
16628 }
16629 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
16630 };
16631 let inter = d.gate_proj.rows();
16632 FFN_SCRATCH.with(|s| {
16633 let mut s = s.borrow_mut();
16634 let [g1, g2, u1, u2] = &mut *s;
16635 g1.resize(inter, 0.0);
16636 g2.resize(inter, 0.0);
16637 u1.resize(inter, 0.0);
16638 u2.resize(inter, 0.0);
16639 QTensor::matvec2_many(
16642 [&d.gate_proj, &d.up_proj],
16643 x1,
16644 x2,
16645 [g1.as_mut_slice(), u1.as_mut_slice()],
16646 [g2.as_mut_slice(), u2.as_mut_slice()],
16647 pool,
16648 );
16649 for i in 0..inter {
16650 g1[i] = d.act.combine(g1[i], u1[i]);
16651 g2[i] = d.act.combine(g2[i], u2[i]);
16652 }
16653 let mut o1 = attention::take_buf(d.down_proj.rows());
16654 let mut o2 = attention::take_buf(d.down_proj.rows());
16655 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
16656 (o1, o2)
16657 })
16658}
16659
16660#[cfg(test)]
16661mod tests {
16662
16663 #[test]
16668 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
16669 use super::{
16670 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
16671 };
16672 let dense_card = ChunkStackFacts {
16673 plain_dense: true,
16674 discrete: true,
16675 gpu_on: true,
16676 ..Default::default()
16677 };
16678 assert!(dense_card.dense_on_discrete());
16679 assert_eq!(
16681 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
16682 DISCRETE_DENSE_PREFILL_CHUNK
16683 );
16684 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
16685 for (label, facts) in [
16686 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
16687 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
16688 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
16689 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
16690 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
16691 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
16692 ] {
16693 assert!(!facts.dense_on_discrete(), "{label}");
16694 assert_eq!(
16695 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
16696 48,
16697 "{label} keeps the historical x86 chunk"
16698 );
16699 }
16700 for dense in [false, true] {
16702 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
16703 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
16704 }
16705 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
16707 for dense in [false, true] {
16708 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
16709 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
16710 }
16711 }
16712 }
16713
16714 #[test]
16715 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
16716 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
16717 let full = |host_rows, device_rows| ReuseLayer {
16718 full: true,
16719 host_rows,
16720 device_rows,
16721 device_state: false,
16722 };
16723 assert_eq!(
16726 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
16727 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
16728 );
16729 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
16731 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
16733 assert_eq!(
16735 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
16736 ReusePlan::Pull(vec![(0, 300, 339)])
16737 );
16738 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
16740 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
16741 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
16742 let conv = |device_state| ReuseLayer {
16745 full: false,
16746 host_rows: 0,
16747 device_rows: None,
16748 device_state,
16749 };
16750 assert_eq!(
16751 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
16752 ReusePlan::Fresh
16753 );
16754 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
16755 }
16756
16757 #[test]
16758 fn nll_graph_policy_scopes_only_the_fused_head() {
16759 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
16760 ("vulkan graph", true, true, false, true, false),
16762 ("native Metal graph", true, true, true, true, true),
16764 ("masked", false, true, false, false, false),
16766 ("graph disabled", true, false, true, false, false),
16767 ] {
16768 let (graph_quality, graph_head_required) =
16769 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
16770 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
16771 assert_eq!(graph_head_required, want_head, "{label}: fused head");
16772 }
16773 }
16774
16775 #[test]
16776 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
16777 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
16778 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
16779 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
16780 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
16781 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
16782 }
16783
16784 #[test]
16785 fn cancel_flag_stops_generation() {
16786 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
16787 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
16790 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
16791 assert_eq!(r.finish_reason, "cancelled");
16792 assert!(
16793 r.token_ids.is_empty(),
16794 "no tokens after cancel: {:?}",
16795 r.token_ids
16796 );
16797 assert_eq!(p.kv_cache.seq_len(), 0);
16798 assert!(p.kv_history.is_empty());
16799 assert!(!p.graph_want_logits);
16800 assert!(p.graph_logits.is_none());
16801 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
16803 assert_ne!(r2.finish_reason, "cancelled");
16804 }
16805 use super::*;
16806
16807 #[test]
16815 fn dynamic_ffn_equals_the_zeroing_arm() {
16816 let (hidden, inter) = (8usize, 32usize);
16817 let synth = |n: usize, salt: usize| -> Vec<f32> {
16818 (0..n)
16819 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
16820 .collect()
16821 };
16822 let down = synth(hidden * inter, 3);
16823 let mut down_t = vec![0.0f32; inter * hidden];
16824 for r in 0..hidden {
16825 for c in 0..inter {
16826 down_t[c * hidden + r] = down[r * inter + c];
16827 }
16828 }
16829 let d = DenseFfn {
16830 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
16831 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
16832 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
16833 act: Act::Silu,
16834 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
16835 segs: Vec::new(),
16836 };
16837 let x = synth(hidden, 11);
16838 let k = 12usize;
16839 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
16840 let mut g = vec![0.0f32; inter];
16842 d.gate_proj.matvec(&x, &mut g, None);
16843 let mut u = vec![0.0f32; inter];
16844 d.up_proj.matvec(&x, &mut u, None);
16845 for v in g.iter_mut() {
16846 *v = inference::silu(*v);
16847 }
16848 keep_top_k(&mut g, k);
16849 for i in 0..inter {
16850 g[i] *= u[i];
16851 }
16852 let mut want = vec![0.0f32; hidden];
16853 d.down_proj.matvec(&g, &mut want, None);
16854 for (a, b) in want.iter().zip(&got) {
16855 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
16856 }
16857 }
16858
16859 #[test]
16865 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
16866 let (hidden, core, tube) = (8usize, 12usize, 8usize);
16867 let inter = core + tube;
16868 let synth = |n: usize, salt: usize| -> Vec<f32> {
16869 (0..n)
16870 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
16871 .collect()
16872 };
16873 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
16874 let d_all = synth(hidden * inter, 3);
16875 let dense = DenseFfn {
16877 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
16878 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
16879 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
16880 act: Act::Silu,
16881 down_t: None,
16882 segs: Vec::new(),
16883 };
16884 let rows =
16885 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
16886 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
16887 let mut o = Vec::with_capacity(hidden * (b - a));
16888 for r in 0..hidden {
16889 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
16890 }
16891 o
16892 };
16893 let tubed = DenseFfn {
16894 down_t: None,
16895 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
16896 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
16897 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
16898 act: Act::Silu,
16899 segs: vec![FfnSeg {
16900 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
16901 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
16902 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
16903 start: core,
16904 width: tube,
16905 }],
16906 };
16907 let x = synth(hidden, 7);
16908 let want = dense_ffn(&dense, &x, None);
16909 let got = tube_ffn(&tubed, &x, 1, None, None);
16910 for (a, b) in want.iter().zip(&got) {
16911 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
16912 }
16913 let mut bits = vec![0u8; inter.div_ceil(8)];
16915 for n in 0..core {
16916 bits[n / 8] |= 1 << (n % 8);
16917 }
16918 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
16919 let masked = dense_ffn_masked(&dense, &x, None, &bits);
16920 for (a, b) in masked.iter().zip(&closed) {
16921 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
16922 }
16923 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
16925 for (a, b) in closed.iter().zip(&batch) {
16926 assert_eq!(a, b, "batch arm disagrees with decode arm");
16927 }
16928 }
16929
16930 #[test]
16932 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
16933 let (hidden, inter) = (16usize, 40usize);
16934 let synth = |n: usize, salt: usize| -> Vec<f32> {
16935 (0..n)
16936 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
16937 .collect()
16938 };
16939 let d = DenseFfn {
16940 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
16941 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
16942 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
16943 act: Act::Silu,
16944 down_t: None,
16945 segs: Vec::new(),
16946 };
16947 let x = synth(hidden, 9);
16948 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
16950
16951 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
16952
16953 let mut g = vec![0.0f32; inter];
16955 d.gate_proj.matvec(&x, &mut g, None);
16956 let mut u = vec![0.0f32; inter];
16957 d.up_proj.matvec(&x, &mut u, None);
16958 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
16959 for i in 0..inter {
16960 g[i] = if act_set.contains(&(i as u16)) {
16961 inference::silu(g[i]) * u[i]
16962 } else {
16963 0.0
16964 };
16965 }
16966 let mut reference = vec![0.0f32; hidden];
16967 d.down_proj.matvec(&g, &mut reference, None);
16968
16969 let max_d = sparse
16970 .iter()
16971 .zip(&reference)
16972 .map(|(a, b)| (a - b).abs())
16973 .fold(0.0f32, f32::max);
16974 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
16975 }
16976
16977 fn attach_test_mtp(p: &mut Pipeline) {
16979 let (h, inter, heads, kv, hd) = (
16980 p.hidden_size,
16981 p.intermediate_size,
16982 p.num_heads,
16983 p.num_kv_heads,
16984 p.head_dim,
16985 );
16986 let synth = |n: usize, salt: usize| -> Vec<f32> {
16987 (0..n)
16988 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
16989 .collect()
16990 };
16991 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16992 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16993 };
16994 p.mtp = Some(MtpModule {
16995 enorm: vec![1.0; h],
16996 hnorm: vec![1.0; h],
16997 eh_proj: qt(h, 2 * h, 301),
16998 layer: LayerWeights {
16999 input_norm: vec![1.0; h],
17000 post_norm: vec![1.0; h],
17001 attn_out_norm: None,
17002 ffn_out_norm: None,
17003 layer_scale: None,
17004 ffn: FfnKind::Dense(DenseFfn {
17005 gate_proj: qt(inter, h, 315),
17006 up_proj: qt(inter, h, 316),
17007 down_proj: qt(h, inter, 317),
17008 act: Act::Silu,
17009 down_t: None,
17010 segs: Vec::new(),
17011 }),
17012 attn: AttnKind::Full {
17013 bias: None,
17014 wq: qt(heads * hd, h, 311),
17015 wk: qt(kv * hd, h, 312),
17016 wv: qt(kv * hd, h, 313),
17017 wo: qt(h, heads * hd, 314),
17018 q_norm: None,
17019 k_norm: None,
17020 output_gate: false,
17021 softplus_gate: None,
17022 },
17023 },
17024 final_norm: vec![1.0; h],
17025 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
17026 });
17027 }
17028
17029 #[test]
17030 fn speculative_equals_vanilla_greedy() {
17031 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
17035 let run = |spec: bool| {
17036 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
17037 p.sampler_config.temperature = 0.0;
17038 attach_test_mtp(&mut p);
17039 p.speculative = spec;
17040 let r = p.generate("abcdef", 12, None, None).unwrap();
17041 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
17042 };
17043 let (vanilla, d0, _) = run(false);
17044 let (spec, d1, a1) = run(true);
17045 assert_eq!(d0, 0, "vanilla path must not draft");
17046 assert!(d1 > 0, "speculative path must draft");
17047 assert_eq!(
17048 vanilla, spec,
17049 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
17050 );
17051 }
17052
17053 #[test]
17054 fn speculative_accepts_constant_oracle() {
17055 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
17057 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
17058 p.sampler_config.temperature = 0.0;
17059 p.sampler_config.repetition_penalty = 1.0;
17060 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
17063 attach_test_mtp(&mut p);
17064 p.speculative = true;
17065 let r = p.generate("abcd", 10, None, None).unwrap();
17066 assert!(r.mtp_drafted > 0);
17067 assert_eq!(
17068 r.mtp_accepted, r.mtp_drafted,
17069 "constant logits → every draft accepted"
17070 );
17071 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
17074 }
17075
17076 #[test]
17077 fn empty_prompt_is_an_error_not_a_panic() {
17078 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
17079 let r = p.generate("", 4, None, None);
17080 assert!(r.is_err(), "empty prompt must be a clean error");
17081 }
17082
17083 #[test]
17084 fn every_token_enters_kv_exactly_once() {
17085 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
17086 p.sampler_config.temperature = 0.0;
17088 let r = p.generate("abc", 2, None, None).unwrap();
17089 assert_eq!(r.prompt_tokens, 3);
17090 assert_eq!(
17094 p.kv_cache.seq_len(),
17095 3 + r.tokens_generated - 1,
17096 "each token must be cached exactly once (v1 cached the last prompt token twice)"
17097 );
17098 }
17099
17100 #[test]
17101 fn generation_is_reproducible_with_seed() {
17102 let run = || {
17103 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
17104 p.generate("hello", 8, None, None).unwrap().token_ids
17105 };
17106 assert_eq!(run(), run());
17107 }
17108
17109 #[test]
17110 fn resetting_sampler_restarts_the_seeded_stream() {
17111 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
17112 let config = SamplerConfig {
17113 seed: Some(1234),
17114 ..SamplerConfig::default()
17115 };
17116 p.set_sampler_config(config.clone());
17117 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
17118 p.set_sampler_config(config);
17119 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
17120 assert_eq!(first, second);
17121 }
17122
17123 #[test]
17124 fn eviction_bounds_the_cache() {
17125 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
17126 p.kv_cache.max_seq_len = 6;
17127 p.sampler_config.temperature = 0.0;
17128 let _ = p.generate("abcd", 12, None, None).unwrap();
17129 assert!(
17130 p.kv_cache.seq_len() <= 6 + 1,
17131 "cache must stay bounded by max_seq_len (got {})",
17132 p.kv_cache.seq_len()
17133 );
17134 }
17135
17136 #[test]
17137 fn confidence_matches_tokens_and_is_a_probability() {
17138 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
17139 p.sampler_config.temperature = 0.0;
17140 p.sampler_config.repetition_penalty = 1.0;
17141 let r = p.generate("abcd", 10, None, None).unwrap();
17142 assert_eq!(
17143 r.token_confidence.len(),
17144 r.token_ids.len(),
17145 "one confidence per emitted token"
17146 );
17147 for &c in &r.token_confidence {
17148 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
17149 }
17150 let logits = [1.0f32, 3.0, 0.5, 3.0];
17152 let p0 = top1_prob_t(&logits, 1, 1.0);
17153 let p1 = top1_prob_t(&logits, 3, 1.0);
17154 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
17155 assert!(p0 > 0.0 && p0 < 1.0);
17156 let sharp = top1_prob_t(&logits, 1, 1.0);
17158 let soft = top1_prob_t(&logits, 1, 2.0);
17159 assert!(soft < sharp, "higher temperature lowers peak confidence");
17160 }
17161
17162 #[test]
17163 fn trace_is_opt_in_and_parallels_the_output() {
17164 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
17166 p.sampler_config.temperature = 0.0;
17167 p.sampler_config.repetition_penalty = 1.0;
17168 let r = p.generate("abcd", 10, None, None).unwrap();
17169 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
17170
17171 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
17173 p.sampler_config.temperature = 0.0;
17174 p.sampler_config.repetition_penalty = 1.0;
17175 p.set_trace(true);
17176 let r = p.generate("abcd", 10, None, None).unwrap();
17177 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
17178 for (i, tr) in r.traces.iter().enumerate() {
17179 assert_eq!(tr.t, i, "trace index is sequential");
17180 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
17181 assert_eq!(
17182 tr.confidence, r.token_confidence[i],
17183 "trace confidence matches the confidence channel"
17184 );
17185 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
17187 }
17188 }
17189
17190 #[test]
17191 fn explain_prefill_logits_match_greedy_first_token() {
17192 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
17196 p.sampler_config.temperature = 0.0;
17197 p.sampler_config.repetition_penalty = 1.0;
17198 let ids = p.tokenizer.encode("abcd");
17199 let logits = p.prefill_next_logits(&ids, None);
17200 let argmax = logits
17201 .iter()
17202 .enumerate()
17203 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
17204 .unwrap()
17205 .0 as u32;
17206 let r = p.generate("abcd", 1, None, None).unwrap();
17207 assert_eq!(
17208 argmax, r.token_ids[0],
17209 "explain preview must match greedy emit"
17210 );
17211 }
17212
17213 #[test]
17214 fn laguna_shared_expert_is_unconditionally_added() {
17215 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
17216 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
17217 let zero_dense = || DenseFfn {
17218 gate_proj: matrix(vec![0.0; 4]),
17219 up_proj: matrix(vec![0.0; 4]),
17220 down_proj: matrix(vec![0.0; 4]),
17221 act: Act::Silu,
17222 down_t: None,
17223 segs: Vec::new(),
17224 };
17225 let shared = DenseFfn {
17226 gate_proj: identity(),
17227 up_proj: identity(),
17228 down_proj: identity(),
17229 act: Act::Silu,
17230 down_t: None,
17231 segs: Vec::new(),
17232 };
17233 let x = [1.0, 2.0];
17234 let expected = dense_ffn(&shared, &x, None);
17235 let moe = MoeFfn {
17236 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
17237 experts: vec![zero_dense()],
17238 top_k: 1,
17239 norm_topk_prob: true,
17240 router_sigmoid: true,
17241 expert_bias: None,
17242 routed_scaling: 1.0,
17243 route_tau: None,
17244 shared: Some((shared, None)),
17245 stats: std::cell::RefCell::new(Vec::new()),
17246 act_sq: std::cell::RefCell::new(Vec::new()),
17247 act_rows: std::cell::RefCell::new(Vec::new()),
17248 mask: None,
17249 per_expert_scale: None,
17250 router_input_norm: false,
17251 resonance: None,
17252 };
17253 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
17254 for (actual, expected) in actual.iter().zip(expected) {
17255 assert!((actual - expected).abs() < 1e-6);
17256 }
17257 }
17258
17259 fn mimo_test_pipeline() -> Pipeline {
17268 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
17269 let kvh = [1usize, 2, 2, 1];
17270 let synth = |n: usize, salt: usize| -> Vec<f32> {
17271 (0..n)
17272 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
17273 .collect()
17274 };
17275 let qt = |rows: usize, cols: usize, salt: usize| {
17276 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17277 };
17278 let dense = |inter: usize, salt: usize| DenseFfn {
17279 gate_proj: qt(inter, hs, salt),
17280 up_proj: qt(inter, hs, salt + 1),
17281 down_proj: qt(hs, inter, salt + 2),
17282 act: Act::Silu,
17283 down_t: None,
17284 segs: Vec::new(),
17285 };
17286 let layers: Vec<LayerWeights> = (0..4)
17287 .map(|li| LayerWeights {
17288 input_norm: vec![1.0; hs],
17289 post_norm: vec![1.0; hs],
17290 attn_out_norm: None,
17291 ffn_out_norm: None,
17292 layer_scale: None,
17293 ffn: if li == 0 {
17294 FfnKind::Dense(dense(inter, 50))
17295 } else {
17296 FfnKind::Moe(MoeFfn {
17297 router: qt(4, hs, 60 + li),
17298 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
17299 top_k: 2,
17300 norm_topk_prob: true,
17301 router_sigmoid: true,
17302 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
17303 routed_scaling: 1.0,
17304 route_tau: None,
17305 shared: None,
17306 stats: std::cell::RefCell::new(Vec::new()),
17307 act_sq: std::cell::RefCell::new(Vec::new()),
17308 act_rows: std::cell::RefCell::new(Vec::new()),
17309 mask: None,
17310 per_expert_scale: None,
17311 router_input_norm: false,
17312 resonance: None,
17313 })
17314 },
17315 attn: AttnKind::Full {
17316 wq: qt(nh * hd, hs, li * 10 + 1),
17317 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
17318 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
17319 wo: qt(hs, nh * vd, li * 10 + 4),
17320 q_norm: None,
17321 k_norm: None,
17322 output_gate: false,
17323 softplus_gate: None,
17324 bias: None,
17325 },
17326 })
17327 .collect();
17328 let mut p = Pipeline::new(
17329 Tokenizer::byte_level(),
17330 PipelineWeights {
17331 embed_tokens: qt(vocab, hs, 100),
17332 layers,
17333 lm_head: qt(vocab, hs, 200),
17334 final_norm: vec![1.0; hs],
17335 },
17336 hs,
17337 inter,
17338 nh,
17339 1, hd,
17341 4,
17342 4,
17343 false,
17344 vocab,
17345 1e-6,
17346 1e7,
17347 NormStyle::Qwen,
17348 4096,
17349 SamplerConfig {
17350 seed: Some(7),
17351 ..Default::default()
17352 },
17353 );
17354 p.layer_dump = None;
17356 p.set_rotary(4, 1e7);
17357 p.sliding_layers = Some(vec![false, true, true, false]);
17358 p.swa = Some((3, usize::MAX));
17359 p.rotary_dim_local = Some(4);
17360 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
17361 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
17362 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
17363 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
17364 p
17365 }
17366
17367 #[test]
17368 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
17369 let mut p = mimo_test_pipeline();
17370 p.speculative = false;
17371 p.ignore_eos = true;
17372 p.sampler_config.temperature = 0.0;
17373 p.sampler_config.repetition_penalty = 1.0;
17374 let a = vec![3, 5, 7, 9, 11, 13];
17375 let b = vec![4, 8, 12, 16, 20, 24];
17376 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
17377 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
17378 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
17382 assert_eq!(actual, expected);
17383 assert!(p.kv_history.is_empty());
17384 let mut extended = a.clone();
17385 extended.push(17);
17386 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
17387 p.reset_session();
17388 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
17389 assert_eq!(after_media, fresh);
17390 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
17391 p.reset_session();
17395 p.generate_from_ids(&a, 1, None, None).unwrap();
17396 let mut media_ids = p.kv_history.clone();
17397 assert!(!media_ids.is_empty());
17398 media_ids.extend_from_slice(&[19, 21, 23]);
17399 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
17400 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
17401 let mut oracle = mimo_test_pipeline();
17402 oracle.speculative = false;
17403 oracle.ignore_eos = true;
17404 oracle.sampler_config.temperature = 0.0;
17405 oracle.sampler_config.repetition_penalty = 1.0;
17406 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
17407 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
17408 assert!(p.kv_history.is_empty());
17409 let mut bad = rows;
17410 bad[0] = f32::NAN;
17411 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
17412 }
17413
17414 fn f32_bits(v: &[f32]) -> Vec<u32> {
17415 v.iter().map(|x| x.to_bits()).collect()
17416 }
17417
17418 #[test]
17425 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
17426 let mut p = mimo_test_pipeline();
17427 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
17428 assert_eq!(kv, vec![1, 2, 2, 1]);
17429 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
17430 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
17431 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
17432 let hs = p.hidden_size;
17433 let mut decode = Vec::new();
17434 for (pos, &id) in ids.iter().enumerate() {
17435 let e = p.embed_single(id);
17436 let h = p.forward_layers(&e, pos, None);
17437 decode.push(p.logits_from_hidden(&h));
17438 }
17439 for l in &p.kv_cache.layers {
17440 assert_eq!(l.seq_len, 12);
17441 assert_eq!(l.head_values(0).len(), 12 * 8);
17443 }
17444 assert!(decode.iter().flatten().all(|v| v.is_finite()));
17445
17446 p.clear_sequence_state();
17447 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
17448 for pos in 0..ids.len() {
17449 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
17450 assert_eq!(
17451 f32_bits(&decode[pos]),
17452 f32_bits(&lg),
17453 "whole prompt, pos {pos}"
17454 );
17455 }
17456
17457 p.clear_sequence_state();
17458 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
17459 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
17460 for pos in 0..ids.len() {
17461 let row = if pos < 5 {
17462 &a[pos * hs..(pos + 1) * hs]
17463 } else {
17464 &b[(pos - 5) * hs..(pos - 4) * hs]
17465 };
17466 let lg = p.logits_from_hidden(row);
17467 assert_eq!(
17468 f32_bits(&decode[pos]),
17469 f32_bits(&lg),
17470 "two chunks, pos {pos}"
17471 );
17472 }
17473
17474 let last = |p: &mut Pipeline| {
17477 p.clear_sequence_state();
17478 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
17479 p.logits_from_hidden(&hb[11 * hs..12 * hs])
17480 };
17481 let base = last(&mut p);
17482 let mut no_sinks = mimo_test_pipeline();
17483 for l in &mut no_sinks.kv_cache.layers {
17484 l.sinks = None;
17485 }
17486 assert_ne!(
17487 f32_bits(&last(&mut no_sinks)),
17488 f32_bits(&base),
17489 "sinks are live"
17490 );
17491 let mut wide = mimo_test_pipeline();
17492 wide.swa = Some((64, usize::MAX));
17493 assert_ne!(
17494 f32_bits(&last(&mut wide)),
17495 f32_bits(&base),
17496 "window is live"
17497 );
17498
17499 p.clear_sequence_state();
17501 p.ignore_eos = true;
17502 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
17503 assert_eq!(r.token_ids.len(), 4);
17504 }
17505
17506 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
17509 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
17510 let synth = |len: usize, salt: usize| -> Vec<f32> {
17511 (0..len)
17512 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
17513 .collect()
17514 };
17515 let qt = |rows: usize, cols: usize, salt: usize| {
17516 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17517 };
17518 let layers = (0..n)
17519 .map(|k| {
17520 let s = 500 + k * 40;
17521 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
17522 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
17523 MtpModule {
17524 enorm: vec![1.0; hs],
17525 hnorm: vec![1.0; hs],
17526 eh_proj: qt(hs, 2 * hs, s),
17527 layer: LayerWeights {
17528 input_norm: vec![1.0; hs],
17529 post_norm: vec![1.0; hs],
17530 attn_out_norm: None,
17531 ffn_out_norm: None,
17532 layer_scale: None,
17533 attn: AttnKind::Full {
17534 wq: qt(nh * hd, hs, s + 1),
17535 wk: qt(nkv * hd, hs, s + 2),
17536 wv: qt(nkv * vd, hs, s + 3),
17537 wo: qt(hs, nh * vd, s + 4),
17538 q_norm: None,
17539 k_norm: None,
17540 output_gate: false,
17541 softplus_gate: None,
17542 bias: None,
17543 },
17544 ffn: FfnKind::Dense(DenseFfn {
17545 gate_proj: qt(inter, hs, s + 5),
17546 up_proj: qt(inter, hs, s + 6),
17547 down_proj: qt(hs, inter, s + 7),
17548 act: Act::Silu,
17549 down_t: None,
17550 segs: Vec::new(),
17551 }),
17552 },
17553 final_norm: vec![1.0; hs],
17554 kv,
17555 }
17556 })
17557 .collect();
17558 mimo_mtp::MimoMtp::from_layers(layers)
17559 }
17560
17561 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
17562 p.clear_sequence_state();
17563 p.speculative = spec;
17564 p.ignore_eos = true;
17565 p.sampler_config.temperature = 0.0;
17566 p.generate_from_ids(ids, n, None, None).unwrap()
17567 }
17568
17569 #[test]
17575 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
17576 for post in [false, true] {
17579 let mut p = mimo_test_pipeline();
17580 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
17582 let mut st0 = mimo_test_mtp(3, 1.0);
17583 st0.post_norm_hidden = post;
17584 p.mimo_mtp = Some(st0);
17585 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
17586 let hs = p.hidden_size;
17587 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
17588 p.mimo_note_rows(&hb, 0);
17589 let mut st = p.mimo_mtp.take().unwrap();
17590 let k = 3;
17593 let mut inc = Vec::new();
17594 for t in 0..ids.len() - k - 1 {
17595 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
17596 }
17597 let s = ids.len();
17600 let mut reference = vec![vec![0u32; k]; s - k - 1];
17601 let mut fresh = mimo_test_mtp(3, 1.0);
17602 for (layer, m) in fresh.layers.iter_mut().enumerate() {
17603 let n = s - layer - 1;
17604 let mut cats = vec![0.0f32; n * 2 * hs];
17605 for j in 0..n {
17606 let e = p.embed_single(ids[j + layer + 1]);
17607 let raw = &hb[j * hs..(j + 1) * hs];
17608 let g = if post {
17609 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
17610 } else {
17611 raw.to_vec()
17612 };
17613 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
17614 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
17615 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
17616 }
17617 let mut x = vec![0.0f32; n * hs];
17618 m.eh_proj.matmat(&cats, n, &mut x, None);
17619 p.mimo_mtp_block(m, &mut x, n, 0);
17620 for (t, row) in reference.iter_mut().enumerate() {
17621 let y = inference::rms_norm(
17622 &x[t * hs..(t + 1) * hs],
17623 &m.final_norm,
17624 p.rms_eps,
17625 p.norm_style,
17626 );
17627 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
17628 }
17629 }
17630 assert_eq!(inc, reference, "post_norm_hidden = {post}");
17631 let distinct: std::collections::HashSet<u32> =
17633 inc.iter().flatten().copied().collect();
17634 assert!(distinct.len() > 3, "{inc:?}");
17635 let last_t = ids.len() - k - 2;
17637 for m in &st.layers {
17638 assert_eq!(m.kv.seq_len, last_t + 1);
17639 }
17640 }
17641 }
17642
17643 #[test]
17650 fn mimo_speculative_greedy_equals_plain_greedy() {
17651 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
17652 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
17653 let n = 24;
17654 let mut p = mimo_test_pipeline();
17655 let plain = mimo_greedy(&mut p, &ids, n, false);
17656 assert_eq!(plain.mtp_drafted, 0);
17657 assert_eq!(plain.token_ids.len(), n);
17658 let plain_kv = p.kv_cache.layers[0].seq_len;
17659
17660 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
17662 let spec = mimo_greedy(&mut p, &ids, n, true);
17663 assert!(spec.mtp_drafted > 0, "the round must draft");
17664 assert_eq!(spec.token_ids, plain.token_ids);
17665 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
17666
17667 let mut truth: Vec<u32> = ids.clone();
17670 truth.extend(&plain.token_ids);
17671 let mut noisy = truth.clone();
17672 for (i, t) in noisy.iter_mut().enumerate() {
17673 if i % 5 == 0 {
17674 *t = (*t + 1) % 64;
17675 }
17676 }
17677 let mut st = mimo_test_mtp(3, 1.0);
17678 st.draft_override = Some(noisy);
17679 p.mimo_mtp = Some(st);
17680 let spec = mimo_greedy(&mut p, &ids, n, true);
17681 assert_eq!(spec.token_ids, plain.token_ids);
17682 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
17683 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
17684 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
17685 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
17686 assert!(
17687 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
17688 "{:?}",
17689 stats.accept_hist
17690 );
17691 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
17692
17693 let mut st = mimo_test_mtp(3, 1.0);
17696 st.draft_override = Some(truth);
17697 p.mimo_mtp = Some(st);
17698 let spec = mimo_greedy(&mut p, &ids, n, true);
17699 assert_eq!(spec.token_ids, plain.token_ids);
17700 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
17701 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
17702
17703 let off = mimo_greedy(&mut p, &ids, n, false);
17705 assert_eq!(off.token_ids, plain.token_ids);
17706 assert_eq!(off.mtp_drafted, 0);
17707 }
17708
17709 #[test]
17718 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
17719 let p = mimo_test_pipeline();
17720 assert_eq!(
17721 p.graph_attn_decline_reason(),
17722 Some("per-layer KV head counts")
17723 );
17724 assert_eq!(p.wgpu_graph_attn_decline(), None);
17725 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
17726 assert_eq!(
17727 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
17728 (1, 4, 4, None, false)
17729 );
17730 assert_eq!(g0.invf, p.inv_freq.as_slice());
17731 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
17732 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
17733 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
17734 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
17735 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
17736 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
17737 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
17738
17739 let emb = p.embed_single(3);
17742 let mut lg = Vec::new();
17743 assert!(
17744 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
17745 .is_none()
17746 );
17747 let mut hid = emb.clone();
17748 assert_eq!(
17749 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
17750 crate::gpu::BatchGraphOutcome::Declined
17751 );
17752 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
17753 assert!(p.try_multi_burst(3, 0, 4).is_none());
17754 assert!(
17755 p.graph_declines().is_empty(),
17756 "no attention decline logged: {:?}",
17757 p.graph_declines()
17758 );
17759 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
17764 assert_eq!(plain().graph_attn_decline_reason(), None);
17765 assert_eq!(plain().wgpu_graph_attn_decline(), None);
17766 assert!(
17767 plain().graph_attn_geom(0).is_none(),
17768 "uniform models keep the historical arms"
17769 );
17770 let mut q = plain();
17771 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
17772 assert_eq!(
17773 q.graph_attn_decline_reason(),
17774 Some("learned attention sinks")
17775 );
17776 assert_eq!(
17777 q.graph_attn_geom(1).unwrap().sink,
17778 Some(&[0.25f32, -0.25][..])
17779 );
17780 let mut q = plain();
17781 q.set_attn_geometry(None, Some(2)).unwrap();
17782 assert_eq!(
17783 q.graph_attn_decline_reason(),
17784 Some("V heads narrower than Q/K heads")
17785 );
17786 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
17787 let mut q = plain();
17788 q.sliding_layers = Some(vec![true, false]);
17789 q.swa = Some((4, usize::MAX));
17790 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
17791 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
17792 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
17793
17794 let mut q = mimo_test_pipeline();
17797 q.rope_scale = 2.0;
17798 assert_eq!(
17799 q.wgpu_graph_attn_decline(),
17800 Some("scaled RoPE positions with per-layer geometry")
17801 );
17802 let emb = q.embed_single(3);
17803 assert!(
17804 q.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, q.num_layers)
17805 .is_none()
17806 );
17807 let _ = q.try_token_graph_wgpu_steps(&emb, 1, &mut lg, 1, None, None, 0, q.num_layers);
17808 let lines = q.graph_declines();
17809 assert_eq!(
17810 lines
17811 .iter()
17812 .filter(|l| l.starts_with("wgpu token graph") && l.contains("scaled RoPE"))
17813 .count(),
17814 1,
17815 "{lines:?}"
17816 );
17817 }
17818
17819 #[test]
17820 fn mimo_verify_rewind_preserves_lagging_host_caches() {
17821 let mut p = mimo_test_pipeline();
17822 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
17823 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
17824 for _ in 0..if li == 0 { 2 } else { 12 } {
17825 layer.append(&row, &row, &[]);
17826 }
17827 }
17828 p.mimo_verify_rewind(9).unwrap();
17829 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
17830 for layer in &p.kv_cache.layers[1..] {
17831 assert_eq!(layer.seq_len, 9);
17832 }
17833 }
17834
17835 #[test]
17839 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
17840 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
17841 let _ = std::fs::remove_dir_all(&dir);
17842 let mut p = mimo_test_pipeline();
17843 let hs = p.hidden_size;
17844 let ids = [5u32, 9, 11, 2, 40];
17845 p.layer_dump = Some(dir.join("decode"));
17846 for (pos, &id) in ids.iter().enumerate() {
17847 let e = p.embed_single(id);
17848 let _ = p.forward_layers(&e, pos, None);
17849 }
17850 p.clear_sequence_state();
17851 p.layer_dump = Some(dir.join("prefill"));
17852 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
17853 for pos in 0..ids.len() {
17854 for li in 0..p.num_layers {
17855 let name = format!("p{pos:06}_l{li:02}.f32");
17856 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
17857 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
17858 assert_eq!(a.len(), hs * 4, "{name}");
17859 assert_eq!(a, b, "{name}");
17860 }
17861 }
17862 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
17863 let vals: Vec<f32> = last
17864 .chunks(4)
17865 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
17866 .collect();
17867 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
17868 let _ = std::fs::remove_dir_all(&dir);
17869 }
17870
17871 #[test]
17872 fn attn_geometry_and_sinks_are_validated() {
17873 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
17874 assert!(
17875 p.set_attn_geometry(Some(vec![2]), None).is_err(),
17876 "one entry per layer"
17877 );
17878 assert!(
17879 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
17880 "3 does not divide 4"
17881 );
17882 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
17883 assert!(p.set_attn_geometry(None, Some(0)).is_err());
17884 assert!(
17885 p.set_attn_geometry(None, Some(5)).is_err(),
17886 "V wider than the head"
17887 );
17888 p.set_attn_geometry(None, Some(4)).unwrap();
17889 assert_eq!(
17890 p.v_head_dim, None,
17891 "v_head_dim == head_dim is the uniform case"
17892 );
17893 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
17894 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
17895 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
17896 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
17897 assert!(
17898 p.kv_cache.layers[1].sinks.is_some(),
17899 "a reshape keeps the layer's sinks"
17900 );
17901 assert_eq!(p.layer_geom(1).0, 4);
17902 assert!(
17903 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
17904 "one sink per Q head"
17905 );
17906 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
17907 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
17908 }
17909
17910 #[test]
17913 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
17914 let cfg = || {
17915 Some(crate::nystrom::O1Cfg {
17916 layers: crate::nystrom::O1Layers::All,
17917 m: 4,
17918 w: 8,
17919 sink: 2,
17920 rect: crate::nystrom::O1Rect::Aggregate,
17921 })
17922 };
17923 let mut p = mimo_test_pipeline();
17924 p.set_o1(cfg());
17925 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
17926 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
17927 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
17928 q.sliding_layers = Some(vec![false, false, true]);
17929 q.swa = Some((4, usize::MAX));
17930 q.set_o1(cfg());
17931 assert_eq!(q.o1_flags, vec![true, false, false]);
17932 }
17933
17934 #[test]
17935 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
17936 const B: usize = 19;
17937 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
17938 p.set_o1(Some(crate::nystrom::O1Cfg {
17939 layers: crate::nystrom::O1Layers::All,
17940 m: 4,
17941 w: 8,
17942 sink: 2,
17943 rect: crate::nystrom::O1Rect::Aggregate,
17944 }));
17945 p.o1_begin_with_prefix(Some(B));
17946 let ids: Vec<u32> = (0..B as u32).collect();
17947 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
17948
17949 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
17950 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
17951 let next = p.embed_single(B as u32);
17952 let _ = p.forward_layers(&next, B, None);
17953 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
17954 }
17955
17956 #[test]
17957 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
17958 const B: usize = 19;
17959 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
17960 let gdn_cfg = crate::linear_core::GdnCfg {
17964 num_v_heads: 2,
17965 num_k_heads: 1,
17966 key_head_dim: 2,
17967 value_head_dim: 4,
17968 conv_kernel: 3,
17969 hidden_size: 8,
17970 rms_eps: 1e-6,
17971 output_gate_sigmoid: false,
17972 };
17973 let synth = |n: usize, salt: usize| -> Vec<f32> {
17974 (0..n)
17975 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
17976 .collect()
17977 };
17978 let qt = |rows: usize, cols: usize, salt: usize| {
17979 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17980 };
17981 let c_dim = gdn_cfg.conv_dim();
17982 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
17983 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
17984 in_proj_qkv: qt(c_dim, 8, 1),
17985 in_proj_z: qt(vd, 8, 2),
17986 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
17987 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
17988 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
17989 a_log: vec![0.2, 0.5],
17990 dt_bias: synth(gdn_cfg.num_v_heads, 6),
17991 norm: vec![1.0; gdn_cfg.value_head_dim],
17992 out_proj: qt(8, vd, 7),
17993 });
17994 p.gdn_cfg = Some(gdn_cfg);
17995 p.set_o1(Some(crate::nystrom::O1Cfg {
17996 layers: crate::nystrom::O1Layers::All,
17997 m: 4,
17998 w: 8,
17999 sink: 2,
18000 rect: crate::nystrom::O1Rect::Aggregate,
18001 }));
18002 p.o1_begin_with_prefix(Some(B));
18003 for pos in 0..B - 2 {
18004 let emb = p.embed_single(pos as u32);
18005 let _ = p.forward_layers(&emb, pos, None);
18006 }
18007 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
18008
18009 let e1 = p.embed_single((B - 2) as u32);
18010 let e2 = p.embed_single((B - 1) as u32);
18011 let _ = p.forward_pair(&e1, &e2, B - 2);
18012
18013 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
18014 assert!(
18015 p.kv_cache
18016 .layers
18017 .iter()
18018 .enumerate()
18019 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
18020 );
18021 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
18022 assert_ne!(
18023 p.kv_cache.layers[0].linear_state, lane1_state,
18024 "real pair must commit GDN lane 2 before returning"
18025 );
18026 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
18027 let next = p.embed_single(B as u32);
18028 let _ = p.forward_layers(&next, B, None);
18029 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
18030 }
18031
18032 #[test]
18033 fn o1_error_observation_stays_terminal_until_reset() {
18034 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18035 p.set_o1(Some(crate::nystrom::O1Cfg {
18036 layers: crate::nystrom::O1Layers::All,
18037 m: 4,
18038 w: 8,
18039 sink: 2,
18040 rect: crate::nystrom::O1Rect::Aggregate,
18041 }));
18042 p.o1_begin();
18043 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
18044
18045 assert!(p.o1_seal_checked().is_err());
18046 assert!(
18047 p.o1_seal_checked().is_err(),
18048 "retry must see the sticky error"
18049 );
18050 let k = vec![0.2f32; 4];
18051 let v = vec![0.3f32; 4];
18052 p.kv_cache.layers[0].append(&k, &v, &[]);
18053 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
18054
18055 p.reset_session();
18056 p.o1_begin();
18057 p.kv_cache.layers[0].append(&k, &v, &[]);
18058 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
18059 }
18060
18061 #[test]
18062 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
18063 let ids = vec![1u32, 2, 3, 4, 5, 6];
18064 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18065 p.graph_logits = Some(vec![123.0]);
18066 p.graph_want_logits = true;
18067 p.graph_failed
18068 .store(true, std::sync::atomic::Ordering::Relaxed);
18069 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
18070 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
18071 assert!(err.contains("before NLL"));
18072 assert!(p.graph_logits.is_none());
18073 assert!(!p.graph_want_logits);
18074 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
18075 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
18076
18077 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18078 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
18079 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
18080 assert_eq!(actual.1, expected.1);
18081 assert!((actual.0 - expected.0).abs() < 1e-9);
18082 }
18083
18084 #[test]
18085 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
18086 let ids = vec![1u32, 2, 3, 4, 5, 6];
18087 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18088 p.nll_test_fail_at = Some(1);
18089 let err = p
18090 .nll_ids_from(&ids, 0)
18091 .expect_err("one-shot forward failure");
18092 assert!(err.contains("forward") || err.contains("score row"));
18093 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
18094 assert!(!p.graph_want_logits);
18095 assert!(p.graph_logits.is_none());
18096 assert!(p.kv_history.is_empty());
18097
18098 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18099 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
18100 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
18101 assert_eq!(actual.1, expected.1);
18102 assert!((actual.0 - expected.0).abs() < 1e-9);
18103 }
18104
18105 #[test]
18106 fn nll_serial_failure_before_first_row_is_reported() {
18107 let ids = vec![1u32, 2, 3, 4];
18108 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18109 p.nll_test_force_serial = true;
18110 p.nll_test_fail_at = Some(0);
18111 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
18112 assert!(err.contains("serial forward"));
18113 assert!(p.kv_history.is_empty());
18114 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
18115 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
18116 }
18117
18118 #[test]
18119 fn ffn_probe_failure_discards_recorder_and_state() {
18120 let ids = vec![1u32, 2, 3, 4];
18121 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18122 p.nll_test_fail_at = Some(0);
18123 let err = p
18124 .probe_ffn_mass_batch(&ids)
18125 .expect_err("probe forward failure");
18126 assert!(err.contains("NLL"));
18127 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
18128 assert!(p.kv_history.is_empty());
18129 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
18130 }
18131
18132 #[test]
18133 fn nll_test_controls_are_pipeline_scoped() {
18134 let ids = vec![1u32, 2, 3, 4];
18135 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18136 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18137 failing.nll_test_force_serial = true;
18138 failing.nll_test_fail_at = Some(0);
18139
18140 assert!(!failing.can_prefill_batched());
18141 assert!(unaffected.can_prefill_batched());
18142 let expected = unaffected
18143 .nll_ids_from(&ids, 0)
18144 .expect("unaffected pipeline remains usable");
18145 let err = failing
18146 .nll_ids_from(&ids, 0)
18147 .expect_err("failure injection belongs to failing pipeline");
18148 assert!(err.contains("serial forward"));
18149 assert!(failing.nll_test_fail_at.is_none());
18150 assert!(unaffected.can_prefill_batched());
18151 let actual = unaffected
18152 .nll_ids_from(&ids, 0)
18153 .expect("unaffected pipeline remains reusable");
18154 assert_eq!(actual.1, expected.1);
18155 assert!((actual.0 - expected.0).abs() < 1e-9);
18156 }
18157
18158 #[test]
18159 fn forward_ids_failure_channel_is_terminal_and_reusable() {
18160 let ids = vec![1u32, 2, 3, 4, 5, 6];
18161 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
18162 p.graph_logits = Some(vec![123.0]);
18163 p.graph_want_logits = true;
18164 p.graph_failed
18165 .store(true, std::sync::atomic::Ordering::Relaxed);
18166 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
18167
18168 let err = p
18169 .forward_ids(&ids, None)
18170 .expect_err("a failed forward must not become a valid head result");
18171 assert!(err.contains("forward_ids setup"));
18172 assert!(p.graph_logits.is_none());
18173 assert!(!p.graph_want_logits);
18174 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
18175 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
18176 assert_eq!(p.kv_cache.seq_len(), 0);
18177
18178 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
18179 .forward_ids(&ids, None)
18180 .expect("fresh forward_ids");
18181 let actual = p
18182 .forward_ids(&ids, None)
18183 .expect("pipeline remains reusable after a failed forward");
18184 assert_eq!(actual.len(), expected.len());
18185 assert!(
18186 actual
18187 .iter()
18188 .zip(expected)
18189 .all(|(a, b)| (a - b).abs() < 1e-9)
18190 );
18191 assert_eq!(p.kv_cache.seq_len(), ids.len());
18192 }
18193}