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 graph_logits: Option<Vec<f32>>,
264 pub embed_multiplier: f32,
266 pub attn_scale: f32,
269 pub swa: Option<(usize, usize)>,
272 pub sliding_layers: Option<Vec<bool>>,
275 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
278 pub rotary_dim_local: Option<usize>,
279 pub rope_scale: f32,
280 pub rope_scale_local: f32,
281 pub global_attn: Option<(usize, usize)>,
284 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
287 pub attn_v_norm: bool,
289 pub final_softcap: Option<f32>,
291 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
295 pub attn_softcap: f32,
297 confidence_on: bool,
301 #[cfg(test)]
304 nll_test_fail_at: Option<usize>,
305 #[cfg(test)]
308 nll_test_force_serial: bool,
309}
310
311#[cfg(target_os = "macos")]
312impl Drop for Pipeline {
313 fn drop(&mut self) {
314 crate::gpu::kv_mirror_drop(self.graph_kv_id);
315 }
316}
317
318pub struct PipelineWeights {
323 pub embed_tokens: QTensor,
325 pub layers: Vec<LayerWeights>,
327 pub lm_head: QTensor,
329 pub final_norm: Vec<f32>,
331}
332
333pub struct LayerWeights {
335 pub input_norm: Vec<f32>,
336 pub post_norm: Vec<f32>,
339 pub attn_out_norm: Option<Vec<f32>>,
342 pub layer_scale: Option<f32>,
344 pub ffn_out_norm: Option<Vec<f32>>,
347 pub ffn: FfnKind,
348 pub attn: AttnKind,
349}
350
351#[derive(Clone, Copy, PartialEq, Debug, Default)]
354pub enum Act {
355 #[default]
356 Silu,
357 GeluTanh,
358 Situ {
361 beta: f32,
362 linear_beta: f32,
363 },
364}
365
366impl Act {
367 pub fn from_arch(name: &str) -> Self {
368 if name == "gelu_tanh" {
369 Self::GeluTanh
370 } else {
371 Self::Silu
372 }
373 }
374
375 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
377 match arch.hidden_act.as_str() {
378 "situ" => Self::Situ {
379 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
380 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
381 },
382 other => Self::from_arch(other),
383 }
384 }
385
386 #[inline]
387 pub fn apply(self, x: f32) -> f32 {
388 match self {
389 Self::Silu => inference::silu(x),
390 Self::GeluTanh => inference::gelu_tanh(x),
391 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
392 }
393 }
394
395 #[inline]
398 pub fn combine(self, g: f32, u: f32) -> f32 {
399 match self {
400 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
401 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
402 }
403 _ => self.apply(g) * u,
404 }
405 }
406}
407
408pub struct DenseFfn {
410 pub gate_proj: QTensor,
411 pub up_proj: QTensor,
412 pub down_proj: QTensor,
413 pub act: Act,
415 pub down_t: Option<QTensor>,
421 pub segs: Vec<FfnSeg>,
428}
429
430pub struct FfnSeg {
435 pub gate: QTensor,
436 pub up: QTensor,
437 pub down: QTensor,
438 pub start: usize,
439 pub width: usize,
440}
441
442pub enum FfnKind {
445 Dense(DenseFfn),
446 Moe(MoeFfn),
450 DenseMoe(Box<DenseMoeFfn>),
457}
458
459pub struct DenseMoeFfn {
461 pub dense: DenseFfn,
462 pub moe: MoeFfn,
463 pub post_norm_1: Vec<f32>,
465 pub pre_norm_2: Vec<f32>,
468 pub post_norm_2: Vec<f32>,
470}
471
472pub struct MoeFfn {
473 pub router: QTensor,
475 pub experts: Vec<DenseFfn>,
476 pub top_k: usize,
477 pub norm_topk_prob: bool,
478 pub router_sigmoid: bool,
481 pub expert_bias: Option<Vec<f32>>,
485 pub routed_scaling: f32,
488 pub route_tau: Option<f32>,
494 pub shared: Option<(DenseFfn, Option<QTensor>)>,
497 pub stats: std::cell::RefCell<Vec<u64>>,
501 pub act_sq: std::cell::RefCell<Vec<f64>>,
508 pub act_rows: std::cell::RefCell<Vec<f32>>,
514 pub mask: Option<Vec<bool>>,
519 pub per_expert_scale: Option<Vec<f32>>,
522 pub router_input_norm: bool,
526 pub resonance: Option<Resonance>,
530}
531
532pub struct Resonance {
534 pub mu: Vec<f32>,
536 pub u: Vec<f32>,
538 pub k: usize,
539 pub bias: Vec<f32>,
541}
542
543impl Resonance {
544 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
546 let h = x.len();
547 let ne = out.len();
548 for e in 0..ne {
549 let mu = &self.mu[e * h..(e + 1) * h];
550 let mut d2 = 0.0f32;
551 for j in 0..h {
552 let d = x[j] - mu[j];
553 d2 += d * d;
554 }
555 let mut proj = 0.0f32;
556 for i in 0..self.k {
557 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
558 let mut p = 0.0f32;
559 for j in 0..h {
560 p += (x[j] - mu[j]) * u[j];
561 }
562 proj += p * p;
563 }
564 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
565 }
566 }
567}
568
569pub enum AttnKind {
572 Full {
574 wq: QTensor,
575 wk: QTensor,
576 wv: QTensor,
577 wo: QTensor,
578 q_norm: Option<Vec<f32>>,
579 k_norm: Option<Vec<f32>>,
580 output_gate: bool,
581 softplus_gate: Option<(QTensor, bool)>,
585 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
587 },
588 Linear(VmfPhaseWeights),
590 LinearGdn(GdnWeights),
592 ShortConv(ShortConvWeights),
595 Mla(Box<MlaWeights>),
603 Kda(Box<crate::linear_core::KdaWeights>),
607}
608
609pub struct MlaWeights {
611 pub q_proj: QTensor,
615 pub q_a: Option<QTensor>,
618 pub q_a_norm: Option<Vec<f32>>,
619 pub kv_a: QTensor,
621 pub kv_a_norm: Vec<f32>,
623 pub kv_b: QTensor,
625 pub o_proj: QTensor,
627 pub nh: usize,
628 pub qk_rope: usize,
629 pub qk_nope: usize,
630 pub v_dim: usize,
631 pub lora: usize,
632 pub scale: f32,
634 pub nope: bool,
636}
637
638pub struct MtpModule {
643 pub enorm: Vec<f32>,
644 pub hnorm: Vec<f32>,
645 pub eh_proj: QTensor,
647 pub layer: LayerWeights,
648 pub final_norm: Vec<f32>,
649 pub kv: crate::kv_cache::LayerKvCache,
650}
651
652#[cfg(target_os = "macos")]
659enum MetalRowsItem<'a> {
660 Gdn {
661 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
662 first: usize,
663 },
664 Attn {
665 l: crate::gpu_metal::AttnGpuLayer<'a>,
666 li: usize,
667 q_norm: Option<&'a [f32]>,
668 k_norm: Option<&'a [f32]>,
669 output_gate: bool,
670 },
671}
672
673#[cfg(target_os = "macos")]
674struct MetalVerifyPending {
675 graph: crate::gpu_metal::VerifyGraph,
676 gdn_layers: Vec<usize>,
677 attn_layers: Vec<(usize, usize)>,
678}
679
680#[derive(Clone, Copy)]
684enum SpecTrial {
685 Spec {
686 t0: std::time::Instant,
687 gen0: usize,
688 rounds: usize,
689 },
690 Plain {
691 t0: std::time::Instant,
692 gen0: usize,
693 },
694 Decided {
695 spec: bool,
696 recheck_at: usize,
697 },
698}
699
700#[derive(Default, Clone, Copy)]
711struct SpecMon {
712 round_ms: f64,
713 tokens: f64,
714 plain_ms: f64,
715 n: u32,
716 fails: u32,
717}
718
719impl SpecMon {
720 fn round(&mut self, dt_ms: f64, produced: usize) {
721 self.n += 1;
722 if self.n == 1 {
723 return; }
725 let a = if self.n == 2 { 1.0 } else { 0.3 };
726 self.round_ms += a * (dt_ms - self.round_ms);
727 self.tokens += a * (produced as f64 - self.tokens);
728 }
729 fn pays(&self) -> bool {
730 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
731 }
732}
733
734pub struct GenerateResult {
736 pub text: String,
737 pub token_ids: Vec<u32>,
738 pub prompt_tokens: usize,
739 pub tokens_generated: usize,
740 pub finish_reason: String,
741 pub mtp_drafted: usize,
743 pub mtp_accepted: usize,
744 pub token_confidence: Vec<f32>,
749 pub traces: Vec<TokenTrace>,
752}
753
754#[derive(Clone, Debug)]
759pub struct TokenTrace {
760 pub t: usize,
762 pub token_id: u32,
764 pub confidence: f32,
766 pub active_skill: Option<String>,
768 pub recon: Option<f32>,
772 pub switched: bool,
775}
776
777#[cfg_attr(not(test), allow(dead_code))]
782fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
783 let t = if temp > 1e-3 { temp } else { 1.0 };
784 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
785 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
786 if sum > 0.0 {
787 (((logits[id as usize] - max) / t).exp()) / sum
788 } else {
789 0.0
790 }
791}
792
793fn prefill_batched() -> bool {
796 std::env::var("CMF_PREFILL")
797 .map(|v| v != "seq")
798 .unwrap_or(true)
799}
800
801#[derive(Clone, Copy)]
805enum PrefillIn<'a> {
806 Ids(&'a [u32]),
807 Hidden(&'a [f32]),
808}
809
810impl Pipeline {
817 fn can_prefill_batched(&self) -> bool {
818 #[cfg(test)]
819 let force_serial = self.nll_test_force_serial;
820 #[cfg(not(test))]
821 let force_serial = false;
822 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
823 }
824
825 fn automatic_gpu_prefix(&self) -> Option<usize> {
828 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
829 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
830 }
831}
832
833pub fn prefill_chunk() -> usize {
840 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
841 .ok()
842 .and_then(|v| v.parse::<usize>().ok())
843 {
844 return n.max(1);
845 }
846 if cfg!(target_os = "macos") {
847 512
848 } else if cfg!(target_arch = "aarch64") {
849 256
852 } else {
853 48
854 }
855}
856
857#[inline]
863fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
864 if end <= start || start >= input_len {
865 return 0;
866 }
867 let rows = (end.min(input_len) - start).min(input_len - start);
868 if end < input_len {
869 rows
870 } else {
871 rows.saturating_sub(1)
872 }
873}
874
875pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
877
878impl Pipeline {
879 fn clear_sequence_state(&mut self) {
887 self.kv_cache.clear();
888 self.kv_history.clear();
889 if let Some(b) = &mut self.dsv41 {
890 b.3.clear();
891 }
892 crate::gpu::graph_kv_reset(self.graph_kv_id);
893 crate::gpu::graph_kv_reset(self.mtp_kv_id());
898 }
899
900 fn finish_generation(
906 &mut self,
907 mtp: &mut Option<MtpModule>,
908 router: &mut Option<crate::swarm::DynRouter>,
909 clear_sequence: bool,
910 ) {
911 if router.is_some() {
915 let _ = self.set_active_skill(None);
916 }
917 if clear_sequence {
918 self.clear_sequence_state();
919 if let Some(m) = mtp.as_mut() {
920 m.kv.clear();
926 }
927 if let Some(m) = self.mtp.as_mut() {
928 m.kv.clear();
932 }
933 }
934 self.graph_want_logits = false;
935 self.graph_logits = None;
936 self.graph_failed
937 .store(false, std::sync::atomic::Ordering::Relaxed);
938 self.cancel
939 .store(false, std::sync::atomic::Ordering::Relaxed);
940 self.dyn_router = router.take().or(self.dyn_router.take());
941 self.mtp = mtp.take().or(self.mtp.take());
942 self.mtp_graph_mode = None;
943 self.spec_forced = None;
944 }
945
946 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
950 if self
951 .graph_failed
952 .swap(false, std::sync::atomic::Ordering::Relaxed)
953 {
954 self.cancel
955 .store(false, std::sync::atomic::Ordering::Relaxed);
956 self.clear_sequence_state();
957 self.graph_logits = None;
958 self.graph_want_logits = false;
959 return Err(format!("GPU graph failed during {phase} at position {pos}"));
960 }
961 Ok(())
962 }
963
964 fn nll_begin(&mut self) -> Result<(), String> {
969 if self
970 .graph_failed
971 .swap(false, std::sync::atomic::Ordering::Relaxed)
972 {
973 self.cancel
974 .store(false, std::sync::atomic::Ordering::Relaxed);
975 self.clear_sequence_state();
976 self.graph_logits = None;
977 self.graph_want_logits = false;
978 return Err("GPU graph failed before NLL scoring".to_string());
979 }
980 self.clear_sequence_state();
981 self.graph_logits = None;
982 self.graph_want_logits = false;
983 Ok(())
984 }
985
986 fn nll_end(&mut self) {
990 self.clear_sequence_state();
991 self.graph_logits = None;
992 self.graph_want_logits = false;
993 self.graph_failed
994 .store(false, std::sync::atomic::Ordering::Relaxed);
995 }
996
997 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1000 #[cfg(test)]
1001 if self.nll_test_fail_at == Some(pos) {
1002 self.nll_test_fail_at = None;
1003 self.graph_failed
1004 .store(true, std::sync::atomic::Ordering::Relaxed);
1005 self.cancel
1006 .store(true, std::sync::atomic::Ordering::Relaxed);
1007 }
1008 if self
1009 .graph_failed
1010 .swap(false, std::sync::atomic::Ordering::Relaxed)
1011 {
1012 self.cancel
1013 .store(false, std::sync::atomic::Ordering::Relaxed);
1014 self.clear_sequence_state();
1015 self.graph_logits = None;
1016 self.graph_want_logits = false;
1017 return Err(format!(
1018 "GPU graph failed during NLL {phase} at position {pos}"
1019 ));
1020 }
1021 Ok(())
1022 }
1023
1024 #[inline]
1028 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1029 virtual_idx % self.physical_layers
1030 }
1031
1032 #[inline]
1035 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1036 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1037 }
1038
1039 #[allow(clippy::too_many_arguments)]
1041
1042 #[cfg(target_os = "macos")]
1061 fn graph_prefill_preferred(&self) -> bool {
1062 if !crate::gpu::enabled_here()
1063 || !crate::gpu::q1_force()
1064 || std::env::var("CMF_GPU_BLOCK")
1065 .map(|v| v == "0")
1066 .unwrap_or(false)
1067 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1070 {
1071 return false;
1072 }
1073 self.weights
1074 .layers
1075 .iter()
1076 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
1077 }
1078
1079 #[cfg(not(target_os = "macos"))]
1080 fn graph_prefill_preferred(&self) -> bool {
1081 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1089 if !graph_on || !crate::gpu::enabled_here() {
1090 return false;
1091 }
1092 if self.o1_active() {
1101 return false;
1102 }
1103 self.weights
1104 .layers
1105 .iter()
1106 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1107 }
1108
1109 #[cfg(target_os = "macos")]
1110 fn q1_graph_gpu(
1111 &mut self,
1112 start: usize,
1113 upto: Option<usize>,
1114 position: usize,
1115 h: &mut [f32],
1116 ) -> usize {
1117 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1119 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1121 || !crate::gpu::q1_force()
1122 || std::env::var("CMF_GPU_BLOCK")
1123 .map(|v| v == "0")
1124 .unwrap_or(false)
1125 {
1126 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1127 eprintln!(
1128 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
1129 self.attn_softcap > 0.0,
1130 crate::gpu::enabled_here(),
1131 crate::gpu::q1_force(),
1132 );
1133 }
1134 return start;
1135 }
1136 if self.swa.is_some()
1140 || self.global_attn.is_some()
1141 || self.attention_heads_per_layer.is_some()
1142 || self.attn_v_norm
1143 || self.weights.layers.iter().any(|lw| {
1144 lw.attn_out_norm.is_some()
1145 || lw.ffn_out_norm.is_some()
1146 || lw.layer_scale.is_some()
1147 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1148 })
1149 {
1150 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1151 eprintln!(
1152 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1153 self.swa.is_some(),
1154 self.global_attn.is_some(),
1155 self.attention_heads_per_layer.is_some(),
1156 self.attn_v_norm,
1157 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1158 );
1159 }
1160 return start;
1161 }
1162 let limit = upto
1165 .map(|u| u + 1)
1166 .unwrap_or(self.num_layers)
1167 .min(self.num_layers);
1168
1169 enum Item<'a> {
1170 Gdn {
1171 run: Vec<GdnGpuLayer<'a>>,
1172 first: usize,
1173 },
1174 Attn {
1175 l: AttnGpuLayer<'a>,
1176 li: usize,
1177 q_norm: Option<&'a [f32]>,
1178 k_norm: Option<&'a [f32]>,
1179 output_gate: bool,
1180 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1181 full_gpu: bool,
1184 },
1185 }
1186
1187 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1194 let attend_contract = attend_mode != "0"
1195 && attend_mode != "off"
1196 && self.head_dim % 4 == 0
1197 && self.head_dim <= 256
1198 && self.rotary_dim >= 2
1199 && self.rotary_dim <= self.head_dim
1200 && (self.rotary_dim / 2) % 32 == 0
1201 && self.num_kv_heads > 0
1202 && self.num_heads % self.num_kv_heads == 0;
1203
1204 let mut plan: Vec<Item> = Vec::new();
1205 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1206 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1208 let mut scan = start;
1209 while scan < limit {
1210 let lw = &self.weights.layers[self.phys_layer(scan)];
1211 let ffn = match &lw.ffn {
1212 FfnKind::Dense(d) if d.segs.is_empty() => {
1213 let (Some(g), Some(u), Some(dn)) = (
1214 d.gate_proj.q1_parts(),
1215 d.up_proj.q1_parts(),
1216 d.down_proj.q1_parts(),
1217 ) else {
1218 if block_diag {
1219 eprintln!(
1220 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1221 );
1222 }
1223 break;
1224 };
1225 MetalFfn::Dense {
1226 gate: g,
1227 up: u,
1228 down: dn,
1229 }
1230 }
1231 FfnKind::Moe(m) => {
1232 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1233 if block_diag {
1234 eprintln!(
1235 "block-graph: L{scan} MoE outside the graph contract — run ends"
1236 );
1237 }
1238 break;
1239 };
1240 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1241 model_ref.get_or_insert_with(|| model.clone());
1242 }
1243 MetalFfn::Moe(moe)
1244 }
1245 _ => {
1246 if block_diag {
1247 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1248 }
1249 break;
1250 }
1251 };
1252 match &lw.attn {
1253 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1254 let parts = (
1255 w.in_proj_qkv.q1_parts(),
1256 w.in_proj_z.q1_parts(),
1257 w.in_proj_a.f32_parts(),
1258 w.in_proj_b.f32_parts(),
1259 w.out_proj.q1_parts(),
1260 );
1261 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1262 if block_diag {
1263 eprintln!(
1264 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1265 w.in_proj_qkv.q1_parts().is_some(),
1266 w.in_proj_z.q1_parts().is_some(),
1267 w.in_proj_a.f32_parts().is_some(),
1268 w.in_proj_b.f32_parts().is_some(),
1269 w.out_proj.q1_parts().is_some(),
1270 );
1271 }
1272 break;
1273 };
1274 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1275 model_ref.get_or_insert_with(|| model.clone());
1276 }
1277 let gl = GdnGpuLayer {
1278 attn_norm: &lw.input_norm,
1279 post_norm: &lw.post_norm,
1280 qkv,
1281 z,
1282 a,
1283 b,
1284 out,
1285 ffn,
1286 conv1d: &w.conv1d,
1287 a_log: &w.a_log,
1288 dt_bias: &w.dt_bias,
1289 gnorm: &w.norm,
1290 };
1291 match plan.last_mut() {
1292 Some(Item::Gdn { run, .. }) => run.push(gl),
1293 _ => plan.push(Item::Gdn {
1294 run: vec![gl],
1295 first: scan,
1296 }),
1297 }
1298 }
1299 AttnKind::Full {
1300 wq,
1301 wk,
1302 wv,
1303 wo,
1304 q_norm,
1305 k_norm,
1306 output_gate,
1307 softplus_gate: None,
1308 bias,
1309 } if !self.kv_cache.layers[scan].o1_sealed()
1310 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1315 {
1316 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1317 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1318 break;
1319 };
1320 if let QTensor::Mapped { model, .. } = wq {
1321 model_ref.get_or_insert_with(|| model.clone());
1322 }
1323 let cache = &self.kv_cache.layers[scan];
1324 let o1_metal = cache.o1.is_some()
1328 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1329 && cache.o1_views().is_some();
1330 let full_gpu = attend_contract
1331 && cache.mode == crate::kv_cache::KvMode::F32
1332 && (cache.o1.is_none() || o1_metal)
1333 && bias.is_none()
1334 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1335 && pk.1 == self.num_kv_heads * self.head_dim
1336 && pv.1 == self.num_kv_heads * self.head_dim
1337 && po.2 == self.num_heads * self.head_dim;
1338 plan.push(Item::Attn {
1339 l: AttnGpuLayer {
1340 attn_norm: &lw.input_norm,
1341 post_norm: &lw.post_norm,
1342 wq: pq,
1343 wk: pk,
1344 wv: pv,
1345 wo: po,
1346 ffn,
1347 },
1348 li: scan,
1349 q_norm: q_norm.as_deref(),
1350 k_norm: k_norm.as_deref(),
1351 output_gate: *output_gate,
1352 bias: bias
1353 .as_ref()
1354 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1355 full_gpu,
1356 });
1357 }
1358 _ => break,
1359 }
1360 scan += 1;
1361 }
1362 let Some(model) = model_ref else {
1363 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1364 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1365 }
1366 return start;
1367 };
1368 if plan.is_empty() {
1369 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1370 eprintln!("q1-graph: empty plan at layer {start}");
1371 }
1372 return start;
1373 }
1374 let has_moe = plan.iter().any(|it| match it {
1375 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1376 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1377 });
1378 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1379 let dev_attend = attend_contract
1380 && (self.head_dim <= 128
1381 || has_moe
1382 || (self.head_dim <= 256 && has_gdn)
1388 || attend_mode == "force"
1389 || attend_mode == "256");
1390 if !dev_attend {
1391 for it in &mut plan {
1392 if let Item::Attn { li, full_gpu, .. } = it {
1393 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1396 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1397 if !keep_o1 {
1398 *full_gpu = false;
1399 }
1400 }
1401 }
1402 }
1403 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1404 use std::sync::atomic::{AtomicBool, Ordering};
1405 static SAID: AtomicBool = AtomicBool::new(false);
1406 if !SAID.swap(true, Ordering::Relaxed) {
1407 let fg = plan
1408 .iter()
1409 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1410 .count();
1411 let att = plan
1412 .iter()
1413 .filter(|it| matches!(it, Item::Attn { .. }))
1414 .count();
1415 eprintln!(
1416 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1417 plan.len(),
1418 self.head_dim,
1419 self.rotary_dim,
1420 self.num_kv_heads,
1421 self.num_heads,
1422 );
1423 }
1424 }
1425 let dims = GraphDims {
1426 hidden: self.hidden_size,
1427 eps: self.rms_eps as f32,
1428 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1429 };
1430 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1431 return start;
1432 };
1433 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1434 nv: cfg.num_v_heads,
1435 nk: cfg.num_k_heads,
1436 dk: cfg.key_head_dim,
1437 dv: cfg.value_head_dim,
1438 kk: cfg.conv_kernel,
1439 hidden: self.hidden_size,
1440 inter: self.intermediate_size,
1441 c_dim: cfg.conv_dim(),
1442 eps: cfg.rms_eps as f32,
1443 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1444 });
1445 let mut valid = 0usize;
1449 let mut end = start;
1450 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1452 static ONCE: std::sync::Once = std::sync::Once::new();
1453 ONCE.call_once(|| {
1454 for it in &plan {
1455 match it {
1456 Item::Gdn { first, run } => {
1457 eprintln!("plan: Gdn first={first} len={}", run.len())
1458 }
1459 Item::Attn { li, full_gpu, .. } => {
1460 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1461 }
1462 }
1463 }
1464 });
1465 }
1466 for item in &plan {
1467 let ok = match item {
1468 Item::Gdn { run, .. } => gcfg
1469 .as_ref()
1470 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1471 .unwrap_or(false),
1472 Item::Attn { l, .. } => graph.attn_ok(l),
1473 };
1474 if !ok {
1475 if block_diag {
1476 eprintln!(
1477 "block-graph: plan item {} ({}) failed graph preflight",
1478 valid,
1479 match item {
1480 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1481 Item::Attn { li, .. } => format!("Attn L{li}"),
1482 }
1483 );
1484 }
1485 break;
1486 }
1487 valid += 1;
1488 end += match item {
1489 Item::Gdn { run, .. } => run.len(),
1490 Item::Attn { .. } => 1,
1491 };
1492 }
1493 plan.truncate(valid);
1494 if plan.is_empty() {
1495 return start;
1496 }
1497
1498 let inv_freq = self.inv_freq.clone();
1499 let pool = self.pool.clone();
1500 let (nh, nkv, hd, hs, rd, eps) = (
1501 self.num_heads,
1502 self.num_kv_heads,
1503 self.head_dim,
1504 self.hidden_size,
1505 self.rotary_dim,
1506 self.rms_eps,
1507 );
1508 let norm_style = self.norm_style;
1509 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1510 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1511 let kv_id = self.graph_kv_id;
1512 let mut pending: Vec<(usize, usize)> = Vec::new();
1515 let mut dev_attn: Vec<usize> = Vec::new();
1518 for item in &plan {
1519 let _xt0 = std::time::Instant::now();
1520 let _xkind: u32 = match item {
1521 Item::Gdn { .. } => 2,
1522 Item::Attn { .. } => 3,
1523 };
1524 if self.loop_final_norm {
1526 let item_start = match item {
1527 Item::Gdn { first, .. } => *first,
1528 Item::Attn { li, .. } => *li,
1529 };
1530 if item_start > start && self.is_loop_end(item_start - 1) {
1531 graph.encode_loop_norm(&self.weights.final_norm);
1532 }
1533 }
1534 match item {
1535 Item::Gdn { run, first } => {
1536 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1537 if l.linear_state.len() != want {
1538 l.linear_state = vec![0f32; want];
1539 }
1540 }
1541 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1542 .iter()
1543 .map(|l| l.linear_state.as_slice())
1544 .collect();
1545 let _ig = std::time::Instant::now();
1546 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1547 tracing::error!("q1 graph: GDN run refused after validation");
1549 return start;
1550 }
1551 graph.commit_kind = 2;
1554 graph.commit();
1555 crate::gpu::stageprof(0, _ig.elapsed());
1556 pending.push((*first, run.len()));
1557 }
1558 Item::Attn {
1559 l,
1560 li,
1561 q_norm,
1562 k_norm,
1563 output_gate,
1564 bias,
1565 full_gpu,
1566 } => {
1567 let _ia = std::time::Instant::now();
1568 if *full_gpu {
1570 let cache = &self.kv_cache.layers[*li];
1571 let o1p = if cache.o1.is_some() {
1572 match cache.o1_views() {
1573 Some(views) => Some(crate::gpu::O1AttnParams {
1574 views,
1575 epoch: self.o1_epoch,
1576 }),
1577 None => None,
1579 }
1580 } else {
1581 None
1582 };
1583 let o1_layer = cache.o1.is_some();
1584 if o1_layer && o1p.is_none() {
1585 }
1587 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1588 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1589 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1590 let p = crate::gpu::AttnDeviceParams {
1591 kv_id,
1592 layer: *li,
1593 nh,
1594 nkv,
1595 hd,
1596 rd,
1597 position,
1598 scale: self.attn_scale,
1599 eps: eps as f32,
1600 gemma,
1601 output_gate: *output_gate,
1602 q_norm: *q_norm,
1603 k_norm: *k_norm,
1604 inv_freq: &inv_freq,
1605 cpu_k,
1606 cpu_v,
1607 cpu_stored,
1608 o1: o1p,
1609 };
1610 let o1_bad = o1_layer && p.o1.is_none();
1611 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1612 {
1613 if p.o1.is_none() {
1615 dev_attn.push(*li);
1616 }
1617 graph.commit_kind = 3;
1618 graph.commit();
1619 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1623 continue;
1624 }
1625 }
1627 graph.encode_attn_prefix(l);
1628 graph.sync();
1629 if !pending.is_empty() {
1630 let idxs: Vec<usize> =
1631 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1632 let mut outs: Vec<&mut [f32]> = self
1633 .kv_cache
1634 .layers
1635 .iter_mut()
1636 .enumerate()
1637 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1638 .map(|(_, s)| s.linear_state.as_mut_slice())
1639 .collect();
1640 graph.read_states(&mut outs);
1641 }
1642 let mut q_raw = attention::take_buf(l.wq.1);
1643 let mut k = attention::take_buf(l.wk.1);
1644 let mut v = attention::take_buf(l.wv.1);
1645 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1646 let cfg = QwenAttnCfg {
1647 num_heads: nh,
1648 num_kv_heads: nkv,
1649 head_dim: hd,
1650 hidden_size: hs,
1651 position,
1652 inv_freq: &inv_freq,
1653 rotary_dim: rd,
1654 scale: self.attn_scale,
1655 softcap: self.attn_softcap,
1656 window: None,
1657 v_norm: false,
1658 q_norm: *q_norm,
1659 k_norm: *k_norm,
1660 output_gate: *output_gate,
1661 softplus_gate: None,
1662 rope_scale: 1.0,
1663 bias: *bias,
1664 rms_eps: eps,
1665 norm_style,
1666 pool: pool.as_deref(),
1667 };
1668 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1671 || std::env::var("CMF_ATTN_DUMP").is_ok();
1672 let _ = full_gpu;
1673 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1674 let mut ao = attention::qwen_attention_core(
1675 q_raw,
1676 k,
1677 v,
1678 &mut self.kv_cache.layers[*li],
1679 &cfg,
1680 );
1681 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1685 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1686 let (cq, _cg, _ck, _cv) =
1687 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1688 let cache = &self.kv_cache.layers[*li];
1689 let n = cache.head_keys(0).len() / hd;
1690 let mut bytes: Vec<u8> = Vec::new();
1691 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1692 bytes.extend_from_slice(&v.to_le_bytes());
1693 }
1694 for v in &cq {
1695 bytes.extend_from_slice(&v.to_le_bytes());
1696 }
1697 for g in 0..nkv {
1698 for v in cache.head_keys(g) {
1699 bytes.extend_from_slice(&v.to_le_bytes());
1700 }
1701 }
1702 for g in 0..nkv {
1703 for v in cache.head_values(g) {
1704 bytes.extend_from_slice(&v.to_le_bytes());
1705 }
1706 }
1707 let _ =
1708 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1709 }
1710 }
1711 if let Some((qr0, k0, v0)) =
1712 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1713 {
1714 let (cq, _cg, ck, cv) =
1715 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1716 let mut h_now = vec![0f32; hs];
1717 graph.read_h(&mut h_now);
1718 let cache = &self.kv_cache.layers[*li];
1719 let n_after = cache.head_keys(0).len() / hd;
1720 let stored = n_after.saturating_sub(1);
1724 let cpu_k: Vec<&[f32]> = (0..nkv)
1725 .map(|g| &cache.head_keys(g)[..stored * hd])
1726 .collect();
1727 let cpu_v: Vec<&[f32]> = (0..nkv)
1728 .map(|g| &cache.head_values(g)[..stored * hd])
1729 .collect();
1730 let p = crate::gpu::AttnDeviceParams {
1731 kv_id,
1732 layer: *li,
1733 nh,
1734 nkv,
1735 hd,
1736 rd,
1737 position,
1738 scale: self.attn_scale,
1739 eps: eps as f32,
1740 gemma,
1741 output_gate: *output_gate,
1742 q_norm: *q_norm,
1743 k_norm: *k_norm,
1744 inv_freq: &inv_freq,
1745 cpu_k,
1746 cpu_v,
1747 cpu_stored: stored,
1748 o1: None,
1749 };
1750 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1751 let md = |a: &[f32], b: &[f32]| {
1752 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1753 };
1754 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1755 eprintln!(
1756 "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}",
1757 nn(&cq),
1758 md(&cq, &dq),
1759 nn(&ck),
1760 md(&ck, &dk),
1761 nn(&cv),
1762 md(&cv, &dv),
1763 nn(&ao),
1764 md(&ao, &dao)
1765 );
1766 } else {
1767 eprintln!("attn-oracle L{li}: device probe declined");
1768 }
1769 }
1770 graph.encode_attn_suffix(l, &ao);
1771 graph.commit();
1774 attention::recycle_buf(&mut ao);
1775 }
1776 }
1777
1778 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1779 }
1780 let mut lm_rows = None;
1785 if self.graph_want_logits
1786 && upto.is_none()
1787 && end == self.num_layers
1788 && std::env::var("CMF_GPU_LMHEAD")
1789 .map(|v| v != "0")
1790 .unwrap_or(true)
1791 {
1792 if let Some(lm) = self.weights.lm_head.q1_parts() {
1793 if graph.lm_head_ok(lm) {
1794 graph.encode_lm_head(&self.weights.final_norm, lm);
1795 lm_rows = Some(lm.1);
1796 }
1797 }
1798 }
1799 let _sy0 = std::time::Instant::now();
1800 graph.sync();
1801 let _rs0 = std::time::Instant::now();
1802 if !pending.is_empty() {
1803 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1804 let mut outs: Vec<&mut [f32]> = self
1805 .kv_cache
1806 .layers
1807 .iter_mut()
1808 .enumerate()
1809 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1810 .map(|(_, s)| s.linear_state.as_mut_slice())
1811 .collect();
1812 graph.read_states(&mut outs);
1813 }
1814 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1815 use std::sync::atomic::{AtomicU64, Ordering};
1816 static SY: AtomicU64 = AtomicU64::new(0);
1817 static RS: AtomicU64 = AtomicU64::new(0);
1818 static N: AtomicU64 = AtomicU64::new(0);
1819 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1820 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1821 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1822 if n % 100 == 0 {
1823 eprintln!(
1824 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1825 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1826 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1827 );
1828 }
1829 }
1830 if let Some(rows) = lm_rows {
1831 crate::gpu::hostprof_encode_done(_mt0);
1832 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1833 graph.read_logits(&mut lg);
1834 crate::gpu::hostprof_total(_mt0);
1835 lg.resize(self.vocab_size, 0.0);
1836 if let Some(c) = self.final_softcap {
1837 for l in lg.iter_mut() {
1838 *l = c * (*l / c).tanh();
1839 }
1840 }
1841 self.graph_logits = Some(lg);
1842 }
1843 graph.finish(h);
1844 for li in dev_attn {
1848 let mut krow = attention::take_buf(nkv * hd);
1849 let mut vrow = attention::take_buf(nkv * hd);
1850 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1851 let cache = &mut self.kv_cache.layers[li];
1852 cache.append(&krow, &vrow, &[]);
1853 let n = cache.seq_len;
1854 let mut imp = attention::take_buf(n);
1855 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1856 cache.accumulate_imp(&imp);
1857 attention::recycle_buf(&mut imp);
1858 }
1859 attention::recycle_buf(&mut krow);
1860 attention::recycle_buf(&mut vrow);
1861 }
1862 end
1863 }
1864
1865 pub fn new(
1866 tokenizer: Tokenizer,
1867 weights: PipelineWeights,
1868 hidden_size: usize,
1869 intermediate_size: usize,
1870 num_heads: usize,
1871 num_kv_heads: usize,
1872 head_dim: usize,
1873 num_layers: usize,
1874 physical_layers: usize,
1875 loop_final_norm: bool,
1876 vocab_size: usize,
1877 rms_eps: f64,
1878 rope_base: f32,
1879 norm_style: NormStyle,
1880 max_seq_len: usize,
1881 sampler_config: SamplerConfig,
1882 ) -> Self {
1883 let rng = match sampler_config.seed {
1884 Some(s) => SplitMix64::new(s),
1885 None => SplitMix64::from_entropy(),
1886 };
1887 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1888 let pool = Pool::from_env();
1889 if let Some(p) = &pool {
1890 tracing::info!("worker pool: {} threads", p.n_workers());
1891 }
1892 Self {
1893 gpu_plan: None,
1894 tokenizer: std::sync::Arc::new(tokenizer),
1895 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1896 sampler_config,
1897 weights,
1898 hidden_size,
1899 intermediate_size,
1900 num_heads,
1901 num_kv_heads,
1902 head_dim,
1903 num_layers,
1904 physical_layers,
1905 loop_final_norm,
1906 vocab_size,
1907 rms_eps,
1908 rope_base,
1909 norm_style,
1910 rotary_dim: head_dim,
1911 attention_heads_per_layer: None,
1912 vmf_cfg: None,
1913 gdn_cfg: None,
1914 kda_cfg: None,
1915 g3n: None,
1916 dsv4: None,
1917 dsv41: None,
1918 dsv41_vision: None,
1919 dsv41_prefill: None,
1920 qwen4_exp: None,
1921 dsv4_mtp: Vec::new(),
1922 dspark: None,
1923 dspark_pending: Vec::new(),
1924 dspark_hist: Vec::new(),
1925 dspark_real: Vec::new(),
1926 dspark_trunk_picks: Vec::new(),
1927 dspark_exp: Vec::new(),
1928 dspark_draft_ns: 0,
1929 logit_multiplier: None,
1930 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1931 graph_failed: std::sync::atomic::AtomicBool::new(false),
1932 kv_history: Vec::new(),
1933 short_conv_cfg: None,
1934 mtp: None,
1935 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1936 rng,
1937 sampler_scratch: SamplerScratch::default(),
1938 spec_forced: None,
1939 spec_q: Vec::new(),
1940 spec_p: Vec::new(),
1941 spec_res: Vec::new(),
1942 spec_qs: Vec::new(),
1943 spec_ps: Vec::new(),
1944 spec_ress: Vec::new(),
1945 mtp_graph_mode: None,
1946 #[cfg(target_os = "macos")]
1947 metal_verify: None,
1948 inv_freq,
1949 ws: ForwardScratch::new(hidden_size),
1950 pool,
1951 model: None,
1952 dyn_force_f32: false,
1953 dyn_skill_layers: Vec::new(),
1954 dyn_active: None,
1955 dyn_blend_loaded: false,
1956 dyn_phi_layer: None,
1957 dyn_phi_ema: Vec::new(),
1958 dyn_phi_seen: 0,
1959 dyn_router: None,
1960 o1_cfg: None,
1961 o1_epoch: 0,
1962 o1_flags: Vec::new(),
1963 trace: false,
1964 calib_temp: 1.0,
1965 confidence_on: true,
1966 embed_multiplier: 1.0,
1967 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1968 swa: None,
1969 sliding_layers: None,
1970 inv_freq_local: None,
1971 rotary_dim_local: None,
1972 rope_scale: 1.0,
1973 rope_scale_local: 1.0,
1974 global_attn: None,
1975 inv_freq_global: None,
1976 attn_v_norm: false,
1977 final_softcap: None,
1978 head_clusters: None,
1979 attn_softcap: 0.0,
1980 graph_want_logits: false,
1981 graph_logits: None,
1982 graph_kv_id: {
1983 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1984 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1985 },
1986 #[cfg(test)]
1987 nll_test_fail_at: None,
1988 #[cfg(test)]
1989 nll_test_force_serial: false,
1990 }
1991 }
1992
1993 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
2001 if let Some(c) = &cfg {
2002 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
2003 tracing::error!(
2004 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
2005 c.w,
2006 c.sink
2007 );
2008 self.o1_flags.clear();
2009 self.o1_cfg = None;
2010 return;
2011 }
2012 }
2013 self.o1_flags = match &cfg {
2014 Some(c) => {
2015 let mut flags = c.layer_flags(self.num_layers);
2016 for (li, f) in flags.iter_mut().enumerate() {
2017 if *f
2018 && !matches!(
2019 self.weights.layers[self.phys_layer(li)].attn,
2020 AttnKind::Full { .. }
2021 )
2022 {
2023 *f = false;
2024 }
2025 }
2026 flags
2027 }
2028 None => Vec::new(),
2029 };
2030 if let Some(c) = &cfg {
2031 let n = self.o1_flags.iter().filter(|&&f| f).count();
2032 tracing::info!(
2033 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
2034 self.num_layers,
2035 c.m,
2036 c.w,
2037 c.sink,
2038 c.rect
2039 );
2040 }
2041 self.o1_cfg = cfg;
2042 }
2043
2044 pub fn o1_active(&self) -> bool {
2046 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2047 }
2048
2049 pub fn generation_graph_prefill(&self) -> bool {
2054 let graph = self.graph_prefill_preferred();
2055 #[cfg(not(target_os = "macos"))]
2063 if graph
2064 && self.mtp.is_some()
2065 && std::env::var("CMF_BATCH_K")
2066 .ok()
2067 .and_then(|v| v.parse::<usize>().ok())
2068 .is_some_and(|k| k > 0)
2069 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2070 {
2071 return false;
2072 }
2073 graph
2074 }
2075
2076 pub fn o1_device_stats(&self) -> (usize, u64) {
2081 crate::gpu::o1_device_stats(self.graph_kv_id)
2082 }
2083
2084 pub fn o1_begin(&mut self) {
2089 self.o1_begin_with_prefix(None);
2090 }
2091
2092 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
2096 if let Some(c) = &self.o1_cfg {
2097 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2098 let boundary = requested_prefix.map(|p| {
2099 p.max(
2100 crate::nystrom::o1_deferred_boundary(w, sink)
2101 .expect("o1 config boundary validated in set_o1"),
2102 )
2103 });
2104 for (li, &f) in self.o1_flags.iter().enumerate() {
2105 if f {
2106 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
2107 }
2108 }
2109 }
2110 }
2111
2112 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
2114 self.o1_cfg.as_ref().and_then(|c| {
2115 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
2116 .map(|floor| requested_prefix.max(floor))
2117 })
2118 }
2119
2120 fn o1_note_transition(&mut self) {
2121 let mut transitioned = false;
2125 for (li, &flagged) in self.o1_flags.iter().enumerate() {
2126 if flagged {
2127 transitioned |= self.kv_cache.layers[li].take_o1_transition();
2128 }
2129 }
2130 if transitioned {
2131 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2132 }
2133 }
2134
2135 fn o1_pending(&self) -> bool {
2136 self.o1_flags.iter().enumerate().any(|(li, &f)| {
2137 f && self.kv_cache.layers[li].seq_len > 0
2138 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
2139 })
2140 }
2141
2142 fn o1_fail(&mut self, err: String) {
2143 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
2144 self.clear_sequence_state();
2145 self.graph_failed
2146 .store(true, std::sync::atomic::Ordering::Relaxed);
2147 self.cancel
2148 .store(true, std::sync::atomic::Ordering::Relaxed);
2149 }
2150
2151 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
2156 if self.o1_cfg.is_none() {
2157 return Ok(false);
2158 }
2159 let mut participating = false;
2160 for li in 0..self.num_layers {
2161 if !self.o1_flags.get(li).copied().unwrap_or(false) {
2162 continue;
2163 }
2164 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2165 return Err(err);
2166 }
2167 if self.kv_cache.layers[li].seq_len == 0 {
2168 continue;
2169 }
2170 participating = true;
2171 let num_heads = self.layer_num_heads(li);
2172 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
2173 }
2174 self.o1_note_transition();
2175 for li in 0..self.num_layers {
2176 if self.o1_flags.get(li).copied().unwrap_or(false) {
2177 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2178 return Err(err);
2179 }
2180 }
2181 }
2182 Ok(participating
2183 && (0..self.num_layers).all(|li| {
2184 !self.o1_flags.get(li).copied().unwrap_or(false)
2185 || self.kv_cache.layers[li].seq_len == 0
2186 || self.kv_cache.layers[li].o1_sealed()
2187 }))
2188 }
2189
2190 fn o1_progress(&mut self) {
2193 if !self.o1_active() {
2194 return;
2195 }
2196 for li in 0..self.num_layers {
2197 if self.o1_flags.get(li).copied().unwrap_or(false) {
2198 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
2199 self.o1_fail(err);
2200 return;
2201 }
2202 }
2203 }
2204 self.o1_note_transition();
2208 if !self.o1_pending() {
2209 return;
2210 }
2211 if let Err(err) = self.o1_seal_checked() {
2212 self.o1_fail(err);
2213 }
2214 }
2215
2216 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
2221 if self
2222 .graph_failed
2223 .swap(false, std::sync::atomic::Ordering::Relaxed)
2224 {
2225 self.cancel
2226 .store(false, std::sync::atomic::Ordering::Relaxed);
2227 self.clear_sequence_state();
2228 return Err(format!("{phase}: deferred O(1) transition failed"));
2229 }
2230 Ok(())
2231 }
2232
2233 pub fn o1_seal(&mut self) {
2237 if let Err(err) = self.o1_seal_checked() {
2238 self.o1_fail(err);
2239 }
2240 }
2241
2242 pub fn set_trace(&mut self, on: bool) {
2244 self.trace = on;
2245 }
2246
2247 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2250 self.rng = match config.seed {
2251 Some(seed) => SplitMix64::new(seed),
2252 None => SplitMix64::from_entropy(),
2253 };
2254 self.sampler_config = config;
2255 }
2256
2257 pub fn set_confidence(&mut self, on: bool) {
2262 self.confidence_on = on;
2263 }
2264
2265 pub fn set_calib_temp(&mut self, t: f32) {
2268 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2269 }
2270
2271 pub fn calib_temp(&self) -> f32 {
2273 self.calib_temp
2274 }
2275
2276 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2279 self.rotary_dim = rotary_dim.min(self.head_dim);
2280 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2281 }
2282
2283 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2284 QwenAttnCfg {
2285 num_heads: self.num_heads,
2286 num_kv_heads: self.num_kv_heads,
2287 head_dim: self.head_dim,
2288 hidden_size: self.hidden_size,
2289 position,
2290 inv_freq: &self.inv_freq,
2291 rotary_dim: self.rotary_dim,
2292 scale: self.attn_scale,
2293 softcap: self.attn_softcap,
2294 window: None,
2295 v_norm: false,
2296 q_norm: None,
2297 k_norm: None,
2298 output_gate: false,
2299 softplus_gate: None,
2300 rope_scale: self.rope_scale,
2301 bias: None,
2302 rms_eps: self.rms_eps,
2303 norm_style: self.norm_style,
2304 pool: self.pool.as_deref(),
2305 }
2306 }
2307
2308 pub fn generate(
2310 &mut self,
2311 prompt: &str,
2312 max_tokens: usize,
2313 task_mask: Option<&TaskMask>,
2314 on_token: Option<TokenCallback>,
2315 ) -> Result<GenerateResult, String> {
2316 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2317 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2318 }
2319
2320 pub fn generate_from_vl(
2323 &mut self,
2324 input: &crate::dsv41_vision::PreparedVlInputs,
2325 max_tokens: usize,
2326 task_mask: Option<&TaskMask>,
2327 on_token: Option<TokenCallback>,
2328 ) -> Result<GenerateResult, String> {
2329 let Some(dsv41) = &self.dsv41 else {
2330 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
2331 };
2332 if input.token_ids.is_empty() {
2333 return Err("empty V4.1 multimodal prompt".into());
2334 }
2335 if input.token_types.len() != input.token_ids.len() {
2336 return Err(format!(
2337 "V4.1 token type count {} != token count {}",
2338 input.token_types.len(),
2339 input.token_ids.len()
2340 ));
2341 }
2342 let dim = dsv41.2.dim;
2343 let mut embeddings = vec![None; input.token_ids.len()];
2344 let mut participates = vec![true; input.token_ids.len()];
2345 if !input.images.is_empty() {
2346 let vision = self
2347 .dsv41_vision
2348 .as_ref()
2349 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
2350 for image in &input.images {
2351 let end = image.start.saturating_add(image.types.len());
2352 if end > input.token_ids.len() {
2353 return Err(format!(
2354 "V4.1 image span {}..{} exceeds prompt length {}",
2355 image.start,
2356 end,
2357 input.token_ids.len()
2358 ));
2359 }
2360 let mut span = vec![0.0f32; image.types.len() * dim];
2361 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
2362 for (offset, &kind) in image.types.iter().enumerate() {
2363 let pos = image.start + offset;
2364 if input.token_types[pos] != kind {
2365 return Err(format!(
2366 "V4.1 image type mismatch at position {pos}: {} != {kind}",
2367 input.token_types[pos]
2368 ));
2369 }
2370 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
2371 participates[pos] = false;
2372 }
2373 }
2374 }
2375 for (pos, &kind) in input.token_types.iter().enumerate() {
2376 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
2377 return Err(format!("V4.1 text position {pos} has an image embedding"));
2378 }
2379 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
2380 return Err(format!("V4.1 image position {pos} has no image embedding"));
2381 }
2382 }
2383 self.dsv41_prefill = Some((embeddings, participates));
2384 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
2385 self.dsv41_prefill = None;
2386 result
2387 }
2388
2389 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2391 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2392 }
2393
2394 pub fn generate_from_ids(
2402 &mut self,
2403 input_ids: &[u32],
2404 max_tokens: usize,
2405 task_mask: Option<&TaskMask>,
2406 mut on_token: Option<TokenCallback>,
2407 ) -> Result<GenerateResult, String> {
2408 if std::env::var("CMF_TRACE_H").is_ok() {
2409 eprintln!("input_ids: {input_ids:?}");
2410 }
2411 if input_ids.is_empty() {
2412 return Err("empty prompt: nothing to generate from".to_string());
2413 }
2414 self.graph_failed
2418 .store(false, std::sync::atomic::Ordering::Relaxed);
2419 let task_mask = self.drop_open_mask(task_mask);
2424
2425 let reuse_from = {
2433 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2434 let h = &self.kv_history;
2435 if on
2436 && task_mask.is_none()
2437 && self.mtp.is_none()
2438 && self.o1_cfg.is_none()
2439 && self.dsv41.is_none()
2440 && !h.is_empty()
2441 && h.len() < input_ids.len()
2442 && input_ids[..h.len()] == h[..]
2443 {
2444 h.len()
2445 } else {
2446 0
2447 }
2448 };
2449 if reuse_from == 0 {
2450 self.clear_sequence_state();
2452 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2453 eprintln!(
2454 "kv-reuse: {} of {} prompt positions already cached",
2455 reuse_from,
2456 input_ids.len()
2457 );
2458 }
2459 crate::gpu::graph_race_begin_generation();
2460 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2464 std::env::var("CMF_O1_PREFILL")
2465 .ok()
2466 .and_then(|v| v.parse::<usize>().ok())
2467 .filter(|&p| p > 0)
2468 } else {
2469 None
2470 };
2471 if task_mask.is_none() {
2472 self.o1_begin_with_prefix(o1_prefill);
2473 }
2474
2475 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2481 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2508 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2509 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2525 for lw in &self.weights.layers {
2526 if let FfnKind::Dense(d) = &lw.ffn {
2527 dense_n += 1;
2528 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2529 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2530 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2531 {
2532 dense_q4tp += 1;
2533 }
2534 }
2535 }
2536 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2537 let penalized = self.sampler_config.repetition_penalty != 1.0
2541 || self.sampler_config.presence_penalty != 0.0
2542 || !self.sampler_config.suppress_tokens.is_empty();
2543 #[cfg(feature = "gpu")]
2548 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2549 #[cfg(not(feature = "gpu"))]
2550 let metal_wgpu = false;
2551 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2552 let spec_wanted = match spec_env.as_deref() {
2553 Some("0") => false,
2554 Some(_) => {
2555 if metal_wgpu {
2556 tracing::warn!(
2557 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2558 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2559 );
2560 }
2561 true
2562 }
2563 None => spec_default_ok && !penalized && !metal_wgpu,
2564 };
2565 #[cfg(target_os = "macos")]
2568 let metal_graph = crate::gpu::q1_force()
2569 && crate::gpu::enabled_here()
2570 && std::env::var("CMF_GPU_BLOCK")
2571 .map(|v| v != "0")
2572 .unwrap_or(true);
2573 #[cfg(not(target_os = "macos"))]
2574 let metal_graph = false;
2575 let graph_spec = self.speculative
2576 && (graph_on || metal_graph)
2577 && self.mtp.is_some()
2578 && task_mask.is_none()
2579 && !self.o1_active()
2580 && spec_sampling_ok
2581 && spec_wanted;
2582 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2589 let spec_active = self.speculative
2590 && self.mtp.is_some()
2591 && task_mask.is_none()
2592 && !self.o1_active()
2593 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2594 let mut mtp = if spec_active { self.mtp.take() } else { None };
2597 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2598 eprintln!(
2599 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2600 mtp.is_some(),
2601 self.speculative,
2602 self.sampler_config.temperature < 1e-6,
2603 );
2604 }
2605 if let Some(m) = &mut mtp {
2606 m.kv.clear();
2607 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2609 self.mtp_graph_mode = None;
2610 }
2611 let mut router = if mtp.is_none() {
2615 self.dyn_router.take()
2616 } else {
2617 None
2618 };
2619 if let Some(r) = &mut router {
2620 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2623 }
2624
2625 let mut all_ids = input_ids.to_vec();
2626 let mut generated = 0usize;
2627 let mut finish_reason = "max_tokens".to_string();
2628 let mut drafted = 0usize;
2629 let mut accepted = 0usize;
2630 let mut dsv4_spec_bad = 0usize;
2637 let mut dsv4_spec_retry_at = 0usize;
2638 let mut confidence: Vec<f32> = Vec::new();
2639 let trace_on = self.trace;
2640 let calib_temp = self.calib_temp;
2641 let mut traces: Vec<TokenTrace> = Vec::new();
2642
2643 let mut hidden = vec![0.0f32; self.hidden_size];
2649 let mut pos = reuse_from;
2650 let fuse_lm = mtp.is_none()
2659 && router.is_none()
2660 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2661 self.graph_logits = None;
2662 self.graph_want_logits = false;
2663 let _tpf = std::time::Instant::now();
2664 let batch_k = std::env::var("CMF_BATCH_K")
2665 .ok()
2666 .and_then(|v| v.parse::<usize>().ok())
2667 .unwrap_or(0);
2668 while self.qwen4_exp.is_some()
2679 && mtp.is_none()
2680 && pos < input_ids.len()
2681 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2682 {
2683 let token_id = input_ids[pos];
2684 let want_logits = pos + 1 == input_ids.len();
2685 let mut lg = Vec::new();
2686 if let Some(b) = &mut self.qwen4_exp {
2687 crate::qwen4_exp::forward_token(
2688 &b.0,
2689 &b.1,
2690 &b.2,
2691 &mut b.3,
2692 token_id,
2693 pos,
2694 &self.inv_freq,
2695 self.pool.as_deref(),
2696 &mut lg,
2697 want_logits,
2698 );
2699 }
2700 if want_logits {
2701 self.graph_logits = Some(lg);
2702 }
2703 pos += 1;
2704 hidden.fill(0.0);
2705 }
2706 while self.dsv4.is_some()
2707 && mtp.is_none()
2708 && pos < input_ids.len()
2709 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2710 {
2711 let end = (pos + prefill_chunk()).min(input_ids.len());
2712 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2713 let mut lg = Vec::new();
2714 if let Some(b) = &mut self.dsv4 {
2715 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2716 crate::dsv4::forward_chunk(
2717 g,
2718 layers,
2719 &cfg,
2720 st,
2721 &ids,
2722 pos,
2723 &self.inv_freq,
2724 self.pool.as_deref(),
2725 &mut lg,
2726 end == input_ids.len(),
2727 );
2728 }
2729 if end == input_ids.len() {
2730 self.graph_logits = Some(lg);
2731 }
2732 pos = end;
2733 hidden = vec![0.0; self.hidden_size];
2734 }
2735 let dsv41_prefill = self.dsv41_prefill.take();
2736 while self.dsv41.is_some()
2737 && mtp.is_none()
2738 && pos < input_ids.len()
2739 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2740 {
2741 let end = (pos + prefill_chunk()).min(input_ids.len());
2742 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2743 let mut lg = Vec::new();
2744 if let Some(b) = &mut self.dsv41 {
2745 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
2746 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
2747 crate::dsv41::forward_chunk_masked_with_embeddings(
2748 g,
2749 layers,
2750 cfg,
2751 st,
2752 &ids,
2753 pos,
2754 &embeddings[pos..end],
2755 &participates[pos..end],
2756 self.pool.as_deref(),
2757 &mut lg,
2758 );
2759 } else {
2760 crate::dsv41::forward_chunk(
2761 g,
2762 layers,
2763 cfg,
2764 st,
2765 &ids,
2766 pos,
2767 self.pool.as_deref(),
2768 &mut lg,
2769 );
2770 }
2771 }
2772 if end == input_ids.len() {
2773 self.graph_logits = Some(lg);
2774 }
2775 pos = end;
2776 hidden = vec![0.0; self.hidden_size];
2777 }
2778 let dyn_prefill = router.is_some();
2783 let o1_prefill_limit = o1_prefill
2791 .and_then(|requested| self.o1_effective_boundary(requested))
2792 .map(|boundary| boundary.min(input_ids.len()));
2793 let mut o1_sealed = false;
2794 if let Some(limit) = o1_prefill_limit {
2795 if self.can_prefill_batched() && limit > 2 {
2798 let chunk = prefill_chunk();
2799 let hs = self.hidden_size;
2800 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2801 let end = (pos + chunk).min(limit);
2802 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2803 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2804 pos = end;
2805 }
2806 } else {
2807 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2808 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2809 pos += 1;
2810 }
2811 }
2812 if pos >= limit {
2813 o1_sealed = match self.o1_seal_checked() {
2814 Ok(sealed) => sealed,
2815 Err(err) => {
2816 self.finish_generation(&mut mtp, &mut router, true);
2817 return Err(err);
2818 }
2819 };
2820 tracing::info!(
2821 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
2822 o1_prefill.unwrap_or(0),
2823 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
2824 .unwrap_or(limit),
2825 limit,
2826 input_ids.len()
2827 );
2828 }
2829 }
2830 let graph_prefill = self.graph_prefill_preferred();
2836 #[cfg(target_os = "macos")]
2844 if task_mask.is_none()
2845 && !dyn_prefill
2846 && crate::gpu::q1_force()
2847 && crate::gpu::enabled_here()
2848 && self.gdn_cfg.is_some()
2849 && self.g3n.is_none()
2850 && input_ids.len() > 8
2851 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2852 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2853 {
2854 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2855 .ok()
2856 .and_then(|v| v.parse().ok())
2857 .filter(|&v| (16..=512).contains(&v))
2858 .unwrap_or(256);
2859 let hs = self.hidden_size;
2860 let _tp = std::time::Instant::now();
2861 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2862 let end = (pos + chunk).min(input_ids.len());
2863 let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2864 break;
2865 };
2866 if let Some(m) = &mut mtp {
2867 let n_pairs = if end < input_ids.len() {
2868 end - pos
2869 } else {
2870 end - pos - 1
2871 };
2872 if n_pairs > 0 {
2873 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2874 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2875 .collect();
2876 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2877 for (j, (h, t)) in pairs.iter().enumerate() {
2878 let h = h.to_vec();
2879 let _ = self.mtp_step(m, &h, *t, pos + j);
2880 }
2881 }
2882 }
2883 }
2884 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2885 pos = end;
2886 }
2887 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2888 eprintln!(
2889 "metal-prefill: {} of {} tokens in {:.1} ms",
2890 pos,
2891 input_ids.len(),
2892 _tp.elapsed().as_secs_f64() * 1e3
2893 );
2894 }
2895 }
2896 if task_mask.is_none()
2897 && !dyn_prefill
2898 && !graph_prefill
2899 && self.can_prefill_batched()
2900 && self.g3n.is_none()
2901 && o1_prefill.is_none()
2902 && input_ids.len() > 2
2903 {
2904 let chunk = prefill_chunk();
2910 let hs = self.hidden_size;
2911 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2912 let end = (pos + chunk).min(input_ids.len());
2913 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2914 if let Some(m) = &mut mtp {
2915 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2916 .ok()
2917 .and_then(|v| v.parse().ok())
2918 .unwrap_or(0);
2919 for p in pos..end {
2920 if p + 1 < input_ids.len() {
2921 if probe >= 1 && p + 2 < input_ids.len() {
2922 let (d1, mut hx) = self.mtp_step_h(
2926 m,
2927 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2928 input_ids[p + 1],
2929 p,
2930 );
2931 let mut ok = d1 == input_ids[p + 2];
2932 Self::chain_probe_note(0, ok);
2933 let mut d_prev = d1;
2934 let mut extra = 0usize;
2935 for j in 1..probe {
2936 if p + 2 + j >= input_ids.len() {
2937 break;
2938 }
2939 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2940 extra += 1;
2941 ok = ok && dj == input_ids[p + 2 + j];
2942 Self::chain_probe_note(j, ok);
2943 d_prev = dj;
2944 hx = hj;
2945 }
2946 m.kv.truncate_last(extra);
2947 } else {
2948 let _ = self.mtp_step(
2949 m,
2950 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2951 input_ids[p + 1],
2952 p,
2953 );
2954 }
2955 }
2956 }
2957 }
2958 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2959 pos = end;
2960 }
2961 }
2962 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2963 if task_mask.is_none()
2964 && !dyn_prefill
2965 && !graph_prefill
2966 && !pair_off
2967 && self.pair_supported()
2968 && o1_prefill.is_none()
2969 {
2970 while pos + 1 < input_ids.len()
2971 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2972 {
2973 let e1 = self.embed_single(input_ids[pos]);
2974 let e2 = self.embed_single(input_ids[pos + 1]);
2975 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2976 self.commit_linear_scratch();
2978 if let Some(m) = &mut mtp {
2979 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2980 if pos + 2 < input_ids.len() {
2981 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2982 .ok()
2983 .and_then(|v| v.parse().ok())
2984 .unwrap_or(0);
2985 if probe >= 1 && pos + 3 < input_ids.len() {
2986 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2990 let mut ok = d1 == input_ids[pos + 3];
2991 Self::chain_probe_note(0, ok);
2992 let mut d_prev = d1;
2993 let mut extra = 0usize;
2994 for j in 1..probe {
2995 if pos + 3 + j >= input_ids.len() {
2996 break;
2997 }
2998 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2999 extra += 1;
3000 ok = ok && dj == input_ids[pos + 3 + j];
3001 Self::chain_probe_note(j, ok);
3002 d_prev = dj;
3003 hx = hj;
3004 }
3005 m.kv.truncate_last(extra);
3006 } else {
3007 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
3008 }
3009 }
3010 }
3011 hidden = h2;
3012 pos += 2;
3013 }
3014 }
3015 let o1_batch_ready = o1_sealed
3028 && o1_prefill.is_some()
3029 && mtp.is_none()
3030 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
3031 && (0..self.num_layers).all(|li| {
3032 let cache = &self.kv_cache.layers[self.phys_layer(li)];
3033 cache.o1.is_none() || cache.o1_views().is_some()
3034 });
3035 let mtp_batch_prefill = mtp.is_some()
3040 && graph_prefill
3041 && task_mask.is_none()
3042 && !dyn_prefill
3043 && !self.o1_active()
3044 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
3045 if batch_k > 0
3046 && (graph_prefill || o1_batch_ready)
3047 && task_mask.is_none()
3048 && (!self.o1_active() || o1_batch_ready)
3049 && (mtp.is_none() || mtp_batch_prefill)
3050 && !dyn_prefill
3051 && pos + 1 < input_ids.len()
3052 {
3053 let hs = self.hidden_size;
3054 let chunk = batch_k;
3055 while pos < input_ids.len() {
3056 let end = (pos + chunk).min(input_ids.len());
3057 let bk = end - pos;
3058 let mut hiddens = vec![0f32; bk * hs];
3059 for (j, &id) in input_ids[pos..end].iter().enumerate() {
3060 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3061 }
3062 let positions: Vec<usize> = (pos..end).collect();
3063 let t_chunk = std::time::Instant::now();
3064 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3065 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
3066 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3067 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3068 eprintln!(
3069 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3070 if o1_batch_ready {
3071 "o1"
3072 } else if mtp_batch_prefill {
3073 "ordinary_mtp"
3074 } else {
3075 "ordinary"
3076 },
3077 bk as f64 / (ms / 1000.0)
3078 );
3079 }
3080 {
3081 use std::sync::atomic::{AtomicBool, Ordering};
3082 static SAID: AtomicBool = AtomicBool::new(false);
3083 if !SAID.swap(true, Ordering::Relaxed) {
3084 if ok_b {
3085 tracing::info!(
3086 "batched prefill: ACTIVE mode={} (k={bk})",
3087 if o1_batch_ready {
3088 "o1"
3089 } else if mtp_batch_prefill {
3090 "ordinary_mtp"
3091 } else {
3092 "ordinary"
3093 }
3094 );
3095 } else {
3096 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
3097 }
3098 }
3099 }
3100 if ok_b {
3101 if mtp_batch_prefill {
3102 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
3103 if n_pairs > 0 {
3104 let rows: Vec<Vec<f32>> = (0..n_pairs)
3110 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
3111 .collect();
3112 let pairs: Vec<(&[f32], u32)> = rows
3113 .iter()
3114 .enumerate()
3115 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
3116 .collect();
3117 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3118 eprintln!(
3119 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
3120 pos,
3121 n_pairs,
3122 pos + n_pairs - 1,
3123 );
3124 }
3125 let warm_error = if let Some(m) = mtp.as_mut() {
3126 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
3127 } else {
3128 None
3129 };
3130 if let Some(err) = warm_error {
3131 self.finish_generation(&mut mtp, &mut router, true);
3136 return Err(err.to_string());
3137 }
3138 }
3139 }
3140 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
3141 pos = end;
3142 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
3143 self.finish_generation(&mut mtp, &mut router, true);
3148 return Err(if o1_batch_ready {
3149 "sealed O(1) batch graph failed after admission".to_string()
3150 } else {
3151 "ordinary recurrent batch graph failed after admission".to_string()
3152 });
3153 } else {
3154 break; }
3156 }
3157 }
3158 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3159 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
3160 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
3161 if let Some(m) = &mut mtp {
3162 if pos + 1 < input_ids.len() {
3163 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
3169 .ok()
3170 .and_then(|v| v.parse().ok())
3171 .unwrap_or(0);
3172 if probe >= 1 && pos + 2 < input_ids.len() {
3173 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
3174 let mut ok = d1 == input_ids[pos + 2];
3175 Self::chain_probe_note(0, ok);
3176 let mut d_prev = d1;
3177 let mut extra = 0usize;
3178 for j in 1..probe {
3179 if pos + 2 + j >= input_ids.len() {
3180 break;
3181 }
3182 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
3183 extra += 1;
3184 ok = ok && dj == input_ids[pos + 2 + j];
3185 Self::chain_probe_note(j, ok);
3186 d_prev = dj;
3187 hx = hj;
3188 }
3189 m.kv.truncate_last(extra);
3192 } else {
3193 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
3194 }
3195 }
3196 }
3197 pos += 1;
3198 }
3199 if std::env::var("CMF_PREFILL_PROF").is_ok() {
3200 eprintln!(
3201 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
3202 input_ids.len(),
3203 _tpf.elapsed().as_secs_f64() * 1000.0
3204 );
3205 }
3206 if self
3207 .graph_failed
3208 .swap(false, std::sync::atomic::Ordering::Relaxed)
3209 {
3210 self.finish_generation(&mut mtp, &mut router, true);
3215 return Err("GPU token graph failed during prefill".to_string());
3216 }
3217 if self
3220 .cancel
3221 .swap(false, std::sync::atomic::Ordering::Relaxed)
3222 {
3223 self.finish_generation(&mut mtp, &mut router, true);
3227 return Ok(GenerateResult {
3228 text: String::new(),
3229 token_ids: Vec::new(),
3230 prompt_tokens: input_ids.len(),
3231 tokens_generated: 0,
3232 finish_reason: "cancelled".to_string(),
3233 mtp_drafted: 0,
3234 mtp_accepted: 0,
3235 token_confidence: Vec::new(),
3236 traces: Vec::new(),
3237 });
3238 }
3239
3240 if !o1_sealed {
3243 match self.o1_seal_checked() {
3244 Ok(_) => {}
3245 Err(err) => {
3246 self.finish_generation(&mut mtp, &mut router, true);
3247 return Err(err);
3248 }
3249 }
3250 }
3251
3252 macro_rules! commit {
3254 ($id:expr) => {{
3255 all_ids.push($id);
3256 generated += 1;
3257 if self.tokenizer.is_eos($id) {
3258 finish_reason = "stop".to_string();
3259 false
3260 } else {
3261 let token_text = self.tokenizer.decode_token($id);
3262 let mut go = true;
3263 if let Some(ref mut cb) = on_token {
3264 if !cb(&token_text) {
3265 finish_reason = "cancelled".to_string();
3266 go = false;
3267 }
3268 }
3269 go
3270 }
3271 }};
3272 }
3273
3274 let mut spec_trial = SpecTrial::Spec {
3285 t0: std::time::Instant::now(),
3286 gen0: generated,
3287 rounds: 0,
3288 };
3289 let mut spec_mon = SpecMon::default();
3290 let mut spec_watchdog_off = false;
3291 let mut next_pos = input_ids.len();
3293 'decode: while generated < max_tokens {
3294 if self
3295 .graph_failed
3296 .swap(false, std::sync::atomic::Ordering::Relaxed)
3297 {
3298 self.finish_generation(&mut mtp, &mut router, true);
3303 return Err("GPU token graph failed during decode".to_string());
3304 }
3305 if self
3306 .cancel
3307 .swap(false, std::sync::atomic::Ordering::Relaxed)
3308 {
3309 finish_reason = "cancelled".to_string();
3310 break 'decode;
3311 }
3312 let forced = self.spec_forced.take();
3317 let mut logits = match (forced, self.graph_logits.take()) {
3318 (Some(_), _) => Vec::new(),
3319 (None, Some(lg)) => lg,
3320 (None, None) => {
3321 inference::rms_norm_into(
3322 &hidden,
3323 &self.weights.final_norm,
3324 self.rms_eps,
3325 self.norm_style,
3326 &mut self.ws.n1,
3327 );
3328 self.lm_head_forward(&self.ws.n1)
3329 }
3330 };
3331 if generated
3334 == std::env::var("CMF_LOGIT_DUMP_STEP")
3335 .ok()
3336 .and_then(|v| v.parse().ok())
3337 .unwrap_or(0)
3338 {
3339 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3340 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3341 for v in hidden.iter().chain(logits.iter()) {
3342 bytes.extend_from_slice(&v.to_le_bytes());
3343 }
3344 if let Err(e) = std::fs::write(&path, &bytes) {
3345 eprintln!("logit dump: failed to write {path}: {e}");
3346 self.finish_generation(&mut mtp, &mut router, true);
3347 return Err(format!("logit dump write failed: {e}"));
3348 }
3349 }
3350 }
3351 let t_next = match forced {
3352 Some(c) => c,
3353 None => sampler::sample_with_scratch_pool(
3354 &logits,
3355 &self.sampler_config,
3356 &all_ids,
3357 &mut self.rng,
3358 &mut self.sampler_scratch,
3359 self.pool.as_deref(),
3360 ),
3361 };
3362 if self.confidence_on {
3363 confidence.push(if logits.is_empty() {
3364 0.0
3365 } else {
3366 sampler::top1_prob_pool(
3367 self.pool.as_deref(),
3368 &mut self.sampler_scratch,
3369 &logits,
3370 t_next,
3371 calib_temp,
3372 )
3373 });
3374 }
3375 if !logits.is_empty() {
3376 attention::recycle_buf(&mut logits);
3377 }
3378 if trace_on {
3379 let skill = router.as_ref().and_then(|r| r.active_id());
3383 traces.push(TokenTrace {
3384 t: generated,
3385 token_id: t_next,
3386 confidence: confidence.last().copied().unwrap_or(0.0),
3387 active_skill: skill,
3388 recon: None,
3389 switched: false,
3390 });
3391 }
3392 if !commit!(t_next) {
3393 break 'decode;
3394 }
3395 if generated >= max_tokens {
3396 break 'decode;
3397 }
3398
3399 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
3400 static SAID: std::sync::Once = std::sync::Once::new();
3406 SAID.call_once(|| {
3407 tracing::warn!(
3408 "KV cache full at {} positions — evicting half; quality \
3409 will degrade. Raise CMF_MAX_SEQ.",
3410 self.kv_cache.max_seq_len,
3411 );
3412 });
3413 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3414 self.kv_cache.evict(keep);
3415 }
3416
3417 if graph_spec {
3420 match spec_trial {
3421 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3422 spec_mon.plain_ms =
3423 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3424 let keep = spec_mon.pays();
3425 tracing::info!(
3426 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3427 spec_mon.tokens,
3428 spec_mon.round_ms,
3429 spec_mon.plain_ms,
3430 if keep { "speculating" } else { "plain" }
3431 );
3432 spec_mon.fails = 0;
3433 spec_trial = SpecTrial::Decided {
3434 spec: keep,
3435 recheck_at: if keep { usize::MAX } else { generated + 128 },
3436 };
3437 }
3438 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3439 spec_mon.n = 0;
3440 spec_trial = SpecTrial::Spec {
3441 t0: std::time::Instant::now(),
3442 gen0: generated,
3443 rounds: 0,
3444 };
3445 }
3446 _ => {}
3447 }
3448 spec_watchdog_off = matches!(
3449 spec_trial,
3450 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3451 );
3452 }
3453 match &mut mtp {
3454 #[cfg(feature = "gpu")]
3456 Some(m)
3457 if graph_spec
3458 && !spec_watchdog_off
3459 && generated + 1 < max_tokens
3460 && next_pos > 0 =>
3461 {
3462 let t_round = std::time::Instant::now();
3463 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3464 m,
3465 &hidden,
3466 t_next,
3467 next_pos,
3468 &mut drafted,
3469 &mut accepted,
3470 &mut all_ids,
3471 ) {
3472 next_pos = n_pos;
3473 hidden = new_h;
3474 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3475 eprintln!(
3476 "spec-round wall {:.1} ms → {} tokens",
3477 t_round.elapsed().as_secs_f64() * 1e3,
3478 extra.len() + 1
3479 );
3480 }
3481 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3485 spec_trial = Self::spec_trial_round(
3488 spec_trial,
3489 &mut spec_mon,
3490 generated + extra.len() + 1,
3491 );
3492 let mut stopped = false;
3493 for &id in &extra {
3494 if self.confidence_on {
3495 confidence.push(0.0);
3496 }
3497 if !commit!(id) {
3498 stopped = true;
3499 break;
3500 }
3501 }
3502 if stopped {
3503 break 'decode;
3504 }
3505 continue 'decode;
3506 }
3507 if self
3508 .graph_failed
3509 .swap(false, std::sync::atomic::Ordering::Relaxed)
3510 {
3511 self.finish_generation(&mut mtp, &mut router, true);
3517 return Err("GPU MTP graph failed during speculative decode".to_string());
3518 }
3519 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3530 spec_mon.tokens = 0.0;
3531 spec_mon.fails = 3;
3532 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3533 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3534 next_pos += 1;
3535 continue 'decode;
3536 }
3537 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3539 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3540 drafted += 1;
3541 let emb1 = self.embed_single(t_next);
3542 let emb2 = self.embed_single(draft);
3543 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3544
3545 inference::rms_norm_into(
3546 &h1,
3547 &self.weights.final_norm,
3548 self.rms_eps,
3549 self.norm_style,
3550 &mut self.ws.n1,
3551 );
3552 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3553 let t_after = sampler::sample_with_scratch_pool(
3554 &logits1,
3555 &self.sampler_config,
3556 &all_ids,
3557 &mut self.rng,
3558 &mut self.sampler_scratch,
3559 self.pool.as_deref(),
3560 );
3561 if self.confidence_on {
3562 confidence.push(sampler::top1_prob_pool(
3563 self.pool.as_deref(),
3564 &mut self.sampler_scratch,
3565 &logits1,
3566 t_after,
3567 calib_temp,
3568 ));
3569 }
3570 attention::recycle_buf(&mut logits1);
3571 if trace_on {
3572 traces.push(TokenTrace {
3575 t: generated,
3576 token_id: t_after,
3577 confidence: confidence.last().copied().unwrap_or(0.0),
3578 active_skill: None,
3579 recon: None,
3580 switched: false,
3581 });
3582 }
3583 let stop = !commit!(t_after);
3584
3585 if t_after == draft {
3586 accepted += 1;
3587 self.commit_linear_scratch();
3588 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3589 hidden = h2;
3590 next_pos += 2;
3591 } else {
3592 for layer in &mut self.kv_cache.layers {
3594 layer.truncate_last(1);
3595 }
3596 if !stop {
3597 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3598 hidden = self.forward_layers(
3599 &self.embed_single(t_after),
3600 next_pos + 1,
3601 None,
3602 );
3603 }
3604 next_pos += 2;
3605 }
3606 if stop {
3607 break 'decode;
3608 }
3609 }
3610 _ => {
3612 #[cfg(feature = "gpu")]
3617 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3618 static SAID: std::sync::Once = std::sync::Once::new();
3619 SAID.call_once(|| {
3620 eprintln!(
3621 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3622 !self.dsv4_mtp.is_empty(),
3623 task_mask.is_none(),
3624 router.is_none(),
3625 !trace_on,
3626 self.sampler_config.temperature < 1e-6,
3627 self.sampler_config.repetition_penalty == 1.0,
3628 );
3629 });
3630 }
3631 #[cfg(feature = "gpu")]
3632 if Self::dsv4_spec_on()
3633 && self.dsv4.is_some()
3634 && !self.dsv4_mtp.is_empty()
3635 && task_mask.is_none()
3636 && router.is_none()
3637 && !trace_on
3638 && self.sampler_config.temperature < 1e-6
3639 && self.sampler_config.repetition_penalty == 1.0
3640 && generated + 1 < max_tokens
3641 && all_ids.len() >= 2
3642 && generated >= dsv4_spec_retry_at
3643 {
3644 let tip_token = all_ids[all_ids.len() - 2];
3645 let drafted0 = drafted;
3646 let round = self.dsv4_spec_step(
3647 tip_token,
3648 t_next,
3649 next_pos,
3650 max_tokens.saturating_sub(generated),
3651 &mut drafted,
3652 &mut accepted,
3653 );
3654 if drafted > drafted0 {
3655 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3656 if useful {
3657 dsv4_spec_bad = 0;
3658 } else {
3659 dsv4_spec_bad += 1;
3660 if dsv4_spec_bad >= 2 {
3661 dsv4_spec_bad = 0;
3662 dsv4_spec_retry_at = generated.saturating_add(32);
3663 tracing::info!(
3664 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3665 );
3666 }
3667 }
3668 }
3669 if let Some((extra, n_pos)) = round {
3670 next_pos = n_pos;
3671 let mut stopped = false;
3672 for &id in &extra {
3673 if self.confidence_on {
3674 confidence.push(0.0);
3675 }
3676 if !commit!(id) {
3677 stopped = true;
3678 break;
3679 }
3680 }
3681 if stopped {
3682 break 'decode;
3683 }
3684 continue 'decode;
3685 }
3686 }
3687 self.graph_want_logits = fuse_lm;
3688 let mut t_fwd = t_next;
3694 let pure_greedy = self.sampler_config.temperature < 1e-6
3695 && self.sampler_config.repetition_penalty == 1.0
3696 && self.sampler_config.suppress_tokens.is_empty();
3697 let burst_k = std::env::var("CMF_MULTISTEP")
3702 .ok()
3703 .and_then(|v| v.parse::<usize>().ok())
3704 .unwrap_or(0);
3705 if pure_greedy
3706 && burst_k >= 1
3707 && fuse_lm
3708 && task_mask.is_none()
3709 && router.is_none()
3710 && !trace_on
3711 && !self.confidence_on
3712 {
3713 let mut stopped = false;
3714 loop {
3715 let room = max_tokens.saturating_sub(generated);
3716 if room <= 2 {
3717 break;
3718 }
3719 let k = burst_k.min(room - 1);
3720 if k < 1 {
3721 break;
3722 }
3723 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3724 if self
3725 .graph_failed
3726 .swap(false, std::sync::atomic::Ordering::Relaxed)
3727 {
3728 self.finish_generation(&mut mtp, &mut router, true);
3729 return Err(
3730 "GPU token graph failed during greedy burst".to_string()
3731 );
3732 }
3733 break;
3734 };
3735 next_pos += k;
3736 for &id in &ids {
3737 if !commit!(id) {
3738 stopped = true;
3739 break;
3740 }
3741 }
3742 if stopped {
3743 break;
3744 }
3745 t_fwd = *ids.last().unwrap();
3746 }
3747 if stopped {
3748 break 'decode;
3749 }
3750 }
3751 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3752 next_pos += 1;
3753 if let Some(r) = &mut router {
3756 let phi = self.dyn_phi_ema.clone();
3757 let decision = r.step(&phi, generated);
3758 if let Some(new_active) = decision {
3759 let _ = self.set_active_skill(new_active);
3760 }
3761 if trace_on {
3764 if let Some(last) = traces.last_mut() {
3765 let e = r.last_best_e();
3766 last.recon = e.is_finite().then_some(e);
3767 last.switched = decision.is_some();
3768 }
3769 }
3770 }
3771 }
3772 }
3773 }
3774
3775 let cancelled = finish_reason == "cancelled";
3776 self.finish_generation(&mut mtp, &mut router, cancelled);
3777
3778 let output_ids = &all_ids[input_ids.len()..];
3779 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3783 if cancelled {
3784 self.kv_history.clear();
3785 } else {
3786 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3787 }
3788 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3790 Ok(GenerateResult {
3791 text: self.tokenizer.decode(output_ids),
3792 token_ids: output_ids.to_vec(),
3793 prompt_tokens: input_ids.len(),
3794 tokens_generated: generated,
3795 finish_reason,
3796 mtp_drafted: drafted,
3797 mtp_accepted: accepted,
3798 token_confidence: confidence,
3799 traces,
3800 })
3801 }
3802
3803 fn mtp_step(
3807 &mut self,
3808 m: &mut MtpModule,
3809 hidden: &[f32],
3810 next_token: u32,
3811 position: usize,
3812 ) -> u32 {
3813 self.mtp_step_h(m, hidden, next_token, position).0
3814 }
3815
3816 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3820 use std::sync::Mutex;
3821 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3822 let mut t = T.lock().unwrap();
3823 if t.len() <= depth {
3824 t.resize(depth + 1, (0, 0));
3825 }
3826 t[depth].0 += 1;
3827 t[depth].1 += prefix_ok as u64;
3828 if depth == 0 && t[0].0 % 128 == 0 {
3829 let line: Vec<String> = t
3830 .iter()
3831 .enumerate()
3832 .map(|(d, (n, k))| {
3833 format!(
3834 "d{}={:.0}%({n})",
3835 d + 1,
3836 100.0 * *k as f64 / (*n).max(1) as f64
3837 )
3838 })
3839 .collect();
3840 eprintln!("mtp-chain: {}", line.join(" "));
3841 }
3842 }
3843
3844 fn mtp_step_hl(
3852 &mut self,
3853 m: &mut MtpModule,
3854 hidden: &[f32],
3855 next_token: u32,
3856 position: usize,
3857 ) -> (Vec<f32>, Vec<f32>) {
3858 #[cfg(target_os = "macos")]
3863 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3864 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3865 self.mtp_graph_mode = Some(true);
3866 return r;
3867 }
3868 if self.mtp_graph_mode == Some(true) {
3869 tracing::error!("mtp Metal graph failed after admission");
3870 self.clear_sequence_state();
3871 self.graph_failed
3872 .store(true, std::sync::atomic::Ordering::Relaxed);
3873 self.cancel
3874 .store(true, std::sync::atomic::Ordering::Relaxed);
3875 return (Vec::new(), Vec::new());
3876 }
3877 self.mtp_graph_mode = Some(false);
3878 }
3879 #[cfg(feature = "gpu")]
3880 if self.mtp_graph_mode != Some(false) {
3881 if !self.mtp_graph_ok(m) {
3882 if self.mtp_graph_mode == Some(true) {
3883 tracing::error!("mtp graph became unavailable after admission");
3888 self.clear_sequence_state();
3889 self.graph_failed
3890 .store(true, std::sync::atomic::Ordering::Relaxed);
3891 self.cancel
3892 .store(true, std::sync::atomic::Ordering::Relaxed);
3893 return (Vec::new(), Vec::new());
3894 }
3895 self.mtp_graph_mode = Some(false);
3896 } else {
3897 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
3898 self.mtp_graph_mode = Some(true);
3899 return r;
3900 }
3901 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
3902 return (Vec::new(), Vec::new());
3909 }
3910 tracing::error!("mtp graph failed or declined after admission");
3914 self.clear_sequence_state();
3915 self.graph_failed
3916 .store(true, std::sync::atomic::Ordering::Relaxed);
3917 self.cancel
3918 .store(true, std::sync::atomic::Ordering::Relaxed);
3919 return (Vec::new(), Vec::new());
3920 }
3921 }
3922 let e = self.embed_single(next_token);
3926 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3927 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3928 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3929 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3930 let mut x = vec![0.0f32; self.hidden_size];
3931 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3932
3933 let lw = &m.layer;
3935 inference::rms_norm_into(
3936 &x,
3937 &lw.input_norm,
3938 self.rms_eps,
3939 self.norm_style,
3940 &mut self.ws.n1,
3941 );
3942 let attn = match &lw.attn {
3943 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3945 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3946 AttnKind::Full {
3947 wq,
3948 wk,
3949 wv,
3950 wo,
3951 q_norm,
3952 k_norm,
3953 output_gate,
3954 softplus_gate,
3955 bias,
3956 } => {
3957 let mut cfg = self.attn_cfg(position);
3958 cfg.q_norm = q_norm.as_deref();
3959 cfg.k_norm = k_norm.as_deref();
3960 cfg.output_gate = *output_gate;
3961 cfg.softplus_gate = softplus_gate
3962 .as_ref()
3963 .map(|(gate, per_head)| (gate, *per_head));
3964 cfg.bias = bias
3965 .as_ref()
3966 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3967 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3968 }
3969 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3970 unreachable!("MTP block is full attention")
3971 }
3972 };
3973 for (i, &a) in attn.iter().enumerate() {
3974 x[i] += a;
3975 }
3976 inference::rms_norm_into(
3977 &x,
3978 &lw.post_norm,
3979 self.rms_eps,
3980 self.norm_style,
3981 &mut self.ws.p1,
3982 );
3983 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3984 for (i, &f) in ffn.iter().enumerate() {
3985 x[i] += f;
3986 }
3987
3988 inference::rms_norm_into(
3989 &x,
3990 &m.final_norm,
3991 self.rms_eps,
3992 self.norm_style,
3993 &mut self.ws.n1,
3994 );
3995 let lg = self.lm_head_forward(&self.ws.n1);
3996 (lg, x)
3997 }
3998
3999 fn mtp_step_h(
4001 &mut self,
4002 m: &mut MtpModule,
4003 hidden: &[f32],
4004 next_token: u32,
4005 position: usize,
4006 ) -> (u32, Vec<f32>) {
4007 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
4008 let draft = sampler::argmax(&lg);
4009 attention::recycle_buf(&mut lg);
4010 (draft, x)
4011 }
4012
4013 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
4019 match trial {
4020 SpecTrial::Spec { t0, gen0, rounds } => {
4021 let rounds = rounds + 1;
4022 if rounds >= 5 {
4023 if mon.plain_ms > 0.0 {
4024 let keep = mon.pays();
4025 mon.fails = 0;
4026 tracing::info!(
4027 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
4028 mon.tokens,
4029 mon.round_ms,
4030 mon.plain_ms,
4031 if keep { "speculating" } else { "plain" }
4032 );
4033 SpecTrial::Decided {
4034 spec: keep,
4035 recheck_at: if keep { usize::MAX } else { generated + 128 },
4036 }
4037 } else {
4038 SpecTrial::Plain {
4039 t0: std::time::Instant::now(),
4040 gen0: generated,
4041 }
4042 }
4043 } else {
4044 SpecTrial::Spec { t0, gen0, rounds }
4045 }
4046 }
4047 SpecTrial::Decided { spec: true, .. } => {
4048 if mon.pays() {
4049 mon.fails = 0;
4050 trial
4051 } else {
4052 mon.fails += 1;
4053 if mon.fails >= 4 {
4054 tracing::info!(
4055 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
4056 mon.tokens,
4057 mon.round_ms,
4058 mon.plain_ms
4059 );
4060 SpecTrial::Decided {
4061 spec: false,
4062 recheck_at: generated + 128,
4063 }
4064 } else {
4065 trial
4066 }
4067 }
4068 }
4069 other => other,
4070 }
4071 }
4072
4073 fn mtp_kv_id(&self) -> u64 {
4076 self.graph_kv_id | (1u64 << 40)
4077 }
4078
4079 const MTP_LAYER_BASE: usize = 0;
4084
4085 #[cfg(feature = "gpu")]
4092 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
4093 self.mtp_graph_mode != Some(true)
4094 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
4095 }
4096
4097 #[cfg(feature = "gpu")]
4103 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
4104 let mut ok = true;
4105 let mut expected = false;
4106 for li in 0..self.num_layers {
4107 if matches!(
4108 self.weights.layers[self.phys_layer(li)].attn,
4109 AttnKind::Full { .. }
4110 ) {
4111 expected = true;
4112 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
4113 }
4114 }
4115 !expected || ok
4116 }
4117
4118 fn graph_gdn_layer_count(&self) -> usize {
4122 (0..self.num_layers)
4123 .filter(|&li| {
4124 matches!(
4125 &self.weights.layers[self.phys_layer(li)].attn,
4126 AttnKind::LinearGdn(_)
4127 )
4128 })
4129 .count()
4130 }
4131
4132 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
4135 let e = self.embed_single(next_token);
4136 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4137 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4138 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4139 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4140 let mut x = vec![0.0f32; self.hidden_size];
4141 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4142 x
4143 }
4144
4145 #[cfg(feature = "gpu")]
4148 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
4149 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
4150 return false;
4151 }
4152 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
4153 || !crate::gpu::enabled_here()
4154 || self.attn_softcap > 0.0
4155 || self.attention_heads_per_layer.is_some()
4156 {
4157 return false;
4158 }
4159 matches!(
4160 &m.layer.attn,
4161 AttnKind::Full {
4162 softplus_gate: None,
4163 ..
4164 }
4165 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
4166 }
4167
4168 #[cfg(feature = "gpu")]
4172 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
4173 if !self.mtp_block_graph_ok(m) {
4174 return false;
4175 }
4176 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
4177 return false;
4178 };
4179 let FfnKind::Dense(d) = &m.layer.ffn else {
4180 return false;
4181 };
4182 d.segs.is_empty()
4183 && wq.graph_weight().is_some()
4184 && wk.graph_weight().is_some()
4185 && wv.graph_weight().is_some()
4186 && wo.graph_weight().is_some()
4187 && d.gate_proj.graph_weight().is_some()
4188 && d.up_proj.graph_weight().is_some()
4189 && d.down_proj.graph_weight().is_some()
4190 && self.weights.lm_head.graph_weight().is_some()
4191 }
4192
4193 #[cfg(feature = "gpu")]
4199 fn mtp_step_graph(
4200 &mut self,
4201 m: &mut MtpModule,
4202 hidden: &[f32],
4203 next_token: u32,
4204 position: usize,
4205 ) -> Option<(Vec<f32>, Vec<f32>)> {
4206 if !self.mtp_graph_ok(m) {
4207 return None;
4208 }
4209 let lw = &m.layer;
4210 let AttnKind::Full {
4211 wq,
4212 wk,
4213 wv,
4214 wo,
4215 q_norm,
4216 k_norm,
4217 output_gate,
4218 softplus_gate,
4219 bias,
4220 } = &lw.attn
4221 else {
4222 return None;
4223 };
4224 if softplus_gate.is_some() {
4225 return None;
4226 }
4227 let FfnKind::Dense(d) = &lw.ffn else {
4228 return None;
4229 };
4230 if !d.segs.is_empty() {
4231 return None; }
4233 let mut x = self.mtp_block_input(m, hidden, next_token);
4236 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4237 let (_, i, kind, rs) = t.graph_weight()?;
4238 Some(crate::gpu::GraphW {
4239 idx: i,
4240 kind,
4241 row_scale: rs,
4242 data: &[],
4243 })
4244 }
4245 let (model, _, _, _) = wq.graph_weight()?;
4246 let model = model.clone();
4247 let (lm_gw, lm_rows) = {
4248 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4249 (
4250 crate::gpu::GraphW {
4251 idx: i,
4252 kind,
4253 row_scale: rs,
4254 data: &[],
4255 },
4256 self.weights.lm_head.rows(),
4257 )
4258 };
4259 let layer = crate::gpu::GraphLayer {
4260 input_norm: &lw.input_norm,
4261 attn: crate::gpu::GraphAttn::Full {
4262 wq: gw(wq)?,
4263 wk: gw(wk)?,
4264 wv: gw(wv)?,
4265 wo: gw(wo)?,
4266 q_norm: q_norm.as_deref(),
4267 k_norm: k_norm.as_deref(),
4268 bias: bias
4269 .as_ref()
4270 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4271 output_gate: *output_gate,
4272 cpu_k: m.kv.k_heads(),
4273 cpu_v: m.kv.v_heads(),
4274 },
4275 post_norm: &lw.post_norm,
4276 ffn: crate::gpu::GraphFfn::Dense {
4277 gate: gw(&d.gate_proj)?,
4278 up: gw(&d.up_proj)?,
4279 down: gw(&d.down_proj)?,
4280 },
4281 };
4282 let nh = self.num_heads;
4283 let (nkv, hd, rd) = self.layer_geom(0);
4284 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4285 let mut logits = Vec::new();
4286 let ok = crate::gpu::forward_token_graph(
4287 &model,
4288 self.mtp_kv_id(),
4289 std::slice::from_ref(&layer),
4290 &[None],
4291 self.o1_epoch,
4292 &self.inv_freq,
4293 &mut x,
4294 nh,
4295 nkv,
4296 hd,
4297 self.attn_scale,
4298 rd,
4299 self.hidden_size,
4300 self.intermediate_size,
4301 position,
4302 self.kv_cache.max_seq_len,
4303 gemma,
4304 self.rms_eps as f32,
4305 Some((&lm_gw, lm_rows)),
4306 &m.final_norm,
4307 &mut logits,
4308 &[],
4309 1,
4310 None,
4311 None,
4312 None,
4313 Self::MTP_LAYER_BASE,
4314 true,
4315 );
4316 match ok {
4317 crate::gpu::TokenGraphOutcome::Completed => {}
4318 crate::gpu::TokenGraphOutcome::Declined => return None,
4319 crate::gpu::TokenGraphOutcome::Failed => {
4320 self.clear_sequence_state();
4324 self.graph_failed
4325 .store(true, std::sync::atomic::Ordering::Relaxed);
4326 self.cancel
4327 .store(true, std::sync::atomic::Ordering::Relaxed);
4328 return None;
4329 }
4330 }
4331 logits.resize(self.vocab_size, 0.0);
4332 Some((logits, x))
4333 }
4334
4335 #[cfg(feature = "gpu")]
4343 fn mtp_warm_graph(
4344 &mut self,
4345 m: &mut MtpModule,
4346 pairs: &[(&[f32], u32)],
4347 first_pos: usize,
4348 ) -> crate::gpu::BatchGraphOutcome {
4349 if pairs.is_empty() {
4350 return crate::gpu::BatchGraphOutcome::Completed;
4351 }
4352 if !self.mtp_block_graph_ok(m) {
4353 return crate::gpu::BatchGraphOutcome::Declined;
4354 }
4355 let hs = self.hidden_size;
4356 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4359 for (h, t) in pairs {
4360 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4361 }
4362 let lw = &m.layer;
4363 let AttnKind::Full {
4364 wq,
4365 wk,
4366 wv,
4367 wo,
4368 q_norm,
4369 k_norm,
4370 output_gate,
4371 bias,
4372 ..
4373 } = &lw.attn
4374 else {
4375 return crate::gpu::BatchGraphOutcome::Declined;
4376 };
4377 let FfnKind::Dense(d) = &lw.ffn else {
4378 return crate::gpu::BatchGraphOutcome::Declined;
4379 };
4380 if !d.segs.is_empty() {
4381 return crate::gpu::BatchGraphOutcome::Declined; }
4383 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4384 let (_, i, kind, rs) = t.graph_weight()?;
4385 Some(crate::gpu::GraphW {
4386 idx: i,
4387 kind,
4388 row_scale: rs,
4389 data: &[],
4390 })
4391 }
4392 let Some((model, _, _, _)) = wq.graph_weight() else {
4393 return crate::gpu::BatchGraphOutcome::Declined;
4394 };
4395 let model = model.clone();
4396 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4397 gw(wq),
4398 gw(wk),
4399 gw(wv),
4400 gw(wo),
4401 gw(&d.gate_proj),
4402 gw(&d.up_proj),
4403 gw(&d.down_proj),
4404 ) else {
4405 return crate::gpu::BatchGraphOutcome::Declined;
4406 };
4407 let layer = crate::gpu::GraphLayer {
4408 input_norm: &lw.input_norm,
4409 attn: crate::gpu::GraphAttn::Full {
4410 wq: gwq,
4411 wk: gwk,
4412 wv: gwv,
4413 wo: gwo,
4414 q_norm: q_norm.as_deref(),
4415 k_norm: k_norm.as_deref(),
4416 bias: bias
4417 .as_ref()
4418 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4419 output_gate: *output_gate,
4420 cpu_k: m.kv.k_heads(),
4421 cpu_v: m.kv.v_heads(),
4422 },
4423 post_norm: &lw.post_norm,
4424 ffn: crate::gpu::GraphFfn::Dense {
4425 gate: gg,
4426 up: gu,
4427 down: gd,
4428 },
4429 };
4430 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4431 let nh = self.num_heads;
4432 let (nkv, hd, rd) = self.layer_geom(0);
4433 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4434 crate::gpu::forward_batch_graph(
4435 &model,
4436 self.mtp_kv_id(),
4437 std::slice::from_ref(&layer),
4438 &self.inv_freq,
4439 &mut hiddens,
4440 nh,
4441 nkv,
4442 hd,
4443 rd,
4444 hs,
4445 self.intermediate_size,
4446 &positions,
4447 self.kv_cache.max_seq_len,
4448 gemma,
4449 self.rms_eps as f32,
4450 self.attn_scale,
4451 pairs.len(),
4452 &[],
4453 0,
4454 None,
4455 )
4456 }
4457
4458 #[cfg(feature = "gpu")]
4465 fn mtp_warm_graph_fallback(
4466 &mut self,
4467 m: &mut MtpModule,
4468 pairs: &[(&[f32], u32)],
4469 first_pos: usize,
4470 ) -> bool {
4471 if pairs.is_empty() {
4472 return true;
4473 }
4474 let graphable = self.mtp_block_graph_ok(m);
4475 if !graphable {
4476 if self.mtp_graph_mode == Some(true) {
4480 return false;
4481 }
4482 self.mtp_graph_mode = Some(false);
4483 for (j, (h, t)) in pairs.iter().enumerate() {
4484 self.mtp_warm(m, h, *t, first_pos + j);
4485 }
4486 return true;
4487 }
4488
4489 for (j, (h, t)) in pairs.iter().enumerate() {
4494 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4495 return false;
4496 }
4497 }
4498 self.mtp_graph_mode = Some(true);
4499 true
4500 }
4501
4502 #[cfg(feature = "gpu")]
4507 fn mtp_warm_prefill_pairs(
4508 &mut self,
4509 m: &mut MtpModule,
4510 pairs: &[(&[f32], u32)],
4511 first_pos: usize,
4512 ) -> Result<(), &'static str> {
4513 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4518 if self.mtp_graph_mode == Some(true) {
4519 return Err("MTP token graph became unavailable after admission");
4520 }
4521 self.mtp_graph_mode = Some(false);
4522 for (j, (h, t)) in pairs.iter().enumerate() {
4523 self.mtp_warm(m, h, *t, first_pos + j);
4524 }
4525 return Ok(());
4526 }
4527 match self.mtp_warm_graph(m, pairs, first_pos) {
4528 crate::gpu::BatchGraphOutcome::Completed => {
4529 if !pairs.is_empty() {
4530 self.mtp_graph_mode = Some(true);
4531 }
4532 Ok(())
4533 }
4534 crate::gpu::BatchGraphOutcome::Declined => {
4535 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4536 Ok(())
4537 } else {
4538 Err("MTP warm-up fallback failed after device admission")
4539 }
4540 }
4541 crate::gpu::BatchGraphOutcome::Failed => {
4542 Err("MTP warm batch graph failed after admission")
4543 }
4544 }
4545 }
4546
4547 #[cfg(not(feature = "gpu"))]
4548 fn mtp_warm_prefill_pairs(
4549 &mut self,
4550 m: &mut MtpModule,
4551 pairs: &[(&[f32], u32)],
4552 first_pos: usize,
4553 ) -> Result<(), &'static str> {
4554 for (j, (h, t)) in pairs.iter().enumerate() {
4555 self.mtp_warm(m, h, *t, first_pos + j);
4556 }
4557 Ok(())
4558 }
4559
4560 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4564 let e = self.embed_single(next_token);
4565 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4566 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4567 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4568 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4569 let mut x = vec![0.0f32; self.hidden_size];
4570 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4571 inference::rms_norm_into(
4572 &x,
4573 &m.layer.input_norm,
4574 self.rms_eps,
4575 self.norm_style,
4576 &mut self.ws.n1,
4577 );
4578 let attn = match &m.layer.attn {
4579 AttnKind::Full {
4580 wq,
4581 wk,
4582 wv,
4583 wo,
4584 q_norm,
4585 k_norm,
4586 output_gate,
4587 softplus_gate,
4588 bias,
4589 } => {
4590 let mut cfg = self.attn_cfg(position);
4591 cfg.q_norm = q_norm.as_deref();
4592 cfg.k_norm = k_norm.as_deref();
4593 cfg.output_gate = *output_gate;
4594 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4595 cfg.bias = bias
4596 .as_ref()
4597 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4598 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4599 }
4600 _ => return,
4601 };
4602 let _ = attn;
4603 }
4604
4605 #[cfg(feature = "gpu")]
4612 #[allow(clippy::too_many_arguments)]
4613 fn graph_spec_step(
4614 &mut self,
4615 m: &mut MtpModule,
4616 hidden: &[f32],
4617 t_next: u32,
4618 next_pos: usize,
4619 drafted: &mut usize,
4620 accepted: &mut usize,
4621 all_ids: &mut Vec<u32>,
4625 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4626 #[cfg(target_os = "macos")]
4637 let metal_native = crate::gpu::q1_force();
4638 #[cfg(not(target_os = "macos"))]
4639 let metal_native = false;
4640 #[cfg(feature = "gpu")]
4641 let k_default = if metal_native {
4642 7
4645 } else if crate::gpu_wgpu::verify_i8_on() {
4646 5
4647 } else {
4648 4
4649 };
4650 #[cfg(not(feature = "gpu"))]
4651 let k_default = 4;
4652 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4653 .ok()
4654 .and_then(|v| v.parse().ok())
4655 .filter(|&v| (1..=8).contains(&v))
4656 .unwrap_or(k_default);
4657 if next_pos == 0 {
4658 return None;
4659 }
4660 let t_round = std::time::Instant::now();
4661 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4677 let sub0 = subs();
4678 let cfg = self.sampler_config.clone();
4683 let penalized = !(cfg.repetition_penalty == 1.0
4684 && cfg.presence_penalty == 0.0
4685 && cfg.suppress_tokens.is_empty());
4686 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4691 let sampling = cfg.temperature >= 1e-6;
4692 let sparse = sampling && sampler::sparse_ok(&cfg);
4698 let base_len = all_ids.len();
4699 if sampling && !sparse && self.spec_q.len() < k_spec {
4700 self.spec_q.resize_with(k_spec, Vec::new);
4701 }
4702 if sparse && self.spec_qs.len() < k_spec {
4703 self.spec_qs.resize_with(k_spec, Vec::new);
4704 }
4705 let mut drafts = Vec::with_capacity(k_spec);
4710 let mut hx = hidden.to_vec();
4711 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4714 for j in 0..k_spec {
4715 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4716 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4717 if spec_dbg {
4718 let saved = self.mtp_graph_mode;
4719 self.mtp_graph_mode = Some(false);
4720 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4721 self.mtp_graph_mode = saved;
4722 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4723 return None;
4724 }
4725 m.kv.truncate_last(1);
4726 dbg_ref = Some(r);
4727 }
4728 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4729 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4730 return None;
4731 }
4732 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4733 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4734 let dl = lg
4735 .iter()
4736 .zip(&lg_cpu)
4737 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4738 let dh = hj
4739 .iter()
4740 .zip(&h_cpu)
4741 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4742 eprintln!(
4743 "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 {}",
4744 next_pos - 1 + j,
4745 sampler::argmax(&lg_cpu),
4746 sampler::argmax(&lg),
4747 n(&h_cpu),
4748 n(&hj),
4749 m.kv.seq_len
4750 );
4751 }
4752 let dj = if sparse {
4753 let mut q = std::mem::take(&mut self.spec_qs[j]);
4754 let ok = sampler::sparse_distribution_into(
4755 &lg,
4756 &cfg,
4757 all_ids,
4758 &mut self.sampler_scratch,
4759 self.pool.as_deref(),
4760 &mut q,
4761 );
4762 let d = if ok {
4763 sampler::draw_sparse(&q, &mut self.rng)
4764 } else {
4765 let t = sampler::argmax(&lg);
4767 q.clear();
4768 q.push((t, 1.0));
4769 t
4770 };
4771 self.spec_qs[j] = q;
4772 all_ids.push(d);
4773 d
4774 } else if sampling {
4775 let mut q = std::mem::take(&mut self.spec_q[j]);
4776 sampler::distribution_into(
4777 &lg,
4778 &cfg,
4779 all_ids,
4780 &mut self.sampler_scratch,
4781 self.pool.as_deref(),
4782 &mut q,
4783 );
4784 let d = sampler::draw(&q, &mut self.rng);
4785 self.spec_q[j] = q;
4786 all_ids.push(d); d
4788 } else if greedy_pen {
4789 let d = sampler::argmax_penalized(
4790 &lg,
4791 &cfg,
4792 all_ids,
4793 &mut self.sampler_scratch,
4794 self.pool.as_deref(),
4795 );
4796 all_ids.push(d);
4797 d
4798 } else {
4799 sampler::argmax(&lg)
4800 };
4801 attention::recycle_buf(&mut lg);
4802 drafts.push(dj);
4803 hx = hj;
4804 }
4805 all_ids.truncate(base_len);
4806 *drafted += k_spec;
4807 let t_draft = t_round.elapsed();
4808 let sub_draft = subs();
4809 let b = k_spec + 1;
4812 let mut hiddens = vec![0.0f32; b * self.hidden_size];
4813 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4814 let e = self.embed_single(t);
4815 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4816 }
4817 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4818 let (lm_gw, lm_rows) = {
4819 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4820 (
4821 crate::gpu::GraphW {
4822 idx: i,
4823 kind,
4824 row_scale: rs,
4825 data: &[],
4826 },
4827 self.weights.lm_head.rows(),
4828 )
4829 };
4830 let mut logits = Vec::new();
4831 let final_norm = self.weights.final_norm.clone();
4832 #[cfg(target_os = "macos")]
4833 let verify_outcome = if metal_native {
4834 let lm = self.weights.lm_head.q1_parts()?;
4835 self.try_batch_graph_metal(
4836 &mut hiddens,
4837 &positions,
4838 b,
4839 Some((lm, &final_norm, &mut logits)),
4840 )
4841 } else {
4842 self.try_batch_graph_wgpu(
4843 &mut hiddens,
4844 &positions,
4845 b,
4846 Some(crate::gpu::SpecTail {
4847 lm: lm_gw,
4848 lm_rows,
4849 final_norm: &final_norm,
4850 logits_out: &mut logits,
4851 }),
4852 )
4853 };
4854 #[cfg(not(target_os = "macos"))]
4855 let verify_outcome = self.try_batch_graph_wgpu(
4856 &mut hiddens,
4857 &positions,
4858 b,
4859 Some(crate::gpu::SpecTail {
4860 lm: lm_gw,
4861 lm_rows,
4862 final_norm: &final_norm,
4863 logits_out: &mut logits,
4864 }),
4865 );
4866 match verify_outcome {
4867 crate::gpu::BatchGraphOutcome::Completed => {}
4868 crate::gpu::BatchGraphOutcome::Declined => {
4869 m.kv.truncate_last(k_spec);
4873 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
4874 self.clear_sequence_state();
4875 self.graph_failed
4876 .store(true, std::sync::atomic::Ordering::Relaxed);
4877 self.cancel
4878 .store(true, std::sync::atomic::Ordering::Relaxed);
4879 tracing::error!("MTP graph mirror rewind failed after verify decline");
4880 }
4881 return None;
4882 }
4883 crate::gpu::BatchGraphOutcome::Failed => {
4884 self.clear_sequence_state();
4888 self.graph_failed
4889 .store(true, std::sync::atomic::Ordering::Relaxed);
4890 self.cancel
4891 .store(true, std::sync::atomic::Ordering::Relaxed);
4892 tracing::error!("MTP verify batch graph failed after admission");
4893 return None;
4894 }
4895 }
4896 #[cfg(target_os = "macos")]
4902 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
4903 let snap: Vec<Vec<f32>> = self
4904 .kv_cache
4905 .layers
4906 .iter()
4907 .map(|l| l.linear_state.clone())
4908 .collect();
4909 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4910 let toks: Vec<u32> = std::iter::once(t_next)
4911 .chain(drafts.iter().copied())
4912 .collect();
4913 let want_save = self.graph_want_logits;
4914 self.graph_want_logits = false;
4915 for (i, &t) in toks.iter().enumerate() {
4916 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4917 let _ = self.graph_logits.take();
4918 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
4922 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
4923 }
4924 let ref_lg = self.logits_from_hidden(&hi);
4925 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
4926 let ra = sampler::argmax(&ref_lg);
4927 let va = sampler::argmax(row);
4928 let mut md = 0f32;
4929 let mut rms = 0f64;
4930 for j in 0..lm_rows.min(ref_lg.len()) {
4931 let d = (ref_lg[j] - row[j]).abs();
4932 md = md.max(d);
4933 rms += (d as f64) * (d as f64);
4934 }
4935 let mut hd = 0f32;
4936 for j in 0..self.hidden_size {
4937 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
4938 }
4939 eprintln!(
4940 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
4941 next_pos + i,
4942 if ra == va { "OK" } else { "MISMATCH" },
4943 (rms / lm_rows as f64).sqrt()
4944 );
4945 }
4946 self.graph_want_logits = want_save;
4947 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
4950 if l.linear_state.len() == st.len() {
4951 l.linear_state.copy_from_slice(&st);
4952 } else {
4953 l.linear_state = st;
4954 }
4955 }
4956 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
4957 let extra = l.seq_len.saturating_sub(n0);
4958 if extra > 0 {
4959 l.truncate_last(extra);
4960 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
4961 }
4962 }
4963 }
4964 let t_verify = t_round.elapsed();
4965 let sub_verify = subs();
4966 let mut a = 0usize;
4971 let mut forced: Option<u32> = None;
4972 let ids: Vec<u32> = if sparse {
4973 let mut p = std::mem::take(&mut self.spec_ps);
4974 let mut res = std::mem::take(&mut self.spec_ress);
4975 while a < k_spec {
4976 let ok = sampler::sparse_distribution_into(
4977 &logits[a * lm_rows..(a + 1) * lm_rows],
4978 &cfg,
4979 all_ids,
4980 &mut self.sampler_scratch,
4981 self.pool.as_deref(),
4982 &mut p,
4983 );
4984 if !ok {
4985 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
4986 p.clear();
4987 p.push((t, 1.0));
4988 }
4989 match sampler::spec_accept_or_correct_sparse(
4990 &p,
4991 &self.spec_qs[a],
4992 drafts[a],
4993 &mut self.rng,
4994 &mut res,
4995 ) {
4996 None => {
4997 all_ids.push(drafts[a]);
4998 a += 1;
4999 }
5000 Some(c) => {
5001 forced = Some(c);
5002 break;
5003 }
5004 }
5005 }
5006 all_ids.truncate(base_len);
5007 self.spec_ps = p;
5008 self.spec_ress = res;
5009 drafts.clone()
5010 } else if sampling {
5011 let mut p = std::mem::take(&mut self.spec_p);
5012 let mut res = std::mem::take(&mut self.spec_res);
5013 while a < k_spec {
5014 sampler::distribution_into(
5015 &logits[a * lm_rows..(a + 1) * lm_rows],
5016 &cfg,
5017 all_ids,
5018 &mut self.sampler_scratch,
5019 self.pool.as_deref(),
5020 &mut p,
5021 );
5022 match sampler::spec_accept_or_correct(
5023 &p,
5024 &self.spec_q[a],
5025 drafts[a],
5026 &mut self.rng,
5027 &mut res,
5028 self.pool.as_deref(),
5029 ) {
5030 None => {
5031 all_ids.push(drafts[a]);
5032 a += 1;
5033 }
5034 Some(c) => {
5035 forced = Some(c);
5036 break;
5037 }
5038 }
5039 }
5040 all_ids.truncate(base_len);
5041 self.spec_p = p;
5042 self.spec_res = res;
5043 drafts.clone()
5045 } else if greedy_pen {
5046 let mut ids: Vec<u32> = Vec::with_capacity(b);
5050 for i in 0..b {
5051 let t = sampler::argmax_penalized(
5052 &logits[i * lm_rows..(i + 1) * lm_rows],
5053 &cfg,
5054 all_ids,
5055 &mut self.sampler_scratch,
5056 self.pool.as_deref(),
5057 );
5058 ids.push(t);
5059 if i < k_spec && t == drafts[i] {
5060 all_ids.push(t);
5061 } else {
5062 break;
5063 }
5064 }
5065 all_ids.truncate(base_len);
5066 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
5067 a += 1;
5068 }
5069 ids
5072 } else {
5073 let ids: Vec<u32> = (0..b)
5074 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
5075 .collect();
5076 while a < k_spec && ids[a] == drafts[a] {
5077 a += 1;
5078 }
5079 ids
5080 };
5081 if spec_dbg {
5082 eprintln!(
5083 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
5084 drafts, ids
5085 );
5086 }
5087 #[cfg(target_os = "macos")]
5091 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
5092 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
5093 {
5094 let snap: Vec<Vec<f32>> = self
5095 .kv_cache
5096 .layers
5097 .iter()
5098 .map(|l| l.linear_state.clone())
5099 .collect();
5100 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
5101 let toks: Vec<u32> = std::iter::once(t_next)
5102 .chain(drafts.iter().copied())
5103 .collect();
5104 let want_save = self.graph_want_logits;
5105 self.graph_want_logits = false;
5106 for (i, &t) in toks.iter().take(a + 1).enumerate() {
5107 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
5108 let _ = self.graph_logits.take();
5109 }
5110 self.graph_want_logits = want_save;
5111 let plain_states: Vec<Vec<f32>> = self
5112 .kv_cache
5113 .layers
5114 .iter()
5115 .map(|l| l.linear_state.clone())
5116 .collect();
5117 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5118 let mut rows = Vec::new();
5119 for (li, (l, n0)) in self
5120 .kv_cache
5121 .layers
5122 .iter_mut()
5123 .zip(attn_lens.iter())
5124 .enumerate()
5125 {
5126 let extra = l.seq_len.saturating_sub(*n0);
5127 if extra > 0 {
5128 let mut kk = Vec::new();
5129 let mut vv = Vec::new();
5130 for g in 0..nkv {
5131 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5132 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5133 }
5134 rows.push((li, kk, vv));
5135 l.truncate_last(extra);
5136 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
5137 }
5138 }
5139 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
5140 if l.linear_state.len() == st.len() {
5141 l.linear_state.copy_from_slice(&st);
5142 } else {
5143 l.linear_state = st;
5144 }
5145 }
5146 Some((plain_states, rows))
5147 } else {
5148 None
5149 };
5150 #[cfg(target_os = "macos")]
5152 if metal_native {
5153 self.metal_verify_commit(a);
5156 if let Some((plain_states, rows)) = commit_ref {
5157 crate::gpu_metal::queue_fence();
5158 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
5159 let mut worst_s = 0f32;
5160 let mut worst_li = 0usize;
5161 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
5162 if l.linear_state.len() != ps.len() || ps.is_empty() {
5163 continue;
5164 }
5165 let d = l
5166 .linear_state
5167 .iter()
5168 .zip(ps)
5169 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5170 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
5171 let rel = d / n.max(1e-6);
5172 if rel > worst_s {
5173 worst_s = rel;
5174 worst_li = li;
5175 }
5176 }
5177 let mut worst_k = 0f32;
5178 for (li, kk, vv) in &rows {
5179 let l = &self.kv_cache.layers[*li];
5180 let n0 = l.seq_len - (kk.len() / (nkv * hd));
5181 let mut ck = Vec::new();
5182 let mut cv = Vec::new();
5183 for g in 0..nkv {
5184 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
5185 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
5186 }
5187 if ck.len() == kk.len() {
5188 let dk = ck
5189 .iter()
5190 .zip(kk)
5191 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5192 let dv = cv
5193 .iter()
5194 .zip(vv)
5195 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
5196 worst_k = worst_k.max(dk).max(dv);
5197 } else {
5198 eprintln!(
5199 "commit-check L{li}: kv row count mismatch {} vs {}",
5200 ck.len(),
5201 kk.len()
5202 );
5203 }
5204 }
5205 eprintln!(
5206 "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}"
5207 );
5208 }
5209 }
5210 if !metal_native && a + 1 < b {
5211 let expected_gdn_layers = self.graph_gdn_layer_count();
5212 if expected_gdn_layers > 0
5213 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
5214 {
5215 self.clear_sequence_state();
5216 self.graph_failed
5217 .store(true, std::sync::atomic::Ordering::Relaxed);
5218 self.cancel
5219 .store(true, std::sync::atomic::Ordering::Relaxed);
5220 tracing::error!("GDN speculative restore failed after verify");
5221 return None;
5222 }
5223 }
5224 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
5225 self.clear_sequence_state();
5230 self.graph_failed
5231 .store(true, std::sync::atomic::Ordering::Relaxed);
5232 self.cancel
5233 .store(true, std::sync::atomic::Ordering::Relaxed);
5234 tracing::error!("trunk graph KV rewind failed after speculative verify");
5235 return None;
5236 }
5237 *accepted += a;
5238 m.kv.truncate_last(k_spec.saturating_sub(1));
5249 #[cfg(target_os = "macos")]
5250 if metal_native && self.mtp_graph_mode == Some(true) {
5251 crate::gpu_metal::kv_mirror_set_stored(
5254 self.mtp_kv_id(),
5255 Self::MTP_LAYER_BASE,
5256 m.kv.seq_len,
5257 );
5258 }
5259 if !metal_native
5260 && self.mtp_graph_mode == Some(true)
5261 && !self.rewind_mtp_graph_mirror(next_pos)
5262 {
5263 self.clear_sequence_state();
5267 self.graph_failed
5268 .store(true, std::sync::atomic::Ordering::Relaxed);
5269 self.cancel
5270 .store(true, std::sync::atomic::Ordering::Relaxed);
5271 tracing::error!("MTP graph mirror rewind failed after verify commit");
5272 return None;
5273 }
5274 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
5275 if !warm_off && a > 0 {
5276 let mut warmed = false;
5279 #[cfg(target_os = "macos")]
5280 if metal_native && self.mtp_graph_mode == Some(true) {
5281 let pairs: Vec<(&[f32], u32)> = (0..a)
5285 .map(|j| {
5286 (
5287 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
5288 ids[j],
5289 )
5290 })
5291 .collect();
5292 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
5293 if !warmed {
5294 warmed = true;
5295 for j in 0..a {
5296 let row =
5297 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
5298 if self
5299 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
5300 .is_none()
5301 {
5302 warmed = false;
5303 break;
5304 }
5305 }
5306 }
5307 }
5308 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5309 let rows: Vec<Vec<f32>> = (0..a)
5310 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5311 .collect();
5312 let pairs: Vec<(&[f32], u32)> = rows
5313 .iter()
5314 .zip(ids.iter())
5315 .map(|(r, &t)| (r.as_slice(), t))
5316 .collect();
5317 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5318 Ok(()) => warmed = true,
5319 Err(err) => {
5320 tracing::error!("{err}");
5326 self.clear_sequence_state();
5327 self.graph_failed
5328 .store(true, std::sync::atomic::Ordering::Relaxed);
5329 self.cancel
5330 .store(true, std::sync::atomic::Ordering::Relaxed);
5331 return None;
5332 }
5333 }
5334 }
5335 if !warmed {
5336 for j in 0..a {
5337 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5338 let row = row.to_vec();
5339 self.mtp_warm(m, &row, ids[j], next_pos + j);
5340 }
5341 }
5342 }
5343 if let Some(c) = forced {
5347 self.spec_forced = Some(c);
5348 self.graph_logits = None;
5349 } else {
5350 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5351 row.resize(self.vocab_size, 0.0);
5352 if let Some(c) = self.final_softcap {
5353 for l in row.iter_mut() {
5354 *l = c * (*l / c).tanh();
5355 }
5356 }
5357 self.graph_logits = Some(row);
5358 }
5359 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5360 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5366 let end = subs();
5367 eprintln!(
5368 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5369 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5370 t_draft.as_secs_f64() * 1e3,
5371 sub_draft - sub0,
5372 (t_verify - t_draft).as_secs_f64() * 1e3,
5373 sub_verify - sub_draft,
5374 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5375 end - sub_verify,
5376 );
5377 }
5378 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5379 }
5380
5381 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5390 if !self.pair_supported() {
5391 return (0.0, 0.0);
5392 }
5393 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5400 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5401 let emb1 = self.embed_single(1);
5402 let emb2 = self.embed_single(2);
5403 let pos = self.kv_cache.seq_len();
5404
5405 let t0 = std::time::Instant::now();
5406 for _ in 0..iters {
5407 let _ = self.forward_layers(&emb1, pos, None);
5408 let _ = self.forward_layers(&emb2, pos + 1, None);
5409 for l in &mut self.kv_cache.layers {
5410 l.truncate_last(2);
5411 }
5412 }
5413 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5414
5415 let t1 = std::time::Instant::now();
5416 for _ in 0..iters {
5417 let _ = self.forward_pair(&emb1, &emb2, pos);
5418 for l in &mut self.kv_cache.layers {
5419 l.truncate_last(2);
5420 }
5421 }
5422 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5423 match graph_env {
5424 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5425 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5426 }
5427 (singles_ms, pair_ms)
5428 }
5429
5430 fn pair_supported(&self) -> bool {
5438 !self.weights.layers.is_empty()
5445 && self.g3n.is_none()
5446 && !self
5447 .weights
5448 .layers
5449 .iter()
5450 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5451 }
5452
5453 fn forward_pair(
5454 &mut self,
5455 emb1: &[f32],
5456 emb2: &[f32],
5457 position: usize,
5458 ) -> (Vec<f32>, Vec<f32>) {
5459 let mut h1 = emb1.to_vec();
5460 let mut h2 = emb2.to_vec();
5461 let (_nkv, _hd, hs, _rd, eps) = (
5462 self.num_kv_heads,
5463 self.head_dim,
5464 self.hidden_size,
5465 self.rotary_dim,
5466 self.rms_eps,
5467 );
5468 let pool = self.pool.clone();
5469
5470 for li in 0..self.num_layers {
5471 let lw = &self.weights.layers[self.phys_layer(li)];
5472 inference::rms_norm_into(
5475 &h1,
5476 &lw.input_norm,
5477 self.rms_eps,
5478 self.norm_style,
5479 &mut self.ws.n1,
5480 );
5481 inference::rms_norm_into(
5482 &h2,
5483 &lw.input_norm,
5484 self.rms_eps,
5485 self.norm_style,
5486 &mut self.ws.n2,
5487 );
5488
5489 let (a1, a2) = match &lw.attn {
5490 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5491 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5492 AttnKind::Linear(w) => {
5493 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5494 let layer = &mut self.kv_cache.layers[li];
5495 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5496 vmf_phase_pair(
5497 &self.ws.n1,
5498 &self.ws.n2,
5499 w,
5500 &cfg,
5501 state,
5502 scratch,
5503 self.pool.as_deref(),
5504 )
5505 }
5506 AttnKind::LinearGdn(w) => {
5507 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5508 let layer = &mut self.kv_cache.layers[li];
5509 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5510 gdn_pair(
5511 &self.ws.n1,
5512 &self.ws.n2,
5513 w,
5514 &cfg,
5515 state,
5516 scratch,
5517 self.pool.as_deref(),
5518 )
5519 }
5520 AttnKind::ShortConv(w) => {
5521 let cfg = self
5522 .short_conv_cfg
5523 .expect("short-conv layer without short_conv_cfg");
5524 let layer = &mut self.kv_cache.layers[li];
5525 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5526 short_conv_pair(
5527 &self.ws.n1,
5528 &self.ws.n2,
5529 w,
5530 &cfg,
5531 state,
5532 scratch,
5533 self.pool.as_deref(),
5534 )
5535 }
5536 AttnKind::Full {
5537 wq,
5538 wk,
5539 wv,
5540 wo,
5541 q_norm,
5542 k_norm,
5543 output_gate,
5544 softplus_gate,
5545 bias,
5546 } => {
5547 let inv_freq_l = self.layer_inv_freq(li);
5548 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5549 let cfg = QwenAttnCfg {
5550 num_heads: self.layer_num_heads(li),
5551 num_kv_heads: nkv_l,
5552 head_dim: hd_l,
5553 hidden_size: hs,
5554 position,
5555 inv_freq: &inv_freq_l,
5556 rotary_dim: rd_l,
5557 scale: self.attn_scale,
5558 softcap: self.attn_softcap,
5559 window: self.layer_window(li),
5560 v_norm: self.attn_v_norm,
5561 q_norm: q_norm.as_deref(),
5562 k_norm: k_norm.as_deref(),
5563 output_gate: *output_gate,
5564 softplus_gate: softplus_gate
5565 .as_ref()
5566 .map(|(gate, per_head)| (gate, *per_head)),
5567 rope_scale: self.layer_rope_scale(li),
5568 bias: bias
5569 .as_ref()
5570 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5571 rms_eps: eps,
5572 norm_style: self.norm_style,
5573 pool: pool.as_deref(),
5574 };
5575 attention::qwen_attention_pair(
5576 &self.ws.n1,
5577 &self.ws.n2,
5578 wq,
5579 wk,
5580 wv,
5581 wo,
5582 &mut self.kv_cache.layers[li],
5583 &cfg,
5584 )
5585 }
5586 };
5587 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5588 Some(w) => (
5589 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5590 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5591 ),
5592 None => (a1, a2),
5593 };
5594 for i in 0..self.hidden_size {
5595 h1[i] += a1[i];
5596 h2[i] += a2[i];
5597 }
5598 let (mut a1, mut a2) = (a1, a2);
5599 attention::recycle_buf(&mut a1);
5600 attention::recycle_buf(&mut a2);
5601
5602 let lw = &self.weights.layers[self.phys_layer(li)];
5603 inference::rms_norm_into(
5604 &h1,
5605 &lw.post_norm,
5606 self.rms_eps,
5607 self.norm_style,
5608 &mut self.ws.p1,
5609 );
5610 inference::rms_norm_into(
5611 &h2,
5612 &lw.post_norm,
5613 self.rms_eps,
5614 self.norm_style,
5615 &mut self.ws.p2,
5616 );
5617 let (f1, f2) = match &lw.ffn {
5618 FfnKind::DenseMoe(dm) => (
5621 dense_moe_ffn(
5622 dm,
5623 &self.ws.p1,
5624 &h1,
5625 self.rms_eps,
5626 self.norm_style,
5627 self.pool.as_deref(),
5628 ),
5629 dense_moe_ffn(
5630 dm,
5631 &self.ws.p2,
5632 &h2,
5633 self.rms_eps,
5634 self.norm_style,
5635 self.pool.as_deref(),
5636 ),
5637 ),
5638 _ => ffn_forward_pair(
5639 &lw.ffn,
5640 &self.ws.p1,
5641 &self.ws.p2,
5642 self.pool.as_deref(),
5643 None,
5644 ),
5645 };
5646 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5647 Some(w) => (
5648 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5649 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5650 ),
5651 None => (f1, f2),
5652 };
5653 for i in 0..self.hidden_size {
5654 h1[i] += f1[i];
5655 h2[i] += f2[i];
5656 }
5657 let (mut f1, mut f2) = (f1, f2);
5658 attention::recycle_buf(&mut f1);
5659 attention::recycle_buf(&mut f2);
5660 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5661 for i in 0..self.hidden_size {
5662 h1[i] *= sc;
5663 h2[i] *= sc;
5664 }
5665 }
5666 if self.is_loop_end(li) && li + 1 < self.num_layers {
5668 h1 = inference::rms_norm(
5669 &h1,
5670 &self.weights.final_norm,
5671 self.rms_eps,
5672 self.norm_style,
5673 );
5674 h2 = inference::rms_norm(
5675 &h2,
5676 &self.weights.final_norm,
5677 self.rms_eps,
5678 self.norm_style,
5679 );
5680 }
5681 }
5682 if self.o1_active() {
5688 self.commit_linear_scratch();
5689 }
5690 self.o1_progress();
5691 (h1, h2)
5692 }
5693
5694 fn commit_linear_scratch(&mut self) {
5696 for layer in &mut self.kv_cache.layers {
5697 if !layer.linear_scratch.is_empty() {
5698 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5699 layer.linear_scratch.clear();
5700 }
5701 }
5702 }
5703
5704 pub fn forward_ids(
5707 &mut self,
5708 ids: &[u32],
5709 task_mask: Option<&TaskMask>,
5710 ) -> Result<Vec<f32>, String> {
5711 if ids.is_empty() {
5712 return Err("empty id sequence".to_string());
5713 }
5714 self.clear_sequence_state();
5715 self.check_forward_graph("forward_ids setup", 0)?;
5716 if task_mask.is_none() {
5717 self.o1_begin();
5718 }
5719 let mut hidden = vec![0.0f32; self.hidden_size];
5720 let mut pos = 0usize;
5721 if let Some(b) = &mut self.dsv41 {
5722 let pool = self.pool.clone();
5723 let mut logits = Vec::new();
5724 crate::dsv41::forward_chunk(
5725 &b.0,
5726 &b.1,
5727 &b.2,
5728 &mut b.3,
5729 ids,
5730 0,
5731 pool.as_deref(),
5732 &mut logits,
5733 );
5734 if let Err(err) = self.o1_seal_checked() {
5735 self.clear_sequence_state();
5736 return Err(err);
5737 }
5738 return Ok(logits);
5739 }
5740 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5748 let chunk = prefill_chunk();
5752 let hs = self.hidden_size;
5753 while pos < ids.len() {
5754 let end = (pos + chunk).min(ids.len());
5755 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5756 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5757 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5758 pos = end;
5759 }
5760 }
5761 if task_mask.is_none()
5770 && !self.graph_prefill_preferred()
5771 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5772 && self.pair_supported()
5773 {
5774 while pos + 1 < ids.len() {
5775 let e1 = self.embed_single(ids[pos]);
5776 let e2 = self.embed_single(ids[pos + 1]);
5777 let (_, h2) = self.forward_pair(&e1, &e2, pos);
5778 self.check_forward_graph("forward_ids pair", pos + 1)?;
5779 self.commit_linear_scratch();
5780 hidden = h2;
5781 pos += 2;
5782 }
5783 }
5784 while pos < ids.len() {
5785 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5786 self.check_forward_graph("forward_ids", pos)?;
5787 pos += 1;
5788 }
5789 if let Err(err) = self.o1_seal_checked() {
5793 self.clear_sequence_state();
5794 return Err(err);
5795 }
5796 let normed = inference::rms_norm(
5797 &hidden,
5798 &self.weights.final_norm,
5799 self.rms_eps,
5800 self.norm_style,
5801 );
5802 Ok(self.lm_head_forward(&normed))
5803 }
5804
5805 #[doc(hidden)]
5809 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
5810 #[cfg(target_os = "macos")]
5811 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5812 if ids.is_empty() {
5813 return Err("empty id sequence".to_string());
5814 }
5815 self.clear_sequence_state();
5816 self.dsv41
5817 .as_ref()
5818 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
5819 self.o1_begin();
5820 let rows = {
5821 let pool = self.pool.clone();
5822 let b = self
5823 .dsv41
5824 .as_mut()
5825 .expect("dsv41 checked above; state cannot change during forward");
5826 let mut rows = Vec::with_capacity(ids.len());
5827 for (position, &id) in ids.iter().enumerate() {
5828 let mut logits = Vec::new();
5829 crate::dsv41::forward_token(
5830 &b.0,
5831 &b.1,
5832 &b.2,
5833 &mut b.3,
5834 id,
5835 position,
5836 pool.as_deref(),
5837 &mut logits,
5838 );
5839 rows.push(logits);
5840 }
5841 rows
5842 };
5843 self.o1_seal();
5844 Ok(rows)
5845 }
5846
5847 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
5854 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
5855 Ok((nll / cnt.max(1) as f64).exp())
5856 }
5857
5858 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
5863 self.clear_sequence_state();
5864 FFN_PROBE.with(|p| {
5865 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
5866 });
5867 crate::gpu::cpu_scope(|| {
5868 for (pos, &id) in ids.iter().enumerate() {
5869 let emb = self.embed_single(id);
5870 let _ = self.forward_layers(&emb, pos, None);
5871 }
5872 });
5873 self.clear_sequence_state();
5874 FFN_PROBE
5875 .with(|p| p.borrow_mut().take())
5876 .unwrap_or_default()
5877 }
5878
5879 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
5883 if let Err(err) = self.nll_begin() {
5884 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
5888 self.nll_end();
5889 return Err(err);
5890 }
5891 FFN_PROBE.with(|p| {
5892 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
5893 });
5894 let result: Result<(), String> = (|| {
5895 for chunk in ids.chunks(256) {
5896 if chunk.len() < 2 {
5897 continue;
5898 }
5899 self.nll_ids_masked(chunk, 0, None)?;
5900 }
5901 Ok(())
5902 })();
5903 self.nll_end();
5904 let probe = FFN_PROBE
5905 .with(|p| p.borrow_mut().take())
5906 .unwrap_or_default();
5907 match result {
5908 Ok(()) => Ok(probe),
5909 Err(err) => {
5910 drop(probe);
5911 Err(err)
5912 }
5913 }
5914 }
5915
5916 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
5920 self.nll_begin()?;
5921 let result: Result<f64, String> = (|| {
5922 let mut nll = 0f64;
5923 let mut cnt = 0usize;
5924 let mut hidden = vec![0f32; self.hidden_size];
5925 for (pos, &id) in ids.iter().enumerate() {
5926 if pos > 0 {
5927 inference::rms_norm_into(
5928 &hidden,
5929 &self.weights.final_norm,
5930 self.rms_eps,
5931 self.norm_style,
5932 &mut self.ws.n1,
5933 );
5934 let mut logits = self.lm_head_forward(&self.ws.n1);
5935 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
5936 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
5937 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
5938 nll -= p.max(1e-300).ln();
5939 cnt += 1;
5940 attention::recycle_buf(&mut logits);
5941 }
5942 let emb = self.embed_single(id);
5943 hidden = self.forward_layers(&emb, pos, Some(mask));
5944 self.nll_check_graph("masked serial forward", pos)?;
5945 let _ = self.graph_logits.take();
5949 }
5950 Ok((nll / cnt.max(1) as f64).exp())
5951 })();
5952 self.nll_end();
5953 result
5954 }
5955
5956 pub fn nll_ids_masked(
5975 &mut self,
5976 ids: &[u32],
5977 start: usize,
5978 task_mask: Option<&TaskMask>,
5979 ) -> Result<(f64, usize), String> {
5980 let task_mask = self.drop_open_mask(task_mask);
5981 self.nll_ids_inner(ids, start, task_mask)
5982 }
5983
5984 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
5985 self.nll_ids_inner(ids, start, None)
5986 }
5987
5988 fn nll_ids_inner(
5989 &mut self,
5990 ids: &[u32],
5991 start: usize,
5992 task_mask: Option<&TaskMask>,
5993 ) -> Result<(f64, usize), String> {
5994 self.nll_begin()?;
5995 let result: Result<(f64, usize), String> = (|| {
5996 let mut nll = 0f64;
5997 let mut cnt = 0usize;
5998 if self.can_prefill_batched() {
5999 const CHUNK: usize = 128;
6005 const LM_SUB: usize = 32;
6006 let n = ids.len().saturating_sub(1);
6007 let hs = self.hidden_size;
6008 let rows = self.weights.lm_head.rows();
6009 let mut pos = 0usize;
6010 while pos < n {
6011 let end = (pos + CHUNK).min(n);
6012 let bsz = end - pos;
6013 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
6014 self.nll_check_graph("batched prefill", pos)?;
6015 let mut k0 = 0usize;
6016 while k0 < bsz {
6017 let k1 = (k0 + LM_SUB).min(bsz);
6018 let sb = k1 - k0;
6019 if pos + k1 <= start {
6022 k0 = k1;
6023 continue;
6024 }
6025 let mut normed = vec![0.0f32; sb * hs];
6026 for k in 0..sb {
6027 let r = inference::rms_norm(
6028 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
6029 &self.weights.final_norm,
6030 self.rms_eps,
6031 self.norm_style,
6032 );
6033 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
6034 }
6035 let mut logits = vec![0.0f32; sb * rows];
6036 self.weights
6037 .lm_head
6038 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
6039 for k in 0..sb {
6040 if pos + k0 + k < start {
6041 continue;
6042 }
6043 self.nll_check_graph("batched score row", pos + k0 + k)?;
6044 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
6045 if let Some(mu) = self.logit_multiplier {
6046 for v in lg.iter_mut() {
6047 *v *= mu;
6048 }
6049 }
6050 if let Some(c) = self.final_softcap {
6054 for v in lg.iter_mut() {
6055 *v = c * (*v / c).tanh();
6056 }
6057 }
6058 if let Some(cm) = self.head_clusters.clone() {
6061 self.hierarchical_head_logprobs(
6062 &normed[k * hs..(k + 1) * hs],
6063 &cm,
6064 lg,
6065 );
6066 }
6067 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
6068 let target = ids[pos + k0 + k + 1] as usize;
6069 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6070 let lse: f64 = lg
6071 .iter()
6072 .map(|&v| ((v - max) as f64).exp())
6073 .sum::<f64>()
6074 .ln()
6075 + max as f64;
6076 nll += lse - lg[target] as f64;
6077 cnt += 1;
6078 if std::env::var("CMF_PPL_TRACE").is_ok() {
6079 let top = lg
6080 .iter()
6081 .enumerate()
6082 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6083 .map(|(i, _)| i)
6084 .unwrap_or(0);
6085 eprintln!(
6086 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
6087 pos + k0 + k,
6088 target,
6089 lse - lg[target] as f64,
6090 top,
6091 lg[target],
6092 lg[top]
6093 );
6094 }
6095 }
6096 k0 = k1;
6097 }
6098 pos = end;
6099 }
6100 return Ok((nll, cnt));
6101 }
6102 for pos in 0..ids.len().saturating_sub(1) {
6103 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
6104 self.nll_check_graph("serial forward", pos)?;
6105 let out_of_band = self.graph_logits.take();
6113 if pos < start {
6114 continue;
6115 }
6116 let logits = match out_of_band {
6117 Some(lg) => lg,
6118 None => {
6119 let normed = inference::rms_norm(
6120 &hidden,
6121 &self.weights.final_norm,
6122 self.rms_eps,
6123 self.norm_style,
6124 );
6125 self.lm_head_forward(&normed)
6129 }
6130 };
6131 let target = ids[pos + 1] as usize;
6132 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6133 let lse: f64 = logits
6134 .iter()
6135 .map(|&v| ((v - max) as f64).exp())
6136 .sum::<f64>()
6137 .ln()
6138 + max as f64;
6139 let tok_nll = lse - logits[target] as f64;
6140 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6141 let top = logits
6142 .iter()
6143 .enumerate()
6144 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6145 .map(|(i, _)| i)
6146 .unwrap_or(0);
6147 eprintln!(
6148 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6149 logits[target], logits[top]
6150 );
6151 }
6152 nll += tok_nll;
6153 cnt += 1;
6154 }
6155 Ok((nll, cnt))
6156 })();
6157 self.nll_end();
6158 result
6159 }
6160
6161 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
6166 let normed = inference::rms_norm(
6167 hidden,
6168 &self.weights.final_norm,
6169 self.rms_eps,
6170 self.norm_style,
6171 );
6172 let mut logits = self.lm_head_forward(&normed);
6175 let target = target as usize;
6176 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6177 let lse: f64 = logits
6178 .iter()
6179 .map(|&v| ((v - max) as f64).exp())
6180 .sum::<f64>()
6181 .ln()
6182 + max as f64;
6183 let tok_nll = lse - logits[target] as f64;
6184 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6185 let top = logits
6186 .iter()
6187 .enumerate()
6188 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6189 .map(|(i, _)| i)
6190 .unwrap_or(0);
6191 eprintln!(
6192 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6193 logits[target], logits[top]
6194 );
6195 }
6196 attention::recycle_buf(&mut logits);
6197 tok_nll
6198 }
6199
6200 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
6218 self.nll_begin()?;
6223 let requested_prefix = (prefill > 0).then_some(prefill);
6224 self.o1_begin_with_prefix(requested_prefix);
6225 let n = ids.len().saturating_sub(1);
6226 let requested_start = prefill.min(n);
6227 let exact_end = if self.o1_active() {
6232 match requested_prefix {
6233 Some(requested) => self.o1_effective_boundary(requested),
6234 None => self
6235 .o1_cfg
6236 .as_ref()
6237 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
6238 }
6239 .unwrap_or(requested_start)
6240 .min(n)
6241 } else {
6242 requested_start
6243 };
6244 let mut nll = 0f64;
6245 let mut cnt = 0usize;
6246
6247 let mut pos = 0usize;
6251 if self.can_prefill_batched() {
6252 const CHUNK: usize = 128;
6253 while pos < exact_end {
6254 let end = (pos + CHUNK).min(exact_end);
6255 let hiddens = self.prefill_batch(&ids[pos..end], pos);
6256 if self
6257 .graph_failed
6258 .swap(false, std::sync::atomic::Ordering::Relaxed)
6259 {
6260 self.cancel
6261 .store(false, std::sync::atomic::Ordering::Relaxed);
6262 self.nll_end();
6263 return Err("GPU graph failed during O(1) NLL prefix".into());
6264 }
6265 for row in 0..end - pos {
6266 let score_pos = pos + row;
6267 if score_pos >= requested_start && score_pos < n {
6268 nll += self.nll_from_hidden(
6269 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
6270 ids[score_pos + 1],
6271 score_pos,
6272 );
6273 cnt += 1;
6274 }
6275 }
6276 pos = end;
6277 }
6278 } else {
6279 while pos < exact_end {
6280 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6281 if self
6282 .graph_failed
6283 .swap(false, std::sync::atomic::Ordering::Relaxed)
6284 {
6285 self.cancel
6286 .store(false, std::sync::atomic::Ordering::Relaxed);
6287 self.nll_end();
6288 return Err("GPU graph failed during O(1) NLL prefix".into());
6289 }
6290 if pos >= requested_start && pos < n {
6291 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6292 cnt += 1;
6293 }
6294 pos += 1;
6295 }
6296 }
6297 self.o1_seal_checked().map_err(|err| {
6298 self.nll_end();
6299 err
6300 })?;
6301
6302 let batch_k = std::env::var("CMF_BATCH_K")
6311 .ok()
6312 .and_then(|v| v.parse::<usize>().ok())
6313 .unwrap_or(0);
6314 let batch_admitted = batch_k > 0
6315 && self.can_prefill_batched()
6316 && self.o1_active()
6317 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6318 && (0..self.num_layers).all(|li| {
6319 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6320 cache.o1.is_none() || cache.o1_views().is_some()
6321 });
6322 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6323 eprintln!(
6324 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
6325 batch_admitted,
6326 batch_k,
6327 n.saturating_sub(exact_end),
6328 );
6329 }
6330 let mut batch_completed = false;
6331 if batch_admitted && exact_end < n {
6332 let hs = self.hidden_size;
6333 let mut batch_pos = exact_end;
6334 while batch_pos < n {
6335 let end = (batch_pos + batch_k).min(n);
6336 let bk = end - batch_pos;
6337 let mut hiddens = vec![0.0f32; bk * hs];
6338 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
6339 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
6340 }
6341 let positions: Vec<usize> = (batch_pos..end).collect();
6342 let t_batch = std::time::Instant::now();
6343 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6344 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6345 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
6346 eprintln!(
6347 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6348 batch_pos,
6349 end.saturating_sub(1),
6350 bk as f64 / (ms / 1000.0),
6351 );
6352 }
6353 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
6354 self.nll_end();
6355 return Err(err);
6356 }
6357 match outcome {
6358 crate::gpu::BatchGraphOutcome::Completed => {
6359 batch_completed = true;
6360 for row in 0..bk {
6361 nll += self.nll_from_hidden(
6362 &hiddens[row * hs..(row + 1) * hs],
6363 ids[batch_pos + row + 1],
6364 batch_pos + row,
6365 );
6366 cnt += 1;
6367 }
6368 batch_pos = end;
6369 }
6370 crate::gpu::BatchGraphOutcome::Declined => {
6371 if batch_completed {
6372 self.nll_end();
6373 return Err(format!(
6374 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
6375 ));
6376 }
6377 break;
6378 }
6379 crate::gpu::BatchGraphOutcome::Failed => {
6380 self.nll_end();
6381 return Err(format!(
6382 "O(1) NLL batch graph failed after admission at position {batch_pos}"
6383 ));
6384 }
6385 }
6386 }
6387 if batch_completed && cnt == n.saturating_sub(requested_start) {
6388 self.nll_end();
6389 return Ok((nll, cnt));
6390 }
6391 }
6392
6393 for pos in exact_end..n {
6398 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6399 if self
6400 .graph_failed
6401 .swap(false, std::sync::atomic::Ordering::Relaxed)
6402 {
6403 self.cancel
6404 .store(false, std::sync::atomic::Ordering::Relaxed);
6405 self.nll_end();
6406 return Err(format!(
6407 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
6408 ));
6409 }
6410 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
6411 cnt += 1;
6412 }
6413 self.nll_end();
6414 Ok((nll, cnt))
6415 }
6416
6417 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6425 self.clear_sequence_state();
6426 let n = ids.len().saturating_sub(1);
6427 let mut correct = Vec::with_capacity(n);
6428 let mut pmax = Vec::with_capacity(n);
6429 for pos in 0..n {
6430 let emb = self.embed_single(ids[pos]);
6431 let hidden = self.forward_layers(&emb, pos, None);
6432 let normed = inference::rms_norm(
6433 &hidden,
6434 &self.weights.final_norm,
6435 self.rms_eps,
6436 self.norm_style,
6437 );
6438 let logits = self.lm_head_forward(&normed);
6442 let target = ids[pos + 1] as usize;
6443 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6444 for (i, &v) in logits.iter().enumerate() {
6445 if v > mval {
6446 mval = v;
6447 amax = i;
6448 }
6449 }
6450 correct.push(amax == target);
6451 let row: Vec<f32> = temps
6452 .iter()
6453 .map(|&t| {
6454 let tt = t.max(1e-3);
6455 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6456 1.0 / s.max(1e-12) })
6458 .collect();
6459 pmax.push(row);
6460 }
6461 self.clear_sequence_state();
6462 (correct, pmax)
6463 }
6464
6465 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6472 if self.dyn_router.is_none() {
6473 return Ok((self.ppl_ids(ids)?, 0));
6474 }
6475 self.nll_begin()?;
6476 let saved_active = self.dyn_active;
6477 let mut router = self
6478 .dyn_router
6479 .take()
6480 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6481 router.reset();
6482 self.dyn_phi_seen = 0;
6483 let _ = self.set_active_skill(None);
6484
6485 let result: Result<(f64, usize), String> = (|| {
6486 let mut nll = 0f64;
6487 let mut cnt = 0usize;
6488 for pos in 0..ids.len().saturating_sub(1) {
6489 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6490 self.nll_check_graph("dynamic serial forward", pos)?;
6491 let out_of_band = self.graph_logits.take();
6492 let mut logits = match out_of_band {
6493 Some(lg) => lg,
6494 None => {
6495 let normed = inference::rms_norm(
6496 &hidden,
6497 &self.weights.final_norm,
6498 self.rms_eps,
6499 self.norm_style,
6500 );
6501 self.lm_head_forward(&normed)
6505 }
6506 };
6507 let target = ids[pos + 1] as usize;
6508 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6509 let lse: f64 = logits
6510 .iter()
6511 .map(|&v| ((v - max) as f64).exp())
6512 .sum::<f64>()
6513 .ln()
6514 + max as f64;
6515 let tok_nll = lse - logits[target] as f64;
6516 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6517 let top = logits
6518 .iter()
6519 .enumerate()
6520 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6521 .map(|(i, _)| i)
6522 .unwrap_or(0);
6523 eprintln!(
6524 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6525 logits[target], logits[top]
6526 );
6527 }
6528 nll += tok_nll;
6529 cnt += 1;
6530 attention::recycle_buf(&mut logits);
6531 let phi = self.dyn_phi_ema.clone();
6533 if let Some(new_active) = router.step(&phi, pos) {
6534 let _ = self.set_active_skill(new_active);
6535 }
6536 }
6537 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6538 })();
6539
6540 let _ = self.set_active_skill(saved_active);
6543 self.dyn_router = Some(router);
6544 self.nll_end();
6545 result
6546 }
6547
6548 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6550 self.clear_sequence_state();
6551 let mut acc = vec![0f32; self.hidden_size];
6552 for (pos, &id) in ids.iter().enumerate() {
6553 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6554 for (a, v) in acc.iter_mut().zip(&h) {
6555 *a += v;
6556 }
6557 }
6558 let n = ids.len().max(1) as f32;
6559 for a in acc.iter_mut() {
6560 *a /= n;
6561 }
6562 self.clear_sequence_state();
6563 acc
6564 }
6565
6566 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6572 self.prefill_batch_masked(ids, start_pos, None)
6573 }
6574
6575 fn prefill_batch_masked(
6581 &mut self,
6582 ids: &[u32],
6583 start_pos: usize,
6584 task_mask: Option<&TaskMask>,
6585 ) -> Vec<f32> {
6586 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6587 }
6588
6589 fn prefill_batch_span(
6595 &mut self,
6596 input: PrefillIn<'_>,
6597 start_pos: usize,
6598 task_mask: Option<&TaskMask>,
6599 from: usize,
6600 upto_excl: usize,
6601 ) -> Vec<f32> {
6602 let hs = self.hidden_size;
6603 let b = match input {
6604 PrefillIn::Ids(ids) => ids.len(),
6605 PrefillIn::Hidden(hb) => hb.len() / hs,
6606 };
6607 let upto_excl = upto_excl.min(self.num_layers);
6608 let mut h: Vec<f32>;
6612 let mut h_ready;
6613 match input {
6614 PrefillIn::Ids(_) => {
6615 h = vec![0.0; b * hs];
6616 h_ready = false;
6617 }
6618 PrefillIn::Hidden(hb) => {
6619 h = hb.to_vec();
6620 h_ready = true;
6621 }
6622 }
6623 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6624 if let PrefillIn::Ids(ids) = input {
6625 for (bi, &id) in ids.iter().enumerate() {
6626 let e = me.embed_single(id);
6627 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6628 }
6629 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6630 if let Ok(t) = tp.parse::<usize>() {
6631 if t >= start_pos && t < start_pos + ids.len() {
6632 let bi = t - start_pos;
6633 let row = &h[bi * hs..(bi + 1) * hs];
6634 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6635 eprintln!(
6636 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6637 ids[bi],
6638 row[0],
6639 row[1],
6640 ids.len(),
6641 &ids[..ids.len().min(8)]
6642 );
6643 }
6644 }
6645 }
6646 }
6647 };
6648 let (_nkv, _hd, _rd, eps) = (
6649 self.num_kv_heads,
6650 self.head_dim,
6651 self.rotary_dim,
6652 self.rms_eps,
6653 );
6654 let pool = self.pool.clone();
6655 let norm_style = self.norm_style;
6656 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6657
6658 #[cfg(target_os = "macos")]
6659 let mut chunk_skip_until = 0usize;
6660 for li in from..upto_excl {
6661 let _capacity_tail = automatic_gpu_prefix
6662 .filter(|&prefix| li >= prefix)
6663 .map(|_| crate::gpu::enter_cpu_scope());
6664 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6671 if task_mask.is_none() {
6672 if li < chunk_skip_until {
6673 continue;
6674 }
6675 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6681 fill_h(&mut h, self);
6682 h_ready = true;
6683 }
6684 let ids_for_embed = match input {
6685 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6686 PrefillIn::Hidden(_) => None,
6687 };
6688 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6689 if end > li {
6690 h_ready = true;
6691 chunk_skip_until = end;
6692 if self.is_loop_end(end - 1) && end < self.num_layers {
6695 for bi in 0..b {
6696 let normed = inference::rms_norm(
6697 &h[bi * hs..(bi + 1) * hs],
6698 &self.weights.final_norm,
6699 eps,
6700 norm_style,
6701 );
6702 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6703 }
6704 }
6705 continue;
6706 }
6707 }
6708 if !h_ready {
6709 fill_h(&mut h, self);
6710 h_ready = true;
6711 }
6712 let lw = &self.weights.layers[self.phys_layer(li)];
6713 match &lw.attn {
6715 AttnKind::Kda(w) => {
6716 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6718 let mut normed = vec![0.0f32; b * hs];
6719 for bi in 0..b {
6720 inference::rms_norm_into(
6721 &h[bi * hs..(bi + 1) * hs],
6722 &lw.input_norm,
6723 eps,
6724 norm_style,
6725 &mut normed[bi * hs..(bi + 1) * hs],
6726 );
6727 }
6728 let attn = crate::linear_core::kda_forward_batch(
6729 &normed,
6730 b,
6731 w,
6732 &cfg,
6733 &mut self.kv_cache.layers[li].linear_state,
6734 pool.as_deref(),
6735 );
6736 for (dst, &a) in h.iter_mut().zip(&attn) {
6737 *dst += a;
6738 }
6739 }
6740 AttnKind::LinearGdn(w) => {
6741 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6743 let mut normed = vec![0.0f32; b * hs];
6744 for bi in 0..b {
6745 let r = inference::rms_norm(
6746 &h[bi * hs..(bi + 1) * hs],
6747 &lw.input_norm,
6748 eps,
6749 norm_style,
6750 );
6751 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6752 }
6753 let attn = crate::linear_core::gdn_forward_batch(
6754 &normed,
6755 b,
6756 w,
6757 &cfg,
6758 &mut self.kv_cache.layers[li].linear_state,
6759 pool.as_deref(),
6760 );
6761 for (dst, &a) in h.iter_mut().zip(&attn) {
6762 *dst += a;
6763 }
6764 }
6765 AttnKind::ShortConv(w) => {
6766 let cfg = self
6769 .short_conv_cfg
6770 .expect("short-conv layer without short_conv_cfg");
6771 let mut normed = vec![0.0f32; b * hs];
6772 for bi in 0..b {
6773 inference::rms_norm_into(
6774 &h[bi * hs..(bi + 1) * hs],
6775 &lw.input_norm,
6776 eps,
6777 norm_style,
6778 &mut normed[bi * hs..(bi + 1) * hs],
6779 );
6780 }
6781 let attn = short_conv_forward_batch(
6782 &normed,
6783 b,
6784 w,
6785 &cfg,
6786 &mut self.kv_cache.layers[li].linear_state,
6787 pool.as_deref(),
6788 );
6789 for (dst, &a) in h.iter_mut().zip(&attn) {
6790 *dst += a;
6791 }
6792 }
6793 AttnKind::Mla(w) => {
6794 let inv_freq_l = self.layer_inv_freq(li);
6797 let rs = self.layer_rope_scale(li);
6798 let mut normed = vec![0.0f32; hs];
6799 for bi in 0..b {
6800 inference::rms_norm_into(
6801 &h[bi * hs..(bi + 1) * hs],
6802 &lw.input_norm,
6803 eps,
6804 norm_style,
6805 &mut normed,
6806 );
6807 let ao = mla_attention(
6808 w,
6809 &normed,
6810 &mut self.kv_cache.layers[li],
6811 start_pos + bi,
6812 &inv_freq_l,
6813 rs,
6814 eps,
6815 pool.as_deref(),
6816 );
6817 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
6818 *dst += a;
6819 }
6820 }
6821 }
6822 AttnKind::Full {
6823 wq,
6824 wk,
6825 wv,
6826 wo,
6827 q_norm,
6828 k_norm,
6829 output_gate,
6830 softplus_gate,
6831 bias,
6832 } => {
6833 let mut normed = vec![0.0f32; b * hs];
6837 for bi in 0..b {
6838 inference::rms_norm_into(
6839 &h[bi * hs..(bi + 1) * hs],
6840 &lw.input_norm,
6841 eps,
6842 norm_style,
6843 &mut normed[bi * hs..(bi + 1) * hs],
6844 );
6845 }
6846 let inv_freq_l = self.layer_inv_freq(li);
6847 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
6848 let cfg = QwenAttnCfg {
6849 num_heads: self.layer_num_heads(li),
6850 num_kv_heads: nkv_l,
6851 head_dim: hd_l,
6852 hidden_size: hs,
6853 position: start_pos,
6854 inv_freq: &inv_freq_l,
6855 rotary_dim: rd_l,
6856 scale: self.attn_scale,
6857 softcap: self.attn_softcap,
6858 window: self.layer_window(li),
6859 v_norm: self.attn_v_norm,
6860 q_norm: q_norm.as_deref(),
6861 k_norm: k_norm.as_deref(),
6862 output_gate: *output_gate,
6863 softplus_gate: softplus_gate
6864 .as_ref()
6865 .map(|(gate, per_head)| (gate, *per_head)),
6866 rope_scale: self.layer_rope_scale(li),
6867 bias: bias
6868 .as_ref()
6869 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6870 rms_eps: eps,
6871 norm_style,
6872 pool: pool.as_deref(),
6873 };
6874 let mut attn = attention::qwen_attention_batch(
6875 &normed,
6876 b,
6877 wq,
6878 wk,
6879 wv,
6880 wo,
6881 &mut self.kv_cache.layers[li],
6882 &cfg,
6883 );
6884 if let Some(w) = &lw.attn_out_norm {
6885 for bi in 0..b {
6886 inference::rms_norm_into(
6887 &attn[bi * hs..(bi + 1) * hs],
6888 w,
6889 eps,
6890 norm_style,
6891 &mut normed[bi * hs..(bi + 1) * hs],
6892 );
6893 }
6894 attn.copy_from_slice(&normed);
6895 }
6896 for (dst, &a) in h.iter_mut().zip(&attn) {
6897 *dst += a;
6898 }
6899 }
6900 AttnKind::Linear(w) => {
6901 for bi in 0..b {
6902 let normed = inference::rms_norm(
6903 &h[bi * hs..(bi + 1) * hs],
6904 &lw.input_norm,
6905 eps,
6906 norm_style,
6907 );
6908 vmf_phase_forward(
6909 &normed,
6910 w,
6911 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
6912 &mut self.kv_cache.layers[li].linear_state,
6913 pool.as_deref(),
6914 )
6915 .iter()
6916 .enumerate()
6917 .for_each(|(i, &a)| h[bi * hs + i] += a);
6918 }
6919 }
6920 }
6921
6922 let lw = &self.weights.layers[self.phys_layer(li)];
6924 let mut post = vec![0.0f32; b * hs];
6925 for bi in 0..b {
6926 let r =
6927 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
6928 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6929 }
6930 let mask_row = task_mask
6933 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
6934 .and_then(|m| m.ffn_masks.get(li))
6935 .map(|v| v.as_slice());
6936 let mut ffn = match &lw.ffn {
6937 FfnKind::Dense(d) if !d.segs.is_empty() => {
6938 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
6939 }
6940 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
6941 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
6942 FfnKind::DenseMoe(dm) => {
6945 let mut out = vec![0.0f32; b * hs];
6946 for bi in 0..b {
6947 let r = dense_moe_ffn(
6948 dm,
6949 &post[bi * hs..(bi + 1) * hs],
6950 &h[bi * hs..(bi + 1) * hs],
6951 eps,
6952 norm_style,
6953 pool.as_deref(),
6954 );
6955 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6956 }
6957 out
6958 }
6959 };
6960 if let Some(w) = &lw.ffn_out_norm {
6961 for bi in 0..b {
6962 inference::rms_norm_into(
6963 &ffn[bi * hs..(bi + 1) * hs],
6964 w,
6965 eps,
6966 norm_style,
6967 &mut post[bi * hs..(bi + 1) * hs],
6968 );
6969 }
6970 ffn.copy_from_slice(&post);
6971 }
6972 for (dst, &f) in h.iter_mut().zip(&ffn) {
6973 *dst += f;
6974 }
6975 if let Some(sc) = lw.layer_scale {
6976 for v in h.iter_mut() {
6977 *v *= sc;
6978 }
6979 }
6980 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6981 if let Ok(t) = tp.parse::<usize>() {
6982 if t >= start_pos && t < start_pos + b {
6983 let bi = t - start_pos;
6984 let row = &h[bi * hs..(bi + 1) * hs];
6985 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6986 eprintln!(
6987 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
6988 row[0], row[1]
6989 );
6990 }
6991 }
6992 }
6993 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
6997 let row = &h[(b - 1) * hs..b * hs];
6998 let rms =
6999 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
7000 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
7001 eprintln!(
7002 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
7003 match &self.weights.layers[self.phys_layer(li)].attn {
7004 AttnKind::LinearGdn(_) => "gdn",
7005 AttnKind::Linear(_) => "vmf",
7006 AttnKind::ShortConv(_) => "conv",
7007 _ => "attn",
7008 },
7009 match &lw.ffn {
7010 FfnKind::Moe(_) => "moe",
7011 FfnKind::Dense(_) => "dense",
7012 FfnKind::DenseMoe(_) => "dense+moe",
7013 },
7014 );
7015 }
7016 if self.is_loop_end(li) && li + 1 < self.num_layers {
7018 for bi in 0..b {
7019 let normed = inference::rms_norm(
7020 &h[bi * hs..(bi + 1) * hs],
7021 &self.weights.final_norm,
7022 eps,
7023 norm_style,
7024 );
7025 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
7026 }
7027 }
7028 if std::env::var("CMF_TRACE_H").is_ok() {
7029 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
7030 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
7031 eprintln!(
7032 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
7033 lw.layer_scale
7034 );
7035 }
7036 }
7037 crate::gpu::set_layer(-1); self.o1_progress();
7043 h
7044 }
7045
7046 fn embed_single(&self, id: u32) -> Vec<f32> {
7048 let mut out = vec![0.0f32; self.hidden_size];
7049 if (id as usize) < self.weights.embed_tokens.rows() {
7050 self.weights.embed_tokens.row_f32(id as usize, &mut out);
7051 }
7052 if self.embed_multiplier != 1.0 {
7053 for v in out.iter_mut() {
7054 *v *= self.embed_multiplier;
7055 }
7056 }
7057 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
7061 let mut v = vec![0.0f32; self.hidden_size.max(1)];
7062 v[0] = id as f32;
7063 return v;
7064 }
7065 if let Some(b) = &self.g3n {
7068 return b.0.extend_embedding(id, &out, self.pool.as_deref());
7069 }
7070 out
7071 }
7072
7073 #[cfg(target_os = "macos")]
7079 fn chunk_run_gpu(
7080 &mut self,
7081 li0: usize,
7082 h: &mut [f32],
7083 b: usize,
7084 pos0: usize,
7085 embed_ids: Option<&[u32]>,
7086 cap: usize,
7087 ) -> usize {
7088 if !crate::gpu::enabled_here()
7092 || std::env::var("CMF_GPU_CHUNK")
7093 .map(|v| v == "0")
7094 .unwrap_or(false)
7095 || b < 32
7096 || self.swa.is_some()
7097 || self.global_attn.is_some()
7098 || self.o1_active()
7101 || self.attn_v_norm
7102 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
7103 {
7104 return li0;
7105 }
7106 let Some(model) = self.model.clone() else {
7107 return li0;
7108 };
7109 let inv_freq = self.inv_freq.clone();
7110 let (nh, nkv, hd, hs) = (
7111 self.num_heads,
7112 self.num_kv_heads,
7113 self.head_dim,
7114 self.hidden_size,
7115 );
7116 let loop_end = if self.loop_final_norm {
7120 ((li0 / self.physical_layers) + 1) * self.physical_layers
7121 } else {
7122 self.num_layers
7123 };
7124 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
7125 let mut stored_at: Vec<usize> = Vec::new();
7126 for li in li0..self.num_layers.min(loop_end).min(cap) {
7127 let lw = &self.weights.layers[self.phys_layer(li)];
7128 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7129 break;
7130 }
7131 let AttnKind::Full {
7132 wq,
7133 wk,
7134 wv,
7135 wo,
7136 q_norm,
7137 k_norm,
7138 output_gate: false,
7139 softplus_gate: None,
7140 bias,
7141 } = &lw.attn
7142 else {
7143 break;
7144 };
7145 let FfnKind::Dense(d) = &lw.ffn else { break };
7146 if d.act != Act::Silu || !d.segs.is_empty() {
7147 break;
7148 }
7149 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
7154 t.q8_row_parts()
7155 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7156 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
7157 }
7158 let parts = (
7159 cw(wq),
7160 cw(wk),
7161 cw(wv),
7162 cw(wo),
7163 cw(&d.gate_proj),
7164 cw(&d.up_proj),
7165 cw(&d.down_proj),
7166 );
7167 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
7168 else {
7169 break;
7170 };
7171 let layer = &self.kv_cache.layers[li];
7172 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
7173 break;
7174 }
7175 stored_at.push(layer.head_len(0));
7176 layers.push(crate::gpu_metal::ChunkLayer {
7177 model: &model,
7178 kv_id: self.graph_kv_id,
7179 layer: li,
7180 wq: pq,
7181 wk: pk,
7182 wv: pv,
7183 wo: po,
7184 gate: pg,
7185 up: pu,
7186 down: pd,
7187 input_norm: &lw.input_norm,
7188 post_norm: &lw.post_norm,
7189 bias: bias
7190 .as_ref()
7191 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
7192 q_norm: q_norm.as_deref(),
7193 k_norm: k_norm.as_deref(),
7194 inv_freq: &inv_freq,
7195 rd: self.rotary_dim,
7196 nh,
7197 nkv,
7198 hd,
7199 hs,
7200 inter: d.gate_proj.rows(),
7201 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
7202 eps: self.rms_eps as f32,
7203 });
7204 }
7205 if layers.is_empty() {
7206 return li0;
7207 }
7208 let row = nkv * hd;
7209 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
7210 .iter()
7211 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
7212 .collect();
7213 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
7214 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
7215 let li = layers[i].layer;
7216 let layer = &self.kv_cache.layers[li];
7217 io.push(crate::gpu_metal::ChunkIo {
7218 cpu_stored: stored_at[i],
7219 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
7220 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
7221 out_k: ok,
7222 out_v: ov,
7223 imp: oi,
7224 });
7225 }
7226 let n_run = layers.len();
7227 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
7228 let ep = embed_ids.and_then(|ids| {
7231 self.weights
7232 .embed_tokens
7233 .q8_row_parts()
7234 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
7235 idx,
7236 rows,
7237 row_scale: rs,
7238 ids,
7239 mult: self.embed_multiplier,
7240 })
7241 });
7242 if embed_ids.is_some() && ep.is_none() {
7243 return li0;
7244 }
7245 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
7246 return li0;
7247 }
7248 drop(io);
7249 drop(layers);
7250 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
7253 let li = li0 + i;
7254 let layer = &mut self.kv_cache.layers[li];
7255 for bi in 0..b {
7256 layer.append(
7257 &ok[bi * row..(bi + 1) * row],
7258 &ov[bi * row..(bi + 1) * row],
7259 &[],
7260 );
7261 }
7262 layer.accumulate_imp(oi);
7263 }
7264 last
7265 }
7266
7267 fn layer_is_local(&self, li: usize) -> bool {
7270 if let Some(layers) = &self.sliding_layers {
7271 return layers.get(li).copied().unwrap_or(false);
7272 }
7273 match self.swa {
7274 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
7275 None => false,
7276 }
7277 }
7278
7279 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
7282 if self.layer_is_local(li) {
7283 if let Some(f) = &self.inv_freq_local {
7284 return f.clone();
7285 }
7286 } else if let Some(f) = &self.inv_freq_global {
7287 return f.clone();
7288 }
7289 self.inv_freq.clone()
7290 }
7291
7292 fn layer_window(&self, li: usize) -> Option<usize> {
7294 self.swa
7295 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
7296 }
7297
7298 fn layer_num_heads(&self, li: usize) -> usize {
7299 self.attention_heads_per_layer
7300 .as_ref()
7301 .and_then(|v| v.get(li).copied())
7302 .unwrap_or(self.num_heads)
7303 }
7304
7305 fn layer_rope_scale(&self, li: usize) -> f32 {
7306 if self.layer_is_local(li) {
7307 self.rope_scale_local
7308 } else {
7309 self.rope_scale
7310 }
7311 }
7312
7313 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
7316 if !self.layer_is_local(li) {
7317 if let Some((ghd, gkv)) = self.global_attn {
7318 return (gkv, ghd, ghd);
7319 }
7320 }
7321 (
7322 self.num_kv_heads,
7323 self.head_dim,
7324 if self.layer_is_local(li) {
7325 self.rotary_dim_local.unwrap_or(self.rotary_dim)
7326 } else {
7327 self.rotary_dim
7328 },
7329 )
7330 }
7331
7332 fn forward_layers(
7334 &mut self,
7335 hidden: &[f32],
7336 position: usize,
7337 task_mask: Option<&TaskMask>,
7338 ) -> Vec<f32> {
7339 let out = self.forward_layers_upto(hidden, position, task_mask, None);
7340 self.o1_progress();
7341 out
7342 }
7343
7344 pub fn embed_id(&self, id: u32) -> Vec<f32> {
7352 self.embed_single(id)
7353 }
7354
7355 pub fn split_supported(&self) -> Result<(), String> {
7359 if self.dsv4.is_some() {
7360 return Err(
7361 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
7362 );
7363 }
7364 if self.dsv41.is_some() {
7365 return Err(
7366 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
7367 .into(),
7368 );
7369 }
7370 if self.qwen4_exp.is_some() {
7371 return Err(
7372 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
7373 );
7374 }
7375 if self.g3n.is_some() {
7376 return Err(
7377 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
7378 );
7379 }
7380 Ok(())
7381 }
7382
7383 pub fn forward_span(
7388 &mut self,
7389 hidden: &[f32],
7390 position: usize,
7391 from: usize,
7392 upto: usize,
7393 task_mask: Option<&TaskMask>,
7394 ) -> Result<Vec<f32>, String> {
7395 self.split_supported()?;
7396 if from > upto || upto >= self.num_layers {
7397 return Err(format!(
7398 "forward_span: layer range {from}..={upto} outside 0..{}",
7399 self.num_layers
7400 ));
7401 }
7402 if hidden.len() != self.hidden_size {
7403 return Err(format!(
7404 "forward_span: hidden len {} ≠ hidden_size {}",
7405 hidden.len(),
7406 self.hidden_size
7407 ));
7408 }
7409 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
7410 self.o1_progress();
7411 if self
7412 .graph_failed
7413 .swap(false, std::sync::atomic::Ordering::Relaxed)
7414 {
7415 self.cancel
7416 .store(false, std::sync::atomic::Ordering::Relaxed);
7417 self.clear_sequence_state();
7418 return Err("forward_span: deferred O(1) transition failed".into());
7419 }
7420 Ok(out)
7421 }
7422
7423 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
7426 let normed = inference::rms_norm(
7427 hidden,
7428 &self.weights.final_norm,
7429 self.rms_eps,
7430 self.norm_style,
7431 );
7432 self.lm_head_forward(&normed)
7433 }
7434
7435 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
7437 sampler::sample_with_scratch(
7438 logits,
7439 &self.sampler_config,
7440 past_tokens,
7441 &mut self.rng,
7442 &mut self.sampler_scratch,
7443 )
7444 }
7445
7446 pub fn reset_session(&mut self) {
7448 self.clear_sequence_state();
7449 }
7450
7451 pub fn prefill_span_ids(
7457 &mut self,
7458 ids: &[u32],
7459 start_pos: usize,
7460 upto: usize,
7461 task_mask: Option<&TaskMask>,
7462 ) -> Result<Vec<f32>, String> {
7463 self.split_supported()?;
7464 if upto >= self.num_layers {
7465 return Err(format!(
7466 "prefill_span_ids: upto {upto} outside 0..{}",
7467 self.num_layers
7468 ));
7469 }
7470 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7474 let out =
7475 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
7476 self.check_o1_progress_failure("prefill_span_ids")?;
7477 Ok(out)
7478 } else {
7479 let hs = self.hidden_size;
7480 let mut out = Vec::with_capacity(ids.len() * hs);
7481 for (i, &id) in ids.iter().enumerate() {
7482 let emb = self.embed_id(id);
7483 out.extend_from_slice(&self.forward_span(
7484 &emb,
7485 start_pos + i,
7486 0,
7487 upto,
7488 task_mask,
7489 )?);
7490 }
7491 Ok(out)
7492 }
7493 }
7494
7495 pub fn prefill_span_hidden(
7498 &mut self,
7499 hidden: &[f32],
7500 start_pos: usize,
7501 from: usize,
7502 upto: usize,
7503 task_mask: Option<&TaskMask>,
7504 ) -> Result<Vec<f32>, String> {
7505 self.split_supported()?;
7506 let hs = self.hidden_size;
7507 if hidden.is_empty() || hidden.len() % hs != 0 {
7508 return Err(format!(
7509 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7510 hidden.len()
7511 ));
7512 }
7513 if from > upto || upto >= self.num_layers {
7514 return Err(format!(
7515 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7516 self.num_layers
7517 ));
7518 }
7519 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7520 let out = self.prefill_batch_span(
7521 PrefillIn::Hidden(hidden),
7522 start_pos,
7523 task_mask,
7524 from,
7525 upto + 1,
7526 );
7527 self.check_o1_progress_failure("prefill_span_hidden")?;
7528 Ok(out)
7529 } else {
7530 let b = hidden.len() / hs;
7531 let mut out = Vec::with_capacity(hidden.len());
7532 for i in 0..b {
7533 let h = self.forward_span(
7534 &hidden[i * hs..(i + 1) * hs],
7535 start_pos + i,
7536 from,
7537 upto,
7538 task_mask,
7539 )?;
7540 out.extend_from_slice(&h);
7541 }
7542 Ok(out)
7543 }
7544 }
7545
7546 fn try_token_graph_wgpu(
7550 &self,
7551 hidden: &[f32],
7552 position: usize,
7553 logits_out: &mut Vec<f32>,
7554 layers_run: &mut usize,
7555 ) -> Option<Result<Vec<f32>, ()>> {
7556 self.try_token_graph_wgpu_steps(
7557 hidden,
7558 position,
7559 logits_out,
7560 1,
7561 None,
7562 Some(layers_run),
7563 0,
7564 self.num_layers,
7565 )
7566 }
7567
7568 fn try_token_graph_wgpu_span(
7572 &self,
7573 hidden: &[f32],
7574 position: usize,
7575 logits_out: &mut Vec<f32>,
7576 from: usize,
7577 upto_excl: usize,
7578 layers_run: &mut usize,
7579 ) -> Option<Result<Vec<f32>, ()>> {
7580 self.try_token_graph_wgpu_steps(
7581 hidden,
7582 position,
7583 logits_out,
7584 1,
7585 None,
7586 Some(layers_run),
7587 from,
7588 upto_excl,
7589 )
7590 }
7591
7592 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7596 if self.o1_active() || self.attn_softcap > 0.0 {
7597 return None;
7598 }
7599 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7600 if !graph_on || crate::gpu::graph_unsupported() {
7601 return None;
7608 }
7609 let emb = self.embed_single(t_next);
7610 let mut lg = Vec::new();
7611 let mut ids = Vec::new();
7612 match self.try_token_graph_wgpu_steps(
7613 &emb,
7614 position,
7615 &mut lg,
7616 k,
7617 Some(&mut ids),
7618 None,
7619 0,
7620 self.num_layers,
7621 ) {
7622 Some(Ok(_)) => {}
7623 Some(Err(())) => {
7624 self.graph_failed
7629 .store(true, std::sync::atomic::Ordering::Relaxed);
7630 return None;
7631 }
7632 None => return None,
7633 }
7634 (ids.len() == k).then_some(ids)
7635 }
7636
7637 fn try_token_graph_wgpu_steps(
7641 &self,
7642 hidden: &[f32],
7643 position: usize,
7644 logits_out: &mut Vec<f32>,
7645 steps: usize,
7646 ids_out: Option<&mut Vec<u32>>,
7647 layers_run: Option<&mut usize>,
7648 from: usize,
7649 upto_excl: usize,
7650 ) -> Option<Result<Vec<f32>, ()>> {
7651 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7654 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7655 return None;
7659 }
7660 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7665 .map(|li| {
7666 if !o1_gpu {
7667 return None;
7668 }
7669 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7670 })
7671 .collect();
7672 if self.o1_active() && o1_gpu {
7673 let want: usize = (from..upto_excl)
7676 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7677 .count();
7678 let have = o1_views.iter().filter(|v| v.is_some()).count();
7679 if want == 0 || have != want {
7680 use std::sync::atomic::{AtomicUsize, Ordering};
7690 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7691 let code = have * 1000 + want;
7692 if LAST.swap(code, Ordering::Relaxed) != code {
7693 tracing::warn!(
7694 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7695 );
7696 }
7697 return None;
7698 }
7699 }
7700 let nh = self.num_heads;
7701 let (nkv, hd, rd) = self.layer_geom(0);
7702 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7703 let mut layers = Vec::with_capacity(upto_excl - from);
7704 let mut model = None;
7705 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7706 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7707 if let Some((_, i, kind, rs)) = t.graph_weight() {
7708 return Some(crate::gpu::GraphW {
7709 idx: i,
7710 kind,
7711 row_scale: rs,
7712 data: &[],
7713 });
7714 }
7715 t.as_f32().map(|d| crate::gpu::GraphW {
7717 idx: 0,
7718 kind: 4,
7719 row_scale: &[],
7720 data: d,
7721 })
7722 }
7723 for li in from..upto_excl {
7724 let lw = &self.weights.layers[self.phys_layer(li)];
7725 if dbg {
7726 let ak = match &lw.attn {
7727 AttnKind::Mla(_) => "Mla".into(),
7728 AttnKind::Full {
7729 output_gate, bias, ..
7730 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7731 AttnKind::LinearGdn(_) => "LinearGdn".into(),
7732 AttnKind::Kda(_) => "Kda".into(),
7733 AttnKind::Linear(_) => "Linear".into(),
7734 AttnKind::ShortConv(_) => "ShortConv".into(),
7735 };
7736 let fk = match &lw.ffn {
7737 FfnKind::Dense(_) => "Dense",
7738 FfnKind::Moe(_) => "Moe",
7739 FfnKind::DenseMoe(_) => "DenseMoe",
7740 };
7741 eprintln!("graph L{li}: attn={ak} ffn={fk}");
7742 }
7743 let gffn = match &lw.ffn {
7744 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7748 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7749 gate: gw(&d.gate_proj)?,
7750 up: gw(&d.up_proj)?,
7751 down: gw(&d.down_proj)?,
7752 },
7753 FfnKind::Moe(m) => {
7754 if m.route_tau.is_some()
7761 || m.mask.is_some()
7762 || (m.routed_scaling - 1.0).abs() > 1e-9
7763 {
7764 return None;
7765 }
7766 let shared = m.shared.as_ref();
7767 let has_shared = shared.is_some();
7768 let sgate = match shared {
7769 Some((_, sg)) => gw(sg.as_ref()?)?,
7770 None => gw(&m.router)?,
7773 };
7774 let router = gw(&m.router)?;
7775 let inter = m.experts.first()?.gate_proj.rows();
7776 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7777 let mut q4tp: Option<bool> = None;
7780 let mut gu_q2: Option<bool> = None;
7783 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
7784 if !matches!(e.act, Act::Silu)
7785 || e.gate_proj.rows() != inter
7786 || e.up_proj.rows() != inter
7787 {
7788 return None;
7789 }
7790 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7791 Some((mm, gi)) => (
7792 mm,
7793 gi,
7794 e.up_proj.mapped_q4t()?.1,
7795 e.down_proj.mapped_q4t()?.1,
7796 false,
7797 false,
7798 ),
7799 None => match e.gate_proj.mapped_q2tp() {
7800 Some((mm, gi)) => (
7801 mm,
7802 gi,
7803 e.up_proj.mapped_q2tp()?.1,
7804 e.down_proj.mapped_q4tp()?.1,
7805 true,
7806 true,
7807 ),
7808 None => {
7809 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7810 (
7811 mm,
7812 gi,
7813 e.up_proj.mapped_q4tp()?.1,
7814 e.down_proj.mapped_q4tp()?.1,
7815 true,
7816 false,
7817 )
7818 }
7819 },
7820 };
7821 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
7822 {
7823 tracing::warn!(
7829 "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."
7830 );
7831 return None;
7832 }
7833 model.get_or_insert_with(|| mm.clone());
7834 experts.push((gi, ui, di));
7835 }
7836 crate::gpu::GraphFfn::Moe {
7837 router,
7838 shared_gate: sgate,
7839 experts,
7840 n_exp: m.experts.len(),
7841 top_k: std::env::var("CMF_TOPK_PROBE")
7847 .ok()
7848 .and_then(|v| v.parse::<usize>().ok())
7849 .filter(|k| *k > 0 && *k <= m.top_k)
7850 .unwrap_or(m.top_k),
7851 inter,
7852 norm_topk: m.norm_topk_prob,
7853 q4tp: q4tp?,
7854 gu_q2: gu_q2.unwrap_or(false),
7855 sigmoid: m.router_sigmoid,
7856 bias: m.expert_bias.as_deref(),
7857 has_shared,
7858 }
7859 }
7860 };
7861 let attn = match &lw.attn {
7862 AttnKind::Full {
7863 wq,
7864 wk,
7865 wv,
7866 wo,
7867 q_norm,
7868 k_norm,
7869 output_gate,
7870 softplus_gate,
7871 bias,
7872 } => {
7873 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7874 return None;
7875 }
7876 let (m, _, _, _) = wq.graph_weight()?;
7877 model = Some(m.clone());
7878 crate::gpu::GraphAttn::Full {
7879 wq: gw(wq)?,
7880 wk: gw(wk)?,
7881 wv: gw(wv)?,
7882 wo: gw(wo)?,
7883 q_norm: q_norm.as_deref(),
7884 k_norm: k_norm.as_deref(),
7885 bias: bias
7886 .as_ref()
7887 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7888 output_gate: *output_gate,
7889 cpu_k: self.kv_cache.layers[li].k_heads(),
7890 cpu_v: self.kv_cache.layers[li].v_heads(),
7891 }
7892 }
7893 AttnKind::LinearGdn(w) => {
7894 let cfg = self.gdn_cfg?;
7895 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7896 model = Some(m.clone());
7897 crate::gpu::GraphAttn::Gdn {
7898 qkv: gw(&w.in_proj_qkv)?,
7899 z: gw(&w.in_proj_z)?,
7900 a: gw(&w.in_proj_a)?,
7901 b: gw(&w.in_proj_b)?,
7902 out: gw(&w.out_proj)?,
7903 conv1d: &w.conv1d,
7904 a_log: &w.a_log,
7905 dt_bias: &w.dt_bias,
7906 norm: &w.norm,
7907 nv: cfg.num_v_heads,
7908 nk: cfg.num_k_heads,
7909 dk: cfg.key_head_dim,
7910 dv: cfg.value_head_dim,
7911 kk: cfg.conv_kernel,
7912 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7913 }
7914 }
7915 AttnKind::ShortConv(w) => {
7916 let cfg = self.short_conv_cfg?;
7917 let (m, _, _, _) = w.in_proj.graph_weight()?;
7918 model = Some(m.clone());
7919 crate::gpu::GraphAttn::ShortConv {
7920 inp: gw(&w.in_proj)?,
7921 out: gw(&w.out_proj)?,
7922 taps: &w.conv,
7923 kernel: cfg.kernel,
7924 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7925 }
7926 }
7927 _ => return None,
7928 };
7929 layers.push(crate::gpu::GraphLayer {
7930 input_norm: &lw.input_norm,
7931 attn,
7932 post_norm: &lw.post_norm,
7933 ffn: gffn,
7934 });
7935 }
7936 let model = model?;
7937 let lm_gw = if upto_excl == self.num_layers
7943 && self.graph_want_logits
7944 && std::env::var("CMF_GPU_LMHEAD")
7945 .map(|v| v != "0")
7946 .unwrap_or(true)
7947 {
7948 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
7949 (
7950 crate::gpu::GraphW {
7951 idx: i,
7952 kind,
7953 row_scale: rs,
7954 data: &[],
7955 },
7956 self.weights.lm_head.rows(),
7957 )
7958 })
7959 } else {
7960 None
7961 };
7962 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
7963 let emb_gw = if steps > 1 {
7965 self.weights
7966 .embed_tokens
7967 .graph_weight()
7968 .map(|(_, i, kind, rs)| {
7969 (
7970 crate::gpu::GraphW {
7971 idx: i,
7972 kind,
7973 row_scale: rs,
7974 data: &[],
7975 },
7976 self.weights.embed_tokens.rows(),
7977 self.embed_multiplier,
7978 )
7979 })
7980 } else {
7981 None
7982 };
7983
7984 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
7990 (from..upto_excl.min(self.num_layers - 1))
7991 .filter(|&li| (li + 1) % self.physical_layers == 0)
7992 .map(|li| li - from)
7993 .collect()
7994 } else {
7995 Vec::new()
7996 };
7997 let mut h = hidden.to_vec();
7998 let outcome = crate::gpu::forward_token_graph(
7999 &model,
8000 self.graph_kv_id,
8001 &layers,
8002 &o1_views,
8003 self.o1_epoch,
8004 &self.inv_freq,
8005 &mut h,
8006 nh,
8007 nkv,
8008 hd,
8009 self.attn_scale,
8010 rd,
8011 self.hidden_size,
8012 self.intermediate_size,
8013 position,
8014 self.kv_cache.max_seq_len,
8015 gemma,
8016 self.rms_eps as f32,
8017 lm,
8018 &self.weights.final_norm,
8019 logits_out,
8020 &loop_norm_at,
8021 steps,
8022 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
8023 ids_out,
8024 layers_run,
8025 from,
8026 false,
8027 );
8028 match outcome {
8029 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
8030 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
8031 crate::gpu::TokenGraphOutcome::Declined => None,
8032 }
8033 }
8034
8035 #[cfg(target_os = "macos")]
8044 #[allow(clippy::type_complexity)]
8045 fn metal_rows_plan(
8046 &self,
8047 ) -> Option<(
8048 Vec<MetalRowsItem<'_>>,
8049 std::sync::Arc<cortiq_core::CmfModel>,
8050 Option<crate::gpu_metal::GdnGpuCfg>,
8051 )> {
8052 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
8053 if !crate::gpu::q1_force()
8054 || !crate::gpu::enabled_here()
8055 || std::env::var("CMF_GPU_BLOCK")
8056 .map(|v| v == "0")
8057 .unwrap_or(false)
8058 || self.attn_softcap > 0.0
8059 || self.o1_active()
8060 || self.swa.is_some()
8061 || self.global_attn.is_some()
8062 || self.attention_heads_per_layer.is_some()
8063 || self.attn_v_norm
8064 || self.loop_final_norm
8065 {
8066 return None;
8067 }
8068 let attend_contract = self.head_dim % 4 == 0
8069 && self.head_dim <= 256
8070 && self.rotary_dim >= 2
8071 && self.rotary_dim <= self.head_dim
8072 && (self.rotary_dim / 2) % 32 == 0
8073 && self.num_kv_heads > 0
8074 && self.num_heads % self.num_kv_heads == 0;
8075 if !attend_contract {
8076 return None;
8077 }
8078 let mut plan: Vec<MetalRowsItem> = Vec::new();
8079 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
8080 for li in 0..self.num_layers {
8081 let lw = &self.weights.layers[self.phys_layer(li)];
8082 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
8083 return None;
8084 }
8085 let ffn = match &lw.ffn {
8086 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
8087 let (Some(g), Some(u), Some(dn)) = (
8088 d.gate_proj.q1_parts(),
8089 d.up_proj.q1_parts(),
8090 d.down_proj.q1_parts(),
8091 ) else {
8092 return None;
8093 };
8094 MetalFfn::Dense {
8095 gate: g,
8096 up: u,
8097 down: dn,
8098 }
8099 }
8100 _ => return None,
8101 };
8102 match &lw.attn {
8103 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
8104 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
8105 w.in_proj_qkv.q1_parts(),
8106 w.in_proj_z.q1_parts(),
8107 w.in_proj_a.f32_parts(),
8108 w.in_proj_b.f32_parts(),
8109 w.out_proj.q1_parts(),
8110 ) else {
8111 return None;
8112 };
8113 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
8114 model_ref.get_or_insert_with(|| model.clone());
8115 }
8116 let gl = GdnGpuLayer {
8117 attn_norm: &lw.input_norm,
8118 post_norm: &lw.post_norm,
8119 qkv,
8120 z,
8121 a,
8122 b: bb,
8123 out,
8124 ffn,
8125 conv1d: &w.conv1d,
8126 a_log: &w.a_log,
8127 dt_bias: &w.dt_bias,
8128 gnorm: &w.norm,
8129 };
8130 match plan.last_mut() {
8131 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
8132 _ => plan.push(MetalRowsItem::Gdn {
8133 run: vec![gl],
8134 first: li,
8135 }),
8136 }
8137 }
8138 AttnKind::Full {
8139 wq,
8140 wk,
8141 wv,
8142 wo,
8143 q_norm,
8144 k_norm,
8145 output_gate,
8146 softplus_gate: None,
8147 bias: None,
8148 } => {
8149 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8150 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
8151 else {
8152 return None;
8153 };
8154 if let QTensor::Mapped { model, .. } = wq {
8155 model_ref.get_or_insert_with(|| model.clone());
8156 }
8157 let cache = &self.kv_cache.layers[li];
8158 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
8159 return None;
8160 }
8161 plan.push(MetalRowsItem::Attn {
8162 l: AttnGpuLayer {
8163 attn_norm: &lw.input_norm,
8164 post_norm: &lw.post_norm,
8165 wq: pq,
8166 wk: pk,
8167 wv: pv,
8168 wo: po,
8169 ffn,
8170 },
8171 li,
8172 q_norm: q_norm.as_deref(),
8173 k_norm: k_norm.as_deref(),
8174 output_gate: *output_gate,
8175 });
8176 }
8177 _ => return None,
8178 }
8179 }
8180 let model = model_ref?;
8181 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
8182 nv: cfg.num_v_heads,
8183 nk: cfg.num_k_heads,
8184 dk: cfg.key_head_dim,
8185 dv: cfg.value_head_dim,
8186 kk: cfg.conv_kernel,
8187 hidden: self.hidden_size,
8188 inter: self.intermediate_size,
8189 c_dim: cfg.conv_dim(),
8190 eps: cfg.rms_eps as f32,
8191 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8192 });
8193 Some((plan, model, gcfg))
8194 }
8195
8196 #[cfg(target_os = "macos")]
8198 #[allow(clippy::too_many_arguments)]
8199 fn metal_attn_params<'a>(
8200 li: usize,
8201 cache: &'a crate::kv_cache::LayerKvCache,
8202 q_norm: Option<&'a [f32]>,
8203 k_norm: Option<&'a [f32]>,
8204 output_gate: bool,
8205 inv_freq: &'a [f32],
8206 geom: (usize, usize, usize, usize),
8207 pos0: usize,
8208 kv_id: u64,
8209 scale: f32,
8210 eps: f32,
8211 gemma: bool,
8212 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
8213 let (nh, nkv, hd, rd) = geom;
8214 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8215 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8216 let cpu_stored = cpu_k[0].len() / hd;
8217 (
8218 crate::gpu_metal::AttnDeviceParams {
8219 kv_id,
8220 layer: li,
8221 nh,
8222 nkv,
8223 hd,
8224 rd,
8225 position: pos0,
8226 scale,
8227 eps,
8228 gemma,
8229 output_gate,
8230 q_norm,
8231 k_norm,
8232 inv_freq,
8233 cpu_k,
8234 cpu_v,
8235 cpu_stored,
8236 o1: None,
8237 },
8238 cpu_stored,
8239 )
8240 }
8241
8242 #[cfg(target_os = "macos")]
8247 #[allow(clippy::type_complexity)]
8248 fn metal_rows_run(
8249 &mut self,
8250 hiddens: &mut [f32],
8251 pos0: usize,
8252 b: usize,
8253 prefill: bool,
8254 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8255 ) -> Option<MetalVerifyPending> {
8256 use crate::gpu_metal::{GraphDims, VerifyGraph};
8257 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
8258 for l in &mut self.kv_cache.layers {
8259 if l.linear_state.len() != want && want > 0 {
8260 l.linear_state = vec![0f32; want];
8261 }
8262 }
8263 let (plan, model, gcfg) = self.metal_rows_plan()?;
8264 let dims = GraphDims {
8265 hidden: self.hidden_size,
8266 eps: self.rms_eps as f32,
8267 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8268 };
8269 let mut graph = if prefill {
8270 VerifyGraph::new_prefill(&model, dims, hiddens, b)?
8271 } else {
8272 VerifyGraph::new(&model, dims, hiddens, b)?
8273 };
8274 let geom = (
8275 self.num_heads,
8276 self.num_kv_heads,
8277 self.head_dim,
8278 self.rotary_dim,
8279 );
8280 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8281 let eps = self.rms_eps as f32;
8282 let kv_id = self.graph_kv_id;
8283 let inv_freq = self.inv_freq.clone();
8284 for item in &plan {
8285 let ok = match item {
8286 MetalRowsItem::Gdn { run, .. } => gcfg
8287 .as_ref()
8288 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
8289 .unwrap_or(false),
8290 MetalRowsItem::Attn {
8291 l,
8292 li,
8293 q_norm,
8294 k_norm,
8295 output_gate,
8296 } => {
8297 let (p, _) = Self::metal_attn_params(
8298 *li,
8299 &self.kv_cache.layers[*li],
8300 *q_norm,
8301 *k_norm,
8302 *output_gate,
8303 &inv_freq,
8304 geom,
8305 pos0,
8306 kv_id,
8307 self.attn_scale,
8308 eps,
8309 gemma,
8310 );
8311 graph.attn_ok(l, &p)
8312 }
8313 };
8314 if !ok {
8315 use std::sync::atomic::{AtomicBool, Ordering};
8316 static SAID: AtomicBool = AtomicBool::new(false);
8317 if !SAID.swap(true, Ordering::Relaxed) {
8318 tracing::warn!("metal rows graph: a layer failed preflight — declining");
8319 }
8320 return None;
8321 }
8322 }
8323 let lm = match &spec {
8324 Some((lm, _, _)) => {
8325 if !graph.lm_head_ok(*lm) {
8326 return None;
8327 }
8328 Some(*lm)
8329 }
8330 None => None,
8331 };
8332 let mut gdn_layers = Vec::new();
8333 let mut attn_layers = Vec::new();
8334 for item in &plan {
8335 match item {
8336 MetalRowsItem::Gdn { run, first } => {
8337 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
8338 .iter()
8339 .map(|l| l.linear_state.as_slice())
8340 .collect();
8341 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
8342 return None;
8343 }
8344 gdn_layers.extend(*first..*first + run.len());
8345 }
8346 MetalRowsItem::Attn {
8347 l,
8348 li,
8349 q_norm,
8350 k_norm,
8351 output_gate,
8352 } => {
8353 let (p, cpu_stored) = Self::metal_attn_params(
8354 *li,
8355 &self.kv_cache.layers[*li],
8356 *q_norm,
8357 *k_norm,
8358 *output_gate,
8359 &inv_freq,
8360 geom,
8361 pos0,
8362 kv_id,
8363 self.attn_scale,
8364 eps,
8365 gemma,
8366 );
8367 if !graph.encode_attn_b(l, &p) {
8368 return None;
8369 }
8370 attn_layers.push((*li, cpu_stored));
8371 }
8372 }
8373 }
8374 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
8375 if !graph.encode_lm_head_b(final_norm, lm) {
8376 return None;
8377 }
8378 }
8379 graph.sync();
8380 if let Some((lm, _, logits)) = spec {
8381 logits.resize(b * lm.1, 0.0);
8382 graph.read_logits(logits);
8383 }
8384 graph.read_hidden(hiddens);
8385 Some(MetalVerifyPending {
8386 graph,
8387 gdn_layers,
8388 attn_layers,
8389 })
8390 }
8391
8392 #[cfg(target_os = "macos")]
8398 fn try_batch_graph_metal(
8399 &mut self,
8400 hiddens: &mut [f32],
8401 positions: &[usize],
8402 b: usize,
8403 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
8404 ) -> crate::gpu::BatchGraphOutcome {
8405 let _t0 = std::time::Instant::now();
8406 if positions.len() != b
8407 || positions.windows(2).any(|w| w[1] != w[0] + 1)
8408 || hiddens.len() != b * self.hidden_size
8409 {
8410 return crate::gpu::BatchGraphOutcome::Declined;
8411 }
8412 let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
8413 return crate::gpu::BatchGraphOutcome::Declined;
8414 };
8415 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8416 eprintln!(
8417 "metal-verify: {:.1} ms | b={b}",
8418 _t0.elapsed().as_secs_f64() * 1e3
8419 );
8420 }
8421 self.metal_verify = Some(pending);
8422 crate::gpu::BatchGraphOutcome::Completed
8423 }
8424
8425 #[cfg(target_os = "macos")]
8430 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
8431 let b = ids.len();
8432 if b == 0 || b > 512 {
8433 return None;
8434 }
8435 let hs = self.hidden_size;
8436 let mut hiddens = vec![0f32; b * hs];
8437 for (j, &id) in ids.iter().enumerate() {
8438 let e = self.embed_single(id);
8439 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
8440 }
8441 let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
8442 let idxs = pending.gdn_layers.clone();
8444 let mut outs: Vec<&mut [f32]> = self
8445 .kv_cache
8446 .layers
8447 .iter_mut()
8448 .enumerate()
8449 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8450 .map(|(_, l)| l.linear_state.as_mut_slice())
8451 .collect();
8452 pending.graph.finish_states(&mut outs);
8453 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8454 let mut kbuf = vec![0f32; b * nkv * hd];
8455 let mut vbuf = vec![0f32; b * nkv * hd];
8456 for (li, cpu_stored) in &pending.attn_layers {
8457 if crate::gpu_metal::kv_mirror_read_rows(
8458 self.graph_kv_id,
8459 *li,
8460 nkv,
8461 hd,
8462 *cpu_stored,
8463 b,
8464 &mut kbuf,
8465 &mut vbuf,
8466 ) {
8467 let cache = &mut self.kv_cache.layers[*li];
8468 for r in 0..b {
8469 cache.append(
8470 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8471 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8472 &[],
8473 );
8474 }
8475 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
8476 }
8477 }
8478 Some(hiddens)
8479 }
8480
8481 #[cfg(target_os = "macos")]
8485 fn metal_verify_commit(&mut self, a: usize) -> bool {
8486 let Some(mut pending) = self.metal_verify.take() else {
8487 return false;
8488 };
8489 let n = a + 1;
8490 let idxs = pending.gdn_layers.clone();
8492 let mut outs: Vec<&mut [f32]> = self
8493 .kv_cache
8494 .layers
8495 .iter_mut()
8496 .enumerate()
8497 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8498 .map(|(_, l)| l.linear_state.as_mut_slice())
8499 .collect();
8500 if !pending.graph.commit(n, &mut outs) {
8501 return false;
8502 }
8503 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8504 let mut kbuf = vec![0f32; n * nkv * hd];
8505 let mut vbuf = vec![0f32; n * nkv * hd];
8506 for (li, cpu_stored) in &pending.attn_layers {
8507 if crate::gpu_metal::kv_mirror_read_rows(
8508 self.graph_kv_id,
8509 *li,
8510 nkv,
8511 hd,
8512 *cpu_stored,
8513 n,
8514 &mut kbuf,
8515 &mut vbuf,
8516 ) {
8517 let cache = &mut self.kv_cache.layers[*li];
8518 for r in 0..n {
8519 cache.append(
8520 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8521 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8522 &[],
8523 );
8524 }
8525 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
8526 }
8527 }
8528 true
8529 }
8530
8531 #[cfg(target_os = "macos")]
8537 fn mtp_warm_batch_metal(
8538 &mut self,
8539 m: &mut MtpModule,
8540 pairs: &[(&[f32], u32)],
8541 first_pos: usize,
8542 ) -> bool {
8543 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
8544 let b = pairs.len();
8545 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
8546 return false;
8547 }
8548 let AttnKind::Full {
8549 wq,
8550 wk,
8551 wv,
8552 wo,
8553 q_norm,
8554 k_norm,
8555 output_gate,
8556 softplus_gate: None,
8557 bias: None,
8558 } = &m.layer.attn
8559 else {
8560 return false;
8561 };
8562 let FfnKind::Dense(d) = &m.layer.ffn else {
8563 return false;
8564 };
8565 if !d.segs.is_empty() {
8566 return false;
8567 }
8568 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8569 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
8570 else {
8571 return false;
8572 };
8573 let (Some(g), Some(u), Some(dn)) = (
8574 d.gate_proj.q1_parts(),
8575 d.up_proj.q1_parts(),
8576 d.down_proj.q1_parts(),
8577 ) else {
8578 return false;
8579 };
8580 let Some(eh) = m.eh_proj.q1_parts() else {
8581 return false;
8582 };
8583 let QTensor::Mapped { model, .. } = wq else {
8584 return false;
8585 };
8586 let model = model.clone();
8587 let hs = self.hidden_size;
8588 let mut cat = vec![0f32; b * 2 * hs];
8590 for (j, (h, tok)) in pairs.iter().enumerate() {
8591 let e = self.embed_single(*tok);
8592 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
8593 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
8594 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
8595 }
8596 let dims = GraphDims {
8597 hidden: hs,
8598 eps: self.rms_eps as f32,
8599 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8600 };
8601 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
8602 return false;
8603 };
8604 let l = AttnGpuLayer {
8605 attn_norm: &m.layer.input_norm,
8606 post_norm: &m.layer.post_norm,
8607 wq: pq,
8608 wk: pk,
8609 wv: pv,
8610 wo: po,
8611 ffn: MetalFfn::Dense {
8612 gate: g,
8613 up: u,
8614 down: dn,
8615 },
8616 };
8617 let (nh, nkv, hd, rd) = (
8618 self.num_heads,
8619 self.num_kv_heads,
8620 self.head_dim,
8621 self.rotary_dim,
8622 );
8623 let inv_freq = self.inv_freq.clone();
8624 let cpu_stored;
8625 {
8626 let cache = &m.kv;
8627 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8628 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8629 cpu_stored = cpu_k[0].len() / hd;
8630 if cpu_stored != first_pos {
8631 return false;
8632 }
8633 let p = AttnDeviceParams {
8634 kv_id: self.mtp_kv_id(),
8635 layer: Self::MTP_LAYER_BASE,
8636 nh,
8637 nkv,
8638 hd,
8639 rd,
8640 position: first_pos,
8641 scale: self.attn_scale,
8642 eps: self.rms_eps as f32,
8643 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8644 output_gate: *output_gate,
8645 q_norm: q_norm.as_deref(),
8646 k_norm: k_norm.as_deref(),
8647 inv_freq: &inv_freq,
8648 cpu_k,
8649 cpu_v,
8650 cpu_stored,
8651 o1: None,
8652 };
8653 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
8654 return false;
8655 }
8656 }
8657 graph.sync();
8658 let mut kbuf = vec![0f32; b * nkv * hd];
8659 let mut vbuf = vec![0f32; b * nkv * hd];
8660 if !crate::gpu_metal::kv_mirror_read_rows(
8661 self.mtp_kv_id(),
8662 Self::MTP_LAYER_BASE,
8663 nkv,
8664 hd,
8665 cpu_stored,
8666 b,
8667 &mut kbuf,
8668 &mut vbuf,
8669 ) {
8670 return false;
8671 }
8672 for r in 0..b {
8673 m.kv.append(
8674 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8675 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8676 &[],
8677 );
8678 }
8679 crate::gpu_metal::kv_mirror_set_stored(
8680 self.mtp_kv_id(),
8681 Self::MTP_LAYER_BASE,
8682 cpu_stored + b,
8683 );
8684 true
8685 }
8686
8687 fn draft_vocab_rows(head_rows: usize) -> usize {
8690 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
8691 let n = *N.get_or_init(|| {
8692 std::env::var("CMF_DRAFT_VOCAB")
8693 .ok()
8694 .and_then(|v| v.parse().ok())
8695 .unwrap_or(65536)
8696 });
8697 if n == 0 { head_rows } else { n.min(head_rows) }
8698 }
8699
8700 #[cfg(target_os = "macos")]
8705 fn mtp_step_metal(
8706 &mut self,
8707 m: &mut MtpModule,
8708 hidden: &[f32],
8709 next_token: u32,
8710 position: usize,
8711 want_logits: bool,
8712 ) -> Option<(Vec<f32>, Vec<f32>)> {
8713 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
8714 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
8715 || !crate::gpu::q1_force()
8716 || !crate::gpu::enabled_here()
8717 || self.attn_softcap > 0.0
8718 || self.attention_heads_per_layer.is_some()
8719 || m.kv.mode != crate::kv_cache::KvMode::F32
8720 || m.kv.o1.is_some()
8721 {
8722 return None;
8723 }
8724 let AttnKind::Full {
8725 wq,
8726 wk,
8727 wv,
8728 wo,
8729 q_norm,
8730 k_norm,
8731 output_gate,
8732 softplus_gate: None,
8733 bias: None,
8734 } = &m.layer.attn
8735 else {
8736 return None;
8737 };
8738 let FfnKind::Dense(d) = &m.layer.ffn else {
8739 return None;
8740 };
8741 if d.act != Act::Silu || !d.segs.is_empty() {
8742 return None;
8743 }
8744 let (pq, pk, pv, po) = (
8745 wq.q1_parts()?,
8746 wk.q1_parts()?,
8747 wv.q1_parts()?,
8748 wo.q1_parts()?,
8749 );
8750 let (g, u, dn) = (
8751 d.gate_proj.q1_parts()?,
8752 d.up_proj.q1_parts()?,
8753 d.down_proj.q1_parts()?,
8754 );
8755 let QTensor::Mapped { model, .. } = wq else {
8756 return None;
8757 };
8758 let model = model.clone();
8759 let lm = if want_logits {
8760 Some(self.weights.lm_head.q1_parts()?)
8761 } else {
8762 None
8763 };
8764 let dims = GraphDims {
8765 hidden: self.hidden_size,
8766 eps: self.rms_eps as f32,
8767 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8768 };
8769 let hs = self.hidden_size;
8772 let mut x = vec![0f32; hs];
8773 let mut graph = TokenGraph::new(&model, dims, &x)?;
8774 let mut folded = false;
8775 if let Some(eh) = m.eh_proj.q1_parts() {
8776 let e = self.embed_single(next_token);
8777 let mut cat = vec![0.0f32; 2 * hs];
8778 let (cat_e, cat_h) = cat.split_at_mut(hs);
8779 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
8780 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
8781 folded = graph.encode_input_proj(eh, &cat);
8782 }
8783 if !folded {
8784 x = self.mtp_block_input(m, hidden, next_token);
8785 graph = TokenGraph::new(&model, dims, &x)?;
8786 }
8787 let l = AttnGpuLayer {
8788 attn_norm: &m.layer.input_norm,
8789 post_norm: &m.layer.post_norm,
8790 wq: pq,
8791 wk: pk,
8792 wv: pv,
8793 wo: po,
8794 ffn: MetalFfn::Dense {
8795 gate: g,
8796 up: u,
8797 down: dn,
8798 },
8799 };
8800 let (nh, nkv, hd, rd) = (
8801 self.num_heads,
8802 self.num_kv_heads,
8803 self.head_dim,
8804 self.rotary_dim,
8805 );
8806 let inv_freq = self.inv_freq.clone();
8807 {
8808 let cache = &m.kv;
8809 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8810 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8811 let cpu_stored = cpu_k[0].len() / hd;
8812 let p = AttnDeviceParams {
8813 kv_id: self.mtp_kv_id(),
8814 layer: Self::MTP_LAYER_BASE,
8815 nh,
8816 nkv,
8817 hd,
8818 rd,
8819 position,
8820 scale: self.attn_scale,
8821 eps: self.rms_eps as f32,
8822 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8823 output_gate: *output_gate,
8824 q_norm: q_norm.as_deref(),
8825 k_norm: k_norm.as_deref(),
8826 inv_freq: &inv_freq,
8827 cpu_k,
8828 cpu_v,
8829 cpu_stored,
8830 o1: None,
8831 };
8832 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
8833 return None;
8834 }
8835 }
8836 let draft_rows = if let Some(lm) = lm {
8842 Self::draft_vocab_rows(lm.1)
8843 } else {
8844 0
8845 };
8846 if let Some(lm) = lm {
8847 if !graph.lm_head_ok(lm) {
8848 return None;
8849 }
8850 if draft_rows < lm.1 {
8851 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
8852 return None;
8853 }
8854 } else {
8855 graph.encode_lm_head(&m.final_norm, lm);
8856 }
8857 }
8858 graph.sync();
8859 let mut logits = Vec::new();
8860 if let Some(lm) = lm {
8861 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
8862 logits = attention::take_buf(n_read);
8863 graph.read_logits(&mut logits);
8864 logits.resize(self.vocab_size, f32::NEG_INFINITY);
8866 }
8867 graph.finish(&mut x);
8868 let mut krow = attention::take_buf(nkv * hd);
8869 let mut vrow = attention::take_buf(nkv * hd);
8870 if crate::gpu_metal::kv_mirror_read_last(
8871 self.mtp_kv_id(),
8872 Self::MTP_LAYER_BASE,
8873 nkv,
8874 hd,
8875 &mut krow,
8876 &mut vrow,
8877 ) {
8878 m.kv.append(&krow, &vrow, &[]);
8879 }
8880 attention::recycle_buf(&mut krow);
8881 attention::recycle_buf(&mut vrow);
8882 Some((logits, x))
8883 }
8884
8885 fn try_batch_graph_wgpu(
8886 &self,
8887 hiddens: &mut [f32],
8888 positions: &[usize],
8889 k: usize,
8890 spec: Option<crate::gpu::SpecTail<'_>>,
8891 ) -> crate::gpu::BatchGraphOutcome {
8892 let _tb = std::time::Instant::now();
8893 if self.attn_softcap > 0.0 {
8894 return crate::gpu::BatchGraphOutcome::Declined; }
8896 let nh = self.num_heads;
8897 let (nkv, hd, rd) = self.layer_geom(0);
8898 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8899 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
8900 if let Some((_, i, kind, rs)) = t.graph_weight() {
8901 return Some(crate::gpu::GraphW {
8902 idx: i,
8903 kind,
8904 row_scale: rs,
8905 data: &[],
8906 });
8907 }
8908 t.as_f32().map(|d| crate::gpu::GraphW {
8909 idx: 0,
8910 kind: 4,
8911 row_scale: &[],
8912 data: d,
8913 })
8914 }
8915 let built: Option<(
8916 Vec<crate::gpu::GraphLayer<'_>>,
8917 std::sync::Arc<cortiq_core::CmfModel>,
8918 )> = (|| {
8919 let mut layers = Vec::with_capacity(self.num_layers);
8920 let mut model = None;
8921 for li in 0..self.num_layers {
8922 let lw = &self.weights.layers[self.phys_layer(li)];
8923 let gffn = match &lw.ffn {
8930 FfnKind::Dense(d) if !d.segs.is_empty() => return None,
8931 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
8932 gate: gw(&d.gate_proj)?,
8933 up: gw(&d.up_proj)?,
8934 down: gw(&d.down_proj)?,
8935 },
8936 FfnKind::Moe(m) => {
8937 if m.router_sigmoid
8938 || m.expert_bias.is_some()
8939 || m.route_tau.is_some()
8940 || m.mask.is_some()
8941 {
8942 return None;
8943 }
8944 let (se, sg) = m.shared.as_ref()?;
8945 let sgate = gw(sg.as_ref()?)?;
8946 let router = gw(&m.router)?;
8947 let inter = m.experts.first()?.gate_proj.rows();
8948 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8949 let mut q4tp: Option<bool> = None;
8950 let mut gu_q2: Option<bool> = None;
8951 for e in m.experts.iter().chain(std::iter::once(se)) {
8952 if !matches!(e.act, Act::Silu)
8953 || e.gate_proj.rows() != inter
8954 || e.up_proj.rows() != inter
8955 {
8956 return None;
8957 }
8958 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8962 Some((mm, gi)) => (
8963 mm,
8964 gi,
8965 e.up_proj.mapped_q4t()?.1,
8966 e.down_proj.mapped_q4t()?.1,
8967 false,
8968 false,
8969 ),
8970 None => match e.gate_proj.mapped_q2tp() {
8971 Some((mm, gi)) => (
8972 mm,
8973 gi,
8974 e.up_proj.mapped_q2tp()?.1,
8975 e.down_proj.mapped_q4tp()?.1,
8976 true,
8977 true,
8978 ),
8979 None => {
8980 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8981 (
8982 mm,
8983 gi,
8984 e.up_proj.mapped_q4tp()?.1,
8985 e.down_proj.mapped_q4tp()?.1,
8986 true,
8987 false,
8988 )
8989 }
8990 },
8991 };
8992 if *q4tp.get_or_insert(is_p) != is_p
8993 || *gu_q2.get_or_insert(is_q2) != is_q2
8994 {
8995 return None;
8996 }
8997 model.get_or_insert_with(|| mm.clone());
8998 experts.push((gi, ui, di));
8999 }
9000 crate::gpu::GraphFfn::Moe {
9001 router,
9002 shared_gate: sgate,
9003 experts,
9004 n_exp: m.experts.len(),
9005 top_k: m.top_k,
9006 inter,
9007 norm_topk: m.norm_topk_prob,
9008 q4tp: q4tp?,
9009 gu_q2: gu_q2.unwrap_or(false),
9010 sigmoid: false,
9011 bias: None,
9012 has_shared: true,
9013 }
9014 }
9015 _ => return None,
9016 };
9017 let attn = match &lw.attn {
9018 AttnKind::Full {
9019 wq,
9020 wk,
9021 wv,
9022 wo,
9023 q_norm,
9024 k_norm,
9025 output_gate,
9026 softplus_gate,
9027 bias,
9028 } => {
9029 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
9030 return None;
9031 }
9032 let (m, _, _, _) = wq.graph_weight()?;
9033 model = Some(m.clone());
9034 crate::gpu::GraphAttn::Full {
9035 wq: gw(wq)?,
9036 wk: gw(wk)?,
9037 wv: gw(wv)?,
9038 wo: gw(wo)?,
9039 q_norm: q_norm.as_deref(),
9040 k_norm: k_norm.as_deref(),
9041 bias: bias
9042 .as_ref()
9043 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9044 output_gate: *output_gate,
9045 cpu_k: self.kv_cache.layers[li].k_heads(),
9046 cpu_v: self.kv_cache.layers[li].v_heads(),
9047 }
9048 }
9049 AttnKind::LinearGdn(w) => {
9050 let cfg = self.gdn_cfg?;
9051 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
9052 model = Some(m.clone());
9053 crate::gpu::GraphAttn::Gdn {
9054 qkv: gw(&w.in_proj_qkv)?,
9055 z: gw(&w.in_proj_z)?,
9056 a: gw(&w.in_proj_a)?,
9057 b: gw(&w.in_proj_b)?,
9058 out: gw(&w.out_proj)?,
9059 conv1d: &w.conv1d,
9060 a_log: &w.a_log,
9061 dt_bias: &w.dt_bias,
9062 norm: &w.norm,
9063 nv: cfg.num_v_heads,
9064 nk: cfg.num_k_heads,
9065 dk: cfg.key_head_dim,
9066 dv: cfg.value_head_dim,
9067 kk: cfg.conv_kernel,
9068 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
9069 }
9070 }
9071 _ => return None,
9072 };
9073 layers.push(crate::gpu::GraphLayer {
9074 input_norm: &lw.input_norm,
9075 attn,
9076 post_norm: &lw.post_norm,
9077 ffn: gffn,
9078 });
9079 }
9080 Some((layers, model?))
9081 })();
9082 let Some((layers, model)) = built else {
9083 {
9084 use std::sync::atomic::{AtomicBool, Ordering};
9085 static SAID: AtomicBool = AtomicBool::new(false);
9086 if !SAID.swap(true, Ordering::Relaxed) {
9087 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
9088 }
9089 }
9090 return crate::gpu::BatchGraphOutcome::Declined;
9091 };
9092 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
9093 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
9094 }
9095 crate::gpu::forward_batch_graph(
9096 &model,
9097 self.graph_kv_id,
9098 &layers,
9099 &self.inv_freq,
9100 hiddens,
9101 nh,
9102 nkv,
9103 hd,
9104 rd,
9105 self.hidden_size,
9106 self.intermediate_size,
9107 positions,
9108 self.kv_cache.max_seq_len,
9109 gemma,
9110 self.rms_eps as f32,
9111 self.attn_scale,
9112 k,
9113 &(0..self.num_layers)
9114 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
9115 .collect::<Vec<_>>(),
9116 self.o1_epoch,
9117 spec,
9118 )
9119 }
9120
9121 fn draft_probe() -> bool {
9125 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9126 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
9127 }
9128
9129 #[cfg(feature = "gpu")]
9141 fn dsv4_spec_on() -> bool {
9142 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9143 *ON.get_or_init(|| {
9144 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
9148 return v != "0";
9149 }
9150 std::env::var("CMF_DSV4_SPEC")
9157 .map(|v| v != "0")
9158 .unwrap_or_else(|_| {
9159 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
9160 })
9161 })
9162 }
9163
9164 #[cfg(feature = "gpu")]
9171 fn dsv4_spec_step(
9172 &mut self,
9173 tip_token: u32,
9174 t_next: u32,
9175 next_pos: usize,
9176 max_extra: usize,
9177 drafted: &mut usize,
9178 accepted_ctr: &mut usize,
9179 ) -> Option<(Vec<u32>, usize)> {
9180 let t_all = std::time::Instant::now();
9181 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9182 thread_local! {
9183 static LAST: std::cell::Cell<Option<std::time::Instant>> =
9184 const { std::cell::Cell::new(None) };
9185 }
9186 LAST.with(|l| {
9187 if let Some(prev) = l.get() {
9188 eprintln!(
9189 "между раундами {:.1} мс",
9190 prev.elapsed().as_secs_f64() * 1e3
9191 );
9192 }
9193 l.set(Some(std::time::Instant::now()));
9194 });
9195 }
9196 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9197 eprintln!("spec_step: вход pos={next_pos}");
9198 }
9199 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
9200 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
9201 if self.dspark.is_none() {
9203 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9204 if t.is_empty() {
9205 return None;
9206 }
9207 crate::dsv4::dspark_arm(&t, cfg.dim);
9208 self.dspark = Some(crate::dsv4::DsparkState::new(
9209 self.dsv4_mtp.len(),
9210 &cfg,
9211 t.len(),
9212 ));
9213 }
9214 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9215 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
9216 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
9217 eprintln!("spec_step: пак не построился (targets {targets:?})");
9218 }
9219 let pack = pack?;
9220 let block = crate::dsv4::dspark_block();
9221 let b_box = self.dsv4.as_mut()?;
9222 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
9223 let ds = self.dspark.as_mut()?;
9224 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
9227 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
9228 if dbg {
9229 eprintln!("spec_step: нет захвата");
9230 }
9231 return None;
9232 }
9233 ds.have_hidden = true;
9234 let tip_pos = next_pos.checked_sub(1)?;
9235 let draft_started = std::time::Instant::now();
9236 let mut conf = Vec::new();
9237 let props = crate::dsv4::dspark_draft_gpu(
9238 g,
9239 &self.dsv4_mtp,
9240 &cfg,
9241 ds,
9242 pack,
9243 st.kv_id,
9244 tip_token,
9245 tip_pos,
9246 self.pool.as_deref(),
9247 &mut conf,
9248 );
9249 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9250 *drafted += block;
9251 if props.is_empty() || props[0] != t_next {
9252 if dbg {
9253 eprintln!(
9254 "spec_step: черновик {} (props0={:?} t_next={t_next})",
9255 if props.is_empty() {
9256 "пуст"
9257 } else {
9258 "мимо"
9259 },
9260 props.first()
9261 );
9262 }
9263 return None;
9264 }
9265 let mut k_verify = crate::dsv4::dspark_verify_k()
9272 .min(props.len())
9273 .min(max_extra.saturating_add(1));
9274 let conf_min = {
9280 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
9281 *M.get_or_init(|| {
9282 std::env::var("CMF_DSPARK_CONF_MIN")
9283 .ok()
9284 .and_then(|v| v.parse().ok())
9285 .unwrap_or(0.0)
9286 })
9287 };
9288 if conf_min > 0.0 && conf.len() >= props.len() {
9289 let mut keep = 1usize;
9290 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
9291 keep += 1;
9292 }
9293 k_verify = k_verify.min(keep.max(2));
9294 }
9295 if k_verify < 2 {
9296 return None;
9297 }
9298 let mut fed = Vec::with_capacity(k_verify);
9299 fed.push(t_next);
9300 fed.extend_from_slice(&props[1..k_verify]);
9301 let mut argmax = Vec::new();
9302 let mut logits_all = Vec::new();
9303 let mut walked = Vec::new();
9304 let txn = crate::dsv4::dsv4_verify_chunk(
9305 g,
9306 layers,
9307 &cfg,
9308 st,
9309 &fed,
9310 next_pos,
9311 &self.inv_freq,
9312 self.pool.as_deref(),
9313 &targets,
9314 &mut argmax,
9315 &mut logits_all,
9316 &mut walked,
9317 );
9318 if txn.is_none() && dbg {
9319 eprintln!("spec_step: verify отказал");
9320 }
9321 let txn = txn?;
9322 let spec_gpu_end = txn.gpu_end;
9323 let b = fed.len();
9324 let mut accepted = 1usize;
9325 while accepted < b && fed[accepted] == argmax[accepted - 1] {
9326 accepted += 1;
9327 }
9328 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
9333 accepted = 1;
9334 }
9335 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
9336 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
9337 }
9338 let t_fin = std::time::Instant::now();
9339 if !crate::dsv4::dsv4_spec_finish(
9340 g,
9341 layers,
9342 &cfg,
9343 st,
9344 txn,
9345 accepted,
9346 &fed,
9347 &self.inv_freq,
9348 self.pool.as_deref(),
9349 ) {
9350 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
9351 return None;
9352 }
9353 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9354 eprintln!(
9355 "finish(k={accepted}): {:.1} мс",
9356 t_fin.elapsed().as_secs_f64() * 1e3
9357 );
9358 }
9359 *accepted_ctr += accepted - 1;
9360 let (hc, dim) = (cfg.hc_mult, cfg.dim);
9365 let dev_caps: Vec<usize> = targets
9370 .iter()
9371 .copied()
9372 .filter(|&t| t < spec_gpu_end)
9373 .collect();
9374 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
9375 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
9376 return None;
9377 }
9378 for t in 0..accepted {
9379 let tip = t + 1 == accepted;
9380 for (slot, &tl) in targets.iter().enumerate() {
9381 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
9382 let lo = (di * b + t) * hc * dim;
9383 crate::dsv4::dspark_capture(
9384 &caps_all[lo..lo + hc * dim],
9385 &cfg,
9386 slot,
9387 &mut ds.main_hidden,
9388 );
9389 } else if tip
9390 && crate::dsv4::dspark_peek_slot(slot, dim, {
9391 let lo = slot * dim;
9392 &mut ds.main_hidden[lo..lo + dim]
9393 })
9394 {
9395 } else {
9400 crate::dsv4::dspark_capture(
9404 &walked[t * hc * dim..(t + 1) * hc * dim],
9405 &cfg,
9406 slot,
9407 &mut ds.main_hidden,
9408 );
9409 }
9410 }
9411 crate::dsv4::dspark_ring_append(
9412 g,
9413 &self.dsv4_mtp,
9414 &cfg,
9415 ds,
9416 next_pos + t,
9417 self.pool.as_deref(),
9418 );
9419 }
9420 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
9421 self.graph_logits = Some(row);
9422 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
9427 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
9428 crate::dsv4::pick_tally_arm();
9429 }
9430 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
9431 eprintln!(
9432 "spec_step total {:.1} мс (k={accepted})",
9433 t_all.elapsed().as_secs_f64() * 1e3
9434 );
9435 }
9436 Some((fed[1..accepted].to_vec(), next_pos + accepted))
9437 }
9438
9439 fn dspark_probe(&mut self, position: usize, token_id: u32) {
9440 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
9441 return;
9442 }
9443 let trunk_now = crate::dsv4::pick_tally_take();
9445 crate::dsv4::trunk_freq_note(&trunk_now);
9446 if !trunk_now.is_empty() {
9447 self.dspark_trunk_picks.push(trunk_now);
9448 let keep = crate::dsv4::dspark_block();
9449 if self.dspark_trunk_picks.len() > keep {
9450 self.dspark_trunk_picks.remove(0);
9451 }
9452 }
9453 for p in std::mem::take(&mut self.dspark_pending) {
9456 let Some(i) = position.checked_sub(p.0 + 1) else {
9457 continue;
9458 };
9459 let mut p = p;
9460 if i < p.1.len() {
9461 if p.2 && p.1[i] == token_id {
9462 p.3 = i + 1;
9463 } else {
9464 p.2 = false;
9465 }
9466 if i + 1 < p.1.len() {
9467 self.dspark_pending.push(p);
9468 continue;
9469 }
9470 }
9471 self.dspark_hist.push(p.3);
9472 self.dspark_real.push(token_id);
9473 }
9474 let Some(b) = &mut self.dsv4 else { return };
9475 let (g, layers, cfg) = (&b.0, &b.1, b.2);
9476 let n_layers = layers.len();
9477 if self.dspark.is_none() {
9478 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9479 if t.is_empty() {
9480 return;
9481 }
9482 eprintln!(
9483 "DSpark: захват со слоёв {t:?}, блок {}",
9484 crate::dsv4::dspark_block()
9485 );
9486 crate::dsv4::dspark_arm(&t, cfg.dim);
9487 self.dspark = Some(crate::dsv4::DsparkState::new(
9488 self.dsv4_mtp.len(),
9489 &cfg,
9490 t.len(),
9491 ));
9492 }
9493 let ds = self.dspark.as_mut().unwrap();
9494 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
9495 return; }
9497 let mut conf = Vec::new();
9498 crate::dsv4::pick_tally_arm();
9499 let draft_started = std::time::Instant::now();
9504 #[cfg(feature = "gpu")]
9505 let gpu_draft = crate::dsv4::dspark_gpu_on();
9506 #[cfg(not(feature = "gpu"))]
9507 let gpu_draft = false;
9508 let props = if gpu_draft {
9509 #[cfg(feature = "gpu")]
9510 {
9511 let kv_id = b.3.kv_id;
9512 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
9513 Some(pk) => crate::dsv4::dspark_draft_gpu(
9514 g,
9515 &self.dsv4_mtp,
9516 &cfg,
9517 ds,
9518 pk,
9519 kv_id,
9520 token_id,
9521 position,
9522 self.pool.as_deref(),
9523 &mut conf,
9524 ),
9525 None => Vec::new(),
9526 }
9527 }
9528 #[cfg(not(feature = "gpu"))]
9529 Vec::new()
9530 } else {
9531 crate::gpu::cpu_scope(|| {
9532 crate::dsv4::dspark_draft(
9533 g,
9534 &self.dsv4_mtp,
9535 &cfg,
9536 ds,
9537 token_id,
9538 position,
9539 self.pool.as_deref(),
9540 &mut conf,
9541 )
9542 })
9543 };
9544 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9545 let draft_picks = crate::dsv4::pick_tally_take();
9546 crate::dsv4::dspark_freq_note(&draft_picks);
9547 crate::dsv4::pick_tally_arm();
9550 if !props.is_empty() {
9551 let (tu, tt) = {
9555 let flat: Vec<(usize, Vec<usize>)> = self
9556 .dspark_trunk_picks
9557 .iter()
9558 .flat_map(|v| v.iter().cloned())
9559 .collect();
9560 let mut per: std::collections::HashMap<usize, Vec<usize>> =
9562 std::collections::HashMap::new();
9563 for (li, picks) in flat {
9564 per.entry(li).or_default().extend(picks);
9565 }
9566 let n = per.len().max(1);
9567 let mut u = 0usize;
9568 let mut t = 0usize;
9569 for (_, v) in per {
9570 t += v.len();
9571 u += v.iter().collect::<std::collections::HashSet<_>>().len();
9572 }
9573 (u / n, t / n)
9574 };
9575 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
9576 self.dspark_exp.push((tu, tt, du, dt));
9577 self.dspark_pending.push((position, props, true, 0));
9578 }
9579 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
9580 let n = self.dspark_hist.len() as f32;
9581 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
9582 let block = crate::dsv4::dspark_block();
9583 let mut at = vec![0usize; block + 1];
9584 for &k in &self.dspark_hist {
9585 at[k] += 1;
9586 }
9587 let mut surv = Vec::with_capacity(block);
9589 for i in 1..=block {
9590 let k = at[i..].iter().sum::<usize>() as f32 / n;
9591 surv.push(format!("{k:.2}"));
9592 }
9593 let distinct = self
9594 .dspark_real
9595 .iter()
9596 .collect::<std::collections::HashSet<_>>()
9597 .len();
9598 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
9599 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
9600 });
9601 let m = self.dspark_exp.len().max(1);
9602 eprintln!(
9603 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
9604 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
9605 self.dspark_hist.len(),
9606 mean + 1.0,
9607 surv.join(" ")
9608 );
9609 eprintln!(
9610 "DSpark: разных токенов {distinct} из {} (вырожденность), \
9611 эксперты ствол {}/{} на слой за {block} токенов, \
9612 черновик {}/{} за блок, draft {:.2} мс/блок",
9613 self.dspark_real.len(),
9614 tu / m,
9615 tt / m,
9616 du / m,
9617 dt / m,
9618 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
9619 );
9620 }
9621 }
9622
9623 fn forward_layers_upto(
9624 &mut self,
9625 hidden: &[f32],
9626 position: usize,
9627 task_mask: Option<&TaskMask>,
9628 upto: Option<usize>,
9629 ) -> Vec<f32> {
9630 if let Some(plan) = self.gpu_plan.clone() {
9636 if upto.is_none() && plan.len() > 1 {
9637 let mut h = hidden.to_vec();
9638 for &(dev, from, upto_incl) in plan.iter() {
9639 h = crate::gpu::with_device(dev, || {
9640 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
9641 });
9642 }
9643 return h;
9644 }
9645 }
9646 self.forward_layers_span(hidden, position, task_mask, 0, upto)
9647 }
9648
9649 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
9654 self.set_gpu_plan_at(devices, None)
9655 }
9656
9657 pub fn set_gpu_plan_at(
9661 &mut self,
9662 devices: Option<&[usize]>,
9663 at: Option<usize>,
9664 ) -> Result<(), String> {
9665 let Some(devs) = devices.filter(|d| d.len() > 1) else {
9666 self.gpu_plan = None;
9667 return Ok(());
9668 };
9669 self.split_supported()?;
9670 let n = self.num_layers;
9671 if devs.len() > n {
9672 return Err(format!("{} devices for {n} layers", devs.len()));
9673 }
9674 if let Some(k) = at {
9675 if k == 0 || k >= n {
9676 return Err(format!("split at {k}: the model has {n} layers"));
9677 }
9678 if devs.len() == 2 {
9679 self.gpu_plan = Some(std::sync::Arc::new(vec![
9680 (devs[0], 0, k - 1),
9681 (devs[1], k, n - 1),
9682 ]));
9683 return Ok(());
9684 }
9685 return Err(format!(
9686 "an explicit split point takes exactly 2 devices, got {}",
9687 devs.len()
9688 ));
9689 }
9690 let per = n.div_ceil(devs.len());
9691 let mut plan = Vec::with_capacity(devs.len());
9692 let mut from = 0usize;
9693 for &d in devs {
9694 if from >= n {
9695 break;
9696 }
9697 let upto = (from + per - 1).min(n - 1);
9698 plan.push((d, from, upto));
9699 from = upto + 1;
9700 }
9701 self.gpu_plan = Some(std::sync::Arc::new(plan));
9702 Ok(())
9703 }
9704
9705 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
9707 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
9708 }
9709
9710 fn forward_layers_span(
9716 &mut self,
9717 hidden: &[f32],
9718 position: usize,
9719 task_mask: Option<&TaskMask>,
9720 from: usize,
9721 upto: Option<usize>,
9722 ) -> Vec<f32> {
9723 debug_assert!(
9724 from == 0
9725 || (self.dsv4.is_none()
9726 && self.dsv41.is_none()
9727 && self.qwen4_exp.is_none()
9728 && self.g3n.is_none())
9729 );
9730 if let Some(b) = &mut self.qwen4_exp {
9731 let _ = (task_mask, upto);
9732 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9733 let mut logits = Vec::new();
9734 crate::qwen4_exp::forward_token(
9735 &b.0,
9736 &b.1,
9737 &b.2,
9738 &mut b.3,
9739 token_id,
9740 position,
9741 &self.inv_freq,
9742 self.pool.as_deref(),
9743 &mut logits,
9744 true,
9745 );
9746 self.graph_logits = Some(logits);
9747 return vec![0.0; self.hidden_size];
9748 }
9749 if let Some(b) = &mut self.dsv4 {
9755 let _ = (task_mask, upto);
9756 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9757 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
9758 st.pos = position;
9759 let mut logits = Vec::new();
9760 crate::dsv4::forward_token(
9761 g,
9762 layers,
9763 &cfg,
9764 st,
9765 token_id,
9766 &self.inv_freq,
9767 self.pool.as_deref(),
9768 &mut logits,
9769 );
9770 self.graph_logits = Some(logits);
9771 self.dspark_probe(position, token_id);
9772 return vec![0.0; self.hidden_size];
9775 }
9776 if let Some(b) = &mut self.dsv41 {
9778 let _ = (task_mask, upto);
9779 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9780 let mut logits = Vec::new();
9781 crate::dsv41::forward_token(
9782 &b.0,
9783 &b.1,
9784 &b.2,
9785 &mut b.3,
9786 token_id,
9787 position,
9788 self.pool.as_deref(),
9789 &mut logits,
9790 );
9791 self.graph_logits = Some(logits);
9792 return vec![0.0; self.hidden_size];
9793 }
9794 if let Some(b) = &self.g3n {
9797 let _ = (task_mask, upto);
9798 return crate::g3n::g3n_forward(
9799 &b.0,
9800 &b.1,
9801 hidden,
9802 position,
9803 &mut self.kv_cache.layers,
9804 self.num_heads,
9805 self.num_kv_heads,
9806 self.head_dim,
9807 self.pool.as_deref(),
9808 );
9809 }
9810 let mut h = hidden.to_vec();
9811 let (nh, _nkv, _hd, hs, _rd, eps) = (
9814 self.num_heads,
9815 self.num_kv_heads,
9816 self.head_dim,
9817 self.hidden_size,
9818 self.rotary_dim,
9819 self.rms_eps,
9820 );
9821 let pool = self.pool.clone();
9822 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
9834 let graph_on = match graph_env.as_deref() {
9835 Some("0") => false,
9836 Some("prefill") => false, Some(_) => true,
9838 None => crate::gpu::wgpu_graph_default(),
9844 };
9845 let graph_trusted =
9846 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
9847 let race_eligible = graph_on
9848 && upto.is_none()
9849 && task_mask.is_none()
9850 && from == 0
9851 && !crate::gpu::graph_unsupported();
9852 let mut tail_start = 0usize;
9853 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
9854 let t_graph = std::time::Instant::now();
9855 let mut lg = Vec::new();
9856 let mut gl = 0usize;
9857 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
9858 let declined = built.is_none();
9859 let built = match built {
9860 Some(Ok(hh)) => Some(hh),
9861 Some(Err(())) => {
9862 self.clear_sequence_state();
9866 self.graph_failed
9867 .store(true, std::sync::atomic::Ordering::Relaxed);
9868 self.cancel
9869 .store(true, std::sync::atomic::Ordering::Relaxed);
9870 tracing::error!("token graph failed after admission; sequence state cleared");
9871 return vec![0.0; self.hidden_size];
9872 }
9873 None => None,
9874 };
9875 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
9880 crate::gpu::graph_mark_unsupported();
9881 }
9882 graph_note(built.is_some(), gl, self.num_layers);
9883 if let Some(hh) = built {
9884 let dur = t_graph.elapsed();
9885 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9886 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
9887 }
9888 if gl > 0 && gl < self.num_layers {
9889 h = hh;
9895 tail_start = gl;
9896 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
9897 if !graph_trusted {
9898 crate::gpu::graph_race_record(true, dur);
9899 }
9900 if !lg.is_empty() {
9901 lg.resize(self.vocab_size, 0.0);
9904 if let Some(c) = self.final_softcap {
9905 for l in lg.iter_mut() {
9906 *l = c * (*l / c).tanh();
9907 }
9908 }
9909 self.graph_logits = Some(lg);
9910 }
9911 return hh;
9912 }
9913 }
9919 }
9920 let span = from > 0 || upto.is_some();
9944 if span && graph_on && task_mask.is_none() && graph_trusted {
9945 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
9946 let mut lg = Vec::new();
9947 let mut gl = 0usize;
9948 let span_res =
9949 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
9950 let span_res = match span_res {
9951 Some(Ok(hh)) => Some(hh),
9952 Some(Err(())) => {
9953 self.clear_sequence_state();
9954 self.graph_failed
9955 .store(true, std::sync::atomic::Ordering::Relaxed);
9956 self.cancel
9957 .store(true, std::sync::atomic::Ordering::Relaxed);
9958 tracing::error!(
9959 "span token graph failed after admission; sequence state cleared"
9960 );
9961 return vec![0.0; self.hidden_size];
9962 }
9963 None => None,
9964 };
9965 graph_note(span_res.is_some(), gl, upto_excl - from);
9966 if std::env::var("CMF_GPU_DEBUG").is_ok() {
9967 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
9971 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
9972 eprintln!(
9973 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
9974 upto_excl - from,
9975 span_res.is_some()
9976 );
9977 }
9978 }
9979 if let Some(hh) = span_res {
9980 if gl == upto_excl - from {
9981 if !lg.is_empty() {
9982 lg.resize(self.vocab_size, 0.0);
9983 if let Some(c) = self.final_softcap {
9984 for l in lg.iter_mut() {
9985 *l = c * (*l / c).tanh();
9986 }
9987 }
9988 self.graph_logits = Some(lg);
9989 }
9990 crate::gpu::set_layer(-1);
9991 return hh;
9992 }
9993 h = hh;
9995 tail_start = from + gl;
9996 }
9997 }
9998 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
9999
10000 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
10006 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10007
10008 #[cfg(target_os = "macos")]
10009 let mut gpu_skip_until = 0usize;
10010 for li in tail_start.max(from)..self.num_layers {
10011 let _capacity_tail = automatic_gpu_prefix
10012 .filter(|&prefix| li >= prefix)
10013 .map(|_| crate::gpu::enter_cpu_scope());
10014 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
10016 if li > u {
10017 break;
10018 }
10019 }
10020 if let Some(mask) = task_mask {
10021 if !mask.layer_alive(li) {
10022 continue; }
10024 }
10025 #[cfg(target_os = "macos")]
10029 {
10030 if li < gpu_skip_until {
10031 continue;
10032 }
10033 if task_mask.is_none() {
10034 let end = self.q1_graph_gpu(li, upto, position, &mut h);
10035 if end > li {
10036 gpu_skip_until = end;
10037 if self.is_loop_end(end - 1) && end < self.num_layers {
10040 h = inference::rms_norm(
10041 &h,
10042 &self.weights.final_norm,
10043 self.rms_eps,
10044 self.norm_style,
10045 );
10046 }
10047 continue;
10048 }
10049 }
10050 }
10051
10052 let lw = &self.weights.layers[self.phys_layer(li)];
10053 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10054 if tp.parse::<usize>().ok() == Some(position) {
10055 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
10056 eprintln!(
10057 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10058 h[0], h[1]
10059 );
10060 }
10061 }
10062 inference::rms_norm_into(
10065 &h,
10066 &lw.input_norm,
10067 self.rms_eps,
10068 self.norm_style,
10069 &mut self.ws.n1,
10070 );
10071
10072 let attn_out = match &lw.attn {
10073 AttnKind::Mla(w) => {
10074 let inv_freq_l = self.layer_inv_freq(li);
10075 let rs = self.layer_rope_scale(li);
10076 let eps = self.rms_eps;
10077 let pool = self.pool.clone();
10078 mla_attention(
10079 w,
10080 &self.ws.n1,
10081 &mut self.kv_cache.layers[li],
10082 position,
10083 &inv_freq_l,
10084 rs,
10085 eps,
10086 pool.as_deref(),
10087 )
10088 }
10089 AttnKind::Linear(w) => {
10090 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
10091 vmf_phase_forward(
10092 &self.ws.n1,
10093 w,
10094 &cfg,
10095 &mut self.kv_cache.layers[li].linear_state,
10096 self.pool.as_deref(),
10097 )
10098 }
10099 AttnKind::Kda(w) => {
10100 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10101 crate::linear_core::kda_forward(
10102 &self.ws.n1,
10103 w,
10104 &cfg,
10105 &mut self.kv_cache.layers[li].linear_state,
10106 self.pool.as_deref(),
10107 )
10108 }
10109 AttnKind::LinearGdn(w) => {
10110 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10111 gdn_forward(
10112 &self.ws.n1,
10113 w,
10114 &cfg,
10115 &mut self.kv_cache.layers[li].linear_state,
10116 self.pool.as_deref(),
10117 )
10118 }
10119 AttnKind::ShortConv(w) => {
10120 let cfg = self
10121 .short_conv_cfg
10122 .expect("short-conv layer without short_conv_cfg");
10123 short_conv_forward(
10124 &self.ws.n1,
10125 w,
10126 &cfg,
10127 &mut self.kv_cache.layers[li].linear_state,
10128 self.pool.as_deref(),
10129 )
10130 }
10131 AttnKind::Full {
10132 wq,
10133 wk,
10134 wv,
10135 wo,
10136 q_norm,
10137 k_norm,
10138 output_gate,
10139 softplus_gate,
10140 bias,
10141 } if self.kv_cache.layers[li].o1_sealed() => {
10142 let inv_freq_l = self.layer_inv_freq(li);
10145 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10146 let cfg = QwenAttnCfg {
10147 num_heads: self.layer_num_heads(li),
10148 num_kv_heads: nkv_l,
10149 head_dim: hd_l,
10150 hidden_size: hs,
10151 position,
10152 inv_freq: &inv_freq_l,
10153 rotary_dim: rd_l,
10154 scale: self.attn_scale,
10155 softcap: self.attn_softcap,
10156 window: None,
10157 v_norm: self.attn_v_norm,
10158 q_norm: q_norm.as_deref(),
10159 k_norm: k_norm.as_deref(),
10160 output_gate: *output_gate,
10161 softplus_gate: softplus_gate
10162 .as_ref()
10163 .map(|(gate, per_head)| (gate, *per_head)),
10164 rope_scale: self.layer_rope_scale(li),
10165 bias: bias
10166 .as_ref()
10167 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10168 rms_eps: eps,
10169 norm_style: self.norm_style,
10170 pool: pool.as_deref(),
10171 };
10172 attention::qwen_attention_nystrom(
10173 &self.ws.n1,
10174 wq,
10175 wk,
10176 wv,
10177 wo,
10178 &mut self.kv_cache.layers[li],
10179 &cfg,
10180 )
10181 }
10182 AttnKind::Full {
10183 wq,
10184 wk,
10185 wv,
10186 wo,
10187 q_norm,
10188 k_norm,
10189 output_gate,
10190 softplus_gate,
10191 bias,
10192 } => 'attn: {
10193 if graph_on
10196 && !*output_gate
10197 && softplus_gate.is_none()
10198 && self.attention_heads_per_layer.is_none()
10199 && bias.is_none()
10200 && task_mask.is_none()
10201 {
10202 let inv_freq_l = self.layer_inv_freq(li);
10203 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10204 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10205 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
10206 wq.mapped_q1(),
10207 wk.mapped_q1(),
10208 wv.mapped_q1(),
10209 wo.mapped_q1(),
10210 ) {
10211 let gm = gm.clone();
10212 let mut out = vec![0f32; hs];
10213 let cache = &self.kv_cache.layers[li];
10214 if crate::gpu::attn_dropin(
10215 &gm,
10216 self.graph_kv_id,
10217 li,
10218 &self.ws.n1,
10219 qi,
10220 ki,
10221 vi,
10222 oi,
10223 q_norm.as_deref(),
10224 k_norm.as_deref(),
10225 &inv_freq_l,
10226 nh,
10227 nkv_l,
10228 hd_l,
10229 rd_l,
10230 hs,
10231 position,
10232 self.kv_cache.max_seq_len,
10233 gemma,
10234 eps as f32,
10235 cache.k_heads(),
10236 cache.v_heads(),
10237 &mut out,
10238 ) {
10239 break 'attn out;
10240 }
10241 }
10242 }
10243 let masked = task_mask
10244 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
10245 .unwrap_or(false);
10246 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
10247 match (masked, f32_view) {
10248 (true, (Some(q), Some(k), Some(v), Some(o))) => {
10251 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
10252 attention::multi_head_attention(
10253 &self.ws.n1,
10254 q,
10255 k,
10256 v,
10257 o,
10258 &mut self.kv_cache.layers[li],
10259 self.num_heads,
10260 self.num_kv_heads,
10261 self.head_dim,
10262 self.hidden_size,
10263 position,
10264 &active_heads,
10265 &self.inv_freq,
10266 )
10267 }
10268 (masked, _) => {
10269 if masked {
10270 tracing::warn!(
10271 "layer {li}: head mask on quantized weights not \
10272 supported yet — executing dense"
10273 );
10274 }
10275 let inv_freq_l = self.layer_inv_freq(li);
10276 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10277 let cfg = QwenAttnCfg {
10278 num_heads: self.layer_num_heads(li),
10279 num_kv_heads: nkv_l,
10280 head_dim: hd_l,
10281 hidden_size: hs,
10282 position,
10283 inv_freq: &inv_freq_l,
10284 rotary_dim: rd_l,
10285 scale: self.attn_scale,
10286 softcap: self.attn_softcap,
10287 window: self.layer_window(li),
10288 v_norm: self.attn_v_norm,
10289 q_norm: q_norm.as_deref(),
10290 k_norm: k_norm.as_deref(),
10291 output_gate: *output_gate,
10292 softplus_gate: softplus_gate
10293 .as_ref()
10294 .map(|(gate, per_head)| (gate, *per_head)),
10295 rope_scale: self.layer_rope_scale(li),
10296 bias: bias
10297 .as_ref()
10298 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10299 rms_eps: eps,
10300 norm_style: self.norm_style,
10301 pool: pool.as_deref(),
10302 };
10303 attention::qwen_attention(
10304 &self.ws.n1,
10305 wq,
10306 wk,
10307 wv,
10308 wo,
10309 &mut self.kv_cache.layers[li],
10310 &cfg,
10311 )
10312 }
10313 }
10314 }
10315 };
10316 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
10319 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
10320 None => attn_out,
10321 };
10322 let lw = &self.weights.layers[self.phys_layer(li)];
10323 inference::add_rmsnorm_fused_into(
10324 &mut h,
10325 &attn_out,
10326 &lw.post_norm,
10327 self.rms_eps,
10328 self.norm_style,
10329 &mut self.ws.p1,
10330 );
10331 let mut attn_out = attn_out;
10332 attention::recycle_buf(&mut attn_out);
10333 let post_normed = &self.ws.p1;
10334
10335 let ffn_masked = task_mask
10336 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
10337 .unwrap_or(false);
10338 let ffn_out = match (ffn_masked, &lw.ffn) {
10350 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
10354 let row = task_mask
10355 .and_then(|tm| tm.ffn_masks.get(li))
10356 .map(|v| v.as_slice());
10357 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
10358 }
10359 (true, FfnKind::Dense(d)) => {
10360 let tm = task_mask.unwrap();
10361 let alive = tm.ffn_active_count(li);
10362 let deep = alive * 2 <= self.intermediate_size;
10363 if deep && d.down_proj.sparse_col_ok() {
10364 let active = tm.ffn_active_indices(li);
10365 sparse_ffn_quant(
10366 d,
10367 post_normed,
10368 &active,
10369 self.hidden_size,
10370 self.pool.as_deref(),
10371 )
10372 } else if deep
10373 && let (Some(g), Some(u), Some(dn)) = (
10374 d.gate_proj.as_f32(),
10375 d.up_proj.as_f32(),
10376 d.down_proj.as_f32(),
10377 )
10378 {
10379 let active = tm.ffn_active_indices(li);
10380 inference::sparse_ffn_forward(
10381 post_normed,
10382 g,
10383 u,
10384 dn,
10385 self.hidden_size,
10386 self.intermediate_size,
10387 &active,
10388 self.pool.as_deref(),
10389 )
10390 } else {
10391 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
10392 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
10393 }
10394 }
10395 (true, FfnKind::Moe(m)) => {
10396 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
10400 ffn_forward(
10401 &lw.ffn,
10402 post_normed,
10403 self.pool.as_deref(),
10404 allowed.as_deref(),
10405 )
10406 }
10407 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
10408 dm,
10409 post_normed,
10410 &h,
10411 self.rms_eps,
10412 self.norm_style,
10413 self.pool.as_deref(),
10414 ),
10415 (false, _) => match &lw.ffn {
10416 FfnKind::DenseMoe(dm) => dense_moe_ffn(
10417 dm,
10418 post_normed,
10419 &h,
10420 self.rms_eps,
10421 self.norm_style,
10422 self.pool.as_deref(),
10423 ),
10424 _ => {
10425 let allowed = match (&lw.ffn, task_mask) {
10426 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
10427 _ => None,
10428 };
10429 ffn_forward(
10430 &lw.ffn,
10431 post_normed,
10432 self.pool.as_deref(),
10433 allowed.as_deref(),
10434 )
10435 }
10436 },
10437 };
10438 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
10439 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
10440 None => ffn_out,
10441 };
10442 for (i, &f) in ffn_out.iter().enumerate() {
10443 h[i] += f;
10444 }
10445 let mut ffn_out = ffn_out;
10446 attention::recycle_buf(&mut ffn_out);
10447
10448 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
10450 for v in h.iter_mut() {
10451 *v *= sc;
10452 }
10453 }
10454
10455 if self.is_loop_end(li) && li + 1 < self.num_layers {
10458 h = inference::rms_norm(
10459 &h,
10460 &self.weights.final_norm,
10461 self.rms_eps,
10462 self.norm_style,
10463 );
10464 }
10465
10466 if self.dyn_phi_layer == Some(li) {
10470 self.update_dyn_phi(&h);
10471 }
10472 }
10473 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
10475 crate::gpu::graph_race_record(false, t.elapsed());
10476 }
10477
10478 h
10479 }
10480
10481 fn update_dyn_phi(&mut self, h: &[f32]) {
10484 const A: f32 = 0.2;
10485 if self.dyn_phi_ema.len() != h.len() {
10486 self.dyn_phi_ema = vec![0.0; h.len()];
10487 self.dyn_phi_seen = 0;
10488 }
10489 if self.dyn_phi_seen == 0 {
10490 self.dyn_phi_ema.copy_from_slice(h);
10491 } else {
10492 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
10493 *e = (1.0 - A) * *e + A * v;
10494 }
10495 }
10496 self.dyn_phi_seen += 1;
10497 }
10498
10499 pub fn dyn_phi(&self) -> &[f32] {
10501 &self.dyn_phi_ema
10502 }
10503
10504 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
10506 self.dyn_phi_layer = layer;
10507 self.dyn_phi_ema.clear();
10508 self.dyn_phi_seen = 0;
10509 }
10510
10511 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
10513 let Some(model) = &self.model else {
10514 return Vec::new();
10515 };
10516 model
10517 .header
10518 .skills
10519 .iter()
10520 .enumerate()
10521 .filter_map(|(i, sk)| {
10522 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
10523 let sel = sk.selection.as_ref()?;
10524 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
10525 })
10526 .collect()
10527 }
10528
10529 pub fn active_skill(&self) -> Option<usize> {
10531 self.dyn_active
10532 }
10533
10534 pub fn enable_dynamic_routing(&mut self) -> usize {
10539 use crate::swarm::{DynRouter, RoutableSkill};
10540 let Some(model) = self.model.clone() else {
10541 return 0;
10542 };
10543 if self.dyn_blend_loaded {
10546 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
10547 return 0;
10548 }
10549 if let Some(a) = self.dyn_active {
10553 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
10554 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
10555 return 0;
10556 }
10557 }
10558 let hidden = self.hidden_size;
10559 let mut skills = Vec::new();
10560 for (idx, id, _phi) in self.dynamic_skills() {
10561 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
10562 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
10563 skills.push(rs);
10564 }
10565 }
10566 }
10567 if skills.is_empty() {
10568 return 0;
10569 }
10570 let phi = skills[0].phi_layer;
10572 if skills.iter().any(|s| s.phi_layer != phi) {
10573 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
10574 }
10575 let n = skills.len();
10576 self.set_dyn_phi_layer(Some(phi));
10577 self.dyn_router = Some(DynRouter::new(skills));
10578 n
10579 }
10580
10581 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
10583 self.dyn_router
10584 .as_ref()
10585 .map(|r| r.switches.clone())
10586 .unwrap_or_default()
10587 }
10588
10589 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
10592 let rows = self.weights.lm_head.rows();
10593 let mut logits = attention::take_buf(rows.min(self.vocab_size));
10594 self.weights
10595 .lm_head
10596 .matvec(hidden, &mut logits, self.pool.as_deref());
10597 logits.resize(self.vocab_size, 0.0);
10598 if let Some(m) = self.logit_multiplier {
10599 for l in logits.iter_mut() {
10600 *l *= m;
10601 }
10602 }
10603 if let Some(c) = self.final_softcap {
10604 for l in logits.iter_mut() {
10605 *l = c * (*l / c).tanh();
10606 }
10607 }
10608 if let Some(cm) = self.head_clusters.as_ref() {
10609 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
10610 }
10611 logits
10612 }
10613
10614 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
10617 let h = hidden.len();
10618 let ncl = cm.len() / h.max(1);
10619 if ncl == 0 || logits.len() % ncl != 0 {
10620 return;
10621 }
10622 let cs = logits.len() / ncl;
10623 let mut lc = vec![0.0f32; ncl];
10625 for c in 0..ncl {
10626 let row = &cm[c * h..(c + 1) * h];
10627 let mut s = 0.0f32;
10628 for j in 0..h {
10629 s += row[j] * hidden[j];
10630 }
10631 lc[c] = s;
10632 }
10633 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10634 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
10635 for c in 0..ncl {
10636 let blk = &mut logits[c * cs..(c + 1) * cs];
10637 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10638 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
10639 let add = lc[c] - lse - bl;
10640 for v in blk.iter_mut() {
10641 *v += add;
10642 }
10643 }
10644 }
10645
10646 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
10651 self.clear_sequence_state();
10652 crate::gpu::graph_race_begin_generation();
10656 if task_mask.is_none() {
10657 self.o1_begin();
10658 }
10659 let mut hidden = vec![0.0f32; self.hidden_size];
10660 for (pos, &id) in ids.iter().enumerate() {
10661 let emb = self.embed_single(id);
10662 hidden = self.forward_layers(&emb, pos, task_mask);
10663 }
10664 if let Err(err) = self.o1_seal_checked() {
10665 self.o1_fail(err);
10666 }
10667 inference::rms_norm_into(
10668 &hidden,
10669 &self.weights.final_norm,
10670 self.rms_eps,
10671 self.norm_style,
10672 &mut self.ws.n1,
10673 );
10674 self.lm_head_forward(&self.ws.n1)
10675 }
10676}
10677
10678pub fn create_test_pipeline(
10680 hidden_size: usize,
10681 intermediate_size: usize,
10682 num_heads: usize,
10683 num_kv_heads: usize,
10684 head_dim: usize,
10685 num_layers: usize,
10686 vocab_size: usize,
10687) -> Pipeline {
10688 let synth = |n: usize, salt: usize| -> Vec<f32> {
10691 (0..n)
10692 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
10693 .collect()
10694 };
10695 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
10696 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
10697 };
10698 let layer_weights: Vec<LayerWeights> = (0..num_layers)
10699 .map(|li| LayerWeights {
10700 input_norm: vec![1.0; hidden_size],
10701 post_norm: vec![1.0; hidden_size],
10702 attn_out_norm: None,
10703 ffn_out_norm: None,
10704 layer_scale: None,
10705 ffn: FfnKind::Dense(DenseFfn {
10706 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
10707 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
10708 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
10709 act: Act::Silu,
10710 down_t: None,
10711 segs: Vec::new(),
10712 }),
10713 attn: AttnKind::Full {
10714 bias: None,
10715 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
10716 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
10717 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
10718 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
10719 q_norm: None,
10720 k_norm: None,
10721 output_gate: false,
10722 softplus_gate: None,
10723 },
10724 })
10725 .collect();
10726
10727 Pipeline::new(
10728 Tokenizer::byte_level(),
10729 PipelineWeights {
10730 embed_tokens: qt(vocab_size, hidden_size, 100),
10731 layers: layer_weights,
10732 lm_head: qt(vocab_size, hidden_size, 200),
10733 final_norm: vec![1.0; hidden_size],
10734 },
10735 hidden_size,
10736 intermediate_size,
10737 num_heads,
10738 num_kv_heads,
10739 head_dim,
10740 num_layers,
10741 num_layers, false, vocab_size,
10744 1e-6,
10745 10_000.0,
10746 NormStyle::Qwen,
10747 4096,
10748 SamplerConfig {
10749 seed: Some(42),
10750 ..Default::default()
10751 },
10752 )
10753}
10754
10755#[inline]
10760fn mask_bit(row: &[u8], j: usize) -> bool {
10761 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
10762}
10763
10764fn mask_gain() -> f32 {
10775 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
10776 *G.get_or_init(|| {
10777 std::env::var("CMF_FFN_MASK_GAIN")
10778 .ok()
10779 .and_then(|v| v.parse().ok())
10780 .unwrap_or(1.0)
10781 })
10782}
10783
10784fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
10785 let fill = meanfill().and_then(|(i, v)| {
10788 let li = crate::gpu::cur_layer();
10789 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
10790 });
10791 for r in 0..rows {
10792 let base = r * inter;
10793 for (bi, &byte) in row.iter().enumerate() {
10794 if byte == 0xFF {
10795 continue;
10796 }
10797 let j0 = bi * 8;
10798 for bit in 0..8 {
10799 let j = j0 + bit;
10800 if j < inter && byte & (1 << bit) == 0 {
10801 g[base + j] = fill.map_or(0.0, |f| f[j]);
10802 }
10803 }
10804 }
10805 }
10806 let gain = mask_gain();
10807 if gain != 1.0 {
10808 for v in g[..rows * inter].iter_mut() {
10809 *v *= gain;
10810 }
10811 }
10812}
10813
10814#[inline]
10816fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
10817 row.is_none_or(|r| mask_bit(r, i))
10818}
10819
10820fn all_bits_on(row: &[u8], n: usize) -> bool {
10823 (0..n).all(|i| mask_bit(row, i))
10824}
10825
10826fn tube_topk() -> usize {
10834 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10835 *K.get_or_init(|| {
10836 std::env::var("CMF_TUBE_TOPK")
10837 .ok()
10838 .and_then(|v| v.parse().ok())
10839 .unwrap_or(0)
10840 })
10841}
10842
10843fn tube_score_oracle() -> bool {
10844 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10845 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
10846}
10847
10848fn tube_ffn_routed(
10855 d: &DenseFfn,
10856 xs: &[f32],
10857 b: usize,
10858 pool: Option<&Pool>,
10859 mask_row: Option<&[u8]>,
10860 k: usize,
10861) -> Vec<f32> {
10862 let hidden = d.down_proj.rows();
10863 let core = d.gate_proj.rows();
10864 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
10865 let mut out = match (b, core_full, mask_row) {
10866 (1, true, _) => dense_ffn(d, xs, pool),
10867 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
10868 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
10869 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
10870 };
10871 let cand: Vec<usize> = (0..d.segs.len())
10872 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
10873 .collect();
10874 if cand.is_empty() {
10875 return out;
10876 }
10877 let oracle = tube_score_oracle();
10881 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
10882 let mut scores = vec![0f32; b * cand.len()];
10883 for (ci, &i) in cand.iter().enumerate() {
10884 let seg = &d.segs[i];
10885 let w = seg.width;
10886 let mut g = vec![0.0f32; b * w];
10887 if b == 1 {
10888 seg.gate.matvec(xs, &mut g, pool);
10889 } else {
10890 seg.gate.matmat(xs, b, &mut g, pool);
10891 }
10892 for v in g.iter_mut() {
10893 *v = Act::Silu.combine(*v, 1.0);
10894 }
10895 if !oracle {
10896 for t in 0..b {
10897 scores[t * cand.len() + ci] =
10898 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
10899 }
10900 }
10901 if oracle || b > 1 {
10902 let mut u = vec![0.0f32; b * w];
10903 if b == 1 {
10904 seg.up.matvec(xs, &mut u, pool);
10905 } else {
10906 seg.up.matmat(xs, b, &mut u, pool);
10907 }
10908 for (a, &v) in g.iter_mut().zip(u.iter()) {
10909 *a *= v;
10910 }
10911 if oracle {
10912 for t in 0..b {
10913 scores[t * cand.len() + ci] =
10914 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
10915 }
10916 }
10917 }
10918 acts.push(g);
10919 }
10920 let keep = k.min(cand.len());
10922 let mut scratch: Vec<f32> = Vec::new();
10923 for t in 0..b {
10924 let mut sc: Vec<(f32, usize)> = (0..cand.len())
10925 .map(|ci| (scores[t * cand.len() + ci], ci))
10926 .collect();
10927 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
10928 let mut alive = vec![false; cand.len()];
10929 for &(_, ci) in sc.iter().take(keep) {
10930 alive[ci] = true;
10931 }
10932 if b > 1 {
10933 for (ci, a) in acts.iter_mut().enumerate() {
10934 if !alive[ci] {
10935 let w = d.segs[cand[ci]].width;
10936 a[t * w..(t + 1) * w].fill(0.0);
10937 }
10938 }
10939 } else {
10940 for (ci, &i) in cand.iter().enumerate() {
10944 if !alive[ci] {
10945 continue;
10946 }
10947 let seg = &d.segs[i];
10948 let w = seg.width;
10949 let g = &mut acts[ci];
10950 if !tube_score_oracle() {
10951 scratch.clear();
10952 scratch.resize(w, 0.0);
10953 seg.up.matvec(xs, &mut scratch, pool);
10954 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
10955 *a *= v;
10956 }
10957 }
10958 let mut acc = vec![0.0f32; hidden];
10959 seg.down.matvec(g, &mut acc, pool);
10960 for (o, a) in out.iter_mut().zip(&acc) {
10961 *o += *a;
10962 }
10963 }
10964 }
10965 }
10966 if b > 1 {
10967 for (ci, &i) in cand.iter().enumerate() {
10968 let seg = &d.segs[i];
10969 let mut acc = vec![0.0f32; b * hidden];
10970 seg.down.matmat(&acts[ci], b, &mut acc, pool);
10971 for (o, a) in out.iter_mut().zip(&acc) {
10972 *o += *a;
10973 }
10974 }
10975 }
10976 out
10977}
10978
10979fn tube_ffn(
10985 d: &DenseFfn,
10986 xs: &[f32],
10987 b: usize,
10988 pool: Option<&Pool>,
10989 mask_row: Option<&[u8]>,
10990) -> Vec<f32> {
10991 if tube_topk() > 0 {
10992 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
10993 }
10994 let hidden = d.down_proj.rows();
10995 let core = d.gate_proj.rows();
10996 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
10997 let mut out = match (b, core_full, mask_row) {
10998 (1, true, _) => dense_ffn(d, xs, pool),
10999 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
11000 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
11001 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
11002 };
11003 TUBE_SCRATCH.with(|sc| {
11004 let mut sc = sc.borrow_mut();
11005 let [g, u, acc] = &mut *sc;
11006 for seg in &d.segs {
11007 if !tube_bit(mask_row, seg.start) {
11008 continue;
11009 }
11010 let w = seg.width;
11011 g.resize(b * w, 0.0);
11012 if b == 1
11013 && d.act == Act::Silu
11014 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
11015 {
11016 } else {
11018 u.resize(b * w, 0.0);
11019 if b == 1 {
11020 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
11021 } else {
11022 seg.gate.matmat(xs, b, g, pool);
11023 seg.up.matmat(xs, b, u, pool);
11024 }
11025 for i in 0..b * w {
11026 g[i] = d.act.combine(g[i], u[i]);
11027 }
11028 }
11029 acc.resize(b * hidden, 0.0);
11030 acc.fill(0.0);
11031 if b == 1 {
11032 seg.down.matvec(g, acc, pool);
11033 } else {
11034 seg.down.matmat(g, b, acc, pool);
11035 }
11036 for (o, a) in out.iter_mut().zip(acc.iter()) {
11037 *o += *a;
11038 }
11039 }
11040 out
11041 })
11042}
11043
11044thread_local! {
11045 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
11049 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
11050}
11051
11052fn dense_ffn_batch(
11053 d: &DenseFfn,
11054 xs: &[f32],
11055 b: usize,
11056 pool: Option<&Pool>,
11057 mask_row: Option<&[u8]>,
11058) -> Vec<f32> {
11059 let inter = d.gate_proj.rows();
11060 let hidden = d.down_proj.rows();
11061 if mask_row.is_none()
11069 && d.act == Act::Silu
11070 && b >= 32
11071 && crate::gpu::enabled_here()
11072 && !crate::gpu::mm_killed()
11073 && refit_dir().is_none()
11078 && !ffn_probe_active()
11083 {
11084 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11085 d.gate_proj.mapped_q4t(),
11086 d.up_proj.mapped_q4t(),
11087 d.down_proj.mapped_q4t(),
11088 ) {
11089 let mut out = vec![0.0f32; b * hidden];
11090 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11091 return out;
11092 }
11093 }
11094 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
11099 d.gate_proj.mapped_q4tp(),
11100 d.up_proj.mapped_q4tp(),
11101 d.down_proj.mapped_q4tp(),
11102 ) {
11103 let mut out = vec![0.0f32; b * hidden];
11104 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
11105 return out;
11106 }
11107 }
11108 }
11109 let mut g = vec![0.0f32; b * inter];
11110 d.gate_proj.matmat(xs, b, &mut g, pool);
11111 let mut u = vec![0.0f32; b * inter];
11112 d.up_proj.matmat(xs, b, &mut u, pool);
11113 if gate_topk() > 0 && d.act == Act::Silu {
11114 for t in 0..b {
11115 let row = &mut g[t * inter..(t + 1) * inter];
11116 for v in row.iter_mut() {
11117 *v = Act::Silu.combine(*v, 1.0);
11118 }
11119 keep_top_k(row, gate_topk());
11120 }
11121 for i in 0..b * inter {
11122 g[i] *= u[i];
11123 }
11124 } else {
11125 for i in 0..b * inter {
11126 g[i] = d.act.combine(g[i], u[i]);
11127 }
11128 }
11129 if let Some(row) = mask_row {
11130 zero_masked_cols(&mut g, b, inter, row);
11131 }
11132 if oracle_topk() > 0 {
11133 for t in 0..b {
11134 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
11135 }
11136 }
11137 let mut out = vec![0.0f32; b * hidden];
11138 d.down_proj.matmat(&g, b, &mut out, pool);
11139 if refit_dir().is_some() {
11140 let li = crate::gpu::cur_layer();
11141 if li >= 0 {
11142 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
11143 }
11144 }
11145 FFN_PROBE.with(|pr| {
11149 if let Some(acc) = pr.borrow_mut().as_mut() {
11150 let li = crate::gpu::cur_layer();
11151 if li < 0 {
11152 return;
11153 }
11154 let Some(row) = acc.get_mut(li as usize) else {
11155 return;
11156 };
11157 let sq = probe_sq();
11158 for t in 0..b {
11159 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
11160 *a += if sq {
11161 (v as f64) * (v as f64)
11162 } else {
11163 (v as f64).abs()
11164 };
11165 }
11166 }
11167 }
11168 });
11169 out
11170}
11171
11172fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
11177 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11178 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11179 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
11180 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
11181 if (!on && !dump) || b == 0 {
11182 return;
11183 }
11184 let hidden = xs.len() / b;
11185 if on {
11186 let mut acc = m.act_sq.borrow_mut();
11187 if acc.len() < hidden {
11188 acc.resize(hidden, 0.0);
11189 }
11190 for t in 0..b {
11191 let row = &xs[t * hidden..(t + 1) * hidden];
11192 for (a, &v) in acc.iter_mut().zip(row) {
11193 *a += (v as f64) * (v as f64);
11194 }
11195 }
11196 }
11197 if dump {
11198 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
11201 .ok()
11202 .and_then(|v| v.parse().ok())
11203 .unwrap_or(4096);
11204 let mut rows = m.act_rows.borrow_mut();
11205 if rows.len() < cap * hidden {
11206 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
11207 rows.extend_from_slice(&xs[..take * hidden]);
11208 }
11209 }
11210}
11211
11212#[derive(Clone, Copy)]
11215struct SendVecs(*mut Vec<f32>);
11216unsafe impl Send for SendVecs {}
11217unsafe impl Sync for SendVecs {}
11218impl SendVecs {
11219 #[inline]
11220 fn at(self, i: usize) -> *mut Vec<f32> {
11221 unsafe { self.0.add(i) }
11222 }
11223}
11224
11225fn moe_ffn_batch(
11226 m: &MoeFfn,
11227 xs: &[f32],
11228 b: usize,
11229 hidden: usize,
11230 pool: Option<&Pool>,
11231 allowed: Option<&[bool]>,
11232) -> Vec<f32> {
11233 accumulate_act(m, xs, b);
11234 let ne = m.experts.len();
11235 let mut logits = vec![0.0f32; b * ne];
11236 match &m.resonance {
11237 Some(r) => {
11238 let hdim = xs.len() / b.max(1);
11239 for bi in 0..b {
11240 r.scores(
11241 &xs[bi * hdim..(bi + 1) * hdim],
11242 &mut logits[bi * ne..(bi + 1) * ne],
11243 );
11244 }
11245 }
11246 None => m.router.matmat(xs, b, &mut logits, pool),
11247 }
11248
11249 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
11252 {
11253 let mut st = m.stats.borrow_mut();
11254 if st.len() < ne {
11255 st.resize(ne, 0);
11256 }
11257 for bi in 0..b {
11258 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
11259 for &e in &idx {
11260 st[e] += 1;
11261 assign[e].push((bi, p[e] / wsum));
11262 }
11263 }
11264 }
11265
11266 let mut out = vec![0.0f32; b * hidden];
11267 let cols = m.experts[0].gate_proj.cols();
11268 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
11269 let sb = list.len();
11270 let mut sub = vec![0.0f32; sb * cols];
11271 for (k, &(bi, _)) in list.iter().enumerate() {
11272 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11273 }
11274 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
11275 for (k, &(bi, w)) in list.iter().enumerate() {
11276 for i in 0..hidden {
11277 out[bi * hidden + i] += w * eo[k * hidden + i];
11278 }
11279 }
11280 };
11281 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
11287 if pool.is_some() && active.len() >= 8 {
11288 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
11289 {
11290 let panel_ptr = SendVecs(panels.as_mut_ptr());
11291 let experts = &m.experts;
11294 let (active_r, assign_r) = (&active, &assign);
11295 let run = |start: usize, end: usize| {
11296 for ai in start..end {
11297 let e = active_r[ai];
11298 let list = &assign_r[e];
11299 let sb = list.len();
11300 let mut sub = vec![0.0f32; sb * cols];
11301 for (k, &(bi, _)) in list.iter().enumerate() {
11302 sub[k * cols..(k + 1) * cols]
11303 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
11304 }
11305 unsafe {
11307 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
11308 }
11309 }
11310 };
11311 match pool {
11312 Some(p) => p.run_rows(active.len(), &run),
11313 None => run(0, active.len()),
11314 }
11315 }
11316 for (ai, &e) in active.iter().enumerate() {
11317 for (k, &(bi, w)) in assign[e].iter().enumerate() {
11318 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
11319 for i in 0..hidden {
11320 out[bi * hidden + i] += w * eo[i];
11321 }
11322 }
11323 }
11324 } else {
11325 for &e in &active {
11326 run_expert(&m.experts[e], &assign[e], &mut out);
11327 }
11328 }
11329 if let Some((se, gate)) = &m.shared {
11330 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
11331 let mut gl = vec![0.0f32; b];
11332 gate.matmat(xs, b, &mut gl, pool);
11333 (0..b)
11334 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
11335 .collect()
11336 } else {
11337 (0..b).map(|bi| (bi, 1.0)).collect()
11338 };
11339 run_expert(se, &all, &mut out);
11340 }
11341 out
11342}
11343
11344thread_local! {
11345 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
11349 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
11350}
11351
11352fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11354 if gate_topk() > 0
11357 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
11358 {
11359 return out;
11360 }
11361 if crate::gpu::enabled_here()
11372 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
11373 {
11374 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
11375 crate::gpu::ProbeArm::Gpu
11376 } else {
11377 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
11378 };
11379 match arm {
11380 crate::gpu::ProbeArm::Gpu => {
11381 let t0 = std::time::Instant::now();
11382 if let Some(out) = dense_ffn_gpu(d, x, pool) {
11383 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
11384 return out;
11385 }
11386 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
11390 }
11391 crate::gpu::ProbeArm::CpuTimed => {
11392 let t0 = std::time::Instant::now();
11393 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11394 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
11395 return out;
11396 }
11397 crate::gpu::ProbeArm::Cpu => {
11398 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
11399 }
11400 }
11401 }
11402 dense_ffn_cpu(d, x, pool)
11403}
11404
11405fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
11407 let inter = d.gate_proj.rows();
11408 FFN_SCRATCH.with(|s| {
11409 let mut s = s.borrow_mut();
11410 let [g, u, ..] = &mut *s;
11411 g.resize(inter, 0.0);
11412 if gate_topk() > 0 {
11415 u.resize(inter, 0.0);
11419 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11420 for i in 0..inter {
11421 g[i] = Act::Silu.combine(g[i], 1.0);
11422 }
11423 keep_top_k(g, gate_topk());
11424 for i in 0..inter {
11425 g[i] *= u[i];
11426 }
11427 } else if d.act == Act::Silu
11428 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
11429 {
11430 } else {
11432 u.resize(inter, 0.0);
11433 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11435 for i in 0..inter {
11436 g[i] = d.act.combine(g[i], u[i]);
11437 }
11438 }
11439 FFN_PROBE.with(|pr| {
11447 if let Some(acc) = pr.borrow_mut().as_mut() {
11448 let li = crate::gpu::cur_layer();
11449 if li >= 0 {
11450 if let Some(row) = acc.get_mut(li as usize) {
11451 match probe_topk() {
11452 0 if probe_sq() => {
11453 for (a, &v) in row.iter_mut().zip(g.iter()) {
11454 *a += (v as f64) * (v as f64);
11455 }
11456 }
11457 0 if probe_signed() => {
11458 for (a, &v) in row.iter_mut().zip(g.iter()) {
11459 *a += v as f64;
11460 }
11461 }
11462 0 => {
11463 for (a, &v) in row.iter_mut().zip(g.iter()) {
11464 *a += (v as f64).abs();
11465 }
11466 }
11467 k => {
11468 let n = g.len();
11469 let k = k.min(n);
11470 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
11471 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11472 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11473 });
11474 let thr = *kth;
11475 for (a, &v) in row.iter_mut().zip(g.iter()) {
11476 if v.abs() >= thr {
11477 *a += 1.0;
11478 }
11479 }
11480 }
11481 }
11482 }
11483 }
11484 }
11485 });
11486 if oracle_topk() > 0 {
11487 keep_top_k(g, oracle_topk());
11488 }
11489 {
11490 let li = crate::gpu::cur_layer();
11491 if li >= 0 {
11492 adump_row(li as usize, g);
11493 }
11494 }
11495 let mut out = attention::take_buf(d.down_proj.rows());
11496 d.down_proj.matvec(g, &mut out, pool);
11497 out
11498 })
11499}
11500
11501pub struct RefitAcc {
11514 pub support: Vec<u32>,
11515 pub gss: Vec<f32>,
11516 pub ya: Vec<f32>,
11517 pub hidden: usize,
11518 pub tokens: u64,
11519 pub buf_g: Vec<f32>,
11525 pub buf_o: Vec<f32>,
11526 pub buf_t: usize,
11527}
11528
11529type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
11533
11534static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
11535 std::sync::OnceLock::new();
11536
11537fn ffn_probe_active() -> bool {
11540 FFN_PROBE.with(|p| p.borrow().is_some())
11541}
11542
11543fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
11544 REFIT
11545 .get_or_init(|| {
11546 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
11547 (
11548 d,
11549 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
11550 )
11551 })
11552 })
11553 .as_ref()
11554}
11555
11556fn refit_accumulate(
11558 li: usize,
11559 g: &[f32],
11560 b: usize,
11561 inter: usize,
11562 out: &[f32],
11563 hidden: usize,
11564 pool: Option<&Pool>,
11565) {
11566 let Some((dir, map)) = refit_dir() else {
11567 return;
11568 };
11569 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
11570 let (from, to) = *SPAN.get_or_init(|| {
11571 let g = |k: &str, d: usize| {
11572 std::env::var(k)
11573 .ok()
11574 .and_then(|v| v.parse().ok())
11575 .unwrap_or(d)
11576 };
11577 (
11578 g("CMF_FFN_REFIT_FROM", 0),
11579 g("CMF_FFN_REFIT_TO", usize::MAX),
11580 )
11581 });
11582 if li < from || li > to {
11583 return;
11584 }
11585 let mut guard = map.lock().unwrap();
11586 let (map, shared) = &mut *guard;
11587 let acc = match map.entry(li) {
11588 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
11589 std::collections::hash_map::Entry::Vacant(e) => {
11590 let path = format!("{dir}/support.{li}.u32");
11591 let Ok(bytes) = std::fs::read(&path) else {
11592 eprintln!("refit: no {path} — layer {li} skipped");
11593 return;
11594 };
11595 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
11596 let support: Vec<u32> = bytes[4..4 + n * 4]
11597 .chunks_exact(4)
11598 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11599 .collect();
11600 eprintln!(
11601 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
11602 (n * n + hidden * n) as f64 * 4.0 / 1e6
11603 );
11604 e.insert(RefitAcc {
11605 gss: vec![0.0; n * n],
11606 ya: vec![0.0; hidden * n],
11607 buf_g: Vec::new(),
11608 buf_o: Vec::new(),
11609 buf_t: 0,
11610 support,
11611 hidden,
11612 tokens: 0,
11613 })
11614 }
11615 };
11616 let ns = acc.support.len();
11617 let cap = refit_batch();
11619 if acc.buf_g.is_empty() {
11620 acc.buf_g = vec![0.0; ns * cap];
11621 acc.buf_o = vec![0.0; hidden * cap];
11622 }
11623 let take = b.min(cap - acc.buf_t);
11624 for t in 0..take {
11625 let col = acc.buf_t + t;
11626 for (j, &n) in acc.support.iter().enumerate() {
11627 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
11628 }
11629 for h in 0..hidden {
11630 acc.buf_o[h * cap + col] = out[t * hidden + h];
11631 }
11632 }
11633 acc.buf_t += take;
11634 acc.tokens += take as u64;
11635 if acc.buf_t < cap {
11636 return;
11637 }
11638 let bt = acc.buf_t;
11639 acc.buf_t = 0;
11640 let RefitAcc {
11650 gss,
11651 ya,
11652 buf_g,
11653 buf_o,
11654 ..
11655 } = acc;
11656 let need = (ns * ns).max(hidden * ns);
11657 if shared.len() < need {
11658 shared.resize(need, 0.0);
11659 }
11660 let scratch = &mut shared[..];
11661 let _ = bt;
11662 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
11663 add_into(gss, &scratch[..ns * ns], pool);
11664 if crate::gpu::gemm_nt_f32_transient(
11665 buf_o,
11666 buf_g,
11667 &mut scratch[..hidden * ns],
11668 hidden,
11669 cap,
11670 ns,
11671 ) {
11672 add_into(ya, &scratch[..hidden * ns], pool);
11673 } else {
11674 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
11675 }
11676 } else {
11677 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
11678 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
11679 }
11680 }
11684
11685fn refit_batch() -> usize {
11687 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11688 *B.get_or_init(|| {
11689 std::env::var("CMF_FFN_REFIT_BATCH")
11690 .ok()
11691 .and_then(|v| v.parse().ok())
11692 .unwrap_or(4096)
11693 })
11694}
11695
11696fn accum_outer_t(
11699 c: &mut [f32],
11700 m: usize,
11701 n: usize,
11702 b: usize,
11703 left: &[f32],
11704 right: &[f32],
11705 pool: Option<&Pool>,
11706) {
11707 let ptr = SendMut(c.as_mut_ptr());
11708 let body = |i: usize| {
11709 let ptr = &ptr;
11710 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
11711 for t in 0..b {
11712 let a = left[i * b + t];
11713 if a == 0.0 {
11714 continue;
11715 }
11716 for (j, o) in row.iter_mut().enumerate() {
11717 *o += a * right[j * b + t];
11718 }
11719 }
11720 };
11721 match pool {
11722 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
11723 for i in s..e {
11724 body(i);
11725 }
11726 }),
11727 _ => {
11728 for i in 0..m {
11729 body(i);
11730 }
11731 }
11732 }
11733}
11734
11735fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
11738 let n = dst.len().min(src.len());
11739 match pool {
11740 Some(p) if n >= 1 << 16 => {
11741 let ptr = SendMut(dst.as_mut_ptr());
11742 let f = |s: usize, e: usize| {
11743 let ptr = &ptr;
11744 for blk in s..e {
11745 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
11746 for i in a..b {
11747 unsafe { *ptr.0.add(i) += src[i] };
11748 }
11749 }
11750 };
11751 p.run_rows(n.div_ceil(4096), &f);
11752 }
11753 _ => {
11754 for (d, v) in dst.iter_mut().zip(&src[..n]) {
11755 *d += *v;
11756 }
11757 }
11758 }
11759}
11760
11761fn accum_outer(
11766 c: &mut [f32],
11767 m: usize,
11768 n: usize,
11769 b: usize,
11770 left: &[f32],
11771 right: &[f32],
11772 pool: Option<&Pool>,
11773) {
11774 const TILE: usize = 32;
11775 let tiles = m.div_ceil(TILE);
11776 let cp = SendMut(c.as_mut_ptr());
11777 let body = |ti: usize| {
11778 let cp = &cp;
11779 let i0 = ti * TILE;
11780 let i1 = (i0 + TILE).min(m);
11781 for t in 0..b {
11782 let r = &right[t * n..t * n + n];
11783 for i in i0..i1 {
11784 let a = left[i * b + t];
11785 if a == 0.0 {
11786 continue;
11787 }
11788 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
11790 for (o, v) in row.iter_mut().zip(r) {
11791 *o += a * *v;
11792 }
11793 }
11794 }
11795 };
11796 match pool {
11797 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
11798 for ti in s..e {
11799 body(ti);
11800 }
11801 }),
11802 _ => {
11803 for ti in 0..tiles {
11804 body(ti);
11805 }
11806 }
11807 }
11808}
11809
11810pub fn refit_flush() -> usize {
11812 let Some((dir, map)) = refit_dir() else {
11813 return 0;
11814 };
11815 let guard = map.lock().unwrap();
11816 let mut n = 0;
11817 for (li, acc) in guard.0.iter() {
11818 let w = |name: &str, v: &[f32]| {
11821 let path = format!("{dir}/{name}.{li}.f32");
11822 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
11823 match std::fs::write(&path, &bytes) {
11824 Ok(()) => {}
11825 Err(e) => eprintln!(
11826 "refit: FAILED to write {path} ({} MB): {e}",
11827 bytes.len() / 1_000_000
11828 ),
11829 }
11830 };
11831 w("gss", &acc.gss);
11832 w("ya", &acc.ya);
11833 println!(
11834 "refit L{li}: {} support, {} tokens, hidden {}",
11835 acc.support.len(),
11836 acc.tokens,
11837 acc.hidden
11838 );
11839 n += 1;
11840 }
11841 n
11842}
11843
11844fn adump_row(li: usize, g: &[f32]) {
11849 use std::io::Write as _;
11850 static FILES: std::sync::OnceLock<
11851 Option<(
11852 String,
11853 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
11854 )>,
11855 > = std::sync::OnceLock::new();
11856 let Some((prefix, map)) = FILES
11857 .get_or_init(|| {
11858 std::env::var("CMF_FFN_ADUMP")
11859 .ok()
11860 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
11861 })
11862 .as_ref()
11863 else {
11864 return;
11865 };
11866 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
11869 let (from, to) = *SPAN.get_or_init(|| {
11870 let g = |k: &str, d: usize| {
11871 std::env::var(k)
11872 .ok()
11873 .and_then(|v| v.parse().ok())
11874 .unwrap_or(d)
11875 };
11876 (
11877 g("CMF_FFN_ADUMP_FROM", 0),
11878 g("CMF_FFN_ADUMP_TO", usize::MAX),
11879 )
11880 });
11881 if li < from || li > to {
11882 return;
11883 }
11884 let mut map = map.lock().unwrap();
11885 let f = map.entry(li).or_insert_with(|| {
11886 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
11887 });
11888 let mut bytes = Vec::with_capacity(g.len() * 2);
11889 for v in g {
11890 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
11891 }
11892 let _ = f.write_all(&bytes);
11893}
11894
11895fn oracle_topk() -> usize {
11901 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11902 *K.get_or_init(|| {
11903 std::env::var("CMF_FFN_ORACLE_TOPK")
11904 .ok()
11905 .and_then(|v| v.parse().ok())
11906 .unwrap_or(0)
11907 })
11908}
11909
11910fn gate_topk() -> usize {
11916 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11917 *K.get_or_init(|| {
11918 std::env::var("CMF_FFN_GATE_TOPK")
11919 .ok()
11920 .and_then(|v| v.parse().ok())
11921 .unwrap_or(0)
11922 })
11923}
11924
11925fn gate_block() -> usize {
11932 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11933 *B.get_or_init(|| {
11934 std::env::var("CMF_FFN_GATE_BLOCK")
11935 .ok()
11936 .and_then(|v| v.parse().ok())
11937 .unwrap_or(1)
11938 })
11939}
11940
11941fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
11943 let n = g.len();
11944 let nb = n.div_ceil(block);
11945 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
11946 if kb >= nb {
11947 return;
11948 }
11949 let mut score: Vec<f32> = (0..nb)
11950 .map(|b| {
11951 g[b * block..((b + 1) * block).min(n)]
11952 .iter()
11953 .map(|v| v * v)
11954 .sum::<f32>()
11955 })
11956 .collect();
11957 let mut ord = score.clone();
11958 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
11959 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11960 });
11961 let thr = *kth;
11962 for b in 0..nb {
11963 if score[b] < thr {
11964 g[b * block..((b + 1) * block).min(n)].fill(0.0);
11965 }
11966 }
11967 score.clear();
11968}
11969
11970fn keep_top_k(g: &mut [f32], k: usize) {
11972 if gate_block() > 1 {
11973 return keep_top_blocks(g, k, gate_block());
11974 }
11975 let n = g.len();
11976 if k == 0 || k >= n {
11977 return;
11978 }
11979 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
11980 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11981 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11982 });
11983 let thr = *kth;
11984 for v in g.iter_mut() {
11985 if v.abs() < thr {
11986 *v = 0.0;
11987 }
11988 }
11989}
11990
11991fn probe_sq() -> bool {
11995 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11996 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
11997}
11998
11999fn probe_signed() -> bool {
12003 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12004 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
12005}
12006
12007fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
12015 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
12016 M.get_or_init(|| {
12017 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
12018 let b = std::fs::read(&p).ok()?;
12019 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
12020 let vals: Vec<f32> = b[8..]
12021 .chunks_exact(4)
12022 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
12023 .collect();
12024 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
12025 Some((inter, vals))
12026 })
12027 .as_ref()
12028}
12029
12030fn probe_topk() -> usize {
12033 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12034 *K.get_or_init(|| {
12035 std::env::var("CMF_FFN_PROBE_TOPK")
12036 .ok()
12037 .and_then(|v| v.parse().ok())
12038 .unwrap_or(0)
12039 })
12040}
12041
12042thread_local! {
12043 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
12046 const { std::cell::RefCell::new(None) };
12047}
12048
12049fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
12062 let dt = d.down_t.as_ref()?;
12063 let inter = d.gate_proj.rows();
12064 let hidden = dt.cols();
12065 if k == 0 || k >= inter || d.act != Act::Silu {
12066 return None;
12067 }
12068 DYN_SCRATCH.with(|sc| {
12069 let mut sc = sc.borrow_mut();
12070 let DynScratch {
12071 g,
12072 mag,
12073 live,
12074 parts,
12075 } = &mut *sc;
12076 g.resize(inter, 0.0);
12077 d.gate_proj.matvec(x, g, pool);
12078 for v in g.iter_mut() {
12079 *v = inference::silu(*v);
12080 }
12081 mag.clear();
12084 mag.extend(g.iter().map(|v| v.abs()));
12085 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
12086 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
12087 });
12088 let thr = *kth;
12089 live.clear();
12090 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
12091 let mut out = vec![0.0f32; hidden];
12092 match pool {
12093 Some(p) if live.len() >= 64 => {
12094 let nw = p.n_workers() + 1;
12095 parts.clear();
12096 parts.resize(nw * hidden, 0.0);
12097 let ptr = SendMut(parts.as_mut_ptr());
12098 let n = live.len();
12099 let live_ref: &[u32] = live;
12100 let g_ref: &[f32] = g;
12101 p.run(&|w, workers| {
12102 let chunk = n.div_ceil(workers);
12103 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
12104 if s >= e {
12105 return;
12106 }
12107 WORKER_SCRATCH.with(|ws| {
12108 let mut ws = ws.borrow_mut();
12109 let [scratch, acc] = &mut *ws;
12110 scratch.resize(hidden.max(x.len()), 0.0);
12111 acc.clear();
12112 acc.resize(hidden, 0.0);
12113 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
12114 if let Some(&nx) = live_ref[s..e].get(o + 1) {
12117 d.up_proj.prefetch_row(nx as usize);
12118 dt.prefetch_row(nx as usize);
12119 }
12120 let idx = nrm as usize;
12121 let up = d.up_proj.row_dot(idx, x, scratch);
12122 let a = g_ref[idx] * up;
12123 if a != 0.0 {
12124 dt.add_row_scaled(idx, a, acc, scratch);
12125 }
12126 }
12127 for (j, v) in acc.iter().enumerate() {
12128 unsafe { *ptr.at(w * hidden + j) = *v };
12129 }
12130 });
12131 });
12132 for w in 0..nw {
12133 for (j, o) in out.iter_mut().enumerate() {
12134 *o += parts[w * hidden + j];
12135 }
12136 }
12137 }
12138 _ => {
12139 WORKER_SCRATCH.with(|ws| {
12140 let mut ws = ws.borrow_mut();
12141 let [scratch, _acc] = &mut *ws;
12142 scratch.resize(hidden.max(x.len()), 0.0);
12143 for &nrm in live.iter() {
12144 let idx = nrm as usize;
12145 let up = d.up_proj.row_dot(idx, x, scratch);
12146 let a = g[idx] * up;
12147 if a != 0.0 {
12148 dt.add_row_scaled(idx, a, &mut out, scratch);
12149 }
12150 }
12151 });
12152 }
12153 }
12154 Some(out)
12155 })
12156}
12157
12158struct DynScratch {
12161 g: Vec<f32>,
12162 mag: Vec<f32>,
12163 live: Vec<u32>,
12164 parts: Vec<f32>,
12165}
12166
12167thread_local! {
12168 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
12169 std::cell::RefCell::new(DynScratch {
12170 g: Vec::new(),
12171 mag: Vec::new(),
12172 live: Vec::new(),
12173 parts: Vec::new(),
12174 })
12175 };
12176 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
12178 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
12179}
12180
12181fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
12186 let inter = d.gate_proj.rows();
12187 FFN_SCRATCH.with(|s| {
12188 let mut s = s.borrow_mut();
12189 let [g, u, ..] = &mut *s;
12190 g.resize(inter, 0.0);
12191 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
12192 } else {
12194 u.resize(inter, 0.0);
12195 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
12196 for i in 0..inter {
12197 g[i] = d.act.combine(g[i], u[i]);
12198 }
12199 }
12200 zero_masked_cols(g, 1, inter, mask_row);
12201 let mut out = attention::take_buf(d.down_proj.rows());
12202 d.down_proj.matvec(g, &mut out, pool);
12203 out
12204 })
12205}
12206
12207fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
12213 if d.act != Act::Silu {
12215 return None;
12216 }
12217 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
12220 return None;
12221 }
12222 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
12223 let mut model_ref = None;
12224 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
12225 let model = model_ref?;
12226 let hidden = jobs[0].down.1;
12227 let mut out = attention::take_buf(hidden);
12228 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12229 Some(out)
12230 } else {
12231 let mut out = out;
12232 attention::recycle_buf(&mut out);
12233 None
12234 }
12235}
12236
12237#[allow(clippy::type_complexity)]
12242#[allow(clippy::type_complexity)]
12243pub(crate) fn moe_parts(
12244 t: &QTensor,
12245) -> Option<(
12246 &std::sync::Arc<cortiq_core::CmfModel>,
12247 usize,
12248 usize,
12249 usize,
12250 &[f32],
12251 &[f32],
12252 bool,
12253 bool,
12254 bool,
12255)> {
12256 match t {
12257 QTensor::Mapped {
12258 model,
12259 idx,
12260 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
12261 rows,
12262 cols,
12263 row_scale,
12264 col_field,
12265 ..
12266 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
12267 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
12268 )),
12269 QTensor::Mapped {
12271 model,
12272 idx,
12273 dtype: cortiq_core::TensorDtype::Q1,
12274 rows,
12275 cols,
12276 ..
12277 } => Some((
12278 model,
12279 *idx,
12280 *rows,
12281 *cols,
12282 &[][..],
12283 &[][..],
12284 true,
12285 false,
12286 false,
12287 )),
12288 QTensor::Mapped {
12290 model,
12291 idx,
12292 dtype: cortiq_core::TensorDtype::Q4Tiled,
12293 rows,
12294 cols,
12295 ..
12296 } => Some((
12297 model,
12298 *idx,
12299 *rows,
12300 *cols,
12301 &[][..],
12302 &[][..],
12303 false,
12304 true,
12305 false,
12306 )),
12307 QTensor::Mapped {
12309 model,
12310 idx,
12311 dtype: cortiq_core::TensorDtype::Q4TiledP,
12312 rows,
12313 cols,
12314 ..
12315 } => Some((
12316 model,
12317 *idx,
12318 *rows,
12319 *cols,
12320 &[][..],
12321 &[][..],
12322 false,
12323 true,
12324 false,
12325 )),
12326 QTensor::Mapped {
12330 model,
12331 idx,
12332 dtype: cortiq_core::TensorDtype::Q2TiledP,
12333 rows,
12334 cols,
12335 ..
12336 } => Some((
12337 model,
12338 *idx,
12339 *rows,
12340 *cols,
12341 &[][..],
12342 &[][..],
12343 false,
12344 true,
12345 true,
12346 )),
12347 _ => None,
12348 }
12349}
12350
12351#[cfg(target_os = "macos")]
12357fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
12358 if m.router_sigmoid
12359 || m.router_input_norm
12360 || m.expert_bias.is_some()
12361 || m.route_tau.is_some()
12362 || m.mask.is_some()
12363 || m.per_expert_scale.is_some()
12364 || m.experts.is_empty()
12365 || m.top_k == 0
12366 || m.resonance.is_some()
12367 {
12368 return None;
12369 }
12370 let (sh, sg) = match &m.shared {
12373 Some((sh, Some(sg))) => (sh, sg),
12374 _ => return None,
12375 };
12376 let (rf, rr, rc) = m.router.f32_parts()?;
12377 if rr != m.experts.len() || rc != hidden {
12378 return None;
12379 }
12380 let (sf, sr, sc) = sg.f32_parts()?;
12381 if sr * sc != hidden {
12382 return None;
12383 }
12384 let inter = m.experts[0].gate_proj.rows();
12385 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
12388 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
12389 if e.act != Act::Silu
12390 || e.gate_proj.rows() != inter
12391 || e.gate_proj.cols() != hidden
12392 || e.up_proj.rows() != inter
12393 || e.up_proj.cols() != hidden
12394 || e.down_proj.rows() != hidden
12395 || e.down_proj.cols() != inter
12396 {
12397 return None;
12398 }
12399 let pick = |t: &QTensor| -> Option<usize> {
12400 if gu_q2 {
12401 t.mapped_q2tp().map(|(_, i)| i)
12402 } else {
12403 t.mapped_q4tp().map(|(_, i)| i)
12404 }
12405 };
12406 Some((
12407 pick(&e.gate_proj)?,
12408 pick(&e.up_proj)?,
12409 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
12410 ))
12411 };
12412 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
12413 let shared = trio(sh)?;
12414 Some(crate::gpu::GpuMoe {
12415 router: rf,
12416 sgate: sf,
12417 experts,
12418 shared,
12419 n_exp: m.experts.len(),
12420 top_k: m.top_k,
12421 inter,
12422 norm_topk: m.norm_topk_prob,
12423 route_scale: m.routed_scaling,
12424 gu_q2,
12425 })
12426}
12427
12428pub(crate) fn moe_push_job_parts<'a>(
12432 gate: &'a QTensor,
12433 up: &'a QTensor,
12434 down: &'a QTensor,
12435 x: &[f32],
12436 w: f32,
12437 swiglu_limit: f32,
12438 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
12439 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
12440) -> Option<()> {
12441 use crate::qtensor::prescale;
12442 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
12443 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
12444 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
12445 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
12446 return None; }
12448 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
12451 return None;
12452 }
12453 if !gq2 && dq2 {
12454 return None;
12455 }
12456 model_ref.get_or_insert_with(|| gm.clone());
12457 let dt = |cf: &[f32]| {
12458 if cf.is_empty() {
12459 cortiq_core::TensorDtype::Q8Row
12460 } else {
12461 cortiq_core::TensorDtype::Q8_2f
12462 }
12463 };
12464 jobs.push(crate::gpu::MoeJob {
12465 gate: (gi, gr, gc, grs),
12466 up: (ui, ur, uc, urs),
12467 down: (di, dr, dc, drs),
12468 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
12469 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
12470 down_col: dcf,
12471 w,
12472 q1: gq1,
12473 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
12474 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
12475 gu_q2: gq2,
12476 swiglu_limit,
12477 });
12478 Some(())
12479}
12480
12481fn moe_push_job<'a>(
12483 d: &'a DenseFfn,
12484 x: &[f32],
12485 w: f32,
12486 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
12487 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
12488) -> Option<()> {
12489 use crate::qtensor::prescale;
12490 if d.act != Act::Silu {
12491 return None; }
12493 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
12494 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
12495 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
12496 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
12497 return None; }
12499 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
12500 return None;
12501 }
12502 if !gq2 && dq2 {
12503 return None;
12504 }
12505 model_ref.get_or_insert_with(|| gm.clone());
12506 let gdt = if gcf.is_empty() {
12507 cortiq_core::TensorDtype::Q8Row
12508 } else {
12509 cortiq_core::TensorDtype::Q8_2f
12510 };
12511 let udt = if ucf.is_empty() {
12512 cortiq_core::TensorDtype::Q8Row
12513 } else {
12514 cortiq_core::TensorDtype::Q8_2f
12515 };
12516 jobs.push(crate::gpu::MoeJob {
12517 gate: (gi, gr, gc, grs),
12518 up: (ui, ur, uc, urs),
12519 down: (di, dr, dc, drs),
12520 xs_gate: prescale(x, gcf, gdt).into_owned(),
12521 xs_up: prescale(x, ucf, udt).into_owned(),
12522 down_col: dcf,
12523 w,
12524 q1: gq1,
12525 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
12526 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
12527 gu_q2: gq2,
12528 swiglu_limit: 0.0,
12529 });
12530 Some(())
12531}
12532
12533fn sparse_ffn_quant(
12540 d: &DenseFfn,
12541 x: &[f32],
12542 active: &[u16],
12543 hidden: usize,
12544 pool: Option<&Pool>,
12545) -> Vec<f32> {
12546 let n = active.len();
12547 let inter = d.gate_proj.rows();
12548 let mut act = vec![0.0f32; n];
12549 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
12552 let compute = |ai: usize| -> f32 {
12553 let idx = active[ai] as usize;
12554 if idx >= inter {
12555 return 0.0; }
12557 let mut s = if need_scratch {
12558 vec![0.0f32; hidden]
12559 } else {
12560 Vec::new()
12561 };
12562 let gate = d.gate_proj.row_dot(idx, x, &mut s);
12563 let up = d.up_proj.row_dot(idx, x, &mut s);
12564 d.act.combine(gate, up)
12565 };
12566 match pool {
12567 Some(p) if n >= 256 => {
12568 let ptr = SendMut(act.as_mut_ptr());
12569 p.run(&|widx, nw| {
12570 let chunk = n.div_ceil(nw);
12571 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
12572 for ai in s..e {
12573 unsafe { *ptr.at(ai) = compute(ai) };
12574 }
12575 });
12576 }
12577 _ => {
12578 for (ai, a) in act.iter_mut().enumerate() {
12579 *a = compute(ai);
12580 }
12581 }
12582 }
12583 let mut out = vec![0.0f32; hidden];
12585 for (ai, &idx) in active.iter().enumerate() {
12586 let w = act[ai];
12587 if w.abs() >= 1e-12 && (idx as usize) < inter {
12588 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
12589 }
12590 }
12591 out
12592}
12593
12594#[doc(hidden)]
12596pub fn sparse_ffn_quant_for_test(
12597 d: &DenseFfn,
12598 x: &[f32],
12599 active: &[u16],
12600 hidden: usize,
12601) -> Vec<f32> {
12602 sparse_ffn_quant(d, x, active, hidden, None)
12603}
12604
12605fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
12609 let deq = |t: &QTensor| -> Vec<f32> {
12610 let (rows, cols) = (t.rows(), t.cols());
12611 let mut out = vec![0.0f32; rows * cols];
12612 for r in 0..rows {
12613 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
12614 }
12615 out
12616 };
12617 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
12618}
12619
12620struct SendMut(*mut f32);
12622unsafe impl Send for SendMut {}
12623unsafe impl Sync for SendMut {}
12624impl SendMut {
12625 #[inline]
12626 #[allow(clippy::mut_from_ref)]
12629 unsafe fn at(&self, i: usize) -> &mut f32 {
12630 unsafe { &mut *self.0.add(i) }
12631 }
12632}
12633
12634pub(crate) fn moe_route(
12644 logits: &[f32],
12645 m: &MoeFfn,
12646 allowed: Option<&[bool]>,
12647) -> (Vec<usize>, Vec<f32>, f32) {
12648 let ne = logits.len();
12649 let p: Vec<f32> = if m.router_sigmoid {
12650 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
12651 } else {
12652 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12653 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
12654 let s: f32 = e.iter().sum();
12655 for v in &mut e {
12656 *v /= s;
12657 }
12658 e
12659 };
12660 let admit = |e: usize| {
12666 m.mask.as_ref().is_none_or(|mk| mk[e])
12667 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
12668 };
12669 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
12670 match &m.expert_bias {
12672 Some(b) => idx.sort_unstable_by(|&x, &y| {
12673 (p[y] + b[y])
12674 .partial_cmp(&(p[x] + b[x]))
12675 .unwrap()
12676 .then(x.cmp(&y))
12677 }),
12678 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
12679 }
12680 idx.truncate(m.top_k);
12681 if let Some(tau) = m.route_tau {
12685 let total: f32 = idx.iter().map(|&e| p[e]).sum();
12686 if total > 0.0 {
12687 let mut acc = 0.0f32;
12688 let mut keep = idx.len();
12689 for (i, &e) in idx.iter().enumerate() {
12690 acc += p[e];
12691 if acc >= tau * total {
12692 keep = i + 1;
12693 break;
12694 }
12695 }
12696 idx.truncate(keep);
12697 }
12698 }
12699 let wsum: f32 = if m.norm_topk_prob {
12700 let s: f32 = idx.iter().map(|&e| p[e]).sum();
12701 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
12704 } else {
12705 1.0 / m.routed_scaling
12706 };
12707 (idx, p, wsum)
12708}
12709
12710fn moe_trace(idx: &[usize]) {
12712 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
12713}
12714
12715pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
12718 use std::io::Write;
12719 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
12720 std::sync::OnceLock::new();
12721 let Some(f) = F.get_or_init(|| {
12722 let p = std::env::var("CMF_MOE_TRACE").ok()?;
12723 Some(std::sync::Mutex::new(
12724 std::fs::OpenOptions::new()
12725 .create(true)
12726 .append(true)
12727 .open(p)
12728 .ok()?,
12729 ))
12730 }) else {
12731 return;
12732 };
12733 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
12734 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
12735}
12736
12737pub(crate) fn moe_ffn(
12740 m: &MoeFfn,
12741 x: &[f32],
12742 pool: Option<&Pool>,
12743 allowed: Option<&[bool]>,
12744) -> Vec<f32> {
12745 accumulate_act(m, x, 1);
12746 let ne = m.experts.len();
12747 let mut logits = vec![0.0f32; ne];
12748 match &m.resonance {
12749 Some(r) => r.scores(x, &mut logits),
12750 None => m.router.matvec(x, &mut logits, pool),
12751 }
12752 let (idx, p, wsum) = moe_route(&logits, m, allowed);
12753 {
12754 let mut st = m.stats.borrow_mut();
12755 if st.len() < ne {
12756 st.resize(ne, 0);
12757 }
12758 for &e in &idx {
12759 st[e] += 1;
12760 }
12761 }
12762 moe_trace(&idx);
12768 if crate::gpu::enabled_here() {
12773 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
12774 crate::gpu::ProbeArm::Gpu => {
12775 let t0 = std::time::Instant::now();
12776 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
12777 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
12778 return out;
12779 }
12780 }
12781 crate::gpu::ProbeArm::CpuTimed => {
12782 let t0 = std::time::Instant::now();
12783 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
12784 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
12785 return out;
12786 }
12787 crate::gpu::ProbeArm::Cpu => {
12788 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
12789 }
12790 }
12791 }
12792 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
12793}
12794
12795fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
12800 use std::sync::atomic::{AtomicBool, Ordering};
12801 if built {
12802 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
12803 if total_layers > 0 && layers_run < total_layers {
12804 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
12805 } else {
12806 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
12807 }
12808 } else {
12809 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
12810 }
12811 static SAID: AtomicBool = AtomicBool::new(false);
12812 if !SAID.swap(true, Ordering::Relaxed) {
12813 if built {
12814 tracing::info!("wgpu whole-token graph: ACTIVE");
12815 } else {
12816 tracing::warn!("wgpu whole-token graph refused — per-op path");
12817 }
12818 }
12819}
12820
12821pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12825pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12826pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12830pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12832
12833fn moe_batch_enabled() -> bool {
12836 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12837 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
12838}
12839
12840fn moe_ffn_cpu_batched(
12846 m: &MoeFfn,
12847 x: &[f32],
12848 idx: &[usize],
12849 p: &[f32],
12850 wsum: f32,
12851 pool: Option<&Pool>,
12852) -> Option<Vec<f32>> {
12853 if idx.is_empty() || !moe_batch_enabled() {
12854 return None;
12855 }
12856 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
12860 return None;
12861 }
12862 let n = idx.len() + usize::from(m.shared.is_some());
12863 let mut pairs = Vec::with_capacity(n);
12864 let mut downs = Vec::with_capacity(n);
12865 let mut ws = Vec::with_capacity(n);
12866 for &e in idx {
12867 let d = &m.experts[e];
12868 if d.act != Act::Silu {
12869 return None;
12870 }
12871 pairs.push((&d.gate_proj, &d.up_proj));
12872 downs.push(&d.down_proj);
12873 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
12874 }
12875 if let Some((se, gate)) = &m.shared {
12878 if se.act != Act::Silu {
12879 return None;
12880 }
12881 let g = gate.as_ref().map_or(1.0, |gate| {
12882 let mut gl = [0.0f32; 1];
12883 gate.matvec(x, &mut gl, pool);
12884 1.0 / (1.0 + (-gl[0]).exp())
12885 });
12886 pairs.push((&se.gate_proj, &se.up_proj));
12887 downs.push(&se.down_proj);
12888 ws.push(g);
12889 }
12890 let inter = pairs[0].0.rows();
12891 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
12892 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
12893 return None;
12894 }
12895 let mut out = attention::take_buf(x.len());
12896 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
12897 attention::recycle_buf(&mut out);
12898 return None;
12899 }
12900 Some(out)
12901}
12902
12903pub(crate) fn moe_cold_experts_cpu(
12909 experts: &[(&DenseFfn, f32)],
12910 x: &[f32],
12911 pool: Option<&Pool>,
12912) -> Vec<f32> {
12913 let mut out = attention::take_buf(x.len());
12914 if experts.is_empty() {
12915 return out;
12916 }
12917 let pairs: Vec<_> = experts
12918 .iter()
12919 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
12920 .collect();
12921 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
12922 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
12923 let inter = experts[0].0.gate_proj.rows();
12924 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
12925 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
12926 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
12927 {
12928 return out;
12929 }
12930 out.fill(0.0);
12931 for &(expert, weight) in experts {
12932 let mut one = dense_ffn(expert, x, pool);
12933 for (o, v) in out.iter_mut().zip(&one) {
12934 *o += weight * v;
12935 }
12936 attention::recycle_buf(&mut one);
12937 }
12938 out
12939}
12940
12941fn moe_ffn_cpu(
12943 m: &MoeFfn,
12944 x: &[f32],
12945 idx: &[usize],
12946 p: &[f32],
12947 wsum: f32,
12948 pool: Option<&Pool>,
12949) -> Vec<f32> {
12950 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
12951 return out;
12952 }
12953 let mut out = attention::take_buf(x.len());
12954 for &e in idx {
12955 let mut eo = dense_ffn(&m.experts[e], x, pool);
12956 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
12957 for i in 0..out.len() {
12958 out[i] += w * eo[i];
12959 }
12960 attention::recycle_buf(&mut eo);
12961 }
12962 if let Some((se, gate)) = &m.shared {
12963 let mut so = dense_ffn(se, x, pool);
12964 let g = gate.as_ref().map_or(1.0, |gate| {
12965 let mut gl = [0.0f32; 1];
12966 gate.matvec(x, &mut gl, pool);
12967 1.0 / (1.0 + (-gl[0]).exp())
12968 });
12969 for i in 0..out.len() {
12970 out[i] += g * so[i];
12971 }
12972 attention::recycle_buf(&mut so);
12973 }
12974 out
12975}
12976
12977#[allow(clippy::too_many_arguments)]
12985fn mla_attention(
12986 w: &MlaWeights,
12987 normed: &[f32],
12988 cache: &mut crate::kv_cache::LayerKvCache,
12989 position: usize,
12990 inv_freq: &[f32],
12991 rope_scale: f32,
12992 eps: f64,
12993 pool: Option<&Pool>,
12994) -> Vec<f32> {
12995 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
12996 let hd = dr + dn;
12997 let mut q = vec![0.0f32; nh * hd];
12998 match (&w.q_a, &w.q_a_norm) {
12999 (Some(qa), Some(qn)) => {
13000 let mut t = vec![0.0f32; qa.rows()];
13001 qa.matvec(normed, &mut t, pool);
13002 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
13003 w.q_proj.matvec(&tn, &mut q, pool);
13004 }
13005 _ => w.q_proj.matvec(normed, &mut q, pool),
13006 }
13007 let mut ca = vec![0.0f32; lora + dr];
13008 w.kv_a.matvec(normed, &mut ca, pool);
13009 let (c_lat, k_rope) = ca.split_at_mut(lora);
13010 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
13011 let mut kvb = vec![0.0f32; nh * (dn + dv)];
13012 w.kv_b.matvec(&latn, &mut kvb, pool);
13013 if !w.nope {
13014 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
13015 }
13016 for h in 0..nh {
13017 if !w.nope {
13018 attention::rope_rotate_scaled(
13019 &mut q[h * hd..h * hd + dr],
13020 position,
13021 inv_freq,
13022 rope_scale,
13023 );
13024 }
13025 }
13026 let mut k = vec![0.0f32; nh * hd];
13027 let mut v = vec![0.0f32; nh * hd];
13028 for h in 0..nh {
13029 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
13030 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
13031 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
13032 }
13033 cache.append(&k, &v, &vec![true; nh]);
13034 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
13035 attention::recycle_buf(&mut imp);
13036 let mut ov = vec![0.0f32; nh * dv];
13037 for h in 0..nh {
13038 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
13039 }
13040 let mut out = vec![0.0f32; w.o_proj.rows()];
13041 w.o_proj.matvec(&ov, &mut out, pool);
13042 out
13043}
13044
13045fn dense_moe_ffn(
13052 dm: &DenseMoeFfn,
13053 x_normed: &[f32],
13054 h_raw: &[f32],
13055 eps: f64,
13056 norm_style: NormStyle,
13057 pool: Option<&Pool>,
13058) -> Vec<f32> {
13059 let mut d = dense_ffn(&dm.dense, x_normed, pool);
13060 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
13061 let m = &dm.moe;
13062 let ne = m.experts.len();
13063 let mut logits = vec![0.0f32; ne];
13064 if m.router_input_norm {
13065 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
13066 let inv = 1.0 / (ss + eps as f32).sqrt();
13067 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
13068 m.router.matvec(&xr, &mut logits, pool);
13069 } else {
13070 m.router.matvec(h_raw, &mut logits, pool);
13071 }
13072 let (idx, p, wsum) = moe_route(&logits, m, None);
13073 {
13074 let mut st = m.stats.borrow_mut();
13075 if st.len() < ne {
13076 st.resize(ne, 0);
13077 }
13078 for &e in &idx {
13079 st[e] += 1;
13080 }
13081 }
13082 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
13083 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
13084 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
13085 for (di, mi) in d.iter_mut().zip(&mo) {
13086 *di += mi;
13087 }
13088 d
13089}
13090
13091fn moe_gpu_refused(why: &'static str) {
13098 use std::sync::atomic::{AtomicBool, Ordering};
13099 static SAID: AtomicBool = AtomicBool::new(false);
13100 if !SAID.swap(true, Ordering::Relaxed) {
13101 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
13102 }
13103}
13104
13105fn moe_ffn_gpu(
13106 m: &MoeFfn,
13107 x: &[f32],
13108 idx: &[usize],
13109 p: &[f32],
13110 wsum: f32,
13111 pool: Option<&Pool>,
13112) -> Option<Vec<f32>> {
13113 use crate::gpu::MoeJob;
13114
13115 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
13116 let mut model_ref = None;
13117 for &e in idx {
13118 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
13119 moe_gpu_refused("push_job(expert)");
13120 return None;
13121 }
13122 }
13123 if let Some((se, gate)) = &m.shared {
13124 let g = gate.as_ref().map_or(1.0, |gate| {
13125 let mut gl = [0.0f32; 1];
13126 gate.matvec(x, &mut gl, pool);
13127 1.0 / (1.0 + (-gl[0]).exp())
13128 });
13129 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
13130 moe_gpu_refused("push_job(shared)");
13131 return None;
13132 }
13133 }
13134 let Some(model) = model_ref else {
13135 moe_gpu_refused("no model_ref");
13136 return None;
13137 };
13138 let hidden = jobs[0].down.1;
13139 let mut out = vec![0.0f32; hidden];
13140 if crate::gpu::moe_block(&model, &jobs, &mut out) {
13141 Some(out)
13142 } else {
13143 moe_gpu_refused("gpu::moe_block");
13144 None
13145 }
13146}
13147
13148fn ffn_forward(
13150 ffn: &FfnKind,
13151 x: &[f32],
13152 pool: Option<&Pool>,
13153 experts_allowed: Option<&[bool]>,
13154) -> Vec<f32> {
13155 match ffn {
13156 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
13157 FfnKind::Dense(d) => dense_ffn(d, x, pool),
13158 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
13159 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13163 }
13164}
13165
13166fn ffn_forward_pair(
13170 ffn: &FfnKind,
13171 x1: &[f32],
13172 x2: &[f32],
13173 pool: Option<&Pool>,
13174 experts_allowed: Option<&[bool]>,
13175) -> (Vec<f32>, Vec<f32>) {
13176 let d = match ffn {
13177 FfnKind::Dense(d) if !d.segs.is_empty() => {
13180 return (
13181 tube_ffn(d, x1, 1, pool, None),
13182 tube_ffn(d, x2, 1, pool, None),
13183 );
13184 }
13185 FfnKind::Dense(d) => d,
13186 FfnKind::Moe(m) => {
13187 return (
13188 moe_ffn(m, x1, pool, experts_allowed),
13189 moe_ffn(m, x2, pool, experts_allowed),
13190 );
13191 }
13192 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
13193 };
13194 let inter = d.gate_proj.rows();
13195 FFN_SCRATCH.with(|s| {
13196 let mut s = s.borrow_mut();
13197 let [g1, g2, u1, u2] = &mut *s;
13198 g1.resize(inter, 0.0);
13199 g2.resize(inter, 0.0);
13200 u1.resize(inter, 0.0);
13201 u2.resize(inter, 0.0);
13202 QTensor::matvec2_many(
13205 [&d.gate_proj, &d.up_proj],
13206 x1,
13207 x2,
13208 [g1.as_mut_slice(), u1.as_mut_slice()],
13209 [g2.as_mut_slice(), u2.as_mut_slice()],
13210 pool,
13211 );
13212 for i in 0..inter {
13213 g1[i] = d.act.combine(g1[i], u1[i]);
13214 g2[i] = d.act.combine(g2[i], u2[i]);
13215 }
13216 let mut o1 = attention::take_buf(d.down_proj.rows());
13217 let mut o2 = attention::take_buf(d.down_proj.rows());
13218 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
13219 (o1, o2)
13220 })
13221}
13222
13223#[cfg(test)]
13224mod tests {
13225
13226 #[test]
13227 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
13228 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
13229 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
13230 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
13231 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
13232 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
13233 }
13234
13235 #[test]
13236 fn cancel_flag_stops_generation() {
13237 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
13238 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13241 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
13242 assert_eq!(r.finish_reason, "cancelled");
13243 assert!(
13244 r.token_ids.is_empty(),
13245 "no tokens after cancel: {:?}",
13246 r.token_ids
13247 );
13248 assert_eq!(p.kv_cache.seq_len(), 0);
13249 assert!(p.kv_history.is_empty());
13250 assert!(!p.graph_want_logits);
13251 assert!(p.graph_logits.is_none());
13252 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
13254 assert_ne!(r2.finish_reason, "cancelled");
13255 }
13256 use super::*;
13257
13258 #[test]
13266 fn dynamic_ffn_equals_the_zeroing_arm() {
13267 let (hidden, inter) = (8usize, 32usize);
13268 let synth = |n: usize, salt: usize| -> Vec<f32> {
13269 (0..n)
13270 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
13271 .collect()
13272 };
13273 let down = synth(hidden * inter, 3);
13274 let mut down_t = vec![0.0f32; inter * hidden];
13275 for r in 0..hidden {
13276 for c in 0..inter {
13277 down_t[c * hidden + r] = down[r * inter + c];
13278 }
13279 }
13280 let d = DenseFfn {
13281 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13282 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13283 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
13284 act: Act::Silu,
13285 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
13286 segs: Vec::new(),
13287 };
13288 let x = synth(hidden, 11);
13289 let k = 12usize;
13290 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
13291 let mut g = vec![0.0f32; inter];
13293 d.gate_proj.matvec(&x, &mut g, None);
13294 let mut u = vec![0.0f32; inter];
13295 d.up_proj.matvec(&x, &mut u, None);
13296 for v in g.iter_mut() {
13297 *v = inference::silu(*v);
13298 }
13299 keep_top_k(&mut g, k);
13300 for i in 0..inter {
13301 g[i] *= u[i];
13302 }
13303 let mut want = vec![0.0f32; hidden];
13304 d.down_proj.matvec(&g, &mut want, None);
13305 for (a, b) in want.iter().zip(&got) {
13306 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
13307 }
13308 }
13309
13310 #[test]
13316 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
13317 let (hidden, core, tube) = (8usize, 12usize, 8usize);
13318 let inter = core + tube;
13319 let synth = |n: usize, salt: usize| -> Vec<f32> {
13320 (0..n)
13321 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
13322 .collect()
13323 };
13324 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
13325 let d_all = synth(hidden * inter, 3);
13326 let dense = DenseFfn {
13328 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
13329 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
13330 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
13331 act: Act::Silu,
13332 down_t: None,
13333 segs: Vec::new(),
13334 };
13335 let rows =
13336 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
13337 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
13338 let mut o = Vec::with_capacity(hidden * (b - a));
13339 for r in 0..hidden {
13340 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
13341 }
13342 o
13343 };
13344 let tubed = DenseFfn {
13345 down_t: None,
13346 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
13347 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
13348 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
13349 act: Act::Silu,
13350 segs: vec![FfnSeg {
13351 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
13352 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
13353 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
13354 start: core,
13355 width: tube,
13356 }],
13357 };
13358 let x = synth(hidden, 7);
13359 let want = dense_ffn(&dense, &x, None);
13360 let got = tube_ffn(&tubed, &x, 1, None, None);
13361 for (a, b) in want.iter().zip(&got) {
13362 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
13363 }
13364 let mut bits = vec![0u8; inter.div_ceil(8)];
13366 for n in 0..core {
13367 bits[n / 8] |= 1 << (n % 8);
13368 }
13369 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13370 let masked = dense_ffn_masked(&dense, &x, None, &bits);
13371 for (a, b) in masked.iter().zip(&closed) {
13372 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
13373 }
13374 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
13376 for (a, b) in closed.iter().zip(&batch) {
13377 assert_eq!(a, b, "batch arm disagrees with decode arm");
13378 }
13379 }
13380
13381 #[test]
13383 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
13384 let (hidden, inter) = (16usize, 40usize);
13385 let synth = |n: usize, salt: usize| -> Vec<f32> {
13386 (0..n)
13387 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
13388 .collect()
13389 };
13390 let d = DenseFfn {
13391 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
13392 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
13393 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
13394 act: Act::Silu,
13395 down_t: None,
13396 segs: Vec::new(),
13397 };
13398 let x = synth(hidden, 9);
13399 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
13401
13402 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
13403
13404 let mut g = vec![0.0f32; inter];
13406 d.gate_proj.matvec(&x, &mut g, None);
13407 let mut u = vec![0.0f32; inter];
13408 d.up_proj.matvec(&x, &mut u, None);
13409 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
13410 for i in 0..inter {
13411 g[i] = if act_set.contains(&(i as u16)) {
13412 inference::silu(g[i]) * u[i]
13413 } else {
13414 0.0
13415 };
13416 }
13417 let mut reference = vec![0.0f32; hidden];
13418 d.down_proj.matvec(&g, &mut reference, None);
13419
13420 let max_d = sparse
13421 .iter()
13422 .zip(&reference)
13423 .map(|(a, b)| (a - b).abs())
13424 .fold(0.0f32, f32::max);
13425 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
13426 }
13427
13428 fn attach_test_mtp(p: &mut Pipeline) {
13430 let (h, inter, heads, kv, hd) = (
13431 p.hidden_size,
13432 p.intermediate_size,
13433 p.num_heads,
13434 p.num_kv_heads,
13435 p.head_dim,
13436 );
13437 let synth = |n: usize, salt: usize| -> Vec<f32> {
13438 (0..n)
13439 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
13440 .collect()
13441 };
13442 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
13443 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
13444 };
13445 p.mtp = Some(MtpModule {
13446 enorm: vec![1.0; h],
13447 hnorm: vec![1.0; h],
13448 eh_proj: qt(h, 2 * h, 301),
13449 layer: LayerWeights {
13450 input_norm: vec![1.0; h],
13451 post_norm: vec![1.0; h],
13452 attn_out_norm: None,
13453 ffn_out_norm: None,
13454 layer_scale: None,
13455 ffn: FfnKind::Dense(DenseFfn {
13456 gate_proj: qt(inter, h, 315),
13457 up_proj: qt(inter, h, 316),
13458 down_proj: qt(h, inter, 317),
13459 act: Act::Silu,
13460 down_t: None,
13461 segs: Vec::new(),
13462 }),
13463 attn: AttnKind::Full {
13464 bias: None,
13465 wq: qt(heads * hd, h, 311),
13466 wk: qt(kv * hd, h, 312),
13467 wv: qt(kv * hd, h, 313),
13468 wo: qt(h, heads * hd, 314),
13469 q_norm: None,
13470 k_norm: None,
13471 output_gate: false,
13472 softplus_gate: None,
13473 },
13474 },
13475 final_norm: vec![1.0; h],
13476 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
13477 });
13478 }
13479
13480 #[test]
13481 fn speculative_equals_vanilla_greedy() {
13482 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
13486 let run = |spec: bool| {
13487 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13488 p.sampler_config.temperature = 0.0;
13489 attach_test_mtp(&mut p);
13490 p.speculative = spec;
13491 let r = p.generate("abcdef", 12, None, None).unwrap();
13492 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
13493 };
13494 let (vanilla, d0, _) = run(false);
13495 let (spec, d1, a1) = run(true);
13496 assert_eq!(d0, 0, "vanilla path must not draft");
13497 assert!(d1 > 0, "speculative path must draft");
13498 assert_eq!(
13499 vanilla, spec,
13500 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
13501 );
13502 }
13503
13504 #[test]
13505 fn speculative_accepts_constant_oracle() {
13506 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
13508 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13509 p.sampler_config.temperature = 0.0;
13510 p.sampler_config.repetition_penalty = 1.0;
13511 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
13514 attach_test_mtp(&mut p);
13515 p.speculative = true;
13516 let r = p.generate("abcd", 10, None, None).unwrap();
13517 assert!(r.mtp_drafted > 0);
13518 assert_eq!(
13519 r.mtp_accepted, r.mtp_drafted,
13520 "constant logits → every draft accepted"
13521 );
13522 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
13525 }
13526
13527 #[test]
13528 fn empty_prompt_is_an_error_not_a_panic() {
13529 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
13530 let r = p.generate("", 4, None, None);
13531 assert!(r.is_err(), "empty prompt must be a clean error");
13532 }
13533
13534 #[test]
13535 fn every_token_enters_kv_exactly_once() {
13536 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13537 p.sampler_config.temperature = 0.0;
13539 let r = p.generate("abc", 2, None, None).unwrap();
13540 assert_eq!(r.prompt_tokens, 3);
13541 assert_eq!(
13545 p.kv_cache.seq_len(),
13546 3 + r.tokens_generated - 1,
13547 "each token must be cached exactly once (v1 cached the last prompt token twice)"
13548 );
13549 }
13550
13551 #[test]
13552 fn generation_is_reproducible_with_seed() {
13553 let run = || {
13554 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13555 p.generate("hello", 8, None, None).unwrap().token_ids
13556 };
13557 assert_eq!(run(), run());
13558 }
13559
13560 #[test]
13561 fn resetting_sampler_restarts_the_seeded_stream() {
13562 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13563 let config = SamplerConfig {
13564 seed: Some(1234),
13565 ..SamplerConfig::default()
13566 };
13567 p.set_sampler_config(config.clone());
13568 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
13569 p.set_sampler_config(config);
13570 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
13571 assert_eq!(first, second);
13572 }
13573
13574 #[test]
13575 fn eviction_bounds_the_cache() {
13576 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
13577 p.kv_cache.max_seq_len = 6;
13578 p.sampler_config.temperature = 0.0;
13579 let _ = p.generate("abcd", 12, None, None).unwrap();
13580 assert!(
13581 p.kv_cache.seq_len() <= 6 + 1,
13582 "cache must stay bounded by max_seq_len (got {})",
13583 p.kv_cache.seq_len()
13584 );
13585 }
13586
13587 #[test]
13588 fn confidence_matches_tokens_and_is_a_probability() {
13589 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13590 p.sampler_config.temperature = 0.0;
13591 p.sampler_config.repetition_penalty = 1.0;
13592 let r = p.generate("abcd", 10, None, None).unwrap();
13593 assert_eq!(
13594 r.token_confidence.len(),
13595 r.token_ids.len(),
13596 "one confidence per emitted token"
13597 );
13598 for &c in &r.token_confidence {
13599 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
13600 }
13601 let logits = [1.0f32, 3.0, 0.5, 3.0];
13603 let p0 = top1_prob_t(&logits, 1, 1.0);
13604 let p1 = top1_prob_t(&logits, 3, 1.0);
13605 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
13606 assert!(p0 > 0.0 && p0 < 1.0);
13607 let sharp = top1_prob_t(&logits, 1, 1.0);
13609 let soft = top1_prob_t(&logits, 1, 2.0);
13610 assert!(soft < sharp, "higher temperature lowers peak confidence");
13611 }
13612
13613 #[test]
13614 fn trace_is_opt_in_and_parallels_the_output() {
13615 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13617 p.sampler_config.temperature = 0.0;
13618 p.sampler_config.repetition_penalty = 1.0;
13619 let r = p.generate("abcd", 10, None, None).unwrap();
13620 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
13621
13622 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13624 p.sampler_config.temperature = 0.0;
13625 p.sampler_config.repetition_penalty = 1.0;
13626 p.set_trace(true);
13627 let r = p.generate("abcd", 10, None, None).unwrap();
13628 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
13629 for (i, tr) in r.traces.iter().enumerate() {
13630 assert_eq!(tr.t, i, "trace index is sequential");
13631 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
13632 assert_eq!(
13633 tr.confidence, r.token_confidence[i],
13634 "trace confidence matches the confidence channel"
13635 );
13636 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
13638 }
13639 }
13640
13641 #[test]
13642 fn explain_prefill_logits_match_greedy_first_token() {
13643 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13647 p.sampler_config.temperature = 0.0;
13648 p.sampler_config.repetition_penalty = 1.0;
13649 let ids = p.tokenizer.encode("abcd");
13650 let logits = p.prefill_next_logits(&ids, None);
13651 let argmax = logits
13652 .iter()
13653 .enumerate()
13654 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
13655 .unwrap()
13656 .0 as u32;
13657 let r = p.generate("abcd", 1, None, None).unwrap();
13658 assert_eq!(
13659 argmax, r.token_ids[0],
13660 "explain preview must match greedy emit"
13661 );
13662 }
13663
13664 #[test]
13665 fn laguna_shared_expert_is_unconditionally_added() {
13666 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
13667 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
13668 let zero_dense = || DenseFfn {
13669 gate_proj: matrix(vec![0.0; 4]),
13670 up_proj: matrix(vec![0.0; 4]),
13671 down_proj: matrix(vec![0.0; 4]),
13672 act: Act::Silu,
13673 down_t: None,
13674 segs: Vec::new(),
13675 };
13676 let shared = DenseFfn {
13677 gate_proj: identity(),
13678 up_proj: identity(),
13679 down_proj: identity(),
13680 act: Act::Silu,
13681 down_t: None,
13682 segs: Vec::new(),
13683 };
13684 let x = [1.0, 2.0];
13685 let expected = dense_ffn(&shared, &x, None);
13686 let moe = MoeFfn {
13687 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
13688 experts: vec![zero_dense()],
13689 top_k: 1,
13690 norm_topk_prob: true,
13691 router_sigmoid: true,
13692 expert_bias: None,
13693 routed_scaling: 1.0,
13694 route_tau: None,
13695 shared: Some((shared, None)),
13696 stats: std::cell::RefCell::new(Vec::new()),
13697 act_sq: std::cell::RefCell::new(Vec::new()),
13698 act_rows: std::cell::RefCell::new(Vec::new()),
13699 mask: None,
13700 per_expert_scale: None,
13701 router_input_norm: false,
13702 resonance: None,
13703 };
13704 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
13705 for (actual, expected) in actual.iter().zip(expected) {
13706 assert!((actual - expected).abs() < 1e-6);
13707 }
13708 }
13709
13710 #[test]
13711 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
13712 const B: usize = 19;
13713 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13714 p.set_o1(Some(crate::nystrom::O1Cfg {
13715 layers: crate::nystrom::O1Layers::All,
13716 m: 4,
13717 w: 8,
13718 sink: 2,
13719 rect: crate::nystrom::O1Rect::Aggregate,
13720 }));
13721 p.o1_begin_with_prefix(Some(B));
13722 let ids: Vec<u32> = (0..B as u32).collect();
13723 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
13724
13725 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
13726 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
13727 let next = p.embed_single(B as u32);
13728 let _ = p.forward_layers(&next, B, None);
13729 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
13730 }
13731
13732 #[test]
13733 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
13734 const B: usize = 19;
13735 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13736 let gdn_cfg = crate::linear_core::GdnCfg {
13740 num_v_heads: 2,
13741 num_k_heads: 1,
13742 key_head_dim: 2,
13743 value_head_dim: 4,
13744 conv_kernel: 3,
13745 hidden_size: 8,
13746 rms_eps: 1e-6,
13747 output_gate_sigmoid: false,
13748 };
13749 let synth = |n: usize, salt: usize| -> Vec<f32> {
13750 (0..n)
13751 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
13752 .collect()
13753 };
13754 let qt = |rows: usize, cols: usize, salt: usize| {
13755 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
13756 };
13757 let c_dim = gdn_cfg.conv_dim();
13758 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
13759 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
13760 in_proj_qkv: qt(c_dim, 8, 1),
13761 in_proj_z: qt(vd, 8, 2),
13762 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
13763 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
13764 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
13765 a_log: vec![0.2, 0.5],
13766 dt_bias: synth(gdn_cfg.num_v_heads, 6),
13767 norm: vec![1.0; gdn_cfg.value_head_dim],
13768 out_proj: qt(8, vd, 7),
13769 });
13770 p.gdn_cfg = Some(gdn_cfg);
13771 p.set_o1(Some(crate::nystrom::O1Cfg {
13772 layers: crate::nystrom::O1Layers::All,
13773 m: 4,
13774 w: 8,
13775 sink: 2,
13776 rect: crate::nystrom::O1Rect::Aggregate,
13777 }));
13778 p.o1_begin_with_prefix(Some(B));
13779 for pos in 0..B - 2 {
13780 let emb = p.embed_single(pos as u32);
13781 let _ = p.forward_layers(&emb, pos, None);
13782 }
13783 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
13784
13785 let e1 = p.embed_single((B - 2) as u32);
13786 let e2 = p.embed_single((B - 1) as u32);
13787 let _ = p.forward_pair(&e1, &e2, B - 2);
13788
13789 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
13790 assert!(
13791 p.kv_cache
13792 .layers
13793 .iter()
13794 .enumerate()
13795 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
13796 );
13797 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
13798 assert_ne!(
13799 p.kv_cache.layers[0].linear_state, lane1_state,
13800 "real pair must commit GDN lane 2 before returning"
13801 );
13802 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
13803 let next = p.embed_single(B as u32);
13804 let _ = p.forward_layers(&next, B, None);
13805 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
13806 }
13807
13808 #[test]
13809 fn o1_error_observation_stays_terminal_until_reset() {
13810 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13811 p.set_o1(Some(crate::nystrom::O1Cfg {
13812 layers: crate::nystrom::O1Layers::All,
13813 m: 4,
13814 w: 8,
13815 sink: 2,
13816 rect: crate::nystrom::O1Rect::Aggregate,
13817 }));
13818 p.o1_begin();
13819 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
13820
13821 assert!(p.o1_seal_checked().is_err());
13822 assert!(
13823 p.o1_seal_checked().is_err(),
13824 "retry must see the sticky error"
13825 );
13826 let k = vec![0.2f32; 4];
13827 let v = vec![0.3f32; 4];
13828 p.kv_cache.layers[0].append(&k, &v, &[]);
13829 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
13830
13831 p.reset_session();
13832 p.o1_begin();
13833 p.kv_cache.layers[0].append(&k, &v, &[]);
13834 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
13835 }
13836
13837 #[test]
13838 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
13839 let ids = vec![1u32, 2, 3, 4, 5, 6];
13840 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13841 p.graph_logits = Some(vec![123.0]);
13842 p.graph_want_logits = true;
13843 p.graph_failed
13844 .store(true, std::sync::atomic::Ordering::Relaxed);
13845 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13846 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
13847 assert!(err.contains("before NLL"));
13848 assert!(p.graph_logits.is_none());
13849 assert!(!p.graph_want_logits);
13850 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13851 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13852
13853 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13854 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
13855 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
13856 assert_eq!(actual.1, expected.1);
13857 assert!((actual.0 - expected.0).abs() < 1e-9);
13858 }
13859
13860 #[test]
13861 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
13862 let ids = vec![1u32, 2, 3, 4, 5, 6];
13863 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13864 p.nll_test_fail_at = Some(1);
13865 let err = p
13866 .nll_ids_from(&ids, 0)
13867 .expect_err("one-shot forward failure");
13868 assert!(err.contains("forward") || err.contains("score row"));
13869 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13870 assert!(!p.graph_want_logits);
13871 assert!(p.graph_logits.is_none());
13872 assert!(p.kv_history.is_empty());
13873
13874 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13875 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
13876 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
13877 assert_eq!(actual.1, expected.1);
13878 assert!((actual.0 - expected.0).abs() < 1e-9);
13879 }
13880
13881 #[test]
13882 fn nll_serial_failure_before_first_row_is_reported() {
13883 let ids = vec![1u32, 2, 3, 4];
13884 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13885 p.nll_test_force_serial = true;
13886 p.nll_test_fail_at = Some(0);
13887 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
13888 assert!(err.contains("serial forward"));
13889 assert!(p.kv_history.is_empty());
13890 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13891 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13892 }
13893
13894 #[test]
13895 fn ffn_probe_failure_discards_recorder_and_state() {
13896 let ids = vec![1u32, 2, 3, 4];
13897 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13898 p.nll_test_fail_at = Some(0);
13899 let err = p
13900 .probe_ffn_mass_batch(&ids)
13901 .expect_err("probe forward failure");
13902 assert!(err.contains("NLL"));
13903 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
13904 assert!(p.kv_history.is_empty());
13905 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13906 }
13907
13908 #[test]
13909 fn nll_test_controls_are_pipeline_scoped() {
13910 let ids = vec![1u32, 2, 3, 4];
13911 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13912 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13913 failing.nll_test_force_serial = true;
13914 failing.nll_test_fail_at = Some(0);
13915
13916 assert!(!failing.can_prefill_batched());
13917 assert!(unaffected.can_prefill_batched());
13918 let expected = unaffected
13919 .nll_ids_from(&ids, 0)
13920 .expect("unaffected pipeline remains usable");
13921 let err = failing
13922 .nll_ids_from(&ids, 0)
13923 .expect_err("failure injection belongs to failing pipeline");
13924 assert!(err.contains("serial forward"));
13925 assert!(failing.nll_test_fail_at.is_none());
13926 assert!(unaffected.can_prefill_batched());
13927 let actual = unaffected
13928 .nll_ids_from(&ids, 0)
13929 .expect("unaffected pipeline remains reusable");
13930 assert_eq!(actual.1, expected.1);
13931 assert!((actual.0 - expected.0).abs() < 1e-9);
13932 }
13933
13934 #[test]
13935 fn forward_ids_failure_channel_is_terminal_and_reusable() {
13936 let ids = vec![1u32, 2, 3, 4, 5, 6];
13937 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13938 p.graph_logits = Some(vec![123.0]);
13939 p.graph_want_logits = true;
13940 p.graph_failed
13941 .store(true, std::sync::atomic::Ordering::Relaxed);
13942 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13943
13944 let err = p
13945 .forward_ids(&ids, None)
13946 .expect_err("a failed forward must not become a valid head result");
13947 assert!(err.contains("forward_ids setup"));
13948 assert!(p.graph_logits.is_none());
13949 assert!(!p.graph_want_logits);
13950 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13951 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13952 assert_eq!(p.kv_cache.seq_len(), 0);
13953
13954 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
13955 .forward_ids(&ids, None)
13956 .expect("fresh forward_ids");
13957 let actual = p
13958 .forward_ids(&ids, None)
13959 .expect("pipeline remains reusable after a failed forward");
13960 assert_eq!(actual.len(), expected.len());
13961 assert!(
13962 actual
13963 .iter()
13964 .zip(expected)
13965 .all(|(a, b)| (a - b).abs() < 1e-9)
13966 );
13967 assert_eq!(p.kv_cache.seq_len(), ids.len());
13968 }
13969}