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 self.weights
1175 .layers
1176 .iter()
1177 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1178 }
1179
1180 #[cfg(target_os = "macos")]
1181 fn q1_graph_gpu(
1182 &mut self,
1183 start: usize,
1184 upto: Option<usize>,
1185 position: usize,
1186 h: &mut [f32],
1187 ) -> usize {
1188 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1190 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1191 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1193 || !graph_force
1194 || std::env::var("CMF_GPU_BLOCK")
1195 .map(|v| v == "0")
1196 .unwrap_or(false)
1197 {
1198 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1199 eprintln!(
1200 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
1201 self.attn_softcap > 0.0,
1202 crate::gpu::enabled_here(),
1203 graph_force,
1204 );
1205 }
1206 if self.graph_head_required {
1207 self.fail_metal_graph("native graph front gate refused");
1208 }
1209 return start;
1210 }
1211 if self.swa.is_some()
1215 || self.global_attn.is_some()
1216 || self.attention_heads_per_layer.is_some()
1217 || self.attn_v_norm
1218 || self.weights.layers.iter().any(|lw| {
1219 lw.attn_out_norm.is_some()
1220 || lw.ffn_out_norm.is_some()
1221 || lw.layer_scale.is_some()
1222 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1223 })
1224 {
1225 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1226 eprintln!(
1227 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1228 self.swa.is_some(),
1229 self.global_attn.is_some(),
1230 self.attention_heads_per_layer.is_some(),
1231 self.attn_v_norm,
1232 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1233 );
1234 }
1235 if self.graph_head_required {
1236 self.fail_metal_graph("native graph architecture gate refused");
1237 }
1238 return start;
1239 }
1240 let limit = upto
1243 .map(|u| u + 1)
1244 .unwrap_or(self.num_layers)
1245 .min(self.num_layers);
1246
1247 enum Item<'a> {
1248 Gdn {
1249 run: Vec<GdnGpuLayer<'a>>,
1250 first: usize,
1251 },
1252 Attn {
1253 l: AttnGpuLayer<'a>,
1254 li: usize,
1255 q_norm: Option<&'a [f32]>,
1256 k_norm: Option<&'a [f32]>,
1257 output_gate: bool,
1258 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1259 full_gpu: bool,
1262 },
1263 }
1264
1265 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1272 let attend_contract = attend_mode != "0"
1273 && attend_mode != "off"
1274 && self.head_dim % 4 == 0
1275 && self.head_dim <= 256
1276 && self.rotary_dim >= 2
1277 && self.rotary_dim <= self.head_dim
1278 && (self.rotary_dim / 2) % 32 == 0
1279 && self.num_kv_heads > 0
1280 && self.num_heads % self.num_kv_heads == 0;
1281
1282 let mut plan: Vec<Item> = Vec::new();
1283 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1284 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1286 let mut scan = start;
1287 while scan < limit {
1288 let lw = &self.weights.layers[self.phys_layer(scan)];
1289 let ffn = match &lw.ffn {
1290 FfnKind::Dense(d) if d.segs.is_empty() => {
1291 let (Some(g), Some(u), Some(dn)) = (
1292 d.gate_proj.metal_graph_parts(),
1293 d.up_proj.metal_graph_parts(),
1294 d.down_proj.metal_graph_parts(),
1295 ) else {
1296 if block_diag {
1297 eprintln!(
1298 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1299 );
1300 }
1301 break;
1302 };
1303 MetalFfn::Dense {
1304 gate: g,
1305 up: u,
1306 down: dn,
1307 }
1308 }
1309 FfnKind::Moe(m) => {
1310 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1311 if block_diag {
1312 eprintln!(
1313 "block-graph: L{scan} MoE outside the graph contract — run ends"
1314 );
1315 }
1316 break;
1317 };
1318 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1319 model_ref.get_or_insert_with(|| model.clone());
1320 }
1321 MetalFfn::Moe(moe)
1322 }
1323 _ => {
1324 if block_diag {
1325 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1326 }
1327 break;
1328 }
1329 };
1330 match &lw.attn {
1331 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1332 let parts = (
1333 w.in_proj_qkv.metal_graph_parts(),
1334 w.in_proj_z.metal_graph_parts(),
1335 w.in_proj_a.f32_parts(),
1336 w.in_proj_b.f32_parts(),
1337 w.out_proj.metal_graph_parts(),
1338 );
1339 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1340 if block_diag {
1341 eprintln!(
1342 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1343 w.in_proj_qkv.metal_graph_parts().is_some(),
1344 w.in_proj_z.metal_graph_parts().is_some(),
1345 w.in_proj_a.f32_parts().is_some(),
1346 w.in_proj_b.f32_parts().is_some(),
1347 w.out_proj.metal_graph_parts().is_some(),
1348 );
1349 }
1350 break;
1351 };
1352 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1353 model_ref.get_or_insert_with(|| model.clone());
1354 }
1355 let gl = GdnGpuLayer {
1356 attn_norm: &lw.input_norm,
1357 post_norm: &lw.post_norm,
1358 qkv,
1359 z,
1360 a,
1361 b,
1362 out,
1363 ffn,
1364 conv1d: &w.conv1d,
1365 a_log: &w.a_log,
1366 dt_bias: &w.dt_bias,
1367 gnorm: &w.norm,
1368 };
1369 match plan.last_mut() {
1370 Some(Item::Gdn { run, .. }) => run.push(gl),
1371 _ => plan.push(Item::Gdn {
1372 run: vec![gl],
1373 first: scan,
1374 }),
1375 }
1376 }
1377 AttnKind::Full {
1378 wq,
1379 wk,
1380 wv,
1381 wo,
1382 q_norm,
1383 k_norm,
1384 output_gate,
1385 softplus_gate: None,
1386 bias,
1387 } if !self.kv_cache.layers[scan].o1_sealed()
1388 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1393 {
1394 let parts = (
1395 wq.metal_graph_parts(),
1396 wk.metal_graph_parts(),
1397 wv.metal_graph_parts(),
1398 wo.metal_graph_parts(),
1399 );
1400 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1401 break;
1402 };
1403 if let QTensor::Mapped { model, .. } = wq {
1404 model_ref.get_or_insert_with(|| model.clone());
1405 }
1406 let cache = &self.kv_cache.layers[scan];
1407 let o1_metal = cache.o1.is_some()
1411 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1412 && cache.o1_views().is_some();
1413 let full_gpu = attend_contract
1414 && cache.mode == crate::kv_cache::KvMode::F32
1415 && (cache.o1.is_none() || o1_metal)
1416 && bias.is_none()
1417 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1418 && pk.1 == self.num_kv_heads * self.head_dim
1419 && pv.1 == self.num_kv_heads * self.head_dim
1420 && po.2 == self.num_heads * self.head_dim;
1421 plan.push(Item::Attn {
1422 l: AttnGpuLayer {
1423 attn_norm: &lw.input_norm,
1424 post_norm: &lw.post_norm,
1425 wq: pq,
1426 wk: pk,
1427 wv: pv,
1428 wo: po,
1429 ffn,
1430 },
1431 li: scan,
1432 q_norm: q_norm.as_deref(),
1433 k_norm: k_norm.as_deref(),
1434 output_gate: *output_gate,
1435 bias: bias
1436 .as_ref()
1437 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1438 full_gpu,
1439 });
1440 }
1441 _ => break,
1442 }
1443 scan += 1;
1444 }
1445 let Some(model) = model_ref else {
1446 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1447 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1448 }
1449 if self.graph_head_required {
1450 self.fail_metal_graph("native graph has no mapped model reference");
1451 }
1452 return start;
1453 };
1454 if plan.is_empty() {
1455 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1456 eprintln!("q1-graph: empty plan at layer {start}");
1457 }
1458 if self.graph_head_required {
1459 self.fail_metal_graph("native graph plan is empty");
1460 }
1461 return start;
1462 }
1463 let has_moe = plan.iter().any(|it| match it {
1464 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1465 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1466 });
1467 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1468 let dev_attend = attend_contract
1469 && (self.head_dim <= 128
1470 || has_moe
1471 || (self.head_dim <= 256 && has_gdn)
1477 || attend_mode == "force"
1478 || attend_mode == "256");
1479 if !dev_attend {
1480 for it in &mut plan {
1481 if let Item::Attn { li, full_gpu, .. } = it {
1482 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1485 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1486 if !keep_o1 {
1487 *full_gpu = false;
1488 }
1489 }
1490 }
1491 }
1492 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1493 use std::sync::atomic::{AtomicBool, Ordering};
1494 static SAID: AtomicBool = AtomicBool::new(false);
1495 if !SAID.swap(true, Ordering::Relaxed) {
1496 let fg = plan
1497 .iter()
1498 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1499 .count();
1500 let att = plan
1501 .iter()
1502 .filter(|it| matches!(it, Item::Attn { .. }))
1503 .count();
1504 eprintln!(
1505 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1506 plan.len(),
1507 self.head_dim,
1508 self.rotary_dim,
1509 self.num_kv_heads,
1510 self.num_heads,
1511 );
1512 }
1513 }
1514 let dims = GraphDims {
1515 hidden: self.hidden_size,
1516 eps: self.rms_eps as f32,
1517 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1518 };
1519 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1520 if self.graph_head_required {
1521 self.fail_metal_graph("native TokenGraph allocation refused");
1522 }
1523 return start;
1524 };
1525 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1526 nv: cfg.num_v_heads,
1527 nk: cfg.num_k_heads,
1528 dk: cfg.key_head_dim,
1529 dv: cfg.value_head_dim,
1530 kk: cfg.conv_kernel,
1531 hidden: self.hidden_size,
1532 inter: self.intermediate_size,
1533 c_dim: cfg.conv_dim(),
1534 eps: cfg.rms_eps as f32,
1535 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1536 });
1537 let mut valid = 0usize;
1541 let mut end = start;
1542 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1544 static ONCE: std::sync::Once = std::sync::Once::new();
1545 ONCE.call_once(|| {
1546 for it in &plan {
1547 match it {
1548 Item::Gdn { first, run } => {
1549 eprintln!("plan: Gdn first={first} len={}", run.len())
1550 }
1551 Item::Attn { li, full_gpu, .. } => {
1552 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1553 }
1554 }
1555 }
1556 });
1557 }
1558 for item in &plan {
1559 let ok = match item {
1560 Item::Gdn { run, .. } => gcfg
1561 .as_ref()
1562 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1563 .unwrap_or(false),
1564 Item::Attn { l, .. } => graph.attn_ok(l),
1565 };
1566 if !ok {
1567 if block_diag {
1568 eprintln!(
1569 "block-graph: plan item {} ({}) failed graph preflight",
1570 valid,
1571 match item {
1572 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1573 Item::Attn { li, .. } => format!("Attn L{li}"),
1574 }
1575 );
1576 }
1577 break;
1578 }
1579 valid += 1;
1580 end += match item {
1581 Item::Gdn { run, .. } => run.len(),
1582 Item::Attn { .. } => 1,
1583 };
1584 }
1585 plan.truncate(valid);
1586 if plan.is_empty() {
1587 if self.graph_head_required {
1588 self.fail_metal_graph("native graph preflight produced no valid items");
1589 }
1590 return start;
1591 }
1592
1593 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
1594 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
1595 return start;
1596 }
1597
1598 let inv_freq = self.inv_freq.clone();
1599 let pool = self.pool.clone();
1600 let (nh, nkv, hd, hs, rd, eps) = (
1601 self.num_heads,
1602 self.num_kv_heads,
1603 self.head_dim,
1604 self.hidden_size,
1605 self.rotary_dim,
1606 self.rms_eps,
1607 );
1608 let norm_style = self.norm_style;
1609 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1610 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1611 let kv_id = self.graph_kv_id;
1612 let mut pending: Vec<(usize, usize)> = Vec::new();
1615 let mut dev_attn: Vec<usize> = Vec::new();
1618 for item in &plan {
1619 let _xt0 = std::time::Instant::now();
1620 let _xkind: u32 = match item {
1621 Item::Gdn { .. } => 2,
1622 Item::Attn { .. } => 3,
1623 };
1624 if self.loop_final_norm {
1626 let item_start = match item {
1627 Item::Gdn { first, .. } => *first,
1628 Item::Attn { li, .. } => *li,
1629 };
1630 if item_start > start && self.is_loop_end(item_start - 1) {
1631 graph.encode_loop_norm(&self.weights.final_norm);
1632 }
1633 }
1634 match item {
1635 Item::Gdn { run, first } => {
1636 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1637 if l.linear_state.len() != want {
1638 l.linear_state = vec![0f32; want];
1639 }
1640 }
1641 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1642 .iter()
1643 .map(|l| l.linear_state.as_slice())
1644 .collect();
1645 let _ig = std::time::Instant::now();
1646 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1647 tracing::error!("q1 graph: GDN run refused after validation");
1649 return start;
1650 }
1651 graph.commit_kind = 2;
1654 graph.commit();
1655 crate::gpu::stageprof(0, _ig.elapsed());
1656 pending.push((*first, run.len()));
1657 }
1658 Item::Attn {
1659 l,
1660 li,
1661 q_norm,
1662 k_norm,
1663 output_gate,
1664 bias,
1665 full_gpu,
1666 } => {
1667 let _ia = std::time::Instant::now();
1668 if *full_gpu {
1670 let cache = &self.kv_cache.layers[*li];
1671 let o1p = if cache.o1.is_some() {
1672 match cache.o1_views() {
1673 Some(views) => Some(crate::gpu::O1AttnParams {
1674 views,
1675 epoch: self.o1_epoch,
1676 }),
1677 None => None,
1679 }
1680 } else {
1681 None
1682 };
1683 let o1_layer = cache.o1.is_some();
1684 if o1_layer && o1p.is_none() {
1685 }
1687 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1688 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1689 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1690 let p = crate::gpu::AttnDeviceParams {
1691 kv_id,
1692 layer: *li,
1693 nh,
1694 nkv,
1695 hd,
1696 rd,
1697 position,
1698 scale: self.attn_scale,
1699 eps: eps as f32,
1700 gemma,
1701 late_qk_norm: self.qk_norm_after_rope,
1702 output_gate: *output_gate,
1703 q_norm: *q_norm,
1704 k_norm: *k_norm,
1705 inv_freq: &inv_freq,
1706 cpu_k,
1707 cpu_v,
1708 cpu_stored,
1709 o1: o1p,
1710 };
1711 let o1_bad = o1_layer && p.o1.is_none();
1712 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1713 {
1714 if p.o1.is_none() {
1716 dev_attn.push(*li);
1717 }
1718 graph.commit_kind = 3;
1719 graph.commit();
1720 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1724 continue;
1725 }
1726 }
1728 graph.encode_attn_prefix(l);
1729 if let Err(err) = graph.sync_checked() {
1730 self.fail_metal_graph(&err);
1731 return start;
1732 }
1733 if !pending.is_empty() {
1734 let idxs: Vec<usize> =
1735 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1736 let mut outs: Vec<&mut [f32]> = self
1737 .kv_cache
1738 .layers
1739 .iter_mut()
1740 .enumerate()
1741 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1742 .map(|(_, s)| s.linear_state.as_mut_slice())
1743 .collect();
1744 graph.read_states(&mut outs);
1745 }
1746 let mut q_raw = attention::take_buf(l.wq.1);
1747 let mut k = attention::take_buf(l.wk.1);
1748 let mut v = attention::take_buf(l.wv.1);
1749 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1750 let cfg = QwenAttnCfg {
1751 num_heads: nh,
1752 num_kv_heads: nkv,
1753 head_dim: hd,
1754 hidden_size: hs,
1755 position,
1756 inv_freq: &inv_freq,
1757 rotary_dim: rd,
1758 scale: self.attn_scale,
1759 softcap: self.attn_softcap,
1760 window: None,
1761 v_norm: false,
1762 qk_norm_after_rope: self.qk_norm_after_rope,
1763 q_norm: *q_norm,
1764 k_norm: *k_norm,
1765 output_gate: *output_gate,
1766 softplus_gate: None,
1767 rope_scale: 1.0,
1768 bias: *bias,
1769 rms_eps: eps,
1770 norm_style,
1771 pool: pool.as_deref(),
1772 };
1773 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1776 || std::env::var("CMF_ATTN_DUMP").is_ok();
1777 let _ = full_gpu;
1778 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1779 let mut ao = attention::qwen_attention_core(
1780 q_raw,
1781 k,
1782 v,
1783 &mut self.kv_cache.layers[*li],
1784 &cfg,
1785 );
1786 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1790 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1791 let (cq, _cg, _ck, _cv) =
1792 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1793 let cache = &self.kv_cache.layers[*li];
1794 let n = cache.head_keys(0).len() / hd;
1795 let mut bytes: Vec<u8> = Vec::new();
1796 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1797 bytes.extend_from_slice(&v.to_le_bytes());
1798 }
1799 for v in &cq {
1800 bytes.extend_from_slice(&v.to_le_bytes());
1801 }
1802 for g in 0..nkv {
1803 for v in cache.head_keys(g) {
1804 bytes.extend_from_slice(&v.to_le_bytes());
1805 }
1806 }
1807 for g in 0..nkv {
1808 for v in cache.head_values(g) {
1809 bytes.extend_from_slice(&v.to_le_bytes());
1810 }
1811 }
1812 let _ =
1813 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1814 }
1815 }
1816 if let Some((qr0, k0, v0)) =
1817 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1818 {
1819 let (cq, _cg, ck, cv) =
1820 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1821 let mut h_now = vec![0f32; hs];
1822 graph.read_h(&mut h_now);
1823 let cache = &self.kv_cache.layers[*li];
1824 let n_after = cache.head_keys(0).len() / hd;
1825 let stored = n_after.saturating_sub(1);
1829 let cpu_k: Vec<&[f32]> = (0..nkv)
1830 .map(|g| &cache.head_keys(g)[..stored * hd])
1831 .collect();
1832 let cpu_v: Vec<&[f32]> = (0..nkv)
1833 .map(|g| &cache.head_values(g)[..stored * hd])
1834 .collect();
1835 let p = crate::gpu::AttnDeviceParams {
1836 kv_id,
1837 layer: *li,
1838 nh,
1839 nkv,
1840 hd,
1841 rd,
1842 position,
1843 scale: self.attn_scale,
1844 eps: eps as f32,
1845 gemma,
1846 late_qk_norm: self.qk_norm_after_rope,
1847 output_gate: *output_gate,
1848 q_norm: *q_norm,
1849 k_norm: *k_norm,
1850 inv_freq: &inv_freq,
1851 cpu_k,
1852 cpu_v,
1853 cpu_stored: stored,
1854 o1: None,
1855 };
1856 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1857 let md = |a: &[f32], b: &[f32]| {
1858 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1859 };
1860 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1861 eprintln!(
1862 "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}",
1863 nn(&cq),
1864 md(&cq, &dq),
1865 nn(&ck),
1866 md(&ck, &dk),
1867 nn(&cv),
1868 md(&cv, &dv),
1869 nn(&ao),
1870 md(&ao, &dao)
1871 );
1872 } else {
1873 eprintln!("attn-oracle L{li}: device probe declined");
1874 }
1875 }
1876 graph.encode_attn_suffix(l, &ao);
1877 graph.commit();
1880 attention::recycle_buf(&mut ao);
1881 }
1882 }
1883
1884 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1885 }
1886 let mut lm_rows = None;
1891 if self.graph_want_logits
1892 && upto.is_none()
1893 && end == self.num_layers
1894 && std::env::var("CMF_GPU_LMHEAD")
1895 .map(|v| v != "0")
1896 .unwrap_or(true)
1897 {
1898 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
1899 if graph.lm_head_ok(lm) {
1900 graph.encode_lm_head(&self.weights.final_norm, lm);
1901 lm_rows = Some(lm.1);
1902 }
1903 }
1904 }
1905 if self.graph_head_required && lm_rows.is_none() {
1906 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1907 self.fail_metal_graph("fused graph head was requested but not encodable");
1908 return start;
1909 }
1910 let _sy0 = std::time::Instant::now();
1911 if let Err(err) = graph.sync_checked() {
1912 self.fail_metal_graph(&err);
1913 return start;
1914 }
1915 let _rs0 = std::time::Instant::now();
1916 if !pending.is_empty() {
1917 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1918 let mut outs: Vec<&mut [f32]> = self
1919 .kv_cache
1920 .layers
1921 .iter_mut()
1922 .enumerate()
1923 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1924 .map(|(_, s)| s.linear_state.as_mut_slice())
1925 .collect();
1926 graph.read_states(&mut outs);
1927 }
1928 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1929 use std::sync::atomic::{AtomicU64, Ordering};
1930 static SY: AtomicU64 = AtomicU64::new(0);
1931 static RS: AtomicU64 = AtomicU64::new(0);
1932 static N: AtomicU64 = AtomicU64::new(0);
1933 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1934 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1935 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1936 if n % 100 == 0 {
1937 eprintln!(
1938 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1939 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1940 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1941 );
1942 }
1943 }
1944 if let Some(rows) = lm_rows {
1945 crate::gpu::hostprof_encode_done(_mt0);
1946 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1947 graph.read_logits(&mut lg);
1948 crate::gpu::hostprof_total(_mt0);
1949 lg.resize(self.vocab_size, 0.0);
1950 if let Some(c) = self.final_softcap {
1951 for l in lg.iter_mut() {
1952 *l = c * (*l / c).tanh();
1953 }
1954 }
1955 self.graph_logits = Some(lg);
1956 }
1957 graph.read_h(h);
1958 if self.graph_head_required && self.graph_logits.is_none() {
1959 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1960 self.fail_metal_graph("fused graph head completed without logits readback");
1961 return start;
1962 }
1963 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1964 METAL_GRAPH_LAYERS.fetch_add(
1965 end.saturating_sub(start) as u64,
1966 std::sync::atomic::Ordering::Relaxed,
1967 );
1968 if self.graph_head_required {
1969 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1970 }
1971 for li in dev_attn {
1975 let mut krow = attention::take_buf(nkv * hd);
1976 let mut vrow = attention::take_buf(nkv * hd);
1977 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1978 let cache = &mut self.kv_cache.layers[li];
1979 cache.append(&krow, &vrow, &[]);
1980 let n = cache.seq_len;
1981 let mut imp = attention::take_buf(n);
1982 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1983 cache.accumulate_imp(&imp);
1984 attention::recycle_buf(&mut imp);
1985 }
1986 attention::recycle_buf(&mut krow);
1987 attention::recycle_buf(&mut vrow);
1988 }
1989 end
1990 }
1991
1992 pub fn new(
1993 tokenizer: Tokenizer,
1994 weights: PipelineWeights,
1995 hidden_size: usize,
1996 intermediate_size: usize,
1997 num_heads: usize,
1998 num_kv_heads: usize,
1999 head_dim: usize,
2000 num_layers: usize,
2001 physical_layers: usize,
2002 loop_final_norm: bool,
2003 vocab_size: usize,
2004 rms_eps: f64,
2005 rope_base: f32,
2006 norm_style: NormStyle,
2007 max_seq_len: usize,
2008 sampler_config: SamplerConfig,
2009 ) -> Self {
2010 let rng = match sampler_config.seed {
2011 Some(s) => SplitMix64::new(s),
2012 None => SplitMix64::from_entropy(),
2013 };
2014 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
2015 let pool = Pool::from_env();
2016 if let Some(p) = &pool {
2017 tracing::info!("worker pool: {} threads", p.n_workers());
2018 }
2019 Self {
2020 gpu_plan: None,
2021 tokenizer: std::sync::Arc::new(tokenizer),
2022 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
2023 sampler_config,
2024 weights,
2025 hidden_size,
2026 intermediate_size,
2027 num_heads,
2028 num_kv_heads,
2029 head_dim,
2030 num_layers,
2031 physical_layers,
2032 loop_final_norm,
2033 vocab_size,
2034 rms_eps,
2035 rope_base,
2036 norm_style,
2037 rotary_dim: head_dim,
2038 attention_heads_per_layer: None,
2039 vmf_cfg: None,
2040 gdn_cfg: None,
2041 kda_cfg: None,
2042 g3n: None,
2043 dsv4: None,
2044 dsv41: None,
2045 dsv41_vision: None,
2046 dsv41_prefill: None,
2047 qwen4_exp: None,
2048 dsv4_mtp: Vec::new(),
2049 dspark: None,
2050 dspark_pending: Vec::new(),
2051 dspark_hist: Vec::new(),
2052 dspark_real: Vec::new(),
2053 dspark_trunk_picks: Vec::new(),
2054 dspark_exp: Vec::new(),
2055 dspark_draft_ns: 0,
2056 logit_multiplier: None,
2057 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2058 graph_failed: std::sync::atomic::AtomicBool::new(false),
2059 kv_history: Vec::new(),
2060 short_conv_cfg: None,
2061 mtp: None,
2062 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
2063 rng,
2064 sampler_scratch: SamplerScratch::default(),
2065 spec_forced: None,
2066 spec_q: Vec::new(),
2067 spec_p: Vec::new(),
2068 spec_res: Vec::new(),
2069 spec_qs: Vec::new(),
2070 spec_ps: Vec::new(),
2071 spec_ress: Vec::new(),
2072 mtp_graph_mode: None,
2073 #[cfg(target_os = "macos")]
2074 metal_verify: None,
2075 inv_freq,
2076 ws: ForwardScratch::new(hidden_size),
2077 pool,
2078 model: None,
2079 dyn_force_f32: false,
2080 dyn_skill_layers: Vec::new(),
2081 dyn_active: None,
2082 dyn_blend_loaded: false,
2083 dyn_phi_layer: None,
2084 dyn_phi_ema: Vec::new(),
2085 dyn_phi_seen: 0,
2086 dyn_router: None,
2087 o1_cfg: None,
2088 o1_epoch: 0,
2089 o1_flags: Vec::new(),
2090 trace: false,
2091 calib_temp: 1.0,
2092 confidence_on: true,
2093 embed_multiplier: 1.0,
2094 attn_scale: 1.0 / (head_dim as f32).sqrt(),
2095 swa: None,
2096 sliding_layers: None,
2097 inv_freq_local: None,
2098 rotary_dim_local: None,
2099 rope_scale: 1.0,
2100 rope_scale_local: 1.0,
2101 global_attn: None,
2102 inv_freq_global: None,
2103 attn_v_norm: false,
2104 qk_norm_after_rope: false,
2105 final_softcap: None,
2106 head_clusters: None,
2107 attn_softcap: 0.0,
2108 graph_want_logits: false,
2109 graph_head_required: false,
2110 graph_logits: None,
2111 graph_kv_id: {
2112 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
2113 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2114 },
2115 #[cfg(test)]
2116 nll_test_fail_at: None,
2117 #[cfg(test)]
2118 nll_test_force_serial: false,
2119 }
2120 }
2121
2122 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2130 if let Some(c) = &cfg {
2131 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2132 tracing::error!(
2133 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2134 c.w,
2135 c.sink
2136 );
2137 self.o1_flags.clear();
2138 self.o1_cfg = None;
2139 return;
2140 }
2141 }
2142 self.o1_flags = match &cfg {
2143 Some(c) => {
2144 let mut flags = c.layer_flags(self.num_layers);
2145 for (li, f) in flags.iter_mut().enumerate() {
2146 if *f
2147 && !matches!(
2148 self.weights.layers[self.phys_layer(li)].attn,
2149 AttnKind::Full { .. }
2150 )
2151 {
2152 *f = false;
2153 }
2154 }
2155 flags
2156 }
2157 None => Vec::new(),
2158 };
2159 if let Some(c) = &cfg {
2160 let n = self.o1_flags.iter().filter(|&&f| f).count();
2161 tracing::info!(
2162 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2163 self.num_layers,
2164 c.m,
2165 c.w,
2166 c.sink,
2167 c.rect
2168 );
2169 }
2170 self.o1_cfg = cfg;
2171 }
2172
2173 pub fn o1_active(&self) -> bool {
2175 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2176 }
2177
2178 pub fn generation_graph_prefill(&self) -> bool {
2183 let graph = self.graph_prefill_preferred();
2184 #[cfg(not(target_os = "macos"))]
2192 if graph
2193 && self.mtp.is_some()
2194 && std::env::var("CMF_BATCH_K")
2195 .ok()
2196 .and_then(|v| v.parse::<usize>().ok())
2197 .is_some_and(|k| k > 0)
2198 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2199 {
2200 return false;
2201 }
2202 graph
2203 }
2204
2205 pub fn o1_device_stats(&self) -> (usize, u64) {
2210 crate::gpu::o1_device_stats(self.graph_kv_id)
2211 }
2212
2213 pub fn o1_begin(&mut self) {
2218 self.o1_begin_with_prefix(None);
2219 }
2220
2221 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2225 if let Some(c) = &self.o1_cfg {
2226 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2227 let boundary = requested_prefix.map(|p| {
2228 p.max(
2229 crate::nystrom::o1_deferred_boundary(w, sink)
2230 .expect("o1 config boundary validated in set_o1"),
2231 )
2232 });
2233 for (li, &f) in self.o1_flags.iter().enumerate() {
2234 if f {
2235 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2236 }
2237 }
2238 }
2239 }
2240
2241 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2243 self.o1_cfg.as_ref().and_then(|c| {
2244 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2245 .map(|floor| requested_prefix.max(floor))
2246 })
2247 }
2248
2249 fn o1_note_transition(&mut self) {
2250 let mut transitioned = false;
2254 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2255 if flagged {
2256 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2257 }
2258 }
2259 if transitioned {
2260 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2261 }
2262 }
2263
2264 fn o1_pending(&self) -> bool {
2265 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2266 f && self.kv_cache.layers[li].seq_len > 0
2267 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2268 })
2269 }
2270
2271 fn o1_fail(&mut self, err: String) {
2272 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2273 self.clear_sequence_state();
2274 self.graph_failed
2275 .store(true, std::sync::atomic::Ordering::Relaxed);
2276 self.cancel
2277 .store(true, std::sync::atomic::Ordering::Relaxed);
2278 }
2279
2280 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2285 if self.o1_cfg.is_none() {
2286 return Ok(false);
2287 }
2288 let mut participating = false;
2289 for li in 0..self.num_layers {
2290 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2291 continue;
2292 }
2293 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2294 return Err(err);
2295 }
2296 if self.kv_cache.layers[li].seq_len == 0 {
2297 continue;
2298 }
2299 participating = true;
2300 let num_heads = self.layer_num_heads(li);
2301 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2302 }
2303 self.o1_note_transition();
2304 for li in 0..self.num_layers {
2305 if self.o1_flags.get(li).copied().unwrap_or(false) {
2306 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2307 return Err(err);
2308 }
2309 }
2310 }
2311 Ok(participating
2312 && (0..self.num_layers).all(|li| {
2313 !self.o1_flags.get(li).copied().unwrap_or(false)
2314 || self.kv_cache.layers[li].seq_len == 0
2315 || self.kv_cache.layers[li].o1_sealed()
2316 }))
2317 }
2318
2319 fn o1_progress(&mut self) {
2322 if !self.o1_active() {
2323 return;
2324 }
2325 for li in 0..self.num_layers {
2326 if self.o1_flags.get(li).copied().unwrap_or(false) {
2327 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2328 self.o1_fail(err);
2329 return;
2330 }
2331 }
2332 }
2333 self.o1_note_transition();
2337 if !self.o1_pending() {
2338 return;
2339 }
2340 if let Err(err) = self.o1_seal_checked() {
2341 self.o1_fail(err);
2342 }
2343 }
2344
2345 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2350 if self
2351 .graph_failed
2352 .swap(false, std::sync::atomic::Ordering::Relaxed)
2353 {
2354 self.cancel
2355 .store(false, std::sync::atomic::Ordering::Relaxed);
2356 self.clear_sequence_state();
2357 return Err(format!("{phase}: deferred O(1) transition failed"));
2358 }
2359 Ok(())
2360 }
2361
2362 pub fn o1_seal(&mut self) {
2366 if let Err(err) = self.o1_seal_checked() {
2367 self.o1_fail(err);
2368 }
2369 }
2370
2371 pub fn set_trace(&mut self, on: bool) {
2373 self.trace = on;
2374 }
2375
2376 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2379 self.rng = match config.seed {
2380 Some(seed) => SplitMix64::new(seed),
2381 None => SplitMix64::from_entropy(),
2382 };
2383 self.sampler_config = config;
2384 }
2385
2386 pub fn set_confidence(&mut self, on: bool) {
2391 self.confidence_on = on;
2392 }
2393
2394 pub fn set_calib_temp(&mut self, t: f32) {
2397 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2398 }
2399
2400 pub fn calib_temp(&self) -> f32 {
2402 self.calib_temp
2403 }
2404
2405 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2408 self.rotary_dim = rotary_dim.min(self.head_dim);
2409 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2410 }
2411
2412 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2413 QwenAttnCfg {
2414 num_heads: self.num_heads,
2415 num_kv_heads: self.num_kv_heads,
2416 head_dim: self.head_dim,
2417 hidden_size: self.hidden_size,
2418 position,
2419 inv_freq: &self.inv_freq,
2420 rotary_dim: self.rotary_dim,
2421 scale: self.attn_scale,
2422 softcap: self.attn_softcap,
2423 window: None,
2424 v_norm: false,
2425 qk_norm_after_rope: self.qk_norm_after_rope,
2426 q_norm: None,
2427 k_norm: None,
2428 output_gate: false,
2429 softplus_gate: None,
2430 rope_scale: self.rope_scale,
2431 bias: None,
2432 rms_eps: self.rms_eps,
2433 norm_style: self.norm_style,
2434 pool: self.pool.as_deref(),
2435 }
2436 }
2437
2438 pub fn generate(
2440 &mut self,
2441 prompt: &str,
2442 max_tokens: usize,
2443 task_mask: Option<&TaskMask>,
2444 on_token: Option<TokenCallback>,
2445 ) -> Result<GenerateResult, String> {
2446 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2447 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2448 }
2449
2450 pub fn generate_from_vl(
2453 &mut self,
2454 input: &crate::dsv41_vision::PreparedVlInputs,
2455 max_tokens: usize,
2456 task_mask: Option<&TaskMask>,
2457 on_token: Option<TokenCallback>,
2458 ) -> Result<GenerateResult, String> {
2459 let Some(dsv41) = &self.dsv41 else {
2460 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2461 };
2462 if input.token_ids.is_empty() {
2463 return Err("empty V4.1 multimodal prompt".into());
2464 }
2465 if input.token_types.len() != input.token_ids.len() {
2466 return Err(format!(
2467 "V4.1 token type count {} != token count {}",
2468 input.token_types.len(),
2469 input.token_ids.len()
2470 ));
2471 }
2472 let dim = dsv41.2.dim;
2473 let mut embeddings = vec![None; input.token_ids.len()];
2474 let mut participates = vec![true; input.token_ids.len()];
2475 if !input.images.is_empty() {
2476 let vision = self
2477 .dsv41_vision
2478 .as_ref()
2479 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2480 for image in &input.images {
2481 let end = image.start.saturating_add(image.types.len());
2482 if end > input.token_ids.len() {
2483 return Err(format!(
2484 "V4.1 image span {}..{} exceeds prompt length {}",
2485 image.start,
2486 end,
2487 input.token_ids.len()
2488 ));
2489 }
2490 let mut span = vec![0.0f32; image.types.len() * dim];
2491 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2492 for (offset, &kind) in image.types.iter().enumerate() {
2493 let pos = image.start + offset;
2494 if input.token_types[pos] != kind {
2495 return Err(format!(
2496 "V4.1 image type mismatch at position {pos}: {} != {kind}",
2497 input.token_types[pos]
2498 ));
2499 }
2500 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2501 participates[pos] = false;
2502 }
2503 }
2504 }
2505 for (pos, &kind) in input.token_types.iter().enumerate() {
2506 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2507 return Err(format!("V4.1 text position {pos} has an image embedding"));
2508 }
2509 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2510 return Err(format!("V4.1 image position {pos} has no image embedding"));
2511 }
2512 }
2513 self.dsv41_prefill = Some((embeddings, participates));
2514 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2515 self.dsv41_prefill = None;
2516 result
2517 }
2518
2519 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2521 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2522 }
2523
2524 pub fn generate_from_ids(
2532 &mut self,
2533 input_ids: &[u32],
2534 max_tokens: usize,
2535 task_mask: Option<&TaskMask>,
2536 mut on_token: Option<TokenCallback>,
2537 ) -> Result<GenerateResult, String> {
2538 if std::env::var("CMF_TRACE_H").is_ok() {
2539 eprintln!("input_ids: {input_ids:?}");
2540 }
2541 if input_ids.is_empty() {
2542 return Err("empty prompt: nothing to generate from".to_string());
2543 }
2544 self.graph_failed
2548 .store(false, std::sync::atomic::Ordering::Relaxed);
2549 let task_mask = self.drop_open_mask(task_mask);
2554
2555 let reuse_from = {
2563 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2564 let h = &self.kv_history;
2565 if on
2566 && task_mask.is_none()
2567 && self.mtp.is_none()
2568 && self.o1_cfg.is_none()
2569 && self.dsv41.is_none()
2570 && !h.is_empty()
2571 && h.len() < input_ids.len()
2572 && input_ids[..h.len()] == h[..]
2573 {
2574 h.len()
2575 } else {
2576 0
2577 }
2578 };
2579 if reuse_from == 0 {
2580 self.clear_sequence_state();
2582 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2583 eprintln!(
2584 "kv-reuse: {} of {} prompt positions already cached",
2585 reuse_from,
2586 input_ids.len()
2587 );
2588 }
2589 crate::gpu::graph_race_begin_generation();
2590 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2594 std::env::var("CMF_O1_PREFILL")
2595 .ok()
2596 .and_then(|v| v.parse::<usize>().ok())
2597 .filter(|&p| p > 0)
2598 } else {
2599 None
2600 };
2601 if task_mask.is_none() {
2602 self.o1_begin_with_prefix(o1_prefill);
2603 }
2604
2605 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2611 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2638 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2639 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2655 for lw in &self.weights.layers {
2656 if let FfnKind::Dense(d) = &lw.ffn {
2657 dense_n += 1;
2658 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2659 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2660 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2661 {
2662 dense_q4tp += 1;
2663 }
2664 }
2665 }
2666 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2667 let penalized = self.sampler_config.repetition_penalty != 1.0
2671 || self.sampler_config.presence_penalty != 0.0
2672 || !self.sampler_config.suppress_tokens.is_empty();
2673 #[cfg(feature = "gpu")]
2678 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2679 #[cfg(not(feature = "gpu"))]
2680 let metal_wgpu = false;
2681 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2682 let spec_wanted = match spec_env.as_deref() {
2683 Some("0") => false,
2684 Some(_) => {
2685 if metal_wgpu {
2686 tracing::warn!(
2687 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2688 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2689 );
2690 }
2691 true
2692 }
2693 None => spec_default_ok && !penalized && !metal_wgpu,
2694 };
2695 #[cfg(target_os = "macos")]
2698 let metal_graph = crate::gpu::q1_force()
2699 && crate::gpu::enabled_here()
2700 && std::env::var("CMF_GPU_BLOCK")
2701 .map(|v| v != "0")
2702 .unwrap_or(true);
2703 #[cfg(not(target_os = "macos"))]
2704 let metal_graph = false;
2705 let graph_spec = self.speculative
2706 && (graph_on || metal_graph)
2707 && self.mtp.is_some()
2708 && task_mask.is_none()
2709 && !self.o1_active()
2710 && spec_sampling_ok
2711 && spec_wanted;
2712 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2719 let spec_active = self.speculative
2720 && self.mtp.is_some()
2721 && task_mask.is_none()
2722 && !self.o1_active()
2723 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2724 let mut mtp = if spec_active { self.mtp.take() } else { None };
2727 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2728 eprintln!(
2729 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2730 mtp.is_some(),
2731 self.speculative,
2732 self.sampler_config.temperature < 1e-6,
2733 );
2734 }
2735 if let Some(m) = &mut mtp {
2736 m.kv.clear();
2737 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2739 self.mtp_graph_mode = None;
2740 }
2741 let mut router = if mtp.is_none() {
2745 self.dyn_router.take()
2746 } else {
2747 None
2748 };
2749 if let Some(r) = &mut router {
2750 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2753 }
2754
2755 let mut all_ids = input_ids.to_vec();
2756 let mut generated = 0usize;
2757 let mut finish_reason = "max_tokens".to_string();
2758 let mut drafted = 0usize;
2759 let mut accepted = 0usize;
2760 let mut dsv4_spec_bad = 0usize;
2767 let mut dsv4_spec_retry_at = 0usize;
2768 let mut confidence: Vec<f32> = Vec::new();
2769 let trace_on = self.trace;
2770 let calib_temp = self.calib_temp;
2771 let mut traces: Vec<TokenTrace> = Vec::new();
2772
2773 let mut hidden = vec![0.0f32; self.hidden_size];
2779 let mut pos = reuse_from;
2780 let fuse_lm = mtp.is_none()
2789 && router.is_none()
2790 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2791 self.graph_logits = None;
2792 self.graph_want_logits = false;
2793 let _tpf = std::time::Instant::now();
2794 let batch_k = std::env::var("CMF_BATCH_K")
2795 .ok()
2796 .and_then(|v| v.parse::<usize>().ok())
2797 .unwrap_or(0);
2798 while self.qwen4_exp.is_some()
2809 && mtp.is_none()
2810 && pos < input_ids.len()
2811 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2812 {
2813 let token_id = input_ids[pos];
2814 let want_logits = pos + 1 == input_ids.len();
2815 let mut lg = Vec::new();
2816 if let Some(b) = &mut self.qwen4_exp {
2817 crate::qwen4_exp::forward_token(
2818 &b.0,
2819 &b.1,
2820 &b.2,
2821 &mut b.3,
2822 token_id,
2823 pos,
2824 &self.inv_freq,
2825 self.pool.as_deref(),
2826 &mut lg,
2827 want_logits,
2828 );
2829 }
2830 if want_logits {
2831 self.graph_logits = Some(lg);
2832 }
2833 pos += 1;
2834 hidden.fill(0.0);
2835 }
2836 while self.dsv4.is_some()
2837 && mtp.is_none()
2838 && pos < input_ids.len()
2839 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2840 {
2841 let end = (pos + prefill_chunk()).min(input_ids.len());
2842 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2843 let mut lg = Vec::new();
2844 if let Some(b) = &mut self.dsv4 {
2845 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2846 crate::dsv4::forward_chunk(
2847 g,
2848 layers,
2849 &cfg,
2850 st,
2851 &ids,
2852 pos,
2853 &self.inv_freq,
2854 self.pool.as_deref(),
2855 &mut lg,
2856 end == input_ids.len(),
2857 );
2858 }
2859 if end == input_ids.len() {
2860 self.graph_logits = Some(lg);
2861 }
2862 pos = end;
2863 hidden = vec![0.0; self.hidden_size];
2864 }
2865 let dsv41_prefill = self.dsv41_prefill.take();
2866 while self.dsv41.is_some()
2867 && mtp.is_none()
2868 && pos < input_ids.len()
2869 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2870 {
2871 let end = (pos + prefill_chunk()).min(input_ids.len());
2872 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2873 let mut lg = Vec::new();
2874 if let Some(b) = &mut self.dsv41 {
2875 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2876 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2877 crate::dsv41::forward_chunk_masked_with_embeddings(
2878 g,
2879 layers,
2880 cfg,
2881 st,
2882 &ids,
2883 pos,
2884 &embeddings[pos..end],
2885 &participates[pos..end],
2886 self.pool.as_deref(),
2887 &mut lg,
2888 );
2889 } else {
2890 crate::dsv41::forward_chunk(
2891 g,
2892 layers,
2893 cfg,
2894 st,
2895 &ids,
2896 pos,
2897 self.pool.as_deref(),
2898 &mut lg,
2899 );
2900 }
2901 }
2902 if end == input_ids.len() {
2903 self.graph_logits = Some(lg);
2904 }
2905 pos = end;
2906 hidden = vec![0.0; self.hidden_size];
2907 }
2908 let dyn_prefill = router.is_some();
2913 let o1_prefill_limit = o1_prefill
2921 .and_then(|requested| self.o1_effective_boundary(requested))
2922 .map(|boundary| boundary.min(input_ids.len()));
2923 let mut o1_sealed = false;
2924 if let Some(limit) = o1_prefill_limit {
2925 if self.can_prefill_batched() && limit > 2 {
2928 let chunk = prefill_chunk();
2929 let hs = self.hidden_size;
2930 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2931 let end = (pos + chunk).min(limit);
2932 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2933 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2934 pos = end;
2935 }
2936 } else {
2937 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2938 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2939 pos += 1;
2940 }
2941 }
2942 if pos >= limit {
2943 o1_sealed = match self.o1_seal_checked() {
2944 Ok(sealed) => sealed,
2945 Err(err) => {
2946 self.finish_generation(&mut mtp, &mut router, true);
2947 return Err(err);
2948 }
2949 };
2950 tracing::info!(
2951 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2952 o1_prefill.unwrap_or(0),
2953 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2954 .unwrap_or(limit),
2955 limit,
2956 input_ids.len()
2957 );
2958 }
2959 }
2960 let graph_prefill = self.graph_prefill_preferred();
2966 #[cfg(target_os = "macos")]
2974 if task_mask.is_none()
2975 && !dyn_prefill
2976 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
2977 && crate::gpu::enabled_here()
2978 && self.gdn_cfg.is_some()
2979 && self.g3n.is_none()
2980 && input_ids.len() > 8
2981 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2982 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2983 {
2984 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2985 .ok()
2986 .and_then(|v| v.parse().ok())
2987 .filter(|&v| (16..=512).contains(&v))
2988 .unwrap_or(256);
2989 let hs = self.hidden_size;
2990 let _tp = std::time::Instant::now();
2991 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2992 let end = (pos + chunk).min(input_ids.len());
2993 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
2994 MetalPrefillOutcome::Completed(hb) => hb,
2995 MetalPrefillOutcome::Declined => break,
2996 MetalPrefillOutcome::Failed => {
2997 self.finish_generation(&mut mtp, &mut router, true);
2998 return Err("ordinary Metal prefill failed after admission".into());
2999 }
3000 };
3001 if let Some(m) = &mut mtp {
3002 let n_pairs = if end < input_ids.len() {
3003 end - pos
3004 } else {
3005 end - pos - 1
3006 };
3007 if n_pairs > 0 {
3008 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
3009 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
3010 .collect();
3011 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
3012 for (j, (h, t)) in pairs.iter().enumerate() {
3013 let h = h.to_vec();
3014 let _ = self.mtp_step(m, &h, *t, pos + j);
3015 }
3016 }
3017 }
3018 }
3019 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3020 pos = end;
3021 }
3022 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3023 eprintln!(
3024 "metal-prefill: {} of {} tokens in {:.1} ms",
3025 pos,
3026 input_ids.len(),
3027 _tp.elapsed().as_secs_f64() * 1e3
3028 );
3029 }
3030 }
3031 if task_mask.is_none()
3032 && !dyn_prefill
3033 && !graph_prefill
3034 && self.can_prefill_batched()
3035 && self.g3n.is_none()
3036 && o1_prefill.is_none()
3037 && input_ids.len() > 2
3038 {
3039 let chunk = prefill_chunk();
3045 let hs = self.hidden_size;
3046 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3047 let end = (pos + chunk).min(input_ids.len());
3048 let hb = self.prefill_batch(&input_ids[pos..end], pos);
3049 if let Some(m) = &mut mtp {
3050 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3051 .ok()
3052 .and_then(|v| v.parse().ok())
3053 .unwrap_or(0);
3054 for p in pos..end {
3055 if p + 1 < input_ids.len() {
3056 if probe >= 1 && p + 2 < input_ids.len() {
3057 let (d1, mut hx) = self.mtp_step_h(
3061 m,
3062 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3063 input_ids[p + 1],
3064 p,
3065 );
3066 let mut ok = d1 == input_ids[p + 2];
3067 Self::chain_probe_note(0, ok);
3068 let mut d_prev = d1;
3069 let mut extra = 0usize;
3070 for j in 1..probe {
3071 if p + 2 + j >= input_ids.len() {
3072 break;
3073 }
3074 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
3075 extra += 1;
3076 ok = ok && dj == input_ids[p + 2 + j];
3077 Self::chain_probe_note(j, ok);
3078 d_prev = dj;
3079 hx = hj;
3080 }
3081 m.kv.truncate_last(extra);
3082 } else {
3083 let _ = self.mtp_step(
3084 m,
3085 &hb[(p - pos) * hs..(p - pos + 1) * hs],
3086 input_ids[p + 1],
3087 p,
3088 );
3089 }
3090 }
3091 }
3092 }
3093 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
3094 pos = end;
3095 }
3096 }
3097 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
3098 if task_mask.is_none()
3099 && !dyn_prefill
3100 && !graph_prefill
3101 && !pair_off
3102 && self.pair_supported()
3103 && o1_prefill.is_none()
3104 {
3105 while pos + 1 < input_ids.len()
3106 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
3107 {
3108 let e1 = self.embed_single(input_ids[pos]);
3109 let e2 = self.embed_single(input_ids[pos + 1]);
3110 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
3111 self.commit_linear_scratch();
3113 if let Some(m) = &mut mtp {
3114 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
3115 if pos + 2 < input_ids.len() {
3116 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3117 .ok()
3118 .and_then(|v| v.parse().ok())
3119 .unwrap_or(0);
3120 if probe >= 1 && pos + 3 < input_ids.len() {
3121 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
3125 let mut ok = d1 == input_ids[pos + 3];
3126 Self::chain_probe_note(0, ok);
3127 let mut d_prev = d1;
3128 let mut extra = 0usize;
3129 for j in 1..probe {
3130 if pos + 3 + j >= input_ids.len() {
3131 break;
3132 }
3133 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
3134 extra += 1;
3135 ok = ok && dj == input_ids[pos + 3 + j];
3136 Self::chain_probe_note(j, ok);
3137 d_prev = dj;
3138 hx = hj;
3139 }
3140 m.kv.truncate_last(extra);
3141 } else {
3142 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3143 }
3144 }
3145 }
3146 hidden = h2;
3147 pos += 2;
3148 }
3149 }
3150 let o1_batch_ready = o1_sealed
3163 && o1_prefill.is_some()
3164 && mtp.is_none()
3165 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3166 && (0..self.num_layers).all(|li| {
3167 let cache = &self.kv_cache.layers[self.phys_layer(li)];
3168 cache.o1.is_none() || cache.o1_views().is_some()
3169 });
3170 let mtp_batch_prefill = mtp.is_some()
3175 && graph_prefill
3176 && task_mask.is_none()
3177 && !dyn_prefill
3178 && !self.o1_active()
3179 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3180 if batch_k > 0
3181 && (graph_prefill || o1_batch_ready)
3182 && task_mask.is_none()
3183 && (!self.o1_active() || o1_batch_ready)
3184 && (mtp.is_none() || mtp_batch_prefill)
3185 && !dyn_prefill
3186 && pos + 1 < input_ids.len()
3187 {
3188 let hs = self.hidden_size;
3189 let chunk = batch_k;
3190 while pos < input_ids.len() {
3191 let end = (pos + chunk).min(input_ids.len());
3192 let bk = end - pos;
3193 let mut hiddens = vec![0f32; bk * hs];
3194 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3195 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3196 }
3197 let positions: Vec<usize> = (pos..end).collect();
3198 let t_chunk = std::time::Instant::now();
3199 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3200 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3201 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3202 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3203 eprintln!(
3204 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3205 if o1_batch_ready {
3206 "o1"
3207 } else if mtp_batch_prefill {
3208 "ordinary_mtp"
3209 } else {
3210 "ordinary"
3211 },
3212 bk as f64 / (ms / 1000.0)
3213 );
3214 }
3215 {
3216 use std::sync::atomic::{AtomicBool, Ordering};
3217 static SAID: AtomicBool = AtomicBool::new(false);
3218 if !SAID.swap(true, Ordering::Relaxed) {
3219 if ok_b {
3220 tracing::info!(
3221 "batched prefill: ACTIVE mode={} (k={bk})",
3222 if o1_batch_ready {
3223 "o1"
3224 } else if mtp_batch_prefill {
3225 "ordinary_mtp"
3226 } else {
3227 "ordinary"
3228 }
3229 );
3230 } else {
3231 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3232 }
3233 }
3234 }
3235 if ok_b {
3236 if mtp_batch_prefill {
3237 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3238 if n_pairs > 0 {
3239 let rows: Vec<Vec<f32>> = (0..n_pairs)
3245 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3246 .collect();
3247 let pairs: Vec<(&[f32], u32)> = rows
3248 .iter()
3249 .enumerate()
3250 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3251 .collect();
3252 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3253 eprintln!(
3254 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3255 pos,
3256 n_pairs,
3257 pos + n_pairs - 1,
3258 );
3259 }
3260 let warm_error = if let Some(m) = mtp.as_mut() {
3261 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3262 } else {
3263 None
3264 };
3265 if let Some(err) = warm_error {
3266 self.finish_generation(&mut mtp, &mut router, true);
3271 return Err(err.to_string());
3272 }
3273 }
3274 }
3275 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3276 pos = end;
3277 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3278 self.finish_generation(&mut mtp, &mut router, true);
3283 return Err(if o1_batch_ready {
3284 "sealed O(1) batch graph failed after admission".to_string()
3285 } else {
3286 "ordinary recurrent batch graph failed after admission".to_string()
3287 });
3288 } else {
3289 break; }
3291 }
3292 }
3293 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3294 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3295 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3296 if let Some(m) = &mut mtp {
3297 if pos + 1 < input_ids.len() {
3298 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3304 .ok()
3305 .and_then(|v| v.parse().ok())
3306 .unwrap_or(0);
3307 if probe >= 1 && pos + 2 < input_ids.len() {
3308 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3309 let mut ok = d1 == input_ids[pos + 2];
3310 Self::chain_probe_note(0, ok);
3311 let mut d_prev = d1;
3312 let mut extra = 0usize;
3313 for j in 1..probe {
3314 if pos + 2 + j >= input_ids.len() {
3315 break;
3316 }
3317 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3318 extra += 1;
3319 ok = ok && dj == input_ids[pos + 2 + j];
3320 Self::chain_probe_note(j, ok);
3321 d_prev = dj;
3322 hx = hj;
3323 }
3324 m.kv.truncate_last(extra);
3327 } else {
3328 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3329 }
3330 }
3331 }
3332 pos += 1;
3333 }
3334 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3335 eprintln!(
3336 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3337 input_ids.len(),
3338 _tpf.elapsed().as_secs_f64() * 1000.0
3339 );
3340 }
3341 if self
3342 .graph_failed
3343 .swap(false, std::sync::atomic::Ordering::Relaxed)
3344 {
3345 self.finish_generation(&mut mtp, &mut router, true);
3350 return Err("GPU token graph failed during prefill".to_string());
3351 }
3352 if self
3355 .cancel
3356 .swap(false, std::sync::atomic::Ordering::Relaxed)
3357 {
3358 self.finish_generation(&mut mtp, &mut router, true);
3362 return Ok(GenerateResult {
3363 text: String::new(),
3364 token_ids: Vec::new(),
3365 prompt_tokens: input_ids.len(),
3366 tokens_generated: 0,
3367 finish_reason: "cancelled".to_string(),
3368 mtp_drafted: 0,
3369 mtp_accepted: 0,
3370 token_confidence: Vec::new(),
3371 traces: Vec::new(),
3372 });
3373 }
3374
3375 if !o1_sealed {
3378 match self.o1_seal_checked() {
3379 Ok(_) => {}
3380 Err(err) => {
3381 self.finish_generation(&mut mtp, &mut router, true);
3382 return Err(err);
3383 }
3384 }
3385 }
3386
3387 macro_rules! commit {
3389 ($id:expr) => {{
3390 all_ids.push($id);
3391 generated += 1;
3392 if self.tokenizer.is_eos($id) {
3393 finish_reason = "stop".to_string();
3394 false
3395 } else {
3396 let token_text = self.tokenizer.decode_token($id);
3397 let mut go = true;
3398 if let Some(ref mut cb) = on_token {
3399 if !cb(&token_text) {
3400 finish_reason = "cancelled".to_string();
3401 go = false;
3402 }
3403 }
3404 go
3405 }
3406 }};
3407 }
3408
3409 let mut spec_trial = SpecTrial::Spec {
3420 t0: std::time::Instant::now(),
3421 gen0: generated,
3422 rounds: 0,
3423 };
3424 let mut spec_mon = SpecMon::default();
3425 let mut spec_watchdog_off = false;
3426 let mut next_pos = input_ids.len();
3428 'decode: while generated < max_tokens {
3429 if self
3430 .graph_failed
3431 .swap(false, std::sync::atomic::Ordering::Relaxed)
3432 {
3433 self.finish_generation(&mut mtp, &mut router, true);
3438 return Err("GPU token graph failed during decode".to_string());
3439 }
3440 if self
3441 .cancel
3442 .swap(false, std::sync::atomic::Ordering::Relaxed)
3443 {
3444 finish_reason = "cancelled".to_string();
3445 break 'decode;
3446 }
3447 let forced = self.spec_forced.take();
3452 let mut logits = match (forced, self.graph_logits.take()) {
3453 (Some(_), _) => Vec::new(),
3454 (None, Some(lg)) => lg,
3455 (None, None) => {
3456 inference::rms_norm_into(
3457 &hidden,
3458 &self.weights.final_norm,
3459 self.rms_eps,
3460 self.norm_style,
3461 &mut self.ws.n1,
3462 );
3463 self.lm_head_forward(&self.ws.n1)
3464 }
3465 };
3466 if generated
3469 == std::env::var("CMF_LOGIT_DUMP_STEP")
3470 .ok()
3471 .and_then(|v| v.parse().ok())
3472 .unwrap_or(0)
3473 {
3474 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3475 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3476 for v in hidden.iter().chain(logits.iter()) {
3477 bytes.extend_from_slice(&v.to_le_bytes());
3478 }
3479 if let Err(e) = std::fs::write(&path, &bytes) {
3480 eprintln!("logit dump: failed to write {path}: {e}");
3481 self.finish_generation(&mut mtp, &mut router, true);
3482 return Err(format!("logit dump write failed: {e}"));
3483 }
3484 }
3485 }
3486 let t_next = match forced {
3487 Some(c) => c,
3488 None => sampler::sample_with_scratch_pool(
3489 &logits,
3490 &self.sampler_config,
3491 &all_ids,
3492 &mut self.rng,
3493 &mut self.sampler_scratch,
3494 self.pool.as_deref(),
3495 ),
3496 };
3497 if self.confidence_on {
3498 confidence.push(if logits.is_empty() {
3499 0.0
3500 } else {
3501 sampler::top1_prob_pool(
3502 self.pool.as_deref(),
3503 &mut self.sampler_scratch,
3504 &logits,
3505 t_next,
3506 calib_temp,
3507 )
3508 });
3509 }
3510 if !logits.is_empty() {
3511 attention::recycle_buf(&mut logits);
3512 }
3513 if trace_on {
3514 let skill = router.as_ref().and_then(|r| r.active_id());
3518 traces.push(TokenTrace {
3519 t: generated,
3520 token_id: t_next,
3521 confidence: confidence.last().copied().unwrap_or(0.0),
3522 active_skill: skill,
3523 recon: None,
3524 switched: false,
3525 });
3526 }
3527 if !commit!(t_next) {
3528 break 'decode;
3529 }
3530 if generated >= max_tokens {
3531 break 'decode;
3532 }
3533
3534 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3535 static SAID: std::sync::Once = std::sync::Once::new();
3541 SAID.call_once(|| {
3542 tracing::warn!(
3543 "KV cache full at {} positions — evicting half; quality \
3544 will degrade. Raise CMF_MAX_SEQ.",
3545 self.kv_cache.max_seq_len,
3546 );
3547 });
3548 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3549 self.kv_cache.evict(keep);
3550 }
3551
3552 if graph_spec {
3555 match spec_trial {
3556 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3557 spec_mon.plain_ms =
3558 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3559 let keep = spec_mon.pays();
3560 tracing::info!(
3561 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3562 spec_mon.tokens,
3563 spec_mon.round_ms,
3564 spec_mon.plain_ms,
3565 if keep { "speculating" } else { "plain" }
3566 );
3567 spec_mon.fails = 0;
3568 spec_trial = SpecTrial::Decided {
3569 spec: keep,
3570 recheck_at: if keep { usize::MAX } else { generated + 128 },
3571 };
3572 }
3573 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3574 spec_mon.n = 0;
3575 spec_trial = SpecTrial::Spec {
3576 t0: std::time::Instant::now(),
3577 gen0: generated,
3578 rounds: 0,
3579 };
3580 }
3581 _ => {}
3582 }
3583 spec_watchdog_off = matches!(
3584 spec_trial,
3585 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3586 );
3587 }
3588 match &mut mtp {
3589 #[cfg(feature = "gpu")]
3591 Some(m)
3592 if graph_spec
3593 && !spec_watchdog_off
3594 && generated + 1 < max_tokens
3595 && next_pos > 0 =>
3596 {
3597 let t_round = std::time::Instant::now();
3598 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3599 m,
3600 &hidden,
3601 t_next,
3602 next_pos,
3603 &mut drafted,
3604 &mut accepted,
3605 &mut all_ids,
3606 ) {
3607 next_pos = n_pos;
3608 hidden = new_h;
3609 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3610 eprintln!(
3611 "spec-round wall {:.1} ms → {} tokens",
3612 t_round.elapsed().as_secs_f64() * 1e3,
3613 extra.len() + 1
3614 );
3615 }
3616 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3620 spec_trial = Self::spec_trial_round(
3623 spec_trial,
3624 &mut spec_mon,
3625 generated + extra.len() + 1,
3626 );
3627 let mut stopped = false;
3628 for &id in &extra {
3629 if self.confidence_on {
3630 confidence.push(0.0);
3631 }
3632 if !commit!(id) {
3633 stopped = true;
3634 break;
3635 }
3636 }
3637 if stopped {
3638 break 'decode;
3639 }
3640 continue 'decode;
3641 }
3642 if self
3643 .graph_failed
3644 .swap(false, std::sync::atomic::Ordering::Relaxed)
3645 {
3646 self.finish_generation(&mut mtp, &mut router, true);
3652 return Err("GPU MTP graph failed during speculative decode".to_string());
3653 }
3654 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3665 spec_mon.tokens = 0.0;
3666 spec_mon.fails = 3;
3667 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3668 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3669 next_pos += 1;
3670 continue 'decode;
3671 }
3672 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3674 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3675 drafted += 1;
3676 let emb1 = self.embed_single(t_next);
3677 let emb2 = self.embed_single(draft);
3678 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3679
3680 inference::rms_norm_into(
3681 &h1,
3682 &self.weights.final_norm,
3683 self.rms_eps,
3684 self.norm_style,
3685 &mut self.ws.n1,
3686 );
3687 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3688 let t_after = sampler::sample_with_scratch_pool(
3689 &logits1,
3690 &self.sampler_config,
3691 &all_ids,
3692 &mut self.rng,
3693 &mut self.sampler_scratch,
3694 self.pool.as_deref(),
3695 );
3696 if self.confidence_on {
3697 confidence.push(sampler::top1_prob_pool(
3698 self.pool.as_deref(),
3699 &mut self.sampler_scratch,
3700 &logits1,
3701 t_after,
3702 calib_temp,
3703 ));
3704 }
3705 attention::recycle_buf(&mut logits1);
3706 if trace_on {
3707 traces.push(TokenTrace {
3710 t: generated,
3711 token_id: t_after,
3712 confidence: confidence.last().copied().unwrap_or(0.0),
3713 active_skill: None,
3714 recon: None,
3715 switched: false,
3716 });
3717 }
3718 let stop = !commit!(t_after);
3719
3720 if t_after == draft {
3721 accepted += 1;
3722 self.commit_linear_scratch();
3723 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3724 hidden = h2;
3725 next_pos += 2;
3726 } else {
3727 for layer in &mut self.kv_cache.layers {
3729 layer.truncate_last(1);
3730 }
3731 if !stop {
3732 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3733 hidden = self.forward_layers(
3734 &self.embed_single(t_after),
3735 next_pos + 1,
3736 None,
3737 );
3738 }
3739 next_pos += 2;
3740 }
3741 if stop {
3742 break 'decode;
3743 }
3744 }
3745 _ => {
3747 #[cfg(feature = "gpu")]
3752 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3753 static SAID: std::sync::Once = std::sync::Once::new();
3754 SAID.call_once(|| {
3755 eprintln!(
3756 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3757 !self.dsv4_mtp.is_empty(),
3758 task_mask.is_none(),
3759 router.is_none(),
3760 !trace_on,
3761 self.sampler_config.temperature < 1e-6,
3762 self.sampler_config.repetition_penalty == 1.0,
3763 );
3764 });
3765 }
3766 #[cfg(feature = "gpu")]
3767 if Self::dsv4_spec_on()
3768 && self.dsv4.is_some()
3769 && !self.dsv4_mtp.is_empty()
3770 && task_mask.is_none()
3771 && router.is_none()
3772 && !trace_on
3773 && self.sampler_config.temperature < 1e-6
3774 && self.sampler_config.repetition_penalty == 1.0
3775 && generated + 1 < max_tokens
3776 && all_ids.len() >= 2
3777 && generated >= dsv4_spec_retry_at
3778 {
3779 let tip_token = all_ids[all_ids.len() - 2];
3780 let drafted0 = drafted;
3781 let round = self.dsv4_spec_step(
3782 tip_token,
3783 t_next,
3784 next_pos,
3785 max_tokens.saturating_sub(generated),
3786 &mut drafted,
3787 &mut accepted,
3788 );
3789 if drafted > drafted0 {
3790 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3791 if useful {
3792 dsv4_spec_bad = 0;
3793 } else {
3794 dsv4_spec_bad += 1;
3795 if dsv4_spec_bad >= 2 {
3796 dsv4_spec_bad = 0;
3797 dsv4_spec_retry_at = generated.saturating_add(32);
3798 tracing::info!(
3799 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3800 );
3801 }
3802 }
3803 }
3804 if let Some((extra, n_pos)) = round {
3805 next_pos = n_pos;
3806 let mut stopped = false;
3807 for &id in &extra {
3808 if self.confidence_on {
3809 confidence.push(0.0);
3810 }
3811 if !commit!(id) {
3812 stopped = true;
3813 break;
3814 }
3815 }
3816 if stopped {
3817 break 'decode;
3818 }
3819 continue 'decode;
3820 }
3821 }
3822 self.graph_want_logits = fuse_lm;
3823 let mut t_fwd = t_next;
3829 let pure_greedy = self.sampler_config.temperature < 1e-6
3830 && self.sampler_config.repetition_penalty == 1.0
3831 && self.sampler_config.suppress_tokens.is_empty();
3832 let burst_k = std::env::var("CMF_MULTISTEP")
3837 .ok()
3838 .and_then(|v| v.parse::<usize>().ok())
3839 .unwrap_or(0);
3840 if pure_greedy
3841 && burst_k >= 1
3842 && fuse_lm
3843 && task_mask.is_none()
3844 && router.is_none()
3845 && !trace_on
3846 && !self.confidence_on
3847 {
3848 let mut stopped = false;
3849 loop {
3850 let room = max_tokens.saturating_sub(generated);
3851 if room <= 2 {
3852 break;
3853 }
3854 let k = burst_k.min(room - 1);
3855 if k < 1 {
3856 break;
3857 }
3858 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3859 if self
3860 .graph_failed
3861 .swap(false, std::sync::atomic::Ordering::Relaxed)
3862 {
3863 self.finish_generation(&mut mtp, &mut router, true);
3864 return Err(
3865 "GPU token graph failed during greedy burst".to_string()
3866 );
3867 }
3868 break;
3869 };
3870 next_pos += k;
3871 for &id in &ids {
3872 if !commit!(id) {
3873 stopped = true;
3874 break;
3875 }
3876 }
3877 if stopped {
3878 break;
3879 }
3880 t_fwd = *ids.last().unwrap();
3881 }
3882 if stopped {
3883 break 'decode;
3884 }
3885 }
3886 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3887 next_pos += 1;
3888 if let Some(r) = &mut router {
3891 let phi = self.dyn_phi_ema.clone();
3892 let decision = r.step(&phi, generated);
3893 if let Some(new_active) = decision {
3894 let _ = self.set_active_skill(new_active);
3895 }
3896 if trace_on {
3899 if let Some(last) = traces.last_mut() {
3900 let e = r.last_best_e();
3901 last.recon = e.is_finite().then_some(e);
3902 last.switched = decision.is_some();
3903 }
3904 }
3905 }
3906 }
3907 }
3908 }
3909
3910 let cancelled = finish_reason == "cancelled";
3911 self.finish_generation(&mut mtp, &mut router, cancelled);
3912
3913 let output_ids = &all_ids[input_ids.len()..];
3914 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3918 if cancelled {
3919 self.kv_history.clear();
3920 } else {
3921 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3922 }
3923 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3925 Ok(GenerateResult {
3926 text: self.tokenizer.decode(output_ids),
3927 token_ids: output_ids.to_vec(),
3928 prompt_tokens: input_ids.len(),
3929 tokens_generated: generated,
3930 finish_reason,
3931 mtp_drafted: drafted,
3932 mtp_accepted: accepted,
3933 token_confidence: confidence,
3934 traces,
3935 })
3936 }
3937
3938 fn mtp_step(
3942 &mut self,
3943 m: &mut MtpModule,
3944 hidden: &[f32],
3945 next_token: u32,
3946 position: usize,
3947 ) -> u32 {
3948 self.mtp_step_h(m, hidden, next_token, position).0
3949 }
3950
3951 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3955 use std::sync::Mutex;
3956 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3957 let mut t = T.lock().unwrap();
3958 if t.len() <= depth {
3959 t.resize(depth + 1, (0, 0));
3960 }
3961 t[depth].0 += 1;
3962 t[depth].1 += prefix_ok as u64;
3963 if depth == 0 && t[0].0 % 128 == 0 {
3964 let line: Vec<String> = t
3965 .iter()
3966 .enumerate()
3967 .map(|(d, (n, k))| {
3968 format!(
3969 "d{}={:.0}%({n})",
3970 d + 1,
3971 100.0 * *k as f64 / (*n).max(1) as f64
3972 )
3973 })
3974 .collect();
3975 eprintln!("mtp-chain: {}", line.join(" "));
3976 }
3977 }
3978
3979 fn mtp_step_hl(
3987 &mut self,
3988 m: &mut MtpModule,
3989 hidden: &[f32],
3990 next_token: u32,
3991 position: usize,
3992 ) -> (Vec<f32>, Vec<f32>) {
3993 #[cfg(target_os = "macos")]
3998 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3999 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
4000 self.mtp_graph_mode = Some(true);
4001 return r;
4002 }
4003 if self.mtp_graph_mode == Some(true) {
4004 tracing::error!("mtp Metal graph failed after admission");
4005 self.clear_sequence_state();
4006 self.graph_failed
4007 .store(true, std::sync::atomic::Ordering::Relaxed);
4008 self.cancel
4009 .store(true, std::sync::atomic::Ordering::Relaxed);
4010 return (Vec::new(), Vec::new());
4011 }
4012 self.mtp_graph_mode = Some(false);
4013 }
4014 #[cfg(feature = "gpu")]
4015 if self.mtp_graph_mode != Some(false) {
4016 if !self.mtp_graph_ok(m) {
4017 if self.mtp_graph_mode == Some(true) {
4018 tracing::error!("mtp graph became unavailable after admission");
4023 self.clear_sequence_state();
4024 self.graph_failed
4025 .store(true, std::sync::atomic::Ordering::Relaxed);
4026 self.cancel
4027 .store(true, std::sync::atomic::Ordering::Relaxed);
4028 return (Vec::new(), Vec::new());
4029 }
4030 self.mtp_graph_mode = Some(false);
4031 } else {
4032 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
4033 self.mtp_graph_mode = Some(true);
4034 return r;
4035 }
4036 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4037 return (Vec::new(), Vec::new());
4044 }
4045 tracing::error!("mtp graph failed or declined after admission");
4049 self.clear_sequence_state();
4050 self.graph_failed
4051 .store(true, std::sync::atomic::Ordering::Relaxed);
4052 self.cancel
4053 .store(true, std::sync::atomic::Ordering::Relaxed);
4054 return (Vec::new(), Vec::new());
4055 }
4056 }
4057 let e = self.embed_single(next_token);
4061 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4062 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4063 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4064 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4065 let mut x = vec![0.0f32; self.hidden_size];
4066 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4067
4068 let lw = &m.layer;
4070 inference::rms_norm_into(
4071 &x,
4072 &lw.input_norm,
4073 self.rms_eps,
4074 self.norm_style,
4075 &mut self.ws.n1,
4076 );
4077 let attn = match &lw.attn {
4078 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4080 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4081 AttnKind::Full {
4082 wq,
4083 wk,
4084 wv,
4085 wo,
4086 q_norm,
4087 k_norm,
4088 output_gate,
4089 softplus_gate,
4090 bias,
4091 } => {
4092 let mut cfg = self.attn_cfg(position);
4093 cfg.q_norm = q_norm.as_deref();
4094 cfg.k_norm = k_norm.as_deref();
4095 cfg.output_gate = *output_gate;
4096 cfg.softplus_gate = softplus_gate
4097 .as_ref()
4098 .map(|(gate, per_head)| (gate, *per_head));
4099 cfg.bias = bias
4100 .as_ref()
4101 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4102 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4103 }
4104 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
4105 unreachable!("MTP block is full attention")
4106 }
4107 };
4108 for (i, &a) in attn.iter().enumerate() {
4109 x[i] += a;
4110 }
4111 inference::rms_norm_into(
4112 &x,
4113 &lw.post_norm,
4114 self.rms_eps,
4115 self.norm_style,
4116 &mut self.ws.p1,
4117 );
4118 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
4119 for (i, &f) in ffn.iter().enumerate() {
4120 x[i] += f;
4121 }
4122
4123 inference::rms_norm_into(
4124 &x,
4125 &m.final_norm,
4126 self.rms_eps,
4127 self.norm_style,
4128 &mut self.ws.n1,
4129 );
4130 let lg = self.lm_head_forward(&self.ws.n1);
4131 (lg, x)
4132 }
4133
4134 fn mtp_step_h(
4136 &mut self,
4137 m: &mut MtpModule,
4138 hidden: &[f32],
4139 next_token: u32,
4140 position: usize,
4141 ) -> (u32, Vec<f32>) {
4142 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4143 let draft = sampler::argmax(&lg);
4144 attention::recycle_buf(&mut lg);
4145 (draft, x)
4146 }
4147
4148 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4154 match trial {
4155 SpecTrial::Spec { t0, gen0, rounds } => {
4156 let rounds = rounds + 1;
4157 if rounds >= 5 {
4158 if mon.plain_ms > 0.0 {
4159 let keep = mon.pays();
4160 mon.fails = 0;
4161 tracing::info!(
4162 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4163 mon.tokens,
4164 mon.round_ms,
4165 mon.plain_ms,
4166 if keep { "speculating" } else { "plain" }
4167 );
4168 SpecTrial::Decided {
4169 spec: keep,
4170 recheck_at: if keep { usize::MAX } else { generated + 128 },
4171 }
4172 } else {
4173 SpecTrial::Plain {
4174 t0: std::time::Instant::now(),
4175 gen0: generated,
4176 }
4177 }
4178 } else {
4179 SpecTrial::Spec { t0, gen0, rounds }
4180 }
4181 }
4182 SpecTrial::Decided { spec: true, .. } => {
4183 if mon.pays() {
4184 mon.fails = 0;
4185 trial
4186 } else {
4187 mon.fails += 1;
4188 if mon.fails >= 4 {
4189 tracing::info!(
4190 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4191 mon.tokens,
4192 mon.round_ms,
4193 mon.plain_ms
4194 );
4195 SpecTrial::Decided {
4196 spec: false,
4197 recheck_at: generated + 128,
4198 }
4199 } else {
4200 trial
4201 }
4202 }
4203 }
4204 other => other,
4205 }
4206 }
4207
4208 fn mtp_kv_id(&self) -> u64 {
4211 self.graph_kv_id | (1u64 << 40)
4212 }
4213
4214 const MTP_LAYER_BASE: usize = 0;
4219
4220 #[cfg(feature = "gpu")]
4227 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4228 self.mtp_graph_mode != Some(true)
4229 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4230 }
4231
4232 #[cfg(feature = "gpu")]
4238 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4239 let mut ok = true;
4240 let mut expected = false;
4241 for li in 0..self.num_layers {
4242 if matches!(
4243 self.weights.layers[self.phys_layer(li)].attn,
4244 AttnKind::Full { .. }
4245 ) {
4246 expected = true;
4247 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4248 }
4249 }
4250 !expected || ok
4251 }
4252
4253 fn graph_gdn_layer_count(&self) -> usize {
4257 (0..self.num_layers)
4258 .filter(|&li| {
4259 matches!(
4260 &self.weights.layers[self.phys_layer(li)].attn,
4261 AttnKind::LinearGdn(_)
4262 )
4263 })
4264 .count()
4265 }
4266
4267 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4270 let e = self.embed_single(next_token);
4271 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4272 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4273 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4274 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4275 let mut x = vec![0.0f32; self.hidden_size];
4276 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4277 x
4278 }
4279
4280 #[cfg(feature = "gpu")]
4283 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4284 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4285 return false;
4286 }
4287 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4288 || !crate::gpu::enabled_here()
4289 || self.attn_softcap > 0.0
4290 || self.attention_heads_per_layer.is_some()
4291 {
4292 return false;
4293 }
4294 matches!(
4295 &m.layer.attn,
4296 AttnKind::Full {
4297 softplus_gate: None,
4298 ..
4299 }
4300 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4301 }
4302
4303 #[cfg(feature = "gpu")]
4307 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4308 if !self.mtp_block_graph_ok(m) {
4309 return false;
4310 }
4311 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4312 return false;
4313 };
4314 let FfnKind::Dense(d) = &m.layer.ffn else {
4315 return false;
4316 };
4317 d.segs.is_empty()
4318 && wq.graph_weight().is_some()
4319 && wk.graph_weight().is_some()
4320 && wv.graph_weight().is_some()
4321 && wo.graph_weight().is_some()
4322 && d.gate_proj.graph_weight().is_some()
4323 && d.up_proj.graph_weight().is_some()
4324 && d.down_proj.graph_weight().is_some()
4325 && self.weights.lm_head.graph_weight().is_some()
4326 }
4327
4328 #[cfg(feature = "gpu")]
4334 fn mtp_step_graph(
4335 &mut self,
4336 m: &mut MtpModule,
4337 hidden: &[f32],
4338 next_token: u32,
4339 position: usize,
4340 ) -> Option<(Vec<f32>, Vec<f32>)> {
4341 if !self.mtp_graph_ok(m) {
4342 return None;
4343 }
4344 let lw = &m.layer;
4345 let AttnKind::Full {
4346 wq,
4347 wk,
4348 wv,
4349 wo,
4350 q_norm,
4351 k_norm,
4352 output_gate,
4353 softplus_gate,
4354 bias,
4355 } = &lw.attn
4356 else {
4357 return None;
4358 };
4359 if softplus_gate.is_some() {
4360 return None;
4361 }
4362 let FfnKind::Dense(d) = &lw.ffn else {
4363 return None;
4364 };
4365 if !d.segs.is_empty() {
4366 return None; }
4368 let mut x = self.mtp_block_input(m, hidden, next_token);
4371 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4372 let (_, i, kind, rs) = t.graph_weight()?;
4373 Some(crate::gpu::GraphW {
4374 idx: i,
4375 kind,
4376 row_scale: rs,
4377 data: &[],
4378 prism: crate::gpu::GraphPrismOp::None,
4379 affine: false,
4380 })
4381 }
4382 let (model, _, _, _) = wq.graph_weight()?;
4383 let model = model.clone();
4384 let (lm_gw, lm_rows) = {
4385 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4386 (
4387 crate::gpu::GraphW {
4388 idx: i,
4389 kind,
4390 row_scale: rs,
4391 data: &[],
4392 prism: crate::gpu::GraphPrismOp::None,
4393 affine: false,
4394 },
4395 self.weights.lm_head.rows(),
4396 )
4397 };
4398 let layer = crate::gpu::GraphLayer {
4399 input_norm: &lw.input_norm,
4400 attn: crate::gpu::GraphAttn::Full {
4401 wq: gw(wq)?,
4402 wk: gw(wk)?,
4403 wv: gw(wv)?,
4404 wo: gw(wo)?,
4405 q_norm: q_norm.as_deref(),
4406 k_norm: k_norm.as_deref(),
4407 late_qk_norm: self.qk_norm_after_rope,
4408 bias: bias
4409 .as_ref()
4410 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4411 output_gate: *output_gate,
4412 cpu_k: m.kv.k_heads(),
4413 cpu_v: m.kv.v_heads(),
4414 },
4415 post_norm: &lw.post_norm,
4416 ffn: crate::gpu::GraphFfn::Dense {
4417 gate: gw(&d.gate_proj)?,
4418 up: gw(&d.up_proj)?,
4419 down: gw(&d.down_proj)?,
4420 },
4421 };
4422 let nh = self.num_heads;
4423 let (nkv, hd, rd) = self.layer_geom(0);
4424 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4425 let mut logits = Vec::new();
4426 let ok = crate::gpu::forward_token_graph(
4427 &model,
4428 self.mtp_kv_id(),
4429 std::slice::from_ref(&layer),
4430 &[None],
4431 self.o1_epoch,
4432 &self.inv_freq,
4433 &mut x,
4434 nh,
4435 nkv,
4436 hd,
4437 self.attn_scale,
4438 rd,
4439 self.hidden_size,
4440 self.intermediate_size,
4441 position,
4442 self.kv_cache.max_seq_len,
4443 gemma,
4444 self.rms_eps as f32,
4445 Some((&lm_gw, lm_rows)),
4446 &m.final_norm,
4447 &mut logits,
4448 &[],
4449 1,
4450 None,
4451 None,
4452 None,
4453 Self::MTP_LAYER_BASE,
4454 true,
4455 );
4456 match ok {
4457 crate::gpu::TokenGraphOutcome::Completed => {}
4458 crate::gpu::TokenGraphOutcome::Declined => return None,
4459 crate::gpu::TokenGraphOutcome::Failed => {
4460 self.clear_sequence_state();
4464 self.graph_failed
4465 .store(true, std::sync::atomic::Ordering::Relaxed);
4466 self.cancel
4467 .store(true, std::sync::atomic::Ordering::Relaxed);
4468 return None;
4469 }
4470 }
4471 logits.resize(self.vocab_size, 0.0);
4472 Some((logits, x))
4473 }
4474
4475 #[cfg(feature = "gpu")]
4483 fn mtp_warm_graph(
4484 &mut self,
4485 m: &mut MtpModule,
4486 pairs: &[(&[f32], u32)],
4487 first_pos: usize,
4488 ) -> crate::gpu::BatchGraphOutcome {
4489 if pairs.is_empty() {
4490 return crate::gpu::BatchGraphOutcome::Completed;
4491 }
4492 if !self.mtp_block_graph_ok(m) {
4493 return crate::gpu::BatchGraphOutcome::Declined;
4494 }
4495 let hs = self.hidden_size;
4496 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4499 for (h, t) in pairs {
4500 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4501 }
4502 let lw = &m.layer;
4503 let AttnKind::Full {
4504 wq,
4505 wk,
4506 wv,
4507 wo,
4508 q_norm,
4509 k_norm,
4510 output_gate,
4511 bias,
4512 ..
4513 } = &lw.attn
4514 else {
4515 return crate::gpu::BatchGraphOutcome::Declined;
4516 };
4517 let FfnKind::Dense(d) = &lw.ffn else {
4518 return crate::gpu::BatchGraphOutcome::Declined;
4519 };
4520 if !d.segs.is_empty() {
4521 return crate::gpu::BatchGraphOutcome::Declined; }
4523 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4524 let (_, i, kind, rs) = t.graph_weight()?;
4525 Some(crate::gpu::GraphW {
4526 idx: i,
4527 kind,
4528 row_scale: rs,
4529 data: &[],
4530 prism: crate::gpu::GraphPrismOp::None,
4531 affine: false,
4532 })
4533 }
4534 let Some((model, _, _, _)) = wq.graph_weight() else {
4535 return crate::gpu::BatchGraphOutcome::Declined;
4536 };
4537 let model = model.clone();
4538 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4539 gw(wq),
4540 gw(wk),
4541 gw(wv),
4542 gw(wo),
4543 gw(&d.gate_proj),
4544 gw(&d.up_proj),
4545 gw(&d.down_proj),
4546 ) else {
4547 return crate::gpu::BatchGraphOutcome::Declined;
4548 };
4549 let layer = crate::gpu::GraphLayer {
4550 input_norm: &lw.input_norm,
4551 attn: crate::gpu::GraphAttn::Full {
4552 wq: gwq,
4553 wk: gwk,
4554 wv: gwv,
4555 wo: gwo,
4556 q_norm: q_norm.as_deref(),
4557 k_norm: k_norm.as_deref(),
4558 late_qk_norm: self.qk_norm_after_rope,
4559 bias: bias
4560 .as_ref()
4561 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4562 output_gate: *output_gate,
4563 cpu_k: m.kv.k_heads(),
4564 cpu_v: m.kv.v_heads(),
4565 },
4566 post_norm: &lw.post_norm,
4567 ffn: crate::gpu::GraphFfn::Dense {
4568 gate: gg,
4569 up: gu,
4570 down: gd,
4571 },
4572 };
4573 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4574 let nh = self.num_heads;
4575 let (nkv, hd, rd) = self.layer_geom(0);
4576 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4577 crate::gpu::forward_batch_graph(
4578 &model,
4579 self.mtp_kv_id(),
4580 std::slice::from_ref(&layer),
4581 &self.inv_freq,
4582 &mut hiddens,
4583 nh,
4584 nkv,
4585 hd,
4586 rd,
4587 hs,
4588 self.intermediate_size,
4589 &positions,
4590 self.kv_cache.max_seq_len,
4591 gemma,
4592 self.rms_eps as f32,
4593 self.attn_scale,
4594 pairs.len(),
4595 &[],
4596 0,
4597 None,
4598 )
4599 }
4600
4601 #[cfg(feature = "gpu")]
4608 fn mtp_warm_graph_fallback(
4609 &mut self,
4610 m: &mut MtpModule,
4611 pairs: &[(&[f32], u32)],
4612 first_pos: usize,
4613 ) -> bool {
4614 if pairs.is_empty() {
4615 return true;
4616 }
4617 let graphable = self.mtp_block_graph_ok(m);
4618 if !graphable {
4619 if self.mtp_graph_mode == Some(true) {
4623 return false;
4624 }
4625 self.mtp_graph_mode = Some(false);
4626 for (j, (h, t)) in pairs.iter().enumerate() {
4627 self.mtp_warm(m, h, *t, first_pos + j);
4628 }
4629 return true;
4630 }
4631
4632 for (j, (h, t)) in pairs.iter().enumerate() {
4637 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4638 return false;
4639 }
4640 }
4641 self.mtp_graph_mode = Some(true);
4642 true
4643 }
4644
4645 #[cfg(feature = "gpu")]
4650 fn mtp_warm_prefill_pairs(
4651 &mut self,
4652 m: &mut MtpModule,
4653 pairs: &[(&[f32], u32)],
4654 first_pos: usize,
4655 ) -> Result<(), &'static str> {
4656 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4661 if self.mtp_graph_mode == Some(true) {
4662 return Err("MTP token graph became unavailable after admission");
4663 }
4664 self.mtp_graph_mode = Some(false);
4665 for (j, (h, t)) in pairs.iter().enumerate() {
4666 self.mtp_warm(m, h, *t, first_pos + j);
4667 }
4668 return Ok(());
4669 }
4670 match self.mtp_warm_graph(m, pairs, first_pos) {
4671 crate::gpu::BatchGraphOutcome::Completed => {
4672 if !pairs.is_empty() {
4673 self.mtp_graph_mode = Some(true);
4674 }
4675 Ok(())
4676 }
4677 crate::gpu::BatchGraphOutcome::Declined => {
4678 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4679 Ok(())
4680 } else {
4681 Err("MTP warm-up fallback failed after device admission")
4682 }
4683 }
4684 crate::gpu::BatchGraphOutcome::Failed => {
4685 Err("MTP warm batch graph failed after admission")
4686 }
4687 }
4688 }
4689
4690 #[cfg(not(feature = "gpu"))]
4691 fn mtp_warm_prefill_pairs(
4692 &mut self,
4693 m: &mut MtpModule,
4694 pairs: &[(&[f32], u32)],
4695 first_pos: usize,
4696 ) -> Result<(), &'static str> {
4697 for (j, (h, t)) in pairs.iter().enumerate() {
4698 self.mtp_warm(m, h, *t, first_pos + j);
4699 }
4700 Ok(())
4701 }
4702
4703 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4707 let e = self.embed_single(next_token);
4708 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4709 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4710 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4711 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4712 let mut x = vec![0.0f32; self.hidden_size];
4713 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4714 inference::rms_norm_into(
4715 &x,
4716 &m.layer.input_norm,
4717 self.rms_eps,
4718 self.norm_style,
4719 &mut self.ws.n1,
4720 );
4721 let attn = match &m.layer.attn {
4722 AttnKind::Full {
4723 wq,
4724 wk,
4725 wv,
4726 wo,
4727 q_norm,
4728 k_norm,
4729 output_gate,
4730 softplus_gate,
4731 bias,
4732 } => {
4733 let mut cfg = self.attn_cfg(position);
4734 cfg.q_norm = q_norm.as_deref();
4735 cfg.k_norm = k_norm.as_deref();
4736 cfg.output_gate = *output_gate;
4737 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4738 cfg.bias = bias
4739 .as_ref()
4740 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4741 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4742 }
4743 _ => return,
4744 };
4745 let _ = attn;
4746 }
4747
4748 #[cfg(feature = "gpu")]
4755 #[allow(clippy::too_many_arguments)]
4756 fn graph_spec_step(
4757 &mut self,
4758 m: &mut MtpModule,
4759 hidden: &[f32],
4760 t_next: u32,
4761 next_pos: usize,
4762 drafted: &mut usize,
4763 accepted: &mut usize,
4764 all_ids: &mut Vec<u32>,
4768 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4769 #[cfg(target_os = "macos")]
4780 let metal_native = crate::gpu::q1_force();
4781 #[cfg(not(target_os = "macos"))]
4782 let metal_native = false;
4783 #[cfg(feature = "gpu")]
4784 let k_default = if metal_native {
4785 7
4788 } else if crate::gpu_wgpu::verify_i8_on() {
4789 5
4790 } else {
4791 4
4792 };
4793 #[cfg(not(feature = "gpu"))]
4794 let k_default = 4;
4795 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4796 .ok()
4797 .and_then(|v| v.parse().ok())
4798 .filter(|&v| (1..=8).contains(&v))
4799 .unwrap_or(k_default);
4800 if next_pos == 0 {
4801 return None;
4802 }
4803 let t_round = std::time::Instant::now();
4804 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4820 let sub0 = subs();
4821 let cfg = self.sampler_config.clone();
4826 let penalized = !(cfg.repetition_penalty == 1.0
4827 && cfg.presence_penalty == 0.0
4828 && cfg.suppress_tokens.is_empty());
4829 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4834 let sampling = cfg.temperature >= 1e-6;
4835 let sparse = sampling && sampler::sparse_ok(&cfg);
4841 let base_len = all_ids.len();
4842 if sampling && !sparse && self.spec_q.len() < k_spec {
4843 self.spec_q.resize_with(k_spec, Vec::new);
4844 }
4845 if sparse && self.spec_qs.len() < k_spec {
4846 self.spec_qs.resize_with(k_spec, Vec::new);
4847 }
4848 let mut drafts = Vec::with_capacity(k_spec);
4853 let mut hx = hidden.to_vec();
4854 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4857 for j in 0..k_spec {
4858 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4859 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4860 if spec_dbg {
4861 let saved = self.mtp_graph_mode;
4862 self.mtp_graph_mode = Some(false);
4863 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4864 self.mtp_graph_mode = saved;
4865 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4866 return None;
4867 }
4868 m.kv.truncate_last(1);
4869 dbg_ref = Some(r);
4870 }
4871 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4872 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4873 return None;
4874 }
4875 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4876 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4877 let dl = lg
4878 .iter()
4879 .zip(&lg_cpu)
4880 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4881 let dh = hj
4882 .iter()
4883 .zip(&h_cpu)
4884 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4885 eprintln!(
4886 "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 {}",
4887 next_pos - 1 + j,
4888 sampler::argmax(&lg_cpu),
4889 sampler::argmax(&lg),
4890 n(&h_cpu),
4891 n(&hj),
4892 m.kv.seq_len
4893 );
4894 }
4895 let dj = if sparse {
4896 let mut q = std::mem::take(&mut self.spec_qs[j]);
4897 let ok = sampler::sparse_distribution_into(
4898 &lg,
4899 &cfg,
4900 all_ids,
4901 &mut self.sampler_scratch,
4902 self.pool.as_deref(),
4903 &mut q,
4904 );
4905 let d = if ok {
4906 sampler::draw_sparse(&q, &mut self.rng)
4907 } else {
4908 let t = sampler::argmax(&lg);
4910 q.clear();
4911 q.push((t, 1.0));
4912 t
4913 };
4914 self.spec_qs[j] = q;
4915 all_ids.push(d);
4916 d
4917 } else if sampling {
4918 let mut q = std::mem::take(&mut self.spec_q[j]);
4919 sampler::distribution_into(
4920 &lg,
4921 &cfg,
4922 all_ids,
4923 &mut self.sampler_scratch,
4924 self.pool.as_deref(),
4925 &mut q,
4926 );
4927 let d = sampler::draw(&q, &mut self.rng);
4928 self.spec_q[j] = q;
4929 all_ids.push(d); d
4931 } else if greedy_pen {
4932 let d = sampler::argmax_penalized(
4933 &lg,
4934 &cfg,
4935 all_ids,
4936 &mut self.sampler_scratch,
4937 self.pool.as_deref(),
4938 );
4939 all_ids.push(d);
4940 d
4941 } else {
4942 sampler::argmax(&lg)
4943 };
4944 attention::recycle_buf(&mut lg);
4945 drafts.push(dj);
4946 hx = hj;
4947 }
4948 all_ids.truncate(base_len);
4949 *drafted += k_spec;
4950 let t_draft = t_round.elapsed();
4951 let sub_draft = subs();
4952 let b = k_spec + 1;
4955 let mut hiddens = vec![0.0f32; b * self.hidden_size];
4956 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4957 let e = self.embed_single(t);
4958 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4959 }
4960 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4961 let (lm_gw, lm_rows) = {
4962 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4963 (
4964 crate::gpu::GraphW {
4965 idx: i,
4966 kind,
4967 row_scale: rs,
4968 data: &[],
4969 prism: crate::gpu::GraphPrismOp::None,
4970 affine: false,
4971 },
4972 self.weights.lm_head.rows(),
4973 )
4974 };
4975 let mut logits = Vec::new();
4976 let final_norm = self.weights.final_norm.clone();
4977 #[cfg(target_os = "macos")]
4978 let verify_outcome = if metal_native {
4979 let lm = self.weights.lm_head.q1_parts()?;
4980 self.try_batch_graph_metal(
4981 &mut hiddens,
4982 &positions,
4983 b,
4984 Some((lm, &final_norm, &mut logits)),
4985 )
4986 } else {
4987 self.try_batch_graph_wgpu(
4988 &mut hiddens,
4989 &positions,
4990 b,
4991 Some(crate::gpu::SpecTail {
4992 lm: lm_gw,
4993 lm_rows,
4994 final_norm: &final_norm,
4995 logits_out: &mut logits,
4996 }),
4997 )
4998 };
4999 #[cfg(not(target_os = "macos"))]
5000 let verify_outcome = self.try_batch_graph_wgpu(
5001 &mut hiddens,
5002 &positions,
5003 b,
5004 Some(crate::gpu::SpecTail {
5005 lm: lm_gw,
5006 lm_rows,
5007 final_norm: &final_norm,
5008 logits_out: &mut logits,
5009 }),
5010 );
5011 match verify_outcome {
5012 crate::gpu::BatchGraphOutcome::Completed => {}
5013 crate::gpu::BatchGraphOutcome::Declined => {
5014 m.kv.truncate_last(k_spec);
5018 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
5019 self.clear_sequence_state();
5020 self.graph_failed
5021 .store(true, std::sync::atomic::Ordering::Relaxed);
5022 self.cancel
5023 .store(true, std::sync::atomic::Ordering::Relaxed);
5024 tracing::error!("MTP graph mirror rewind failed after verify decline");
5025 }
5026 return None;
5027 }
5028 crate::gpu::BatchGraphOutcome::Failed => {
5029 self.clear_sequence_state();
5033 self.graph_failed
5034 .store(true, std::sync::atomic::Ordering::Relaxed);
5035 self.cancel
5036 .store(true, std::sync::atomic::Ordering::Relaxed);
5037 tracing::error!("MTP verify batch graph failed after admission");
5038 return None;
5039 }
5040 }
5041 #[cfg(target_os = "macos")]
5047 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
5048 let snap: Vec<Vec<f32>> = self
5049 .kv_cache
5050 .layers
5051 .iter()
5052 .map(|l| l.linear_state.clone())
5053 .collect();
5054 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5055 let toks: Vec<u32> = std::iter::once(t_next)
5056 .chain(drafts.iter().copied())
5057 .collect();
5058 let want_save = self.graph_want_logits;
5059 self.graph_want_logits = false;
5060 for (i, &t) in toks.iter().enumerate() {
5061 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5062 let _ = self.graph_logits.take();
5063 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
5067 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
5068 }
5069 let ref_lg = self.logits_from_hidden(&hi);
5070 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
5071 let ra = sampler::argmax(&ref_lg);
5072 let va = sampler::argmax(row);
5073 let mut md = 0f32;
5074 let mut rms = 0f64;
5075 for j in 0..lm_rows.min(ref_lg.len()) {
5076 let d = (ref_lg[j] - row[j]).abs();
5077 md = md.max(d);
5078 rms += (d as f64) * (d as f64);
5079 }
5080 let mut hd = 0f32;
5081 for j in 0..self.hidden_size {
5082 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
5083 }
5084 eprintln!(
5085 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
5086 next_pos + i,
5087 if ra == va { "OK" } else { "MISMATCH" },
5088 (rms / lm_rows as f64).sqrt()
5089 );
5090 }
5091 self.graph_want_logits = want_save;
5092 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5095 if l.linear_state.len() == st.len() {
5096 l.linear_state.copy_from_slice(&st);
5097 } else {
5098 l.linear_state = st;
5099 }
5100 }
5101 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
5102 let extra = l.seq_len.saturating_sub(n0);
5103 if extra > 0 {
5104 l.truncate_last(extra);
5105 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
5106 }
5107 }
5108 }
5109 let t_verify = t_round.elapsed();
5110 let sub_verify = subs();
5111 let mut a = 0usize;
5116 let mut forced: Option<u32> = None;
5117 let ids: Vec<u32> = if sparse {
5118 let mut p = std::mem::take(&mut self.spec_ps);
5119 let mut res = std::mem::take(&mut self.spec_ress);
5120 while a < k_spec {
5121 let ok = sampler::sparse_distribution_into(
5122 &logits[a * lm_rows..(a + 1) * lm_rows],
5123 &cfg,
5124 all_ids,
5125 &mut self.sampler_scratch,
5126 self.pool.as_deref(),
5127 &mut p,
5128 );
5129 if !ok {
5130 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
5131 p.clear();
5132 p.push((t, 1.0));
5133 }
5134 match sampler::spec_accept_or_correct_sparse(
5135 &p,
5136 &self.spec_qs[a],
5137 drafts[a],
5138 &mut self.rng,
5139 &mut res,
5140 ) {
5141 None => {
5142 all_ids.push(drafts[a]);
5143 a += 1;
5144 }
5145 Some(c) => {
5146 forced = Some(c);
5147 break;
5148 }
5149 }
5150 }
5151 all_ids.truncate(base_len);
5152 self.spec_ps = p;
5153 self.spec_ress = res;
5154 drafts.clone()
5155 } else if sampling {
5156 let mut p = std::mem::take(&mut self.spec_p);
5157 let mut res = std::mem::take(&mut self.spec_res);
5158 while a < k_spec {
5159 sampler::distribution_into(
5160 &logits[a * lm_rows..(a + 1) * lm_rows],
5161 &cfg,
5162 all_ids,
5163 &mut self.sampler_scratch,
5164 self.pool.as_deref(),
5165 &mut p,
5166 );
5167 match sampler::spec_accept_or_correct(
5168 &p,
5169 &self.spec_q[a],
5170 drafts[a],
5171 &mut self.rng,
5172 &mut res,
5173 self.pool.as_deref(),
5174 ) {
5175 None => {
5176 all_ids.push(drafts[a]);
5177 a += 1;
5178 }
5179 Some(c) => {
5180 forced = Some(c);
5181 break;
5182 }
5183 }
5184 }
5185 all_ids.truncate(base_len);
5186 self.spec_p = p;
5187 self.spec_res = res;
5188 drafts.clone()
5190 } else if greedy_pen {
5191 let mut ids: Vec<u32> = Vec::with_capacity(b);
5195 for i in 0..b {
5196 let t = sampler::argmax_penalized(
5197 &logits[i * lm_rows..(i + 1) * lm_rows],
5198 &cfg,
5199 all_ids,
5200 &mut self.sampler_scratch,
5201 self.pool.as_deref(),
5202 );
5203 ids.push(t);
5204 if i < k_spec && t == drafts[i] {
5205 all_ids.push(t);
5206 } else {
5207 break;
5208 }
5209 }
5210 all_ids.truncate(base_len);
5211 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5212 a += 1;
5213 }
5214 ids
5217 } else {
5218 let ids: Vec<u32> = (0..b)
5219 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5220 .collect();
5221 while a < k_spec && ids[a] == drafts[a] {
5222 a += 1;
5223 }
5224 ids
5225 };
5226 if spec_dbg {
5227 eprintln!(
5228 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5229 drafts, ids
5230 );
5231 }
5232 #[cfg(target_os = "macos")]
5236 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5237 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5238 {
5239 let snap: Vec<Vec<f32>> = self
5240 .kv_cache
5241 .layers
5242 .iter()
5243 .map(|l| l.linear_state.clone())
5244 .collect();
5245 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5246 let toks: Vec<u32> = std::iter::once(t_next)
5247 .chain(drafts.iter().copied())
5248 .collect();
5249 let want_save = self.graph_want_logits;
5250 self.graph_want_logits = false;
5251 for (i, &t) in toks.iter().take(a + 1).enumerate() {
5252 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5253 let _ = self.graph_logits.take();
5254 }
5255 self.graph_want_logits = want_save;
5256 let plain_states: Vec<Vec<f32>> = self
5257 .kv_cache
5258 .layers
5259 .iter()
5260 .map(|l| l.linear_state.clone())
5261 .collect();
5262 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5263 let mut rows = Vec::new();
5264 for (li, (l, n0)) in self
5265 .kv_cache
5266 .layers
5267 .iter_mut()
5268 .zip(attn_lens.iter())
5269 .enumerate()
5270 {
5271 let extra = l.seq_len.saturating_sub(*n0);
5272 if extra > 0 {
5273 let mut kk = Vec::new();
5274 let mut vv = Vec::new();
5275 for g in 0..nkv {
5276 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5277 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5278 }
5279 rows.push((li, kk, vv));
5280 l.truncate_last(extra);
5281 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5282 }
5283 }
5284 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5285 if l.linear_state.len() == st.len() {
5286 l.linear_state.copy_from_slice(&st);
5287 } else {
5288 l.linear_state = st;
5289 }
5290 }
5291 Some((plain_states, rows))
5292 } else {
5293 None
5294 };
5295 #[cfg(target_os = "macos")]
5297 if metal_native {
5298 if !self.metal_verify_commit(a) {
5301 self.clear_sequence_state();
5302 self.graph_failed
5303 .store(true, std::sync::atomic::Ordering::Relaxed);
5304 self.cancel
5305 .store(true, std::sync::atomic::Ordering::Relaxed);
5306 tracing::error!("Metal verify state/KV handoff failed after admission");
5307 return None;
5308 }
5309 if let Some((plain_states, rows)) = commit_ref {
5310 crate::gpu_metal::queue_fence();
5311 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5312 let mut worst_s = 0f32;
5313 let mut worst_li = 0usize;
5314 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5315 if l.linear_state.len() != ps.len() || ps.is_empty() {
5316 continue;
5317 }
5318 let d = l
5319 .linear_state
5320 .iter()
5321 .zip(ps)
5322 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5323 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5324 let rel = d / n.max(1e-6);
5325 if rel > worst_s {
5326 worst_s = rel;
5327 worst_li = li;
5328 }
5329 }
5330 let mut worst_k = 0f32;
5331 for (li, kk, vv) in &rows {
5332 let l = &self.kv_cache.layers[*li];
5333 let n0 = l.seq_len - (kk.len() / (nkv * hd));
5334 let mut ck = Vec::new();
5335 let mut cv = Vec::new();
5336 for g in 0..nkv {
5337 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5338 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5339 }
5340 if ck.len() == kk.len() {
5341 let dk = ck
5342 .iter()
5343 .zip(kk)
5344 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5345 let dv = cv
5346 .iter()
5347 .zip(vv)
5348 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5349 worst_k = worst_k.max(dk).max(dv);
5350 } else {
5351 eprintln!(
5352 "commit-check L{li}: kv row count mismatch {} vs {}",
5353 ck.len(),
5354 kk.len()
5355 );
5356 }
5357 }
5358 eprintln!(
5359 "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}"
5360 );
5361 }
5362 }
5363 if !metal_native && a + 1 < b {
5364 let expected_gdn_layers = self.graph_gdn_layer_count();
5365 if expected_gdn_layers > 0
5366 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5367 {
5368 self.clear_sequence_state();
5369 self.graph_failed
5370 .store(true, std::sync::atomic::Ordering::Relaxed);
5371 self.cancel
5372 .store(true, std::sync::atomic::Ordering::Relaxed);
5373 tracing::error!("GDN speculative restore failed after verify");
5374 return None;
5375 }
5376 }
5377 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5378 self.clear_sequence_state();
5383 self.graph_failed
5384 .store(true, std::sync::atomic::Ordering::Relaxed);
5385 self.cancel
5386 .store(true, std::sync::atomic::Ordering::Relaxed);
5387 tracing::error!("trunk graph KV rewind failed after speculative verify");
5388 return None;
5389 }
5390 *accepted += a;
5391 m.kv.truncate_last(k_spec.saturating_sub(1));
5402 #[cfg(target_os = "macos")]
5403 if metal_native && self.mtp_graph_mode == Some(true) {
5404 crate::gpu_metal::kv_mirror_set_stored(
5407 self.mtp_kv_id(),
5408 Self::MTP_LAYER_BASE,
5409 m.kv.seq_len,
5410 );
5411 }
5412 if !metal_native
5413 && self.mtp_graph_mode == Some(true)
5414 && !self.rewind_mtp_graph_mirror(next_pos)
5415 {
5416 self.clear_sequence_state();
5420 self.graph_failed
5421 .store(true, std::sync::atomic::Ordering::Relaxed);
5422 self.cancel
5423 .store(true, std::sync::atomic::Ordering::Relaxed);
5424 tracing::error!("MTP graph mirror rewind failed after verify commit");
5425 return None;
5426 }
5427 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5428 if !warm_off && a > 0 {
5429 let mut warmed = false;
5432 #[cfg(target_os = "macos")]
5433 if metal_native && self.mtp_graph_mode == Some(true) {
5434 let pairs: Vec<(&[f32], u32)> = (0..a)
5438 .map(|j| {
5439 (
5440 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5441 ids[j],
5442 )
5443 })
5444 .collect();
5445 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5446 if !warmed {
5447 warmed = true;
5448 for j in 0..a {
5449 let row =
5450 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5451 if self
5452 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5453 .is_none()
5454 {
5455 warmed = false;
5456 break;
5457 }
5458 }
5459 }
5460 }
5461 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5462 let rows: Vec<Vec<f32>> = (0..a)
5463 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5464 .collect();
5465 let pairs: Vec<(&[f32], u32)> = rows
5466 .iter()
5467 .zip(ids.iter())
5468 .map(|(r, &t)| (r.as_slice(), t))
5469 .collect();
5470 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5471 Ok(()) => warmed = true,
5472 Err(err) => {
5473 tracing::error!("{err}");
5479 self.clear_sequence_state();
5480 self.graph_failed
5481 .store(true, std::sync::atomic::Ordering::Relaxed);
5482 self.cancel
5483 .store(true, std::sync::atomic::Ordering::Relaxed);
5484 return None;
5485 }
5486 }
5487 }
5488 if !warmed {
5489 for j in 0..a {
5490 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5491 let row = row.to_vec();
5492 self.mtp_warm(m, &row, ids[j], next_pos + j);
5493 }
5494 }
5495 }
5496 if let Some(c) = forced {
5500 self.spec_forced = Some(c);
5501 self.graph_logits = None;
5502 } else {
5503 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5504 row.resize(self.vocab_size, 0.0);
5505 if let Some(c) = self.final_softcap {
5506 for l in row.iter_mut() {
5507 *l = c * (*l / c).tanh();
5508 }
5509 }
5510 self.graph_logits = Some(row);
5511 }
5512 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5513 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5519 let end = subs();
5520 eprintln!(
5521 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5522 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5523 t_draft.as_secs_f64() * 1e3,
5524 sub_draft - sub0,
5525 (t_verify - t_draft).as_secs_f64() * 1e3,
5526 sub_verify - sub_draft,
5527 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5528 end - sub_verify,
5529 );
5530 }
5531 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5532 }
5533
5534 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5543 if !self.pair_supported() {
5544 return (0.0, 0.0);
5545 }
5546 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5553 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5554 let emb1 = self.embed_single(1);
5555 let emb2 = self.embed_single(2);
5556 let pos = self.kv_cache.seq_len();
5557
5558 let t0 = std::time::Instant::now();
5559 for _ in 0..iters {
5560 let _ = self.forward_layers(&emb1, pos, None);
5561 let _ = self.forward_layers(&emb2, pos + 1, None);
5562 for l in &mut self.kv_cache.layers {
5563 l.truncate_last(2);
5564 }
5565 }
5566 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5567
5568 let t1 = std::time::Instant::now();
5569 for _ in 0..iters {
5570 let _ = self.forward_pair(&emb1, &emb2, pos);
5571 for l in &mut self.kv_cache.layers {
5572 l.truncate_last(2);
5573 }
5574 }
5575 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5576 match graph_env {
5577 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5578 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5579 }
5580 (singles_ms, pair_ms)
5581 }
5582
5583 fn pair_supported(&self) -> bool {
5591 !self.weights.layers.is_empty()
5598 && self.g3n.is_none()
5599 && !self
5600 .weights
5601 .layers
5602 .iter()
5603 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5604 }
5605
5606 fn forward_pair(
5607 &mut self,
5608 emb1: &[f32],
5609 emb2: &[f32],
5610 position: usize,
5611 ) -> (Vec<f32>, Vec<f32>) {
5612 let mut h1 = emb1.to_vec();
5613 let mut h2 = emb2.to_vec();
5614 let (_nkv, _hd, hs, _rd, eps) = (
5615 self.num_kv_heads,
5616 self.head_dim,
5617 self.hidden_size,
5618 self.rotary_dim,
5619 self.rms_eps,
5620 );
5621 let pool = self.pool.clone();
5622
5623 for li in 0..self.num_layers {
5624 let lw = &self.weights.layers[self.phys_layer(li)];
5625 inference::rms_norm_into(
5628 &h1,
5629 &lw.input_norm,
5630 self.rms_eps,
5631 self.norm_style,
5632 &mut self.ws.n1,
5633 );
5634 inference::rms_norm_into(
5635 &h2,
5636 &lw.input_norm,
5637 self.rms_eps,
5638 self.norm_style,
5639 &mut self.ws.n2,
5640 );
5641
5642 let (a1, a2) = match &lw.attn {
5643 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5644 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5645 AttnKind::Linear(w) => {
5646 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5647 let layer = &mut self.kv_cache.layers[li];
5648 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5649 vmf_phase_pair(
5650 &self.ws.n1,
5651 &self.ws.n2,
5652 w,
5653 &cfg,
5654 state,
5655 scratch,
5656 self.pool.as_deref(),
5657 )
5658 }
5659 AttnKind::LinearGdn(w) => {
5660 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5661 let layer = &mut self.kv_cache.layers[li];
5662 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5663 gdn_pair(
5664 &self.ws.n1,
5665 &self.ws.n2,
5666 w,
5667 &cfg,
5668 state,
5669 scratch,
5670 self.pool.as_deref(),
5671 )
5672 }
5673 AttnKind::ShortConv(w) => {
5674 let cfg = self
5675 .short_conv_cfg
5676 .expect("short-conv layer without short_conv_cfg");
5677 let layer = &mut self.kv_cache.layers[li];
5678 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5679 short_conv_pair(
5680 &self.ws.n1,
5681 &self.ws.n2,
5682 w,
5683 &cfg,
5684 state,
5685 scratch,
5686 self.pool.as_deref(),
5687 )
5688 }
5689 AttnKind::Full {
5690 wq,
5691 wk,
5692 wv,
5693 wo,
5694 q_norm,
5695 k_norm,
5696 output_gate,
5697 softplus_gate,
5698 bias,
5699 } => {
5700 let inv_freq_l = self.layer_inv_freq(li);
5701 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5702 let cfg = QwenAttnCfg {
5703 num_heads: self.layer_num_heads(li),
5704 num_kv_heads: nkv_l,
5705 head_dim: hd_l,
5706 hidden_size: hs,
5707 position,
5708 inv_freq: &inv_freq_l,
5709 rotary_dim: rd_l,
5710 scale: self.attn_scale,
5711 softcap: self.attn_softcap,
5712 window: self.layer_window(li),
5713 v_norm: self.attn_v_norm,
5714 qk_norm_after_rope: self.qk_norm_after_rope,
5715 q_norm: q_norm.as_deref(),
5716 k_norm: k_norm.as_deref(),
5717 output_gate: *output_gate,
5718 softplus_gate: softplus_gate
5719 .as_ref()
5720 .map(|(gate, per_head)| (gate, *per_head)),
5721 rope_scale: self.layer_rope_scale(li),
5722 bias: bias
5723 .as_ref()
5724 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5725 rms_eps: eps,
5726 norm_style: self.norm_style,
5727 pool: pool.as_deref(),
5728 };
5729 attention::qwen_attention_pair(
5730 &self.ws.n1,
5731 &self.ws.n2,
5732 wq,
5733 wk,
5734 wv,
5735 wo,
5736 &mut self.kv_cache.layers[li],
5737 &cfg,
5738 )
5739 }
5740 };
5741 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5742 Some(w) => (
5743 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5744 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5745 ),
5746 None => (a1, a2),
5747 };
5748 for i in 0..self.hidden_size {
5749 h1[i] += a1[i];
5750 h2[i] += a2[i];
5751 }
5752 let (mut a1, mut a2) = (a1, a2);
5753 attention::recycle_buf(&mut a1);
5754 attention::recycle_buf(&mut a2);
5755
5756 let lw = &self.weights.layers[self.phys_layer(li)];
5757 inference::rms_norm_into(
5758 &h1,
5759 &lw.post_norm,
5760 self.rms_eps,
5761 self.norm_style,
5762 &mut self.ws.p1,
5763 );
5764 inference::rms_norm_into(
5765 &h2,
5766 &lw.post_norm,
5767 self.rms_eps,
5768 self.norm_style,
5769 &mut self.ws.p2,
5770 );
5771 let (f1, f2) = match &lw.ffn {
5772 FfnKind::DenseMoe(dm) => (
5775 dense_moe_ffn(
5776 dm,
5777 &self.ws.p1,
5778 &h1,
5779 self.rms_eps,
5780 self.norm_style,
5781 self.pool.as_deref(),
5782 ),
5783 dense_moe_ffn(
5784 dm,
5785 &self.ws.p2,
5786 &h2,
5787 self.rms_eps,
5788 self.norm_style,
5789 self.pool.as_deref(),
5790 ),
5791 ),
5792 _ => ffn_forward_pair(
5793 &lw.ffn,
5794 &self.ws.p1,
5795 &self.ws.p2,
5796 self.pool.as_deref(),
5797 None,
5798 ),
5799 };
5800 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5801 Some(w) => (
5802 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5803 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5804 ),
5805 None => (f1, f2),
5806 };
5807 for i in 0..self.hidden_size {
5808 h1[i] += f1[i];
5809 h2[i] += f2[i];
5810 }
5811 let (mut f1, mut f2) = (f1, f2);
5812 attention::recycle_buf(&mut f1);
5813 attention::recycle_buf(&mut f2);
5814 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5815 for i in 0..self.hidden_size {
5816 h1[i] *= sc;
5817 h2[i] *= sc;
5818 }
5819 }
5820 if self.is_loop_end(li) && li + 1 < self.num_layers {
5822 h1 = inference::rms_norm(
5823 &h1,
5824 &self.weights.final_norm,
5825 self.rms_eps,
5826 self.norm_style,
5827 );
5828 h2 = inference::rms_norm(
5829 &h2,
5830 &self.weights.final_norm,
5831 self.rms_eps,
5832 self.norm_style,
5833 );
5834 }
5835 }
5836 if self.o1_active() {
5842 self.commit_linear_scratch();
5843 }
5844 self.o1_progress();
5845 (h1, h2)
5846 }
5847
5848 fn commit_linear_scratch(&mut self) {
5850 for layer in &mut self.kv_cache.layers {
5851 if !layer.linear_scratch.is_empty() {
5852 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5853 layer.linear_scratch.clear();
5854 }
5855 }
5856 }
5857
5858 pub fn forward_ids(
5861 &mut self,
5862 ids: &[u32],
5863 task_mask: Option<&TaskMask>,
5864 ) -> Result<Vec<f32>, String> {
5865 if ids.is_empty() {
5866 return Err("empty id sequence".to_string());
5867 }
5868 self.clear_sequence_state();
5869 self.check_forward_graph("forward_ids setup", 0)?;
5870 if task_mask.is_none() {
5871 self.o1_begin();
5872 }
5873 let mut hidden = vec![0.0f32; self.hidden_size];
5874 let mut pos = 0usize;
5875 if let Some(b) = &mut self.dsv41 {
5876 let pool = self.pool.clone();
5877 let mut logits = Vec::new();
5878 crate::dsv41::forward_chunk(
5879 &b.0,
5880 &b.1,
5881 &b.2,
5882 &mut b.3,
5883 ids,
5884 0,
5885 pool.as_deref(),
5886 &mut logits,
5887 );
5888 if let Err(err) = self.o1_seal_checked() {
5889 self.clear_sequence_state();
5890 return Err(err);
5891 }
5892 return Ok(logits);
5893 }
5894 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5902 let chunk = prefill_chunk();
5906 let hs = self.hidden_size;
5907 while pos < ids.len() {
5908 let end = (pos + chunk).min(ids.len());
5909 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5910 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5911 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5912 pos = end;
5913 }
5914 }
5915 if task_mask.is_none()
5924 && !self.graph_prefill_preferred()
5925 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5926 && self.pair_supported()
5927 {
5928 while pos + 1 < ids.len() {
5929 let e1 = self.embed_single(ids[pos]);
5930 let e2 = self.embed_single(ids[pos + 1]);
5931 let (_, h2) = self.forward_pair(&e1, &e2, pos);
5932 self.check_forward_graph("forward_ids pair", pos + 1)?;
5933 self.commit_linear_scratch();
5934 hidden = h2;
5935 pos += 2;
5936 }
5937 }
5938 while pos < ids.len() {
5939 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5940 self.check_forward_graph("forward_ids", pos)?;
5941 pos += 1;
5942 }
5943 if let Err(err) = self.o1_seal_checked() {
5947 self.clear_sequence_state();
5948 return Err(err);
5949 }
5950 let normed = inference::rms_norm(
5951 &hidden,
5952 &self.weights.final_norm,
5953 self.rms_eps,
5954 self.norm_style,
5955 );
5956 Ok(self.lm_head_forward(&normed))
5957 }
5958
5959 #[doc(hidden)]
5963 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
5964 #[cfg(target_os = "macos")]
5965 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5966 if ids.is_empty() {
5967 return Err("empty id sequence".to_string());
5968 }
5969 self.clear_sequence_state();
5970 self.dsv41
5971 .as_ref()
5972 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
5973 self.o1_begin();
5974 let rows = {
5975 let pool = self.pool.clone();
5976 let b = self
5977 .dsv41
5978 .as_mut()
5979 .expect("dsv41 checked above; state cannot change during forward");
5980 let mut rows = Vec::with_capacity(ids.len());
5981 for (position, &id) in ids.iter().enumerate() {
5982 let mut logits = Vec::new();
5983 crate::dsv41::forward_token(
5984 &b.0,
5985 &b.1,
5986 &b.2,
5987 &mut b.3,
5988 id,
5989 position,
5990 pool.as_deref(),
5991 &mut logits,
5992 );
5993 rows.push(logits);
5994 }
5995 rows
5996 };
5997 self.o1_seal();
5998 Ok(rows)
5999 }
6000
6001 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
6008 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
6009 Ok((nll / cnt.max(1) as f64).exp())
6010 }
6011
6012 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
6017 self.clear_sequence_state();
6018 FFN_PROBE.with(|p| {
6019 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6020 });
6021 crate::gpu::cpu_scope(|| {
6022 for (pos, &id) in ids.iter().enumerate() {
6023 let emb = self.embed_single(id);
6024 let _ = self.forward_layers(&emb, pos, None);
6025 }
6026 });
6027 self.clear_sequence_state();
6028 FFN_PROBE
6029 .with(|p| p.borrow_mut().take())
6030 .unwrap_or_default()
6031 }
6032
6033 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
6037 if let Err(err) = self.nll_begin() {
6038 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
6042 self.nll_end();
6043 return Err(err);
6044 }
6045 FFN_PROBE.with(|p| {
6046 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
6047 });
6048 let result: Result<(), String> = (|| {
6049 for chunk in ids.chunks(256) {
6050 if chunk.len() < 2 {
6051 continue;
6052 }
6053 self.nll_ids_masked(chunk, 0, None)?;
6054 }
6055 Ok(())
6056 })();
6057 self.nll_end();
6058 let probe = FFN_PROBE
6059 .with(|p| p.borrow_mut().take())
6060 .unwrap_or_default();
6061 match result {
6062 Ok(()) => Ok(probe),
6063 Err(err) => {
6064 drop(probe);
6065 Err(err)
6066 }
6067 }
6068 }
6069
6070 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
6074 self.nll_begin()?;
6075 let result: Result<f64, String> = (|| {
6076 let mut nll = 0f64;
6077 let mut cnt = 0usize;
6078 let mut hidden = vec![0f32; self.hidden_size];
6079 for (pos, &id) in ids.iter().enumerate() {
6080 if pos > 0 {
6081 inference::rms_norm_into(
6082 &hidden,
6083 &self.weights.final_norm,
6084 self.rms_eps,
6085 self.norm_style,
6086 &mut self.ws.n1,
6087 );
6088 let mut logits = self.lm_head_forward(&self.ws.n1);
6089 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6090 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
6091 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
6092 nll -= p.max(1e-300).ln();
6093 cnt += 1;
6094 attention::recycle_buf(&mut logits);
6095 }
6096 let emb = self.embed_single(id);
6097 hidden = self.forward_layers(&emb, pos, Some(mask));
6098 self.nll_check_graph("masked serial forward", pos)?;
6099 let _ = self.graph_logits.take();
6103 }
6104 Ok((nll / cnt.max(1) as f64).exp())
6105 })();
6106 self.nll_end();
6107 result
6108 }
6109
6110 pub fn nll_ids_masked(
6129 &mut self,
6130 ids: &[u32],
6131 start: usize,
6132 task_mask: Option<&TaskMask>,
6133 ) -> Result<(f64, usize), String> {
6134 let task_mask = self.drop_open_mask(task_mask);
6135 self.nll_ids_inner(ids, start, task_mask)
6136 }
6137
6138 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
6139 self.nll_ids_inner(ids, start, None)
6140 }
6141
6142 fn nll_ids_inner(
6143 &mut self,
6144 ids: &[u32],
6145 start: usize,
6146 task_mask: Option<&TaskMask>,
6147 ) -> Result<(f64, usize), String> {
6148 self.nll_begin()?;
6149 let result: Result<(f64, usize), String> = (|| {
6150 let mut nll = 0f64;
6151 let mut cnt = 0usize;
6152 let (graph_quality, fused_head_quality) = nll_graph_policy(
6165 task_mask.is_none(),
6166 self.graph_prefill_preferred(),
6167 crate::gpu::q1_force(),
6168 );
6169 self.graph_head_required = fused_head_quality;
6170 self.graph_want_logits = fused_head_quality;
6171 #[cfg(target_os = "macos")]
6172 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
6173 match self.nll_batch_metal(ids, start) {
6174 MetalBatchNllOutcome::Completed(nll, count) => {
6175 return Ok((nll, count));
6176 }
6177 MetalBatchNllOutcome::Declined => {}
6178 MetalBatchNllOutcome::Failed(err) => return Err(err),
6179 }
6180 }
6181 if self.can_prefill_batched() && !graph_quality {
6182 const CHUNK: usize = 128;
6188 const LM_SUB: usize = 32;
6189 let n = ids.len().saturating_sub(1);
6190 let hs = self.hidden_size;
6191 let rows = self.weights.lm_head.rows();
6192 let mut pos = 0usize;
6193 while pos < n {
6194 let end = (pos + CHUNK).min(n);
6195 let bsz = end - pos;
6196 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6197 self.nll_check_graph("batched prefill", pos)?;
6198 let mut k0 = 0usize;
6199 while k0 < bsz {
6200 let k1 = (k0 + LM_SUB).min(bsz);
6201 let sb = k1 - k0;
6202 if pos + k1 <= start {
6205 k0 = k1;
6206 continue;
6207 }
6208 let mut normed = vec![0.0f32; sb * hs];
6209 for k in 0..sb {
6210 let r = inference::rms_norm(
6211 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6212 &self.weights.final_norm,
6213 self.rms_eps,
6214 self.norm_style,
6215 );
6216 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6217 }
6218 let mut logits = vec![0.0f32; sb * rows];
6219 self.weights
6220 .lm_head
6221 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6222 for k in 0..sb {
6223 if pos + k0 + k < start {
6224 continue;
6225 }
6226 self.nll_check_graph("batched score row", pos + k0 + k)?;
6227 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6228 if let Some(mu) = self.logit_multiplier {
6229 for v in lg.iter_mut() {
6230 *v *= mu;
6231 }
6232 }
6233 if let Some(c) = self.final_softcap {
6237 for v in lg.iter_mut() {
6238 *v = c * (*v / c).tanh();
6239 }
6240 }
6241 if let Some(cm) = self.head_clusters.clone() {
6244 self.hierarchical_head_logprobs(
6245 &normed[k * hs..(k + 1) * hs],
6246 &cm,
6247 lg,
6248 );
6249 }
6250 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6251 let target = ids[pos + k0 + k + 1] as usize;
6252 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6253 let lse: f64 = lg
6254 .iter()
6255 .map(|&v| ((v - max) as f64).exp())
6256 .sum::<f64>()
6257 .ln()
6258 + max as f64;
6259 nll += lse - lg[target] as f64;
6260 cnt += 1;
6261 if std::env::var("CMF_PPL_TRACE").is_ok() {
6262 let top = lg
6263 .iter()
6264 .enumerate()
6265 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6266 .map(|(i, _)| i)
6267 .unwrap_or(0);
6268 eprintln!(
6269 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6270 pos + k0 + k,
6271 target,
6272 lse - lg[target] as f64,
6273 top,
6274 lg[target],
6275 lg[top]
6276 );
6277 }
6278 }
6279 k0 = k1;
6280 }
6281 pos = end;
6282 }
6283 return Ok((nll, cnt));
6284 }
6285 for pos in 0..ids.len().saturating_sub(1) {
6286 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6287 self.nll_check_graph("serial forward", pos)?;
6288 let out_of_band = self.graph_logits.take();
6296 if self.graph_head_required && out_of_band.is_none() {
6297 METAL_GRAPH_HEAD_MISS.fetch_add(
6298 1,
6299 std::sync::atomic::Ordering::Relaxed,
6300 );
6301 return Err(format!(
6302 "fused Metal graph head did not complete at NLL position {pos}"
6303 ));
6304 }
6305 if pos < start {
6306 continue;
6307 }
6308 let logits = match out_of_band {
6309 Some(lg) => lg,
6310 None => {
6311 let normed = inference::rms_norm(
6312 &hidden,
6313 &self.weights.final_norm,
6314 self.rms_eps,
6315 self.norm_style,
6316 );
6317 self.lm_head_forward(&normed)
6321 }
6322 };
6323 let target = ids[pos + 1] as usize;
6324 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6325 let lse: f64 = logits
6326 .iter()
6327 .map(|&v| ((v - max) as f64).exp())
6328 .sum::<f64>()
6329 .ln()
6330 + max as f64;
6331 let tok_nll = lse - logits[target] as f64;
6332 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6333 let top = logits
6334 .iter()
6335 .enumerate()
6336 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6337 .map(|(i, _)| i)
6338 .unwrap_or(0);
6339 eprintln!(
6340 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6341 logits[target], logits[top]
6342 );
6343 }
6344 nll += tok_nll;
6345 cnt += 1;
6346 }
6347 Ok((nll, cnt))
6348 })();
6349 self.nll_end();
6350 result
6351 }
6352
6353 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6358 let normed = inference::rms_norm(
6359 hidden,
6360 &self.weights.final_norm,
6361 self.rms_eps,
6362 self.norm_style,
6363 );
6364 let mut logits = self.lm_head_forward(&normed);
6367 let target = target as usize;
6368 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6369 let lse: f64 = logits
6370 .iter()
6371 .map(|&v| ((v - max) as f64).exp())
6372 .sum::<f64>()
6373 .ln()
6374 + max as f64;
6375 let tok_nll = lse - logits[target] as f64;
6376 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6377 let top = logits
6378 .iter()
6379 .enumerate()
6380 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6381 .map(|(i, _)| i)
6382 .unwrap_or(0);
6383 eprintln!(
6384 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6385 logits[target], logits[top]
6386 );
6387 }
6388 attention::recycle_buf(&mut logits);
6389 tok_nll
6390 }
6391
6392 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6410 self.nll_begin()?;
6415 let requested_prefix = (prefill > 0).then_some(prefill);
6416 self.o1_begin_with_prefix(requested_prefix);
6417 let n = ids.len().saturating_sub(1);
6418 let requested_start = prefill.min(n);
6419 let exact_end = if self.o1_active() {
6424 match requested_prefix {
6425 Some(requested) => self.o1_effective_boundary(requested),
6426 None => self
6427 .o1_cfg
6428 .as_ref()
6429 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6430 }
6431 .unwrap_or(requested_start)
6432 .min(n)
6433 } else {
6434 requested_start
6435 };
6436 let mut nll = 0f64;
6437 let mut cnt = 0usize;
6438
6439 let mut pos = 0usize;
6443 if self.can_prefill_batched() {
6444 const CHUNK: usize = 128;
6445 while pos < exact_end {
6446 let end = (pos + CHUNK).min(exact_end);
6447 let hiddens = self.prefill_batch(&ids[pos..end], pos);
6448 if self
6449 .graph_failed
6450 .swap(false, std::sync::atomic::Ordering::Relaxed)
6451 {
6452 self.cancel
6453 .store(false, std::sync::atomic::Ordering::Relaxed);
6454 self.nll_end();
6455 return Err("GPU graph failed during O(1) NLL prefix".into());
6456 }
6457 for row in 0..end - pos {
6458 let score_pos = pos + row;
6459 if score_pos >= requested_start && score_pos < n {
6460 nll += self.nll_from_hidden(
6461 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6462 ids[score_pos + 1],
6463 score_pos,
6464 );
6465 cnt += 1;
6466 }
6467 }
6468 pos = end;
6469 }
6470 } else {
6471 while pos < exact_end {
6472 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6473 if self
6474 .graph_failed
6475 .swap(false, std::sync::atomic::Ordering::Relaxed)
6476 {
6477 self.cancel
6478 .store(false, std::sync::atomic::Ordering::Relaxed);
6479 self.nll_end();
6480 return Err("GPU graph failed during O(1) NLL prefix".into());
6481 }
6482 if pos >= requested_start && pos < n {
6483 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6484 cnt += 1;
6485 }
6486 pos += 1;
6487 }
6488 }
6489 self.o1_seal_checked().map_err(|err| {
6490 self.nll_end();
6491 err
6492 })?;
6493
6494 let batch_k = std::env::var("CMF_BATCH_K")
6503 .ok()
6504 .and_then(|v| v.parse::<usize>().ok())
6505 .unwrap_or(0);
6506 let batch_admitted = batch_k > 0
6507 && self.can_prefill_batched()
6508 && self.o1_active()
6509 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6510 && (0..self.num_layers).all(|li| {
6511 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6512 cache.o1.is_none() || cache.o1_views().is_some()
6513 });
6514 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6515 eprintln!(
6516 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6517 batch_admitted,
6518 batch_k,
6519 n.saturating_sub(exact_end),
6520 );
6521 }
6522 let mut batch_completed = false;
6523 if batch_admitted && exact_end < n {
6524 let hs = self.hidden_size;
6525 let mut batch_pos = exact_end;
6526 while batch_pos < n {
6527 let end = (batch_pos + batch_k).min(n);
6528 let bk = end - batch_pos;
6529 let mut hiddens = vec![0.0f32; bk * hs];
6530 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6531 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6532 }
6533 let positions: Vec<usize> = (batch_pos..end).collect();
6534 let t_batch = std::time::Instant::now();
6535 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6536 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6537 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6538 eprintln!(
6539 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6540 batch_pos,
6541 end.saturating_sub(1),
6542 bk as f64 / (ms / 1000.0),
6543 );
6544 }
6545 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6546 self.nll_end();
6547 return Err(err);
6548 }
6549 match outcome {
6550 crate::gpu::BatchGraphOutcome::Completed => {
6551 batch_completed = true;
6552 for row in 0..bk {
6553 nll += self.nll_from_hidden(
6554 &hiddens[row * hs..(row + 1) * hs],
6555 ids[batch_pos + row + 1],
6556 batch_pos + row,
6557 );
6558 cnt += 1;
6559 }
6560 batch_pos = end;
6561 }
6562 crate::gpu::BatchGraphOutcome::Declined => {
6563 if batch_completed {
6564 self.nll_end();
6565 return Err(format!(
6566 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6567 ));
6568 }
6569 break;
6570 }
6571 crate::gpu::BatchGraphOutcome::Failed => {
6572 self.nll_end();
6573 return Err(format!(
6574 "O(1) NLL batch graph failed after admission at position {batch_pos}"
6575 ));
6576 }
6577 }
6578 }
6579 if batch_completed && cnt == n.saturating_sub(requested_start) {
6580 self.nll_end();
6581 return Ok((nll, cnt));
6582 }
6583 }
6584
6585 for pos in exact_end..n {
6590 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6591 if self
6592 .graph_failed
6593 .swap(false, std::sync::atomic::Ordering::Relaxed)
6594 {
6595 self.cancel
6596 .store(false, std::sync::atomic::Ordering::Relaxed);
6597 self.nll_end();
6598 return Err(format!(
6599 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6600 ));
6601 }
6602 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6603 cnt += 1;
6604 }
6605 self.nll_end();
6606 Ok((nll, cnt))
6607 }
6608
6609 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6617 self.clear_sequence_state();
6618 let n = ids.len().saturating_sub(1);
6619 let mut correct = Vec::with_capacity(n);
6620 let mut pmax = Vec::with_capacity(n);
6621 for pos in 0..n {
6622 let emb = self.embed_single(ids[pos]);
6623 let hidden = self.forward_layers(&emb, pos, None);
6624 let normed = inference::rms_norm(
6625 &hidden,
6626 &self.weights.final_norm,
6627 self.rms_eps,
6628 self.norm_style,
6629 );
6630 let logits = self.lm_head_forward(&normed);
6634 let target = ids[pos + 1] as usize;
6635 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6636 for (i, &v) in logits.iter().enumerate() {
6637 if v > mval {
6638 mval = v;
6639 amax = i;
6640 }
6641 }
6642 correct.push(amax == target);
6643 let row: Vec<f32> = temps
6644 .iter()
6645 .map(|&t| {
6646 let tt = t.max(1e-3);
6647 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6648 1.0 / s.max(1e-12) })
6650 .collect();
6651 pmax.push(row);
6652 }
6653 self.clear_sequence_state();
6654 (correct, pmax)
6655 }
6656
6657 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6664 if self.dyn_router.is_none() {
6665 return Ok((self.ppl_ids(ids)?, 0));
6666 }
6667 self.nll_begin()?;
6668 let saved_active = self.dyn_active;
6669 let mut router = self
6670 .dyn_router
6671 .take()
6672 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6673 router.reset();
6674 self.dyn_phi_seen = 0;
6675 let _ = self.set_active_skill(None);
6676
6677 let result: Result<(f64, usize), String> = (|| {
6678 let mut nll = 0f64;
6679 let mut cnt = 0usize;
6680 for pos in 0..ids.len().saturating_sub(1) {
6681 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6682 self.nll_check_graph("dynamic serial forward", pos)?;
6683 let out_of_band = self.graph_logits.take();
6684 let mut logits = match out_of_band {
6685 Some(lg) => lg,
6686 None => {
6687 let normed = inference::rms_norm(
6688 &hidden,
6689 &self.weights.final_norm,
6690 self.rms_eps,
6691 self.norm_style,
6692 );
6693 self.lm_head_forward(&normed)
6697 }
6698 };
6699 let target = ids[pos + 1] as usize;
6700 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6701 let lse: f64 = logits
6702 .iter()
6703 .map(|&v| ((v - max) as f64).exp())
6704 .sum::<f64>()
6705 .ln()
6706 + max as f64;
6707 let tok_nll = lse - logits[target] as f64;
6708 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6709 let top = logits
6710 .iter()
6711 .enumerate()
6712 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6713 .map(|(i, _)| i)
6714 .unwrap_or(0);
6715 eprintln!(
6716 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6717 logits[target], logits[top]
6718 );
6719 }
6720 nll += tok_nll;
6721 cnt += 1;
6722 attention::recycle_buf(&mut logits);
6723 let phi = self.dyn_phi_ema.clone();
6725 if let Some(new_active) = router.step(&phi, pos) {
6726 let _ = self.set_active_skill(new_active);
6727 }
6728 }
6729 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6730 })();
6731
6732 let _ = self.set_active_skill(saved_active);
6735 self.dyn_router = Some(router);
6736 self.nll_end();
6737 result
6738 }
6739
6740 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6742 self.clear_sequence_state();
6743 let mut acc = vec![0f32; self.hidden_size];
6744 for (pos, &id) in ids.iter().enumerate() {
6745 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6746 for (a, v) in acc.iter_mut().zip(&h) {
6747 *a += v;
6748 }
6749 }
6750 let n = ids.len().max(1) as f32;
6751 for a in acc.iter_mut() {
6752 *a /= n;
6753 }
6754 self.clear_sequence_state();
6755 acc
6756 }
6757
6758 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6764 self.prefill_batch_masked(ids, start_pos, None)
6765 }
6766
6767 fn prefill_batch_masked(
6773 &mut self,
6774 ids: &[u32],
6775 start_pos: usize,
6776 task_mask: Option<&TaskMask>,
6777 ) -> Vec<f32> {
6778 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6779 }
6780
6781 fn prefill_batch_span(
6787 &mut self,
6788 input: PrefillIn<'_>,
6789 start_pos: usize,
6790 task_mask: Option<&TaskMask>,
6791 from: usize,
6792 upto_excl: usize,
6793 ) -> Vec<f32> {
6794 let hs = self.hidden_size;
6795 let b = match input {
6796 PrefillIn::Ids(ids) => ids.len(),
6797 PrefillIn::Hidden(hb) => hb.len() / hs,
6798 };
6799 let upto_excl = upto_excl.min(self.num_layers);
6800 let mut h: Vec<f32>;
6804 let mut h_ready;
6805 match input {
6806 PrefillIn::Ids(_) => {
6807 h = vec![0.0; b * hs];
6808 h_ready = false;
6809 }
6810 PrefillIn::Hidden(hb) => {
6811 h = hb.to_vec();
6812 h_ready = true;
6813 }
6814 }
6815 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6816 if let PrefillIn::Ids(ids) = input {
6817 for (bi, &id) in ids.iter().enumerate() {
6818 let e = me.embed_single(id);
6819 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6820 }
6821 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6822 if let Ok(t) = tp.parse::<usize>() {
6823 if t >= start_pos && t < start_pos + ids.len() {
6824 let bi = t - start_pos;
6825 let row = &h[bi * hs..(bi + 1) * hs];
6826 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6827 eprintln!(
6828 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6829 ids[bi],
6830 row[0],
6831 row[1],
6832 ids.len(),
6833 &ids[..ids.len().min(8)]
6834 );
6835 }
6836 }
6837 }
6838 }
6839 };
6840 let (_nkv, _hd, _rd, eps) = (
6841 self.num_kv_heads,
6842 self.head_dim,
6843 self.rotary_dim,
6844 self.rms_eps,
6845 );
6846 let pool = self.pool.clone();
6847 let norm_style = self.norm_style;
6848 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6849
6850 #[cfg(target_os = "macos")]
6851 let mut chunk_skip_until = 0usize;
6852 for li in from..upto_excl {
6853 let _capacity_tail = automatic_gpu_prefix
6854 .filter(|&prefix| li >= prefix)
6855 .map(|_| crate::gpu::enter_cpu_scope());
6856 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6863 if task_mask.is_none() {
6864 if li < chunk_skip_until {
6865 continue;
6866 }
6867 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6873 fill_h(&mut h, self);
6874 h_ready = true;
6875 }
6876 let ids_for_embed = match input {
6877 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6878 PrefillIn::Hidden(_) => None,
6879 };
6880 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6881 if end > li {
6882 h_ready = true;
6883 chunk_skip_until = end;
6884 if self.is_loop_end(end - 1) && end < self.num_layers {
6887 for bi in 0..b {
6888 let normed = inference::rms_norm(
6889 &h[bi * hs..(bi + 1) * hs],
6890 &self.weights.final_norm,
6891 eps,
6892 norm_style,
6893 );
6894 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6895 }
6896 }
6897 continue;
6898 }
6899 }
6900 if !h_ready {
6901 fill_h(&mut h, self);
6902 h_ready = true;
6903 }
6904 let lw = &self.weights.layers[self.phys_layer(li)];
6905 match &lw.attn {
6907 AttnKind::Kda(w) => {
6908 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6910 let mut normed = vec![0.0f32; b * hs];
6911 for bi in 0..b {
6912 inference::rms_norm_into(
6913 &h[bi * hs..(bi + 1) * hs],
6914 &lw.input_norm,
6915 eps,
6916 norm_style,
6917 &mut normed[bi * hs..(bi + 1) * hs],
6918 );
6919 }
6920 let attn = crate::linear_core::kda_forward_batch(
6921 &normed,
6922 b,
6923 w,
6924 &cfg,
6925 &mut self.kv_cache.layers[li].linear_state,
6926 pool.as_deref(),
6927 );
6928 for (dst, &a) in h.iter_mut().zip(&attn) {
6929 *dst += a;
6930 }
6931 }
6932 AttnKind::LinearGdn(w) => {
6933 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6935 let mut normed = vec![0.0f32; b * hs];
6936 for bi in 0..b {
6937 let r = inference::rms_norm(
6938 &h[bi * hs..(bi + 1) * hs],
6939 &lw.input_norm,
6940 eps,
6941 norm_style,
6942 );
6943 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6944 }
6945 let attn = crate::linear_core::gdn_forward_batch(
6946 &normed,
6947 b,
6948 w,
6949 &cfg,
6950 &mut self.kv_cache.layers[li].linear_state,
6951 pool.as_deref(),
6952 );
6953 for (dst, &a) in h.iter_mut().zip(&attn) {
6954 *dst += a;
6955 }
6956 }
6957 AttnKind::ShortConv(w) => {
6958 let cfg = self
6961 .short_conv_cfg
6962 .expect("short-conv layer without short_conv_cfg");
6963 let mut normed = vec![0.0f32; b * hs];
6964 for bi in 0..b {
6965 inference::rms_norm_into(
6966 &h[bi * hs..(bi + 1) * hs],
6967 &lw.input_norm,
6968 eps,
6969 norm_style,
6970 &mut normed[bi * hs..(bi + 1) * hs],
6971 );
6972 }
6973 let attn = short_conv_forward_batch(
6974 &normed,
6975 b,
6976 w,
6977 &cfg,
6978 &mut self.kv_cache.layers[li].linear_state,
6979 pool.as_deref(),
6980 );
6981 for (dst, &a) in h.iter_mut().zip(&attn) {
6982 *dst += a;
6983 }
6984 }
6985 AttnKind::Mla(w) => {
6986 let inv_freq_l = self.layer_inv_freq(li);
6989 let rs = self.layer_rope_scale(li);
6990 let mut normed = vec![0.0f32; hs];
6991 for bi in 0..b {
6992 inference::rms_norm_into(
6993 &h[bi * hs..(bi + 1) * hs],
6994 &lw.input_norm,
6995 eps,
6996 norm_style,
6997 &mut normed,
6998 );
6999 let ao = mla_attention(
7000 w,
7001 &normed,
7002 &mut self.kv_cache.layers[li],
7003 start_pos + bi,
7004 &inv_freq_l,
7005 rs,
7006 eps,
7007 pool.as_deref(),
7008 );
7009 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
7010 *dst += a;
7011 }
7012 }
7013 }
7014 AttnKind::Full {
7015 wq,
7016 wk,
7017 wv,
7018 wo,
7019 q_norm,
7020 k_norm,
7021 output_gate,
7022 softplus_gate,
7023 bias,
7024 } => {
7025 let mut normed = vec![0.0f32; b * hs];
7029 for bi in 0..b {
7030 inference::rms_norm_into(
7031 &h[bi * hs..(bi + 1) * hs],
7032 &lw.input_norm,
7033 eps,
7034 norm_style,
7035 &mut normed[bi * hs..(bi + 1) * hs],
7036 );
7037 }
7038 let inv_freq_l = self.layer_inv_freq(li);
7039 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7040 let cfg = QwenAttnCfg {
7041 num_heads: self.layer_num_heads(li),
7042 num_kv_heads: nkv_l,
7043 head_dim: hd_l,
7044 hidden_size: hs,
7045 position: start_pos,
7046 inv_freq: &inv_freq_l,
7047 rotary_dim: rd_l,
7048 scale: self.attn_scale,
7049 softcap: self.attn_softcap,
7050 window: self.layer_window(li),
7051 v_norm: self.attn_v_norm,
7052 qk_norm_after_rope: self.qk_norm_after_rope,
7053 q_norm: q_norm.as_deref(),
7054 k_norm: k_norm.as_deref(),
7055 output_gate: *output_gate,
7056 softplus_gate: softplus_gate
7057 .as_ref()
7058 .map(|(gate, per_head)| (gate, *per_head)),
7059 rope_scale: self.layer_rope_scale(li),
7060 bias: bias
7061 .as_ref()
7062 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7063 rms_eps: eps,
7064 norm_style,
7065 pool: pool.as_deref(),
7066 };
7067 let mut attn = attention::qwen_attention_batch(
7068 &normed,
7069 b,
7070 wq,
7071 wk,
7072 wv,
7073 wo,
7074 &mut self.kv_cache.layers[li],
7075 &cfg,
7076 );
7077 if let Some(w) = &lw.attn_out_norm {
7078 for bi in 0..b {
7079 inference::rms_norm_into(
7080 &attn[bi * hs..(bi + 1) * hs],
7081 w,
7082 eps,
7083 norm_style,
7084 &mut normed[bi * hs..(bi + 1) * hs],
7085 );
7086 }
7087 attn.copy_from_slice(&normed);
7088 }
7089 for (dst, &a) in h.iter_mut().zip(&attn) {
7090 *dst += a;
7091 }
7092 }
7093 AttnKind::Linear(w) => {
7094 for bi in 0..b {
7095 let normed = inference::rms_norm(
7096 &h[bi * hs..(bi + 1) * hs],
7097 &lw.input_norm,
7098 eps,
7099 norm_style,
7100 );
7101 vmf_phase_forward(
7102 &normed,
7103 w,
7104 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
7105 &mut self.kv_cache.layers[li].linear_state,
7106 pool.as_deref(),
7107 )
7108 .iter()
7109 .enumerate()
7110 .for_each(|(i, &a)| h[bi * hs + i] += a);
7111 }
7112 }
7113 }
7114
7115 let lw = &self.weights.layers[self.phys_layer(li)];
7117 let mut post = vec![0.0f32; b * hs];
7118 for bi in 0..b {
7119 let r =
7120 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
7121 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7122 }
7123 let mask_row = task_mask
7126 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
7127 .and_then(|m| m.ffn_masks.get(li))
7128 .map(|v| v.as_slice());
7129 let mut ffn = match &lw.ffn {
7130 FfnKind::Dense(d) if !d.segs.is_empty() => {
7131 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
7132 }
7133 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
7134 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
7135 FfnKind::DenseMoe(dm) => {
7138 let mut out = vec![0.0f32; b * hs];
7139 for bi in 0..b {
7140 let r = dense_moe_ffn(
7141 dm,
7142 &post[bi * hs..(bi + 1) * hs],
7143 &h[bi * hs..(bi + 1) * hs],
7144 eps,
7145 norm_style,
7146 pool.as_deref(),
7147 );
7148 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
7149 }
7150 out
7151 }
7152 };
7153 if let Some(w) = &lw.ffn_out_norm {
7154 for bi in 0..b {
7155 inference::rms_norm_into(
7156 &ffn[bi * hs..(bi + 1) * hs],
7157 w,
7158 eps,
7159 norm_style,
7160 &mut post[bi * hs..(bi + 1) * hs],
7161 );
7162 }
7163 ffn.copy_from_slice(&post);
7164 }
7165 for (dst, &f) in h.iter_mut().zip(&ffn) {
7166 *dst += f;
7167 }
7168 if let Some(sc) = lw.layer_scale {
7169 for v in h.iter_mut() {
7170 *v *= sc;
7171 }
7172 }
7173 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
7174 if let Ok(t) = tp.parse::<usize>() {
7175 if t >= start_pos && t < start_pos + b {
7176 let bi = t - start_pos;
7177 let row = &h[bi * hs..(bi + 1) * hs];
7178 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
7179 eprintln!(
7180 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
7181 row[0], row[1]
7182 );
7183 }
7184 }
7185 }
7186 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
7190 let row = &h[(b - 1) * hs..b * hs];
7191 let rms =
7192 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7193 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7194 eprintln!(
7195 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7196 match &self.weights.layers[self.phys_layer(li)].attn {
7197 AttnKind::LinearGdn(_) => "gdn",
7198 AttnKind::Linear(_) => "vmf",
7199 AttnKind::ShortConv(_) => "conv",
7200 _ => "attn",
7201 },
7202 match &lw.ffn {
7203 FfnKind::Moe(_) => "moe",
7204 FfnKind::Dense(_) => "dense",
7205 FfnKind::DenseMoe(_) => "dense+moe",
7206 },
7207 );
7208 }
7209 if self.is_loop_end(li) && li + 1 < self.num_layers {
7211 for bi in 0..b {
7212 let normed = inference::rms_norm(
7213 &h[bi * hs..(bi + 1) * hs],
7214 &self.weights.final_norm,
7215 eps,
7216 norm_style,
7217 );
7218 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7219 }
7220 }
7221 if std::env::var("CMF_TRACE_H").is_ok() {
7222 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7223 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7224 eprintln!(
7225 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7226 lw.layer_scale
7227 );
7228 }
7229 }
7230 crate::gpu::set_layer(-1); self.o1_progress();
7236 h
7237 }
7238
7239 fn embed_single(&self, id: u32) -> Vec<f32> {
7241 let mut out = vec![0.0f32; self.hidden_size];
7242 if (id as usize) < self.weights.embed_tokens.rows() {
7243 self.weights.embed_tokens.row_f32(id as usize, &mut out);
7244 }
7245 if self.embed_multiplier != 1.0 {
7246 for v in out.iter_mut() {
7247 *v *= self.embed_multiplier;
7248 }
7249 }
7250 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7254 let mut v = vec![0.0f32; self.hidden_size.max(1)];
7255 v[0] = id as f32;
7256 return v;
7257 }
7258 if let Some(b) = &self.g3n {
7261 return b.0.extend_embedding(id, &out, self.pool.as_deref());
7262 }
7263 out
7264 }
7265
7266 #[cfg(target_os = "macos")]
7272 fn chunk_run_gpu(
7273 &mut self,
7274 li0: usize,
7275 h: &mut [f32],
7276 b: usize,
7277 pos0: usize,
7278 embed_ids: Option<&[u32]>,
7279 cap: usize,
7280 ) -> usize {
7281 if !crate::gpu::enabled_here()
7285 || std::env::var("CMF_GPU_CHUNK")
7286 .map(|v| v == "0")
7287 .unwrap_or(false)
7288 || b < 32
7289 || self.swa.is_some()
7290 || self.global_attn.is_some()
7291 || self.o1_active()
7294 || self.attn_v_norm
7295 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7296 {
7297 return li0;
7298 }
7299 let Some(model) = self.model.clone() else {
7300 return li0;
7301 };
7302 let inv_freq = self.inv_freq.clone();
7303 let (nh, nkv, hd, hs) = (
7304 self.num_heads,
7305 self.num_kv_heads,
7306 self.head_dim,
7307 self.hidden_size,
7308 );
7309 let loop_end = if self.loop_final_norm {
7313 ((li0 / self.physical_layers) + 1) * self.physical_layers
7314 } else {
7315 self.num_layers
7316 };
7317 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7318 let mut stored_at: Vec<usize> = Vec::new();
7319 for li in li0..self.num_layers.min(loop_end).min(cap) {
7320 let lw = &self.weights.layers[self.phys_layer(li)];
7321 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7322 break;
7323 }
7324 let AttnKind::Full {
7325 wq,
7326 wk,
7327 wv,
7328 wo,
7329 q_norm,
7330 k_norm,
7331 output_gate: false,
7332 softplus_gate: None,
7333 bias,
7334 } = &lw.attn
7335 else {
7336 break;
7337 };
7338 let FfnKind::Dense(d) = &lw.ffn else { break };
7339 if d.act != Act::Silu || !d.segs.is_empty() {
7340 break;
7341 }
7342 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7347 t.q8_row_parts()
7348 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7349 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7350 }
7351 let parts = (
7352 cw(wq),
7353 cw(wk),
7354 cw(wv),
7355 cw(wo),
7356 cw(&d.gate_proj),
7357 cw(&d.up_proj),
7358 cw(&d.down_proj),
7359 );
7360 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7361 else {
7362 break;
7363 };
7364 let layer = &self.kv_cache.layers[li];
7365 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7366 break;
7367 }
7368 stored_at.push(layer.head_len(0));
7369 layers.push(crate::gpu_metal::ChunkLayer {
7370 model: &model,
7371 kv_id: self.graph_kv_id,
7372 layer: li,
7373 wq: pq,
7374 wk: pk,
7375 wv: pv,
7376 wo: po,
7377 gate: pg,
7378 up: pu,
7379 down: pd,
7380 input_norm: &lw.input_norm,
7381 post_norm: &lw.post_norm,
7382 bias: bias
7383 .as_ref()
7384 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7385 q_norm: q_norm.as_deref(),
7386 k_norm: k_norm.as_deref(),
7387 inv_freq: &inv_freq,
7388 rd: self.rotary_dim,
7389 nh,
7390 nkv,
7391 hd,
7392 hs,
7393 inter: d.gate_proj.rows(),
7394 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7395 late_qk_norm: self.qk_norm_after_rope,
7396 eps: self.rms_eps as f32,
7397 });
7398 }
7399 if layers.is_empty() {
7400 return li0;
7401 }
7402 let row = nkv * hd;
7403 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7404 .iter()
7405 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7406 .collect();
7407 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7408 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7409 let li = layers[i].layer;
7410 let layer = &self.kv_cache.layers[li];
7411 io.push(crate::gpu_metal::ChunkIo {
7412 cpu_stored: stored_at[i],
7413 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7414 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7415 out_k: ok,
7416 out_v: ov,
7417 imp: oi,
7418 });
7419 }
7420 let n_run = layers.len();
7421 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7422 let ep = embed_ids.and_then(|ids| {
7425 self.weights
7426 .embed_tokens
7427 .q8_row_parts()
7428 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7429 idx,
7430 rows,
7431 row_scale: rs,
7432 ids,
7433 mult: self.embed_multiplier,
7434 })
7435 });
7436 if embed_ids.is_some() && ep.is_none() {
7437 return li0;
7438 }
7439 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7440 return li0;
7441 }
7442 drop(io);
7443 drop(layers);
7444 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7447 let li = li0 + i;
7448 let layer = &mut self.kv_cache.layers[li];
7449 for bi in 0..b {
7450 layer.append(
7451 &ok[bi * row..(bi + 1) * row],
7452 &ov[bi * row..(bi + 1) * row],
7453 &[],
7454 );
7455 }
7456 layer.accumulate_imp(oi);
7457 }
7458 last
7459 }
7460
7461 fn layer_is_local(&self, li: usize) -> bool {
7464 if let Some(layers) = &self.sliding_layers {
7465 return layers.get(li).copied().unwrap_or(false);
7466 }
7467 match self.swa {
7468 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7469 None => false,
7470 }
7471 }
7472
7473 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7476 if self.layer_is_local(li) {
7477 if let Some(f) = &self.inv_freq_local {
7478 return f.clone();
7479 }
7480 } else if let Some(f) = &self.inv_freq_global {
7481 return f.clone();
7482 }
7483 self.inv_freq.clone()
7484 }
7485
7486 fn layer_window(&self, li: usize) -> Option<usize> {
7488 self.swa
7489 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7490 }
7491
7492 fn layer_num_heads(&self, li: usize) -> usize {
7493 self.attention_heads_per_layer
7494 .as_ref()
7495 .and_then(|v| v.get(li).copied())
7496 .unwrap_or(self.num_heads)
7497 }
7498
7499 fn layer_rope_scale(&self, li: usize) -> f32 {
7500 if self.layer_is_local(li) {
7501 self.rope_scale_local
7502 } else {
7503 self.rope_scale
7504 }
7505 }
7506
7507 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7510 if !self.layer_is_local(li) {
7511 if let Some((ghd, gkv)) = self.global_attn {
7512 return (gkv, ghd, ghd);
7513 }
7514 }
7515 (
7516 self.num_kv_heads,
7517 self.head_dim,
7518 if self.layer_is_local(li) {
7519 self.rotary_dim_local.unwrap_or(self.rotary_dim)
7520 } else {
7521 self.rotary_dim
7522 },
7523 )
7524 }
7525
7526 fn forward_layers(
7528 &mut self,
7529 hidden: &[f32],
7530 position: usize,
7531 task_mask: Option<&TaskMask>,
7532 ) -> Vec<f32> {
7533 let out = self.forward_layers_upto(hidden, position, task_mask, None);
7534 self.o1_progress();
7535 out
7536 }
7537
7538 pub fn embed_id(&self, id: u32) -> Vec<f32> {
7546 self.embed_single(id)
7547 }
7548
7549 pub fn split_supported(&self) -> Result<(), String> {
7553 if self.dsv4.is_some() {
7554 return Err(
7555 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7556 );
7557 }
7558 if self.dsv41.is_some() {
7559 return Err(
7560 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7561 .into(),
7562 );
7563 }
7564 if self.qwen4_exp.is_some() {
7565 return Err(
7566 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7567 );
7568 }
7569 if self.g3n.is_some() {
7570 return Err(
7571 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7572 );
7573 }
7574 Ok(())
7575 }
7576
7577 pub fn forward_span(
7582 &mut self,
7583 hidden: &[f32],
7584 position: usize,
7585 from: usize,
7586 upto: usize,
7587 task_mask: Option<&TaskMask>,
7588 ) -> Result<Vec<f32>, String> {
7589 self.split_supported()?;
7590 if from > upto || upto >= self.num_layers {
7591 return Err(format!(
7592 "forward_span: layer range {from}..={upto} outside 0..{}",
7593 self.num_layers
7594 ));
7595 }
7596 if hidden.len() != self.hidden_size {
7597 return Err(format!(
7598 "forward_span: hidden len {} ≠ hidden_size {}",
7599 hidden.len(),
7600 self.hidden_size
7601 ));
7602 }
7603 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7604 self.o1_progress();
7605 if self
7606 .graph_failed
7607 .swap(false, std::sync::atomic::Ordering::Relaxed)
7608 {
7609 self.cancel
7610 .store(false, std::sync::atomic::Ordering::Relaxed);
7611 self.clear_sequence_state();
7612 return Err("forward_span: deferred O(1) transition failed".into());
7613 }
7614 Ok(out)
7615 }
7616
7617 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7620 let normed = inference::rms_norm(
7621 hidden,
7622 &self.weights.final_norm,
7623 self.rms_eps,
7624 self.norm_style,
7625 );
7626 self.lm_head_forward(&normed)
7627 }
7628
7629 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7631 sampler::sample_with_scratch(
7632 logits,
7633 &self.sampler_config,
7634 past_tokens,
7635 &mut self.rng,
7636 &mut self.sampler_scratch,
7637 )
7638 }
7639
7640 pub fn reset_session(&mut self) {
7642 self.clear_sequence_state();
7643 }
7644
7645 pub fn prefill_span_ids(
7651 &mut self,
7652 ids: &[u32],
7653 start_pos: usize,
7654 upto: usize,
7655 task_mask: Option<&TaskMask>,
7656 ) -> Result<Vec<f32>, String> {
7657 self.split_supported()?;
7658 if upto >= self.num_layers {
7659 return Err(format!(
7660 "prefill_span_ids: upto {upto} outside 0..{}",
7661 self.num_layers
7662 ));
7663 }
7664 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7668 let out =
7669 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7670 self.check_o1_progress_failure("prefill_span_ids")?;
7671 Ok(out)
7672 } else {
7673 let hs = self.hidden_size;
7674 let mut out = Vec::with_capacity(ids.len() * hs);
7675 for (i, &id) in ids.iter().enumerate() {
7676 let emb = self.embed_id(id);
7677 out.extend_from_slice(&self.forward_span(
7678 &emb,
7679 start_pos + i,
7680 0,
7681 upto,
7682 task_mask,
7683 )?);
7684 }
7685 Ok(out)
7686 }
7687 }
7688
7689 pub fn prefill_span_hidden(
7692 &mut self,
7693 hidden: &[f32],
7694 start_pos: usize,
7695 from: usize,
7696 upto: usize,
7697 task_mask: Option<&TaskMask>,
7698 ) -> Result<Vec<f32>, String> {
7699 self.split_supported()?;
7700 let hs = self.hidden_size;
7701 if hidden.is_empty() || hidden.len() % hs != 0 {
7702 return Err(format!(
7703 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7704 hidden.len()
7705 ));
7706 }
7707 if from > upto || upto >= self.num_layers {
7708 return Err(format!(
7709 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7710 self.num_layers
7711 ));
7712 }
7713 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7714 let out = self.prefill_batch_span(
7715 PrefillIn::Hidden(hidden),
7716 start_pos,
7717 task_mask,
7718 from,
7719 upto + 1,
7720 );
7721 self.check_o1_progress_failure("prefill_span_hidden")?;
7722 Ok(out)
7723 } else {
7724 let b = hidden.len() / hs;
7725 let mut out = Vec::with_capacity(hidden.len());
7726 for i in 0..b {
7727 let h = self.forward_span(
7728 &hidden[i * hs..(i + 1) * hs],
7729 start_pos + i,
7730 from,
7731 upto,
7732 task_mask,
7733 )?;
7734 out.extend_from_slice(&h);
7735 }
7736 Ok(out)
7737 }
7738 }
7739
7740 fn try_token_graph_wgpu(
7744 &self,
7745 hidden: &[f32],
7746 position: usize,
7747 logits_out: &mut Vec<f32>,
7748 layers_run: &mut usize,
7749 ) -> Option<Result<Vec<f32>, ()>> {
7750 self.try_token_graph_wgpu_steps(
7751 hidden,
7752 position,
7753 logits_out,
7754 1,
7755 None,
7756 Some(layers_run),
7757 0,
7758 self.num_layers,
7759 )
7760 }
7761
7762 fn try_token_graph_wgpu_span(
7766 &self,
7767 hidden: &[f32],
7768 position: usize,
7769 logits_out: &mut Vec<f32>,
7770 from: usize,
7771 upto_excl: usize,
7772 layers_run: &mut usize,
7773 ) -> Option<Result<Vec<f32>, ()>> {
7774 self.try_token_graph_wgpu_steps(
7775 hidden,
7776 position,
7777 logits_out,
7778 1,
7779 None,
7780 Some(layers_run),
7781 from,
7782 upto_excl,
7783 )
7784 }
7785
7786 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7790 if self.o1_active() || self.attn_softcap > 0.0 {
7791 return None;
7792 }
7793 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7794 if !graph_on || crate::gpu::graph_unsupported() {
7795 return None;
7802 }
7803 let emb = self.embed_single(t_next);
7804 let mut lg = Vec::new();
7805 let mut ids = Vec::new();
7806 match self.try_token_graph_wgpu_steps(
7807 &emb,
7808 position,
7809 &mut lg,
7810 k,
7811 Some(&mut ids),
7812 None,
7813 0,
7814 self.num_layers,
7815 ) {
7816 Some(Ok(_)) => {}
7817 Some(Err(())) => {
7818 self.graph_failed
7823 .store(true, std::sync::atomic::Ordering::Relaxed);
7824 return None;
7825 }
7826 None => return None,
7827 }
7828 (ids.len() == k).then_some(ids)
7829 }
7830
7831 fn try_token_graph_wgpu_steps(
7835 &self,
7836 hidden: &[f32],
7837 position: usize,
7838 logits_out: &mut Vec<f32>,
7839 steps: usize,
7840 ids_out: Option<&mut Vec<u32>>,
7841 layers_run: Option<&mut usize>,
7842 from: usize,
7843 upto_excl: usize,
7844 ) -> Option<Result<Vec<f32>, ()>> {
7845 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7848 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7849 return None;
7853 }
7854 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7859 .map(|li| {
7860 if !o1_gpu {
7861 return None;
7862 }
7863 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7864 })
7865 .collect();
7866 if self.o1_active() && o1_gpu {
7867 let want: usize = (from..upto_excl)
7870 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7871 .count();
7872 let have = o1_views.iter().filter(|v| v.is_some()).count();
7873 if want == 0 || have != want {
7874 use std::sync::atomic::{AtomicUsize, Ordering};
7884 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7885 let code = have * 1000 + want;
7886 if LAST.swap(code, Ordering::Relaxed) != code {
7887 tracing::warn!(
7888 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7889 );
7890 }
7891 return None;
7892 }
7893 }
7894 let nh = self.num_heads;
7895 let (nkv, hd, rd) = self.layer_geom(0);
7896 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7897 let mut layers = Vec::with_capacity(upto_excl - from);
7898 let mut model = None;
7899 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7900 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7901 if let Some((m, i, kind, rs)) = t
7902 .graph_weight()
7903 .or_else(|| t.graph_weight_descriptor())
7904 {
7905 let name = &m.tensors[i].name;
7906 let prism = if crate::prism::is_inverse_embedding(m, name) {
7907 crate::gpu::GraphPrismOp::InverseEmbedding
7908 } else if crate::prism::is_forward_weight(m, name) {
7909 crate::gpu::GraphPrismOp::Forward
7910 } else {
7911 crate::gpu::GraphPrismOp::None
7912 };
7913 return Some(crate::gpu::GraphW {
7914 idx: i,
7915 kind,
7916 row_scale: rs,
7917 data: &[],
7918 prism,
7919 affine: crate::prism::is_affine_target(m, name),
7920 });
7921 }
7922 match t.as_f32() {
7924 Some(d) => Some(crate::gpu::GraphW {
7925 idx: 0,
7926 kind: 4,
7927 row_scale: &[],
7928 data: d,
7929 prism: crate::gpu::GraphPrismOp::None,
7930 affine: false,
7931 }),
7932 None => {
7933 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
7934 eprintln!("batch graph: weight has no graph/f32 representation");
7935 }
7936 None
7937 }
7938 }
7939 }
7940 for li in from..upto_excl {
7941 let lw = &self.weights.layers[self.phys_layer(li)];
7942 if dbg {
7943 let ak = match &lw.attn {
7944 AttnKind::Mla(_) => "Mla".into(),
7945 AttnKind::Full {
7946 output_gate, bias, ..
7947 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7948 AttnKind::LinearGdn(_) => "LinearGdn".into(),
7949 AttnKind::Kda(_) => "Kda".into(),
7950 AttnKind::Linear(_) => "Linear".into(),
7951 AttnKind::ShortConv(_) => "ShortConv".into(),
7952 };
7953 let fk = match &lw.ffn {
7954 FfnKind::Dense(_) => "Dense",
7955 FfnKind::Moe(_) => "Moe",
7956 FfnKind::DenseMoe(_) => "DenseMoe",
7957 };
7958 eprintln!("graph L{li}: attn={ak} ffn={fk}");
7959 }
7960 let gffn = match &lw.ffn {
7961 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7965 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7966 gate: gw(&d.gate_proj)?,
7967 up: gw(&d.up_proj)?,
7968 down: gw(&d.down_proj)?,
7969 },
7970 FfnKind::Moe(m) => {
7971 if m.route_tau.is_some() || m.mask.is_some() {
7979 return None;
7980 }
7981 let shared = m.shared.as_ref();
7982 let has_shared = shared.is_some();
7983 let shared_gated = matches!(shared, Some((_, Some(_))));
7984 let sgate = match shared {
7985 Some((_, Some(sg))) => gw(sg)?,
7986 _ => gw(&m.router)?,
7990 };
7991 let router = gw(&m.router)?;
7992 if router.prism != crate::gpu::GraphPrismOp::None
7998 || sgate.prism != crate::gpu::GraphPrismOp::None
7999 || router.affine
8000 || sgate.affine
8001 {
8002 tracing::warn!(
8003 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
8004 );
8005 return None;
8006 }
8007 let inter = m.experts.first()?.gate_proj.rows();
8008 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8009 let mut q4tp: Option<bool> = None;
8012 let mut gu_q2: Option<bool> = None;
8015 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
8016 if !matches!(e.act, Act::Silu)
8017 || e.gate_proj.rows() != inter
8018 || e.up_proj.rows() != inter
8019 {
8020 return None;
8021 }
8022 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
8027 let Some((em, ei, _, _)) = expert_weight
8028 .graph_weight()
8029 .or_else(|| expert_weight.graph_weight_descriptor())
8030 else {
8031 return None;
8032 };
8033 let name = &em.tensors[ei].name;
8034 if crate::prism::is_forward_weight(em, name)
8035 || crate::prism::is_inverse_embedding(em, name)
8036 || crate::prism::is_affine_target(em, name)
8037 {
8038 tracing::warn!(
8039 "resident MoE declined: expert Prism/affine transform is not implemented"
8040 );
8041 return None;
8042 }
8043 }
8044 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8045 Some((mm, gi)) => (
8046 mm,
8047 gi,
8048 e.up_proj.mapped_q4t()?.1,
8049 e.down_proj.mapped_q4t()?.1,
8050 false,
8051 false,
8052 ),
8053 None => match e.gate_proj.mapped_q2tp() {
8054 Some((mm, gi)) => (
8055 mm,
8056 gi,
8057 e.up_proj.mapped_q2tp()?.1,
8058 e.down_proj.mapped_q4tp()?.1,
8059 true,
8060 true,
8061 ),
8062 None => {
8063 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8064 (
8065 mm,
8066 gi,
8067 e.up_proj.mapped_q4tp()?.1,
8068 e.down_proj.mapped_q4tp()?.1,
8069 true,
8070 false,
8071 )
8072 }
8073 },
8074 };
8075 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
8076 {
8077 tracing::warn!(
8083 "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."
8084 );
8085 return None;
8086 }
8087 model.get_or_insert_with(|| mm.clone());
8088 experts.push((gi, ui, di));
8089 }
8090 crate::gpu::GraphFfn::Moe {
8091 router,
8092 shared_gate: sgate,
8093 experts,
8094 n_exp: m.experts.len(),
8095 top_k: std::env::var("CMF_TOPK_PROBE")
8101 .ok()
8102 .and_then(|v| v.parse::<usize>().ok())
8103 .filter(|k| *k > 0 && *k <= m.top_k)
8104 .unwrap_or(m.top_k),
8105 inter,
8106 norm_topk: m.norm_topk_prob,
8107 q4tp: q4tp?,
8108 gu_q2: gu_q2.unwrap_or(false),
8109 sigmoid: m.router_sigmoid,
8110 bias: m.expert_bias.as_deref(),
8111 has_shared,
8112 shared_gated,
8113 route_scale: m.routed_scaling,
8114 }
8115 }
8116 };
8117 let attn = match &lw.attn {
8118 AttnKind::Full {
8119 wq,
8120 wk,
8121 wv,
8122 wo,
8123 q_norm,
8124 k_norm,
8125 output_gate,
8126 softplus_gate,
8127 bias,
8128 } => {
8129 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8130 return None;
8131 }
8132 let (m, _, _, _) = wq
8133 .graph_weight()
8134 .or_else(|| wq.graph_weight_descriptor())?;
8135 model = Some(m.clone());
8136 crate::gpu::GraphAttn::Full {
8137 wq: gw(wq)?,
8138 wk: gw(wk)?,
8139 wv: gw(wv)?,
8140 wo: gw(wo)?,
8141 q_norm: q_norm.as_deref(),
8142 k_norm: k_norm.as_deref(),
8143 late_qk_norm: self.qk_norm_after_rope,
8144 bias: bias
8145 .as_ref()
8146 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8147 output_gate: *output_gate,
8148 cpu_k: self.kv_cache.layers[li].k_heads(),
8149 cpu_v: self.kv_cache.layers[li].v_heads(),
8150 }
8151 }
8152 AttnKind::LinearGdn(w) => {
8153 let cfg = self.gdn_cfg?;
8154 let (m, _, _, _) = w
8155 .in_proj_qkv
8156 .graph_weight()
8157 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
8158 model = Some(m.clone());
8159 crate::gpu::GraphAttn::Gdn {
8160 qkv: gw(&w.in_proj_qkv)?,
8161 z: gw(&w.in_proj_z)?,
8162 a: gw(&w.in_proj_a)?,
8163 b: gw(&w.in_proj_b)?,
8164 out: gw(&w.out_proj)?,
8165 conv1d: &w.conv1d,
8166 a_log: &w.a_log,
8167 dt_bias: &w.dt_bias,
8168 norm: &w.norm,
8169 nv: cfg.num_v_heads,
8170 nk: cfg.num_k_heads,
8171 dk: cfg.key_head_dim,
8172 dv: cfg.value_head_dim,
8173 kk: cfg.conv_kernel,
8174 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8175 }
8176 }
8177 AttnKind::ShortConv(w) => {
8178 let cfg = self.short_conv_cfg?;
8179 let (m, _, _, _) = w
8180 .in_proj
8181 .graph_weight()
8182 .or_else(|| w.in_proj.graph_weight_descriptor())?;
8183 model = Some(m.clone());
8184 crate::gpu::GraphAttn::ShortConv {
8185 inp: gw(&w.in_proj)?,
8186 out: gw(&w.out_proj)?,
8187 taps: &w.conv,
8188 kernel: cfg.kernel,
8189 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8190 }
8191 }
8192 _ => return None,
8193 };
8194 layers.push(crate::gpu::GraphLayer {
8195 input_norm: &lw.input_norm,
8196 attn,
8197 post_norm: &lw.post_norm,
8198 ffn: gffn,
8199 });
8200 }
8201 let model = model?;
8202 let lm_gw = if upto_excl == self.num_layers
8208 && self.graph_want_logits
8209 && std::env::var("CMF_GPU_LMHEAD")
8210 .map(|v| v != "0")
8211 .unwrap_or(true)
8212 {
8213 self.weights
8214 .lm_head
8215 .graph_weight()
8216 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
8217 .map(|(m, i, kind, rs)| {
8218 let name = &m.tensors[i].name;
8219 let prism = if crate::prism::is_inverse_embedding(m, name) {
8220 crate::gpu::GraphPrismOp::InverseEmbedding
8221 } else if crate::prism::is_forward_weight(m, name) {
8222 crate::gpu::GraphPrismOp::Forward
8223 } else {
8224 crate::gpu::GraphPrismOp::None
8225 };
8226 (
8227 crate::gpu::GraphW {
8228 idx: i,
8229 kind,
8230 row_scale: rs,
8231 data: &[],
8232 prism,
8233 affine: crate::prism::is_affine_target(m, name),
8234 },
8235 self.weights.lm_head.rows(),
8236 )
8237 })
8238 } else {
8239 None
8240 };
8241 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
8242 let emb_gw = if steps > 1 {
8244 self.weights
8245 .embed_tokens
8246 .graph_weight()
8247 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
8248 .map(|(m, i, kind, rs)| {
8249 let name = &m.tensors[i].name;
8250 let prism = if crate::prism::is_inverse_embedding(m, name) {
8251 crate::gpu::GraphPrismOp::InverseEmbedding
8252 } else if crate::prism::is_forward_weight(m, name) {
8253 crate::gpu::GraphPrismOp::Forward
8254 } else {
8255 crate::gpu::GraphPrismOp::None
8256 };
8257 (
8258 crate::gpu::GraphW {
8259 idx: i,
8260 kind,
8261 row_scale: rs,
8262 data: &[],
8263 prism,
8264 affine: crate::prism::is_affine_target(m, name),
8265 },
8266 self.weights.embed_tokens.rows(),
8267 self.embed_multiplier,
8268 )
8269 })
8270 } else {
8271 None
8272 };
8273
8274 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
8280 (from..upto_excl.min(self.num_layers - 1))
8281 .filter(|&li| (li + 1) % self.physical_layers == 0)
8282 .map(|li| li - from)
8283 .collect()
8284 } else {
8285 Vec::new()
8286 };
8287 let mut h = hidden.to_vec();
8288 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
8294 let outcome = crate::gpu::forward_token_graph(
8295 &model,
8296 self.graph_kv_id,
8297 &layers,
8298 &o1_views,
8299 self.o1_epoch,
8300 &self.inv_freq,
8301 &mut h,
8302 nh,
8303 nkv,
8304 hd,
8305 self.attn_scale,
8306 rd,
8307 self.hidden_size,
8308 self.intermediate_size,
8309 position,
8310 self.kv_cache.max_seq_len,
8311 gemma,
8312 self.rms_eps as f32,
8313 lm,
8314 &self.weights.final_norm,
8315 logits_out,
8316 &loop_norm_at,
8317 steps,
8318 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8319 ids_out,
8320 layers_run,
8321 from,
8322 dump_hidden,
8323 );
8324 match outcome {
8325 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8326 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8327 crate::gpu::TokenGraphOutcome::Declined => None,
8328 }
8329 }
8330
8331 #[cfg(target_os = "macos")]
8340 #[allow(clippy::type_complexity)]
8341 fn metal_rows_plan(
8342 &self,
8343 ) -> Option<(
8344 Vec<MetalRowsItem<'_>>,
8345 std::sync::Arc<cortiq_core::CmfModel>,
8346 Option<crate::gpu_metal::GdnGpuCfg>,
8347 )> {
8348 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8349 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
8350 if !graph_force
8351 || !crate::gpu::enabled_here()
8352 || std::env::var("CMF_GPU_BLOCK")
8353 .map(|v| v == "0")
8354 .unwrap_or(false)
8355 || self.attn_softcap > 0.0
8356 || self.o1_active()
8357 || self.swa.is_some()
8358 || self.global_attn.is_some()
8359 || self.attention_heads_per_layer.is_some()
8360 || self.attn_v_norm
8361 || self.loop_final_norm
8362 {
8363 return None;
8364 }
8365 let attend_contract = self.head_dim % 4 == 0
8366 && self.head_dim <= 256
8367 && self.rotary_dim >= 2
8368 && self.rotary_dim <= self.head_dim
8369 && (self.rotary_dim / 2) % 32 == 0
8370 && self.num_kv_heads > 0
8371 && self.num_heads % self.num_kv_heads == 0;
8372 if !attend_contract {
8373 return None;
8374 }
8375 let mut plan: Vec<MetalRowsItem> = Vec::new();
8376 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8377 for li in 0..self.num_layers {
8378 let lw = &self.weights.layers[self.phys_layer(li)];
8379 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8380 return None;
8381 }
8382 let ffn = match &lw.ffn {
8383 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8384 let (Some(g), Some(u), Some(dn)) = (
8385 d.gate_proj.metal_graph_parts(),
8386 d.up_proj.metal_graph_parts(),
8387 d.down_proj.metal_graph_parts(),
8388 ) else {
8389 return None;
8390 };
8391 MetalFfn::Dense {
8392 gate: g,
8393 up: u,
8394 down: dn,
8395 }
8396 }
8397 _ => return None,
8398 };
8399 match &lw.attn {
8400 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8401 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8402 w.in_proj_qkv.metal_graph_parts(),
8403 w.in_proj_z.metal_graph_parts(),
8404 w.in_proj_a.f32_parts(),
8405 w.in_proj_b.f32_parts(),
8406 w.out_proj.metal_graph_parts(),
8407 ) else {
8408 return None;
8409 };
8410 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8411 model_ref.get_or_insert_with(|| model.clone());
8412 }
8413 let gl = GdnGpuLayer {
8414 attn_norm: &lw.input_norm,
8415 post_norm: &lw.post_norm,
8416 qkv,
8417 z,
8418 a,
8419 b: bb,
8420 out,
8421 ffn,
8422 conv1d: &w.conv1d,
8423 a_log: &w.a_log,
8424 dt_bias: &w.dt_bias,
8425 gnorm: &w.norm,
8426 };
8427 match plan.last_mut() {
8428 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8429 _ => plan.push(MetalRowsItem::Gdn {
8430 run: vec![gl],
8431 first: li,
8432 }),
8433 }
8434 }
8435 AttnKind::Full {
8436 wq,
8437 wk,
8438 wv,
8439 wo,
8440 q_norm,
8441 k_norm,
8442 output_gate,
8443 softplus_gate: None,
8444 bias: None,
8445 } => {
8446 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8447 (
8448 wq.metal_graph_parts(),
8449 wk.metal_graph_parts(),
8450 wv.metal_graph_parts(),
8451 wo.metal_graph_parts(),
8452 )
8453 else {
8454 return None;
8455 };
8456 if let QTensor::Mapped { model, .. } = wq {
8457 model_ref.get_or_insert_with(|| model.clone());
8458 }
8459 let cache = &self.kv_cache.layers[li];
8460 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8461 return None;
8462 }
8463 plan.push(MetalRowsItem::Attn {
8464 l: AttnGpuLayer {
8465 attn_norm: &lw.input_norm,
8466 post_norm: &lw.post_norm,
8467 wq: pq,
8468 wk: pk,
8469 wv: pv,
8470 wo: po,
8471 ffn,
8472 },
8473 li,
8474 q_norm: q_norm.as_deref(),
8475 k_norm: k_norm.as_deref(),
8476 output_gate: *output_gate,
8477 });
8478 }
8479 _ => return None,
8480 }
8481 }
8482 let model = model_ref?;
8483 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8484 nv: cfg.num_v_heads,
8485 nk: cfg.num_k_heads,
8486 dk: cfg.key_head_dim,
8487 dv: cfg.value_head_dim,
8488 kk: cfg.conv_kernel,
8489 hidden: self.hidden_size,
8490 inter: self.intermediate_size,
8491 c_dim: cfg.conv_dim(),
8492 eps: cfg.rms_eps as f32,
8493 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8494 });
8495 Some((plan, model, gcfg))
8496 }
8497
8498 #[cfg(target_os = "macos")]
8500 #[allow(clippy::too_many_arguments)]
8501 fn metal_attn_params<'a>(
8502 li: usize,
8503 cache: &'a crate::kv_cache::LayerKvCache,
8504 q_norm: Option<&'a [f32]>,
8505 k_norm: Option<&'a [f32]>,
8506 output_gate: bool,
8507 inv_freq: &'a [f32],
8508 geom: (usize, usize, usize, usize),
8509 pos0: usize,
8510 kv_id: u64,
8511 scale: f32,
8512 eps: f32,
8513 gemma: bool,
8514 late_qk_norm: bool,
8515 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8516 let (nh, nkv, hd, rd) = geom;
8517 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8518 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8519 let cpu_stored = cpu_k[0].len() / hd;
8520 (
8521 crate::gpu_metal::AttnDeviceParams {
8522 kv_id,
8523 layer: li,
8524 nh,
8525 nkv,
8526 hd,
8527 rd,
8528 position: pos0,
8529 scale,
8530 eps,
8531 gemma,
8532 late_qk_norm,
8533 output_gate,
8534 q_norm,
8535 k_norm,
8536 inv_freq,
8537 cpu_k,
8538 cpu_v,
8539 cpu_stored,
8540 o1: None,
8541 },
8542 cpu_stored,
8543 )
8544 }
8545
8546 #[cfg(target_os = "macos")]
8551 #[allow(clippy::type_complexity)]
8552 fn metal_rows_run(
8553 &mut self,
8554 hiddens: &mut [f32],
8555 pos0: usize,
8556 b: usize,
8557 prefill: bool,
8558 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8559 ) -> MetalRowsRun {
8560 use crate::gpu_metal::{GraphDims, VerifyGraph};
8561 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8562 for l in &mut self.kv_cache.layers {
8563 if l.linear_state.len() != want && want > 0 {
8564 l.linear_state = vec![0f32; want];
8565 }
8566 }
8567 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
8568 return MetalRowsRun::Declined;
8569 };
8570 let dims = GraphDims {
8571 hidden: self.hidden_size,
8572 eps: self.rms_eps as f32,
8573 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8574 };
8575 let Some(mut graph) = (if prefill {
8576 VerifyGraph::new_prefill(&model, dims, hiddens, b)
8577 } else {
8578 VerifyGraph::new(&model, dims, hiddens, b)
8579 }) else {
8580 return MetalRowsRun::Declined;
8581 };
8582 let geom = (
8583 self.num_heads,
8584 self.num_kv_heads,
8585 self.head_dim,
8586 self.rotary_dim,
8587 );
8588 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8589 let eps = self.rms_eps as f32;
8590 let kv_id = self.graph_kv_id;
8591 let inv_freq = self.inv_freq.clone();
8592 for item in &plan {
8593 let ok = match item {
8594 MetalRowsItem::Gdn { run, .. } => gcfg
8595 .as_ref()
8596 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8597 .unwrap_or(false),
8598 MetalRowsItem::Attn {
8599 l,
8600 li,
8601 q_norm,
8602 k_norm,
8603 output_gate,
8604 } => {
8605 let (p, _) = Self::metal_attn_params(
8606 *li,
8607 &self.kv_cache.layers[*li],
8608 *q_norm,
8609 *k_norm,
8610 *output_gate,
8611 &inv_freq,
8612 geom,
8613 pos0,
8614 kv_id,
8615 self.attn_scale,
8616 eps,
8617 gemma,
8618 self.qk_norm_after_rope,
8619 );
8620 graph.attn_ok(l, &p)
8621 }
8622 };
8623 if !ok {
8624 use std::sync::atomic::{AtomicBool, Ordering};
8625 static SAID: AtomicBool = AtomicBool::new(false);
8626 if !SAID.swap(true, Ordering::Relaxed) {
8627 tracing::warn!("metal rows graph: a layer failed preflight — declining");
8628 }
8629 return MetalRowsRun::Declined;
8630 }
8631 }
8632 let lm = match &spec {
8633 Some((lm, _, _)) => {
8634 if !graph.lm_head_ok(*lm) {
8635 return MetalRowsRun::Declined;
8636 }
8637 Some(*lm)
8638 }
8639 None => None,
8640 };
8641 let mut gdn_layers = Vec::new();
8642 let mut attn_layers = Vec::new();
8643 for item in &plan {
8644 match item {
8645 MetalRowsItem::Gdn { run, first } => {
8646 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8647 .iter()
8648 .map(|l| l.linear_state.as_slice())
8649 .collect();
8650 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8651 return MetalRowsRun::Declined;
8652 }
8653 gdn_layers.extend(*first..*first + run.len());
8654 }
8655 MetalRowsItem::Attn {
8656 l,
8657 li,
8658 q_norm,
8659 k_norm,
8660 output_gate,
8661 } => {
8662 let (p, cpu_stored) = Self::metal_attn_params(
8663 *li,
8664 &self.kv_cache.layers[*li],
8665 *q_norm,
8666 *k_norm,
8667 *output_gate,
8668 &inv_freq,
8669 geom,
8670 pos0,
8671 kv_id,
8672 self.attn_scale,
8673 eps,
8674 gemma,
8675 self.qk_norm_after_rope,
8676 );
8677 if !graph.encode_attn_b(l, &p) {
8678 return MetalRowsRun::Declined;
8679 }
8680 attn_layers.push((*li, cpu_stored));
8681 }
8682 }
8683 }
8684 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8685 if !graph.encode_lm_head_b(final_norm, lm) {
8686 return MetalRowsRun::Declined;
8687 }
8688 }
8689 if !graph.sync() {
8690 return MetalRowsRun::Failed;
8691 }
8692 if let Some((lm, _, logits)) = spec {
8693 logits.resize(b * lm.1, 0.0);
8694 if !graph.read_logits(logits) {
8695 return MetalRowsRun::Failed;
8696 }
8697 }
8698 if !graph.read_hidden(hiddens) {
8699 return MetalRowsRun::Failed;
8700 }
8701 MetalRowsRun::Completed(MetalVerifyPending {
8702 graph,
8703 gdn_layers,
8704 attn_layers,
8705 })
8706 }
8707
8708 #[cfg(target_os = "macos")]
8714 fn try_batch_graph_metal(
8715 &mut self,
8716 hiddens: &mut [f32],
8717 positions: &[usize],
8718 b: usize,
8719 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8720 ) -> crate::gpu::BatchGraphOutcome {
8721 let _t0 = std::time::Instant::now();
8722 if positions.len() != b
8723 || positions.windows(2).any(|w| w[1] != w[0] + 1)
8724 || hiddens.len() != b * self.hidden_size
8725 {
8726 return crate::gpu::BatchGraphOutcome::Declined;
8727 }
8728 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec) {
8729 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
8730 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
8731 MetalRowsRun::Completed(pending) => pending,
8732 };
8733 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8734 eprintln!(
8735 "metal-verify: {:.1} ms | b={b}",
8736 _t0.elapsed().as_secs_f64() * 1e3
8737 );
8738 }
8739 self.metal_verify = Some(pending);
8740 crate::gpu::BatchGraphOutcome::Completed
8741 }
8742
8743 #[cfg(target_os = "macos")]
8748 fn prefill_rows_metal(
8749 &mut self,
8750 ids: &[u32],
8751 start_pos: usize,
8752 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8753 ) -> MetalPrefillOutcome {
8754 let b = ids.len();
8755 if b == 0 || b > 512 {
8756 return MetalPrefillOutcome::Declined;
8757 }
8758 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8759 let with_head = spec.is_some();
8760 let hs = self.hidden_size;
8761 let mut hiddens = vec![0f32; b * hs];
8762 for (j, &id) in ids.iter().enumerate() {
8763 let e = self.embed_single(id);
8764 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8765 }
8766 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec) {
8767 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
8768 MetalRowsRun::Failed => {
8769 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8770 return MetalPrefillOutcome::Failed;
8771 }
8772 MetalRowsRun::Completed(pending) => pending,
8773 };
8774 let idxs = pending.gdn_layers.clone();
8776 let mut outs: Vec<&mut [f32]> = self
8777 .kv_cache
8778 .layers
8779 .iter_mut()
8780 .enumerate()
8781 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8782 .map(|(_, l)| l.linear_state.as_mut_slice())
8783 .collect();
8784 if !pending.graph.finish_states(&mut outs) {
8785 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8786 return MetalPrefillOutcome::Failed;
8787 }
8788 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8789 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8793 for (li, cpu_stored) in &pending.attn_layers {
8794 let mut kbuf = vec![0f32; b * nkv * hd];
8795 let mut vbuf = vec![0f32; b * nkv * hd];
8796 if !crate::gpu_metal::kv_mirror_read_rows(
8797 self.graph_kv_id,
8798 *li,
8799 nkv,
8800 hd,
8801 *cpu_stored,
8802 b,
8803 &mut kbuf,
8804 &mut vbuf,
8805 ) {
8806 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8807 return MetalPrefillOutcome::Failed;
8808 }
8809 rows.push((*li, *cpu_stored, kbuf, vbuf));
8810 }
8811 for (li, cpu_stored, kbuf, vbuf) in rows {
8812 let cache = &mut self.kv_cache.layers[li];
8813 for r in 0..b {
8814 cache.append(
8815 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8816 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8817 &[],
8818 );
8819 }
8820 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
8821 }
8822 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8823 if with_head {
8824 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
8825 }
8826 MetalPrefillOutcome::Completed(hiddens)
8827 }
8828
8829 #[cfg(target_os = "macos")]
8830 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
8831 self.prefill_rows_metal(ids, start_pos, None)
8832 }
8833
8834 #[cfg(target_os = "macos")]
8839 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
8840 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
8841 return MetalBatchNllOutcome::Declined;
8842 }
8843 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
8844 return MetalBatchNllOutcome::Declined;
8845 };
8846 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
8847 .ok()
8848 .and_then(|v| v.parse::<usize>().ok())
8849 .filter(|&v| (1..=512).contains(&v))
8850 .unwrap_or(32);
8851 let final_norm = self.weights.final_norm.clone();
8852 let mut nll = 0.0f64;
8853 let mut count = 0usize;
8854 let mut pos = 0usize;
8855 let mut completed = 0usize;
8856 while pos < ids.len() {
8857 let end = (pos + chunk).min(ids.len());
8858 let mut logits = Vec::new();
8859 let outcome = self.prefill_rows_metal(
8860 &ids[pos..end],
8861 pos,
8862 Some((lm, &final_norm, &mut logits)),
8863 );
8864 match outcome {
8865 MetalPrefillOutcome::Declined => {
8866 return if completed == 0 {
8867 MetalBatchNllOutcome::Declined
8868 } else {
8869 MetalBatchNllOutcome::Failed(format!(
8870 "ordinary Metal NLL batch declined after {completed} chunks"
8871 ))
8872 };
8873 }
8874 MetalPrefillOutcome::Failed => {
8875 return MetalBatchNllOutcome::Failed(
8876 "ordinary Metal NLL batch failed after admission".to_string(),
8877 );
8878 }
8879 MetalPrefillOutcome::Completed(_) => {}
8880 }
8881 completed += 1;
8882 let vocab = self.vocab_size.min(lm.1);
8883 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
8884 return MetalBatchNllOutcome::Failed(
8885 "ordinary Metal NLL head returned an invalid shape".to_string(),
8886 );
8887 }
8888 for row in 0..(end - pos) {
8889 let absolute = pos + row;
8890 if absolute < start || absolute + 1 >= ids.len() {
8891 continue;
8892 }
8893 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
8894 if let Some(mu) = self.logit_multiplier {
8895 for v in lg.iter_mut() {
8896 *v *= mu;
8897 }
8898 }
8899 if let Some(c) = self.final_softcap {
8900 for v in lg.iter_mut() {
8901 *v = c * (*v / c).tanh();
8902 }
8903 }
8904 let target = ids[absolute + 1] as usize;
8905 if target >= vocab {
8906 return MetalBatchNllOutcome::Failed(format!(
8907 "target token {target} exceeds Metal head rows {vocab}"
8908 ));
8909 }
8910 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8911 let lse: f64 = lg
8912 .iter()
8913 .map(|&v| ((v - max) as f64).exp())
8914 .sum::<f64>()
8915 .ln()
8916 + max as f64;
8917 nll += lse - lg[target] as f64;
8918 count += 1;
8919 }
8920 pos = end;
8921 }
8922 MetalBatchNllOutcome::Completed(nll, count)
8923 }
8924
8925 #[cfg(target_os = "macos")]
8929 fn metal_verify_commit(&mut self, a: usize) -> bool {
8930 let Some(mut pending) = self.metal_verify.take() else {
8931 return false;
8932 };
8933 let n = a + 1;
8934 let idxs = pending.gdn_layers.clone();
8936 let mut outs: Vec<&mut [f32]> = self
8937 .kv_cache
8938 .layers
8939 .iter_mut()
8940 .enumerate()
8941 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8942 .map(|(_, l)| l.linear_state.as_mut_slice())
8943 .collect();
8944 if !pending.graph.commit(n, &mut outs) {
8945 return false;
8946 }
8947 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8948 let mut rows = Vec::with_capacity(pending.attn_layers.len());
8952 for (li, cpu_stored) in &pending.attn_layers {
8953 let mut kbuf = vec![0f32; n * nkv * hd];
8954 let mut vbuf = vec![0f32; n * nkv * hd];
8955 if !crate::gpu_metal::kv_mirror_read_rows(
8956 self.graph_kv_id,
8957 *li,
8958 nkv,
8959 hd,
8960 *cpu_stored,
8961 n,
8962 &mut kbuf,
8963 &mut vbuf,
8964 ) {
8965 return false;
8966 }
8967 rows.push((*li, *cpu_stored, kbuf, vbuf));
8968 }
8969 for (li, cpu_stored, kbuf, vbuf) in rows {
8970 let cache = &mut self.kv_cache.layers[li];
8971 for r in 0..n {
8972 cache.append(
8973 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8974 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8975 &[],
8976 );
8977 }
8978 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
8979 }
8980 true
8981 }
8982
8983 #[cfg(target_os = "macos")]
8989 fn mtp_warm_batch_metal(
8990 &mut self,
8991 m: &mut MtpModule,
8992 pairs: &[(&[f32], u32)],
8993 first_pos: usize,
8994 ) -> bool {
8995 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
8996 let b = pairs.len();
8997 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
8998 return false;
8999 }
9000 let AttnKind::Full {
9001 wq,
9002 wk,
9003 wv,
9004 wo,
9005 q_norm,
9006 k_norm,
9007 output_gate,
9008 softplus_gate: None,
9009 bias: None,
9010 } = &m.layer.attn
9011 else {
9012 return false;
9013 };
9014 let FfnKind::Dense(d) = &m.layer.ffn else {
9015 return false;
9016 };
9017 if !d.segs.is_empty() {
9018 return false;
9019 }
9020 let (Some(pq), Some(pk), Some(pv), Some(po)) =
9021 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
9022 else {
9023 return false;
9024 };
9025 let (Some(g), Some(u), Some(dn)) = (
9026 d.gate_proj.q1_parts(),
9027 d.up_proj.q1_parts(),
9028 d.down_proj.q1_parts(),
9029 ) else {
9030 return false;
9031 };
9032 let Some(eh) = m.eh_proj.q1_parts() else {
9033 return false;
9034 };
9035 let QTensor::Mapped { model, .. } = wq else {
9036 return false;
9037 };
9038 let model = model.clone();
9039 let hs = self.hidden_size;
9040 let mut cat = vec![0f32; b * 2 * hs];
9042 for (j, (h, tok)) in pairs.iter().enumerate() {
9043 let e = self.embed_single(*tok);
9044 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
9045 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
9046 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
9047 }
9048 let dims = GraphDims {
9049 hidden: hs,
9050 eps: self.rms_eps as f32,
9051 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9052 };
9053 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
9054 return false;
9055 };
9056 let l = AttnGpuLayer {
9057 attn_norm: &m.layer.input_norm,
9058 post_norm: &m.layer.post_norm,
9059 wq: pq,
9060 wk: pk,
9061 wv: pv,
9062 wo: po,
9063 ffn: MetalFfn::Dense {
9064 gate: g,
9065 up: u,
9066 down: dn,
9067 },
9068 };
9069 let (nh, nkv, hd, rd) = (
9070 self.num_heads,
9071 self.num_kv_heads,
9072 self.head_dim,
9073 self.rotary_dim,
9074 );
9075 let inv_freq = self.inv_freq.clone();
9076 let cpu_stored;
9077 {
9078 let cache = &m.kv;
9079 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9080 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9081 cpu_stored = cpu_k[0].len() / hd;
9082 if cpu_stored != first_pos {
9083 return false;
9084 }
9085 let p = AttnDeviceParams {
9086 kv_id: self.mtp_kv_id(),
9087 layer: Self::MTP_LAYER_BASE,
9088 nh,
9089 nkv,
9090 hd,
9091 rd,
9092 position: first_pos,
9093 scale: self.attn_scale,
9094 eps: self.rms_eps as f32,
9095 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9096 late_qk_norm: self.qk_norm_after_rope,
9097 output_gate: *output_gate,
9098 q_norm: q_norm.as_deref(),
9099 k_norm: k_norm.as_deref(),
9100 inv_freq: &inv_freq,
9101 cpu_k,
9102 cpu_v,
9103 cpu_stored,
9104 o1: None,
9105 };
9106 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
9107 return false;
9108 }
9109 }
9110 if !graph.sync() {
9111 return false;
9112 }
9113 let mut kbuf = vec![0f32; b * nkv * hd];
9114 let mut vbuf = vec![0f32; b * nkv * hd];
9115 if !crate::gpu_metal::kv_mirror_read_rows(
9116 self.mtp_kv_id(),
9117 Self::MTP_LAYER_BASE,
9118 nkv,
9119 hd,
9120 cpu_stored,
9121 b,
9122 &mut kbuf,
9123 &mut vbuf,
9124 ) {
9125 return false;
9126 }
9127 for r in 0..b {
9128 m.kv.append(
9129 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
9130 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
9131 &[],
9132 );
9133 }
9134 crate::gpu_metal::kv_mirror_set_stored(
9135 self.mtp_kv_id(),
9136 Self::MTP_LAYER_BASE,
9137 cpu_stored + b,
9138 );
9139 true
9140 }
9141
9142 fn draft_vocab_rows(head_rows: usize) -> usize {
9145 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9146 let n = *N.get_or_init(|| {
9147 std::env::var("CMF_DRAFT_VOCAB")
9148 .ok()
9149 .and_then(|v| v.parse().ok())
9150 .unwrap_or(65536)
9151 });
9152 if n == 0 { head_rows } else { n.min(head_rows) }
9153 }
9154
9155 #[cfg(target_os = "macos")]
9160 fn mtp_step_metal(
9161 &mut self,
9162 m: &mut MtpModule,
9163 hidden: &[f32],
9164 next_token: u32,
9165 position: usize,
9166 want_logits: bool,
9167 ) -> Option<(Vec<f32>, Vec<f32>)> {
9168 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
9169 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9170 || !crate::gpu::q1_force()
9171 || !crate::gpu::enabled_here()
9172 || self.attn_softcap > 0.0
9173 || self.attention_heads_per_layer.is_some()
9174 || m.kv.mode != crate::kv_cache::KvMode::F32
9175 || m.kv.o1.is_some()
9176 {
9177 return None;
9178 }
9179 let AttnKind::Full {
9180 wq,
9181 wk,
9182 wv,
9183 wo,
9184 q_norm,
9185 k_norm,
9186 output_gate,
9187 softplus_gate: None,
9188 bias: None,
9189 } = &m.layer.attn
9190 else {
9191 return None;
9192 };
9193 let FfnKind::Dense(d) = &m.layer.ffn else {
9194 return None;
9195 };
9196 if d.act != Act::Silu || !d.segs.is_empty() {
9197 return None;
9198 }
9199 let (pq, pk, pv, po) = (
9200 wq.q1_parts()?,
9201 wk.q1_parts()?,
9202 wv.q1_parts()?,
9203 wo.q1_parts()?,
9204 );
9205 let (g, u, dn) = (
9206 d.gate_proj.q1_parts()?,
9207 d.up_proj.q1_parts()?,
9208 d.down_proj.q1_parts()?,
9209 );
9210 let QTensor::Mapped { model, .. } = wq else {
9211 return None;
9212 };
9213 let model = model.clone();
9214 let lm = if want_logits {
9215 Some(self.weights.lm_head.q1_parts()?)
9216 } else {
9217 None
9218 };
9219 let dims = GraphDims {
9220 hidden: self.hidden_size,
9221 eps: self.rms_eps as f32,
9222 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9223 };
9224 let hs = self.hidden_size;
9227 let mut x = vec![0f32; hs];
9228 let mut graph = TokenGraph::new(&model, dims, &x)?;
9229 let mut folded = false;
9230 if let Some(eh) = m.eh_proj.q1_parts() {
9231 let e = self.embed_single(next_token);
9232 let mut cat = vec![0.0f32; 2 * hs];
9233 let (cat_e, cat_h) = cat.split_at_mut(hs);
9234 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9235 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9236 folded = graph.encode_input_proj(eh, &cat);
9237 }
9238 if !folded {
9239 x = self.mtp_block_input(m, hidden, next_token);
9240 graph = TokenGraph::new(&model, dims, &x)?;
9241 }
9242 let l = AttnGpuLayer {
9243 attn_norm: &m.layer.input_norm,
9244 post_norm: &m.layer.post_norm,
9245 wq: pq,
9246 wk: pk,
9247 wv: pv,
9248 wo: po,
9249 ffn: MetalFfn::Dense {
9250 gate: g,
9251 up: u,
9252 down: dn,
9253 },
9254 };
9255 let (nh, nkv, hd, rd) = (
9256 self.num_heads,
9257 self.num_kv_heads,
9258 self.head_dim,
9259 self.rotary_dim,
9260 );
9261 let inv_freq = self.inv_freq.clone();
9262 {
9263 let cache = &m.kv;
9264 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
9265 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
9266 let cpu_stored = cpu_k[0].len() / hd;
9267 let p = AttnDeviceParams {
9268 kv_id: self.mtp_kv_id(),
9269 layer: Self::MTP_LAYER_BASE,
9270 nh,
9271 nkv,
9272 hd,
9273 rd,
9274 position,
9275 scale: self.attn_scale,
9276 eps: self.rms_eps as f32,
9277 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
9278 late_qk_norm: self.qk_norm_after_rope,
9279 output_gate: *output_gate,
9280 q_norm: q_norm.as_deref(),
9281 k_norm: k_norm.as_deref(),
9282 inv_freq: &inv_freq,
9283 cpu_k,
9284 cpu_v,
9285 cpu_stored,
9286 o1: None,
9287 };
9288 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
9289 return None;
9290 }
9291 }
9292 let draft_rows = if let Some(lm) = lm {
9298 Self::draft_vocab_rows(lm.1)
9299 } else {
9300 0
9301 };
9302 if let Some(lm) = lm {
9303 if !graph.lm_head_ok(lm) {
9304 return None;
9305 }
9306 if draft_rows < lm.1 {
9307 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
9308 return None;
9309 }
9310 } else {
9311 graph.encode_lm_head(&m.final_norm, lm);
9312 }
9313 }
9314 if graph.sync_checked().is_err() {
9315 return None;
9316 }
9317 let mut logits = Vec::new();
9318 if let Some(lm) = lm {
9319 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
9320 logits = attention::take_buf(n_read);
9321 graph.read_logits(&mut logits);
9322 logits.resize(self.vocab_size, f32::NEG_INFINITY);
9324 }
9325 graph.finish(&mut x);
9326 let mut krow = attention::take_buf(nkv * hd);
9327 let mut vrow = attention::take_buf(nkv * hd);
9328 if crate::gpu_metal::kv_mirror_read_last(
9329 self.mtp_kv_id(),
9330 Self::MTP_LAYER_BASE,
9331 nkv,
9332 hd,
9333 &mut krow,
9334 &mut vrow,
9335 ) {
9336 m.kv.append(&krow, &vrow, &[]);
9337 }
9338 attention::recycle_buf(&mut krow);
9339 attention::recycle_buf(&mut vrow);
9340 Some((logits, x))
9341 }
9342
9343 fn try_batch_graph_wgpu(
9344 &self,
9345 hiddens: &mut [f32],
9346 positions: &[usize],
9347 k: usize,
9348 spec: Option<crate::gpu::SpecTail<'_>>,
9349 ) -> crate::gpu::BatchGraphOutcome {
9350 let _tb = std::time::Instant::now();
9351 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
9352 if self.attn_softcap > 0.0 {
9353 return crate::gpu::BatchGraphOutcome::Declined; }
9355 let nh = self.num_heads;
9356 let (nkv, hd, rd) = self.layer_geom(0);
9357 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9358 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9359 if let Some((m, i, kind, rs)) = t
9360 .graph_weight()
9361 .or_else(|| t.graph_weight_descriptor())
9362 {
9363 let name = &m.tensors[i].name;
9364 let prism = if crate::prism::is_inverse_embedding(m, name) {
9365 crate::gpu::GraphPrismOp::InverseEmbedding
9366 } else if crate::prism::is_forward_weight(m, name) {
9367 crate::gpu::GraphPrismOp::Forward
9368 } else {
9369 crate::gpu::GraphPrismOp::None
9370 };
9371 return Some(crate::gpu::GraphW {
9372 idx: i,
9373 kind,
9374 row_scale: rs,
9375 data: &[],
9376 prism,
9377 affine: crate::prism::is_affine_target(m, name),
9378 });
9379 }
9380 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
9381 eprintln!(
9382 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
9383 t.rows(),
9384 t.cols()
9385 );
9386 }
9387 t.as_f32().map(|d| crate::gpu::GraphW {
9388 idx: 0,
9389 kind: 4,
9390 row_scale: &[],
9391 data: d,
9392 prism: crate::gpu::GraphPrismOp::None,
9393 affine: false,
9394 })
9395 }
9396 let built: Option<(
9397 Vec<crate::gpu::GraphLayer<'_>>,
9398 std::sync::Arc<cortiq_core::CmfModel>,
9399 )> = (|| {
9400 let mut layers = Vec::with_capacity(self.num_layers);
9401 let mut model = None;
9402 for li in 0..self.num_layers {
9403 let lw = &self.weights.layers[self.phys_layer(li)];
9404 let gffn = match &lw.ffn {
9411 FfnKind::Dense(d) if !d.segs.is_empty() => {
9412 if batch_debug {
9413 eprintln!("batch graph: dense segmented FFN at layer {li}");
9414 }
9415 return None;
9416 }
9417 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
9418 gate: gw(&d.gate_proj)?,
9419 up: gw(&d.up_proj)?,
9420 down: gw(&d.down_proj)?,
9421 },
9422 FfnKind::Moe(m) => {
9423 if m.router_sigmoid
9424 || m.expert_bias.is_some()
9425 || m.route_tau.is_some()
9426 || m.mask.is_some()
9427 {
9428 return None;
9429 }
9430 let (se, sg) = m.shared.as_ref()?;
9431 let sgate = gw(sg.as_ref()?)?;
9432 let router = gw(&m.router)?;
9433 if router.prism != crate::gpu::GraphPrismOp::None
9439 || router.affine
9440 || sgate.prism != crate::gpu::GraphPrismOp::None
9441 || sgate.affine
9442 {
9443 return None;
9444 }
9445 let inter = m.experts.first()?.gate_proj.rows();
9446 let mut experts = Vec::with_capacity(m.experts.len() + 1);
9447 let mut q4tp: Option<bool> = None;
9448 let mut gu_q2: Option<bool> = None;
9449 for e in m.experts.iter().chain(std::iter::once(se)) {
9450 if !matches!(e.act, Act::Silu)
9451 || e.gate_proj.rows() != inter
9452 || e.up_proj.rows() != inter
9453 {
9454 return None;
9455 }
9456 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
9460 Some((mm, gi)) => (
9461 mm,
9462 gi,
9463 e.up_proj.mapped_q4t()?.1,
9464 e.down_proj.mapped_q4t()?.1,
9465 false,
9466 false,
9467 ),
9468 None => match e.gate_proj.mapped_q2tp() {
9469 Some((mm, gi)) => (
9470 mm,
9471 gi,
9472 e.up_proj.mapped_q2tp()?.1,
9473 e.down_proj.mapped_q4tp()?.1,
9474 true,
9475 true,
9476 ),
9477 None => {
9478 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
9479 (
9480 mm,
9481 gi,
9482 e.up_proj.mapped_q4tp()?.1,
9483 e.down_proj.mapped_q4tp()?.1,
9484 true,
9485 false,
9486 )
9487 }
9488 },
9489 };
9490 if *q4tp.get_or_insert(is_p) != is_p
9491 || *gu_q2.get_or_insert(is_q2) != is_q2
9492 {
9493 return None;
9494 }
9495 if [gi, ui, di].into_iter().any(|idx| {
9496 mm.tensors
9497 .get(idx)
9498 .is_some_and(|t| {
9499 crate::prism::is_forward_weight(mm, &t.name)
9500 || crate::prism::is_affine_target(mm, &t.name)
9501 })
9502 }) {
9503 return None;
9504 }
9505 model.get_or_insert_with(|| mm.clone());
9506 experts.push((gi, ui, di));
9507 }
9508 crate::gpu::GraphFfn::Moe {
9509 router,
9510 shared_gate: sgate,
9511 experts,
9512 n_exp: m.experts.len(),
9513 top_k: m.top_k,
9514 inter,
9515 norm_topk: m.norm_topk_prob,
9516 q4tp: q4tp?,
9517 gu_q2: gu_q2.unwrap_or(false),
9518 sigmoid: false,
9519 bias: None,
9520 has_shared: true,
9521 shared_gated: true,
9522 route_scale: 1.0,
9523 }
9524 }
9525 _ => return None,
9526 };
9527 let attn = match &lw.attn {
9528 AttnKind::Full {
9529 wq,
9530 wk,
9531 wv,
9532 wo,
9533 q_norm,
9534 k_norm,
9535 output_gate,
9536 softplus_gate,
9537 bias,
9538 } => {
9539 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9540 if batch_debug {
9541 eprintln!(
9542 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
9543 softplus_gate.is_some(),
9544 self.attention_heads_per_layer.is_some()
9545 );
9546 }
9547 return None;
9548 }
9549 let (m, _, _, _) = wq
9550 .graph_weight()
9551 .or_else(|| wq.graph_weight_descriptor())?;
9552 model = Some(m.clone());
9553 crate::gpu::GraphAttn::Full {
9554 wq: gw(wq)?,
9555 wk: gw(wk)?,
9556 wv: gw(wv)?,
9557 wo: gw(wo)?,
9558 q_norm: q_norm.as_deref(),
9559 k_norm: k_norm.as_deref(),
9560 late_qk_norm: self.qk_norm_after_rope,
9561 bias: bias
9562 .as_ref()
9563 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9564 output_gate: *output_gate,
9565 cpu_k: self.kv_cache.layers[li].k_heads(),
9566 cpu_v: self.kv_cache.layers[li].v_heads(),
9567 }
9568 }
9569 AttnKind::LinearGdn(w) => {
9570 let Some(cfg) = self.gdn_cfg else {
9571 if batch_debug {
9572 eprintln!("batch graph: no GDN config at layer {li}");
9573 }
9574 return None;
9575 };
9576 let (m, _, _, _) = w
9577 .in_proj_qkv
9578 .graph_weight()
9579 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
9580 model = Some(m.clone());
9581 crate::gpu::GraphAttn::Gdn {
9582 qkv: gw(&w.in_proj_qkv)?,
9583 z: gw(&w.in_proj_z)?,
9584 a: gw(&w.in_proj_a)?,
9585 b: gw(&w.in_proj_b)?,
9586 out: gw(&w.out_proj)?,
9587 conv1d: &w.conv1d,
9588 a_log: &w.a_log,
9589 dt_bias: &w.dt_bias,
9590 norm: &w.norm,
9591 nv: cfg.num_v_heads,
9592 nk: cfg.num_k_heads,
9593 dk: cfg.key_head_dim,
9594 dv: cfg.value_head_dim,
9595 kk: cfg.conv_kernel,
9596 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9597 }
9598 }
9599 _ => return None,
9600 };
9601 layers.push(crate::gpu::GraphLayer {
9602 input_norm: &lw.input_norm,
9603 attn,
9604 post_norm: &lw.post_norm,
9605 ffn: gffn,
9606 });
9607 }
9608 Some((layers, model?))
9609 })();
9610 let Some((layers, model)) = built else {
9611 {
9612 use std::sync::atomic::{AtomicBool, Ordering};
9613 static SAID: AtomicBool = AtomicBool::new(false);
9614 if !SAID.swap(true, Ordering::Relaxed) {
9615 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9616 }
9617 }
9618 return crate::gpu::BatchGraphOutcome::Declined;
9619 };
9620 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9621 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9622 }
9623 crate::gpu::forward_batch_graph(
9624 &model,
9625 self.graph_kv_id,
9626 &layers,
9627 &self.inv_freq,
9628 hiddens,
9629 nh,
9630 nkv,
9631 hd,
9632 rd,
9633 self.hidden_size,
9634 self.intermediate_size,
9635 positions,
9636 self.kv_cache.max_seq_len,
9637 gemma,
9638 self.rms_eps as f32,
9639 self.attn_scale,
9640 k,
9641 &(0..self.num_layers)
9642 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9643 .collect::<Vec<_>>(),
9644 self.o1_epoch,
9645 spec,
9646 )
9647 }
9648
9649 fn draft_probe() -> bool {
9653 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9654 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9655 }
9656
9657 #[cfg(feature = "gpu")]
9669 fn dsv4_spec_on() -> bool {
9670 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9671 *ON.get_or_init(|| {
9672 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9676 return v != "0";
9677 }
9678 std::env::var("CMF_DSV4_SPEC")
9685 .map(|v| v != "0")
9686 .unwrap_or_else(|_| {
9687 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9688 })
9689 })
9690 }
9691
9692 #[cfg(feature = "gpu")]
9699 fn dsv4_spec_step(
9700 &mut self,
9701 tip_token: u32,
9702 t_next: u32,
9703 next_pos: usize,
9704 max_extra: usize,
9705 drafted: &mut usize,
9706 accepted_ctr: &mut usize,
9707 ) -> Option<(Vec<u32>, usize)> {
9708 let t_all = std::time::Instant::now();
9709 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9710 thread_local! {
9711 static LAST: std::cell::Cell<Option<std::time::Instant>> =
9712 const { std::cell::Cell::new(None) };
9713 }
9714 LAST.with(|l| {
9715 if let Some(prev) = l.get() {
9716 eprintln!(
9717 "между раундами {:.1} мс",
9718 prev.elapsed().as_secs_f64() * 1e3
9719 );
9720 }
9721 l.set(Some(std::time::Instant::now()));
9722 });
9723 }
9724 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9725 eprintln!("spec_step: вход pos={next_pos}");
9726 }
9727 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9728 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9729 if self.dspark.is_none() {
9731 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9732 if t.is_empty() {
9733 return None;
9734 }
9735 crate::dsv4::dspark_arm(&t, cfg.dim);
9736 self.dspark = Some(crate::dsv4::DsparkState::new(
9737 self.dsv4_mtp.len(),
9738 &cfg,
9739 t.len(),
9740 ));
9741 }
9742 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9743 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9744 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9745 eprintln!("spec_step: пак не построился (targets {targets:?})");
9746 }
9747 let pack = pack?;
9748 let block = crate::dsv4::dspark_block();
9749 let b_box = self.dsv4.as_mut()?;
9750 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9751 let ds = self.dspark.as_mut()?;
9752 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9755 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9756 if dbg {
9757 eprintln!("spec_step: нет захвата");
9758 }
9759 return None;
9760 }
9761 ds.have_hidden = true;
9762 let tip_pos = next_pos.checked_sub(1)?;
9763 let draft_started = std::time::Instant::now();
9764 let mut conf = Vec::new();
9765 let props = crate::dsv4::dspark_draft_gpu(
9766 g,
9767 &self.dsv4_mtp,
9768 &cfg,
9769 ds,
9770 pack,
9771 st.kv_id,
9772 tip_token,
9773 tip_pos,
9774 self.pool.as_deref(),
9775 &mut conf,
9776 );
9777 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9778 *drafted += block;
9779 if props.is_empty() || props[0] != t_next {
9780 if dbg {
9781 eprintln!(
9782 "spec_step: черновик {} (props0={:?} t_next={t_next})",
9783 if props.is_empty() {
9784 "пуст"
9785 } else {
9786 "мимо"
9787 },
9788 props.first()
9789 );
9790 }
9791 return None;
9792 }
9793 let mut k_verify = crate::dsv4::dspark_verify_k()
9800 .min(props.len())
9801 .min(max_extra.saturating_add(1));
9802 let conf_min = {
9808 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9809 *M.get_or_init(|| {
9810 std::env::var("CMF_DSPARK_CONF_MIN")
9811 .ok()
9812 .and_then(|v| v.parse().ok())
9813 .unwrap_or(0.0)
9814 })
9815 };
9816 if conf_min > 0.0 && conf.len() >= props.len() {
9817 let mut keep = 1usize;
9818 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9819 keep += 1;
9820 }
9821 k_verify = k_verify.min(keep.max(2));
9822 }
9823 if k_verify < 2 {
9824 return None;
9825 }
9826 let mut fed = Vec::with_capacity(k_verify);
9827 fed.push(t_next);
9828 fed.extend_from_slice(&props[1..k_verify]);
9829 let mut argmax = Vec::new();
9830 let mut logits_all = Vec::new();
9831 let mut walked = Vec::new();
9832 let txn = crate::dsv4::dsv4_verify_chunk(
9833 g,
9834 layers,
9835 &cfg,
9836 st,
9837 &fed,
9838 next_pos,
9839 &self.inv_freq,
9840 self.pool.as_deref(),
9841 &targets,
9842 &mut argmax,
9843 &mut logits_all,
9844 &mut walked,
9845 );
9846 if txn.is_none() && dbg {
9847 eprintln!("spec_step: verify отказал");
9848 }
9849 let txn = txn?;
9850 let spec_gpu_end = txn.gpu_end;
9851 let b = fed.len();
9852 let mut accepted = 1usize;
9853 while accepted < b && fed[accepted] == argmax[accepted - 1] {
9854 accepted += 1;
9855 }
9856 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9861 accepted = 1;
9862 }
9863 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9864 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9865 }
9866 let t_fin = std::time::Instant::now();
9867 if !crate::dsv4::dsv4_spec_finish(
9868 g,
9869 layers,
9870 &cfg,
9871 st,
9872 txn,
9873 accepted,
9874 &fed,
9875 &self.inv_freq,
9876 self.pool.as_deref(),
9877 ) {
9878 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9879 return None;
9880 }
9881 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9882 eprintln!(
9883 "finish(k={accepted}): {:.1} мс",
9884 t_fin.elapsed().as_secs_f64() * 1e3
9885 );
9886 }
9887 *accepted_ctr += accepted - 1;
9888 let (hc, dim) = (cfg.hc_mult, cfg.dim);
9893 let dev_caps: Vec<usize> = targets
9898 .iter()
9899 .copied()
9900 .filter(|&t| t < spec_gpu_end)
9901 .collect();
9902 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9903 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9904 return None;
9905 }
9906 for t in 0..accepted {
9907 let tip = t + 1 == accepted;
9908 for (slot, &tl) in targets.iter().enumerate() {
9909 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9910 let lo = (di * b + t) * hc * dim;
9911 crate::dsv4::dspark_capture(
9912 &caps_all[lo..lo + hc * dim],
9913 &cfg,
9914 slot,
9915 &mut ds.main_hidden,
9916 );
9917 } else if tip
9918 && crate::dsv4::dspark_peek_slot(slot, dim, {
9919 let lo = slot * dim;
9920 &mut ds.main_hidden[lo..lo + dim]
9921 })
9922 {
9923 } else {
9928 crate::dsv4::dspark_capture(
9932 &walked[t * hc * dim..(t + 1) * hc * dim],
9933 &cfg,
9934 slot,
9935 &mut ds.main_hidden,
9936 );
9937 }
9938 }
9939 crate::dsv4::dspark_ring_append(
9940 g,
9941 &self.dsv4_mtp,
9942 &cfg,
9943 ds,
9944 next_pos + t,
9945 self.pool.as_deref(),
9946 );
9947 }
9948 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9949 self.graph_logits = Some(row);
9950 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9955 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9956 crate::dsv4::pick_tally_arm();
9957 }
9958 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9959 eprintln!(
9960 "spec_step total {:.1} мс (k={accepted})",
9961 t_all.elapsed().as_secs_f64() * 1e3
9962 );
9963 }
9964 Some((fed[1..accepted].to_vec(), next_pos + accepted))
9965 }
9966
9967 fn dspark_probe(&mut self, position: usize, token_id: u32) {
9968 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9969 return;
9970 }
9971 let trunk_now = crate::dsv4::pick_tally_take();
9973 crate::dsv4::trunk_freq_note(&trunk_now);
9974 if !trunk_now.is_empty() {
9975 self.dspark_trunk_picks.push(trunk_now);
9976 let keep = crate::dsv4::dspark_block();
9977 if self.dspark_trunk_picks.len() > keep {
9978 self.dspark_trunk_picks.remove(0);
9979 }
9980 }
9981 for p in std::mem::take(&mut self.dspark_pending) {
9984 let Some(i) = position.checked_sub(p.0 + 1) else {
9985 continue;
9986 };
9987 let mut p = p;
9988 if i < p.1.len() {
9989 if p.2 && p.1[i] == token_id {
9990 p.3 = i + 1;
9991 } else {
9992 p.2 = false;
9993 }
9994 if i + 1 < p.1.len() {
9995 self.dspark_pending.push(p);
9996 continue;
9997 }
9998 }
9999 self.dspark_hist.push(p.3);
10000 self.dspark_real.push(token_id);
10001 }
10002 let Some(b) = &mut self.dsv4 else { return };
10003 let (g, layers, cfg) = (&b.0, &b.1, b.2);
10004 let n_layers = layers.len();
10005 if self.dspark.is_none() {
10006 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
10007 if t.is_empty() {
10008 return;
10009 }
10010 eprintln!(
10011 "DSpark: захват со слоёв {t:?}, блок {}",
10012 crate::dsv4::dspark_block()
10013 );
10014 crate::dsv4::dspark_arm(&t, cfg.dim);
10015 self.dspark = Some(crate::dsv4::DsparkState::new(
10016 self.dsv4_mtp.len(),
10017 &cfg,
10018 t.len(),
10019 ));
10020 }
10021 let ds = self.dspark.as_mut().unwrap();
10022 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
10023 return; }
10025 let mut conf = Vec::new();
10026 crate::dsv4::pick_tally_arm();
10027 let draft_started = std::time::Instant::now();
10032 #[cfg(feature = "gpu")]
10033 let gpu_draft = crate::dsv4::dspark_gpu_on();
10034 #[cfg(not(feature = "gpu"))]
10035 let gpu_draft = false;
10036 let props = if gpu_draft {
10037 #[cfg(feature = "gpu")]
10038 {
10039 let kv_id = b.3.kv_id;
10040 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
10041 Some(pk) => crate::dsv4::dspark_draft_gpu(
10042 g,
10043 &self.dsv4_mtp,
10044 &cfg,
10045 ds,
10046 pk,
10047 kv_id,
10048 token_id,
10049 position,
10050 self.pool.as_deref(),
10051 &mut conf,
10052 ),
10053 None => Vec::new(),
10054 }
10055 }
10056 #[cfg(not(feature = "gpu"))]
10057 Vec::new()
10058 } else {
10059 crate::gpu::cpu_scope(|| {
10060 crate::dsv4::dspark_draft(
10061 g,
10062 &self.dsv4_mtp,
10063 &cfg,
10064 ds,
10065 token_id,
10066 position,
10067 self.pool.as_deref(),
10068 &mut conf,
10069 )
10070 })
10071 };
10072 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
10073 let draft_picks = crate::dsv4::pick_tally_take();
10074 crate::dsv4::dspark_freq_note(&draft_picks);
10075 crate::dsv4::pick_tally_arm();
10078 if !props.is_empty() {
10079 let (tu, tt) = {
10083 let flat: Vec<(usize, Vec<usize>)> = self
10084 .dspark_trunk_picks
10085 .iter()
10086 .flat_map(|v| v.iter().cloned())
10087 .collect();
10088 let mut per: std::collections::HashMap<usize, Vec<usize>> =
10090 std::collections::HashMap::new();
10091 for (li, picks) in flat {
10092 per.entry(li).or_default().extend(picks);
10093 }
10094 let n = per.len().max(1);
10095 let mut u = 0usize;
10096 let mut t = 0usize;
10097 for (_, v) in per {
10098 t += v.len();
10099 u += v.iter().collect::<std::collections::HashSet<_>>().len();
10100 }
10101 (u / n, t / n)
10102 };
10103 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
10104 self.dspark_exp.push((tu, tt, du, dt));
10105 self.dspark_pending.push((position, props, true, 0));
10106 }
10107 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
10108 let n = self.dspark_hist.len() as f32;
10109 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
10110 let block = crate::dsv4::dspark_block();
10111 let mut at = vec![0usize; block + 1];
10112 for &k in &self.dspark_hist {
10113 at[k] += 1;
10114 }
10115 let mut surv = Vec::with_capacity(block);
10117 for i in 1..=block {
10118 let k = at[i..].iter().sum::<usize>() as f32 / n;
10119 surv.push(format!("{k:.2}"));
10120 }
10121 let distinct = self
10122 .dspark_real
10123 .iter()
10124 .collect::<std::collections::HashSet<_>>()
10125 .len();
10126 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
10127 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
10128 });
10129 let m = self.dspark_exp.len().max(1);
10130 eprintln!(
10131 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
10132 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
10133 self.dspark_hist.len(),
10134 mean + 1.0,
10135 surv.join(" ")
10136 );
10137 eprintln!(
10138 "DSpark: разных токенов {distinct} из {} (вырожденность), \
10139 эксперты ствол {}/{} на слой за {block} токенов, \
10140 черновик {}/{} за блок, draft {:.2} мс/блок",
10141 self.dspark_real.len(),
10142 tu / m,
10143 tt / m,
10144 du / m,
10145 dt / m,
10146 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
10147 );
10148 }
10149 }
10150
10151 fn forward_layers_upto(
10152 &mut self,
10153 hidden: &[f32],
10154 position: usize,
10155 task_mask: Option<&TaskMask>,
10156 upto: Option<usize>,
10157 ) -> Vec<f32> {
10158 if let Some(plan) = self.gpu_plan.clone() {
10164 if upto.is_none() && plan.len() > 1 {
10165 let mut h = hidden.to_vec();
10166 for &(dev, from, upto_incl) in plan.iter() {
10167 h = crate::gpu::with_device(dev, || {
10168 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
10169 });
10170 }
10171 return h;
10172 }
10173 }
10174 self.forward_layers_span(hidden, position, task_mask, 0, upto)
10175 }
10176
10177 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
10182 self.set_gpu_plan_at(devices, None)
10183 }
10184
10185 pub fn set_gpu_plan_at(
10189 &mut self,
10190 devices: Option<&[usize]>,
10191 at: Option<usize>,
10192 ) -> Result<(), String> {
10193 let Some(devs) = devices.filter(|d| d.len() > 1) else {
10194 self.gpu_plan = None;
10195 return Ok(());
10196 };
10197 self.split_supported()?;
10198 let n = self.num_layers;
10199 if devs.len() > n {
10200 return Err(format!("{} devices for {n} layers", devs.len()));
10201 }
10202 if let Some(k) = at {
10203 if k == 0 || k >= n {
10204 return Err(format!("split at {k}: the model has {n} layers"));
10205 }
10206 if devs.len() == 2 {
10207 self.gpu_plan = Some(std::sync::Arc::new(vec![
10208 (devs[0], 0, k - 1),
10209 (devs[1], k, n - 1),
10210 ]));
10211 return Ok(());
10212 }
10213 return Err(format!(
10214 "an explicit split point takes exactly 2 devices, got {}",
10215 devs.len()
10216 ));
10217 }
10218 let per = n.div_ceil(devs.len());
10219 let mut plan = Vec::with_capacity(devs.len());
10220 let mut from = 0usize;
10221 for &d in devs {
10222 if from >= n {
10223 break;
10224 }
10225 let upto = (from + per - 1).min(n - 1);
10226 plan.push((d, from, upto));
10227 from = upto + 1;
10228 }
10229 self.gpu_plan = Some(std::sync::Arc::new(plan));
10230 Ok(())
10231 }
10232
10233 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
10235 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
10236 }
10237
10238 fn forward_layers_span(
10244 &mut self,
10245 hidden: &[f32],
10246 position: usize,
10247 task_mask: Option<&TaskMask>,
10248 from: usize,
10249 upto: Option<usize>,
10250 ) -> Vec<f32> {
10251 debug_assert!(
10252 from == 0
10253 || (self.dsv4.is_none()
10254 && self.dsv41.is_none()
10255 && self.qwen4_exp.is_none()
10256 && self.g3n.is_none())
10257 );
10258 if let Some(b) = &mut self.qwen4_exp {
10259 let _ = (task_mask, upto);
10260 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10261 let mut logits = Vec::new();
10262 crate::qwen4_exp::forward_token(
10263 &b.0,
10264 &b.1,
10265 &b.2,
10266 &mut b.3,
10267 token_id,
10268 position,
10269 &self.inv_freq,
10270 self.pool.as_deref(),
10271 &mut logits,
10272 true,
10273 );
10274 self.graph_logits = Some(logits);
10275 return vec![0.0; self.hidden_size];
10276 }
10277 if let Some(b) = &mut self.dsv4 {
10283 let _ = (task_mask, upto);
10284 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10285 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
10286 st.pos = position;
10287 let mut logits = Vec::new();
10288 crate::dsv4::forward_token(
10289 g,
10290 layers,
10291 &cfg,
10292 st,
10293 token_id,
10294 &self.inv_freq,
10295 self.pool.as_deref(),
10296 &mut logits,
10297 );
10298 self.graph_logits = Some(logits);
10299 self.dspark_probe(position, token_id);
10300 return vec![0.0; self.hidden_size];
10303 }
10304 if let Some(b) = &mut self.dsv41 {
10306 let _ = (task_mask, upto);
10307 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
10308 let mut logits = Vec::new();
10309 crate::dsv41::forward_token(
10310 &b.0,
10311 &b.1,
10312 &b.2,
10313 &mut b.3,
10314 token_id,
10315 position,
10316 self.pool.as_deref(),
10317 &mut logits,
10318 );
10319 self.graph_logits = Some(logits);
10320 return vec![0.0; self.hidden_size];
10321 }
10322 if let Some(b) = &self.g3n {
10325 let _ = (task_mask, upto);
10326 return crate::g3n::g3n_forward(
10327 &b.0,
10328 &b.1,
10329 hidden,
10330 position,
10331 &mut self.kv_cache.layers,
10332 self.num_heads,
10333 self.num_kv_heads,
10334 self.head_dim,
10335 self.pool.as_deref(),
10336 );
10337 }
10338 let mut h = hidden.to_vec();
10339 let (nh, _nkv, _hd, hs, _rd, eps) = (
10342 self.num_heads,
10343 self.num_kv_heads,
10344 self.head_dim,
10345 self.hidden_size,
10346 self.rotary_dim,
10347 self.rms_eps,
10348 );
10349 let pool = self.pool.clone();
10350 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
10362 let graph_on = match graph_env.as_deref() {
10363 Some("0") => false,
10364 Some("prefill") => false, Some(_) => true,
10366 None => crate::gpu::wgpu_graph_default(),
10372 };
10373 let graph_trusted =
10374 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
10375 let race_eligible = graph_on
10376 && upto.is_none()
10377 && task_mask.is_none()
10378 && from == 0
10379 && !crate::gpu::graph_unsupported();
10380 let mut tail_start = 0usize;
10381 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
10382 let t_graph = std::time::Instant::now();
10383 let mut lg = Vec::new();
10384 let mut gl = 0usize;
10385 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
10386 let declined = built.is_none();
10387 let built = match built {
10388 Some(Ok(hh)) => Some(hh),
10389 Some(Err(())) => {
10390 self.clear_sequence_state();
10394 self.graph_failed
10395 .store(true, std::sync::atomic::Ordering::Relaxed);
10396 self.cancel
10397 .store(true, std::sync::atomic::Ordering::Relaxed);
10398 tracing::error!("token graph failed after admission; sequence state cleared");
10399 return vec![0.0; self.hidden_size];
10400 }
10401 None => None,
10402 };
10403 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
10408 crate::gpu::graph_mark_unsupported();
10409 }
10410 graph_note(built.is_some(), gl, self.num_layers);
10411 if let Some(hh) = built {
10412 let dur = t_graph.elapsed();
10413 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10414 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
10415 }
10416 if gl > 0 && gl < self.num_layers {
10417 h = hh;
10423 tail_start = gl;
10424 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
10425 if !graph_trusted {
10426 crate::gpu::graph_race_record(true, dur);
10427 }
10428 if !lg.is_empty() {
10429 lg.resize(self.vocab_size, 0.0);
10432 if let Some(c) = self.final_softcap {
10433 for l in lg.iter_mut() {
10434 *l = c * (*l / c).tanh();
10435 }
10436 }
10437 self.graph_logits = Some(lg);
10438 }
10439 return hh;
10440 }
10441 }
10447 }
10448 let span = from > 0 || upto.is_some();
10472 if span && graph_on && task_mask.is_none() && graph_trusted {
10473 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
10474 let mut lg = Vec::new();
10475 let mut gl = 0usize;
10476 let span_res =
10477 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
10478 let span_res = match span_res {
10479 Some(Ok(hh)) => Some(hh),
10480 Some(Err(())) => {
10481 self.clear_sequence_state();
10482 self.graph_failed
10483 .store(true, std::sync::atomic::Ordering::Relaxed);
10484 self.cancel
10485 .store(true, std::sync::atomic::Ordering::Relaxed);
10486 tracing::error!(
10487 "span token graph failed after admission; sequence state cleared"
10488 );
10489 return vec![0.0; self.hidden_size];
10490 }
10491 None => None,
10492 };
10493 graph_note(span_res.is_some(), gl, upto_excl - from);
10494 if std::env::var("CMF_GPU_DEBUG").is_ok() {
10495 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
10499 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
10500 eprintln!(
10501 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
10502 upto_excl - from,
10503 span_res.is_some()
10504 );
10505 }
10506 }
10507 if let Some(hh) = span_res {
10508 if gl == upto_excl - from {
10509 if !lg.is_empty() {
10510 lg.resize(self.vocab_size, 0.0);
10511 if let Some(c) = self.final_softcap {
10512 for l in lg.iter_mut() {
10513 *l = c * (*l / c).tanh();
10514 }
10515 }
10516 self.graph_logits = Some(lg);
10517 }
10518 crate::gpu::set_layer(-1);
10519 return hh;
10520 }
10521 h = hh;
10523 tail_start = from + gl;
10524 }
10525 }
10526 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
10527
10528 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10534 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10535
10536 #[cfg(target_os = "macos")]
10537 let mut gpu_skip_until = 0usize;
10538 for li in tail_start.max(from)..self.num_layers {
10539 let _capacity_tail = automatic_gpu_prefix
10540 .filter(|&prefix| li >= prefix)
10541 .map(|_| crate::gpu::enter_cpu_scope());
10542 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
10544 if li > u {
10545 break;
10546 }
10547 }
10548 if let Some(mask) = task_mask {
10549 if !mask.layer_alive(li) {
10550 continue; }
10552 }
10553 #[cfg(target_os = "macos")]
10557 {
10558 if li < gpu_skip_until {
10559 continue;
10560 }
10561 if task_mask.is_none() {
10562 let end = self.q1_graph_gpu(li, upto, position, &mut h);
10563 if self
10564 .graph_failed
10565 .load(std::sync::atomic::Ordering::Relaxed)
10566 {
10567 return vec![0.0; self.hidden_size];
10571 }
10572 if end > li {
10573 gpu_skip_until = end;
10574 if self.is_loop_end(end - 1) && end < self.num_layers {
10577 h = inference::rms_norm(
10578 &h,
10579 &self.weights.final_norm,
10580 self.rms_eps,
10581 self.norm_style,
10582 );
10583 }
10584 continue;
10585 }
10586 }
10587 }
10588
10589 let lw = &self.weights.layers[self.phys_layer(li)];
10590 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10591 if tp.parse::<usize>().ok() == Some(position) {
10592 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10593 eprintln!(
10594 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10595 h[0], h[1]
10596 );
10597 }
10598 }
10599 inference::rms_norm_into(
10602 &h,
10603 &lw.input_norm,
10604 self.rms_eps,
10605 self.norm_style,
10606 &mut self.ws.n1,
10607 );
10608
10609 let attn_out = match &lw.attn {
10610 AttnKind::Mla(w) => {
10611 let inv_freq_l = self.layer_inv_freq(li);
10612 let rs = self.layer_rope_scale(li);
10613 let eps = self.rms_eps;
10614 let pool = self.pool.clone();
10615 mla_attention(
10616 w,
10617 &self.ws.n1,
10618 &mut self.kv_cache.layers[li],
10619 position,
10620 &inv_freq_l,
10621 rs,
10622 eps,
10623 pool.as_deref(),
10624 )
10625 }
10626 AttnKind::Linear(w) => {
10627 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10628 vmf_phase_forward(
10629 &self.ws.n1,
10630 w,
10631 &cfg,
10632 &mut self.kv_cache.layers[li].linear_state,
10633 self.pool.as_deref(),
10634 )
10635 }
10636 AttnKind::Kda(w) => {
10637 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10638 crate::linear_core::kda_forward(
10639 &self.ws.n1,
10640 w,
10641 &cfg,
10642 &mut self.kv_cache.layers[li].linear_state,
10643 self.pool.as_deref(),
10644 )
10645 }
10646 AttnKind::LinearGdn(w) => {
10647 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10648 gdn_forward(
10649 &self.ws.n1,
10650 w,
10651 &cfg,
10652 &mut self.kv_cache.layers[li].linear_state,
10653 self.pool.as_deref(),
10654 )
10655 }
10656 AttnKind::ShortConv(w) => {
10657 let cfg = self
10658 .short_conv_cfg
10659 .expect("short-conv layer without short_conv_cfg");
10660 short_conv_forward(
10661 &self.ws.n1,
10662 w,
10663 &cfg,
10664 &mut self.kv_cache.layers[li].linear_state,
10665 self.pool.as_deref(),
10666 )
10667 }
10668 AttnKind::Full {
10669 wq,
10670 wk,
10671 wv,
10672 wo,
10673 q_norm,
10674 k_norm,
10675 output_gate,
10676 softplus_gate,
10677 bias,
10678 } if self.kv_cache.layers[li].o1_sealed() => {
10679 let inv_freq_l = self.layer_inv_freq(li);
10682 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10683 let cfg = QwenAttnCfg {
10684 num_heads: self.layer_num_heads(li),
10685 num_kv_heads: nkv_l,
10686 head_dim: hd_l,
10687 hidden_size: hs,
10688 position,
10689 inv_freq: &inv_freq_l,
10690 rotary_dim: rd_l,
10691 scale: self.attn_scale,
10692 softcap: self.attn_softcap,
10693 window: None,
10694 v_norm: self.attn_v_norm,
10695 qk_norm_after_rope: self.qk_norm_after_rope,
10696 q_norm: q_norm.as_deref(),
10697 k_norm: k_norm.as_deref(),
10698 output_gate: *output_gate,
10699 softplus_gate: softplus_gate
10700 .as_ref()
10701 .map(|(gate, per_head)| (gate, *per_head)),
10702 rope_scale: self.layer_rope_scale(li),
10703 bias: bias
10704 .as_ref()
10705 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10706 rms_eps: eps,
10707 norm_style: self.norm_style,
10708 pool: pool.as_deref(),
10709 };
10710 attention::qwen_attention_nystrom(
10711 &self.ws.n1,
10712 wq,
10713 wk,
10714 wv,
10715 wo,
10716 &mut self.kv_cache.layers[li],
10717 &cfg,
10718 )
10719 }
10720 AttnKind::Full {
10721 wq,
10722 wk,
10723 wv,
10724 wo,
10725 q_norm,
10726 k_norm,
10727 output_gate,
10728 softplus_gate,
10729 bias,
10730 } => 'attn: {
10731 if graph_on
10734 && !*output_gate
10735 && softplus_gate.is_none()
10736 && self.attention_heads_per_layer.is_none()
10737 && bias.is_none()
10738 && task_mask.is_none()
10739 {
10740 let inv_freq_l = self.layer_inv_freq(li);
10741 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10742 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10743 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10744 wq.mapped_q1(),
10745 wk.mapped_q1(),
10746 wv.mapped_q1(),
10747 wo.mapped_q1(),
10748 ) {
10749 let gm = gm.clone();
10750 let mut out = vec![0f32; hs];
10751 let cache = &self.kv_cache.layers[li];
10752 if crate::gpu::attn_dropin(
10753 &gm,
10754 self.graph_kv_id,
10755 li,
10756 &self.ws.n1,
10757 qi,
10758 ki,
10759 vi,
10760 oi,
10761 q_norm.as_deref(),
10762 k_norm.as_deref(),
10763 self.qk_norm_after_rope,
10764 &inv_freq_l,
10765 nh,
10766 nkv_l,
10767 hd_l,
10768 rd_l,
10769 hs,
10770 position,
10771 self.kv_cache.max_seq_len,
10772 gemma,
10773 eps as f32,
10774 cache.k_heads(),
10775 cache.v_heads(),
10776 &mut out,
10777 ) {
10778 break 'attn out;
10779 }
10780 }
10781 }
10782 let masked = task_mask
10783 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10784 .unwrap_or(false);
10785 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10786 match (masked, f32_view) {
10787 (true, (Some(q), Some(k), Some(v), Some(o))) => {
10790 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10791 attention::multi_head_attention(
10792 &self.ws.n1,
10793 q,
10794 k,
10795 v,
10796 o,
10797 &mut self.kv_cache.layers[li],
10798 self.num_heads,
10799 self.num_kv_heads,
10800 self.head_dim,
10801 self.hidden_size,
10802 position,
10803 &active_heads,
10804 &self.inv_freq,
10805 )
10806 }
10807 (masked, _) => {
10808 if masked {
10809 tracing::warn!(
10810 "layer {li}: head mask on quantized weights not \
10811 supported yet — executing dense"
10812 );
10813 }
10814 let inv_freq_l = self.layer_inv_freq(li);
10815 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10816 let cfg = QwenAttnCfg {
10817 num_heads: self.layer_num_heads(li),
10818 num_kv_heads: nkv_l,
10819 head_dim: hd_l,
10820 hidden_size: hs,
10821 position,
10822 inv_freq: &inv_freq_l,
10823 rotary_dim: rd_l,
10824 scale: self.attn_scale,
10825 softcap: self.attn_softcap,
10826 window: self.layer_window(li),
10827 v_norm: self.attn_v_norm,
10828 qk_norm_after_rope: self.qk_norm_after_rope,
10829 q_norm: q_norm.as_deref(),
10830 k_norm: k_norm.as_deref(),
10831 output_gate: *output_gate,
10832 softplus_gate: softplus_gate
10833 .as_ref()
10834 .map(|(gate, per_head)| (gate, *per_head)),
10835 rope_scale: self.layer_rope_scale(li),
10836 bias: bias
10837 .as_ref()
10838 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10839 rms_eps: eps,
10840 norm_style: self.norm_style,
10841 pool: pool.as_deref(),
10842 };
10843 attention::qwen_attention(
10844 &self.ws.n1,
10845 wq,
10846 wk,
10847 wv,
10848 wo,
10849 &mut self.kv_cache.layers[li],
10850 &cfg,
10851 )
10852 }
10853 }
10854 }
10855 };
10856 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10859 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10860 None => attn_out,
10861 };
10862 let lw = &self.weights.layers[self.phys_layer(li)];
10863 inference::add_rmsnorm_fused_into(
10864 &mut h,
10865 &attn_out,
10866 &lw.post_norm,
10867 self.rms_eps,
10868 self.norm_style,
10869 &mut self.ws.p1,
10870 );
10871 let mut attn_out = attn_out;
10872 attention::recycle_buf(&mut attn_out);
10873 let post_normed = &self.ws.p1;
10874
10875 let ffn_masked = task_mask
10876 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10877 .unwrap_or(false);
10878 let ffn_out = match (ffn_masked, &lw.ffn) {
10890 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10894 let row = task_mask
10895 .and_then(|tm| tm.ffn_masks.get(li))
10896 .map(|v| v.as_slice());
10897 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10898 }
10899 (true, FfnKind::Dense(d)) => {
10900 let tm = task_mask.unwrap();
10901 let alive = tm.ffn_active_count(li);
10902 let deep = alive * 2 <= self.intermediate_size;
10903 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
10904 let active = tm.ffn_active_indices(li);
10905 sparse_ffn_quant(
10906 d,
10907 post_normed,
10908 &active,
10909 self.hidden_size,
10910 self.pool.as_deref(),
10911 )
10912 } else if deep
10913 && let (Some(g), Some(u), Some(dn)) = (
10914 d.gate_proj.as_f32(),
10915 d.up_proj.as_f32(),
10916 d.down_proj.as_f32(),
10917 )
10918 {
10919 let active = tm.ffn_active_indices(li);
10920 inference::sparse_ffn_forward(
10921 post_normed,
10922 g,
10923 u,
10924 dn,
10925 self.hidden_size,
10926 self.intermediate_size,
10927 &active,
10928 self.pool.as_deref(),
10929 )
10930 } else {
10931 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10932 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10933 }
10934 }
10935 (true, FfnKind::Moe(m)) => {
10936 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10940 ffn_forward(
10941 &lw.ffn,
10942 post_normed,
10943 self.pool.as_deref(),
10944 allowed.as_deref(),
10945 )
10946 }
10947 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10948 dm,
10949 post_normed,
10950 &h,
10951 self.rms_eps,
10952 self.norm_style,
10953 self.pool.as_deref(),
10954 ),
10955 (false, _) => match &lw.ffn {
10956 FfnKind::DenseMoe(dm) => dense_moe_ffn(
10957 dm,
10958 post_normed,
10959 &h,
10960 self.rms_eps,
10961 self.norm_style,
10962 self.pool.as_deref(),
10963 ),
10964 _ => {
10965 let allowed = match (&lw.ffn, task_mask) {
10966 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10967 _ => None,
10968 };
10969 ffn_forward(
10970 &lw.ffn,
10971 post_normed,
10972 self.pool.as_deref(),
10973 allowed.as_deref(),
10974 )
10975 }
10976 },
10977 };
10978 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
10979 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
10980 None => ffn_out,
10981 };
10982 for (i, &f) in ffn_out.iter().enumerate() {
10983 h[i] += f;
10984 }
10985 let mut ffn_out = ffn_out;
10986 attention::recycle_buf(&mut ffn_out);
10987
10988 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
10990 for v in h.iter_mut() {
10991 *v *= sc;
10992 }
10993 }
10994
10995 if self.is_loop_end(li) && li + 1 < self.num_layers {
10998 h = inference::rms_norm(
10999 &h,
11000 &self.weights.final_norm,
11001 self.rms_eps,
11002 self.norm_style,
11003 );
11004 }
11005
11006 if self.dyn_phi_layer == Some(li) {
11010 self.update_dyn_phi(&h);
11011 }
11012 }
11013 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
11015 crate::gpu::graph_race_record(false, t.elapsed());
11016 }
11017
11018 h
11019 }
11020
11021 fn update_dyn_phi(&mut self, h: &[f32]) {
11024 const A: f32 = 0.2;
11025 if self.dyn_phi_ema.len() != h.len() {
11026 self.dyn_phi_ema = vec![0.0; h.len()];
11027 self.dyn_phi_seen = 0;
11028 }
11029 if self.dyn_phi_seen == 0 {
11030 self.dyn_phi_ema.copy_from_slice(h);
11031 } else {
11032 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
11033 *e = (1.0 - A) * *e + A * v;
11034 }
11035 }
11036 self.dyn_phi_seen += 1;
11037 }
11038
11039 pub fn dyn_phi(&self) -> &[f32] {
11041 &self.dyn_phi_ema
11042 }
11043
11044 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
11046 self.dyn_phi_layer = layer;
11047 self.dyn_phi_ema.clear();
11048 self.dyn_phi_seen = 0;
11049 }
11050
11051 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
11053 let Some(model) = &self.model else {
11054 return Vec::new();
11055 };
11056 model
11057 .header
11058 .skills
11059 .iter()
11060 .enumerate()
11061 .filter_map(|(i, sk)| {
11062 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
11063 let sel = sk.selection.as_ref()?;
11064 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
11065 })
11066 .collect()
11067 }
11068
11069 pub fn active_skill(&self) -> Option<usize> {
11071 self.dyn_active
11072 }
11073
11074 pub fn enable_dynamic_routing(&mut self) -> usize {
11079 use crate::swarm::{DynRouter, RoutableSkill};
11080 let Some(model) = self.model.clone() else {
11081 return 0;
11082 };
11083 if self.dyn_blend_loaded {
11086 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
11087 return 0;
11088 }
11089 if let Some(a) = self.dyn_active {
11093 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
11094 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
11095 return 0;
11096 }
11097 }
11098 let hidden = self.hidden_size;
11099 let mut skills = Vec::new();
11100 for (idx, id, _phi) in self.dynamic_skills() {
11101 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
11102 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
11103 skills.push(rs);
11104 }
11105 }
11106 }
11107 if skills.is_empty() {
11108 return 0;
11109 }
11110 let phi = skills[0].phi_layer;
11112 if skills.iter().any(|s| s.phi_layer != phi) {
11113 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
11114 }
11115 let n = skills.len();
11116 self.set_dyn_phi_layer(Some(phi));
11117 self.dyn_router = Some(DynRouter::new(skills));
11118 n
11119 }
11120
11121 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
11123 self.dyn_router
11124 .as_ref()
11125 .map(|r| r.switches.clone())
11126 .unwrap_or_default()
11127 }
11128
11129 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
11132 let rows = self.weights.lm_head.rows();
11133 let mut logits = attention::take_buf(rows.min(self.vocab_size));
11134 self.weights
11135 .lm_head
11136 .matvec(hidden, &mut logits, self.pool.as_deref());
11137 logits.resize(self.vocab_size, 0.0);
11138 if let Some(m) = self.logit_multiplier {
11139 for l in logits.iter_mut() {
11140 *l *= m;
11141 }
11142 }
11143 if let Some(c) = self.final_softcap {
11144 for l in logits.iter_mut() {
11145 *l = c * (*l / c).tanh();
11146 }
11147 }
11148 if let Some(cm) = self.head_clusters.as_ref() {
11149 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
11150 }
11151 logits
11152 }
11153
11154 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
11157 let h = hidden.len();
11158 let ncl = cm.len() / h.max(1);
11159 if ncl == 0 || logits.len() % ncl != 0 {
11160 return;
11161 }
11162 let cs = logits.len() / ncl;
11163 let mut lc = vec![0.0f32; ncl];
11165 for c in 0..ncl {
11166 let row = &cm[c * h..(c + 1) * h];
11167 let mut s = 0.0f32;
11168 for j in 0..h {
11169 s += row[j] * hidden[j];
11170 }
11171 lc[c] = s;
11172 }
11173 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11174 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
11175 for c in 0..ncl {
11176 let blk = &mut logits[c * cs..(c + 1) * cs];
11177 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
11178 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
11179 let add = lc[c] - lse - bl;
11180 for v in blk.iter_mut() {
11181 *v += add;
11182 }
11183 }
11184 }
11185
11186 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
11191 self.clear_sequence_state();
11192 crate::gpu::graph_race_begin_generation();
11196 if task_mask.is_none() {
11197 self.o1_begin();
11198 }
11199 let mut hidden = vec![0.0f32; self.hidden_size];
11200 for (pos, &id) in ids.iter().enumerate() {
11201 let emb = self.embed_single(id);
11202 hidden = self.forward_layers(&emb, pos, task_mask);
11203 }
11204 if let Err(err) = self.o1_seal_checked() {
11205 self.o1_fail(err);
11206 }
11207 inference::rms_norm_into(
11208 &hidden,
11209 &self.weights.final_norm,
11210 self.rms_eps,
11211 self.norm_style,
11212 &mut self.ws.n1,
11213 );
11214 self.lm_head_forward(&self.ws.n1)
11215 }
11216}
11217
11218pub fn create_test_pipeline(
11220 hidden_size: usize,
11221 intermediate_size: usize,
11222 num_heads: usize,
11223 num_kv_heads: usize,
11224 head_dim: usize,
11225 num_layers: usize,
11226 vocab_size: usize,
11227) -> Pipeline {
11228 let synth = |n: usize, salt: usize| -> Vec<f32> {
11231 (0..n)
11232 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
11233 .collect()
11234 };
11235 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11236 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11237 };
11238 let layer_weights: Vec<LayerWeights> = (0..num_layers)
11239 .map(|li| LayerWeights {
11240 input_norm: vec![1.0; hidden_size],
11241 post_norm: vec![1.0; hidden_size],
11242 attn_out_norm: None,
11243 ffn_out_norm: None,
11244 layer_scale: None,
11245 ffn: FfnKind::Dense(DenseFfn {
11246 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
11247 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
11248 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
11249 act: Act::Silu,
11250 down_t: None,
11251 segs: Vec::new(),
11252 }),
11253 attn: AttnKind::Full {
11254 bias: None,
11255 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
11256 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
11257 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
11258 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
11259 q_norm: None,
11260 k_norm: None,
11261 output_gate: false,
11262 softplus_gate: None,
11263 },
11264 })
11265 .collect();
11266
11267 Pipeline::new(
11268 Tokenizer::byte_level(),
11269 PipelineWeights {
11270 embed_tokens: qt(vocab_size, hidden_size, 100),
11271 layers: layer_weights,
11272 lm_head: qt(vocab_size, hidden_size, 200),
11273 final_norm: vec![1.0; hidden_size],
11274 },
11275 hidden_size,
11276 intermediate_size,
11277 num_heads,
11278 num_kv_heads,
11279 head_dim,
11280 num_layers,
11281 num_layers, false, vocab_size,
11284 1e-6,
11285 10_000.0,
11286 NormStyle::Qwen,
11287 4096,
11288 SamplerConfig {
11289 seed: Some(42),
11290 ..Default::default()
11291 },
11292 )
11293}
11294
11295#[inline]
11300fn mask_bit(row: &[u8], j: usize) -> bool {
11301 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
11302}
11303
11304fn mask_gain() -> f32 {
11315 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
11316 *G.get_or_init(|| {
11317 std::env::var("CMF_FFN_MASK_GAIN")
11318 .ok()
11319 .and_then(|v| v.parse().ok())
11320 .unwrap_or(1.0)
11321 })
11322}
11323
11324fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
11325 let fill = meanfill().and_then(|(i, v)| {
11328 let li = crate::gpu::cur_layer();
11329 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
11330 });
11331 for r in 0..rows {
11332 let base = r * inter;
11333 for (bi, &byte) in row.iter().enumerate() {
11334 if byte == 0xFF {
11335 continue;
11336 }
11337 let j0 = bi * 8;
11338 for bit in 0..8 {
11339 let j = j0 + bit;
11340 if j < inter && byte & (1 << bit) == 0 {
11341 g[base + j] = fill.map_or(0.0, |f| f[j]);
11342 }
11343 }
11344 }
11345 }
11346 let gain = mask_gain();
11347 if gain != 1.0 {
11348 for v in g[..rows * inter].iter_mut() {
11349 *v *= gain;
11350 }
11351 }
11352}
11353
11354#[inline]
11356fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
11357 row.is_none_or(|r| mask_bit(r, i))
11358}
11359
11360fn all_bits_on(row: &[u8], n: usize) -> bool {
11363 (0..n).all(|i| mask_bit(row, i))
11364}
11365
11366fn tube_topk() -> usize {
11374 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11375 *K.get_or_init(|| {
11376 std::env::var("CMF_TUBE_TOPK")
11377 .ok()
11378 .and_then(|v| v.parse().ok())
11379 .unwrap_or(0)
11380 })
11381}
11382
11383fn tube_score_oracle() -> bool {
11384 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11385 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
11386}
11387
11388fn tube_ffn_routed(
11395 d: &DenseFfn,
11396 xs: &[f32],
11397 b: usize,
11398 pool: Option<&Pool>,
11399 mask_row: Option<&[u8]>,
11400 k: usize,
11401) -> Vec<f32> {
11402 let hidden = d.down_proj.rows();
11403 let core = d.gate_proj.rows();
11404 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11405 let mut out = match (b, core_full, mask_row) {
11406 (1, true, _) => dense_ffn(d, xs, pool),
11407 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11408 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11409 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11410 };
11411 let cand: Vec<usize> = (0..d.segs.len())
11412 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
11413 .collect();
11414 if cand.is_empty() {
11415 return out;
11416 }
11417 let oracle = tube_score_oracle();
11421 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
11422 let mut scores = vec![0f32; b * cand.len()];
11423 for (ci, &i) in cand.iter().enumerate() {
11424 let seg = &d.segs[i];
11425 let w = seg.width;
11426 let mut g = vec![0.0f32; b * w];
11427 if b == 1 {
11428 seg.gate.matvec(xs, &mut g, pool);
11429 } else {
11430 seg.gate.matmat(xs, b, &mut g, pool);
11431 }
11432 for v in g.iter_mut() {
11433 *v = Act::Silu.combine(*v, 1.0);
11434 }
11435 if !oracle {
11436 for t in 0..b {
11437 scores[t * cand.len() + ci] =
11438 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11439 }
11440 }
11441 if oracle || b > 1 {
11442 let mut u = vec![0.0f32; b * w];
11443 if b == 1 {
11444 seg.up.matvec(xs, &mut u, pool);
11445 } else {
11446 seg.up.matmat(xs, b, &mut u, pool);
11447 }
11448 for (a, &v) in g.iter_mut().zip(u.iter()) {
11449 *a *= v;
11450 }
11451 if oracle {
11452 for t in 0..b {
11453 scores[t * cand.len() + ci] =
11454 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
11455 }
11456 }
11457 }
11458 acts.push(g);
11459 }
11460 let keep = k.min(cand.len());
11462 let mut scratch: Vec<f32> = Vec::new();
11463 for t in 0..b {
11464 let mut sc: Vec<(f32, usize)> = (0..cand.len())
11465 .map(|ci| (scores[t * cand.len() + ci], ci))
11466 .collect();
11467 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
11468 let mut alive = vec![false; cand.len()];
11469 for &(_, ci) in sc.iter().take(keep) {
11470 alive[ci] = true;
11471 }
11472 if b > 1 {
11473 for (ci, a) in acts.iter_mut().enumerate() {
11474 if !alive[ci] {
11475 let w = d.segs[cand[ci]].width;
11476 a[t * w..(t + 1) * w].fill(0.0);
11477 }
11478 }
11479 } else {
11480 for (ci, &i) in cand.iter().enumerate() {
11484 if !alive[ci] {
11485 continue;
11486 }
11487 let seg = &d.segs[i];
11488 let w = seg.width;
11489 let g = &mut acts[ci];
11490 if !tube_score_oracle() {
11491 scratch.clear();
11492 scratch.resize(w, 0.0);
11493 seg.up.matvec(xs, &mut scratch, pool);
11494 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
11495 *a *= v;
11496 }
11497 }
11498 let mut acc = vec![0.0f32; hidden];
11499 seg.down.matvec(g, &mut acc, pool);
11500 for (o, a) in out.iter_mut().zip(&acc) {
11501 *o += *a;
11502 }
11503 }
11504 }
11505 }
11506 if b > 1 {
11507 for (ci, &i) in cand.iter().enumerate() {
11508 let seg = &d.segs[i];
11509 let mut acc = vec![0.0f32; b * hidden];
11510 seg.down.matmat(&acts[ci], b, &mut acc, pool);
11511 for (o, a) in out.iter_mut().zip(&acc) {
11512 *o += *a;
11513 }
11514 }
11515 }
11516 out
11517}
11518
11519fn tube_ffn(
11525 d: &DenseFfn,
11526 xs: &[f32],
11527 b: usize,
11528 pool: Option<&Pool>,
11529 mask_row: Option<&[u8]>,
11530) -> Vec<f32> {
11531 if tube_topk() > 0 {
11532 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
11533 }
11534 let hidden = d.down_proj.rows();
11535 let core = d.gate_proj.rows();
11536 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
11537 let mut out = match (b, core_full, mask_row) {
11538 (1, true, _) => dense_ffn(d, xs, pool),
11539 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11540 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11541 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11542 };
11543 TUBE_SCRATCH.with(|sc| {
11544 let mut sc = sc.borrow_mut();
11545 let [g, u, acc] = &mut *sc;
11546 for seg in &d.segs {
11547 if !tube_bit(mask_row, seg.start) {
11548 continue;
11549 }
11550 let w = seg.width;
11551 g.resize(b * w, 0.0);
11552 if b == 1
11553 && d.act == Act::Silu
11554 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11555 {
11556 } else {
11558 u.resize(b * w, 0.0);
11559 if b == 1 {
11560 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11561 } else {
11562 seg.gate.matmat(xs, b, g, pool);
11563 seg.up.matmat(xs, b, u, pool);
11564 }
11565 for i in 0..b * w {
11566 g[i] = d.act.combine(g[i], u[i]);
11567 }
11568 }
11569 acc.resize(b * hidden, 0.0);
11570 acc.fill(0.0);
11571 if b == 1 {
11572 seg.down.matvec(g, acc, pool);
11573 } else {
11574 seg.down.matmat(g, b, acc, pool);
11575 }
11576 for (o, a) in out.iter_mut().zip(acc.iter()) {
11577 *o += *a;
11578 }
11579 }
11580 out
11581 })
11582}
11583
11584thread_local! {
11585 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11589 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11590}
11591
11592fn dense_ffn_batch(
11593 d: &DenseFfn,
11594 xs: &[f32],
11595 b: usize,
11596 pool: Option<&Pool>,
11597 mask_row: Option<&[u8]>,
11598) -> Vec<f32> {
11599 let inter = d.gate_proj.rows();
11600 let hidden = d.down_proj.rows();
11601 if mask_row.is_none()
11609 && d.act == Act::Silu
11610 && b >= 32
11611 && crate::gpu::enabled_here()
11612 && !crate::gpu::mm_killed()
11613 && refit_dir().is_none()
11618 && !ffn_probe_active()
11623 {
11624 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11625 d.gate_proj.mapped_q4t(),
11626 d.up_proj.mapped_q4t(),
11627 d.down_proj.mapped_q4t(),
11628 ) {
11629 let mut out = vec![0.0f32; b * hidden];
11630 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11631 return out;
11632 }
11633 }
11634 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11639 d.gate_proj.mapped_q4tp(),
11640 d.up_proj.mapped_q4tp(),
11641 d.down_proj.mapped_q4tp(),
11642 ) {
11643 let mut out = vec![0.0f32; b * hidden];
11644 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11645 return out;
11646 }
11647 }
11648 }
11649 let mut g = vec![0.0f32; b * inter];
11650 d.gate_proj.matmat(xs, b, &mut g, pool);
11651 let mut u = vec![0.0f32; b * inter];
11652 d.up_proj.matmat(xs, b, &mut u, pool);
11653 if gate_topk() > 0 && d.act == Act::Silu {
11654 for t in 0..b {
11655 let row = &mut g[t * inter..(t + 1) * inter];
11656 for v in row.iter_mut() {
11657 *v = Act::Silu.combine(*v, 1.0);
11658 }
11659 keep_top_k(row, gate_topk());
11660 }
11661 for i in 0..b * inter {
11662 g[i] *= u[i];
11663 }
11664 } else {
11665 for i in 0..b * inter {
11666 g[i] = d.act.combine(g[i], u[i]);
11667 }
11668 }
11669 if let Some(row) = mask_row {
11670 zero_masked_cols(&mut g, b, inter, row);
11671 }
11672 if oracle_topk() > 0 {
11673 for t in 0..b {
11674 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11675 }
11676 }
11677 let mut out = vec![0.0f32; b * hidden];
11678 d.down_proj.matmat(&g, b, &mut out, pool);
11679 if refit_dir().is_some() {
11680 let li = crate::gpu::cur_layer();
11681 if li >= 0 {
11682 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11683 }
11684 }
11685 FFN_PROBE.with(|pr| {
11689 if let Some(acc) = pr.borrow_mut().as_mut() {
11690 let li = crate::gpu::cur_layer();
11691 if li < 0 {
11692 return;
11693 }
11694 let Some(row) = acc.get_mut(li as usize) else {
11695 return;
11696 };
11697 let sq = probe_sq();
11698 for t in 0..b {
11699 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11700 *a += if sq {
11701 (v as f64) * (v as f64)
11702 } else {
11703 (v as f64).abs()
11704 };
11705 }
11706 }
11707 }
11708 });
11709 out
11710}
11711
11712fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11717 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11718 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11719 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11720 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11721 if (!on && !dump) || b == 0 {
11722 return;
11723 }
11724 let hidden = xs.len() / b;
11725 if on {
11726 let mut acc = m.act_sq.borrow_mut();
11727 if acc.len() < hidden {
11728 acc.resize(hidden, 0.0);
11729 }
11730 for t in 0..b {
11731 let row = &xs[t * hidden..(t + 1) * hidden];
11732 for (a, &v) in acc.iter_mut().zip(row) {
11733 *a += (v as f64) * (v as f64);
11734 }
11735 }
11736 }
11737 if dump {
11738 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11741 .ok()
11742 .and_then(|v| v.parse().ok())
11743 .unwrap_or(4096);
11744 let mut rows = m.act_rows.borrow_mut();
11745 if rows.len() < cap * hidden {
11746 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11747 rows.extend_from_slice(&xs[..take * hidden]);
11748 }
11749 }
11750}
11751
11752#[derive(Clone, Copy)]
11755struct SendVecs(*mut Vec<f32>);
11756unsafe impl Send for SendVecs {}
11757unsafe impl Sync for SendVecs {}
11758impl SendVecs {
11759 #[inline]
11760 fn at(self, i: usize) -> *mut Vec<f32> {
11761 unsafe { self.0.add(i) }
11762 }
11763}
11764
11765fn moe_ffn_batch(
11766 m: &MoeFfn,
11767 xs: &[f32],
11768 b: usize,
11769 hidden: usize,
11770 pool: Option<&Pool>,
11771 allowed: Option<&[bool]>,
11772) -> Vec<f32> {
11773 accumulate_act(m, xs, b);
11774 let ne = m.experts.len();
11775 let mut logits = vec![0.0f32; b * ne];
11776 match &m.resonance {
11777 Some(r) => {
11778 let hdim = xs.len() / b.max(1);
11779 for bi in 0..b {
11780 r.scores(
11781 &xs[bi * hdim..(bi + 1) * hdim],
11782 &mut logits[bi * ne..(bi + 1) * ne],
11783 );
11784 }
11785 }
11786 None => m.router.matmat(xs, b, &mut logits, pool),
11787 }
11788
11789 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11792 {
11793 let mut st = m.stats.borrow_mut();
11794 if st.len() < ne {
11795 st.resize(ne, 0);
11796 }
11797 for bi in 0..b {
11798 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11799 for &e in &idx {
11800 st[e] += 1;
11801 assign[e].push((bi, p[e] / wsum));
11802 }
11803 }
11804 }
11805
11806 let mut out = vec![0.0f32; b * hidden];
11807 let cols = m.experts[0].gate_proj.cols();
11808 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11809 let sb = list.len();
11810 let mut sub = vec![0.0f32; sb * cols];
11811 for (k, &(bi, _)) in list.iter().enumerate() {
11812 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11813 }
11814 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11815 for (k, &(bi, w)) in list.iter().enumerate() {
11816 for i in 0..hidden {
11817 out[bi * hidden + i] += w * eo[k * hidden + i];
11818 }
11819 }
11820 };
11821 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11827 if pool.is_some() && active.len() >= 8 {
11828 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11829 {
11830 let panel_ptr = SendVecs(panels.as_mut_ptr());
11831 let experts = &m.experts;
11834 let (active_r, assign_r) = (&active, &assign);
11835 let run = |start: usize, end: usize| {
11836 for ai in start..end {
11837 let e = active_r[ai];
11838 let list = &assign_r[e];
11839 let sb = list.len();
11840 let mut sub = vec![0.0f32; sb * cols];
11841 for (k, &(bi, _)) in list.iter().enumerate() {
11842 sub[k * cols..(k + 1) * cols]
11843 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11844 }
11845 unsafe {
11847 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11848 }
11849 }
11850 };
11851 match pool {
11852 Some(p) => p.run_rows(active.len(), &run),
11853 None => run(0, active.len()),
11854 }
11855 }
11856 for (ai, &e) in active.iter().enumerate() {
11857 for (k, &(bi, w)) in assign[e].iter().enumerate() {
11858 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11859 for i in 0..hidden {
11860 out[bi * hidden + i] += w * eo[i];
11861 }
11862 }
11863 }
11864 } else {
11865 for &e in &active {
11866 run_expert(&m.experts[e], &assign[e], &mut out);
11867 }
11868 }
11869 if let Some((se, gate)) = &m.shared {
11870 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11871 let mut gl = vec![0.0f32; b];
11872 gate.matmat(xs, b, &mut gl, pool);
11873 (0..b)
11874 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11875 .collect()
11876 } else {
11877 (0..b).map(|bi| (bi, 1.0)).collect()
11878 };
11879 run_expert(se, &all, &mut out);
11880 }
11881 out
11882}
11883
11884thread_local! {
11885 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11889 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11890}
11891
11892fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11894 if gate_topk() > 0
11897 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11898 {
11899 return out;
11900 }
11901 let prism_body = d.gate_proj.has_prism_contract()
11917 || d.up_proj.has_prism_contract()
11918 || d.down_proj.has_prism_contract();
11919 if !prism_body
11920 && crate::gpu::enabled_here()
11921 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11922 {
11923 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11924 crate::gpu::ProbeArm::Gpu
11925 } else {
11926 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11927 };
11928 match arm {
11929 crate::gpu::ProbeArm::Gpu => {
11930 let t0 = std::time::Instant::now();
11931 if let Some(out) = dense_ffn_gpu(d, x, pool) {
11932 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11933 return out;
11934 }
11935 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11939 }
11940 crate::gpu::ProbeArm::CpuTimed => {
11941 let t0 = std::time::Instant::now();
11942 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11943 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11944 return out;
11945 }
11946 crate::gpu::ProbeArm::Cpu => {
11947 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11948 }
11949 }
11950 }
11951 dense_ffn_cpu(d, x, pool)
11952}
11953
11954fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11956 let inter = d.gate_proj.rows();
11957 FFN_SCRATCH.with(|s| {
11958 let mut s = s.borrow_mut();
11959 let [g, u, ..] = &mut *s;
11960 g.resize(inter, 0.0);
11961 if gate_topk() > 0 {
11964 u.resize(inter, 0.0);
11968 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11969 for i in 0..inter {
11970 g[i] = Act::Silu.combine(g[i], 1.0);
11971 }
11972 keep_top_k(g, gate_topk());
11973 for i in 0..inter {
11974 g[i] *= u[i];
11975 }
11976 } else if d.act == Act::Silu
11977 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
11978 {
11979 } else {
11981 u.resize(inter, 0.0);
11982 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11984 for i in 0..inter {
11985 g[i] = d.act.combine(g[i], u[i]);
11986 }
11987 }
11988 FFN_PROBE.with(|pr| {
11996 if let Some(acc) = pr.borrow_mut().as_mut() {
11997 let li = crate::gpu::cur_layer();
11998 if li >= 0 {
11999 if let Some(row) = acc.get_mut(li as usize) {
12000 match probe_topk() {
12001 0 if probe_sq() => {
12002 for (a, &v) in row.iter_mut().zip(g.iter()) {
12003 *a += (v as f64) * (v as f64);
12004 }
12005 }
12006 0 if probe_signed() => {
12007 for (a, &v) in row.iter_mut().zip(g.iter()) {
12008 *a += v as f64;
12009 }
12010 }
12011 0 => {
12012 for (a, &v) in row.iter_mut().zip(g.iter()) {
12013 *a += (v as f64).abs();
12014 }
12015 }
12016 k => {
12017 let n = g.len();
12018 let k = k.min(n);
12019 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12020 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12021 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12022 });
12023 let thr = *kth;
12024 for (a, &v) in row.iter_mut().zip(g.iter()) {
12025 if v.abs() >= thr {
12026 *a += 1.0;
12027 }
12028 }
12029 }
12030 }
12031 }
12032 }
12033 }
12034 });
12035 if oracle_topk() > 0 {
12036 keep_top_k(g, oracle_topk());
12037 }
12038 {
12039 let li = crate::gpu::cur_layer();
12040 if li >= 0 {
12041 adump_row(li as usize, g);
12042 }
12043 }
12044 let mut out = attention::take_buf(d.down_proj.rows());
12045 d.down_proj.matvec(g, &mut out, pool);
12046 out
12047 })
12048}
12049
12050pub struct RefitAcc {
12063 pub support: Vec<u32>,
12064 pub gss: Vec<f32>,
12065 pub ya: Vec<f32>,
12066 pub hidden: usize,
12067 pub tokens: u64,
12068 pub buf_g: Vec<f32>,
12074 pub buf_o: Vec<f32>,
12075 pub buf_t: usize,
12076}
12077
12078type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
12082
12083static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
12084 std::sync::OnceLock::new();
12085
12086fn ffn_probe_active() -> bool {
12089 FFN_PROBE.with(|p| p.borrow().is_some())
12090}
12091
12092fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
12093 REFIT
12094 .get_or_init(|| {
12095 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
12096 (
12097 d,
12098 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
12099 )
12100 })
12101 })
12102 .as_ref()
12103}
12104
12105fn refit_accumulate(
12107 li: usize,
12108 g: &[f32],
12109 b: usize,
12110 inter: usize,
12111 out: &[f32],
12112 hidden: usize,
12113 pool: Option<&Pool>,
12114) {
12115 let Some((dir, map)) = refit_dir() else {
12116 return;
12117 };
12118 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12119 let (from, to) = *SPAN.get_or_init(|| {
12120 let g = |k: &str, d: usize| {
12121 std::env::var(k)
12122 .ok()
12123 .and_then(|v| v.parse().ok())
12124 .unwrap_or(d)
12125 };
12126 (
12127 g("CMF_FFN_REFIT_FROM", 0),
12128 g("CMF_FFN_REFIT_TO", usize::MAX),
12129 )
12130 });
12131 if li < from || li > to {
12132 return;
12133 }
12134 let mut guard = map.lock().unwrap();
12135 let (map, shared) = &mut *guard;
12136 let acc = match map.entry(li) {
12137 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
12138 std::collections::hash_map::Entry::Vacant(e) => {
12139 let path = format!("{dir}/support.{li}.u32");
12140 let Ok(bytes) = std::fs::read(&path) else {
12141 eprintln!("refit: no {path} — layer {li} skipped");
12142 return;
12143 };
12144 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
12145 let support: Vec<u32> = bytes[4..4 + n * 4]
12146 .chunks_exact(4)
12147 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12148 .collect();
12149 eprintln!(
12150 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
12151 (n * n + hidden * n) as f64 * 4.0 / 1e6
12152 );
12153 e.insert(RefitAcc {
12154 gss: vec![0.0; n * n],
12155 ya: vec![0.0; hidden * n],
12156 buf_g: Vec::new(),
12157 buf_o: Vec::new(),
12158 buf_t: 0,
12159 support,
12160 hidden,
12161 tokens: 0,
12162 })
12163 }
12164 };
12165 let ns = acc.support.len();
12166 let cap = refit_batch();
12168 if acc.buf_g.is_empty() {
12169 acc.buf_g = vec![0.0; ns * cap];
12170 acc.buf_o = vec![0.0; hidden * cap];
12171 }
12172 let take = b.min(cap - acc.buf_t);
12173 for t in 0..take {
12174 let col = acc.buf_t + t;
12175 for (j, &n) in acc.support.iter().enumerate() {
12176 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
12177 }
12178 for h in 0..hidden {
12179 acc.buf_o[h * cap + col] = out[t * hidden + h];
12180 }
12181 }
12182 acc.buf_t += take;
12183 acc.tokens += take as u64;
12184 if acc.buf_t < cap {
12185 return;
12186 }
12187 let bt = acc.buf_t;
12188 acc.buf_t = 0;
12189 let RefitAcc {
12199 gss,
12200 ya,
12201 buf_g,
12202 buf_o,
12203 ..
12204 } = acc;
12205 let need = (ns * ns).max(hidden * ns);
12206 if shared.len() < need {
12207 shared.resize(need, 0.0);
12208 }
12209 let scratch = &mut shared[..];
12210 let _ = bt;
12211 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
12212 add_into(gss, &scratch[..ns * ns], pool);
12213 if crate::gpu::gemm_nt_f32_transient(
12214 buf_o,
12215 buf_g,
12216 &mut scratch[..hidden * ns],
12217 hidden,
12218 cap,
12219 ns,
12220 ) {
12221 add_into(ya, &scratch[..hidden * ns], pool);
12222 } else {
12223 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12224 }
12225 } else {
12226 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
12227 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
12228 }
12229 }
12233
12234fn refit_batch() -> usize {
12236 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12237 *B.get_or_init(|| {
12238 std::env::var("CMF_FFN_REFIT_BATCH")
12239 .ok()
12240 .and_then(|v| v.parse().ok())
12241 .unwrap_or(4096)
12242 })
12243}
12244
12245fn accum_outer_t(
12248 c: &mut [f32],
12249 m: usize,
12250 n: usize,
12251 b: usize,
12252 left: &[f32],
12253 right: &[f32],
12254 pool: Option<&Pool>,
12255) {
12256 let ptr = SendMut(c.as_mut_ptr());
12257 let body = |i: usize| {
12258 let ptr = &ptr;
12259 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
12260 for t in 0..b {
12261 let a = left[i * b + t];
12262 if a == 0.0 {
12263 continue;
12264 }
12265 for (j, o) in row.iter_mut().enumerate() {
12266 *o += a * right[j * b + t];
12267 }
12268 }
12269 };
12270 match pool {
12271 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
12272 for i in s..e {
12273 body(i);
12274 }
12275 }),
12276 _ => {
12277 for i in 0..m {
12278 body(i);
12279 }
12280 }
12281 }
12282}
12283
12284fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
12287 let n = dst.len().min(src.len());
12288 match pool {
12289 Some(p) if n >= 1 << 16 => {
12290 let ptr = SendMut(dst.as_mut_ptr());
12291 let f = |s: usize, e: usize| {
12292 let ptr = &ptr;
12293 for blk in s..e {
12294 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
12295 for i in a..b {
12296 unsafe { *ptr.0.add(i) += src[i] };
12297 }
12298 }
12299 };
12300 p.run_rows(n.div_ceil(4096), &f);
12301 }
12302 _ => {
12303 for (d, v) in dst.iter_mut().zip(&src[..n]) {
12304 *d += *v;
12305 }
12306 }
12307 }
12308}
12309
12310fn accum_outer(
12315 c: &mut [f32],
12316 m: usize,
12317 n: usize,
12318 b: usize,
12319 left: &[f32],
12320 right: &[f32],
12321 pool: Option<&Pool>,
12322) {
12323 const TILE: usize = 32;
12324 let tiles = m.div_ceil(TILE);
12325 let cp = SendMut(c.as_mut_ptr());
12326 let body = |ti: usize| {
12327 let cp = &cp;
12328 let i0 = ti * TILE;
12329 let i1 = (i0 + TILE).min(m);
12330 for t in 0..b {
12331 let r = &right[t * n..t * n + n];
12332 for i in i0..i1 {
12333 let a = left[i * b + t];
12334 if a == 0.0 {
12335 continue;
12336 }
12337 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
12339 for (o, v) in row.iter_mut().zip(r) {
12340 *o += a * *v;
12341 }
12342 }
12343 }
12344 };
12345 match pool {
12346 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
12347 for ti in s..e {
12348 body(ti);
12349 }
12350 }),
12351 _ => {
12352 for ti in 0..tiles {
12353 body(ti);
12354 }
12355 }
12356 }
12357}
12358
12359pub fn refit_flush() -> usize {
12361 let Some((dir, map)) = refit_dir() else {
12362 return 0;
12363 };
12364 let guard = map.lock().unwrap();
12365 let mut n = 0;
12366 for (li, acc) in guard.0.iter() {
12367 let w = |name: &str, v: &[f32]| {
12370 let path = format!("{dir}/{name}.{li}.f32");
12371 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
12372 match std::fs::write(&path, &bytes) {
12373 Ok(()) => {}
12374 Err(e) => eprintln!(
12375 "refit: FAILED to write {path} ({} MB): {e}",
12376 bytes.len() / 1_000_000
12377 ),
12378 }
12379 };
12380 w("gss", &acc.gss);
12381 w("ya", &acc.ya);
12382 println!(
12383 "refit L{li}: {} support, {} tokens, hidden {}",
12384 acc.support.len(),
12385 acc.tokens,
12386 acc.hidden
12387 );
12388 n += 1;
12389 }
12390 n
12391}
12392
12393fn adump_row(li: usize, g: &[f32]) {
12398 use std::io::Write as _;
12399 static FILES: std::sync::OnceLock<
12400 Option<(
12401 String,
12402 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
12403 )>,
12404 > = std::sync::OnceLock::new();
12405 let Some((prefix, map)) = FILES
12406 .get_or_init(|| {
12407 std::env::var("CMF_FFN_ADUMP")
12408 .ok()
12409 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
12410 })
12411 .as_ref()
12412 else {
12413 return;
12414 };
12415 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
12418 let (from, to) = *SPAN.get_or_init(|| {
12419 let g = |k: &str, d: usize| {
12420 std::env::var(k)
12421 .ok()
12422 .and_then(|v| v.parse().ok())
12423 .unwrap_or(d)
12424 };
12425 (
12426 g("CMF_FFN_ADUMP_FROM", 0),
12427 g("CMF_FFN_ADUMP_TO", usize::MAX),
12428 )
12429 });
12430 if li < from || li > to {
12431 return;
12432 }
12433 let mut map = map.lock().unwrap();
12434 let f = map.entry(li).or_insert_with(|| {
12435 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
12436 });
12437 let mut bytes = Vec::with_capacity(g.len() * 2);
12438 for v in g {
12439 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
12440 }
12441 let _ = f.write_all(&bytes);
12442}
12443
12444fn oracle_topk() -> usize {
12450 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12451 *K.get_or_init(|| {
12452 std::env::var("CMF_FFN_ORACLE_TOPK")
12453 .ok()
12454 .and_then(|v| v.parse().ok())
12455 .unwrap_or(0)
12456 })
12457}
12458
12459fn gate_topk() -> usize {
12465 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12466 *K.get_or_init(|| {
12467 std::env::var("CMF_FFN_GATE_TOPK")
12468 .ok()
12469 .and_then(|v| v.parse().ok())
12470 .unwrap_or(0)
12471 })
12472}
12473
12474fn gate_block() -> usize {
12481 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12482 *B.get_or_init(|| {
12483 std::env::var("CMF_FFN_GATE_BLOCK")
12484 .ok()
12485 .and_then(|v| v.parse().ok())
12486 .unwrap_or(1)
12487 })
12488}
12489
12490fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
12492 let n = g.len();
12493 let nb = n.div_ceil(block);
12494 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
12495 if kb >= nb {
12496 return;
12497 }
12498 let mut score: Vec<f32> = (0..nb)
12499 .map(|b| {
12500 g[b * block..((b + 1) * block).min(n)]
12501 .iter()
12502 .map(|v| v * v)
12503 .sum::<f32>()
12504 })
12505 .collect();
12506 let mut ord = score.clone();
12507 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
12508 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12509 });
12510 let thr = *kth;
12511 for b in 0..nb {
12512 if score[b] < thr {
12513 g[b * block..((b + 1) * block).min(n)].fill(0.0);
12514 }
12515 }
12516 score.clear();
12517}
12518
12519fn keep_top_k(g: &mut [f32], k: usize) {
12521 if gate_block() > 1 {
12522 return keep_top_blocks(g, k, gate_block());
12523 }
12524 let n = g.len();
12525 if k == 0 || k >= n {
12526 return;
12527 }
12528 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
12529 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12530 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12531 });
12532 let thr = *kth;
12533 for v in g.iter_mut() {
12534 if v.abs() < thr {
12535 *v = 0.0;
12536 }
12537 }
12538}
12539
12540fn probe_sq() -> bool {
12544 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12545 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
12546}
12547
12548fn probe_signed() -> bool {
12552 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12553 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12554}
12555
12556fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12564 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12565 M.get_or_init(|| {
12566 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12567 let b = std::fs::read(&p).ok()?;
12568 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12569 let vals: Vec<f32> = b[8..]
12570 .chunks_exact(4)
12571 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12572 .collect();
12573 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12574 Some((inter, vals))
12575 })
12576 .as_ref()
12577}
12578
12579fn probe_topk() -> usize {
12582 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12583 *K.get_or_init(|| {
12584 std::env::var("CMF_FFN_PROBE_TOPK")
12585 .ok()
12586 .and_then(|v| v.parse().ok())
12587 .unwrap_or(0)
12588 })
12589}
12590
12591thread_local! {
12592 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12595 const { std::cell::RefCell::new(None) };
12596}
12597
12598fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12611 if d.gate_proj.has_prism_contract()
12616 || d.up_proj.has_prism_contract()
12617 || d.down_proj.has_prism_contract()
12618 {
12619 return None;
12620 }
12621 let dt = d.down_t.as_ref()?;
12622 let inter = d.gate_proj.rows();
12623 let hidden = dt.cols();
12624 if k == 0 || k >= inter || d.act != Act::Silu {
12625 return None;
12626 }
12627 DYN_SCRATCH.with(|sc| {
12628 let mut sc = sc.borrow_mut();
12629 let DynScratch {
12630 g,
12631 mag,
12632 live,
12633 parts,
12634 } = &mut *sc;
12635 g.resize(inter, 0.0);
12636 d.gate_proj.matvec(x, g, pool);
12637 for v in g.iter_mut() {
12638 *v = inference::silu(*v);
12639 }
12640 mag.clear();
12643 mag.extend(g.iter().map(|v| v.abs()));
12644 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12645 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12646 });
12647 let thr = *kth;
12648 live.clear();
12649 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12650 let mut out = vec![0.0f32; hidden];
12651 match pool {
12652 Some(p) if live.len() >= 64 => {
12653 let nw = p.n_workers() + 1;
12654 parts.clear();
12655 parts.resize(nw * hidden, 0.0);
12656 let ptr = SendMut(parts.as_mut_ptr());
12657 let n = live.len();
12658 let live_ref: &[u32] = live;
12659 let g_ref: &[f32] = g;
12660 p.run(&|w, workers| {
12661 let chunk = n.div_ceil(workers);
12662 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12663 if s >= e {
12664 return;
12665 }
12666 WORKER_SCRATCH.with(|ws| {
12667 let mut ws = ws.borrow_mut();
12668 let [scratch, acc] = &mut *ws;
12669 scratch.resize(hidden.max(x.len()), 0.0);
12670 acc.clear();
12671 acc.resize(hidden, 0.0);
12672 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12673 if let Some(&nx) = live_ref[s..e].get(o + 1) {
12676 d.up_proj.prefetch_row(nx as usize);
12677 dt.prefetch_row(nx as usize);
12678 }
12679 let idx = nrm as usize;
12680 let up = d.up_proj.row_dot(idx, x, scratch);
12681 let a = g_ref[idx] * up;
12682 if a != 0.0 {
12683 dt.add_row_scaled(idx, a, acc, scratch);
12684 }
12685 }
12686 for (j, v) in acc.iter().enumerate() {
12687 unsafe { *ptr.at(w * hidden + j) = *v };
12688 }
12689 });
12690 });
12691 for w in 0..nw {
12692 for (j, o) in out.iter_mut().enumerate() {
12693 *o += parts[w * hidden + j];
12694 }
12695 }
12696 }
12697 _ => {
12698 WORKER_SCRATCH.with(|ws| {
12699 let mut ws = ws.borrow_mut();
12700 let [scratch, _acc] = &mut *ws;
12701 scratch.resize(hidden.max(x.len()), 0.0);
12702 for &nrm in live.iter() {
12703 let idx = nrm as usize;
12704 let up = d.up_proj.row_dot(idx, x, scratch);
12705 let a = g[idx] * up;
12706 if a != 0.0 {
12707 dt.add_row_scaled(idx, a, &mut out, scratch);
12708 }
12709 }
12710 });
12711 }
12712 }
12713 Some(out)
12714 })
12715}
12716
12717struct DynScratch {
12720 g: Vec<f32>,
12721 mag: Vec<f32>,
12722 live: Vec<u32>,
12723 parts: Vec<f32>,
12724}
12725
12726thread_local! {
12727 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12728 std::cell::RefCell::new(DynScratch {
12729 g: Vec::new(),
12730 mag: Vec::new(),
12731 live: Vec::new(),
12732 parts: Vec::new(),
12733 })
12734 };
12735 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12737 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12738}
12739
12740fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12745 let inter = d.gate_proj.rows();
12746 FFN_SCRATCH.with(|s| {
12747 let mut s = s.borrow_mut();
12748 let [g, u, ..] = &mut *s;
12749 g.resize(inter, 0.0);
12750 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12751 } else {
12753 u.resize(inter, 0.0);
12754 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12755 for i in 0..inter {
12756 g[i] = d.act.combine(g[i], u[i]);
12757 }
12758 }
12759 zero_masked_cols(g, 1, inter, mask_row);
12760 let mut out = attention::take_buf(d.down_proj.rows());
12761 d.down_proj.matvec(g, &mut out, pool);
12762 out
12763 })
12764}
12765
12766fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12772 if d.gate_proj.has_prism_contract()
12773 || d.up_proj.has_prism_contract()
12774 || d.down_proj.has_prism_contract()
12775 {
12776 return None;
12777 }
12778 if d.act != Act::Silu {
12780 return None;
12781 }
12782 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12785 return None;
12786 }
12787 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12788 let mut model_ref = None;
12789 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12790 let model = model_ref?;
12791 let hidden = jobs[0].down.1;
12792 let mut out = attention::take_buf(hidden);
12793 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12794 Some(out)
12795 } else {
12796 let mut out = out;
12797 attention::recycle_buf(&mut out);
12798 None
12799 }
12800}
12801
12802#[allow(clippy::type_complexity)]
12807#[allow(clippy::type_complexity)]
12808pub(crate) fn moe_parts(
12809 t: &QTensor,
12810) -> Option<(
12811 &std::sync::Arc<cortiq_core::CmfModel>,
12812 usize,
12813 usize,
12814 usize,
12815 &[f32],
12816 &[f32],
12817 bool,
12818 bool,
12819 bool,
12820)> {
12821 match t {
12822 QTensor::Mapped {
12823 model,
12824 idx,
12825 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12826 rows,
12827 cols,
12828 row_scale,
12829 col_field,
12830 ..
12831 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12832 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12833 )),
12834 QTensor::Mapped {
12836 model,
12837 idx,
12838 dtype: cortiq_core::TensorDtype::Q1,
12839 rows,
12840 cols,
12841 ..
12842 } => Some((
12843 model,
12844 *idx,
12845 *rows,
12846 *cols,
12847 &[][..],
12848 &[][..],
12849 true,
12850 false,
12851 false,
12852 )),
12853 QTensor::Mapped {
12855 model,
12856 idx,
12857 dtype: cortiq_core::TensorDtype::Q4Tiled,
12858 rows,
12859 cols,
12860 ..
12861 } => Some((
12862 model,
12863 *idx,
12864 *rows,
12865 *cols,
12866 &[][..],
12867 &[][..],
12868 false,
12869 true,
12870 false,
12871 )),
12872 QTensor::Mapped {
12874 model,
12875 idx,
12876 dtype: cortiq_core::TensorDtype::Q4TiledP,
12877 rows,
12878 cols,
12879 ..
12880 } => Some((
12881 model,
12882 *idx,
12883 *rows,
12884 *cols,
12885 &[][..],
12886 &[][..],
12887 false,
12888 true,
12889 false,
12890 )),
12891 QTensor::Mapped {
12895 model,
12896 idx,
12897 dtype: cortiq_core::TensorDtype::Q2TiledP,
12898 rows,
12899 cols,
12900 ..
12901 } => Some((
12902 model,
12903 *idx,
12904 *rows,
12905 *cols,
12906 &[][..],
12907 &[][..],
12908 false,
12909 true,
12910 true,
12911 )),
12912 _ => None,
12913 }
12914}
12915
12916#[cfg(target_os = "macos")]
12922fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12923 if m.router_sigmoid
12924 || m.router_input_norm
12925 || m.expert_bias.is_some()
12926 || m.route_tau.is_some()
12927 || m.mask.is_some()
12928 || m.per_expert_scale.is_some()
12929 || m.experts.is_empty()
12930 || m.top_k == 0
12931 || m.resonance.is_some()
12932 {
12933 return None;
12934 }
12935 let (sh, sg) = match &m.shared {
12938 Some((sh, Some(sg))) => (sh, sg),
12939 _ => return None,
12940 };
12941 let (rf, rr, rc) = m.router.f32_parts()?;
12942 if rr != m.experts.len() || rc != hidden {
12943 return None;
12944 }
12945 let (sf, sr, sc) = sg.f32_parts()?;
12946 if sr * sc != hidden {
12947 return None;
12948 }
12949 let inter = m.experts[0].gate_proj.rows();
12950 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12953 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12954 if e.act != Act::Silu
12955 || e.gate_proj.rows() != inter
12956 || e.gate_proj.cols() != hidden
12957 || e.up_proj.rows() != inter
12958 || e.up_proj.cols() != hidden
12959 || e.down_proj.rows() != hidden
12960 || e.down_proj.cols() != inter
12961 {
12962 return None;
12963 }
12964 let pick = |t: &QTensor| -> Option<usize> {
12965 if gu_q2 {
12966 t.mapped_q2tp().map(|(_, i)| i)
12967 } else {
12968 t.mapped_q4tp().map(|(_, i)| i)
12969 }
12970 };
12971 Some((
12972 pick(&e.gate_proj)?,
12973 pick(&e.up_proj)?,
12974 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
12975 ))
12976 };
12977 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
12978 let shared = trio(sh)?;
12979 Some(crate::gpu::GpuMoe {
12980 router: rf,
12981 sgate: sf,
12982 experts,
12983 shared,
12984 n_exp: m.experts.len(),
12985 top_k: m.top_k,
12986 inter,
12987 norm_topk: m.norm_topk_prob,
12988 route_scale: m.routed_scaling,
12989 gu_q2,
12990 })
12991}
12992
12993pub(crate) fn moe_push_job_parts<'a>(
12997 gate: &'a QTensor,
12998 up: &'a QTensor,
12999 down: &'a QTensor,
13000 x: &[f32],
13001 w: f32,
13002 swiglu_limit: f32,
13003 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13004 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13005) -> Option<()> {
13006 use crate::qtensor::prescale;
13007 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
13008 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
13009 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
13010 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13011 return None; }
13013 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
13016 return None;
13017 }
13018 if !gq2 && dq2 {
13019 return None;
13020 }
13021 model_ref.get_or_insert_with(|| gm.clone());
13022 let dt = |cf: &[f32]| {
13023 if cf.is_empty() {
13024 cortiq_core::TensorDtype::Q8Row
13025 } else {
13026 cortiq_core::TensorDtype::Q8_2f
13027 }
13028 };
13029 jobs.push(crate::gpu::MoeJob {
13030 gate: (gi, gr, gc, grs),
13031 up: (ui, ur, uc, urs),
13032 down: (di, dr, dc, drs),
13033 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
13034 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
13035 down_col: dcf,
13036 w,
13037 q1: gq1,
13038 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
13039 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
13040 gu_q2: gq2,
13041 swiglu_limit,
13042 });
13043 Some(())
13044}
13045
13046fn moe_push_job<'a>(
13048 d: &'a DenseFfn,
13049 x: &[f32],
13050 w: f32,
13051 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
13052 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
13053) -> Option<()> {
13054 use crate::qtensor::prescale;
13055 if d.act != Act::Silu {
13056 return None; }
13058 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
13059 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
13060 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
13061 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
13062 return None; }
13064 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
13065 return None;
13066 }
13067 if !gq2 && dq2 {
13068 return None;
13069 }
13070 model_ref.get_or_insert_with(|| gm.clone());
13071 let gdt = if gcf.is_empty() {
13072 cortiq_core::TensorDtype::Q8Row
13073 } else {
13074 cortiq_core::TensorDtype::Q8_2f
13075 };
13076 let udt = if ucf.is_empty() {
13077 cortiq_core::TensorDtype::Q8Row
13078 } else {
13079 cortiq_core::TensorDtype::Q8_2f
13080 };
13081 jobs.push(crate::gpu::MoeJob {
13082 gate: (gi, gr, gc, grs),
13083 up: (ui, ur, uc, urs),
13084 down: (di, dr, dc, drs),
13085 xs_gate: prescale(x, gcf, gdt).into_owned(),
13086 xs_up: prescale(x, ucf, udt).into_owned(),
13087 down_col: dcf,
13088 w,
13089 q1: gq1,
13090 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
13091 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
13092 gu_q2: gq2,
13093 swiglu_limit: 0.0,
13094 });
13095 Some(())
13096}
13097
13098fn sparse_ffn_quant(
13105 d: &DenseFfn,
13106 x: &[f32],
13107 active: &[u16],
13108 hidden: usize,
13109 pool: Option<&Pool>,
13110) -> Vec<f32> {
13111 let n = active.len();
13112 let inter = d.gate_proj.rows();
13113 let mut act = vec![0.0f32; n];
13114 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
13117 let compute = |ai: usize| -> f32 {
13118 let idx = active[ai] as usize;
13119 if idx >= inter {
13120 return 0.0; }
13122 let mut s = if need_scratch {
13123 vec![0.0f32; hidden]
13124 } else {
13125 Vec::new()
13126 };
13127 let gate = d.gate_proj.row_dot(idx, x, &mut s);
13128 let up = d.up_proj.row_dot(idx, x, &mut s);
13129 d.act.combine(gate, up)
13130 };
13131 match pool {
13132 Some(p) if n >= 256 => {
13133 let ptr = SendMut(act.as_mut_ptr());
13134 p.run(&|widx, nw| {
13135 let chunk = n.div_ceil(nw);
13136 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
13137 for ai in s..e {
13138 unsafe { *ptr.at(ai) = compute(ai) };
13139 }
13140 });
13141 }
13142 _ => {
13143 for (ai, a) in act.iter_mut().enumerate() {
13144 *a = compute(ai);
13145 }
13146 }
13147 }
13148 let mut out = vec![0.0f32; hidden];
13150 for (ai, &idx) in active.iter().enumerate() {
13151 let w = act[ai];
13152 if w.abs() >= 1e-12 && (idx as usize) < inter {
13153 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
13154 }
13155 }
13156 out
13157}
13158
13159#[doc(hidden)]
13161pub fn sparse_ffn_quant_for_test(
13162 d: &DenseFfn,
13163 x: &[f32],
13164 active: &[u16],
13165 hidden: usize,
13166) -> Vec<f32> {
13167 sparse_ffn_quant(d, x, active, hidden, None)
13168}
13169
13170fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
13174 let deq = |t: &QTensor| -> Vec<f32> {
13175 let (rows, cols) = (t.rows(), t.cols());
13176 let mut out = vec![0.0f32; rows * cols];
13177 for r in 0..rows {
13178 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
13179 }
13180 out
13181 };
13182 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
13183}
13184
13185struct SendMut(*mut f32);
13187unsafe impl Send for SendMut {}
13188unsafe impl Sync for SendMut {}
13189impl SendMut {
13190 #[inline]
13191 #[allow(clippy::mut_from_ref)]
13194 unsafe fn at(&self, i: usize) -> &mut f32 {
13195 unsafe { &mut *self.0.add(i) }
13196 }
13197}
13198
13199pub(crate) fn moe_route(
13209 logits: &[f32],
13210 m: &MoeFfn,
13211 allowed: Option<&[bool]>,
13212) -> (Vec<usize>, Vec<f32>, f32) {
13213 let ne = logits.len();
13214 let p: Vec<f32> = if m.router_sigmoid {
13215 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
13216 } else {
13217 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13218 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
13219 let s: f32 = e.iter().sum();
13220 for v in &mut e {
13221 *v /= s;
13222 }
13223 e
13224 };
13225 let admit = |e: usize| {
13231 m.mask.as_ref().is_none_or(|mk| mk[e])
13232 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
13233 };
13234 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
13235 match &m.expert_bias {
13237 Some(b) => idx.sort_unstable_by(|&x, &y| {
13238 (p[y] + b[y])
13239 .partial_cmp(&(p[x] + b[x]))
13240 .unwrap()
13241 .then(x.cmp(&y))
13242 }),
13243 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
13244 }
13245 idx.truncate(m.top_k);
13246 if let Some(tau) = m.route_tau {
13250 let total: f32 = idx.iter().map(|&e| p[e]).sum();
13251 if total > 0.0 {
13252 let mut acc = 0.0f32;
13253 let mut keep = idx.len();
13254 for (i, &e) in idx.iter().enumerate() {
13255 acc += p[e];
13256 if acc >= tau * total {
13257 keep = i + 1;
13258 break;
13259 }
13260 }
13261 idx.truncate(keep);
13262 }
13263 }
13264 let wsum: f32 = if m.norm_topk_prob {
13265 let s: f32 = idx.iter().map(|&e| p[e]).sum();
13266 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
13269 } else {
13270 1.0 / m.routed_scaling
13271 };
13272 (idx, p, wsum)
13273}
13274
13275fn moe_trace(idx: &[usize]) {
13277 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
13278}
13279
13280pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
13283 use std::io::Write;
13284 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
13285 std::sync::OnceLock::new();
13286 let Some(f) = F.get_or_init(|| {
13287 let p = std::env::var("CMF_MOE_TRACE").ok()?;
13288 Some(std::sync::Mutex::new(
13289 std::fs::OpenOptions::new()
13290 .create(true)
13291 .append(true)
13292 .open(p)
13293 .ok()?,
13294 ))
13295 }) else {
13296 return;
13297 };
13298 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
13299 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
13300}
13301
13302pub(crate) fn moe_ffn(
13305 m: &MoeFfn,
13306 x: &[f32],
13307 pool: Option<&Pool>,
13308 allowed: Option<&[bool]>,
13309) -> Vec<f32> {
13310 accumulate_act(m, x, 1);
13311 let ne = m.experts.len();
13312 let mut logits = vec![0.0f32; ne];
13313 match &m.resonance {
13314 Some(r) => r.scores(x, &mut logits),
13315 None => m.router.matvec(x, &mut logits, pool),
13316 }
13317 let (idx, p, wsum) = moe_route(&logits, m, allowed);
13318 {
13319 let mut st = m.stats.borrow_mut();
13320 if st.len() < ne {
13321 st.resize(ne, 0);
13322 }
13323 for &e in &idx {
13324 st[e] += 1;
13325 }
13326 }
13327 moe_trace(&idx);
13333 if crate::gpu::enabled_here() {
13338 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
13339 crate::gpu::ProbeArm::Gpu => {
13340 let t0 = std::time::Instant::now();
13341 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
13342 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
13343 return out;
13344 }
13345 }
13346 crate::gpu::ProbeArm::CpuTimed => {
13347 let t0 = std::time::Instant::now();
13348 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13349 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
13350 return out;
13351 }
13352 crate::gpu::ProbeArm::Cpu => {
13353 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
13354 }
13355 }
13356 }
13357 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
13358}
13359
13360fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
13365 use std::sync::atomic::{AtomicBool, Ordering};
13366 if built {
13367 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
13368 if total_layers > 0 && layers_run < total_layers {
13369 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
13370 } else {
13371 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
13372 }
13373 } else {
13374 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
13375 }
13376 static SAID: AtomicBool = AtomicBool::new(false);
13377 if !SAID.swap(true, Ordering::Relaxed) {
13378 if built {
13379 tracing::info!("wgpu whole-token graph: ACTIVE");
13380 } else {
13381 tracing::warn!("wgpu whole-token graph refused — per-op path");
13382 }
13383 }
13384}
13385
13386pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13390pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13391pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13395pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
13397
13398pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
13402 std::sync::atomic::AtomicU64::new(0);
13403pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
13404 std::sync::atomic::AtomicU64::new(0);
13405pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
13406 std::sync::atomic::AtomicU64::new(0);
13407pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
13408 std::sync::atomic::AtomicU64::new(0);
13409pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
13410 std::sync::atomic::AtomicU64::new(0);
13411pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
13415 std::sync::atomic::AtomicU64::new(0);
13416pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
13417 std::sync::atomic::AtomicU64::new(0);
13418pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
13419 std::sync::atomic::AtomicU64::new(0);
13420pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
13421 std::sync::atomic::AtomicU64::new(0);
13422
13423fn moe_batch_enabled() -> bool {
13426 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13427 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
13428}
13429
13430fn moe_ffn_cpu_batched(
13436 m: &MoeFfn,
13437 x: &[f32],
13438 idx: &[usize],
13439 p: &[f32],
13440 wsum: f32,
13441 pool: Option<&Pool>,
13442) -> Option<Vec<f32>> {
13443 if idx.is_empty() || !moe_batch_enabled() {
13444 return None;
13445 }
13446 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
13450 return None;
13451 }
13452 let n = idx.len() + usize::from(m.shared.is_some());
13453 let mut pairs = Vec::with_capacity(n);
13454 let mut downs = Vec::with_capacity(n);
13455 let mut ws = Vec::with_capacity(n);
13456 for &e in idx {
13457 let d = &m.experts[e];
13458 if d.act != Act::Silu {
13459 return None;
13460 }
13461 pairs.push((&d.gate_proj, &d.up_proj));
13462 downs.push(&d.down_proj);
13463 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
13464 }
13465 if let Some((se, gate)) = &m.shared {
13468 if se.act != Act::Silu {
13469 return None;
13470 }
13471 let g = gate.as_ref().map_or(1.0, |gate| {
13472 let mut gl = [0.0f32; 1];
13473 gate.matvec(x, &mut gl, pool);
13474 1.0 / (1.0 + (-gl[0]).exp())
13475 });
13476 pairs.push((&se.gate_proj, &se.up_proj));
13477 downs.push(&se.down_proj);
13478 ws.push(g);
13479 }
13480 let inter = pairs[0].0.rows();
13481 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
13482 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
13483 return None;
13484 }
13485 let mut out = attention::take_buf(x.len());
13486 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
13487 attention::recycle_buf(&mut out);
13488 return None;
13489 }
13490 Some(out)
13491}
13492
13493pub(crate) fn moe_cold_experts_cpu(
13499 experts: &[(&DenseFfn, f32)],
13500 x: &[f32],
13501 pool: Option<&Pool>,
13502) -> Vec<f32> {
13503 let mut out = attention::take_buf(x.len());
13504 if experts.is_empty() {
13505 return out;
13506 }
13507 let pairs: Vec<_> = experts
13508 .iter()
13509 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
13510 .collect();
13511 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
13512 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
13513 let inter = experts[0].0.gate_proj.rows();
13514 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
13515 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
13516 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
13517 {
13518 return out;
13519 }
13520 out.fill(0.0);
13521 for &(expert, weight) in experts {
13522 let mut one = dense_ffn(expert, x, pool);
13523 for (o, v) in out.iter_mut().zip(&one) {
13524 *o += weight * v;
13525 }
13526 attention::recycle_buf(&mut one);
13527 }
13528 out
13529}
13530
13531fn moe_ffn_cpu(
13533 m: &MoeFfn,
13534 x: &[f32],
13535 idx: &[usize],
13536 p: &[f32],
13537 wsum: f32,
13538 pool: Option<&Pool>,
13539) -> Vec<f32> {
13540 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
13541 return out;
13542 }
13543 let mut out = attention::take_buf(x.len());
13544 for &e in idx {
13545 let mut eo = dense_ffn(&m.experts[e], x, pool);
13546 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
13547 for i in 0..out.len() {
13548 out[i] += w * eo[i];
13549 }
13550 attention::recycle_buf(&mut eo);
13551 }
13552 if let Some((se, gate)) = &m.shared {
13553 let mut so = dense_ffn(se, x, pool);
13554 let g = gate.as_ref().map_or(1.0, |gate| {
13555 let mut gl = [0.0f32; 1];
13556 gate.matvec(x, &mut gl, pool);
13557 1.0 / (1.0 + (-gl[0]).exp())
13558 });
13559 for i in 0..out.len() {
13560 out[i] += g * so[i];
13561 }
13562 attention::recycle_buf(&mut so);
13563 }
13564 out
13565}
13566
13567#[allow(clippy::too_many_arguments)]
13575fn mla_attention(
13576 w: &MlaWeights,
13577 normed: &[f32],
13578 cache: &mut crate::kv_cache::LayerKvCache,
13579 position: usize,
13580 inv_freq: &[f32],
13581 rope_scale: f32,
13582 eps: f64,
13583 pool: Option<&Pool>,
13584) -> Vec<f32> {
13585 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
13586 let hd = dr + dn;
13587 let mut q = vec![0.0f32; nh * hd];
13588 match (&w.q_a, &w.q_a_norm) {
13589 (Some(qa), Some(qn)) => {
13590 let mut t = vec![0.0f32; qa.rows()];
13591 qa.matvec(normed, &mut t, pool);
13592 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13593 w.q_proj.matvec(&tn, &mut q, pool);
13594 }
13595 _ => w.q_proj.matvec(normed, &mut q, pool),
13596 }
13597 let mut ca = vec![0.0f32; lora + dr];
13598 w.kv_a.matvec(normed, &mut ca, pool);
13599 let (c_lat, k_rope) = ca.split_at_mut(lora);
13600 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13601 let mut kvb = vec![0.0f32; nh * (dn + dv)];
13602 w.kv_b.matvec(&latn, &mut kvb, pool);
13603 if !w.nope {
13604 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13605 }
13606 for h in 0..nh {
13607 if !w.nope {
13608 attention::rope_rotate_scaled(
13609 &mut q[h * hd..h * hd + dr],
13610 position,
13611 inv_freq,
13612 rope_scale,
13613 );
13614 }
13615 }
13616 let mut k = vec![0.0f32; nh * hd];
13617 let mut v = vec![0.0f32; nh * hd];
13618 for h in 0..nh {
13619 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13620 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13621 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13622 }
13623 cache.append(&k, &v, &vec![true; nh]);
13624 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13625 attention::recycle_buf(&mut imp);
13626 let mut ov = vec![0.0f32; nh * dv];
13627 for h in 0..nh {
13628 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13629 }
13630 let mut out = vec![0.0f32; w.o_proj.rows()];
13631 w.o_proj.matvec(&ov, &mut out, pool);
13632 out
13633}
13634
13635fn dense_moe_ffn(
13642 dm: &DenseMoeFfn,
13643 x_normed: &[f32],
13644 h_raw: &[f32],
13645 eps: f64,
13646 norm_style: NormStyle,
13647 pool: Option<&Pool>,
13648) -> Vec<f32> {
13649 let mut d = dense_ffn(&dm.dense, x_normed, pool);
13650 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13651 let m = &dm.moe;
13652 let ne = m.experts.len();
13653 let mut logits = vec![0.0f32; ne];
13654 if m.router_input_norm {
13655 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13656 let inv = 1.0 / (ss + eps as f32).sqrt();
13657 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13658 m.router.matvec(&xr, &mut logits, pool);
13659 } else {
13660 m.router.matvec(h_raw, &mut logits, pool);
13661 }
13662 let (idx, p, wsum) = moe_route(&logits, m, None);
13663 {
13664 let mut st = m.stats.borrow_mut();
13665 if st.len() < ne {
13666 st.resize(ne, 0);
13667 }
13668 for &e in &idx {
13669 st[e] += 1;
13670 }
13671 }
13672 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13673 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13674 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13675 for (di, mi) in d.iter_mut().zip(&mo) {
13676 *di += mi;
13677 }
13678 d
13679}
13680
13681fn moe_gpu_refused(why: &'static str) {
13688 use std::sync::atomic::{AtomicBool, Ordering};
13689 static SAID: AtomicBool = AtomicBool::new(false);
13690 if !SAID.swap(true, Ordering::Relaxed) {
13691 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13692 }
13693}
13694
13695fn moe_ffn_gpu(
13696 m: &MoeFfn,
13697 x: &[f32],
13698 idx: &[usize],
13699 p: &[f32],
13700 wsum: f32,
13701 pool: Option<&Pool>,
13702) -> Option<Vec<f32>> {
13703 use crate::gpu::MoeJob;
13704
13705 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13706 let mut model_ref = None;
13707 for &e in idx {
13708 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13709 moe_gpu_refused("push_job(expert)");
13710 return None;
13711 }
13712 }
13713 if let Some((se, gate)) = &m.shared {
13714 let g = gate.as_ref().map_or(1.0, |gate| {
13715 let mut gl = [0.0f32; 1];
13716 gate.matvec(x, &mut gl, pool);
13717 1.0 / (1.0 + (-gl[0]).exp())
13718 });
13719 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13720 moe_gpu_refused("push_job(shared)");
13721 return None;
13722 }
13723 }
13724 let Some(model) = model_ref else {
13725 moe_gpu_refused("no model_ref");
13726 return None;
13727 };
13728 let hidden = jobs[0].down.1;
13729 let mut out = vec![0.0f32; hidden];
13730 if crate::gpu::moe_block(&model, &jobs, &mut out) {
13731 Some(out)
13732 } else {
13733 moe_gpu_refused("gpu::moe_block");
13734 None
13735 }
13736}
13737
13738fn ffn_forward(
13740 ffn: &FfnKind,
13741 x: &[f32],
13742 pool: Option<&Pool>,
13743 experts_allowed: Option<&[bool]>,
13744) -> Vec<f32> {
13745 match ffn {
13746 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13747 FfnKind::Dense(d) => dense_ffn(d, x, pool),
13748 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13749 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13753 }
13754}
13755
13756fn ffn_forward_pair(
13760 ffn: &FfnKind,
13761 x1: &[f32],
13762 x2: &[f32],
13763 pool: Option<&Pool>,
13764 experts_allowed: Option<&[bool]>,
13765) -> (Vec<f32>, Vec<f32>) {
13766 let d = match ffn {
13767 FfnKind::Dense(d) if !d.segs.is_empty() => {
13770 return (
13771 tube_ffn(d, x1, 1, pool, None),
13772 tube_ffn(d, x2, 1, pool, None),
13773 );
13774 }
13775 FfnKind::Dense(d) => d,
13776 FfnKind::Moe(m) => {
13777 return (
13778 moe_ffn(m, x1, pool, experts_allowed),
13779 moe_ffn(m, x2, pool, experts_allowed),
13780 );
13781 }
13782 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13783 };
13784 let inter = d.gate_proj.rows();
13785 FFN_SCRATCH.with(|s| {
13786 let mut s = s.borrow_mut();
13787 let [g1, g2, u1, u2] = &mut *s;
13788 g1.resize(inter, 0.0);
13789 g2.resize(inter, 0.0);
13790 u1.resize(inter, 0.0);
13791 u2.resize(inter, 0.0);
13792 QTensor::matvec2_many(
13795 [&d.gate_proj, &d.up_proj],
13796 x1,
13797 x2,
13798 [g1.as_mut_slice(), u1.as_mut_slice()],
13799 [g2.as_mut_slice(), u2.as_mut_slice()],
13800 pool,
13801 );
13802 for i in 0..inter {
13803 g1[i] = d.act.combine(g1[i], u1[i]);
13804 g2[i] = d.act.combine(g2[i], u2[i]);
13805 }
13806 let mut o1 = attention::take_buf(d.down_proj.rows());
13807 let mut o2 = attention::take_buf(d.down_proj.rows());
13808 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13809 (o1, o2)
13810 })
13811}
13812
13813#[cfg(test)]
13814mod tests {
13815
13816 #[test]
13817 fn nll_graph_policy_scopes_only_the_fused_head() {
13818 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
13819 ("vulkan graph", true, true, false, true, false),
13821 ("native Metal graph", true, true, true, true, true),
13823 ("masked", false, true, false, false, false),
13825 ("graph disabled", true, false, true, false, false),
13826 ] {
13827 let (graph_quality, graph_head_required) =
13828 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
13829 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
13830 assert_eq!(graph_head_required, want_head, "{label}: fused head");
13831 }
13832 }
13833
13834 #[test]
13835 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13836 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13837 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13838 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13839 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13840 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13841 }
13842
13843 #[test]
13844 fn cancel_flag_stops_generation() {
13845 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13846 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13849 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13850 assert_eq!(r.finish_reason, "cancelled");
13851 assert!(
13852 r.token_ids.is_empty(),
13853 "no tokens after cancel: {:?}",
13854 r.token_ids
13855 );
13856 assert_eq!(p.kv_cache.seq_len(), 0);
13857 assert!(p.kv_history.is_empty());
13858 assert!(!p.graph_want_logits);
13859 assert!(p.graph_logits.is_none());
13860 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13862 assert_ne!(r2.finish_reason, "cancelled");
13863 }
13864 use super::*;
13865
13866 #[test]
13874 fn dynamic_ffn_equals_the_zeroing_arm() {
13875 let (hidden, inter) = (8usize, 32usize);
13876 let synth = |n: usize, salt: usize| -> Vec<f32> {
13877 (0..n)
13878 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13879 .collect()
13880 };
13881 let down = synth(hidden * inter, 3);
13882 let mut down_t = vec![0.0f32; inter * hidden];
13883 for r in 0..hidden {
13884 for c in 0..inter {
13885 down_t[c * hidden + r] = down[r * inter + c];
13886 }
13887 }
13888 let d = DenseFfn {
13889 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13890 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13891 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13892 act: Act::Silu,
13893 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13894 segs: Vec::new(),
13895 };
13896 let x = synth(hidden, 11);
13897 let k = 12usize;
13898 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13899 let mut g = vec![0.0f32; inter];
13901 d.gate_proj.matvec(&x, &mut g, None);
13902 let mut u = vec![0.0f32; inter];
13903 d.up_proj.matvec(&x, &mut u, None);
13904 for v in g.iter_mut() {
13905 *v = inference::silu(*v);
13906 }
13907 keep_top_k(&mut g, k);
13908 for i in 0..inter {
13909 g[i] *= u[i];
13910 }
13911 let mut want = vec![0.0f32; hidden];
13912 d.down_proj.matvec(&g, &mut want, None);
13913 for (a, b) in want.iter().zip(&got) {
13914 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13915 }
13916 }
13917
13918 #[test]
13924 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13925 let (hidden, core, tube) = (8usize, 12usize, 8usize);
13926 let inter = core + tube;
13927 let synth = |n: usize, salt: usize| -> Vec<f32> {
13928 (0..n)
13929 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13930 .collect()
13931 };
13932 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13933 let d_all = synth(hidden * inter, 3);
13934 let dense = DenseFfn {
13936 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13937 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13938 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13939 act: Act::Silu,
13940 down_t: None,
13941 segs: Vec::new(),
13942 };
13943 let rows =
13944 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13945 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13946 let mut o = Vec::with_capacity(hidden * (b - a));
13947 for r in 0..hidden {
13948 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13949 }
13950 o
13951 };
13952 let tubed = DenseFfn {
13953 down_t: None,
13954 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
13955 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
13956 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
13957 act: Act::Silu,
13958 segs: vec![FfnSeg {
13959 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
13960 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
13961 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
13962 start: core,
13963 width: tube,
13964 }],
13965 };
13966 let x = synth(hidden, 7);
13967 let want = dense_ffn(&dense, &x, None);
13968 let got = tube_ffn(&tubed, &x, 1, None, None);
13969 for (a, b) in want.iter().zip(&got) {
13970 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
13971 }
13972 let mut bits = vec![0u8; inter.div_ceil(8)];
13974 for n in 0..core {
13975 bits[n / 8] |= 1 << (n % 8);
13976 }
13977 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13978 let masked = dense_ffn_masked(&dense, &x, None, &bits);
13979 for (a, b) in masked.iter().zip(&closed) {
13980 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
13981 }
13982 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13984 for (a, b) in closed.iter().zip(&batch) {
13985 assert_eq!(a, b, "batch arm disagrees with decode arm");
13986 }
13987 }
13988
13989 #[test]
13991 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
13992 let (hidden, inter) = (16usize, 40usize);
13993 let synth = |n: usize, salt: usize| -> Vec<f32> {
13994 (0..n)
13995 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
13996 .collect()
13997 };
13998 let d = DenseFfn {
13999 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
14000 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
14001 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
14002 act: Act::Silu,
14003 down_t: None,
14004 segs: Vec::new(),
14005 };
14006 let x = synth(hidden, 9);
14007 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
14009
14010 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
14011
14012 let mut g = vec![0.0f32; inter];
14014 d.gate_proj.matvec(&x, &mut g, None);
14015 let mut u = vec![0.0f32; inter];
14016 d.up_proj.matvec(&x, &mut u, None);
14017 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
14018 for i in 0..inter {
14019 g[i] = if act_set.contains(&(i as u16)) {
14020 inference::silu(g[i]) * u[i]
14021 } else {
14022 0.0
14023 };
14024 }
14025 let mut reference = vec![0.0f32; hidden];
14026 d.down_proj.matvec(&g, &mut reference, None);
14027
14028 let max_d = sparse
14029 .iter()
14030 .zip(&reference)
14031 .map(|(a, b)| (a - b).abs())
14032 .fold(0.0f32, f32::max);
14033 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
14034 }
14035
14036 fn attach_test_mtp(p: &mut Pipeline) {
14038 let (h, inter, heads, kv, hd) = (
14039 p.hidden_size,
14040 p.intermediate_size,
14041 p.num_heads,
14042 p.num_kv_heads,
14043 p.head_dim,
14044 );
14045 let synth = |n: usize, salt: usize| -> Vec<f32> {
14046 (0..n)
14047 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
14048 .collect()
14049 };
14050 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
14051 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14052 };
14053 p.mtp = Some(MtpModule {
14054 enorm: vec![1.0; h],
14055 hnorm: vec![1.0; h],
14056 eh_proj: qt(h, 2 * h, 301),
14057 layer: LayerWeights {
14058 input_norm: vec![1.0; h],
14059 post_norm: vec![1.0; h],
14060 attn_out_norm: None,
14061 ffn_out_norm: None,
14062 layer_scale: None,
14063 ffn: FfnKind::Dense(DenseFfn {
14064 gate_proj: qt(inter, h, 315),
14065 up_proj: qt(inter, h, 316),
14066 down_proj: qt(h, inter, 317),
14067 act: Act::Silu,
14068 down_t: None,
14069 segs: Vec::new(),
14070 }),
14071 attn: AttnKind::Full {
14072 bias: None,
14073 wq: qt(heads * hd, h, 311),
14074 wk: qt(kv * hd, h, 312),
14075 wv: qt(kv * hd, h, 313),
14076 wo: qt(h, heads * hd, 314),
14077 q_norm: None,
14078 k_norm: None,
14079 output_gate: false,
14080 softplus_gate: None,
14081 },
14082 },
14083 final_norm: vec![1.0; h],
14084 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
14085 });
14086 }
14087
14088 #[test]
14089 fn speculative_equals_vanilla_greedy() {
14090 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14094 let run = |spec: bool| {
14095 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14096 p.sampler_config.temperature = 0.0;
14097 attach_test_mtp(&mut p);
14098 p.speculative = spec;
14099 let r = p.generate("abcdef", 12, None, None).unwrap();
14100 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
14101 };
14102 let (vanilla, d0, _) = run(false);
14103 let (spec, d1, a1) = run(true);
14104 assert_eq!(d0, 0, "vanilla path must not draft");
14105 assert!(d1 > 0, "speculative path must draft");
14106 assert_eq!(
14107 vanilla, spec,
14108 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
14109 );
14110 }
14111
14112 #[test]
14113 fn speculative_accepts_constant_oracle() {
14114 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
14116 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14117 p.sampler_config.temperature = 0.0;
14118 p.sampler_config.repetition_penalty = 1.0;
14119 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
14122 attach_test_mtp(&mut p);
14123 p.speculative = true;
14124 let r = p.generate("abcd", 10, None, None).unwrap();
14125 assert!(r.mtp_drafted > 0);
14126 assert_eq!(
14127 r.mtp_accepted, r.mtp_drafted,
14128 "constant logits → every draft accepted"
14129 );
14130 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
14133 }
14134
14135 #[test]
14136 fn empty_prompt_is_an_error_not_a_panic() {
14137 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14138 let r = p.generate("", 4, None, None);
14139 assert!(r.is_err(), "empty prompt must be a clean error");
14140 }
14141
14142 #[test]
14143 fn every_token_enters_kv_exactly_once() {
14144 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14145 p.sampler_config.temperature = 0.0;
14147 let r = p.generate("abc", 2, None, None).unwrap();
14148 assert_eq!(r.prompt_tokens, 3);
14149 assert_eq!(
14153 p.kv_cache.seq_len(),
14154 3 + r.tokens_generated - 1,
14155 "each token must be cached exactly once (v1 cached the last prompt token twice)"
14156 );
14157 }
14158
14159 #[test]
14160 fn generation_is_reproducible_with_seed() {
14161 let run = || {
14162 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14163 p.generate("hello", 8, None, None).unwrap().token_ids
14164 };
14165 assert_eq!(run(), run());
14166 }
14167
14168 #[test]
14169 fn resetting_sampler_restarts_the_seeded_stream() {
14170 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14171 let config = SamplerConfig {
14172 seed: Some(1234),
14173 ..SamplerConfig::default()
14174 };
14175 p.set_sampler_config(config.clone());
14176 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
14177 p.set_sampler_config(config);
14178 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
14179 assert_eq!(first, second);
14180 }
14181
14182 #[test]
14183 fn eviction_bounds_the_cache() {
14184 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
14185 p.kv_cache.max_seq_len = 6;
14186 p.sampler_config.temperature = 0.0;
14187 let _ = p.generate("abcd", 12, None, None).unwrap();
14188 assert!(
14189 p.kv_cache.seq_len() <= 6 + 1,
14190 "cache must stay bounded by max_seq_len (got {})",
14191 p.kv_cache.seq_len()
14192 );
14193 }
14194
14195 #[test]
14196 fn confidence_matches_tokens_and_is_a_probability() {
14197 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14198 p.sampler_config.temperature = 0.0;
14199 p.sampler_config.repetition_penalty = 1.0;
14200 let r = p.generate("abcd", 10, None, None).unwrap();
14201 assert_eq!(
14202 r.token_confidence.len(),
14203 r.token_ids.len(),
14204 "one confidence per emitted token"
14205 );
14206 for &c in &r.token_confidence {
14207 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
14208 }
14209 let logits = [1.0f32, 3.0, 0.5, 3.0];
14211 let p0 = top1_prob_t(&logits, 1, 1.0);
14212 let p1 = top1_prob_t(&logits, 3, 1.0);
14213 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
14214 assert!(p0 > 0.0 && p0 < 1.0);
14215 let sharp = top1_prob_t(&logits, 1, 1.0);
14217 let soft = top1_prob_t(&logits, 1, 2.0);
14218 assert!(soft < sharp, "higher temperature lowers peak confidence");
14219 }
14220
14221 #[test]
14222 fn trace_is_opt_in_and_parallels_the_output() {
14223 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14225 p.sampler_config.temperature = 0.0;
14226 p.sampler_config.repetition_penalty = 1.0;
14227 let r = p.generate("abcd", 10, None, None).unwrap();
14228 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
14229
14230 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14232 p.sampler_config.temperature = 0.0;
14233 p.sampler_config.repetition_penalty = 1.0;
14234 p.set_trace(true);
14235 let r = p.generate("abcd", 10, None, None).unwrap();
14236 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
14237 for (i, tr) in r.traces.iter().enumerate() {
14238 assert_eq!(tr.t, i, "trace index is sequential");
14239 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
14240 assert_eq!(
14241 tr.confidence, r.token_confidence[i],
14242 "trace confidence matches the confidence channel"
14243 );
14244 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
14246 }
14247 }
14248
14249 #[test]
14250 fn explain_prefill_logits_match_greedy_first_token() {
14251 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14255 p.sampler_config.temperature = 0.0;
14256 p.sampler_config.repetition_penalty = 1.0;
14257 let ids = p.tokenizer.encode("abcd");
14258 let logits = p.prefill_next_logits(&ids, None);
14259 let argmax = logits
14260 .iter()
14261 .enumerate()
14262 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14263 .unwrap()
14264 .0 as u32;
14265 let r = p.generate("abcd", 1, None, None).unwrap();
14266 assert_eq!(
14267 argmax, r.token_ids[0],
14268 "explain preview must match greedy emit"
14269 );
14270 }
14271
14272 #[test]
14273 fn laguna_shared_expert_is_unconditionally_added() {
14274 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
14275 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
14276 let zero_dense = || DenseFfn {
14277 gate_proj: matrix(vec![0.0; 4]),
14278 up_proj: matrix(vec![0.0; 4]),
14279 down_proj: matrix(vec![0.0; 4]),
14280 act: Act::Silu,
14281 down_t: None,
14282 segs: Vec::new(),
14283 };
14284 let shared = DenseFfn {
14285 gate_proj: identity(),
14286 up_proj: identity(),
14287 down_proj: identity(),
14288 act: Act::Silu,
14289 down_t: None,
14290 segs: Vec::new(),
14291 };
14292 let x = [1.0, 2.0];
14293 let expected = dense_ffn(&shared, &x, None);
14294 let moe = MoeFfn {
14295 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
14296 experts: vec![zero_dense()],
14297 top_k: 1,
14298 norm_topk_prob: true,
14299 router_sigmoid: true,
14300 expert_bias: None,
14301 routed_scaling: 1.0,
14302 route_tau: None,
14303 shared: Some((shared, None)),
14304 stats: std::cell::RefCell::new(Vec::new()),
14305 act_sq: std::cell::RefCell::new(Vec::new()),
14306 act_rows: std::cell::RefCell::new(Vec::new()),
14307 mask: None,
14308 per_expert_scale: None,
14309 router_input_norm: false,
14310 resonance: None,
14311 };
14312 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
14313 for (actual, expected) in actual.iter().zip(expected) {
14314 assert!((actual - expected).abs() < 1e-6);
14315 }
14316 }
14317
14318 #[test]
14319 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
14320 const B: usize = 19;
14321 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14322 p.set_o1(Some(crate::nystrom::O1Cfg {
14323 layers: crate::nystrom::O1Layers::All,
14324 m: 4,
14325 w: 8,
14326 sink: 2,
14327 rect: crate::nystrom::O1Rect::Aggregate,
14328 }));
14329 p.o1_begin_with_prefix(Some(B));
14330 let ids: Vec<u32> = (0..B as u32).collect();
14331 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
14332
14333 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
14334 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
14335 let next = p.embed_single(B as u32);
14336 let _ = p.forward_layers(&next, B, None);
14337 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
14338 }
14339
14340 #[test]
14341 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
14342 const B: usize = 19;
14343 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
14344 let gdn_cfg = crate::linear_core::GdnCfg {
14348 num_v_heads: 2,
14349 num_k_heads: 1,
14350 key_head_dim: 2,
14351 value_head_dim: 4,
14352 conv_kernel: 3,
14353 hidden_size: 8,
14354 rms_eps: 1e-6,
14355 output_gate_sigmoid: false,
14356 };
14357 let synth = |n: usize, salt: usize| -> Vec<f32> {
14358 (0..n)
14359 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
14360 .collect()
14361 };
14362 let qt = |rows: usize, cols: usize, salt: usize| {
14363 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
14364 };
14365 let c_dim = gdn_cfg.conv_dim();
14366 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
14367 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
14368 in_proj_qkv: qt(c_dim, 8, 1),
14369 in_proj_z: qt(vd, 8, 2),
14370 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
14371 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
14372 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
14373 a_log: vec![0.2, 0.5],
14374 dt_bias: synth(gdn_cfg.num_v_heads, 6),
14375 norm: vec![1.0; gdn_cfg.value_head_dim],
14376 out_proj: qt(8, vd, 7),
14377 });
14378 p.gdn_cfg = Some(gdn_cfg);
14379 p.set_o1(Some(crate::nystrom::O1Cfg {
14380 layers: crate::nystrom::O1Layers::All,
14381 m: 4,
14382 w: 8,
14383 sink: 2,
14384 rect: crate::nystrom::O1Rect::Aggregate,
14385 }));
14386 p.o1_begin_with_prefix(Some(B));
14387 for pos in 0..B - 2 {
14388 let emb = p.embed_single(pos as u32);
14389 let _ = p.forward_layers(&emb, pos, None);
14390 }
14391 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
14392
14393 let e1 = p.embed_single((B - 2) as u32);
14394 let e2 = p.embed_single((B - 1) as u32);
14395 let _ = p.forward_pair(&e1, &e2, B - 2);
14396
14397 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
14398 assert!(
14399 p.kv_cache
14400 .layers
14401 .iter()
14402 .enumerate()
14403 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
14404 );
14405 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
14406 assert_ne!(
14407 p.kv_cache.layers[0].linear_state, lane1_state,
14408 "real pair must commit GDN lane 2 before returning"
14409 );
14410 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
14411 let next = p.embed_single(B as u32);
14412 let _ = p.forward_layers(&next, B, None);
14413 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
14414 }
14415
14416 #[test]
14417 fn o1_error_observation_stays_terminal_until_reset() {
14418 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14419 p.set_o1(Some(crate::nystrom::O1Cfg {
14420 layers: crate::nystrom::O1Layers::All,
14421 m: 4,
14422 w: 8,
14423 sink: 2,
14424 rect: crate::nystrom::O1Rect::Aggregate,
14425 }));
14426 p.o1_begin();
14427 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
14428
14429 assert!(p.o1_seal_checked().is_err());
14430 assert!(
14431 p.o1_seal_checked().is_err(),
14432 "retry must see the sticky error"
14433 );
14434 let k = vec![0.2f32; 4];
14435 let v = vec![0.3f32; 4];
14436 p.kv_cache.layers[0].append(&k, &v, &[]);
14437 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
14438
14439 p.reset_session();
14440 p.o1_begin();
14441 p.kv_cache.layers[0].append(&k, &v, &[]);
14442 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
14443 }
14444
14445 #[test]
14446 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
14447 let ids = vec![1u32, 2, 3, 4, 5, 6];
14448 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14449 p.graph_logits = Some(vec![123.0]);
14450 p.graph_want_logits = true;
14451 p.graph_failed
14452 .store(true, std::sync::atomic::Ordering::Relaxed);
14453 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14454 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
14455 assert!(err.contains("before NLL"));
14456 assert!(p.graph_logits.is_none());
14457 assert!(!p.graph_want_logits);
14458 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14459 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14460
14461 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14462 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14463 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14464 assert_eq!(actual.1, expected.1);
14465 assert!((actual.0 - expected.0).abs() < 1e-9);
14466 }
14467
14468 #[test]
14469 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
14470 let ids = vec![1u32, 2, 3, 4, 5, 6];
14471 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14472 p.nll_test_fail_at = Some(1);
14473 let err = p
14474 .nll_ids_from(&ids, 0)
14475 .expect_err("one-shot forward failure");
14476 assert!(err.contains("forward") || err.contains("score row"));
14477 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14478 assert!(!p.graph_want_logits);
14479 assert!(p.graph_logits.is_none());
14480 assert!(p.kv_history.is_empty());
14481
14482 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14483 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
14484 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
14485 assert_eq!(actual.1, expected.1);
14486 assert!((actual.0 - expected.0).abs() < 1e-9);
14487 }
14488
14489 #[test]
14490 fn nll_serial_failure_before_first_row_is_reported() {
14491 let ids = vec![1u32, 2, 3, 4];
14492 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14493 p.nll_test_force_serial = true;
14494 p.nll_test_fail_at = Some(0);
14495 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
14496 assert!(err.contains("serial forward"));
14497 assert!(p.kv_history.is_empty());
14498 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14499 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14500 }
14501
14502 #[test]
14503 fn ffn_probe_failure_discards_recorder_and_state() {
14504 let ids = vec![1u32, 2, 3, 4];
14505 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14506 p.nll_test_fail_at = Some(0);
14507 let err = p
14508 .probe_ffn_mass_batch(&ids)
14509 .expect_err("probe forward failure");
14510 assert!(err.contains("NLL"));
14511 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
14512 assert!(p.kv_history.is_empty());
14513 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14514 }
14515
14516 #[test]
14517 fn nll_test_controls_are_pipeline_scoped() {
14518 let ids = vec![1u32, 2, 3, 4];
14519 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14520 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14521 failing.nll_test_force_serial = true;
14522 failing.nll_test_fail_at = Some(0);
14523
14524 assert!(!failing.can_prefill_batched());
14525 assert!(unaffected.can_prefill_batched());
14526 let expected = unaffected
14527 .nll_ids_from(&ids, 0)
14528 .expect("unaffected pipeline remains usable");
14529 let err = failing
14530 .nll_ids_from(&ids, 0)
14531 .expect_err("failure injection belongs to failing pipeline");
14532 assert!(err.contains("serial forward"));
14533 assert!(failing.nll_test_fail_at.is_none());
14534 assert!(unaffected.can_prefill_batched());
14535 let actual = unaffected
14536 .nll_ids_from(&ids, 0)
14537 .expect("unaffected pipeline remains reusable");
14538 assert_eq!(actual.1, expected.1);
14539 assert!((actual.0 - expected.0).abs() < 1e-9);
14540 }
14541
14542 #[test]
14543 fn forward_ids_failure_channel_is_terminal_and_reusable() {
14544 let ids = vec![1u32, 2, 3, 4, 5, 6];
14545 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
14546 p.graph_logits = Some(vec![123.0]);
14547 p.graph_want_logits = true;
14548 p.graph_failed
14549 .store(true, std::sync::atomic::Ordering::Relaxed);
14550 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
14551
14552 let err = p
14553 .forward_ids(&ids, None)
14554 .expect_err("a failed forward must not become a valid head result");
14555 assert!(err.contains("forward_ids setup"));
14556 assert!(p.graph_logits.is_none());
14557 assert!(!p.graph_want_logits);
14558 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
14559 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
14560 assert_eq!(p.kv_cache.seq_len(), 0);
14561
14562 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
14563 .forward_ids(&ids, None)
14564 .expect("fresh forward_ids");
14565 let actual = p
14566 .forward_ids(&ids, None)
14567 .expect("pipeline remains reusable after a failed forward");
14568 assert_eq!(actual.len(), expected.len());
14569 assert!(
14570 actual
14571 .iter()
14572 .zip(expected)
14573 .all(|(a, b)| (a - b).abs() < 1e-9)
14574 );
14575 assert_eq!(p.kv_cache.seq_len(), ids.len());
14576 }
14577}