1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
14 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
15 vmf_phase_pair,
16};
17use crate::pool::Pool;
18use crate::qtensor::QTensor;
19use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
20use crate::tokenizer::Tokenizer;
21use cortiq_core::mask::TaskMask;
22use cortiq_core::types::NormStyle;
23
24pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
25 std::sync::atomic::AtomicBool::new(false);
26
27struct ForwardScratch {
31 n1: Vec<f32>,
32 n2: Vec<f32>,
33 p1: Vec<f32>,
34 p2: Vec<f32>,
35}
36
37impl ForwardScratch {
38 fn new(hidden: usize) -> Self {
39 Self {
40 n1: vec![0.0; hidden],
41 n2: vec![0.0; hidden],
42 p1: vec![0.0; hidden],
43 p2: vec![0.0; hidden],
44 }
45 }
46}
47
48pub struct Pipeline {
50 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
55 pub tokenizer: std::sync::Arc<Tokenizer>,
58 pub kv_cache: KvCache,
59 pub sampler_config: SamplerConfig,
60 pub weights: PipelineWeights,
61 pub hidden_size: usize,
62 pub intermediate_size: usize,
63 pub num_heads: usize,
64 pub num_kv_heads: usize,
65 pub head_dim: usize,
66 pub num_layers: usize,
68 pub physical_layers: usize,
70 pub loop_final_norm: bool,
72 pub vocab_size: usize,
73 pub rms_eps: f64,
74 pub rope_base: f32,
75 pub norm_style: NormStyle,
76 pub rotary_dim: usize,
78 pub attention_heads_per_layer: Option<Vec<usize>>,
80 pub vmf_cfg: Option<VmfPhaseCfg>,
82 pub gdn_cfg: Option<GdnCfg>,
84 pub logit_multiplier: Option<f32>,
86 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
91 graph_failed: std::sync::atomic::AtomicBool,
96 pub kv_history: Vec<u32>,
101 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
103 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
106 pub dsv4: Option<
110 Box<(
111 crate::dsv4::Dsv4Globals,
112 Vec<crate::dsv4::Dsv4Layer>,
113 crate::dsv4::Dsv4Cfg,
114 crate::dsv4::Dsv4State,
115 )>,
116 >,
117 pub dsv41: Option<
121 Box<(
122 crate::dsv41::Dsv41Globals,
123 Vec<crate::dsv41::Dsv41Layer>,
124 crate::dsv41::Dsv41Cfg,
125 crate::dsv41::Dsv41State,
126 )>,
127 >,
128 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
130 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
132 pub qwen4_exp: Option<
135 Box<(
136 crate::qwen4_exp::Globals,
137 Vec<crate::qwen4_exp::Layer>,
138 crate::qwen4_exp::Cfg,
139 crate::qwen4_exp::State,
140 )>,
141 >,
142 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
146 pub dspark: Option<crate::dsv4::DsparkState>,
148 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
151 pub dspark_hist: Vec<usize>,
153 pub dspark_real: Vec<u32>,
157 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
161 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
163 pub dspark_draft_ns: u128,
167 pub short_conv_cfg: Option<ShortConvCfg>,
170 pub mtp: Option<MtpModule>,
172 pub speculative: bool,
174 pub ignore_eos: bool,
180 pub draft_full_streak: u32,
186 pub spec_k_adapt: Option<usize>,
194 pub spec_acc_ewma: f32,
196 rng: SplitMix64,
197 sampler_scratch: SamplerScratch,
198 spec_forced: Option<u32>,
204 spec_q: Vec<Vec<f32>>,
205 spec_p: Vec<f32>,
206 spec_res: Vec<f32>,
207 spec_qs: Vec<sampler::Sparse>,
209 spec_ps: sampler::Sparse,
210 spec_ress: sampler::Sparse,
211 mtp_graph_mode: Option<bool>,
218 #[cfg(target_os = "macos")]
221 metal_verify: Option<MetalVerifyPending>,
222 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
226 ws: ForwardScratch,
230 pool: Option<std::sync::Arc<Pool>>,
232 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
236 pub(crate) dyn_force_f32: bool,
238 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
243 pub(crate) dyn_active: Option<usize>,
249 pub(crate) dyn_blend_loaded: bool,
253 pub(crate) dyn_phi_layer: Option<usize>,
256 dyn_phi_ema: Vec<f32>,
258 dyn_phi_seen: usize,
259 pub dyn_router: Option<crate::swarm::DynRouter>,
262 o1_cfg: Option<crate::nystrom::O1Cfg>,
265 o1_epoch: u64,
268 o1_flags: Vec<bool>,
270 trace: bool,
273 calib_temp: f32,
276 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
278 graph_kv_id: u64,
279 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
282 graph_want_logits: bool,
283 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
287 graph_head_required: bool,
288 graph_logits: Option<Vec<f32>>,
291 pub embed_multiplier: f32,
293 pub attn_scale: f32,
296 pub swa: Option<(usize, usize)>,
299 pub sliding_layers: Option<Vec<bool>>,
302 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
305 pub rotary_dim_local: Option<usize>,
306 pub rope_scale: f32,
307 pub rope_scale_local: f32,
308 pub global_attn: Option<(usize, usize)>,
311 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
314 pub attn_v_norm: bool,
316 pub qk_norm_after_rope: bool,
318 pub final_softcap: Option<f32>,
320 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
324 pub attn_softcap: f32,
326 confidence_on: bool,
330 #[cfg(test)]
333 nll_test_fail_at: Option<usize>,
334 #[cfg(test)]
337 nll_test_force_serial: bool,
338}
339
340#[cfg(target_os = "macos")]
341impl Drop for Pipeline {
342 fn drop(&mut self) {
343 crate::gpu::kv_mirror_drop(self.graph_kv_id);
344 }
345}
346
347pub struct PipelineWeights {
352 pub embed_tokens: QTensor,
354 pub layers: Vec<LayerWeights>,
356 pub lm_head: QTensor,
358 pub final_norm: Vec<f32>,
360}
361
362pub struct LayerWeights {
364 pub input_norm: Vec<f32>,
365 pub post_norm: Vec<f32>,
368 pub attn_out_norm: Option<Vec<f32>>,
371 pub layer_scale: Option<f32>,
373 pub ffn_out_norm: Option<Vec<f32>>,
376 pub ffn: FfnKind,
377 pub attn: AttnKind,
378}
379
380#[derive(Clone, Copy, PartialEq, Debug, Default)]
383pub enum Act {
384 #[default]
385 Silu,
386 GeluTanh,
387 Situ {
390 beta: f32,
391 linear_beta: f32,
392 },
393}
394
395impl Act {
396 pub fn from_arch(name: &str) -> Self {
397 if name == "gelu_tanh" {
398 Self::GeluTanh
399 } else {
400 Self::Silu
401 }
402 }
403
404 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
406 match arch.hidden_act.as_str() {
407 "situ" => Self::Situ {
408 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
409 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
410 },
411 other => Self::from_arch(other),
412 }
413 }
414
415 #[inline]
416 pub fn apply(self, x: f32) -> f32 {
417 match self {
418 Self::Silu => inference::silu(x),
419 Self::GeluTanh => inference::gelu_tanh(x),
420 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
421 }
422 }
423
424 #[inline]
427 pub fn combine(self, g: f32, u: f32) -> f32 {
428 match self {
429 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
430 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
431 }
432 _ => self.apply(g) * u,
433 }
434 }
435}
436
437pub struct DenseFfn {
439 pub gate_proj: QTensor,
440 pub up_proj: QTensor,
441 pub down_proj: QTensor,
442 pub act: Act,
444 pub down_t: Option<QTensor>,
450 pub segs: Vec<FfnSeg>,
457}
458
459pub struct FfnSeg {
464 pub gate: QTensor,
465 pub up: QTensor,
466 pub down: QTensor,
467 pub start: usize,
468 pub width: usize,
469}
470
471pub enum FfnKind {
474 Dense(DenseFfn),
475 Moe(MoeFfn),
479 DenseMoe(Box<DenseMoeFfn>),
486}
487
488pub struct DenseMoeFfn {
490 pub dense: DenseFfn,
491 pub moe: MoeFfn,
492 pub post_norm_1: Vec<f32>,
494 pub pre_norm_2: Vec<f32>,
497 pub post_norm_2: Vec<f32>,
499}
500
501pub struct MoeFfn {
502 pub router: QTensor,
504 pub experts: Vec<DenseFfn>,
505 pub top_k: usize,
506 pub norm_topk_prob: bool,
507 pub router_sigmoid: bool,
510 pub expert_bias: Option<Vec<f32>>,
514 pub routed_scaling: f32,
517 pub route_tau: Option<f32>,
523 pub shared: Option<(DenseFfn, Option<QTensor>)>,
526 pub stats: std::cell::RefCell<Vec<u64>>,
530 pub act_sq: std::cell::RefCell<Vec<f64>>,
537 pub act_rows: std::cell::RefCell<Vec<f32>>,
543 pub mask: Option<Vec<bool>>,
548 pub per_expert_scale: Option<Vec<f32>>,
551 pub router_input_norm: bool,
555 pub resonance: Option<Resonance>,
559}
560
561pub struct Resonance {
563 pub mu: Vec<f32>,
565 pub u: Vec<f32>,
567 pub k: usize,
568 pub bias: Vec<f32>,
570}
571
572impl Resonance {
573 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
575 let h = x.len();
576 let ne = out.len();
577 for e in 0..ne {
578 let mu = &self.mu[e * h..(e + 1) * h];
579 let mut d2 = 0.0f32;
580 for j in 0..h {
581 let d = x[j] - mu[j];
582 d2 += d * d;
583 }
584 let mut proj = 0.0f32;
585 for i in 0..self.k {
586 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
587 let mut p = 0.0f32;
588 for j in 0..h {
589 p += (x[j] - mu[j]) * u[j];
590 }
591 proj += p * p;
592 }
593 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
594 }
595 }
596}
597
598pub enum AttnKind {
601 Full {
603 wq: QTensor,
604 wk: QTensor,
605 wv: QTensor,
606 wo: QTensor,
607 q_norm: Option<Vec<f32>>,
608 k_norm: Option<Vec<f32>>,
609 output_gate: bool,
610 softplus_gate: Option<(QTensor, bool)>,
614 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
616 },
617 Linear(VmfPhaseWeights),
619 LinearGdn(GdnWeights),
621 ShortConv(ShortConvWeights),
624 Mla(Box<MlaWeights>),
632 Kda(Box<crate::linear_core::KdaWeights>),
636}
637
638pub struct MlaWeights {
640 pub q_proj: QTensor,
644 pub q_a: Option<QTensor>,
647 pub q_a_norm: Option<Vec<f32>>,
648 pub kv_a: QTensor,
650 pub kv_a_norm: Vec<f32>,
652 pub kv_b: QTensor,
654 pub o_proj: QTensor,
656 pub nh: usize,
657 pub qk_rope: usize,
658 pub qk_nope: usize,
659 pub v_dim: usize,
660 pub lora: usize,
661 pub scale: f32,
663 pub nope: bool,
665}
666
667pub struct MtpModule {
672 pub enorm: Vec<f32>,
673 pub hnorm: Vec<f32>,
674 pub eh_proj: QTensor,
676 pub layer: LayerWeights,
677 pub final_norm: Vec<f32>,
678 pub kv: crate::kv_cache::LayerKvCache,
679}
680
681#[cfg(target_os = "macos")]
688enum MetalRowsItem<'a> {
689 Gdn {
690 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
691 first: usize,
692 },
693 Attn {
694 l: crate::gpu_metal::AttnGpuLayer<'a>,
695 li: usize,
696 q_norm: Option<&'a [f32]>,
697 k_norm: Option<&'a [f32]>,
698 output_gate: bool,
699 },
700}
701
702#[cfg(target_os = "macos")]
703struct MetalVerifyPending {
704 graph: crate::gpu_metal::VerifyGraph,
705 gdn_layers: Vec<usize>,
706 attn_layers: Vec<(usize, usize)>,
707}
708
709#[cfg(target_os = "macos")]
710enum MetalRowsRun {
711 Declined,
713 Failed,
716 Completed(MetalVerifyPending),
717}
718
719#[cfg(target_os = "macos")]
720enum MetalPrefillOutcome {
721 Declined,
722 Failed,
723 Completed(Vec<f32>),
724}
725
726#[cfg(target_os = "macos")]
727enum MetalBatchNllOutcome {
728 Declined,
729 Failed(String),
730 Completed(f64, usize),
731}
732
733#[derive(Clone, Copy)]
737enum SpecTrial {
738 Spec {
739 t0: std::time::Instant,
740 gen0: usize,
741 rounds: usize,
742 },
743 Plain {
744 t0: std::time::Instant,
745 gen0: usize,
746 },
747 Decided {
748 spec: bool,
749 recheck_at: usize,
750 },
751}
752
753#[derive(Default, Clone, Copy)]
764struct SpecMon {
765 round_ms: f64,
766 tokens: f64,
767 plain_ms: f64,
768 n: u32,
769 fails: u32,
770}
771
772impl SpecMon {
773 fn round(&mut self, dt_ms: f64, produced: usize) {
774 self.n += 1;
775 if self.n == 1 {
776 return; }
778 let a = if self.n == 2 { 1.0 } else { 0.3 };
779 self.round_ms += a * (dt_ms - self.round_ms);
780 self.tokens += a * (produced as f64 - self.tokens);
781 }
782 fn pays(&self) -> bool {
783 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
784 }
785}
786
787pub struct GenerateResult {
789 pub text: String,
790 pub token_ids: Vec<u32>,
791 pub prompt_tokens: usize,
792 pub tokens_generated: usize,
793 pub finish_reason: String,
794 pub mtp_drafted: usize,
796 pub mtp_accepted: usize,
797 pub token_confidence: Vec<f32>,
802 pub traces: Vec<TokenTrace>,
805}
806
807#[derive(Clone, Debug)]
812pub struct TokenTrace {
813 pub t: usize,
815 pub token_id: u32,
817 pub confidence: f32,
819 pub active_skill: Option<String>,
821 pub recon: Option<f32>,
825 pub switched: bool,
828}
829
830#[cfg_attr(not(test), allow(dead_code))]
835fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
836 let t = if temp > 1e-3 { temp } else { 1.0 };
837 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
838 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
839 if sum > 0.0 {
840 (((logits[id as usize] - max) / t).exp()) / sum
841 } else {
842 0.0
843 }
844}
845
846fn prefill_batched() -> bool {
849 std::env::var("CMF_PREFILL")
850 .map(|v| v != "seq")
851 .unwrap_or(true)
852}
853
854#[inline]
858fn nll_graph_policy(
859 unmasked: bool,
860 prefer_graph: bool,
861 native_metal: bool,
862) -> (bool, bool) {
863 let graph_quality = unmasked && prefer_graph;
864 let fused_head_quality = graph_quality && native_metal;
865 (graph_quality, fused_head_quality)
866}
867
868#[derive(Clone, Copy)]
872enum PrefillIn<'a> {
873 Ids(&'a [u32]),
874 Hidden(&'a [f32]),
875}
876
877impl Pipeline {
884 fn can_prefill_batched(&self) -> bool {
885 #[cfg(test)]
886 let force_serial = self.nll_test_force_serial;
887 #[cfg(not(test))]
888 let force_serial = false;
889 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
890 }
891
892 fn automatic_gpu_prefix(&self) -> Option<usize> {
895 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
896 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
897 }
898}
899
900pub fn prefill_chunk() -> usize {
907 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
908 .ok()
909 .and_then(|v| v.parse::<usize>().ok())
910 {
911 return n.max(1);
912 }
913 if cfg!(target_os = "macos") {
914 512
915 } else if cfg!(target_arch = "aarch64") {
916 256
919 } else {
920 48
921 }
922}
923
924#[inline]
930fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
931 if end <= start || start >= input_len {
932 return 0;
933 }
934 let rows = (end.min(input_len) - start).min(input_len - start);
935 if end < input_len {
936 rows
937 } else {
938 rows.saturating_sub(1)
939 }
940}
941
942pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
944
945impl Pipeline {
946 fn clear_sequence_state(&mut self) {
954 self.kv_cache.clear();
955 self.kv_history.clear();
956 if let Some(b) = &mut self.dsv41 {
957 b.3.clear();
958 }
959 crate::gpu::graph_kv_reset(self.graph_kv_id);
960 crate::gpu::graph_kv_reset(self.mtp_kv_id());
965 }
966
967 fn finish_generation(
973 &mut self,
974 mtp: &mut Option<MtpModule>,
975 router: &mut Option<crate::swarm::DynRouter>,
976 clear_sequence: bool,
977 ) {
978 if router.is_some() {
982 let _ = self.set_active_skill(None);
983 }
984 if clear_sequence {
985 self.clear_sequence_state();
986 if let Some(m) = mtp.as_mut() {
987 m.kv.clear();
993 }
994 if let Some(m) = self.mtp.as_mut() {
995 m.kv.clear();
999 }
1000 }
1001 self.graph_want_logits = false;
1002 self.graph_head_required = false;
1003 self.graph_logits = None;
1004 self.graph_failed
1005 .store(false, std::sync::atomic::Ordering::Relaxed);
1006 self.cancel
1007 .store(false, std::sync::atomic::Ordering::Relaxed);
1008 self.dyn_router = router.take().or(self.dyn_router.take());
1009 self.mtp = mtp.take().or(self.mtp.take());
1010 self.mtp_graph_mode = None;
1011 self.spec_forced = None;
1012 }
1013
1014 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1018 if self
1019 .graph_failed
1020 .swap(false, std::sync::atomic::Ordering::Relaxed)
1021 {
1022 self.cancel
1023 .store(false, std::sync::atomic::Ordering::Relaxed);
1024 self.clear_sequence_state();
1025 self.graph_logits = None;
1026 self.graph_want_logits = false;
1027 self.graph_head_required = false;
1028 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1029 }
1030 Ok(())
1031 }
1032
1033 #[cfg(target_os = "macos")]
1034 fn fail_metal_graph(&mut self, reason: &str) {
1035 crate::pipeline::METAL_GRAPH_ERRORS
1036 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1037 self.clear_sequence_state();
1038 self.graph_logits = None;
1039 self.graph_failed
1040 .store(true, std::sync::atomic::Ordering::Relaxed);
1041 self.cancel
1042 .store(true, std::sync::atomic::Ordering::Relaxed);
1043 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1044 }
1045
1046 fn nll_begin(&mut self) -> Result<(), String> {
1051 if self
1052 .graph_failed
1053 .swap(false, std::sync::atomic::Ordering::Relaxed)
1054 {
1055 self.cancel
1056 .store(false, std::sync::atomic::Ordering::Relaxed);
1057 self.clear_sequence_state();
1058 self.graph_logits = None;
1059 self.graph_want_logits = false;
1060 self.graph_head_required = false;
1061 return Err("GPU graph failed before NLL scoring".to_string());
1062 }
1063 self.clear_sequence_state();
1064 self.graph_logits = None;
1065 self.graph_want_logits = false;
1066 self.graph_head_required = false;
1067 Ok(())
1068 }
1069
1070 fn nll_end(&mut self) {
1074 self.clear_sequence_state();
1075 self.graph_logits = None;
1076 self.graph_want_logits = false;
1077 self.graph_head_required = false;
1078 self.graph_failed
1079 .store(false, std::sync::atomic::Ordering::Relaxed);
1080 }
1081
1082 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1085 #[cfg(test)]
1086 if self.nll_test_fail_at == Some(pos) {
1087 self.nll_test_fail_at = None;
1088 self.graph_failed
1089 .store(true, std::sync::atomic::Ordering::Relaxed);
1090 self.cancel
1091 .store(true, std::sync::atomic::Ordering::Relaxed);
1092 }
1093 if self
1094 .graph_failed
1095 .swap(false, std::sync::atomic::Ordering::Relaxed)
1096 {
1097 self.cancel
1098 .store(false, std::sync::atomic::Ordering::Relaxed);
1099 self.clear_sequence_state();
1100 self.graph_logits = None;
1101 self.graph_want_logits = false;
1102 return Err(format!(
1103 "GPU graph failed during NLL {phase} at position {pos}"
1104 ));
1105 }
1106 Ok(())
1107 }
1108
1109 #[inline]
1113 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1114 virtual_idx % self.physical_layers
1115 }
1116
1117 #[inline]
1120 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1121 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1122 }
1123
1124 #[allow(clippy::too_many_arguments)]
1126
1127 #[cfg(target_os = "macos")]
1146 fn graph_prefill_preferred(&self) -> bool {
1147 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1148 if !crate::gpu::enabled_here()
1149 || !graph_force
1150 || std::env::var("CMF_GPU_BLOCK")
1151 .map(|v| v == "0")
1152 .unwrap_or(false)
1153 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1156 {
1157 return false;
1158 }
1159 self.weights
1160 .layers
1161 .iter()
1162 .any(|lw| {
1163 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1164 })
1165 }
1166
1167 #[cfg(not(target_os = "macos"))]
1168 fn graph_prefill_preferred(&self) -> bool {
1169 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1177 if !graph_on || !crate::gpu::enabled_here() {
1178 return false;
1179 }
1180 if self.o1_active() {
1194 return false;
1195 }
1196 if self
1197 .weights
1198 .layers
1199 .iter()
1200 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1201 {
1202 return true;
1203 }
1204 self.weights
1215 .layers
1216 .iter()
1217 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1218 && self.automatic_gpu_prefix().is_none()
1219 }
1220
1221 #[cfg(target_os = "macos")]
1222 fn q1_graph_gpu(
1223 &mut self,
1224 start: usize,
1225 upto: Option<usize>,
1226 position: usize,
1227 h: &mut [f32],
1228 ) -> usize {
1229 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1231 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1232 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1234 || !graph_force
1235 || std::env::var("CMF_GPU_BLOCK")
1236 .map(|v| v == "0")
1237 .unwrap_or(false)
1238 {
1239 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1240 eprintln!(
1241 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1242 self.attn_softcap > 0.0,
1243 crate::gpu::enabled_here(),
1244 graph_force,
1245 );
1246 }
1247 if self.graph_head_required {
1248 self.fail_metal_graph("native graph front gate refused");
1249 }
1250 return start;
1251 }
1252 if self.swa.is_some()
1256 || self.global_attn.is_some()
1257 || self.attention_heads_per_layer.is_some()
1258 || self.attn_v_norm
1259 || self.weights.layers.iter().any(|lw| {
1260 lw.attn_out_norm.is_some()
1261 || lw.ffn_out_norm.is_some()
1262 || lw.layer_scale.is_some()
1263 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1264 })
1265 {
1266 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1267 eprintln!(
1268 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1269 self.swa.is_some(),
1270 self.global_attn.is_some(),
1271 self.attention_heads_per_layer.is_some(),
1272 self.attn_v_norm,
1273 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1274 );
1275 }
1276 if self.graph_head_required {
1277 self.fail_metal_graph("native graph architecture gate refused");
1278 }
1279 return start;
1280 }
1281 let limit = upto
1284 .map(|u| u + 1)
1285 .unwrap_or(self.num_layers)
1286 .min(self.num_layers);
1287
1288 enum Item<'a> {
1289 Gdn {
1290 run: Vec<GdnGpuLayer<'a>>,
1291 first: usize,
1292 },
1293 Attn {
1294 l: AttnGpuLayer<'a>,
1295 li: usize,
1296 q_norm: Option<&'a [f32]>,
1297 k_norm: Option<&'a [f32]>,
1298 output_gate: bool,
1299 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1300 full_gpu: bool,
1303 },
1304 }
1305
1306 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1313 let attend_contract = attend_mode != "0"
1314 && attend_mode != "off"
1315 && self.head_dim % 4 == 0
1316 && self.head_dim <= 256
1317 && self.rotary_dim >= 2
1318 && self.rotary_dim <= self.head_dim
1319 && (self.rotary_dim / 2) % 32 == 0
1320 && self.num_kv_heads > 0
1321 && self.num_heads % self.num_kv_heads == 0;
1322
1323 let mut plan: Vec<Item> = Vec::new();
1324 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1325 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1327 let mut scan = start;
1328 while scan < limit {
1329 let lw = &self.weights.layers[self.phys_layer(scan)];
1330 let ffn = match &lw.ffn {
1331 FfnKind::Dense(d) if d.segs.is_empty() => {
1332 let (Some(g), Some(u), Some(dn)) = (
1333 d.gate_proj.metal_graph_parts(),
1334 d.up_proj.metal_graph_parts(),
1335 d.down_proj.metal_graph_parts(),
1336 ) else {
1337 if block_diag {
1338 eprintln!(
1339 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1340 );
1341 }
1342 break;
1343 };
1344 MetalFfn::Dense {
1345 gate: g,
1346 up: u,
1347 down: dn,
1348 }
1349 }
1350 FfnKind::Moe(m) => {
1351 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1352 if block_diag {
1353 eprintln!(
1354 "block-graph: L{scan} MoE outside the graph contract — run ends"
1355 );
1356 }
1357 break;
1358 };
1359 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1360 model_ref.get_or_insert_with(|| model.clone());
1361 }
1362 MetalFfn::Moe(moe)
1363 }
1364 _ => {
1365 if block_diag {
1366 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1367 }
1368 break;
1369 }
1370 };
1371 match &lw.attn {
1372 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1373 let parts = (
1374 w.in_proj_qkv.metal_graph_parts(),
1375 w.in_proj_z.metal_graph_parts(),
1376 w.in_proj_a.f32_parts(),
1377 w.in_proj_b.f32_parts(),
1378 w.out_proj.metal_graph_parts(),
1379 );
1380 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1381 if block_diag {
1382 eprintln!(
1383 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1384 w.in_proj_qkv.metal_graph_parts().is_some(),
1385 w.in_proj_z.metal_graph_parts().is_some(),
1386 w.in_proj_a.f32_parts().is_some(),
1387 w.in_proj_b.f32_parts().is_some(),
1388 w.out_proj.metal_graph_parts().is_some(),
1389 );
1390 }
1391 break;
1392 };
1393 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1394 model_ref.get_or_insert_with(|| model.clone());
1395 }
1396 let gl = GdnGpuLayer {
1397 attn_norm: &lw.input_norm,
1398 post_norm: &lw.post_norm,
1399 qkv,
1400 z,
1401 a,
1402 b,
1403 out,
1404 ffn,
1405 conv1d: &w.conv1d,
1406 a_log: &w.a_log,
1407 dt_bias: &w.dt_bias,
1408 gnorm: &w.norm,
1409 };
1410 match plan.last_mut() {
1411 Some(Item::Gdn { run, .. }) => run.push(gl),
1412 _ => plan.push(Item::Gdn {
1413 run: vec![gl],
1414 first: scan,
1415 }),
1416 }
1417 }
1418 AttnKind::Full {
1419 wq,
1420 wk,
1421 wv,
1422 wo,
1423 q_norm,
1424 k_norm,
1425 output_gate,
1426 softplus_gate: None,
1427 bias,
1428 } if !self.kv_cache.layers[scan].o1_sealed()
1429 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1434 {
1435 let parts = (
1436 wq.metal_graph_parts(),
1437 wk.metal_graph_parts(),
1438 wv.metal_graph_parts(),
1439 wo.metal_graph_parts(),
1440 );
1441 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1442 break;
1443 };
1444 if let QTensor::Mapped { model, .. } = wq {
1445 model_ref.get_or_insert_with(|| model.clone());
1446 }
1447 let cache = &self.kv_cache.layers[scan];
1448 let o1_metal = cache.o1.is_some()
1452 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1453 && cache.o1_views().is_some();
1454 let full_gpu = attend_contract
1455 && cache.mode == crate::kv_cache::KvMode::F32
1456 && (cache.o1.is_none() || o1_metal)
1457 && bias.is_none()
1458 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1459 && pk.1 == self.num_kv_heads * self.head_dim
1460 && pv.1 == self.num_kv_heads * self.head_dim
1461 && po.2 == self.num_heads * self.head_dim;
1462 plan.push(Item::Attn {
1463 l: AttnGpuLayer {
1464 attn_norm: &lw.input_norm,
1465 post_norm: &lw.post_norm,
1466 wq: pq,
1467 wk: pk,
1468 wv: pv,
1469 wo: po,
1470 ffn,
1471 },
1472 li: scan,
1473 q_norm: q_norm.as_deref(),
1474 k_norm: k_norm.as_deref(),
1475 output_gate: *output_gate,
1476 bias: bias
1477 .as_ref()
1478 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1479 full_gpu,
1480 });
1481 }
1482 _ => break,
1483 }
1484 scan += 1;
1485 }
1486 let Some(model) = model_ref else {
1487 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1488 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1489 }
1490 if self.graph_head_required {
1491 self.fail_metal_graph("native graph has no mapped model reference");
1492 }
1493 return start;
1494 };
1495 if plan.is_empty() {
1496 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1497 eprintln!("q1-graph: empty plan at layer {start}");
1498 }
1499 if self.graph_head_required {
1500 self.fail_metal_graph("native graph plan is empty");
1501 }
1502 return start;
1503 }
1504 let has_moe = plan.iter().any(|it| match it {
1505 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1506 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1507 });
1508 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1509 let dev_attend = attend_contract
1510 && (self.head_dim <= 128
1511 || has_moe
1512 || (self.head_dim <= 256 && has_gdn)
1518 || attend_mode == "force"
1519 || attend_mode == "256");
1520 if !dev_attend {
1521 for it in &mut plan {
1522 if let Item::Attn { li, full_gpu, .. } = it {
1523 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1526 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1527 if !keep_o1 {
1528 *full_gpu = false;
1529 }
1530 }
1531 }
1532 }
1533 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1534 use std::sync::atomic::{AtomicBool, Ordering};
1535 static SAID: AtomicBool = AtomicBool::new(false);
1536 if !SAID.swap(true, Ordering::Relaxed) {
1537 let fg = plan
1538 .iter()
1539 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1540 .count();
1541 let att = plan
1542 .iter()
1543 .filter(|it| matches!(it, Item::Attn { .. }))
1544 .count();
1545 eprintln!(
1546 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1547 plan.len(),
1548 self.head_dim,
1549 self.rotary_dim,
1550 self.num_kv_heads,
1551 self.num_heads,
1552 );
1553 }
1554 }
1555 let dims = GraphDims {
1556 hidden: self.hidden_size,
1557 eps: self.rms_eps as f32,
1558 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1559 };
1560 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1561 if self.graph_head_required {
1562 self.fail_metal_graph("native TokenGraph allocation refused");
1563 }
1564 return start;
1565 };
1566 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1567 nv: cfg.num_v_heads,
1568 nk: cfg.num_k_heads,
1569 dk: cfg.key_head_dim,
1570 dv: cfg.value_head_dim,
1571 kk: cfg.conv_kernel,
1572 hidden: self.hidden_size,
1573 inter: self.intermediate_size,
1574 c_dim: cfg.conv_dim(),
1575 eps: cfg.rms_eps as f32,
1576 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1577 });
1578 let mut valid = 0usize;
1582 let mut end = start;
1583 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1585 static ONCE: std::sync::Once = std::sync::Once::new();
1586 ONCE.call_once(|| {
1587 for it in &plan {
1588 match it {
1589 Item::Gdn { first, run } => {
1590 eprintln!("plan: Gdn first={first} len={}", run.len())
1591 }
1592 Item::Attn { li, full_gpu, .. } => {
1593 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1594 }
1595 }
1596 }
1597 });
1598 }
1599 for item in &plan {
1600 let ok = match item {
1601 Item::Gdn { run, .. } => gcfg
1602 .as_ref()
1603 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1604 .unwrap_or(false),
1605 Item::Attn { l, .. } => graph.attn_ok(l),
1606 };
1607 if !ok {
1608 if block_diag {
1609 eprintln!(
1610 "block-graph: plan item {} ({}) failed graph preflight",
1611 valid,
1612 match item {
1613 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1614 Item::Attn { li, .. } => format!("Attn L{li}"),
1615 }
1616 );
1617 }
1618 break;
1619 }
1620 valid += 1;
1621 end += match item {
1622 Item::Gdn { run, .. } => run.len(),
1623 Item::Attn { .. } => 1,
1624 };
1625 }
1626 plan.truncate(valid);
1627 if plan.is_empty() {
1628 if self.graph_head_required {
1629 self.fail_metal_graph("native graph preflight produced no valid items");
1630 }
1631 return start;
1632 }
1633
1634 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1635 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1636 return start;
1637 }
1638
1639 let inv_freq = self.inv_freq.clone();
1640 let pool = self.pool.clone();
1641 let (nh, nkv, hd, hs, rd, eps) = (
1642 self.num_heads,
1643 self.num_kv_heads,
1644 self.head_dim,
1645 self.hidden_size,
1646 self.rotary_dim,
1647 self.rms_eps,
1648 );
1649 let norm_style = self.norm_style;
1650 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1651 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1652 let kv_id = self.graph_kv_id;
1653 let mut pending: Vec<(usize, usize)> = Vec::new();
1656 let mut dev_attn: Vec<usize> = Vec::new();
1659 for item in &plan {
1660 let _xt0 = std::time::Instant::now();
1661 let _xkind: u32 = match item {
1662 Item::Gdn { .. } => 2,
1663 Item::Attn { .. } => 3,
1664 };
1665 if self.loop_final_norm {
1667 let item_start = match item {
1668 Item::Gdn { first, .. } => *first,
1669 Item::Attn { li, .. } => *li,
1670 };
1671 if item_start > start && self.is_loop_end(item_start - 1) {
1672 graph.encode_loop_norm(&self.weights.final_norm);
1673 }
1674 }
1675 match item {
1676 Item::Gdn { run, first } => {
1677 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1678 if l.linear_state.len() != want {
1679 l.linear_state = vec![0f32; want];
1680 }
1681 }
1682 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1683 .iter()
1684 .map(|l| l.linear_state.as_slice())
1685 .collect();
1686 let _ig = std::time::Instant::now();
1687 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1688 tracing::error!("q1 graph: GDN run refused after validation");
1690 return start;
1691 }
1692 graph.commit_kind = 2;
1695 graph.commit();
1696 crate::gpu::stageprof(0, _ig.elapsed());
1697 pending.push((*first, run.len()));
1698 }
1699 Item::Attn {
1700 l,
1701 li,
1702 q_norm,
1703 k_norm,
1704 output_gate,
1705 bias,
1706 full_gpu,
1707 } => {
1708 let _ia = std::time::Instant::now();
1709 if *full_gpu {
1711 let cache = &self.kv_cache.layers[*li];
1712 let o1p = if cache.o1.is_some() {
1713 match cache.o1_views() {
1714 Some(views) => Some(crate::gpu::O1AttnParams {
1715 views,
1716 epoch: self.o1_epoch,
1717 }),
1718 None => None,
1720 }
1721 } else {
1722 None
1723 };
1724 let o1_layer = cache.o1.is_some();
1725 if o1_layer && o1p.is_none() {
1726 }
1728 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1729 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1730 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1731 let p = crate::gpu::AttnDeviceParams {
1732 kv_id,
1733 layer: *li,
1734 nh,
1735 nkv,
1736 hd,
1737 rd,
1738 position,
1739 scale: self.attn_scale,
1740 eps: eps as f32,
1741 gemma,
1742 late_qk_norm: self.qk_norm_after_rope,
1743 output_gate: *output_gate,
1744 q_norm: *q_norm,
1745 k_norm: *k_norm,
1746 inv_freq: &inv_freq,
1747 cpu_k,
1748 cpu_v,
1749 cpu_stored,
1750 o1: o1p,
1751 };
1752 let o1_bad = o1_layer && p.o1.is_none();
1753 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1754 {
1755 if p.o1.is_none() {
1757 dev_attn.push(*li);
1758 }
1759 graph.commit_kind = 3;
1760 graph.commit();
1761 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1765 continue;
1766 }
1767 }
1769 graph.encode_attn_prefix(l);
1770 if let Err(err) = graph.sync_checked() {
1771 self.fail_metal_graph(&err);
1772 return start;
1773 }
1774 if !pending.is_empty() {
1775 let idxs: Vec<usize> =
1776 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1777 let mut outs: Vec<&mut [f32]> = self
1778 .kv_cache
1779 .layers
1780 .iter_mut()
1781 .enumerate()
1782 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1783 .map(|(_, s)| s.linear_state.as_mut_slice())
1784 .collect();
1785 graph.read_states(&mut outs);
1786 }
1787 let mut q_raw = attention::take_buf(l.wq.1);
1788 let mut k = attention::take_buf(l.wk.1);
1789 let mut v = attention::take_buf(l.wv.1);
1790 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1791 let cfg = QwenAttnCfg {
1792 num_heads: nh,
1793 num_kv_heads: nkv,
1794 head_dim: hd,
1795 hidden_size: hs,
1796 position,
1797 inv_freq: &inv_freq,
1798 rotary_dim: rd,
1799 scale: self.attn_scale,
1800 softcap: self.attn_softcap,
1801 window: None,
1802 v_norm: false,
1803 qk_norm_after_rope: self.qk_norm_after_rope,
1804 q_norm: *q_norm,
1805 k_norm: *k_norm,
1806 output_gate: *output_gate,
1807 softplus_gate: None,
1808 rope_scale: 1.0,
1809 bias: *bias,
1810 rms_eps: eps,
1811 norm_style,
1812 pool: pool.as_deref(),
1813 };
1814 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1817 || std::env::var("CMF_ATTN_DUMP").is_ok();
1818 let _ = full_gpu;
1819 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1820 let mut ao = attention::qwen_attention_core(
1821 q_raw,
1822 k,
1823 v,
1824 &mut self.kv_cache.layers[*li],
1825 &cfg,
1826 );
1827 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1831 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1832 let (cq, _cg, _ck, _cv) =
1833 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1834 let cache = &self.kv_cache.layers[*li];
1835 let n = cache.head_keys(0).len() / hd;
1836 let mut bytes: Vec<u8> = Vec::new();
1837 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1838 bytes.extend_from_slice(&v.to_le_bytes());
1839 }
1840 for v in &cq {
1841 bytes.extend_from_slice(&v.to_le_bytes());
1842 }
1843 for g in 0..nkv {
1844 for v in cache.head_keys(g) {
1845 bytes.extend_from_slice(&v.to_le_bytes());
1846 }
1847 }
1848 for g in 0..nkv {
1849 for v in cache.head_values(g) {
1850 bytes.extend_from_slice(&v.to_le_bytes());
1851 }
1852 }
1853 let _ =
1854 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1855 }
1856 }
1857 if let Some((qr0, k0, v0)) =
1858 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1859 {
1860 let (cq, _cg, ck, cv) =
1861 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1862 let mut h_now = vec![0f32; hs];
1863 graph.read_h(&mut h_now);
1864 let cache = &self.kv_cache.layers[*li];
1865 let n_after = cache.head_keys(0).len() / hd;
1866 let stored = n_after.saturating_sub(1);
1870 let cpu_k: Vec<&[f32]> = (0..nkv)
1871 .map(|g| &cache.head_keys(g)[..stored * hd])
1872 .collect();
1873 let cpu_v: Vec<&[f32]> = (0..nkv)
1874 .map(|g| &cache.head_values(g)[..stored * hd])
1875 .collect();
1876 let p = crate::gpu::AttnDeviceParams {
1877 kv_id,
1878 layer: *li,
1879 nh,
1880 nkv,
1881 hd,
1882 rd,
1883 position,
1884 scale: self.attn_scale,
1885 eps: eps as f32,
1886 gemma,
1887 late_qk_norm: self.qk_norm_after_rope,
1888 output_gate: *output_gate,
1889 q_norm: *q_norm,
1890 k_norm: *k_norm,
1891 inv_freq: &inv_freq,
1892 cpu_k,
1893 cpu_v,
1894 cpu_stored: stored,
1895 o1: None,
1896 };
1897 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1898 let md = |a: &[f32], b: &[f32]| {
1899 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1900 };
1901 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1902 eprintln!(
1903 "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}",
1904 nn(&cq),
1905 md(&cq, &dq),
1906 nn(&ck),
1907 md(&ck, &dk),
1908 nn(&cv),
1909 md(&cv, &dv),
1910 nn(&ao),
1911 md(&ao, &dao)
1912 );
1913 } else {
1914 eprintln!("attn-oracle L{li}: device probe declined");
1915 }
1916 }
1917 graph.encode_attn_suffix(l, &ao);
1918 graph.commit();
1921 attention::recycle_buf(&mut ao);
1922 }
1923 }
1924
1925 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1926 }
1927 let mut lm_rows = None;
1932 if self.graph_want_logits
1933 && upto.is_none()
1934 && end == self.num_layers
1935 && std::env::var("CMF_GPU_LMHEAD")
1936 .map(|v| v != "0")
1937 .unwrap_or(true)
1938 {
1939 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
1940 if graph.lm_head_ok(lm) {
1941 graph.encode_lm_head(&self.weights.final_norm, lm);
1942 lm_rows = Some(lm.1);
1943 }
1944 }
1945 }
1946 if self.graph_head_required && lm_rows.is_none() {
1947 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1948 self.fail_metal_graph("fused graph head was requested but not encodable");
1949 return start;
1950 }
1951 let _sy0 = std::time::Instant::now();
1952 if let Err(err) = graph.sync_checked() {
1953 self.fail_metal_graph(&err);
1954 return start;
1955 }
1956 let _rs0 = std::time::Instant::now();
1957 if !pending.is_empty() {
1958 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1959 let mut outs: Vec<&mut [f32]> = self
1960 .kv_cache
1961 .layers
1962 .iter_mut()
1963 .enumerate()
1964 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1965 .map(|(_, s)| s.linear_state.as_mut_slice())
1966 .collect();
1967 graph.read_states(&mut outs);
1968 }
1969 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1970 use std::sync::atomic::{AtomicU64, Ordering};
1971 static SY: AtomicU64 = AtomicU64::new(0);
1972 static RS: AtomicU64 = AtomicU64::new(0);
1973 static N: AtomicU64 = AtomicU64::new(0);
1974 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1975 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1976 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1977 if n % 100 == 0 {
1978 eprintln!(
1979 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1980 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1981 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1982 );
1983 }
1984 }
1985 if let Some(rows) = lm_rows {
1986 crate::gpu::hostprof_encode_done(_mt0);
1987 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1988 graph.read_logits(&mut lg);
1989 crate::gpu::hostprof_total(_mt0);
1990 lg.resize(self.vocab_size, 0.0);
1991 if let Some(c) = self.final_softcap {
1992 for l in lg.iter_mut() {
1993 *l = c * (*l / c).tanh();
1994 }
1995 }
1996 self.graph_logits = Some(lg);
1997 }
1998 graph.read_h(h);
1999 if self.graph_head_required && self.graph_logits.is_none() {
2000 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2001 self.fail_metal_graph("fused graph head completed without logits readback");
2002 return start;
2003 }
2004 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2005 METAL_GRAPH_LAYERS.fetch_add(
2006 end.saturating_sub(start) as u64,
2007 std::sync::atomic::Ordering::Relaxed,
2008 );
2009 if self.graph_head_required {
2010 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2011 }
2012 for li in dev_attn {
2016 let mut krow = attention::take_buf(nkv * hd);
2017 let mut vrow = attention::take_buf(nkv * hd);
2018 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
2019 let cache = &mut self.kv_cache.layers[li];
2020 cache.append(&krow, &vrow, &[]);
2021 let n = cache.seq_len;
2022 let mut imp = attention::take_buf(n);
2023 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2024 cache.accumulate_imp(&imp);
2025 attention::recycle_buf(&mut imp);
2026 }
2027 attention::recycle_buf(&mut krow);
2028 attention::recycle_buf(&mut vrow);
2029 }
2030 end
2031 }
2032
2033 pub fn new(
2034 tokenizer: Tokenizer,
2035 weights: PipelineWeights,
2036 hidden_size: usize,
2037 intermediate_size: usize,
2038 num_heads: usize,
2039 num_kv_heads: usize,
2040 head_dim: usize,
2041 num_layers: usize,
2042 physical_layers: usize,
2043 loop_final_norm: bool,
2044 vocab_size: usize,
2045 rms_eps: f64,
2046 rope_base: f32,
2047 norm_style: NormStyle,
2048 max_seq_len: usize,
2049 sampler_config: SamplerConfig,
2050 ) -> Self {
2051 let rng = match sampler_config.seed {
2052 Some(s) => SplitMix64::new(s),
2053 None => SplitMix64::from_entropy(),
2054 };
2055 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2056 let pool = Pool::from_env();
2057 if let Some(p) = &pool {
2058 tracing::info!("worker pool: {} threads", p.n_workers());
2059 }
2060 Self {
2061 gpu_plan: None,
2062 tokenizer: std::sync::Arc::new(tokenizer),
2063 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2064 sampler_config,
2065 weights,
2066 hidden_size,
2067 intermediate_size,
2068 num_heads,
2069 num_kv_heads,
2070 head_dim,
2071 num_layers,
2072 physical_layers,
2073 loop_final_norm,
2074 vocab_size,
2075 rms_eps,
2076 rope_base,
2077 norm_style,
2078 rotary_dim: head_dim,
2079 attention_heads_per_layer: None,
2080 vmf_cfg: None,
2081 gdn_cfg: None,
2082 kda_cfg: None,
2083 g3n: None,
2084 dsv4: None,
2085 dsv41: None,
2086 dsv41_vision: None,
2087 dsv41_prefill: None,
2088 qwen4_exp: None,
2089 dsv4_mtp: Vec::new(),
2090 dspark: None,
2091 dspark_pending: Vec::new(),
2092 dspark_hist: Vec::new(),
2093 dspark_real: Vec::new(),
2094 dspark_trunk_picks: Vec::new(),
2095 dspark_exp: Vec::new(),
2096 dspark_draft_ns: 0,
2097 logit_multiplier: None,
2098 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2099 graph_failed: std::sync::atomic::AtomicBool::new(false),
2100 kv_history: Vec::new(),
2101 short_conv_cfg: None,
2102 mtp: None,
2103 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2104 ignore_eos: false,
2105 draft_full_streak: 0,
2106 spec_k_adapt: None,
2107 spec_acc_ewma: 0.7,
2108 rng,
2109 sampler_scratch: SamplerScratch::default(),
2110 spec_forced: None,
2111 spec_q: Vec::new(),
2112 spec_p: Vec::new(),
2113 spec_res: Vec::new(),
2114 spec_qs: Vec::new(),
2115 spec_ps: Vec::new(),
2116 spec_ress: Vec::new(),
2117 mtp_graph_mode: None,
2118 #[cfg(target_os = "macos")]
2119 metal_verify: None,
2120 inv_freq,
2121 ws: ForwardScratch::new(hidden_size),
2122 pool,
2123 model: None,
2124 dyn_force_f32: false,
2125 dyn_skill_layers: Vec::new(),
2126 dyn_active: None,
2127 dyn_blend_loaded: false,
2128 dyn_phi_layer: None,
2129 dyn_phi_ema: Vec::new(),
2130 dyn_phi_seen: 0,
2131 dyn_router: None,
2132 o1_cfg: None,
2133 o1_epoch: 0,
2134 o1_flags: Vec::new(),
2135 trace: false,
2136 calib_temp: 1.0,
2137 confidence_on: true,
2138 embed_multiplier: 1.0,
2139 attn_scale: 1.0 / (head_dim as f32).sqrt(),
2140 swa: None,
2141 sliding_layers: None,
2142 inv_freq_local: None,
2143 rotary_dim_local: None,
2144 rope_scale: 1.0,
2145 rope_scale_local: 1.0,
2146 global_attn: None,
2147 inv_freq_global: None,
2148 attn_v_norm: false,
2149 qk_norm_after_rope: false,
2150 final_softcap: None,
2151 head_clusters: None,
2152 attn_softcap: 0.0,
2153 graph_want_logits: false,
2154 graph_head_required: false,
2155 graph_logits: None,
2156 graph_kv_id: {
2157 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2158 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2159 },
2160 #[cfg(test)]
2161 nll_test_fail_at: None,
2162 #[cfg(test)]
2163 nll_test_force_serial: false,
2164 }
2165 }
2166
2167 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2175 if let Some(c) = &cfg {
2176 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2177 tracing::error!(
2178 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2179 c.w,
2180 c.sink
2181 );
2182 self.o1_flags.clear();
2183 self.o1_cfg = None;
2184 return;
2185 }
2186 }
2187 self.o1_flags = match &cfg {
2188 Some(c) => {
2189 let mut flags = c.layer_flags(self.num_layers);
2190 for (li, f) in flags.iter_mut().enumerate() {
2191 if *f
2192 && !matches!(
2193 self.weights.layers[self.phys_layer(li)].attn,
2194 AttnKind::Full { .. }
2195 )
2196 {
2197 *f = false;
2198 }
2199 }
2200 flags
2201 }
2202 None => Vec::new(),
2203 };
2204 if let Some(c) = &cfg {
2205 let n = self.o1_flags.iter().filter(|&&f| f).count();
2206 tracing::info!(
2207 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2208 self.num_layers,
2209 c.m,
2210 c.w,
2211 c.sink,
2212 c.rect
2213 );
2214 }
2215 self.o1_cfg = cfg;
2216 }
2217
2218 pub fn o1_active(&self) -> bool {
2220 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2221 }
2222
2223 pub fn generation_batch_k(&self) -> usize {
2235 if let Some(k) = std::env::var("CMF_BATCH_K")
2236 .ok()
2237 .and_then(|v| v.parse::<usize>().ok())
2238 {
2239 return k;
2240 }
2241 #[cfg(not(target_os = "macos"))]
2242 if self.graph_prefill_preferred() && !self.o1_active() {
2243 return 32;
2244 }
2245 0
2246 }
2247
2248 pub fn generation_graph_prefill(&self) -> bool {
2249 let graph = self.graph_prefill_preferred();
2250 #[cfg(not(target_os = "macos"))]
2261 if graph
2262 && self.generation_batch_k() > 0
2263 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2264 {
2265 return false;
2266 }
2267 graph
2268 }
2269
2270 pub fn o1_device_stats(&self) -> (usize, u64) {
2275 crate::gpu::o1_device_stats(self.graph_kv_id)
2276 }
2277
2278 pub fn o1_begin(&mut self) {
2283 self.o1_begin_with_prefix(None);
2284 }
2285
2286 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2290 if let Some(c) = &self.o1_cfg {
2291 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2292 let boundary = requested_prefix.map(|p| {
2293 p.max(
2294 crate::nystrom::o1_deferred_boundary(w, sink)
2295 .expect("o1 config boundary validated in set_o1"),
2296 )
2297 });
2298 for (li, &f) in self.o1_flags.iter().enumerate() {
2299 if f {
2300 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2301 }
2302 }
2303 }
2304 }
2305
2306 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2308 self.o1_cfg.as_ref().and_then(|c| {
2309 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2310 .map(|floor| requested_prefix.max(floor))
2311 })
2312 }
2313
2314 fn o1_note_transition(&mut self) {
2315 let mut transitioned = false;
2319 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2320 if flagged {
2321 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2322 }
2323 }
2324 if transitioned {
2325 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2326 }
2327 }
2328
2329 fn o1_pending(&self) -> bool {
2330 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2331 f && self.kv_cache.layers[li].seq_len > 0
2332 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2333 })
2334 }
2335
2336 fn o1_fail(&mut self, err: String) {
2337 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2338 self.clear_sequence_state();
2339 self.graph_failed
2340 .store(true, std::sync::atomic::Ordering::Relaxed);
2341 self.cancel
2342 .store(true, std::sync::atomic::Ordering::Relaxed);
2343 }
2344
2345 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2350 if self.o1_cfg.is_none() {
2351 return Ok(false);
2352 }
2353 let mut participating = false;
2354 for li in 0..self.num_layers {
2355 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2356 continue;
2357 }
2358 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2359 return Err(err);
2360 }
2361 if self.kv_cache.layers[li].seq_len == 0 {
2362 continue;
2363 }
2364 participating = true;
2365 let num_heads = self.layer_num_heads(li);
2366 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2367 }
2368 self.o1_note_transition();
2369 for li in 0..self.num_layers {
2370 if self.o1_flags.get(li).copied().unwrap_or(false) {
2371 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2372 return Err(err);
2373 }
2374 }
2375 }
2376 Ok(participating
2377 && (0..self.num_layers).all(|li| {
2378 !self.o1_flags.get(li).copied().unwrap_or(false)
2379 || self.kv_cache.layers[li].seq_len == 0
2380 || self.kv_cache.layers[li].o1_sealed()
2381 }))
2382 }
2383
2384 fn o1_progress(&mut self) {
2387 if !self.o1_active() {
2388 return;
2389 }
2390 for li in 0..self.num_layers {
2391 if self.o1_flags.get(li).copied().unwrap_or(false) {
2392 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2393 self.o1_fail(err);
2394 return;
2395 }
2396 }
2397 }
2398 self.o1_note_transition();
2402 if !self.o1_pending() {
2403 return;
2404 }
2405 if let Err(err) = self.o1_seal_checked() {
2406 self.o1_fail(err);
2407 }
2408 }
2409
2410 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2415 if self
2416 .graph_failed
2417 .swap(false, std::sync::atomic::Ordering::Relaxed)
2418 {
2419 self.cancel
2420 .store(false, std::sync::atomic::Ordering::Relaxed);
2421 self.clear_sequence_state();
2422 return Err(format!("{phase}: deferred O(1) transition failed"));
2423 }
2424 Ok(())
2425 }
2426
2427 pub fn o1_seal(&mut self) {
2431 if let Err(err) = self.o1_seal_checked() {
2432 self.o1_fail(err);
2433 }
2434 }
2435
2436 pub fn set_trace(&mut self, on: bool) {
2438 self.trace = on;
2439 }
2440
2441 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2444 self.rng = match config.seed {
2445 Some(seed) => SplitMix64::new(seed),
2446 None => SplitMix64::from_entropy(),
2447 };
2448 self.sampler_config = config;
2449 }
2450
2451 pub fn set_confidence(&mut self, on: bool) {
2456 self.confidence_on = on;
2457 }
2458
2459 pub fn set_calib_temp(&mut self, t: f32) {
2462 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2463 }
2464
2465 pub fn calib_temp(&self) -> f32 {
2467 self.calib_temp
2468 }
2469
2470 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2473 self.rotary_dim = rotary_dim.min(self.head_dim);
2474 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2475 }
2476
2477 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2478 QwenAttnCfg {
2479 num_heads: self.num_heads,
2480 num_kv_heads: self.num_kv_heads,
2481 head_dim: self.head_dim,
2482 hidden_size: self.hidden_size,
2483 position,
2484 inv_freq: &self.inv_freq,
2485 rotary_dim: self.rotary_dim,
2486 scale: self.attn_scale,
2487 softcap: self.attn_softcap,
2488 window: None,
2489 v_norm: false,
2490 qk_norm_after_rope: self.qk_norm_after_rope,
2491 q_norm: None,
2492 k_norm: None,
2493 output_gate: false,
2494 softplus_gate: None,
2495 rope_scale: self.rope_scale,
2496 bias: None,
2497 rms_eps: self.rms_eps,
2498 norm_style: self.norm_style,
2499 pool: self.pool.as_deref(),
2500 }
2501 }
2502
2503 pub fn generate(
2505 &mut self,
2506 prompt: &str,
2507 max_tokens: usize,
2508 task_mask: Option<&TaskMask>,
2509 on_token: Option<TokenCallback>,
2510 ) -> Result<GenerateResult, String> {
2511 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2512 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2513 }
2514
2515 pub fn generate_from_vl(
2518 &mut self,
2519 input: &crate::dsv41_vision::PreparedVlInputs,
2520 max_tokens: usize,
2521 task_mask: Option<&TaskMask>,
2522 on_token: Option<TokenCallback>,
2523 ) -> Result<GenerateResult, String> {
2524 let Some(dsv41) = &self.dsv41 else {
2525 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2526 };
2527 if input.token_ids.is_empty() {
2528 return Err("empty V4.1 multimodal prompt".into());
2529 }
2530 if input.token_types.len() != input.token_ids.len() {
2531 return Err(format!(
2532 "V4.1 token type count {} != token count {}",
2533 input.token_types.len(),
2534 input.token_ids.len()
2535 ));
2536 }
2537 let dim = dsv41.2.dim;
2538 let mut embeddings = vec![None; input.token_ids.len()];
2539 let mut participates = vec![true; input.token_ids.len()];
2540 if !input.images.is_empty() {
2541 let vision = self
2542 .dsv41_vision
2543 .as_ref()
2544 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2545 for image in &input.images {
2546 let end = image.start.saturating_add(image.types.len());
2547 if end > input.token_ids.len() {
2548 return Err(format!(
2549 "V4.1 image span {}..{} exceeds prompt length {}",
2550 image.start,
2551 end,
2552 input.token_ids.len()
2553 ));
2554 }
2555 let mut span = vec![0.0f32; image.types.len() * dim];
2556 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2557 for (offset, &kind) in image.types.iter().enumerate() {
2558 let pos = image.start + offset;
2559 if input.token_types[pos] != kind {
2560 return Err(format!(
2561 "V4.1 image type mismatch at position {pos}: {} != {kind}",
2562 input.token_types[pos]
2563 ));
2564 }
2565 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2566 participates[pos] = false;
2567 }
2568 }
2569 }
2570 for (pos, &kind) in input.token_types.iter().enumerate() {
2571 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2572 return Err(format!("V4.1 text position {pos} has an image embedding"));
2573 }
2574 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2575 return Err(format!("V4.1 image position {pos} has no image embedding"));
2576 }
2577 }
2578 self.dsv41_prefill = Some((embeddings, participates));
2579 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2580 self.dsv41_prefill = None;
2581 result
2582 }
2583
2584 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2586 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2587 }
2588
2589 pub fn generate_from_ids(
2597 &mut self,
2598 input_ids: &[u32],
2599 max_tokens: usize,
2600 task_mask: Option<&TaskMask>,
2601 mut on_token: Option<TokenCallback>,
2602 ) -> Result<GenerateResult, String> {
2603 if std::env::var("CMF_TRACE_H").is_ok() {
2604 eprintln!("input_ids: {input_ids:?}");
2605 }
2606 if input_ids.is_empty() {
2607 return Err("empty prompt: nothing to generate from".to_string());
2608 }
2609 self.graph_failed
2613 .store(false, std::sync::atomic::Ordering::Relaxed);
2614 let task_mask = self.drop_open_mask(task_mask);
2619
2620 let reuse_from = {
2628 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2629 let h = &self.kv_history;
2630 if on
2631 && task_mask.is_none()
2632 && self.mtp.is_none()
2633 && self.o1_cfg.is_none()
2634 && self.dsv41.is_none()
2635 && !h.is_empty()
2636 && h.len() < input_ids.len()
2637 && input_ids[..h.len()] == h[..]
2638 {
2639 h.len()
2640 } else {
2641 0
2642 }
2643 };
2644 if reuse_from == 0 {
2645 self.clear_sequence_state();
2647 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2648 eprintln!(
2649 "kv-reuse: {} of {} prompt positions already cached",
2650 reuse_from,
2651 input_ids.len()
2652 );
2653 }
2654 crate::gpu::graph_race_begin_generation();
2655 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2659 std::env::var("CMF_O1_PREFILL")
2660 .ok()
2661 .and_then(|v| v.parse::<usize>().ok())
2662 .filter(|&p| p > 0)
2663 } else {
2664 None
2665 };
2666 if task_mask.is_none() {
2667 self.o1_begin_with_prefix(o1_prefill);
2668 }
2669
2670 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2676 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2703 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2704 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2720 for lw in &self.weights.layers {
2721 if let FfnKind::Dense(d) = &lw.ffn {
2722 dense_n += 1;
2723 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2724 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2725 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2726 {
2727 dense_q4tp += 1;
2728 }
2729 }
2730 }
2731 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2732 let penalized = self.sampler_config.repetition_penalty != 1.0
2736 || self.sampler_config.presence_penalty != 0.0
2737 || !self.sampler_config.suppress_tokens.is_empty();
2738 #[cfg(feature = "gpu")]
2743 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2744 #[cfg(not(feature = "gpu"))]
2745 let metal_wgpu = false;
2746 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2747 let spec_wanted = match spec_env.as_deref() {
2748 Some("0") => false,
2749 Some(_) => {
2750 if metal_wgpu {
2751 tracing::warn!(
2752 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2753 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2754 );
2755 }
2756 true
2757 }
2758 None => spec_default_ok && !penalized && !metal_wgpu,
2759 };
2760 #[cfg(target_os = "macos")]
2763 let metal_graph = crate::gpu::q1_force()
2764 && crate::gpu::enabled_here()
2765 && std::env::var("CMF_GPU_BLOCK")
2766 .map(|v| v != "0")
2767 .unwrap_or(true);
2768 #[cfg(not(target_os = "macos"))]
2769 let metal_graph = false;
2770 let graph_spec = self.speculative
2771 && (graph_on || metal_graph)
2772 && self.mtp.is_some()
2773 && task_mask.is_none()
2774 && !self.o1_active()
2775 && spec_sampling_ok
2776 && spec_wanted;
2777 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2784 let spec_active = self.speculative
2785 && self.mtp.is_some()
2786 && task_mask.is_none()
2787 && !self.o1_active()
2788 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2789 let mut mtp = if spec_active { self.mtp.take() } else { None };
2792 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2793 eprintln!(
2794 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2795 mtp.is_some(),
2796 self.speculative,
2797 self.sampler_config.temperature < 1e-6,
2798 );
2799 }
2800 if let Some(m) = &mut mtp {
2801 m.kv.clear();
2802 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2804 self.mtp_graph_mode = None;
2805 }
2806 let mut router = if mtp.is_none() {
2810 self.dyn_router.take()
2811 } else {
2812 None
2813 };
2814 if let Some(r) = &mut router {
2815 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2818 }
2819
2820 let mut all_ids = input_ids.to_vec();
2821 let mut generated = 0usize;
2822 let mut finish_reason = "max_tokens".to_string();
2823 let mut drafted = 0usize;
2824 let mut accepted = 0usize;
2825 let mut dsv4_spec_bad = 0usize;
2832 let mut dsv4_spec_retry_at = 0usize;
2833 let mut confidence: Vec<f32> = Vec::new();
2834 let trace_on = self.trace;
2835 let calib_temp = self.calib_temp;
2836 let mut traces: Vec<TokenTrace> = Vec::new();
2837
2838 let mut hidden = vec![0.0f32; self.hidden_size];
2844 let mut pos = reuse_from;
2845 let fuse_lm = mtp.is_none()
2854 && router.is_none()
2855 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2856 self.graph_logits = None;
2857 self.graph_want_logits = false;
2858 let _tpf = std::time::Instant::now();
2859 let batch_k = self.generation_batch_k();
2860 while self.qwen4_exp.is_some()
2871 && mtp.is_none()
2872 && pos < input_ids.len()
2873 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2874 {
2875 let token_id = input_ids[pos];
2876 let want_logits = pos + 1 == input_ids.len();
2877 let mut lg = Vec::new();
2878 if let Some(b) = &mut self.qwen4_exp {
2879 crate::qwen4_exp::forward_token(
2880 &b.0,
2881 &b.1,
2882 &b.2,
2883 &mut b.3,
2884 token_id,
2885 pos,
2886 &self.inv_freq,
2887 self.pool.as_deref(),
2888 &mut lg,
2889 want_logits,
2890 );
2891 }
2892 if want_logits {
2893 self.graph_logits = Some(lg);
2894 }
2895 pos += 1;
2896 hidden.fill(0.0);
2897 }
2898 while self.dsv4.is_some()
2899 && mtp.is_none()
2900 && pos < input_ids.len()
2901 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2902 {
2903 let end = (pos + prefill_chunk()).min(input_ids.len());
2904 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2905 let mut lg = Vec::new();
2906 if let Some(b) = &mut self.dsv4 {
2907 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2908 crate::dsv4::forward_chunk(
2909 g,
2910 layers,
2911 &cfg,
2912 st,
2913 &ids,
2914 pos,
2915 &self.inv_freq,
2916 self.pool.as_deref(),
2917 &mut lg,
2918 end == input_ids.len(),
2919 );
2920 }
2921 if end == input_ids.len() {
2922 self.graph_logits = Some(lg);
2923 }
2924 pos = end;
2925 hidden = vec![0.0; self.hidden_size];
2926 }
2927 let dsv41_prefill = self.dsv41_prefill.take();
2928 while self.dsv41.is_some()
2929 && mtp.is_none()
2930 && pos < input_ids.len()
2931 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2932 {
2933 let end = (pos + prefill_chunk()).min(input_ids.len());
2934 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2935 let mut lg = Vec::new();
2936 if let Some(b) = &mut self.dsv41 {
2937 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2938 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2939 crate::dsv41::forward_chunk_masked_with_embeddings(
2940 g,
2941 layers,
2942 cfg,
2943 st,
2944 &ids,
2945 pos,
2946 &embeddings[pos..end],
2947 &participates[pos..end],
2948 self.pool.as_deref(),
2949 &mut lg,
2950 );
2951 } else {
2952 crate::dsv41::forward_chunk(
2953 g,
2954 layers,
2955 cfg,
2956 st,
2957 &ids,
2958 pos,
2959 self.pool.as_deref(),
2960 &mut lg,
2961 );
2962 }
2963 }
2964 if end == input_ids.len() {
2965 self.graph_logits = Some(lg);
2966 }
2967 pos = end;
2968 hidden = vec![0.0; self.hidden_size];
2969 }
2970 let dyn_prefill = router.is_some();
2975 let o1_prefill_limit = o1_prefill
2983 .and_then(|requested| self.o1_effective_boundary(requested))
2984 .map(|boundary| boundary.min(input_ids.len()));
2985 let mut o1_sealed = false;
2986 if let Some(limit) = o1_prefill_limit {
2987 if self.can_prefill_batched() && limit > 2 {
2990 let chunk = prefill_chunk();
2991 let hs = self.hidden_size;
2992 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2993 let end = (pos + chunk).min(limit);
2994 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2995 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2996 pos = end;
2997 }
2998 } else {
2999 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3000 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
3001 pos += 1;
3002 }
3003 }
3004 if pos >= limit {
3005 o1_sealed = match self.o1_seal_checked() {
3006 Ok(sealed) => sealed,
3007 Err(err) => {
3008 self.finish_generation(&mut mtp, &mut router, true);
3009 return Err(err);
3010 }
3011 };
3012 tracing::info!(
3013 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
3014 o1_prefill.unwrap_or(0),
3015 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
3016 .unwrap_or(limit),
3017 limit,
3018 input_ids.len()
3019 );
3020 }
3021 }
3022 let graph_prefill = self.graph_prefill_preferred();
3028 #[cfg(target_os = "macos")]
3036 if task_mask.is_none()
3037 && !dyn_prefill
3038 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
3039 && crate::gpu::enabled_here()
3040 && self.gdn_cfg.is_some()
3041 && self.g3n.is_none()
3042 && input_ids.len() > 8
3043 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3044 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
3045 {
3046 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
3047 .ok()
3048 .and_then(|v| v.parse().ok())
3049 .filter(|&v| (16..=512).contains(&v))
3050 .unwrap_or(256);
3051 let hs = self.hidden_size;
3052 let _tp = std::time::Instant::now();
3053 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3054 let end = (pos + chunk).min(input_ids.len());
3055 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
3056 MetalPrefillOutcome::Completed(hb) => hb,
3057 MetalPrefillOutcome::Declined => break,
3058 MetalPrefillOutcome::Failed => {
3059 self.finish_generation(&mut mtp, &mut router, true);
3060 return Err("ordinary Metal prefill failed after admission".into());
3061 }
3062 };
3063 if let Some(m) = &mut mtp {
3064 let n_pairs = if end < input_ids.len() {
3065 end - pos
3066 } else {
3067 end - pos - 1
3068 };
3069 if n_pairs > 0 {
3070 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3071 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3072 .collect();
3073 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3074 for (j, (h, t)) in pairs.iter().enumerate() {
3075 let h = h.to_vec();
3076 let _ = self.mtp_step(m, &h, *t, pos + j);
3077 }
3078 }
3079 }
3080 }
3081 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3082 pos = end;
3083 }
3084 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3085 eprintln!(
3086 "metal-prefill: {} of {} tokens in {:.1} ms",
3087 pos,
3088 input_ids.len(),
3089 _tp.elapsed().as_secs_f64() * 1e3
3090 );
3091 }
3092 }
3093 if task_mask.is_none()
3094 && !dyn_prefill
3095 && !graph_prefill
3096 && self.can_prefill_batched()
3097 && self.g3n.is_none()
3098 && o1_prefill.is_none()
3099 && input_ids.len() > 2
3100 {
3101 let chunk = prefill_chunk();
3107 let hs = self.hidden_size;
3108 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3109 let end = (pos + chunk).min(input_ids.len());
3110 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3111 if let Some(m) = &mut mtp {
3112 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3113 .ok()
3114 .and_then(|v| v.parse().ok())
3115 .unwrap_or(0);
3116 for p in pos..end {
3117 if p + 1 < input_ids.len() {
3118 if probe >= 1 && p + 2 < input_ids.len() {
3119 let (d1, mut hx) = self.mtp_step_h(
3123 m,
3124 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3125 input_ids[p + 1],
3126 p,
3127 );
3128 let mut ok = d1 == input_ids[p + 2];
3129 Self::chain_probe_note(0, ok);
3130 let mut d_prev = d1;
3131 let mut extra = 0usize;
3132 for j in 1..probe {
3133 if p + 2 + j >= input_ids.len() {
3134 break;
3135 }
3136 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3137 extra += 1;
3138 ok = ok && dj == input_ids[p + 2 + j];
3139 Self::chain_probe_note(j, ok);
3140 d_prev = dj;
3141 hx = hj;
3142 }
3143 m.kv.truncate_last(extra);
3144 } else {
3145 let _ = self.mtp_step(
3146 m,
3147 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3148 input_ids[p + 1],
3149 p,
3150 );
3151 }
3152 }
3153 }
3154 }
3155 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3156 pos = end;
3157 }
3158 }
3159 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3160 if task_mask.is_none()
3161 && !dyn_prefill
3162 && !graph_prefill
3163 && !pair_off
3164 && self.pair_supported()
3165 && o1_prefill.is_none()
3166 {
3167 while pos + 1 < input_ids.len()
3168 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3169 {
3170 let e1 = self.embed_single(input_ids[pos]);
3171 let e2 = self.embed_single(input_ids[pos + 1]);
3172 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3173 self.commit_linear_scratch();
3175 if let Some(m) = &mut mtp {
3176 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3177 if pos + 2 < input_ids.len() {
3178 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3179 .ok()
3180 .and_then(|v| v.parse().ok())
3181 .unwrap_or(0);
3182 if probe >= 1 && pos + 3 < input_ids.len() {
3183 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3187 let mut ok = d1 == input_ids[pos + 3];
3188 Self::chain_probe_note(0, ok);
3189 let mut d_prev = d1;
3190 let mut extra = 0usize;
3191 for j in 1..probe {
3192 if pos + 3 + j >= input_ids.len() {
3193 break;
3194 }
3195 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3196 extra += 1;
3197 ok = ok && dj == input_ids[pos + 3 + j];
3198 Self::chain_probe_note(j, ok);
3199 d_prev = dj;
3200 hx = hj;
3201 }
3202 m.kv.truncate_last(extra);
3203 } else {
3204 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3205 }
3206 }
3207 }
3208 hidden = h2;
3209 pos += 2;
3210 }
3211 }
3212 let o1_batch_ready = o1_sealed
3225 && o1_prefill.is_some()
3226 && mtp.is_none()
3227 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3228 && (0..self.num_layers).all(|li| {
3229 let cache = &self.kv_cache.layers[self.phys_layer(li)];
3230 cache.o1.is_none() || cache.o1_views().is_some()
3231 });
3232 let mtp_batch_prefill = mtp.is_some()
3237 && graph_prefill
3238 && task_mask.is_none()
3239 && !dyn_prefill
3240 && !self.o1_active()
3241 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3242 if batch_k > 0
3243 && (graph_prefill || o1_batch_ready)
3244 && task_mask.is_none()
3245 && (!self.o1_active() || o1_batch_ready)
3246 && (mtp.is_none() || mtp_batch_prefill)
3247 && !dyn_prefill
3248 && pos + 1 < input_ids.len()
3249 {
3250 let hs = self.hidden_size;
3251 let chunk = batch_k;
3252 while pos < input_ids.len() {
3253 let end = (pos + chunk).min(input_ids.len());
3254 let bk = end - pos;
3255 let mut hiddens = vec![0f32; bk * hs];
3256 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3257 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3258 }
3259 let positions: Vec<usize> = (pos..end).collect();
3260 let t_chunk = std::time::Instant::now();
3261 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3262 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3263 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3264 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3265 eprintln!(
3266 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3267 if o1_batch_ready {
3268 "o1"
3269 } else if mtp_batch_prefill {
3270 "ordinary_mtp"
3271 } else {
3272 "ordinary"
3273 },
3274 bk as f64 / (ms / 1000.0)
3275 );
3276 }
3277 {
3278 use std::sync::atomic::{AtomicBool, Ordering};
3279 static SAID: AtomicBool = AtomicBool::new(false);
3280 if !SAID.swap(true, Ordering::Relaxed) {
3281 if ok_b {
3282 tracing::info!(
3283 "batched prefill: ACTIVE mode={} (k={bk})",
3284 if o1_batch_ready {
3285 "o1"
3286 } else if mtp_batch_prefill {
3287 "ordinary_mtp"
3288 } else {
3289 "ordinary"
3290 }
3291 );
3292 } else {
3293 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3294 }
3295 }
3296 }
3297 if ok_b {
3298 if mtp_batch_prefill {
3299 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3300 if n_pairs > 0 {
3301 let rows: Vec<Vec<f32>> = (0..n_pairs)
3307 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3308 .collect();
3309 let pairs: Vec<(&[f32], u32)> = rows
3310 .iter()
3311 .enumerate()
3312 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3313 .collect();
3314 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3315 eprintln!(
3316 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3317 pos,
3318 n_pairs,
3319 pos + n_pairs - 1,
3320 );
3321 }
3322 let warm_error = if let Some(m) = mtp.as_mut() {
3323 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3324 } else {
3325 None
3326 };
3327 if let Some(err) = warm_error {
3328 self.finish_generation(&mut mtp, &mut router, true);
3333 return Err(err.to_string());
3334 }
3335 }
3336 }
3337 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3338 pos = end;
3339 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3340 self.finish_generation(&mut mtp, &mut router, true);
3345 return Err(if o1_batch_ready {
3346 "sealed O(1) batch graph failed after admission".to_string()
3347 } else {
3348 "ordinary recurrent batch graph failed after admission".to_string()
3349 });
3350 } else {
3351 break; }
3353 }
3354 }
3355 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3356 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3357 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3358 if let Some(m) = &mut mtp {
3359 if pos + 1 < input_ids.len() {
3360 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3366 .ok()
3367 .and_then(|v| v.parse().ok())
3368 .unwrap_or(0);
3369 if probe >= 1 && pos + 2 < input_ids.len() {
3370 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3371 let mut ok = d1 == input_ids[pos + 2];
3372 Self::chain_probe_note(0, ok);
3373 let mut d_prev = d1;
3374 let mut extra = 0usize;
3375 for j in 1..probe {
3376 if pos + 2 + j >= input_ids.len() {
3377 break;
3378 }
3379 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3380 extra += 1;
3381 ok = ok && dj == input_ids[pos + 2 + j];
3382 Self::chain_probe_note(j, ok);
3383 d_prev = dj;
3384 hx = hj;
3385 }
3386 m.kv.truncate_last(extra);
3389 } else {
3390 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3391 }
3392 }
3393 }
3394 pos += 1;
3395 }
3396 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3397 eprintln!(
3398 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3399 input_ids.len(),
3400 _tpf.elapsed().as_secs_f64() * 1000.0
3401 );
3402 }
3403 if self
3404 .graph_failed
3405 .swap(false, std::sync::atomic::Ordering::Relaxed)
3406 {
3407 self.finish_generation(&mut mtp, &mut router, true);
3412 return Err("GPU token graph failed during prefill".to_string());
3413 }
3414 if self
3417 .cancel
3418 .swap(false, std::sync::atomic::Ordering::Relaxed)
3419 {
3420 self.finish_generation(&mut mtp, &mut router, true);
3424 return Ok(GenerateResult {
3425 text: String::new(),
3426 token_ids: Vec::new(),
3427 prompt_tokens: input_ids.len(),
3428 tokens_generated: 0,
3429 finish_reason: "cancelled".to_string(),
3430 mtp_drafted: 0,
3431 mtp_accepted: 0,
3432 token_confidence: Vec::new(),
3433 traces: Vec::new(),
3434 });
3435 }
3436
3437 if !o1_sealed {
3440 match self.o1_seal_checked() {
3441 Ok(_) => {}
3442 Err(err) => {
3443 self.finish_generation(&mut mtp, &mut router, true);
3444 return Err(err);
3445 }
3446 }
3447 }
3448
3449 macro_rules! commit {
3451 ($id:expr) => {{
3452 all_ids.push($id);
3453 generated += 1;
3454 self.note_draft_id($id);
3455 if self.tokenizer.is_eos($id) && !self.ignore_eos {
3456 finish_reason = "stop".to_string();
3457 false
3458 } else {
3459 let token_text = self.tokenizer.decode_token($id);
3460 let mut go = true;
3461 if let Some(ref mut cb) = on_token {
3462 if !cb(&token_text) {
3463 finish_reason = "cancelled".to_string();
3464 go = false;
3465 }
3466 }
3467 go
3468 }
3469 }};
3470 }
3471
3472 let mut spec_trial = SpecTrial::Spec {
3483 t0: std::time::Instant::now(),
3484 gen0: generated,
3485 rounds: 0,
3486 };
3487 let mut spec_mon = SpecMon::default();
3488 let mut spec_watchdog_off = false;
3489 let mut next_pos = input_ids.len();
3491 'decode: while generated < max_tokens {
3492 if self
3493 .graph_failed
3494 .swap(false, std::sync::atomic::Ordering::Relaxed)
3495 {
3496 self.finish_generation(&mut mtp, &mut router, true);
3501 return Err("GPU token graph failed during decode".to_string());
3502 }
3503 if self
3504 .cancel
3505 .swap(false, std::sync::atomic::Ordering::Relaxed)
3506 {
3507 finish_reason = "cancelled".to_string();
3508 break 'decode;
3509 }
3510 let forced = self.spec_forced.take();
3515 let mut logits = match (forced, self.graph_logits.take()) {
3516 (Some(_), _) => Vec::new(),
3517 (None, Some(lg)) => lg,
3518 (None, None) => {
3519 inference::rms_norm_into(
3520 &hidden,
3521 &self.weights.final_norm,
3522 self.rms_eps,
3523 self.norm_style,
3524 &mut self.ws.n1,
3525 );
3526 self.lm_head_forward(&self.ws.n1)
3527 }
3528 };
3529 if generated
3532 == std::env::var("CMF_LOGIT_DUMP_STEP")
3533 .ok()
3534 .and_then(|v| v.parse().ok())
3535 .unwrap_or(0)
3536 {
3537 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3538 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3539 for v in hidden.iter().chain(logits.iter()) {
3540 bytes.extend_from_slice(&v.to_le_bytes());
3541 }
3542 if let Err(e) = std::fs::write(&path, &bytes) {
3543 eprintln!("logit dump: failed to write {path}: {e}");
3544 self.finish_generation(&mut mtp, &mut router, true);
3545 return Err(format!("logit dump write failed: {e}"));
3546 }
3547 }
3548 }
3549 let t_next = match forced {
3550 Some(c) => c,
3551 None => sampler::sample_with_scratch_pool(
3552 &logits,
3553 &self.sampler_config,
3554 &all_ids,
3555 &mut self.rng,
3556 &mut self.sampler_scratch,
3557 self.pool.as_deref(),
3558 ),
3559 };
3560 if self.confidence_on {
3561 confidence.push(if logits.is_empty() {
3562 0.0
3563 } else {
3564 sampler::top1_prob_pool(
3565 self.pool.as_deref(),
3566 &mut self.sampler_scratch,
3567 &logits,
3568 t_next,
3569 calib_temp,
3570 )
3571 });
3572 }
3573 if !logits.is_empty() {
3574 attention::recycle_buf(&mut logits);
3575 }
3576 if trace_on {
3577 let skill = router.as_ref().and_then(|r| r.active_id());
3581 traces.push(TokenTrace {
3582 t: generated,
3583 token_id: t_next,
3584 confidence: confidence.last().copied().unwrap_or(0.0),
3585 active_skill: skill,
3586 recon: None,
3587 switched: false,
3588 });
3589 }
3590 if !commit!(t_next) {
3591 break 'decode;
3592 }
3593 if generated >= max_tokens {
3594 break 'decode;
3595 }
3596
3597 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3598 static SAID: std::sync::Once = std::sync::Once::new();
3604 SAID.call_once(|| {
3605 tracing::warn!(
3606 "KV cache full at {} positions — evicting half; quality \
3607 will degrade. Raise CMF_MAX_SEQ.",
3608 self.kv_cache.max_seq_len,
3609 );
3610 });
3611 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3612 self.kv_cache.evict(keep);
3613 }
3614
3615 if graph_spec {
3618 match spec_trial {
3619 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3620 spec_mon.plain_ms =
3621 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3622 let keep = spec_mon.pays();
3623 tracing::info!(
3624 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3625 spec_mon.tokens,
3626 spec_mon.round_ms,
3627 spec_mon.plain_ms,
3628 if keep { "speculating" } else { "plain" }
3629 );
3630 spec_mon.fails = 0;
3631 spec_trial = SpecTrial::Decided {
3632 spec: keep,
3633 recheck_at: if keep { usize::MAX } else { generated + 128 },
3634 };
3635 }
3636 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3637 spec_mon.n = 0;
3638 spec_trial = SpecTrial::Spec {
3639 t0: std::time::Instant::now(),
3640 gen0: generated,
3641 rounds: 0,
3642 };
3643 }
3644 _ => {}
3645 }
3646 spec_watchdog_off = matches!(
3647 spec_trial,
3648 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3649 );
3650 }
3651 match &mut mtp {
3652 #[cfg(feature = "gpu")]
3654 Some(m)
3655 if graph_spec
3656 && !spec_watchdog_off
3657 && generated + 1 < max_tokens
3658 && next_pos > 0 =>
3659 {
3660 let t_round = std::time::Instant::now();
3661 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3662 m,
3663 &hidden,
3664 t_next,
3665 next_pos,
3666 &mut drafted,
3667 &mut accepted,
3668 &mut all_ids,
3669 ) {
3670 next_pos = n_pos;
3671 hidden = new_h;
3672 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3673 eprintln!(
3674 "spec-round wall {:.1} ms → {} tokens",
3675 t_round.elapsed().as_secs_f64() * 1e3,
3676 extra.len() + 1
3677 );
3678 }
3679 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3683 spec_trial = Self::spec_trial_round(
3686 spec_trial,
3687 &mut spec_mon,
3688 generated + extra.len() + 1,
3689 );
3690 let mut stopped = false;
3691 for &id in &extra {
3692 if self.confidence_on {
3693 confidence.push(0.0);
3694 }
3695 if !commit!(id) {
3696 stopped = true;
3697 break;
3698 }
3699 }
3700 if stopped {
3701 break 'decode;
3702 }
3703 continue 'decode;
3704 }
3705 if self
3706 .graph_failed
3707 .swap(false, std::sync::atomic::Ordering::Relaxed)
3708 {
3709 self.finish_generation(&mut mtp, &mut router, true);
3715 return Err("GPU MTP graph failed during speculative decode".to_string());
3716 }
3717 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3728 spec_mon.tokens = 0.0;
3729 spec_mon.fails = 3;
3730 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3731 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3732 next_pos += 1;
3733 continue 'decode;
3734 }
3735 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3737 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3738 drafted += 1;
3739 let emb1 = self.embed_single(t_next);
3740 let emb2 = self.embed_single(draft);
3741 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3742
3743 inference::rms_norm_into(
3744 &h1,
3745 &self.weights.final_norm,
3746 self.rms_eps,
3747 self.norm_style,
3748 &mut self.ws.n1,
3749 );
3750 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3751 let t_after = sampler::sample_with_scratch_pool(
3752 &logits1,
3753 &self.sampler_config,
3754 &all_ids,
3755 &mut self.rng,
3756 &mut self.sampler_scratch,
3757 self.pool.as_deref(),
3758 );
3759 if self.confidence_on {
3760 confidence.push(sampler::top1_prob_pool(
3761 self.pool.as_deref(),
3762 &mut self.sampler_scratch,
3763 &logits1,
3764 t_after,
3765 calib_temp,
3766 ));
3767 }
3768 attention::recycle_buf(&mut logits1);
3769 if trace_on {
3770 traces.push(TokenTrace {
3773 t: generated,
3774 token_id: t_after,
3775 confidence: confidence.last().copied().unwrap_or(0.0),
3776 active_skill: None,
3777 recon: None,
3778 switched: false,
3779 });
3780 }
3781 let stop = !commit!(t_after);
3782
3783 if t_after == draft {
3784 accepted += 1;
3785 self.commit_linear_scratch();
3786 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3787 hidden = h2;
3788 next_pos += 2;
3789 } else {
3790 for layer in &mut self.kv_cache.layers {
3792 layer.truncate_last(1);
3793 }
3794 if !stop {
3795 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3796 hidden = self.forward_layers(
3797 &self.embed_single(t_after),
3798 next_pos + 1,
3799 None,
3800 );
3801 }
3802 next_pos += 2;
3803 }
3804 if stop {
3805 break 'decode;
3806 }
3807 }
3808 _ => {
3810 #[cfg(feature = "gpu")]
3815 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3816 static SAID: std::sync::Once = std::sync::Once::new();
3817 SAID.call_once(|| {
3818 eprintln!(
3819 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3820 !self.dsv4_mtp.is_empty(),
3821 task_mask.is_none(),
3822 router.is_none(),
3823 !trace_on,
3824 self.sampler_config.temperature < 1e-6,
3825 self.sampler_config.repetition_penalty == 1.0,
3826 );
3827 });
3828 }
3829 #[cfg(feature = "gpu")]
3830 if Self::dsv4_spec_on()
3831 && self.dsv4.is_some()
3832 && !self.dsv4_mtp.is_empty()
3833 && task_mask.is_none()
3834 && router.is_none()
3835 && !trace_on
3836 && self.sampler_config.temperature < 1e-6
3837 && self.sampler_config.repetition_penalty == 1.0
3838 && generated + 1 < max_tokens
3839 && all_ids.len() >= 2
3840 && generated >= dsv4_spec_retry_at
3841 {
3842 let tip_token = all_ids[all_ids.len() - 2];
3843 let drafted0 = drafted;
3844 let round = self.dsv4_spec_step(
3845 tip_token,
3846 t_next,
3847 next_pos,
3848 max_tokens.saturating_sub(generated),
3849 &mut drafted,
3850 &mut accepted,
3851 );
3852 if drafted > drafted0 {
3853 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3854 if useful {
3855 dsv4_spec_bad = 0;
3856 } else {
3857 dsv4_spec_bad += 1;
3858 if dsv4_spec_bad >= 2 {
3859 dsv4_spec_bad = 0;
3860 dsv4_spec_retry_at = generated.saturating_add(32);
3861 tracing::info!(
3862 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3863 );
3864 }
3865 }
3866 }
3867 if let Some((extra, n_pos)) = round {
3868 next_pos = n_pos;
3869 let mut stopped = false;
3870 for &id in &extra {
3871 if self.confidence_on {
3872 confidence.push(0.0);
3873 }
3874 if !commit!(id) {
3875 stopped = true;
3876 break;
3877 }
3878 }
3879 if stopped {
3880 break 'decode;
3881 }
3882 continue 'decode;
3883 }
3884 }
3885 self.graph_want_logits = fuse_lm;
3886 let mut t_fwd = t_next;
3892 let pure_greedy = self.sampler_config.temperature < 1e-6
3893 && self.sampler_config.repetition_penalty == 1.0
3894 && self.sampler_config.suppress_tokens.is_empty();
3895 let burst_k = std::env::var("CMF_MULTISTEP")
3900 .ok()
3901 .and_then(|v| v.parse::<usize>().ok())
3902 .unwrap_or(0);
3903 if pure_greedy
3904 && burst_k >= 1
3905 && fuse_lm
3906 && task_mask.is_none()
3907 && router.is_none()
3908 && !trace_on
3909 && !self.confidence_on
3910 {
3911 let mut stopped = false;
3912 loop {
3913 let room = max_tokens.saturating_sub(generated);
3914 if room <= 2 {
3915 break;
3916 }
3917 let k = burst_k.min(room - 1);
3918 if k < 1 {
3919 break;
3920 }
3921 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3922 if self
3923 .graph_failed
3924 .swap(false, std::sync::atomic::Ordering::Relaxed)
3925 {
3926 self.finish_generation(&mut mtp, &mut router, true);
3927 return Err(
3928 "GPU token graph failed during greedy burst".to_string()
3929 );
3930 }
3931 break;
3932 };
3933 next_pos += k;
3934 for &id in &ids {
3935 if !commit!(id) {
3936 stopped = true;
3937 break;
3938 }
3939 }
3940 if stopped {
3941 break;
3942 }
3943 t_fwd = *ids.last().unwrap();
3944 }
3945 if stopped {
3946 break 'decode;
3947 }
3948 }
3949 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3950 next_pos += 1;
3951 if let Some(r) = &mut router {
3954 let phi = self.dyn_phi_ema.clone();
3955 let decision = r.step(&phi, generated);
3956 if let Some(new_active) = decision {
3957 let _ = self.set_active_skill(new_active);
3958 }
3959 if trace_on {
3962 if let Some(last) = traces.last_mut() {
3963 let e = r.last_best_e();
3964 last.recon = e.is_finite().then_some(e);
3965 last.switched = decision.is_some();
3966 }
3967 }
3968 }
3969 }
3970 }
3971 }
3972
3973 let cancelled = finish_reason == "cancelled";
3974 self.finish_generation(&mut mtp, &mut router, cancelled);
3975
3976 let output_ids = &all_ids[input_ids.len()..];
3977 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3981 if cancelled {
3982 self.kv_history.clear();
3983 } else {
3984 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3985 }
3986 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3988 Ok(GenerateResult {
3989 text: self.tokenizer.decode(output_ids),
3990 token_ids: output_ids.to_vec(),
3991 prompt_tokens: input_ids.len(),
3992 tokens_generated: generated,
3993 finish_reason,
3994 mtp_drafted: drafted,
3995 mtp_accepted: accepted,
3996 token_confidence: confidence,
3997 traces,
3998 })
3999 }
4000
4001 fn mtp_step(
4005 &mut self,
4006 m: &mut MtpModule,
4007 hidden: &[f32],
4008 next_token: u32,
4009 position: usize,
4010 ) -> u32 {
4011 self.mtp_step_h(m, hidden, next_token, position).0
4012 }
4013
4014 fn chain_probe_note(depth: usize, prefix_ok: bool) {
4018 use std::sync::Mutex;
4019 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
4020 let mut t = T.lock().unwrap();
4021 if t.len() <= depth {
4022 t.resize(depth + 1, (0, 0));
4023 }
4024 t[depth].0 += 1;
4025 t[depth].1 += prefix_ok as u64;
4026 if depth == 0 && t[0].0 % 128 == 0 {
4027 let line: Vec<String> = t
4028 .iter()
4029 .enumerate()
4030 .map(|(d, (n, k))| {
4031 format!(
4032 "d{}={:.0}%({n})",
4033 d + 1,
4034 100.0 * *k as f64 / (*n).max(1) as f64
4035 )
4036 })
4037 .collect();
4038 eprintln!("mtp-chain: {}", line.join(" "));
4039 }
4040 }
4041
4042 fn mtp_step_hl(
4050 &mut self,
4051 m: &mut MtpModule,
4052 hidden: &[f32],
4053 next_token: u32,
4054 position: usize,
4055 ) -> (Vec<f32>, Vec<f32>) {
4056 #[cfg(target_os = "macos")]
4061 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
4062 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4063 self.mtp_graph_mode = Some(true);
4064 return r;
4065 }
4066 if self.mtp_graph_mode == Some(true) {
4067 tracing::error!("mtp Metal graph failed after admission");
4068 self.clear_sequence_state();
4069 self.graph_failed
4070 .store(true, std::sync::atomic::Ordering::Relaxed);
4071 self.cancel
4072 .store(true, std::sync::atomic::Ordering::Relaxed);
4073 return (Vec::new(), Vec::new());
4074 }
4075 self.mtp_graph_mode = Some(false);
4076 }
4077 #[cfg(feature = "gpu")]
4078 if self.mtp_graph_mode != Some(false) {
4079 if !self.mtp_graph_ok(m) {
4080 if self.mtp_graph_mode == Some(true) {
4081 tracing::error!("mtp graph became unavailable after admission");
4086 self.clear_sequence_state();
4087 self.graph_failed
4088 .store(true, std::sync::atomic::Ordering::Relaxed);
4089 self.cancel
4090 .store(true, std::sync::atomic::Ordering::Relaxed);
4091 return (Vec::new(), Vec::new());
4092 }
4093 self.mtp_graph_mode = Some(false);
4094 } else {
4095 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4096 self.mtp_graph_mode = Some(true);
4097 return r;
4098 }
4099 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4100 return (Vec::new(), Vec::new());
4107 }
4108 tracing::error!("mtp graph failed or declined after admission");
4112 self.clear_sequence_state();
4113 self.graph_failed
4114 .store(true, std::sync::atomic::Ordering::Relaxed);
4115 self.cancel
4116 .store(true, std::sync::atomic::Ordering::Relaxed);
4117 return (Vec::new(), Vec::new());
4118 }
4119 }
4120 let e = self.embed_single(next_token);
4124 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4125 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4126 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4127 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4128 let mut x = vec![0.0f32; self.hidden_size];
4129 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4130
4131 let lw = &m.layer;
4133 inference::rms_norm_into(
4134 &x,
4135 &lw.input_norm,
4136 self.rms_eps,
4137 self.norm_style,
4138 &mut self.ws.n1,
4139 );
4140 let attn = match &lw.attn {
4141 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4143 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4144 AttnKind::Full {
4145 wq,
4146 wk,
4147 wv,
4148 wo,
4149 q_norm,
4150 k_norm,
4151 output_gate,
4152 softplus_gate,
4153 bias,
4154 } => {
4155 let mut cfg = self.attn_cfg(position);
4156 cfg.q_norm = q_norm.as_deref();
4157 cfg.k_norm = k_norm.as_deref();
4158 cfg.output_gate = *output_gate;
4159 cfg.softplus_gate = softplus_gate
4160 .as_ref()
4161 .map(|(gate, per_head)| (gate, *per_head));
4162 cfg.bias = bias
4163 .as_ref()
4164 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4165 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4166 }
4167 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4168 unreachable!("MTP block is full attention")
4169 }
4170 };
4171 for (i, &a) in attn.iter().enumerate() {
4172 x[i] += a;
4173 }
4174 inference::rms_norm_into(
4175 &x,
4176 &lw.post_norm,
4177 self.rms_eps,
4178 self.norm_style,
4179 &mut self.ws.p1,
4180 );
4181 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4182 for (i, &f) in ffn.iter().enumerate() {
4183 x[i] += f;
4184 }
4185
4186 inference::rms_norm_into(
4187 &x,
4188 &m.final_norm,
4189 self.rms_eps,
4190 self.norm_style,
4191 &mut self.ws.n1,
4192 );
4193 let lg = self.lm_head_forward(&self.ws.n1);
4194 (lg, x)
4195 }
4196
4197 fn mtp_step_h(
4199 &mut self,
4200 m: &mut MtpModule,
4201 hidden: &[f32],
4202 next_token: u32,
4203 position: usize,
4204 ) -> (u32, Vec<f32>) {
4205 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4206 let draft = sampler::argmax(&lg);
4207 attention::recycle_buf(&mut lg);
4208 (draft, x)
4209 }
4210
4211 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4217 match trial {
4218 SpecTrial::Spec { t0, gen0, rounds } => {
4219 let rounds = rounds + 1;
4220 if rounds >= 5 {
4221 if mon.plain_ms > 0.0 {
4222 let keep = mon.pays();
4223 mon.fails = 0;
4224 tracing::info!(
4225 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4226 mon.tokens,
4227 mon.round_ms,
4228 mon.plain_ms,
4229 if keep { "speculating" } else { "plain" }
4230 );
4231 SpecTrial::Decided {
4232 spec: keep,
4233 recheck_at: if keep { usize::MAX } else { generated + 128 },
4234 }
4235 } else {
4236 SpecTrial::Plain {
4237 t0: std::time::Instant::now(),
4238 gen0: generated,
4239 }
4240 }
4241 } else {
4242 SpecTrial::Spec { t0, gen0, rounds }
4243 }
4244 }
4245 SpecTrial::Decided { spec: true, .. } => {
4246 if mon.pays() {
4247 mon.fails = 0;
4248 trial
4249 } else {
4250 mon.fails += 1;
4251 if mon.fails >= 4 {
4252 tracing::info!(
4253 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4254 mon.tokens,
4255 mon.round_ms,
4256 mon.plain_ms
4257 );
4258 SpecTrial::Decided {
4259 spec: false,
4260 recheck_at: generated + 128,
4261 }
4262 } else {
4263 trial
4264 }
4265 }
4266 }
4267 other => other,
4268 }
4269 }
4270
4271 fn mtp_kv_id(&self) -> u64 {
4274 self.graph_kv_id | (1u64 << 40)
4275 }
4276
4277 const MTP_LAYER_BASE: usize = 0;
4282
4283 #[cfg(feature = "gpu")]
4290 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4291 self.mtp_graph_mode != Some(true)
4292 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4293 }
4294
4295 #[cfg(feature = "gpu")]
4301 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4302 let mut ok = true;
4303 let mut expected = false;
4304 for li in 0..self.num_layers {
4305 if matches!(
4306 self.weights.layers[self.phys_layer(li)].attn,
4307 AttnKind::Full { .. }
4308 ) {
4309 expected = true;
4310 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4311 }
4312 }
4313 !expected || ok
4314 }
4315
4316 fn graph_gdn_layer_count(&self) -> usize {
4320 (0..self.num_layers)
4321 .filter(|&li| {
4322 matches!(
4323 &self.weights.layers[self.phys_layer(li)].attn,
4324 AttnKind::LinearGdn(_)
4325 )
4326 })
4327 .count()
4328 }
4329
4330 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4333 let e = self.embed_single(next_token);
4334 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4335 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4336 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4337 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4338 let mut x = vec![0.0f32; self.hidden_size];
4339 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4340 x
4341 }
4342
4343 #[cfg(feature = "gpu")]
4346 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4347 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4348 return false;
4349 }
4350 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4351 || !crate::gpu::enabled_here()
4352 || self.attn_softcap > 0.0
4353 || self.attention_heads_per_layer.is_some()
4354 {
4355 return false;
4356 }
4357 matches!(
4358 &m.layer.attn,
4359 AttnKind::Full {
4360 softplus_gate: None,
4361 ..
4362 }
4363 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4364 }
4365
4366 #[cfg(feature = "gpu")]
4370 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4371 if !self.mtp_block_graph_ok(m) {
4372 return false;
4373 }
4374 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4375 return false;
4376 };
4377 let FfnKind::Dense(d) = &m.layer.ffn else {
4378 return false;
4379 };
4380 d.segs.is_empty()
4381 && wq.graph_weight().is_some()
4382 && wk.graph_weight().is_some()
4383 && wv.graph_weight().is_some()
4384 && wo.graph_weight().is_some()
4385 && d.gate_proj.graph_weight().is_some()
4386 && d.up_proj.graph_weight().is_some()
4387 && d.down_proj.graph_weight().is_some()
4388 && self.weights.lm_head.graph_weight().is_some()
4389 }
4390
4391 #[cfg(feature = "gpu")]
4397 fn mtp_step_graph(
4398 &mut self,
4399 m: &mut MtpModule,
4400 hidden: &[f32],
4401 next_token: u32,
4402 position: usize,
4403 ) -> Option<(Vec<f32>, Vec<f32>)> {
4404 if !self.mtp_graph_ok(m) {
4405 return None;
4406 }
4407 let lw = &m.layer;
4408 let AttnKind::Full {
4409 wq,
4410 wk,
4411 wv,
4412 wo,
4413 q_norm,
4414 k_norm,
4415 output_gate,
4416 softplus_gate,
4417 bias,
4418 } = &lw.attn
4419 else {
4420 return None;
4421 };
4422 if softplus_gate.is_some() {
4423 return None;
4424 }
4425 let FfnKind::Dense(d) = &lw.ffn else {
4426 return None;
4427 };
4428 if !d.segs.is_empty() {
4429 return None; }
4431 let mut x = self.mtp_block_input(m, hidden, next_token);
4434 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4435 let (_, i, kind, rs) = t.graph_weight()?;
4436 Some(crate::gpu::GraphW {
4437 idx: i,
4438 kind,
4439 row_scale: rs,
4440 data: &[],
4441 prism: crate::gpu::GraphPrismOp::None,
4442 affine: false,
4443 })
4444 }
4445 let (model, _, _, _) = wq.graph_weight()?;
4446 let model = model.clone();
4447 let (lm_gw, lm_rows) = {
4448 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4449 let rows = if kind == 6 {
4453 self.draft_head_rows(self.weights.lm_head.rows())
4454 } else {
4455 self.weights.lm_head.rows()
4456 };
4457 (
4458 crate::gpu::GraphW {
4459 idx: i,
4460 kind,
4461 row_scale: rs,
4462 data: &[],
4463 prism: crate::gpu::GraphPrismOp::None,
4464 affine: false,
4465 },
4466 rows,
4467 )
4468 };
4469 let layer = crate::gpu::GraphLayer {
4470 input_norm: &lw.input_norm,
4471 attn: crate::gpu::GraphAttn::Full {
4472 wq: gw(wq)?,
4473 wk: gw(wk)?,
4474 wv: gw(wv)?,
4475 wo: gw(wo)?,
4476 q_norm: q_norm.as_deref(),
4477 k_norm: k_norm.as_deref(),
4478 late_qk_norm: self.qk_norm_after_rope,
4479 bias: bias
4480 .as_ref()
4481 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4482 output_gate: *output_gate,
4483 cpu_k: m.kv.k_heads(),
4484 cpu_v: m.kv.v_heads(),
4485 },
4486 post_norm: &lw.post_norm,
4487 ffn: crate::gpu::GraphFfn::Dense {
4488 gate: gw(&d.gate_proj)?,
4489 up: gw(&d.up_proj)?,
4490 down: gw(&d.down_proj)?,
4491 },
4492 };
4493 let nh = self.num_heads;
4494 let (nkv, hd, rd) = self.layer_geom(0);
4495 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4496 let mut logits = Vec::new();
4497 let ok = crate::gpu::forward_token_graph(
4498 &model,
4499 self.mtp_kv_id(),
4500 std::slice::from_ref(&layer),
4501 &[None],
4502 self.o1_epoch,
4503 &self.inv_freq,
4504 &mut x,
4505 nh,
4506 nkv,
4507 hd,
4508 self.attn_scale,
4509 rd,
4510 self.hidden_size,
4511 self.intermediate_size,
4512 position,
4513 self.kv_cache.max_seq_len,
4514 gemma,
4515 self.rms_eps as f32,
4516 Some((&lm_gw, lm_rows)),
4517 &m.final_norm,
4518 &mut logits,
4519 &[],
4520 1,
4521 None,
4522 None,
4523 None,
4524 Self::MTP_LAYER_BASE,
4525 true,
4526 );
4527 match ok {
4528 crate::gpu::TokenGraphOutcome::Completed => {}
4529 crate::gpu::TokenGraphOutcome::Declined => return None,
4530 crate::gpu::TokenGraphOutcome::Failed => {
4531 self.clear_sequence_state();
4535 self.graph_failed
4536 .store(true, std::sync::atomic::Ordering::Relaxed);
4537 self.cancel
4538 .store(true, std::sync::atomic::Ordering::Relaxed);
4539 return None;
4540 }
4541 }
4542 logits.resize(self.vocab_size, 0.0);
4543 Some((logits, x))
4544 }
4545
4546 #[cfg(feature = "gpu")]
4554 fn mtp_warm_graph(
4555 &mut self,
4556 m: &mut MtpModule,
4557 pairs: &[(&[f32], u32)],
4558 first_pos: usize,
4559 ) -> crate::gpu::BatchGraphOutcome {
4560 if pairs.is_empty() {
4561 return crate::gpu::BatchGraphOutcome::Completed;
4562 }
4563 if !self.mtp_block_graph_ok(m) {
4564 return crate::gpu::BatchGraphOutcome::Declined;
4565 }
4566 let hs = self.hidden_size;
4567 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4570 for (h, t) in pairs {
4571 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4572 }
4573 let lw = &m.layer;
4574 let AttnKind::Full {
4575 wq,
4576 wk,
4577 wv,
4578 wo,
4579 q_norm,
4580 k_norm,
4581 output_gate,
4582 bias,
4583 ..
4584 } = &lw.attn
4585 else {
4586 return crate::gpu::BatchGraphOutcome::Declined;
4587 };
4588 let FfnKind::Dense(d) = &lw.ffn else {
4589 return crate::gpu::BatchGraphOutcome::Declined;
4590 };
4591 if !d.segs.is_empty() {
4592 return crate::gpu::BatchGraphOutcome::Declined; }
4594 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4595 let (_, i, kind, rs) = t.graph_weight()?;
4596 Some(crate::gpu::GraphW {
4597 idx: i,
4598 kind,
4599 row_scale: rs,
4600 data: &[],
4601 prism: crate::gpu::GraphPrismOp::None,
4602 affine: false,
4603 })
4604 }
4605 let Some((model, _, _, _)) = wq.graph_weight() else {
4606 return crate::gpu::BatchGraphOutcome::Declined;
4607 };
4608 let model = model.clone();
4609 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4610 gw(wq),
4611 gw(wk),
4612 gw(wv),
4613 gw(wo),
4614 gw(&d.gate_proj),
4615 gw(&d.up_proj),
4616 gw(&d.down_proj),
4617 ) else {
4618 return crate::gpu::BatchGraphOutcome::Declined;
4619 };
4620 let layer = crate::gpu::GraphLayer {
4621 input_norm: &lw.input_norm,
4622 attn: crate::gpu::GraphAttn::Full {
4623 wq: gwq,
4624 wk: gwk,
4625 wv: gwv,
4626 wo: gwo,
4627 q_norm: q_norm.as_deref(),
4628 k_norm: k_norm.as_deref(),
4629 late_qk_norm: self.qk_norm_after_rope,
4630 bias: bias
4631 .as_ref()
4632 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4633 output_gate: *output_gate,
4634 cpu_k: m.kv.k_heads(),
4635 cpu_v: m.kv.v_heads(),
4636 },
4637 post_norm: &lw.post_norm,
4638 ffn: crate::gpu::GraphFfn::Dense {
4639 gate: gg,
4640 up: gu,
4641 down: gd,
4642 },
4643 };
4644 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4645 let nh = self.num_heads;
4646 let (nkv, hd, rd) = self.layer_geom(0);
4647 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4648 crate::gpu::forward_batch_graph(
4649 &model,
4650 self.mtp_kv_id(),
4651 std::slice::from_ref(&layer),
4652 &self.inv_freq,
4653 &mut hiddens,
4654 nh,
4655 nkv,
4656 hd,
4657 rd,
4658 hs,
4659 self.intermediate_size,
4660 &positions,
4661 self.kv_cache.max_seq_len,
4662 gemma,
4663 self.rms_eps as f32,
4664 self.attn_scale,
4665 pairs.len(),
4666 &[],
4667 0,
4668 None,
4669 )
4670 }
4671
4672 #[cfg(feature = "gpu")]
4679 fn mtp_warm_graph_fallback(
4680 &mut self,
4681 m: &mut MtpModule,
4682 pairs: &[(&[f32], u32)],
4683 first_pos: usize,
4684 ) -> bool {
4685 if pairs.is_empty() {
4686 return true;
4687 }
4688 let graphable = self.mtp_block_graph_ok(m);
4689 if !graphable {
4690 if self.mtp_graph_mode == Some(true) {
4694 return false;
4695 }
4696 self.mtp_graph_mode = Some(false);
4697 for (j, (h, t)) in pairs.iter().enumerate() {
4698 self.mtp_warm(m, h, *t, first_pos + j);
4699 }
4700 return true;
4701 }
4702
4703 for (j, (h, t)) in pairs.iter().enumerate() {
4708 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4709 return false;
4710 }
4711 }
4712 self.mtp_graph_mode = Some(true);
4713 true
4714 }
4715
4716 #[cfg(feature = "gpu")]
4721 fn mtp_warm_prefill_pairs(
4722 &mut self,
4723 m: &mut MtpModule,
4724 pairs: &[(&[f32], u32)],
4725 first_pos: usize,
4726 ) -> Result<(), &'static str> {
4727 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4732 if self.mtp_graph_mode == Some(true) {
4733 return Err("MTP token graph became unavailable after admission");
4734 }
4735 self.mtp_graph_mode = Some(false);
4736 for (j, (h, t)) in pairs.iter().enumerate() {
4737 self.mtp_warm(m, h, *t, first_pos + j);
4738 }
4739 return Ok(());
4740 }
4741 match self.mtp_warm_graph(m, pairs, first_pos) {
4742 crate::gpu::BatchGraphOutcome::Completed => {
4743 if !pairs.is_empty() {
4744 self.mtp_graph_mode = Some(true);
4745 }
4746 Ok(())
4747 }
4748 crate::gpu::BatchGraphOutcome::Declined => {
4749 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4750 Ok(())
4751 } else {
4752 Err("MTP warm-up fallback failed after device admission")
4753 }
4754 }
4755 crate::gpu::BatchGraphOutcome::Failed => {
4756 Err("MTP warm batch graph failed after admission")
4757 }
4758 }
4759 }
4760
4761 #[cfg(not(feature = "gpu"))]
4762 fn mtp_warm_prefill_pairs(
4763 &mut self,
4764 m: &mut MtpModule,
4765 pairs: &[(&[f32], u32)],
4766 first_pos: usize,
4767 ) -> Result<(), &'static str> {
4768 for (j, (h, t)) in pairs.iter().enumerate() {
4769 self.mtp_warm(m, h, *t, first_pos + j);
4770 }
4771 Ok(())
4772 }
4773
4774 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4778 let e = self.embed_single(next_token);
4779 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4780 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4781 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4782 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4783 let mut x = vec![0.0f32; self.hidden_size];
4784 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4785 inference::rms_norm_into(
4786 &x,
4787 &m.layer.input_norm,
4788 self.rms_eps,
4789 self.norm_style,
4790 &mut self.ws.n1,
4791 );
4792 let attn = match &m.layer.attn {
4793 AttnKind::Full {
4794 wq,
4795 wk,
4796 wv,
4797 wo,
4798 q_norm,
4799 k_norm,
4800 output_gate,
4801 softplus_gate,
4802 bias,
4803 } => {
4804 let mut cfg = self.attn_cfg(position);
4805 cfg.q_norm = q_norm.as_deref();
4806 cfg.k_norm = k_norm.as_deref();
4807 cfg.output_gate = *output_gate;
4808 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4809 cfg.bias = bias
4810 .as_ref()
4811 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4812 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4813 }
4814 _ => return,
4815 };
4816 let _ = attn;
4817 }
4818
4819 #[cfg(feature = "gpu")]
4826 #[allow(clippy::too_many_arguments)]
4827 fn graph_spec_step(
4828 &mut self,
4829 m: &mut MtpModule,
4830 hidden: &[f32],
4831 t_next: u32,
4832 next_pos: usize,
4833 drafted: &mut usize,
4834 accepted: &mut usize,
4835 all_ids: &mut Vec<u32>,
4839 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4840 #[cfg(target_os = "macos")]
4851 let metal_native = crate::gpu::q1_force();
4852 #[cfg(not(target_os = "macos"))]
4853 let metal_native = false;
4854 #[cfg(feature = "gpu")]
4855 let k_default = if metal_native {
4856 7
4859 } else if crate::gpu_wgpu::verify_i8_on() {
4860 5
4861 } else {
4862 4
4863 };
4864 #[cfg(not(feature = "gpu"))]
4865 let k_default = 4;
4866 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
4867 .ok()
4868 .and_then(|v| v.parse().ok())
4869 .filter(|&v| (1..=8).contains(&v));
4870 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
4875 let k_spec: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
4876 if next_pos == 0 {
4877 return None;
4878 }
4879 let t_round = std::time::Instant::now();
4880 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4896 let sub0 = subs();
4897 let cfg = self.sampler_config.clone();
4902 let penalized = !(cfg.repetition_penalty == 1.0
4903 && cfg.presence_penalty == 0.0
4904 && cfg.suppress_tokens.is_empty());
4905 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4910 let sampling = cfg.temperature >= 1e-6;
4911 let sparse = sampling && sampler::sparse_ok(&cfg);
4917 let base_len = all_ids.len();
4918 if sampling && !sparse && self.spec_q.len() < k_spec {
4919 self.spec_q.resize_with(k_spec, Vec::new);
4920 }
4921 if sparse && self.spec_qs.len() < k_spec {
4922 self.spec_qs.resize_with(k_spec, Vec::new);
4923 }
4924 let mut drafts = Vec::with_capacity(k_spec);
4929 let mut hx = hidden.to_vec();
4930 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4933 for j in 0..k_spec {
4934 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4935 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4936 if spec_dbg {
4937 let saved = self.mtp_graph_mode;
4938 self.mtp_graph_mode = Some(false);
4939 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4940 self.mtp_graph_mode = saved;
4941 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4942 return None;
4943 }
4944 m.kv.truncate_last(1);
4945 dbg_ref = Some(r);
4946 }
4947 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4948 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4949 return None;
4950 }
4951 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4952 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4953 let dl = lg
4954 .iter()
4955 .zip(&lg_cpu)
4956 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4957 let dh = hj
4958 .iter()
4959 .zip(&h_cpu)
4960 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4961 eprintln!(
4962 "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 {}",
4963 next_pos - 1 + j,
4964 sampler::argmax(&lg_cpu),
4965 sampler::argmax(&lg),
4966 n(&h_cpu),
4967 n(&hj),
4968 m.kv.seq_len
4969 );
4970 }
4971 let dj = if sparse {
4972 let mut q = std::mem::take(&mut self.spec_qs[j]);
4973 let ok = sampler::sparse_distribution_into(
4974 &lg,
4975 &cfg,
4976 all_ids,
4977 &mut self.sampler_scratch,
4978 self.pool.as_deref(),
4979 &mut q,
4980 );
4981 let d = if ok {
4982 sampler::draw_sparse(&q, &mut self.rng)
4983 } else {
4984 let t = sampler::argmax(&lg);
4986 q.clear();
4987 q.push((t, 1.0));
4988 t
4989 };
4990 self.spec_qs[j] = q;
4991 all_ids.push(d);
4992 d
4993 } else if sampling {
4994 let mut q = std::mem::take(&mut self.spec_q[j]);
4995 sampler::distribution_into(
4996 &lg,
4997 &cfg,
4998 all_ids,
4999 &mut self.sampler_scratch,
5000 self.pool.as_deref(),
5001 &mut q,
5002 );
5003 let d = sampler::draw(&q, &mut self.rng);
5004 self.spec_q[j] = q;
5005 all_ids.push(d); d
5007 } else if greedy_pen {
5008 let d = sampler::argmax_penalized(
5009 &lg,
5010 &cfg,
5011 all_ids,
5012 &mut self.sampler_scratch,
5013 self.pool.as_deref(),
5014 );
5015 all_ids.push(d);
5016 d
5017 } else {
5018 sampler::argmax(&lg)
5019 };
5020 attention::recycle_buf(&mut lg);
5021 drafts.push(dj);
5022 hx = hj;
5023 }
5024 all_ids.truncate(base_len);
5025 *drafted += k_spec;
5026 let t_draft = t_round.elapsed();
5027 let sub_draft = subs();
5028 let b = k_spec + 1;
5031 let mut hiddens = vec![0.0f32; b * self.hidden_size];
5032 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
5033 let e = self.embed_single(t);
5034 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
5035 }
5036 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
5037 let (lm_gw, lm_rows) = {
5038 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
5039 (
5040 crate::gpu::GraphW {
5041 idx: i,
5042 kind,
5043 row_scale: rs,
5044 data: &[],
5045 prism: crate::gpu::GraphPrismOp::None,
5046 affine: false,
5047 },
5048 self.weights.lm_head.rows(),
5049 )
5050 };
5051 let mut logits = Vec::new();
5052 let final_norm = self.weights.final_norm.clone();
5053 #[cfg(target_os = "macos")]
5054 let verify_outcome = if metal_native {
5055 let lm = self.weights.lm_head.q1_parts()?;
5056 self.try_batch_graph_metal(
5057 &mut hiddens,
5058 &positions,
5059 b,
5060 Some((lm, &final_norm, &mut logits)),
5061 )
5062 } else {
5063 self.try_batch_graph_wgpu(
5064 &mut hiddens,
5065 &positions,
5066 b,
5067 Some(crate::gpu::SpecTail {
5068 lm: lm_gw,
5069 lm_rows,
5070 final_norm: &final_norm,
5071 logits_out: &mut logits,
5072 }),
5073 )
5074 };
5075 #[cfg(not(target_os = "macos"))]
5076 let verify_outcome = self.try_batch_graph_wgpu(
5077 &mut hiddens,
5078 &positions,
5079 b,
5080 Some(crate::gpu::SpecTail {
5081 lm: lm_gw,
5082 lm_rows,
5083 final_norm: &final_norm,
5084 logits_out: &mut logits,
5085 }),
5086 );
5087 match verify_outcome {
5088 crate::gpu::BatchGraphOutcome::Completed => {}
5089 crate::gpu::BatchGraphOutcome::Declined => {
5090 m.kv.truncate_last(k_spec);
5094 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5095 self.clear_sequence_state();
5096 self.graph_failed
5097 .store(true, std::sync::atomic::Ordering::Relaxed);
5098 self.cancel
5099 .store(true, std::sync::atomic::Ordering::Relaxed);
5100 tracing::error!("MTP graph mirror rewind failed after verify decline");
5101 }
5102 return None;
5103 }
5104 crate::gpu::BatchGraphOutcome::Failed => {
5105 self.clear_sequence_state();
5109 self.graph_failed
5110 .store(true, std::sync::atomic::Ordering::Relaxed);
5111 self.cancel
5112 .store(true, std::sync::atomic::Ordering::Relaxed);
5113 tracing::error!("MTP verify batch graph failed after admission");
5114 return None;
5115 }
5116 }
5117 #[cfg(target_os = "macos")]
5123 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5124 let snap: Vec<Vec<f32>> = self
5125 .kv_cache
5126 .layers
5127 .iter()
5128 .map(|l| l.linear_state.clone())
5129 .collect();
5130 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5131 let toks: Vec<u32> = std::iter::once(t_next)
5132 .chain(drafts.iter().copied())
5133 .collect();
5134 let want_save = self.graph_want_logits;
5135 self.graph_want_logits = false;
5136 for (i, &t) in toks.iter().enumerate() {
5137 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5138 let _ = self.graph_logits.take();
5139 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5143 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5144 }
5145 let ref_lg = self.logits_from_hidden(&hi);
5146 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5147 let ra = sampler::argmax(&ref_lg);
5148 let va = sampler::argmax(row);
5149 let mut md = 0f32;
5150 let mut rms = 0f64;
5151 for j in 0..lm_rows.min(ref_lg.len()) {
5152 let d = (ref_lg[j] - row[j]).abs();
5153 md = md.max(d);
5154 rms += (d as f64) * (d as f64);
5155 }
5156 let mut hd = 0f32;
5157 for j in 0..self.hidden_size {
5158 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5159 }
5160 eprintln!(
5161 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5162 next_pos + i,
5163 if ra == va { "OK" } else { "MISMATCH" },
5164 (rms / lm_rows as f64).sqrt()
5165 );
5166 }
5167 self.graph_want_logits = want_save;
5168 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5171 if l.linear_state.len() == st.len() {
5172 l.linear_state.copy_from_slice(&st);
5173 } else {
5174 l.linear_state = st;
5175 }
5176 }
5177 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5178 let extra = l.seq_len.saturating_sub(n0);
5179 if extra > 0 {
5180 l.truncate_last(extra);
5181 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5182 }
5183 }
5184 }
5185 let t_verify = t_round.elapsed();
5186 let sub_verify = subs();
5187 let mut a = 0usize;
5192 let mut forced: Option<u32> = None;
5193 let ids: Vec<u32> = if sparse {
5194 let mut p = std::mem::take(&mut self.spec_ps);
5195 let mut res = std::mem::take(&mut self.spec_ress);
5196 while a < k_spec {
5197 let ok = sampler::sparse_distribution_into(
5198 &logits[a * lm_rows..(a + 1) * lm_rows],
5199 &cfg,
5200 all_ids,
5201 &mut self.sampler_scratch,
5202 self.pool.as_deref(),
5203 &mut p,
5204 );
5205 if !ok {
5206 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5207 p.clear();
5208 p.push((t, 1.0));
5209 }
5210 match sampler::spec_accept_or_correct_sparse(
5211 &p,
5212 &self.spec_qs[a],
5213 drafts[a],
5214 &mut self.rng,
5215 &mut res,
5216 ) {
5217 None => {
5218 all_ids.push(drafts[a]);
5219 a += 1;
5220 }
5221 Some(c) => {
5222 forced = Some(c);
5223 break;
5224 }
5225 }
5226 }
5227 all_ids.truncate(base_len);
5228 self.spec_ps = p;
5229 self.spec_ress = res;
5230 drafts.clone()
5231 } else if sampling {
5232 let mut p = std::mem::take(&mut self.spec_p);
5233 let mut res = std::mem::take(&mut self.spec_res);
5234 while a < k_spec {
5235 sampler::distribution_into(
5236 &logits[a * lm_rows..(a + 1) * lm_rows],
5237 &cfg,
5238 all_ids,
5239 &mut self.sampler_scratch,
5240 self.pool.as_deref(),
5241 &mut p,
5242 );
5243 match sampler::spec_accept_or_correct(
5244 &p,
5245 &self.spec_q[a],
5246 drafts[a],
5247 &mut self.rng,
5248 &mut res,
5249 self.pool.as_deref(),
5250 ) {
5251 None => {
5252 all_ids.push(drafts[a]);
5253 a += 1;
5254 }
5255 Some(c) => {
5256 forced = Some(c);
5257 break;
5258 }
5259 }
5260 }
5261 all_ids.truncate(base_len);
5262 self.spec_p = p;
5263 self.spec_res = res;
5264 drafts.clone()
5266 } else if greedy_pen {
5267 let mut ids: Vec<u32> = Vec::with_capacity(b);
5271 for i in 0..b {
5272 let t = sampler::argmax_penalized(
5273 &logits[i * lm_rows..(i + 1) * lm_rows],
5274 &cfg,
5275 all_ids,
5276 &mut self.sampler_scratch,
5277 self.pool.as_deref(),
5278 );
5279 ids.push(t);
5280 if i < k_spec && t == drafts[i] {
5281 all_ids.push(t);
5282 } else {
5283 break;
5284 }
5285 }
5286 all_ids.truncate(base_len);
5287 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5288 a += 1;
5289 }
5290 ids
5293 } else {
5294 let ids: Vec<u32> = (0..b)
5295 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5296 .collect();
5297 while a < k_spec && ids[a] == drafts[a] {
5298 a += 1;
5299 }
5300 ids
5301 };
5302 if spec_dbg {
5303 eprintln!(
5304 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5305 drafts, ids
5306 );
5307 }
5308 #[cfg(target_os = "macos")]
5312 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5313 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5314 {
5315 let snap: Vec<Vec<f32>> = self
5316 .kv_cache
5317 .layers
5318 .iter()
5319 .map(|l| l.linear_state.clone())
5320 .collect();
5321 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5322 let toks: Vec<u32> = std::iter::once(t_next)
5323 .chain(drafts.iter().copied())
5324 .collect();
5325 let want_save = self.graph_want_logits;
5326 self.graph_want_logits = false;
5327 for (i, &t) in toks.iter().take(a + 1).enumerate() {
5328 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5329 let _ = self.graph_logits.take();
5330 }
5331 self.graph_want_logits = want_save;
5332 let plain_states: Vec<Vec<f32>> = self
5333 .kv_cache
5334 .layers
5335 .iter()
5336 .map(|l| l.linear_state.clone())
5337 .collect();
5338 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5339 let mut rows = Vec::new();
5340 for (li, (l, n0)) in self
5341 .kv_cache
5342 .layers
5343 .iter_mut()
5344 .zip(attn_lens.iter())
5345 .enumerate()
5346 {
5347 let extra = l.seq_len.saturating_sub(*n0);
5348 if extra > 0 {
5349 let mut kk = Vec::new();
5350 let mut vv = Vec::new();
5351 for g in 0..nkv {
5352 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5353 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5354 }
5355 rows.push((li, kk, vv));
5356 l.truncate_last(extra);
5357 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5358 }
5359 }
5360 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5361 if l.linear_state.len() == st.len() {
5362 l.linear_state.copy_from_slice(&st);
5363 } else {
5364 l.linear_state = st;
5365 }
5366 }
5367 Some((plain_states, rows))
5368 } else {
5369 None
5370 };
5371 #[cfg(target_os = "macos")]
5373 if metal_native {
5374 if !self.metal_verify_commit(a) {
5377 self.clear_sequence_state();
5378 self.graph_failed
5379 .store(true, std::sync::atomic::Ordering::Relaxed);
5380 self.cancel
5381 .store(true, std::sync::atomic::Ordering::Relaxed);
5382 tracing::error!("Metal verify state/KV handoff failed after admission");
5383 return None;
5384 }
5385 if let Some((plain_states, rows)) = commit_ref {
5386 crate::gpu_metal::queue_fence();
5387 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5388 let mut worst_s = 0f32;
5389 let mut worst_li = 0usize;
5390 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5391 if l.linear_state.len() != ps.len() || ps.is_empty() {
5392 continue;
5393 }
5394 let d = l
5395 .linear_state
5396 .iter()
5397 .zip(ps)
5398 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5399 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5400 let rel = d / n.max(1e-6);
5401 if rel > worst_s {
5402 worst_s = rel;
5403 worst_li = li;
5404 }
5405 }
5406 let mut worst_k = 0f32;
5407 for (li, kk, vv) in &rows {
5408 let l = &self.kv_cache.layers[*li];
5409 let n0 = l.seq_len - (kk.len() / (nkv * hd));
5410 let mut ck = Vec::new();
5411 let mut cv = Vec::new();
5412 for g in 0..nkv {
5413 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5414 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5415 }
5416 if ck.len() == kk.len() {
5417 let dk = ck
5418 .iter()
5419 .zip(kk)
5420 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5421 let dv = cv
5422 .iter()
5423 .zip(vv)
5424 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5425 worst_k = worst_k.max(dk).max(dv);
5426 } else {
5427 eprintln!(
5428 "commit-check L{li}: kv row count mismatch {} vs {}",
5429 ck.len(),
5430 kk.len()
5431 );
5432 }
5433 }
5434 eprintln!(
5435 "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}"
5436 );
5437 }
5438 }
5439 if !metal_native && a + 1 < b {
5440 let expected_gdn_layers = self.graph_gdn_layer_count();
5441 if expected_gdn_layers > 0
5442 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5443 {
5444 self.clear_sequence_state();
5445 self.graph_failed
5446 .store(true, std::sync::atomic::Ordering::Relaxed);
5447 self.cancel
5448 .store(true, std::sync::atomic::Ordering::Relaxed);
5449 tracing::error!("GDN speculative restore failed after verify");
5450 return None;
5451 }
5452 }
5453 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5454 self.clear_sequence_state();
5459 self.graph_failed
5460 .store(true, std::sync::atomic::Ordering::Relaxed);
5461 self.cancel
5462 .store(true, std::sync::atomic::Ordering::Relaxed);
5463 tracing::error!("trunk graph KV rewind failed after speculative verify");
5464 return None;
5465 }
5466 *accepted += a;
5467 m.kv.truncate_last(k_spec.saturating_sub(1));
5478 #[cfg(target_os = "macos")]
5479 if metal_native && self.mtp_graph_mode == Some(true) {
5480 crate::gpu_metal::kv_mirror_set_stored(
5483 self.mtp_kv_id(),
5484 Self::MTP_LAYER_BASE,
5485 m.kv.seq_len,
5486 );
5487 }
5488 if !metal_native
5489 && self.mtp_graph_mode == Some(true)
5490 && !self.rewind_mtp_graph_mirror(next_pos)
5491 {
5492 self.clear_sequence_state();
5496 self.graph_failed
5497 .store(true, std::sync::atomic::Ordering::Relaxed);
5498 self.cancel
5499 .store(true, std::sync::atomic::Ordering::Relaxed);
5500 tracing::error!("MTP graph mirror rewind failed after verify commit");
5501 return None;
5502 }
5503 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5504 if !warm_off && a > 0 {
5505 let mut warmed = false;
5508 #[cfg(target_os = "macos")]
5509 if metal_native && self.mtp_graph_mode == Some(true) {
5510 let pairs: Vec<(&[f32], u32)> = (0..a)
5514 .map(|j| {
5515 (
5516 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5517 ids[j],
5518 )
5519 })
5520 .collect();
5521 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5522 if !warmed {
5523 warmed = true;
5524 for j in 0..a {
5525 let row =
5526 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5527 if self
5528 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5529 .is_none()
5530 {
5531 warmed = false;
5532 break;
5533 }
5534 }
5535 }
5536 }
5537 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5538 let rows: Vec<Vec<f32>> = (0..a)
5539 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5540 .collect();
5541 let pairs: Vec<(&[f32], u32)> = rows
5542 .iter()
5543 .zip(ids.iter())
5544 .map(|(r, &t)| (r.as_slice(), t))
5545 .collect();
5546 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5547 Ok(()) => warmed = true,
5548 Err(err) => {
5549 tracing::error!("{err}");
5555 self.clear_sequence_state();
5556 self.graph_failed
5557 .store(true, std::sync::atomic::Ordering::Relaxed);
5558 self.cancel
5559 .store(true, std::sync::atomic::Ordering::Relaxed);
5560 return None;
5561 }
5562 }
5563 }
5564 if !warmed {
5565 for j in 0..a {
5566 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5567 let row = row.to_vec();
5568 self.mtp_warm(m, &row, ids[j], next_pos + j);
5569 }
5570 }
5571 }
5572 if let Some(c) = forced {
5576 self.spec_forced = Some(c);
5577 self.graph_logits = None;
5578 } else {
5579 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5580 row.resize(self.vocab_size, 0.0);
5581 if let Some(c) = self.final_softcap {
5582 for l in row.iter_mut() {
5583 *l = c * (*l / c).tanh();
5584 }
5585 }
5586 self.graph_logits = Some(row);
5587 }
5588 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5589 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5595 let end = subs();
5596 eprintln!(
5597 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5598 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
5599 t_draft.as_secs_f64() * 1e3,
5600 sub_draft - sub0,
5601 (t_verify - t_draft).as_secs_f64() * 1e3,
5602 sub_verify - sub_draft,
5603 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5604 end - sub_verify,
5605 self.draft_full_streak,
5606 );
5607 }
5608 if k_env.is_none() && !metal_native {
5613 let f = a as f32 / k_spec.max(1) as f32;
5617 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
5618 let mut k_next = k_spec;
5619 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
5620 k_next = k_spec + 1;
5621 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
5622 k_next = k_spec - 1;
5623 }
5624 if k_next != k_spec {
5625 self.spec_acc_ewma = 0.6;
5626 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5627 eprintln!("spec-k: {k_spec} → {k_next}");
5628 }
5629 }
5630 self.spec_k_adapt = Some(k_next);
5631 }
5632 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5633 }
5634
5635 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5644 if !self.pair_supported() {
5645 return (0.0, 0.0);
5646 }
5647 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5654 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5655 let emb1 = self.embed_single(1);
5656 let emb2 = self.embed_single(2);
5657 let pos = self.kv_cache.seq_len();
5658
5659 let t0 = std::time::Instant::now();
5660 for _ in 0..iters {
5661 let _ = self.forward_layers(&emb1, pos, None);
5662 let _ = self.forward_layers(&emb2, pos + 1, None);
5663 for l in &mut self.kv_cache.layers {
5664 l.truncate_last(2);
5665 }
5666 }
5667 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5668
5669 let t1 = std::time::Instant::now();
5670 for _ in 0..iters {
5671 let _ = self.forward_pair(&emb1, &emb2, pos);
5672 for l in &mut self.kv_cache.layers {
5673 l.truncate_last(2);
5674 }
5675 }
5676 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5677 match graph_env {
5678 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5679 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5680 }
5681 (singles_ms, pair_ms)
5682 }
5683
5684 fn pair_supported(&self) -> bool {
5692 !self.weights.layers.is_empty()
5699 && self.g3n.is_none()
5700 && !self
5701 .weights
5702 .layers
5703 .iter()
5704 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5705 }
5706
5707 fn forward_pair(
5708 &mut self,
5709 emb1: &[f32],
5710 emb2: &[f32],
5711 position: usize,
5712 ) -> (Vec<f32>, Vec<f32>) {
5713 let mut h1 = emb1.to_vec();
5714 let mut h2 = emb2.to_vec();
5715 let (_nkv, _hd, hs, _rd, eps) = (
5716 self.num_kv_heads,
5717 self.head_dim,
5718 self.hidden_size,
5719 self.rotary_dim,
5720 self.rms_eps,
5721 );
5722 let pool = self.pool.clone();
5723
5724 for li in 0..self.num_layers {
5725 let lw = &self.weights.layers[self.phys_layer(li)];
5726 inference::rms_norm_into(
5729 &h1,
5730 &lw.input_norm,
5731 self.rms_eps,
5732 self.norm_style,
5733 &mut self.ws.n1,
5734 );
5735 inference::rms_norm_into(
5736 &h2,
5737 &lw.input_norm,
5738 self.rms_eps,
5739 self.norm_style,
5740 &mut self.ws.n2,
5741 );
5742
5743 let (a1, a2) = match &lw.attn {
5744 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5745 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5746 AttnKind::Linear(w) => {
5747 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5748 let layer = &mut self.kv_cache.layers[li];
5749 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5750 vmf_phase_pair(
5751 &self.ws.n1,
5752 &self.ws.n2,
5753 w,
5754 &cfg,
5755 state,
5756 scratch,
5757 self.pool.as_deref(),
5758 )
5759 }
5760 AttnKind::LinearGdn(w) => {
5761 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5762 let layer = &mut self.kv_cache.layers[li];
5763 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5764 gdn_pair(
5765 &self.ws.n1,
5766 &self.ws.n2,
5767 w,
5768 &cfg,
5769 state,
5770 scratch,
5771 self.pool.as_deref(),
5772 )
5773 }
5774 AttnKind::ShortConv(w) => {
5775 let cfg = self
5776 .short_conv_cfg
5777 .expect("short-conv layer without short_conv_cfg");
5778 let layer = &mut self.kv_cache.layers[li];
5779 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5780 short_conv_pair(
5781 &self.ws.n1,
5782 &self.ws.n2,
5783 w,
5784 &cfg,
5785 state,
5786 scratch,
5787 self.pool.as_deref(),
5788 )
5789 }
5790 AttnKind::Full {
5791 wq,
5792 wk,
5793 wv,
5794 wo,
5795 q_norm,
5796 k_norm,
5797 output_gate,
5798 softplus_gate,
5799 bias,
5800 } => {
5801 let inv_freq_l = self.layer_inv_freq(li);
5802 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5803 let cfg = QwenAttnCfg {
5804 num_heads: self.layer_num_heads(li),
5805 num_kv_heads: nkv_l,
5806 head_dim: hd_l,
5807 hidden_size: hs,
5808 position,
5809 inv_freq: &inv_freq_l,
5810 rotary_dim: rd_l,
5811 scale: self.attn_scale,
5812 softcap: self.attn_softcap,
5813 window: self.layer_window(li),
5814 v_norm: self.attn_v_norm,
5815 qk_norm_after_rope: self.qk_norm_after_rope,
5816 q_norm: q_norm.as_deref(),
5817 k_norm: k_norm.as_deref(),
5818 output_gate: *output_gate,
5819 softplus_gate: softplus_gate
5820 .as_ref()
5821 .map(|(gate, per_head)| (gate, *per_head)),
5822 rope_scale: self.layer_rope_scale(li),
5823 bias: bias
5824 .as_ref()
5825 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5826 rms_eps: eps,
5827 norm_style: self.norm_style,
5828 pool: pool.as_deref(),
5829 };
5830 attention::qwen_attention_pair(
5831 &self.ws.n1,
5832 &self.ws.n2,
5833 wq,
5834 wk,
5835 wv,
5836 wo,
5837 &mut self.kv_cache.layers[li],
5838 &cfg,
5839 )
5840 }
5841 };
5842 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5843 Some(w) => (
5844 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5845 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5846 ),
5847 None => (a1, a2),
5848 };
5849 for i in 0..self.hidden_size {
5850 h1[i] += a1[i];
5851 h2[i] += a2[i];
5852 }
5853 let (mut a1, mut a2) = (a1, a2);
5854 attention::recycle_buf(&mut a1);
5855 attention::recycle_buf(&mut a2);
5856
5857 let lw = &self.weights.layers[self.phys_layer(li)];
5858 inference::rms_norm_into(
5859 &h1,
5860 &lw.post_norm,
5861 self.rms_eps,
5862 self.norm_style,
5863 &mut self.ws.p1,
5864 );
5865 inference::rms_norm_into(
5866 &h2,
5867 &lw.post_norm,
5868 self.rms_eps,
5869 self.norm_style,
5870 &mut self.ws.p2,
5871 );
5872 let (f1, f2) = match &lw.ffn {
5873 FfnKind::DenseMoe(dm) => (
5876 dense_moe_ffn(
5877 dm,
5878 &self.ws.p1,
5879 &h1,
5880 self.rms_eps,
5881 self.norm_style,
5882 self.pool.as_deref(),
5883 ),
5884 dense_moe_ffn(
5885 dm,
5886 &self.ws.p2,
5887 &h2,
5888 self.rms_eps,
5889 self.norm_style,
5890 self.pool.as_deref(),
5891 ),
5892 ),
5893 _ => ffn_forward_pair(
5894 &lw.ffn,
5895 &self.ws.p1,
5896 &self.ws.p2,
5897 self.pool.as_deref(),
5898 None,
5899 ),
5900 };
5901 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5902 Some(w) => (
5903 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5904 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5905 ),
5906 None => (f1, f2),
5907 };
5908 for i in 0..self.hidden_size {
5909 h1[i] += f1[i];
5910 h2[i] += f2[i];
5911 }
5912 let (mut f1, mut f2) = (f1, f2);
5913 attention::recycle_buf(&mut f1);
5914 attention::recycle_buf(&mut f2);
5915 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5916 for i in 0..self.hidden_size {
5917 h1[i] *= sc;
5918 h2[i] *= sc;
5919 }
5920 }
5921 if self.is_loop_end(li) && li + 1 < self.num_layers {
5923 h1 = inference::rms_norm(
5924 &h1,
5925 &self.weights.final_norm,
5926 self.rms_eps,
5927 self.norm_style,
5928 );
5929 h2 = inference::rms_norm(
5930 &h2,
5931 &self.weights.final_norm,
5932 self.rms_eps,
5933 self.norm_style,
5934 );
5935 }
5936 }
5937 if self.o1_active() {
5943 self.commit_linear_scratch();
5944 }
5945 self.o1_progress();
5946 (h1, h2)
5947 }
5948
5949 fn commit_linear_scratch(&mut self) {
5951 for layer in &mut self.kv_cache.layers {
5952 if !layer.linear_scratch.is_empty() {
5953 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5954 layer.linear_scratch.clear();
5955 }
5956 }
5957 }
5958
5959 pub fn forward_ids(
5962 &mut self,
5963 ids: &[u32],
5964 task_mask: Option<&TaskMask>,
5965 ) -> Result<Vec<f32>, String> {
5966 if ids.is_empty() {
5967 return Err("empty id sequence".to_string());
5968 }
5969 self.clear_sequence_state();
5970 self.check_forward_graph("forward_ids setup", 0)?;
5971 if task_mask.is_none() {
5972 self.o1_begin();
5973 }
5974 let mut hidden = vec![0.0f32; self.hidden_size];
5975 let mut pos = 0usize;
5976 if let Some(b) = &mut self.dsv41 {
5977 let pool = self.pool.clone();
5978 let mut logits = Vec::new();
5979 crate::dsv41::forward_chunk(
5980 &b.0,
5981 &b.1,
5982 &b.2,
5983 &mut b.3,
5984 ids,
5985 0,
5986 pool.as_deref(),
5987 &mut logits,
5988 );
5989 if let Err(err) = self.o1_seal_checked() {
5990 self.clear_sequence_state();
5991 return Err(err);
5992 }
5993 return Ok(logits);
5994 }
5995 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
6003 let chunk = prefill_chunk();
6007 let hs = self.hidden_size;
6008 while pos < ids.len() {
6009 let end = (pos + chunk).min(ids.len());
6010 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6011 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
6012 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
6013 pos = end;
6014 }
6015 }
6016 if task_mask.is_none()
6025 && !self.graph_prefill_preferred()
6026 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
6027 && self.pair_supported()
6028 {
6029 while pos + 1 < ids.len() {
6030 let e1 = self.embed_single(ids[pos]);
6031 let e2 = self.embed_single(ids[pos + 1]);
6032 let (_, h2) = self.forward_pair(&e1, &e2, pos);
6033 self.check_forward_graph("forward_ids pair", pos + 1)?;
6034 self.commit_linear_scratch();
6035 hidden = h2;
6036 pos += 2;
6037 }
6038 }
6039 while pos < ids.len() {
6040 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6041 self.check_forward_graph("forward_ids", pos)?;
6042 pos += 1;
6043 }
6044 if let Err(err) = self.o1_seal_checked() {
6048 self.clear_sequence_state();
6049 return Err(err);
6050 }
6051 let normed = inference::rms_norm(
6052 &hidden,
6053 &self.weights.final_norm,
6054 self.rms_eps,
6055 self.norm_style,
6056 );
6057 Ok(self.lm_head_forward(&normed))
6058 }
6059
6060 #[doc(hidden)]
6064 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
6065 #[cfg(target_os = "macos")]
6066 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
6067 if ids.is_empty() {
6068 return Err("empty id sequence".to_string());
6069 }
6070 self.clear_sequence_state();
6071 self.dsv41
6072 .as_ref()
6073 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
6074 self.o1_begin();
6075 let rows = {
6076 let pool = self.pool.clone();
6077 let b = self
6078 .dsv41
6079 .as_mut()
6080 .expect("dsv41 checked above; state cannot change during forward");
6081 let mut rows = Vec::with_capacity(ids.len());
6082 for (position, &id) in ids.iter().enumerate() {
6083 let mut logits = Vec::new();
6084 crate::dsv41::forward_token(
6085 &b.0,
6086 &b.1,
6087 &b.2,
6088 &mut b.3,
6089 id,
6090 position,
6091 pool.as_deref(),
6092 &mut logits,
6093 );
6094 rows.push(logits);
6095 }
6096 rows
6097 };
6098 self.o1_seal();
6099 Ok(rows)
6100 }
6101
6102 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6109 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6110 Ok((nll / cnt.max(1) as f64).exp())
6111 }
6112
6113 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6118 self.clear_sequence_state();
6119 FFN_PROBE.with(|p| {
6120 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6121 });
6122 crate::gpu::cpu_scope(|| {
6123 for (pos, &id) in ids.iter().enumerate() {
6124 let emb = self.embed_single(id);
6125 let _ = self.forward_layers(&emb, pos, None);
6126 }
6127 });
6128 self.clear_sequence_state();
6129 FFN_PROBE
6130 .with(|p| p.borrow_mut().take())
6131 .unwrap_or_default()
6132 }
6133
6134 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6138 if let Err(err) = self.nll_begin() {
6139 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6143 self.nll_end();
6144 return Err(err);
6145 }
6146 FFN_PROBE.with(|p| {
6147 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6148 });
6149 let result: Result<(), String> = (|| {
6150 for chunk in ids.chunks(256) {
6151 if chunk.len() < 2 {
6152 continue;
6153 }
6154 self.nll_ids_masked(chunk, 0, None)?;
6155 }
6156 Ok(())
6157 })();
6158 self.nll_end();
6159 let probe = FFN_PROBE
6160 .with(|p| p.borrow_mut().take())
6161 .unwrap_or_default();
6162 match result {
6163 Ok(()) => Ok(probe),
6164 Err(err) => {
6165 drop(probe);
6166 Err(err)
6167 }
6168 }
6169 }
6170
6171 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6175 self.nll_begin()?;
6176 let result: Result<f64, String> = (|| {
6177 let mut nll = 0f64;
6178 let mut cnt = 0usize;
6179 let mut hidden = vec![0f32; self.hidden_size];
6180 for (pos, &id) in ids.iter().enumerate() {
6181 if pos > 0 {
6182 inference::rms_norm_into(
6183 &hidden,
6184 &self.weights.final_norm,
6185 self.rms_eps,
6186 self.norm_style,
6187 &mut self.ws.n1,
6188 );
6189 let mut logits = self.lm_head_forward(&self.ws.n1);
6190 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6191 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6192 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6193 nll -= p.max(1e-300).ln();
6194 cnt += 1;
6195 attention::recycle_buf(&mut logits);
6196 }
6197 let emb = self.embed_single(id);
6198 hidden = self.forward_layers(&emb, pos, Some(mask));
6199 self.nll_check_graph("masked serial forward", pos)?;
6200 let _ = self.graph_logits.take();
6204 }
6205 Ok((nll / cnt.max(1) as f64).exp())
6206 })();
6207 self.nll_end();
6208 result
6209 }
6210
6211 pub fn nll_ids_masked(
6230 &mut self,
6231 ids: &[u32],
6232 start: usize,
6233 task_mask: Option<&TaskMask>,
6234 ) -> Result<(f64, usize), String> {
6235 let task_mask = self.drop_open_mask(task_mask);
6236 self.nll_ids_inner(ids, start, task_mask)
6237 }
6238
6239 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6240 self.nll_ids_inner(ids, start, None)
6241 }
6242
6243 fn nll_ids_inner(
6244 &mut self,
6245 ids: &[u32],
6246 start: usize,
6247 task_mask: Option<&TaskMask>,
6248 ) -> Result<(f64, usize), String> {
6249 self.nll_begin()?;
6250 let result: Result<(f64, usize), String> = (|| {
6251 let mut nll = 0f64;
6252 let mut cnt = 0usize;
6253 let (graph_quality, fused_head_quality) = nll_graph_policy(
6266 task_mask.is_none(),
6267 self.graph_prefill_preferred(),
6268 crate::gpu::q1_force(),
6269 );
6270 self.graph_head_required = fused_head_quality;
6271 self.graph_want_logits = fused_head_quality;
6272 #[cfg(target_os = "macos")]
6273 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6274 match self.nll_batch_metal(ids, start) {
6275 MetalBatchNllOutcome::Completed(nll, count) => {
6276 return Ok((nll, count));
6277 }
6278 MetalBatchNllOutcome::Declined => {}
6279 MetalBatchNllOutcome::Failed(err) => return Err(err),
6280 }
6281 }
6282 if self.can_prefill_batched() && !graph_quality {
6283 const CHUNK: usize = 128;
6289 const LM_SUB: usize = 32;
6290 let n = ids.len().saturating_sub(1);
6291 let hs = self.hidden_size;
6292 let rows = self.weights.lm_head.rows();
6293 let mut pos = 0usize;
6294 while pos < n {
6295 let end = (pos + CHUNK).min(n);
6296 let bsz = end - pos;
6297 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6298 self.nll_check_graph("batched prefill", pos)?;
6299 let mut k0 = 0usize;
6300 while k0 < bsz {
6301 let k1 = (k0 + LM_SUB).min(bsz);
6302 let sb = k1 - k0;
6303 if pos + k1 <= start {
6306 k0 = k1;
6307 continue;
6308 }
6309 let mut normed = vec![0.0f32; sb * hs];
6310 for k in 0..sb {
6311 let r = inference::rms_norm(
6312 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6313 &self.weights.final_norm,
6314 self.rms_eps,
6315 self.norm_style,
6316 );
6317 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6318 }
6319 let mut logits = vec![0.0f32; sb * rows];
6320 self.weights
6321 .lm_head
6322 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6323 for k in 0..sb {
6324 if pos + k0 + k < start {
6325 continue;
6326 }
6327 self.nll_check_graph("batched score row", pos + k0 + k)?;
6328 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6329 if let Some(mu) = self.logit_multiplier {
6330 for v in lg.iter_mut() {
6331 *v *= mu;
6332 }
6333 }
6334 if let Some(c) = self.final_softcap {
6338 for v in lg.iter_mut() {
6339 *v = c * (*v / c).tanh();
6340 }
6341 }
6342 if let Some(cm) = self.head_clusters.clone() {
6345 self.hierarchical_head_logprobs(
6346 &normed[k * hs..(k + 1) * hs],
6347 &cm,
6348 lg,
6349 );
6350 }
6351 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6352 let target = ids[pos + k0 + k + 1] as usize;
6353 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6354 let lse: f64 = lg
6355 .iter()
6356 .map(|&v| ((v - max) as f64).exp())
6357 .sum::<f64>()
6358 .ln()
6359 + max as f64;
6360 nll += lse - lg[target] as f64;
6361 cnt += 1;
6362 if std::env::var("CMF_PPL_TRACE").is_ok() {
6363 let top = lg
6364 .iter()
6365 .enumerate()
6366 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6367 .map(|(i, _)| i)
6368 .unwrap_or(0);
6369 eprintln!(
6370 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6371 pos + k0 + k,
6372 target,
6373 lse - lg[target] as f64,
6374 top,
6375 lg[target],
6376 lg[top]
6377 );
6378 }
6379 }
6380 k0 = k1;
6381 }
6382 pos = end;
6383 }
6384 return Ok((nll, cnt));
6385 }
6386 for pos in 0..ids.len().saturating_sub(1) {
6387 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6388 self.nll_check_graph("serial forward", pos)?;
6389 let out_of_band = self.graph_logits.take();
6397 if self.graph_head_required && out_of_band.is_none() {
6398 METAL_GRAPH_HEAD_MISS.fetch_add(
6399 1,
6400 std::sync::atomic::Ordering::Relaxed,
6401 );
6402 return Err(format!(
6403 "fused Metal graph head did not complete at NLL position {pos}"
6404 ));
6405 }
6406 if pos < start {
6407 continue;
6408 }
6409 let logits = match out_of_band {
6410 Some(lg) => lg,
6411 None => {
6412 let normed = inference::rms_norm(
6413 &hidden,
6414 &self.weights.final_norm,
6415 self.rms_eps,
6416 self.norm_style,
6417 );
6418 self.lm_head_forward(&normed)
6422 }
6423 };
6424 let target = ids[pos + 1] as usize;
6425 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6426 let lse: f64 = logits
6427 .iter()
6428 .map(|&v| ((v - max) as f64).exp())
6429 .sum::<f64>()
6430 .ln()
6431 + max as f64;
6432 let tok_nll = lse - logits[target] as f64;
6433 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6434 let top = logits
6435 .iter()
6436 .enumerate()
6437 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6438 .map(|(i, _)| i)
6439 .unwrap_or(0);
6440 eprintln!(
6441 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6442 logits[target], logits[top]
6443 );
6444 }
6445 nll += tok_nll;
6446 cnt += 1;
6447 }
6448 Ok((nll, cnt))
6449 })();
6450 self.nll_end();
6451 result
6452 }
6453
6454 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6459 let normed = inference::rms_norm(
6460 hidden,
6461 &self.weights.final_norm,
6462 self.rms_eps,
6463 self.norm_style,
6464 );
6465 let mut logits = self.lm_head_forward(&normed);
6468 let target = target as usize;
6469 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6470 let lse: f64 = logits
6471 .iter()
6472 .map(|&v| ((v - max) as f64).exp())
6473 .sum::<f64>()
6474 .ln()
6475 + max as f64;
6476 let tok_nll = lse - logits[target] as f64;
6477 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6478 let top = logits
6479 .iter()
6480 .enumerate()
6481 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6482 .map(|(i, _)| i)
6483 .unwrap_or(0);
6484 eprintln!(
6485 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6486 logits[target], logits[top]
6487 );
6488 }
6489 attention::recycle_buf(&mut logits);
6490 tok_nll
6491 }
6492
6493 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6511 self.nll_begin()?;
6516 let requested_prefix = (prefill > 0).then_some(prefill);
6517 self.o1_begin_with_prefix(requested_prefix);
6518 let n = ids.len().saturating_sub(1);
6519 let requested_start = prefill.min(n);
6520 let exact_end = if self.o1_active() {
6525 match requested_prefix {
6526 Some(requested) => self.o1_effective_boundary(requested),
6527 None => self
6528 .o1_cfg
6529 .as_ref()
6530 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6531 }
6532 .unwrap_or(requested_start)
6533 .min(n)
6534 } else {
6535 requested_start
6536 };
6537 let mut nll = 0f64;
6538 let mut cnt = 0usize;
6539
6540 let mut pos = 0usize;
6544 if self.can_prefill_batched() {
6545 const CHUNK: usize = 128;
6546 while pos < exact_end {
6547 let end = (pos + CHUNK).min(exact_end);
6548 let hiddens = self.prefill_batch(&ids[pos..end], pos);
6549 if self
6550 .graph_failed
6551 .swap(false, std::sync::atomic::Ordering::Relaxed)
6552 {
6553 self.cancel
6554 .store(false, std::sync::atomic::Ordering::Relaxed);
6555 self.nll_end();
6556 return Err("GPU graph failed during O(1) NLL prefix".into());
6557 }
6558 for row in 0..end - pos {
6559 let score_pos = pos + row;
6560 if score_pos >= requested_start && score_pos < n {
6561 nll += self.nll_from_hidden(
6562 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6563 ids[score_pos + 1],
6564 score_pos,
6565 );
6566 cnt += 1;
6567 }
6568 }
6569 pos = end;
6570 }
6571 } else {
6572 while pos < exact_end {
6573 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6574 if self
6575 .graph_failed
6576 .swap(false, std::sync::atomic::Ordering::Relaxed)
6577 {
6578 self.cancel
6579 .store(false, std::sync::atomic::Ordering::Relaxed);
6580 self.nll_end();
6581 return Err("GPU graph failed during O(1) NLL prefix".into());
6582 }
6583 if pos >= requested_start && pos < n {
6584 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6585 cnt += 1;
6586 }
6587 pos += 1;
6588 }
6589 }
6590 self.o1_seal_checked().map_err(|err| {
6591 self.nll_end();
6592 err
6593 })?;
6594
6595 let batch_k = std::env::var("CMF_BATCH_K")
6604 .ok()
6605 .and_then(|v| v.parse::<usize>().ok())
6606 .unwrap_or(0);
6607 let batch_admitted = batch_k > 0
6608 && self.can_prefill_batched()
6609 && self.o1_active()
6610 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6611 && (0..self.num_layers).all(|li| {
6612 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6613 cache.o1.is_none() || cache.o1_views().is_some()
6614 });
6615 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6616 eprintln!(
6617 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6618 batch_admitted,
6619 batch_k,
6620 n.saturating_sub(exact_end),
6621 );
6622 }
6623 let mut batch_completed = false;
6624 if batch_admitted && exact_end < n {
6625 let hs = self.hidden_size;
6626 let mut batch_pos = exact_end;
6627 while batch_pos < n {
6628 let end = (batch_pos + batch_k).min(n);
6629 let bk = end - batch_pos;
6630 let mut hiddens = vec![0.0f32; bk * hs];
6631 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6632 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6633 }
6634 let positions: Vec<usize> = (batch_pos..end).collect();
6635 let t_batch = std::time::Instant::now();
6636 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6637 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6638 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6639 eprintln!(
6640 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6641 batch_pos,
6642 end.saturating_sub(1),
6643 bk as f64 / (ms / 1000.0),
6644 );
6645 }
6646 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6647 self.nll_end();
6648 return Err(err);
6649 }
6650 match outcome {
6651 crate::gpu::BatchGraphOutcome::Completed => {
6652 batch_completed = true;
6653 for row in 0..bk {
6654 nll += self.nll_from_hidden(
6655 &hiddens[row * hs..(row + 1) * hs],
6656 ids[batch_pos + row + 1],
6657 batch_pos + row,
6658 );
6659 cnt += 1;
6660 }
6661 batch_pos = end;
6662 }
6663 crate::gpu::BatchGraphOutcome::Declined => {
6664 if batch_completed {
6665 self.nll_end();
6666 return Err(format!(
6667 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6668 ));
6669 }
6670 break;
6671 }
6672 crate::gpu::BatchGraphOutcome::Failed => {
6673 self.nll_end();
6674 return Err(format!(
6675 "O(1) NLL batch graph failed after admission at position {batch_pos}"
6676 ));
6677 }
6678 }
6679 }
6680 if batch_completed && cnt == n.saturating_sub(requested_start) {
6681 self.nll_end();
6682 return Ok((nll, cnt));
6683 }
6684 }
6685
6686 for pos in exact_end..n {
6691 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6692 if self
6693 .graph_failed
6694 .swap(false, std::sync::atomic::Ordering::Relaxed)
6695 {
6696 self.cancel
6697 .store(false, std::sync::atomic::Ordering::Relaxed);
6698 self.nll_end();
6699 return Err(format!(
6700 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6701 ));
6702 }
6703 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6704 cnt += 1;
6705 }
6706 self.nll_end();
6707 Ok((nll, cnt))
6708 }
6709
6710 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6718 self.clear_sequence_state();
6719 let n = ids.len().saturating_sub(1);
6720 let mut correct = Vec::with_capacity(n);
6721 let mut pmax = Vec::with_capacity(n);
6722 for pos in 0..n {
6723 let emb = self.embed_single(ids[pos]);
6724 let hidden = self.forward_layers(&emb, pos, None);
6725 let normed = inference::rms_norm(
6726 &hidden,
6727 &self.weights.final_norm,
6728 self.rms_eps,
6729 self.norm_style,
6730 );
6731 let logits = self.lm_head_forward(&normed);
6735 let target = ids[pos + 1] as usize;
6736 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6737 for (i, &v) in logits.iter().enumerate() {
6738 if v > mval {
6739 mval = v;
6740 amax = i;
6741 }
6742 }
6743 correct.push(amax == target);
6744 let row: Vec<f32> = temps
6745 .iter()
6746 .map(|&t| {
6747 let tt = t.max(1e-3);
6748 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6749 1.0 / s.max(1e-12) })
6751 .collect();
6752 pmax.push(row);
6753 }
6754 self.clear_sequence_state();
6755 (correct, pmax)
6756 }
6757
6758 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6765 if self.dyn_router.is_none() {
6766 return Ok((self.ppl_ids(ids)?, 0));
6767 }
6768 self.nll_begin()?;
6769 let saved_active = self.dyn_active;
6770 let mut router = self
6771 .dyn_router
6772 .take()
6773 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6774 router.reset();
6775 self.dyn_phi_seen = 0;
6776 let _ = self.set_active_skill(None);
6777
6778 let result: Result<(f64, usize), String> = (|| {
6779 let mut nll = 0f64;
6780 let mut cnt = 0usize;
6781 for pos in 0..ids.len().saturating_sub(1) {
6782 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6783 self.nll_check_graph("dynamic serial forward", pos)?;
6784 let out_of_band = self.graph_logits.take();
6785 let mut logits = match out_of_band {
6786 Some(lg) => lg,
6787 None => {
6788 let normed = inference::rms_norm(
6789 &hidden,
6790 &self.weights.final_norm,
6791 self.rms_eps,
6792 self.norm_style,
6793 );
6794 self.lm_head_forward(&normed)
6798 }
6799 };
6800 let target = ids[pos + 1] as usize;
6801 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6802 let lse: f64 = logits
6803 .iter()
6804 .map(|&v| ((v - max) as f64).exp())
6805 .sum::<f64>()
6806 .ln()
6807 + max as f64;
6808 let tok_nll = lse - logits[target] as f64;
6809 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6810 let top = logits
6811 .iter()
6812 .enumerate()
6813 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6814 .map(|(i, _)| i)
6815 .unwrap_or(0);
6816 eprintln!(
6817 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6818 logits[target], logits[top]
6819 );
6820 }
6821 nll += tok_nll;
6822 cnt += 1;
6823 attention::recycle_buf(&mut logits);
6824 let phi = self.dyn_phi_ema.clone();
6826 if let Some(new_active) = router.step(&phi, pos) {
6827 let _ = self.set_active_skill(new_active);
6828 }
6829 }
6830 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6831 })();
6832
6833 let _ = self.set_active_skill(saved_active);
6836 self.dyn_router = Some(router);
6837 self.nll_end();
6838 result
6839 }
6840
6841 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6843 self.clear_sequence_state();
6844 let mut acc = vec![0f32; self.hidden_size];
6845 for (pos, &id) in ids.iter().enumerate() {
6846 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6847 for (a, v) in acc.iter_mut().zip(&h) {
6848 *a += v;
6849 }
6850 }
6851 let n = ids.len().max(1) as f32;
6852 for a in acc.iter_mut() {
6853 *a /= n;
6854 }
6855 self.clear_sequence_state();
6856 acc
6857 }
6858
6859 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6865 self.prefill_batch_masked(ids, start_pos, None)
6866 }
6867
6868 fn prefill_batch_masked(
6874 &mut self,
6875 ids: &[u32],
6876 start_pos: usize,
6877 task_mask: Option<&TaskMask>,
6878 ) -> Vec<f32> {
6879 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6880 }
6881
6882 fn prefill_batch_span(
6888 &mut self,
6889 input: PrefillIn<'_>,
6890 start_pos: usize,
6891 task_mask: Option<&TaskMask>,
6892 from: usize,
6893 upto_excl: usize,
6894 ) -> Vec<f32> {
6895 let hs = self.hidden_size;
6896 let b = match input {
6897 PrefillIn::Ids(ids) => ids.len(),
6898 PrefillIn::Hidden(hb) => hb.len() / hs,
6899 };
6900 let upto_excl = upto_excl.min(self.num_layers);
6901 let mut h: Vec<f32>;
6905 let mut h_ready;
6906 match input {
6907 PrefillIn::Ids(_) => {
6908 h = vec![0.0; b * hs];
6909 h_ready = false;
6910 }
6911 PrefillIn::Hidden(hb) => {
6912 h = hb.to_vec();
6913 h_ready = true;
6914 }
6915 }
6916 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6917 if let PrefillIn::Ids(ids) = input {
6918 for (bi, &id) in ids.iter().enumerate() {
6919 let e = me.embed_single(id);
6920 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6921 }
6922 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6923 if let Ok(t) = tp.parse::<usize>() {
6924 if t >= start_pos && t < start_pos + ids.len() {
6925 let bi = t - start_pos;
6926 let row = &h[bi * hs..(bi + 1) * hs];
6927 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6928 eprintln!(
6929 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6930 ids[bi],
6931 row[0],
6932 row[1],
6933 ids.len(),
6934 &ids[..ids.len().min(8)]
6935 );
6936 }
6937 }
6938 }
6939 }
6940 };
6941 let (_nkv, _hd, _rd, eps) = (
6942 self.num_kv_heads,
6943 self.head_dim,
6944 self.rotary_dim,
6945 self.rms_eps,
6946 );
6947 let pool = self.pool.clone();
6948 let norm_style = self.norm_style;
6949 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6950
6951 #[cfg(target_os = "macos")]
6952 let mut chunk_skip_until = 0usize;
6953 for li in from..upto_excl {
6954 let _capacity_tail = automatic_gpu_prefix
6955 .filter(|&prefix| li >= prefix)
6956 .map(|_| crate::gpu::enter_cpu_scope());
6957 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6964 if task_mask.is_none() {
6965 if li < chunk_skip_until {
6966 continue;
6967 }
6968 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6974 fill_h(&mut h, self);
6975 h_ready = true;
6976 }
6977 let ids_for_embed = match input {
6978 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6979 PrefillIn::Hidden(_) => None,
6980 };
6981 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6982 if end > li {
6983 h_ready = true;
6984 chunk_skip_until = end;
6985 if self.is_loop_end(end - 1) && end < self.num_layers {
6988 for bi in 0..b {
6989 let normed = inference::rms_norm(
6990 &h[bi * hs..(bi + 1) * hs],
6991 &self.weights.final_norm,
6992 eps,
6993 norm_style,
6994 );
6995 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6996 }
6997 }
6998 continue;
6999 }
7000 }
7001 if !h_ready {
7002 fill_h(&mut h, self);
7003 h_ready = true;
7004 }
7005 let lw = &self.weights.layers[self.phys_layer(li)];
7006 match &lw.attn {
7008 AttnKind::Kda(w) => {
7009 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
7011 let mut normed = vec![0.0f32; b * hs];
7012 for bi in 0..b {
7013 inference::rms_norm_into(
7014 &h[bi * hs..(bi + 1) * hs],
7015 &lw.input_norm,
7016 eps,
7017 norm_style,
7018 &mut normed[bi * hs..(bi + 1) * hs],
7019 );
7020 }
7021 let attn = crate::linear_core::kda_forward_batch(
7022 &normed,
7023 b,
7024 w,
7025 &cfg,
7026 &mut self.kv_cache.layers[li].linear_state,
7027 pool.as_deref(),
7028 );
7029 for (dst, &a) in h.iter_mut().zip(&attn) {
7030 *dst += a;
7031 }
7032 }
7033 AttnKind::LinearGdn(w) => {
7034 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7036 let mut normed = vec![0.0f32; b * hs];
7037 for bi in 0..b {
7038 let r = inference::rms_norm(
7039 &h[bi * hs..(bi + 1) * hs],
7040 &lw.input_norm,
7041 eps,
7042 norm_style,
7043 );
7044 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7045 }
7046 let attn = crate::linear_core::gdn_forward_batch(
7047 &normed,
7048 b,
7049 w,
7050 &cfg,
7051 &mut self.kv_cache.layers[li].linear_state,
7052 pool.as_deref(),
7053 );
7054 for (dst, &a) in h.iter_mut().zip(&attn) {
7055 *dst += a;
7056 }
7057 }
7058 AttnKind::ShortConv(w) => {
7059 let cfg = self
7062 .short_conv_cfg
7063 .expect("short-conv layer without short_conv_cfg");
7064 let mut normed = vec![0.0f32; b * hs];
7065 for bi in 0..b {
7066 inference::rms_norm_into(
7067 &h[bi * hs..(bi + 1) * hs],
7068 &lw.input_norm,
7069 eps,
7070 norm_style,
7071 &mut normed[bi * hs..(bi + 1) * hs],
7072 );
7073 }
7074 let attn = short_conv_forward_batch(
7075 &normed,
7076 b,
7077 w,
7078 &cfg,
7079 &mut self.kv_cache.layers[li].linear_state,
7080 pool.as_deref(),
7081 );
7082 for (dst, &a) in h.iter_mut().zip(&attn) {
7083 *dst += a;
7084 }
7085 }
7086 AttnKind::Mla(w) => {
7087 let inv_freq_l = self.layer_inv_freq(li);
7090 let rs = self.layer_rope_scale(li);
7091 let mut normed = vec![0.0f32; hs];
7092 for bi in 0..b {
7093 inference::rms_norm_into(
7094 &h[bi * hs..(bi + 1) * hs],
7095 &lw.input_norm,
7096 eps,
7097 norm_style,
7098 &mut normed,
7099 );
7100 let ao = mla_attention(
7101 w,
7102 &normed,
7103 &mut self.kv_cache.layers[li],
7104 start_pos + bi,
7105 &inv_freq_l,
7106 rs,
7107 eps,
7108 pool.as_deref(),
7109 );
7110 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7111 *dst += a;
7112 }
7113 }
7114 }
7115 AttnKind::Full {
7116 wq,
7117 wk,
7118 wv,
7119 wo,
7120 q_norm,
7121 k_norm,
7122 output_gate,
7123 softplus_gate,
7124 bias,
7125 } => {
7126 let mut normed = vec![0.0f32; b * hs];
7130 for bi in 0..b {
7131 inference::rms_norm_into(
7132 &h[bi * hs..(bi + 1) * hs],
7133 &lw.input_norm,
7134 eps,
7135 norm_style,
7136 &mut normed[bi * hs..(bi + 1) * hs],
7137 );
7138 }
7139 let inv_freq_l = self.layer_inv_freq(li);
7140 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7141 let cfg = QwenAttnCfg {
7142 num_heads: self.layer_num_heads(li),
7143 num_kv_heads: nkv_l,
7144 head_dim: hd_l,
7145 hidden_size: hs,
7146 position: start_pos,
7147 inv_freq: &inv_freq_l,
7148 rotary_dim: rd_l,
7149 scale: self.attn_scale,
7150 softcap: self.attn_softcap,
7151 window: self.layer_window(li),
7152 v_norm: self.attn_v_norm,
7153 qk_norm_after_rope: self.qk_norm_after_rope,
7154 q_norm: q_norm.as_deref(),
7155 k_norm: k_norm.as_deref(),
7156 output_gate: *output_gate,
7157 softplus_gate: softplus_gate
7158 .as_ref()
7159 .map(|(gate, per_head)| (gate, *per_head)),
7160 rope_scale: self.layer_rope_scale(li),
7161 bias: bias
7162 .as_ref()
7163 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7164 rms_eps: eps,
7165 norm_style,
7166 pool: pool.as_deref(),
7167 };
7168 let mut attn = attention::qwen_attention_batch(
7169 &normed,
7170 b,
7171 wq,
7172 wk,
7173 wv,
7174 wo,
7175 &mut self.kv_cache.layers[li],
7176 &cfg,
7177 );
7178 if let Some(w) = &lw.attn_out_norm {
7179 for bi in 0..b {
7180 inference::rms_norm_into(
7181 &attn[bi * hs..(bi + 1) * hs],
7182 w,
7183 eps,
7184 norm_style,
7185 &mut normed[bi * hs..(bi + 1) * hs],
7186 );
7187 }
7188 attn.copy_from_slice(&normed);
7189 }
7190 for (dst, &a) in h.iter_mut().zip(&attn) {
7191 *dst += a;
7192 }
7193 }
7194 AttnKind::Linear(w) => {
7195 for bi in 0..b {
7196 let normed = inference::rms_norm(
7197 &h[bi * hs..(bi + 1) * hs],
7198 &lw.input_norm,
7199 eps,
7200 norm_style,
7201 );
7202 vmf_phase_forward(
7203 &normed,
7204 w,
7205 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7206 &mut self.kv_cache.layers[li].linear_state,
7207 pool.as_deref(),
7208 )
7209 .iter()
7210 .enumerate()
7211 .for_each(|(i, &a)| h[bi * hs + i] += a);
7212 }
7213 }
7214 }
7215
7216 let lw = &self.weights.layers[self.phys_layer(li)];
7218 let mut post = vec![0.0f32; b * hs];
7219 for bi in 0..b {
7220 let r =
7221 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7222 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7223 }
7224 let mask_row = task_mask
7227 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7228 .and_then(|m| m.ffn_masks.get(li))
7229 .map(|v| v.as_slice());
7230 let mut ffn = match &lw.ffn {
7231 FfnKind::Dense(d) if !d.segs.is_empty() => {
7232 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7233 }
7234 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7235 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7236 FfnKind::DenseMoe(dm) => {
7239 let mut out = vec![0.0f32; b * hs];
7240 for bi in 0..b {
7241 let r = dense_moe_ffn(
7242 dm,
7243 &post[bi * hs..(bi + 1) * hs],
7244 &h[bi * hs..(bi + 1) * hs],
7245 eps,
7246 norm_style,
7247 pool.as_deref(),
7248 );
7249 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7250 }
7251 out
7252 }
7253 };
7254 if let Some(w) = &lw.ffn_out_norm {
7255 for bi in 0..b {
7256 inference::rms_norm_into(
7257 &ffn[bi * hs..(bi + 1) * hs],
7258 w,
7259 eps,
7260 norm_style,
7261 &mut post[bi * hs..(bi + 1) * hs],
7262 );
7263 }
7264 ffn.copy_from_slice(&post);
7265 }
7266 for (dst, &f) in h.iter_mut().zip(&ffn) {
7267 *dst += f;
7268 }
7269 if let Some(sc) = lw.layer_scale {
7270 for v in h.iter_mut() {
7271 *v *= sc;
7272 }
7273 }
7274 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7275 if let Ok(t) = tp.parse::<usize>() {
7276 if t >= start_pos && t < start_pos + b {
7277 let bi = t - start_pos;
7278 let row = &h[bi * hs..(bi + 1) * hs];
7279 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7280 eprintln!(
7281 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7282 row[0], row[1]
7283 );
7284 }
7285 }
7286 }
7287 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7291 let row = &h[(b - 1) * hs..b * hs];
7292 let rms =
7293 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7294 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7295 eprintln!(
7296 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7297 match &self.weights.layers[self.phys_layer(li)].attn {
7298 AttnKind::LinearGdn(_) => "gdn",
7299 AttnKind::Linear(_) => "vmf",
7300 AttnKind::ShortConv(_) => "conv",
7301 _ => "attn",
7302 },
7303 match &lw.ffn {
7304 FfnKind::Moe(_) => "moe",
7305 FfnKind::Dense(_) => "dense",
7306 FfnKind::DenseMoe(_) => "dense+moe",
7307 },
7308 );
7309 }
7310 if self.is_loop_end(li) && li + 1 < self.num_layers {
7312 for bi in 0..b {
7313 let normed = inference::rms_norm(
7314 &h[bi * hs..(bi + 1) * hs],
7315 &self.weights.final_norm,
7316 eps,
7317 norm_style,
7318 );
7319 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7320 }
7321 }
7322 if std::env::var("CMF_TRACE_H").is_ok() {
7323 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7324 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7325 eprintln!(
7326 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7327 lw.layer_scale
7328 );
7329 }
7330 }
7331 crate::gpu::set_layer(-1); self.o1_progress();
7337 h
7338 }
7339
7340 fn embed_single(&self, id: u32) -> Vec<f32> {
7342 let mut out = vec![0.0f32; self.hidden_size];
7343 if (id as usize) < self.weights.embed_tokens.rows() {
7344 self.weights.embed_tokens.row_f32(id as usize, &mut out);
7345 }
7346 if self.embed_multiplier != 1.0 {
7347 for v in out.iter_mut() {
7348 *v *= self.embed_multiplier;
7349 }
7350 }
7351 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7355 let mut v = vec![0.0f32; self.hidden_size.max(1)];
7356 v[0] = id as f32;
7357 return v;
7358 }
7359 if let Some(b) = &self.g3n {
7362 return b.0.extend_embedding(id, &out, self.pool.as_deref());
7363 }
7364 out
7365 }
7366
7367 #[cfg(target_os = "macos")]
7373 fn chunk_run_gpu(
7374 &mut self,
7375 li0: usize,
7376 h: &mut [f32],
7377 b: usize,
7378 pos0: usize,
7379 embed_ids: Option<&[u32]>,
7380 cap: usize,
7381 ) -> usize {
7382 if !crate::gpu::enabled_here()
7386 || std::env::var("CMF_GPU_CHUNK")
7387 .map(|v| v == "0")
7388 .unwrap_or(false)
7389 || b < 32
7390 || self.swa.is_some()
7391 || self.global_attn.is_some()
7392 || self.o1_active()
7395 || self.attn_v_norm
7396 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7397 {
7398 return li0;
7399 }
7400 let Some(model) = self.model.clone() else {
7401 return li0;
7402 };
7403 let inv_freq = self.inv_freq.clone();
7404 let (nh, nkv, hd, hs) = (
7405 self.num_heads,
7406 self.num_kv_heads,
7407 self.head_dim,
7408 self.hidden_size,
7409 );
7410 let loop_end = if self.loop_final_norm {
7414 ((li0 / self.physical_layers) + 1) * self.physical_layers
7415 } else {
7416 self.num_layers
7417 };
7418 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7419 let mut stored_at: Vec<usize> = Vec::new();
7420 for li in li0..self.num_layers.min(loop_end).min(cap) {
7421 let lw = &self.weights.layers[self.phys_layer(li)];
7422 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7423 break;
7424 }
7425 let AttnKind::Full {
7426 wq,
7427 wk,
7428 wv,
7429 wo,
7430 q_norm,
7431 k_norm,
7432 output_gate: false,
7433 softplus_gate: None,
7434 bias,
7435 } = &lw.attn
7436 else {
7437 break;
7438 };
7439 let FfnKind::Dense(d) = &lw.ffn else { break };
7440 if d.act != Act::Silu || !d.segs.is_empty() {
7441 break;
7442 }
7443 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7448 t.q8_row_parts()
7449 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7450 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7451 }
7452 let parts = (
7453 cw(wq),
7454 cw(wk),
7455 cw(wv),
7456 cw(wo),
7457 cw(&d.gate_proj),
7458 cw(&d.up_proj),
7459 cw(&d.down_proj),
7460 );
7461 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7462 else {
7463 break;
7464 };
7465 let layer = &self.kv_cache.layers[li];
7466 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7467 break;
7468 }
7469 stored_at.push(layer.head_len(0));
7470 layers.push(crate::gpu_metal::ChunkLayer {
7471 model: &model,
7472 kv_id: self.graph_kv_id,
7473 layer: li,
7474 wq: pq,
7475 wk: pk,
7476 wv: pv,
7477 wo: po,
7478 gate: pg,
7479 up: pu,
7480 down: pd,
7481 input_norm: &lw.input_norm,
7482 post_norm: &lw.post_norm,
7483 bias: bias
7484 .as_ref()
7485 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7486 q_norm: q_norm.as_deref(),
7487 k_norm: k_norm.as_deref(),
7488 inv_freq: &inv_freq,
7489 rd: self.rotary_dim,
7490 nh,
7491 nkv,
7492 hd,
7493 hs,
7494 inter: d.gate_proj.rows(),
7495 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7496 late_qk_norm: self.qk_norm_after_rope,
7497 eps: self.rms_eps as f32,
7498 });
7499 }
7500 if layers.is_empty() {
7501 return li0;
7502 }
7503 let row = nkv * hd;
7504 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7505 .iter()
7506 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7507 .collect();
7508 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7509 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7510 let li = layers[i].layer;
7511 let layer = &self.kv_cache.layers[li];
7512 io.push(crate::gpu_metal::ChunkIo {
7513 cpu_stored: stored_at[i],
7514 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7515 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7516 out_k: ok,
7517 out_v: ov,
7518 imp: oi,
7519 });
7520 }
7521 let n_run = layers.len();
7522 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7523 let ep = embed_ids.and_then(|ids| {
7526 self.weights
7527 .embed_tokens
7528 .q8_row_parts()
7529 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7530 idx,
7531 rows,
7532 row_scale: rs,
7533 ids,
7534 mult: self.embed_multiplier,
7535 })
7536 });
7537 if embed_ids.is_some() && ep.is_none() {
7538 return li0;
7539 }
7540 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7541 return li0;
7542 }
7543 drop(io);
7544 drop(layers);
7545 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7548 let li = li0 + i;
7549 let layer = &mut self.kv_cache.layers[li];
7550 for bi in 0..b {
7551 layer.append(
7552 &ok[bi * row..(bi + 1) * row],
7553 &ov[bi * row..(bi + 1) * row],
7554 &[],
7555 );
7556 }
7557 layer.accumulate_imp(oi);
7558 }
7559 last
7560 }
7561
7562 fn layer_is_local(&self, li: usize) -> bool {
7565 if let Some(layers) = &self.sliding_layers {
7566 return layers.get(li).copied().unwrap_or(false);
7567 }
7568 match self.swa {
7569 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7570 None => false,
7571 }
7572 }
7573
7574 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7577 if self.layer_is_local(li) {
7578 if let Some(f) = &self.inv_freq_local {
7579 return f.clone();
7580 }
7581 } else if let Some(f) = &self.inv_freq_global {
7582 return f.clone();
7583 }
7584 self.inv_freq.clone()
7585 }
7586
7587 fn layer_window(&self, li: usize) -> Option<usize> {
7589 self.swa
7590 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7591 }
7592
7593 fn layer_num_heads(&self, li: usize) -> usize {
7594 self.attention_heads_per_layer
7595 .as_ref()
7596 .and_then(|v| v.get(li).copied())
7597 .unwrap_or(self.num_heads)
7598 }
7599
7600 fn layer_rope_scale(&self, li: usize) -> f32 {
7601 if self.layer_is_local(li) {
7602 self.rope_scale_local
7603 } else {
7604 self.rope_scale
7605 }
7606 }
7607
7608 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7611 if !self.layer_is_local(li) {
7612 if let Some((ghd, gkv)) = self.global_attn {
7613 return (gkv, ghd, ghd);
7614 }
7615 }
7616 (
7617 self.num_kv_heads,
7618 self.head_dim,
7619 if self.layer_is_local(li) {
7620 self.rotary_dim_local.unwrap_or(self.rotary_dim)
7621 } else {
7622 self.rotary_dim
7623 },
7624 )
7625 }
7626
7627 fn forward_layers(
7629 &mut self,
7630 hidden: &[f32],
7631 position: usize,
7632 task_mask: Option<&TaskMask>,
7633 ) -> Vec<f32> {
7634 let out = self.forward_layers_upto(hidden, position, task_mask, None);
7635 self.o1_progress();
7636 out
7637 }
7638
7639 pub fn embed_id(&self, id: u32) -> Vec<f32> {
7647 self.embed_single(id)
7648 }
7649
7650 pub fn split_supported(&self) -> Result<(), String> {
7654 if self.dsv4.is_some() {
7655 return Err(
7656 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7657 );
7658 }
7659 if self.dsv41.is_some() {
7660 return Err(
7661 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7662 .into(),
7663 );
7664 }
7665 if self.qwen4_exp.is_some() {
7666 return Err(
7667 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7668 );
7669 }
7670 if self.g3n.is_some() {
7671 return Err(
7672 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7673 );
7674 }
7675 Ok(())
7676 }
7677
7678 pub fn forward_span(
7683 &mut self,
7684 hidden: &[f32],
7685 position: usize,
7686 from: usize,
7687 upto: usize,
7688 task_mask: Option<&TaskMask>,
7689 ) -> Result<Vec<f32>, String> {
7690 self.split_supported()?;
7691 if from > upto || upto >= self.num_layers {
7692 return Err(format!(
7693 "forward_span: layer range {from}..={upto} outside 0..{}",
7694 self.num_layers
7695 ));
7696 }
7697 if hidden.len() != self.hidden_size {
7698 return Err(format!(
7699 "forward_span: hidden len {} ≠ hidden_size {}",
7700 hidden.len(),
7701 self.hidden_size
7702 ));
7703 }
7704 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7705 self.o1_progress();
7706 if self
7707 .graph_failed
7708 .swap(false, std::sync::atomic::Ordering::Relaxed)
7709 {
7710 self.cancel
7711 .store(false, std::sync::atomic::Ordering::Relaxed);
7712 self.clear_sequence_state();
7713 return Err("forward_span: deferred O(1) transition failed".into());
7714 }
7715 Ok(out)
7716 }
7717
7718 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7721 let normed = inference::rms_norm(
7722 hidden,
7723 &self.weights.final_norm,
7724 self.rms_eps,
7725 self.norm_style,
7726 );
7727 self.lm_head_forward(&normed)
7728 }
7729
7730 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7732 sampler::sample_with_scratch(
7733 logits,
7734 &self.sampler_config,
7735 past_tokens,
7736 &mut self.rng,
7737 &mut self.sampler_scratch,
7738 )
7739 }
7740
7741 pub fn reset_session(&mut self) {
7743 self.clear_sequence_state();
7744 }
7745
7746 pub fn prefill_span_ids(
7752 &mut self,
7753 ids: &[u32],
7754 start_pos: usize,
7755 upto: usize,
7756 task_mask: Option<&TaskMask>,
7757 ) -> Result<Vec<f32>, String> {
7758 self.split_supported()?;
7759 if upto >= self.num_layers {
7760 return Err(format!(
7761 "prefill_span_ids: upto {upto} outside 0..{}",
7762 self.num_layers
7763 ));
7764 }
7765 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7769 let out =
7770 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7771 self.check_o1_progress_failure("prefill_span_ids")?;
7772 Ok(out)
7773 } else {
7774 let hs = self.hidden_size;
7775 let mut out = Vec::with_capacity(ids.len() * hs);
7776 for (i, &id) in ids.iter().enumerate() {
7777 let emb = self.embed_id(id);
7778 out.extend_from_slice(&self.forward_span(
7779 &emb,
7780 start_pos + i,
7781 0,
7782 upto,
7783 task_mask,
7784 )?);
7785 }
7786 Ok(out)
7787 }
7788 }
7789
7790 pub fn prefill_span_hidden(
7793 &mut self,
7794 hidden: &[f32],
7795 start_pos: usize,
7796 from: usize,
7797 upto: usize,
7798 task_mask: Option<&TaskMask>,
7799 ) -> Result<Vec<f32>, String> {
7800 self.split_supported()?;
7801 let hs = self.hidden_size;
7802 if hidden.is_empty() || hidden.len() % hs != 0 {
7803 return Err(format!(
7804 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7805 hidden.len()
7806 ));
7807 }
7808 if from > upto || upto >= self.num_layers {
7809 return Err(format!(
7810 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7811 self.num_layers
7812 ));
7813 }
7814 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7815 let out = self.prefill_batch_span(
7816 PrefillIn::Hidden(hidden),
7817 start_pos,
7818 task_mask,
7819 from,
7820 upto + 1,
7821 );
7822 self.check_o1_progress_failure("prefill_span_hidden")?;
7823 Ok(out)
7824 } else {
7825 let b = hidden.len() / hs;
7826 let mut out = Vec::with_capacity(hidden.len());
7827 for i in 0..b {
7828 let h = self.forward_span(
7829 &hidden[i * hs..(i + 1) * hs],
7830 start_pos + i,
7831 from,
7832 upto,
7833 task_mask,
7834 )?;
7835 out.extend_from_slice(&h);
7836 }
7837 Ok(out)
7838 }
7839 }
7840
7841 fn try_token_graph_wgpu(
7845 &self,
7846 hidden: &[f32],
7847 position: usize,
7848 logits_out: &mut Vec<f32>,
7849 layers_run: &mut usize,
7850 ) -> Option<Result<Vec<f32>, ()>> {
7851 self.try_token_graph_wgpu_steps(
7852 hidden,
7853 position,
7854 logits_out,
7855 1,
7856 None,
7857 Some(layers_run),
7858 0,
7859 self.num_layers,
7860 )
7861 }
7862
7863 fn try_token_graph_wgpu_span(
7867 &self,
7868 hidden: &[f32],
7869 position: usize,
7870 logits_out: &mut Vec<f32>,
7871 from: usize,
7872 upto_excl: usize,
7873 layers_run: &mut usize,
7874 ) -> Option<Result<Vec<f32>, ()>> {
7875 self.try_token_graph_wgpu_steps(
7876 hidden,
7877 position,
7878 logits_out,
7879 1,
7880 None,
7881 Some(layers_run),
7882 from,
7883 upto_excl,
7884 )
7885 }
7886
7887 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7891 if self.o1_active() || self.attn_softcap > 0.0 {
7892 return None;
7893 }
7894 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7895 if !graph_on || crate::gpu::graph_unsupported() {
7896 return None;
7903 }
7904 let emb = self.embed_single(t_next);
7905 let mut lg = Vec::new();
7906 let mut ids = Vec::new();
7907 match self.try_token_graph_wgpu_steps(
7908 &emb,
7909 position,
7910 &mut lg,
7911 k,
7912 Some(&mut ids),
7913 None,
7914 0,
7915 self.num_layers,
7916 ) {
7917 Some(Ok(_)) => {}
7918 Some(Err(())) => {
7919 self.graph_failed
7924 .store(true, std::sync::atomic::Ordering::Relaxed);
7925 return None;
7926 }
7927 None => return None,
7928 }
7929 (ids.len() == k).then_some(ids)
7930 }
7931
7932 fn try_token_graph_wgpu_steps(
7936 &self,
7937 hidden: &[f32],
7938 position: usize,
7939 logits_out: &mut Vec<f32>,
7940 steps: usize,
7941 ids_out: Option<&mut Vec<u32>>,
7942 layers_run: Option<&mut usize>,
7943 from: usize,
7944 upto_excl: usize,
7945 ) -> Option<Result<Vec<f32>, ()>> {
7946 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7949 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7950 return None;
7954 }
7955 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7960 .map(|li| {
7961 if !o1_gpu {
7962 return None;
7963 }
7964 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7965 })
7966 .collect();
7967 if self.o1_active() && o1_gpu {
7968 let want: usize = (from..upto_excl)
7971 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7972 .count();
7973 let have = o1_views.iter().filter(|v| v.is_some()).count();
7974 if want == 0 || have != want {
7975 use std::sync::atomic::{AtomicUsize, Ordering};
7985 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7986 let code = have * 1000 + want;
7987 if LAST.swap(code, Ordering::Relaxed) != code {
7988 tracing::warn!(
7989 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7990 );
7991 }
7992 return None;
7993 }
7994 }
7995 let nh = self.num_heads;
7996 let (nkv, hd, rd) = self.layer_geom(0);
7997 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7998 let mut layers = Vec::with_capacity(upto_excl - from);
7999 let mut model = None;
8000 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
8001 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
8002 if let Some((m, i, kind, rs)) = t
8003 .graph_weight()
8004 .or_else(|| t.graph_weight_descriptor())
8005 {
8006 let name = &m.tensors[i].name;
8007 let prism = if crate::prism::is_inverse_embedding(m, name) {
8008 crate::gpu::GraphPrismOp::InverseEmbedding
8009 } else if crate::prism::is_forward_weight(m, name) {
8010 crate::gpu::GraphPrismOp::Forward
8011 } else {
8012 crate::gpu::GraphPrismOp::None
8013 };
8014 return Some(crate::gpu::GraphW {
8015 idx: i,
8016 kind,
8017 row_scale: rs,
8018 data: &[],
8019 prism,
8020 affine: crate::prism::is_affine_target(m, name),
8021 });
8022 }
8023 match t.as_f32() {
8025 Some(d) => Some(crate::gpu::GraphW {
8026 idx: 0,
8027 kind: 4,
8028 row_scale: &[],
8029 data: d,
8030 prism: crate::gpu::GraphPrismOp::None,
8031 affine: false,
8032 }),
8033 None => {
8034 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
8035 eprintln!("batch graph: weight has no graph/f32 representation");
8036 }
8037 None
8038 }
8039 }
8040 }
8041 for li in from..upto_excl {
8042 let lw = &self.weights.layers[self.phys_layer(li)];
8043 if dbg {
8044 let ak = match &lw.attn {
8045 AttnKind::Mla(_) => "Mla".into(),
8046 AttnKind::Full {
8047 output_gate, bias, ..
8048 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
8049 AttnKind::LinearGdn(_) => "LinearGdn".into(),
8050 AttnKind::Kda(_) => "Kda".into(),
8051 AttnKind::Linear(_) => "Linear".into(),
8052 AttnKind::ShortConv(_) => "ShortConv".into(),
8053 };
8054 let fk = match &lw.ffn {
8055 FfnKind::Dense(_) => "Dense",
8056 FfnKind::Moe(_) => "Moe",
8057 FfnKind::DenseMoe(_) => "DenseMoe",
8058 };
8059 eprintln!("graph L{li}: attn={ak} ffn={fk}");
8060 }
8061 let gffn = match &lw.ffn {
8062 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
8066 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
8067 gate: gw(&d.gate_proj)?,
8068 up: gw(&d.up_proj)?,
8069 down: gw(&d.down_proj)?,
8070 },
8071 FfnKind::Moe(m) => {
8072 if m.route_tau.is_some() || m.mask.is_some() {
8080 return None;
8081 }
8082 let shared = m.shared.as_ref();
8083 let has_shared = shared.is_some();
8084 let shared_gated = matches!(shared, Some((_, Some(_))));
8085 let sgate = match shared {
8086 Some((_, Some(sg))) => gw(sg)?,
8087 _ => gw(&m.router)?,
8091 };
8092 let router = gw(&m.router)?;
8093 if router.prism != crate::gpu::GraphPrismOp::None
8099 || sgate.prism != crate::gpu::GraphPrismOp::None
8100 || router.affine
8101 || sgate.affine
8102 {
8103 tracing::warn!(
8104 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8105 );
8106 return None;
8107 }
8108 let inter = m.experts.first()?.gate_proj.rows();
8109 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8110 let mut q4tp: Option<bool> = None;
8113 let mut gu_q2: Option<bool> = None;
8116 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8117 if !matches!(e.act, Act::Silu)
8118 || e.gate_proj.rows() != inter
8119 || e.up_proj.rows() != inter
8120 {
8121 return None;
8122 }
8123 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8128 let Some((em, ei, _, _)) = expert_weight
8129 .graph_weight()
8130 .or_else(|| expert_weight.graph_weight_descriptor())
8131 else {
8132 return None;
8133 };
8134 let name = &em.tensors[ei].name;
8135 if crate::prism::is_forward_weight(em, name)
8136 || crate::prism::is_inverse_embedding(em, name)
8137 || crate::prism::is_affine_target(em, name)
8138 {
8139 tracing::warn!(
8140 "resident MoE declined: expert Prism/affine transform is not implemented"
8141 );
8142 return None;
8143 }
8144 }
8145 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8146 Some((mm, gi)) => (
8147 mm,
8148 gi,
8149 e.up_proj.mapped_q4t()?.1,
8150 e.down_proj.mapped_q4t()?.1,
8151 false,
8152 false,
8153 ),
8154 None => match e.gate_proj.mapped_q2tp() {
8155 Some((mm, gi)) => (
8156 mm,
8157 gi,
8158 e.up_proj.mapped_q2tp()?.1,
8159 e.down_proj.mapped_q4tp()?.1,
8160 true,
8161 true,
8162 ),
8163 None => {
8164 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8165 (
8166 mm,
8167 gi,
8168 e.up_proj.mapped_q4tp()?.1,
8169 e.down_proj.mapped_q4tp()?.1,
8170 true,
8171 false,
8172 )
8173 }
8174 },
8175 };
8176 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8177 {
8178 tracing::warn!(
8184 "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."
8185 );
8186 return None;
8187 }
8188 model.get_or_insert_with(|| mm.clone());
8189 experts.push((gi, ui, di));
8190 }
8191 crate::gpu::GraphFfn::Moe {
8192 router,
8193 shared_gate: sgate,
8194 experts,
8195 n_exp: m.experts.len(),
8196 top_k: std::env::var("CMF_TOPK_PROBE")
8202 .ok()
8203 .and_then(|v| v.parse::<usize>().ok())
8204 .filter(|k| *k > 0 && *k <= m.top_k)
8205 .unwrap_or(m.top_k),
8206 inter,
8207 norm_topk: m.norm_topk_prob,
8208 q4tp: q4tp?,
8209 gu_q2: gu_q2.unwrap_or(false),
8210 sigmoid: m.router_sigmoid,
8211 bias: m.expert_bias.as_deref(),
8212 has_shared,
8213 shared_gated,
8214 route_scale: m.routed_scaling,
8215 }
8216 }
8217 };
8218 let attn = match &lw.attn {
8219 AttnKind::Full {
8220 wq,
8221 wk,
8222 wv,
8223 wo,
8224 q_norm,
8225 k_norm,
8226 output_gate,
8227 softplus_gate,
8228 bias,
8229 } => {
8230 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8231 return None;
8232 }
8233 let (m, _, _, _) = wq
8234 .graph_weight()
8235 .or_else(|| wq.graph_weight_descriptor())?;
8236 model = Some(m.clone());
8237 crate::gpu::GraphAttn::Full {
8238 wq: gw(wq)?,
8239 wk: gw(wk)?,
8240 wv: gw(wv)?,
8241 wo: gw(wo)?,
8242 q_norm: q_norm.as_deref(),
8243 k_norm: k_norm.as_deref(),
8244 late_qk_norm: self.qk_norm_after_rope,
8245 bias: bias
8246 .as_ref()
8247 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8248 output_gate: *output_gate,
8249 cpu_k: self.kv_cache.layers[li].k_heads(),
8250 cpu_v: self.kv_cache.layers[li].v_heads(),
8251 }
8252 }
8253 AttnKind::LinearGdn(w) => {
8254 let cfg = self.gdn_cfg?;
8255 let (m, _, _, _) = w
8256 .in_proj_qkv
8257 .graph_weight()
8258 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8259 model = Some(m.clone());
8260 crate::gpu::GraphAttn::Gdn {
8261 qkv: gw(&w.in_proj_qkv)?,
8262 z: gw(&w.in_proj_z)?,
8263 a: gw(&w.in_proj_a)?,
8264 b: gw(&w.in_proj_b)?,
8265 out: gw(&w.out_proj)?,
8266 conv1d: &w.conv1d,
8267 a_log: &w.a_log,
8268 dt_bias: &w.dt_bias,
8269 norm: &w.norm,
8270 nv: cfg.num_v_heads,
8271 nk: cfg.num_k_heads,
8272 dk: cfg.key_head_dim,
8273 dv: cfg.value_head_dim,
8274 kk: cfg.conv_kernel,
8275 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8276 }
8277 }
8278 AttnKind::ShortConv(w) => {
8279 let cfg = self.short_conv_cfg?;
8280 let (m, _, _, _) = w
8281 .in_proj
8282 .graph_weight()
8283 .or_else(|| w.in_proj.graph_weight_descriptor())?;
8284 model = Some(m.clone());
8285 crate::gpu::GraphAttn::ShortConv {
8286 inp: gw(&w.in_proj)?,
8287 out: gw(&w.out_proj)?,
8288 taps: &w.conv,
8289 kernel: cfg.kernel,
8290 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8291 }
8292 }
8293 _ => return None,
8294 };
8295 layers.push(crate::gpu::GraphLayer {
8296 input_norm: &lw.input_norm,
8297 attn,
8298 post_norm: &lw.post_norm,
8299 ffn: gffn,
8300 });
8301 }
8302 let model = model?;
8303 let lm_gw = if upto_excl == self.num_layers
8309 && self.graph_want_logits
8310 && std::env::var("CMF_GPU_LMHEAD")
8311 .map(|v| v != "0")
8312 .unwrap_or(true)
8313 {
8314 self.weights
8315 .lm_head
8316 .graph_weight()
8317 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8318 .map(|(m, i, kind, rs)| {
8319 let name = &m.tensors[i].name;
8320 let prism = if crate::prism::is_inverse_embedding(m, name) {
8321 crate::gpu::GraphPrismOp::InverseEmbedding
8322 } else if crate::prism::is_forward_weight(m, name) {
8323 crate::gpu::GraphPrismOp::Forward
8324 } else {
8325 crate::gpu::GraphPrismOp::None
8326 };
8327 (
8328 crate::gpu::GraphW {
8329 idx: i,
8330 kind,
8331 row_scale: rs,
8332 data: &[],
8333 prism,
8334 affine: crate::prism::is_affine_target(m, name),
8335 },
8336 self.weights.lm_head.rows(),
8337 )
8338 })
8339 } else {
8340 None
8341 };
8342 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8343 let emb_gw = if steps > 1 {
8345 self.weights
8346 .embed_tokens
8347 .graph_weight()
8348 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8349 .map(|(m, i, kind, rs)| {
8350 let name = &m.tensors[i].name;
8351 let prism = if crate::prism::is_inverse_embedding(m, name) {
8352 crate::gpu::GraphPrismOp::InverseEmbedding
8353 } else if crate::prism::is_forward_weight(m, name) {
8354 crate::gpu::GraphPrismOp::Forward
8355 } else {
8356 crate::gpu::GraphPrismOp::None
8357 };
8358 (
8359 crate::gpu::GraphW {
8360 idx: i,
8361 kind,
8362 row_scale: rs,
8363 data: &[],
8364 prism,
8365 affine: crate::prism::is_affine_target(m, name),
8366 },
8367 self.weights.embed_tokens.rows(),
8368 self.embed_multiplier,
8369 )
8370 })
8371 } else {
8372 None
8373 };
8374
8375 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8381 (from..upto_excl.min(self.num_layers - 1))
8382 .filter(|&li| (li + 1) % self.physical_layers == 0)
8383 .map(|li| li - from)
8384 .collect()
8385 } else {
8386 Vec::new()
8387 };
8388 let mut h = hidden.to_vec();
8389 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8395 let outcome = crate::gpu::forward_token_graph(
8396 &model,
8397 self.graph_kv_id,
8398 &layers,
8399 &o1_views,
8400 self.o1_epoch,
8401 &self.inv_freq,
8402 &mut h,
8403 nh,
8404 nkv,
8405 hd,
8406 self.attn_scale,
8407 rd,
8408 self.hidden_size,
8409 self.intermediate_size,
8410 position,
8411 self.kv_cache.max_seq_len,
8412 gemma,
8413 self.rms_eps as f32,
8414 lm,
8415 &self.weights.final_norm,
8416 logits_out,
8417 &loop_norm_at,
8418 steps,
8419 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8420 ids_out,
8421 layers_run,
8422 from,
8423 dump_hidden,
8424 );
8425 match outcome {
8426 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8427 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8428 crate::gpu::TokenGraphOutcome::Declined => None,
8429 }
8430 }
8431
8432 #[cfg(target_os = "macos")]
8441 #[allow(clippy::type_complexity)]
8442 fn metal_rows_plan(
8443 &self,
8444 ) -> Option<(
8445 Vec<MetalRowsItem<'_>>,
8446 std::sync::Arc<cortiq_core::CmfModel>,
8447 Option<crate::gpu_metal::GdnGpuCfg>,
8448 )> {
8449 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8450 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8451 if !graph_force
8452 || !crate::gpu::enabled_here()
8453 || std::env::var("CMF_GPU_BLOCK")
8454 .map(|v| v == "0")
8455 .unwrap_or(false)
8456 || self.attn_softcap > 0.0
8457 || self.o1_active()
8458 || self.swa.is_some()
8459 || self.global_attn.is_some()
8460 || self.attention_heads_per_layer.is_some()
8461 || self.attn_v_norm
8462 || self.loop_final_norm
8463 {
8464 return None;
8465 }
8466 let attend_contract = self.head_dim % 4 == 0
8467 && self.head_dim <= 256
8468 && self.rotary_dim >= 2
8469 && self.rotary_dim <= self.head_dim
8470 && (self.rotary_dim / 2) % 32 == 0
8471 && self.num_kv_heads > 0
8472 && self.num_heads % self.num_kv_heads == 0;
8473 if !attend_contract {
8474 return None;
8475 }
8476 let mut plan: Vec<MetalRowsItem> = Vec::new();
8477 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8478 for li in 0..self.num_layers {
8479 let lw = &self.weights.layers[self.phys_layer(li)];
8480 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8481 return None;
8482 }
8483 let ffn = match &lw.ffn {
8484 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8485 let (Some(g), Some(u), Some(dn)) = (
8486 d.gate_proj.metal_graph_parts(),
8487 d.up_proj.metal_graph_parts(),
8488 d.down_proj.metal_graph_parts(),
8489 ) else {
8490 return None;
8491 };
8492 MetalFfn::Dense {
8493 gate: g,
8494 up: u,
8495 down: dn,
8496 }
8497 }
8498 _ => return None,
8499 };
8500 match &lw.attn {
8501 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8502 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8503 w.in_proj_qkv.metal_graph_parts(),
8504 w.in_proj_z.metal_graph_parts(),
8505 w.in_proj_a.f32_parts(),
8506 w.in_proj_b.f32_parts(),
8507 w.out_proj.metal_graph_parts(),
8508 ) else {
8509 return None;
8510 };
8511 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8512 model_ref.get_or_insert_with(|| model.clone());
8513 }
8514 let gl = GdnGpuLayer {
8515 attn_norm: &lw.input_norm,
8516 post_norm: &lw.post_norm,
8517 qkv,
8518 z,
8519 a,
8520 b: bb,
8521 out,
8522 ffn,
8523 conv1d: &w.conv1d,
8524 a_log: &w.a_log,
8525 dt_bias: &w.dt_bias,
8526 gnorm: &w.norm,
8527 };
8528 match plan.last_mut() {
8529 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8530 _ => plan.push(MetalRowsItem::Gdn {
8531 run: vec![gl],
8532 first: li,
8533 }),
8534 }
8535 }
8536 AttnKind::Full {
8537 wq,
8538 wk,
8539 wv,
8540 wo,
8541 q_norm,
8542 k_norm,
8543 output_gate,
8544 softplus_gate: None,
8545 bias: None,
8546 } => {
8547 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8548 (
8549 wq.metal_graph_parts(),
8550 wk.metal_graph_parts(),
8551 wv.metal_graph_parts(),
8552 wo.metal_graph_parts(),
8553 )
8554 else {
8555 return None;
8556 };
8557 if let QTensor::Mapped { model, .. } = wq {
8558 model_ref.get_or_insert_with(|| model.clone());
8559 }
8560 let cache = &self.kv_cache.layers[li];
8561 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8562 return None;
8563 }
8564 plan.push(MetalRowsItem::Attn {
8565 l: AttnGpuLayer {
8566 attn_norm: &lw.input_norm,
8567 post_norm: &lw.post_norm,
8568 wq: pq,
8569 wk: pk,
8570 wv: pv,
8571 wo: po,
8572 ffn,
8573 },
8574 li,
8575 q_norm: q_norm.as_deref(),
8576 k_norm: k_norm.as_deref(),
8577 output_gate: *output_gate,
8578 });
8579 }
8580 _ => return None,
8581 }
8582 }
8583 let model = model_ref?;
8584 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8585 nv: cfg.num_v_heads,
8586 nk: cfg.num_k_heads,
8587 dk: cfg.key_head_dim,
8588 dv: cfg.value_head_dim,
8589 kk: cfg.conv_kernel,
8590 hidden: self.hidden_size,
8591 inter: self.intermediate_size,
8592 c_dim: cfg.conv_dim(),
8593 eps: cfg.rms_eps as f32,
8594 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8595 });
8596 Some((plan, model, gcfg))
8597 }
8598
8599 #[cfg(target_os = "macos")]
8601 #[allow(clippy::too_many_arguments)]
8602 fn metal_attn_params<'a>(
8603 li: usize,
8604 cache: &'a crate::kv_cache::LayerKvCache,
8605 q_norm: Option<&'a [f32]>,
8606 k_norm: Option<&'a [f32]>,
8607 output_gate: bool,
8608 inv_freq: &'a [f32],
8609 geom: (usize, usize, usize, usize),
8610 pos0: usize,
8611 kv_id: u64,
8612 scale: f32,
8613 eps: f32,
8614 gemma: bool,
8615 late_qk_norm: bool,
8616 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8617 let (nh, nkv, hd, rd) = geom;
8618 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8619 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8620 let cpu_stored = cpu_k[0].len() / hd;
8621 (
8622 crate::gpu_metal::AttnDeviceParams {
8623 kv_id,
8624 layer: li,
8625 nh,
8626 nkv,
8627 hd,
8628 rd,
8629 position: pos0,
8630 scale,
8631 eps,
8632 gemma,
8633 late_qk_norm,
8634 output_gate,
8635 q_norm,
8636 k_norm,
8637 inv_freq,
8638 cpu_k,
8639 cpu_v,
8640 cpu_stored,
8641 o1: None,
8642 },
8643 cpu_stored,
8644 )
8645 }
8646
8647 #[cfg(target_os = "macos")]
8652 #[allow(clippy::type_complexity)]
8653 fn metal_rows_run(
8654 &mut self,
8655 hiddens: &mut [f32],
8656 pos0: usize,
8657 b: usize,
8658 prefill: bool,
8659 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8660 ) -> MetalRowsRun {
8661 use crate::gpu_metal::{GraphDims, VerifyGraph};
8662 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8663 for l in &mut self.kv_cache.layers {
8664 if l.linear_state.len() != want && want > 0 {
8665 l.linear_state = vec![0f32; want];
8666 }
8667 }
8668 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
8669 return MetalRowsRun::Declined;
8670 };
8671 let dims = GraphDims {
8672 hidden: self.hidden_size,
8673 eps: self.rms_eps as f32,
8674 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8675 };
8676 let Some(mut graph) = (if prefill {
8677 VerifyGraph::new_prefill(&model, dims, hiddens, b)
8678 } else {
8679 VerifyGraph::new(&model, dims, hiddens, b)
8680 }) else {
8681 return MetalRowsRun::Declined;
8682 };
8683 let geom = (
8684 self.num_heads,
8685 self.num_kv_heads,
8686 self.head_dim,
8687 self.rotary_dim,
8688 );
8689 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8690 let eps = self.rms_eps as f32;
8691 let kv_id = self.graph_kv_id;
8692 let inv_freq = self.inv_freq.clone();
8693 for item in &plan {
8694 let ok = match item {
8695 MetalRowsItem::Gdn { run, .. } => gcfg
8696 .as_ref()
8697 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8698 .unwrap_or(false),
8699 MetalRowsItem::Attn {
8700 l,
8701 li,
8702 q_norm,
8703 k_norm,
8704 output_gate,
8705 } => {
8706 let (p, _) = Self::metal_attn_params(
8707 *li,
8708 &self.kv_cache.layers[*li],
8709 *q_norm,
8710 *k_norm,
8711 *output_gate,
8712 &inv_freq,
8713 geom,
8714 pos0,
8715 kv_id,
8716 self.attn_scale,
8717 eps,
8718 gemma,
8719 self.qk_norm_after_rope,
8720 );
8721 graph.attn_ok(l, &p)
8722 }
8723 };
8724 if !ok {
8725 use std::sync::atomic::{AtomicBool, Ordering};
8726 static SAID: AtomicBool = AtomicBool::new(false);
8727 if !SAID.swap(true, Ordering::Relaxed) {
8728 tracing::warn!("metal rows graph: a layer failed preflight — declining");
8729 }
8730 return MetalRowsRun::Declined;
8731 }
8732 }
8733 let lm = match &spec {
8734 Some((lm, _, _)) => {
8735 if !graph.lm_head_ok(*lm) {
8736 return MetalRowsRun::Declined;
8737 }
8738 Some(*lm)
8739 }
8740 None => None,
8741 };
8742 let mut gdn_layers = Vec::new();
8743 let mut attn_layers = Vec::new();
8744 for item in &plan {
8745 match item {
8746 MetalRowsItem::Gdn { run, first } => {
8747 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8748 .iter()
8749 .map(|l| l.linear_state.as_slice())
8750 .collect();
8751 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8752 return MetalRowsRun::Declined;
8753 }
8754 gdn_layers.extend(*first..*first + run.len());
8755 }
8756 MetalRowsItem::Attn {
8757 l,
8758 li,
8759 q_norm,
8760 k_norm,
8761 output_gate,
8762 } => {
8763 let (p, cpu_stored) = Self::metal_attn_params(
8764 *li,
8765 &self.kv_cache.layers[*li],
8766 *q_norm,
8767 *k_norm,
8768 *output_gate,
8769 &inv_freq,
8770 geom,
8771 pos0,
8772 kv_id,
8773 self.attn_scale,
8774 eps,
8775 gemma,
8776 self.qk_norm_after_rope,
8777 );
8778 if !graph.encode_attn_b(l, &p) {
8779 return MetalRowsRun::Declined;
8780 }
8781 attn_layers.push((*li, cpu_stored));
8782 }
8783 }
8784 }
8785 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8786 if !graph.encode_lm_head_b(final_norm, lm) {
8787 return MetalRowsRun::Declined;
8788 }
8789 }
8790 if !graph.sync() {
8791 return MetalRowsRun::Failed;
8792 }
8793 if let Some((lm, _, logits)) = spec {
8794 logits.resize(b * lm.1, 0.0);
8795 if !graph.read_logits(logits) {
8796 return MetalRowsRun::Failed;
8797 }
8798 }
8799 if !graph.read_hidden(hiddens) {
8800 return MetalRowsRun::Failed;
8801 }
8802 MetalRowsRun::Completed(MetalVerifyPending {
8803 graph,
8804 gdn_layers,
8805 attn_layers,
8806 })
8807 }
8808
8809 #[cfg(target_os = "macos")]
8815 fn try_batch_graph_metal(
8816 &mut self,
8817 hiddens: &mut [f32],
8818 positions: &[usize],
8819 b: usize,
8820 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8821 ) -> crate::gpu::BatchGraphOutcome {
8822 let _t0 = std::time::Instant::now();
8823 if positions.len() != b
8824 || positions.windows(2).any(|w| w[1] != w[0] + 1)
8825 || hiddens.len() != b * self.hidden_size
8826 {
8827 return crate::gpu::BatchGraphOutcome::Declined;
8828 }
8829 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec) {
8830 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
8831 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
8832 MetalRowsRun::Completed(pending) => pending,
8833 };
8834 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8835 eprintln!(
8836 "metal-verify: {:.1} ms | b={b}",
8837 _t0.elapsed().as_secs_f64() * 1e3
8838 );
8839 }
8840 self.metal_verify = Some(pending);
8841 crate::gpu::BatchGraphOutcome::Completed
8842 }
8843
8844 #[cfg(target_os = "macos")]
8849 fn prefill_rows_metal(
8850 &mut self,
8851 ids: &[u32],
8852 start_pos: usize,
8853 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8854 ) -> MetalPrefillOutcome {
8855 let b = ids.len();
8856 if b == 0 || b > 512 {
8857 return MetalPrefillOutcome::Declined;
8858 }
8859 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8860 let with_head = spec.is_some();
8861 let hs = self.hidden_size;
8862 let mut hiddens = vec![0f32; b * hs];
8863 for (j, &id) in ids.iter().enumerate() {
8864 let e = self.embed_single(id);
8865 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8866 }
8867 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec) {
8868 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
8869 MetalRowsRun::Failed => {
8870 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8871 return MetalPrefillOutcome::Failed;
8872 }
8873 MetalRowsRun::Completed(pending) => pending,
8874 };
8875 let idxs = pending.gdn_layers.clone();
8877 let mut outs: Vec<&mut [f32]> = self
8878 .kv_cache
8879 .layers
8880 .iter_mut()
8881 .enumerate()
8882 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8883 .map(|(_, l)| l.linear_state.as_mut_slice())
8884 .collect();
8885 if !pending.graph.finish_states(&mut outs) {
8886 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8887 return MetalPrefillOutcome::Failed;
8888 }
8889 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8890 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8894 for (li, cpu_stored) in &pending.attn_layers {
8895 let mut kbuf = vec![0f32; b * nkv * hd];
8896 let mut vbuf = vec![0f32; b * nkv * hd];
8897 if !crate::gpu_metal::kv_mirror_read_rows(
8898 self.graph_kv_id,
8899 *li,
8900 nkv,
8901 hd,
8902 *cpu_stored,
8903 b,
8904 &mut kbuf,
8905 &mut vbuf,
8906 ) {
8907 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8908 return MetalPrefillOutcome::Failed;
8909 }
8910 rows.push((*li, *cpu_stored, kbuf, vbuf));
8911 }
8912 for (li, cpu_stored, kbuf, vbuf) in rows {
8913 let cache = &mut self.kv_cache.layers[li];
8914 for r in 0..b {
8915 cache.append(
8916 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8917 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8918 &[],
8919 );
8920 }
8921 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
8922 }
8923 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8924 if with_head {
8925 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8926 }
8927 MetalPrefillOutcome::Completed(hiddens)
8928 }
8929
8930 #[cfg(target_os = "macos")]
8931 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
8932 self.prefill_rows_metal(ids, start_pos, None)
8933 }
8934
8935 #[cfg(target_os = "macos")]
8940 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
8941 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
8942 return MetalBatchNllOutcome::Declined;
8943 }
8944 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
8945 return MetalBatchNllOutcome::Declined;
8946 };
8947 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
8948 .ok()
8949 .and_then(|v| v.parse::<usize>().ok())
8950 .filter(|&v| (1..=512).contains(&v))
8951 .unwrap_or(32);
8952 let final_norm = self.weights.final_norm.clone();
8953 let mut nll = 0.0f64;
8954 let mut count = 0usize;
8955 let mut pos = 0usize;
8956 let mut completed = 0usize;
8957 while pos < ids.len() {
8958 let end = (pos + chunk).min(ids.len());
8959 let mut logits = Vec::new();
8960 let outcome = self.prefill_rows_metal(
8961 &ids[pos..end],
8962 pos,
8963 Some((lm, &final_norm, &mut logits)),
8964 );
8965 match outcome {
8966 MetalPrefillOutcome::Declined => {
8967 return if completed == 0 {
8968 MetalBatchNllOutcome::Declined
8969 } else {
8970 MetalBatchNllOutcome::Failed(format!(
8971 "ordinary Metal NLL batch declined after {completed} chunks"
8972 ))
8973 };
8974 }
8975 MetalPrefillOutcome::Failed => {
8976 return MetalBatchNllOutcome::Failed(
8977 "ordinary Metal NLL batch failed after admission".to_string(),
8978 );
8979 }
8980 MetalPrefillOutcome::Completed(_) => {}
8981 }
8982 completed += 1;
8983 let vocab = self.vocab_size.min(lm.1);
8984 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
8985 return MetalBatchNllOutcome::Failed(
8986 "ordinary Metal NLL head returned an invalid shape".to_string(),
8987 );
8988 }
8989 for row in 0..(end - pos) {
8990 let absolute = pos + row;
8991 if absolute < start || absolute + 1 >= ids.len() {
8992 continue;
8993 }
8994 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
8995 if let Some(mu) = self.logit_multiplier {
8996 for v in lg.iter_mut() {
8997 *v *= mu;
8998 }
8999 }
9000 if let Some(c) = self.final_softcap {
9001 for v in lg.iter_mut() {
9002 *v = c * (*v / c).tanh();
9003 }
9004 }
9005 let target = ids[absolute + 1] as usize;
9006 if target >= vocab {
9007 return MetalBatchNllOutcome::Failed(format!(
9008 "target token {target} exceeds Metal head rows {vocab}"
9009 ));
9010 }
9011 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9012 let lse: f64 = lg
9013 .iter()
9014 .map(|&v| ((v - max) as f64).exp())
9015 .sum::<f64>()
9016 .ln()
9017 + max as f64;
9018 nll += lse - lg[target] as f64;
9019 count += 1;
9020 }
9021 pos = end;
9022 }
9023 MetalBatchNllOutcome::Completed(nll, count)
9024 }
9025
9026 #[cfg(target_os = "macos")]
9030 fn metal_verify_commit(&mut self, a: usize) -> bool {
9031 let Some(mut pending) = self.metal_verify.take() else {
9032 return false;
9033 };
9034 let n = a + 1;
9035 let idxs = pending.gdn_layers.clone();
9037 let mut outs: Vec<&mut [f32]> = self
9038 .kv_cache
9039 .layers
9040 .iter_mut()
9041 .enumerate()
9042 .filter(|(i, _)| idxs.binary_search(i).is_ok())
9043 .map(|(_, l)| l.linear_state.as_mut_slice())
9044 .collect();
9045 if !pending.graph.commit(n, &mut outs) {
9046 return false;
9047 }
9048 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
9049 let mut rows = Vec::with_capacity(pending.attn_layers.len());
9053 for (li, cpu_stored) in &pending.attn_layers {
9054 let mut kbuf = vec![0f32; n * nkv * hd];
9055 let mut vbuf = vec![0f32; n * nkv * hd];
9056 if !crate::gpu_metal::kv_mirror_read_rows(
9057 self.graph_kv_id,
9058 *li,
9059 nkv,
9060 hd,
9061 *cpu_stored,
9062 n,
9063 &mut kbuf,
9064 &mut vbuf,
9065 ) {
9066 return false;
9067 }
9068 rows.push((*li, *cpu_stored, kbuf, vbuf));
9069 }
9070 for (li, cpu_stored, kbuf, vbuf) in rows {
9071 let cache = &mut self.kv_cache.layers[li];
9072 for r in 0..n {
9073 cache.append(
9074 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9075 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9076 &[],
9077 );
9078 }
9079 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
9080 }
9081 true
9082 }
9083
9084 #[cfg(target_os = "macos")]
9090 fn mtp_warm_batch_metal(
9091 &mut self,
9092 m: &mut MtpModule,
9093 pairs: &[(&[f32], u32)],
9094 first_pos: usize,
9095 ) -> bool {
9096 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
9097 let b = pairs.len();
9098 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
9099 return false;
9100 }
9101 let AttnKind::Full {
9102 wq,
9103 wk,
9104 wv,
9105 wo,
9106 q_norm,
9107 k_norm,
9108 output_gate,
9109 softplus_gate: None,
9110 bias: None,
9111 } = &m.layer.attn
9112 else {
9113 return false;
9114 };
9115 let FfnKind::Dense(d) = &m.layer.ffn else {
9116 return false;
9117 };
9118 if !d.segs.is_empty() {
9119 return false;
9120 }
9121 let (Some(pq), Some(pk), Some(pv), Some(po)) =
9122 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9123 else {
9124 return false;
9125 };
9126 let (Some(g), Some(u), Some(dn)) = (
9127 d.gate_proj.q1_parts(),
9128 d.up_proj.q1_parts(),
9129 d.down_proj.q1_parts(),
9130 ) else {
9131 return false;
9132 };
9133 let Some(eh) = m.eh_proj.q1_parts() else {
9134 return false;
9135 };
9136 let QTensor::Mapped { model, .. } = wq else {
9137 return false;
9138 };
9139 let model = model.clone();
9140 let hs = self.hidden_size;
9141 let mut cat = vec![0f32; b * 2 * hs];
9143 for (j, (h, tok)) in pairs.iter().enumerate() {
9144 let e = self.embed_single(*tok);
9145 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9146 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9147 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9148 }
9149 let dims = GraphDims {
9150 hidden: hs,
9151 eps: self.rms_eps as f32,
9152 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9153 };
9154 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9155 return false;
9156 };
9157 let l = AttnGpuLayer {
9158 attn_norm: &m.layer.input_norm,
9159 post_norm: &m.layer.post_norm,
9160 wq: pq,
9161 wk: pk,
9162 wv: pv,
9163 wo: po,
9164 ffn: MetalFfn::Dense {
9165 gate: g,
9166 up: u,
9167 down: dn,
9168 },
9169 };
9170 let (nh, nkv, hd, rd) = (
9171 self.num_heads,
9172 self.num_kv_heads,
9173 self.head_dim,
9174 self.rotary_dim,
9175 );
9176 let inv_freq = self.inv_freq.clone();
9177 let cpu_stored;
9178 {
9179 let cache = &m.kv;
9180 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9181 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9182 cpu_stored = cpu_k[0].len() / hd;
9183 if cpu_stored != first_pos {
9184 return false;
9185 }
9186 let p = AttnDeviceParams {
9187 kv_id: self.mtp_kv_id(),
9188 layer: Self::MTP_LAYER_BASE,
9189 nh,
9190 nkv,
9191 hd,
9192 rd,
9193 position: first_pos,
9194 scale: self.attn_scale,
9195 eps: self.rms_eps as f32,
9196 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9197 late_qk_norm: self.qk_norm_after_rope,
9198 output_gate: *output_gate,
9199 q_norm: q_norm.as_deref(),
9200 k_norm: k_norm.as_deref(),
9201 inv_freq: &inv_freq,
9202 cpu_k,
9203 cpu_v,
9204 cpu_stored,
9205 o1: None,
9206 };
9207 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9208 return false;
9209 }
9210 }
9211 if !graph.sync() {
9212 return false;
9213 }
9214 let mut kbuf = vec![0f32; b * nkv * hd];
9215 let mut vbuf = vec![0f32; b * nkv * hd];
9216 if !crate::gpu_metal::kv_mirror_read_rows(
9217 self.mtp_kv_id(),
9218 Self::MTP_LAYER_BASE,
9219 nkv,
9220 hd,
9221 cpu_stored,
9222 b,
9223 &mut kbuf,
9224 &mut vbuf,
9225 ) {
9226 return false;
9227 }
9228 for r in 0..b {
9229 m.kv.append(
9230 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9231 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9232 &[],
9233 );
9234 }
9235 crate::gpu_metal::kv_mirror_set_stored(
9236 self.mtp_kv_id(),
9237 Self::MTP_LAYER_BASE,
9238 cpu_stored + b,
9239 );
9240 true
9241 }
9242
9243 pub(crate) fn note_draft_id(&mut self, id: u32) {
9250 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
9251 if (id as usize) >= cut {
9252 self.draft_full_streak = 16;
9253 } else {
9254 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
9255 }
9256 }
9257
9258 fn draft_head_rows(&self, head_rows: usize) -> usize {
9261 if self.draft_full_streak > 0 {
9262 head_rows
9263 } else {
9264 Self::draft_vocab_rows(head_rows)
9265 }
9266 }
9267
9268 fn draft_vocab_rows(head_rows: usize) -> usize {
9271 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9272 let n = *N.get_or_init(|| {
9273 std::env::var("CMF_DRAFT_VOCAB")
9274 .ok()
9275 .and_then(|v| v.parse().ok())
9276 .unwrap_or(65536)
9277 });
9278 if n == 0 { head_rows } else { n.min(head_rows) }
9279 }
9280
9281 #[cfg(target_os = "macos")]
9286 fn mtp_step_metal(
9287 &mut self,
9288 m: &mut MtpModule,
9289 hidden: &[f32],
9290 next_token: u32,
9291 position: usize,
9292 want_logits: bool,
9293 ) -> Option<(Vec<f32>, Vec<f32>)> {
9294 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9295 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9296 || !crate::gpu::q1_force()
9297 || !crate::gpu::enabled_here()
9298 || self.attn_softcap > 0.0
9299 || self.attention_heads_per_layer.is_some()
9300 || m.kv.mode != crate::kv_cache::KvMode::F32
9301 || m.kv.o1.is_some()
9302 {
9303 return None;
9304 }
9305 let AttnKind::Full {
9306 wq,
9307 wk,
9308 wv,
9309 wo,
9310 q_norm,
9311 k_norm,
9312 output_gate,
9313 softplus_gate: None,
9314 bias: None,
9315 } = &m.layer.attn
9316 else {
9317 return None;
9318 };
9319 let FfnKind::Dense(d) = &m.layer.ffn else {
9320 return None;
9321 };
9322 if d.act != Act::Silu || !d.segs.is_empty() {
9323 return None;
9324 }
9325 let (pq, pk, pv, po) = (
9326 wq.q1_parts()?,
9327 wk.q1_parts()?,
9328 wv.q1_parts()?,
9329 wo.q1_parts()?,
9330 );
9331 let (g, u, dn) = (
9332 d.gate_proj.q1_parts()?,
9333 d.up_proj.q1_parts()?,
9334 d.down_proj.q1_parts()?,
9335 );
9336 let QTensor::Mapped { model, .. } = wq else {
9337 return None;
9338 };
9339 let model = model.clone();
9340 let lm = if want_logits {
9341 Some(self.weights.lm_head.q1_parts()?)
9342 } else {
9343 None
9344 };
9345 let dims = GraphDims {
9346 hidden: self.hidden_size,
9347 eps: self.rms_eps as f32,
9348 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9349 };
9350 let hs = self.hidden_size;
9353 let mut x = vec![0f32; hs];
9354 let mut graph = TokenGraph::new(&model, dims, &x)?;
9355 let mut folded = false;
9356 if let Some(eh) = m.eh_proj.q1_parts() {
9357 let e = self.embed_single(next_token);
9358 let mut cat = vec![0.0f32; 2 * hs];
9359 let (cat_e, cat_h) = cat.split_at_mut(hs);
9360 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9361 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9362 folded = graph.encode_input_proj(eh, &cat);
9363 }
9364 if !folded {
9365 x = self.mtp_block_input(m, hidden, next_token);
9366 graph = TokenGraph::new(&model, dims, &x)?;
9367 }
9368 let l = AttnGpuLayer {
9369 attn_norm: &m.layer.input_norm,
9370 post_norm: &m.layer.post_norm,
9371 wq: pq,
9372 wk: pk,
9373 wv: pv,
9374 wo: po,
9375 ffn: MetalFfn::Dense {
9376 gate: g,
9377 up: u,
9378 down: dn,
9379 },
9380 };
9381 let (nh, nkv, hd, rd) = (
9382 self.num_heads,
9383 self.num_kv_heads,
9384 self.head_dim,
9385 self.rotary_dim,
9386 );
9387 let inv_freq = self.inv_freq.clone();
9388 {
9389 let cache = &m.kv;
9390 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9391 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9392 let cpu_stored = cpu_k[0].len() / hd;
9393 let p = AttnDeviceParams {
9394 kv_id: self.mtp_kv_id(),
9395 layer: Self::MTP_LAYER_BASE,
9396 nh,
9397 nkv,
9398 hd,
9399 rd,
9400 position,
9401 scale: self.attn_scale,
9402 eps: self.rms_eps as f32,
9403 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9404 late_qk_norm: self.qk_norm_after_rope,
9405 output_gate: *output_gate,
9406 q_norm: q_norm.as_deref(),
9407 k_norm: k_norm.as_deref(),
9408 inv_freq: &inv_freq,
9409 cpu_k,
9410 cpu_v,
9411 cpu_stored,
9412 o1: None,
9413 };
9414 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9415 return None;
9416 }
9417 }
9418 let draft_rows = if let Some(lm) = lm {
9424 self.draft_head_rows(lm.1)
9425 } else {
9426 0
9427 };
9428 if let Some(lm) = lm {
9429 if !graph.lm_head_ok(lm) {
9430 return None;
9431 }
9432 if draft_rows < lm.1 {
9433 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9434 return None;
9435 }
9436 } else {
9437 graph.encode_lm_head(&m.final_norm, lm);
9438 }
9439 }
9440 if graph.sync_checked().is_err() {
9441 return None;
9442 }
9443 let mut logits = Vec::new();
9444 if let Some(lm) = lm {
9445 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
9446 logits = attention::take_buf(n_read);
9447 graph.read_logits(&mut logits);
9448 logits.resize(self.vocab_size, f32::NEG_INFINITY);
9450 }
9451 graph.finish(&mut x);
9452 let mut krow = attention::take_buf(nkv * hd);
9453 let mut vrow = attention::take_buf(nkv * hd);
9454 if crate::gpu_metal::kv_mirror_read_last(
9455 self.mtp_kv_id(),
9456 Self::MTP_LAYER_BASE,
9457 nkv,
9458 hd,
9459 &mut krow,
9460 &mut vrow,
9461 ) {
9462 m.kv.append(&krow, &vrow, &[]);
9463 }
9464 attention::recycle_buf(&mut krow);
9465 attention::recycle_buf(&mut vrow);
9466 Some((logits, x))
9467 }
9468
9469 fn try_batch_graph_wgpu(
9470 &self,
9471 hiddens: &mut [f32],
9472 positions: &[usize],
9473 k: usize,
9474 spec: Option<crate::gpu::SpecTail<'_>>,
9475 ) -> crate::gpu::BatchGraphOutcome {
9476 let _tb = std::time::Instant::now();
9477 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
9478 if self.attn_softcap > 0.0 {
9479 return crate::gpu::BatchGraphOutcome::Declined; }
9481 let nh = self.num_heads;
9482 let (nkv, hd, rd) = self.layer_geom(0);
9483 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9484 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9485 if let Some((m, i, kind, rs)) = t
9486 .graph_weight()
9487 .or_else(|| t.graph_weight_descriptor())
9488 {
9489 let name = &m.tensors[i].name;
9490 let prism = if crate::prism::is_inverse_embedding(m, name) {
9491 crate::gpu::GraphPrismOp::InverseEmbedding
9492 } else if crate::prism::is_forward_weight(m, name) {
9493 crate::gpu::GraphPrismOp::Forward
9494 } else {
9495 crate::gpu::GraphPrismOp::None
9496 };
9497 return Some(crate::gpu::GraphW {
9498 idx: i,
9499 kind,
9500 row_scale: rs,
9501 data: &[],
9502 prism,
9503 affine: crate::prism::is_affine_target(m, name),
9504 });
9505 }
9506 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9507 eprintln!(
9508 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
9509 t.rows(),
9510 t.cols()
9511 );
9512 }
9513 t.as_f32().map(|d| crate::gpu::GraphW {
9514 idx: 0,
9515 kind: 4,
9516 row_scale: &[],
9517 data: d,
9518 prism: crate::gpu::GraphPrismOp::None,
9519 affine: false,
9520 })
9521 }
9522 let built: Option<(
9523 Vec<crate::gpu::GraphLayer<'_>>,
9524 std::sync::Arc<cortiq_core::CmfModel>,
9525 )> = (|| {
9526 let mut layers = Vec::with_capacity(self.num_layers);
9527 let mut model = None;
9528 for li in 0..self.num_layers {
9529 let lw = &self.weights.layers[self.phys_layer(li)];
9530 let gffn = match &lw.ffn {
9537 FfnKind::Dense(d) if !d.segs.is_empty() => {
9538 if batch_debug {
9539 eprintln!("batch graph: dense segmented FFN at layer {li}");
9540 }
9541 return None;
9542 }
9543 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9544 gate: gw(&d.gate_proj)?,
9545 up: gw(&d.up_proj)?,
9546 down: gw(&d.down_proj)?,
9547 },
9548 FfnKind::Moe(m) => {
9549 if m.route_tau.is_some() || m.mask.is_some() {
9556 return None;
9557 }
9558 let (se, sg) = m.shared.as_ref()?;
9561 let shared_gated = sg.is_some();
9562 let sgate = match sg {
9563 Some(sg) => gw(sg)?,
9564 None => gw(&m.router)?,
9567 };
9568 let router = gw(&m.router)?;
9569 if router.prism != crate::gpu::GraphPrismOp::None
9575 || router.affine
9576 || sgate.prism != crate::gpu::GraphPrismOp::None
9577 || sgate.affine
9578 {
9579 return None;
9580 }
9581 let inter = m.experts.first()?.gate_proj.rows();
9582 let mut experts = Vec::with_capacity(m.experts.len() + 1);
9583 let mut q4tp: Option<bool> = None;
9584 let mut gu_q2: Option<bool> = None;
9585 for e in m.experts.iter().chain(std::iter::once(se)) {
9586 if !matches!(e.act, Act::Silu)
9587 || e.gate_proj.rows() != inter
9588 || e.up_proj.rows() != inter
9589 {
9590 return None;
9591 }
9592 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
9596 Some((mm, gi)) => (
9597 mm,
9598 gi,
9599 e.up_proj.mapped_q4t()?.1,
9600 e.down_proj.mapped_q4t()?.1,
9601 false,
9602 false,
9603 ),
9604 None => match e.gate_proj.mapped_q2tp() {
9605 Some((mm, gi)) => (
9606 mm,
9607 gi,
9608 e.up_proj.mapped_q2tp()?.1,
9609 e.down_proj.mapped_q4tp()?.1,
9610 true,
9611 true,
9612 ),
9613 None => {
9614 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
9615 (
9616 mm,
9617 gi,
9618 e.up_proj.mapped_q4tp()?.1,
9619 e.down_proj.mapped_q4tp()?.1,
9620 true,
9621 false,
9622 )
9623 }
9624 },
9625 };
9626 if *q4tp.get_or_insert(is_p) != is_p
9627 || *gu_q2.get_or_insert(is_q2) != is_q2
9628 {
9629 return None;
9630 }
9631 if [gi, ui, di].into_iter().any(|idx| {
9632 mm.tensors
9633 .get(idx)
9634 .is_some_and(|t| {
9635 crate::prism::is_forward_weight(mm, &t.name)
9636 || crate::prism::is_affine_target(mm, &t.name)
9637 })
9638 }) {
9639 return None;
9640 }
9641 model.get_or_insert_with(|| mm.clone());
9642 experts.push((gi, ui, di));
9643 }
9644 crate::gpu::GraphFfn::Moe {
9645 router,
9646 shared_gate: sgate,
9647 experts,
9648 n_exp: m.experts.len(),
9649 top_k: m.top_k,
9650 inter,
9651 norm_topk: m.norm_topk_prob,
9652 q4tp: q4tp?,
9653 gu_q2: gu_q2.unwrap_or(false),
9654 sigmoid: m.router_sigmoid,
9655 bias: m.expert_bias.as_deref(),
9656 has_shared: true,
9657 shared_gated,
9658 route_scale: m.routed_scaling,
9659 }
9660 }
9661 _ => return None,
9662 };
9663 let attn = match &lw.attn {
9664 AttnKind::Full {
9665 wq,
9666 wk,
9667 wv,
9668 wo,
9669 q_norm,
9670 k_norm,
9671 output_gate,
9672 softplus_gate,
9673 bias,
9674 } => {
9675 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9676 if batch_debug {
9677 eprintln!(
9678 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
9679 softplus_gate.is_some(),
9680 self.attention_heads_per_layer.is_some()
9681 );
9682 }
9683 return None;
9684 }
9685 let (m, _, _, _) = wq
9686 .graph_weight()
9687 .or_else(|| wq.graph_weight_descriptor())?;
9688 model = Some(m.clone());
9689 crate::gpu::GraphAttn::Full {
9690 wq: gw(wq)?,
9691 wk: gw(wk)?,
9692 wv: gw(wv)?,
9693 wo: gw(wo)?,
9694 q_norm: q_norm.as_deref(),
9695 k_norm: k_norm.as_deref(),
9696 late_qk_norm: self.qk_norm_after_rope,
9697 bias: bias
9698 .as_ref()
9699 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9700 output_gate: *output_gate,
9701 cpu_k: self.kv_cache.layers[li].k_heads(),
9702 cpu_v: self.kv_cache.layers[li].v_heads(),
9703 }
9704 }
9705 AttnKind::LinearGdn(w) => {
9706 let Some(cfg) = self.gdn_cfg else {
9707 if batch_debug {
9708 eprintln!("batch graph: no GDN config at layer {li}");
9709 }
9710 return None;
9711 };
9712 let (m, _, _, _) = w
9713 .in_proj_qkv
9714 .graph_weight()
9715 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9716 model = Some(m.clone());
9717 crate::gpu::GraphAttn::Gdn {
9718 qkv: gw(&w.in_proj_qkv)?,
9719 z: gw(&w.in_proj_z)?,
9720 a: gw(&w.in_proj_a)?,
9721 b: gw(&w.in_proj_b)?,
9722 out: gw(&w.out_proj)?,
9723 conv1d: &w.conv1d,
9724 a_log: &w.a_log,
9725 dt_bias: &w.dt_bias,
9726 norm: &w.norm,
9727 nv: cfg.num_v_heads,
9728 nk: cfg.num_k_heads,
9729 dk: cfg.key_head_dim,
9730 dv: cfg.value_head_dim,
9731 kk: cfg.conv_kernel,
9732 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9733 }
9734 }
9735 _ => return None,
9736 };
9737 layers.push(crate::gpu::GraphLayer {
9738 input_norm: &lw.input_norm,
9739 attn,
9740 post_norm: &lw.post_norm,
9741 ffn: gffn,
9742 });
9743 }
9744 Some((layers, model?))
9745 })();
9746 let Some((layers, model)) = built else {
9747 {
9748 use std::sync::atomic::{AtomicBool, Ordering};
9749 static SAID: AtomicBool = AtomicBool::new(false);
9750 if !SAID.swap(true, Ordering::Relaxed) {
9751 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9752 }
9753 }
9754 return crate::gpu::BatchGraphOutcome::Declined;
9755 };
9756 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9757 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9758 }
9759 crate::gpu::forward_batch_graph(
9760 &model,
9761 self.graph_kv_id,
9762 &layers,
9763 &self.inv_freq,
9764 hiddens,
9765 nh,
9766 nkv,
9767 hd,
9768 rd,
9769 self.hidden_size,
9770 self.intermediate_size,
9771 positions,
9772 self.kv_cache.max_seq_len,
9773 gemma,
9774 self.rms_eps as f32,
9775 self.attn_scale,
9776 k,
9777 &(0..self.num_layers)
9778 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9779 .collect::<Vec<_>>(),
9780 self.o1_epoch,
9781 spec,
9782 )
9783 }
9784
9785 fn draft_probe() -> bool {
9789 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9790 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9791 }
9792
9793 #[cfg(feature = "gpu")]
9805 fn dsv4_spec_on() -> bool {
9806 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9807 *ON.get_or_init(|| {
9808 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9812 return v != "0";
9813 }
9814 std::env::var("CMF_DSV4_SPEC")
9821 .map(|v| v != "0")
9822 .unwrap_or_else(|_| {
9823 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9824 })
9825 })
9826 }
9827
9828 #[cfg(feature = "gpu")]
9835 fn dsv4_spec_step(
9836 &mut self,
9837 tip_token: u32,
9838 t_next: u32,
9839 next_pos: usize,
9840 max_extra: usize,
9841 drafted: &mut usize,
9842 accepted_ctr: &mut usize,
9843 ) -> Option<(Vec<u32>, usize)> {
9844 let t_all = std::time::Instant::now();
9845 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9846 thread_local! {
9847 static LAST: std::cell::Cell<Option<std::time::Instant>> =
9848 const { std::cell::Cell::new(None) };
9849 }
9850 LAST.with(|l| {
9851 if let Some(prev) = l.get() {
9852 eprintln!(
9853 "между раундами {:.1} мс",
9854 prev.elapsed().as_secs_f64() * 1e3
9855 );
9856 }
9857 l.set(Some(std::time::Instant::now()));
9858 });
9859 }
9860 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9861 eprintln!("spec_step: вход pos={next_pos}");
9862 }
9863 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9864 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9865 if self.dspark.is_none() {
9867 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9868 if t.is_empty() {
9869 return None;
9870 }
9871 crate::dsv4::dspark_arm(&t, cfg.dim);
9872 self.dspark = Some(crate::dsv4::DsparkState::new(
9873 self.dsv4_mtp.len(),
9874 &cfg,
9875 t.len(),
9876 ));
9877 }
9878 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9879 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9880 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9881 eprintln!("spec_step: пак не построился (targets {targets:?})");
9882 }
9883 let pack = pack?;
9884 let block = crate::dsv4::dspark_block();
9885 let b_box = self.dsv4.as_mut()?;
9886 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9887 let ds = self.dspark.as_mut()?;
9888 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9891 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9892 if dbg {
9893 eprintln!("spec_step: нет захвата");
9894 }
9895 return None;
9896 }
9897 ds.have_hidden = true;
9898 let tip_pos = next_pos.checked_sub(1)?;
9899 let draft_started = std::time::Instant::now();
9900 let mut conf = Vec::new();
9901 let props = crate::dsv4::dspark_draft_gpu(
9902 g,
9903 &self.dsv4_mtp,
9904 &cfg,
9905 ds,
9906 pack,
9907 st.kv_id,
9908 tip_token,
9909 tip_pos,
9910 self.pool.as_deref(),
9911 &mut conf,
9912 );
9913 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9914 *drafted += block;
9915 if props.is_empty() || props[0] != t_next {
9916 if dbg {
9917 eprintln!(
9918 "spec_step: черновик {} (props0={:?} t_next={t_next})",
9919 if props.is_empty() {
9920 "пуст"
9921 } else {
9922 "мимо"
9923 },
9924 props.first()
9925 );
9926 }
9927 return None;
9928 }
9929 let mut k_verify = crate::dsv4::dspark_verify_k()
9936 .min(props.len())
9937 .min(max_extra.saturating_add(1));
9938 let conf_min = {
9944 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9945 *M.get_or_init(|| {
9946 std::env::var("CMF_DSPARK_CONF_MIN")
9947 .ok()
9948 .and_then(|v| v.parse().ok())
9949 .unwrap_or(0.0)
9950 })
9951 };
9952 if conf_min > 0.0 && conf.len() >= props.len() {
9953 let mut keep = 1usize;
9954 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9955 keep += 1;
9956 }
9957 k_verify = k_verify.min(keep.max(2));
9958 }
9959 if k_verify < 2 {
9960 return None;
9961 }
9962 let mut fed = Vec::with_capacity(k_verify);
9963 fed.push(t_next);
9964 fed.extend_from_slice(&props[1..k_verify]);
9965 let mut argmax = Vec::new();
9966 let mut logits_all = Vec::new();
9967 let mut walked = Vec::new();
9968 let txn = crate::dsv4::dsv4_verify_chunk(
9969 g,
9970 layers,
9971 &cfg,
9972 st,
9973 &fed,
9974 next_pos,
9975 &self.inv_freq,
9976 self.pool.as_deref(),
9977 &targets,
9978 &mut argmax,
9979 &mut logits_all,
9980 &mut walked,
9981 );
9982 if txn.is_none() && dbg {
9983 eprintln!("spec_step: verify отказал");
9984 }
9985 let txn = txn?;
9986 let spec_gpu_end = txn.gpu_end;
9987 let b = fed.len();
9988 let mut accepted = 1usize;
9989 while accepted < b && fed[accepted] == argmax[accepted - 1] {
9990 accepted += 1;
9991 }
9992 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9997 accepted = 1;
9998 }
9999 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
10000 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
10001 }
10002 let t_fin = std::time::Instant::now();
10003 if !crate::dsv4::dsv4_spec_finish(
10004 g,
10005 layers,
10006 &cfg,
10007 st,
10008 txn,
10009 accepted,
10010 &fed,
10011 &self.inv_freq,
10012 self.pool.as_deref(),
10013 ) {
10014 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
10015 return None;
10016 }
10017 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
10018 eprintln!(
10019 "finish(k={accepted}): {:.1} мс",
10020 t_fin.elapsed().as_secs_f64() * 1e3
10021 );
10022 }
10023 *accepted_ctr += accepted - 1;
10024 let (hc, dim) = (cfg.hc_mult, cfg.dim);
10029 let dev_caps: Vec<usize> = targets
10034 .iter()
10035 .copied()
10036 .filter(|&t| t < spec_gpu_end)
10037 .collect();
10038 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
10039 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
10040 return None;
10041 }
10042 for t in 0..accepted {
10043 let tip = t + 1 == accepted;
10044 for (slot, &tl) in targets.iter().enumerate() {
10045 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
10046 let lo = (di * b + t) * hc * dim;
10047 crate::dsv4::dspark_capture(
10048 &caps_all[lo..lo + hc * dim],
10049 &cfg,
10050 slot,
10051 &mut ds.main_hidden,
10052 );
10053 } else if tip
10054 && crate::dsv4::dspark_peek_slot(slot, dim, {
10055 let lo = slot * dim;
10056 &mut ds.main_hidden[lo..lo + dim]
10057 })
10058 {
10059 } else {
10064 crate::dsv4::dspark_capture(
10068 &walked[t * hc * dim..(t + 1) * hc * dim],
10069 &cfg,
10070 slot,
10071 &mut ds.main_hidden,
10072 );
10073 }
10074 }
10075 crate::dsv4::dspark_ring_append(
10076 g,
10077 &self.dsv4_mtp,
10078 &cfg,
10079 ds,
10080 next_pos + t,
10081 self.pool.as_deref(),
10082 );
10083 }
10084 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
10085 self.graph_logits = Some(row);
10086 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
10091 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
10092 crate::dsv4::pick_tally_arm();
10093 }
10094 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
10095 eprintln!(
10096 "spec_step total {:.1} мс (k={accepted})",
10097 t_all.elapsed().as_secs_f64() * 1e3
10098 );
10099 }
10100 Some((fed[1..accepted].to_vec(), next_pos + accepted))
10101 }
10102
10103 fn dspark_probe(&mut self, position: usize, token_id: u32) {
10104 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
10105 return;
10106 }
10107 let trunk_now = crate::dsv4::pick_tally_take();
10109 crate::dsv4::trunk_freq_note(&trunk_now);
10110 if !trunk_now.is_empty() {
10111 self.dspark_trunk_picks.push(trunk_now);
10112 let keep = crate::dsv4::dspark_block();
10113 if self.dspark_trunk_picks.len() > keep {
10114 self.dspark_trunk_picks.remove(0);
10115 }
10116 }
10117 for p in std::mem::take(&mut self.dspark_pending) {
10120 let Some(i) = position.checked_sub(p.0 + 1) else {
10121 continue;
10122 };
10123 let mut p = p;
10124 if i < p.1.len() {
10125 if p.2 && p.1[i] == token_id {
10126 p.3 = i + 1;
10127 } else {
10128 p.2 = false;
10129 }
10130 if i + 1 < p.1.len() {
10131 self.dspark_pending.push(p);
10132 continue;
10133 }
10134 }
10135 self.dspark_hist.push(p.3);
10136 self.dspark_real.push(token_id);
10137 }
10138 let Some(b) = &mut self.dsv4 else { return };
10139 let (g, layers, cfg) = (&b.0, &b.1, b.2);
10140 let n_layers = layers.len();
10141 if self.dspark.is_none() {
10142 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10143 if t.is_empty() {
10144 return;
10145 }
10146 eprintln!(
10147 "DSpark: захват со слоёв {t:?}, блок {}",
10148 crate::dsv4::dspark_block()
10149 );
10150 crate::dsv4::dspark_arm(&t, cfg.dim);
10151 self.dspark = Some(crate::dsv4::DsparkState::new(
10152 self.dsv4_mtp.len(),
10153 &cfg,
10154 t.len(),
10155 ));
10156 }
10157 let ds = self.dspark.as_mut().unwrap();
10158 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
10159 return; }
10161 let mut conf = Vec::new();
10162 crate::dsv4::pick_tally_arm();
10163 let draft_started = std::time::Instant::now();
10168 #[cfg(feature = "gpu")]
10169 let gpu_draft = crate::dsv4::dspark_gpu_on();
10170 #[cfg(not(feature = "gpu"))]
10171 let gpu_draft = false;
10172 let props = if gpu_draft {
10173 #[cfg(feature = "gpu")]
10174 {
10175 let kv_id = b.3.kv_id;
10176 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
10177 Some(pk) => crate::dsv4::dspark_draft_gpu(
10178 g,
10179 &self.dsv4_mtp,
10180 &cfg,
10181 ds,
10182 pk,
10183 kv_id,
10184 token_id,
10185 position,
10186 self.pool.as_deref(),
10187 &mut conf,
10188 ),
10189 None => Vec::new(),
10190 }
10191 }
10192 #[cfg(not(feature = "gpu"))]
10193 Vec::new()
10194 } else {
10195 crate::gpu::cpu_scope(|| {
10196 crate::dsv4::dspark_draft(
10197 g,
10198 &self.dsv4_mtp,
10199 &cfg,
10200 ds,
10201 token_id,
10202 position,
10203 self.pool.as_deref(),
10204 &mut conf,
10205 )
10206 })
10207 };
10208 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10209 let draft_picks = crate::dsv4::pick_tally_take();
10210 crate::dsv4::dspark_freq_note(&draft_picks);
10211 crate::dsv4::pick_tally_arm();
10214 if !props.is_empty() {
10215 let (tu, tt) = {
10219 let flat: Vec<(usize, Vec<usize>)> = self
10220 .dspark_trunk_picks
10221 .iter()
10222 .flat_map(|v| v.iter().cloned())
10223 .collect();
10224 let mut per: std::collections::HashMap<usize, Vec<usize>> =
10226 std::collections::HashMap::new();
10227 for (li, picks) in flat {
10228 per.entry(li).or_default().extend(picks);
10229 }
10230 let n = per.len().max(1);
10231 let mut u = 0usize;
10232 let mut t = 0usize;
10233 for (_, v) in per {
10234 t += v.len();
10235 u += v.iter().collect::<std::collections::HashSet<_>>().len();
10236 }
10237 (u / n, t / n)
10238 };
10239 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
10240 self.dspark_exp.push((tu, tt, du, dt));
10241 self.dspark_pending.push((position, props, true, 0));
10242 }
10243 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
10244 let n = self.dspark_hist.len() as f32;
10245 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
10246 let block = crate::dsv4::dspark_block();
10247 let mut at = vec![0usize; block + 1];
10248 for &k in &self.dspark_hist {
10249 at[k] += 1;
10250 }
10251 let mut surv = Vec::with_capacity(block);
10253 for i in 1..=block {
10254 let k = at[i..].iter().sum::<usize>() as f32 / n;
10255 surv.push(format!("{k:.2}"));
10256 }
10257 let distinct = self
10258 .dspark_real
10259 .iter()
10260 .collect::<std::collections::HashSet<_>>()
10261 .len();
10262 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
10263 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
10264 });
10265 let m = self.dspark_exp.len().max(1);
10266 eprintln!(
10267 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
10268 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
10269 self.dspark_hist.len(),
10270 mean + 1.0,
10271 surv.join(" ")
10272 );
10273 eprintln!(
10274 "DSpark: разных токенов {distinct} из {} (вырожденность), \
10275 эксперты ствол {}/{} на слой за {block} токенов, \
10276 черновик {}/{} за блок, draft {:.2} мс/блок",
10277 self.dspark_real.len(),
10278 tu / m,
10279 tt / m,
10280 du / m,
10281 dt / m,
10282 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
10283 );
10284 }
10285 }
10286
10287 fn forward_layers_upto(
10288 &mut self,
10289 hidden: &[f32],
10290 position: usize,
10291 task_mask: Option<&TaskMask>,
10292 upto: Option<usize>,
10293 ) -> Vec<f32> {
10294 if let Some(plan) = self.gpu_plan.clone() {
10300 if upto.is_none() && plan.len() > 1 {
10301 let mut h = hidden.to_vec();
10302 for &(dev, from, upto_incl) in plan.iter() {
10303 h = crate::gpu::with_device(dev, || {
10304 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
10305 });
10306 }
10307 return h;
10308 }
10309 }
10310 self.forward_layers_span(hidden, position, task_mask, 0, upto)
10311 }
10312
10313 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
10318 self.set_gpu_plan_at(devices, None)
10319 }
10320
10321 pub fn set_gpu_plan_at(
10325 &mut self,
10326 devices: Option<&[usize]>,
10327 at: Option<usize>,
10328 ) -> Result<(), String> {
10329 let Some(devs) = devices.filter(|d| d.len() > 1) else {
10330 self.gpu_plan = None;
10331 return Ok(());
10332 };
10333 self.split_supported()?;
10334 let n = self.num_layers;
10335 if devs.len() > n {
10336 return Err(format!("{} devices for {n} layers", devs.len()));
10337 }
10338 if let Some(k) = at {
10339 if k == 0 || k >= n {
10340 return Err(format!("split at {k}: the model has {n} layers"));
10341 }
10342 if devs.len() == 2 {
10343 self.gpu_plan = Some(std::sync::Arc::new(vec![
10344 (devs[0], 0, k - 1),
10345 (devs[1], k, n - 1),
10346 ]));
10347 return Ok(());
10348 }
10349 return Err(format!(
10350 "an explicit split point takes exactly 2 devices, got {}",
10351 devs.len()
10352 ));
10353 }
10354 let per = n.div_ceil(devs.len());
10355 let mut plan = Vec::with_capacity(devs.len());
10356 let mut from = 0usize;
10357 for &d in devs {
10358 if from >= n {
10359 break;
10360 }
10361 let upto = (from + per - 1).min(n - 1);
10362 plan.push((d, from, upto));
10363 from = upto + 1;
10364 }
10365 self.gpu_plan = Some(std::sync::Arc::new(plan));
10366 Ok(())
10367 }
10368
10369 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
10371 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
10372 }
10373
10374 fn forward_layers_span(
10380 &mut self,
10381 hidden: &[f32],
10382 position: usize,
10383 task_mask: Option<&TaskMask>,
10384 from: usize,
10385 upto: Option<usize>,
10386 ) -> Vec<f32> {
10387 debug_assert!(
10388 from == 0
10389 || (self.dsv4.is_none()
10390 && self.dsv41.is_none()
10391 && self.qwen4_exp.is_none()
10392 && self.g3n.is_none())
10393 );
10394 if let Some(b) = &mut self.qwen4_exp {
10395 let _ = (task_mask, upto);
10396 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10397 let mut logits = Vec::new();
10398 crate::qwen4_exp::forward_token(
10399 &b.0,
10400 &b.1,
10401 &b.2,
10402 &mut b.3,
10403 token_id,
10404 position,
10405 &self.inv_freq,
10406 self.pool.as_deref(),
10407 &mut logits,
10408 true,
10409 );
10410 self.graph_logits = Some(logits);
10411 return vec![0.0; self.hidden_size];
10412 }
10413 if let Some(b) = &mut self.dsv4 {
10419 let _ = (task_mask, upto);
10420 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10421 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
10422 st.pos = position;
10423 let mut logits = Vec::new();
10424 crate::dsv4::forward_token(
10425 g,
10426 layers,
10427 &cfg,
10428 st,
10429 token_id,
10430 &self.inv_freq,
10431 self.pool.as_deref(),
10432 &mut logits,
10433 );
10434 self.graph_logits = Some(logits);
10435 self.dspark_probe(position, token_id);
10436 return vec![0.0; self.hidden_size];
10439 }
10440 if let Some(b) = &mut self.dsv41 {
10442 let _ = (task_mask, upto);
10443 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10444 let mut logits = Vec::new();
10445 crate::dsv41::forward_token(
10446 &b.0,
10447 &b.1,
10448 &b.2,
10449 &mut b.3,
10450 token_id,
10451 position,
10452 self.pool.as_deref(),
10453 &mut logits,
10454 );
10455 self.graph_logits = Some(logits);
10456 return vec![0.0; self.hidden_size];
10457 }
10458 if let Some(b) = &self.g3n {
10461 let _ = (task_mask, upto);
10462 return crate::g3n::g3n_forward(
10463 &b.0,
10464 &b.1,
10465 hidden,
10466 position,
10467 &mut self.kv_cache.layers,
10468 self.num_heads,
10469 self.num_kv_heads,
10470 self.head_dim,
10471 self.pool.as_deref(),
10472 );
10473 }
10474 let mut h = hidden.to_vec();
10475 let (nh, _nkv, _hd, hs, _rd, eps) = (
10478 self.num_heads,
10479 self.num_kv_heads,
10480 self.head_dim,
10481 self.hidden_size,
10482 self.rotary_dim,
10483 self.rms_eps,
10484 );
10485 let pool = self.pool.clone();
10486 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
10498 let graph_on = match graph_env.as_deref() {
10499 Some("0") => false,
10500 Some("prefill") => false, Some(_) => true,
10502 None => crate::gpu::wgpu_graph_default(),
10508 };
10509 let graph_trusted =
10510 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
10511 let race_eligible = graph_on
10512 && upto.is_none()
10513 && task_mask.is_none()
10514 && from == 0
10515 && !crate::gpu::graph_unsupported();
10516 let mut tail_start = 0usize;
10517 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
10518 let t_graph = std::time::Instant::now();
10519 let mut lg = Vec::new();
10520 let mut gl = 0usize;
10521 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
10522 let declined = built.is_none();
10523 let built = match built {
10524 Some(Ok(hh)) => Some(hh),
10525 Some(Err(())) => {
10526 self.clear_sequence_state();
10530 self.graph_failed
10531 .store(true, std::sync::atomic::Ordering::Relaxed);
10532 self.cancel
10533 .store(true, std::sync::atomic::Ordering::Relaxed);
10534 tracing::error!("token graph failed after admission; sequence state cleared");
10535 return vec![0.0; self.hidden_size];
10536 }
10537 None => None,
10538 };
10539 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
10544 crate::gpu::graph_mark_unsupported();
10545 }
10546 graph_note(built.is_some(), gl, self.num_layers);
10547 if let Some(hh) = built {
10548 let dur = t_graph.elapsed();
10549 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10550 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
10551 }
10552 if gl > 0 && gl < self.num_layers {
10553 h = hh;
10559 tail_start = gl;
10560 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
10561 if !graph_trusted {
10562 crate::gpu::graph_race_record(true, dur);
10563 }
10564 if !lg.is_empty() {
10565 lg.resize(self.vocab_size, 0.0);
10568 if let Some(c) = self.final_softcap {
10569 for l in lg.iter_mut() {
10570 *l = c * (*l / c).tanh();
10571 }
10572 }
10573 self.graph_logits = Some(lg);
10574 }
10575 return hh;
10576 }
10577 }
10583 }
10584 let span = from > 0 || upto.is_some();
10608 if span && graph_on && task_mask.is_none() && graph_trusted {
10609 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
10610 let mut lg = Vec::new();
10611 let mut gl = 0usize;
10612 let span_res =
10613 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
10614 let span_res = match span_res {
10615 Some(Ok(hh)) => Some(hh),
10616 Some(Err(())) => {
10617 self.clear_sequence_state();
10618 self.graph_failed
10619 .store(true, std::sync::atomic::Ordering::Relaxed);
10620 self.cancel
10621 .store(true, std::sync::atomic::Ordering::Relaxed);
10622 tracing::error!(
10623 "span token graph failed after admission; sequence state cleared"
10624 );
10625 return vec![0.0; self.hidden_size];
10626 }
10627 None => None,
10628 };
10629 graph_note(span_res.is_some(), gl, upto_excl - from);
10630 if std::env::var("CMF_GPU_DEBUG").is_ok() {
10631 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
10635 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
10636 eprintln!(
10637 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
10638 upto_excl - from,
10639 span_res.is_some()
10640 );
10641 }
10642 }
10643 if let Some(hh) = span_res {
10644 if gl == upto_excl - from {
10645 if !lg.is_empty() {
10646 lg.resize(self.vocab_size, 0.0);
10647 if let Some(c) = self.final_softcap {
10648 for l in lg.iter_mut() {
10649 *l = c * (*l / c).tanh();
10650 }
10651 }
10652 self.graph_logits = Some(lg);
10653 }
10654 crate::gpu::set_layer(-1);
10655 return hh;
10656 }
10657 h = hh;
10659 tail_start = from + gl;
10660 }
10661 }
10662 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
10663
10664 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10670 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10671
10672 #[cfg(target_os = "macos")]
10673 let mut gpu_skip_until = 0usize;
10674 for li in tail_start.max(from)..self.num_layers {
10675 let _capacity_tail = automatic_gpu_prefix
10676 .filter(|&prefix| li >= prefix)
10677 .map(|_| crate::gpu::enter_cpu_scope());
10678 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
10680 if li > u {
10681 break;
10682 }
10683 }
10684 if let Some(mask) = task_mask {
10685 if !mask.layer_alive(li) {
10686 continue; }
10688 }
10689 #[cfg(target_os = "macos")]
10693 {
10694 if li < gpu_skip_until {
10695 continue;
10696 }
10697 if task_mask.is_none() {
10698 let end = self.q1_graph_gpu(li, upto, position, &mut h);
10699 if self
10700 .graph_failed
10701 .load(std::sync::atomic::Ordering::Relaxed)
10702 {
10703 return vec![0.0; self.hidden_size];
10707 }
10708 if end > li {
10709 gpu_skip_until = end;
10710 if self.is_loop_end(end - 1) && end < self.num_layers {
10713 h = inference::rms_norm(
10714 &h,
10715 &self.weights.final_norm,
10716 self.rms_eps,
10717 self.norm_style,
10718 );
10719 }
10720 continue;
10721 }
10722 }
10723 }
10724
10725 let lw = &self.weights.layers[self.phys_layer(li)];
10726 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10727 if tp.parse::<usize>().ok() == Some(position) {
10728 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10729 eprintln!(
10730 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10731 h[0], h[1]
10732 );
10733 }
10734 }
10735 inference::rms_norm_into(
10738 &h,
10739 &lw.input_norm,
10740 self.rms_eps,
10741 self.norm_style,
10742 &mut self.ws.n1,
10743 );
10744
10745 let attn_out = match &lw.attn {
10746 AttnKind::Mla(w) => {
10747 let inv_freq_l = self.layer_inv_freq(li);
10748 let rs = self.layer_rope_scale(li);
10749 let eps = self.rms_eps;
10750 let pool = self.pool.clone();
10751 mla_attention(
10752 w,
10753 &self.ws.n1,
10754 &mut self.kv_cache.layers[li],
10755 position,
10756 &inv_freq_l,
10757 rs,
10758 eps,
10759 pool.as_deref(),
10760 )
10761 }
10762 AttnKind::Linear(w) => {
10763 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10764 vmf_phase_forward(
10765 &self.ws.n1,
10766 w,
10767 &cfg,
10768 &mut self.kv_cache.layers[li].linear_state,
10769 self.pool.as_deref(),
10770 )
10771 }
10772 AttnKind::Kda(w) => {
10773 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10774 crate::linear_core::kda_forward(
10775 &self.ws.n1,
10776 w,
10777 &cfg,
10778 &mut self.kv_cache.layers[li].linear_state,
10779 self.pool.as_deref(),
10780 )
10781 }
10782 AttnKind::LinearGdn(w) => {
10783 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10784 gdn_forward(
10785 &self.ws.n1,
10786 w,
10787 &cfg,
10788 &mut self.kv_cache.layers[li].linear_state,
10789 self.pool.as_deref(),
10790 )
10791 }
10792 AttnKind::ShortConv(w) => {
10793 let cfg = self
10794 .short_conv_cfg
10795 .expect("short-conv layer without short_conv_cfg");
10796 short_conv_forward(
10797 &self.ws.n1,
10798 w,
10799 &cfg,
10800 &mut self.kv_cache.layers[li].linear_state,
10801 self.pool.as_deref(),
10802 )
10803 }
10804 AttnKind::Full {
10805 wq,
10806 wk,
10807 wv,
10808 wo,
10809 q_norm,
10810 k_norm,
10811 output_gate,
10812 softplus_gate,
10813 bias,
10814 } if self.kv_cache.layers[li].o1_sealed() => {
10815 let inv_freq_l = self.layer_inv_freq(li);
10818 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10819 let cfg = QwenAttnCfg {
10820 num_heads: self.layer_num_heads(li),
10821 num_kv_heads: nkv_l,
10822 head_dim: hd_l,
10823 hidden_size: hs,
10824 position,
10825 inv_freq: &inv_freq_l,
10826 rotary_dim: rd_l,
10827 scale: self.attn_scale,
10828 softcap: self.attn_softcap,
10829 window: None,
10830 v_norm: self.attn_v_norm,
10831 qk_norm_after_rope: self.qk_norm_after_rope,
10832 q_norm: q_norm.as_deref(),
10833 k_norm: k_norm.as_deref(),
10834 output_gate: *output_gate,
10835 softplus_gate: softplus_gate
10836 .as_ref()
10837 .map(|(gate, per_head)| (gate, *per_head)),
10838 rope_scale: self.layer_rope_scale(li),
10839 bias: bias
10840 .as_ref()
10841 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10842 rms_eps: eps,
10843 norm_style: self.norm_style,
10844 pool: pool.as_deref(),
10845 };
10846 attention::qwen_attention_nystrom(
10847 &self.ws.n1,
10848 wq,
10849 wk,
10850 wv,
10851 wo,
10852 &mut self.kv_cache.layers[li],
10853 &cfg,
10854 )
10855 }
10856 AttnKind::Full {
10857 wq,
10858 wk,
10859 wv,
10860 wo,
10861 q_norm,
10862 k_norm,
10863 output_gate,
10864 softplus_gate,
10865 bias,
10866 } => 'attn: {
10867 if graph_on
10870 && !*output_gate
10871 && softplus_gate.is_none()
10872 && self.attention_heads_per_layer.is_none()
10873 && bias.is_none()
10874 && task_mask.is_none()
10875 {
10876 let inv_freq_l = self.layer_inv_freq(li);
10877 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10878 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10879 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10880 wq.mapped_q1(),
10881 wk.mapped_q1(),
10882 wv.mapped_q1(),
10883 wo.mapped_q1(),
10884 ) {
10885 let gm = gm.clone();
10886 let mut out = vec![0f32; hs];
10887 let cache = &self.kv_cache.layers[li];
10888 if crate::gpu::attn_dropin(
10889 &gm,
10890 self.graph_kv_id,
10891 li,
10892 &self.ws.n1,
10893 qi,
10894 ki,
10895 vi,
10896 oi,
10897 q_norm.as_deref(),
10898 k_norm.as_deref(),
10899 self.qk_norm_after_rope,
10900 &inv_freq_l,
10901 nh,
10902 nkv_l,
10903 hd_l,
10904 rd_l,
10905 hs,
10906 position,
10907 self.kv_cache.max_seq_len,
10908 gemma,
10909 eps as f32,
10910 cache.k_heads(),
10911 cache.v_heads(),
10912 &mut out,
10913 ) {
10914 break 'attn out;
10915 }
10916 }
10917 }
10918 let masked = task_mask
10919 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10920 .unwrap_or(false);
10921 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10922 match (masked, f32_view) {
10923 (true, (Some(q), Some(k), Some(v), Some(o))) => {
10926 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10927 attention::multi_head_attention(
10928 &self.ws.n1,
10929 q,
10930 k,
10931 v,
10932 o,
10933 &mut self.kv_cache.layers[li],
10934 self.num_heads,
10935 self.num_kv_heads,
10936 self.head_dim,
10937 self.hidden_size,
10938 position,
10939 &active_heads,
10940 &self.inv_freq,
10941 )
10942 }
10943 (masked, _) => {
10944 if masked {
10945 tracing::warn!(
10946 "layer {li}: head mask on quantized weights not \
10947 supported yet — executing dense"
10948 );
10949 }
10950 let inv_freq_l = self.layer_inv_freq(li);
10951 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10952 let cfg = QwenAttnCfg {
10953 num_heads: self.layer_num_heads(li),
10954 num_kv_heads: nkv_l,
10955 head_dim: hd_l,
10956 hidden_size: hs,
10957 position,
10958 inv_freq: &inv_freq_l,
10959 rotary_dim: rd_l,
10960 scale: self.attn_scale,
10961 softcap: self.attn_softcap,
10962 window: self.layer_window(li),
10963 v_norm: self.attn_v_norm,
10964 qk_norm_after_rope: self.qk_norm_after_rope,
10965 q_norm: q_norm.as_deref(),
10966 k_norm: k_norm.as_deref(),
10967 output_gate: *output_gate,
10968 softplus_gate: softplus_gate
10969 .as_ref()
10970 .map(|(gate, per_head)| (gate, *per_head)),
10971 rope_scale: self.layer_rope_scale(li),
10972 bias: bias
10973 .as_ref()
10974 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10975 rms_eps: eps,
10976 norm_style: self.norm_style,
10977 pool: pool.as_deref(),
10978 };
10979 attention::qwen_attention(
10980 &self.ws.n1,
10981 wq,
10982 wk,
10983 wv,
10984 wo,
10985 &mut self.kv_cache.layers[li],
10986 &cfg,
10987 )
10988 }
10989 }
10990 }
10991 };
10992 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10995 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10996 None => attn_out,
10997 };
10998 let lw = &self.weights.layers[self.phys_layer(li)];
10999 inference::add_rmsnorm_fused_into(
11000 &mut h,
11001 &attn_out,
11002 &lw.post_norm,
11003 self.rms_eps,
11004 self.norm_style,
11005 &mut self.ws.p1,
11006 );
11007 let mut attn_out = attn_out;
11008 attention::recycle_buf(&mut attn_out);
11009 let post_normed = &self.ws.p1;
11010
11011 let ffn_masked = task_mask
11012 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
11013 .unwrap_or(false);
11014 let ffn_out = match (ffn_masked, &lw.ffn) {
11026 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
11030 let row = task_mask
11031 .and_then(|tm| tm.ffn_masks.get(li))
11032 .map(|v| v.as_slice());
11033 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
11034 }
11035 (true, FfnKind::Dense(d)) => {
11036 let tm = task_mask.unwrap();
11037 let alive = tm.ffn_active_count(li);
11038 let deep = alive * 2 <= self.intermediate_size;
11039 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
11040 let active = tm.ffn_active_indices(li);
11041 sparse_ffn_quant(
11042 d,
11043 post_normed,
11044 &active,
11045 self.hidden_size,
11046 self.pool.as_deref(),
11047 )
11048 } else if deep
11049 && let (Some(g), Some(u), Some(dn)) = (
11050 d.gate_proj.as_f32(),
11051 d.up_proj.as_f32(),
11052 d.down_proj.as_f32(),
11053 )
11054 {
11055 let active = tm.ffn_active_indices(li);
11056 inference::sparse_ffn_forward(
11057 post_normed,
11058 g,
11059 u,
11060 dn,
11061 self.hidden_size,
11062 self.intermediate_size,
11063 &active,
11064 self.pool.as_deref(),
11065 )
11066 } else {
11067 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
11068 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
11069 }
11070 }
11071 (true, FfnKind::Moe(m)) => {
11072 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
11076 ffn_forward(
11077 &lw.ffn,
11078 post_normed,
11079 self.pool.as_deref(),
11080 allowed.as_deref(),
11081 )
11082 }
11083 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
11084 dm,
11085 post_normed,
11086 &h,
11087 self.rms_eps,
11088 self.norm_style,
11089 self.pool.as_deref(),
11090 ),
11091 (false, _) => match &lw.ffn {
11092 FfnKind::DenseMoe(dm) => dense_moe_ffn(
11093 dm,
11094 post_normed,
11095 &h,
11096 self.rms_eps,
11097 self.norm_style,
11098 self.pool.as_deref(),
11099 ),
11100 _ => {
11101 let allowed = match (&lw.ffn, task_mask) {
11102 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
11103 _ => None,
11104 };
11105 ffn_forward(
11106 &lw.ffn,
11107 post_normed,
11108 self.pool.as_deref(),
11109 allowed.as_deref(),
11110 )
11111 }
11112 },
11113 };
11114 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
11115 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
11116 None => ffn_out,
11117 };
11118 for (i, &f) in ffn_out.iter().enumerate() {
11119 h[i] += f;
11120 }
11121 let mut ffn_out = ffn_out;
11122 attention::recycle_buf(&mut ffn_out);
11123
11124 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
11126 for v in h.iter_mut() {
11127 *v *= sc;
11128 }
11129 }
11130
11131 if self.is_loop_end(li) && li + 1 < self.num_layers {
11134 h = inference::rms_norm(
11135 &h,
11136 &self.weights.final_norm,
11137 self.rms_eps,
11138 self.norm_style,
11139 );
11140 }
11141
11142 if self.dyn_phi_layer == Some(li) {
11146 self.update_dyn_phi(&h);
11147 }
11148 }
11149 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
11151 crate::gpu::graph_race_record(false, t.elapsed());
11152 }
11153
11154 h
11155 }
11156
11157 fn update_dyn_phi(&mut self, h: &[f32]) {
11160 const A: f32 = 0.2;
11161 if self.dyn_phi_ema.len() != h.len() {
11162 self.dyn_phi_ema = vec![0.0; h.len()];
11163 self.dyn_phi_seen = 0;
11164 }
11165 if self.dyn_phi_seen == 0 {
11166 self.dyn_phi_ema.copy_from_slice(h);
11167 } else {
11168 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
11169 *e = (1.0 - A) * *e + A * v;
11170 }
11171 }
11172 self.dyn_phi_seen += 1;
11173 }
11174
11175 pub fn dyn_phi(&self) -> &[f32] {
11177 &self.dyn_phi_ema
11178 }
11179
11180 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
11182 self.dyn_phi_layer = layer;
11183 self.dyn_phi_ema.clear();
11184 self.dyn_phi_seen = 0;
11185 }
11186
11187 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
11189 let Some(model) = &self.model else {
11190 return Vec::new();
11191 };
11192 model
11193 .header
11194 .skills
11195 .iter()
11196 .enumerate()
11197 .filter_map(|(i, sk)| {
11198 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
11199 let sel = sk.selection.as_ref()?;
11200 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
11201 })
11202 .collect()
11203 }
11204
11205 pub fn active_skill(&self) -> Option<usize> {
11207 self.dyn_active
11208 }
11209
11210 pub fn enable_dynamic_routing(&mut self) -> usize {
11215 use crate::swarm::{DynRouter, RoutableSkill};
11216 let Some(model) = self.model.clone() else {
11217 return 0;
11218 };
11219 if self.dyn_blend_loaded {
11222 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
11223 return 0;
11224 }
11225 if let Some(a) = self.dyn_active {
11229 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
11230 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
11231 return 0;
11232 }
11233 }
11234 let hidden = self.hidden_size;
11235 let mut skills = Vec::new();
11236 for (idx, id, _phi) in self.dynamic_skills() {
11237 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
11238 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
11239 skills.push(rs);
11240 }
11241 }
11242 }
11243 if skills.is_empty() {
11244 return 0;
11245 }
11246 let phi = skills[0].phi_layer;
11248 if skills.iter().any(|s| s.phi_layer != phi) {
11249 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
11250 }
11251 let n = skills.len();
11252 self.set_dyn_phi_layer(Some(phi));
11253 self.dyn_router = Some(DynRouter::new(skills));
11254 n
11255 }
11256
11257 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
11259 self.dyn_router
11260 .as_ref()
11261 .map(|r| r.switches.clone())
11262 .unwrap_or_default()
11263 }
11264
11265 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
11268 let rows = self.weights.lm_head.rows();
11269 let mut logits = attention::take_buf(rows.min(self.vocab_size));
11270 self.weights
11271 .lm_head
11272 .matvec(hidden, &mut logits, self.pool.as_deref());
11273 logits.resize(self.vocab_size, 0.0);
11274 if let Some(m) = self.logit_multiplier {
11275 for l in logits.iter_mut() {
11276 *l *= m;
11277 }
11278 }
11279 if let Some(c) = self.final_softcap {
11280 for l in logits.iter_mut() {
11281 *l = c * (*l / c).tanh();
11282 }
11283 }
11284 if let Some(cm) = self.head_clusters.as_ref() {
11285 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
11286 }
11287 logits
11288 }
11289
11290 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
11293 let h = hidden.len();
11294 let ncl = cm.len() / h.max(1);
11295 if ncl == 0 || logits.len() % ncl != 0 {
11296 return;
11297 }
11298 let cs = logits.len() / ncl;
11299 let mut lc = vec![0.0f32; ncl];
11301 for c in 0..ncl {
11302 let row = &cm[c * h..(c + 1) * h];
11303 let mut s = 0.0f32;
11304 for j in 0..h {
11305 s += row[j] * hidden[j];
11306 }
11307 lc[c] = s;
11308 }
11309 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11310 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
11311 for c in 0..ncl {
11312 let blk = &mut logits[c * cs..(c + 1) * cs];
11313 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11314 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
11315 let add = lc[c] - lse - bl;
11316 for v in blk.iter_mut() {
11317 *v += add;
11318 }
11319 }
11320 }
11321
11322 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
11327 self.clear_sequence_state();
11328 crate::gpu::graph_race_begin_generation();
11332 if task_mask.is_none() {
11333 self.o1_begin();
11334 }
11335 let mut hidden = vec![0.0f32; self.hidden_size];
11336 for (pos, &id) in ids.iter().enumerate() {
11337 let emb = self.embed_single(id);
11338 hidden = self.forward_layers(&emb, pos, task_mask);
11339 }
11340 if let Err(err) = self.o1_seal_checked() {
11341 self.o1_fail(err);
11342 }
11343 inference::rms_norm_into(
11344 &hidden,
11345 &self.weights.final_norm,
11346 self.rms_eps,
11347 self.norm_style,
11348 &mut self.ws.n1,
11349 );
11350 self.lm_head_forward(&self.ws.n1)
11351 }
11352}
11353
11354pub fn create_test_pipeline(
11356 hidden_size: usize,
11357 intermediate_size: usize,
11358 num_heads: usize,
11359 num_kv_heads: usize,
11360 head_dim: usize,
11361 num_layers: usize,
11362 vocab_size: usize,
11363) -> Pipeline {
11364 let synth = |n: usize, salt: usize| -> Vec<f32> {
11367 (0..n)
11368 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
11369 .collect()
11370 };
11371 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11372 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11373 };
11374 let layer_weights: Vec<LayerWeights> = (0..num_layers)
11375 .map(|li| LayerWeights {
11376 input_norm: vec![1.0; hidden_size],
11377 post_norm: vec![1.0; hidden_size],
11378 attn_out_norm: None,
11379 ffn_out_norm: None,
11380 layer_scale: None,
11381 ffn: FfnKind::Dense(DenseFfn {
11382 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
11383 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
11384 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
11385 act: Act::Silu,
11386 down_t: None,
11387 segs: Vec::new(),
11388 }),
11389 attn: AttnKind::Full {
11390 bias: None,
11391 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
11392 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
11393 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
11394 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
11395 q_norm: None,
11396 k_norm: None,
11397 output_gate: false,
11398 softplus_gate: None,
11399 },
11400 })
11401 .collect();
11402
11403 Pipeline::new(
11404 Tokenizer::byte_level(),
11405 PipelineWeights {
11406 embed_tokens: qt(vocab_size, hidden_size, 100),
11407 layers: layer_weights,
11408 lm_head: qt(vocab_size, hidden_size, 200),
11409 final_norm: vec![1.0; hidden_size],
11410 },
11411 hidden_size,
11412 intermediate_size,
11413 num_heads,
11414 num_kv_heads,
11415 head_dim,
11416 num_layers,
11417 num_layers, false, vocab_size,
11420 1e-6,
11421 10_000.0,
11422 NormStyle::Qwen,
11423 4096,
11424 SamplerConfig {
11425 seed: Some(42),
11426 ..Default::default()
11427 },
11428 )
11429}
11430
11431#[inline]
11436fn mask_bit(row: &[u8], j: usize) -> bool {
11437 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
11438}
11439
11440fn mask_gain() -> f32 {
11451 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11452 *G.get_or_init(|| {
11453 std::env::var("CMF_FFN_MASK_GAIN")
11454 .ok()
11455 .and_then(|v| v.parse().ok())
11456 .unwrap_or(1.0)
11457 })
11458}
11459
11460fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
11461 let fill = meanfill().and_then(|(i, v)| {
11464 let li = crate::gpu::cur_layer();
11465 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
11466 });
11467 for r in 0..rows {
11468 let base = r * inter;
11469 for (bi, &byte) in row.iter().enumerate() {
11470 if byte == 0xFF {
11471 continue;
11472 }
11473 let j0 = bi * 8;
11474 for bit in 0..8 {
11475 let j = j0 + bit;
11476 if j < inter && byte & (1 << bit) == 0 {
11477 g[base + j] = fill.map_or(0.0, |f| f[j]);
11478 }
11479 }
11480 }
11481 }
11482 let gain = mask_gain();
11483 if gain != 1.0 {
11484 for v in g[..rows * inter].iter_mut() {
11485 *v *= gain;
11486 }
11487 }
11488}
11489
11490#[inline]
11492fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
11493 row.is_none_or(|r| mask_bit(r, i))
11494}
11495
11496fn all_bits_on(row: &[u8], n: usize) -> bool {
11499 (0..n).all(|i| mask_bit(row, i))
11500}
11501
11502fn tube_topk() -> usize {
11510 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11511 *K.get_or_init(|| {
11512 std::env::var("CMF_TUBE_TOPK")
11513 .ok()
11514 .and_then(|v| v.parse().ok())
11515 .unwrap_or(0)
11516 })
11517}
11518
11519fn tube_score_oracle() -> bool {
11520 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11521 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
11522}
11523
11524fn tube_ffn_routed(
11531 d: &DenseFfn,
11532 xs: &[f32],
11533 b: usize,
11534 pool: Option<&Pool>,
11535 mask_row: Option<&[u8]>,
11536 k: usize,
11537) -> Vec<f32> {
11538 let hidden = d.down_proj.rows();
11539 let core = d.gate_proj.rows();
11540 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11541 let mut out = match (b, core_full, mask_row) {
11542 (1, true, _) => dense_ffn(d, xs, pool),
11543 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11544 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11545 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11546 };
11547 let cand: Vec<usize> = (0..d.segs.len())
11548 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
11549 .collect();
11550 if cand.is_empty() {
11551 return out;
11552 }
11553 let oracle = tube_score_oracle();
11557 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
11558 let mut scores = vec![0f32; b * cand.len()];
11559 for (ci, &i) in cand.iter().enumerate() {
11560 let seg = &d.segs[i];
11561 let w = seg.width;
11562 let mut g = vec![0.0f32; b * w];
11563 if b == 1 {
11564 seg.gate.matvec(xs, &mut g, pool);
11565 } else {
11566 seg.gate.matmat(xs, b, &mut g, pool);
11567 }
11568 for v in g.iter_mut() {
11569 *v = Act::Silu.combine(*v, 1.0);
11570 }
11571 if !oracle {
11572 for t in 0..b {
11573 scores[t * cand.len() + ci] =
11574 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11575 }
11576 }
11577 if oracle || b > 1 {
11578 let mut u = vec![0.0f32; b * w];
11579 if b == 1 {
11580 seg.up.matvec(xs, &mut u, pool);
11581 } else {
11582 seg.up.matmat(xs, b, &mut u, pool);
11583 }
11584 for (a, &v) in g.iter_mut().zip(u.iter()) {
11585 *a *= v;
11586 }
11587 if oracle {
11588 for t in 0..b {
11589 scores[t * cand.len() + ci] =
11590 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11591 }
11592 }
11593 }
11594 acts.push(g);
11595 }
11596 let keep = k.min(cand.len());
11598 let mut scratch: Vec<f32> = Vec::new();
11599 for t in 0..b {
11600 let mut sc: Vec<(f32, usize)> = (0..cand.len())
11601 .map(|ci| (scores[t * cand.len() + ci], ci))
11602 .collect();
11603 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
11604 let mut alive = vec![false; cand.len()];
11605 for &(_, ci) in sc.iter().take(keep) {
11606 alive[ci] = true;
11607 }
11608 if b > 1 {
11609 for (ci, a) in acts.iter_mut().enumerate() {
11610 if !alive[ci] {
11611 let w = d.segs[cand[ci]].width;
11612 a[t * w..(t + 1) * w].fill(0.0);
11613 }
11614 }
11615 } else {
11616 for (ci, &i) in cand.iter().enumerate() {
11620 if !alive[ci] {
11621 continue;
11622 }
11623 let seg = &d.segs[i];
11624 let w = seg.width;
11625 let g = &mut acts[ci];
11626 if !tube_score_oracle() {
11627 scratch.clear();
11628 scratch.resize(w, 0.0);
11629 seg.up.matvec(xs, &mut scratch, pool);
11630 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
11631 *a *= v;
11632 }
11633 }
11634 let mut acc = vec![0.0f32; hidden];
11635 seg.down.matvec(g, &mut acc, pool);
11636 for (o, a) in out.iter_mut().zip(&acc) {
11637 *o += *a;
11638 }
11639 }
11640 }
11641 }
11642 if b > 1 {
11643 for (ci, &i) in cand.iter().enumerate() {
11644 let seg = &d.segs[i];
11645 let mut acc = vec![0.0f32; b * hidden];
11646 seg.down.matmat(&acts[ci], b, &mut acc, pool);
11647 for (o, a) in out.iter_mut().zip(&acc) {
11648 *o += *a;
11649 }
11650 }
11651 }
11652 out
11653}
11654
11655fn tube_ffn(
11661 d: &DenseFfn,
11662 xs: &[f32],
11663 b: usize,
11664 pool: Option<&Pool>,
11665 mask_row: Option<&[u8]>,
11666) -> Vec<f32> {
11667 if tube_topk() > 0 {
11668 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
11669 }
11670 let hidden = d.down_proj.rows();
11671 let core = d.gate_proj.rows();
11672 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11673 let mut out = match (b, core_full, mask_row) {
11674 (1, true, _) => dense_ffn(d, xs, pool),
11675 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11676 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11677 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11678 };
11679 TUBE_SCRATCH.with(|sc| {
11680 let mut sc = sc.borrow_mut();
11681 let [g, u, acc] = &mut *sc;
11682 for seg in &d.segs {
11683 if !tube_bit(mask_row, seg.start) {
11684 continue;
11685 }
11686 let w = seg.width;
11687 g.resize(b * w, 0.0);
11688 if b == 1
11689 && d.act == Act::Silu
11690 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11691 {
11692 } else {
11694 u.resize(b * w, 0.0);
11695 if b == 1 {
11696 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11697 } else {
11698 seg.gate.matmat(xs, b, g, pool);
11699 seg.up.matmat(xs, b, u, pool);
11700 }
11701 for i in 0..b * w {
11702 g[i] = d.act.combine(g[i], u[i]);
11703 }
11704 }
11705 acc.resize(b * hidden, 0.0);
11706 acc.fill(0.0);
11707 if b == 1 {
11708 seg.down.matvec(g, acc, pool);
11709 } else {
11710 seg.down.matmat(g, b, acc, pool);
11711 }
11712 for (o, a) in out.iter_mut().zip(acc.iter()) {
11713 *o += *a;
11714 }
11715 }
11716 out
11717 })
11718}
11719
11720thread_local! {
11721 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11725 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11726}
11727
11728fn dense_ffn_batch(
11729 d: &DenseFfn,
11730 xs: &[f32],
11731 b: usize,
11732 pool: Option<&Pool>,
11733 mask_row: Option<&[u8]>,
11734) -> Vec<f32> {
11735 let inter = d.gate_proj.rows();
11736 let hidden = d.down_proj.rows();
11737 if mask_row.is_none()
11745 && d.act == Act::Silu
11746 && b >= 32
11747 && crate::gpu::enabled_here()
11748 && !crate::gpu::mm_killed()
11749 && refit_dir().is_none()
11754 && !ffn_probe_active()
11759 {
11760 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11761 d.gate_proj.mapped_q4t(),
11762 d.up_proj.mapped_q4t(),
11763 d.down_proj.mapped_q4t(),
11764 ) {
11765 let mut out = vec![0.0f32; b * hidden];
11766 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11767 return out;
11768 }
11769 }
11770 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11775 d.gate_proj.mapped_q4tp(),
11776 d.up_proj.mapped_q4tp(),
11777 d.down_proj.mapped_q4tp(),
11778 ) {
11779 let mut out = vec![0.0f32; b * hidden];
11780 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11781 return out;
11782 }
11783 }
11784 }
11785 let mut g = vec![0.0f32; b * inter];
11786 d.gate_proj.matmat(xs, b, &mut g, pool);
11787 let mut u = vec![0.0f32; b * inter];
11788 d.up_proj.matmat(xs, b, &mut u, pool);
11789 if gate_topk() > 0 && d.act == Act::Silu {
11790 for t in 0..b {
11791 let row = &mut g[t * inter..(t + 1) * inter];
11792 for v in row.iter_mut() {
11793 *v = Act::Silu.combine(*v, 1.0);
11794 }
11795 keep_top_k(row, gate_topk());
11796 }
11797 for i in 0..b * inter {
11798 g[i] *= u[i];
11799 }
11800 } else {
11801 for i in 0..b * inter {
11802 g[i] = d.act.combine(g[i], u[i]);
11803 }
11804 }
11805 if let Some(row) = mask_row {
11806 zero_masked_cols(&mut g, b, inter, row);
11807 }
11808 if oracle_topk() > 0 {
11809 for t in 0..b {
11810 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11811 }
11812 }
11813 let mut out = vec![0.0f32; b * hidden];
11814 d.down_proj.matmat(&g, b, &mut out, pool);
11815 if refit_dir().is_some() {
11816 let li = crate::gpu::cur_layer();
11817 if li >= 0 {
11818 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11819 }
11820 }
11821 FFN_PROBE.with(|pr| {
11825 if let Some(acc) = pr.borrow_mut().as_mut() {
11826 let li = crate::gpu::cur_layer();
11827 if li < 0 {
11828 return;
11829 }
11830 let Some(row) = acc.get_mut(li as usize) else {
11831 return;
11832 };
11833 let sq = probe_sq();
11834 for t in 0..b {
11835 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11836 *a += if sq {
11837 (v as f64) * (v as f64)
11838 } else {
11839 (v as f64).abs()
11840 };
11841 }
11842 }
11843 }
11844 });
11845 out
11846}
11847
11848fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11853 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11854 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11855 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11856 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11857 if (!on && !dump) || b == 0 {
11858 return;
11859 }
11860 let hidden = xs.len() / b;
11861 if on {
11862 let mut acc = m.act_sq.borrow_mut();
11863 if acc.len() < hidden {
11864 acc.resize(hidden, 0.0);
11865 }
11866 for t in 0..b {
11867 let row = &xs[t * hidden..(t + 1) * hidden];
11868 for (a, &v) in acc.iter_mut().zip(row) {
11869 *a += (v as f64) * (v as f64);
11870 }
11871 }
11872 }
11873 if dump {
11874 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11877 .ok()
11878 .and_then(|v| v.parse().ok())
11879 .unwrap_or(4096);
11880 let mut rows = m.act_rows.borrow_mut();
11881 if rows.len() < cap * hidden {
11882 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11883 rows.extend_from_slice(&xs[..take * hidden]);
11884 }
11885 }
11886}
11887
11888#[derive(Clone, Copy)]
11891struct SendVecs(*mut Vec<f32>);
11892unsafe impl Send for SendVecs {}
11893unsafe impl Sync for SendVecs {}
11894impl SendVecs {
11895 #[inline]
11896 fn at(self, i: usize) -> *mut Vec<f32> {
11897 unsafe { self.0.add(i) }
11898 }
11899}
11900
11901fn moe_ffn_batch(
11902 m: &MoeFfn,
11903 xs: &[f32],
11904 b: usize,
11905 hidden: usize,
11906 pool: Option<&Pool>,
11907 allowed: Option<&[bool]>,
11908) -> Vec<f32> {
11909 accumulate_act(m, xs, b);
11910 let ne = m.experts.len();
11911 let mut logits = vec![0.0f32; b * ne];
11912 match &m.resonance {
11913 Some(r) => {
11914 let hdim = xs.len() / b.max(1);
11915 for bi in 0..b {
11916 r.scores(
11917 &xs[bi * hdim..(bi + 1) * hdim],
11918 &mut logits[bi * ne..(bi + 1) * ne],
11919 );
11920 }
11921 }
11922 None => m.router.matmat(xs, b, &mut logits, pool),
11923 }
11924
11925 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11928 {
11929 let mut st = m.stats.borrow_mut();
11930 if st.len() < ne {
11931 st.resize(ne, 0);
11932 }
11933 for bi in 0..b {
11934 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11935 for &e in &idx {
11936 st[e] += 1;
11937 assign[e].push((bi, p[e] / wsum));
11938 }
11939 }
11940 }
11941
11942 let mut out = vec![0.0f32; b * hidden];
11943 let cols = m.experts[0].gate_proj.cols();
11944 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11945 let sb = list.len();
11946 let mut sub = vec![0.0f32; sb * cols];
11947 for (k, &(bi, _)) in list.iter().enumerate() {
11948 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11949 }
11950 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11951 for (k, &(bi, w)) in list.iter().enumerate() {
11952 for i in 0..hidden {
11953 out[bi * hidden + i] += w * eo[k * hidden + i];
11954 }
11955 }
11956 };
11957 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11963 if pool.is_some() && active.len() >= 8 {
11964 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11965 {
11966 let panel_ptr = SendVecs(panels.as_mut_ptr());
11967 let experts = &m.experts;
11970 let (active_r, assign_r) = (&active, &assign);
11971 let run = |start: usize, end: usize| {
11972 for ai in start..end {
11973 let e = active_r[ai];
11974 let list = &assign_r[e];
11975 let sb = list.len();
11976 let mut sub = vec![0.0f32; sb * cols];
11977 for (k, &(bi, _)) in list.iter().enumerate() {
11978 sub[k * cols..(k + 1) * cols]
11979 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11980 }
11981 unsafe {
11983 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11984 }
11985 }
11986 };
11987 match pool {
11988 Some(p) => p.run_rows(active.len(), &run),
11989 None => run(0, active.len()),
11990 }
11991 }
11992 for (ai, &e) in active.iter().enumerate() {
11993 for (k, &(bi, w)) in assign[e].iter().enumerate() {
11994 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11995 for i in 0..hidden {
11996 out[bi * hidden + i] += w * eo[i];
11997 }
11998 }
11999 }
12000 } else {
12001 for &e in &active {
12002 run_expert(&m.experts[e], &assign[e], &mut out);
12003 }
12004 }
12005 if let Some((se, gate)) = &m.shared {
12006 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
12007 let mut gl = vec![0.0f32; b];
12008 gate.matmat(xs, b, &mut gl, pool);
12009 (0..b)
12010 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
12011 .collect()
12012 } else {
12013 (0..b).map(|bi| (bi, 1.0)).collect()
12014 };
12015 run_expert(se, &all, &mut out);
12016 }
12017 out
12018}
12019
12020thread_local! {
12021 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
12025 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
12026}
12027
12028fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
12030 if gate_topk() > 0
12033 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
12034 {
12035 return out;
12036 }
12037 let prism_body = d.gate_proj.has_prism_contract()
12053 || d.up_proj.has_prism_contract()
12054 || d.down_proj.has_prism_contract();
12055 if !prism_body
12056 && crate::gpu::enabled_here()
12057 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
12058 {
12059 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
12060 crate::gpu::ProbeArm::Gpu
12061 } else {
12062 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
12063 };
12064 match arm {
12065 crate::gpu::ProbeArm::Gpu => {
12066 let t0 = std::time::Instant::now();
12067 if let Some(out) = dense_ffn_gpu(d, x, pool) {
12068 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
12069 return out;
12070 }
12071 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
12075 }
12076 crate::gpu::ProbeArm::CpuTimed => {
12077 let t0 = std::time::Instant::now();
12078 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
12079 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
12080 return out;
12081 }
12082 crate::gpu::ProbeArm::Cpu => {
12083 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
12084 }
12085 }
12086 }
12087 dense_ffn_cpu(d, x, pool)
12088}
12089
12090fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
12092 let inter = d.gate_proj.rows();
12093 FFN_SCRATCH.with(|s| {
12094 let mut s = s.borrow_mut();
12095 let [g, u, ..] = &mut *s;
12096 g.resize(inter, 0.0);
12097 if gate_topk() > 0 {
12100 u.resize(inter, 0.0);
12104 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12105 for i in 0..inter {
12106 g[i] = Act::Silu.combine(g[i], 1.0);
12107 }
12108 keep_top_k(g, gate_topk());
12109 for i in 0..inter {
12110 g[i] *= u[i];
12111 }
12112 } else if d.act == Act::Silu
12113 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
12114 {
12115 } else {
12117 u.resize(inter, 0.0);
12118 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12120 for i in 0..inter {
12121 g[i] = d.act.combine(g[i], u[i]);
12122 }
12123 }
12124 FFN_PROBE.with(|pr| {
12132 if let Some(acc) = pr.borrow_mut().as_mut() {
12133 let li = crate::gpu::cur_layer();
12134 if li >= 0 {
12135 if let Some(row) = acc.get_mut(li as usize) {
12136 match probe_topk() {
12137 0 if probe_sq() => {
12138 for (a, &v) in row.iter_mut().zip(g.iter()) {
12139 *a += (v as f64) * (v as f64);
12140 }
12141 }
12142 0 if probe_signed() => {
12143 for (a, &v) in row.iter_mut().zip(g.iter()) {
12144 *a += v as f64;
12145 }
12146 }
12147 0 => {
12148 for (a, &v) in row.iter_mut().zip(g.iter()) {
12149 *a += (v as f64).abs();
12150 }
12151 }
12152 k => {
12153 let n = g.len();
12154 let k = k.min(n);
12155 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12156 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12157 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12158 });
12159 let thr = *kth;
12160 for (a, &v) in row.iter_mut().zip(g.iter()) {
12161 if v.abs() >= thr {
12162 *a += 1.0;
12163 }
12164 }
12165 }
12166 }
12167 }
12168 }
12169 }
12170 });
12171 if oracle_topk() > 0 {
12172 keep_top_k(g, oracle_topk());
12173 }
12174 {
12175 let li = crate::gpu::cur_layer();
12176 if li >= 0 {
12177 adump_row(li as usize, g);
12178 }
12179 }
12180 let mut out = attention::take_buf(d.down_proj.rows());
12181 d.down_proj.matvec(g, &mut out, pool);
12182 out
12183 })
12184}
12185
12186pub struct RefitAcc {
12199 pub support: Vec<u32>,
12200 pub gss: Vec<f32>,
12201 pub ya: Vec<f32>,
12202 pub hidden: usize,
12203 pub tokens: u64,
12204 pub buf_g: Vec<f32>,
12210 pub buf_o: Vec<f32>,
12211 pub buf_t: usize,
12212}
12213
12214type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
12218
12219static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
12220 std::sync::OnceLock::new();
12221
12222fn ffn_probe_active() -> bool {
12225 FFN_PROBE.with(|p| p.borrow().is_some())
12226}
12227
12228fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
12229 REFIT
12230 .get_or_init(|| {
12231 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
12232 (
12233 d,
12234 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
12235 )
12236 })
12237 })
12238 .as_ref()
12239}
12240
12241fn refit_accumulate(
12243 li: usize,
12244 g: &[f32],
12245 b: usize,
12246 inter: usize,
12247 out: &[f32],
12248 hidden: usize,
12249 pool: Option<&Pool>,
12250) {
12251 let Some((dir, map)) = refit_dir() else {
12252 return;
12253 };
12254 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12255 let (from, to) = *SPAN.get_or_init(|| {
12256 let g = |k: &str, d: usize| {
12257 std::env::var(k)
12258 .ok()
12259 .and_then(|v| v.parse().ok())
12260 .unwrap_or(d)
12261 };
12262 (
12263 g("CMF_FFN_REFIT_FROM", 0),
12264 g("CMF_FFN_REFIT_TO", usize::MAX),
12265 )
12266 });
12267 if li < from || li > to {
12268 return;
12269 }
12270 let mut guard = map.lock().unwrap();
12271 let (map, shared) = &mut *guard;
12272 let acc = match map.entry(li) {
12273 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
12274 std::collections::hash_map::Entry::Vacant(e) => {
12275 let path = format!("{dir}/support.{li}.u32");
12276 let Ok(bytes) = std::fs::read(&path) else {
12277 eprintln!("refit: no {path} — layer {li} skipped");
12278 return;
12279 };
12280 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
12281 let support: Vec<u32> = bytes[4..4 + n * 4]
12282 .chunks_exact(4)
12283 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12284 .collect();
12285 eprintln!(
12286 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
12287 (n * n + hidden * n) as f64 * 4.0 / 1e6
12288 );
12289 e.insert(RefitAcc {
12290 gss: vec![0.0; n * n],
12291 ya: vec![0.0; hidden * n],
12292 buf_g: Vec::new(),
12293 buf_o: Vec::new(),
12294 buf_t: 0,
12295 support,
12296 hidden,
12297 tokens: 0,
12298 })
12299 }
12300 };
12301 let ns = acc.support.len();
12302 let cap = refit_batch();
12304 if acc.buf_g.is_empty() {
12305 acc.buf_g = vec![0.0; ns * cap];
12306 acc.buf_o = vec![0.0; hidden * cap];
12307 }
12308 let take = b.min(cap - acc.buf_t);
12309 for t in 0..take {
12310 let col = acc.buf_t + t;
12311 for (j, &n) in acc.support.iter().enumerate() {
12312 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
12313 }
12314 for h in 0..hidden {
12315 acc.buf_o[h * cap + col] = out[t * hidden + h];
12316 }
12317 }
12318 acc.buf_t += take;
12319 acc.tokens += take as u64;
12320 if acc.buf_t < cap {
12321 return;
12322 }
12323 let bt = acc.buf_t;
12324 acc.buf_t = 0;
12325 let RefitAcc {
12335 gss,
12336 ya,
12337 buf_g,
12338 buf_o,
12339 ..
12340 } = acc;
12341 let need = (ns * ns).max(hidden * ns);
12342 if shared.len() < need {
12343 shared.resize(need, 0.0);
12344 }
12345 let scratch = &mut shared[..];
12346 let _ = bt;
12347 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
12348 add_into(gss, &scratch[..ns * ns], pool);
12349 if crate::gpu::gemm_nt_f32_transient(
12350 buf_o,
12351 buf_g,
12352 &mut scratch[..hidden * ns],
12353 hidden,
12354 cap,
12355 ns,
12356 ) {
12357 add_into(ya, &scratch[..hidden * ns], pool);
12358 } else {
12359 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12360 }
12361 } else {
12362 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
12363 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12364 }
12365 }
12369
12370fn refit_batch() -> usize {
12372 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12373 *B.get_or_init(|| {
12374 std::env::var("CMF_FFN_REFIT_BATCH")
12375 .ok()
12376 .and_then(|v| v.parse().ok())
12377 .unwrap_or(4096)
12378 })
12379}
12380
12381fn accum_outer_t(
12384 c: &mut [f32],
12385 m: usize,
12386 n: usize,
12387 b: usize,
12388 left: &[f32],
12389 right: &[f32],
12390 pool: Option<&Pool>,
12391) {
12392 let ptr = SendMut(c.as_mut_ptr());
12393 let body = |i: usize| {
12394 let ptr = &ptr;
12395 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
12396 for t in 0..b {
12397 let a = left[i * b + t];
12398 if a == 0.0 {
12399 continue;
12400 }
12401 for (j, o) in row.iter_mut().enumerate() {
12402 *o += a * right[j * b + t];
12403 }
12404 }
12405 };
12406 match pool {
12407 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
12408 for i in s..e {
12409 body(i);
12410 }
12411 }),
12412 _ => {
12413 for i in 0..m {
12414 body(i);
12415 }
12416 }
12417 }
12418}
12419
12420fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
12423 let n = dst.len().min(src.len());
12424 match pool {
12425 Some(p) if n >= 1 << 16 => {
12426 let ptr = SendMut(dst.as_mut_ptr());
12427 let f = |s: usize, e: usize| {
12428 let ptr = &ptr;
12429 for blk in s..e {
12430 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
12431 for i in a..b {
12432 unsafe { *ptr.0.add(i) += src[i] };
12433 }
12434 }
12435 };
12436 p.run_rows(n.div_ceil(4096), &f);
12437 }
12438 _ => {
12439 for (d, v) in dst.iter_mut().zip(&src[..n]) {
12440 *d += *v;
12441 }
12442 }
12443 }
12444}
12445
12446fn accum_outer(
12451 c: &mut [f32],
12452 m: usize,
12453 n: usize,
12454 b: usize,
12455 left: &[f32],
12456 right: &[f32],
12457 pool: Option<&Pool>,
12458) {
12459 const TILE: usize = 32;
12460 let tiles = m.div_ceil(TILE);
12461 let cp = SendMut(c.as_mut_ptr());
12462 let body = |ti: usize| {
12463 let cp = &cp;
12464 let i0 = ti * TILE;
12465 let i1 = (i0 + TILE).min(m);
12466 for t in 0..b {
12467 let r = &right[t * n..t * n + n];
12468 for i in i0..i1 {
12469 let a = left[i * b + t];
12470 if a == 0.0 {
12471 continue;
12472 }
12473 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
12475 for (o, v) in row.iter_mut().zip(r) {
12476 *o += a * *v;
12477 }
12478 }
12479 }
12480 };
12481 match pool {
12482 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
12483 for ti in s..e {
12484 body(ti);
12485 }
12486 }),
12487 _ => {
12488 for ti in 0..tiles {
12489 body(ti);
12490 }
12491 }
12492 }
12493}
12494
12495pub fn refit_flush() -> usize {
12497 let Some((dir, map)) = refit_dir() else {
12498 return 0;
12499 };
12500 let guard = map.lock().unwrap();
12501 let mut n = 0;
12502 for (li, acc) in guard.0.iter() {
12503 let w = |name: &str, v: &[f32]| {
12506 let path = format!("{dir}/{name}.{li}.f32");
12507 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
12508 match std::fs::write(&path, &bytes) {
12509 Ok(()) => {}
12510 Err(e) => eprintln!(
12511 "refit: FAILED to write {path} ({} MB): {e}",
12512 bytes.len() / 1_000_000
12513 ),
12514 }
12515 };
12516 w("gss", &acc.gss);
12517 w("ya", &acc.ya);
12518 println!(
12519 "refit L{li}: {} support, {} tokens, hidden {}",
12520 acc.support.len(),
12521 acc.tokens,
12522 acc.hidden
12523 );
12524 n += 1;
12525 }
12526 n
12527}
12528
12529fn adump_row(li: usize, g: &[f32]) {
12534 use std::io::Write as _;
12535 static FILES: std::sync::OnceLock<
12536 Option<(
12537 String,
12538 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
12539 )>,
12540 > = std::sync::OnceLock::new();
12541 let Some((prefix, map)) = FILES
12542 .get_or_init(|| {
12543 std::env::var("CMF_FFN_ADUMP")
12544 .ok()
12545 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
12546 })
12547 .as_ref()
12548 else {
12549 return;
12550 };
12551 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12554 let (from, to) = *SPAN.get_or_init(|| {
12555 let g = |k: &str, d: usize| {
12556 std::env::var(k)
12557 .ok()
12558 .and_then(|v| v.parse().ok())
12559 .unwrap_or(d)
12560 };
12561 (
12562 g("CMF_FFN_ADUMP_FROM", 0),
12563 g("CMF_FFN_ADUMP_TO", usize::MAX),
12564 )
12565 });
12566 if li < from || li > to {
12567 return;
12568 }
12569 let mut map = map.lock().unwrap();
12570 let f = map.entry(li).or_insert_with(|| {
12571 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
12572 });
12573 let mut bytes = Vec::with_capacity(g.len() * 2);
12574 for v in g {
12575 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
12576 }
12577 let _ = f.write_all(&bytes);
12578}
12579
12580fn oracle_topk() -> usize {
12586 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12587 *K.get_or_init(|| {
12588 std::env::var("CMF_FFN_ORACLE_TOPK")
12589 .ok()
12590 .and_then(|v| v.parse().ok())
12591 .unwrap_or(0)
12592 })
12593}
12594
12595fn gate_topk() -> usize {
12601 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12602 *K.get_or_init(|| {
12603 std::env::var("CMF_FFN_GATE_TOPK")
12604 .ok()
12605 .and_then(|v| v.parse().ok())
12606 .unwrap_or(0)
12607 })
12608}
12609
12610fn gate_block() -> usize {
12617 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12618 *B.get_or_init(|| {
12619 std::env::var("CMF_FFN_GATE_BLOCK")
12620 .ok()
12621 .and_then(|v| v.parse().ok())
12622 .unwrap_or(1)
12623 })
12624}
12625
12626fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
12628 let n = g.len();
12629 let nb = n.div_ceil(block);
12630 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
12631 if kb >= nb {
12632 return;
12633 }
12634 let mut score: Vec<f32> = (0..nb)
12635 .map(|b| {
12636 g[b * block..((b + 1) * block).min(n)]
12637 .iter()
12638 .map(|v| v * v)
12639 .sum::<f32>()
12640 })
12641 .collect();
12642 let mut ord = score.clone();
12643 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
12644 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12645 });
12646 let thr = *kth;
12647 for b in 0..nb {
12648 if score[b] < thr {
12649 g[b * block..((b + 1) * block).min(n)].fill(0.0);
12650 }
12651 }
12652 score.clear();
12653}
12654
12655fn keep_top_k(g: &mut [f32], k: usize) {
12657 if gate_block() > 1 {
12658 return keep_top_blocks(g, k, gate_block());
12659 }
12660 let n = g.len();
12661 if k == 0 || k >= n {
12662 return;
12663 }
12664 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12665 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12666 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12667 });
12668 let thr = *kth;
12669 for v in g.iter_mut() {
12670 if v.abs() < thr {
12671 *v = 0.0;
12672 }
12673 }
12674}
12675
12676fn probe_sq() -> bool {
12680 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12681 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
12682}
12683
12684fn probe_signed() -> bool {
12688 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12689 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12690}
12691
12692fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12700 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12701 M.get_or_init(|| {
12702 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12703 let b = std::fs::read(&p).ok()?;
12704 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12705 let vals: Vec<f32> = b[8..]
12706 .chunks_exact(4)
12707 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12708 .collect();
12709 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12710 Some((inter, vals))
12711 })
12712 .as_ref()
12713}
12714
12715fn probe_topk() -> usize {
12718 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12719 *K.get_or_init(|| {
12720 std::env::var("CMF_FFN_PROBE_TOPK")
12721 .ok()
12722 .and_then(|v| v.parse().ok())
12723 .unwrap_or(0)
12724 })
12725}
12726
12727thread_local! {
12728 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12731 const { std::cell::RefCell::new(None) };
12732}
12733
12734fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12747 if d.gate_proj.has_prism_contract()
12752 || d.up_proj.has_prism_contract()
12753 || d.down_proj.has_prism_contract()
12754 {
12755 return None;
12756 }
12757 let dt = d.down_t.as_ref()?;
12758 let inter = d.gate_proj.rows();
12759 let hidden = dt.cols();
12760 if k == 0 || k >= inter || d.act != Act::Silu {
12761 return None;
12762 }
12763 DYN_SCRATCH.with(|sc| {
12764 let mut sc = sc.borrow_mut();
12765 let DynScratch {
12766 g,
12767 mag,
12768 live,
12769 parts,
12770 } = &mut *sc;
12771 g.resize(inter, 0.0);
12772 d.gate_proj.matvec(x, g, pool);
12773 for v in g.iter_mut() {
12774 *v = inference::silu(*v);
12775 }
12776 mag.clear();
12779 mag.extend(g.iter().map(|v| v.abs()));
12780 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12781 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12782 });
12783 let thr = *kth;
12784 live.clear();
12785 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12786 let mut out = vec![0.0f32; hidden];
12787 match pool {
12788 Some(p) if live.len() >= 64 => {
12789 let nw = p.n_workers() + 1;
12790 parts.clear();
12791 parts.resize(nw * hidden, 0.0);
12792 let ptr = SendMut(parts.as_mut_ptr());
12793 let n = live.len();
12794 let live_ref: &[u32] = live;
12795 let g_ref: &[f32] = g;
12796 p.run(&|w, workers| {
12797 let chunk = n.div_ceil(workers);
12798 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12799 if s >= e {
12800 return;
12801 }
12802 WORKER_SCRATCH.with(|ws| {
12803 let mut ws = ws.borrow_mut();
12804 let [scratch, acc] = &mut *ws;
12805 scratch.resize(hidden.max(x.len()), 0.0);
12806 acc.clear();
12807 acc.resize(hidden, 0.0);
12808 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12809 if let Some(&nx) = live_ref[s..e].get(o + 1) {
12812 d.up_proj.prefetch_row(nx as usize);
12813 dt.prefetch_row(nx as usize);
12814 }
12815 let idx = nrm as usize;
12816 let up = d.up_proj.row_dot(idx, x, scratch);
12817 let a = g_ref[idx] * up;
12818 if a != 0.0 {
12819 dt.add_row_scaled(idx, a, acc, scratch);
12820 }
12821 }
12822 for (j, v) in acc.iter().enumerate() {
12823 unsafe { *ptr.at(w * hidden + j) = *v };
12824 }
12825 });
12826 });
12827 for w in 0..nw {
12828 for (j, o) in out.iter_mut().enumerate() {
12829 *o += parts[w * hidden + j];
12830 }
12831 }
12832 }
12833 _ => {
12834 WORKER_SCRATCH.with(|ws| {
12835 let mut ws = ws.borrow_mut();
12836 let [scratch, _acc] = &mut *ws;
12837 scratch.resize(hidden.max(x.len()), 0.0);
12838 for &nrm in live.iter() {
12839 let idx = nrm as usize;
12840 let up = d.up_proj.row_dot(idx, x, scratch);
12841 let a = g[idx] * up;
12842 if a != 0.0 {
12843 dt.add_row_scaled(idx, a, &mut out, scratch);
12844 }
12845 }
12846 });
12847 }
12848 }
12849 Some(out)
12850 })
12851}
12852
12853struct DynScratch {
12856 g: Vec<f32>,
12857 mag: Vec<f32>,
12858 live: Vec<u32>,
12859 parts: Vec<f32>,
12860}
12861
12862thread_local! {
12863 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12864 std::cell::RefCell::new(DynScratch {
12865 g: Vec::new(),
12866 mag: Vec::new(),
12867 live: Vec::new(),
12868 parts: Vec::new(),
12869 })
12870 };
12871 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12873 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12874}
12875
12876fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12881 let inter = d.gate_proj.rows();
12882 FFN_SCRATCH.with(|s| {
12883 let mut s = s.borrow_mut();
12884 let [g, u, ..] = &mut *s;
12885 g.resize(inter, 0.0);
12886 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12887 } else {
12889 u.resize(inter, 0.0);
12890 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12891 for i in 0..inter {
12892 g[i] = d.act.combine(g[i], u[i]);
12893 }
12894 }
12895 zero_masked_cols(g, 1, inter, mask_row);
12896 let mut out = attention::take_buf(d.down_proj.rows());
12897 d.down_proj.matvec(g, &mut out, pool);
12898 out
12899 })
12900}
12901
12902fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12908 if d.gate_proj.has_prism_contract()
12909 || d.up_proj.has_prism_contract()
12910 || d.down_proj.has_prism_contract()
12911 {
12912 return None;
12913 }
12914 if d.act != Act::Silu {
12916 return None;
12917 }
12918 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12921 return None;
12922 }
12923 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12924 let mut model_ref = None;
12925 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12926 let model = model_ref?;
12927 let hidden = jobs[0].down.1;
12928 let mut out = attention::take_buf(hidden);
12929 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12930 Some(out)
12931 } else {
12932 let mut out = out;
12933 attention::recycle_buf(&mut out);
12934 None
12935 }
12936}
12937
12938#[allow(clippy::type_complexity)]
12943#[allow(clippy::type_complexity)]
12944pub(crate) fn moe_parts(
12945 t: &QTensor,
12946) -> Option<(
12947 &std::sync::Arc<cortiq_core::CmfModel>,
12948 usize,
12949 usize,
12950 usize,
12951 &[f32],
12952 &[f32],
12953 bool,
12954 bool,
12955 bool,
12956)> {
12957 match t {
12958 QTensor::Mapped {
12959 model,
12960 idx,
12961 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12962 rows,
12963 cols,
12964 row_scale,
12965 col_field,
12966 ..
12967 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12968 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12969 )),
12970 QTensor::Mapped {
12972 model,
12973 idx,
12974 dtype: cortiq_core::TensorDtype::Q1,
12975 rows,
12976 cols,
12977 ..
12978 } => Some((
12979 model,
12980 *idx,
12981 *rows,
12982 *cols,
12983 &[][..],
12984 &[][..],
12985 true,
12986 false,
12987 false,
12988 )),
12989 QTensor::Mapped {
12991 model,
12992 idx,
12993 dtype: cortiq_core::TensorDtype::Q4Tiled,
12994 rows,
12995 cols,
12996 ..
12997 } => Some((
12998 model,
12999 *idx,
13000 *rows,
13001 *cols,
13002 &[][..],
13003 &[][..],
13004 false,
13005 true,
13006 false,
13007 )),
13008 QTensor::Mapped {
13010 model,
13011 idx,
13012 dtype: cortiq_core::TensorDtype::Q4TiledP,
13013 rows,
13014 cols,
13015 ..
13016 } => Some((
13017 model,
13018 *idx,
13019 *rows,
13020 *cols,
13021 &[][..],
13022 &[][..],
13023 false,
13024 true,
13025 false,
13026 )),
13027 QTensor::Mapped {
13031 model,
13032 idx,
13033 dtype: cortiq_core::TensorDtype::Q2TiledP,
13034 rows,
13035 cols,
13036 ..
13037 } => Some((
13038 model,
13039 *idx,
13040 *rows,
13041 *cols,
13042 &[][..],
13043 &[][..],
13044 false,
13045 true,
13046 true,
13047 )),
13048 _ => None,
13049 }
13050}
13051
13052#[cfg(target_os = "macos")]
13060fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
13061 if m.router_input_norm
13062 || m.route_tau.is_some()
13063 || m.mask.is_some()
13064 || m.per_expert_scale.is_some()
13065 || m.experts.is_empty()
13066 || m.top_k == 0
13067 || m.resonance.is_some()
13068 {
13069 return None;
13070 }
13071 let (sh, sg) = match &m.shared {
13074 Some((sh, sg)) => (sh, sg.as_ref()),
13075 None => return None,
13076 };
13077 let (rf, rr, rc) = m.router.f32_parts()?;
13078 if rr != m.experts.len() || rc != hidden {
13079 return None;
13080 }
13081 let shared_gated = sg.is_some();
13082 let sf = match sg {
13083 Some(sg) => {
13084 let (sf, sr, sc) = sg.f32_parts()?;
13085 if sr * sc != hidden {
13086 return None;
13087 }
13088 sf
13089 }
13090 None => &rf[..hidden],
13093 };
13094 if let Some(b) = &m.expert_bias {
13095 if b.len() != m.experts.len() {
13096 return None;
13097 }
13098 }
13099 let inter = m.experts[0].gate_proj.rows();
13100 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
13103 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
13104 if e.act != Act::Silu
13105 || e.gate_proj.rows() != inter
13106 || e.gate_proj.cols() != hidden
13107 || e.up_proj.rows() != inter
13108 || e.up_proj.cols() != hidden
13109 || e.down_proj.rows() != hidden
13110 || e.down_proj.cols() != inter
13111 {
13112 return None;
13113 }
13114 let pick = |t: &QTensor| -> Option<usize> {
13115 if gu_q2 {
13116 t.mapped_q2tp().map(|(_, i)| i)
13117 } else {
13118 t.mapped_q4tp().map(|(_, i)| i)
13119 }
13120 };
13121 Some((
13122 pick(&e.gate_proj)?,
13123 pick(&e.up_proj)?,
13124 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
13125 ))
13126 };
13127 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
13128 let shared = trio(sh)?;
13129 Some(crate::gpu::GpuMoe {
13130 router: rf,
13131 sgate: sf,
13132 experts,
13133 shared,
13134 n_exp: m.experts.len(),
13135 top_k: m.top_k,
13136 inter,
13137 norm_topk: m.norm_topk_prob,
13138 route_scale: m.routed_scaling,
13139 gu_q2,
13140 sigmoid: m.router_sigmoid,
13141 bias: m.expert_bias.as_deref(),
13142 shared_gated,
13143 })
13144}
13145
13146pub(crate) fn moe_push_job_parts<'a>(
13150 gate: &'a QTensor,
13151 up: &'a QTensor,
13152 down: &'a QTensor,
13153 x: &[f32],
13154 w: f32,
13155 swiglu_limit: f32,
13156 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13157 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13158) -> Option<()> {
13159 use crate::qtensor::prescale;
13160 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
13161 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
13162 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
13163 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13164 return None; }
13166 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
13169 return None;
13170 }
13171 if !gq2 && dq2 {
13172 return None;
13173 }
13174 model_ref.get_or_insert_with(|| gm.clone());
13175 let dt = |cf: &[f32]| {
13176 if cf.is_empty() {
13177 cortiq_core::TensorDtype::Q8Row
13178 } else {
13179 cortiq_core::TensorDtype::Q8_2f
13180 }
13181 };
13182 jobs.push(crate::gpu::MoeJob {
13183 gate: (gi, gr, gc, grs),
13184 up: (ui, ur, uc, urs),
13185 down: (di, dr, dc, drs),
13186 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
13187 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
13188 down_col: dcf,
13189 w,
13190 q1: gq1,
13191 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
13192 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
13193 gu_q2: gq2,
13194 swiglu_limit,
13195 });
13196 Some(())
13197}
13198
13199fn moe_push_job<'a>(
13201 d: &'a DenseFfn,
13202 x: &[f32],
13203 w: f32,
13204 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13205 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13206) -> Option<()> {
13207 use crate::qtensor::prescale;
13208 if d.act != Act::Silu {
13209 return None; }
13211 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
13212 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
13213 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
13214 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13215 return None; }
13217 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
13218 return None;
13219 }
13220 if !gq2 && dq2 {
13221 return None;
13222 }
13223 model_ref.get_or_insert_with(|| gm.clone());
13224 let gdt = if gcf.is_empty() {
13225 cortiq_core::TensorDtype::Q8Row
13226 } else {
13227 cortiq_core::TensorDtype::Q8_2f
13228 };
13229 let udt = if ucf.is_empty() {
13230 cortiq_core::TensorDtype::Q8Row
13231 } else {
13232 cortiq_core::TensorDtype::Q8_2f
13233 };
13234 jobs.push(crate::gpu::MoeJob {
13235 gate: (gi, gr, gc, grs),
13236 up: (ui, ur, uc, urs),
13237 down: (di, dr, dc, drs),
13238 xs_gate: prescale(x, gcf, gdt).into_owned(),
13239 xs_up: prescale(x, ucf, udt).into_owned(),
13240 down_col: dcf,
13241 w,
13242 q1: gq1,
13243 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
13244 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
13245 gu_q2: gq2,
13246 swiglu_limit: 0.0,
13247 });
13248 Some(())
13249}
13250
13251fn sparse_ffn_quant(
13258 d: &DenseFfn,
13259 x: &[f32],
13260 active: &[u16],
13261 hidden: usize,
13262 pool: Option<&Pool>,
13263) -> Vec<f32> {
13264 let n = active.len();
13265 let inter = d.gate_proj.rows();
13266 let mut act = vec![0.0f32; n];
13267 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
13270 let compute = |ai: usize| -> f32 {
13271 let idx = active[ai] as usize;
13272 if idx >= inter {
13273 return 0.0; }
13275 let mut s = if need_scratch {
13276 vec![0.0f32; hidden]
13277 } else {
13278 Vec::new()
13279 };
13280 let gate = d.gate_proj.row_dot(idx, x, &mut s);
13281 let up = d.up_proj.row_dot(idx, x, &mut s);
13282 d.act.combine(gate, up)
13283 };
13284 match pool {
13285 Some(p) if n >= 256 => {
13286 let ptr = SendMut(act.as_mut_ptr());
13287 p.run(&|widx, nw| {
13288 let chunk = n.div_ceil(nw);
13289 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
13290 for ai in s..e {
13291 unsafe { *ptr.at(ai) = compute(ai) };
13292 }
13293 });
13294 }
13295 _ => {
13296 for (ai, a) in act.iter_mut().enumerate() {
13297 *a = compute(ai);
13298 }
13299 }
13300 }
13301 let mut out = vec![0.0f32; hidden];
13303 for (ai, &idx) in active.iter().enumerate() {
13304 let w = act[ai];
13305 if w.abs() >= 1e-12 && (idx as usize) < inter {
13306 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
13307 }
13308 }
13309 out
13310}
13311
13312#[doc(hidden)]
13314pub fn sparse_ffn_quant_for_test(
13315 d: &DenseFfn,
13316 x: &[f32],
13317 active: &[u16],
13318 hidden: usize,
13319) -> Vec<f32> {
13320 sparse_ffn_quant(d, x, active, hidden, None)
13321}
13322
13323fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
13327 let deq = |t: &QTensor| -> Vec<f32> {
13328 let (rows, cols) = (t.rows(), t.cols());
13329 let mut out = vec![0.0f32; rows * cols];
13330 for r in 0..rows {
13331 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
13332 }
13333 out
13334 };
13335 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
13336}
13337
13338struct SendMut(*mut f32);
13340unsafe impl Send for SendMut {}
13341unsafe impl Sync for SendMut {}
13342impl SendMut {
13343 #[inline]
13344 #[allow(clippy::mut_from_ref)]
13347 unsafe fn at(&self, i: usize) -> &mut f32 {
13348 unsafe { &mut *self.0.add(i) }
13349 }
13350}
13351
13352pub(crate) fn moe_route(
13362 logits: &[f32],
13363 m: &MoeFfn,
13364 allowed: Option<&[bool]>,
13365) -> (Vec<usize>, Vec<f32>, f32) {
13366 let ne = logits.len();
13367 let p: Vec<f32> = if m.router_sigmoid {
13368 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
13369 } else {
13370 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13371 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
13372 let s: f32 = e.iter().sum();
13373 for v in &mut e {
13374 *v /= s;
13375 }
13376 e
13377 };
13378 let admit = |e: usize| {
13384 m.mask.as_ref().is_none_or(|mk| mk[e])
13385 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
13386 };
13387 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
13388 match &m.expert_bias {
13390 Some(b) => idx.sort_unstable_by(|&x, &y| {
13391 (p[y] + b[y])
13392 .partial_cmp(&(p[x] + b[x]))
13393 .unwrap()
13394 .then(x.cmp(&y))
13395 }),
13396 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
13397 }
13398 idx.truncate(m.top_k);
13399 if let Some(tau) = m.route_tau {
13403 let total: f32 = idx.iter().map(|&e| p[e]).sum();
13404 if total > 0.0 {
13405 let mut acc = 0.0f32;
13406 let mut keep = idx.len();
13407 for (i, &e) in idx.iter().enumerate() {
13408 acc += p[e];
13409 if acc >= tau * total {
13410 keep = i + 1;
13411 break;
13412 }
13413 }
13414 idx.truncate(keep);
13415 }
13416 }
13417 let wsum: f32 = if m.norm_topk_prob {
13418 let s: f32 = idx.iter().map(|&e| p[e]).sum();
13419 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
13422 } else {
13423 1.0 / m.routed_scaling
13424 };
13425 (idx, p, wsum)
13426}
13427
13428fn moe_trace(idx: &[usize]) {
13430 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
13431}
13432
13433pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
13436 use std::io::Write;
13437 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
13438 std::sync::OnceLock::new();
13439 let Some(f) = F.get_or_init(|| {
13440 let p = std::env::var("CMF_MOE_TRACE").ok()?;
13441 Some(std::sync::Mutex::new(
13442 std::fs::OpenOptions::new()
13443 .create(true)
13444 .append(true)
13445 .open(p)
13446 .ok()?,
13447 ))
13448 }) else {
13449 return;
13450 };
13451 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
13452 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
13453}
13454
13455pub(crate) fn moe_ffn(
13458 m: &MoeFfn,
13459 x: &[f32],
13460 pool: Option<&Pool>,
13461 allowed: Option<&[bool]>,
13462) -> Vec<f32> {
13463 accumulate_act(m, x, 1);
13464 let ne = m.experts.len();
13465 let mut logits = vec![0.0f32; ne];
13466 match &m.resonance {
13467 Some(r) => r.scores(x, &mut logits),
13468 None => m.router.matvec(x, &mut logits, pool),
13469 }
13470 let (idx, p, wsum) = moe_route(&logits, m, allowed);
13471 {
13472 let mut st = m.stats.borrow_mut();
13473 if st.len() < ne {
13474 st.resize(ne, 0);
13475 }
13476 for &e in &idx {
13477 st[e] += 1;
13478 }
13479 }
13480 moe_trace(&idx);
13486 if crate::gpu::enabled_here() {
13491 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
13492 crate::gpu::ProbeArm::Gpu => {
13493 let t0 = std::time::Instant::now();
13494 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
13495 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13496 return out;
13497 }
13498 }
13499 crate::gpu::ProbeArm::CpuTimed => {
13500 let t0 = std::time::Instant::now();
13501 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13502 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13503 return out;
13504 }
13505 crate::gpu::ProbeArm::Cpu => {
13506 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13507 }
13508 }
13509 }
13510 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
13511}
13512
13513fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
13518 use std::sync::atomic::{AtomicBool, Ordering};
13519 if built {
13520 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
13521 if total_layers > 0 && layers_run < total_layers {
13522 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
13523 } else {
13524 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
13525 }
13526 } else {
13527 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
13528 }
13529 static SAID: AtomicBool = AtomicBool::new(false);
13530 if !SAID.swap(true, Ordering::Relaxed) {
13531 if built {
13532 tracing::info!("wgpu whole-token graph: ACTIVE");
13533 } else {
13534 tracing::warn!("wgpu whole-token graph refused — per-op path");
13535 }
13536 }
13537}
13538
13539pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13543pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13544pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13548pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13550
13551pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
13555 std::sync::atomic::AtomicU64::new(0);
13556pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
13557 std::sync::atomic::AtomicU64::new(0);
13558pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
13559 std::sync::atomic::AtomicU64::new(0);
13560pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
13561 std::sync::atomic::AtomicU64::new(0);
13562pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
13563 std::sync::atomic::AtomicU64::new(0);
13564pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
13568 std::sync::atomic::AtomicU64::new(0);
13569pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
13570 std::sync::atomic::AtomicU64::new(0);
13571pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
13572 std::sync::atomic::AtomicU64::new(0);
13573pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
13574 std::sync::atomic::AtomicU64::new(0);
13575
13576fn moe_batch_enabled() -> bool {
13579 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13580 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
13581}
13582
13583fn moe_ffn_cpu_batched(
13589 m: &MoeFfn,
13590 x: &[f32],
13591 idx: &[usize],
13592 p: &[f32],
13593 wsum: f32,
13594 pool: Option<&Pool>,
13595) -> Option<Vec<f32>> {
13596 if idx.is_empty() || !moe_batch_enabled() {
13597 return None;
13598 }
13599 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
13603 return None;
13604 }
13605 let n = idx.len() + usize::from(m.shared.is_some());
13606 let mut pairs = Vec::with_capacity(n);
13607 let mut downs = Vec::with_capacity(n);
13608 let mut ws = Vec::with_capacity(n);
13609 for &e in idx {
13610 let d = &m.experts[e];
13611 if d.act != Act::Silu {
13612 return None;
13613 }
13614 pairs.push((&d.gate_proj, &d.up_proj));
13615 downs.push(&d.down_proj);
13616 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
13617 }
13618 if let Some((se, gate)) = &m.shared {
13621 if se.act != Act::Silu {
13622 return None;
13623 }
13624 let g = gate.as_ref().map_or(1.0, |gate| {
13625 let mut gl = [0.0f32; 1];
13626 gate.matvec(x, &mut gl, pool);
13627 1.0 / (1.0 + (-gl[0]).exp())
13628 });
13629 pairs.push((&se.gate_proj, &se.up_proj));
13630 downs.push(&se.down_proj);
13631 ws.push(g);
13632 }
13633 let inter = pairs[0].0.rows();
13634 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
13635 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
13636 return None;
13637 }
13638 let mut out = attention::take_buf(x.len());
13639 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
13640 attention::recycle_buf(&mut out);
13641 return None;
13642 }
13643 Some(out)
13644}
13645
13646pub(crate) fn moe_cold_experts_cpu(
13652 experts: &[(&DenseFfn, f32)],
13653 x: &[f32],
13654 pool: Option<&Pool>,
13655) -> Vec<f32> {
13656 let mut out = attention::take_buf(x.len());
13657 if experts.is_empty() {
13658 return out;
13659 }
13660 let pairs: Vec<_> = experts
13661 .iter()
13662 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
13663 .collect();
13664 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
13665 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
13666 let inter = experts[0].0.gate_proj.rows();
13667 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
13668 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
13669 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
13670 {
13671 return out;
13672 }
13673 out.fill(0.0);
13674 for &(expert, weight) in experts {
13675 let mut one = dense_ffn(expert, x, pool);
13676 for (o, v) in out.iter_mut().zip(&one) {
13677 *o += weight * v;
13678 }
13679 attention::recycle_buf(&mut one);
13680 }
13681 out
13682}
13683
13684fn moe_ffn_cpu(
13686 m: &MoeFfn,
13687 x: &[f32],
13688 idx: &[usize],
13689 p: &[f32],
13690 wsum: f32,
13691 pool: Option<&Pool>,
13692) -> Vec<f32> {
13693 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
13694 return out;
13695 }
13696 let mut out = attention::take_buf(x.len());
13697 for &e in idx {
13698 let mut eo = dense_ffn(&m.experts[e], x, pool);
13699 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
13700 for i in 0..out.len() {
13701 out[i] += w * eo[i];
13702 }
13703 attention::recycle_buf(&mut eo);
13704 }
13705 if let Some((se, gate)) = &m.shared {
13706 let mut so = dense_ffn(se, x, pool);
13707 let g = gate.as_ref().map_or(1.0, |gate| {
13708 let mut gl = [0.0f32; 1];
13709 gate.matvec(x, &mut gl, pool);
13710 1.0 / (1.0 + (-gl[0]).exp())
13711 });
13712 for i in 0..out.len() {
13713 out[i] += g * so[i];
13714 }
13715 attention::recycle_buf(&mut so);
13716 }
13717 out
13718}
13719
13720#[allow(clippy::too_many_arguments)]
13728fn mla_attention(
13729 w: &MlaWeights,
13730 normed: &[f32],
13731 cache: &mut crate::kv_cache::LayerKvCache,
13732 position: usize,
13733 inv_freq: &[f32],
13734 rope_scale: f32,
13735 eps: f64,
13736 pool: Option<&Pool>,
13737) -> Vec<f32> {
13738 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
13739 let hd = dr + dn;
13740 let mut q = vec![0.0f32; nh * hd];
13741 match (&w.q_a, &w.q_a_norm) {
13742 (Some(qa), Some(qn)) => {
13743 let mut t = vec![0.0f32; qa.rows()];
13744 qa.matvec(normed, &mut t, pool);
13745 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13746 w.q_proj.matvec(&tn, &mut q, pool);
13747 }
13748 _ => w.q_proj.matvec(normed, &mut q, pool),
13749 }
13750 let mut ca = vec![0.0f32; lora + dr];
13751 w.kv_a.matvec(normed, &mut ca, pool);
13752 let (c_lat, k_rope) = ca.split_at_mut(lora);
13753 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13754 let mut kvb = vec![0.0f32; nh * (dn + dv)];
13755 w.kv_b.matvec(&latn, &mut kvb, pool);
13756 if !w.nope {
13757 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13758 }
13759 for h in 0..nh {
13760 if !w.nope {
13761 attention::rope_rotate_scaled(
13762 &mut q[h * hd..h * hd + dr],
13763 position,
13764 inv_freq,
13765 rope_scale,
13766 );
13767 }
13768 }
13769 let mut k = vec![0.0f32; nh * hd];
13770 let mut v = vec![0.0f32; nh * hd];
13771 for h in 0..nh {
13772 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13773 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13774 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13775 }
13776 cache.append(&k, &v, &vec![true; nh]);
13777 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13778 attention::recycle_buf(&mut imp);
13779 let mut ov = vec![0.0f32; nh * dv];
13780 for h in 0..nh {
13781 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13782 }
13783 let mut out = vec![0.0f32; w.o_proj.rows()];
13784 w.o_proj.matvec(&ov, &mut out, pool);
13785 out
13786}
13787
13788fn dense_moe_ffn(
13795 dm: &DenseMoeFfn,
13796 x_normed: &[f32],
13797 h_raw: &[f32],
13798 eps: f64,
13799 norm_style: NormStyle,
13800 pool: Option<&Pool>,
13801) -> Vec<f32> {
13802 let mut d = dense_ffn(&dm.dense, x_normed, pool);
13803 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13804 let m = &dm.moe;
13805 let ne = m.experts.len();
13806 let mut logits = vec![0.0f32; ne];
13807 if m.router_input_norm {
13808 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13809 let inv = 1.0 / (ss + eps as f32).sqrt();
13810 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13811 m.router.matvec(&xr, &mut logits, pool);
13812 } else {
13813 m.router.matvec(h_raw, &mut logits, pool);
13814 }
13815 let (idx, p, wsum) = moe_route(&logits, m, None);
13816 {
13817 let mut st = m.stats.borrow_mut();
13818 if st.len() < ne {
13819 st.resize(ne, 0);
13820 }
13821 for &e in &idx {
13822 st[e] += 1;
13823 }
13824 }
13825 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13826 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13827 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13828 for (di, mi) in d.iter_mut().zip(&mo) {
13829 *di += mi;
13830 }
13831 d
13832}
13833
13834fn moe_gpu_refused(why: &'static str) {
13841 use std::sync::atomic::{AtomicBool, Ordering};
13842 static SAID: AtomicBool = AtomicBool::new(false);
13843 if !SAID.swap(true, Ordering::Relaxed) {
13844 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13845 }
13846}
13847
13848fn moe_ffn_gpu(
13849 m: &MoeFfn,
13850 x: &[f32],
13851 idx: &[usize],
13852 p: &[f32],
13853 wsum: f32,
13854 pool: Option<&Pool>,
13855) -> Option<Vec<f32>> {
13856 use crate::gpu::MoeJob;
13857
13858 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13859 let mut model_ref = None;
13860 for &e in idx {
13861 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13862 moe_gpu_refused("push_job(expert)");
13863 return None;
13864 }
13865 }
13866 if let Some((se, gate)) = &m.shared {
13867 let g = gate.as_ref().map_or(1.0, |gate| {
13868 let mut gl = [0.0f32; 1];
13869 gate.matvec(x, &mut gl, pool);
13870 1.0 / (1.0 + (-gl[0]).exp())
13871 });
13872 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13873 moe_gpu_refused("push_job(shared)");
13874 return None;
13875 }
13876 }
13877 let Some(model) = model_ref else {
13878 moe_gpu_refused("no model_ref");
13879 return None;
13880 };
13881 let hidden = jobs[0].down.1;
13882 let mut out = vec![0.0f32; hidden];
13883 if crate::gpu::moe_block(&model, &jobs, &mut out) {
13884 Some(out)
13885 } else {
13886 moe_gpu_refused("gpu::moe_block");
13887 None
13888 }
13889}
13890
13891fn ffn_forward(
13893 ffn: &FfnKind,
13894 x: &[f32],
13895 pool: Option<&Pool>,
13896 experts_allowed: Option<&[bool]>,
13897) -> Vec<f32> {
13898 match ffn {
13899 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13900 FfnKind::Dense(d) => dense_ffn(d, x, pool),
13901 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13902 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13906 }
13907}
13908
13909fn ffn_forward_pair(
13913 ffn: &FfnKind,
13914 x1: &[f32],
13915 x2: &[f32],
13916 pool: Option<&Pool>,
13917 experts_allowed: Option<&[bool]>,
13918) -> (Vec<f32>, Vec<f32>) {
13919 let d = match ffn {
13920 FfnKind::Dense(d) if !d.segs.is_empty() => {
13923 return (
13924 tube_ffn(d, x1, 1, pool, None),
13925 tube_ffn(d, x2, 1, pool, None),
13926 );
13927 }
13928 FfnKind::Dense(d) => d,
13929 FfnKind::Moe(m) => {
13930 return (
13931 moe_ffn(m, x1, pool, experts_allowed),
13932 moe_ffn(m, x2, pool, experts_allowed),
13933 );
13934 }
13935 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13936 };
13937 let inter = d.gate_proj.rows();
13938 FFN_SCRATCH.with(|s| {
13939 let mut s = s.borrow_mut();
13940 let [g1, g2, u1, u2] = &mut *s;
13941 g1.resize(inter, 0.0);
13942 g2.resize(inter, 0.0);
13943 u1.resize(inter, 0.0);
13944 u2.resize(inter, 0.0);
13945 QTensor::matvec2_many(
13948 [&d.gate_proj, &d.up_proj],
13949 x1,
13950 x2,
13951 [g1.as_mut_slice(), u1.as_mut_slice()],
13952 [g2.as_mut_slice(), u2.as_mut_slice()],
13953 pool,
13954 );
13955 for i in 0..inter {
13956 g1[i] = d.act.combine(g1[i], u1[i]);
13957 g2[i] = d.act.combine(g2[i], u2[i]);
13958 }
13959 let mut o1 = attention::take_buf(d.down_proj.rows());
13960 let mut o2 = attention::take_buf(d.down_proj.rows());
13961 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13962 (o1, o2)
13963 })
13964}
13965
13966#[cfg(test)]
13967mod tests {
13968
13969 #[test]
13970 fn nll_graph_policy_scopes_only_the_fused_head() {
13971 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
13972 ("vulkan graph", true, true, false, true, false),
13974 ("native Metal graph", true, true, true, true, true),
13976 ("masked", false, true, false, false, false),
13978 ("graph disabled", true, false, true, false, false),
13979 ] {
13980 let (graph_quality, graph_head_required) =
13981 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
13982 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
13983 assert_eq!(graph_head_required, want_head, "{label}: fused head");
13984 }
13985 }
13986
13987 #[test]
13988 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13989 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13990 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13991 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13992 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13993 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13994 }
13995
13996 #[test]
13997 fn cancel_flag_stops_generation() {
13998 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13999 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14002 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
14003 assert_eq!(r.finish_reason, "cancelled");
14004 assert!(
14005 r.token_ids.is_empty(),
14006 "no tokens after cancel: {:?}",
14007 r.token_ids
14008 );
14009 assert_eq!(p.kv_cache.seq_len(), 0);
14010 assert!(p.kv_history.is_empty());
14011 assert!(!p.graph_want_logits);
14012 assert!(p.graph_logits.is_none());
14013 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
14015 assert_ne!(r2.finish_reason, "cancelled");
14016 }
14017 use super::*;
14018
14019 #[test]
14027 fn dynamic_ffn_equals_the_zeroing_arm() {
14028 let (hidden, inter) = (8usize, 32usize);
14029 let synth = |n: usize, salt: usize| -> Vec<f32> {
14030 (0..n)
14031 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
14032 .collect()
14033 };
14034 let down = synth(hidden * inter, 3);
14035 let mut down_t = vec![0.0f32; inter * hidden];
14036 for r in 0..hidden {
14037 for c in 0..inter {
14038 down_t[c * hidden + r] = down[r * inter + c];
14039 }
14040 }
14041 let d = DenseFfn {
14042 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14043 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14044 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
14045 act: Act::Silu,
14046 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
14047 segs: Vec::new(),
14048 };
14049 let x = synth(hidden, 11);
14050 let k = 12usize;
14051 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
14052 let mut g = vec![0.0f32; inter];
14054 d.gate_proj.matvec(&x, &mut g, None);
14055 let mut u = vec![0.0f32; inter];
14056 d.up_proj.matvec(&x, &mut u, None);
14057 for v in g.iter_mut() {
14058 *v = inference::silu(*v);
14059 }
14060 keep_top_k(&mut g, k);
14061 for i in 0..inter {
14062 g[i] *= u[i];
14063 }
14064 let mut want = vec![0.0f32; hidden];
14065 d.down_proj.matvec(&g, &mut want, None);
14066 for (a, b) in want.iter().zip(&got) {
14067 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
14068 }
14069 }
14070
14071 #[test]
14077 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
14078 let (hidden, core, tube) = (8usize, 12usize, 8usize);
14079 let inter = core + tube;
14080 let synth = |n: usize, salt: usize| -> Vec<f32> {
14081 (0..n)
14082 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
14083 .collect()
14084 };
14085 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
14086 let d_all = synth(hidden * inter, 3);
14087 let dense = DenseFfn {
14089 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
14090 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
14091 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
14092 act: Act::Silu,
14093 down_t: None,
14094 segs: Vec::new(),
14095 };
14096 let rows =
14097 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
14098 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
14099 let mut o = Vec::with_capacity(hidden * (b - a));
14100 for r in 0..hidden {
14101 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
14102 }
14103 o
14104 };
14105 let tubed = DenseFfn {
14106 down_t: None,
14107 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
14108 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
14109 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
14110 act: Act::Silu,
14111 segs: vec![FfnSeg {
14112 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
14113 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
14114 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
14115 start: core,
14116 width: tube,
14117 }],
14118 };
14119 let x = synth(hidden, 7);
14120 let want = dense_ffn(&dense, &x, None);
14121 let got = tube_ffn(&tubed, &x, 1, None, None);
14122 for (a, b) in want.iter().zip(&got) {
14123 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
14124 }
14125 let mut bits = vec![0u8; inter.div_ceil(8)];
14127 for n in 0..core {
14128 bits[n / 8] |= 1 << (n % 8);
14129 }
14130 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14131 let masked = dense_ffn_masked(&dense, &x, None, &bits);
14132 for (a, b) in masked.iter().zip(&closed) {
14133 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
14134 }
14135 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14137 for (a, b) in closed.iter().zip(&batch) {
14138 assert_eq!(a, b, "batch arm disagrees with decode arm");
14139 }
14140 }
14141
14142 #[test]
14144 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
14145 let (hidden, inter) = (16usize, 40usize);
14146 let synth = |n: usize, salt: usize| -> Vec<f32> {
14147 (0..n)
14148 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
14149 .collect()
14150 };
14151 let d = DenseFfn {
14152 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14153 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14154 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
14155 act: Act::Silu,
14156 down_t: None,
14157 segs: Vec::new(),
14158 };
14159 let x = synth(hidden, 9);
14160 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
14162
14163 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
14164
14165 let mut g = vec![0.0f32; inter];
14167 d.gate_proj.matvec(&x, &mut g, None);
14168 let mut u = vec![0.0f32; inter];
14169 d.up_proj.matvec(&x, &mut u, None);
14170 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
14171 for i in 0..inter {
14172 g[i] = if act_set.contains(&(i as u16)) {
14173 inference::silu(g[i]) * u[i]
14174 } else {
14175 0.0
14176 };
14177 }
14178 let mut reference = vec![0.0f32; hidden];
14179 d.down_proj.matvec(&g, &mut reference, None);
14180
14181 let max_d = sparse
14182 .iter()
14183 .zip(&reference)
14184 .map(|(a, b)| (a - b).abs())
14185 .fold(0.0f32, f32::max);
14186 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
14187 }
14188
14189 fn attach_test_mtp(p: &mut Pipeline) {
14191 let (h, inter, heads, kv, hd) = (
14192 p.hidden_size,
14193 p.intermediate_size,
14194 p.num_heads,
14195 p.num_kv_heads,
14196 p.head_dim,
14197 );
14198 let synth = |n: usize, salt: usize| -> Vec<f32> {
14199 (0..n)
14200 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
14201 .collect()
14202 };
14203 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
14204 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14205 };
14206 p.mtp = Some(MtpModule {
14207 enorm: vec![1.0; h],
14208 hnorm: vec![1.0; h],
14209 eh_proj: qt(h, 2 * h, 301),
14210 layer: LayerWeights {
14211 input_norm: vec![1.0; h],
14212 post_norm: vec![1.0; h],
14213 attn_out_norm: None,
14214 ffn_out_norm: None,
14215 layer_scale: None,
14216 ffn: FfnKind::Dense(DenseFfn {
14217 gate_proj: qt(inter, h, 315),
14218 up_proj: qt(inter, h, 316),
14219 down_proj: qt(h, inter, 317),
14220 act: Act::Silu,
14221 down_t: None,
14222 segs: Vec::new(),
14223 }),
14224 attn: AttnKind::Full {
14225 bias: None,
14226 wq: qt(heads * hd, h, 311),
14227 wk: qt(kv * hd, h, 312),
14228 wv: qt(kv * hd, h, 313),
14229 wo: qt(h, heads * hd, 314),
14230 q_norm: None,
14231 k_norm: None,
14232 output_gate: false,
14233 softplus_gate: None,
14234 },
14235 },
14236 final_norm: vec![1.0; h],
14237 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
14238 });
14239 }
14240
14241 #[test]
14242 fn speculative_equals_vanilla_greedy() {
14243 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14247 let run = |spec: bool| {
14248 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14249 p.sampler_config.temperature = 0.0;
14250 attach_test_mtp(&mut p);
14251 p.speculative = spec;
14252 let r = p.generate("abcdef", 12, None, None).unwrap();
14253 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
14254 };
14255 let (vanilla, d0, _) = run(false);
14256 let (spec, d1, a1) = run(true);
14257 assert_eq!(d0, 0, "vanilla path must not draft");
14258 assert!(d1 > 0, "speculative path must draft");
14259 assert_eq!(
14260 vanilla, spec,
14261 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
14262 );
14263 }
14264
14265 #[test]
14266 fn speculative_accepts_constant_oracle() {
14267 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14269 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14270 p.sampler_config.temperature = 0.0;
14271 p.sampler_config.repetition_penalty = 1.0;
14272 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
14275 attach_test_mtp(&mut p);
14276 p.speculative = true;
14277 let r = p.generate("abcd", 10, None, None).unwrap();
14278 assert!(r.mtp_drafted > 0);
14279 assert_eq!(
14280 r.mtp_accepted, r.mtp_drafted,
14281 "constant logits → every draft accepted"
14282 );
14283 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
14286 }
14287
14288 #[test]
14289 fn empty_prompt_is_an_error_not_a_panic() {
14290 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14291 let r = p.generate("", 4, None, None);
14292 assert!(r.is_err(), "empty prompt must be a clean error");
14293 }
14294
14295 #[test]
14296 fn every_token_enters_kv_exactly_once() {
14297 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14298 p.sampler_config.temperature = 0.0;
14300 let r = p.generate("abc", 2, None, None).unwrap();
14301 assert_eq!(r.prompt_tokens, 3);
14302 assert_eq!(
14306 p.kv_cache.seq_len(),
14307 3 + r.tokens_generated - 1,
14308 "each token must be cached exactly once (v1 cached the last prompt token twice)"
14309 );
14310 }
14311
14312 #[test]
14313 fn generation_is_reproducible_with_seed() {
14314 let run = || {
14315 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14316 p.generate("hello", 8, None, None).unwrap().token_ids
14317 };
14318 assert_eq!(run(), run());
14319 }
14320
14321 #[test]
14322 fn resetting_sampler_restarts_the_seeded_stream() {
14323 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14324 let config = SamplerConfig {
14325 seed: Some(1234),
14326 ..SamplerConfig::default()
14327 };
14328 p.set_sampler_config(config.clone());
14329 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
14330 p.set_sampler_config(config);
14331 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
14332 assert_eq!(first, second);
14333 }
14334
14335 #[test]
14336 fn eviction_bounds_the_cache() {
14337 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14338 p.kv_cache.max_seq_len = 6;
14339 p.sampler_config.temperature = 0.0;
14340 let _ = p.generate("abcd", 12, None, None).unwrap();
14341 assert!(
14342 p.kv_cache.seq_len() <= 6 + 1,
14343 "cache must stay bounded by max_seq_len (got {})",
14344 p.kv_cache.seq_len()
14345 );
14346 }
14347
14348 #[test]
14349 fn confidence_matches_tokens_and_is_a_probability() {
14350 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14351 p.sampler_config.temperature = 0.0;
14352 p.sampler_config.repetition_penalty = 1.0;
14353 let r = p.generate("abcd", 10, None, None).unwrap();
14354 assert_eq!(
14355 r.token_confidence.len(),
14356 r.token_ids.len(),
14357 "one confidence per emitted token"
14358 );
14359 for &c in &r.token_confidence {
14360 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
14361 }
14362 let logits = [1.0f32, 3.0, 0.5, 3.0];
14364 let p0 = top1_prob_t(&logits, 1, 1.0);
14365 let p1 = top1_prob_t(&logits, 3, 1.0);
14366 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
14367 assert!(p0 > 0.0 && p0 < 1.0);
14368 let sharp = top1_prob_t(&logits, 1, 1.0);
14370 let soft = top1_prob_t(&logits, 1, 2.0);
14371 assert!(soft < sharp, "higher temperature lowers peak confidence");
14372 }
14373
14374 #[test]
14375 fn trace_is_opt_in_and_parallels_the_output() {
14376 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14378 p.sampler_config.temperature = 0.0;
14379 p.sampler_config.repetition_penalty = 1.0;
14380 let r = p.generate("abcd", 10, None, None).unwrap();
14381 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
14382
14383 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14385 p.sampler_config.temperature = 0.0;
14386 p.sampler_config.repetition_penalty = 1.0;
14387 p.set_trace(true);
14388 let r = p.generate("abcd", 10, None, None).unwrap();
14389 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
14390 for (i, tr) in r.traces.iter().enumerate() {
14391 assert_eq!(tr.t, i, "trace index is sequential");
14392 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
14393 assert_eq!(
14394 tr.confidence, r.token_confidence[i],
14395 "trace confidence matches the confidence channel"
14396 );
14397 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
14399 }
14400 }
14401
14402 #[test]
14403 fn explain_prefill_logits_match_greedy_first_token() {
14404 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14408 p.sampler_config.temperature = 0.0;
14409 p.sampler_config.repetition_penalty = 1.0;
14410 let ids = p.tokenizer.encode("abcd");
14411 let logits = p.prefill_next_logits(&ids, None);
14412 let argmax = logits
14413 .iter()
14414 .enumerate()
14415 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14416 .unwrap()
14417 .0 as u32;
14418 let r = p.generate("abcd", 1, None, None).unwrap();
14419 assert_eq!(
14420 argmax, r.token_ids[0],
14421 "explain preview must match greedy emit"
14422 );
14423 }
14424
14425 #[test]
14426 fn laguna_shared_expert_is_unconditionally_added() {
14427 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
14428 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
14429 let zero_dense = || DenseFfn {
14430 gate_proj: matrix(vec![0.0; 4]),
14431 up_proj: matrix(vec![0.0; 4]),
14432 down_proj: matrix(vec![0.0; 4]),
14433 act: Act::Silu,
14434 down_t: None,
14435 segs: Vec::new(),
14436 };
14437 let shared = DenseFfn {
14438 gate_proj: identity(),
14439 up_proj: identity(),
14440 down_proj: identity(),
14441 act: Act::Silu,
14442 down_t: None,
14443 segs: Vec::new(),
14444 };
14445 let x = [1.0, 2.0];
14446 let expected = dense_ffn(&shared, &x, None);
14447 let moe = MoeFfn {
14448 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
14449 experts: vec![zero_dense()],
14450 top_k: 1,
14451 norm_topk_prob: true,
14452 router_sigmoid: true,
14453 expert_bias: None,
14454 routed_scaling: 1.0,
14455 route_tau: None,
14456 shared: Some((shared, None)),
14457 stats: std::cell::RefCell::new(Vec::new()),
14458 act_sq: std::cell::RefCell::new(Vec::new()),
14459 act_rows: std::cell::RefCell::new(Vec::new()),
14460 mask: None,
14461 per_expert_scale: None,
14462 router_input_norm: false,
14463 resonance: None,
14464 };
14465 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
14466 for (actual, expected) in actual.iter().zip(expected) {
14467 assert!((actual - expected).abs() < 1e-6);
14468 }
14469 }
14470
14471 #[test]
14472 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
14473 const B: usize = 19;
14474 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14475 p.set_o1(Some(crate::nystrom::O1Cfg {
14476 layers: crate::nystrom::O1Layers::All,
14477 m: 4,
14478 w: 8,
14479 sink: 2,
14480 rect: crate::nystrom::O1Rect::Aggregate,
14481 }));
14482 p.o1_begin_with_prefix(Some(B));
14483 let ids: Vec<u32> = (0..B as u32).collect();
14484 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
14485
14486 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
14487 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
14488 let next = p.embed_single(B as u32);
14489 let _ = p.forward_layers(&next, B, None);
14490 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
14491 }
14492
14493 #[test]
14494 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
14495 const B: usize = 19;
14496 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14497 let gdn_cfg = crate::linear_core::GdnCfg {
14501 num_v_heads: 2,
14502 num_k_heads: 1,
14503 key_head_dim: 2,
14504 value_head_dim: 4,
14505 conv_kernel: 3,
14506 hidden_size: 8,
14507 rms_eps: 1e-6,
14508 output_gate_sigmoid: false,
14509 };
14510 let synth = |n: usize, salt: usize| -> Vec<f32> {
14511 (0..n)
14512 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
14513 .collect()
14514 };
14515 let qt = |rows: usize, cols: usize, salt: usize| {
14516 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14517 };
14518 let c_dim = gdn_cfg.conv_dim();
14519 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
14520 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
14521 in_proj_qkv: qt(c_dim, 8, 1),
14522 in_proj_z: qt(vd, 8, 2),
14523 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
14524 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
14525 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
14526 a_log: vec![0.2, 0.5],
14527 dt_bias: synth(gdn_cfg.num_v_heads, 6),
14528 norm: vec![1.0; gdn_cfg.value_head_dim],
14529 out_proj: qt(8, vd, 7),
14530 });
14531 p.gdn_cfg = Some(gdn_cfg);
14532 p.set_o1(Some(crate::nystrom::O1Cfg {
14533 layers: crate::nystrom::O1Layers::All,
14534 m: 4,
14535 w: 8,
14536 sink: 2,
14537 rect: crate::nystrom::O1Rect::Aggregate,
14538 }));
14539 p.o1_begin_with_prefix(Some(B));
14540 for pos in 0..B - 2 {
14541 let emb = p.embed_single(pos as u32);
14542 let _ = p.forward_layers(&emb, pos, None);
14543 }
14544 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
14545
14546 let e1 = p.embed_single((B - 2) as u32);
14547 let e2 = p.embed_single((B - 1) as u32);
14548 let _ = p.forward_pair(&e1, &e2, B - 2);
14549
14550 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
14551 assert!(
14552 p.kv_cache
14553 .layers
14554 .iter()
14555 .enumerate()
14556 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
14557 );
14558 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
14559 assert_ne!(
14560 p.kv_cache.layers[0].linear_state, lane1_state,
14561 "real pair must commit GDN lane 2 before returning"
14562 );
14563 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
14564 let next = p.embed_single(B as u32);
14565 let _ = p.forward_layers(&next, B, None);
14566 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
14567 }
14568
14569 #[test]
14570 fn o1_error_observation_stays_terminal_until_reset() {
14571 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14572 p.set_o1(Some(crate::nystrom::O1Cfg {
14573 layers: crate::nystrom::O1Layers::All,
14574 m: 4,
14575 w: 8,
14576 sink: 2,
14577 rect: crate::nystrom::O1Rect::Aggregate,
14578 }));
14579 p.o1_begin();
14580 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
14581
14582 assert!(p.o1_seal_checked().is_err());
14583 assert!(
14584 p.o1_seal_checked().is_err(),
14585 "retry must see the sticky error"
14586 );
14587 let k = vec![0.2f32; 4];
14588 let v = vec![0.3f32; 4];
14589 p.kv_cache.layers[0].append(&k, &v, &[]);
14590 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
14591
14592 p.reset_session();
14593 p.o1_begin();
14594 p.kv_cache.layers[0].append(&k, &v, &[]);
14595 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
14596 }
14597
14598 #[test]
14599 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
14600 let ids = vec![1u32, 2, 3, 4, 5, 6];
14601 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14602 p.graph_logits = Some(vec![123.0]);
14603 p.graph_want_logits = true;
14604 p.graph_failed
14605 .store(true, std::sync::atomic::Ordering::Relaxed);
14606 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14607 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
14608 assert!(err.contains("before NLL"));
14609 assert!(p.graph_logits.is_none());
14610 assert!(!p.graph_want_logits);
14611 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14612 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14613
14614 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14615 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14616 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14617 assert_eq!(actual.1, expected.1);
14618 assert!((actual.0 - expected.0).abs() < 1e-9);
14619 }
14620
14621 #[test]
14622 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
14623 let ids = vec![1u32, 2, 3, 4, 5, 6];
14624 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14625 p.nll_test_fail_at = Some(1);
14626 let err = p
14627 .nll_ids_from(&ids, 0)
14628 .expect_err("one-shot forward failure");
14629 assert!(err.contains("forward") || err.contains("score row"));
14630 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14631 assert!(!p.graph_want_logits);
14632 assert!(p.graph_logits.is_none());
14633 assert!(p.kv_history.is_empty());
14634
14635 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14636 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14637 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14638 assert_eq!(actual.1, expected.1);
14639 assert!((actual.0 - expected.0).abs() < 1e-9);
14640 }
14641
14642 #[test]
14643 fn nll_serial_failure_before_first_row_is_reported() {
14644 let ids = vec![1u32, 2, 3, 4];
14645 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14646 p.nll_test_force_serial = true;
14647 p.nll_test_fail_at = Some(0);
14648 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
14649 assert!(err.contains("serial forward"));
14650 assert!(p.kv_history.is_empty());
14651 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14652 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14653 }
14654
14655 #[test]
14656 fn ffn_probe_failure_discards_recorder_and_state() {
14657 let ids = vec![1u32, 2, 3, 4];
14658 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14659 p.nll_test_fail_at = Some(0);
14660 let err = p
14661 .probe_ffn_mass_batch(&ids)
14662 .expect_err("probe forward failure");
14663 assert!(err.contains("NLL"));
14664 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
14665 assert!(p.kv_history.is_empty());
14666 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14667 }
14668
14669 #[test]
14670 fn nll_test_controls_are_pipeline_scoped() {
14671 let ids = vec![1u32, 2, 3, 4];
14672 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14673 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14674 failing.nll_test_force_serial = true;
14675 failing.nll_test_fail_at = Some(0);
14676
14677 assert!(!failing.can_prefill_batched());
14678 assert!(unaffected.can_prefill_batched());
14679 let expected = unaffected
14680 .nll_ids_from(&ids, 0)
14681 .expect("unaffected pipeline remains usable");
14682 let err = failing
14683 .nll_ids_from(&ids, 0)
14684 .expect_err("failure injection belongs to failing pipeline");
14685 assert!(err.contains("serial forward"));
14686 assert!(failing.nll_test_fail_at.is_none());
14687 assert!(unaffected.can_prefill_batched());
14688 let actual = unaffected
14689 .nll_ids_from(&ids, 0)
14690 .expect("unaffected pipeline remains reusable");
14691 assert_eq!(actual.1, expected.1);
14692 assert!((actual.0 - expected.0).abs() < 1e-9);
14693 }
14694
14695 #[test]
14696 fn forward_ids_failure_channel_is_terminal_and_reusable() {
14697 let ids = vec![1u32, 2, 3, 4, 5, 6];
14698 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14699 p.graph_logits = Some(vec![123.0]);
14700 p.graph_want_logits = true;
14701 p.graph_failed
14702 .store(true, std::sync::atomic::Ordering::Relaxed);
14703 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14704
14705 let err = p
14706 .forward_ids(&ids, None)
14707 .expect_err("a failed forward must not become a valid head result");
14708 assert!(err.contains("forward_ids setup"));
14709 assert!(p.graph_logits.is_none());
14710 assert!(!p.graph_want_logits);
14711 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14712 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14713 assert_eq!(p.kv_cache.seq_len(), 0);
14714
14715 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
14716 .forward_ids(&ids, None)
14717 .expect("fresh forward_ids");
14718 let actual = p
14719 .forward_ids(&ids, None)
14720 .expect("pipeline remains reusable after a failed forward");
14721 assert_eq!(actual.len(), expected.len());
14722 assert!(
14723 actual
14724 .iter()
14725 .zip(expected)
14726 .all(|(a, b)| (a - b).abs() < 1e-9)
14727 );
14728 assert_eq!(p.kv_cache.seq_len(), ids.len());
14729 }
14730}