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 rng: SplitMix64,
175 sampler_scratch: SamplerScratch,
176 spec_forced: Option<u32>,
182 spec_q: Vec<Vec<f32>>,
183 spec_p: Vec<f32>,
184 spec_res: Vec<f32>,
185 spec_qs: Vec<sampler::Sparse>,
187 spec_ps: sampler::Sparse,
188 spec_ress: sampler::Sparse,
189 mtp_graph_mode: Option<bool>,
196 #[cfg(target_os = "macos")]
199 metal_verify: Option<MetalVerifyPending>,
200 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
204 ws: ForwardScratch,
208 pool: Option<std::sync::Arc<Pool>>,
210 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
214 pub(crate) dyn_force_f32: bool,
216 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
221 pub(crate) dyn_active: Option<usize>,
227 pub(crate) dyn_blend_loaded: bool,
231 pub(crate) dyn_phi_layer: Option<usize>,
234 dyn_phi_ema: Vec<f32>,
236 dyn_phi_seen: usize,
237 pub dyn_router: Option<crate::swarm::DynRouter>,
240 o1_cfg: Option<crate::nystrom::O1Cfg>,
243 o1_epoch: u64,
246 o1_flags: Vec<bool>,
248 trace: bool,
251 calib_temp: f32,
254 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
256 graph_kv_id: u64,
257 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
260 graph_want_logits: bool,
261 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
265 graph_head_required: bool,
266 graph_logits: Option<Vec<f32>>,
269 pub embed_multiplier: f32,
271 pub attn_scale: f32,
274 pub swa: Option<(usize, usize)>,
277 pub sliding_layers: Option<Vec<bool>>,
280 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
283 pub rotary_dim_local: Option<usize>,
284 pub rope_scale: f32,
285 pub rope_scale_local: f32,
286 pub global_attn: Option<(usize, usize)>,
289 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
292 pub attn_v_norm: bool,
294 pub qk_norm_after_rope: bool,
296 pub final_softcap: Option<f32>,
298 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
302 pub attn_softcap: f32,
304 confidence_on: bool,
308 #[cfg(test)]
311 nll_test_fail_at: Option<usize>,
312 #[cfg(test)]
315 nll_test_force_serial: bool,
316}
317
318#[cfg(target_os = "macos")]
319impl Drop for Pipeline {
320 fn drop(&mut self) {
321 crate::gpu::kv_mirror_drop(self.graph_kv_id);
322 }
323}
324
325pub struct PipelineWeights {
330 pub embed_tokens: QTensor,
332 pub layers: Vec<LayerWeights>,
334 pub lm_head: QTensor,
336 pub final_norm: Vec<f32>,
338}
339
340pub struct LayerWeights {
342 pub input_norm: Vec<f32>,
343 pub post_norm: Vec<f32>,
346 pub attn_out_norm: Option<Vec<f32>>,
349 pub layer_scale: Option<f32>,
351 pub ffn_out_norm: Option<Vec<f32>>,
354 pub ffn: FfnKind,
355 pub attn: AttnKind,
356}
357
358#[derive(Clone, Copy, PartialEq, Debug, Default)]
361pub enum Act {
362 #[default]
363 Silu,
364 GeluTanh,
365 Situ {
368 beta: f32,
369 linear_beta: f32,
370 },
371}
372
373impl Act {
374 pub fn from_arch(name: &str) -> Self {
375 if name == "gelu_tanh" {
376 Self::GeluTanh
377 } else {
378 Self::Silu
379 }
380 }
381
382 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
384 match arch.hidden_act.as_str() {
385 "situ" => Self::Situ {
386 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
387 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
388 },
389 other => Self::from_arch(other),
390 }
391 }
392
393 #[inline]
394 pub fn apply(self, x: f32) -> f32 {
395 match self {
396 Self::Silu => inference::silu(x),
397 Self::GeluTanh => inference::gelu_tanh(x),
398 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
399 }
400 }
401
402 #[inline]
405 pub fn combine(self, g: f32, u: f32) -> f32 {
406 match self {
407 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
408 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
409 }
410 _ => self.apply(g) * u,
411 }
412 }
413}
414
415pub struct DenseFfn {
417 pub gate_proj: QTensor,
418 pub up_proj: QTensor,
419 pub down_proj: QTensor,
420 pub act: Act,
422 pub down_t: Option<QTensor>,
428 pub segs: Vec<FfnSeg>,
435}
436
437pub struct FfnSeg {
442 pub gate: QTensor,
443 pub up: QTensor,
444 pub down: QTensor,
445 pub start: usize,
446 pub width: usize,
447}
448
449pub enum FfnKind {
452 Dense(DenseFfn),
453 Moe(MoeFfn),
457 DenseMoe(Box<DenseMoeFfn>),
464}
465
466pub struct DenseMoeFfn {
468 pub dense: DenseFfn,
469 pub moe: MoeFfn,
470 pub post_norm_1: Vec<f32>,
472 pub pre_norm_2: Vec<f32>,
475 pub post_norm_2: Vec<f32>,
477}
478
479pub struct MoeFfn {
480 pub router: QTensor,
482 pub experts: Vec<DenseFfn>,
483 pub top_k: usize,
484 pub norm_topk_prob: bool,
485 pub router_sigmoid: bool,
488 pub expert_bias: Option<Vec<f32>>,
492 pub routed_scaling: f32,
495 pub route_tau: Option<f32>,
501 pub shared: Option<(DenseFfn, Option<QTensor>)>,
504 pub stats: std::cell::RefCell<Vec<u64>>,
508 pub act_sq: std::cell::RefCell<Vec<f64>>,
515 pub act_rows: std::cell::RefCell<Vec<f32>>,
521 pub mask: Option<Vec<bool>>,
526 pub per_expert_scale: Option<Vec<f32>>,
529 pub router_input_norm: bool,
533 pub resonance: Option<Resonance>,
537}
538
539pub struct Resonance {
541 pub mu: Vec<f32>,
543 pub u: Vec<f32>,
545 pub k: usize,
546 pub bias: Vec<f32>,
548}
549
550impl Resonance {
551 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
553 let h = x.len();
554 let ne = out.len();
555 for e in 0..ne {
556 let mu = &self.mu[e * h..(e + 1) * h];
557 let mut d2 = 0.0f32;
558 for j in 0..h {
559 let d = x[j] - mu[j];
560 d2 += d * d;
561 }
562 let mut proj = 0.0f32;
563 for i in 0..self.k {
564 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
565 let mut p = 0.0f32;
566 for j in 0..h {
567 p += (x[j] - mu[j]) * u[j];
568 }
569 proj += p * p;
570 }
571 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
572 }
573 }
574}
575
576pub enum AttnKind {
579 Full {
581 wq: QTensor,
582 wk: QTensor,
583 wv: QTensor,
584 wo: QTensor,
585 q_norm: Option<Vec<f32>>,
586 k_norm: Option<Vec<f32>>,
587 output_gate: bool,
588 softplus_gate: Option<(QTensor, bool)>,
592 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
594 },
595 Linear(VmfPhaseWeights),
597 LinearGdn(GdnWeights),
599 ShortConv(ShortConvWeights),
602 Mla(Box<MlaWeights>),
610 Kda(Box<crate::linear_core::KdaWeights>),
614}
615
616pub struct MlaWeights {
618 pub q_proj: QTensor,
622 pub q_a: Option<QTensor>,
625 pub q_a_norm: Option<Vec<f32>>,
626 pub kv_a: QTensor,
628 pub kv_a_norm: Vec<f32>,
630 pub kv_b: QTensor,
632 pub o_proj: QTensor,
634 pub nh: usize,
635 pub qk_rope: usize,
636 pub qk_nope: usize,
637 pub v_dim: usize,
638 pub lora: usize,
639 pub scale: f32,
641 pub nope: bool,
643}
644
645pub struct MtpModule {
650 pub enorm: Vec<f32>,
651 pub hnorm: Vec<f32>,
652 pub eh_proj: QTensor,
654 pub layer: LayerWeights,
655 pub final_norm: Vec<f32>,
656 pub kv: crate::kv_cache::LayerKvCache,
657}
658
659#[cfg(target_os = "macos")]
666enum MetalRowsItem<'a> {
667 Gdn {
668 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
669 first: usize,
670 },
671 Attn {
672 l: crate::gpu_metal::AttnGpuLayer<'a>,
673 li: usize,
674 q_norm: Option<&'a [f32]>,
675 k_norm: Option<&'a [f32]>,
676 output_gate: bool,
677 },
678}
679
680#[cfg(target_os = "macos")]
681struct MetalVerifyPending {
682 graph: crate::gpu_metal::VerifyGraph,
683 gdn_layers: Vec<usize>,
684 attn_layers: Vec<(usize, usize)>,
685}
686
687#[cfg(target_os = "macos")]
688enum MetalRowsRun {
689 Declined,
691 Failed,
694 Completed(MetalVerifyPending),
695}
696
697#[cfg(target_os = "macos")]
698enum MetalPrefillOutcome {
699 Declined,
700 Failed,
701 Completed(Vec<f32>),
702}
703
704#[cfg(target_os = "macos")]
705enum MetalBatchNllOutcome {
706 Declined,
707 Failed(String),
708 Completed(f64, usize),
709}
710
711#[derive(Clone, Copy)]
715enum SpecTrial {
716 Spec {
717 t0: std::time::Instant,
718 gen0: usize,
719 rounds: usize,
720 },
721 Plain {
722 t0: std::time::Instant,
723 gen0: usize,
724 },
725 Decided {
726 spec: bool,
727 recheck_at: usize,
728 },
729}
730
731#[derive(Default, Clone, Copy)]
742struct SpecMon {
743 round_ms: f64,
744 tokens: f64,
745 plain_ms: f64,
746 n: u32,
747 fails: u32,
748}
749
750impl SpecMon {
751 fn round(&mut self, dt_ms: f64, produced: usize) {
752 self.n += 1;
753 if self.n == 1 {
754 return; }
756 let a = if self.n == 2 { 1.0 } else { 0.3 };
757 self.round_ms += a * (dt_ms - self.round_ms);
758 self.tokens += a * (produced as f64 - self.tokens);
759 }
760 fn pays(&self) -> bool {
761 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
762 }
763}
764
765pub struct GenerateResult {
767 pub text: String,
768 pub token_ids: Vec<u32>,
769 pub prompt_tokens: usize,
770 pub tokens_generated: usize,
771 pub finish_reason: String,
772 pub mtp_drafted: usize,
774 pub mtp_accepted: usize,
775 pub token_confidence: Vec<f32>,
780 pub traces: Vec<TokenTrace>,
783}
784
785#[derive(Clone, Debug)]
790pub struct TokenTrace {
791 pub t: usize,
793 pub token_id: u32,
795 pub confidence: f32,
797 pub active_skill: Option<String>,
799 pub recon: Option<f32>,
803 pub switched: bool,
806}
807
808#[cfg_attr(not(test), allow(dead_code))]
813fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
814 let t = if temp > 1e-3 { temp } else { 1.0 };
815 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
816 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
817 if sum > 0.0 {
818 (((logits[id as usize] - max) / t).exp()) / sum
819 } else {
820 0.0
821 }
822}
823
824fn prefill_batched() -> bool {
827 std::env::var("CMF_PREFILL")
828 .map(|v| v != "seq")
829 .unwrap_or(true)
830}
831
832#[inline]
836fn nll_graph_policy(
837 unmasked: bool,
838 prefer_graph: bool,
839 native_metal: bool,
840) -> (bool, bool) {
841 let graph_quality = unmasked && prefer_graph;
842 let fused_head_quality = graph_quality && native_metal;
843 (graph_quality, fused_head_quality)
844}
845
846#[derive(Clone, Copy)]
850enum PrefillIn<'a> {
851 Ids(&'a [u32]),
852 Hidden(&'a [f32]),
853}
854
855impl Pipeline {
862 fn can_prefill_batched(&self) -> bool {
863 #[cfg(test)]
864 let force_serial = self.nll_test_force_serial;
865 #[cfg(not(test))]
866 let force_serial = false;
867 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
868 }
869
870 fn automatic_gpu_prefix(&self) -> Option<usize> {
873 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
874 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
875 }
876}
877
878pub fn prefill_chunk() -> usize {
885 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
886 .ok()
887 .and_then(|v| v.parse::<usize>().ok())
888 {
889 return n.max(1);
890 }
891 if cfg!(target_os = "macos") {
892 512
893 } else if cfg!(target_arch = "aarch64") {
894 256
897 } else {
898 48
899 }
900}
901
902#[inline]
908fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
909 if end <= start || start >= input_len {
910 return 0;
911 }
912 let rows = (end.min(input_len) - start).min(input_len - start);
913 if end < input_len {
914 rows
915 } else {
916 rows.saturating_sub(1)
917 }
918}
919
920pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
922
923impl Pipeline {
924 fn clear_sequence_state(&mut self) {
932 self.kv_cache.clear();
933 self.kv_history.clear();
934 if let Some(b) = &mut self.dsv41 {
935 b.3.clear();
936 }
937 crate::gpu::graph_kv_reset(self.graph_kv_id);
938 crate::gpu::graph_kv_reset(self.mtp_kv_id());
943 }
944
945 fn finish_generation(
951 &mut self,
952 mtp: &mut Option<MtpModule>,
953 router: &mut Option<crate::swarm::DynRouter>,
954 clear_sequence: bool,
955 ) {
956 if router.is_some() {
960 let _ = self.set_active_skill(None);
961 }
962 if clear_sequence {
963 self.clear_sequence_state();
964 if let Some(m) = mtp.as_mut() {
965 m.kv.clear();
971 }
972 if let Some(m) = self.mtp.as_mut() {
973 m.kv.clear();
977 }
978 }
979 self.graph_want_logits = false;
980 self.graph_head_required = false;
981 self.graph_logits = None;
982 self.graph_failed
983 .store(false, std::sync::atomic::Ordering::Relaxed);
984 self.cancel
985 .store(false, std::sync::atomic::Ordering::Relaxed);
986 self.dyn_router = router.take().or(self.dyn_router.take());
987 self.mtp = mtp.take().or(self.mtp.take());
988 self.mtp_graph_mode = None;
989 self.spec_forced = None;
990 }
991
992 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
996 if self
997 .graph_failed
998 .swap(false, std::sync::atomic::Ordering::Relaxed)
999 {
1000 self.cancel
1001 .store(false, std::sync::atomic::Ordering::Relaxed);
1002 self.clear_sequence_state();
1003 self.graph_logits = None;
1004 self.graph_want_logits = false;
1005 self.graph_head_required = false;
1006 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1007 }
1008 Ok(())
1009 }
1010
1011 #[cfg(target_os = "macos")]
1012 fn fail_metal_graph(&mut self, reason: &str) {
1013 crate::pipeline::METAL_GRAPH_ERRORS
1014 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1015 self.clear_sequence_state();
1016 self.graph_logits = None;
1017 self.graph_failed
1018 .store(true, std::sync::atomic::Ordering::Relaxed);
1019 self.cancel
1020 .store(true, std::sync::atomic::Ordering::Relaxed);
1021 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1022 }
1023
1024 fn nll_begin(&mut self) -> Result<(), String> {
1029 if self
1030 .graph_failed
1031 .swap(false, std::sync::atomic::Ordering::Relaxed)
1032 {
1033 self.cancel
1034 .store(false, std::sync::atomic::Ordering::Relaxed);
1035 self.clear_sequence_state();
1036 self.graph_logits = None;
1037 self.graph_want_logits = false;
1038 self.graph_head_required = false;
1039 return Err("GPU graph failed before NLL scoring".to_string());
1040 }
1041 self.clear_sequence_state();
1042 self.graph_logits = None;
1043 self.graph_want_logits = false;
1044 self.graph_head_required = false;
1045 Ok(())
1046 }
1047
1048 fn nll_end(&mut self) {
1052 self.clear_sequence_state();
1053 self.graph_logits = None;
1054 self.graph_want_logits = false;
1055 self.graph_head_required = false;
1056 self.graph_failed
1057 .store(false, std::sync::atomic::Ordering::Relaxed);
1058 }
1059
1060 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1063 #[cfg(test)]
1064 if self.nll_test_fail_at == Some(pos) {
1065 self.nll_test_fail_at = None;
1066 self.graph_failed
1067 .store(true, std::sync::atomic::Ordering::Relaxed);
1068 self.cancel
1069 .store(true, std::sync::atomic::Ordering::Relaxed);
1070 }
1071 if self
1072 .graph_failed
1073 .swap(false, std::sync::atomic::Ordering::Relaxed)
1074 {
1075 self.cancel
1076 .store(false, std::sync::atomic::Ordering::Relaxed);
1077 self.clear_sequence_state();
1078 self.graph_logits = None;
1079 self.graph_want_logits = false;
1080 return Err(format!(
1081 "GPU graph failed during NLL {phase} at position {pos}"
1082 ));
1083 }
1084 Ok(())
1085 }
1086
1087 #[inline]
1091 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1092 virtual_idx % self.physical_layers
1093 }
1094
1095 #[inline]
1098 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1099 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1100 }
1101
1102 #[allow(clippy::too_many_arguments)]
1104
1105 #[cfg(target_os = "macos")]
1124 fn graph_prefill_preferred(&self) -> bool {
1125 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1126 if !crate::gpu::enabled_here()
1127 || !graph_force
1128 || std::env::var("CMF_GPU_BLOCK")
1129 .map(|v| v == "0")
1130 .unwrap_or(false)
1131 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1134 {
1135 return false;
1136 }
1137 self.weights
1138 .layers
1139 .iter()
1140 .any(|lw| {
1141 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1142 })
1143 }
1144
1145 #[cfg(not(target_os = "macos"))]
1146 fn graph_prefill_preferred(&self) -> bool {
1147 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1155 if !graph_on || !crate::gpu::enabled_here() {
1156 return false;
1157 }
1158 if self.o1_active() {
1172 return false;
1173 }
1174 if self
1175 .weights
1176 .layers
1177 .iter()
1178 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1179 {
1180 return true;
1181 }
1182 self.weights
1193 .layers
1194 .iter()
1195 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1196 && self.automatic_gpu_prefix().is_none()
1197 }
1198
1199 #[cfg(target_os = "macos")]
1200 fn q1_graph_gpu(
1201 &mut self,
1202 start: usize,
1203 upto: Option<usize>,
1204 position: usize,
1205 h: &mut [f32],
1206 ) -> usize {
1207 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1209 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1210 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1212 || !graph_force
1213 || std::env::var("CMF_GPU_BLOCK")
1214 .map(|v| v == "0")
1215 .unwrap_or(false)
1216 {
1217 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1218 eprintln!(
1219 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1220 self.attn_softcap > 0.0,
1221 crate::gpu::enabled_here(),
1222 graph_force,
1223 );
1224 }
1225 if self.graph_head_required {
1226 self.fail_metal_graph("native graph front gate refused");
1227 }
1228 return start;
1229 }
1230 if self.swa.is_some()
1234 || self.global_attn.is_some()
1235 || self.attention_heads_per_layer.is_some()
1236 || self.attn_v_norm
1237 || self.weights.layers.iter().any(|lw| {
1238 lw.attn_out_norm.is_some()
1239 || lw.ffn_out_norm.is_some()
1240 || lw.layer_scale.is_some()
1241 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1242 })
1243 {
1244 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1245 eprintln!(
1246 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1247 self.swa.is_some(),
1248 self.global_attn.is_some(),
1249 self.attention_heads_per_layer.is_some(),
1250 self.attn_v_norm,
1251 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1252 );
1253 }
1254 if self.graph_head_required {
1255 self.fail_metal_graph("native graph architecture gate refused");
1256 }
1257 return start;
1258 }
1259 let limit = upto
1262 .map(|u| u + 1)
1263 .unwrap_or(self.num_layers)
1264 .min(self.num_layers);
1265
1266 enum Item<'a> {
1267 Gdn {
1268 run: Vec<GdnGpuLayer<'a>>,
1269 first: usize,
1270 },
1271 Attn {
1272 l: AttnGpuLayer<'a>,
1273 li: usize,
1274 q_norm: Option<&'a [f32]>,
1275 k_norm: Option<&'a [f32]>,
1276 output_gate: bool,
1277 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1278 full_gpu: bool,
1281 },
1282 }
1283
1284 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1291 let attend_contract = attend_mode != "0"
1292 && attend_mode != "off"
1293 && self.head_dim % 4 == 0
1294 && self.head_dim <= 256
1295 && self.rotary_dim >= 2
1296 && self.rotary_dim <= self.head_dim
1297 && (self.rotary_dim / 2) % 32 == 0
1298 && self.num_kv_heads > 0
1299 && self.num_heads % self.num_kv_heads == 0;
1300
1301 let mut plan: Vec<Item> = Vec::new();
1302 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1303 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1305 let mut scan = start;
1306 while scan < limit {
1307 let lw = &self.weights.layers[self.phys_layer(scan)];
1308 let ffn = match &lw.ffn {
1309 FfnKind::Dense(d) if d.segs.is_empty() => {
1310 let (Some(g), Some(u), Some(dn)) = (
1311 d.gate_proj.metal_graph_parts(),
1312 d.up_proj.metal_graph_parts(),
1313 d.down_proj.metal_graph_parts(),
1314 ) else {
1315 if block_diag {
1316 eprintln!(
1317 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1318 );
1319 }
1320 break;
1321 };
1322 MetalFfn::Dense {
1323 gate: g,
1324 up: u,
1325 down: dn,
1326 }
1327 }
1328 FfnKind::Moe(m) => {
1329 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1330 if block_diag {
1331 eprintln!(
1332 "block-graph: L{scan} MoE outside the graph contract — run ends"
1333 );
1334 }
1335 break;
1336 };
1337 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1338 model_ref.get_or_insert_with(|| model.clone());
1339 }
1340 MetalFfn::Moe(moe)
1341 }
1342 _ => {
1343 if block_diag {
1344 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1345 }
1346 break;
1347 }
1348 };
1349 match &lw.attn {
1350 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1351 let parts = (
1352 w.in_proj_qkv.metal_graph_parts(),
1353 w.in_proj_z.metal_graph_parts(),
1354 w.in_proj_a.f32_parts(),
1355 w.in_proj_b.f32_parts(),
1356 w.out_proj.metal_graph_parts(),
1357 );
1358 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1359 if block_diag {
1360 eprintln!(
1361 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1362 w.in_proj_qkv.metal_graph_parts().is_some(),
1363 w.in_proj_z.metal_graph_parts().is_some(),
1364 w.in_proj_a.f32_parts().is_some(),
1365 w.in_proj_b.f32_parts().is_some(),
1366 w.out_proj.metal_graph_parts().is_some(),
1367 );
1368 }
1369 break;
1370 };
1371 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1372 model_ref.get_or_insert_with(|| model.clone());
1373 }
1374 let gl = GdnGpuLayer {
1375 attn_norm: &lw.input_norm,
1376 post_norm: &lw.post_norm,
1377 qkv,
1378 z,
1379 a,
1380 b,
1381 out,
1382 ffn,
1383 conv1d: &w.conv1d,
1384 a_log: &w.a_log,
1385 dt_bias: &w.dt_bias,
1386 gnorm: &w.norm,
1387 };
1388 match plan.last_mut() {
1389 Some(Item::Gdn { run, .. }) => run.push(gl),
1390 _ => plan.push(Item::Gdn {
1391 run: vec![gl],
1392 first: scan,
1393 }),
1394 }
1395 }
1396 AttnKind::Full {
1397 wq,
1398 wk,
1399 wv,
1400 wo,
1401 q_norm,
1402 k_norm,
1403 output_gate,
1404 softplus_gate: None,
1405 bias,
1406 } if !self.kv_cache.layers[scan].o1_sealed()
1407 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1412 {
1413 let parts = (
1414 wq.metal_graph_parts(),
1415 wk.metal_graph_parts(),
1416 wv.metal_graph_parts(),
1417 wo.metal_graph_parts(),
1418 );
1419 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1420 break;
1421 };
1422 if let QTensor::Mapped { model, .. } = wq {
1423 model_ref.get_or_insert_with(|| model.clone());
1424 }
1425 let cache = &self.kv_cache.layers[scan];
1426 let o1_metal = cache.o1.is_some()
1430 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1431 && cache.o1_views().is_some();
1432 let full_gpu = attend_contract
1433 && cache.mode == crate::kv_cache::KvMode::F32
1434 && (cache.o1.is_none() || o1_metal)
1435 && bias.is_none()
1436 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1437 && pk.1 == self.num_kv_heads * self.head_dim
1438 && pv.1 == self.num_kv_heads * self.head_dim
1439 && po.2 == self.num_heads * self.head_dim;
1440 plan.push(Item::Attn {
1441 l: AttnGpuLayer {
1442 attn_norm: &lw.input_norm,
1443 post_norm: &lw.post_norm,
1444 wq: pq,
1445 wk: pk,
1446 wv: pv,
1447 wo: po,
1448 ffn,
1449 },
1450 li: scan,
1451 q_norm: q_norm.as_deref(),
1452 k_norm: k_norm.as_deref(),
1453 output_gate: *output_gate,
1454 bias: bias
1455 .as_ref()
1456 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1457 full_gpu,
1458 });
1459 }
1460 _ => break,
1461 }
1462 scan += 1;
1463 }
1464 let Some(model) = model_ref else {
1465 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1466 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1467 }
1468 if self.graph_head_required {
1469 self.fail_metal_graph("native graph has no mapped model reference");
1470 }
1471 return start;
1472 };
1473 if plan.is_empty() {
1474 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1475 eprintln!("q1-graph: empty plan at layer {start}");
1476 }
1477 if self.graph_head_required {
1478 self.fail_metal_graph("native graph plan is empty");
1479 }
1480 return start;
1481 }
1482 let has_moe = plan.iter().any(|it| match it {
1483 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1484 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1485 });
1486 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1487 let dev_attend = attend_contract
1488 && (self.head_dim <= 128
1489 || has_moe
1490 || (self.head_dim <= 256 && has_gdn)
1496 || attend_mode == "force"
1497 || attend_mode == "256");
1498 if !dev_attend {
1499 for it in &mut plan {
1500 if let Item::Attn { li, full_gpu, .. } = it {
1501 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1504 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1505 if !keep_o1 {
1506 *full_gpu = false;
1507 }
1508 }
1509 }
1510 }
1511 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1512 use std::sync::atomic::{AtomicBool, Ordering};
1513 static SAID: AtomicBool = AtomicBool::new(false);
1514 if !SAID.swap(true, Ordering::Relaxed) {
1515 let fg = plan
1516 .iter()
1517 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1518 .count();
1519 let att = plan
1520 .iter()
1521 .filter(|it| matches!(it, Item::Attn { .. }))
1522 .count();
1523 eprintln!(
1524 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1525 plan.len(),
1526 self.head_dim,
1527 self.rotary_dim,
1528 self.num_kv_heads,
1529 self.num_heads,
1530 );
1531 }
1532 }
1533 let dims = GraphDims {
1534 hidden: self.hidden_size,
1535 eps: self.rms_eps as f32,
1536 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1537 };
1538 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1539 if self.graph_head_required {
1540 self.fail_metal_graph("native TokenGraph allocation refused");
1541 }
1542 return start;
1543 };
1544 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1545 nv: cfg.num_v_heads,
1546 nk: cfg.num_k_heads,
1547 dk: cfg.key_head_dim,
1548 dv: cfg.value_head_dim,
1549 kk: cfg.conv_kernel,
1550 hidden: self.hidden_size,
1551 inter: self.intermediate_size,
1552 c_dim: cfg.conv_dim(),
1553 eps: cfg.rms_eps as f32,
1554 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1555 });
1556 let mut valid = 0usize;
1560 let mut end = start;
1561 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1563 static ONCE: std::sync::Once = std::sync::Once::new();
1564 ONCE.call_once(|| {
1565 for it in &plan {
1566 match it {
1567 Item::Gdn { first, run } => {
1568 eprintln!("plan: Gdn first={first} len={}", run.len())
1569 }
1570 Item::Attn { li, full_gpu, .. } => {
1571 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1572 }
1573 }
1574 }
1575 });
1576 }
1577 for item in &plan {
1578 let ok = match item {
1579 Item::Gdn { run, .. } => gcfg
1580 .as_ref()
1581 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1582 .unwrap_or(false),
1583 Item::Attn { l, .. } => graph.attn_ok(l),
1584 };
1585 if !ok {
1586 if block_diag {
1587 eprintln!(
1588 "block-graph: plan item {} ({}) failed graph preflight",
1589 valid,
1590 match item {
1591 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1592 Item::Attn { li, .. } => format!("Attn L{li}"),
1593 }
1594 );
1595 }
1596 break;
1597 }
1598 valid += 1;
1599 end += match item {
1600 Item::Gdn { run, .. } => run.len(),
1601 Item::Attn { .. } => 1,
1602 };
1603 }
1604 plan.truncate(valid);
1605 if plan.is_empty() {
1606 if self.graph_head_required {
1607 self.fail_metal_graph("native graph preflight produced no valid items");
1608 }
1609 return start;
1610 }
1611
1612 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1613 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1614 return start;
1615 }
1616
1617 let inv_freq = self.inv_freq.clone();
1618 let pool = self.pool.clone();
1619 let (nh, nkv, hd, hs, rd, eps) = (
1620 self.num_heads,
1621 self.num_kv_heads,
1622 self.head_dim,
1623 self.hidden_size,
1624 self.rotary_dim,
1625 self.rms_eps,
1626 );
1627 let norm_style = self.norm_style;
1628 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1629 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1630 let kv_id = self.graph_kv_id;
1631 let mut pending: Vec<(usize, usize)> = Vec::new();
1634 let mut dev_attn: Vec<usize> = Vec::new();
1637 for item in &plan {
1638 let _xt0 = std::time::Instant::now();
1639 let _xkind: u32 = match item {
1640 Item::Gdn { .. } => 2,
1641 Item::Attn { .. } => 3,
1642 };
1643 if self.loop_final_norm {
1645 let item_start = match item {
1646 Item::Gdn { first, .. } => *first,
1647 Item::Attn { li, .. } => *li,
1648 };
1649 if item_start > start && self.is_loop_end(item_start - 1) {
1650 graph.encode_loop_norm(&self.weights.final_norm);
1651 }
1652 }
1653 match item {
1654 Item::Gdn { run, first } => {
1655 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1656 if l.linear_state.len() != want {
1657 l.linear_state = vec![0f32; want];
1658 }
1659 }
1660 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1661 .iter()
1662 .map(|l| l.linear_state.as_slice())
1663 .collect();
1664 let _ig = std::time::Instant::now();
1665 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1666 tracing::error!("q1 graph: GDN run refused after validation");
1668 return start;
1669 }
1670 graph.commit_kind = 2;
1673 graph.commit();
1674 crate::gpu::stageprof(0, _ig.elapsed());
1675 pending.push((*first, run.len()));
1676 }
1677 Item::Attn {
1678 l,
1679 li,
1680 q_norm,
1681 k_norm,
1682 output_gate,
1683 bias,
1684 full_gpu,
1685 } => {
1686 let _ia = std::time::Instant::now();
1687 if *full_gpu {
1689 let cache = &self.kv_cache.layers[*li];
1690 let o1p = if cache.o1.is_some() {
1691 match cache.o1_views() {
1692 Some(views) => Some(crate::gpu::O1AttnParams {
1693 views,
1694 epoch: self.o1_epoch,
1695 }),
1696 None => None,
1698 }
1699 } else {
1700 None
1701 };
1702 let o1_layer = cache.o1.is_some();
1703 if o1_layer && o1p.is_none() {
1704 }
1706 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1707 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1708 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1709 let p = crate::gpu::AttnDeviceParams {
1710 kv_id,
1711 layer: *li,
1712 nh,
1713 nkv,
1714 hd,
1715 rd,
1716 position,
1717 scale: self.attn_scale,
1718 eps: eps as f32,
1719 gemma,
1720 late_qk_norm: self.qk_norm_after_rope,
1721 output_gate: *output_gate,
1722 q_norm: *q_norm,
1723 k_norm: *k_norm,
1724 inv_freq: &inv_freq,
1725 cpu_k,
1726 cpu_v,
1727 cpu_stored,
1728 o1: o1p,
1729 };
1730 let o1_bad = o1_layer && p.o1.is_none();
1731 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1732 {
1733 if p.o1.is_none() {
1735 dev_attn.push(*li);
1736 }
1737 graph.commit_kind = 3;
1738 graph.commit();
1739 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1743 continue;
1744 }
1745 }
1747 graph.encode_attn_prefix(l);
1748 if let Err(err) = graph.sync_checked() {
1749 self.fail_metal_graph(&err);
1750 return start;
1751 }
1752 if !pending.is_empty() {
1753 let idxs: Vec<usize> =
1754 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1755 let mut outs: Vec<&mut [f32]> = self
1756 .kv_cache
1757 .layers
1758 .iter_mut()
1759 .enumerate()
1760 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1761 .map(|(_, s)| s.linear_state.as_mut_slice())
1762 .collect();
1763 graph.read_states(&mut outs);
1764 }
1765 let mut q_raw = attention::take_buf(l.wq.1);
1766 let mut k = attention::take_buf(l.wk.1);
1767 let mut v = attention::take_buf(l.wv.1);
1768 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1769 let cfg = QwenAttnCfg {
1770 num_heads: nh,
1771 num_kv_heads: nkv,
1772 head_dim: hd,
1773 hidden_size: hs,
1774 position,
1775 inv_freq: &inv_freq,
1776 rotary_dim: rd,
1777 scale: self.attn_scale,
1778 softcap: self.attn_softcap,
1779 window: None,
1780 v_norm: false,
1781 qk_norm_after_rope: self.qk_norm_after_rope,
1782 q_norm: *q_norm,
1783 k_norm: *k_norm,
1784 output_gate: *output_gate,
1785 softplus_gate: None,
1786 rope_scale: 1.0,
1787 bias: *bias,
1788 rms_eps: eps,
1789 norm_style,
1790 pool: pool.as_deref(),
1791 };
1792 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1795 || std::env::var("CMF_ATTN_DUMP").is_ok();
1796 let _ = full_gpu;
1797 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1798 let mut ao = attention::qwen_attention_core(
1799 q_raw,
1800 k,
1801 v,
1802 &mut self.kv_cache.layers[*li],
1803 &cfg,
1804 );
1805 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1809 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1810 let (cq, _cg, _ck, _cv) =
1811 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1812 let cache = &self.kv_cache.layers[*li];
1813 let n = cache.head_keys(0).len() / hd;
1814 let mut bytes: Vec<u8> = Vec::new();
1815 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1816 bytes.extend_from_slice(&v.to_le_bytes());
1817 }
1818 for v in &cq {
1819 bytes.extend_from_slice(&v.to_le_bytes());
1820 }
1821 for g in 0..nkv {
1822 for v in cache.head_keys(g) {
1823 bytes.extend_from_slice(&v.to_le_bytes());
1824 }
1825 }
1826 for g in 0..nkv {
1827 for v in cache.head_values(g) {
1828 bytes.extend_from_slice(&v.to_le_bytes());
1829 }
1830 }
1831 let _ =
1832 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1833 }
1834 }
1835 if let Some((qr0, k0, v0)) =
1836 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1837 {
1838 let (cq, _cg, ck, cv) =
1839 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1840 let mut h_now = vec![0f32; hs];
1841 graph.read_h(&mut h_now);
1842 let cache = &self.kv_cache.layers[*li];
1843 let n_after = cache.head_keys(0).len() / hd;
1844 let stored = n_after.saturating_sub(1);
1848 let cpu_k: Vec<&[f32]> = (0..nkv)
1849 .map(|g| &cache.head_keys(g)[..stored * hd])
1850 .collect();
1851 let cpu_v: Vec<&[f32]> = (0..nkv)
1852 .map(|g| &cache.head_values(g)[..stored * hd])
1853 .collect();
1854 let p = crate::gpu::AttnDeviceParams {
1855 kv_id,
1856 layer: *li,
1857 nh,
1858 nkv,
1859 hd,
1860 rd,
1861 position,
1862 scale: self.attn_scale,
1863 eps: eps as f32,
1864 gemma,
1865 late_qk_norm: self.qk_norm_after_rope,
1866 output_gate: *output_gate,
1867 q_norm: *q_norm,
1868 k_norm: *k_norm,
1869 inv_freq: &inv_freq,
1870 cpu_k,
1871 cpu_v,
1872 cpu_stored: stored,
1873 o1: None,
1874 };
1875 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1876 let md = |a: &[f32], b: &[f32]| {
1877 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1878 };
1879 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1880 eprintln!(
1881 "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}",
1882 nn(&cq),
1883 md(&cq, &dq),
1884 nn(&ck),
1885 md(&ck, &dk),
1886 nn(&cv),
1887 md(&cv, &dv),
1888 nn(&ao),
1889 md(&ao, &dao)
1890 );
1891 } else {
1892 eprintln!("attn-oracle L{li}: device probe declined");
1893 }
1894 }
1895 graph.encode_attn_suffix(l, &ao);
1896 graph.commit();
1899 attention::recycle_buf(&mut ao);
1900 }
1901 }
1902
1903 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1904 }
1905 let mut lm_rows = None;
1910 if self.graph_want_logits
1911 && upto.is_none()
1912 && end == self.num_layers
1913 && std::env::var("CMF_GPU_LMHEAD")
1914 .map(|v| v != "0")
1915 .unwrap_or(true)
1916 {
1917 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
1918 if graph.lm_head_ok(lm) {
1919 graph.encode_lm_head(&self.weights.final_norm, lm);
1920 lm_rows = Some(lm.1);
1921 }
1922 }
1923 }
1924 if self.graph_head_required && lm_rows.is_none() {
1925 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1926 self.fail_metal_graph("fused graph head was requested but not encodable");
1927 return start;
1928 }
1929 let _sy0 = std::time::Instant::now();
1930 if let Err(err) = graph.sync_checked() {
1931 self.fail_metal_graph(&err);
1932 return start;
1933 }
1934 let _rs0 = std::time::Instant::now();
1935 if !pending.is_empty() {
1936 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1937 let mut outs: Vec<&mut [f32]> = self
1938 .kv_cache
1939 .layers
1940 .iter_mut()
1941 .enumerate()
1942 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1943 .map(|(_, s)| s.linear_state.as_mut_slice())
1944 .collect();
1945 graph.read_states(&mut outs);
1946 }
1947 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1948 use std::sync::atomic::{AtomicU64, Ordering};
1949 static SY: AtomicU64 = AtomicU64::new(0);
1950 static RS: AtomicU64 = AtomicU64::new(0);
1951 static N: AtomicU64 = AtomicU64::new(0);
1952 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1953 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1954 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1955 if n % 100 == 0 {
1956 eprintln!(
1957 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1958 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1959 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1960 );
1961 }
1962 }
1963 if let Some(rows) = lm_rows {
1964 crate::gpu::hostprof_encode_done(_mt0);
1965 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1966 graph.read_logits(&mut lg);
1967 crate::gpu::hostprof_total(_mt0);
1968 lg.resize(self.vocab_size, 0.0);
1969 if let Some(c) = self.final_softcap {
1970 for l in lg.iter_mut() {
1971 *l = c * (*l / c).tanh();
1972 }
1973 }
1974 self.graph_logits = Some(lg);
1975 }
1976 graph.read_h(h);
1977 if self.graph_head_required && self.graph_logits.is_none() {
1978 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1979 self.fail_metal_graph("fused graph head completed without logits readback");
1980 return start;
1981 }
1982 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1983 METAL_GRAPH_LAYERS.fetch_add(
1984 end.saturating_sub(start) as u64,
1985 std::sync::atomic::Ordering::Relaxed,
1986 );
1987 if self.graph_head_required {
1988 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1989 }
1990 for li in dev_attn {
1994 let mut krow = attention::take_buf(nkv * hd);
1995 let mut vrow = attention::take_buf(nkv * hd);
1996 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1997 let cache = &mut self.kv_cache.layers[li];
1998 cache.append(&krow, &vrow, &[]);
1999 let n = cache.seq_len;
2000 let mut imp = attention::take_buf(n);
2001 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2002 cache.accumulate_imp(&imp);
2003 attention::recycle_buf(&mut imp);
2004 }
2005 attention::recycle_buf(&mut krow);
2006 attention::recycle_buf(&mut vrow);
2007 }
2008 end
2009 }
2010
2011 pub fn new(
2012 tokenizer: Tokenizer,
2013 weights: PipelineWeights,
2014 hidden_size: usize,
2015 intermediate_size: usize,
2016 num_heads: usize,
2017 num_kv_heads: usize,
2018 head_dim: usize,
2019 num_layers: usize,
2020 physical_layers: usize,
2021 loop_final_norm: bool,
2022 vocab_size: usize,
2023 rms_eps: f64,
2024 rope_base: f32,
2025 norm_style: NormStyle,
2026 max_seq_len: usize,
2027 sampler_config: SamplerConfig,
2028 ) -> Self {
2029 let rng = match sampler_config.seed {
2030 Some(s) => SplitMix64::new(s),
2031 None => SplitMix64::from_entropy(),
2032 };
2033 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2034 let pool = Pool::from_env();
2035 if let Some(p) = &pool {
2036 tracing::info!("worker pool: {} threads", p.n_workers());
2037 }
2038 Self {
2039 gpu_plan: None,
2040 tokenizer: std::sync::Arc::new(tokenizer),
2041 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2042 sampler_config,
2043 weights,
2044 hidden_size,
2045 intermediate_size,
2046 num_heads,
2047 num_kv_heads,
2048 head_dim,
2049 num_layers,
2050 physical_layers,
2051 loop_final_norm,
2052 vocab_size,
2053 rms_eps,
2054 rope_base,
2055 norm_style,
2056 rotary_dim: head_dim,
2057 attention_heads_per_layer: None,
2058 vmf_cfg: None,
2059 gdn_cfg: None,
2060 kda_cfg: None,
2061 g3n: None,
2062 dsv4: None,
2063 dsv41: None,
2064 dsv41_vision: None,
2065 dsv41_prefill: None,
2066 qwen4_exp: None,
2067 dsv4_mtp: Vec::new(),
2068 dspark: None,
2069 dspark_pending: Vec::new(),
2070 dspark_hist: Vec::new(),
2071 dspark_real: Vec::new(),
2072 dspark_trunk_picks: Vec::new(),
2073 dspark_exp: Vec::new(),
2074 dspark_draft_ns: 0,
2075 logit_multiplier: None,
2076 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2077 graph_failed: std::sync::atomic::AtomicBool::new(false),
2078 kv_history: Vec::new(),
2079 short_conv_cfg: None,
2080 mtp: None,
2081 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2082 rng,
2083 sampler_scratch: SamplerScratch::default(),
2084 spec_forced: None,
2085 spec_q: Vec::new(),
2086 spec_p: Vec::new(),
2087 spec_res: Vec::new(),
2088 spec_qs: Vec::new(),
2089 spec_ps: Vec::new(),
2090 spec_ress: Vec::new(),
2091 mtp_graph_mode: None,
2092 #[cfg(target_os = "macos")]
2093 metal_verify: None,
2094 inv_freq,
2095 ws: ForwardScratch::new(hidden_size),
2096 pool,
2097 model: None,
2098 dyn_force_f32: false,
2099 dyn_skill_layers: Vec::new(),
2100 dyn_active: None,
2101 dyn_blend_loaded: false,
2102 dyn_phi_layer: None,
2103 dyn_phi_ema: Vec::new(),
2104 dyn_phi_seen: 0,
2105 dyn_router: None,
2106 o1_cfg: None,
2107 o1_epoch: 0,
2108 o1_flags: Vec::new(),
2109 trace: false,
2110 calib_temp: 1.0,
2111 confidence_on: true,
2112 embed_multiplier: 1.0,
2113 attn_scale: 1.0 / (head_dim as f32).sqrt(),
2114 swa: None,
2115 sliding_layers: None,
2116 inv_freq_local: None,
2117 rotary_dim_local: None,
2118 rope_scale: 1.0,
2119 rope_scale_local: 1.0,
2120 global_attn: None,
2121 inv_freq_global: None,
2122 attn_v_norm: false,
2123 qk_norm_after_rope: false,
2124 final_softcap: None,
2125 head_clusters: None,
2126 attn_softcap: 0.0,
2127 graph_want_logits: false,
2128 graph_head_required: false,
2129 graph_logits: None,
2130 graph_kv_id: {
2131 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2132 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2133 },
2134 #[cfg(test)]
2135 nll_test_fail_at: None,
2136 #[cfg(test)]
2137 nll_test_force_serial: false,
2138 }
2139 }
2140
2141 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2149 if let Some(c) = &cfg {
2150 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2151 tracing::error!(
2152 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2153 c.w,
2154 c.sink
2155 );
2156 self.o1_flags.clear();
2157 self.o1_cfg = None;
2158 return;
2159 }
2160 }
2161 self.o1_flags = match &cfg {
2162 Some(c) => {
2163 let mut flags = c.layer_flags(self.num_layers);
2164 for (li, f) in flags.iter_mut().enumerate() {
2165 if *f
2166 && !matches!(
2167 self.weights.layers[self.phys_layer(li)].attn,
2168 AttnKind::Full { .. }
2169 )
2170 {
2171 *f = false;
2172 }
2173 }
2174 flags
2175 }
2176 None => Vec::new(),
2177 };
2178 if let Some(c) = &cfg {
2179 let n = self.o1_flags.iter().filter(|&&f| f).count();
2180 tracing::info!(
2181 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2182 self.num_layers,
2183 c.m,
2184 c.w,
2185 c.sink,
2186 c.rect
2187 );
2188 }
2189 self.o1_cfg = cfg;
2190 }
2191
2192 pub fn o1_active(&self) -> bool {
2194 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2195 }
2196
2197 pub fn generation_graph_prefill(&self) -> bool {
2202 let graph = self.graph_prefill_preferred();
2203 #[cfg(not(target_os = "macos"))]
2211 if graph
2212 && self.mtp.is_some()
2213 && std::env::var("CMF_BATCH_K")
2214 .ok()
2215 .and_then(|v| v.parse::<usize>().ok())
2216 .is_some_and(|k| k > 0)
2217 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2218 {
2219 return false;
2220 }
2221 graph
2222 }
2223
2224 pub fn o1_device_stats(&self) -> (usize, u64) {
2229 crate::gpu::o1_device_stats(self.graph_kv_id)
2230 }
2231
2232 pub fn o1_begin(&mut self) {
2237 self.o1_begin_with_prefix(None);
2238 }
2239
2240 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2244 if let Some(c) = &self.o1_cfg {
2245 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2246 let boundary = requested_prefix.map(|p| {
2247 p.max(
2248 crate::nystrom::o1_deferred_boundary(w, sink)
2249 .expect("o1 config boundary validated in set_o1"),
2250 )
2251 });
2252 for (li, &f) in self.o1_flags.iter().enumerate() {
2253 if f {
2254 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2255 }
2256 }
2257 }
2258 }
2259
2260 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2262 self.o1_cfg.as_ref().and_then(|c| {
2263 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2264 .map(|floor| requested_prefix.max(floor))
2265 })
2266 }
2267
2268 fn o1_note_transition(&mut self) {
2269 let mut transitioned = false;
2273 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2274 if flagged {
2275 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2276 }
2277 }
2278 if transitioned {
2279 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2280 }
2281 }
2282
2283 fn o1_pending(&self) -> bool {
2284 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2285 f && self.kv_cache.layers[li].seq_len > 0
2286 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2287 })
2288 }
2289
2290 fn o1_fail(&mut self, err: String) {
2291 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2292 self.clear_sequence_state();
2293 self.graph_failed
2294 .store(true, std::sync::atomic::Ordering::Relaxed);
2295 self.cancel
2296 .store(true, std::sync::atomic::Ordering::Relaxed);
2297 }
2298
2299 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2304 if self.o1_cfg.is_none() {
2305 return Ok(false);
2306 }
2307 let mut participating = false;
2308 for li in 0..self.num_layers {
2309 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2310 continue;
2311 }
2312 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2313 return Err(err);
2314 }
2315 if self.kv_cache.layers[li].seq_len == 0 {
2316 continue;
2317 }
2318 participating = true;
2319 let num_heads = self.layer_num_heads(li);
2320 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2321 }
2322 self.o1_note_transition();
2323 for li in 0..self.num_layers {
2324 if self.o1_flags.get(li).copied().unwrap_or(false) {
2325 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2326 return Err(err);
2327 }
2328 }
2329 }
2330 Ok(participating
2331 && (0..self.num_layers).all(|li| {
2332 !self.o1_flags.get(li).copied().unwrap_or(false)
2333 || self.kv_cache.layers[li].seq_len == 0
2334 || self.kv_cache.layers[li].o1_sealed()
2335 }))
2336 }
2337
2338 fn o1_progress(&mut self) {
2341 if !self.o1_active() {
2342 return;
2343 }
2344 for li in 0..self.num_layers {
2345 if self.o1_flags.get(li).copied().unwrap_or(false) {
2346 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2347 self.o1_fail(err);
2348 return;
2349 }
2350 }
2351 }
2352 self.o1_note_transition();
2356 if !self.o1_pending() {
2357 return;
2358 }
2359 if let Err(err) = self.o1_seal_checked() {
2360 self.o1_fail(err);
2361 }
2362 }
2363
2364 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2369 if self
2370 .graph_failed
2371 .swap(false, std::sync::atomic::Ordering::Relaxed)
2372 {
2373 self.cancel
2374 .store(false, std::sync::atomic::Ordering::Relaxed);
2375 self.clear_sequence_state();
2376 return Err(format!("{phase}: deferred O(1) transition failed"));
2377 }
2378 Ok(())
2379 }
2380
2381 pub fn o1_seal(&mut self) {
2385 if let Err(err) = self.o1_seal_checked() {
2386 self.o1_fail(err);
2387 }
2388 }
2389
2390 pub fn set_trace(&mut self, on: bool) {
2392 self.trace = on;
2393 }
2394
2395 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2398 self.rng = match config.seed {
2399 Some(seed) => SplitMix64::new(seed),
2400 None => SplitMix64::from_entropy(),
2401 };
2402 self.sampler_config = config;
2403 }
2404
2405 pub fn set_confidence(&mut self, on: bool) {
2410 self.confidence_on = on;
2411 }
2412
2413 pub fn set_calib_temp(&mut self, t: f32) {
2416 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2417 }
2418
2419 pub fn calib_temp(&self) -> f32 {
2421 self.calib_temp
2422 }
2423
2424 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2427 self.rotary_dim = rotary_dim.min(self.head_dim);
2428 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2429 }
2430
2431 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2432 QwenAttnCfg {
2433 num_heads: self.num_heads,
2434 num_kv_heads: self.num_kv_heads,
2435 head_dim: self.head_dim,
2436 hidden_size: self.hidden_size,
2437 position,
2438 inv_freq: &self.inv_freq,
2439 rotary_dim: self.rotary_dim,
2440 scale: self.attn_scale,
2441 softcap: self.attn_softcap,
2442 window: None,
2443 v_norm: false,
2444 qk_norm_after_rope: self.qk_norm_after_rope,
2445 q_norm: None,
2446 k_norm: None,
2447 output_gate: false,
2448 softplus_gate: None,
2449 rope_scale: self.rope_scale,
2450 bias: None,
2451 rms_eps: self.rms_eps,
2452 norm_style: self.norm_style,
2453 pool: self.pool.as_deref(),
2454 }
2455 }
2456
2457 pub fn generate(
2459 &mut self,
2460 prompt: &str,
2461 max_tokens: usize,
2462 task_mask: Option<&TaskMask>,
2463 on_token: Option<TokenCallback>,
2464 ) -> Result<GenerateResult, String> {
2465 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2466 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2467 }
2468
2469 pub fn generate_from_vl(
2472 &mut self,
2473 input: &crate::dsv41_vision::PreparedVlInputs,
2474 max_tokens: usize,
2475 task_mask: Option<&TaskMask>,
2476 on_token: Option<TokenCallback>,
2477 ) -> Result<GenerateResult, String> {
2478 let Some(dsv41) = &self.dsv41 else {
2479 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2480 };
2481 if input.token_ids.is_empty() {
2482 return Err("empty V4.1 multimodal prompt".into());
2483 }
2484 if input.token_types.len() != input.token_ids.len() {
2485 return Err(format!(
2486 "V4.1 token type count {} != token count {}",
2487 input.token_types.len(),
2488 input.token_ids.len()
2489 ));
2490 }
2491 let dim = dsv41.2.dim;
2492 let mut embeddings = vec![None; input.token_ids.len()];
2493 let mut participates = vec![true; input.token_ids.len()];
2494 if !input.images.is_empty() {
2495 let vision = self
2496 .dsv41_vision
2497 .as_ref()
2498 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2499 for image in &input.images {
2500 let end = image.start.saturating_add(image.types.len());
2501 if end > input.token_ids.len() {
2502 return Err(format!(
2503 "V4.1 image span {}..{} exceeds prompt length {}",
2504 image.start,
2505 end,
2506 input.token_ids.len()
2507 ));
2508 }
2509 let mut span = vec![0.0f32; image.types.len() * dim];
2510 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2511 for (offset, &kind) in image.types.iter().enumerate() {
2512 let pos = image.start + offset;
2513 if input.token_types[pos] != kind {
2514 return Err(format!(
2515 "V4.1 image type mismatch at position {pos}: {} != {kind}",
2516 input.token_types[pos]
2517 ));
2518 }
2519 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2520 participates[pos] = false;
2521 }
2522 }
2523 }
2524 for (pos, &kind) in input.token_types.iter().enumerate() {
2525 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2526 return Err(format!("V4.1 text position {pos} has an image embedding"));
2527 }
2528 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2529 return Err(format!("V4.1 image position {pos} has no image embedding"));
2530 }
2531 }
2532 self.dsv41_prefill = Some((embeddings, participates));
2533 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2534 self.dsv41_prefill = None;
2535 result
2536 }
2537
2538 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2540 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2541 }
2542
2543 pub fn generate_from_ids(
2551 &mut self,
2552 input_ids: &[u32],
2553 max_tokens: usize,
2554 task_mask: Option<&TaskMask>,
2555 mut on_token: Option<TokenCallback>,
2556 ) -> Result<GenerateResult, String> {
2557 if std::env::var("CMF_TRACE_H").is_ok() {
2558 eprintln!("input_ids: {input_ids:?}");
2559 }
2560 if input_ids.is_empty() {
2561 return Err("empty prompt: nothing to generate from".to_string());
2562 }
2563 self.graph_failed
2567 .store(false, std::sync::atomic::Ordering::Relaxed);
2568 let task_mask = self.drop_open_mask(task_mask);
2573
2574 let reuse_from = {
2582 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2583 let h = &self.kv_history;
2584 if on
2585 && task_mask.is_none()
2586 && self.mtp.is_none()
2587 && self.o1_cfg.is_none()
2588 && self.dsv41.is_none()
2589 && !h.is_empty()
2590 && h.len() < input_ids.len()
2591 && input_ids[..h.len()] == h[..]
2592 {
2593 h.len()
2594 } else {
2595 0
2596 }
2597 };
2598 if reuse_from == 0 {
2599 self.clear_sequence_state();
2601 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2602 eprintln!(
2603 "kv-reuse: {} of {} prompt positions already cached",
2604 reuse_from,
2605 input_ids.len()
2606 );
2607 }
2608 crate::gpu::graph_race_begin_generation();
2609 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2613 std::env::var("CMF_O1_PREFILL")
2614 .ok()
2615 .and_then(|v| v.parse::<usize>().ok())
2616 .filter(|&p| p > 0)
2617 } else {
2618 None
2619 };
2620 if task_mask.is_none() {
2621 self.o1_begin_with_prefix(o1_prefill);
2622 }
2623
2624 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2630 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2657 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2658 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2674 for lw in &self.weights.layers {
2675 if let FfnKind::Dense(d) = &lw.ffn {
2676 dense_n += 1;
2677 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2678 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2679 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2680 {
2681 dense_q4tp += 1;
2682 }
2683 }
2684 }
2685 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2686 let penalized = self.sampler_config.repetition_penalty != 1.0
2690 || self.sampler_config.presence_penalty != 0.0
2691 || !self.sampler_config.suppress_tokens.is_empty();
2692 #[cfg(feature = "gpu")]
2697 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2698 #[cfg(not(feature = "gpu"))]
2699 let metal_wgpu = false;
2700 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2701 let spec_wanted = match spec_env.as_deref() {
2702 Some("0") => false,
2703 Some(_) => {
2704 if metal_wgpu {
2705 tracing::warn!(
2706 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2707 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2708 );
2709 }
2710 true
2711 }
2712 None => spec_default_ok && !penalized && !metal_wgpu,
2713 };
2714 #[cfg(target_os = "macos")]
2717 let metal_graph = crate::gpu::q1_force()
2718 && crate::gpu::enabled_here()
2719 && std::env::var("CMF_GPU_BLOCK")
2720 .map(|v| v != "0")
2721 .unwrap_or(true);
2722 #[cfg(not(target_os = "macos"))]
2723 let metal_graph = false;
2724 let graph_spec = self.speculative
2725 && (graph_on || metal_graph)
2726 && self.mtp.is_some()
2727 && task_mask.is_none()
2728 && !self.o1_active()
2729 && spec_sampling_ok
2730 && spec_wanted;
2731 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2738 let spec_active = self.speculative
2739 && self.mtp.is_some()
2740 && task_mask.is_none()
2741 && !self.o1_active()
2742 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2743 let mut mtp = if spec_active { self.mtp.take() } else { None };
2746 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2747 eprintln!(
2748 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2749 mtp.is_some(),
2750 self.speculative,
2751 self.sampler_config.temperature < 1e-6,
2752 );
2753 }
2754 if let Some(m) = &mut mtp {
2755 m.kv.clear();
2756 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2758 self.mtp_graph_mode = None;
2759 }
2760 let mut router = if mtp.is_none() {
2764 self.dyn_router.take()
2765 } else {
2766 None
2767 };
2768 if let Some(r) = &mut router {
2769 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2772 }
2773
2774 let mut all_ids = input_ids.to_vec();
2775 let mut generated = 0usize;
2776 let mut finish_reason = "max_tokens".to_string();
2777 let mut drafted = 0usize;
2778 let mut accepted = 0usize;
2779 let mut dsv4_spec_bad = 0usize;
2786 let mut dsv4_spec_retry_at = 0usize;
2787 let mut confidence: Vec<f32> = Vec::new();
2788 let trace_on = self.trace;
2789 let calib_temp = self.calib_temp;
2790 let mut traces: Vec<TokenTrace> = Vec::new();
2791
2792 let mut hidden = vec![0.0f32; self.hidden_size];
2798 let mut pos = reuse_from;
2799 let fuse_lm = mtp.is_none()
2808 && router.is_none()
2809 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2810 self.graph_logits = None;
2811 self.graph_want_logits = false;
2812 let _tpf = std::time::Instant::now();
2813 let batch_k = std::env::var("CMF_BATCH_K")
2814 .ok()
2815 .and_then(|v| v.parse::<usize>().ok())
2816 .unwrap_or(0);
2817 while self.qwen4_exp.is_some()
2828 && mtp.is_none()
2829 && pos < input_ids.len()
2830 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2831 {
2832 let token_id = input_ids[pos];
2833 let want_logits = pos + 1 == input_ids.len();
2834 let mut lg = Vec::new();
2835 if let Some(b) = &mut self.qwen4_exp {
2836 crate::qwen4_exp::forward_token(
2837 &b.0,
2838 &b.1,
2839 &b.2,
2840 &mut b.3,
2841 token_id,
2842 pos,
2843 &self.inv_freq,
2844 self.pool.as_deref(),
2845 &mut lg,
2846 want_logits,
2847 );
2848 }
2849 if want_logits {
2850 self.graph_logits = Some(lg);
2851 }
2852 pos += 1;
2853 hidden.fill(0.0);
2854 }
2855 while self.dsv4.is_some()
2856 && mtp.is_none()
2857 && pos < input_ids.len()
2858 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2859 {
2860 let end = (pos + prefill_chunk()).min(input_ids.len());
2861 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2862 let mut lg = Vec::new();
2863 if let Some(b) = &mut self.dsv4 {
2864 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2865 crate::dsv4::forward_chunk(
2866 g,
2867 layers,
2868 &cfg,
2869 st,
2870 &ids,
2871 pos,
2872 &self.inv_freq,
2873 self.pool.as_deref(),
2874 &mut lg,
2875 end == input_ids.len(),
2876 );
2877 }
2878 if end == input_ids.len() {
2879 self.graph_logits = Some(lg);
2880 }
2881 pos = end;
2882 hidden = vec![0.0; self.hidden_size];
2883 }
2884 let dsv41_prefill = self.dsv41_prefill.take();
2885 while self.dsv41.is_some()
2886 && mtp.is_none()
2887 && pos < input_ids.len()
2888 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2889 {
2890 let end = (pos + prefill_chunk()).min(input_ids.len());
2891 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2892 let mut lg = Vec::new();
2893 if let Some(b) = &mut self.dsv41 {
2894 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2895 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2896 crate::dsv41::forward_chunk_masked_with_embeddings(
2897 g,
2898 layers,
2899 cfg,
2900 st,
2901 &ids,
2902 pos,
2903 &embeddings[pos..end],
2904 &participates[pos..end],
2905 self.pool.as_deref(),
2906 &mut lg,
2907 );
2908 } else {
2909 crate::dsv41::forward_chunk(
2910 g,
2911 layers,
2912 cfg,
2913 st,
2914 &ids,
2915 pos,
2916 self.pool.as_deref(),
2917 &mut lg,
2918 );
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 dyn_prefill = router.is_some();
2932 let o1_prefill_limit = o1_prefill
2940 .and_then(|requested| self.o1_effective_boundary(requested))
2941 .map(|boundary| boundary.min(input_ids.len()));
2942 let mut o1_sealed = false;
2943 if let Some(limit) = o1_prefill_limit {
2944 if self.can_prefill_batched() && limit > 2 {
2947 let chunk = prefill_chunk();
2948 let hs = self.hidden_size;
2949 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2950 let end = (pos + chunk).min(limit);
2951 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2952 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2953 pos = end;
2954 }
2955 } else {
2956 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2957 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2958 pos += 1;
2959 }
2960 }
2961 if pos >= limit {
2962 o1_sealed = match self.o1_seal_checked() {
2963 Ok(sealed) => sealed,
2964 Err(err) => {
2965 self.finish_generation(&mut mtp, &mut router, true);
2966 return Err(err);
2967 }
2968 };
2969 tracing::info!(
2970 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2971 o1_prefill.unwrap_or(0),
2972 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2973 .unwrap_or(limit),
2974 limit,
2975 input_ids.len()
2976 );
2977 }
2978 }
2979 let graph_prefill = self.graph_prefill_preferred();
2985 #[cfg(target_os = "macos")]
2993 if task_mask.is_none()
2994 && !dyn_prefill
2995 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
2996 && crate::gpu::enabled_here()
2997 && self.gdn_cfg.is_some()
2998 && self.g3n.is_none()
2999 && input_ids.len() > 8
3000 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3001 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
3002 {
3003 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
3004 .ok()
3005 .and_then(|v| v.parse().ok())
3006 .filter(|&v| (16..=512).contains(&v))
3007 .unwrap_or(256);
3008 let hs = self.hidden_size;
3009 let _tp = std::time::Instant::now();
3010 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3011 let end = (pos + chunk).min(input_ids.len());
3012 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
3013 MetalPrefillOutcome::Completed(hb) => hb,
3014 MetalPrefillOutcome::Declined => break,
3015 MetalPrefillOutcome::Failed => {
3016 self.finish_generation(&mut mtp, &mut router, true);
3017 return Err("ordinary Metal prefill failed after admission".into());
3018 }
3019 };
3020 if let Some(m) = &mut mtp {
3021 let n_pairs = if end < input_ids.len() {
3022 end - pos
3023 } else {
3024 end - pos - 1
3025 };
3026 if n_pairs > 0 {
3027 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3028 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3029 .collect();
3030 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3031 for (j, (h, t)) in pairs.iter().enumerate() {
3032 let h = h.to_vec();
3033 let _ = self.mtp_step(m, &h, *t, pos + j);
3034 }
3035 }
3036 }
3037 }
3038 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3039 pos = end;
3040 }
3041 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3042 eprintln!(
3043 "metal-prefill: {} of {} tokens in {:.1} ms",
3044 pos,
3045 input_ids.len(),
3046 _tp.elapsed().as_secs_f64() * 1e3
3047 );
3048 }
3049 }
3050 if task_mask.is_none()
3051 && !dyn_prefill
3052 && !graph_prefill
3053 && self.can_prefill_batched()
3054 && self.g3n.is_none()
3055 && o1_prefill.is_none()
3056 && input_ids.len() > 2
3057 {
3058 let chunk = prefill_chunk();
3064 let hs = self.hidden_size;
3065 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3066 let end = (pos + chunk).min(input_ids.len());
3067 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3068 if let Some(m) = &mut mtp {
3069 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3070 .ok()
3071 .and_then(|v| v.parse().ok())
3072 .unwrap_or(0);
3073 for p in pos..end {
3074 if p + 1 < input_ids.len() {
3075 if probe >= 1 && p + 2 < input_ids.len() {
3076 let (d1, mut hx) = self.mtp_step_h(
3080 m,
3081 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3082 input_ids[p + 1],
3083 p,
3084 );
3085 let mut ok = d1 == input_ids[p + 2];
3086 Self::chain_probe_note(0, ok);
3087 let mut d_prev = d1;
3088 let mut extra = 0usize;
3089 for j in 1..probe {
3090 if p + 2 + j >= input_ids.len() {
3091 break;
3092 }
3093 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3094 extra += 1;
3095 ok = ok && dj == input_ids[p + 2 + j];
3096 Self::chain_probe_note(j, ok);
3097 d_prev = dj;
3098 hx = hj;
3099 }
3100 m.kv.truncate_last(extra);
3101 } else {
3102 let _ = self.mtp_step(
3103 m,
3104 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3105 input_ids[p + 1],
3106 p,
3107 );
3108 }
3109 }
3110 }
3111 }
3112 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3113 pos = end;
3114 }
3115 }
3116 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3117 if task_mask.is_none()
3118 && !dyn_prefill
3119 && !graph_prefill
3120 && !pair_off
3121 && self.pair_supported()
3122 && o1_prefill.is_none()
3123 {
3124 while pos + 1 < input_ids.len()
3125 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3126 {
3127 let e1 = self.embed_single(input_ids[pos]);
3128 let e2 = self.embed_single(input_ids[pos + 1]);
3129 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3130 self.commit_linear_scratch();
3132 if let Some(m) = &mut mtp {
3133 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3134 if pos + 2 < input_ids.len() {
3135 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3136 .ok()
3137 .and_then(|v| v.parse().ok())
3138 .unwrap_or(0);
3139 if probe >= 1 && pos + 3 < input_ids.len() {
3140 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3144 let mut ok = d1 == input_ids[pos + 3];
3145 Self::chain_probe_note(0, ok);
3146 let mut d_prev = d1;
3147 let mut extra = 0usize;
3148 for j in 1..probe {
3149 if pos + 3 + j >= input_ids.len() {
3150 break;
3151 }
3152 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3153 extra += 1;
3154 ok = ok && dj == input_ids[pos + 3 + j];
3155 Self::chain_probe_note(j, ok);
3156 d_prev = dj;
3157 hx = hj;
3158 }
3159 m.kv.truncate_last(extra);
3160 } else {
3161 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3162 }
3163 }
3164 }
3165 hidden = h2;
3166 pos += 2;
3167 }
3168 }
3169 let o1_batch_ready = o1_sealed
3182 && o1_prefill.is_some()
3183 && mtp.is_none()
3184 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3185 && (0..self.num_layers).all(|li| {
3186 let cache = &self.kv_cache.layers[self.phys_layer(li)];
3187 cache.o1.is_none() || cache.o1_views().is_some()
3188 });
3189 let mtp_batch_prefill = mtp.is_some()
3194 && graph_prefill
3195 && task_mask.is_none()
3196 && !dyn_prefill
3197 && !self.o1_active()
3198 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3199 if batch_k > 0
3200 && (graph_prefill || o1_batch_ready)
3201 && task_mask.is_none()
3202 && (!self.o1_active() || o1_batch_ready)
3203 && (mtp.is_none() || mtp_batch_prefill)
3204 && !dyn_prefill
3205 && pos + 1 < input_ids.len()
3206 {
3207 let hs = self.hidden_size;
3208 let chunk = batch_k;
3209 while pos < input_ids.len() {
3210 let end = (pos + chunk).min(input_ids.len());
3211 let bk = end - pos;
3212 let mut hiddens = vec![0f32; bk * hs];
3213 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3214 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3215 }
3216 let positions: Vec<usize> = (pos..end).collect();
3217 let t_chunk = std::time::Instant::now();
3218 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3219 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3220 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3221 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3222 eprintln!(
3223 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3224 if o1_batch_ready {
3225 "o1"
3226 } else if mtp_batch_prefill {
3227 "ordinary_mtp"
3228 } else {
3229 "ordinary"
3230 },
3231 bk as f64 / (ms / 1000.0)
3232 );
3233 }
3234 {
3235 use std::sync::atomic::{AtomicBool, Ordering};
3236 static SAID: AtomicBool = AtomicBool::new(false);
3237 if !SAID.swap(true, Ordering::Relaxed) {
3238 if ok_b {
3239 tracing::info!(
3240 "batched prefill: ACTIVE mode={} (k={bk})",
3241 if o1_batch_ready {
3242 "o1"
3243 } else if mtp_batch_prefill {
3244 "ordinary_mtp"
3245 } else {
3246 "ordinary"
3247 }
3248 );
3249 } else {
3250 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3251 }
3252 }
3253 }
3254 if ok_b {
3255 if mtp_batch_prefill {
3256 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3257 if n_pairs > 0 {
3258 let rows: Vec<Vec<f32>> = (0..n_pairs)
3264 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3265 .collect();
3266 let pairs: Vec<(&[f32], u32)> = rows
3267 .iter()
3268 .enumerate()
3269 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3270 .collect();
3271 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3272 eprintln!(
3273 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3274 pos,
3275 n_pairs,
3276 pos + n_pairs - 1,
3277 );
3278 }
3279 let warm_error = if let Some(m) = mtp.as_mut() {
3280 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3281 } else {
3282 None
3283 };
3284 if let Some(err) = warm_error {
3285 self.finish_generation(&mut mtp, &mut router, true);
3290 return Err(err.to_string());
3291 }
3292 }
3293 }
3294 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3295 pos = end;
3296 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3297 self.finish_generation(&mut mtp, &mut router, true);
3302 return Err(if o1_batch_ready {
3303 "sealed O(1) batch graph failed after admission".to_string()
3304 } else {
3305 "ordinary recurrent batch graph failed after admission".to_string()
3306 });
3307 } else {
3308 break; }
3310 }
3311 }
3312 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3313 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3314 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3315 if let Some(m) = &mut mtp {
3316 if pos + 1 < input_ids.len() {
3317 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3323 .ok()
3324 .and_then(|v| v.parse().ok())
3325 .unwrap_or(0);
3326 if probe >= 1 && pos + 2 < input_ids.len() {
3327 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3328 let mut ok = d1 == input_ids[pos + 2];
3329 Self::chain_probe_note(0, ok);
3330 let mut d_prev = d1;
3331 let mut extra = 0usize;
3332 for j in 1..probe {
3333 if pos + 2 + j >= input_ids.len() {
3334 break;
3335 }
3336 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3337 extra += 1;
3338 ok = ok && dj == input_ids[pos + 2 + j];
3339 Self::chain_probe_note(j, ok);
3340 d_prev = dj;
3341 hx = hj;
3342 }
3343 m.kv.truncate_last(extra);
3346 } else {
3347 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3348 }
3349 }
3350 }
3351 pos += 1;
3352 }
3353 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3354 eprintln!(
3355 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3356 input_ids.len(),
3357 _tpf.elapsed().as_secs_f64() * 1000.0
3358 );
3359 }
3360 if self
3361 .graph_failed
3362 .swap(false, std::sync::atomic::Ordering::Relaxed)
3363 {
3364 self.finish_generation(&mut mtp, &mut router, true);
3369 return Err("GPU token graph failed during prefill".to_string());
3370 }
3371 if self
3374 .cancel
3375 .swap(false, std::sync::atomic::Ordering::Relaxed)
3376 {
3377 self.finish_generation(&mut mtp, &mut router, true);
3381 return Ok(GenerateResult {
3382 text: String::new(),
3383 token_ids: Vec::new(),
3384 prompt_tokens: input_ids.len(),
3385 tokens_generated: 0,
3386 finish_reason: "cancelled".to_string(),
3387 mtp_drafted: 0,
3388 mtp_accepted: 0,
3389 token_confidence: Vec::new(),
3390 traces: Vec::new(),
3391 });
3392 }
3393
3394 if !o1_sealed {
3397 match self.o1_seal_checked() {
3398 Ok(_) => {}
3399 Err(err) => {
3400 self.finish_generation(&mut mtp, &mut router, true);
3401 return Err(err);
3402 }
3403 }
3404 }
3405
3406 macro_rules! commit {
3408 ($id:expr) => {{
3409 all_ids.push($id);
3410 generated += 1;
3411 if self.tokenizer.is_eos($id) {
3412 finish_reason = "stop".to_string();
3413 false
3414 } else {
3415 let token_text = self.tokenizer.decode_token($id);
3416 let mut go = true;
3417 if let Some(ref mut cb) = on_token {
3418 if !cb(&token_text) {
3419 finish_reason = "cancelled".to_string();
3420 go = false;
3421 }
3422 }
3423 go
3424 }
3425 }};
3426 }
3427
3428 let mut spec_trial = SpecTrial::Spec {
3439 t0: std::time::Instant::now(),
3440 gen0: generated,
3441 rounds: 0,
3442 };
3443 let mut spec_mon = SpecMon::default();
3444 let mut spec_watchdog_off = false;
3445 let mut next_pos = input_ids.len();
3447 'decode: while generated < max_tokens {
3448 if self
3449 .graph_failed
3450 .swap(false, std::sync::atomic::Ordering::Relaxed)
3451 {
3452 self.finish_generation(&mut mtp, &mut router, true);
3457 return Err("GPU token graph failed during decode".to_string());
3458 }
3459 if self
3460 .cancel
3461 .swap(false, std::sync::atomic::Ordering::Relaxed)
3462 {
3463 finish_reason = "cancelled".to_string();
3464 break 'decode;
3465 }
3466 let forced = self.spec_forced.take();
3471 let mut logits = match (forced, self.graph_logits.take()) {
3472 (Some(_), _) => Vec::new(),
3473 (None, Some(lg)) => lg,
3474 (None, None) => {
3475 inference::rms_norm_into(
3476 &hidden,
3477 &self.weights.final_norm,
3478 self.rms_eps,
3479 self.norm_style,
3480 &mut self.ws.n1,
3481 );
3482 self.lm_head_forward(&self.ws.n1)
3483 }
3484 };
3485 if generated
3488 == std::env::var("CMF_LOGIT_DUMP_STEP")
3489 .ok()
3490 .and_then(|v| v.parse().ok())
3491 .unwrap_or(0)
3492 {
3493 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3494 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3495 for v in hidden.iter().chain(logits.iter()) {
3496 bytes.extend_from_slice(&v.to_le_bytes());
3497 }
3498 if let Err(e) = std::fs::write(&path, &bytes) {
3499 eprintln!("logit dump: failed to write {path}: {e}");
3500 self.finish_generation(&mut mtp, &mut router, true);
3501 return Err(format!("logit dump write failed: {e}"));
3502 }
3503 }
3504 }
3505 let t_next = match forced {
3506 Some(c) => c,
3507 None => sampler::sample_with_scratch_pool(
3508 &logits,
3509 &self.sampler_config,
3510 &all_ids,
3511 &mut self.rng,
3512 &mut self.sampler_scratch,
3513 self.pool.as_deref(),
3514 ),
3515 };
3516 if self.confidence_on {
3517 confidence.push(if logits.is_empty() {
3518 0.0
3519 } else {
3520 sampler::top1_prob_pool(
3521 self.pool.as_deref(),
3522 &mut self.sampler_scratch,
3523 &logits,
3524 t_next,
3525 calib_temp,
3526 )
3527 });
3528 }
3529 if !logits.is_empty() {
3530 attention::recycle_buf(&mut logits);
3531 }
3532 if trace_on {
3533 let skill = router.as_ref().and_then(|r| r.active_id());
3537 traces.push(TokenTrace {
3538 t: generated,
3539 token_id: t_next,
3540 confidence: confidence.last().copied().unwrap_or(0.0),
3541 active_skill: skill,
3542 recon: None,
3543 switched: false,
3544 });
3545 }
3546 if !commit!(t_next) {
3547 break 'decode;
3548 }
3549 if generated >= max_tokens {
3550 break 'decode;
3551 }
3552
3553 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3554 static SAID: std::sync::Once = std::sync::Once::new();
3560 SAID.call_once(|| {
3561 tracing::warn!(
3562 "KV cache full at {} positions — evicting half; quality \
3563 will degrade. Raise CMF_MAX_SEQ.",
3564 self.kv_cache.max_seq_len,
3565 );
3566 });
3567 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3568 self.kv_cache.evict(keep);
3569 }
3570
3571 if graph_spec {
3574 match spec_trial {
3575 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3576 spec_mon.plain_ms =
3577 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3578 let keep = spec_mon.pays();
3579 tracing::info!(
3580 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3581 spec_mon.tokens,
3582 spec_mon.round_ms,
3583 spec_mon.plain_ms,
3584 if keep { "speculating" } else { "plain" }
3585 );
3586 spec_mon.fails = 0;
3587 spec_trial = SpecTrial::Decided {
3588 spec: keep,
3589 recheck_at: if keep { usize::MAX } else { generated + 128 },
3590 };
3591 }
3592 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3593 spec_mon.n = 0;
3594 spec_trial = SpecTrial::Spec {
3595 t0: std::time::Instant::now(),
3596 gen0: generated,
3597 rounds: 0,
3598 };
3599 }
3600 _ => {}
3601 }
3602 spec_watchdog_off = matches!(
3603 spec_trial,
3604 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3605 );
3606 }
3607 match &mut mtp {
3608 #[cfg(feature = "gpu")]
3610 Some(m)
3611 if graph_spec
3612 && !spec_watchdog_off
3613 && generated + 1 < max_tokens
3614 && next_pos > 0 =>
3615 {
3616 let t_round = std::time::Instant::now();
3617 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3618 m,
3619 &hidden,
3620 t_next,
3621 next_pos,
3622 &mut drafted,
3623 &mut accepted,
3624 &mut all_ids,
3625 ) {
3626 next_pos = n_pos;
3627 hidden = new_h;
3628 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3629 eprintln!(
3630 "spec-round wall {:.1} ms → {} tokens",
3631 t_round.elapsed().as_secs_f64() * 1e3,
3632 extra.len() + 1
3633 );
3634 }
3635 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3639 spec_trial = Self::spec_trial_round(
3642 spec_trial,
3643 &mut spec_mon,
3644 generated + extra.len() + 1,
3645 );
3646 let mut stopped = false;
3647 for &id in &extra {
3648 if self.confidence_on {
3649 confidence.push(0.0);
3650 }
3651 if !commit!(id) {
3652 stopped = true;
3653 break;
3654 }
3655 }
3656 if stopped {
3657 break 'decode;
3658 }
3659 continue 'decode;
3660 }
3661 if self
3662 .graph_failed
3663 .swap(false, std::sync::atomic::Ordering::Relaxed)
3664 {
3665 self.finish_generation(&mut mtp, &mut router, true);
3671 return Err("GPU MTP graph failed during speculative decode".to_string());
3672 }
3673 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3684 spec_mon.tokens = 0.0;
3685 spec_mon.fails = 3;
3686 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3687 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3688 next_pos += 1;
3689 continue 'decode;
3690 }
3691 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3693 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3694 drafted += 1;
3695 let emb1 = self.embed_single(t_next);
3696 let emb2 = self.embed_single(draft);
3697 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3698
3699 inference::rms_norm_into(
3700 &h1,
3701 &self.weights.final_norm,
3702 self.rms_eps,
3703 self.norm_style,
3704 &mut self.ws.n1,
3705 );
3706 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3707 let t_after = sampler::sample_with_scratch_pool(
3708 &logits1,
3709 &self.sampler_config,
3710 &all_ids,
3711 &mut self.rng,
3712 &mut self.sampler_scratch,
3713 self.pool.as_deref(),
3714 );
3715 if self.confidence_on {
3716 confidence.push(sampler::top1_prob_pool(
3717 self.pool.as_deref(),
3718 &mut self.sampler_scratch,
3719 &logits1,
3720 t_after,
3721 calib_temp,
3722 ));
3723 }
3724 attention::recycle_buf(&mut logits1);
3725 if trace_on {
3726 traces.push(TokenTrace {
3729 t: generated,
3730 token_id: t_after,
3731 confidence: confidence.last().copied().unwrap_or(0.0),
3732 active_skill: None,
3733 recon: None,
3734 switched: false,
3735 });
3736 }
3737 let stop = !commit!(t_after);
3738
3739 if t_after == draft {
3740 accepted += 1;
3741 self.commit_linear_scratch();
3742 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3743 hidden = h2;
3744 next_pos += 2;
3745 } else {
3746 for layer in &mut self.kv_cache.layers {
3748 layer.truncate_last(1);
3749 }
3750 if !stop {
3751 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3752 hidden = self.forward_layers(
3753 &self.embed_single(t_after),
3754 next_pos + 1,
3755 None,
3756 );
3757 }
3758 next_pos += 2;
3759 }
3760 if stop {
3761 break 'decode;
3762 }
3763 }
3764 _ => {
3766 #[cfg(feature = "gpu")]
3771 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3772 static SAID: std::sync::Once = std::sync::Once::new();
3773 SAID.call_once(|| {
3774 eprintln!(
3775 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3776 !self.dsv4_mtp.is_empty(),
3777 task_mask.is_none(),
3778 router.is_none(),
3779 !trace_on,
3780 self.sampler_config.temperature < 1e-6,
3781 self.sampler_config.repetition_penalty == 1.0,
3782 );
3783 });
3784 }
3785 #[cfg(feature = "gpu")]
3786 if Self::dsv4_spec_on()
3787 && self.dsv4.is_some()
3788 && !self.dsv4_mtp.is_empty()
3789 && task_mask.is_none()
3790 && router.is_none()
3791 && !trace_on
3792 && self.sampler_config.temperature < 1e-6
3793 && self.sampler_config.repetition_penalty == 1.0
3794 && generated + 1 < max_tokens
3795 && all_ids.len() >= 2
3796 && generated >= dsv4_spec_retry_at
3797 {
3798 let tip_token = all_ids[all_ids.len() - 2];
3799 let drafted0 = drafted;
3800 let round = self.dsv4_spec_step(
3801 tip_token,
3802 t_next,
3803 next_pos,
3804 max_tokens.saturating_sub(generated),
3805 &mut drafted,
3806 &mut accepted,
3807 );
3808 if drafted > drafted0 {
3809 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3810 if useful {
3811 dsv4_spec_bad = 0;
3812 } else {
3813 dsv4_spec_bad += 1;
3814 if dsv4_spec_bad >= 2 {
3815 dsv4_spec_bad = 0;
3816 dsv4_spec_retry_at = generated.saturating_add(32);
3817 tracing::info!(
3818 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3819 );
3820 }
3821 }
3822 }
3823 if let Some((extra, n_pos)) = round {
3824 next_pos = n_pos;
3825 let mut stopped = false;
3826 for &id in &extra {
3827 if self.confidence_on {
3828 confidence.push(0.0);
3829 }
3830 if !commit!(id) {
3831 stopped = true;
3832 break;
3833 }
3834 }
3835 if stopped {
3836 break 'decode;
3837 }
3838 continue 'decode;
3839 }
3840 }
3841 self.graph_want_logits = fuse_lm;
3842 let mut t_fwd = t_next;
3848 let pure_greedy = self.sampler_config.temperature < 1e-6
3849 && self.sampler_config.repetition_penalty == 1.0
3850 && self.sampler_config.suppress_tokens.is_empty();
3851 let burst_k = std::env::var("CMF_MULTISTEP")
3856 .ok()
3857 .and_then(|v| v.parse::<usize>().ok())
3858 .unwrap_or(0);
3859 if pure_greedy
3860 && burst_k >= 1
3861 && fuse_lm
3862 && task_mask.is_none()
3863 && router.is_none()
3864 && !trace_on
3865 && !self.confidence_on
3866 {
3867 let mut stopped = false;
3868 loop {
3869 let room = max_tokens.saturating_sub(generated);
3870 if room <= 2 {
3871 break;
3872 }
3873 let k = burst_k.min(room - 1);
3874 if k < 1 {
3875 break;
3876 }
3877 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3878 if self
3879 .graph_failed
3880 .swap(false, std::sync::atomic::Ordering::Relaxed)
3881 {
3882 self.finish_generation(&mut mtp, &mut router, true);
3883 return Err(
3884 "GPU token graph failed during greedy burst".to_string()
3885 );
3886 }
3887 break;
3888 };
3889 next_pos += k;
3890 for &id in &ids {
3891 if !commit!(id) {
3892 stopped = true;
3893 break;
3894 }
3895 }
3896 if stopped {
3897 break;
3898 }
3899 t_fwd = *ids.last().unwrap();
3900 }
3901 if stopped {
3902 break 'decode;
3903 }
3904 }
3905 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3906 next_pos += 1;
3907 if let Some(r) = &mut router {
3910 let phi = self.dyn_phi_ema.clone();
3911 let decision = r.step(&phi, generated);
3912 if let Some(new_active) = decision {
3913 let _ = self.set_active_skill(new_active);
3914 }
3915 if trace_on {
3918 if let Some(last) = traces.last_mut() {
3919 let e = r.last_best_e();
3920 last.recon = e.is_finite().then_some(e);
3921 last.switched = decision.is_some();
3922 }
3923 }
3924 }
3925 }
3926 }
3927 }
3928
3929 let cancelled = finish_reason == "cancelled";
3930 self.finish_generation(&mut mtp, &mut router, cancelled);
3931
3932 let output_ids = &all_ids[input_ids.len()..];
3933 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3937 if cancelled {
3938 self.kv_history.clear();
3939 } else {
3940 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3941 }
3942 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3944 Ok(GenerateResult {
3945 text: self.tokenizer.decode(output_ids),
3946 token_ids: output_ids.to_vec(),
3947 prompt_tokens: input_ids.len(),
3948 tokens_generated: generated,
3949 finish_reason,
3950 mtp_drafted: drafted,
3951 mtp_accepted: accepted,
3952 token_confidence: confidence,
3953 traces,
3954 })
3955 }
3956
3957 fn mtp_step(
3961 &mut self,
3962 m: &mut MtpModule,
3963 hidden: &[f32],
3964 next_token: u32,
3965 position: usize,
3966 ) -> u32 {
3967 self.mtp_step_h(m, hidden, next_token, position).0
3968 }
3969
3970 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3974 use std::sync::Mutex;
3975 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3976 let mut t = T.lock().unwrap();
3977 if t.len() <= depth {
3978 t.resize(depth + 1, (0, 0));
3979 }
3980 t[depth].0 += 1;
3981 t[depth].1 += prefix_ok as u64;
3982 if depth == 0 && t[0].0 % 128 == 0 {
3983 let line: Vec<String> = t
3984 .iter()
3985 .enumerate()
3986 .map(|(d, (n, k))| {
3987 format!(
3988 "d{}={:.0}%({n})",
3989 d + 1,
3990 100.0 * *k as f64 / (*n).max(1) as f64
3991 )
3992 })
3993 .collect();
3994 eprintln!("mtp-chain: {}", line.join(" "));
3995 }
3996 }
3997
3998 fn mtp_step_hl(
4006 &mut self,
4007 m: &mut MtpModule,
4008 hidden: &[f32],
4009 next_token: u32,
4010 position: usize,
4011 ) -> (Vec<f32>, Vec<f32>) {
4012 #[cfg(target_os = "macos")]
4017 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
4018 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4019 self.mtp_graph_mode = Some(true);
4020 return r;
4021 }
4022 if self.mtp_graph_mode == Some(true) {
4023 tracing::error!("mtp Metal graph failed after admission");
4024 self.clear_sequence_state();
4025 self.graph_failed
4026 .store(true, std::sync::atomic::Ordering::Relaxed);
4027 self.cancel
4028 .store(true, std::sync::atomic::Ordering::Relaxed);
4029 return (Vec::new(), Vec::new());
4030 }
4031 self.mtp_graph_mode = Some(false);
4032 }
4033 #[cfg(feature = "gpu")]
4034 if self.mtp_graph_mode != Some(false) {
4035 if !self.mtp_graph_ok(m) {
4036 if self.mtp_graph_mode == Some(true) {
4037 tracing::error!("mtp graph became unavailable after admission");
4042 self.clear_sequence_state();
4043 self.graph_failed
4044 .store(true, std::sync::atomic::Ordering::Relaxed);
4045 self.cancel
4046 .store(true, std::sync::atomic::Ordering::Relaxed);
4047 return (Vec::new(), Vec::new());
4048 }
4049 self.mtp_graph_mode = Some(false);
4050 } else {
4051 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4052 self.mtp_graph_mode = Some(true);
4053 return r;
4054 }
4055 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4056 return (Vec::new(), Vec::new());
4063 }
4064 tracing::error!("mtp graph failed or declined 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 }
4076 let e = self.embed_single(next_token);
4080 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4081 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4082 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4083 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4084 let mut x = vec![0.0f32; self.hidden_size];
4085 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4086
4087 let lw = &m.layer;
4089 inference::rms_norm_into(
4090 &x,
4091 &lw.input_norm,
4092 self.rms_eps,
4093 self.norm_style,
4094 &mut self.ws.n1,
4095 );
4096 let attn = match &lw.attn {
4097 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4099 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4100 AttnKind::Full {
4101 wq,
4102 wk,
4103 wv,
4104 wo,
4105 q_norm,
4106 k_norm,
4107 output_gate,
4108 softplus_gate,
4109 bias,
4110 } => {
4111 let mut cfg = self.attn_cfg(position);
4112 cfg.q_norm = q_norm.as_deref();
4113 cfg.k_norm = k_norm.as_deref();
4114 cfg.output_gate = *output_gate;
4115 cfg.softplus_gate = softplus_gate
4116 .as_ref()
4117 .map(|(gate, per_head)| (gate, *per_head));
4118 cfg.bias = bias
4119 .as_ref()
4120 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4121 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4122 }
4123 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4124 unreachable!("MTP block is full attention")
4125 }
4126 };
4127 for (i, &a) in attn.iter().enumerate() {
4128 x[i] += a;
4129 }
4130 inference::rms_norm_into(
4131 &x,
4132 &lw.post_norm,
4133 self.rms_eps,
4134 self.norm_style,
4135 &mut self.ws.p1,
4136 );
4137 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4138 for (i, &f) in ffn.iter().enumerate() {
4139 x[i] += f;
4140 }
4141
4142 inference::rms_norm_into(
4143 &x,
4144 &m.final_norm,
4145 self.rms_eps,
4146 self.norm_style,
4147 &mut self.ws.n1,
4148 );
4149 let lg = self.lm_head_forward(&self.ws.n1);
4150 (lg, x)
4151 }
4152
4153 fn mtp_step_h(
4155 &mut self,
4156 m: &mut MtpModule,
4157 hidden: &[f32],
4158 next_token: u32,
4159 position: usize,
4160 ) -> (u32, Vec<f32>) {
4161 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4162 let draft = sampler::argmax(&lg);
4163 attention::recycle_buf(&mut lg);
4164 (draft, x)
4165 }
4166
4167 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4173 match trial {
4174 SpecTrial::Spec { t0, gen0, rounds } => {
4175 let rounds = rounds + 1;
4176 if rounds >= 5 {
4177 if mon.plain_ms > 0.0 {
4178 let keep = mon.pays();
4179 mon.fails = 0;
4180 tracing::info!(
4181 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4182 mon.tokens,
4183 mon.round_ms,
4184 mon.plain_ms,
4185 if keep { "speculating" } else { "plain" }
4186 );
4187 SpecTrial::Decided {
4188 spec: keep,
4189 recheck_at: if keep { usize::MAX } else { generated + 128 },
4190 }
4191 } else {
4192 SpecTrial::Plain {
4193 t0: std::time::Instant::now(),
4194 gen0: generated,
4195 }
4196 }
4197 } else {
4198 SpecTrial::Spec { t0, gen0, rounds }
4199 }
4200 }
4201 SpecTrial::Decided { spec: true, .. } => {
4202 if mon.pays() {
4203 mon.fails = 0;
4204 trial
4205 } else {
4206 mon.fails += 1;
4207 if mon.fails >= 4 {
4208 tracing::info!(
4209 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4210 mon.tokens,
4211 mon.round_ms,
4212 mon.plain_ms
4213 );
4214 SpecTrial::Decided {
4215 spec: false,
4216 recheck_at: generated + 128,
4217 }
4218 } else {
4219 trial
4220 }
4221 }
4222 }
4223 other => other,
4224 }
4225 }
4226
4227 fn mtp_kv_id(&self) -> u64 {
4230 self.graph_kv_id | (1u64 << 40)
4231 }
4232
4233 const MTP_LAYER_BASE: usize = 0;
4238
4239 #[cfg(feature = "gpu")]
4246 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4247 self.mtp_graph_mode != Some(true)
4248 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4249 }
4250
4251 #[cfg(feature = "gpu")]
4257 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4258 let mut ok = true;
4259 let mut expected = false;
4260 for li in 0..self.num_layers {
4261 if matches!(
4262 self.weights.layers[self.phys_layer(li)].attn,
4263 AttnKind::Full { .. }
4264 ) {
4265 expected = true;
4266 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4267 }
4268 }
4269 !expected || ok
4270 }
4271
4272 fn graph_gdn_layer_count(&self) -> usize {
4276 (0..self.num_layers)
4277 .filter(|&li| {
4278 matches!(
4279 &self.weights.layers[self.phys_layer(li)].attn,
4280 AttnKind::LinearGdn(_)
4281 )
4282 })
4283 .count()
4284 }
4285
4286 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4289 let e = self.embed_single(next_token);
4290 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4291 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4292 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4293 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4294 let mut x = vec![0.0f32; self.hidden_size];
4295 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4296 x
4297 }
4298
4299 #[cfg(feature = "gpu")]
4302 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4303 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4304 return false;
4305 }
4306 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4307 || !crate::gpu::enabled_here()
4308 || self.attn_softcap > 0.0
4309 || self.attention_heads_per_layer.is_some()
4310 {
4311 return false;
4312 }
4313 matches!(
4314 &m.layer.attn,
4315 AttnKind::Full {
4316 softplus_gate: None,
4317 ..
4318 }
4319 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4320 }
4321
4322 #[cfg(feature = "gpu")]
4326 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4327 if !self.mtp_block_graph_ok(m) {
4328 return false;
4329 }
4330 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4331 return false;
4332 };
4333 let FfnKind::Dense(d) = &m.layer.ffn else {
4334 return false;
4335 };
4336 d.segs.is_empty()
4337 && wq.graph_weight().is_some()
4338 && wk.graph_weight().is_some()
4339 && wv.graph_weight().is_some()
4340 && wo.graph_weight().is_some()
4341 && d.gate_proj.graph_weight().is_some()
4342 && d.up_proj.graph_weight().is_some()
4343 && d.down_proj.graph_weight().is_some()
4344 && self.weights.lm_head.graph_weight().is_some()
4345 }
4346
4347 #[cfg(feature = "gpu")]
4353 fn mtp_step_graph(
4354 &mut self,
4355 m: &mut MtpModule,
4356 hidden: &[f32],
4357 next_token: u32,
4358 position: usize,
4359 ) -> Option<(Vec<f32>, Vec<f32>)> {
4360 if !self.mtp_graph_ok(m) {
4361 return None;
4362 }
4363 let lw = &m.layer;
4364 let AttnKind::Full {
4365 wq,
4366 wk,
4367 wv,
4368 wo,
4369 q_norm,
4370 k_norm,
4371 output_gate,
4372 softplus_gate,
4373 bias,
4374 } = &lw.attn
4375 else {
4376 return None;
4377 };
4378 if softplus_gate.is_some() {
4379 return None;
4380 }
4381 let FfnKind::Dense(d) = &lw.ffn else {
4382 return None;
4383 };
4384 if !d.segs.is_empty() {
4385 return None; }
4387 let mut x = self.mtp_block_input(m, hidden, next_token);
4390 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4391 let (_, i, kind, rs) = t.graph_weight()?;
4392 Some(crate::gpu::GraphW {
4393 idx: i,
4394 kind,
4395 row_scale: rs,
4396 data: &[],
4397 prism: crate::gpu::GraphPrismOp::None,
4398 affine: false,
4399 })
4400 }
4401 let (model, _, _, _) = wq.graph_weight()?;
4402 let model = model.clone();
4403 let (lm_gw, lm_rows) = {
4404 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4405 (
4406 crate::gpu::GraphW {
4407 idx: i,
4408 kind,
4409 row_scale: rs,
4410 data: &[],
4411 prism: crate::gpu::GraphPrismOp::None,
4412 affine: false,
4413 },
4414 self.weights.lm_head.rows(),
4415 )
4416 };
4417 let layer = crate::gpu::GraphLayer {
4418 input_norm: &lw.input_norm,
4419 attn: crate::gpu::GraphAttn::Full {
4420 wq: gw(wq)?,
4421 wk: gw(wk)?,
4422 wv: gw(wv)?,
4423 wo: gw(wo)?,
4424 q_norm: q_norm.as_deref(),
4425 k_norm: k_norm.as_deref(),
4426 late_qk_norm: self.qk_norm_after_rope,
4427 bias: bias
4428 .as_ref()
4429 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4430 output_gate: *output_gate,
4431 cpu_k: m.kv.k_heads(),
4432 cpu_v: m.kv.v_heads(),
4433 },
4434 post_norm: &lw.post_norm,
4435 ffn: crate::gpu::GraphFfn::Dense {
4436 gate: gw(&d.gate_proj)?,
4437 up: gw(&d.up_proj)?,
4438 down: gw(&d.down_proj)?,
4439 },
4440 };
4441 let nh = self.num_heads;
4442 let (nkv, hd, rd) = self.layer_geom(0);
4443 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4444 let mut logits = Vec::new();
4445 let ok = crate::gpu::forward_token_graph(
4446 &model,
4447 self.mtp_kv_id(),
4448 std::slice::from_ref(&layer),
4449 &[None],
4450 self.o1_epoch,
4451 &self.inv_freq,
4452 &mut x,
4453 nh,
4454 nkv,
4455 hd,
4456 self.attn_scale,
4457 rd,
4458 self.hidden_size,
4459 self.intermediate_size,
4460 position,
4461 self.kv_cache.max_seq_len,
4462 gemma,
4463 self.rms_eps as f32,
4464 Some((&lm_gw, lm_rows)),
4465 &m.final_norm,
4466 &mut logits,
4467 &[],
4468 1,
4469 None,
4470 None,
4471 None,
4472 Self::MTP_LAYER_BASE,
4473 true,
4474 );
4475 match ok {
4476 crate::gpu::TokenGraphOutcome::Completed => {}
4477 crate::gpu::TokenGraphOutcome::Declined => return None,
4478 crate::gpu::TokenGraphOutcome::Failed => {
4479 self.clear_sequence_state();
4483 self.graph_failed
4484 .store(true, std::sync::atomic::Ordering::Relaxed);
4485 self.cancel
4486 .store(true, std::sync::atomic::Ordering::Relaxed);
4487 return None;
4488 }
4489 }
4490 logits.resize(self.vocab_size, 0.0);
4491 Some((logits, x))
4492 }
4493
4494 #[cfg(feature = "gpu")]
4502 fn mtp_warm_graph(
4503 &mut self,
4504 m: &mut MtpModule,
4505 pairs: &[(&[f32], u32)],
4506 first_pos: usize,
4507 ) -> crate::gpu::BatchGraphOutcome {
4508 if pairs.is_empty() {
4509 return crate::gpu::BatchGraphOutcome::Completed;
4510 }
4511 if !self.mtp_block_graph_ok(m) {
4512 return crate::gpu::BatchGraphOutcome::Declined;
4513 }
4514 let hs = self.hidden_size;
4515 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4518 for (h, t) in pairs {
4519 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4520 }
4521 let lw = &m.layer;
4522 let AttnKind::Full {
4523 wq,
4524 wk,
4525 wv,
4526 wo,
4527 q_norm,
4528 k_norm,
4529 output_gate,
4530 bias,
4531 ..
4532 } = &lw.attn
4533 else {
4534 return crate::gpu::BatchGraphOutcome::Declined;
4535 };
4536 let FfnKind::Dense(d) = &lw.ffn else {
4537 return crate::gpu::BatchGraphOutcome::Declined;
4538 };
4539 if !d.segs.is_empty() {
4540 return crate::gpu::BatchGraphOutcome::Declined; }
4542 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4543 let (_, i, kind, rs) = t.graph_weight()?;
4544 Some(crate::gpu::GraphW {
4545 idx: i,
4546 kind,
4547 row_scale: rs,
4548 data: &[],
4549 prism: crate::gpu::GraphPrismOp::None,
4550 affine: false,
4551 })
4552 }
4553 let Some((model, _, _, _)) = wq.graph_weight() else {
4554 return crate::gpu::BatchGraphOutcome::Declined;
4555 };
4556 let model = model.clone();
4557 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4558 gw(wq),
4559 gw(wk),
4560 gw(wv),
4561 gw(wo),
4562 gw(&d.gate_proj),
4563 gw(&d.up_proj),
4564 gw(&d.down_proj),
4565 ) else {
4566 return crate::gpu::BatchGraphOutcome::Declined;
4567 };
4568 let layer = crate::gpu::GraphLayer {
4569 input_norm: &lw.input_norm,
4570 attn: crate::gpu::GraphAttn::Full {
4571 wq: gwq,
4572 wk: gwk,
4573 wv: gwv,
4574 wo: gwo,
4575 q_norm: q_norm.as_deref(),
4576 k_norm: k_norm.as_deref(),
4577 late_qk_norm: self.qk_norm_after_rope,
4578 bias: bias
4579 .as_ref()
4580 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4581 output_gate: *output_gate,
4582 cpu_k: m.kv.k_heads(),
4583 cpu_v: m.kv.v_heads(),
4584 },
4585 post_norm: &lw.post_norm,
4586 ffn: crate::gpu::GraphFfn::Dense {
4587 gate: gg,
4588 up: gu,
4589 down: gd,
4590 },
4591 };
4592 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4593 let nh = self.num_heads;
4594 let (nkv, hd, rd) = self.layer_geom(0);
4595 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4596 crate::gpu::forward_batch_graph(
4597 &model,
4598 self.mtp_kv_id(),
4599 std::slice::from_ref(&layer),
4600 &self.inv_freq,
4601 &mut hiddens,
4602 nh,
4603 nkv,
4604 hd,
4605 rd,
4606 hs,
4607 self.intermediate_size,
4608 &positions,
4609 self.kv_cache.max_seq_len,
4610 gemma,
4611 self.rms_eps as f32,
4612 self.attn_scale,
4613 pairs.len(),
4614 &[],
4615 0,
4616 None,
4617 )
4618 }
4619
4620 #[cfg(feature = "gpu")]
4627 fn mtp_warm_graph_fallback(
4628 &mut self,
4629 m: &mut MtpModule,
4630 pairs: &[(&[f32], u32)],
4631 first_pos: usize,
4632 ) -> bool {
4633 if pairs.is_empty() {
4634 return true;
4635 }
4636 let graphable = self.mtp_block_graph_ok(m);
4637 if !graphable {
4638 if self.mtp_graph_mode == Some(true) {
4642 return false;
4643 }
4644 self.mtp_graph_mode = Some(false);
4645 for (j, (h, t)) in pairs.iter().enumerate() {
4646 self.mtp_warm(m, h, *t, first_pos + j);
4647 }
4648 return true;
4649 }
4650
4651 for (j, (h, t)) in pairs.iter().enumerate() {
4656 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4657 return false;
4658 }
4659 }
4660 self.mtp_graph_mode = Some(true);
4661 true
4662 }
4663
4664 #[cfg(feature = "gpu")]
4669 fn mtp_warm_prefill_pairs(
4670 &mut self,
4671 m: &mut MtpModule,
4672 pairs: &[(&[f32], u32)],
4673 first_pos: usize,
4674 ) -> Result<(), &'static str> {
4675 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4680 if self.mtp_graph_mode == Some(true) {
4681 return Err("MTP token graph became unavailable after admission");
4682 }
4683 self.mtp_graph_mode = Some(false);
4684 for (j, (h, t)) in pairs.iter().enumerate() {
4685 self.mtp_warm(m, h, *t, first_pos + j);
4686 }
4687 return Ok(());
4688 }
4689 match self.mtp_warm_graph(m, pairs, first_pos) {
4690 crate::gpu::BatchGraphOutcome::Completed => {
4691 if !pairs.is_empty() {
4692 self.mtp_graph_mode = Some(true);
4693 }
4694 Ok(())
4695 }
4696 crate::gpu::BatchGraphOutcome::Declined => {
4697 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4698 Ok(())
4699 } else {
4700 Err("MTP warm-up fallback failed after device admission")
4701 }
4702 }
4703 crate::gpu::BatchGraphOutcome::Failed => {
4704 Err("MTP warm batch graph failed after admission")
4705 }
4706 }
4707 }
4708
4709 #[cfg(not(feature = "gpu"))]
4710 fn mtp_warm_prefill_pairs(
4711 &mut self,
4712 m: &mut MtpModule,
4713 pairs: &[(&[f32], u32)],
4714 first_pos: usize,
4715 ) -> Result<(), &'static str> {
4716 for (j, (h, t)) in pairs.iter().enumerate() {
4717 self.mtp_warm(m, h, *t, first_pos + j);
4718 }
4719 Ok(())
4720 }
4721
4722 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4726 let e = self.embed_single(next_token);
4727 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4728 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4729 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4730 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4731 let mut x = vec![0.0f32; self.hidden_size];
4732 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4733 inference::rms_norm_into(
4734 &x,
4735 &m.layer.input_norm,
4736 self.rms_eps,
4737 self.norm_style,
4738 &mut self.ws.n1,
4739 );
4740 let attn = match &m.layer.attn {
4741 AttnKind::Full {
4742 wq,
4743 wk,
4744 wv,
4745 wo,
4746 q_norm,
4747 k_norm,
4748 output_gate,
4749 softplus_gate,
4750 bias,
4751 } => {
4752 let mut cfg = self.attn_cfg(position);
4753 cfg.q_norm = q_norm.as_deref();
4754 cfg.k_norm = k_norm.as_deref();
4755 cfg.output_gate = *output_gate;
4756 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4757 cfg.bias = bias
4758 .as_ref()
4759 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4760 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4761 }
4762 _ => return,
4763 };
4764 let _ = attn;
4765 }
4766
4767 #[cfg(feature = "gpu")]
4774 #[allow(clippy::too_many_arguments)]
4775 fn graph_spec_step(
4776 &mut self,
4777 m: &mut MtpModule,
4778 hidden: &[f32],
4779 t_next: u32,
4780 next_pos: usize,
4781 drafted: &mut usize,
4782 accepted: &mut usize,
4783 all_ids: &mut Vec<u32>,
4787 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4788 #[cfg(target_os = "macos")]
4799 let metal_native = crate::gpu::q1_force();
4800 #[cfg(not(target_os = "macos"))]
4801 let metal_native = false;
4802 #[cfg(feature = "gpu")]
4803 let k_default = if metal_native {
4804 7
4807 } else if crate::gpu_wgpu::verify_i8_on() {
4808 5
4809 } else {
4810 4
4811 };
4812 #[cfg(not(feature = "gpu"))]
4813 let k_default = 4;
4814 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4815 .ok()
4816 .and_then(|v| v.parse().ok())
4817 .filter(|&v| (1..=8).contains(&v))
4818 .unwrap_or(k_default);
4819 if next_pos == 0 {
4820 return None;
4821 }
4822 let t_round = std::time::Instant::now();
4823 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4839 let sub0 = subs();
4840 let cfg = self.sampler_config.clone();
4845 let penalized = !(cfg.repetition_penalty == 1.0
4846 && cfg.presence_penalty == 0.0
4847 && cfg.suppress_tokens.is_empty());
4848 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4853 let sampling = cfg.temperature >= 1e-6;
4854 let sparse = sampling && sampler::sparse_ok(&cfg);
4860 let base_len = all_ids.len();
4861 if sampling && !sparse && self.spec_q.len() < k_spec {
4862 self.spec_q.resize_with(k_spec, Vec::new);
4863 }
4864 if sparse && self.spec_qs.len() < k_spec {
4865 self.spec_qs.resize_with(k_spec, Vec::new);
4866 }
4867 let mut drafts = Vec::with_capacity(k_spec);
4872 let mut hx = hidden.to_vec();
4873 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4876 for j in 0..k_spec {
4877 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4878 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4879 if spec_dbg {
4880 let saved = self.mtp_graph_mode;
4881 self.mtp_graph_mode = Some(false);
4882 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4883 self.mtp_graph_mode = saved;
4884 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4885 return None;
4886 }
4887 m.kv.truncate_last(1);
4888 dbg_ref = Some(r);
4889 }
4890 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4891 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4892 return None;
4893 }
4894 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4895 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4896 let dl = lg
4897 .iter()
4898 .zip(&lg_cpu)
4899 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4900 let dh = hj
4901 .iter()
4902 .zip(&h_cpu)
4903 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4904 eprintln!(
4905 "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 {}",
4906 next_pos - 1 + j,
4907 sampler::argmax(&lg_cpu),
4908 sampler::argmax(&lg),
4909 n(&h_cpu),
4910 n(&hj),
4911 m.kv.seq_len
4912 );
4913 }
4914 let dj = if sparse {
4915 let mut q = std::mem::take(&mut self.spec_qs[j]);
4916 let ok = sampler::sparse_distribution_into(
4917 &lg,
4918 &cfg,
4919 all_ids,
4920 &mut self.sampler_scratch,
4921 self.pool.as_deref(),
4922 &mut q,
4923 );
4924 let d = if ok {
4925 sampler::draw_sparse(&q, &mut self.rng)
4926 } else {
4927 let t = sampler::argmax(&lg);
4929 q.clear();
4930 q.push((t, 1.0));
4931 t
4932 };
4933 self.spec_qs[j] = q;
4934 all_ids.push(d);
4935 d
4936 } else if sampling {
4937 let mut q = std::mem::take(&mut self.spec_q[j]);
4938 sampler::distribution_into(
4939 &lg,
4940 &cfg,
4941 all_ids,
4942 &mut self.sampler_scratch,
4943 self.pool.as_deref(),
4944 &mut q,
4945 );
4946 let d = sampler::draw(&q, &mut self.rng);
4947 self.spec_q[j] = q;
4948 all_ids.push(d); d
4950 } else if greedy_pen {
4951 let d = sampler::argmax_penalized(
4952 &lg,
4953 &cfg,
4954 all_ids,
4955 &mut self.sampler_scratch,
4956 self.pool.as_deref(),
4957 );
4958 all_ids.push(d);
4959 d
4960 } else {
4961 sampler::argmax(&lg)
4962 };
4963 attention::recycle_buf(&mut lg);
4964 drafts.push(dj);
4965 hx = hj;
4966 }
4967 all_ids.truncate(base_len);
4968 *drafted += k_spec;
4969 let t_draft = t_round.elapsed();
4970 let sub_draft = subs();
4971 let b = k_spec + 1;
4974 let mut hiddens = vec![0.0f32; b * self.hidden_size];
4975 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4976 let e = self.embed_single(t);
4977 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4978 }
4979 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4980 let (lm_gw, lm_rows) = {
4981 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4982 (
4983 crate::gpu::GraphW {
4984 idx: i,
4985 kind,
4986 row_scale: rs,
4987 data: &[],
4988 prism: crate::gpu::GraphPrismOp::None,
4989 affine: false,
4990 },
4991 self.weights.lm_head.rows(),
4992 )
4993 };
4994 let mut logits = Vec::new();
4995 let final_norm = self.weights.final_norm.clone();
4996 #[cfg(target_os = "macos")]
4997 let verify_outcome = if metal_native {
4998 let lm = self.weights.lm_head.q1_parts()?;
4999 self.try_batch_graph_metal(
5000 &mut hiddens,
5001 &positions,
5002 b,
5003 Some((lm, &final_norm, &mut logits)),
5004 )
5005 } else {
5006 self.try_batch_graph_wgpu(
5007 &mut hiddens,
5008 &positions,
5009 b,
5010 Some(crate::gpu::SpecTail {
5011 lm: lm_gw,
5012 lm_rows,
5013 final_norm: &final_norm,
5014 logits_out: &mut logits,
5015 }),
5016 )
5017 };
5018 #[cfg(not(target_os = "macos"))]
5019 let verify_outcome = self.try_batch_graph_wgpu(
5020 &mut hiddens,
5021 &positions,
5022 b,
5023 Some(crate::gpu::SpecTail {
5024 lm: lm_gw,
5025 lm_rows,
5026 final_norm: &final_norm,
5027 logits_out: &mut logits,
5028 }),
5029 );
5030 match verify_outcome {
5031 crate::gpu::BatchGraphOutcome::Completed => {}
5032 crate::gpu::BatchGraphOutcome::Declined => {
5033 m.kv.truncate_last(k_spec);
5037 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5038 self.clear_sequence_state();
5039 self.graph_failed
5040 .store(true, std::sync::atomic::Ordering::Relaxed);
5041 self.cancel
5042 .store(true, std::sync::atomic::Ordering::Relaxed);
5043 tracing::error!("MTP graph mirror rewind failed after verify decline");
5044 }
5045 return None;
5046 }
5047 crate::gpu::BatchGraphOutcome::Failed => {
5048 self.clear_sequence_state();
5052 self.graph_failed
5053 .store(true, std::sync::atomic::Ordering::Relaxed);
5054 self.cancel
5055 .store(true, std::sync::atomic::Ordering::Relaxed);
5056 tracing::error!("MTP verify batch graph failed after admission");
5057 return None;
5058 }
5059 }
5060 #[cfg(target_os = "macos")]
5066 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5067 let snap: Vec<Vec<f32>> = self
5068 .kv_cache
5069 .layers
5070 .iter()
5071 .map(|l| l.linear_state.clone())
5072 .collect();
5073 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5074 let toks: Vec<u32> = std::iter::once(t_next)
5075 .chain(drafts.iter().copied())
5076 .collect();
5077 let want_save = self.graph_want_logits;
5078 self.graph_want_logits = false;
5079 for (i, &t) in toks.iter().enumerate() {
5080 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5081 let _ = self.graph_logits.take();
5082 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5086 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5087 }
5088 let ref_lg = self.logits_from_hidden(&hi);
5089 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5090 let ra = sampler::argmax(&ref_lg);
5091 let va = sampler::argmax(row);
5092 let mut md = 0f32;
5093 let mut rms = 0f64;
5094 for j in 0..lm_rows.min(ref_lg.len()) {
5095 let d = (ref_lg[j] - row[j]).abs();
5096 md = md.max(d);
5097 rms += (d as f64) * (d as f64);
5098 }
5099 let mut hd = 0f32;
5100 for j in 0..self.hidden_size {
5101 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5102 }
5103 eprintln!(
5104 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5105 next_pos + i,
5106 if ra == va { "OK" } else { "MISMATCH" },
5107 (rms / lm_rows as f64).sqrt()
5108 );
5109 }
5110 self.graph_want_logits = want_save;
5111 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5114 if l.linear_state.len() == st.len() {
5115 l.linear_state.copy_from_slice(&st);
5116 } else {
5117 l.linear_state = st;
5118 }
5119 }
5120 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5121 let extra = l.seq_len.saturating_sub(n0);
5122 if extra > 0 {
5123 l.truncate_last(extra);
5124 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5125 }
5126 }
5127 }
5128 let t_verify = t_round.elapsed();
5129 let sub_verify = subs();
5130 let mut a = 0usize;
5135 let mut forced: Option<u32> = None;
5136 let ids: Vec<u32> = if sparse {
5137 let mut p = std::mem::take(&mut self.spec_ps);
5138 let mut res = std::mem::take(&mut self.spec_ress);
5139 while a < k_spec {
5140 let ok = sampler::sparse_distribution_into(
5141 &logits[a * lm_rows..(a + 1) * lm_rows],
5142 &cfg,
5143 all_ids,
5144 &mut self.sampler_scratch,
5145 self.pool.as_deref(),
5146 &mut p,
5147 );
5148 if !ok {
5149 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5150 p.clear();
5151 p.push((t, 1.0));
5152 }
5153 match sampler::spec_accept_or_correct_sparse(
5154 &p,
5155 &self.spec_qs[a],
5156 drafts[a],
5157 &mut self.rng,
5158 &mut res,
5159 ) {
5160 None => {
5161 all_ids.push(drafts[a]);
5162 a += 1;
5163 }
5164 Some(c) => {
5165 forced = Some(c);
5166 break;
5167 }
5168 }
5169 }
5170 all_ids.truncate(base_len);
5171 self.spec_ps = p;
5172 self.spec_ress = res;
5173 drafts.clone()
5174 } else if sampling {
5175 let mut p = std::mem::take(&mut self.spec_p);
5176 let mut res = std::mem::take(&mut self.spec_res);
5177 while a < k_spec {
5178 sampler::distribution_into(
5179 &logits[a * lm_rows..(a + 1) * lm_rows],
5180 &cfg,
5181 all_ids,
5182 &mut self.sampler_scratch,
5183 self.pool.as_deref(),
5184 &mut p,
5185 );
5186 match sampler::spec_accept_or_correct(
5187 &p,
5188 &self.spec_q[a],
5189 drafts[a],
5190 &mut self.rng,
5191 &mut res,
5192 self.pool.as_deref(),
5193 ) {
5194 None => {
5195 all_ids.push(drafts[a]);
5196 a += 1;
5197 }
5198 Some(c) => {
5199 forced = Some(c);
5200 break;
5201 }
5202 }
5203 }
5204 all_ids.truncate(base_len);
5205 self.spec_p = p;
5206 self.spec_res = res;
5207 drafts.clone()
5209 } else if greedy_pen {
5210 let mut ids: Vec<u32> = Vec::with_capacity(b);
5214 for i in 0..b {
5215 let t = sampler::argmax_penalized(
5216 &logits[i * lm_rows..(i + 1) * lm_rows],
5217 &cfg,
5218 all_ids,
5219 &mut self.sampler_scratch,
5220 self.pool.as_deref(),
5221 );
5222 ids.push(t);
5223 if i < k_spec && t == drafts[i] {
5224 all_ids.push(t);
5225 } else {
5226 break;
5227 }
5228 }
5229 all_ids.truncate(base_len);
5230 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5231 a += 1;
5232 }
5233 ids
5236 } else {
5237 let ids: Vec<u32> = (0..b)
5238 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5239 .collect();
5240 while a < k_spec && ids[a] == drafts[a] {
5241 a += 1;
5242 }
5243 ids
5244 };
5245 if spec_dbg {
5246 eprintln!(
5247 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5248 drafts, ids
5249 );
5250 }
5251 #[cfg(target_os = "macos")]
5255 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5256 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5257 {
5258 let snap: Vec<Vec<f32>> = self
5259 .kv_cache
5260 .layers
5261 .iter()
5262 .map(|l| l.linear_state.clone())
5263 .collect();
5264 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5265 let toks: Vec<u32> = std::iter::once(t_next)
5266 .chain(drafts.iter().copied())
5267 .collect();
5268 let want_save = self.graph_want_logits;
5269 self.graph_want_logits = false;
5270 for (i, &t) in toks.iter().take(a + 1).enumerate() {
5271 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5272 let _ = self.graph_logits.take();
5273 }
5274 self.graph_want_logits = want_save;
5275 let plain_states: Vec<Vec<f32>> = self
5276 .kv_cache
5277 .layers
5278 .iter()
5279 .map(|l| l.linear_state.clone())
5280 .collect();
5281 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5282 let mut rows = Vec::new();
5283 for (li, (l, n0)) in self
5284 .kv_cache
5285 .layers
5286 .iter_mut()
5287 .zip(attn_lens.iter())
5288 .enumerate()
5289 {
5290 let extra = l.seq_len.saturating_sub(*n0);
5291 if extra > 0 {
5292 let mut kk = Vec::new();
5293 let mut vv = Vec::new();
5294 for g in 0..nkv {
5295 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5296 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5297 }
5298 rows.push((li, kk, vv));
5299 l.truncate_last(extra);
5300 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5301 }
5302 }
5303 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5304 if l.linear_state.len() == st.len() {
5305 l.linear_state.copy_from_slice(&st);
5306 } else {
5307 l.linear_state = st;
5308 }
5309 }
5310 Some((plain_states, rows))
5311 } else {
5312 None
5313 };
5314 #[cfg(target_os = "macos")]
5316 if metal_native {
5317 if !self.metal_verify_commit(a) {
5320 self.clear_sequence_state();
5321 self.graph_failed
5322 .store(true, std::sync::atomic::Ordering::Relaxed);
5323 self.cancel
5324 .store(true, std::sync::atomic::Ordering::Relaxed);
5325 tracing::error!("Metal verify state/KV handoff failed after admission");
5326 return None;
5327 }
5328 if let Some((plain_states, rows)) = commit_ref {
5329 crate::gpu_metal::queue_fence();
5330 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5331 let mut worst_s = 0f32;
5332 let mut worst_li = 0usize;
5333 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5334 if l.linear_state.len() != ps.len() || ps.is_empty() {
5335 continue;
5336 }
5337 let d = l
5338 .linear_state
5339 .iter()
5340 .zip(ps)
5341 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5342 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5343 let rel = d / n.max(1e-6);
5344 if rel > worst_s {
5345 worst_s = rel;
5346 worst_li = li;
5347 }
5348 }
5349 let mut worst_k = 0f32;
5350 for (li, kk, vv) in &rows {
5351 let l = &self.kv_cache.layers[*li];
5352 let n0 = l.seq_len - (kk.len() / (nkv * hd));
5353 let mut ck = Vec::new();
5354 let mut cv = Vec::new();
5355 for g in 0..nkv {
5356 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5357 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5358 }
5359 if ck.len() == kk.len() {
5360 let dk = ck
5361 .iter()
5362 .zip(kk)
5363 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5364 let dv = cv
5365 .iter()
5366 .zip(vv)
5367 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5368 worst_k = worst_k.max(dk).max(dv);
5369 } else {
5370 eprintln!(
5371 "commit-check L{li}: kv row count mismatch {} vs {}",
5372 ck.len(),
5373 kk.len()
5374 );
5375 }
5376 }
5377 eprintln!(
5378 "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}"
5379 );
5380 }
5381 }
5382 if !metal_native && a + 1 < b {
5383 let expected_gdn_layers = self.graph_gdn_layer_count();
5384 if expected_gdn_layers > 0
5385 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5386 {
5387 self.clear_sequence_state();
5388 self.graph_failed
5389 .store(true, std::sync::atomic::Ordering::Relaxed);
5390 self.cancel
5391 .store(true, std::sync::atomic::Ordering::Relaxed);
5392 tracing::error!("GDN speculative restore failed after verify");
5393 return None;
5394 }
5395 }
5396 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5397 self.clear_sequence_state();
5402 self.graph_failed
5403 .store(true, std::sync::atomic::Ordering::Relaxed);
5404 self.cancel
5405 .store(true, std::sync::atomic::Ordering::Relaxed);
5406 tracing::error!("trunk graph KV rewind failed after speculative verify");
5407 return None;
5408 }
5409 *accepted += a;
5410 m.kv.truncate_last(k_spec.saturating_sub(1));
5421 #[cfg(target_os = "macos")]
5422 if metal_native && self.mtp_graph_mode == Some(true) {
5423 crate::gpu_metal::kv_mirror_set_stored(
5426 self.mtp_kv_id(),
5427 Self::MTP_LAYER_BASE,
5428 m.kv.seq_len,
5429 );
5430 }
5431 if !metal_native
5432 && self.mtp_graph_mode == Some(true)
5433 && !self.rewind_mtp_graph_mirror(next_pos)
5434 {
5435 self.clear_sequence_state();
5439 self.graph_failed
5440 .store(true, std::sync::atomic::Ordering::Relaxed);
5441 self.cancel
5442 .store(true, std::sync::atomic::Ordering::Relaxed);
5443 tracing::error!("MTP graph mirror rewind failed after verify commit");
5444 return None;
5445 }
5446 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5447 if !warm_off && a > 0 {
5448 let mut warmed = false;
5451 #[cfg(target_os = "macos")]
5452 if metal_native && self.mtp_graph_mode == Some(true) {
5453 let pairs: Vec<(&[f32], u32)> = (0..a)
5457 .map(|j| {
5458 (
5459 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5460 ids[j],
5461 )
5462 })
5463 .collect();
5464 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5465 if !warmed {
5466 warmed = true;
5467 for j in 0..a {
5468 let row =
5469 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5470 if self
5471 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5472 .is_none()
5473 {
5474 warmed = false;
5475 break;
5476 }
5477 }
5478 }
5479 }
5480 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5481 let rows: Vec<Vec<f32>> = (0..a)
5482 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5483 .collect();
5484 let pairs: Vec<(&[f32], u32)> = rows
5485 .iter()
5486 .zip(ids.iter())
5487 .map(|(r, &t)| (r.as_slice(), t))
5488 .collect();
5489 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5490 Ok(()) => warmed = true,
5491 Err(err) => {
5492 tracing::error!("{err}");
5498 self.clear_sequence_state();
5499 self.graph_failed
5500 .store(true, std::sync::atomic::Ordering::Relaxed);
5501 self.cancel
5502 .store(true, std::sync::atomic::Ordering::Relaxed);
5503 return None;
5504 }
5505 }
5506 }
5507 if !warmed {
5508 for j in 0..a {
5509 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5510 let row = row.to_vec();
5511 self.mtp_warm(m, &row, ids[j], next_pos + j);
5512 }
5513 }
5514 }
5515 if let Some(c) = forced {
5519 self.spec_forced = Some(c);
5520 self.graph_logits = None;
5521 } else {
5522 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5523 row.resize(self.vocab_size, 0.0);
5524 if let Some(c) = self.final_softcap {
5525 for l in row.iter_mut() {
5526 *l = c * (*l / c).tanh();
5527 }
5528 }
5529 self.graph_logits = Some(row);
5530 }
5531 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5532 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5538 let end = subs();
5539 eprintln!(
5540 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5541 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5542 t_draft.as_secs_f64() * 1e3,
5543 sub_draft - sub0,
5544 (t_verify - t_draft).as_secs_f64() * 1e3,
5545 sub_verify - sub_draft,
5546 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5547 end - sub_verify,
5548 );
5549 }
5550 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5551 }
5552
5553 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5562 if !self.pair_supported() {
5563 return (0.0, 0.0);
5564 }
5565 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5572 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5573 let emb1 = self.embed_single(1);
5574 let emb2 = self.embed_single(2);
5575 let pos = self.kv_cache.seq_len();
5576
5577 let t0 = std::time::Instant::now();
5578 for _ in 0..iters {
5579 let _ = self.forward_layers(&emb1, pos, None);
5580 let _ = self.forward_layers(&emb2, pos + 1, None);
5581 for l in &mut self.kv_cache.layers {
5582 l.truncate_last(2);
5583 }
5584 }
5585 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5586
5587 let t1 = std::time::Instant::now();
5588 for _ in 0..iters {
5589 let _ = self.forward_pair(&emb1, &emb2, pos);
5590 for l in &mut self.kv_cache.layers {
5591 l.truncate_last(2);
5592 }
5593 }
5594 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5595 match graph_env {
5596 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5597 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5598 }
5599 (singles_ms, pair_ms)
5600 }
5601
5602 fn pair_supported(&self) -> bool {
5610 !self.weights.layers.is_empty()
5617 && self.g3n.is_none()
5618 && !self
5619 .weights
5620 .layers
5621 .iter()
5622 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5623 }
5624
5625 fn forward_pair(
5626 &mut self,
5627 emb1: &[f32],
5628 emb2: &[f32],
5629 position: usize,
5630 ) -> (Vec<f32>, Vec<f32>) {
5631 let mut h1 = emb1.to_vec();
5632 let mut h2 = emb2.to_vec();
5633 let (_nkv, _hd, hs, _rd, eps) = (
5634 self.num_kv_heads,
5635 self.head_dim,
5636 self.hidden_size,
5637 self.rotary_dim,
5638 self.rms_eps,
5639 );
5640 let pool = self.pool.clone();
5641
5642 for li in 0..self.num_layers {
5643 let lw = &self.weights.layers[self.phys_layer(li)];
5644 inference::rms_norm_into(
5647 &h1,
5648 &lw.input_norm,
5649 self.rms_eps,
5650 self.norm_style,
5651 &mut self.ws.n1,
5652 );
5653 inference::rms_norm_into(
5654 &h2,
5655 &lw.input_norm,
5656 self.rms_eps,
5657 self.norm_style,
5658 &mut self.ws.n2,
5659 );
5660
5661 let (a1, a2) = match &lw.attn {
5662 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5663 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5664 AttnKind::Linear(w) => {
5665 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5666 let layer = &mut self.kv_cache.layers[li];
5667 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5668 vmf_phase_pair(
5669 &self.ws.n1,
5670 &self.ws.n2,
5671 w,
5672 &cfg,
5673 state,
5674 scratch,
5675 self.pool.as_deref(),
5676 )
5677 }
5678 AttnKind::LinearGdn(w) => {
5679 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5680 let layer = &mut self.kv_cache.layers[li];
5681 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5682 gdn_pair(
5683 &self.ws.n1,
5684 &self.ws.n2,
5685 w,
5686 &cfg,
5687 state,
5688 scratch,
5689 self.pool.as_deref(),
5690 )
5691 }
5692 AttnKind::ShortConv(w) => {
5693 let cfg = self
5694 .short_conv_cfg
5695 .expect("short-conv layer without short_conv_cfg");
5696 let layer = &mut self.kv_cache.layers[li];
5697 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5698 short_conv_pair(
5699 &self.ws.n1,
5700 &self.ws.n2,
5701 w,
5702 &cfg,
5703 state,
5704 scratch,
5705 self.pool.as_deref(),
5706 )
5707 }
5708 AttnKind::Full {
5709 wq,
5710 wk,
5711 wv,
5712 wo,
5713 q_norm,
5714 k_norm,
5715 output_gate,
5716 softplus_gate,
5717 bias,
5718 } => {
5719 let inv_freq_l = self.layer_inv_freq(li);
5720 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5721 let cfg = QwenAttnCfg {
5722 num_heads: self.layer_num_heads(li),
5723 num_kv_heads: nkv_l,
5724 head_dim: hd_l,
5725 hidden_size: hs,
5726 position,
5727 inv_freq: &inv_freq_l,
5728 rotary_dim: rd_l,
5729 scale: self.attn_scale,
5730 softcap: self.attn_softcap,
5731 window: self.layer_window(li),
5732 v_norm: self.attn_v_norm,
5733 qk_norm_after_rope: self.qk_norm_after_rope,
5734 q_norm: q_norm.as_deref(),
5735 k_norm: k_norm.as_deref(),
5736 output_gate: *output_gate,
5737 softplus_gate: softplus_gate
5738 .as_ref()
5739 .map(|(gate, per_head)| (gate, *per_head)),
5740 rope_scale: self.layer_rope_scale(li),
5741 bias: bias
5742 .as_ref()
5743 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5744 rms_eps: eps,
5745 norm_style: self.norm_style,
5746 pool: pool.as_deref(),
5747 };
5748 attention::qwen_attention_pair(
5749 &self.ws.n1,
5750 &self.ws.n2,
5751 wq,
5752 wk,
5753 wv,
5754 wo,
5755 &mut self.kv_cache.layers[li],
5756 &cfg,
5757 )
5758 }
5759 };
5760 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5761 Some(w) => (
5762 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5763 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5764 ),
5765 None => (a1, a2),
5766 };
5767 for i in 0..self.hidden_size {
5768 h1[i] += a1[i];
5769 h2[i] += a2[i];
5770 }
5771 let (mut a1, mut a2) = (a1, a2);
5772 attention::recycle_buf(&mut a1);
5773 attention::recycle_buf(&mut a2);
5774
5775 let lw = &self.weights.layers[self.phys_layer(li)];
5776 inference::rms_norm_into(
5777 &h1,
5778 &lw.post_norm,
5779 self.rms_eps,
5780 self.norm_style,
5781 &mut self.ws.p1,
5782 );
5783 inference::rms_norm_into(
5784 &h2,
5785 &lw.post_norm,
5786 self.rms_eps,
5787 self.norm_style,
5788 &mut self.ws.p2,
5789 );
5790 let (f1, f2) = match &lw.ffn {
5791 FfnKind::DenseMoe(dm) => (
5794 dense_moe_ffn(
5795 dm,
5796 &self.ws.p1,
5797 &h1,
5798 self.rms_eps,
5799 self.norm_style,
5800 self.pool.as_deref(),
5801 ),
5802 dense_moe_ffn(
5803 dm,
5804 &self.ws.p2,
5805 &h2,
5806 self.rms_eps,
5807 self.norm_style,
5808 self.pool.as_deref(),
5809 ),
5810 ),
5811 _ => ffn_forward_pair(
5812 &lw.ffn,
5813 &self.ws.p1,
5814 &self.ws.p2,
5815 self.pool.as_deref(),
5816 None,
5817 ),
5818 };
5819 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5820 Some(w) => (
5821 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5822 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5823 ),
5824 None => (f1, f2),
5825 };
5826 for i in 0..self.hidden_size {
5827 h1[i] += f1[i];
5828 h2[i] += f2[i];
5829 }
5830 let (mut f1, mut f2) = (f1, f2);
5831 attention::recycle_buf(&mut f1);
5832 attention::recycle_buf(&mut f2);
5833 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5834 for i in 0..self.hidden_size {
5835 h1[i] *= sc;
5836 h2[i] *= sc;
5837 }
5838 }
5839 if self.is_loop_end(li) && li + 1 < self.num_layers {
5841 h1 = inference::rms_norm(
5842 &h1,
5843 &self.weights.final_norm,
5844 self.rms_eps,
5845 self.norm_style,
5846 );
5847 h2 = inference::rms_norm(
5848 &h2,
5849 &self.weights.final_norm,
5850 self.rms_eps,
5851 self.norm_style,
5852 );
5853 }
5854 }
5855 if self.o1_active() {
5861 self.commit_linear_scratch();
5862 }
5863 self.o1_progress();
5864 (h1, h2)
5865 }
5866
5867 fn commit_linear_scratch(&mut self) {
5869 for layer in &mut self.kv_cache.layers {
5870 if !layer.linear_scratch.is_empty() {
5871 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5872 layer.linear_scratch.clear();
5873 }
5874 }
5875 }
5876
5877 pub fn forward_ids(
5880 &mut self,
5881 ids: &[u32],
5882 task_mask: Option<&TaskMask>,
5883 ) -> Result<Vec<f32>, String> {
5884 if ids.is_empty() {
5885 return Err("empty id sequence".to_string());
5886 }
5887 self.clear_sequence_state();
5888 self.check_forward_graph("forward_ids setup", 0)?;
5889 if task_mask.is_none() {
5890 self.o1_begin();
5891 }
5892 let mut hidden = vec![0.0f32; self.hidden_size];
5893 let mut pos = 0usize;
5894 if let Some(b) = &mut self.dsv41 {
5895 let pool = self.pool.clone();
5896 let mut logits = Vec::new();
5897 crate::dsv41::forward_chunk(
5898 &b.0,
5899 &b.1,
5900 &b.2,
5901 &mut b.3,
5902 ids,
5903 0,
5904 pool.as_deref(),
5905 &mut logits,
5906 );
5907 if let Err(err) = self.o1_seal_checked() {
5908 self.clear_sequence_state();
5909 return Err(err);
5910 }
5911 return Ok(logits);
5912 }
5913 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5921 let chunk = prefill_chunk();
5925 let hs = self.hidden_size;
5926 while pos < ids.len() {
5927 let end = (pos + chunk).min(ids.len());
5928 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5929 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5930 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5931 pos = end;
5932 }
5933 }
5934 if task_mask.is_none()
5943 && !self.graph_prefill_preferred()
5944 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5945 && self.pair_supported()
5946 {
5947 while pos + 1 < ids.len() {
5948 let e1 = self.embed_single(ids[pos]);
5949 let e2 = self.embed_single(ids[pos + 1]);
5950 let (_, h2) = self.forward_pair(&e1, &e2, pos);
5951 self.check_forward_graph("forward_ids pair", pos + 1)?;
5952 self.commit_linear_scratch();
5953 hidden = h2;
5954 pos += 2;
5955 }
5956 }
5957 while pos < ids.len() {
5958 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5959 self.check_forward_graph("forward_ids", pos)?;
5960 pos += 1;
5961 }
5962 if let Err(err) = self.o1_seal_checked() {
5966 self.clear_sequence_state();
5967 return Err(err);
5968 }
5969 let normed = inference::rms_norm(
5970 &hidden,
5971 &self.weights.final_norm,
5972 self.rms_eps,
5973 self.norm_style,
5974 );
5975 Ok(self.lm_head_forward(&normed))
5976 }
5977
5978 #[doc(hidden)]
5982 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
5983 #[cfg(target_os = "macos")]
5984 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5985 if ids.is_empty() {
5986 return Err("empty id sequence".to_string());
5987 }
5988 self.clear_sequence_state();
5989 self.dsv41
5990 .as_ref()
5991 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
5992 self.o1_begin();
5993 let rows = {
5994 let pool = self.pool.clone();
5995 let b = self
5996 .dsv41
5997 .as_mut()
5998 .expect("dsv41 checked above; state cannot change during forward");
5999 let mut rows = Vec::with_capacity(ids.len());
6000 for (position, &id) in ids.iter().enumerate() {
6001 let mut logits = Vec::new();
6002 crate::dsv41::forward_token(
6003 &b.0,
6004 &b.1,
6005 &b.2,
6006 &mut b.3,
6007 id,
6008 position,
6009 pool.as_deref(),
6010 &mut logits,
6011 );
6012 rows.push(logits);
6013 }
6014 rows
6015 };
6016 self.o1_seal();
6017 Ok(rows)
6018 }
6019
6020 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6027 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6028 Ok((nll / cnt.max(1) as f64).exp())
6029 }
6030
6031 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6036 self.clear_sequence_state();
6037 FFN_PROBE.with(|p| {
6038 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6039 });
6040 crate::gpu::cpu_scope(|| {
6041 for (pos, &id) in ids.iter().enumerate() {
6042 let emb = self.embed_single(id);
6043 let _ = self.forward_layers(&emb, pos, None);
6044 }
6045 });
6046 self.clear_sequence_state();
6047 FFN_PROBE
6048 .with(|p| p.borrow_mut().take())
6049 .unwrap_or_default()
6050 }
6051
6052 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6056 if let Err(err) = self.nll_begin() {
6057 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6061 self.nll_end();
6062 return Err(err);
6063 }
6064 FFN_PROBE.with(|p| {
6065 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6066 });
6067 let result: Result<(), String> = (|| {
6068 for chunk in ids.chunks(256) {
6069 if chunk.len() < 2 {
6070 continue;
6071 }
6072 self.nll_ids_masked(chunk, 0, None)?;
6073 }
6074 Ok(())
6075 })();
6076 self.nll_end();
6077 let probe = FFN_PROBE
6078 .with(|p| p.borrow_mut().take())
6079 .unwrap_or_default();
6080 match result {
6081 Ok(()) => Ok(probe),
6082 Err(err) => {
6083 drop(probe);
6084 Err(err)
6085 }
6086 }
6087 }
6088
6089 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6093 self.nll_begin()?;
6094 let result: Result<f64, String> = (|| {
6095 let mut nll = 0f64;
6096 let mut cnt = 0usize;
6097 let mut hidden = vec![0f32; self.hidden_size];
6098 for (pos, &id) in ids.iter().enumerate() {
6099 if pos > 0 {
6100 inference::rms_norm_into(
6101 &hidden,
6102 &self.weights.final_norm,
6103 self.rms_eps,
6104 self.norm_style,
6105 &mut self.ws.n1,
6106 );
6107 let mut logits = self.lm_head_forward(&self.ws.n1);
6108 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6109 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6110 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6111 nll -= p.max(1e-300).ln();
6112 cnt += 1;
6113 attention::recycle_buf(&mut logits);
6114 }
6115 let emb = self.embed_single(id);
6116 hidden = self.forward_layers(&emb, pos, Some(mask));
6117 self.nll_check_graph("masked serial forward", pos)?;
6118 let _ = self.graph_logits.take();
6122 }
6123 Ok((nll / cnt.max(1) as f64).exp())
6124 })();
6125 self.nll_end();
6126 result
6127 }
6128
6129 pub fn nll_ids_masked(
6148 &mut self,
6149 ids: &[u32],
6150 start: usize,
6151 task_mask: Option<&TaskMask>,
6152 ) -> Result<(f64, usize), String> {
6153 let task_mask = self.drop_open_mask(task_mask);
6154 self.nll_ids_inner(ids, start, task_mask)
6155 }
6156
6157 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6158 self.nll_ids_inner(ids, start, None)
6159 }
6160
6161 fn nll_ids_inner(
6162 &mut self,
6163 ids: &[u32],
6164 start: usize,
6165 task_mask: Option<&TaskMask>,
6166 ) -> Result<(f64, usize), String> {
6167 self.nll_begin()?;
6168 let result: Result<(f64, usize), String> = (|| {
6169 let mut nll = 0f64;
6170 let mut cnt = 0usize;
6171 let (graph_quality, fused_head_quality) = nll_graph_policy(
6184 task_mask.is_none(),
6185 self.graph_prefill_preferred(),
6186 crate::gpu::q1_force(),
6187 );
6188 self.graph_head_required = fused_head_quality;
6189 self.graph_want_logits = fused_head_quality;
6190 #[cfg(target_os = "macos")]
6191 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6192 match self.nll_batch_metal(ids, start) {
6193 MetalBatchNllOutcome::Completed(nll, count) => {
6194 return Ok((nll, count));
6195 }
6196 MetalBatchNllOutcome::Declined => {}
6197 MetalBatchNllOutcome::Failed(err) => return Err(err),
6198 }
6199 }
6200 if self.can_prefill_batched() && !graph_quality {
6201 const CHUNK: usize = 128;
6207 const LM_SUB: usize = 32;
6208 let n = ids.len().saturating_sub(1);
6209 let hs = self.hidden_size;
6210 let rows = self.weights.lm_head.rows();
6211 let mut pos = 0usize;
6212 while pos < n {
6213 let end = (pos + CHUNK).min(n);
6214 let bsz = end - pos;
6215 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6216 self.nll_check_graph("batched prefill", pos)?;
6217 let mut k0 = 0usize;
6218 while k0 < bsz {
6219 let k1 = (k0 + LM_SUB).min(bsz);
6220 let sb = k1 - k0;
6221 if pos + k1 <= start {
6224 k0 = k1;
6225 continue;
6226 }
6227 let mut normed = vec![0.0f32; sb * hs];
6228 for k in 0..sb {
6229 let r = inference::rms_norm(
6230 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6231 &self.weights.final_norm,
6232 self.rms_eps,
6233 self.norm_style,
6234 );
6235 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6236 }
6237 let mut logits = vec![0.0f32; sb * rows];
6238 self.weights
6239 .lm_head
6240 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6241 for k in 0..sb {
6242 if pos + k0 + k < start {
6243 continue;
6244 }
6245 self.nll_check_graph("batched score row", pos + k0 + k)?;
6246 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6247 if let Some(mu) = self.logit_multiplier {
6248 for v in lg.iter_mut() {
6249 *v *= mu;
6250 }
6251 }
6252 if let Some(c) = self.final_softcap {
6256 for v in lg.iter_mut() {
6257 *v = c * (*v / c).tanh();
6258 }
6259 }
6260 if let Some(cm) = self.head_clusters.clone() {
6263 self.hierarchical_head_logprobs(
6264 &normed[k * hs..(k + 1) * hs],
6265 &cm,
6266 lg,
6267 );
6268 }
6269 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6270 let target = ids[pos + k0 + k + 1] as usize;
6271 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6272 let lse: f64 = lg
6273 .iter()
6274 .map(|&v| ((v - max) as f64).exp())
6275 .sum::<f64>()
6276 .ln()
6277 + max as f64;
6278 nll += lse - lg[target] as f64;
6279 cnt += 1;
6280 if std::env::var("CMF_PPL_TRACE").is_ok() {
6281 let top = lg
6282 .iter()
6283 .enumerate()
6284 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6285 .map(|(i, _)| i)
6286 .unwrap_or(0);
6287 eprintln!(
6288 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6289 pos + k0 + k,
6290 target,
6291 lse - lg[target] as f64,
6292 top,
6293 lg[target],
6294 lg[top]
6295 );
6296 }
6297 }
6298 k0 = k1;
6299 }
6300 pos = end;
6301 }
6302 return Ok((nll, cnt));
6303 }
6304 for pos in 0..ids.len().saturating_sub(1) {
6305 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6306 self.nll_check_graph("serial forward", pos)?;
6307 let out_of_band = self.graph_logits.take();
6315 if self.graph_head_required && out_of_band.is_none() {
6316 METAL_GRAPH_HEAD_MISS.fetch_add(
6317 1,
6318 std::sync::atomic::Ordering::Relaxed,
6319 );
6320 return Err(format!(
6321 "fused Metal graph head did not complete at NLL position {pos}"
6322 ));
6323 }
6324 if pos < start {
6325 continue;
6326 }
6327 let logits = match out_of_band {
6328 Some(lg) => lg,
6329 None => {
6330 let normed = inference::rms_norm(
6331 &hidden,
6332 &self.weights.final_norm,
6333 self.rms_eps,
6334 self.norm_style,
6335 );
6336 self.lm_head_forward(&normed)
6340 }
6341 };
6342 let target = ids[pos + 1] as usize;
6343 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6344 let lse: f64 = logits
6345 .iter()
6346 .map(|&v| ((v - max) as f64).exp())
6347 .sum::<f64>()
6348 .ln()
6349 + max as f64;
6350 let tok_nll = lse - logits[target] as f64;
6351 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6352 let top = logits
6353 .iter()
6354 .enumerate()
6355 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6356 .map(|(i, _)| i)
6357 .unwrap_or(0);
6358 eprintln!(
6359 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6360 logits[target], logits[top]
6361 );
6362 }
6363 nll += tok_nll;
6364 cnt += 1;
6365 }
6366 Ok((nll, cnt))
6367 })();
6368 self.nll_end();
6369 result
6370 }
6371
6372 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6377 let normed = inference::rms_norm(
6378 hidden,
6379 &self.weights.final_norm,
6380 self.rms_eps,
6381 self.norm_style,
6382 );
6383 let mut logits = self.lm_head_forward(&normed);
6386 let target = target as usize;
6387 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6388 let lse: f64 = logits
6389 .iter()
6390 .map(|&v| ((v - max) as f64).exp())
6391 .sum::<f64>()
6392 .ln()
6393 + max as f64;
6394 let tok_nll = lse - logits[target] as f64;
6395 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6396 let top = logits
6397 .iter()
6398 .enumerate()
6399 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6400 .map(|(i, _)| i)
6401 .unwrap_or(0);
6402 eprintln!(
6403 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6404 logits[target], logits[top]
6405 );
6406 }
6407 attention::recycle_buf(&mut logits);
6408 tok_nll
6409 }
6410
6411 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6429 self.nll_begin()?;
6434 let requested_prefix = (prefill > 0).then_some(prefill);
6435 self.o1_begin_with_prefix(requested_prefix);
6436 let n = ids.len().saturating_sub(1);
6437 let requested_start = prefill.min(n);
6438 let exact_end = if self.o1_active() {
6443 match requested_prefix {
6444 Some(requested) => self.o1_effective_boundary(requested),
6445 None => self
6446 .o1_cfg
6447 .as_ref()
6448 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6449 }
6450 .unwrap_or(requested_start)
6451 .min(n)
6452 } else {
6453 requested_start
6454 };
6455 let mut nll = 0f64;
6456 let mut cnt = 0usize;
6457
6458 let mut pos = 0usize;
6462 if self.can_prefill_batched() {
6463 const CHUNK: usize = 128;
6464 while pos < exact_end {
6465 let end = (pos + CHUNK).min(exact_end);
6466 let hiddens = self.prefill_batch(&ids[pos..end], pos);
6467 if self
6468 .graph_failed
6469 .swap(false, std::sync::atomic::Ordering::Relaxed)
6470 {
6471 self.cancel
6472 .store(false, std::sync::atomic::Ordering::Relaxed);
6473 self.nll_end();
6474 return Err("GPU graph failed during O(1) NLL prefix".into());
6475 }
6476 for row in 0..end - pos {
6477 let score_pos = pos + row;
6478 if score_pos >= requested_start && score_pos < n {
6479 nll += self.nll_from_hidden(
6480 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6481 ids[score_pos + 1],
6482 score_pos,
6483 );
6484 cnt += 1;
6485 }
6486 }
6487 pos = end;
6488 }
6489 } else {
6490 while pos < exact_end {
6491 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6492 if self
6493 .graph_failed
6494 .swap(false, std::sync::atomic::Ordering::Relaxed)
6495 {
6496 self.cancel
6497 .store(false, std::sync::atomic::Ordering::Relaxed);
6498 self.nll_end();
6499 return Err("GPU graph failed during O(1) NLL prefix".into());
6500 }
6501 if pos >= requested_start && pos < n {
6502 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6503 cnt += 1;
6504 }
6505 pos += 1;
6506 }
6507 }
6508 self.o1_seal_checked().map_err(|err| {
6509 self.nll_end();
6510 err
6511 })?;
6512
6513 let batch_k = std::env::var("CMF_BATCH_K")
6522 .ok()
6523 .and_then(|v| v.parse::<usize>().ok())
6524 .unwrap_or(0);
6525 let batch_admitted = batch_k > 0
6526 && self.can_prefill_batched()
6527 && self.o1_active()
6528 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6529 && (0..self.num_layers).all(|li| {
6530 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6531 cache.o1.is_none() || cache.o1_views().is_some()
6532 });
6533 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6534 eprintln!(
6535 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6536 batch_admitted,
6537 batch_k,
6538 n.saturating_sub(exact_end),
6539 );
6540 }
6541 let mut batch_completed = false;
6542 if batch_admitted && exact_end < n {
6543 let hs = self.hidden_size;
6544 let mut batch_pos = exact_end;
6545 while batch_pos < n {
6546 let end = (batch_pos + batch_k).min(n);
6547 let bk = end - batch_pos;
6548 let mut hiddens = vec![0.0f32; bk * hs];
6549 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6550 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6551 }
6552 let positions: Vec<usize> = (batch_pos..end).collect();
6553 let t_batch = std::time::Instant::now();
6554 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6555 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6556 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6557 eprintln!(
6558 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6559 batch_pos,
6560 end.saturating_sub(1),
6561 bk as f64 / (ms / 1000.0),
6562 );
6563 }
6564 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6565 self.nll_end();
6566 return Err(err);
6567 }
6568 match outcome {
6569 crate::gpu::BatchGraphOutcome::Completed => {
6570 batch_completed = true;
6571 for row in 0..bk {
6572 nll += self.nll_from_hidden(
6573 &hiddens[row * hs..(row + 1) * hs],
6574 ids[batch_pos + row + 1],
6575 batch_pos + row,
6576 );
6577 cnt += 1;
6578 }
6579 batch_pos = end;
6580 }
6581 crate::gpu::BatchGraphOutcome::Declined => {
6582 if batch_completed {
6583 self.nll_end();
6584 return Err(format!(
6585 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6586 ));
6587 }
6588 break;
6589 }
6590 crate::gpu::BatchGraphOutcome::Failed => {
6591 self.nll_end();
6592 return Err(format!(
6593 "O(1) NLL batch graph failed after admission at position {batch_pos}"
6594 ));
6595 }
6596 }
6597 }
6598 if batch_completed && cnt == n.saturating_sub(requested_start) {
6599 self.nll_end();
6600 return Ok((nll, cnt));
6601 }
6602 }
6603
6604 for pos in exact_end..n {
6609 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6610 if self
6611 .graph_failed
6612 .swap(false, std::sync::atomic::Ordering::Relaxed)
6613 {
6614 self.cancel
6615 .store(false, std::sync::atomic::Ordering::Relaxed);
6616 self.nll_end();
6617 return Err(format!(
6618 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6619 ));
6620 }
6621 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6622 cnt += 1;
6623 }
6624 self.nll_end();
6625 Ok((nll, cnt))
6626 }
6627
6628 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6636 self.clear_sequence_state();
6637 let n = ids.len().saturating_sub(1);
6638 let mut correct = Vec::with_capacity(n);
6639 let mut pmax = Vec::with_capacity(n);
6640 for pos in 0..n {
6641 let emb = self.embed_single(ids[pos]);
6642 let hidden = self.forward_layers(&emb, pos, None);
6643 let normed = inference::rms_norm(
6644 &hidden,
6645 &self.weights.final_norm,
6646 self.rms_eps,
6647 self.norm_style,
6648 );
6649 let logits = self.lm_head_forward(&normed);
6653 let target = ids[pos + 1] as usize;
6654 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6655 for (i, &v) in logits.iter().enumerate() {
6656 if v > mval {
6657 mval = v;
6658 amax = i;
6659 }
6660 }
6661 correct.push(amax == target);
6662 let row: Vec<f32> = temps
6663 .iter()
6664 .map(|&t| {
6665 let tt = t.max(1e-3);
6666 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6667 1.0 / s.max(1e-12) })
6669 .collect();
6670 pmax.push(row);
6671 }
6672 self.clear_sequence_state();
6673 (correct, pmax)
6674 }
6675
6676 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6683 if self.dyn_router.is_none() {
6684 return Ok((self.ppl_ids(ids)?, 0));
6685 }
6686 self.nll_begin()?;
6687 let saved_active = self.dyn_active;
6688 let mut router = self
6689 .dyn_router
6690 .take()
6691 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6692 router.reset();
6693 self.dyn_phi_seen = 0;
6694 let _ = self.set_active_skill(None);
6695
6696 let result: Result<(f64, usize), String> = (|| {
6697 let mut nll = 0f64;
6698 let mut cnt = 0usize;
6699 for pos in 0..ids.len().saturating_sub(1) {
6700 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6701 self.nll_check_graph("dynamic serial forward", pos)?;
6702 let out_of_band = self.graph_logits.take();
6703 let mut logits = match out_of_band {
6704 Some(lg) => lg,
6705 None => {
6706 let normed = inference::rms_norm(
6707 &hidden,
6708 &self.weights.final_norm,
6709 self.rms_eps,
6710 self.norm_style,
6711 );
6712 self.lm_head_forward(&normed)
6716 }
6717 };
6718 let target = ids[pos + 1] as usize;
6719 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6720 let lse: f64 = logits
6721 .iter()
6722 .map(|&v| ((v - max) as f64).exp())
6723 .sum::<f64>()
6724 .ln()
6725 + max as f64;
6726 let tok_nll = lse - logits[target] as f64;
6727 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6728 let top = logits
6729 .iter()
6730 .enumerate()
6731 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6732 .map(|(i, _)| i)
6733 .unwrap_or(0);
6734 eprintln!(
6735 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6736 logits[target], logits[top]
6737 );
6738 }
6739 nll += tok_nll;
6740 cnt += 1;
6741 attention::recycle_buf(&mut logits);
6742 let phi = self.dyn_phi_ema.clone();
6744 if let Some(new_active) = router.step(&phi, pos) {
6745 let _ = self.set_active_skill(new_active);
6746 }
6747 }
6748 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6749 })();
6750
6751 let _ = self.set_active_skill(saved_active);
6754 self.dyn_router = Some(router);
6755 self.nll_end();
6756 result
6757 }
6758
6759 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6761 self.clear_sequence_state();
6762 let mut acc = vec![0f32; self.hidden_size];
6763 for (pos, &id) in ids.iter().enumerate() {
6764 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6765 for (a, v) in acc.iter_mut().zip(&h) {
6766 *a += v;
6767 }
6768 }
6769 let n = ids.len().max(1) as f32;
6770 for a in acc.iter_mut() {
6771 *a /= n;
6772 }
6773 self.clear_sequence_state();
6774 acc
6775 }
6776
6777 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6783 self.prefill_batch_masked(ids, start_pos, None)
6784 }
6785
6786 fn prefill_batch_masked(
6792 &mut self,
6793 ids: &[u32],
6794 start_pos: usize,
6795 task_mask: Option<&TaskMask>,
6796 ) -> Vec<f32> {
6797 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6798 }
6799
6800 fn prefill_batch_span(
6806 &mut self,
6807 input: PrefillIn<'_>,
6808 start_pos: usize,
6809 task_mask: Option<&TaskMask>,
6810 from: usize,
6811 upto_excl: usize,
6812 ) -> Vec<f32> {
6813 let hs = self.hidden_size;
6814 let b = match input {
6815 PrefillIn::Ids(ids) => ids.len(),
6816 PrefillIn::Hidden(hb) => hb.len() / hs,
6817 };
6818 let upto_excl = upto_excl.min(self.num_layers);
6819 let mut h: Vec<f32>;
6823 let mut h_ready;
6824 match input {
6825 PrefillIn::Ids(_) => {
6826 h = vec![0.0; b * hs];
6827 h_ready = false;
6828 }
6829 PrefillIn::Hidden(hb) => {
6830 h = hb.to_vec();
6831 h_ready = true;
6832 }
6833 }
6834 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6835 if let PrefillIn::Ids(ids) = input {
6836 for (bi, &id) in ids.iter().enumerate() {
6837 let e = me.embed_single(id);
6838 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6839 }
6840 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6841 if let Ok(t) = tp.parse::<usize>() {
6842 if t >= start_pos && t < start_pos + ids.len() {
6843 let bi = t - start_pos;
6844 let row = &h[bi * hs..(bi + 1) * hs];
6845 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6846 eprintln!(
6847 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6848 ids[bi],
6849 row[0],
6850 row[1],
6851 ids.len(),
6852 &ids[..ids.len().min(8)]
6853 );
6854 }
6855 }
6856 }
6857 }
6858 };
6859 let (_nkv, _hd, _rd, eps) = (
6860 self.num_kv_heads,
6861 self.head_dim,
6862 self.rotary_dim,
6863 self.rms_eps,
6864 );
6865 let pool = self.pool.clone();
6866 let norm_style = self.norm_style;
6867 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6868
6869 #[cfg(target_os = "macos")]
6870 let mut chunk_skip_until = 0usize;
6871 for li in from..upto_excl {
6872 let _capacity_tail = automatic_gpu_prefix
6873 .filter(|&prefix| li >= prefix)
6874 .map(|_| crate::gpu::enter_cpu_scope());
6875 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6882 if task_mask.is_none() {
6883 if li < chunk_skip_until {
6884 continue;
6885 }
6886 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6892 fill_h(&mut h, self);
6893 h_ready = true;
6894 }
6895 let ids_for_embed = match input {
6896 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6897 PrefillIn::Hidden(_) => None,
6898 };
6899 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6900 if end > li {
6901 h_ready = true;
6902 chunk_skip_until = end;
6903 if self.is_loop_end(end - 1) && end < self.num_layers {
6906 for bi in 0..b {
6907 let normed = inference::rms_norm(
6908 &h[bi * hs..(bi + 1) * hs],
6909 &self.weights.final_norm,
6910 eps,
6911 norm_style,
6912 );
6913 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6914 }
6915 }
6916 continue;
6917 }
6918 }
6919 if !h_ready {
6920 fill_h(&mut h, self);
6921 h_ready = true;
6922 }
6923 let lw = &self.weights.layers[self.phys_layer(li)];
6924 match &lw.attn {
6926 AttnKind::Kda(w) => {
6927 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6929 let mut normed = vec![0.0f32; b * hs];
6930 for bi in 0..b {
6931 inference::rms_norm_into(
6932 &h[bi * hs..(bi + 1) * hs],
6933 &lw.input_norm,
6934 eps,
6935 norm_style,
6936 &mut normed[bi * hs..(bi + 1) * hs],
6937 );
6938 }
6939 let attn = crate::linear_core::kda_forward_batch(
6940 &normed,
6941 b,
6942 w,
6943 &cfg,
6944 &mut self.kv_cache.layers[li].linear_state,
6945 pool.as_deref(),
6946 );
6947 for (dst, &a) in h.iter_mut().zip(&attn) {
6948 *dst += a;
6949 }
6950 }
6951 AttnKind::LinearGdn(w) => {
6952 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6954 let mut normed = vec![0.0f32; b * hs];
6955 for bi in 0..b {
6956 let r = inference::rms_norm(
6957 &h[bi * hs..(bi + 1) * hs],
6958 &lw.input_norm,
6959 eps,
6960 norm_style,
6961 );
6962 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6963 }
6964 let attn = crate::linear_core::gdn_forward_batch(
6965 &normed,
6966 b,
6967 w,
6968 &cfg,
6969 &mut self.kv_cache.layers[li].linear_state,
6970 pool.as_deref(),
6971 );
6972 for (dst, &a) in h.iter_mut().zip(&attn) {
6973 *dst += a;
6974 }
6975 }
6976 AttnKind::ShortConv(w) => {
6977 let cfg = self
6980 .short_conv_cfg
6981 .expect("short-conv layer without short_conv_cfg");
6982 let mut normed = vec![0.0f32; b * hs];
6983 for bi in 0..b {
6984 inference::rms_norm_into(
6985 &h[bi * hs..(bi + 1) * hs],
6986 &lw.input_norm,
6987 eps,
6988 norm_style,
6989 &mut normed[bi * hs..(bi + 1) * hs],
6990 );
6991 }
6992 let attn = short_conv_forward_batch(
6993 &normed,
6994 b,
6995 w,
6996 &cfg,
6997 &mut self.kv_cache.layers[li].linear_state,
6998 pool.as_deref(),
6999 );
7000 for (dst, &a) in h.iter_mut().zip(&attn) {
7001 *dst += a;
7002 }
7003 }
7004 AttnKind::Mla(w) => {
7005 let inv_freq_l = self.layer_inv_freq(li);
7008 let rs = self.layer_rope_scale(li);
7009 let mut normed = vec![0.0f32; hs];
7010 for bi in 0..b {
7011 inference::rms_norm_into(
7012 &h[bi * hs..(bi + 1) * hs],
7013 &lw.input_norm,
7014 eps,
7015 norm_style,
7016 &mut normed,
7017 );
7018 let ao = mla_attention(
7019 w,
7020 &normed,
7021 &mut self.kv_cache.layers[li],
7022 start_pos + bi,
7023 &inv_freq_l,
7024 rs,
7025 eps,
7026 pool.as_deref(),
7027 );
7028 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7029 *dst += a;
7030 }
7031 }
7032 }
7033 AttnKind::Full {
7034 wq,
7035 wk,
7036 wv,
7037 wo,
7038 q_norm,
7039 k_norm,
7040 output_gate,
7041 softplus_gate,
7042 bias,
7043 } => {
7044 let mut normed = vec![0.0f32; b * hs];
7048 for bi in 0..b {
7049 inference::rms_norm_into(
7050 &h[bi * hs..(bi + 1) * hs],
7051 &lw.input_norm,
7052 eps,
7053 norm_style,
7054 &mut normed[bi * hs..(bi + 1) * hs],
7055 );
7056 }
7057 let inv_freq_l = self.layer_inv_freq(li);
7058 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7059 let cfg = QwenAttnCfg {
7060 num_heads: self.layer_num_heads(li),
7061 num_kv_heads: nkv_l,
7062 head_dim: hd_l,
7063 hidden_size: hs,
7064 position: start_pos,
7065 inv_freq: &inv_freq_l,
7066 rotary_dim: rd_l,
7067 scale: self.attn_scale,
7068 softcap: self.attn_softcap,
7069 window: self.layer_window(li),
7070 v_norm: self.attn_v_norm,
7071 qk_norm_after_rope: self.qk_norm_after_rope,
7072 q_norm: q_norm.as_deref(),
7073 k_norm: k_norm.as_deref(),
7074 output_gate: *output_gate,
7075 softplus_gate: softplus_gate
7076 .as_ref()
7077 .map(|(gate, per_head)| (gate, *per_head)),
7078 rope_scale: self.layer_rope_scale(li),
7079 bias: bias
7080 .as_ref()
7081 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7082 rms_eps: eps,
7083 norm_style,
7084 pool: pool.as_deref(),
7085 };
7086 let mut attn = attention::qwen_attention_batch(
7087 &normed,
7088 b,
7089 wq,
7090 wk,
7091 wv,
7092 wo,
7093 &mut self.kv_cache.layers[li],
7094 &cfg,
7095 );
7096 if let Some(w) = &lw.attn_out_norm {
7097 for bi in 0..b {
7098 inference::rms_norm_into(
7099 &attn[bi * hs..(bi + 1) * hs],
7100 w,
7101 eps,
7102 norm_style,
7103 &mut normed[bi * hs..(bi + 1) * hs],
7104 );
7105 }
7106 attn.copy_from_slice(&normed);
7107 }
7108 for (dst, &a) in h.iter_mut().zip(&attn) {
7109 *dst += a;
7110 }
7111 }
7112 AttnKind::Linear(w) => {
7113 for bi in 0..b {
7114 let normed = inference::rms_norm(
7115 &h[bi * hs..(bi + 1) * hs],
7116 &lw.input_norm,
7117 eps,
7118 norm_style,
7119 );
7120 vmf_phase_forward(
7121 &normed,
7122 w,
7123 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7124 &mut self.kv_cache.layers[li].linear_state,
7125 pool.as_deref(),
7126 )
7127 .iter()
7128 .enumerate()
7129 .for_each(|(i, &a)| h[bi * hs + i] += a);
7130 }
7131 }
7132 }
7133
7134 let lw = &self.weights.layers[self.phys_layer(li)];
7136 let mut post = vec![0.0f32; b * hs];
7137 for bi in 0..b {
7138 let r =
7139 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7140 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7141 }
7142 let mask_row = task_mask
7145 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7146 .and_then(|m| m.ffn_masks.get(li))
7147 .map(|v| v.as_slice());
7148 let mut ffn = match &lw.ffn {
7149 FfnKind::Dense(d) if !d.segs.is_empty() => {
7150 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7151 }
7152 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7153 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7154 FfnKind::DenseMoe(dm) => {
7157 let mut out = vec![0.0f32; b * hs];
7158 for bi in 0..b {
7159 let r = dense_moe_ffn(
7160 dm,
7161 &post[bi * hs..(bi + 1) * hs],
7162 &h[bi * hs..(bi + 1) * hs],
7163 eps,
7164 norm_style,
7165 pool.as_deref(),
7166 );
7167 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7168 }
7169 out
7170 }
7171 };
7172 if let Some(w) = &lw.ffn_out_norm {
7173 for bi in 0..b {
7174 inference::rms_norm_into(
7175 &ffn[bi * hs..(bi + 1) * hs],
7176 w,
7177 eps,
7178 norm_style,
7179 &mut post[bi * hs..(bi + 1) * hs],
7180 );
7181 }
7182 ffn.copy_from_slice(&post);
7183 }
7184 for (dst, &f) in h.iter_mut().zip(&ffn) {
7185 *dst += f;
7186 }
7187 if let Some(sc) = lw.layer_scale {
7188 for v in h.iter_mut() {
7189 *v *= sc;
7190 }
7191 }
7192 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7193 if let Ok(t) = tp.parse::<usize>() {
7194 if t >= start_pos && t < start_pos + b {
7195 let bi = t - start_pos;
7196 let row = &h[bi * hs..(bi + 1) * hs];
7197 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7198 eprintln!(
7199 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7200 row[0], row[1]
7201 );
7202 }
7203 }
7204 }
7205 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7209 let row = &h[(b - 1) * hs..b * hs];
7210 let rms =
7211 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7212 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7213 eprintln!(
7214 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7215 match &self.weights.layers[self.phys_layer(li)].attn {
7216 AttnKind::LinearGdn(_) => "gdn",
7217 AttnKind::Linear(_) => "vmf",
7218 AttnKind::ShortConv(_) => "conv",
7219 _ => "attn",
7220 },
7221 match &lw.ffn {
7222 FfnKind::Moe(_) => "moe",
7223 FfnKind::Dense(_) => "dense",
7224 FfnKind::DenseMoe(_) => "dense+moe",
7225 },
7226 );
7227 }
7228 if self.is_loop_end(li) && li + 1 < self.num_layers {
7230 for bi in 0..b {
7231 let normed = inference::rms_norm(
7232 &h[bi * hs..(bi + 1) * hs],
7233 &self.weights.final_norm,
7234 eps,
7235 norm_style,
7236 );
7237 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7238 }
7239 }
7240 if std::env::var("CMF_TRACE_H").is_ok() {
7241 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7242 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7243 eprintln!(
7244 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7245 lw.layer_scale
7246 );
7247 }
7248 }
7249 crate::gpu::set_layer(-1); self.o1_progress();
7255 h
7256 }
7257
7258 fn embed_single(&self, id: u32) -> Vec<f32> {
7260 let mut out = vec![0.0f32; self.hidden_size];
7261 if (id as usize) < self.weights.embed_tokens.rows() {
7262 self.weights.embed_tokens.row_f32(id as usize, &mut out);
7263 }
7264 if self.embed_multiplier != 1.0 {
7265 for v in out.iter_mut() {
7266 *v *= self.embed_multiplier;
7267 }
7268 }
7269 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7273 let mut v = vec![0.0f32; self.hidden_size.max(1)];
7274 v[0] = id as f32;
7275 return v;
7276 }
7277 if let Some(b) = &self.g3n {
7280 return b.0.extend_embedding(id, &out, self.pool.as_deref());
7281 }
7282 out
7283 }
7284
7285 #[cfg(target_os = "macos")]
7291 fn chunk_run_gpu(
7292 &mut self,
7293 li0: usize,
7294 h: &mut [f32],
7295 b: usize,
7296 pos0: usize,
7297 embed_ids: Option<&[u32]>,
7298 cap: usize,
7299 ) -> usize {
7300 if !crate::gpu::enabled_here()
7304 || std::env::var("CMF_GPU_CHUNK")
7305 .map(|v| v == "0")
7306 .unwrap_or(false)
7307 || b < 32
7308 || self.swa.is_some()
7309 || self.global_attn.is_some()
7310 || self.o1_active()
7313 || self.attn_v_norm
7314 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7315 {
7316 return li0;
7317 }
7318 let Some(model) = self.model.clone() else {
7319 return li0;
7320 };
7321 let inv_freq = self.inv_freq.clone();
7322 let (nh, nkv, hd, hs) = (
7323 self.num_heads,
7324 self.num_kv_heads,
7325 self.head_dim,
7326 self.hidden_size,
7327 );
7328 let loop_end = if self.loop_final_norm {
7332 ((li0 / self.physical_layers) + 1) * self.physical_layers
7333 } else {
7334 self.num_layers
7335 };
7336 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7337 let mut stored_at: Vec<usize> = Vec::new();
7338 for li in li0..self.num_layers.min(loop_end).min(cap) {
7339 let lw = &self.weights.layers[self.phys_layer(li)];
7340 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7341 break;
7342 }
7343 let AttnKind::Full {
7344 wq,
7345 wk,
7346 wv,
7347 wo,
7348 q_norm,
7349 k_norm,
7350 output_gate: false,
7351 softplus_gate: None,
7352 bias,
7353 } = &lw.attn
7354 else {
7355 break;
7356 };
7357 let FfnKind::Dense(d) = &lw.ffn else { break };
7358 if d.act != Act::Silu || !d.segs.is_empty() {
7359 break;
7360 }
7361 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7366 t.q8_row_parts()
7367 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7368 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7369 }
7370 let parts = (
7371 cw(wq),
7372 cw(wk),
7373 cw(wv),
7374 cw(wo),
7375 cw(&d.gate_proj),
7376 cw(&d.up_proj),
7377 cw(&d.down_proj),
7378 );
7379 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7380 else {
7381 break;
7382 };
7383 let layer = &self.kv_cache.layers[li];
7384 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7385 break;
7386 }
7387 stored_at.push(layer.head_len(0));
7388 layers.push(crate::gpu_metal::ChunkLayer {
7389 model: &model,
7390 kv_id: self.graph_kv_id,
7391 layer: li,
7392 wq: pq,
7393 wk: pk,
7394 wv: pv,
7395 wo: po,
7396 gate: pg,
7397 up: pu,
7398 down: pd,
7399 input_norm: &lw.input_norm,
7400 post_norm: &lw.post_norm,
7401 bias: bias
7402 .as_ref()
7403 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7404 q_norm: q_norm.as_deref(),
7405 k_norm: k_norm.as_deref(),
7406 inv_freq: &inv_freq,
7407 rd: self.rotary_dim,
7408 nh,
7409 nkv,
7410 hd,
7411 hs,
7412 inter: d.gate_proj.rows(),
7413 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7414 late_qk_norm: self.qk_norm_after_rope,
7415 eps: self.rms_eps as f32,
7416 });
7417 }
7418 if layers.is_empty() {
7419 return li0;
7420 }
7421 let row = nkv * hd;
7422 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7423 .iter()
7424 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7425 .collect();
7426 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7427 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7428 let li = layers[i].layer;
7429 let layer = &self.kv_cache.layers[li];
7430 io.push(crate::gpu_metal::ChunkIo {
7431 cpu_stored: stored_at[i],
7432 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7433 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7434 out_k: ok,
7435 out_v: ov,
7436 imp: oi,
7437 });
7438 }
7439 let n_run = layers.len();
7440 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7441 let ep = embed_ids.and_then(|ids| {
7444 self.weights
7445 .embed_tokens
7446 .q8_row_parts()
7447 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7448 idx,
7449 rows,
7450 row_scale: rs,
7451 ids,
7452 mult: self.embed_multiplier,
7453 })
7454 });
7455 if embed_ids.is_some() && ep.is_none() {
7456 return li0;
7457 }
7458 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7459 return li0;
7460 }
7461 drop(io);
7462 drop(layers);
7463 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7466 let li = li0 + i;
7467 let layer = &mut self.kv_cache.layers[li];
7468 for bi in 0..b {
7469 layer.append(
7470 &ok[bi * row..(bi + 1) * row],
7471 &ov[bi * row..(bi + 1) * row],
7472 &[],
7473 );
7474 }
7475 layer.accumulate_imp(oi);
7476 }
7477 last
7478 }
7479
7480 fn layer_is_local(&self, li: usize) -> bool {
7483 if let Some(layers) = &self.sliding_layers {
7484 return layers.get(li).copied().unwrap_or(false);
7485 }
7486 match self.swa {
7487 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7488 None => false,
7489 }
7490 }
7491
7492 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7495 if self.layer_is_local(li) {
7496 if let Some(f) = &self.inv_freq_local {
7497 return f.clone();
7498 }
7499 } else if let Some(f) = &self.inv_freq_global {
7500 return f.clone();
7501 }
7502 self.inv_freq.clone()
7503 }
7504
7505 fn layer_window(&self, li: usize) -> Option<usize> {
7507 self.swa
7508 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7509 }
7510
7511 fn layer_num_heads(&self, li: usize) -> usize {
7512 self.attention_heads_per_layer
7513 .as_ref()
7514 .and_then(|v| v.get(li).copied())
7515 .unwrap_or(self.num_heads)
7516 }
7517
7518 fn layer_rope_scale(&self, li: usize) -> f32 {
7519 if self.layer_is_local(li) {
7520 self.rope_scale_local
7521 } else {
7522 self.rope_scale
7523 }
7524 }
7525
7526 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7529 if !self.layer_is_local(li) {
7530 if let Some((ghd, gkv)) = self.global_attn {
7531 return (gkv, ghd, ghd);
7532 }
7533 }
7534 (
7535 self.num_kv_heads,
7536 self.head_dim,
7537 if self.layer_is_local(li) {
7538 self.rotary_dim_local.unwrap_or(self.rotary_dim)
7539 } else {
7540 self.rotary_dim
7541 },
7542 )
7543 }
7544
7545 fn forward_layers(
7547 &mut self,
7548 hidden: &[f32],
7549 position: usize,
7550 task_mask: Option<&TaskMask>,
7551 ) -> Vec<f32> {
7552 let out = self.forward_layers_upto(hidden, position, task_mask, None);
7553 self.o1_progress();
7554 out
7555 }
7556
7557 pub fn embed_id(&self, id: u32) -> Vec<f32> {
7565 self.embed_single(id)
7566 }
7567
7568 pub fn split_supported(&self) -> Result<(), String> {
7572 if self.dsv4.is_some() {
7573 return Err(
7574 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7575 );
7576 }
7577 if self.dsv41.is_some() {
7578 return Err(
7579 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7580 .into(),
7581 );
7582 }
7583 if self.qwen4_exp.is_some() {
7584 return Err(
7585 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7586 );
7587 }
7588 if self.g3n.is_some() {
7589 return Err(
7590 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7591 );
7592 }
7593 Ok(())
7594 }
7595
7596 pub fn forward_span(
7601 &mut self,
7602 hidden: &[f32],
7603 position: usize,
7604 from: usize,
7605 upto: usize,
7606 task_mask: Option<&TaskMask>,
7607 ) -> Result<Vec<f32>, String> {
7608 self.split_supported()?;
7609 if from > upto || upto >= self.num_layers {
7610 return Err(format!(
7611 "forward_span: layer range {from}..={upto} outside 0..{}",
7612 self.num_layers
7613 ));
7614 }
7615 if hidden.len() != self.hidden_size {
7616 return Err(format!(
7617 "forward_span: hidden len {} ≠ hidden_size {}",
7618 hidden.len(),
7619 self.hidden_size
7620 ));
7621 }
7622 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7623 self.o1_progress();
7624 if self
7625 .graph_failed
7626 .swap(false, std::sync::atomic::Ordering::Relaxed)
7627 {
7628 self.cancel
7629 .store(false, std::sync::atomic::Ordering::Relaxed);
7630 self.clear_sequence_state();
7631 return Err("forward_span: deferred O(1) transition failed".into());
7632 }
7633 Ok(out)
7634 }
7635
7636 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7639 let normed = inference::rms_norm(
7640 hidden,
7641 &self.weights.final_norm,
7642 self.rms_eps,
7643 self.norm_style,
7644 );
7645 self.lm_head_forward(&normed)
7646 }
7647
7648 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7650 sampler::sample_with_scratch(
7651 logits,
7652 &self.sampler_config,
7653 past_tokens,
7654 &mut self.rng,
7655 &mut self.sampler_scratch,
7656 )
7657 }
7658
7659 pub fn reset_session(&mut self) {
7661 self.clear_sequence_state();
7662 }
7663
7664 pub fn prefill_span_ids(
7670 &mut self,
7671 ids: &[u32],
7672 start_pos: usize,
7673 upto: usize,
7674 task_mask: Option<&TaskMask>,
7675 ) -> Result<Vec<f32>, String> {
7676 self.split_supported()?;
7677 if upto >= self.num_layers {
7678 return Err(format!(
7679 "prefill_span_ids: upto {upto} outside 0..{}",
7680 self.num_layers
7681 ));
7682 }
7683 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7687 let out =
7688 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7689 self.check_o1_progress_failure("prefill_span_ids")?;
7690 Ok(out)
7691 } else {
7692 let hs = self.hidden_size;
7693 let mut out = Vec::with_capacity(ids.len() * hs);
7694 for (i, &id) in ids.iter().enumerate() {
7695 let emb = self.embed_id(id);
7696 out.extend_from_slice(&self.forward_span(
7697 &emb,
7698 start_pos + i,
7699 0,
7700 upto,
7701 task_mask,
7702 )?);
7703 }
7704 Ok(out)
7705 }
7706 }
7707
7708 pub fn prefill_span_hidden(
7711 &mut self,
7712 hidden: &[f32],
7713 start_pos: usize,
7714 from: usize,
7715 upto: usize,
7716 task_mask: Option<&TaskMask>,
7717 ) -> Result<Vec<f32>, String> {
7718 self.split_supported()?;
7719 let hs = self.hidden_size;
7720 if hidden.is_empty() || hidden.len() % hs != 0 {
7721 return Err(format!(
7722 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7723 hidden.len()
7724 ));
7725 }
7726 if from > upto || upto >= self.num_layers {
7727 return Err(format!(
7728 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7729 self.num_layers
7730 ));
7731 }
7732 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7733 let out = self.prefill_batch_span(
7734 PrefillIn::Hidden(hidden),
7735 start_pos,
7736 task_mask,
7737 from,
7738 upto + 1,
7739 );
7740 self.check_o1_progress_failure("prefill_span_hidden")?;
7741 Ok(out)
7742 } else {
7743 let b = hidden.len() / hs;
7744 let mut out = Vec::with_capacity(hidden.len());
7745 for i in 0..b {
7746 let h = self.forward_span(
7747 &hidden[i * hs..(i + 1) * hs],
7748 start_pos + i,
7749 from,
7750 upto,
7751 task_mask,
7752 )?;
7753 out.extend_from_slice(&h);
7754 }
7755 Ok(out)
7756 }
7757 }
7758
7759 fn try_token_graph_wgpu(
7763 &self,
7764 hidden: &[f32],
7765 position: usize,
7766 logits_out: &mut Vec<f32>,
7767 layers_run: &mut usize,
7768 ) -> Option<Result<Vec<f32>, ()>> {
7769 self.try_token_graph_wgpu_steps(
7770 hidden,
7771 position,
7772 logits_out,
7773 1,
7774 None,
7775 Some(layers_run),
7776 0,
7777 self.num_layers,
7778 )
7779 }
7780
7781 fn try_token_graph_wgpu_span(
7785 &self,
7786 hidden: &[f32],
7787 position: usize,
7788 logits_out: &mut Vec<f32>,
7789 from: usize,
7790 upto_excl: usize,
7791 layers_run: &mut usize,
7792 ) -> Option<Result<Vec<f32>, ()>> {
7793 self.try_token_graph_wgpu_steps(
7794 hidden,
7795 position,
7796 logits_out,
7797 1,
7798 None,
7799 Some(layers_run),
7800 from,
7801 upto_excl,
7802 )
7803 }
7804
7805 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7809 if self.o1_active() || self.attn_softcap > 0.0 {
7810 return None;
7811 }
7812 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7813 if !graph_on || crate::gpu::graph_unsupported() {
7814 return None;
7821 }
7822 let emb = self.embed_single(t_next);
7823 let mut lg = Vec::new();
7824 let mut ids = Vec::new();
7825 match self.try_token_graph_wgpu_steps(
7826 &emb,
7827 position,
7828 &mut lg,
7829 k,
7830 Some(&mut ids),
7831 None,
7832 0,
7833 self.num_layers,
7834 ) {
7835 Some(Ok(_)) => {}
7836 Some(Err(())) => {
7837 self.graph_failed
7842 .store(true, std::sync::atomic::Ordering::Relaxed);
7843 return None;
7844 }
7845 None => return None,
7846 }
7847 (ids.len() == k).then_some(ids)
7848 }
7849
7850 fn try_token_graph_wgpu_steps(
7854 &self,
7855 hidden: &[f32],
7856 position: usize,
7857 logits_out: &mut Vec<f32>,
7858 steps: usize,
7859 ids_out: Option<&mut Vec<u32>>,
7860 layers_run: Option<&mut usize>,
7861 from: usize,
7862 upto_excl: usize,
7863 ) -> Option<Result<Vec<f32>, ()>> {
7864 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7867 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7868 return None;
7872 }
7873 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7878 .map(|li| {
7879 if !o1_gpu {
7880 return None;
7881 }
7882 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7883 })
7884 .collect();
7885 if self.o1_active() && o1_gpu {
7886 let want: usize = (from..upto_excl)
7889 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7890 .count();
7891 let have = o1_views.iter().filter(|v| v.is_some()).count();
7892 if want == 0 || have != want {
7893 use std::sync::atomic::{AtomicUsize, Ordering};
7903 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7904 let code = have * 1000 + want;
7905 if LAST.swap(code, Ordering::Relaxed) != code {
7906 tracing::warn!(
7907 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7908 );
7909 }
7910 return None;
7911 }
7912 }
7913 let nh = self.num_heads;
7914 let (nkv, hd, rd) = self.layer_geom(0);
7915 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7916 let mut layers = Vec::with_capacity(upto_excl - from);
7917 let mut model = None;
7918 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7919 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7920 if let Some((m, i, kind, rs)) = t
7921 .graph_weight()
7922 .or_else(|| t.graph_weight_descriptor())
7923 {
7924 let name = &m.tensors[i].name;
7925 let prism = if crate::prism::is_inverse_embedding(m, name) {
7926 crate::gpu::GraphPrismOp::InverseEmbedding
7927 } else if crate::prism::is_forward_weight(m, name) {
7928 crate::gpu::GraphPrismOp::Forward
7929 } else {
7930 crate::gpu::GraphPrismOp::None
7931 };
7932 return Some(crate::gpu::GraphW {
7933 idx: i,
7934 kind,
7935 row_scale: rs,
7936 data: &[],
7937 prism,
7938 affine: crate::prism::is_affine_target(m, name),
7939 });
7940 }
7941 match t.as_f32() {
7943 Some(d) => Some(crate::gpu::GraphW {
7944 idx: 0,
7945 kind: 4,
7946 row_scale: &[],
7947 data: d,
7948 prism: crate::gpu::GraphPrismOp::None,
7949 affine: false,
7950 }),
7951 None => {
7952 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
7953 eprintln!("batch graph: weight has no graph/f32 representation");
7954 }
7955 None
7956 }
7957 }
7958 }
7959 for li in from..upto_excl {
7960 let lw = &self.weights.layers[self.phys_layer(li)];
7961 if dbg {
7962 let ak = match &lw.attn {
7963 AttnKind::Mla(_) => "Mla".into(),
7964 AttnKind::Full {
7965 output_gate, bias, ..
7966 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7967 AttnKind::LinearGdn(_) => "LinearGdn".into(),
7968 AttnKind::Kda(_) => "Kda".into(),
7969 AttnKind::Linear(_) => "Linear".into(),
7970 AttnKind::ShortConv(_) => "ShortConv".into(),
7971 };
7972 let fk = match &lw.ffn {
7973 FfnKind::Dense(_) => "Dense",
7974 FfnKind::Moe(_) => "Moe",
7975 FfnKind::DenseMoe(_) => "DenseMoe",
7976 };
7977 eprintln!("graph L{li}: attn={ak} ffn={fk}");
7978 }
7979 let gffn = match &lw.ffn {
7980 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7984 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7985 gate: gw(&d.gate_proj)?,
7986 up: gw(&d.up_proj)?,
7987 down: gw(&d.down_proj)?,
7988 },
7989 FfnKind::Moe(m) => {
7990 if m.route_tau.is_some() || m.mask.is_some() {
7998 return None;
7999 }
8000 let shared = m.shared.as_ref();
8001 let has_shared = shared.is_some();
8002 let shared_gated = matches!(shared, Some((_, Some(_))));
8003 let sgate = match shared {
8004 Some((_, Some(sg))) => gw(sg)?,
8005 _ => gw(&m.router)?,
8009 };
8010 let router = gw(&m.router)?;
8011 if router.prism != crate::gpu::GraphPrismOp::None
8017 || sgate.prism != crate::gpu::GraphPrismOp::None
8018 || router.affine
8019 || sgate.affine
8020 {
8021 tracing::warn!(
8022 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8023 );
8024 return None;
8025 }
8026 let inter = m.experts.first()?.gate_proj.rows();
8027 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8028 let mut q4tp: Option<bool> = None;
8031 let mut gu_q2: Option<bool> = None;
8034 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8035 if !matches!(e.act, Act::Silu)
8036 || e.gate_proj.rows() != inter
8037 || e.up_proj.rows() != inter
8038 {
8039 return None;
8040 }
8041 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8046 let Some((em, ei, _, _)) = expert_weight
8047 .graph_weight()
8048 .or_else(|| expert_weight.graph_weight_descriptor())
8049 else {
8050 return None;
8051 };
8052 let name = &em.tensors[ei].name;
8053 if crate::prism::is_forward_weight(em, name)
8054 || crate::prism::is_inverse_embedding(em, name)
8055 || crate::prism::is_affine_target(em, name)
8056 {
8057 tracing::warn!(
8058 "resident MoE declined: expert Prism/affine transform is not implemented"
8059 );
8060 return None;
8061 }
8062 }
8063 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8064 Some((mm, gi)) => (
8065 mm,
8066 gi,
8067 e.up_proj.mapped_q4t()?.1,
8068 e.down_proj.mapped_q4t()?.1,
8069 false,
8070 false,
8071 ),
8072 None => match e.gate_proj.mapped_q2tp() {
8073 Some((mm, gi)) => (
8074 mm,
8075 gi,
8076 e.up_proj.mapped_q2tp()?.1,
8077 e.down_proj.mapped_q4tp()?.1,
8078 true,
8079 true,
8080 ),
8081 None => {
8082 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8083 (
8084 mm,
8085 gi,
8086 e.up_proj.mapped_q4tp()?.1,
8087 e.down_proj.mapped_q4tp()?.1,
8088 true,
8089 false,
8090 )
8091 }
8092 },
8093 };
8094 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8095 {
8096 tracing::warn!(
8102 "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."
8103 );
8104 return None;
8105 }
8106 model.get_or_insert_with(|| mm.clone());
8107 experts.push((gi, ui, di));
8108 }
8109 crate::gpu::GraphFfn::Moe {
8110 router,
8111 shared_gate: sgate,
8112 experts,
8113 n_exp: m.experts.len(),
8114 top_k: std::env::var("CMF_TOPK_PROBE")
8120 .ok()
8121 .and_then(|v| v.parse::<usize>().ok())
8122 .filter(|k| *k > 0 && *k <= m.top_k)
8123 .unwrap_or(m.top_k),
8124 inter,
8125 norm_topk: m.norm_topk_prob,
8126 q4tp: q4tp?,
8127 gu_q2: gu_q2.unwrap_or(false),
8128 sigmoid: m.router_sigmoid,
8129 bias: m.expert_bias.as_deref(),
8130 has_shared,
8131 shared_gated,
8132 route_scale: m.routed_scaling,
8133 }
8134 }
8135 };
8136 let attn = match &lw.attn {
8137 AttnKind::Full {
8138 wq,
8139 wk,
8140 wv,
8141 wo,
8142 q_norm,
8143 k_norm,
8144 output_gate,
8145 softplus_gate,
8146 bias,
8147 } => {
8148 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8149 return None;
8150 }
8151 let (m, _, _, _) = wq
8152 .graph_weight()
8153 .or_else(|| wq.graph_weight_descriptor())?;
8154 model = Some(m.clone());
8155 crate::gpu::GraphAttn::Full {
8156 wq: gw(wq)?,
8157 wk: gw(wk)?,
8158 wv: gw(wv)?,
8159 wo: gw(wo)?,
8160 q_norm: q_norm.as_deref(),
8161 k_norm: k_norm.as_deref(),
8162 late_qk_norm: self.qk_norm_after_rope,
8163 bias: bias
8164 .as_ref()
8165 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8166 output_gate: *output_gate,
8167 cpu_k: self.kv_cache.layers[li].k_heads(),
8168 cpu_v: self.kv_cache.layers[li].v_heads(),
8169 }
8170 }
8171 AttnKind::LinearGdn(w) => {
8172 let cfg = self.gdn_cfg?;
8173 let (m, _, _, _) = w
8174 .in_proj_qkv
8175 .graph_weight()
8176 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8177 model = Some(m.clone());
8178 crate::gpu::GraphAttn::Gdn {
8179 qkv: gw(&w.in_proj_qkv)?,
8180 z: gw(&w.in_proj_z)?,
8181 a: gw(&w.in_proj_a)?,
8182 b: gw(&w.in_proj_b)?,
8183 out: gw(&w.out_proj)?,
8184 conv1d: &w.conv1d,
8185 a_log: &w.a_log,
8186 dt_bias: &w.dt_bias,
8187 norm: &w.norm,
8188 nv: cfg.num_v_heads,
8189 nk: cfg.num_k_heads,
8190 dk: cfg.key_head_dim,
8191 dv: cfg.value_head_dim,
8192 kk: cfg.conv_kernel,
8193 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8194 }
8195 }
8196 AttnKind::ShortConv(w) => {
8197 let cfg = self.short_conv_cfg?;
8198 let (m, _, _, _) = w
8199 .in_proj
8200 .graph_weight()
8201 .or_else(|| w.in_proj.graph_weight_descriptor())?;
8202 model = Some(m.clone());
8203 crate::gpu::GraphAttn::ShortConv {
8204 inp: gw(&w.in_proj)?,
8205 out: gw(&w.out_proj)?,
8206 taps: &w.conv,
8207 kernel: cfg.kernel,
8208 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8209 }
8210 }
8211 _ => return None,
8212 };
8213 layers.push(crate::gpu::GraphLayer {
8214 input_norm: &lw.input_norm,
8215 attn,
8216 post_norm: &lw.post_norm,
8217 ffn: gffn,
8218 });
8219 }
8220 let model = model?;
8221 let lm_gw = if upto_excl == self.num_layers
8227 && self.graph_want_logits
8228 && std::env::var("CMF_GPU_LMHEAD")
8229 .map(|v| v != "0")
8230 .unwrap_or(true)
8231 {
8232 self.weights
8233 .lm_head
8234 .graph_weight()
8235 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8236 .map(|(m, i, kind, rs)| {
8237 let name = &m.tensors[i].name;
8238 let prism = if crate::prism::is_inverse_embedding(m, name) {
8239 crate::gpu::GraphPrismOp::InverseEmbedding
8240 } else if crate::prism::is_forward_weight(m, name) {
8241 crate::gpu::GraphPrismOp::Forward
8242 } else {
8243 crate::gpu::GraphPrismOp::None
8244 };
8245 (
8246 crate::gpu::GraphW {
8247 idx: i,
8248 kind,
8249 row_scale: rs,
8250 data: &[],
8251 prism,
8252 affine: crate::prism::is_affine_target(m, name),
8253 },
8254 self.weights.lm_head.rows(),
8255 )
8256 })
8257 } else {
8258 None
8259 };
8260 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8261 let emb_gw = if steps > 1 {
8263 self.weights
8264 .embed_tokens
8265 .graph_weight()
8266 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8267 .map(|(m, i, kind, rs)| {
8268 let name = &m.tensors[i].name;
8269 let prism = if crate::prism::is_inverse_embedding(m, name) {
8270 crate::gpu::GraphPrismOp::InverseEmbedding
8271 } else if crate::prism::is_forward_weight(m, name) {
8272 crate::gpu::GraphPrismOp::Forward
8273 } else {
8274 crate::gpu::GraphPrismOp::None
8275 };
8276 (
8277 crate::gpu::GraphW {
8278 idx: i,
8279 kind,
8280 row_scale: rs,
8281 data: &[],
8282 prism,
8283 affine: crate::prism::is_affine_target(m, name),
8284 },
8285 self.weights.embed_tokens.rows(),
8286 self.embed_multiplier,
8287 )
8288 })
8289 } else {
8290 None
8291 };
8292
8293 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8299 (from..upto_excl.min(self.num_layers - 1))
8300 .filter(|&li| (li + 1) % self.physical_layers == 0)
8301 .map(|li| li - from)
8302 .collect()
8303 } else {
8304 Vec::new()
8305 };
8306 let mut h = hidden.to_vec();
8307 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8313 let outcome = crate::gpu::forward_token_graph(
8314 &model,
8315 self.graph_kv_id,
8316 &layers,
8317 &o1_views,
8318 self.o1_epoch,
8319 &self.inv_freq,
8320 &mut h,
8321 nh,
8322 nkv,
8323 hd,
8324 self.attn_scale,
8325 rd,
8326 self.hidden_size,
8327 self.intermediate_size,
8328 position,
8329 self.kv_cache.max_seq_len,
8330 gemma,
8331 self.rms_eps as f32,
8332 lm,
8333 &self.weights.final_norm,
8334 logits_out,
8335 &loop_norm_at,
8336 steps,
8337 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8338 ids_out,
8339 layers_run,
8340 from,
8341 dump_hidden,
8342 );
8343 match outcome {
8344 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8345 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8346 crate::gpu::TokenGraphOutcome::Declined => None,
8347 }
8348 }
8349
8350 #[cfg(target_os = "macos")]
8359 #[allow(clippy::type_complexity)]
8360 fn metal_rows_plan(
8361 &self,
8362 ) -> Option<(
8363 Vec<MetalRowsItem<'_>>,
8364 std::sync::Arc<cortiq_core::CmfModel>,
8365 Option<crate::gpu_metal::GdnGpuCfg>,
8366 )> {
8367 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8368 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8369 if !graph_force
8370 || !crate::gpu::enabled_here()
8371 || std::env::var("CMF_GPU_BLOCK")
8372 .map(|v| v == "0")
8373 .unwrap_or(false)
8374 || self.attn_softcap > 0.0
8375 || self.o1_active()
8376 || self.swa.is_some()
8377 || self.global_attn.is_some()
8378 || self.attention_heads_per_layer.is_some()
8379 || self.attn_v_norm
8380 || self.loop_final_norm
8381 {
8382 return None;
8383 }
8384 let attend_contract = self.head_dim % 4 == 0
8385 && self.head_dim <= 256
8386 && self.rotary_dim >= 2
8387 && self.rotary_dim <= self.head_dim
8388 && (self.rotary_dim / 2) % 32 == 0
8389 && self.num_kv_heads > 0
8390 && self.num_heads % self.num_kv_heads == 0;
8391 if !attend_contract {
8392 return None;
8393 }
8394 let mut plan: Vec<MetalRowsItem> = Vec::new();
8395 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8396 for li in 0..self.num_layers {
8397 let lw = &self.weights.layers[self.phys_layer(li)];
8398 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8399 return None;
8400 }
8401 let ffn = match &lw.ffn {
8402 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8403 let (Some(g), Some(u), Some(dn)) = (
8404 d.gate_proj.metal_graph_parts(),
8405 d.up_proj.metal_graph_parts(),
8406 d.down_proj.metal_graph_parts(),
8407 ) else {
8408 return None;
8409 };
8410 MetalFfn::Dense {
8411 gate: g,
8412 up: u,
8413 down: dn,
8414 }
8415 }
8416 _ => return None,
8417 };
8418 match &lw.attn {
8419 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8420 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8421 w.in_proj_qkv.metal_graph_parts(),
8422 w.in_proj_z.metal_graph_parts(),
8423 w.in_proj_a.f32_parts(),
8424 w.in_proj_b.f32_parts(),
8425 w.out_proj.metal_graph_parts(),
8426 ) else {
8427 return None;
8428 };
8429 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8430 model_ref.get_or_insert_with(|| model.clone());
8431 }
8432 let gl = GdnGpuLayer {
8433 attn_norm: &lw.input_norm,
8434 post_norm: &lw.post_norm,
8435 qkv,
8436 z,
8437 a,
8438 b: bb,
8439 out,
8440 ffn,
8441 conv1d: &w.conv1d,
8442 a_log: &w.a_log,
8443 dt_bias: &w.dt_bias,
8444 gnorm: &w.norm,
8445 };
8446 match plan.last_mut() {
8447 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8448 _ => plan.push(MetalRowsItem::Gdn {
8449 run: vec![gl],
8450 first: li,
8451 }),
8452 }
8453 }
8454 AttnKind::Full {
8455 wq,
8456 wk,
8457 wv,
8458 wo,
8459 q_norm,
8460 k_norm,
8461 output_gate,
8462 softplus_gate: None,
8463 bias: None,
8464 } => {
8465 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8466 (
8467 wq.metal_graph_parts(),
8468 wk.metal_graph_parts(),
8469 wv.metal_graph_parts(),
8470 wo.metal_graph_parts(),
8471 )
8472 else {
8473 return None;
8474 };
8475 if let QTensor::Mapped { model, .. } = wq {
8476 model_ref.get_or_insert_with(|| model.clone());
8477 }
8478 let cache = &self.kv_cache.layers[li];
8479 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8480 return None;
8481 }
8482 plan.push(MetalRowsItem::Attn {
8483 l: AttnGpuLayer {
8484 attn_norm: &lw.input_norm,
8485 post_norm: &lw.post_norm,
8486 wq: pq,
8487 wk: pk,
8488 wv: pv,
8489 wo: po,
8490 ffn,
8491 },
8492 li,
8493 q_norm: q_norm.as_deref(),
8494 k_norm: k_norm.as_deref(),
8495 output_gate: *output_gate,
8496 });
8497 }
8498 _ => return None,
8499 }
8500 }
8501 let model = model_ref?;
8502 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8503 nv: cfg.num_v_heads,
8504 nk: cfg.num_k_heads,
8505 dk: cfg.key_head_dim,
8506 dv: cfg.value_head_dim,
8507 kk: cfg.conv_kernel,
8508 hidden: self.hidden_size,
8509 inter: self.intermediate_size,
8510 c_dim: cfg.conv_dim(),
8511 eps: cfg.rms_eps as f32,
8512 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8513 });
8514 Some((plan, model, gcfg))
8515 }
8516
8517 #[cfg(target_os = "macos")]
8519 #[allow(clippy::too_many_arguments)]
8520 fn metal_attn_params<'a>(
8521 li: usize,
8522 cache: &'a crate::kv_cache::LayerKvCache,
8523 q_norm: Option<&'a [f32]>,
8524 k_norm: Option<&'a [f32]>,
8525 output_gate: bool,
8526 inv_freq: &'a [f32],
8527 geom: (usize, usize, usize, usize),
8528 pos0: usize,
8529 kv_id: u64,
8530 scale: f32,
8531 eps: f32,
8532 gemma: bool,
8533 late_qk_norm: bool,
8534 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8535 let (nh, nkv, hd, rd) = geom;
8536 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8537 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8538 let cpu_stored = cpu_k[0].len() / hd;
8539 (
8540 crate::gpu_metal::AttnDeviceParams {
8541 kv_id,
8542 layer: li,
8543 nh,
8544 nkv,
8545 hd,
8546 rd,
8547 position: pos0,
8548 scale,
8549 eps,
8550 gemma,
8551 late_qk_norm,
8552 output_gate,
8553 q_norm,
8554 k_norm,
8555 inv_freq,
8556 cpu_k,
8557 cpu_v,
8558 cpu_stored,
8559 o1: None,
8560 },
8561 cpu_stored,
8562 )
8563 }
8564
8565 #[cfg(target_os = "macos")]
8570 #[allow(clippy::type_complexity)]
8571 fn metal_rows_run(
8572 &mut self,
8573 hiddens: &mut [f32],
8574 pos0: usize,
8575 b: usize,
8576 prefill: bool,
8577 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8578 ) -> MetalRowsRun {
8579 use crate::gpu_metal::{GraphDims, VerifyGraph};
8580 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8581 for l in &mut self.kv_cache.layers {
8582 if l.linear_state.len() != want && want > 0 {
8583 l.linear_state = vec![0f32; want];
8584 }
8585 }
8586 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
8587 return MetalRowsRun::Declined;
8588 };
8589 let dims = GraphDims {
8590 hidden: self.hidden_size,
8591 eps: self.rms_eps as f32,
8592 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8593 };
8594 let Some(mut graph) = (if prefill {
8595 VerifyGraph::new_prefill(&model, dims, hiddens, b)
8596 } else {
8597 VerifyGraph::new(&model, dims, hiddens, b)
8598 }) else {
8599 return MetalRowsRun::Declined;
8600 };
8601 let geom = (
8602 self.num_heads,
8603 self.num_kv_heads,
8604 self.head_dim,
8605 self.rotary_dim,
8606 );
8607 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8608 let eps = self.rms_eps as f32;
8609 let kv_id = self.graph_kv_id;
8610 let inv_freq = self.inv_freq.clone();
8611 for item in &plan {
8612 let ok = match item {
8613 MetalRowsItem::Gdn { run, .. } => gcfg
8614 .as_ref()
8615 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8616 .unwrap_or(false),
8617 MetalRowsItem::Attn {
8618 l,
8619 li,
8620 q_norm,
8621 k_norm,
8622 output_gate,
8623 } => {
8624 let (p, _) = Self::metal_attn_params(
8625 *li,
8626 &self.kv_cache.layers[*li],
8627 *q_norm,
8628 *k_norm,
8629 *output_gate,
8630 &inv_freq,
8631 geom,
8632 pos0,
8633 kv_id,
8634 self.attn_scale,
8635 eps,
8636 gemma,
8637 self.qk_norm_after_rope,
8638 );
8639 graph.attn_ok(l, &p)
8640 }
8641 };
8642 if !ok {
8643 use std::sync::atomic::{AtomicBool, Ordering};
8644 static SAID: AtomicBool = AtomicBool::new(false);
8645 if !SAID.swap(true, Ordering::Relaxed) {
8646 tracing::warn!("metal rows graph: a layer failed preflight — declining");
8647 }
8648 return MetalRowsRun::Declined;
8649 }
8650 }
8651 let lm = match &spec {
8652 Some((lm, _, _)) => {
8653 if !graph.lm_head_ok(*lm) {
8654 return MetalRowsRun::Declined;
8655 }
8656 Some(*lm)
8657 }
8658 None => None,
8659 };
8660 let mut gdn_layers = Vec::new();
8661 let mut attn_layers = Vec::new();
8662 for item in &plan {
8663 match item {
8664 MetalRowsItem::Gdn { run, first } => {
8665 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8666 .iter()
8667 .map(|l| l.linear_state.as_slice())
8668 .collect();
8669 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8670 return MetalRowsRun::Declined;
8671 }
8672 gdn_layers.extend(*first..*first + run.len());
8673 }
8674 MetalRowsItem::Attn {
8675 l,
8676 li,
8677 q_norm,
8678 k_norm,
8679 output_gate,
8680 } => {
8681 let (p, cpu_stored) = Self::metal_attn_params(
8682 *li,
8683 &self.kv_cache.layers[*li],
8684 *q_norm,
8685 *k_norm,
8686 *output_gate,
8687 &inv_freq,
8688 geom,
8689 pos0,
8690 kv_id,
8691 self.attn_scale,
8692 eps,
8693 gemma,
8694 self.qk_norm_after_rope,
8695 );
8696 if !graph.encode_attn_b(l, &p) {
8697 return MetalRowsRun::Declined;
8698 }
8699 attn_layers.push((*li, cpu_stored));
8700 }
8701 }
8702 }
8703 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8704 if !graph.encode_lm_head_b(final_norm, lm) {
8705 return MetalRowsRun::Declined;
8706 }
8707 }
8708 if !graph.sync() {
8709 return MetalRowsRun::Failed;
8710 }
8711 if let Some((lm, _, logits)) = spec {
8712 logits.resize(b * lm.1, 0.0);
8713 if !graph.read_logits(logits) {
8714 return MetalRowsRun::Failed;
8715 }
8716 }
8717 if !graph.read_hidden(hiddens) {
8718 return MetalRowsRun::Failed;
8719 }
8720 MetalRowsRun::Completed(MetalVerifyPending {
8721 graph,
8722 gdn_layers,
8723 attn_layers,
8724 })
8725 }
8726
8727 #[cfg(target_os = "macos")]
8733 fn try_batch_graph_metal(
8734 &mut self,
8735 hiddens: &mut [f32],
8736 positions: &[usize],
8737 b: usize,
8738 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8739 ) -> crate::gpu::BatchGraphOutcome {
8740 let _t0 = std::time::Instant::now();
8741 if positions.len() != b
8742 || positions.windows(2).any(|w| w[1] != w[0] + 1)
8743 || hiddens.len() != b * self.hidden_size
8744 {
8745 return crate::gpu::BatchGraphOutcome::Declined;
8746 }
8747 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec) {
8748 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
8749 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
8750 MetalRowsRun::Completed(pending) => pending,
8751 };
8752 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8753 eprintln!(
8754 "metal-verify: {:.1} ms | b={b}",
8755 _t0.elapsed().as_secs_f64() * 1e3
8756 );
8757 }
8758 self.metal_verify = Some(pending);
8759 crate::gpu::BatchGraphOutcome::Completed
8760 }
8761
8762 #[cfg(target_os = "macos")]
8767 fn prefill_rows_metal(
8768 &mut self,
8769 ids: &[u32],
8770 start_pos: usize,
8771 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8772 ) -> MetalPrefillOutcome {
8773 let b = ids.len();
8774 if b == 0 || b > 512 {
8775 return MetalPrefillOutcome::Declined;
8776 }
8777 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8778 let with_head = spec.is_some();
8779 let hs = self.hidden_size;
8780 let mut hiddens = vec![0f32; b * hs];
8781 for (j, &id) in ids.iter().enumerate() {
8782 let e = self.embed_single(id);
8783 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8784 }
8785 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec) {
8786 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
8787 MetalRowsRun::Failed => {
8788 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8789 return MetalPrefillOutcome::Failed;
8790 }
8791 MetalRowsRun::Completed(pending) => pending,
8792 };
8793 let idxs = pending.gdn_layers.clone();
8795 let mut outs: Vec<&mut [f32]> = self
8796 .kv_cache
8797 .layers
8798 .iter_mut()
8799 .enumerate()
8800 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8801 .map(|(_, l)| l.linear_state.as_mut_slice())
8802 .collect();
8803 if !pending.graph.finish_states(&mut outs) {
8804 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8805 return MetalPrefillOutcome::Failed;
8806 }
8807 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8808 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8812 for (li, cpu_stored) in &pending.attn_layers {
8813 let mut kbuf = vec![0f32; b * nkv * hd];
8814 let mut vbuf = vec![0f32; b * nkv * hd];
8815 if !crate::gpu_metal::kv_mirror_read_rows(
8816 self.graph_kv_id,
8817 *li,
8818 nkv,
8819 hd,
8820 *cpu_stored,
8821 b,
8822 &mut kbuf,
8823 &mut vbuf,
8824 ) {
8825 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8826 return MetalPrefillOutcome::Failed;
8827 }
8828 rows.push((*li, *cpu_stored, kbuf, vbuf));
8829 }
8830 for (li, cpu_stored, kbuf, vbuf) in rows {
8831 let cache = &mut self.kv_cache.layers[li];
8832 for r in 0..b {
8833 cache.append(
8834 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8835 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8836 &[],
8837 );
8838 }
8839 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
8840 }
8841 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8842 if with_head {
8843 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8844 }
8845 MetalPrefillOutcome::Completed(hiddens)
8846 }
8847
8848 #[cfg(target_os = "macos")]
8849 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
8850 self.prefill_rows_metal(ids, start_pos, None)
8851 }
8852
8853 #[cfg(target_os = "macos")]
8858 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
8859 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
8860 return MetalBatchNllOutcome::Declined;
8861 }
8862 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
8863 return MetalBatchNllOutcome::Declined;
8864 };
8865 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
8866 .ok()
8867 .and_then(|v| v.parse::<usize>().ok())
8868 .filter(|&v| (1..=512).contains(&v))
8869 .unwrap_or(32);
8870 let final_norm = self.weights.final_norm.clone();
8871 let mut nll = 0.0f64;
8872 let mut count = 0usize;
8873 let mut pos = 0usize;
8874 let mut completed = 0usize;
8875 while pos < ids.len() {
8876 let end = (pos + chunk).min(ids.len());
8877 let mut logits = Vec::new();
8878 let outcome = self.prefill_rows_metal(
8879 &ids[pos..end],
8880 pos,
8881 Some((lm, &final_norm, &mut logits)),
8882 );
8883 match outcome {
8884 MetalPrefillOutcome::Declined => {
8885 return if completed == 0 {
8886 MetalBatchNllOutcome::Declined
8887 } else {
8888 MetalBatchNllOutcome::Failed(format!(
8889 "ordinary Metal NLL batch declined after {completed} chunks"
8890 ))
8891 };
8892 }
8893 MetalPrefillOutcome::Failed => {
8894 return MetalBatchNllOutcome::Failed(
8895 "ordinary Metal NLL batch failed after admission".to_string(),
8896 );
8897 }
8898 MetalPrefillOutcome::Completed(_) => {}
8899 }
8900 completed += 1;
8901 let vocab = self.vocab_size.min(lm.1);
8902 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
8903 return MetalBatchNllOutcome::Failed(
8904 "ordinary Metal NLL head returned an invalid shape".to_string(),
8905 );
8906 }
8907 for row in 0..(end - pos) {
8908 let absolute = pos + row;
8909 if absolute < start || absolute + 1 >= ids.len() {
8910 continue;
8911 }
8912 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
8913 if let Some(mu) = self.logit_multiplier {
8914 for v in lg.iter_mut() {
8915 *v *= mu;
8916 }
8917 }
8918 if let Some(c) = self.final_softcap {
8919 for v in lg.iter_mut() {
8920 *v = c * (*v / c).tanh();
8921 }
8922 }
8923 let target = ids[absolute + 1] as usize;
8924 if target >= vocab {
8925 return MetalBatchNllOutcome::Failed(format!(
8926 "target token {target} exceeds Metal head rows {vocab}"
8927 ));
8928 }
8929 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8930 let lse: f64 = lg
8931 .iter()
8932 .map(|&v| ((v - max) as f64).exp())
8933 .sum::<f64>()
8934 .ln()
8935 + max as f64;
8936 nll += lse - lg[target] as f64;
8937 count += 1;
8938 }
8939 pos = end;
8940 }
8941 MetalBatchNllOutcome::Completed(nll, count)
8942 }
8943
8944 #[cfg(target_os = "macos")]
8948 fn metal_verify_commit(&mut self, a: usize) -> bool {
8949 let Some(mut pending) = self.metal_verify.take() else {
8950 return false;
8951 };
8952 let n = a + 1;
8953 let idxs = pending.gdn_layers.clone();
8955 let mut outs: Vec<&mut [f32]> = self
8956 .kv_cache
8957 .layers
8958 .iter_mut()
8959 .enumerate()
8960 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8961 .map(|(_, l)| l.linear_state.as_mut_slice())
8962 .collect();
8963 if !pending.graph.commit(n, &mut outs) {
8964 return false;
8965 }
8966 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8967 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8971 for (li, cpu_stored) in &pending.attn_layers {
8972 let mut kbuf = vec![0f32; n * nkv * hd];
8973 let mut vbuf = vec![0f32; n * nkv * hd];
8974 if !crate::gpu_metal::kv_mirror_read_rows(
8975 self.graph_kv_id,
8976 *li,
8977 nkv,
8978 hd,
8979 *cpu_stored,
8980 n,
8981 &mut kbuf,
8982 &mut vbuf,
8983 ) {
8984 return false;
8985 }
8986 rows.push((*li, *cpu_stored, kbuf, vbuf));
8987 }
8988 for (li, cpu_stored, kbuf, vbuf) in rows {
8989 let cache = &mut self.kv_cache.layers[li];
8990 for r in 0..n {
8991 cache.append(
8992 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8993 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8994 &[],
8995 );
8996 }
8997 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
8998 }
8999 true
9000 }
9001
9002 #[cfg(target_os = "macos")]
9008 fn mtp_warm_batch_metal(
9009 &mut self,
9010 m: &mut MtpModule,
9011 pairs: &[(&[f32], u32)],
9012 first_pos: usize,
9013 ) -> bool {
9014 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
9015 let b = pairs.len();
9016 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
9017 return false;
9018 }
9019 let AttnKind::Full {
9020 wq,
9021 wk,
9022 wv,
9023 wo,
9024 q_norm,
9025 k_norm,
9026 output_gate,
9027 softplus_gate: None,
9028 bias: None,
9029 } = &m.layer.attn
9030 else {
9031 return false;
9032 };
9033 let FfnKind::Dense(d) = &m.layer.ffn else {
9034 return false;
9035 };
9036 if !d.segs.is_empty() {
9037 return false;
9038 }
9039 let (Some(pq), Some(pk), Some(pv), Some(po)) =
9040 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9041 else {
9042 return false;
9043 };
9044 let (Some(g), Some(u), Some(dn)) = (
9045 d.gate_proj.q1_parts(),
9046 d.up_proj.q1_parts(),
9047 d.down_proj.q1_parts(),
9048 ) else {
9049 return false;
9050 };
9051 let Some(eh) = m.eh_proj.q1_parts() else {
9052 return false;
9053 };
9054 let QTensor::Mapped { model, .. } = wq else {
9055 return false;
9056 };
9057 let model = model.clone();
9058 let hs = self.hidden_size;
9059 let mut cat = vec![0f32; b * 2 * hs];
9061 for (j, (h, tok)) in pairs.iter().enumerate() {
9062 let e = self.embed_single(*tok);
9063 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9064 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9065 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9066 }
9067 let dims = GraphDims {
9068 hidden: hs,
9069 eps: self.rms_eps as f32,
9070 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9071 };
9072 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9073 return false;
9074 };
9075 let l = AttnGpuLayer {
9076 attn_norm: &m.layer.input_norm,
9077 post_norm: &m.layer.post_norm,
9078 wq: pq,
9079 wk: pk,
9080 wv: pv,
9081 wo: po,
9082 ffn: MetalFfn::Dense {
9083 gate: g,
9084 up: u,
9085 down: dn,
9086 },
9087 };
9088 let (nh, nkv, hd, rd) = (
9089 self.num_heads,
9090 self.num_kv_heads,
9091 self.head_dim,
9092 self.rotary_dim,
9093 );
9094 let inv_freq = self.inv_freq.clone();
9095 let cpu_stored;
9096 {
9097 let cache = &m.kv;
9098 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9099 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9100 cpu_stored = cpu_k[0].len() / hd;
9101 if cpu_stored != first_pos {
9102 return false;
9103 }
9104 let p = AttnDeviceParams {
9105 kv_id: self.mtp_kv_id(),
9106 layer: Self::MTP_LAYER_BASE,
9107 nh,
9108 nkv,
9109 hd,
9110 rd,
9111 position: first_pos,
9112 scale: self.attn_scale,
9113 eps: self.rms_eps as f32,
9114 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9115 late_qk_norm: self.qk_norm_after_rope,
9116 output_gate: *output_gate,
9117 q_norm: q_norm.as_deref(),
9118 k_norm: k_norm.as_deref(),
9119 inv_freq: &inv_freq,
9120 cpu_k,
9121 cpu_v,
9122 cpu_stored,
9123 o1: None,
9124 };
9125 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9126 return false;
9127 }
9128 }
9129 if !graph.sync() {
9130 return false;
9131 }
9132 let mut kbuf = vec![0f32; b * nkv * hd];
9133 let mut vbuf = vec![0f32; b * nkv * hd];
9134 if !crate::gpu_metal::kv_mirror_read_rows(
9135 self.mtp_kv_id(),
9136 Self::MTP_LAYER_BASE,
9137 nkv,
9138 hd,
9139 cpu_stored,
9140 b,
9141 &mut kbuf,
9142 &mut vbuf,
9143 ) {
9144 return false;
9145 }
9146 for r in 0..b {
9147 m.kv.append(
9148 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9149 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9150 &[],
9151 );
9152 }
9153 crate::gpu_metal::kv_mirror_set_stored(
9154 self.mtp_kv_id(),
9155 Self::MTP_LAYER_BASE,
9156 cpu_stored + b,
9157 );
9158 true
9159 }
9160
9161 fn draft_vocab_rows(head_rows: usize) -> usize {
9164 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9165 let n = *N.get_or_init(|| {
9166 std::env::var("CMF_DRAFT_VOCAB")
9167 .ok()
9168 .and_then(|v| v.parse().ok())
9169 .unwrap_or(65536)
9170 });
9171 if n == 0 { head_rows } else { n.min(head_rows) }
9172 }
9173
9174 #[cfg(target_os = "macos")]
9179 fn mtp_step_metal(
9180 &mut self,
9181 m: &mut MtpModule,
9182 hidden: &[f32],
9183 next_token: u32,
9184 position: usize,
9185 want_logits: bool,
9186 ) -> Option<(Vec<f32>, Vec<f32>)> {
9187 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9188 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9189 || !crate::gpu::q1_force()
9190 || !crate::gpu::enabled_here()
9191 || self.attn_softcap > 0.0
9192 || self.attention_heads_per_layer.is_some()
9193 || m.kv.mode != crate::kv_cache::KvMode::F32
9194 || m.kv.o1.is_some()
9195 {
9196 return None;
9197 }
9198 let AttnKind::Full {
9199 wq,
9200 wk,
9201 wv,
9202 wo,
9203 q_norm,
9204 k_norm,
9205 output_gate,
9206 softplus_gate: None,
9207 bias: None,
9208 } = &m.layer.attn
9209 else {
9210 return None;
9211 };
9212 let FfnKind::Dense(d) = &m.layer.ffn else {
9213 return None;
9214 };
9215 if d.act != Act::Silu || !d.segs.is_empty() {
9216 return None;
9217 }
9218 let (pq, pk, pv, po) = (
9219 wq.q1_parts()?,
9220 wk.q1_parts()?,
9221 wv.q1_parts()?,
9222 wo.q1_parts()?,
9223 );
9224 let (g, u, dn) = (
9225 d.gate_proj.q1_parts()?,
9226 d.up_proj.q1_parts()?,
9227 d.down_proj.q1_parts()?,
9228 );
9229 let QTensor::Mapped { model, .. } = wq else {
9230 return None;
9231 };
9232 let model = model.clone();
9233 let lm = if want_logits {
9234 Some(self.weights.lm_head.q1_parts()?)
9235 } else {
9236 None
9237 };
9238 let dims = GraphDims {
9239 hidden: self.hidden_size,
9240 eps: self.rms_eps as f32,
9241 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9242 };
9243 let hs = self.hidden_size;
9246 let mut x = vec![0f32; hs];
9247 let mut graph = TokenGraph::new(&model, dims, &x)?;
9248 let mut folded = false;
9249 if let Some(eh) = m.eh_proj.q1_parts() {
9250 let e = self.embed_single(next_token);
9251 let mut cat = vec![0.0f32; 2 * hs];
9252 let (cat_e, cat_h) = cat.split_at_mut(hs);
9253 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9254 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9255 folded = graph.encode_input_proj(eh, &cat);
9256 }
9257 if !folded {
9258 x = self.mtp_block_input(m, hidden, next_token);
9259 graph = TokenGraph::new(&model, dims, &x)?;
9260 }
9261 let l = AttnGpuLayer {
9262 attn_norm: &m.layer.input_norm,
9263 post_norm: &m.layer.post_norm,
9264 wq: pq,
9265 wk: pk,
9266 wv: pv,
9267 wo: po,
9268 ffn: MetalFfn::Dense {
9269 gate: g,
9270 up: u,
9271 down: dn,
9272 },
9273 };
9274 let (nh, nkv, hd, rd) = (
9275 self.num_heads,
9276 self.num_kv_heads,
9277 self.head_dim,
9278 self.rotary_dim,
9279 );
9280 let inv_freq = self.inv_freq.clone();
9281 {
9282 let cache = &m.kv;
9283 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9284 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9285 let cpu_stored = cpu_k[0].len() / hd;
9286 let p = AttnDeviceParams {
9287 kv_id: self.mtp_kv_id(),
9288 layer: Self::MTP_LAYER_BASE,
9289 nh,
9290 nkv,
9291 hd,
9292 rd,
9293 position,
9294 scale: self.attn_scale,
9295 eps: self.rms_eps as f32,
9296 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9297 late_qk_norm: self.qk_norm_after_rope,
9298 output_gate: *output_gate,
9299 q_norm: q_norm.as_deref(),
9300 k_norm: k_norm.as_deref(),
9301 inv_freq: &inv_freq,
9302 cpu_k,
9303 cpu_v,
9304 cpu_stored,
9305 o1: None,
9306 };
9307 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9308 return None;
9309 }
9310 }
9311 let draft_rows = if let Some(lm) = lm {
9317 Self::draft_vocab_rows(lm.1)
9318 } else {
9319 0
9320 };
9321 if let Some(lm) = lm {
9322 if !graph.lm_head_ok(lm) {
9323 return None;
9324 }
9325 if draft_rows < lm.1 {
9326 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9327 return None;
9328 }
9329 } else {
9330 graph.encode_lm_head(&m.final_norm, lm);
9331 }
9332 }
9333 if graph.sync_checked().is_err() {
9334 return None;
9335 }
9336 let mut logits = Vec::new();
9337 if let Some(lm) = lm {
9338 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
9339 logits = attention::take_buf(n_read);
9340 graph.read_logits(&mut logits);
9341 logits.resize(self.vocab_size, f32::NEG_INFINITY);
9343 }
9344 graph.finish(&mut x);
9345 let mut krow = attention::take_buf(nkv * hd);
9346 let mut vrow = attention::take_buf(nkv * hd);
9347 if crate::gpu_metal::kv_mirror_read_last(
9348 self.mtp_kv_id(),
9349 Self::MTP_LAYER_BASE,
9350 nkv,
9351 hd,
9352 &mut krow,
9353 &mut vrow,
9354 ) {
9355 m.kv.append(&krow, &vrow, &[]);
9356 }
9357 attention::recycle_buf(&mut krow);
9358 attention::recycle_buf(&mut vrow);
9359 Some((logits, x))
9360 }
9361
9362 fn try_batch_graph_wgpu(
9363 &self,
9364 hiddens: &mut [f32],
9365 positions: &[usize],
9366 k: usize,
9367 spec: Option<crate::gpu::SpecTail<'_>>,
9368 ) -> crate::gpu::BatchGraphOutcome {
9369 let _tb = std::time::Instant::now();
9370 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
9371 if self.attn_softcap > 0.0 {
9372 return crate::gpu::BatchGraphOutcome::Declined; }
9374 let nh = self.num_heads;
9375 let (nkv, hd, rd) = self.layer_geom(0);
9376 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9377 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9378 if let Some((m, i, kind, rs)) = t
9379 .graph_weight()
9380 .or_else(|| t.graph_weight_descriptor())
9381 {
9382 let name = &m.tensors[i].name;
9383 let prism = if crate::prism::is_inverse_embedding(m, name) {
9384 crate::gpu::GraphPrismOp::InverseEmbedding
9385 } else if crate::prism::is_forward_weight(m, name) {
9386 crate::gpu::GraphPrismOp::Forward
9387 } else {
9388 crate::gpu::GraphPrismOp::None
9389 };
9390 return Some(crate::gpu::GraphW {
9391 idx: i,
9392 kind,
9393 row_scale: rs,
9394 data: &[],
9395 prism,
9396 affine: crate::prism::is_affine_target(m, name),
9397 });
9398 }
9399 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9400 eprintln!(
9401 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
9402 t.rows(),
9403 t.cols()
9404 );
9405 }
9406 t.as_f32().map(|d| crate::gpu::GraphW {
9407 idx: 0,
9408 kind: 4,
9409 row_scale: &[],
9410 data: d,
9411 prism: crate::gpu::GraphPrismOp::None,
9412 affine: false,
9413 })
9414 }
9415 let built: Option<(
9416 Vec<crate::gpu::GraphLayer<'_>>,
9417 std::sync::Arc<cortiq_core::CmfModel>,
9418 )> = (|| {
9419 let mut layers = Vec::with_capacity(self.num_layers);
9420 let mut model = None;
9421 for li in 0..self.num_layers {
9422 let lw = &self.weights.layers[self.phys_layer(li)];
9423 let gffn = match &lw.ffn {
9430 FfnKind::Dense(d) if !d.segs.is_empty() => {
9431 if batch_debug {
9432 eprintln!("batch graph: dense segmented FFN at layer {li}");
9433 }
9434 return None;
9435 }
9436 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9437 gate: gw(&d.gate_proj)?,
9438 up: gw(&d.up_proj)?,
9439 down: gw(&d.down_proj)?,
9440 },
9441 FfnKind::Moe(m) => {
9442 if m.route_tau.is_some() || m.mask.is_some() {
9449 return None;
9450 }
9451 let (se, sg) = m.shared.as_ref()?;
9454 let shared_gated = sg.is_some();
9455 let sgate = match sg {
9456 Some(sg) => gw(sg)?,
9457 None => gw(&m.router)?,
9460 };
9461 let router = gw(&m.router)?;
9462 if router.prism != crate::gpu::GraphPrismOp::None
9468 || router.affine
9469 || sgate.prism != crate::gpu::GraphPrismOp::None
9470 || sgate.affine
9471 {
9472 return None;
9473 }
9474 let inter = m.experts.first()?.gate_proj.rows();
9475 let mut experts = Vec::with_capacity(m.experts.len() + 1);
9476 let mut q4tp: Option<bool> = None;
9477 let mut gu_q2: Option<bool> = None;
9478 for e in m.experts.iter().chain(std::iter::once(se)) {
9479 if !matches!(e.act, Act::Silu)
9480 || e.gate_proj.rows() != inter
9481 || e.up_proj.rows() != inter
9482 {
9483 return None;
9484 }
9485 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
9489 Some((mm, gi)) => (
9490 mm,
9491 gi,
9492 e.up_proj.mapped_q4t()?.1,
9493 e.down_proj.mapped_q4t()?.1,
9494 false,
9495 false,
9496 ),
9497 None => match e.gate_proj.mapped_q2tp() {
9498 Some((mm, gi)) => (
9499 mm,
9500 gi,
9501 e.up_proj.mapped_q2tp()?.1,
9502 e.down_proj.mapped_q4tp()?.1,
9503 true,
9504 true,
9505 ),
9506 None => {
9507 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
9508 (
9509 mm,
9510 gi,
9511 e.up_proj.mapped_q4tp()?.1,
9512 e.down_proj.mapped_q4tp()?.1,
9513 true,
9514 false,
9515 )
9516 }
9517 },
9518 };
9519 if *q4tp.get_or_insert(is_p) != is_p
9520 || *gu_q2.get_or_insert(is_q2) != is_q2
9521 {
9522 return None;
9523 }
9524 if [gi, ui, di].into_iter().any(|idx| {
9525 mm.tensors
9526 .get(idx)
9527 .is_some_and(|t| {
9528 crate::prism::is_forward_weight(mm, &t.name)
9529 || crate::prism::is_affine_target(mm, &t.name)
9530 })
9531 }) {
9532 return None;
9533 }
9534 model.get_or_insert_with(|| mm.clone());
9535 experts.push((gi, ui, di));
9536 }
9537 crate::gpu::GraphFfn::Moe {
9538 router,
9539 shared_gate: sgate,
9540 experts,
9541 n_exp: m.experts.len(),
9542 top_k: m.top_k,
9543 inter,
9544 norm_topk: m.norm_topk_prob,
9545 q4tp: q4tp?,
9546 gu_q2: gu_q2.unwrap_or(false),
9547 sigmoid: m.router_sigmoid,
9548 bias: m.expert_bias.as_deref(),
9549 has_shared: true,
9550 shared_gated,
9551 route_scale: m.routed_scaling,
9552 }
9553 }
9554 _ => return None,
9555 };
9556 let attn = match &lw.attn {
9557 AttnKind::Full {
9558 wq,
9559 wk,
9560 wv,
9561 wo,
9562 q_norm,
9563 k_norm,
9564 output_gate,
9565 softplus_gate,
9566 bias,
9567 } => {
9568 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9569 if batch_debug {
9570 eprintln!(
9571 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
9572 softplus_gate.is_some(),
9573 self.attention_heads_per_layer.is_some()
9574 );
9575 }
9576 return None;
9577 }
9578 let (m, _, _, _) = wq
9579 .graph_weight()
9580 .or_else(|| wq.graph_weight_descriptor())?;
9581 model = Some(m.clone());
9582 crate::gpu::GraphAttn::Full {
9583 wq: gw(wq)?,
9584 wk: gw(wk)?,
9585 wv: gw(wv)?,
9586 wo: gw(wo)?,
9587 q_norm: q_norm.as_deref(),
9588 k_norm: k_norm.as_deref(),
9589 late_qk_norm: self.qk_norm_after_rope,
9590 bias: bias
9591 .as_ref()
9592 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9593 output_gate: *output_gate,
9594 cpu_k: self.kv_cache.layers[li].k_heads(),
9595 cpu_v: self.kv_cache.layers[li].v_heads(),
9596 }
9597 }
9598 AttnKind::LinearGdn(w) => {
9599 let Some(cfg) = self.gdn_cfg else {
9600 if batch_debug {
9601 eprintln!("batch graph: no GDN config at layer {li}");
9602 }
9603 return None;
9604 };
9605 let (m, _, _, _) = w
9606 .in_proj_qkv
9607 .graph_weight()
9608 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9609 model = Some(m.clone());
9610 crate::gpu::GraphAttn::Gdn {
9611 qkv: gw(&w.in_proj_qkv)?,
9612 z: gw(&w.in_proj_z)?,
9613 a: gw(&w.in_proj_a)?,
9614 b: gw(&w.in_proj_b)?,
9615 out: gw(&w.out_proj)?,
9616 conv1d: &w.conv1d,
9617 a_log: &w.a_log,
9618 dt_bias: &w.dt_bias,
9619 norm: &w.norm,
9620 nv: cfg.num_v_heads,
9621 nk: cfg.num_k_heads,
9622 dk: cfg.key_head_dim,
9623 dv: cfg.value_head_dim,
9624 kk: cfg.conv_kernel,
9625 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9626 }
9627 }
9628 _ => return None,
9629 };
9630 layers.push(crate::gpu::GraphLayer {
9631 input_norm: &lw.input_norm,
9632 attn,
9633 post_norm: &lw.post_norm,
9634 ffn: gffn,
9635 });
9636 }
9637 Some((layers, model?))
9638 })();
9639 let Some((layers, model)) = built else {
9640 {
9641 use std::sync::atomic::{AtomicBool, Ordering};
9642 static SAID: AtomicBool = AtomicBool::new(false);
9643 if !SAID.swap(true, Ordering::Relaxed) {
9644 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9645 }
9646 }
9647 return crate::gpu::BatchGraphOutcome::Declined;
9648 };
9649 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9650 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9651 }
9652 crate::gpu::forward_batch_graph(
9653 &model,
9654 self.graph_kv_id,
9655 &layers,
9656 &self.inv_freq,
9657 hiddens,
9658 nh,
9659 nkv,
9660 hd,
9661 rd,
9662 self.hidden_size,
9663 self.intermediate_size,
9664 positions,
9665 self.kv_cache.max_seq_len,
9666 gemma,
9667 self.rms_eps as f32,
9668 self.attn_scale,
9669 k,
9670 &(0..self.num_layers)
9671 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9672 .collect::<Vec<_>>(),
9673 self.o1_epoch,
9674 spec,
9675 )
9676 }
9677
9678 fn draft_probe() -> bool {
9682 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9683 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9684 }
9685
9686 #[cfg(feature = "gpu")]
9698 fn dsv4_spec_on() -> bool {
9699 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9700 *ON.get_or_init(|| {
9701 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9705 return v != "0";
9706 }
9707 std::env::var("CMF_DSV4_SPEC")
9714 .map(|v| v != "0")
9715 .unwrap_or_else(|_| {
9716 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9717 })
9718 })
9719 }
9720
9721 #[cfg(feature = "gpu")]
9728 fn dsv4_spec_step(
9729 &mut self,
9730 tip_token: u32,
9731 t_next: u32,
9732 next_pos: usize,
9733 max_extra: usize,
9734 drafted: &mut usize,
9735 accepted_ctr: &mut usize,
9736 ) -> Option<(Vec<u32>, usize)> {
9737 let t_all = std::time::Instant::now();
9738 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9739 thread_local! {
9740 static LAST: std::cell::Cell<Option<std::time::Instant>> =
9741 const { std::cell::Cell::new(None) };
9742 }
9743 LAST.with(|l| {
9744 if let Some(prev) = l.get() {
9745 eprintln!(
9746 "между раундами {:.1} мс",
9747 prev.elapsed().as_secs_f64() * 1e3
9748 );
9749 }
9750 l.set(Some(std::time::Instant::now()));
9751 });
9752 }
9753 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9754 eprintln!("spec_step: вход pos={next_pos}");
9755 }
9756 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9757 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9758 if self.dspark.is_none() {
9760 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9761 if t.is_empty() {
9762 return None;
9763 }
9764 crate::dsv4::dspark_arm(&t, cfg.dim);
9765 self.dspark = Some(crate::dsv4::DsparkState::new(
9766 self.dsv4_mtp.len(),
9767 &cfg,
9768 t.len(),
9769 ));
9770 }
9771 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9772 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9773 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9774 eprintln!("spec_step: пак не построился (targets {targets:?})");
9775 }
9776 let pack = pack?;
9777 let block = crate::dsv4::dspark_block();
9778 let b_box = self.dsv4.as_mut()?;
9779 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9780 let ds = self.dspark.as_mut()?;
9781 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9784 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9785 if dbg {
9786 eprintln!("spec_step: нет захвата");
9787 }
9788 return None;
9789 }
9790 ds.have_hidden = true;
9791 let tip_pos = next_pos.checked_sub(1)?;
9792 let draft_started = std::time::Instant::now();
9793 let mut conf = Vec::new();
9794 let props = crate::dsv4::dspark_draft_gpu(
9795 g,
9796 &self.dsv4_mtp,
9797 &cfg,
9798 ds,
9799 pack,
9800 st.kv_id,
9801 tip_token,
9802 tip_pos,
9803 self.pool.as_deref(),
9804 &mut conf,
9805 );
9806 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9807 *drafted += block;
9808 if props.is_empty() || props[0] != t_next {
9809 if dbg {
9810 eprintln!(
9811 "spec_step: черновик {} (props0={:?} t_next={t_next})",
9812 if props.is_empty() {
9813 "пуст"
9814 } else {
9815 "мимо"
9816 },
9817 props.first()
9818 );
9819 }
9820 return None;
9821 }
9822 let mut k_verify = crate::dsv4::dspark_verify_k()
9829 .min(props.len())
9830 .min(max_extra.saturating_add(1));
9831 let conf_min = {
9837 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9838 *M.get_or_init(|| {
9839 std::env::var("CMF_DSPARK_CONF_MIN")
9840 .ok()
9841 .and_then(|v| v.parse().ok())
9842 .unwrap_or(0.0)
9843 })
9844 };
9845 if conf_min > 0.0 && conf.len() >= props.len() {
9846 let mut keep = 1usize;
9847 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9848 keep += 1;
9849 }
9850 k_verify = k_verify.min(keep.max(2));
9851 }
9852 if k_verify < 2 {
9853 return None;
9854 }
9855 let mut fed = Vec::with_capacity(k_verify);
9856 fed.push(t_next);
9857 fed.extend_from_slice(&props[1..k_verify]);
9858 let mut argmax = Vec::new();
9859 let mut logits_all = Vec::new();
9860 let mut walked = Vec::new();
9861 let txn = crate::dsv4::dsv4_verify_chunk(
9862 g,
9863 layers,
9864 &cfg,
9865 st,
9866 &fed,
9867 next_pos,
9868 &self.inv_freq,
9869 self.pool.as_deref(),
9870 &targets,
9871 &mut argmax,
9872 &mut logits_all,
9873 &mut walked,
9874 );
9875 if txn.is_none() && dbg {
9876 eprintln!("spec_step: verify отказал");
9877 }
9878 let txn = txn?;
9879 let spec_gpu_end = txn.gpu_end;
9880 let b = fed.len();
9881 let mut accepted = 1usize;
9882 while accepted < b && fed[accepted] == argmax[accepted - 1] {
9883 accepted += 1;
9884 }
9885 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9890 accepted = 1;
9891 }
9892 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9893 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9894 }
9895 let t_fin = std::time::Instant::now();
9896 if !crate::dsv4::dsv4_spec_finish(
9897 g,
9898 layers,
9899 &cfg,
9900 st,
9901 txn,
9902 accepted,
9903 &fed,
9904 &self.inv_freq,
9905 self.pool.as_deref(),
9906 ) {
9907 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9908 return None;
9909 }
9910 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9911 eprintln!(
9912 "finish(k={accepted}): {:.1} мс",
9913 t_fin.elapsed().as_secs_f64() * 1e3
9914 );
9915 }
9916 *accepted_ctr += accepted - 1;
9917 let (hc, dim) = (cfg.hc_mult, cfg.dim);
9922 let dev_caps: Vec<usize> = targets
9927 .iter()
9928 .copied()
9929 .filter(|&t| t < spec_gpu_end)
9930 .collect();
9931 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9932 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9933 return None;
9934 }
9935 for t in 0..accepted {
9936 let tip = t + 1 == accepted;
9937 for (slot, &tl) in targets.iter().enumerate() {
9938 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9939 let lo = (di * b + t) * hc * dim;
9940 crate::dsv4::dspark_capture(
9941 &caps_all[lo..lo + hc * dim],
9942 &cfg,
9943 slot,
9944 &mut ds.main_hidden,
9945 );
9946 } else if tip
9947 && crate::dsv4::dspark_peek_slot(slot, dim, {
9948 let lo = slot * dim;
9949 &mut ds.main_hidden[lo..lo + dim]
9950 })
9951 {
9952 } else {
9957 crate::dsv4::dspark_capture(
9961 &walked[t * hc * dim..(t + 1) * hc * dim],
9962 &cfg,
9963 slot,
9964 &mut ds.main_hidden,
9965 );
9966 }
9967 }
9968 crate::dsv4::dspark_ring_append(
9969 g,
9970 &self.dsv4_mtp,
9971 &cfg,
9972 ds,
9973 next_pos + t,
9974 self.pool.as_deref(),
9975 );
9976 }
9977 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9978 self.graph_logits = Some(row);
9979 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9984 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9985 crate::dsv4::pick_tally_arm();
9986 }
9987 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9988 eprintln!(
9989 "spec_step total {:.1} мс (k={accepted})",
9990 t_all.elapsed().as_secs_f64() * 1e3
9991 );
9992 }
9993 Some((fed[1..accepted].to_vec(), next_pos + accepted))
9994 }
9995
9996 fn dspark_probe(&mut self, position: usize, token_id: u32) {
9997 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9998 return;
9999 }
10000 let trunk_now = crate::dsv4::pick_tally_take();
10002 crate::dsv4::trunk_freq_note(&trunk_now);
10003 if !trunk_now.is_empty() {
10004 self.dspark_trunk_picks.push(trunk_now);
10005 let keep = crate::dsv4::dspark_block();
10006 if self.dspark_trunk_picks.len() > keep {
10007 self.dspark_trunk_picks.remove(0);
10008 }
10009 }
10010 for p in std::mem::take(&mut self.dspark_pending) {
10013 let Some(i) = position.checked_sub(p.0 + 1) else {
10014 continue;
10015 };
10016 let mut p = p;
10017 if i < p.1.len() {
10018 if p.2 && p.1[i] == token_id {
10019 p.3 = i + 1;
10020 } else {
10021 p.2 = false;
10022 }
10023 if i + 1 < p.1.len() {
10024 self.dspark_pending.push(p);
10025 continue;
10026 }
10027 }
10028 self.dspark_hist.push(p.3);
10029 self.dspark_real.push(token_id);
10030 }
10031 let Some(b) = &mut self.dsv4 else { return };
10032 let (g, layers, cfg) = (&b.0, &b.1, b.2);
10033 let n_layers = layers.len();
10034 if self.dspark.is_none() {
10035 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10036 if t.is_empty() {
10037 return;
10038 }
10039 eprintln!(
10040 "DSpark: захват со слоёв {t:?}, блок {}",
10041 crate::dsv4::dspark_block()
10042 );
10043 crate::dsv4::dspark_arm(&t, cfg.dim);
10044 self.dspark = Some(crate::dsv4::DsparkState::new(
10045 self.dsv4_mtp.len(),
10046 &cfg,
10047 t.len(),
10048 ));
10049 }
10050 let ds = self.dspark.as_mut().unwrap();
10051 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
10052 return; }
10054 let mut conf = Vec::new();
10055 crate::dsv4::pick_tally_arm();
10056 let draft_started = std::time::Instant::now();
10061 #[cfg(feature = "gpu")]
10062 let gpu_draft = crate::dsv4::dspark_gpu_on();
10063 #[cfg(not(feature = "gpu"))]
10064 let gpu_draft = false;
10065 let props = if gpu_draft {
10066 #[cfg(feature = "gpu")]
10067 {
10068 let kv_id = b.3.kv_id;
10069 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
10070 Some(pk) => crate::dsv4::dspark_draft_gpu(
10071 g,
10072 &self.dsv4_mtp,
10073 &cfg,
10074 ds,
10075 pk,
10076 kv_id,
10077 token_id,
10078 position,
10079 self.pool.as_deref(),
10080 &mut conf,
10081 ),
10082 None => Vec::new(),
10083 }
10084 }
10085 #[cfg(not(feature = "gpu"))]
10086 Vec::new()
10087 } else {
10088 crate::gpu::cpu_scope(|| {
10089 crate::dsv4::dspark_draft(
10090 g,
10091 &self.dsv4_mtp,
10092 &cfg,
10093 ds,
10094 token_id,
10095 position,
10096 self.pool.as_deref(),
10097 &mut conf,
10098 )
10099 })
10100 };
10101 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10102 let draft_picks = crate::dsv4::pick_tally_take();
10103 crate::dsv4::dspark_freq_note(&draft_picks);
10104 crate::dsv4::pick_tally_arm();
10107 if !props.is_empty() {
10108 let (tu, tt) = {
10112 let flat: Vec<(usize, Vec<usize>)> = self
10113 .dspark_trunk_picks
10114 .iter()
10115 .flat_map(|v| v.iter().cloned())
10116 .collect();
10117 let mut per: std::collections::HashMap<usize, Vec<usize>> =
10119 std::collections::HashMap::new();
10120 for (li, picks) in flat {
10121 per.entry(li).or_default().extend(picks);
10122 }
10123 let n = per.len().max(1);
10124 let mut u = 0usize;
10125 let mut t = 0usize;
10126 for (_, v) in per {
10127 t += v.len();
10128 u += v.iter().collect::<std::collections::HashSet<_>>().len();
10129 }
10130 (u / n, t / n)
10131 };
10132 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
10133 self.dspark_exp.push((tu, tt, du, dt));
10134 self.dspark_pending.push((position, props, true, 0));
10135 }
10136 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
10137 let n = self.dspark_hist.len() as f32;
10138 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
10139 let block = crate::dsv4::dspark_block();
10140 let mut at = vec![0usize; block + 1];
10141 for &k in &self.dspark_hist {
10142 at[k] += 1;
10143 }
10144 let mut surv = Vec::with_capacity(block);
10146 for i in 1..=block {
10147 let k = at[i..].iter().sum::<usize>() as f32 / n;
10148 surv.push(format!("{k:.2}"));
10149 }
10150 let distinct = self
10151 .dspark_real
10152 .iter()
10153 .collect::<std::collections::HashSet<_>>()
10154 .len();
10155 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
10156 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
10157 });
10158 let m = self.dspark_exp.len().max(1);
10159 eprintln!(
10160 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
10161 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
10162 self.dspark_hist.len(),
10163 mean + 1.0,
10164 surv.join(" ")
10165 );
10166 eprintln!(
10167 "DSpark: разных токенов {distinct} из {} (вырожденность), \
10168 эксперты ствол {}/{} на слой за {block} токенов, \
10169 черновик {}/{} за блок, draft {:.2} мс/блок",
10170 self.dspark_real.len(),
10171 tu / m,
10172 tt / m,
10173 du / m,
10174 dt / m,
10175 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
10176 );
10177 }
10178 }
10179
10180 fn forward_layers_upto(
10181 &mut self,
10182 hidden: &[f32],
10183 position: usize,
10184 task_mask: Option<&TaskMask>,
10185 upto: Option<usize>,
10186 ) -> Vec<f32> {
10187 if let Some(plan) = self.gpu_plan.clone() {
10193 if upto.is_none() && plan.len() > 1 {
10194 let mut h = hidden.to_vec();
10195 for &(dev, from, upto_incl) in plan.iter() {
10196 h = crate::gpu::with_device(dev, || {
10197 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
10198 });
10199 }
10200 return h;
10201 }
10202 }
10203 self.forward_layers_span(hidden, position, task_mask, 0, upto)
10204 }
10205
10206 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
10211 self.set_gpu_plan_at(devices, None)
10212 }
10213
10214 pub fn set_gpu_plan_at(
10218 &mut self,
10219 devices: Option<&[usize]>,
10220 at: Option<usize>,
10221 ) -> Result<(), String> {
10222 let Some(devs) = devices.filter(|d| d.len() > 1) else {
10223 self.gpu_plan = None;
10224 return Ok(());
10225 };
10226 self.split_supported()?;
10227 let n = self.num_layers;
10228 if devs.len() > n {
10229 return Err(format!("{} devices for {n} layers", devs.len()));
10230 }
10231 if let Some(k) = at {
10232 if k == 0 || k >= n {
10233 return Err(format!("split at {k}: the model has {n} layers"));
10234 }
10235 if devs.len() == 2 {
10236 self.gpu_plan = Some(std::sync::Arc::new(vec![
10237 (devs[0], 0, k - 1),
10238 (devs[1], k, n - 1),
10239 ]));
10240 return Ok(());
10241 }
10242 return Err(format!(
10243 "an explicit split point takes exactly 2 devices, got {}",
10244 devs.len()
10245 ));
10246 }
10247 let per = n.div_ceil(devs.len());
10248 let mut plan = Vec::with_capacity(devs.len());
10249 let mut from = 0usize;
10250 for &d in devs {
10251 if from >= n {
10252 break;
10253 }
10254 let upto = (from + per - 1).min(n - 1);
10255 plan.push((d, from, upto));
10256 from = upto + 1;
10257 }
10258 self.gpu_plan = Some(std::sync::Arc::new(plan));
10259 Ok(())
10260 }
10261
10262 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
10264 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
10265 }
10266
10267 fn forward_layers_span(
10273 &mut self,
10274 hidden: &[f32],
10275 position: usize,
10276 task_mask: Option<&TaskMask>,
10277 from: usize,
10278 upto: Option<usize>,
10279 ) -> Vec<f32> {
10280 debug_assert!(
10281 from == 0
10282 || (self.dsv4.is_none()
10283 && self.dsv41.is_none()
10284 && self.qwen4_exp.is_none()
10285 && self.g3n.is_none())
10286 );
10287 if let Some(b) = &mut self.qwen4_exp {
10288 let _ = (task_mask, upto);
10289 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10290 let mut logits = Vec::new();
10291 crate::qwen4_exp::forward_token(
10292 &b.0,
10293 &b.1,
10294 &b.2,
10295 &mut b.3,
10296 token_id,
10297 position,
10298 &self.inv_freq,
10299 self.pool.as_deref(),
10300 &mut logits,
10301 true,
10302 );
10303 self.graph_logits = Some(logits);
10304 return vec![0.0; self.hidden_size];
10305 }
10306 if let Some(b) = &mut self.dsv4 {
10312 let _ = (task_mask, upto);
10313 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10314 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
10315 st.pos = position;
10316 let mut logits = Vec::new();
10317 crate::dsv4::forward_token(
10318 g,
10319 layers,
10320 &cfg,
10321 st,
10322 token_id,
10323 &self.inv_freq,
10324 self.pool.as_deref(),
10325 &mut logits,
10326 );
10327 self.graph_logits = Some(logits);
10328 self.dspark_probe(position, token_id);
10329 return vec![0.0; self.hidden_size];
10332 }
10333 if let Some(b) = &mut self.dsv41 {
10335 let _ = (task_mask, upto);
10336 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10337 let mut logits = Vec::new();
10338 crate::dsv41::forward_token(
10339 &b.0,
10340 &b.1,
10341 &b.2,
10342 &mut b.3,
10343 token_id,
10344 position,
10345 self.pool.as_deref(),
10346 &mut logits,
10347 );
10348 self.graph_logits = Some(logits);
10349 return vec![0.0; self.hidden_size];
10350 }
10351 if let Some(b) = &self.g3n {
10354 let _ = (task_mask, upto);
10355 return crate::g3n::g3n_forward(
10356 &b.0,
10357 &b.1,
10358 hidden,
10359 position,
10360 &mut self.kv_cache.layers,
10361 self.num_heads,
10362 self.num_kv_heads,
10363 self.head_dim,
10364 self.pool.as_deref(),
10365 );
10366 }
10367 let mut h = hidden.to_vec();
10368 let (nh, _nkv, _hd, hs, _rd, eps) = (
10371 self.num_heads,
10372 self.num_kv_heads,
10373 self.head_dim,
10374 self.hidden_size,
10375 self.rotary_dim,
10376 self.rms_eps,
10377 );
10378 let pool = self.pool.clone();
10379 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
10391 let graph_on = match graph_env.as_deref() {
10392 Some("0") => false,
10393 Some("prefill") => false, Some(_) => true,
10395 None => crate::gpu::wgpu_graph_default(),
10401 };
10402 let graph_trusted =
10403 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
10404 let race_eligible = graph_on
10405 && upto.is_none()
10406 && task_mask.is_none()
10407 && from == 0
10408 && !crate::gpu::graph_unsupported();
10409 let mut tail_start = 0usize;
10410 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
10411 let t_graph = std::time::Instant::now();
10412 let mut lg = Vec::new();
10413 let mut gl = 0usize;
10414 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
10415 let declined = built.is_none();
10416 let built = match built {
10417 Some(Ok(hh)) => Some(hh),
10418 Some(Err(())) => {
10419 self.clear_sequence_state();
10423 self.graph_failed
10424 .store(true, std::sync::atomic::Ordering::Relaxed);
10425 self.cancel
10426 .store(true, std::sync::atomic::Ordering::Relaxed);
10427 tracing::error!("token graph failed after admission; sequence state cleared");
10428 return vec![0.0; self.hidden_size];
10429 }
10430 None => None,
10431 };
10432 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
10437 crate::gpu::graph_mark_unsupported();
10438 }
10439 graph_note(built.is_some(), gl, self.num_layers);
10440 if let Some(hh) = built {
10441 let dur = t_graph.elapsed();
10442 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10443 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
10444 }
10445 if gl > 0 && gl < self.num_layers {
10446 h = hh;
10452 tail_start = gl;
10453 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
10454 if !graph_trusted {
10455 crate::gpu::graph_race_record(true, dur);
10456 }
10457 if !lg.is_empty() {
10458 lg.resize(self.vocab_size, 0.0);
10461 if let Some(c) = self.final_softcap {
10462 for l in lg.iter_mut() {
10463 *l = c * (*l / c).tanh();
10464 }
10465 }
10466 self.graph_logits = Some(lg);
10467 }
10468 return hh;
10469 }
10470 }
10476 }
10477 let span = from > 0 || upto.is_some();
10501 if span && graph_on && task_mask.is_none() && graph_trusted {
10502 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
10503 let mut lg = Vec::new();
10504 let mut gl = 0usize;
10505 let span_res =
10506 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
10507 let span_res = match span_res {
10508 Some(Ok(hh)) => Some(hh),
10509 Some(Err(())) => {
10510 self.clear_sequence_state();
10511 self.graph_failed
10512 .store(true, std::sync::atomic::Ordering::Relaxed);
10513 self.cancel
10514 .store(true, std::sync::atomic::Ordering::Relaxed);
10515 tracing::error!(
10516 "span token graph failed after admission; sequence state cleared"
10517 );
10518 return vec![0.0; self.hidden_size];
10519 }
10520 None => None,
10521 };
10522 graph_note(span_res.is_some(), gl, upto_excl - from);
10523 if std::env::var("CMF_GPU_DEBUG").is_ok() {
10524 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
10528 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
10529 eprintln!(
10530 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
10531 upto_excl - from,
10532 span_res.is_some()
10533 );
10534 }
10535 }
10536 if let Some(hh) = span_res {
10537 if gl == upto_excl - from {
10538 if !lg.is_empty() {
10539 lg.resize(self.vocab_size, 0.0);
10540 if let Some(c) = self.final_softcap {
10541 for l in lg.iter_mut() {
10542 *l = c * (*l / c).tanh();
10543 }
10544 }
10545 self.graph_logits = Some(lg);
10546 }
10547 crate::gpu::set_layer(-1);
10548 return hh;
10549 }
10550 h = hh;
10552 tail_start = from + gl;
10553 }
10554 }
10555 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
10556
10557 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10563 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10564
10565 #[cfg(target_os = "macos")]
10566 let mut gpu_skip_until = 0usize;
10567 for li in tail_start.max(from)..self.num_layers {
10568 let _capacity_tail = automatic_gpu_prefix
10569 .filter(|&prefix| li >= prefix)
10570 .map(|_| crate::gpu::enter_cpu_scope());
10571 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
10573 if li > u {
10574 break;
10575 }
10576 }
10577 if let Some(mask) = task_mask {
10578 if !mask.layer_alive(li) {
10579 continue; }
10581 }
10582 #[cfg(target_os = "macos")]
10586 {
10587 if li < gpu_skip_until {
10588 continue;
10589 }
10590 if task_mask.is_none() {
10591 let end = self.q1_graph_gpu(li, upto, position, &mut h);
10592 if self
10593 .graph_failed
10594 .load(std::sync::atomic::Ordering::Relaxed)
10595 {
10596 return vec![0.0; self.hidden_size];
10600 }
10601 if end > li {
10602 gpu_skip_until = end;
10603 if self.is_loop_end(end - 1) && end < self.num_layers {
10606 h = inference::rms_norm(
10607 &h,
10608 &self.weights.final_norm,
10609 self.rms_eps,
10610 self.norm_style,
10611 );
10612 }
10613 continue;
10614 }
10615 }
10616 }
10617
10618 let lw = &self.weights.layers[self.phys_layer(li)];
10619 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10620 if tp.parse::<usize>().ok() == Some(position) {
10621 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10622 eprintln!(
10623 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10624 h[0], h[1]
10625 );
10626 }
10627 }
10628 inference::rms_norm_into(
10631 &h,
10632 &lw.input_norm,
10633 self.rms_eps,
10634 self.norm_style,
10635 &mut self.ws.n1,
10636 );
10637
10638 let attn_out = match &lw.attn {
10639 AttnKind::Mla(w) => {
10640 let inv_freq_l = self.layer_inv_freq(li);
10641 let rs = self.layer_rope_scale(li);
10642 let eps = self.rms_eps;
10643 let pool = self.pool.clone();
10644 mla_attention(
10645 w,
10646 &self.ws.n1,
10647 &mut self.kv_cache.layers[li],
10648 position,
10649 &inv_freq_l,
10650 rs,
10651 eps,
10652 pool.as_deref(),
10653 )
10654 }
10655 AttnKind::Linear(w) => {
10656 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10657 vmf_phase_forward(
10658 &self.ws.n1,
10659 w,
10660 &cfg,
10661 &mut self.kv_cache.layers[li].linear_state,
10662 self.pool.as_deref(),
10663 )
10664 }
10665 AttnKind::Kda(w) => {
10666 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10667 crate::linear_core::kda_forward(
10668 &self.ws.n1,
10669 w,
10670 &cfg,
10671 &mut self.kv_cache.layers[li].linear_state,
10672 self.pool.as_deref(),
10673 )
10674 }
10675 AttnKind::LinearGdn(w) => {
10676 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10677 gdn_forward(
10678 &self.ws.n1,
10679 w,
10680 &cfg,
10681 &mut self.kv_cache.layers[li].linear_state,
10682 self.pool.as_deref(),
10683 )
10684 }
10685 AttnKind::ShortConv(w) => {
10686 let cfg = self
10687 .short_conv_cfg
10688 .expect("short-conv layer without short_conv_cfg");
10689 short_conv_forward(
10690 &self.ws.n1,
10691 w,
10692 &cfg,
10693 &mut self.kv_cache.layers[li].linear_state,
10694 self.pool.as_deref(),
10695 )
10696 }
10697 AttnKind::Full {
10698 wq,
10699 wk,
10700 wv,
10701 wo,
10702 q_norm,
10703 k_norm,
10704 output_gate,
10705 softplus_gate,
10706 bias,
10707 } if self.kv_cache.layers[li].o1_sealed() => {
10708 let inv_freq_l = self.layer_inv_freq(li);
10711 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10712 let cfg = QwenAttnCfg {
10713 num_heads: self.layer_num_heads(li),
10714 num_kv_heads: nkv_l,
10715 head_dim: hd_l,
10716 hidden_size: hs,
10717 position,
10718 inv_freq: &inv_freq_l,
10719 rotary_dim: rd_l,
10720 scale: self.attn_scale,
10721 softcap: self.attn_softcap,
10722 window: None,
10723 v_norm: self.attn_v_norm,
10724 qk_norm_after_rope: self.qk_norm_after_rope,
10725 q_norm: q_norm.as_deref(),
10726 k_norm: k_norm.as_deref(),
10727 output_gate: *output_gate,
10728 softplus_gate: softplus_gate
10729 .as_ref()
10730 .map(|(gate, per_head)| (gate, *per_head)),
10731 rope_scale: self.layer_rope_scale(li),
10732 bias: bias
10733 .as_ref()
10734 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10735 rms_eps: eps,
10736 norm_style: self.norm_style,
10737 pool: pool.as_deref(),
10738 };
10739 attention::qwen_attention_nystrom(
10740 &self.ws.n1,
10741 wq,
10742 wk,
10743 wv,
10744 wo,
10745 &mut self.kv_cache.layers[li],
10746 &cfg,
10747 )
10748 }
10749 AttnKind::Full {
10750 wq,
10751 wk,
10752 wv,
10753 wo,
10754 q_norm,
10755 k_norm,
10756 output_gate,
10757 softplus_gate,
10758 bias,
10759 } => 'attn: {
10760 if graph_on
10763 && !*output_gate
10764 && softplus_gate.is_none()
10765 && self.attention_heads_per_layer.is_none()
10766 && bias.is_none()
10767 && task_mask.is_none()
10768 {
10769 let inv_freq_l = self.layer_inv_freq(li);
10770 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10771 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10772 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10773 wq.mapped_q1(),
10774 wk.mapped_q1(),
10775 wv.mapped_q1(),
10776 wo.mapped_q1(),
10777 ) {
10778 let gm = gm.clone();
10779 let mut out = vec![0f32; hs];
10780 let cache = &self.kv_cache.layers[li];
10781 if crate::gpu::attn_dropin(
10782 &gm,
10783 self.graph_kv_id,
10784 li,
10785 &self.ws.n1,
10786 qi,
10787 ki,
10788 vi,
10789 oi,
10790 q_norm.as_deref(),
10791 k_norm.as_deref(),
10792 self.qk_norm_after_rope,
10793 &inv_freq_l,
10794 nh,
10795 nkv_l,
10796 hd_l,
10797 rd_l,
10798 hs,
10799 position,
10800 self.kv_cache.max_seq_len,
10801 gemma,
10802 eps as f32,
10803 cache.k_heads(),
10804 cache.v_heads(),
10805 &mut out,
10806 ) {
10807 break 'attn out;
10808 }
10809 }
10810 }
10811 let masked = task_mask
10812 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10813 .unwrap_or(false);
10814 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10815 match (masked, f32_view) {
10816 (true, (Some(q), Some(k), Some(v), Some(o))) => {
10819 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10820 attention::multi_head_attention(
10821 &self.ws.n1,
10822 q,
10823 k,
10824 v,
10825 o,
10826 &mut self.kv_cache.layers[li],
10827 self.num_heads,
10828 self.num_kv_heads,
10829 self.head_dim,
10830 self.hidden_size,
10831 position,
10832 &active_heads,
10833 &self.inv_freq,
10834 )
10835 }
10836 (masked, _) => {
10837 if masked {
10838 tracing::warn!(
10839 "layer {li}: head mask on quantized weights not \
10840 supported yet — executing dense"
10841 );
10842 }
10843 let inv_freq_l = self.layer_inv_freq(li);
10844 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10845 let cfg = QwenAttnCfg {
10846 num_heads: self.layer_num_heads(li),
10847 num_kv_heads: nkv_l,
10848 head_dim: hd_l,
10849 hidden_size: hs,
10850 position,
10851 inv_freq: &inv_freq_l,
10852 rotary_dim: rd_l,
10853 scale: self.attn_scale,
10854 softcap: self.attn_softcap,
10855 window: self.layer_window(li),
10856 v_norm: self.attn_v_norm,
10857 qk_norm_after_rope: self.qk_norm_after_rope,
10858 q_norm: q_norm.as_deref(),
10859 k_norm: k_norm.as_deref(),
10860 output_gate: *output_gate,
10861 softplus_gate: softplus_gate
10862 .as_ref()
10863 .map(|(gate, per_head)| (gate, *per_head)),
10864 rope_scale: self.layer_rope_scale(li),
10865 bias: bias
10866 .as_ref()
10867 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10868 rms_eps: eps,
10869 norm_style: self.norm_style,
10870 pool: pool.as_deref(),
10871 };
10872 attention::qwen_attention(
10873 &self.ws.n1,
10874 wq,
10875 wk,
10876 wv,
10877 wo,
10878 &mut self.kv_cache.layers[li],
10879 &cfg,
10880 )
10881 }
10882 }
10883 }
10884 };
10885 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10888 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10889 None => attn_out,
10890 };
10891 let lw = &self.weights.layers[self.phys_layer(li)];
10892 inference::add_rmsnorm_fused_into(
10893 &mut h,
10894 &attn_out,
10895 &lw.post_norm,
10896 self.rms_eps,
10897 self.norm_style,
10898 &mut self.ws.p1,
10899 );
10900 let mut attn_out = attn_out;
10901 attention::recycle_buf(&mut attn_out);
10902 let post_normed = &self.ws.p1;
10903
10904 let ffn_masked = task_mask
10905 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10906 .unwrap_or(false);
10907 let ffn_out = match (ffn_masked, &lw.ffn) {
10919 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10923 let row = task_mask
10924 .and_then(|tm| tm.ffn_masks.get(li))
10925 .map(|v| v.as_slice());
10926 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10927 }
10928 (true, FfnKind::Dense(d)) => {
10929 let tm = task_mask.unwrap();
10930 let alive = tm.ffn_active_count(li);
10931 let deep = alive * 2 <= self.intermediate_size;
10932 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
10933 let active = tm.ffn_active_indices(li);
10934 sparse_ffn_quant(
10935 d,
10936 post_normed,
10937 &active,
10938 self.hidden_size,
10939 self.pool.as_deref(),
10940 )
10941 } else if deep
10942 && let (Some(g), Some(u), Some(dn)) = (
10943 d.gate_proj.as_f32(),
10944 d.up_proj.as_f32(),
10945 d.down_proj.as_f32(),
10946 )
10947 {
10948 let active = tm.ffn_active_indices(li);
10949 inference::sparse_ffn_forward(
10950 post_normed,
10951 g,
10952 u,
10953 dn,
10954 self.hidden_size,
10955 self.intermediate_size,
10956 &active,
10957 self.pool.as_deref(),
10958 )
10959 } else {
10960 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10961 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10962 }
10963 }
10964 (true, FfnKind::Moe(m)) => {
10965 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10969 ffn_forward(
10970 &lw.ffn,
10971 post_normed,
10972 self.pool.as_deref(),
10973 allowed.as_deref(),
10974 )
10975 }
10976 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10977 dm,
10978 post_normed,
10979 &h,
10980 self.rms_eps,
10981 self.norm_style,
10982 self.pool.as_deref(),
10983 ),
10984 (false, _) => match &lw.ffn {
10985 FfnKind::DenseMoe(dm) => dense_moe_ffn(
10986 dm,
10987 post_normed,
10988 &h,
10989 self.rms_eps,
10990 self.norm_style,
10991 self.pool.as_deref(),
10992 ),
10993 _ => {
10994 let allowed = match (&lw.ffn, task_mask) {
10995 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10996 _ => None,
10997 };
10998 ffn_forward(
10999 &lw.ffn,
11000 post_normed,
11001 self.pool.as_deref(),
11002 allowed.as_deref(),
11003 )
11004 }
11005 },
11006 };
11007 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
11008 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
11009 None => ffn_out,
11010 };
11011 for (i, &f) in ffn_out.iter().enumerate() {
11012 h[i] += f;
11013 }
11014 let mut ffn_out = ffn_out;
11015 attention::recycle_buf(&mut ffn_out);
11016
11017 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
11019 for v in h.iter_mut() {
11020 *v *= sc;
11021 }
11022 }
11023
11024 if self.is_loop_end(li) && li + 1 < self.num_layers {
11027 h = inference::rms_norm(
11028 &h,
11029 &self.weights.final_norm,
11030 self.rms_eps,
11031 self.norm_style,
11032 );
11033 }
11034
11035 if self.dyn_phi_layer == Some(li) {
11039 self.update_dyn_phi(&h);
11040 }
11041 }
11042 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
11044 crate::gpu::graph_race_record(false, t.elapsed());
11045 }
11046
11047 h
11048 }
11049
11050 fn update_dyn_phi(&mut self, h: &[f32]) {
11053 const A: f32 = 0.2;
11054 if self.dyn_phi_ema.len() != h.len() {
11055 self.dyn_phi_ema = vec![0.0; h.len()];
11056 self.dyn_phi_seen = 0;
11057 }
11058 if self.dyn_phi_seen == 0 {
11059 self.dyn_phi_ema.copy_from_slice(h);
11060 } else {
11061 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
11062 *e = (1.0 - A) * *e + A * v;
11063 }
11064 }
11065 self.dyn_phi_seen += 1;
11066 }
11067
11068 pub fn dyn_phi(&self) -> &[f32] {
11070 &self.dyn_phi_ema
11071 }
11072
11073 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
11075 self.dyn_phi_layer = layer;
11076 self.dyn_phi_ema.clear();
11077 self.dyn_phi_seen = 0;
11078 }
11079
11080 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
11082 let Some(model) = &self.model else {
11083 return Vec::new();
11084 };
11085 model
11086 .header
11087 .skills
11088 .iter()
11089 .enumerate()
11090 .filter_map(|(i, sk)| {
11091 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
11092 let sel = sk.selection.as_ref()?;
11093 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
11094 })
11095 .collect()
11096 }
11097
11098 pub fn active_skill(&self) -> Option<usize> {
11100 self.dyn_active
11101 }
11102
11103 pub fn enable_dynamic_routing(&mut self) -> usize {
11108 use crate::swarm::{DynRouter, RoutableSkill};
11109 let Some(model) = self.model.clone() else {
11110 return 0;
11111 };
11112 if self.dyn_blend_loaded {
11115 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
11116 return 0;
11117 }
11118 if let Some(a) = self.dyn_active {
11122 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
11123 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
11124 return 0;
11125 }
11126 }
11127 let hidden = self.hidden_size;
11128 let mut skills = Vec::new();
11129 for (idx, id, _phi) in self.dynamic_skills() {
11130 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
11131 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
11132 skills.push(rs);
11133 }
11134 }
11135 }
11136 if skills.is_empty() {
11137 return 0;
11138 }
11139 let phi = skills[0].phi_layer;
11141 if skills.iter().any(|s| s.phi_layer != phi) {
11142 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
11143 }
11144 let n = skills.len();
11145 self.set_dyn_phi_layer(Some(phi));
11146 self.dyn_router = Some(DynRouter::new(skills));
11147 n
11148 }
11149
11150 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
11152 self.dyn_router
11153 .as_ref()
11154 .map(|r| r.switches.clone())
11155 .unwrap_or_default()
11156 }
11157
11158 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
11161 let rows = self.weights.lm_head.rows();
11162 let mut logits = attention::take_buf(rows.min(self.vocab_size));
11163 self.weights
11164 .lm_head
11165 .matvec(hidden, &mut logits, self.pool.as_deref());
11166 logits.resize(self.vocab_size, 0.0);
11167 if let Some(m) = self.logit_multiplier {
11168 for l in logits.iter_mut() {
11169 *l *= m;
11170 }
11171 }
11172 if let Some(c) = self.final_softcap {
11173 for l in logits.iter_mut() {
11174 *l = c * (*l / c).tanh();
11175 }
11176 }
11177 if let Some(cm) = self.head_clusters.as_ref() {
11178 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
11179 }
11180 logits
11181 }
11182
11183 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
11186 let h = hidden.len();
11187 let ncl = cm.len() / h.max(1);
11188 if ncl == 0 || logits.len() % ncl != 0 {
11189 return;
11190 }
11191 let cs = logits.len() / ncl;
11192 let mut lc = vec![0.0f32; ncl];
11194 for c in 0..ncl {
11195 let row = &cm[c * h..(c + 1) * h];
11196 let mut s = 0.0f32;
11197 for j in 0..h {
11198 s += row[j] * hidden[j];
11199 }
11200 lc[c] = s;
11201 }
11202 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11203 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
11204 for c in 0..ncl {
11205 let blk = &mut logits[c * cs..(c + 1) * cs];
11206 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11207 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
11208 let add = lc[c] - lse - bl;
11209 for v in blk.iter_mut() {
11210 *v += add;
11211 }
11212 }
11213 }
11214
11215 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
11220 self.clear_sequence_state();
11221 crate::gpu::graph_race_begin_generation();
11225 if task_mask.is_none() {
11226 self.o1_begin();
11227 }
11228 let mut hidden = vec![0.0f32; self.hidden_size];
11229 for (pos, &id) in ids.iter().enumerate() {
11230 let emb = self.embed_single(id);
11231 hidden = self.forward_layers(&emb, pos, task_mask);
11232 }
11233 if let Err(err) = self.o1_seal_checked() {
11234 self.o1_fail(err);
11235 }
11236 inference::rms_norm_into(
11237 &hidden,
11238 &self.weights.final_norm,
11239 self.rms_eps,
11240 self.norm_style,
11241 &mut self.ws.n1,
11242 );
11243 self.lm_head_forward(&self.ws.n1)
11244 }
11245}
11246
11247pub fn create_test_pipeline(
11249 hidden_size: usize,
11250 intermediate_size: usize,
11251 num_heads: usize,
11252 num_kv_heads: usize,
11253 head_dim: usize,
11254 num_layers: usize,
11255 vocab_size: usize,
11256) -> Pipeline {
11257 let synth = |n: usize, salt: usize| -> Vec<f32> {
11260 (0..n)
11261 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
11262 .collect()
11263 };
11264 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11265 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11266 };
11267 let layer_weights: Vec<LayerWeights> = (0..num_layers)
11268 .map(|li| LayerWeights {
11269 input_norm: vec![1.0; hidden_size],
11270 post_norm: vec![1.0; hidden_size],
11271 attn_out_norm: None,
11272 ffn_out_norm: None,
11273 layer_scale: None,
11274 ffn: FfnKind::Dense(DenseFfn {
11275 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
11276 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
11277 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
11278 act: Act::Silu,
11279 down_t: None,
11280 segs: Vec::new(),
11281 }),
11282 attn: AttnKind::Full {
11283 bias: None,
11284 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
11285 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
11286 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
11287 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
11288 q_norm: None,
11289 k_norm: None,
11290 output_gate: false,
11291 softplus_gate: None,
11292 },
11293 })
11294 .collect();
11295
11296 Pipeline::new(
11297 Tokenizer::byte_level(),
11298 PipelineWeights {
11299 embed_tokens: qt(vocab_size, hidden_size, 100),
11300 layers: layer_weights,
11301 lm_head: qt(vocab_size, hidden_size, 200),
11302 final_norm: vec![1.0; hidden_size],
11303 },
11304 hidden_size,
11305 intermediate_size,
11306 num_heads,
11307 num_kv_heads,
11308 head_dim,
11309 num_layers,
11310 num_layers, false, vocab_size,
11313 1e-6,
11314 10_000.0,
11315 NormStyle::Qwen,
11316 4096,
11317 SamplerConfig {
11318 seed: Some(42),
11319 ..Default::default()
11320 },
11321 )
11322}
11323
11324#[inline]
11329fn mask_bit(row: &[u8], j: usize) -> bool {
11330 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
11331}
11332
11333fn mask_gain() -> f32 {
11344 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11345 *G.get_or_init(|| {
11346 std::env::var("CMF_FFN_MASK_GAIN")
11347 .ok()
11348 .and_then(|v| v.parse().ok())
11349 .unwrap_or(1.0)
11350 })
11351}
11352
11353fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
11354 let fill = meanfill().and_then(|(i, v)| {
11357 let li = crate::gpu::cur_layer();
11358 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
11359 });
11360 for r in 0..rows {
11361 let base = r * inter;
11362 for (bi, &byte) in row.iter().enumerate() {
11363 if byte == 0xFF {
11364 continue;
11365 }
11366 let j0 = bi * 8;
11367 for bit in 0..8 {
11368 let j = j0 + bit;
11369 if j < inter && byte & (1 << bit) == 0 {
11370 g[base + j] = fill.map_or(0.0, |f| f[j]);
11371 }
11372 }
11373 }
11374 }
11375 let gain = mask_gain();
11376 if gain != 1.0 {
11377 for v in g[..rows * inter].iter_mut() {
11378 *v *= gain;
11379 }
11380 }
11381}
11382
11383#[inline]
11385fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
11386 row.is_none_or(|r| mask_bit(r, i))
11387}
11388
11389fn all_bits_on(row: &[u8], n: usize) -> bool {
11392 (0..n).all(|i| mask_bit(row, i))
11393}
11394
11395fn tube_topk() -> usize {
11403 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11404 *K.get_or_init(|| {
11405 std::env::var("CMF_TUBE_TOPK")
11406 .ok()
11407 .and_then(|v| v.parse().ok())
11408 .unwrap_or(0)
11409 })
11410}
11411
11412fn tube_score_oracle() -> bool {
11413 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11414 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
11415}
11416
11417fn tube_ffn_routed(
11424 d: &DenseFfn,
11425 xs: &[f32],
11426 b: usize,
11427 pool: Option<&Pool>,
11428 mask_row: Option<&[u8]>,
11429 k: usize,
11430) -> Vec<f32> {
11431 let hidden = d.down_proj.rows();
11432 let core = d.gate_proj.rows();
11433 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11434 let mut out = match (b, core_full, mask_row) {
11435 (1, true, _) => dense_ffn(d, xs, pool),
11436 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11437 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11438 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11439 };
11440 let cand: Vec<usize> = (0..d.segs.len())
11441 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
11442 .collect();
11443 if cand.is_empty() {
11444 return out;
11445 }
11446 let oracle = tube_score_oracle();
11450 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
11451 let mut scores = vec![0f32; b * cand.len()];
11452 for (ci, &i) in cand.iter().enumerate() {
11453 let seg = &d.segs[i];
11454 let w = seg.width;
11455 let mut g = vec![0.0f32; b * w];
11456 if b == 1 {
11457 seg.gate.matvec(xs, &mut g, pool);
11458 } else {
11459 seg.gate.matmat(xs, b, &mut g, pool);
11460 }
11461 for v in g.iter_mut() {
11462 *v = Act::Silu.combine(*v, 1.0);
11463 }
11464 if !oracle {
11465 for t in 0..b {
11466 scores[t * cand.len() + ci] =
11467 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11468 }
11469 }
11470 if oracle || b > 1 {
11471 let mut u = vec![0.0f32; b * w];
11472 if b == 1 {
11473 seg.up.matvec(xs, &mut u, pool);
11474 } else {
11475 seg.up.matmat(xs, b, &mut u, pool);
11476 }
11477 for (a, &v) in g.iter_mut().zip(u.iter()) {
11478 *a *= v;
11479 }
11480 if oracle {
11481 for t in 0..b {
11482 scores[t * cand.len() + ci] =
11483 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11484 }
11485 }
11486 }
11487 acts.push(g);
11488 }
11489 let keep = k.min(cand.len());
11491 let mut scratch: Vec<f32> = Vec::new();
11492 for t in 0..b {
11493 let mut sc: Vec<(f32, usize)> = (0..cand.len())
11494 .map(|ci| (scores[t * cand.len() + ci], ci))
11495 .collect();
11496 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
11497 let mut alive = vec![false; cand.len()];
11498 for &(_, ci) in sc.iter().take(keep) {
11499 alive[ci] = true;
11500 }
11501 if b > 1 {
11502 for (ci, a) in acts.iter_mut().enumerate() {
11503 if !alive[ci] {
11504 let w = d.segs[cand[ci]].width;
11505 a[t * w..(t + 1) * w].fill(0.0);
11506 }
11507 }
11508 } else {
11509 for (ci, &i) in cand.iter().enumerate() {
11513 if !alive[ci] {
11514 continue;
11515 }
11516 let seg = &d.segs[i];
11517 let w = seg.width;
11518 let g = &mut acts[ci];
11519 if !tube_score_oracle() {
11520 scratch.clear();
11521 scratch.resize(w, 0.0);
11522 seg.up.matvec(xs, &mut scratch, pool);
11523 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
11524 *a *= v;
11525 }
11526 }
11527 let mut acc = vec![0.0f32; hidden];
11528 seg.down.matvec(g, &mut acc, pool);
11529 for (o, a) in out.iter_mut().zip(&acc) {
11530 *o += *a;
11531 }
11532 }
11533 }
11534 }
11535 if b > 1 {
11536 for (ci, &i) in cand.iter().enumerate() {
11537 let seg = &d.segs[i];
11538 let mut acc = vec![0.0f32; b * hidden];
11539 seg.down.matmat(&acts[ci], b, &mut acc, pool);
11540 for (o, a) in out.iter_mut().zip(&acc) {
11541 *o += *a;
11542 }
11543 }
11544 }
11545 out
11546}
11547
11548fn tube_ffn(
11554 d: &DenseFfn,
11555 xs: &[f32],
11556 b: usize,
11557 pool: Option<&Pool>,
11558 mask_row: Option<&[u8]>,
11559) -> Vec<f32> {
11560 if tube_topk() > 0 {
11561 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
11562 }
11563 let hidden = d.down_proj.rows();
11564 let core = d.gate_proj.rows();
11565 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11566 let mut out = match (b, core_full, mask_row) {
11567 (1, true, _) => dense_ffn(d, xs, pool),
11568 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11569 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11570 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11571 };
11572 TUBE_SCRATCH.with(|sc| {
11573 let mut sc = sc.borrow_mut();
11574 let [g, u, acc] = &mut *sc;
11575 for seg in &d.segs {
11576 if !tube_bit(mask_row, seg.start) {
11577 continue;
11578 }
11579 let w = seg.width;
11580 g.resize(b * w, 0.0);
11581 if b == 1
11582 && d.act == Act::Silu
11583 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11584 {
11585 } else {
11587 u.resize(b * w, 0.0);
11588 if b == 1 {
11589 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11590 } else {
11591 seg.gate.matmat(xs, b, g, pool);
11592 seg.up.matmat(xs, b, u, pool);
11593 }
11594 for i in 0..b * w {
11595 g[i] = d.act.combine(g[i], u[i]);
11596 }
11597 }
11598 acc.resize(b * hidden, 0.0);
11599 acc.fill(0.0);
11600 if b == 1 {
11601 seg.down.matvec(g, acc, pool);
11602 } else {
11603 seg.down.matmat(g, b, acc, pool);
11604 }
11605 for (o, a) in out.iter_mut().zip(acc.iter()) {
11606 *o += *a;
11607 }
11608 }
11609 out
11610 })
11611}
11612
11613thread_local! {
11614 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11618 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11619}
11620
11621fn dense_ffn_batch(
11622 d: &DenseFfn,
11623 xs: &[f32],
11624 b: usize,
11625 pool: Option<&Pool>,
11626 mask_row: Option<&[u8]>,
11627) -> Vec<f32> {
11628 let inter = d.gate_proj.rows();
11629 let hidden = d.down_proj.rows();
11630 if mask_row.is_none()
11638 && d.act == Act::Silu
11639 && b >= 32
11640 && crate::gpu::enabled_here()
11641 && !crate::gpu::mm_killed()
11642 && refit_dir().is_none()
11647 && !ffn_probe_active()
11652 {
11653 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11654 d.gate_proj.mapped_q4t(),
11655 d.up_proj.mapped_q4t(),
11656 d.down_proj.mapped_q4t(),
11657 ) {
11658 let mut out = vec![0.0f32; b * hidden];
11659 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11660 return out;
11661 }
11662 }
11663 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11668 d.gate_proj.mapped_q4tp(),
11669 d.up_proj.mapped_q4tp(),
11670 d.down_proj.mapped_q4tp(),
11671 ) {
11672 let mut out = vec![0.0f32; b * hidden];
11673 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11674 return out;
11675 }
11676 }
11677 }
11678 let mut g = vec![0.0f32; b * inter];
11679 d.gate_proj.matmat(xs, b, &mut g, pool);
11680 let mut u = vec![0.0f32; b * inter];
11681 d.up_proj.matmat(xs, b, &mut u, pool);
11682 if gate_topk() > 0 && d.act == Act::Silu {
11683 for t in 0..b {
11684 let row = &mut g[t * inter..(t + 1) * inter];
11685 for v in row.iter_mut() {
11686 *v = Act::Silu.combine(*v, 1.0);
11687 }
11688 keep_top_k(row, gate_topk());
11689 }
11690 for i in 0..b * inter {
11691 g[i] *= u[i];
11692 }
11693 } else {
11694 for i in 0..b * inter {
11695 g[i] = d.act.combine(g[i], u[i]);
11696 }
11697 }
11698 if let Some(row) = mask_row {
11699 zero_masked_cols(&mut g, b, inter, row);
11700 }
11701 if oracle_topk() > 0 {
11702 for t in 0..b {
11703 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11704 }
11705 }
11706 let mut out = vec![0.0f32; b * hidden];
11707 d.down_proj.matmat(&g, b, &mut out, pool);
11708 if refit_dir().is_some() {
11709 let li = crate::gpu::cur_layer();
11710 if li >= 0 {
11711 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11712 }
11713 }
11714 FFN_PROBE.with(|pr| {
11718 if let Some(acc) = pr.borrow_mut().as_mut() {
11719 let li = crate::gpu::cur_layer();
11720 if li < 0 {
11721 return;
11722 }
11723 let Some(row) = acc.get_mut(li as usize) else {
11724 return;
11725 };
11726 let sq = probe_sq();
11727 for t in 0..b {
11728 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11729 *a += if sq {
11730 (v as f64) * (v as f64)
11731 } else {
11732 (v as f64).abs()
11733 };
11734 }
11735 }
11736 }
11737 });
11738 out
11739}
11740
11741fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11746 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11747 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11748 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11749 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11750 if (!on && !dump) || b == 0 {
11751 return;
11752 }
11753 let hidden = xs.len() / b;
11754 if on {
11755 let mut acc = m.act_sq.borrow_mut();
11756 if acc.len() < hidden {
11757 acc.resize(hidden, 0.0);
11758 }
11759 for t in 0..b {
11760 let row = &xs[t * hidden..(t + 1) * hidden];
11761 for (a, &v) in acc.iter_mut().zip(row) {
11762 *a += (v as f64) * (v as f64);
11763 }
11764 }
11765 }
11766 if dump {
11767 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11770 .ok()
11771 .and_then(|v| v.parse().ok())
11772 .unwrap_or(4096);
11773 let mut rows = m.act_rows.borrow_mut();
11774 if rows.len() < cap * hidden {
11775 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11776 rows.extend_from_slice(&xs[..take * hidden]);
11777 }
11778 }
11779}
11780
11781#[derive(Clone, Copy)]
11784struct SendVecs(*mut Vec<f32>);
11785unsafe impl Send for SendVecs {}
11786unsafe impl Sync for SendVecs {}
11787impl SendVecs {
11788 #[inline]
11789 fn at(self, i: usize) -> *mut Vec<f32> {
11790 unsafe { self.0.add(i) }
11791 }
11792}
11793
11794fn moe_ffn_batch(
11795 m: &MoeFfn,
11796 xs: &[f32],
11797 b: usize,
11798 hidden: usize,
11799 pool: Option<&Pool>,
11800 allowed: Option<&[bool]>,
11801) -> Vec<f32> {
11802 accumulate_act(m, xs, b);
11803 let ne = m.experts.len();
11804 let mut logits = vec![0.0f32; b * ne];
11805 match &m.resonance {
11806 Some(r) => {
11807 let hdim = xs.len() / b.max(1);
11808 for bi in 0..b {
11809 r.scores(
11810 &xs[bi * hdim..(bi + 1) * hdim],
11811 &mut logits[bi * ne..(bi + 1) * ne],
11812 );
11813 }
11814 }
11815 None => m.router.matmat(xs, b, &mut logits, pool),
11816 }
11817
11818 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11821 {
11822 let mut st = m.stats.borrow_mut();
11823 if st.len() < ne {
11824 st.resize(ne, 0);
11825 }
11826 for bi in 0..b {
11827 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11828 for &e in &idx {
11829 st[e] += 1;
11830 assign[e].push((bi, p[e] / wsum));
11831 }
11832 }
11833 }
11834
11835 let mut out = vec![0.0f32; b * hidden];
11836 let cols = m.experts[0].gate_proj.cols();
11837 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11838 let sb = list.len();
11839 let mut sub = vec![0.0f32; sb * cols];
11840 for (k, &(bi, _)) in list.iter().enumerate() {
11841 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11842 }
11843 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11844 for (k, &(bi, w)) in list.iter().enumerate() {
11845 for i in 0..hidden {
11846 out[bi * hidden + i] += w * eo[k * hidden + i];
11847 }
11848 }
11849 };
11850 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11856 if pool.is_some() && active.len() >= 8 {
11857 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11858 {
11859 let panel_ptr = SendVecs(panels.as_mut_ptr());
11860 let experts = &m.experts;
11863 let (active_r, assign_r) = (&active, &assign);
11864 let run = |start: usize, end: usize| {
11865 for ai in start..end {
11866 let e = active_r[ai];
11867 let list = &assign_r[e];
11868 let sb = list.len();
11869 let mut sub = vec![0.0f32; sb * cols];
11870 for (k, &(bi, _)) in list.iter().enumerate() {
11871 sub[k * cols..(k + 1) * cols]
11872 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11873 }
11874 unsafe {
11876 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11877 }
11878 }
11879 };
11880 match pool {
11881 Some(p) => p.run_rows(active.len(), &run),
11882 None => run(0, active.len()),
11883 }
11884 }
11885 for (ai, &e) in active.iter().enumerate() {
11886 for (k, &(bi, w)) in assign[e].iter().enumerate() {
11887 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11888 for i in 0..hidden {
11889 out[bi * hidden + i] += w * eo[i];
11890 }
11891 }
11892 }
11893 } else {
11894 for &e in &active {
11895 run_expert(&m.experts[e], &assign[e], &mut out);
11896 }
11897 }
11898 if let Some((se, gate)) = &m.shared {
11899 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11900 let mut gl = vec![0.0f32; b];
11901 gate.matmat(xs, b, &mut gl, pool);
11902 (0..b)
11903 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11904 .collect()
11905 } else {
11906 (0..b).map(|bi| (bi, 1.0)).collect()
11907 };
11908 run_expert(se, &all, &mut out);
11909 }
11910 out
11911}
11912
11913thread_local! {
11914 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11918 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11919}
11920
11921fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11923 if gate_topk() > 0
11926 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11927 {
11928 return out;
11929 }
11930 let prism_body = d.gate_proj.has_prism_contract()
11946 || d.up_proj.has_prism_contract()
11947 || d.down_proj.has_prism_contract();
11948 if !prism_body
11949 && crate::gpu::enabled_here()
11950 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11951 {
11952 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11953 crate::gpu::ProbeArm::Gpu
11954 } else {
11955 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11956 };
11957 match arm {
11958 crate::gpu::ProbeArm::Gpu => {
11959 let t0 = std::time::Instant::now();
11960 if let Some(out) = dense_ffn_gpu(d, x, pool) {
11961 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11962 return out;
11963 }
11964 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11968 }
11969 crate::gpu::ProbeArm::CpuTimed => {
11970 let t0 = std::time::Instant::now();
11971 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11972 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11973 return out;
11974 }
11975 crate::gpu::ProbeArm::Cpu => {
11976 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11977 }
11978 }
11979 }
11980 dense_ffn_cpu(d, x, pool)
11981}
11982
11983fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11985 let inter = d.gate_proj.rows();
11986 FFN_SCRATCH.with(|s| {
11987 let mut s = s.borrow_mut();
11988 let [g, u, ..] = &mut *s;
11989 g.resize(inter, 0.0);
11990 if gate_topk() > 0 {
11993 u.resize(inter, 0.0);
11997 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11998 for i in 0..inter {
11999 g[i] = Act::Silu.combine(g[i], 1.0);
12000 }
12001 keep_top_k(g, gate_topk());
12002 for i in 0..inter {
12003 g[i] *= u[i];
12004 }
12005 } else if d.act == Act::Silu
12006 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
12007 {
12008 } else {
12010 u.resize(inter, 0.0);
12011 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12013 for i in 0..inter {
12014 g[i] = d.act.combine(g[i], u[i]);
12015 }
12016 }
12017 FFN_PROBE.with(|pr| {
12025 if let Some(acc) = pr.borrow_mut().as_mut() {
12026 let li = crate::gpu::cur_layer();
12027 if li >= 0 {
12028 if let Some(row) = acc.get_mut(li as usize) {
12029 match probe_topk() {
12030 0 if probe_sq() => {
12031 for (a, &v) in row.iter_mut().zip(g.iter()) {
12032 *a += (v as f64) * (v as f64);
12033 }
12034 }
12035 0 if probe_signed() => {
12036 for (a, &v) in row.iter_mut().zip(g.iter()) {
12037 *a += v as f64;
12038 }
12039 }
12040 0 => {
12041 for (a, &v) in row.iter_mut().zip(g.iter()) {
12042 *a += (v as f64).abs();
12043 }
12044 }
12045 k => {
12046 let n = g.len();
12047 let k = k.min(n);
12048 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12049 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12050 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12051 });
12052 let thr = *kth;
12053 for (a, &v) in row.iter_mut().zip(g.iter()) {
12054 if v.abs() >= thr {
12055 *a += 1.0;
12056 }
12057 }
12058 }
12059 }
12060 }
12061 }
12062 }
12063 });
12064 if oracle_topk() > 0 {
12065 keep_top_k(g, oracle_topk());
12066 }
12067 {
12068 let li = crate::gpu::cur_layer();
12069 if li >= 0 {
12070 adump_row(li as usize, g);
12071 }
12072 }
12073 let mut out = attention::take_buf(d.down_proj.rows());
12074 d.down_proj.matvec(g, &mut out, pool);
12075 out
12076 })
12077}
12078
12079pub struct RefitAcc {
12092 pub support: Vec<u32>,
12093 pub gss: Vec<f32>,
12094 pub ya: Vec<f32>,
12095 pub hidden: usize,
12096 pub tokens: u64,
12097 pub buf_g: Vec<f32>,
12103 pub buf_o: Vec<f32>,
12104 pub buf_t: usize,
12105}
12106
12107type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
12111
12112static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
12113 std::sync::OnceLock::new();
12114
12115fn ffn_probe_active() -> bool {
12118 FFN_PROBE.with(|p| p.borrow().is_some())
12119}
12120
12121fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
12122 REFIT
12123 .get_or_init(|| {
12124 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
12125 (
12126 d,
12127 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
12128 )
12129 })
12130 })
12131 .as_ref()
12132}
12133
12134fn refit_accumulate(
12136 li: usize,
12137 g: &[f32],
12138 b: usize,
12139 inter: usize,
12140 out: &[f32],
12141 hidden: usize,
12142 pool: Option<&Pool>,
12143) {
12144 let Some((dir, map)) = refit_dir() else {
12145 return;
12146 };
12147 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12148 let (from, to) = *SPAN.get_or_init(|| {
12149 let g = |k: &str, d: usize| {
12150 std::env::var(k)
12151 .ok()
12152 .and_then(|v| v.parse().ok())
12153 .unwrap_or(d)
12154 };
12155 (
12156 g("CMF_FFN_REFIT_FROM", 0),
12157 g("CMF_FFN_REFIT_TO", usize::MAX),
12158 )
12159 });
12160 if li < from || li > to {
12161 return;
12162 }
12163 let mut guard = map.lock().unwrap();
12164 let (map, shared) = &mut *guard;
12165 let acc = match map.entry(li) {
12166 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
12167 std::collections::hash_map::Entry::Vacant(e) => {
12168 let path = format!("{dir}/support.{li}.u32");
12169 let Ok(bytes) = std::fs::read(&path) else {
12170 eprintln!("refit: no {path} — layer {li} skipped");
12171 return;
12172 };
12173 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
12174 let support: Vec<u32> = bytes[4..4 + n * 4]
12175 .chunks_exact(4)
12176 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12177 .collect();
12178 eprintln!(
12179 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
12180 (n * n + hidden * n) as f64 * 4.0 / 1e6
12181 );
12182 e.insert(RefitAcc {
12183 gss: vec![0.0; n * n],
12184 ya: vec![0.0; hidden * n],
12185 buf_g: Vec::new(),
12186 buf_o: Vec::new(),
12187 buf_t: 0,
12188 support,
12189 hidden,
12190 tokens: 0,
12191 })
12192 }
12193 };
12194 let ns = acc.support.len();
12195 let cap = refit_batch();
12197 if acc.buf_g.is_empty() {
12198 acc.buf_g = vec![0.0; ns * cap];
12199 acc.buf_o = vec![0.0; hidden * cap];
12200 }
12201 let take = b.min(cap - acc.buf_t);
12202 for t in 0..take {
12203 let col = acc.buf_t + t;
12204 for (j, &n) in acc.support.iter().enumerate() {
12205 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
12206 }
12207 for h in 0..hidden {
12208 acc.buf_o[h * cap + col] = out[t * hidden + h];
12209 }
12210 }
12211 acc.buf_t += take;
12212 acc.tokens += take as u64;
12213 if acc.buf_t < cap {
12214 return;
12215 }
12216 let bt = acc.buf_t;
12217 acc.buf_t = 0;
12218 let RefitAcc {
12228 gss,
12229 ya,
12230 buf_g,
12231 buf_o,
12232 ..
12233 } = acc;
12234 let need = (ns * ns).max(hidden * ns);
12235 if shared.len() < need {
12236 shared.resize(need, 0.0);
12237 }
12238 let scratch = &mut shared[..];
12239 let _ = bt;
12240 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
12241 add_into(gss, &scratch[..ns * ns], pool);
12242 if crate::gpu::gemm_nt_f32_transient(
12243 buf_o,
12244 buf_g,
12245 &mut scratch[..hidden * ns],
12246 hidden,
12247 cap,
12248 ns,
12249 ) {
12250 add_into(ya, &scratch[..hidden * ns], pool);
12251 } else {
12252 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12253 }
12254 } else {
12255 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
12256 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12257 }
12258 }
12262
12263fn refit_batch() -> usize {
12265 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12266 *B.get_or_init(|| {
12267 std::env::var("CMF_FFN_REFIT_BATCH")
12268 .ok()
12269 .and_then(|v| v.parse().ok())
12270 .unwrap_or(4096)
12271 })
12272}
12273
12274fn accum_outer_t(
12277 c: &mut [f32],
12278 m: usize,
12279 n: usize,
12280 b: usize,
12281 left: &[f32],
12282 right: &[f32],
12283 pool: Option<&Pool>,
12284) {
12285 let ptr = SendMut(c.as_mut_ptr());
12286 let body = |i: usize| {
12287 let ptr = &ptr;
12288 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
12289 for t in 0..b {
12290 let a = left[i * b + t];
12291 if a == 0.0 {
12292 continue;
12293 }
12294 for (j, o) in row.iter_mut().enumerate() {
12295 *o += a * right[j * b + t];
12296 }
12297 }
12298 };
12299 match pool {
12300 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
12301 for i in s..e {
12302 body(i);
12303 }
12304 }),
12305 _ => {
12306 for i in 0..m {
12307 body(i);
12308 }
12309 }
12310 }
12311}
12312
12313fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
12316 let n = dst.len().min(src.len());
12317 match pool {
12318 Some(p) if n >= 1 << 16 => {
12319 let ptr = SendMut(dst.as_mut_ptr());
12320 let f = |s: usize, e: usize| {
12321 let ptr = &ptr;
12322 for blk in s..e {
12323 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
12324 for i in a..b {
12325 unsafe { *ptr.0.add(i) += src[i] };
12326 }
12327 }
12328 };
12329 p.run_rows(n.div_ceil(4096), &f);
12330 }
12331 _ => {
12332 for (d, v) in dst.iter_mut().zip(&src[..n]) {
12333 *d += *v;
12334 }
12335 }
12336 }
12337}
12338
12339fn accum_outer(
12344 c: &mut [f32],
12345 m: usize,
12346 n: usize,
12347 b: usize,
12348 left: &[f32],
12349 right: &[f32],
12350 pool: Option<&Pool>,
12351) {
12352 const TILE: usize = 32;
12353 let tiles = m.div_ceil(TILE);
12354 let cp = SendMut(c.as_mut_ptr());
12355 let body = |ti: usize| {
12356 let cp = &cp;
12357 let i0 = ti * TILE;
12358 let i1 = (i0 + TILE).min(m);
12359 for t in 0..b {
12360 let r = &right[t * n..t * n + n];
12361 for i in i0..i1 {
12362 let a = left[i * b + t];
12363 if a == 0.0 {
12364 continue;
12365 }
12366 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
12368 for (o, v) in row.iter_mut().zip(r) {
12369 *o += a * *v;
12370 }
12371 }
12372 }
12373 };
12374 match pool {
12375 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
12376 for ti in s..e {
12377 body(ti);
12378 }
12379 }),
12380 _ => {
12381 for ti in 0..tiles {
12382 body(ti);
12383 }
12384 }
12385 }
12386}
12387
12388pub fn refit_flush() -> usize {
12390 let Some((dir, map)) = refit_dir() else {
12391 return 0;
12392 };
12393 let guard = map.lock().unwrap();
12394 let mut n = 0;
12395 for (li, acc) in guard.0.iter() {
12396 let w = |name: &str, v: &[f32]| {
12399 let path = format!("{dir}/{name}.{li}.f32");
12400 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
12401 match std::fs::write(&path, &bytes) {
12402 Ok(()) => {}
12403 Err(e) => eprintln!(
12404 "refit: FAILED to write {path} ({} MB): {e}",
12405 bytes.len() / 1_000_000
12406 ),
12407 }
12408 };
12409 w("gss", &acc.gss);
12410 w("ya", &acc.ya);
12411 println!(
12412 "refit L{li}: {} support, {} tokens, hidden {}",
12413 acc.support.len(),
12414 acc.tokens,
12415 acc.hidden
12416 );
12417 n += 1;
12418 }
12419 n
12420}
12421
12422fn adump_row(li: usize, g: &[f32]) {
12427 use std::io::Write as _;
12428 static FILES: std::sync::OnceLock<
12429 Option<(
12430 String,
12431 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
12432 )>,
12433 > = std::sync::OnceLock::new();
12434 let Some((prefix, map)) = FILES
12435 .get_or_init(|| {
12436 std::env::var("CMF_FFN_ADUMP")
12437 .ok()
12438 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
12439 })
12440 .as_ref()
12441 else {
12442 return;
12443 };
12444 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12447 let (from, to) = *SPAN.get_or_init(|| {
12448 let g = |k: &str, d: usize| {
12449 std::env::var(k)
12450 .ok()
12451 .and_then(|v| v.parse().ok())
12452 .unwrap_or(d)
12453 };
12454 (
12455 g("CMF_FFN_ADUMP_FROM", 0),
12456 g("CMF_FFN_ADUMP_TO", usize::MAX),
12457 )
12458 });
12459 if li < from || li > to {
12460 return;
12461 }
12462 let mut map = map.lock().unwrap();
12463 let f = map.entry(li).or_insert_with(|| {
12464 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
12465 });
12466 let mut bytes = Vec::with_capacity(g.len() * 2);
12467 for v in g {
12468 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
12469 }
12470 let _ = f.write_all(&bytes);
12471}
12472
12473fn oracle_topk() -> usize {
12479 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12480 *K.get_or_init(|| {
12481 std::env::var("CMF_FFN_ORACLE_TOPK")
12482 .ok()
12483 .and_then(|v| v.parse().ok())
12484 .unwrap_or(0)
12485 })
12486}
12487
12488fn gate_topk() -> usize {
12494 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12495 *K.get_or_init(|| {
12496 std::env::var("CMF_FFN_GATE_TOPK")
12497 .ok()
12498 .and_then(|v| v.parse().ok())
12499 .unwrap_or(0)
12500 })
12501}
12502
12503fn gate_block() -> usize {
12510 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12511 *B.get_or_init(|| {
12512 std::env::var("CMF_FFN_GATE_BLOCK")
12513 .ok()
12514 .and_then(|v| v.parse().ok())
12515 .unwrap_or(1)
12516 })
12517}
12518
12519fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
12521 let n = g.len();
12522 let nb = n.div_ceil(block);
12523 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
12524 if kb >= nb {
12525 return;
12526 }
12527 let mut score: Vec<f32> = (0..nb)
12528 .map(|b| {
12529 g[b * block..((b + 1) * block).min(n)]
12530 .iter()
12531 .map(|v| v * v)
12532 .sum::<f32>()
12533 })
12534 .collect();
12535 let mut ord = score.clone();
12536 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
12537 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12538 });
12539 let thr = *kth;
12540 for b in 0..nb {
12541 if score[b] < thr {
12542 g[b * block..((b + 1) * block).min(n)].fill(0.0);
12543 }
12544 }
12545 score.clear();
12546}
12547
12548fn keep_top_k(g: &mut [f32], k: usize) {
12550 if gate_block() > 1 {
12551 return keep_top_blocks(g, k, gate_block());
12552 }
12553 let n = g.len();
12554 if k == 0 || k >= n {
12555 return;
12556 }
12557 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12558 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12559 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12560 });
12561 let thr = *kth;
12562 for v in g.iter_mut() {
12563 if v.abs() < thr {
12564 *v = 0.0;
12565 }
12566 }
12567}
12568
12569fn probe_sq() -> bool {
12573 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12574 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
12575}
12576
12577fn probe_signed() -> bool {
12581 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12582 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12583}
12584
12585fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12593 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12594 M.get_or_init(|| {
12595 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12596 let b = std::fs::read(&p).ok()?;
12597 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12598 let vals: Vec<f32> = b[8..]
12599 .chunks_exact(4)
12600 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12601 .collect();
12602 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12603 Some((inter, vals))
12604 })
12605 .as_ref()
12606}
12607
12608fn probe_topk() -> usize {
12611 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12612 *K.get_or_init(|| {
12613 std::env::var("CMF_FFN_PROBE_TOPK")
12614 .ok()
12615 .and_then(|v| v.parse().ok())
12616 .unwrap_or(0)
12617 })
12618}
12619
12620thread_local! {
12621 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12624 const { std::cell::RefCell::new(None) };
12625}
12626
12627fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12640 if d.gate_proj.has_prism_contract()
12645 || d.up_proj.has_prism_contract()
12646 || d.down_proj.has_prism_contract()
12647 {
12648 return None;
12649 }
12650 let dt = d.down_t.as_ref()?;
12651 let inter = d.gate_proj.rows();
12652 let hidden = dt.cols();
12653 if k == 0 || k >= inter || d.act != Act::Silu {
12654 return None;
12655 }
12656 DYN_SCRATCH.with(|sc| {
12657 let mut sc = sc.borrow_mut();
12658 let DynScratch {
12659 g,
12660 mag,
12661 live,
12662 parts,
12663 } = &mut *sc;
12664 g.resize(inter, 0.0);
12665 d.gate_proj.matvec(x, g, pool);
12666 for v in g.iter_mut() {
12667 *v = inference::silu(*v);
12668 }
12669 mag.clear();
12672 mag.extend(g.iter().map(|v| v.abs()));
12673 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12674 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12675 });
12676 let thr = *kth;
12677 live.clear();
12678 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12679 let mut out = vec![0.0f32; hidden];
12680 match pool {
12681 Some(p) if live.len() >= 64 => {
12682 let nw = p.n_workers() + 1;
12683 parts.clear();
12684 parts.resize(nw * hidden, 0.0);
12685 let ptr = SendMut(parts.as_mut_ptr());
12686 let n = live.len();
12687 let live_ref: &[u32] = live;
12688 let g_ref: &[f32] = g;
12689 p.run(&|w, workers| {
12690 let chunk = n.div_ceil(workers);
12691 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12692 if s >= e {
12693 return;
12694 }
12695 WORKER_SCRATCH.with(|ws| {
12696 let mut ws = ws.borrow_mut();
12697 let [scratch, acc] = &mut *ws;
12698 scratch.resize(hidden.max(x.len()), 0.0);
12699 acc.clear();
12700 acc.resize(hidden, 0.0);
12701 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12702 if let Some(&nx) = live_ref[s..e].get(o + 1) {
12705 d.up_proj.prefetch_row(nx as usize);
12706 dt.prefetch_row(nx as usize);
12707 }
12708 let idx = nrm as usize;
12709 let up = d.up_proj.row_dot(idx, x, scratch);
12710 let a = g_ref[idx] * up;
12711 if a != 0.0 {
12712 dt.add_row_scaled(idx, a, acc, scratch);
12713 }
12714 }
12715 for (j, v) in acc.iter().enumerate() {
12716 unsafe { *ptr.at(w * hidden + j) = *v };
12717 }
12718 });
12719 });
12720 for w in 0..nw {
12721 for (j, o) in out.iter_mut().enumerate() {
12722 *o += parts[w * hidden + j];
12723 }
12724 }
12725 }
12726 _ => {
12727 WORKER_SCRATCH.with(|ws| {
12728 let mut ws = ws.borrow_mut();
12729 let [scratch, _acc] = &mut *ws;
12730 scratch.resize(hidden.max(x.len()), 0.0);
12731 for &nrm in live.iter() {
12732 let idx = nrm as usize;
12733 let up = d.up_proj.row_dot(idx, x, scratch);
12734 let a = g[idx] * up;
12735 if a != 0.0 {
12736 dt.add_row_scaled(idx, a, &mut out, scratch);
12737 }
12738 }
12739 });
12740 }
12741 }
12742 Some(out)
12743 })
12744}
12745
12746struct DynScratch {
12749 g: Vec<f32>,
12750 mag: Vec<f32>,
12751 live: Vec<u32>,
12752 parts: Vec<f32>,
12753}
12754
12755thread_local! {
12756 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12757 std::cell::RefCell::new(DynScratch {
12758 g: Vec::new(),
12759 mag: Vec::new(),
12760 live: Vec::new(),
12761 parts: Vec::new(),
12762 })
12763 };
12764 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12766 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12767}
12768
12769fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12774 let inter = d.gate_proj.rows();
12775 FFN_SCRATCH.with(|s| {
12776 let mut s = s.borrow_mut();
12777 let [g, u, ..] = &mut *s;
12778 g.resize(inter, 0.0);
12779 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12780 } else {
12782 u.resize(inter, 0.0);
12783 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12784 for i in 0..inter {
12785 g[i] = d.act.combine(g[i], u[i]);
12786 }
12787 }
12788 zero_masked_cols(g, 1, inter, mask_row);
12789 let mut out = attention::take_buf(d.down_proj.rows());
12790 d.down_proj.matvec(g, &mut out, pool);
12791 out
12792 })
12793}
12794
12795fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12801 if d.gate_proj.has_prism_contract()
12802 || d.up_proj.has_prism_contract()
12803 || d.down_proj.has_prism_contract()
12804 {
12805 return None;
12806 }
12807 if d.act != Act::Silu {
12809 return None;
12810 }
12811 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12814 return None;
12815 }
12816 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12817 let mut model_ref = None;
12818 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12819 let model = model_ref?;
12820 let hidden = jobs[0].down.1;
12821 let mut out = attention::take_buf(hidden);
12822 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12823 Some(out)
12824 } else {
12825 let mut out = out;
12826 attention::recycle_buf(&mut out);
12827 None
12828 }
12829}
12830
12831#[allow(clippy::type_complexity)]
12836#[allow(clippy::type_complexity)]
12837pub(crate) fn moe_parts(
12838 t: &QTensor,
12839) -> Option<(
12840 &std::sync::Arc<cortiq_core::CmfModel>,
12841 usize,
12842 usize,
12843 usize,
12844 &[f32],
12845 &[f32],
12846 bool,
12847 bool,
12848 bool,
12849)> {
12850 match t {
12851 QTensor::Mapped {
12852 model,
12853 idx,
12854 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12855 rows,
12856 cols,
12857 row_scale,
12858 col_field,
12859 ..
12860 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12861 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12862 )),
12863 QTensor::Mapped {
12865 model,
12866 idx,
12867 dtype: cortiq_core::TensorDtype::Q1,
12868 rows,
12869 cols,
12870 ..
12871 } => Some((
12872 model,
12873 *idx,
12874 *rows,
12875 *cols,
12876 &[][..],
12877 &[][..],
12878 true,
12879 false,
12880 false,
12881 )),
12882 QTensor::Mapped {
12884 model,
12885 idx,
12886 dtype: cortiq_core::TensorDtype::Q4Tiled,
12887 rows,
12888 cols,
12889 ..
12890 } => Some((
12891 model,
12892 *idx,
12893 *rows,
12894 *cols,
12895 &[][..],
12896 &[][..],
12897 false,
12898 true,
12899 false,
12900 )),
12901 QTensor::Mapped {
12903 model,
12904 idx,
12905 dtype: cortiq_core::TensorDtype::Q4TiledP,
12906 rows,
12907 cols,
12908 ..
12909 } => Some((
12910 model,
12911 *idx,
12912 *rows,
12913 *cols,
12914 &[][..],
12915 &[][..],
12916 false,
12917 true,
12918 false,
12919 )),
12920 QTensor::Mapped {
12924 model,
12925 idx,
12926 dtype: cortiq_core::TensorDtype::Q2TiledP,
12927 rows,
12928 cols,
12929 ..
12930 } => Some((
12931 model,
12932 *idx,
12933 *rows,
12934 *cols,
12935 &[][..],
12936 &[][..],
12937 false,
12938 true,
12939 true,
12940 )),
12941 _ => None,
12942 }
12943}
12944
12945#[cfg(target_os = "macos")]
12953fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12954 if m.router_input_norm
12955 || m.route_tau.is_some()
12956 || m.mask.is_some()
12957 || m.per_expert_scale.is_some()
12958 || m.experts.is_empty()
12959 || m.top_k == 0
12960 || m.resonance.is_some()
12961 {
12962 return None;
12963 }
12964 let (sh, sg) = match &m.shared {
12967 Some((sh, sg)) => (sh, sg.as_ref()),
12968 None => return None,
12969 };
12970 let (rf, rr, rc) = m.router.f32_parts()?;
12971 if rr != m.experts.len() || rc != hidden {
12972 return None;
12973 }
12974 let shared_gated = sg.is_some();
12975 let sf = match sg {
12976 Some(sg) => {
12977 let (sf, sr, sc) = sg.f32_parts()?;
12978 if sr * sc != hidden {
12979 return None;
12980 }
12981 sf
12982 }
12983 None => &rf[..hidden],
12986 };
12987 if let Some(b) = &m.expert_bias {
12988 if b.len() != m.experts.len() {
12989 return None;
12990 }
12991 }
12992 let inter = m.experts[0].gate_proj.rows();
12993 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12996 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12997 if e.act != Act::Silu
12998 || e.gate_proj.rows() != inter
12999 || e.gate_proj.cols() != hidden
13000 || e.up_proj.rows() != inter
13001 || e.up_proj.cols() != hidden
13002 || e.down_proj.rows() != hidden
13003 || e.down_proj.cols() != inter
13004 {
13005 return None;
13006 }
13007 let pick = |t: &QTensor| -> Option<usize> {
13008 if gu_q2 {
13009 t.mapped_q2tp().map(|(_, i)| i)
13010 } else {
13011 t.mapped_q4tp().map(|(_, i)| i)
13012 }
13013 };
13014 Some((
13015 pick(&e.gate_proj)?,
13016 pick(&e.up_proj)?,
13017 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
13018 ))
13019 };
13020 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
13021 let shared = trio(sh)?;
13022 Some(crate::gpu::GpuMoe {
13023 router: rf,
13024 sgate: sf,
13025 experts,
13026 shared,
13027 n_exp: m.experts.len(),
13028 top_k: m.top_k,
13029 inter,
13030 norm_topk: m.norm_topk_prob,
13031 route_scale: m.routed_scaling,
13032 gu_q2,
13033 sigmoid: m.router_sigmoid,
13034 bias: m.expert_bias.as_deref(),
13035 shared_gated,
13036 })
13037}
13038
13039pub(crate) fn moe_push_job_parts<'a>(
13043 gate: &'a QTensor,
13044 up: &'a QTensor,
13045 down: &'a QTensor,
13046 x: &[f32],
13047 w: f32,
13048 swiglu_limit: f32,
13049 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13050 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13051) -> Option<()> {
13052 use crate::qtensor::prescale;
13053 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
13054 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
13055 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
13056 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13057 return None; }
13059 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
13062 return None;
13063 }
13064 if !gq2 && dq2 {
13065 return None;
13066 }
13067 model_ref.get_or_insert_with(|| gm.clone());
13068 let dt = |cf: &[f32]| {
13069 if cf.is_empty() {
13070 cortiq_core::TensorDtype::Q8Row
13071 } else {
13072 cortiq_core::TensorDtype::Q8_2f
13073 }
13074 };
13075 jobs.push(crate::gpu::MoeJob {
13076 gate: (gi, gr, gc, grs),
13077 up: (ui, ur, uc, urs),
13078 down: (di, dr, dc, drs),
13079 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
13080 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
13081 down_col: dcf,
13082 w,
13083 q1: gq1,
13084 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
13085 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
13086 gu_q2: gq2,
13087 swiglu_limit,
13088 });
13089 Some(())
13090}
13091
13092fn moe_push_job<'a>(
13094 d: &'a DenseFfn,
13095 x: &[f32],
13096 w: f32,
13097 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13098 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13099) -> Option<()> {
13100 use crate::qtensor::prescale;
13101 if d.act != Act::Silu {
13102 return None; }
13104 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
13105 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
13106 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
13107 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13108 return None; }
13110 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
13111 return None;
13112 }
13113 if !gq2 && dq2 {
13114 return None;
13115 }
13116 model_ref.get_or_insert_with(|| gm.clone());
13117 let gdt = if gcf.is_empty() {
13118 cortiq_core::TensorDtype::Q8Row
13119 } else {
13120 cortiq_core::TensorDtype::Q8_2f
13121 };
13122 let udt = if ucf.is_empty() {
13123 cortiq_core::TensorDtype::Q8Row
13124 } else {
13125 cortiq_core::TensorDtype::Q8_2f
13126 };
13127 jobs.push(crate::gpu::MoeJob {
13128 gate: (gi, gr, gc, grs),
13129 up: (ui, ur, uc, urs),
13130 down: (di, dr, dc, drs),
13131 xs_gate: prescale(x, gcf, gdt).into_owned(),
13132 xs_up: prescale(x, ucf, udt).into_owned(),
13133 down_col: dcf,
13134 w,
13135 q1: gq1,
13136 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
13137 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
13138 gu_q2: gq2,
13139 swiglu_limit: 0.0,
13140 });
13141 Some(())
13142}
13143
13144fn sparse_ffn_quant(
13151 d: &DenseFfn,
13152 x: &[f32],
13153 active: &[u16],
13154 hidden: usize,
13155 pool: Option<&Pool>,
13156) -> Vec<f32> {
13157 let n = active.len();
13158 let inter = d.gate_proj.rows();
13159 let mut act = vec![0.0f32; n];
13160 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
13163 let compute = |ai: usize| -> f32 {
13164 let idx = active[ai] as usize;
13165 if idx >= inter {
13166 return 0.0; }
13168 let mut s = if need_scratch {
13169 vec![0.0f32; hidden]
13170 } else {
13171 Vec::new()
13172 };
13173 let gate = d.gate_proj.row_dot(idx, x, &mut s);
13174 let up = d.up_proj.row_dot(idx, x, &mut s);
13175 d.act.combine(gate, up)
13176 };
13177 match pool {
13178 Some(p) if n >= 256 => {
13179 let ptr = SendMut(act.as_mut_ptr());
13180 p.run(&|widx, nw| {
13181 let chunk = n.div_ceil(nw);
13182 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
13183 for ai in s..e {
13184 unsafe { *ptr.at(ai) = compute(ai) };
13185 }
13186 });
13187 }
13188 _ => {
13189 for (ai, a) in act.iter_mut().enumerate() {
13190 *a = compute(ai);
13191 }
13192 }
13193 }
13194 let mut out = vec![0.0f32; hidden];
13196 for (ai, &idx) in active.iter().enumerate() {
13197 let w = act[ai];
13198 if w.abs() >= 1e-12 && (idx as usize) < inter {
13199 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
13200 }
13201 }
13202 out
13203}
13204
13205#[doc(hidden)]
13207pub fn sparse_ffn_quant_for_test(
13208 d: &DenseFfn,
13209 x: &[f32],
13210 active: &[u16],
13211 hidden: usize,
13212) -> Vec<f32> {
13213 sparse_ffn_quant(d, x, active, hidden, None)
13214}
13215
13216fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
13220 let deq = |t: &QTensor| -> Vec<f32> {
13221 let (rows, cols) = (t.rows(), t.cols());
13222 let mut out = vec![0.0f32; rows * cols];
13223 for r in 0..rows {
13224 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
13225 }
13226 out
13227 };
13228 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
13229}
13230
13231struct SendMut(*mut f32);
13233unsafe impl Send for SendMut {}
13234unsafe impl Sync for SendMut {}
13235impl SendMut {
13236 #[inline]
13237 #[allow(clippy::mut_from_ref)]
13240 unsafe fn at(&self, i: usize) -> &mut f32 {
13241 unsafe { &mut *self.0.add(i) }
13242 }
13243}
13244
13245pub(crate) fn moe_route(
13255 logits: &[f32],
13256 m: &MoeFfn,
13257 allowed: Option<&[bool]>,
13258) -> (Vec<usize>, Vec<f32>, f32) {
13259 let ne = logits.len();
13260 let p: Vec<f32> = if m.router_sigmoid {
13261 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
13262 } else {
13263 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13264 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
13265 let s: f32 = e.iter().sum();
13266 for v in &mut e {
13267 *v /= s;
13268 }
13269 e
13270 };
13271 let admit = |e: usize| {
13277 m.mask.as_ref().is_none_or(|mk| mk[e])
13278 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
13279 };
13280 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
13281 match &m.expert_bias {
13283 Some(b) => idx.sort_unstable_by(|&x, &y| {
13284 (p[y] + b[y])
13285 .partial_cmp(&(p[x] + b[x]))
13286 .unwrap()
13287 .then(x.cmp(&y))
13288 }),
13289 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
13290 }
13291 idx.truncate(m.top_k);
13292 if let Some(tau) = m.route_tau {
13296 let total: f32 = idx.iter().map(|&e| p[e]).sum();
13297 if total > 0.0 {
13298 let mut acc = 0.0f32;
13299 let mut keep = idx.len();
13300 for (i, &e) in idx.iter().enumerate() {
13301 acc += p[e];
13302 if acc >= tau * total {
13303 keep = i + 1;
13304 break;
13305 }
13306 }
13307 idx.truncate(keep);
13308 }
13309 }
13310 let wsum: f32 = if m.norm_topk_prob {
13311 let s: f32 = idx.iter().map(|&e| p[e]).sum();
13312 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
13315 } else {
13316 1.0 / m.routed_scaling
13317 };
13318 (idx, p, wsum)
13319}
13320
13321fn moe_trace(idx: &[usize]) {
13323 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
13324}
13325
13326pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
13329 use std::io::Write;
13330 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
13331 std::sync::OnceLock::new();
13332 let Some(f) = F.get_or_init(|| {
13333 let p = std::env::var("CMF_MOE_TRACE").ok()?;
13334 Some(std::sync::Mutex::new(
13335 std::fs::OpenOptions::new()
13336 .create(true)
13337 .append(true)
13338 .open(p)
13339 .ok()?,
13340 ))
13341 }) else {
13342 return;
13343 };
13344 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
13345 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
13346}
13347
13348pub(crate) fn moe_ffn(
13351 m: &MoeFfn,
13352 x: &[f32],
13353 pool: Option<&Pool>,
13354 allowed: Option<&[bool]>,
13355) -> Vec<f32> {
13356 accumulate_act(m, x, 1);
13357 let ne = m.experts.len();
13358 let mut logits = vec![0.0f32; ne];
13359 match &m.resonance {
13360 Some(r) => r.scores(x, &mut logits),
13361 None => m.router.matvec(x, &mut logits, pool),
13362 }
13363 let (idx, p, wsum) = moe_route(&logits, m, allowed);
13364 {
13365 let mut st = m.stats.borrow_mut();
13366 if st.len() < ne {
13367 st.resize(ne, 0);
13368 }
13369 for &e in &idx {
13370 st[e] += 1;
13371 }
13372 }
13373 moe_trace(&idx);
13379 if crate::gpu::enabled_here() {
13384 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
13385 crate::gpu::ProbeArm::Gpu => {
13386 let t0 = std::time::Instant::now();
13387 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
13388 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13389 return out;
13390 }
13391 }
13392 crate::gpu::ProbeArm::CpuTimed => {
13393 let t0 = std::time::Instant::now();
13394 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13395 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13396 return out;
13397 }
13398 crate::gpu::ProbeArm::Cpu => {
13399 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13400 }
13401 }
13402 }
13403 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
13404}
13405
13406fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
13411 use std::sync::atomic::{AtomicBool, Ordering};
13412 if built {
13413 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
13414 if total_layers > 0 && layers_run < total_layers {
13415 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
13416 } else {
13417 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
13418 }
13419 } else {
13420 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
13421 }
13422 static SAID: AtomicBool = AtomicBool::new(false);
13423 if !SAID.swap(true, Ordering::Relaxed) {
13424 if built {
13425 tracing::info!("wgpu whole-token graph: ACTIVE");
13426 } else {
13427 tracing::warn!("wgpu whole-token graph refused — per-op path");
13428 }
13429 }
13430}
13431
13432pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13436pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13437pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13441pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13443
13444pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
13448 std::sync::atomic::AtomicU64::new(0);
13449pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
13450 std::sync::atomic::AtomicU64::new(0);
13451pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
13452 std::sync::atomic::AtomicU64::new(0);
13453pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
13454 std::sync::atomic::AtomicU64::new(0);
13455pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
13456 std::sync::atomic::AtomicU64::new(0);
13457pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
13461 std::sync::atomic::AtomicU64::new(0);
13462pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
13463 std::sync::atomic::AtomicU64::new(0);
13464pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
13465 std::sync::atomic::AtomicU64::new(0);
13466pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
13467 std::sync::atomic::AtomicU64::new(0);
13468
13469fn moe_batch_enabled() -> bool {
13472 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13473 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
13474}
13475
13476fn moe_ffn_cpu_batched(
13482 m: &MoeFfn,
13483 x: &[f32],
13484 idx: &[usize],
13485 p: &[f32],
13486 wsum: f32,
13487 pool: Option<&Pool>,
13488) -> Option<Vec<f32>> {
13489 if idx.is_empty() || !moe_batch_enabled() {
13490 return None;
13491 }
13492 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
13496 return None;
13497 }
13498 let n = idx.len() + usize::from(m.shared.is_some());
13499 let mut pairs = Vec::with_capacity(n);
13500 let mut downs = Vec::with_capacity(n);
13501 let mut ws = Vec::with_capacity(n);
13502 for &e in idx {
13503 let d = &m.experts[e];
13504 if d.act != Act::Silu {
13505 return None;
13506 }
13507 pairs.push((&d.gate_proj, &d.up_proj));
13508 downs.push(&d.down_proj);
13509 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
13510 }
13511 if let Some((se, gate)) = &m.shared {
13514 if se.act != Act::Silu {
13515 return None;
13516 }
13517 let g = gate.as_ref().map_or(1.0, |gate| {
13518 let mut gl = [0.0f32; 1];
13519 gate.matvec(x, &mut gl, pool);
13520 1.0 / (1.0 + (-gl[0]).exp())
13521 });
13522 pairs.push((&se.gate_proj, &se.up_proj));
13523 downs.push(&se.down_proj);
13524 ws.push(g);
13525 }
13526 let inter = pairs[0].0.rows();
13527 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
13528 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
13529 return None;
13530 }
13531 let mut out = attention::take_buf(x.len());
13532 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
13533 attention::recycle_buf(&mut out);
13534 return None;
13535 }
13536 Some(out)
13537}
13538
13539pub(crate) fn moe_cold_experts_cpu(
13545 experts: &[(&DenseFfn, f32)],
13546 x: &[f32],
13547 pool: Option<&Pool>,
13548) -> Vec<f32> {
13549 let mut out = attention::take_buf(x.len());
13550 if experts.is_empty() {
13551 return out;
13552 }
13553 let pairs: Vec<_> = experts
13554 .iter()
13555 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
13556 .collect();
13557 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
13558 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
13559 let inter = experts[0].0.gate_proj.rows();
13560 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
13561 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
13562 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
13563 {
13564 return out;
13565 }
13566 out.fill(0.0);
13567 for &(expert, weight) in experts {
13568 let mut one = dense_ffn(expert, x, pool);
13569 for (o, v) in out.iter_mut().zip(&one) {
13570 *o += weight * v;
13571 }
13572 attention::recycle_buf(&mut one);
13573 }
13574 out
13575}
13576
13577fn moe_ffn_cpu(
13579 m: &MoeFfn,
13580 x: &[f32],
13581 idx: &[usize],
13582 p: &[f32],
13583 wsum: f32,
13584 pool: Option<&Pool>,
13585) -> Vec<f32> {
13586 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
13587 return out;
13588 }
13589 let mut out = attention::take_buf(x.len());
13590 for &e in idx {
13591 let mut eo = dense_ffn(&m.experts[e], x, pool);
13592 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
13593 for i in 0..out.len() {
13594 out[i] += w * eo[i];
13595 }
13596 attention::recycle_buf(&mut eo);
13597 }
13598 if let Some((se, gate)) = &m.shared {
13599 let mut so = dense_ffn(se, x, pool);
13600 let g = gate.as_ref().map_or(1.0, |gate| {
13601 let mut gl = [0.0f32; 1];
13602 gate.matvec(x, &mut gl, pool);
13603 1.0 / (1.0 + (-gl[0]).exp())
13604 });
13605 for i in 0..out.len() {
13606 out[i] += g * so[i];
13607 }
13608 attention::recycle_buf(&mut so);
13609 }
13610 out
13611}
13612
13613#[allow(clippy::too_many_arguments)]
13621fn mla_attention(
13622 w: &MlaWeights,
13623 normed: &[f32],
13624 cache: &mut crate::kv_cache::LayerKvCache,
13625 position: usize,
13626 inv_freq: &[f32],
13627 rope_scale: f32,
13628 eps: f64,
13629 pool: Option<&Pool>,
13630) -> Vec<f32> {
13631 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
13632 let hd = dr + dn;
13633 let mut q = vec![0.0f32; nh * hd];
13634 match (&w.q_a, &w.q_a_norm) {
13635 (Some(qa), Some(qn)) => {
13636 let mut t = vec![0.0f32; qa.rows()];
13637 qa.matvec(normed, &mut t, pool);
13638 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13639 w.q_proj.matvec(&tn, &mut q, pool);
13640 }
13641 _ => w.q_proj.matvec(normed, &mut q, pool),
13642 }
13643 let mut ca = vec![0.0f32; lora + dr];
13644 w.kv_a.matvec(normed, &mut ca, pool);
13645 let (c_lat, k_rope) = ca.split_at_mut(lora);
13646 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13647 let mut kvb = vec![0.0f32; nh * (dn + dv)];
13648 w.kv_b.matvec(&latn, &mut kvb, pool);
13649 if !w.nope {
13650 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13651 }
13652 for h in 0..nh {
13653 if !w.nope {
13654 attention::rope_rotate_scaled(
13655 &mut q[h * hd..h * hd + dr],
13656 position,
13657 inv_freq,
13658 rope_scale,
13659 );
13660 }
13661 }
13662 let mut k = vec![0.0f32; nh * hd];
13663 let mut v = vec![0.0f32; nh * hd];
13664 for h in 0..nh {
13665 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13666 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13667 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13668 }
13669 cache.append(&k, &v, &vec![true; nh]);
13670 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13671 attention::recycle_buf(&mut imp);
13672 let mut ov = vec![0.0f32; nh * dv];
13673 for h in 0..nh {
13674 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13675 }
13676 let mut out = vec![0.0f32; w.o_proj.rows()];
13677 w.o_proj.matvec(&ov, &mut out, pool);
13678 out
13679}
13680
13681fn dense_moe_ffn(
13688 dm: &DenseMoeFfn,
13689 x_normed: &[f32],
13690 h_raw: &[f32],
13691 eps: f64,
13692 norm_style: NormStyle,
13693 pool: Option<&Pool>,
13694) -> Vec<f32> {
13695 let mut d = dense_ffn(&dm.dense, x_normed, pool);
13696 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13697 let m = &dm.moe;
13698 let ne = m.experts.len();
13699 let mut logits = vec![0.0f32; ne];
13700 if m.router_input_norm {
13701 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13702 let inv = 1.0 / (ss + eps as f32).sqrt();
13703 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13704 m.router.matvec(&xr, &mut logits, pool);
13705 } else {
13706 m.router.matvec(h_raw, &mut logits, pool);
13707 }
13708 let (idx, p, wsum) = moe_route(&logits, m, None);
13709 {
13710 let mut st = m.stats.borrow_mut();
13711 if st.len() < ne {
13712 st.resize(ne, 0);
13713 }
13714 for &e in &idx {
13715 st[e] += 1;
13716 }
13717 }
13718 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13719 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13720 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13721 for (di, mi) in d.iter_mut().zip(&mo) {
13722 *di += mi;
13723 }
13724 d
13725}
13726
13727fn moe_gpu_refused(why: &'static str) {
13734 use std::sync::atomic::{AtomicBool, Ordering};
13735 static SAID: AtomicBool = AtomicBool::new(false);
13736 if !SAID.swap(true, Ordering::Relaxed) {
13737 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13738 }
13739}
13740
13741fn moe_ffn_gpu(
13742 m: &MoeFfn,
13743 x: &[f32],
13744 idx: &[usize],
13745 p: &[f32],
13746 wsum: f32,
13747 pool: Option<&Pool>,
13748) -> Option<Vec<f32>> {
13749 use crate::gpu::MoeJob;
13750
13751 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13752 let mut model_ref = None;
13753 for &e in idx {
13754 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13755 moe_gpu_refused("push_job(expert)");
13756 return None;
13757 }
13758 }
13759 if let Some((se, gate)) = &m.shared {
13760 let g = gate.as_ref().map_or(1.0, |gate| {
13761 let mut gl = [0.0f32; 1];
13762 gate.matvec(x, &mut gl, pool);
13763 1.0 / (1.0 + (-gl[0]).exp())
13764 });
13765 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13766 moe_gpu_refused("push_job(shared)");
13767 return None;
13768 }
13769 }
13770 let Some(model) = model_ref else {
13771 moe_gpu_refused("no model_ref");
13772 return None;
13773 };
13774 let hidden = jobs[0].down.1;
13775 let mut out = vec![0.0f32; hidden];
13776 if crate::gpu::moe_block(&model, &jobs, &mut out) {
13777 Some(out)
13778 } else {
13779 moe_gpu_refused("gpu::moe_block");
13780 None
13781 }
13782}
13783
13784fn ffn_forward(
13786 ffn: &FfnKind,
13787 x: &[f32],
13788 pool: Option<&Pool>,
13789 experts_allowed: Option<&[bool]>,
13790) -> Vec<f32> {
13791 match ffn {
13792 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13793 FfnKind::Dense(d) => dense_ffn(d, x, pool),
13794 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13795 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13799 }
13800}
13801
13802fn ffn_forward_pair(
13806 ffn: &FfnKind,
13807 x1: &[f32],
13808 x2: &[f32],
13809 pool: Option<&Pool>,
13810 experts_allowed: Option<&[bool]>,
13811) -> (Vec<f32>, Vec<f32>) {
13812 let d = match ffn {
13813 FfnKind::Dense(d) if !d.segs.is_empty() => {
13816 return (
13817 tube_ffn(d, x1, 1, pool, None),
13818 tube_ffn(d, x2, 1, pool, None),
13819 );
13820 }
13821 FfnKind::Dense(d) => d,
13822 FfnKind::Moe(m) => {
13823 return (
13824 moe_ffn(m, x1, pool, experts_allowed),
13825 moe_ffn(m, x2, pool, experts_allowed),
13826 );
13827 }
13828 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13829 };
13830 let inter = d.gate_proj.rows();
13831 FFN_SCRATCH.with(|s| {
13832 let mut s = s.borrow_mut();
13833 let [g1, g2, u1, u2] = &mut *s;
13834 g1.resize(inter, 0.0);
13835 g2.resize(inter, 0.0);
13836 u1.resize(inter, 0.0);
13837 u2.resize(inter, 0.0);
13838 QTensor::matvec2_many(
13841 [&d.gate_proj, &d.up_proj],
13842 x1,
13843 x2,
13844 [g1.as_mut_slice(), u1.as_mut_slice()],
13845 [g2.as_mut_slice(), u2.as_mut_slice()],
13846 pool,
13847 );
13848 for i in 0..inter {
13849 g1[i] = d.act.combine(g1[i], u1[i]);
13850 g2[i] = d.act.combine(g2[i], u2[i]);
13851 }
13852 let mut o1 = attention::take_buf(d.down_proj.rows());
13853 let mut o2 = attention::take_buf(d.down_proj.rows());
13854 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13855 (o1, o2)
13856 })
13857}
13858
13859#[cfg(test)]
13860mod tests {
13861
13862 #[test]
13863 fn nll_graph_policy_scopes_only_the_fused_head() {
13864 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
13865 ("vulkan graph", true, true, false, true, false),
13867 ("native Metal graph", true, true, true, true, true),
13869 ("masked", false, true, false, false, false),
13871 ("graph disabled", true, false, true, false, false),
13872 ] {
13873 let (graph_quality, graph_head_required) =
13874 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
13875 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
13876 assert_eq!(graph_head_required, want_head, "{label}: fused head");
13877 }
13878 }
13879
13880 #[test]
13881 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13882 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13883 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13884 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13885 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13886 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13887 }
13888
13889 #[test]
13890 fn cancel_flag_stops_generation() {
13891 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13892 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13895 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13896 assert_eq!(r.finish_reason, "cancelled");
13897 assert!(
13898 r.token_ids.is_empty(),
13899 "no tokens after cancel: {:?}",
13900 r.token_ids
13901 );
13902 assert_eq!(p.kv_cache.seq_len(), 0);
13903 assert!(p.kv_history.is_empty());
13904 assert!(!p.graph_want_logits);
13905 assert!(p.graph_logits.is_none());
13906 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13908 assert_ne!(r2.finish_reason, "cancelled");
13909 }
13910 use super::*;
13911
13912 #[test]
13920 fn dynamic_ffn_equals_the_zeroing_arm() {
13921 let (hidden, inter) = (8usize, 32usize);
13922 let synth = |n: usize, salt: usize| -> Vec<f32> {
13923 (0..n)
13924 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13925 .collect()
13926 };
13927 let down = synth(hidden * inter, 3);
13928 let mut down_t = vec![0.0f32; inter * hidden];
13929 for r in 0..hidden {
13930 for c in 0..inter {
13931 down_t[c * hidden + r] = down[r * inter + c];
13932 }
13933 }
13934 let d = DenseFfn {
13935 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13936 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13937 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13938 act: Act::Silu,
13939 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13940 segs: Vec::new(),
13941 };
13942 let x = synth(hidden, 11);
13943 let k = 12usize;
13944 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13945 let mut g = vec![0.0f32; inter];
13947 d.gate_proj.matvec(&x, &mut g, None);
13948 let mut u = vec![0.0f32; inter];
13949 d.up_proj.matvec(&x, &mut u, None);
13950 for v in g.iter_mut() {
13951 *v = inference::silu(*v);
13952 }
13953 keep_top_k(&mut g, k);
13954 for i in 0..inter {
13955 g[i] *= u[i];
13956 }
13957 let mut want = vec![0.0f32; hidden];
13958 d.down_proj.matvec(&g, &mut want, None);
13959 for (a, b) in want.iter().zip(&got) {
13960 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13961 }
13962 }
13963
13964 #[test]
13970 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13971 let (hidden, core, tube) = (8usize, 12usize, 8usize);
13972 let inter = core + tube;
13973 let synth = |n: usize, salt: usize| -> Vec<f32> {
13974 (0..n)
13975 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13976 .collect()
13977 };
13978 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13979 let d_all = synth(hidden * inter, 3);
13980 let dense = DenseFfn {
13982 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13983 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13984 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13985 act: Act::Silu,
13986 down_t: None,
13987 segs: Vec::new(),
13988 };
13989 let rows =
13990 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13991 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13992 let mut o = Vec::with_capacity(hidden * (b - a));
13993 for r in 0..hidden {
13994 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13995 }
13996 o
13997 };
13998 let tubed = DenseFfn {
13999 down_t: None,
14000 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
14001 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
14002 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
14003 act: Act::Silu,
14004 segs: vec![FfnSeg {
14005 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
14006 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
14007 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
14008 start: core,
14009 width: tube,
14010 }],
14011 };
14012 let x = synth(hidden, 7);
14013 let want = dense_ffn(&dense, &x, None);
14014 let got = tube_ffn(&tubed, &x, 1, None, None);
14015 for (a, b) in want.iter().zip(&got) {
14016 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
14017 }
14018 let mut bits = vec![0u8; inter.div_ceil(8)];
14020 for n in 0..core {
14021 bits[n / 8] |= 1 << (n % 8);
14022 }
14023 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14024 let masked = dense_ffn_masked(&dense, &x, None, &bits);
14025 for (a, b) in masked.iter().zip(&closed) {
14026 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
14027 }
14028 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
14030 for (a, b) in closed.iter().zip(&batch) {
14031 assert_eq!(a, b, "batch arm disagrees with decode arm");
14032 }
14033 }
14034
14035 #[test]
14037 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
14038 let (hidden, inter) = (16usize, 40usize);
14039 let synth = |n: usize, salt: usize| -> Vec<f32> {
14040 (0..n)
14041 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
14042 .collect()
14043 };
14044 let d = DenseFfn {
14045 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14046 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14047 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
14048 act: Act::Silu,
14049 down_t: None,
14050 segs: Vec::new(),
14051 };
14052 let x = synth(hidden, 9);
14053 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
14055
14056 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
14057
14058 let mut g = vec![0.0f32; inter];
14060 d.gate_proj.matvec(&x, &mut g, None);
14061 let mut u = vec![0.0f32; inter];
14062 d.up_proj.matvec(&x, &mut u, None);
14063 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
14064 for i in 0..inter {
14065 g[i] = if act_set.contains(&(i as u16)) {
14066 inference::silu(g[i]) * u[i]
14067 } else {
14068 0.0
14069 };
14070 }
14071 let mut reference = vec![0.0f32; hidden];
14072 d.down_proj.matvec(&g, &mut reference, None);
14073
14074 let max_d = sparse
14075 .iter()
14076 .zip(&reference)
14077 .map(|(a, b)| (a - b).abs())
14078 .fold(0.0f32, f32::max);
14079 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
14080 }
14081
14082 fn attach_test_mtp(p: &mut Pipeline) {
14084 let (h, inter, heads, kv, hd) = (
14085 p.hidden_size,
14086 p.intermediate_size,
14087 p.num_heads,
14088 p.num_kv_heads,
14089 p.head_dim,
14090 );
14091 let synth = |n: usize, salt: usize| -> Vec<f32> {
14092 (0..n)
14093 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
14094 .collect()
14095 };
14096 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
14097 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14098 };
14099 p.mtp = Some(MtpModule {
14100 enorm: vec![1.0; h],
14101 hnorm: vec![1.0; h],
14102 eh_proj: qt(h, 2 * h, 301),
14103 layer: LayerWeights {
14104 input_norm: vec![1.0; h],
14105 post_norm: vec![1.0; h],
14106 attn_out_norm: None,
14107 ffn_out_norm: None,
14108 layer_scale: None,
14109 ffn: FfnKind::Dense(DenseFfn {
14110 gate_proj: qt(inter, h, 315),
14111 up_proj: qt(inter, h, 316),
14112 down_proj: qt(h, inter, 317),
14113 act: Act::Silu,
14114 down_t: None,
14115 segs: Vec::new(),
14116 }),
14117 attn: AttnKind::Full {
14118 bias: None,
14119 wq: qt(heads * hd, h, 311),
14120 wk: qt(kv * hd, h, 312),
14121 wv: qt(kv * hd, h, 313),
14122 wo: qt(h, heads * hd, 314),
14123 q_norm: None,
14124 k_norm: None,
14125 output_gate: false,
14126 softplus_gate: None,
14127 },
14128 },
14129 final_norm: vec![1.0; h],
14130 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
14131 });
14132 }
14133
14134 #[test]
14135 fn speculative_equals_vanilla_greedy() {
14136 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14140 let run = |spec: bool| {
14141 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14142 p.sampler_config.temperature = 0.0;
14143 attach_test_mtp(&mut p);
14144 p.speculative = spec;
14145 let r = p.generate("abcdef", 12, None, None).unwrap();
14146 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
14147 };
14148 let (vanilla, d0, _) = run(false);
14149 let (spec, d1, a1) = run(true);
14150 assert_eq!(d0, 0, "vanilla path must not draft");
14151 assert!(d1 > 0, "speculative path must draft");
14152 assert_eq!(
14153 vanilla, spec,
14154 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
14155 );
14156 }
14157
14158 #[test]
14159 fn speculative_accepts_constant_oracle() {
14160 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14162 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14163 p.sampler_config.temperature = 0.0;
14164 p.sampler_config.repetition_penalty = 1.0;
14165 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
14168 attach_test_mtp(&mut p);
14169 p.speculative = true;
14170 let r = p.generate("abcd", 10, None, None).unwrap();
14171 assert!(r.mtp_drafted > 0);
14172 assert_eq!(
14173 r.mtp_accepted, r.mtp_drafted,
14174 "constant logits → every draft accepted"
14175 );
14176 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
14179 }
14180
14181 #[test]
14182 fn empty_prompt_is_an_error_not_a_panic() {
14183 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14184 let r = p.generate("", 4, None, None);
14185 assert!(r.is_err(), "empty prompt must be a clean error");
14186 }
14187
14188 #[test]
14189 fn every_token_enters_kv_exactly_once() {
14190 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14191 p.sampler_config.temperature = 0.0;
14193 let r = p.generate("abc", 2, None, None).unwrap();
14194 assert_eq!(r.prompt_tokens, 3);
14195 assert_eq!(
14199 p.kv_cache.seq_len(),
14200 3 + r.tokens_generated - 1,
14201 "each token must be cached exactly once (v1 cached the last prompt token twice)"
14202 );
14203 }
14204
14205 #[test]
14206 fn generation_is_reproducible_with_seed() {
14207 let run = || {
14208 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14209 p.generate("hello", 8, None, None).unwrap().token_ids
14210 };
14211 assert_eq!(run(), run());
14212 }
14213
14214 #[test]
14215 fn resetting_sampler_restarts_the_seeded_stream() {
14216 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14217 let config = SamplerConfig {
14218 seed: Some(1234),
14219 ..SamplerConfig::default()
14220 };
14221 p.set_sampler_config(config.clone());
14222 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
14223 p.set_sampler_config(config);
14224 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
14225 assert_eq!(first, second);
14226 }
14227
14228 #[test]
14229 fn eviction_bounds_the_cache() {
14230 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14231 p.kv_cache.max_seq_len = 6;
14232 p.sampler_config.temperature = 0.0;
14233 let _ = p.generate("abcd", 12, None, None).unwrap();
14234 assert!(
14235 p.kv_cache.seq_len() <= 6 + 1,
14236 "cache must stay bounded by max_seq_len (got {})",
14237 p.kv_cache.seq_len()
14238 );
14239 }
14240
14241 #[test]
14242 fn confidence_matches_tokens_and_is_a_probability() {
14243 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14244 p.sampler_config.temperature = 0.0;
14245 p.sampler_config.repetition_penalty = 1.0;
14246 let r = p.generate("abcd", 10, None, None).unwrap();
14247 assert_eq!(
14248 r.token_confidence.len(),
14249 r.token_ids.len(),
14250 "one confidence per emitted token"
14251 );
14252 for &c in &r.token_confidence {
14253 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
14254 }
14255 let logits = [1.0f32, 3.0, 0.5, 3.0];
14257 let p0 = top1_prob_t(&logits, 1, 1.0);
14258 let p1 = top1_prob_t(&logits, 3, 1.0);
14259 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
14260 assert!(p0 > 0.0 && p0 < 1.0);
14261 let sharp = top1_prob_t(&logits, 1, 1.0);
14263 let soft = top1_prob_t(&logits, 1, 2.0);
14264 assert!(soft < sharp, "higher temperature lowers peak confidence");
14265 }
14266
14267 #[test]
14268 fn trace_is_opt_in_and_parallels_the_output() {
14269 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14271 p.sampler_config.temperature = 0.0;
14272 p.sampler_config.repetition_penalty = 1.0;
14273 let r = p.generate("abcd", 10, None, None).unwrap();
14274 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
14275
14276 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14278 p.sampler_config.temperature = 0.0;
14279 p.sampler_config.repetition_penalty = 1.0;
14280 p.set_trace(true);
14281 let r = p.generate("abcd", 10, None, None).unwrap();
14282 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
14283 for (i, tr) in r.traces.iter().enumerate() {
14284 assert_eq!(tr.t, i, "trace index is sequential");
14285 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
14286 assert_eq!(
14287 tr.confidence, r.token_confidence[i],
14288 "trace confidence matches the confidence channel"
14289 );
14290 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
14292 }
14293 }
14294
14295 #[test]
14296 fn explain_prefill_logits_match_greedy_first_token() {
14297 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14301 p.sampler_config.temperature = 0.0;
14302 p.sampler_config.repetition_penalty = 1.0;
14303 let ids = p.tokenizer.encode("abcd");
14304 let logits = p.prefill_next_logits(&ids, None);
14305 let argmax = logits
14306 .iter()
14307 .enumerate()
14308 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14309 .unwrap()
14310 .0 as u32;
14311 let r = p.generate("abcd", 1, None, None).unwrap();
14312 assert_eq!(
14313 argmax, r.token_ids[0],
14314 "explain preview must match greedy emit"
14315 );
14316 }
14317
14318 #[test]
14319 fn laguna_shared_expert_is_unconditionally_added() {
14320 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
14321 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
14322 let zero_dense = || DenseFfn {
14323 gate_proj: matrix(vec![0.0; 4]),
14324 up_proj: matrix(vec![0.0; 4]),
14325 down_proj: matrix(vec![0.0; 4]),
14326 act: Act::Silu,
14327 down_t: None,
14328 segs: Vec::new(),
14329 };
14330 let shared = DenseFfn {
14331 gate_proj: identity(),
14332 up_proj: identity(),
14333 down_proj: identity(),
14334 act: Act::Silu,
14335 down_t: None,
14336 segs: Vec::new(),
14337 };
14338 let x = [1.0, 2.0];
14339 let expected = dense_ffn(&shared, &x, None);
14340 let moe = MoeFfn {
14341 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
14342 experts: vec![zero_dense()],
14343 top_k: 1,
14344 norm_topk_prob: true,
14345 router_sigmoid: true,
14346 expert_bias: None,
14347 routed_scaling: 1.0,
14348 route_tau: None,
14349 shared: Some((shared, None)),
14350 stats: std::cell::RefCell::new(Vec::new()),
14351 act_sq: std::cell::RefCell::new(Vec::new()),
14352 act_rows: std::cell::RefCell::new(Vec::new()),
14353 mask: None,
14354 per_expert_scale: None,
14355 router_input_norm: false,
14356 resonance: None,
14357 };
14358 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
14359 for (actual, expected) in actual.iter().zip(expected) {
14360 assert!((actual - expected).abs() < 1e-6);
14361 }
14362 }
14363
14364 #[test]
14365 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
14366 const B: usize = 19;
14367 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14368 p.set_o1(Some(crate::nystrom::O1Cfg {
14369 layers: crate::nystrom::O1Layers::All,
14370 m: 4,
14371 w: 8,
14372 sink: 2,
14373 rect: crate::nystrom::O1Rect::Aggregate,
14374 }));
14375 p.o1_begin_with_prefix(Some(B));
14376 let ids: Vec<u32> = (0..B as u32).collect();
14377 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
14378
14379 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
14380 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
14381 let next = p.embed_single(B as u32);
14382 let _ = p.forward_layers(&next, B, None);
14383 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
14384 }
14385
14386 #[test]
14387 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
14388 const B: usize = 19;
14389 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14390 let gdn_cfg = crate::linear_core::GdnCfg {
14394 num_v_heads: 2,
14395 num_k_heads: 1,
14396 key_head_dim: 2,
14397 value_head_dim: 4,
14398 conv_kernel: 3,
14399 hidden_size: 8,
14400 rms_eps: 1e-6,
14401 output_gate_sigmoid: false,
14402 };
14403 let synth = |n: usize, salt: usize| -> Vec<f32> {
14404 (0..n)
14405 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
14406 .collect()
14407 };
14408 let qt = |rows: usize, cols: usize, salt: usize| {
14409 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14410 };
14411 let c_dim = gdn_cfg.conv_dim();
14412 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
14413 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
14414 in_proj_qkv: qt(c_dim, 8, 1),
14415 in_proj_z: qt(vd, 8, 2),
14416 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
14417 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
14418 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
14419 a_log: vec![0.2, 0.5],
14420 dt_bias: synth(gdn_cfg.num_v_heads, 6),
14421 norm: vec![1.0; gdn_cfg.value_head_dim],
14422 out_proj: qt(8, vd, 7),
14423 });
14424 p.gdn_cfg = Some(gdn_cfg);
14425 p.set_o1(Some(crate::nystrom::O1Cfg {
14426 layers: crate::nystrom::O1Layers::All,
14427 m: 4,
14428 w: 8,
14429 sink: 2,
14430 rect: crate::nystrom::O1Rect::Aggregate,
14431 }));
14432 p.o1_begin_with_prefix(Some(B));
14433 for pos in 0..B - 2 {
14434 let emb = p.embed_single(pos as u32);
14435 let _ = p.forward_layers(&emb, pos, None);
14436 }
14437 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
14438
14439 let e1 = p.embed_single((B - 2) as u32);
14440 let e2 = p.embed_single((B - 1) as u32);
14441 let _ = p.forward_pair(&e1, &e2, B - 2);
14442
14443 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
14444 assert!(
14445 p.kv_cache
14446 .layers
14447 .iter()
14448 .enumerate()
14449 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
14450 );
14451 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
14452 assert_ne!(
14453 p.kv_cache.layers[0].linear_state, lane1_state,
14454 "real pair must commit GDN lane 2 before returning"
14455 );
14456 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
14457 let next = p.embed_single(B as u32);
14458 let _ = p.forward_layers(&next, B, None);
14459 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
14460 }
14461
14462 #[test]
14463 fn o1_error_observation_stays_terminal_until_reset() {
14464 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14465 p.set_o1(Some(crate::nystrom::O1Cfg {
14466 layers: crate::nystrom::O1Layers::All,
14467 m: 4,
14468 w: 8,
14469 sink: 2,
14470 rect: crate::nystrom::O1Rect::Aggregate,
14471 }));
14472 p.o1_begin();
14473 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
14474
14475 assert!(p.o1_seal_checked().is_err());
14476 assert!(
14477 p.o1_seal_checked().is_err(),
14478 "retry must see the sticky error"
14479 );
14480 let k = vec![0.2f32; 4];
14481 let v = vec![0.3f32; 4];
14482 p.kv_cache.layers[0].append(&k, &v, &[]);
14483 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
14484
14485 p.reset_session();
14486 p.o1_begin();
14487 p.kv_cache.layers[0].append(&k, &v, &[]);
14488 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
14489 }
14490
14491 #[test]
14492 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
14493 let ids = vec![1u32, 2, 3, 4, 5, 6];
14494 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14495 p.graph_logits = Some(vec![123.0]);
14496 p.graph_want_logits = true;
14497 p.graph_failed
14498 .store(true, std::sync::atomic::Ordering::Relaxed);
14499 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14500 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
14501 assert!(err.contains("before NLL"));
14502 assert!(p.graph_logits.is_none());
14503 assert!(!p.graph_want_logits);
14504 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14505 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14506
14507 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14508 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14509 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14510 assert_eq!(actual.1, expected.1);
14511 assert!((actual.0 - expected.0).abs() < 1e-9);
14512 }
14513
14514 #[test]
14515 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
14516 let ids = vec![1u32, 2, 3, 4, 5, 6];
14517 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14518 p.nll_test_fail_at = Some(1);
14519 let err = p
14520 .nll_ids_from(&ids, 0)
14521 .expect_err("one-shot forward failure");
14522 assert!(err.contains("forward") || err.contains("score row"));
14523 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14524 assert!(!p.graph_want_logits);
14525 assert!(p.graph_logits.is_none());
14526 assert!(p.kv_history.is_empty());
14527
14528 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14529 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14530 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14531 assert_eq!(actual.1, expected.1);
14532 assert!((actual.0 - expected.0).abs() < 1e-9);
14533 }
14534
14535 #[test]
14536 fn nll_serial_failure_before_first_row_is_reported() {
14537 let ids = vec![1u32, 2, 3, 4];
14538 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14539 p.nll_test_force_serial = true;
14540 p.nll_test_fail_at = Some(0);
14541 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
14542 assert!(err.contains("serial forward"));
14543 assert!(p.kv_history.is_empty());
14544 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14545 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14546 }
14547
14548 #[test]
14549 fn ffn_probe_failure_discards_recorder_and_state() {
14550 let ids = vec![1u32, 2, 3, 4];
14551 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14552 p.nll_test_fail_at = Some(0);
14553 let err = p
14554 .probe_ffn_mass_batch(&ids)
14555 .expect_err("probe forward failure");
14556 assert!(err.contains("NLL"));
14557 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
14558 assert!(p.kv_history.is_empty());
14559 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14560 }
14561
14562 #[test]
14563 fn nll_test_controls_are_pipeline_scoped() {
14564 let ids = vec![1u32, 2, 3, 4];
14565 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14566 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14567 failing.nll_test_force_serial = true;
14568 failing.nll_test_fail_at = Some(0);
14569
14570 assert!(!failing.can_prefill_batched());
14571 assert!(unaffected.can_prefill_batched());
14572 let expected = unaffected
14573 .nll_ids_from(&ids, 0)
14574 .expect("unaffected pipeline remains usable");
14575 let err = failing
14576 .nll_ids_from(&ids, 0)
14577 .expect_err("failure injection belongs to failing pipeline");
14578 assert!(err.contains("serial forward"));
14579 assert!(failing.nll_test_fail_at.is_none());
14580 assert!(unaffected.can_prefill_batched());
14581 let actual = unaffected
14582 .nll_ids_from(&ids, 0)
14583 .expect("unaffected pipeline remains reusable");
14584 assert_eq!(actual.1, expected.1);
14585 assert!((actual.0 - expected.0).abs() < 1e-9);
14586 }
14587
14588 #[test]
14589 fn forward_ids_failure_channel_is_terminal_and_reusable() {
14590 let ids = vec![1u32, 2, 3, 4, 5, 6];
14591 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14592 p.graph_logits = Some(vec![123.0]);
14593 p.graph_want_logits = true;
14594 p.graph_failed
14595 .store(true, std::sync::atomic::Ordering::Relaxed);
14596 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14597
14598 let err = p
14599 .forward_ids(&ids, None)
14600 .expect_err("a failed forward must not become a valid head result");
14601 assert!(err.contains("forward_ids setup"));
14602 assert!(p.graph_logits.is_none());
14603 assert!(!p.graph_want_logits);
14604 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14605 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14606 assert_eq!(p.kv_cache.seq_len(), 0);
14607
14608 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
14609 .forward_ids(&ids, None)
14610 .expect("fresh forward_ids");
14611 let actual = p
14612 .forward_ids(&ids, None)
14613 .expect("pipeline remains reusable after a failed forward");
14614 assert_eq!(actual.len(), expected.len());
14615 assert!(
14616 actual
14617 .iter()
14618 .zip(expected)
14619 .all(|(a, b)| (a - b).abs() < 1e-9)
14620 );
14621 assert_eq!(p.kv_cache.seq_len(), ids.len());
14622 }
14623}