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 qwen4_exp: Option<
120 Box<(
121 crate::qwen4_exp::Globals,
122 Vec<crate::qwen4_exp::Layer>,
123 crate::qwen4_exp::Cfg,
124 crate::qwen4_exp::State,
125 )>,
126 >,
127 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
131 pub dspark: Option<crate::dsv4::DsparkState>,
133 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
136 pub dspark_hist: Vec<usize>,
138 pub dspark_real: Vec<u32>,
142 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
146 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
148 pub dspark_draft_ns: u128,
152 pub short_conv_cfg: Option<ShortConvCfg>,
155 pub mtp: Option<MtpModule>,
157 pub speculative: bool,
159 rng: SplitMix64,
160 sampler_scratch: SamplerScratch,
161 spec_forced: Option<u32>,
167 spec_q: Vec<Vec<f32>>,
168 spec_p: Vec<f32>,
169 spec_res: Vec<f32>,
170 spec_qs: Vec<sampler::Sparse>,
172 spec_ps: sampler::Sparse,
173 spec_ress: sampler::Sparse,
174 mtp_graph_mode: Option<bool>,
181 #[cfg(target_os = "macos")]
184 metal_verify: Option<MetalVerifyPending>,
185 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
189 ws: ForwardScratch,
193 pool: Option<std::sync::Arc<Pool>>,
195 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
199 pub(crate) dyn_force_f32: bool,
201 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
206 pub(crate) dyn_active: Option<usize>,
212 pub(crate) dyn_blend_loaded: bool,
216 pub(crate) dyn_phi_layer: Option<usize>,
219 dyn_phi_ema: Vec<f32>,
221 dyn_phi_seen: usize,
222 pub dyn_router: Option<crate::swarm::DynRouter>,
225 o1_cfg: Option<crate::nystrom::O1Cfg>,
228 o1_epoch: u64,
231 o1_flags: Vec<bool>,
233 trace: bool,
236 calib_temp: f32,
239 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
241 graph_kv_id: u64,
242 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
245 graph_want_logits: bool,
246 graph_logits: Option<Vec<f32>>,
249 pub embed_multiplier: f32,
251 pub attn_scale: f32,
254 pub swa: Option<(usize, usize)>,
257 pub sliding_layers: Option<Vec<bool>>,
260 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
263 pub rotary_dim_local: Option<usize>,
264 pub rope_scale: f32,
265 pub rope_scale_local: f32,
266 pub global_attn: Option<(usize, usize)>,
269 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
272 pub attn_v_norm: bool,
274 pub final_softcap: Option<f32>,
276 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
280 pub attn_softcap: f32,
282 confidence_on: bool,
286 #[cfg(test)]
289 nll_test_fail_at: Option<usize>,
290 #[cfg(test)]
293 nll_test_force_serial: bool,
294}
295
296#[cfg(target_os = "macos")]
297impl Drop for Pipeline {
298 fn drop(&mut self) {
299 crate::gpu::kv_mirror_drop(self.graph_kv_id);
300 }
301}
302
303pub struct PipelineWeights {
308 pub embed_tokens: QTensor,
310 pub layers: Vec<LayerWeights>,
312 pub lm_head: QTensor,
314 pub final_norm: Vec<f32>,
316}
317
318pub struct LayerWeights {
320 pub input_norm: Vec<f32>,
321 pub post_norm: Vec<f32>,
324 pub attn_out_norm: Option<Vec<f32>>,
327 pub layer_scale: Option<f32>,
329 pub ffn_out_norm: Option<Vec<f32>>,
332 pub ffn: FfnKind,
333 pub attn: AttnKind,
334}
335
336#[derive(Clone, Copy, PartialEq, Debug, Default)]
339pub enum Act {
340 #[default]
341 Silu,
342 GeluTanh,
343 Situ {
346 beta: f32,
347 linear_beta: f32,
348 },
349}
350
351impl Act {
352 pub fn from_arch(name: &str) -> Self {
353 if name == "gelu_tanh" {
354 Self::GeluTanh
355 } else {
356 Self::Silu
357 }
358 }
359
360 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
362 match arch.hidden_act.as_str() {
363 "situ" => Self::Situ {
364 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
365 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
366 },
367 other => Self::from_arch(other),
368 }
369 }
370
371 #[inline]
372 pub fn apply(self, x: f32) -> f32 {
373 match self {
374 Self::Silu => inference::silu(x),
375 Self::GeluTanh => inference::gelu_tanh(x),
376 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
377 }
378 }
379
380 #[inline]
383 pub fn combine(self, g: f32, u: f32) -> f32 {
384 match self {
385 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
386 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
387 }
388 _ => self.apply(g) * u,
389 }
390 }
391}
392
393pub struct DenseFfn {
395 pub gate_proj: QTensor,
396 pub up_proj: QTensor,
397 pub down_proj: QTensor,
398 pub act: Act,
400 pub down_t: Option<QTensor>,
406 pub segs: Vec<FfnSeg>,
413}
414
415pub struct FfnSeg {
420 pub gate: QTensor,
421 pub up: QTensor,
422 pub down: QTensor,
423 pub start: usize,
424 pub width: usize,
425}
426
427pub enum FfnKind {
430 Dense(DenseFfn),
431 Moe(MoeFfn),
435 DenseMoe(Box<DenseMoeFfn>),
442}
443
444pub struct DenseMoeFfn {
446 pub dense: DenseFfn,
447 pub moe: MoeFfn,
448 pub post_norm_1: Vec<f32>,
450 pub pre_norm_2: Vec<f32>,
453 pub post_norm_2: Vec<f32>,
455}
456
457pub struct MoeFfn {
458 pub router: QTensor,
460 pub experts: Vec<DenseFfn>,
461 pub top_k: usize,
462 pub norm_topk_prob: bool,
463 pub router_sigmoid: bool,
466 pub expert_bias: Option<Vec<f32>>,
470 pub routed_scaling: f32,
473 pub route_tau: Option<f32>,
479 pub shared: Option<(DenseFfn, Option<QTensor>)>,
482 pub stats: std::cell::RefCell<Vec<u64>>,
486 pub act_sq: std::cell::RefCell<Vec<f64>>,
493 pub act_rows: std::cell::RefCell<Vec<f32>>,
499 pub mask: Option<Vec<bool>>,
504 pub per_expert_scale: Option<Vec<f32>>,
507 pub router_input_norm: bool,
511 pub resonance: Option<Resonance>,
515}
516
517pub struct Resonance {
519 pub mu: Vec<f32>,
521 pub u: Vec<f32>,
523 pub k: usize,
524 pub bias: Vec<f32>,
526}
527
528impl Resonance {
529 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
531 let h = x.len();
532 let ne = out.len();
533 for e in 0..ne {
534 let mu = &self.mu[e * h..(e + 1) * h];
535 let mut d2 = 0.0f32;
536 for j in 0..h {
537 let d = x[j] - mu[j];
538 d2 += d * d;
539 }
540 let mut proj = 0.0f32;
541 for i in 0..self.k {
542 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
543 let mut p = 0.0f32;
544 for j in 0..h {
545 p += (x[j] - mu[j]) * u[j];
546 }
547 proj += p * p;
548 }
549 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
550 }
551 }
552}
553
554pub enum AttnKind {
557 Full {
559 wq: QTensor,
560 wk: QTensor,
561 wv: QTensor,
562 wo: QTensor,
563 q_norm: Option<Vec<f32>>,
564 k_norm: Option<Vec<f32>>,
565 output_gate: bool,
566 softplus_gate: Option<(QTensor, bool)>,
570 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
572 },
573 Linear(VmfPhaseWeights),
575 LinearGdn(GdnWeights),
577 ShortConv(ShortConvWeights),
580 Mla(Box<MlaWeights>),
588 Kda(Box<crate::linear_core::KdaWeights>),
592}
593
594pub struct MlaWeights {
596 pub q_proj: QTensor,
600 pub q_a: Option<QTensor>,
603 pub q_a_norm: Option<Vec<f32>>,
604 pub kv_a: QTensor,
606 pub kv_a_norm: Vec<f32>,
608 pub kv_b: QTensor,
610 pub o_proj: QTensor,
612 pub nh: usize,
613 pub qk_rope: usize,
614 pub qk_nope: usize,
615 pub v_dim: usize,
616 pub lora: usize,
617 pub scale: f32,
619 pub nope: bool,
621}
622
623pub struct MtpModule {
628 pub enorm: Vec<f32>,
629 pub hnorm: Vec<f32>,
630 pub eh_proj: QTensor,
632 pub layer: LayerWeights,
633 pub final_norm: Vec<f32>,
634 pub kv: crate::kv_cache::LayerKvCache,
635}
636
637#[cfg(target_os = "macos")]
644enum MetalRowsItem<'a> {
645 Gdn {
646 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
647 first: usize,
648 },
649 Attn {
650 l: crate::gpu_metal::AttnGpuLayer<'a>,
651 li: usize,
652 q_norm: Option<&'a [f32]>,
653 k_norm: Option<&'a [f32]>,
654 output_gate: bool,
655 },
656}
657
658#[cfg(target_os = "macos")]
659struct MetalVerifyPending {
660 graph: crate::gpu_metal::VerifyGraph,
661 gdn_layers: Vec<usize>,
662 attn_layers: Vec<(usize, usize)>,
663}
664
665#[derive(Clone, Copy)]
669enum SpecTrial {
670 Spec {
671 t0: std::time::Instant,
672 gen0: usize,
673 rounds: usize,
674 },
675 Plain {
676 t0: std::time::Instant,
677 gen0: usize,
678 },
679 Decided {
680 spec: bool,
681 recheck_at: usize,
682 },
683}
684
685#[derive(Default, Clone, Copy)]
696struct SpecMon {
697 round_ms: f64,
698 tokens: f64,
699 plain_ms: f64,
700 n: u32,
701 fails: u32,
702}
703
704impl SpecMon {
705 fn round(&mut self, dt_ms: f64, produced: usize) {
706 self.n += 1;
707 if self.n == 1 {
708 return; }
710 let a = if self.n == 2 { 1.0 } else { 0.3 };
711 self.round_ms += a * (dt_ms - self.round_ms);
712 self.tokens += a * (produced as f64 - self.tokens);
713 }
714 fn pays(&self) -> bool {
715 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
716 }
717}
718
719pub struct GenerateResult {
721 pub text: String,
722 pub token_ids: Vec<u32>,
723 pub prompt_tokens: usize,
724 pub tokens_generated: usize,
725 pub finish_reason: String,
726 pub mtp_drafted: usize,
728 pub mtp_accepted: usize,
729 pub token_confidence: Vec<f32>,
734 pub traces: Vec<TokenTrace>,
737}
738
739#[derive(Clone, Debug)]
744pub struct TokenTrace {
745 pub t: usize,
747 pub token_id: u32,
749 pub confidence: f32,
751 pub active_skill: Option<String>,
753 pub recon: Option<f32>,
757 pub switched: bool,
760}
761
762#[cfg_attr(not(test), allow(dead_code))]
767fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
768 let t = if temp > 1e-3 { temp } else { 1.0 };
769 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
770 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
771 if sum > 0.0 {
772 (((logits[id as usize] - max) / t).exp()) / sum
773 } else {
774 0.0
775 }
776}
777
778fn prefill_batched() -> bool {
781 std::env::var("CMF_PREFILL")
782 .map(|v| v != "seq")
783 .unwrap_or(true)
784}
785
786#[derive(Clone, Copy)]
790enum PrefillIn<'a> {
791 Ids(&'a [u32]),
792 Hidden(&'a [f32]),
793}
794
795impl Pipeline {
802 fn can_prefill_batched(&self) -> bool {
803 #[cfg(test)]
804 let force_serial = self.nll_test_force_serial;
805 #[cfg(not(test))]
806 let force_serial = false;
807 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
808 }
809
810 fn automatic_gpu_prefix(&self) -> Option<usize> {
813 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
814 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
815 }
816}
817
818pub fn prefill_chunk() -> usize {
825 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
826 .ok()
827 .and_then(|v| v.parse::<usize>().ok())
828 {
829 return n.max(1);
830 }
831 if cfg!(target_os = "macos") {
832 512
833 } else if cfg!(target_arch = "aarch64") {
834 256
837 } else {
838 48
839 }
840}
841
842#[inline]
848fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
849 if end <= start || start >= input_len {
850 return 0;
851 }
852 let rows = (end.min(input_len) - start).min(input_len - start);
853 if end < input_len {
854 rows
855 } else {
856 rows.saturating_sub(1)
857 }
858}
859
860pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
862
863impl Pipeline {
864 fn clear_sequence_state(&mut self) {
872 self.kv_cache.clear();
873 self.kv_history.clear();
874 crate::gpu::graph_kv_reset(self.graph_kv_id);
875 crate::gpu::graph_kv_reset(self.mtp_kv_id());
880 }
881
882 fn finish_generation(
888 &mut self,
889 mtp: &mut Option<MtpModule>,
890 router: &mut Option<crate::swarm::DynRouter>,
891 clear_sequence: bool,
892 ) {
893 if router.is_some() {
897 let _ = self.set_active_skill(None);
898 }
899 if clear_sequence {
900 self.clear_sequence_state();
901 if let Some(m) = mtp.as_mut() {
902 m.kv.clear();
908 }
909 if let Some(m) = self.mtp.as_mut() {
910 m.kv.clear();
914 }
915 }
916 self.graph_want_logits = false;
917 self.graph_logits = None;
918 self.graph_failed
919 .store(false, std::sync::atomic::Ordering::Relaxed);
920 self.cancel
921 .store(false, std::sync::atomic::Ordering::Relaxed);
922 self.dyn_router = router.take().or(self.dyn_router.take());
923 self.mtp = mtp.take().or(self.mtp.take());
924 self.mtp_graph_mode = None;
925 self.spec_forced = None;
926 }
927
928 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
932 if self
933 .graph_failed
934 .swap(false, std::sync::atomic::Ordering::Relaxed)
935 {
936 self.cancel
937 .store(false, std::sync::atomic::Ordering::Relaxed);
938 self.clear_sequence_state();
939 self.graph_logits = None;
940 self.graph_want_logits = false;
941 return Err(format!("GPU graph failed during {phase} at position {pos}"));
942 }
943 Ok(())
944 }
945
946 fn nll_begin(&mut self) -> Result<(), String> {
951 if self
952 .graph_failed
953 .swap(false, std::sync::atomic::Ordering::Relaxed)
954 {
955 self.cancel
956 .store(false, std::sync::atomic::Ordering::Relaxed);
957 self.clear_sequence_state();
958 self.graph_logits = None;
959 self.graph_want_logits = false;
960 return Err("GPU graph failed before NLL scoring".to_string());
961 }
962 self.clear_sequence_state();
963 self.graph_logits = None;
964 self.graph_want_logits = false;
965 Ok(())
966 }
967
968 fn nll_end(&mut self) {
972 self.clear_sequence_state();
973 self.graph_logits = None;
974 self.graph_want_logits = false;
975 self.graph_failed
976 .store(false, std::sync::atomic::Ordering::Relaxed);
977 }
978
979 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
982 #[cfg(test)]
983 if self.nll_test_fail_at == Some(pos) {
984 self.nll_test_fail_at = None;
985 self.graph_failed
986 .store(true, std::sync::atomic::Ordering::Relaxed);
987 self.cancel
988 .store(true, std::sync::atomic::Ordering::Relaxed);
989 }
990 if self
991 .graph_failed
992 .swap(false, std::sync::atomic::Ordering::Relaxed)
993 {
994 self.cancel
995 .store(false, std::sync::atomic::Ordering::Relaxed);
996 self.clear_sequence_state();
997 self.graph_logits = None;
998 self.graph_want_logits = false;
999 return Err(format!(
1000 "GPU graph failed during NLL {phase} at position {pos}"
1001 ));
1002 }
1003 Ok(())
1004 }
1005
1006 #[inline]
1010 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1011 virtual_idx % self.physical_layers
1012 }
1013
1014 #[inline]
1017 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1018 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1019 }
1020
1021 #[allow(clippy::too_many_arguments)]
1023
1024 #[cfg(target_os = "macos")]
1043 fn graph_prefill_preferred(&self) -> bool {
1044 if !crate::gpu::enabled_here()
1045 || !crate::gpu::q1_force()
1046 || std::env::var("CMF_GPU_BLOCK")
1047 .map(|v| v == "0")
1048 .unwrap_or(false)
1049 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1052 {
1053 return false;
1054 }
1055 self.weights
1056 .layers
1057 .iter()
1058 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
1059 }
1060
1061 #[cfg(not(target_os = "macos"))]
1062 fn graph_prefill_preferred(&self) -> bool {
1063 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1071 if !graph_on || !crate::gpu::enabled_here() {
1072 return false;
1073 }
1074 if self.o1_active() {
1083 return false;
1084 }
1085 self.weights
1086 .layers
1087 .iter()
1088 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1089 }
1090
1091 #[cfg(target_os = "macos")]
1092 fn q1_graph_gpu(
1093 &mut self,
1094 start: usize,
1095 upto: Option<usize>,
1096 position: usize,
1097 h: &mut [f32],
1098 ) -> usize {
1099 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
1101 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
1103 || !crate::gpu::q1_force()
1104 || std::env::var("CMF_GPU_BLOCK")
1105 .map(|v| v == "0")
1106 .unwrap_or(false)
1107 {
1108 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1109 eprintln!(
1110 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
1111 self.attn_softcap > 0.0,
1112 crate::gpu::enabled_here(),
1113 crate::gpu::q1_force(),
1114 );
1115 }
1116 return start;
1117 }
1118 if self.swa.is_some()
1122 || self.global_attn.is_some()
1123 || self.attention_heads_per_layer.is_some()
1124 || self.attn_v_norm
1125 || self.weights.layers.iter().any(|lw| {
1126 lw.attn_out_norm.is_some()
1127 || lw.ffn_out_norm.is_some()
1128 || lw.layer_scale.is_some()
1129 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
1130 })
1131 {
1132 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1133 eprintln!(
1134 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
1135 self.swa.is_some(),
1136 self.global_attn.is_some(),
1137 self.attention_heads_per_layer.is_some(),
1138 self.attn_v_norm,
1139 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
1140 );
1141 }
1142 return start;
1143 }
1144 let limit = upto
1147 .map(|u| u + 1)
1148 .unwrap_or(self.num_layers)
1149 .min(self.num_layers);
1150
1151 enum Item<'a> {
1152 Gdn {
1153 run: Vec<GdnGpuLayer<'a>>,
1154 first: usize,
1155 },
1156 Attn {
1157 l: AttnGpuLayer<'a>,
1158 li: usize,
1159 q_norm: Option<&'a [f32]>,
1160 k_norm: Option<&'a [f32]>,
1161 output_gate: bool,
1162 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1163 full_gpu: bool,
1166 },
1167 }
1168
1169 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
1176 let attend_contract = attend_mode != "0"
1177 && attend_mode != "off"
1178 && self.head_dim % 4 == 0
1179 && self.head_dim <= 256
1180 && self.rotary_dim >= 2
1181 && self.rotary_dim <= self.head_dim
1182 && (self.rotary_dim / 2) % 32 == 0
1183 && self.num_kv_heads > 0
1184 && self.num_heads % self.num_kv_heads == 0;
1185
1186 let mut plan: Vec<Item> = Vec::new();
1187 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
1188 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
1190 let mut scan = start;
1191 while scan < limit {
1192 let lw = &self.weights.layers[self.phys_layer(scan)];
1193 let ffn = match &lw.ffn {
1194 FfnKind::Dense(d) if d.segs.is_empty() => {
1195 let (Some(g), Some(u), Some(dn)) = (
1196 d.gate_proj.q1_parts(),
1197 d.up_proj.q1_parts(),
1198 d.down_proj.q1_parts(),
1199 ) else {
1200 if block_diag {
1201 eprintln!(
1202 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1203 );
1204 }
1205 break;
1206 };
1207 MetalFfn::Dense {
1208 gate: g,
1209 up: u,
1210 down: dn,
1211 }
1212 }
1213 FfnKind::Moe(m) => {
1214 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1215 if block_diag {
1216 eprintln!(
1217 "block-graph: L{scan} MoE outside the graph contract — run ends"
1218 );
1219 }
1220 break;
1221 };
1222 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1223 model_ref.get_or_insert_with(|| model.clone());
1224 }
1225 MetalFfn::Moe(moe)
1226 }
1227 _ => {
1228 if block_diag {
1229 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1230 }
1231 break;
1232 }
1233 };
1234 match &lw.attn {
1235 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1236 let parts = (
1237 w.in_proj_qkv.q1_parts(),
1238 w.in_proj_z.q1_parts(),
1239 w.in_proj_a.f32_parts(),
1240 w.in_proj_b.f32_parts(),
1241 w.out_proj.q1_parts(),
1242 );
1243 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1244 if block_diag {
1245 eprintln!(
1246 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1247 w.in_proj_qkv.q1_parts().is_some(),
1248 w.in_proj_z.q1_parts().is_some(),
1249 w.in_proj_a.f32_parts().is_some(),
1250 w.in_proj_b.f32_parts().is_some(),
1251 w.out_proj.q1_parts().is_some(),
1252 );
1253 }
1254 break;
1255 };
1256 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1257 model_ref.get_or_insert_with(|| model.clone());
1258 }
1259 let gl = GdnGpuLayer {
1260 attn_norm: &lw.input_norm,
1261 post_norm: &lw.post_norm,
1262 qkv,
1263 z,
1264 a,
1265 b,
1266 out,
1267 ffn,
1268 conv1d: &w.conv1d,
1269 a_log: &w.a_log,
1270 dt_bias: &w.dt_bias,
1271 gnorm: &w.norm,
1272 };
1273 match plan.last_mut() {
1274 Some(Item::Gdn { run, .. }) => run.push(gl),
1275 _ => plan.push(Item::Gdn {
1276 run: vec![gl],
1277 first: scan,
1278 }),
1279 }
1280 }
1281 AttnKind::Full {
1282 wq,
1283 wk,
1284 wv,
1285 wo,
1286 q_norm,
1287 k_norm,
1288 output_gate,
1289 softplus_gate: None,
1290 bias,
1291 } if !self.kv_cache.layers[scan].o1_sealed()
1292 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1297 {
1298 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1299 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1300 break;
1301 };
1302 if let QTensor::Mapped { model, .. } = wq {
1303 model_ref.get_or_insert_with(|| model.clone());
1304 }
1305 let cache = &self.kv_cache.layers[scan];
1306 let o1_metal = cache.o1.is_some()
1310 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1311 && cache.o1_views().is_some();
1312 let full_gpu = attend_contract
1313 && cache.mode == crate::kv_cache::KvMode::F32
1314 && (cache.o1.is_none() || o1_metal)
1315 && bias.is_none()
1316 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1317 && pk.1 == self.num_kv_heads * self.head_dim
1318 && pv.1 == self.num_kv_heads * self.head_dim
1319 && po.2 == self.num_heads * self.head_dim;
1320 plan.push(Item::Attn {
1321 l: AttnGpuLayer {
1322 attn_norm: &lw.input_norm,
1323 post_norm: &lw.post_norm,
1324 wq: pq,
1325 wk: pk,
1326 wv: pv,
1327 wo: po,
1328 ffn,
1329 },
1330 li: scan,
1331 q_norm: q_norm.as_deref(),
1332 k_norm: k_norm.as_deref(),
1333 output_gate: *output_gate,
1334 bias: bias
1335 .as_ref()
1336 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1337 full_gpu,
1338 });
1339 }
1340 _ => break,
1341 }
1342 scan += 1;
1343 }
1344 let Some(model) = model_ref else {
1345 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1346 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1347 }
1348 return start;
1349 };
1350 if plan.is_empty() {
1351 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1352 eprintln!("q1-graph: empty plan at layer {start}");
1353 }
1354 return start;
1355 }
1356 let has_moe = plan.iter().any(|it| match it {
1357 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1358 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1359 });
1360 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1361 let dev_attend = attend_contract
1362 && (self.head_dim <= 128
1363 || has_moe
1364 || (self.head_dim <= 256 && has_gdn)
1370 || attend_mode == "force"
1371 || attend_mode == "256");
1372 if !dev_attend {
1373 for it in &mut plan {
1374 if let Item::Attn { li, full_gpu, .. } = it {
1375 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1378 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1379 if !keep_o1 {
1380 *full_gpu = false;
1381 }
1382 }
1383 }
1384 }
1385 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1386 use std::sync::atomic::{AtomicBool, Ordering};
1387 static SAID: AtomicBool = AtomicBool::new(false);
1388 if !SAID.swap(true, Ordering::Relaxed) {
1389 let fg = plan
1390 .iter()
1391 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1392 .count();
1393 let att = plan
1394 .iter()
1395 .filter(|it| matches!(it, Item::Attn { .. }))
1396 .count();
1397 eprintln!(
1398 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1399 plan.len(),
1400 self.head_dim,
1401 self.rotary_dim,
1402 self.num_kv_heads,
1403 self.num_heads,
1404 );
1405 }
1406 }
1407 let dims = GraphDims {
1408 hidden: self.hidden_size,
1409 eps: self.rms_eps as f32,
1410 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1411 };
1412 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1413 return start;
1414 };
1415 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1416 nv: cfg.num_v_heads,
1417 nk: cfg.num_k_heads,
1418 dk: cfg.key_head_dim,
1419 dv: cfg.value_head_dim,
1420 kk: cfg.conv_kernel,
1421 hidden: self.hidden_size,
1422 inter: self.intermediate_size,
1423 c_dim: cfg.conv_dim(),
1424 eps: cfg.rms_eps as f32,
1425 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1426 });
1427 let mut valid = 0usize;
1431 let mut end = start;
1432 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1434 static ONCE: std::sync::Once = std::sync::Once::new();
1435 ONCE.call_once(|| {
1436 for it in &plan {
1437 match it {
1438 Item::Gdn { first, run } => {
1439 eprintln!("plan: Gdn first={first} len={}", run.len())
1440 }
1441 Item::Attn { li, full_gpu, .. } => {
1442 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1443 }
1444 }
1445 }
1446 });
1447 }
1448 for item in &plan {
1449 let ok = match item {
1450 Item::Gdn { run, .. } => gcfg
1451 .as_ref()
1452 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1453 .unwrap_or(false),
1454 Item::Attn { l, .. } => graph.attn_ok(l),
1455 };
1456 if !ok {
1457 if block_diag {
1458 eprintln!(
1459 "block-graph: plan item {} ({}) failed graph preflight",
1460 valid,
1461 match item {
1462 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1463 Item::Attn { li, .. } => format!("Attn L{li}"),
1464 }
1465 );
1466 }
1467 break;
1468 }
1469 valid += 1;
1470 end += match item {
1471 Item::Gdn { run, .. } => run.len(),
1472 Item::Attn { .. } => 1,
1473 };
1474 }
1475 plan.truncate(valid);
1476 if plan.is_empty() {
1477 return start;
1478 }
1479
1480 let inv_freq = self.inv_freq.clone();
1481 let pool = self.pool.clone();
1482 let (nh, nkv, hd, hs, rd, eps) = (
1483 self.num_heads,
1484 self.num_kv_heads,
1485 self.head_dim,
1486 self.hidden_size,
1487 self.rotary_dim,
1488 self.rms_eps,
1489 );
1490 let norm_style = self.norm_style;
1491 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1492 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1493 let kv_id = self.graph_kv_id;
1494 let mut pending: Vec<(usize, usize)> = Vec::new();
1497 let mut dev_attn: Vec<usize> = Vec::new();
1500 for item in &plan {
1501 let _xt0 = std::time::Instant::now();
1502 let _xkind: u32 = match item {
1503 Item::Gdn { .. } => 2,
1504 Item::Attn { .. } => 3,
1505 };
1506 if self.loop_final_norm {
1508 let item_start = match item {
1509 Item::Gdn { first, .. } => *first,
1510 Item::Attn { li, .. } => *li,
1511 };
1512 if item_start > start && self.is_loop_end(item_start - 1) {
1513 graph.encode_loop_norm(&self.weights.final_norm);
1514 }
1515 }
1516 match item {
1517 Item::Gdn { run, first } => {
1518 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1519 if l.linear_state.len() != want {
1520 l.linear_state = vec![0f32; want];
1521 }
1522 }
1523 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1524 .iter()
1525 .map(|l| l.linear_state.as_slice())
1526 .collect();
1527 let _ig = std::time::Instant::now();
1528 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1529 tracing::error!("q1 graph: GDN run refused after validation");
1531 return start;
1532 }
1533 graph.commit_kind = 2;
1536 graph.commit();
1537 crate::gpu::stageprof(0, _ig.elapsed());
1538 pending.push((*first, run.len()));
1539 }
1540 Item::Attn {
1541 l,
1542 li,
1543 q_norm,
1544 k_norm,
1545 output_gate,
1546 bias,
1547 full_gpu,
1548 } => {
1549 let _ia = std::time::Instant::now();
1550 if *full_gpu {
1552 let cache = &self.kv_cache.layers[*li];
1553 let o1p = if cache.o1.is_some() {
1554 match cache.o1_views() {
1555 Some(views) => Some(crate::gpu::O1AttnParams {
1556 views,
1557 epoch: self.o1_epoch,
1558 }),
1559 None => None,
1561 }
1562 } else {
1563 None
1564 };
1565 let o1_layer = cache.o1.is_some();
1566 if o1_layer && o1p.is_none() {
1567 }
1569 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1570 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1571 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1572 let p = crate::gpu::AttnDeviceParams {
1573 kv_id,
1574 layer: *li,
1575 nh,
1576 nkv,
1577 hd,
1578 rd,
1579 position,
1580 scale: self.attn_scale,
1581 eps: eps as f32,
1582 gemma,
1583 output_gate: *output_gate,
1584 q_norm: *q_norm,
1585 k_norm: *k_norm,
1586 inv_freq: &inv_freq,
1587 cpu_k,
1588 cpu_v,
1589 cpu_stored,
1590 o1: o1p,
1591 };
1592 let o1_bad = o1_layer && p.o1.is_none();
1593 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1594 {
1595 if p.o1.is_none() {
1597 dev_attn.push(*li);
1598 }
1599 graph.commit_kind = 3;
1600 graph.commit();
1601 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1605 continue;
1606 }
1607 }
1609 graph.encode_attn_prefix(l);
1610 graph.sync();
1611 if !pending.is_empty() {
1612 let idxs: Vec<usize> =
1613 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1614 let mut outs: Vec<&mut [f32]> = self
1615 .kv_cache
1616 .layers
1617 .iter_mut()
1618 .enumerate()
1619 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1620 .map(|(_, s)| s.linear_state.as_mut_slice())
1621 .collect();
1622 graph.read_states(&mut outs);
1623 }
1624 let mut q_raw = attention::take_buf(l.wq.1);
1625 let mut k = attention::take_buf(l.wk.1);
1626 let mut v = attention::take_buf(l.wv.1);
1627 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1628 let cfg = QwenAttnCfg {
1629 num_heads: nh,
1630 num_kv_heads: nkv,
1631 head_dim: hd,
1632 hidden_size: hs,
1633 position,
1634 inv_freq: &inv_freq,
1635 rotary_dim: rd,
1636 scale: self.attn_scale,
1637 softcap: self.attn_softcap,
1638 window: None,
1639 v_norm: false,
1640 q_norm: *q_norm,
1641 k_norm: *k_norm,
1642 output_gate: *output_gate,
1643 softplus_gate: None,
1644 rope_scale: 1.0,
1645 bias: *bias,
1646 rms_eps: eps,
1647 norm_style,
1648 pool: pool.as_deref(),
1649 };
1650 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1653 || std::env::var("CMF_ATTN_DUMP").is_ok();
1654 let _ = full_gpu;
1655 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1656 let mut ao = attention::qwen_attention_core(
1657 q_raw,
1658 k,
1659 v,
1660 &mut self.kv_cache.layers[*li],
1661 &cfg,
1662 );
1663 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1667 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1668 let (cq, _cg, _ck, _cv) =
1669 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1670 let cache = &self.kv_cache.layers[*li];
1671 let n = cache.head_keys(0).len() / hd;
1672 let mut bytes: Vec<u8> = Vec::new();
1673 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1674 bytes.extend_from_slice(&v.to_le_bytes());
1675 }
1676 for v in &cq {
1677 bytes.extend_from_slice(&v.to_le_bytes());
1678 }
1679 for g in 0..nkv {
1680 for v in cache.head_keys(g) {
1681 bytes.extend_from_slice(&v.to_le_bytes());
1682 }
1683 }
1684 for g in 0..nkv {
1685 for v in cache.head_values(g) {
1686 bytes.extend_from_slice(&v.to_le_bytes());
1687 }
1688 }
1689 let _ =
1690 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1691 }
1692 }
1693 if let Some((qr0, k0, v0)) =
1694 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
1695 {
1696 let (cq, _cg, ck, cv) =
1697 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1698 let mut h_now = vec![0f32; hs];
1699 graph.read_h(&mut h_now);
1700 let cache = &self.kv_cache.layers[*li];
1701 let n_after = cache.head_keys(0).len() / hd;
1702 let cpu_k: Vec<&[f32]> = (0..nkv)
1703 .map(|g| &cache.head_keys(g)[..(n_after - 1) * hd])
1704 .collect();
1705 let cpu_v: Vec<&[f32]> = (0..nkv)
1706 .map(|g| &cache.head_values(g)[..(n_after - 1) * hd])
1707 .collect();
1708 let p = crate::gpu::AttnDeviceParams {
1709 kv_id,
1710 layer: *li,
1711 nh,
1712 nkv,
1713 hd,
1714 rd,
1715 position,
1716 scale: self.attn_scale,
1717 eps: eps as f32,
1718 gemma,
1719 output_gate: *output_gate,
1720 q_norm: *q_norm,
1721 k_norm: *k_norm,
1722 inv_freq: &inv_freq,
1723 cpu_k,
1724 cpu_v,
1725 cpu_stored: n_after - 1,
1726 o1: None,
1727 };
1728 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1729 let md = |a: &[f32], b: &[f32]| {
1730 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
1731 };
1732 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1733 eprintln!(
1734 "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}",
1735 nn(&cq),
1736 md(&cq, &dq),
1737 nn(&ck),
1738 md(&ck, &dk),
1739 nn(&cv),
1740 md(&cv, &dv),
1741 nn(&ao),
1742 md(&ao, &dao)
1743 );
1744 } else {
1745 eprintln!("attn-oracle L{li}: device probe declined");
1746 }
1747 }
1748 graph.encode_attn_suffix(l, &ao);
1749 graph.commit();
1752 attention::recycle_buf(&mut ao);
1753 }
1754 }
1755
1756 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1757 }
1758 let mut lm_rows = None;
1763 if self.graph_want_logits
1764 && upto.is_none()
1765 && end == self.num_layers
1766 && std::env::var("CMF_GPU_LMHEAD")
1767 .map(|v| v != "0")
1768 .unwrap_or(true)
1769 {
1770 if let Some(lm) = self.weights.lm_head.q1_parts() {
1771 if graph.lm_head_ok(lm) {
1772 graph.encode_lm_head(&self.weights.final_norm, lm);
1773 lm_rows = Some(lm.1);
1774 }
1775 }
1776 }
1777 let _sy0 = std::time::Instant::now();
1778 graph.sync();
1779 let _rs0 = std::time::Instant::now();
1780 if !pending.is_empty() {
1781 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1782 let mut outs: Vec<&mut [f32]> = self
1783 .kv_cache
1784 .layers
1785 .iter_mut()
1786 .enumerate()
1787 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1788 .map(|(_, s)| s.linear_state.as_mut_slice())
1789 .collect();
1790 graph.read_states(&mut outs);
1791 }
1792 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1793 use std::sync::atomic::{AtomicU64, Ordering};
1794 static SY: AtomicU64 = AtomicU64::new(0);
1795 static RS: AtomicU64 = AtomicU64::new(0);
1796 static N: AtomicU64 = AtomicU64::new(0);
1797 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1798 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1799 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1800 if n % 100 == 0 {
1801 eprintln!(
1802 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1803 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1804 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1805 );
1806 }
1807 }
1808 if let Some(rows) = lm_rows {
1809 crate::gpu::hostprof_encode_done(_mt0);
1810 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1811 graph.read_logits(&mut lg);
1812 crate::gpu::hostprof_total(_mt0);
1813 lg.resize(self.vocab_size, 0.0);
1814 if let Some(c) = self.final_softcap {
1815 for l in lg.iter_mut() {
1816 *l = c * (*l / c).tanh();
1817 }
1818 }
1819 self.graph_logits = Some(lg);
1820 }
1821 graph.finish(h);
1822 for li in dev_attn {
1826 let mut krow = attention::take_buf(nkv * hd);
1827 let mut vrow = attention::take_buf(nkv * hd);
1828 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1829 let cache = &mut self.kv_cache.layers[li];
1830 cache.append(&krow, &vrow, &[]);
1831 let n = cache.seq_len;
1832 let mut imp = attention::take_buf(n);
1833 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1834 cache.accumulate_imp(&imp);
1835 attention::recycle_buf(&mut imp);
1836 }
1837 attention::recycle_buf(&mut krow);
1838 attention::recycle_buf(&mut vrow);
1839 }
1840 end
1841 }
1842
1843 pub fn new(
1844 tokenizer: Tokenizer,
1845 weights: PipelineWeights,
1846 hidden_size: usize,
1847 intermediate_size: usize,
1848 num_heads: usize,
1849 num_kv_heads: usize,
1850 head_dim: usize,
1851 num_layers: usize,
1852 physical_layers: usize,
1853 loop_final_norm: bool,
1854 vocab_size: usize,
1855 rms_eps: f64,
1856 rope_base: f32,
1857 norm_style: NormStyle,
1858 max_seq_len: usize,
1859 sampler_config: SamplerConfig,
1860 ) -> Self {
1861 let rng = match sampler_config.seed {
1862 Some(s) => SplitMix64::new(s),
1863 None => SplitMix64::from_entropy(),
1864 };
1865 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1866 let pool = Pool::from_env();
1867 if let Some(p) = &pool {
1868 tracing::info!("worker pool: {} threads", p.n_workers());
1869 }
1870 Self {
1871 gpu_plan: None,
1872 tokenizer: std::sync::Arc::new(tokenizer),
1873 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1874 sampler_config,
1875 weights,
1876 hidden_size,
1877 intermediate_size,
1878 num_heads,
1879 num_kv_heads,
1880 head_dim,
1881 num_layers,
1882 physical_layers,
1883 loop_final_norm,
1884 vocab_size,
1885 rms_eps,
1886 rope_base,
1887 norm_style,
1888 rotary_dim: head_dim,
1889 attention_heads_per_layer: None,
1890 vmf_cfg: None,
1891 gdn_cfg: None,
1892 kda_cfg: None,
1893 g3n: None,
1894 dsv4: None,
1895 qwen4_exp: None,
1896 dsv4_mtp: Vec::new(),
1897 dspark: None,
1898 dspark_pending: Vec::new(),
1899 dspark_hist: Vec::new(),
1900 dspark_real: Vec::new(),
1901 dspark_trunk_picks: Vec::new(),
1902 dspark_exp: Vec::new(),
1903 dspark_draft_ns: 0,
1904 logit_multiplier: None,
1905 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1906 graph_failed: std::sync::atomic::AtomicBool::new(false),
1907 kv_history: Vec::new(),
1908 short_conv_cfg: None,
1909 mtp: None,
1910 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1911 rng,
1912 sampler_scratch: SamplerScratch::default(),
1913 spec_forced: None,
1914 spec_q: Vec::new(),
1915 spec_p: Vec::new(),
1916 spec_res: Vec::new(),
1917 spec_qs: Vec::new(),
1918 spec_ps: Vec::new(),
1919 spec_ress: Vec::new(),
1920 mtp_graph_mode: None,
1921 #[cfg(target_os = "macos")]
1922 metal_verify: None,
1923 inv_freq,
1924 ws: ForwardScratch::new(hidden_size),
1925 pool,
1926 model: None,
1927 dyn_force_f32: false,
1928 dyn_skill_layers: Vec::new(),
1929 dyn_active: None,
1930 dyn_blend_loaded: false,
1931 dyn_phi_layer: None,
1932 dyn_phi_ema: Vec::new(),
1933 dyn_phi_seen: 0,
1934 dyn_router: None,
1935 o1_cfg: None,
1936 o1_epoch: 0,
1937 o1_flags: Vec::new(),
1938 trace: false,
1939 calib_temp: 1.0,
1940 confidence_on: true,
1941 embed_multiplier: 1.0,
1942 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1943 swa: None,
1944 sliding_layers: None,
1945 inv_freq_local: None,
1946 rotary_dim_local: None,
1947 rope_scale: 1.0,
1948 rope_scale_local: 1.0,
1949 global_attn: None,
1950 inv_freq_global: None,
1951 attn_v_norm: false,
1952 final_softcap: None,
1953 head_clusters: None,
1954 attn_softcap: 0.0,
1955 graph_want_logits: false,
1956 graph_logits: None,
1957 graph_kv_id: {
1958 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1959 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1960 },
1961 #[cfg(test)]
1962 nll_test_fail_at: None,
1963 #[cfg(test)]
1964 nll_test_force_serial: false,
1965 }
1966 }
1967
1968 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1975 self.o1_flags = match &cfg {
1976 Some(c) => {
1977 let mut flags = c.layer_flags(self.num_layers);
1978 for (li, f) in flags.iter_mut().enumerate() {
1979 if *f
1980 && !matches!(
1981 self.weights.layers[self.phys_layer(li)].attn,
1982 AttnKind::Full { .. }
1983 )
1984 {
1985 *f = false;
1986 }
1987 }
1988 flags
1989 }
1990 None => Vec::new(),
1991 };
1992 if let Some(c) = &cfg {
1993 let n = self.o1_flags.iter().filter(|&&f| f).count();
1994 tracing::info!(
1995 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1996 self.num_layers,
1997 c.m,
1998 c.w,
1999 c.sink,
2000 c.rect
2001 );
2002 }
2003 self.o1_cfg = cfg;
2004 }
2005
2006 pub fn o1_active(&self) -> bool {
2008 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
2009 }
2010
2011 pub fn generation_graph_prefill(&self) -> bool {
2016 let graph = self.graph_prefill_preferred();
2017 #[cfg(not(target_os = "macos"))]
2025 if graph
2026 && self.mtp.is_some()
2027 && std::env::var("CMF_BATCH_K")
2028 .ok()
2029 .and_then(|v| v.parse::<usize>().ok())
2030 .is_some_and(|k| k > 0)
2031 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2032 {
2033 return false;
2034 }
2035 graph
2036 }
2037
2038 pub fn o1_device_stats(&self) -> (usize, u64) {
2043 crate::gpu::o1_device_stats(self.graph_kv_id)
2044 }
2045
2046 pub fn o1_begin(&mut self) {
2051 if let Some(c) = &self.o1_cfg {
2052 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
2053 for (li, &f) in self.o1_flags.iter().enumerate() {
2054 if f {
2055 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
2056 }
2057 }
2058 }
2059 }
2060
2061 pub fn o1_seal(&mut self) {
2065 self.o1_epoch = self.o1_epoch.wrapping_add(1);
2066 if self.o1_cfg.is_none() {
2067 return;
2068 }
2069 for li in 0..self.num_layers {
2070 if self.o1_flags.get(li).copied().unwrap_or(false) {
2071 self.kv_cache.layers[li].o1_seal(self.num_heads);
2072 }
2073 }
2074 }
2075
2076 pub fn set_trace(&mut self, on: bool) {
2078 self.trace = on;
2079 }
2080
2081 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
2084 self.rng = match config.seed {
2085 Some(seed) => SplitMix64::new(seed),
2086 None => SplitMix64::from_entropy(),
2087 };
2088 self.sampler_config = config;
2089 }
2090
2091 pub fn set_confidence(&mut self, on: bool) {
2096 self.confidence_on = on;
2097 }
2098
2099 pub fn set_calib_temp(&mut self, t: f32) {
2102 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
2103 }
2104
2105 pub fn calib_temp(&self) -> f32 {
2107 self.calib_temp
2108 }
2109
2110 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
2113 self.rotary_dim = rotary_dim.min(self.head_dim);
2114 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
2115 }
2116
2117 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
2118 QwenAttnCfg {
2119 num_heads: self.num_heads,
2120 num_kv_heads: self.num_kv_heads,
2121 head_dim: self.head_dim,
2122 hidden_size: self.hidden_size,
2123 position,
2124 inv_freq: &self.inv_freq,
2125 rotary_dim: self.rotary_dim,
2126 scale: self.attn_scale,
2127 softcap: self.attn_softcap,
2128 window: None,
2129 v_norm: false,
2130 q_norm: None,
2131 k_norm: None,
2132 output_gate: false,
2133 softplus_gate: None,
2134 rope_scale: self.rope_scale,
2135 bias: None,
2136 rms_eps: self.rms_eps,
2137 norm_style: self.norm_style,
2138 pool: self.pool.as_deref(),
2139 }
2140 }
2141
2142 pub fn generate(
2144 &mut self,
2145 prompt: &str,
2146 max_tokens: usize,
2147 task_mask: Option<&TaskMask>,
2148 on_token: Option<TokenCallback>,
2149 ) -> Result<GenerateResult, String> {
2150 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
2151 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
2152 }
2153
2154 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
2156 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
2157 }
2158
2159 pub fn generate_from_ids(
2167 &mut self,
2168 input_ids: &[u32],
2169 max_tokens: usize,
2170 task_mask: Option<&TaskMask>,
2171 mut on_token: Option<TokenCallback>,
2172 ) -> Result<GenerateResult, String> {
2173 if std::env::var("CMF_TRACE_H").is_ok() {
2174 eprintln!("input_ids: {input_ids:?}");
2175 }
2176 if input_ids.is_empty() {
2177 return Err("empty prompt: nothing to generate from".to_string());
2178 }
2179 self.graph_failed
2183 .store(false, std::sync::atomic::Ordering::Relaxed);
2184 let task_mask = self.drop_open_mask(task_mask);
2189
2190 let reuse_from = {
2198 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
2199 let h = &self.kv_history;
2200 if on
2201 && task_mask.is_none()
2202 && self.mtp.is_none()
2203 && self.o1_cfg.is_none()
2204 && !h.is_empty()
2205 && h.len() < input_ids.len()
2206 && input_ids[..h.len()] == h[..]
2207 {
2208 h.len()
2209 } else {
2210 0
2211 }
2212 };
2213 if reuse_from == 0 {
2214 self.clear_sequence_state();
2216 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
2217 eprintln!(
2218 "kv-reuse: {} of {} prompt positions already cached",
2219 reuse_from,
2220 input_ids.len()
2221 );
2222 }
2223 crate::gpu::graph_race_begin_generation();
2224 self.o1_begin();
2225
2226 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2232 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2259 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2260 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2276 for lw in &self.weights.layers {
2277 if let FfnKind::Dense(d) = &lw.ffn {
2278 dense_n += 1;
2279 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2280 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2281 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2282 {
2283 dense_q4tp += 1;
2284 }
2285 }
2286 }
2287 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2288 let penalized = self.sampler_config.repetition_penalty != 1.0
2292 || self.sampler_config.presence_penalty != 0.0
2293 || !self.sampler_config.suppress_tokens.is_empty();
2294 #[cfg(feature = "gpu")]
2299 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2300 #[cfg(not(feature = "gpu"))]
2301 let metal_wgpu = false;
2302 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2303 let spec_wanted = match spec_env.as_deref() {
2304 Some("0") => false,
2305 Some(_) => {
2306 if metal_wgpu {
2307 tracing::warn!(
2308 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2309 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2310 );
2311 }
2312 true
2313 }
2314 None => spec_default_ok && !penalized && !metal_wgpu,
2315 };
2316 #[cfg(target_os = "macos")]
2319 let metal_graph = crate::gpu::q1_force()
2320 && crate::gpu::enabled_here()
2321 && std::env::var("CMF_GPU_BLOCK")
2322 .map(|v| v != "0")
2323 .unwrap_or(true);
2324 #[cfg(not(target_os = "macos"))]
2325 let metal_graph = false;
2326 let graph_spec = self.speculative
2327 && (graph_on || metal_graph)
2328 && self.mtp.is_some()
2329 && task_mask.is_none()
2330 && !self.o1_active()
2331 && spec_sampling_ok
2332 && spec_wanted;
2333 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2340 let spec_active = self.speculative
2341 && self.mtp.is_some()
2342 && task_mask.is_none()
2343 && !self.o1_active()
2344 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2345 let mut mtp = if spec_active { self.mtp.take() } else { None };
2348 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2349 eprintln!(
2350 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2351 mtp.is_some(),
2352 self.speculative,
2353 self.sampler_config.temperature < 1e-6,
2354 );
2355 }
2356 if let Some(m) = &mut mtp {
2357 m.kv.clear();
2358 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2360 self.mtp_graph_mode = None;
2361 }
2362 let mut router = if mtp.is_none() {
2366 self.dyn_router.take()
2367 } else {
2368 None
2369 };
2370 if let Some(r) = &mut router {
2371 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2374 }
2375
2376 let mut all_ids = input_ids.to_vec();
2377 let mut generated = 0usize;
2378 let mut finish_reason = "max_tokens".to_string();
2379 let mut drafted = 0usize;
2380 let mut accepted = 0usize;
2381 let mut dsv4_spec_bad = 0usize;
2388 let mut dsv4_spec_retry_at = 0usize;
2389 let mut confidence: Vec<f32> = Vec::new();
2390 let trace_on = self.trace;
2391 let calib_temp = self.calib_temp;
2392 let mut traces: Vec<TokenTrace> = Vec::new();
2393
2394 let mut hidden = vec![0.0f32; self.hidden_size];
2400 let mut pos = reuse_from;
2401 let fuse_lm = mtp.is_none()
2410 && router.is_none()
2411 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2412 self.graph_logits = None;
2413 self.graph_want_logits = false;
2414 let _tpf = std::time::Instant::now();
2415 let batch_k = std::env::var("CMF_BATCH_K")
2416 .ok()
2417 .and_then(|v| v.parse::<usize>().ok())
2418 .unwrap_or(0);
2419 while self.qwen4_exp.is_some()
2430 && mtp.is_none()
2431 && pos < input_ids.len()
2432 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2433 {
2434 let token_id = input_ids[pos];
2435 let want_logits = pos + 1 == input_ids.len();
2436 let mut lg = Vec::new();
2437 if let Some(b) = &mut self.qwen4_exp {
2438 crate::qwen4_exp::forward_token(
2439 &b.0,
2440 &b.1,
2441 &b.2,
2442 &mut b.3,
2443 token_id,
2444 pos,
2445 &self.inv_freq,
2446 self.pool.as_deref(),
2447 &mut lg,
2448 want_logits,
2449 );
2450 }
2451 if want_logits {
2452 self.graph_logits = Some(lg);
2453 }
2454 pos += 1;
2455 hidden.fill(0.0);
2456 }
2457 while self.dsv4.is_some()
2458 && mtp.is_none()
2459 && pos < input_ids.len()
2460 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2461 {
2462 let end = (pos + prefill_chunk()).min(input_ids.len());
2463 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2464 let mut lg = Vec::new();
2465 if let Some(b) = &mut self.dsv4 {
2466 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2467 crate::dsv4::forward_chunk(
2468 g,
2469 layers,
2470 &cfg,
2471 st,
2472 &ids,
2473 pos,
2474 &self.inv_freq,
2475 self.pool.as_deref(),
2476 &mut lg,
2477 end == input_ids.len(),
2478 );
2479 }
2480 if end == input_ids.len() {
2481 self.graph_logits = Some(lg);
2482 }
2483 pos = end;
2484 hidden = vec![0.0; self.hidden_size];
2485 }
2486 let dyn_prefill = router.is_some();
2491 let o1_prefill = if self.o1_active() && task_mask.is_none() {
2499 std::env::var("CMF_O1_PREFILL")
2500 .ok()
2501 .and_then(|v| v.parse::<usize>().ok())
2502 .filter(|&p| p > 0 && p < input_ids.len())
2503 } else {
2504 None
2505 };
2506 let mut o1_sealed = false;
2507 if let Some(limit) = o1_prefill {
2508 if self.can_prefill_batched() && limit > 2 {
2511 let chunk = prefill_chunk();
2512 let hs = self.hidden_size;
2513 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2514 let end = (pos + chunk).min(limit);
2515 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2516 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2517 pos = end;
2518 }
2519 } else {
2520 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2521 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
2522 pos += 1;
2523 }
2524 }
2525 if pos >= limit {
2526 self.o1_seal();
2527 o1_sealed = true;
2528 tracing::info!(
2529 "o1 bounded prompt prefix: sealed after {limit} of {} token(s)",
2530 input_ids.len()
2531 );
2532 }
2533 }
2534 let graph_prefill = self.graph_prefill_preferred();
2540 #[cfg(target_os = "macos")]
2548 if task_mask.is_none()
2549 && !dyn_prefill
2550 && crate::gpu::q1_force()
2551 && crate::gpu::enabled_here()
2552 && self.gdn_cfg.is_some()
2553 && self.g3n.is_none()
2554 && input_ids.len() > 8
2555 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2556 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2557 {
2558 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2559 .ok()
2560 .and_then(|v| v.parse().ok())
2561 .filter(|&v| (16..=512).contains(&v))
2562 .unwrap_or(256);
2563 let hs = self.hidden_size;
2564 let _tp = std::time::Instant::now();
2565 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2566 let end = (pos + chunk).min(input_ids.len());
2567 let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2568 break;
2569 };
2570 if let Some(m) = &mut mtp {
2571 let n_pairs = if end < input_ids.len() {
2572 end - pos
2573 } else {
2574 end - pos - 1
2575 };
2576 if n_pairs > 0 {
2577 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2578 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2579 .collect();
2580 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2581 for (j, (h, t)) in pairs.iter().enumerate() {
2582 let h = h.to_vec();
2583 let _ = self.mtp_step(m, &h, *t, pos + j);
2584 }
2585 }
2586 }
2587 }
2588 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2589 pos = end;
2590 }
2591 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2592 eprintln!(
2593 "metal-prefill: {} of {} tokens in {:.1} ms",
2594 pos,
2595 input_ids.len(),
2596 _tp.elapsed().as_secs_f64() * 1e3
2597 );
2598 }
2599 }
2600 if task_mask.is_none()
2601 && !dyn_prefill
2602 && !graph_prefill
2603 && self.can_prefill_batched()
2604 && self.g3n.is_none()
2605 && o1_prefill.is_none()
2606 && input_ids.len() > 2
2607 {
2608 let chunk = prefill_chunk();
2614 let hs = self.hidden_size;
2615 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2616 let end = (pos + chunk).min(input_ids.len());
2617 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2618 if let Some(m) = &mut mtp {
2619 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2620 .ok()
2621 .and_then(|v| v.parse().ok())
2622 .unwrap_or(0);
2623 for p in pos..end {
2624 if p + 1 < input_ids.len() {
2625 if probe >= 1 && p + 2 < input_ids.len() {
2626 let (d1, mut hx) = self.mtp_step_h(
2630 m,
2631 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2632 input_ids[p + 1],
2633 p,
2634 );
2635 let mut ok = d1 == input_ids[p + 2];
2636 Self::chain_probe_note(0, ok);
2637 let mut d_prev = d1;
2638 let mut extra = 0usize;
2639 for j in 1..probe {
2640 if p + 2 + j >= input_ids.len() {
2641 break;
2642 }
2643 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2644 extra += 1;
2645 ok = ok && dj == input_ids[p + 2 + j];
2646 Self::chain_probe_note(j, ok);
2647 d_prev = dj;
2648 hx = hj;
2649 }
2650 m.kv.truncate_last(extra);
2651 } else {
2652 let _ = self.mtp_step(
2653 m,
2654 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2655 input_ids[p + 1],
2656 p,
2657 );
2658 }
2659 }
2660 }
2661 }
2662 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2663 pos = end;
2664 }
2665 }
2666 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2667 if task_mask.is_none()
2668 && !dyn_prefill
2669 && !graph_prefill
2670 && !pair_off
2671 && self.pair_supported()
2672 && o1_prefill.is_none()
2673 {
2674 while pos + 1 < input_ids.len()
2675 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2676 {
2677 let e1 = self.embed_single(input_ids[pos]);
2678 let e2 = self.embed_single(input_ids[pos + 1]);
2679 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2680 self.commit_linear_scratch();
2682 if let Some(m) = &mut mtp {
2683 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2684 if pos + 2 < input_ids.len() {
2685 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2686 .ok()
2687 .and_then(|v| v.parse().ok())
2688 .unwrap_or(0);
2689 if probe >= 1 && pos + 3 < input_ids.len() {
2690 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2694 let mut ok = d1 == input_ids[pos + 3];
2695 Self::chain_probe_note(0, ok);
2696 let mut d_prev = d1;
2697 let mut extra = 0usize;
2698 for j in 1..probe {
2699 if pos + 3 + j >= input_ids.len() {
2700 break;
2701 }
2702 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2703 extra += 1;
2704 ok = ok && dj == input_ids[pos + 3 + j];
2705 Self::chain_probe_note(j, ok);
2706 d_prev = dj;
2707 hx = hj;
2708 }
2709 m.kv.truncate_last(extra);
2710 } else {
2711 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2712 }
2713 }
2714 }
2715 hidden = h2;
2716 pos += 2;
2717 }
2718 }
2719 let o1_batch_ready = o1_sealed
2732 && o1_prefill.is_some()
2733 && mtp.is_none()
2734 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
2735 && (0..self.num_layers).all(|li| {
2736 let cache = &self.kv_cache.layers[self.phys_layer(li)];
2737 cache.o1.is_none() || cache.o1_views().is_some()
2738 });
2739 let mtp_batch_prefill = mtp.is_some()
2744 && graph_prefill
2745 && task_mask.is_none()
2746 && !dyn_prefill
2747 && !self.o1_active()
2748 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
2749 if batch_k > 0
2750 && (graph_prefill || o1_batch_ready)
2751 && task_mask.is_none()
2752 && (!self.o1_active() || o1_batch_ready)
2753 && (mtp.is_none() || mtp_batch_prefill)
2754 && !dyn_prefill
2755 && pos + 1 < input_ids.len()
2756 {
2757 let hs = self.hidden_size;
2758 let chunk = batch_k;
2759 while pos < input_ids.len() {
2760 let end = (pos + chunk).min(input_ids.len());
2761 let bk = end - pos;
2762 let mut hiddens = vec![0f32; bk * hs];
2763 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2764 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2765 }
2766 let positions: Vec<usize> = (pos..end).collect();
2767 let t_chunk = std::time::Instant::now();
2768 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2769 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
2770 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2771 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2772 eprintln!(
2773 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2774 if o1_batch_ready {
2775 "o1"
2776 } else if mtp_batch_prefill {
2777 "ordinary_mtp"
2778 } else {
2779 "ordinary"
2780 },
2781 bk as f64 / (ms / 1000.0)
2782 );
2783 }
2784 {
2785 use std::sync::atomic::{AtomicBool, Ordering};
2786 static SAID: AtomicBool = AtomicBool::new(false);
2787 if !SAID.swap(true, Ordering::Relaxed) {
2788 if ok_b {
2789 tracing::info!(
2790 "batched prefill: ACTIVE mode={} (k={bk})",
2791 if o1_batch_ready {
2792 "o1"
2793 } else if mtp_batch_prefill {
2794 "ordinary_mtp"
2795 } else {
2796 "ordinary"
2797 }
2798 );
2799 } else {
2800 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
2801 }
2802 }
2803 }
2804 if ok_b {
2805 if mtp_batch_prefill {
2806 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
2807 if n_pairs > 0 {
2808 let rows: Vec<Vec<f32>> = (0..n_pairs)
2814 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
2815 .collect();
2816 let pairs: Vec<(&[f32], u32)> = rows
2817 .iter()
2818 .enumerate()
2819 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
2820 .collect();
2821 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2822 eprintln!(
2823 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
2824 pos,
2825 n_pairs,
2826 pos + n_pairs - 1,
2827 );
2828 }
2829 let warm_error = if let Some(m) = mtp.as_mut() {
2830 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
2831 } else {
2832 None
2833 };
2834 if let Some(err) = warm_error {
2835 self.finish_generation(&mut mtp, &mut router, true);
2840 return Err(err.to_string());
2841 }
2842 }
2843 }
2844 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2845 pos = end;
2846 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
2847 self.finish_generation(&mut mtp, &mut router, true);
2852 return Err(if o1_batch_ready {
2853 "sealed O(1) batch graph failed after admission".to_string()
2854 } else {
2855 "ordinary recurrent batch graph failed after admission".to_string()
2856 });
2857 } else {
2858 break; }
2860 }
2861 }
2862 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2863 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2864 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2865 if let Some(m) = &mut mtp {
2866 if pos + 1 < input_ids.len() {
2867 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2873 .ok()
2874 .and_then(|v| v.parse().ok())
2875 .unwrap_or(0);
2876 if probe >= 1 && pos + 2 < input_ids.len() {
2877 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2878 let mut ok = d1 == input_ids[pos + 2];
2879 Self::chain_probe_note(0, ok);
2880 let mut d_prev = d1;
2881 let mut extra = 0usize;
2882 for j in 1..probe {
2883 if pos + 2 + j >= input_ids.len() {
2884 break;
2885 }
2886 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2887 extra += 1;
2888 ok = ok && dj == input_ids[pos + 2 + j];
2889 Self::chain_probe_note(j, ok);
2890 d_prev = dj;
2891 hx = hj;
2892 }
2893 m.kv.truncate_last(extra);
2896 } else {
2897 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2898 }
2899 }
2900 }
2901 pos += 1;
2902 }
2903 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2904 eprintln!(
2905 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2906 input_ids.len(),
2907 _tpf.elapsed().as_secs_f64() * 1000.0
2908 );
2909 }
2910 if self
2911 .graph_failed
2912 .swap(false, std::sync::atomic::Ordering::Relaxed)
2913 {
2914 self.finish_generation(&mut mtp, &mut router, true);
2919 return Err("GPU token graph failed during prefill".to_string());
2920 }
2921 if self
2924 .cancel
2925 .swap(false, std::sync::atomic::Ordering::Relaxed)
2926 {
2927 self.finish_generation(&mut mtp, &mut router, true);
2931 return Ok(GenerateResult {
2932 text: String::new(),
2933 token_ids: Vec::new(),
2934 prompt_tokens: input_ids.len(),
2935 tokens_generated: 0,
2936 finish_reason: "cancelled".to_string(),
2937 mtp_drafted: 0,
2938 mtp_accepted: 0,
2939 token_confidence: Vec::new(),
2940 traces: Vec::new(),
2941 });
2942 }
2943
2944 if !o1_sealed {
2947 self.o1_seal();
2948 }
2949
2950 macro_rules! commit {
2952 ($id:expr) => {{
2953 all_ids.push($id);
2954 generated += 1;
2955 if self.tokenizer.is_eos($id) {
2956 finish_reason = "stop".to_string();
2957 false
2958 } else {
2959 let token_text = self.tokenizer.decode_token($id);
2960 let mut go = true;
2961 if let Some(ref mut cb) = on_token {
2962 if !cb(&token_text) {
2963 finish_reason = "cancelled".to_string();
2964 go = false;
2965 }
2966 }
2967 go
2968 }
2969 }};
2970 }
2971
2972 let mut spec_trial = SpecTrial::Spec {
2983 t0: std::time::Instant::now(),
2984 gen0: generated,
2985 rounds: 0,
2986 };
2987 let mut spec_mon = SpecMon::default();
2988 let mut spec_watchdog_off = false;
2989 let mut next_pos = input_ids.len();
2991 'decode: while generated < max_tokens {
2992 if self
2993 .graph_failed
2994 .swap(false, std::sync::atomic::Ordering::Relaxed)
2995 {
2996 self.finish_generation(&mut mtp, &mut router, true);
3001 return Err("GPU token graph failed during decode".to_string());
3002 }
3003 if self
3004 .cancel
3005 .swap(false, std::sync::atomic::Ordering::Relaxed)
3006 {
3007 finish_reason = "cancelled".to_string();
3008 break 'decode;
3009 }
3010 let forced = self.spec_forced.take();
3015 let mut logits = match (forced, self.graph_logits.take()) {
3016 (Some(_), _) => Vec::new(),
3017 (None, Some(lg)) => lg,
3018 (None, None) => {
3019 inference::rms_norm_into(
3020 &hidden,
3021 &self.weights.final_norm,
3022 self.rms_eps,
3023 self.norm_style,
3024 &mut self.ws.n1,
3025 );
3026 self.lm_head_forward(&self.ws.n1)
3027 }
3028 };
3029 if generated
3032 == std::env::var("CMF_LOGIT_DUMP_STEP")
3033 .ok()
3034 .and_then(|v| v.parse().ok())
3035 .unwrap_or(0)
3036 {
3037 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
3038 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
3039 for v in hidden.iter().chain(logits.iter()) {
3040 bytes.extend_from_slice(&v.to_le_bytes());
3041 }
3042 if let Err(e) = std::fs::write(&path, &bytes) {
3043 eprintln!("logit dump: failed to write {path}: {e}");
3044 self.finish_generation(&mut mtp, &mut router, true);
3045 return Err(format!("logit dump write failed: {e}"));
3046 }
3047 }
3048 }
3049 let t_next = match forced {
3050 Some(c) => c,
3051 None => sampler::sample_with_scratch_pool(
3052 &logits,
3053 &self.sampler_config,
3054 &all_ids,
3055 &mut self.rng,
3056 &mut self.sampler_scratch,
3057 self.pool.as_deref(),
3058 ),
3059 };
3060 if self.confidence_on {
3061 confidence.push(if logits.is_empty() {
3062 0.0
3063 } else {
3064 sampler::top1_prob_pool(
3065 self.pool.as_deref(),
3066 &mut self.sampler_scratch,
3067 &logits,
3068 t_next,
3069 calib_temp,
3070 )
3071 });
3072 }
3073 if !logits.is_empty() {
3074 attention::recycle_buf(&mut logits);
3075 }
3076 if trace_on {
3077 let skill = router.as_ref().and_then(|r| r.active_id());
3081 traces.push(TokenTrace {
3082 t: generated,
3083 token_id: t_next,
3084 confidence: confidence.last().copied().unwrap_or(0.0),
3085 active_skill: skill,
3086 recon: None,
3087 switched: false,
3088 });
3089 }
3090 if !commit!(t_next) {
3091 break 'decode;
3092 }
3093 if generated >= max_tokens {
3094 break 'decode;
3095 }
3096
3097 if self.kv_cache.needs_eviction() {
3098 static SAID: std::sync::Once = std::sync::Once::new();
3104 SAID.call_once(|| {
3105 tracing::warn!(
3106 "KV cache full at {} positions — evicting half; quality \
3107 will degrade. Raise CMF_MAX_SEQ.",
3108 self.kv_cache.max_seq_len,
3109 );
3110 });
3111 let keep = (self.kv_cache.max_seq_len / 2).max(1);
3112 self.kv_cache.evict(keep);
3113 }
3114
3115 if graph_spec {
3118 match spec_trial {
3119 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
3120 spec_mon.plain_ms =
3121 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
3122 let keep = spec_mon.pays();
3123 tracing::info!(
3124 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3125 spec_mon.tokens,
3126 spec_mon.round_ms,
3127 spec_mon.plain_ms,
3128 if keep { "speculating" } else { "plain" }
3129 );
3130 spec_mon.fails = 0;
3131 spec_trial = SpecTrial::Decided {
3132 spec: keep,
3133 recheck_at: if keep { usize::MAX } else { generated + 128 },
3134 };
3135 }
3136 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
3137 spec_mon.n = 0;
3138 spec_trial = SpecTrial::Spec {
3139 t0: std::time::Instant::now(),
3140 gen0: generated,
3141 rounds: 0,
3142 };
3143 }
3144 _ => {}
3145 }
3146 spec_watchdog_off = matches!(
3147 spec_trial,
3148 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
3149 );
3150 }
3151 match &mut mtp {
3152 #[cfg(feature = "gpu")]
3154 Some(m)
3155 if graph_spec
3156 && !spec_watchdog_off
3157 && generated + 1 < max_tokens
3158 && next_pos > 0 =>
3159 {
3160 let t_round = std::time::Instant::now();
3161 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
3162 m,
3163 &hidden,
3164 t_next,
3165 next_pos,
3166 &mut drafted,
3167 &mut accepted,
3168 &mut all_ids,
3169 ) {
3170 next_pos = n_pos;
3171 hidden = new_h;
3172 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3173 eprintln!(
3174 "spec-round wall {:.1} ms → {} tokens",
3175 t_round.elapsed().as_secs_f64() * 1e3,
3176 extra.len() + 1
3177 );
3178 }
3179 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
3183 spec_trial = Self::spec_trial_round(
3186 spec_trial,
3187 &mut spec_mon,
3188 generated + extra.len() + 1,
3189 );
3190 let mut stopped = false;
3191 for &id in &extra {
3192 if self.confidence_on {
3193 confidence.push(0.0);
3194 }
3195 if !commit!(id) {
3196 stopped = true;
3197 break;
3198 }
3199 }
3200 if stopped {
3201 break 'decode;
3202 }
3203 continue 'decode;
3204 }
3205 if self
3206 .graph_failed
3207 .swap(false, std::sync::atomic::Ordering::Relaxed)
3208 {
3209 self.finish_generation(&mut mtp, &mut router, true);
3215 return Err("GPU MTP graph failed during speculative decode".to_string());
3216 }
3217 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
3228 spec_mon.tokens = 0.0;
3229 spec_mon.fails = 3;
3230 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
3231 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
3232 next_pos += 1;
3233 continue 'decode;
3234 }
3235 Some(m) if !graph_spec && generated + 1 < max_tokens => {
3237 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
3238 drafted += 1;
3239 let emb1 = self.embed_single(t_next);
3240 let emb2 = self.embed_single(draft);
3241 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
3242
3243 inference::rms_norm_into(
3244 &h1,
3245 &self.weights.final_norm,
3246 self.rms_eps,
3247 self.norm_style,
3248 &mut self.ws.n1,
3249 );
3250 let mut logits1 = self.lm_head_forward(&self.ws.n1);
3251 let t_after = sampler::sample_with_scratch_pool(
3252 &logits1,
3253 &self.sampler_config,
3254 &all_ids,
3255 &mut self.rng,
3256 &mut self.sampler_scratch,
3257 self.pool.as_deref(),
3258 );
3259 if self.confidence_on {
3260 confidence.push(sampler::top1_prob_pool(
3261 self.pool.as_deref(),
3262 &mut self.sampler_scratch,
3263 &logits1,
3264 t_after,
3265 calib_temp,
3266 ));
3267 }
3268 attention::recycle_buf(&mut logits1);
3269 if trace_on {
3270 traces.push(TokenTrace {
3273 t: generated,
3274 token_id: t_after,
3275 confidence: confidence.last().copied().unwrap_or(0.0),
3276 active_skill: None,
3277 recon: None,
3278 switched: false,
3279 });
3280 }
3281 let stop = !commit!(t_after);
3282
3283 if t_after == draft {
3284 accepted += 1;
3285 self.commit_linear_scratch();
3286 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3287 hidden = h2;
3288 next_pos += 2;
3289 } else {
3290 for layer in &mut self.kv_cache.layers {
3292 layer.truncate_last(1);
3293 }
3294 if !stop {
3295 let _ = self.mtp_step(m, &h1, t_after, next_pos);
3296 hidden = self.forward_layers(
3297 &self.embed_single(t_after),
3298 next_pos + 1,
3299 None,
3300 );
3301 }
3302 next_pos += 2;
3303 }
3304 if stop {
3305 break 'decode;
3306 }
3307 }
3308 _ => {
3310 #[cfg(feature = "gpu")]
3315 if Self::dsv4_spec_on() && self.dsv4.is_some() {
3316 static SAID: std::sync::Once = std::sync::Once::new();
3317 SAID.call_once(|| {
3318 eprintln!(
3319 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
3320 !self.dsv4_mtp.is_empty(),
3321 task_mask.is_none(),
3322 router.is_none(),
3323 !trace_on,
3324 self.sampler_config.temperature < 1e-6,
3325 self.sampler_config.repetition_penalty == 1.0,
3326 );
3327 });
3328 }
3329 #[cfg(feature = "gpu")]
3330 if Self::dsv4_spec_on()
3331 && self.dsv4.is_some()
3332 && !self.dsv4_mtp.is_empty()
3333 && task_mask.is_none()
3334 && router.is_none()
3335 && !trace_on
3336 && self.sampler_config.temperature < 1e-6
3337 && self.sampler_config.repetition_penalty == 1.0
3338 && generated + 1 < max_tokens
3339 && all_ids.len() >= 2
3340 && generated >= dsv4_spec_retry_at
3341 {
3342 let tip_token = all_ids[all_ids.len() - 2];
3343 let drafted0 = drafted;
3344 let round = self.dsv4_spec_step(
3345 tip_token,
3346 t_next,
3347 next_pos,
3348 max_tokens.saturating_sub(generated),
3349 &mut drafted,
3350 &mut accepted,
3351 );
3352 if drafted > drafted0 {
3353 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
3354 if useful {
3355 dsv4_spec_bad = 0;
3356 } else {
3357 dsv4_spec_bad += 1;
3358 if dsv4_spec_bad >= 2 {
3359 dsv4_spec_bad = 0;
3360 dsv4_spec_retry_at = generated.saturating_add(32);
3361 tracing::info!(
3362 "dsv4: draft не окупился дважды — точный walk на 32 токена"
3363 );
3364 }
3365 }
3366 }
3367 if let Some((extra, n_pos)) = round {
3368 next_pos = n_pos;
3369 let mut stopped = false;
3370 for &id in &extra {
3371 if self.confidence_on {
3372 confidence.push(0.0);
3373 }
3374 if !commit!(id) {
3375 stopped = true;
3376 break;
3377 }
3378 }
3379 if stopped {
3380 break 'decode;
3381 }
3382 continue 'decode;
3383 }
3384 }
3385 self.graph_want_logits = fuse_lm;
3386 let mut t_fwd = t_next;
3392 let pure_greedy = self.sampler_config.temperature < 1e-6
3393 && self.sampler_config.repetition_penalty == 1.0
3394 && self.sampler_config.suppress_tokens.is_empty();
3395 let burst_k = std::env::var("CMF_MULTISTEP")
3400 .ok()
3401 .and_then(|v| v.parse::<usize>().ok())
3402 .unwrap_or(0);
3403 if pure_greedy
3404 && burst_k >= 1
3405 && fuse_lm
3406 && task_mask.is_none()
3407 && router.is_none()
3408 && !trace_on
3409 && !self.confidence_on
3410 {
3411 let mut stopped = false;
3412 loop {
3413 let room = max_tokens.saturating_sub(generated);
3414 if room <= 2 {
3415 break;
3416 }
3417 let k = burst_k.min(room - 1);
3418 if k < 1 {
3419 break;
3420 }
3421 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
3422 if self
3423 .graph_failed
3424 .swap(false, std::sync::atomic::Ordering::Relaxed)
3425 {
3426 self.finish_generation(&mut mtp, &mut router, true);
3427 return Err(
3428 "GPU token graph failed during greedy burst".to_string()
3429 );
3430 }
3431 break;
3432 };
3433 next_pos += k;
3434 for &id in &ids {
3435 if !commit!(id) {
3436 stopped = true;
3437 break;
3438 }
3439 }
3440 if stopped {
3441 break;
3442 }
3443 t_fwd = *ids.last().unwrap();
3444 }
3445 if stopped {
3446 break 'decode;
3447 }
3448 }
3449 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
3450 next_pos += 1;
3451 if let Some(r) = &mut router {
3454 let phi = self.dyn_phi_ema.clone();
3455 let decision = r.step(&phi, generated);
3456 if let Some(new_active) = decision {
3457 let _ = self.set_active_skill(new_active);
3458 }
3459 if trace_on {
3462 if let Some(last) = traces.last_mut() {
3463 let e = r.last_best_e();
3464 last.recon = e.is_finite().then_some(e);
3465 last.switched = decision.is_some();
3466 }
3467 }
3468 }
3469 }
3470 }
3471 }
3472
3473 let cancelled = finish_reason == "cancelled";
3474 self.finish_generation(&mut mtp, &mut router, cancelled);
3475
3476 let output_ids = &all_ids[input_ids.len()..];
3477 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3481 if cancelled {
3482 self.kv_history.clear();
3483 } else {
3484 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3485 }
3486 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3488 Ok(GenerateResult {
3489 text: self.tokenizer.decode(output_ids),
3490 token_ids: output_ids.to_vec(),
3491 prompt_tokens: input_ids.len(),
3492 tokens_generated: generated,
3493 finish_reason,
3494 mtp_drafted: drafted,
3495 mtp_accepted: accepted,
3496 token_confidence: confidence,
3497 traces,
3498 })
3499 }
3500
3501 fn mtp_step(
3505 &mut self,
3506 m: &mut MtpModule,
3507 hidden: &[f32],
3508 next_token: u32,
3509 position: usize,
3510 ) -> u32 {
3511 self.mtp_step_h(m, hidden, next_token, position).0
3512 }
3513
3514 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3518 use std::sync::Mutex;
3519 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3520 let mut t = T.lock().unwrap();
3521 if t.len() <= depth {
3522 t.resize(depth + 1, (0, 0));
3523 }
3524 t[depth].0 += 1;
3525 t[depth].1 += prefix_ok as u64;
3526 if depth == 0 && t[0].0 % 128 == 0 {
3527 let line: Vec<String> = t
3528 .iter()
3529 .enumerate()
3530 .map(|(d, (n, k))| {
3531 format!(
3532 "d{}={:.0}%({n})",
3533 d + 1,
3534 100.0 * *k as f64 / (*n).max(1) as f64
3535 )
3536 })
3537 .collect();
3538 eprintln!("mtp-chain: {}", line.join(" "));
3539 }
3540 }
3541
3542 fn mtp_step_hl(
3550 &mut self,
3551 m: &mut MtpModule,
3552 hidden: &[f32],
3553 next_token: u32,
3554 position: usize,
3555 ) -> (Vec<f32>, Vec<f32>) {
3556 #[cfg(target_os = "macos")]
3561 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3562 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3563 self.mtp_graph_mode = Some(true);
3564 return r;
3565 }
3566 if self.mtp_graph_mode == Some(true) {
3567 tracing::error!("mtp Metal graph failed after admission");
3568 self.clear_sequence_state();
3569 self.graph_failed
3570 .store(true, std::sync::atomic::Ordering::Relaxed);
3571 self.cancel
3572 .store(true, std::sync::atomic::Ordering::Relaxed);
3573 return (Vec::new(), Vec::new());
3574 }
3575 self.mtp_graph_mode = Some(false);
3576 }
3577 #[cfg(feature = "gpu")]
3578 if self.mtp_graph_mode != Some(false) {
3579 if !self.mtp_graph_ok(m) {
3580 if self.mtp_graph_mode == Some(true) {
3581 tracing::error!("mtp graph became unavailable after admission");
3586 self.clear_sequence_state();
3587 self.graph_failed
3588 .store(true, std::sync::atomic::Ordering::Relaxed);
3589 self.cancel
3590 .store(true, std::sync::atomic::Ordering::Relaxed);
3591 return (Vec::new(), Vec::new());
3592 }
3593 self.mtp_graph_mode = Some(false);
3594 } else {
3595 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
3596 self.mtp_graph_mode = Some(true);
3597 return r;
3598 }
3599 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
3600 return (Vec::new(), Vec::new());
3607 }
3608 tracing::error!("mtp graph failed or declined after admission");
3612 self.clear_sequence_state();
3613 self.graph_failed
3614 .store(true, std::sync::atomic::Ordering::Relaxed);
3615 self.cancel
3616 .store(true, std::sync::atomic::Ordering::Relaxed);
3617 return (Vec::new(), Vec::new());
3618 }
3619 }
3620 let e = self.embed_single(next_token);
3624 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3625 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3626 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3627 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3628 let mut x = vec![0.0f32; self.hidden_size];
3629 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3630
3631 let lw = &m.layer;
3633 inference::rms_norm_into(
3634 &x,
3635 &lw.input_norm,
3636 self.rms_eps,
3637 self.norm_style,
3638 &mut self.ws.n1,
3639 );
3640 let attn = match &lw.attn {
3641 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3643 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3644 AttnKind::Full {
3645 wq,
3646 wk,
3647 wv,
3648 wo,
3649 q_norm,
3650 k_norm,
3651 output_gate,
3652 softplus_gate,
3653 bias,
3654 } => {
3655 let mut cfg = self.attn_cfg(position);
3656 cfg.q_norm = q_norm.as_deref();
3657 cfg.k_norm = k_norm.as_deref();
3658 cfg.output_gate = *output_gate;
3659 cfg.softplus_gate = softplus_gate
3660 .as_ref()
3661 .map(|(gate, per_head)| (gate, *per_head));
3662 cfg.bias = bias
3663 .as_ref()
3664 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3665 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3666 }
3667 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3668 unreachable!("MTP block is full attention")
3669 }
3670 };
3671 for (i, &a) in attn.iter().enumerate() {
3672 x[i] += a;
3673 }
3674 inference::rms_norm_into(
3675 &x,
3676 &lw.post_norm,
3677 self.rms_eps,
3678 self.norm_style,
3679 &mut self.ws.p1,
3680 );
3681 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3682 for (i, &f) in ffn.iter().enumerate() {
3683 x[i] += f;
3684 }
3685
3686 inference::rms_norm_into(
3687 &x,
3688 &m.final_norm,
3689 self.rms_eps,
3690 self.norm_style,
3691 &mut self.ws.n1,
3692 );
3693 let lg = self.lm_head_forward(&self.ws.n1);
3694 (lg, x)
3695 }
3696
3697 fn mtp_step_h(
3699 &mut self,
3700 m: &mut MtpModule,
3701 hidden: &[f32],
3702 next_token: u32,
3703 position: usize,
3704 ) -> (u32, Vec<f32>) {
3705 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
3706 let draft = sampler::argmax(&lg);
3707 attention::recycle_buf(&mut lg);
3708 (draft, x)
3709 }
3710
3711 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
3717 match trial {
3718 SpecTrial::Spec { t0, gen0, rounds } => {
3719 let rounds = rounds + 1;
3720 if rounds >= 5 {
3721 if mon.plain_ms > 0.0 {
3722 let keep = mon.pays();
3723 mon.fails = 0;
3724 tracing::info!(
3725 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3726 mon.tokens,
3727 mon.round_ms,
3728 mon.plain_ms,
3729 if keep { "speculating" } else { "plain" }
3730 );
3731 SpecTrial::Decided {
3732 spec: keep,
3733 recheck_at: if keep { usize::MAX } else { generated + 128 },
3734 }
3735 } else {
3736 SpecTrial::Plain {
3737 t0: std::time::Instant::now(),
3738 gen0: generated,
3739 }
3740 }
3741 } else {
3742 SpecTrial::Spec { t0, gen0, rounds }
3743 }
3744 }
3745 SpecTrial::Decided { spec: true, .. } => {
3746 if mon.pays() {
3747 mon.fails = 0;
3748 trial
3749 } else {
3750 mon.fails += 1;
3751 if mon.fails >= 4 {
3752 tracing::info!(
3753 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
3754 mon.tokens,
3755 mon.round_ms,
3756 mon.plain_ms
3757 );
3758 SpecTrial::Decided {
3759 spec: false,
3760 recheck_at: generated + 128,
3761 }
3762 } else {
3763 trial
3764 }
3765 }
3766 }
3767 other => other,
3768 }
3769 }
3770
3771 fn mtp_kv_id(&self) -> u64 {
3774 self.graph_kv_id | (1u64 << 40)
3775 }
3776
3777 const MTP_LAYER_BASE: usize = 0;
3782
3783 #[cfg(feature = "gpu")]
3790 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
3791 self.mtp_graph_mode != Some(true)
3792 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
3793 }
3794
3795 #[cfg(feature = "gpu")]
3801 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
3802 let mut ok = true;
3803 let mut expected = false;
3804 for li in 0..self.num_layers {
3805 if matches!(
3806 self.weights.layers[self.phys_layer(li)].attn,
3807 AttnKind::Full { .. }
3808 ) {
3809 expected = true;
3810 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
3811 }
3812 }
3813 !expected || ok
3814 }
3815
3816 fn graph_gdn_layer_count(&self) -> usize {
3820 (0..self.num_layers)
3821 .filter(|&li| {
3822 matches!(
3823 &self.weights.layers[self.phys_layer(li)].attn,
3824 AttnKind::LinearGdn(_)
3825 )
3826 })
3827 .count()
3828 }
3829
3830 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
3833 let e = self.embed_single(next_token);
3834 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3835 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3836 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3837 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3838 let mut x = vec![0.0f32; self.hidden_size];
3839 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3840 x
3841 }
3842
3843 #[cfg(feature = "gpu")]
3846 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
3847 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
3848 return false;
3849 }
3850 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3851 || !crate::gpu::enabled_here()
3852 || self.attn_softcap > 0.0
3853 || self.attention_heads_per_layer.is_some()
3854 {
3855 return false;
3856 }
3857 matches!(
3858 &m.layer.attn,
3859 AttnKind::Full {
3860 softplus_gate: None,
3861 ..
3862 }
3863 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
3864 }
3865
3866 #[cfg(feature = "gpu")]
3870 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
3871 if !self.mtp_block_graph_ok(m) {
3872 return false;
3873 }
3874 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
3875 return false;
3876 };
3877 let FfnKind::Dense(d) = &m.layer.ffn else {
3878 return false;
3879 };
3880 d.segs.is_empty()
3881 && wq.graph_weight().is_some()
3882 && wk.graph_weight().is_some()
3883 && wv.graph_weight().is_some()
3884 && wo.graph_weight().is_some()
3885 && d.gate_proj.graph_weight().is_some()
3886 && d.up_proj.graph_weight().is_some()
3887 && d.down_proj.graph_weight().is_some()
3888 && self.weights.lm_head.graph_weight().is_some()
3889 }
3890
3891 #[cfg(feature = "gpu")]
3897 fn mtp_step_graph(
3898 &mut self,
3899 m: &mut MtpModule,
3900 hidden: &[f32],
3901 next_token: u32,
3902 position: usize,
3903 ) -> Option<(Vec<f32>, Vec<f32>)> {
3904 if !self.mtp_graph_ok(m) {
3905 return None;
3906 }
3907 let lw = &m.layer;
3908 let AttnKind::Full {
3909 wq,
3910 wk,
3911 wv,
3912 wo,
3913 q_norm,
3914 k_norm,
3915 output_gate,
3916 softplus_gate,
3917 bias,
3918 } = &lw.attn
3919 else {
3920 return None;
3921 };
3922 if softplus_gate.is_some() {
3923 return None;
3924 }
3925 let FfnKind::Dense(d) = &lw.ffn else {
3926 return None;
3927 };
3928 if !d.segs.is_empty() {
3929 return None; }
3931 let mut x = self.mtp_block_input(m, hidden, next_token);
3934 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3935 let (_, i, kind, rs) = t.graph_weight()?;
3936 Some(crate::gpu::GraphW {
3937 idx: i,
3938 kind,
3939 row_scale: rs,
3940 data: &[],
3941 })
3942 }
3943 let (model, _, _, _) = wq.graph_weight()?;
3944 let model = model.clone();
3945 let (lm_gw, lm_rows) = {
3946 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3947 (
3948 crate::gpu::GraphW {
3949 idx: i,
3950 kind,
3951 row_scale: rs,
3952 data: &[],
3953 },
3954 self.weights.lm_head.rows(),
3955 )
3956 };
3957 let layer = crate::gpu::GraphLayer {
3958 input_norm: &lw.input_norm,
3959 attn: crate::gpu::GraphAttn::Full {
3960 wq: gw(wq)?,
3961 wk: gw(wk)?,
3962 wv: gw(wv)?,
3963 wo: gw(wo)?,
3964 q_norm: q_norm.as_deref(),
3965 k_norm: k_norm.as_deref(),
3966 bias: bias
3967 .as_ref()
3968 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3969 output_gate: *output_gate,
3970 cpu_k: m.kv.k_heads(),
3971 cpu_v: m.kv.v_heads(),
3972 },
3973 post_norm: &lw.post_norm,
3974 ffn: crate::gpu::GraphFfn::Dense {
3975 gate: gw(&d.gate_proj)?,
3976 up: gw(&d.up_proj)?,
3977 down: gw(&d.down_proj)?,
3978 },
3979 };
3980 let nh = self.num_heads;
3981 let (nkv, hd, rd) = self.layer_geom(0);
3982 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3983 let mut logits = Vec::new();
3984 let ok = crate::gpu::forward_token_graph(
3985 &model,
3986 self.mtp_kv_id(),
3987 std::slice::from_ref(&layer),
3988 &[None],
3989 self.o1_epoch,
3990 &self.inv_freq,
3991 &mut x,
3992 nh,
3993 nkv,
3994 hd,
3995 self.attn_scale,
3996 rd,
3997 self.hidden_size,
3998 self.intermediate_size,
3999 position,
4000 self.kv_cache.max_seq_len,
4001 gemma,
4002 self.rms_eps as f32,
4003 Some((&lm_gw, lm_rows)),
4004 &m.final_norm,
4005 &mut logits,
4006 &[],
4007 1,
4008 None,
4009 None,
4010 None,
4011 Self::MTP_LAYER_BASE,
4012 true,
4013 );
4014 match ok {
4015 crate::gpu::TokenGraphOutcome::Completed => {}
4016 crate::gpu::TokenGraphOutcome::Declined => return None,
4017 crate::gpu::TokenGraphOutcome::Failed => {
4018 self.clear_sequence_state();
4022 self.graph_failed
4023 .store(true, std::sync::atomic::Ordering::Relaxed);
4024 self.cancel
4025 .store(true, std::sync::atomic::Ordering::Relaxed);
4026 return None;
4027 }
4028 }
4029 logits.resize(self.vocab_size, 0.0);
4030 Some((logits, x))
4031 }
4032
4033 #[cfg(feature = "gpu")]
4041 fn mtp_warm_graph(
4042 &mut self,
4043 m: &mut MtpModule,
4044 pairs: &[(&[f32], u32)],
4045 first_pos: usize,
4046 ) -> crate::gpu::BatchGraphOutcome {
4047 if pairs.is_empty() {
4048 return crate::gpu::BatchGraphOutcome::Completed;
4049 }
4050 if !self.mtp_block_graph_ok(m) {
4051 return crate::gpu::BatchGraphOutcome::Declined;
4052 }
4053 let hs = self.hidden_size;
4054 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
4057 for (h, t) in pairs {
4058 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
4059 }
4060 let lw = &m.layer;
4061 let AttnKind::Full {
4062 wq,
4063 wk,
4064 wv,
4065 wo,
4066 q_norm,
4067 k_norm,
4068 output_gate,
4069 bias,
4070 ..
4071 } = &lw.attn
4072 else {
4073 return crate::gpu::BatchGraphOutcome::Declined;
4074 };
4075 let FfnKind::Dense(d) = &lw.ffn else {
4076 return crate::gpu::BatchGraphOutcome::Declined;
4077 };
4078 if !d.segs.is_empty() {
4079 return crate::gpu::BatchGraphOutcome::Declined; }
4081 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4082 let (_, i, kind, rs) = t.graph_weight()?;
4083 Some(crate::gpu::GraphW {
4084 idx: i,
4085 kind,
4086 row_scale: rs,
4087 data: &[],
4088 })
4089 }
4090 let Some((model, _, _, _)) = wq.graph_weight() else {
4091 return crate::gpu::BatchGraphOutcome::Declined;
4092 };
4093 let model = model.clone();
4094 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
4095 gw(wq),
4096 gw(wk),
4097 gw(wv),
4098 gw(wo),
4099 gw(&d.gate_proj),
4100 gw(&d.up_proj),
4101 gw(&d.down_proj),
4102 ) else {
4103 return crate::gpu::BatchGraphOutcome::Declined;
4104 };
4105 let layer = crate::gpu::GraphLayer {
4106 input_norm: &lw.input_norm,
4107 attn: crate::gpu::GraphAttn::Full {
4108 wq: gwq,
4109 wk: gwk,
4110 wv: gwv,
4111 wo: gwo,
4112 q_norm: q_norm.as_deref(),
4113 k_norm: k_norm.as_deref(),
4114 bias: bias
4115 .as_ref()
4116 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4117 output_gate: *output_gate,
4118 cpu_k: m.kv.k_heads(),
4119 cpu_v: m.kv.v_heads(),
4120 },
4121 post_norm: &lw.post_norm,
4122 ffn: crate::gpu::GraphFfn::Dense {
4123 gate: gg,
4124 up: gu,
4125 down: gd,
4126 },
4127 };
4128 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
4129 let nh = self.num_heads;
4130 let (nkv, hd, rd) = self.layer_geom(0);
4131 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4132 crate::gpu::forward_batch_graph(
4133 &model,
4134 self.mtp_kv_id(),
4135 std::slice::from_ref(&layer),
4136 &self.inv_freq,
4137 &mut hiddens,
4138 nh,
4139 nkv,
4140 hd,
4141 rd,
4142 hs,
4143 self.intermediate_size,
4144 &positions,
4145 self.kv_cache.max_seq_len,
4146 gemma,
4147 self.rms_eps as f32,
4148 self.attn_scale,
4149 pairs.len(),
4150 &[],
4151 0,
4152 None,
4153 )
4154 }
4155
4156 #[cfg(feature = "gpu")]
4163 fn mtp_warm_graph_fallback(
4164 &mut self,
4165 m: &mut MtpModule,
4166 pairs: &[(&[f32], u32)],
4167 first_pos: usize,
4168 ) -> bool {
4169 if pairs.is_empty() {
4170 return true;
4171 }
4172 let graphable = self.mtp_block_graph_ok(m);
4173 if !graphable {
4174 if self.mtp_graph_mode == Some(true) {
4178 return false;
4179 }
4180 self.mtp_graph_mode = Some(false);
4181 for (j, (h, t)) in pairs.iter().enumerate() {
4182 self.mtp_warm(m, h, *t, first_pos + j);
4183 }
4184 return true;
4185 }
4186
4187 for (j, (h, t)) in pairs.iter().enumerate() {
4192 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
4193 return false;
4194 }
4195 }
4196 self.mtp_graph_mode = Some(true);
4197 true
4198 }
4199
4200 #[cfg(feature = "gpu")]
4205 fn mtp_warm_prefill_pairs(
4206 &mut self,
4207 m: &mut MtpModule,
4208 pairs: &[(&[f32], u32)],
4209 first_pos: usize,
4210 ) -> Result<(), &'static str> {
4211 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
4216 if self.mtp_graph_mode == Some(true) {
4217 return Err("MTP token graph became unavailable after admission");
4218 }
4219 self.mtp_graph_mode = Some(false);
4220 for (j, (h, t)) in pairs.iter().enumerate() {
4221 self.mtp_warm(m, h, *t, first_pos + j);
4222 }
4223 return Ok(());
4224 }
4225 match self.mtp_warm_graph(m, pairs, first_pos) {
4226 crate::gpu::BatchGraphOutcome::Completed => {
4227 if !pairs.is_empty() {
4228 self.mtp_graph_mode = Some(true);
4229 }
4230 Ok(())
4231 }
4232 crate::gpu::BatchGraphOutcome::Declined => {
4233 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
4234 Ok(())
4235 } else {
4236 Err("MTP warm-up fallback failed after device admission")
4237 }
4238 }
4239 crate::gpu::BatchGraphOutcome::Failed => {
4240 Err("MTP warm batch graph failed after admission")
4241 }
4242 }
4243 }
4244
4245 #[cfg(not(feature = "gpu"))]
4246 fn mtp_warm_prefill_pairs(
4247 &mut self,
4248 m: &mut MtpModule,
4249 pairs: &[(&[f32], u32)],
4250 first_pos: usize,
4251 ) -> Result<(), &'static str> {
4252 for (j, (h, t)) in pairs.iter().enumerate() {
4253 self.mtp_warm(m, h, *t, first_pos + j);
4254 }
4255 Ok(())
4256 }
4257
4258 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
4262 let e = self.embed_single(next_token);
4263 let mut cat = vec![0.0f32; 2 * self.hidden_size];
4264 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
4265 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
4266 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
4267 let mut x = vec![0.0f32; self.hidden_size];
4268 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
4269 inference::rms_norm_into(
4270 &x,
4271 &m.layer.input_norm,
4272 self.rms_eps,
4273 self.norm_style,
4274 &mut self.ws.n1,
4275 );
4276 let attn = match &m.layer.attn {
4277 AttnKind::Full {
4278 wq,
4279 wk,
4280 wv,
4281 wo,
4282 q_norm,
4283 k_norm,
4284 output_gate,
4285 softplus_gate,
4286 bias,
4287 } => {
4288 let mut cfg = self.attn_cfg(position);
4289 cfg.q_norm = q_norm.as_deref();
4290 cfg.k_norm = k_norm.as_deref();
4291 cfg.output_gate = *output_gate;
4292 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
4293 cfg.bias = bias
4294 .as_ref()
4295 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
4296 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
4297 }
4298 _ => return,
4299 };
4300 let _ = attn;
4301 }
4302
4303 #[cfg(feature = "gpu")]
4310 #[allow(clippy::too_many_arguments)]
4311 fn graph_spec_step(
4312 &mut self,
4313 m: &mut MtpModule,
4314 hidden: &[f32],
4315 t_next: u32,
4316 next_pos: usize,
4317 drafted: &mut usize,
4318 accepted: &mut usize,
4319 all_ids: &mut Vec<u32>,
4323 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
4324 #[cfg(target_os = "macos")]
4335 let metal_native = crate::gpu::q1_force();
4336 #[cfg(not(target_os = "macos"))]
4337 let metal_native = false;
4338 #[cfg(feature = "gpu")]
4339 let k_default = if metal_native {
4340 7
4343 } else if crate::gpu_wgpu::verify_i8_on() {
4344 5
4345 } else {
4346 4
4347 };
4348 #[cfg(not(feature = "gpu"))]
4349 let k_default = 4;
4350 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
4351 .ok()
4352 .and_then(|v| v.parse().ok())
4353 .filter(|&v| (1..=8).contains(&v))
4354 .unwrap_or(k_default);
4355 if next_pos == 0 {
4356 return None;
4357 }
4358 let t_round = std::time::Instant::now();
4359 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
4375 let sub0 = subs();
4376 let cfg = self.sampler_config.clone();
4381 let penalized = !(cfg.repetition_penalty == 1.0
4382 && cfg.presence_penalty == 0.0
4383 && cfg.suppress_tokens.is_empty());
4384 let greedy_pen = cfg.temperature < 1e-6 && penalized;
4389 let sampling = cfg.temperature >= 1e-6;
4390 let sparse = sampling && sampler::sparse_ok(&cfg);
4396 let base_len = all_ids.len();
4397 if sampling && !sparse && self.spec_q.len() < k_spec {
4398 self.spec_q.resize_with(k_spec, Vec::new);
4399 }
4400 if sparse && self.spec_qs.len() < k_spec {
4401 self.spec_qs.resize_with(k_spec, Vec::new);
4402 }
4403 let mut drafts = Vec::with_capacity(k_spec);
4408 let mut hx = hidden.to_vec();
4409 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
4412 for j in 0..k_spec {
4413 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
4414 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
4415 if spec_dbg {
4416 let saved = self.mtp_graph_mode;
4417 self.mtp_graph_mode = Some(false);
4418 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4419 self.mtp_graph_mode = saved;
4420 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4421 return None;
4422 }
4423 m.kv.truncate_last(1);
4424 dbg_ref = Some(r);
4425 }
4426 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
4427 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
4428 return None;
4429 }
4430 if let Some((lg_cpu, h_cpu)) = dbg_ref {
4431 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
4432 let dl = lg
4433 .iter()
4434 .zip(&lg_cpu)
4435 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4436 let dh = hj
4437 .iter()
4438 .zip(&h_cpu)
4439 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
4440 eprintln!(
4441 "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 {}",
4442 next_pos - 1 + j,
4443 sampler::argmax(&lg_cpu),
4444 sampler::argmax(&lg),
4445 n(&h_cpu),
4446 n(&hj),
4447 m.kv.seq_len
4448 );
4449 }
4450 let dj = if sparse {
4451 let mut q = std::mem::take(&mut self.spec_qs[j]);
4452 let ok = sampler::sparse_distribution_into(
4453 &lg,
4454 &cfg,
4455 all_ids,
4456 &mut self.sampler_scratch,
4457 self.pool.as_deref(),
4458 &mut q,
4459 );
4460 let d = if ok {
4461 sampler::draw_sparse(&q, &mut self.rng)
4462 } else {
4463 let t = sampler::argmax(&lg);
4465 q.clear();
4466 q.push((t, 1.0));
4467 t
4468 };
4469 self.spec_qs[j] = q;
4470 all_ids.push(d);
4471 d
4472 } else if sampling {
4473 let mut q = std::mem::take(&mut self.spec_q[j]);
4474 sampler::distribution_into(
4475 &lg,
4476 &cfg,
4477 all_ids,
4478 &mut self.sampler_scratch,
4479 self.pool.as_deref(),
4480 &mut q,
4481 );
4482 let d = sampler::draw(&q, &mut self.rng);
4483 self.spec_q[j] = q;
4484 all_ids.push(d); d
4486 } else if greedy_pen {
4487 let d = sampler::argmax_penalized(
4488 &lg,
4489 &cfg,
4490 all_ids,
4491 &mut self.sampler_scratch,
4492 self.pool.as_deref(),
4493 );
4494 all_ids.push(d);
4495 d
4496 } else {
4497 sampler::argmax(&lg)
4498 };
4499 attention::recycle_buf(&mut lg);
4500 drafts.push(dj);
4501 hx = hj;
4502 }
4503 all_ids.truncate(base_len);
4504 *drafted += k_spec;
4505 let t_draft = t_round.elapsed();
4506 let sub_draft = subs();
4507 let b = k_spec + 1;
4510 let mut hiddens = vec![0.0f32; b * self.hidden_size];
4511 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
4512 let e = self.embed_single(t);
4513 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
4514 }
4515 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
4516 let (lm_gw, lm_rows) = {
4517 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
4518 (
4519 crate::gpu::GraphW {
4520 idx: i,
4521 kind,
4522 row_scale: rs,
4523 data: &[],
4524 },
4525 self.weights.lm_head.rows(),
4526 )
4527 };
4528 let mut logits = Vec::new();
4529 let final_norm = self.weights.final_norm.clone();
4530 #[cfg(target_os = "macos")]
4531 let verify_outcome = if metal_native {
4532 let lm = self.weights.lm_head.q1_parts()?;
4533 self.try_batch_graph_metal(
4534 &mut hiddens,
4535 &positions,
4536 b,
4537 Some((lm, &final_norm, &mut logits)),
4538 )
4539 } else {
4540 self.try_batch_graph_wgpu(
4541 &mut hiddens,
4542 &positions,
4543 b,
4544 Some(crate::gpu::SpecTail {
4545 lm: lm_gw,
4546 lm_rows,
4547 final_norm: &final_norm,
4548 logits_out: &mut logits,
4549 }),
4550 )
4551 };
4552 #[cfg(not(target_os = "macos"))]
4553 let verify_outcome = self.try_batch_graph_wgpu(
4554 &mut hiddens,
4555 &positions,
4556 b,
4557 Some(crate::gpu::SpecTail {
4558 lm: lm_gw,
4559 lm_rows,
4560 final_norm: &final_norm,
4561 logits_out: &mut logits,
4562 }),
4563 );
4564 match verify_outcome {
4565 crate::gpu::BatchGraphOutcome::Completed => {}
4566 crate::gpu::BatchGraphOutcome::Declined => {
4567 m.kv.truncate_last(k_spec);
4571 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
4572 self.clear_sequence_state();
4573 self.graph_failed
4574 .store(true, std::sync::atomic::Ordering::Relaxed);
4575 self.cancel
4576 .store(true, std::sync::atomic::Ordering::Relaxed);
4577 tracing::error!("MTP graph mirror rewind failed after verify decline");
4578 }
4579 return None;
4580 }
4581 crate::gpu::BatchGraphOutcome::Failed => {
4582 self.clear_sequence_state();
4586 self.graph_failed
4587 .store(true, std::sync::atomic::Ordering::Relaxed);
4588 self.cancel
4589 .store(true, std::sync::atomic::Ordering::Relaxed);
4590 tracing::error!("MTP verify batch graph failed after admission");
4591 return None;
4592 }
4593 }
4594 #[cfg(target_os = "macos")]
4600 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
4601 let snap: Vec<Vec<f32>> = self
4602 .kv_cache
4603 .layers
4604 .iter()
4605 .map(|l| l.linear_state.clone())
4606 .collect();
4607 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4608 let toks: Vec<u32> = std::iter::once(t_next)
4609 .chain(drafts.iter().copied())
4610 .collect();
4611 let want_save = self.graph_want_logits;
4612 self.graph_want_logits = false;
4613 for (i, &t) in toks.iter().enumerate() {
4614 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4615 let _ = self.graph_logits.take();
4616 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
4620 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
4621 }
4622 let ref_lg = self.logits_from_hidden(&hi);
4623 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
4624 let ra = sampler::argmax(&ref_lg);
4625 let va = sampler::argmax(row);
4626 let mut md = 0f32;
4627 let mut rms = 0f64;
4628 for j in 0..lm_rows.min(ref_lg.len()) {
4629 let d = (ref_lg[j] - row[j]).abs();
4630 md = md.max(d);
4631 rms += (d as f64) * (d as f64);
4632 }
4633 let mut hd = 0f32;
4634 for j in 0..self.hidden_size {
4635 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
4636 }
4637 eprintln!(
4638 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
4639 next_pos + i,
4640 if ra == va { "OK" } else { "MISMATCH" },
4641 (rms / lm_rows as f64).sqrt()
4642 );
4643 }
4644 self.graph_want_logits = want_save;
4645 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
4648 if l.linear_state.len() == st.len() {
4649 l.linear_state.copy_from_slice(&st);
4650 } else {
4651 l.linear_state = st;
4652 }
4653 }
4654 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
4655 let extra = l.seq_len.saturating_sub(n0);
4656 if extra > 0 {
4657 l.truncate_last(extra);
4658 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
4659 }
4660 }
4661 }
4662 let t_verify = t_round.elapsed();
4663 let sub_verify = subs();
4664 let mut a = 0usize;
4669 let mut forced: Option<u32> = None;
4670 let ids: Vec<u32> = if sparse {
4671 let mut p = std::mem::take(&mut self.spec_ps);
4672 let mut res = std::mem::take(&mut self.spec_ress);
4673 while a < k_spec {
4674 let ok = sampler::sparse_distribution_into(
4675 &logits[a * lm_rows..(a + 1) * lm_rows],
4676 &cfg,
4677 all_ids,
4678 &mut self.sampler_scratch,
4679 self.pool.as_deref(),
4680 &mut p,
4681 );
4682 if !ok {
4683 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
4684 p.clear();
4685 p.push((t, 1.0));
4686 }
4687 match sampler::spec_accept_or_correct_sparse(
4688 &p,
4689 &self.spec_qs[a],
4690 drafts[a],
4691 &mut self.rng,
4692 &mut res,
4693 ) {
4694 None => {
4695 all_ids.push(drafts[a]);
4696 a += 1;
4697 }
4698 Some(c) => {
4699 forced = Some(c);
4700 break;
4701 }
4702 }
4703 }
4704 all_ids.truncate(base_len);
4705 self.spec_ps = p;
4706 self.spec_ress = res;
4707 drafts.clone()
4708 } else if sampling {
4709 let mut p = std::mem::take(&mut self.spec_p);
4710 let mut res = std::mem::take(&mut self.spec_res);
4711 while a < k_spec {
4712 sampler::distribution_into(
4713 &logits[a * lm_rows..(a + 1) * lm_rows],
4714 &cfg,
4715 all_ids,
4716 &mut self.sampler_scratch,
4717 self.pool.as_deref(),
4718 &mut p,
4719 );
4720 match sampler::spec_accept_or_correct(
4721 &p,
4722 &self.spec_q[a],
4723 drafts[a],
4724 &mut self.rng,
4725 &mut res,
4726 self.pool.as_deref(),
4727 ) {
4728 None => {
4729 all_ids.push(drafts[a]);
4730 a += 1;
4731 }
4732 Some(c) => {
4733 forced = Some(c);
4734 break;
4735 }
4736 }
4737 }
4738 all_ids.truncate(base_len);
4739 self.spec_p = p;
4740 self.spec_res = res;
4741 drafts.clone()
4743 } else if greedy_pen {
4744 let mut ids: Vec<u32> = Vec::with_capacity(b);
4748 for i in 0..b {
4749 let t = sampler::argmax_penalized(
4750 &logits[i * lm_rows..(i + 1) * lm_rows],
4751 &cfg,
4752 all_ids,
4753 &mut self.sampler_scratch,
4754 self.pool.as_deref(),
4755 );
4756 ids.push(t);
4757 if i < k_spec && t == drafts[i] {
4758 all_ids.push(t);
4759 } else {
4760 break;
4761 }
4762 }
4763 all_ids.truncate(base_len);
4764 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
4765 a += 1;
4766 }
4767 ids
4770 } else {
4771 let ids: Vec<u32> = (0..b)
4772 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
4773 .collect();
4774 while a < k_spec && ids[a] == drafts[a] {
4775 a += 1;
4776 }
4777 ids
4778 };
4779 if spec_dbg {
4780 eprintln!(
4781 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
4782 drafts, ids
4783 );
4784 }
4785 #[cfg(target_os = "macos")]
4789 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
4790 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
4791 {
4792 let snap: Vec<Vec<f32>> = self
4793 .kv_cache
4794 .layers
4795 .iter()
4796 .map(|l| l.linear_state.clone())
4797 .collect();
4798 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4799 let toks: Vec<u32> = std::iter::once(t_next)
4800 .chain(drafts.iter().copied())
4801 .collect();
4802 let want_save = self.graph_want_logits;
4803 self.graph_want_logits = false;
4804 for (i, &t) in toks.iter().take(a + 1).enumerate() {
4805 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4806 let _ = self.graph_logits.take();
4807 }
4808 self.graph_want_logits = want_save;
4809 let plain_states: Vec<Vec<f32>> = self
4810 .kv_cache
4811 .layers
4812 .iter()
4813 .map(|l| l.linear_state.clone())
4814 .collect();
4815 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4816 let mut rows = Vec::new();
4817 for (li, (l, n0)) in self
4818 .kv_cache
4819 .layers
4820 .iter_mut()
4821 .zip(attn_lens.iter())
4822 .enumerate()
4823 {
4824 let extra = l.seq_len.saturating_sub(*n0);
4825 if extra > 0 {
4826 let mut kk = Vec::new();
4827 let mut vv = Vec::new();
4828 for g in 0..nkv {
4829 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4830 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4831 }
4832 rows.push((li, kk, vv));
4833 l.truncate_last(extra);
4834 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
4835 }
4836 }
4837 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
4838 if l.linear_state.len() == st.len() {
4839 l.linear_state.copy_from_slice(&st);
4840 } else {
4841 l.linear_state = st;
4842 }
4843 }
4844 Some((plain_states, rows))
4845 } else {
4846 None
4847 };
4848 #[cfg(target_os = "macos")]
4850 if metal_native {
4851 self.metal_verify_commit(a);
4854 if let Some((plain_states, rows)) = commit_ref {
4855 crate::gpu_metal::queue_fence();
4856 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4857 let mut worst_s = 0f32;
4858 let mut worst_li = 0usize;
4859 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
4860 if l.linear_state.len() != ps.len() || ps.is_empty() {
4861 continue;
4862 }
4863 let d = l
4864 .linear_state
4865 .iter()
4866 .zip(ps)
4867 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4868 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
4869 let rel = d / n.max(1e-6);
4870 if rel > worst_s {
4871 worst_s = rel;
4872 worst_li = li;
4873 }
4874 }
4875 let mut worst_k = 0f32;
4876 for (li, kk, vv) in &rows {
4877 let l = &self.kv_cache.layers[*li];
4878 let n0 = l.seq_len - (kk.len() / (nkv * hd));
4879 let mut ck = Vec::new();
4880 let mut cv = Vec::new();
4881 for g in 0..nkv {
4882 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4883 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4884 }
4885 if ck.len() == kk.len() {
4886 let dk = ck
4887 .iter()
4888 .zip(kk)
4889 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4890 let dv = cv
4891 .iter()
4892 .zip(vv)
4893 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4894 worst_k = worst_k.max(dk).max(dv);
4895 } else {
4896 eprintln!(
4897 "commit-check L{li}: kv row count mismatch {} vs {}",
4898 ck.len(),
4899 kk.len()
4900 );
4901 }
4902 }
4903 eprintln!(
4904 "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}"
4905 );
4906 }
4907 }
4908 if !metal_native && a + 1 < b {
4909 let expected_gdn_layers = self.graph_gdn_layer_count();
4910 if expected_gdn_layers > 0
4911 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
4912 {
4913 self.clear_sequence_state();
4914 self.graph_failed
4915 .store(true, std::sync::atomic::Ordering::Relaxed);
4916 self.cancel
4917 .store(true, std::sync::atomic::Ordering::Relaxed);
4918 tracing::error!("GDN speculative restore failed after verify");
4919 return None;
4920 }
4921 }
4922 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
4923 self.clear_sequence_state();
4928 self.graph_failed
4929 .store(true, std::sync::atomic::Ordering::Relaxed);
4930 self.cancel
4931 .store(true, std::sync::atomic::Ordering::Relaxed);
4932 tracing::error!("trunk graph KV rewind failed after speculative verify");
4933 return None;
4934 }
4935 *accepted += a;
4936 m.kv.truncate_last(k_spec.saturating_sub(1));
4947 #[cfg(target_os = "macos")]
4948 if metal_native && self.mtp_graph_mode == Some(true) {
4949 crate::gpu_metal::kv_mirror_set_stored(
4952 self.mtp_kv_id(),
4953 Self::MTP_LAYER_BASE,
4954 m.kv.seq_len,
4955 );
4956 }
4957 if !metal_native
4958 && self.mtp_graph_mode == Some(true)
4959 && !self.rewind_mtp_graph_mirror(next_pos)
4960 {
4961 self.clear_sequence_state();
4965 self.graph_failed
4966 .store(true, std::sync::atomic::Ordering::Relaxed);
4967 self.cancel
4968 .store(true, std::sync::atomic::Ordering::Relaxed);
4969 tracing::error!("MTP graph mirror rewind failed after verify commit");
4970 return None;
4971 }
4972 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
4973 if !warm_off && a > 0 {
4974 let mut warmed = false;
4977 #[cfg(target_os = "macos")]
4978 if metal_native && self.mtp_graph_mode == Some(true) {
4979 let pairs: Vec<(&[f32], u32)> = (0..a)
4983 .map(|j| {
4984 (
4985 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
4986 ids[j],
4987 )
4988 })
4989 .collect();
4990 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
4991 if !warmed {
4992 warmed = true;
4993 for j in 0..a {
4994 let row =
4995 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
4996 if self
4997 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
4998 .is_none()
4999 {
5000 warmed = false;
5001 break;
5002 }
5003 }
5004 }
5005 }
5006 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
5007 let rows: Vec<Vec<f32>> = (0..a)
5008 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
5009 .collect();
5010 let pairs: Vec<(&[f32], u32)> = rows
5011 .iter()
5012 .zip(ids.iter())
5013 .map(|(r, &t)| (r.as_slice(), t))
5014 .collect();
5015 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
5016 Ok(()) => warmed = true,
5017 Err(err) => {
5018 tracing::error!("{err}");
5024 self.clear_sequence_state();
5025 self.graph_failed
5026 .store(true, std::sync::atomic::Ordering::Relaxed);
5027 self.cancel
5028 .store(true, std::sync::atomic::Ordering::Relaxed);
5029 return None;
5030 }
5031 }
5032 }
5033 if !warmed {
5034 for j in 0..a {
5035 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
5036 let row = row.to_vec();
5037 self.mtp_warm(m, &row, ids[j], next_pos + j);
5038 }
5039 }
5040 }
5041 if let Some(c) = forced {
5045 self.spec_forced = Some(c);
5046 self.graph_logits = None;
5047 } else {
5048 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
5049 row.resize(self.vocab_size, 0.0);
5050 if let Some(c) = self.final_softcap {
5051 for l in row.iter_mut() {
5052 *l = c * (*l / c).tanh();
5053 }
5054 }
5055 self.graph_logits = Some(row);
5056 }
5057 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
5058 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
5064 let end = subs();
5065 eprintln!(
5066 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
5067 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
5068 t_draft.as_secs_f64() * 1e3,
5069 sub_draft - sub0,
5070 (t_verify - t_draft).as_secs_f64() * 1e3,
5071 sub_verify - sub_draft,
5072 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
5073 end - sub_verify,
5074 );
5075 }
5076 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
5077 }
5078
5079 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
5088 if !self.pair_supported() {
5089 return (0.0, 0.0);
5090 }
5091 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
5098 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5099 let emb1 = self.embed_single(1);
5100 let emb2 = self.embed_single(2);
5101 let pos = self.kv_cache.seq_len();
5102
5103 let t0 = std::time::Instant::now();
5104 for _ in 0..iters {
5105 let _ = self.forward_layers(&emb1, pos, None);
5106 let _ = self.forward_layers(&emb2, pos + 1, None);
5107 for l in &mut self.kv_cache.layers {
5108 l.truncate_last(2);
5109 }
5110 }
5111 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5112
5113 let t1 = std::time::Instant::now();
5114 for _ in 0..iters {
5115 let _ = self.forward_pair(&emb1, &emb2, pos);
5116 for l in &mut self.kv_cache.layers {
5117 l.truncate_last(2);
5118 }
5119 }
5120 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
5121 match graph_env {
5122 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
5123 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
5124 }
5125 (singles_ms, pair_ms)
5126 }
5127
5128 fn pair_supported(&self) -> bool {
5136 !self.weights.layers.is_empty()
5143 && self.g3n.is_none()
5144 && !self
5145 .weights
5146 .layers
5147 .iter()
5148 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
5149 }
5150
5151 fn forward_pair(
5152 &mut self,
5153 emb1: &[f32],
5154 emb2: &[f32],
5155 position: usize,
5156 ) -> (Vec<f32>, Vec<f32>) {
5157 let mut h1 = emb1.to_vec();
5158 let mut h2 = emb2.to_vec();
5159 let (_nkv, _hd, hs, _rd, eps) = (
5160 self.num_kv_heads,
5161 self.head_dim,
5162 self.hidden_size,
5163 self.rotary_dim,
5164 self.rms_eps,
5165 );
5166 let pool = self.pool.clone();
5167
5168 for li in 0..self.num_layers {
5169 let lw = &self.weights.layers[self.phys_layer(li)];
5170 inference::rms_norm_into(
5173 &h1,
5174 &lw.input_norm,
5175 self.rms_eps,
5176 self.norm_style,
5177 &mut self.ws.n1,
5178 );
5179 inference::rms_norm_into(
5180 &h2,
5181 &lw.input_norm,
5182 self.rms_eps,
5183 self.norm_style,
5184 &mut self.ws.n2,
5185 );
5186
5187 let (a1, a2) = match &lw.attn {
5188 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5189 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5190 AttnKind::Linear(w) => {
5191 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5192 let layer = &mut self.kv_cache.layers[li];
5193 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5194 vmf_phase_pair(
5195 &self.ws.n1,
5196 &self.ws.n2,
5197 w,
5198 &cfg,
5199 state,
5200 scratch,
5201 self.pool.as_deref(),
5202 )
5203 }
5204 AttnKind::LinearGdn(w) => {
5205 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5206 let layer = &mut self.kv_cache.layers[li];
5207 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5208 gdn_pair(
5209 &self.ws.n1,
5210 &self.ws.n2,
5211 w,
5212 &cfg,
5213 state,
5214 scratch,
5215 self.pool.as_deref(),
5216 )
5217 }
5218 AttnKind::ShortConv(w) => {
5219 let cfg = self
5220 .short_conv_cfg
5221 .expect("short-conv layer without short_conv_cfg");
5222 let layer = &mut self.kv_cache.layers[li];
5223 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
5224 short_conv_pair(
5225 &self.ws.n1,
5226 &self.ws.n2,
5227 w,
5228 &cfg,
5229 state,
5230 scratch,
5231 self.pool.as_deref(),
5232 )
5233 }
5234 AttnKind::Full {
5235 wq,
5236 wk,
5237 wv,
5238 wo,
5239 q_norm,
5240 k_norm,
5241 output_gate,
5242 softplus_gate,
5243 bias,
5244 } => {
5245 let inv_freq_l = self.layer_inv_freq(li);
5246 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5247 let cfg = QwenAttnCfg {
5248 num_heads: self.layer_num_heads(li),
5249 num_kv_heads: nkv_l,
5250 head_dim: hd_l,
5251 hidden_size: hs,
5252 position,
5253 inv_freq: &inv_freq_l,
5254 rotary_dim: rd_l,
5255 scale: self.attn_scale,
5256 softcap: self.attn_softcap,
5257 window: self.layer_window(li),
5258 v_norm: self.attn_v_norm,
5259 q_norm: q_norm.as_deref(),
5260 k_norm: k_norm.as_deref(),
5261 output_gate: *output_gate,
5262 softplus_gate: softplus_gate
5263 .as_ref()
5264 .map(|(gate, per_head)| (gate, *per_head)),
5265 rope_scale: self.layer_rope_scale(li),
5266 bias: bias
5267 .as_ref()
5268 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5269 rms_eps: eps,
5270 norm_style: self.norm_style,
5271 pool: pool.as_deref(),
5272 };
5273 attention::qwen_attention_pair(
5274 &self.ws.n1,
5275 &self.ws.n2,
5276 wq,
5277 wk,
5278 wv,
5279 wo,
5280 &mut self.kv_cache.layers[li],
5281 &cfg,
5282 )
5283 }
5284 };
5285 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5286 Some(w) => (
5287 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
5288 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
5289 ),
5290 None => (a1, a2),
5291 };
5292 for i in 0..self.hidden_size {
5293 h1[i] += a1[i];
5294 h2[i] += a2[i];
5295 }
5296 let (mut a1, mut a2) = (a1, a2);
5297 attention::recycle_buf(&mut a1);
5298 attention::recycle_buf(&mut a2);
5299
5300 let lw = &self.weights.layers[self.phys_layer(li)];
5301 inference::rms_norm_into(
5302 &h1,
5303 &lw.post_norm,
5304 self.rms_eps,
5305 self.norm_style,
5306 &mut self.ws.p1,
5307 );
5308 inference::rms_norm_into(
5309 &h2,
5310 &lw.post_norm,
5311 self.rms_eps,
5312 self.norm_style,
5313 &mut self.ws.p2,
5314 );
5315 let (f1, f2) = match &lw.ffn {
5316 FfnKind::DenseMoe(dm) => (
5319 dense_moe_ffn(
5320 dm,
5321 &self.ws.p1,
5322 &h1,
5323 self.rms_eps,
5324 self.norm_style,
5325 self.pool.as_deref(),
5326 ),
5327 dense_moe_ffn(
5328 dm,
5329 &self.ws.p2,
5330 &h2,
5331 self.rms_eps,
5332 self.norm_style,
5333 self.pool.as_deref(),
5334 ),
5335 ),
5336 _ => ffn_forward_pair(
5337 &lw.ffn,
5338 &self.ws.p1,
5339 &self.ws.p2,
5340 self.pool.as_deref(),
5341 None,
5342 ),
5343 };
5344 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5345 Some(w) => (
5346 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
5347 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
5348 ),
5349 None => (f1, f2),
5350 };
5351 for i in 0..self.hidden_size {
5352 h1[i] += f1[i];
5353 h2[i] += f2[i];
5354 }
5355 let (mut f1, mut f2) = (f1, f2);
5356 attention::recycle_buf(&mut f1);
5357 attention::recycle_buf(&mut f2);
5358 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5359 for i in 0..self.hidden_size {
5360 h1[i] *= sc;
5361 h2[i] *= sc;
5362 }
5363 }
5364 if self.is_loop_end(li) && li + 1 < self.num_layers {
5366 h1 = inference::rms_norm(
5367 &h1,
5368 &self.weights.final_norm,
5369 self.rms_eps,
5370 self.norm_style,
5371 );
5372 h2 = inference::rms_norm(
5373 &h2,
5374 &self.weights.final_norm,
5375 self.rms_eps,
5376 self.norm_style,
5377 );
5378 }
5379 }
5380 (h1, h2)
5381 }
5382
5383 fn commit_linear_scratch(&mut self) {
5385 for layer in &mut self.kv_cache.layers {
5386 if !layer.linear_scratch.is_empty() {
5387 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
5388 layer.linear_scratch.clear();
5389 }
5390 }
5391 }
5392
5393 pub fn forward_ids(
5396 &mut self,
5397 ids: &[u32],
5398 task_mask: Option<&TaskMask>,
5399 ) -> Result<Vec<f32>, String> {
5400 if ids.is_empty() {
5401 return Err("empty id sequence".to_string());
5402 }
5403 self.clear_sequence_state();
5404 self.check_forward_graph("forward_ids setup", 0)?;
5405 self.o1_begin();
5406 let mut hidden = vec![0.0f32; self.hidden_size];
5407 let mut pos = 0usize;
5408 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
5416 let chunk = prefill_chunk();
5420 let hs = self.hidden_size;
5421 while pos < ids.len() {
5422 let end = (pos + chunk).min(ids.len());
5423 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5424 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
5425 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5426 pos = end;
5427 }
5428 }
5429 if task_mask.is_none()
5438 && !self.graph_prefill_preferred()
5439 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
5440 && self.pair_supported()
5441 {
5442 while pos + 1 < ids.len() {
5443 let e1 = self.embed_single(ids[pos]);
5444 let e2 = self.embed_single(ids[pos + 1]);
5445 let (_, h2) = self.forward_pair(&e1, &e2, pos);
5446 self.check_forward_graph("forward_ids pair", pos + 1)?;
5447 self.commit_linear_scratch();
5448 hidden = h2;
5449 pos += 2;
5450 }
5451 }
5452 while pos < ids.len() {
5453 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5454 self.check_forward_graph("forward_ids", pos)?;
5455 pos += 1;
5456 }
5457 self.o1_seal();
5461 let normed = inference::rms_norm(
5462 &hidden,
5463 &self.weights.final_norm,
5464 self.rms_eps,
5465 self.norm_style,
5466 );
5467 Ok(self.lm_head_forward(&normed))
5468 }
5469
5470 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
5477 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
5478 Ok((nll / cnt.max(1) as f64).exp())
5479 }
5480
5481 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
5486 self.clear_sequence_state();
5487 FFN_PROBE.with(|p| {
5488 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
5489 });
5490 crate::gpu::cpu_scope(|| {
5491 for (pos, &id) in ids.iter().enumerate() {
5492 let emb = self.embed_single(id);
5493 let _ = self.forward_layers(&emb, pos, None);
5494 }
5495 });
5496 self.clear_sequence_state();
5497 FFN_PROBE
5498 .with(|p| p.borrow_mut().take())
5499 .unwrap_or_default()
5500 }
5501
5502 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
5506 if let Err(err) = self.nll_begin() {
5507 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
5511 self.nll_end();
5512 return Err(err);
5513 }
5514 FFN_PROBE.with(|p| {
5515 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
5516 });
5517 let result: Result<(), String> = (|| {
5518 for chunk in ids.chunks(256) {
5519 if chunk.len() < 2 {
5520 continue;
5521 }
5522 self.nll_ids_masked(chunk, 0, None)?;
5523 }
5524 Ok(())
5525 })();
5526 self.nll_end();
5527 let probe = FFN_PROBE
5528 .with(|p| p.borrow_mut().take())
5529 .unwrap_or_default();
5530 match result {
5531 Ok(()) => Ok(probe),
5532 Err(err) => {
5533 drop(probe);
5534 Err(err)
5535 }
5536 }
5537 }
5538
5539 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
5543 self.nll_begin()?;
5544 let result: Result<f64, String> = (|| {
5545 let mut nll = 0f64;
5546 let mut cnt = 0usize;
5547 let mut hidden = vec![0f32; self.hidden_size];
5548 for (pos, &id) in ids.iter().enumerate() {
5549 if pos > 0 {
5550 inference::rms_norm_into(
5551 &hidden,
5552 &self.weights.final_norm,
5553 self.rms_eps,
5554 self.norm_style,
5555 &mut self.ws.n1,
5556 );
5557 let mut logits = self.lm_head_forward(&self.ws.n1);
5558 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
5559 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
5560 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
5561 nll -= p.max(1e-300).ln();
5562 cnt += 1;
5563 attention::recycle_buf(&mut logits);
5564 }
5565 let emb = self.embed_single(id);
5566 hidden = self.forward_layers(&emb, pos, Some(mask));
5567 self.nll_check_graph("masked serial forward", pos)?;
5568 let _ = self.graph_logits.take();
5572 }
5573 Ok((nll / cnt.max(1) as f64).exp())
5574 })();
5575 self.nll_end();
5576 result
5577 }
5578
5579 pub fn nll_ids_masked(
5598 &mut self,
5599 ids: &[u32],
5600 start: usize,
5601 task_mask: Option<&TaskMask>,
5602 ) -> Result<(f64, usize), String> {
5603 let task_mask = self.drop_open_mask(task_mask);
5604 self.nll_ids_inner(ids, start, task_mask)
5605 }
5606
5607 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
5608 self.nll_ids_inner(ids, start, None)
5609 }
5610
5611 fn nll_ids_inner(
5612 &mut self,
5613 ids: &[u32],
5614 start: usize,
5615 task_mask: Option<&TaskMask>,
5616 ) -> Result<(f64, usize), String> {
5617 self.nll_begin()?;
5618 let result: Result<(f64, usize), String> = (|| {
5619 let mut nll = 0f64;
5620 let mut cnt = 0usize;
5621 if self.can_prefill_batched() {
5622 const CHUNK: usize = 128;
5628 const LM_SUB: usize = 32;
5629 let n = ids.len().saturating_sub(1);
5630 let hs = self.hidden_size;
5631 let rows = self.weights.lm_head.rows();
5632 let mut pos = 0usize;
5633 while pos < n {
5634 let end = (pos + CHUNK).min(n);
5635 let bsz = end - pos;
5636 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
5637 self.nll_check_graph("batched prefill", pos)?;
5638 let mut k0 = 0usize;
5639 while k0 < bsz {
5640 let k1 = (k0 + LM_SUB).min(bsz);
5641 let sb = k1 - k0;
5642 if pos + k1 <= start {
5645 k0 = k1;
5646 continue;
5647 }
5648 let mut normed = vec![0.0f32; sb * hs];
5649 for k in 0..sb {
5650 let r = inference::rms_norm(
5651 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
5652 &self.weights.final_norm,
5653 self.rms_eps,
5654 self.norm_style,
5655 );
5656 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
5657 }
5658 let mut logits = vec![0.0f32; sb * rows];
5659 self.weights
5660 .lm_head
5661 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
5662 for k in 0..sb {
5663 if pos + k0 + k < start {
5664 continue;
5665 }
5666 self.nll_check_graph("batched score row", pos + k0 + k)?;
5667 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
5668 if let Some(mu) = self.logit_multiplier {
5669 for v in lg.iter_mut() {
5670 *v *= mu;
5671 }
5672 }
5673 if let Some(c) = self.final_softcap {
5677 for v in lg.iter_mut() {
5678 *v = c * (*v / c).tanh();
5679 }
5680 }
5681 if let Some(cm) = self.head_clusters.clone() {
5684 self.hierarchical_head_logprobs(
5685 &normed[k * hs..(k + 1) * hs],
5686 &cm,
5687 lg,
5688 );
5689 }
5690 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
5691 let target = ids[pos + k0 + k + 1] as usize;
5692 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5693 let lse: f64 = lg
5694 .iter()
5695 .map(|&v| ((v - max) as f64).exp())
5696 .sum::<f64>()
5697 .ln()
5698 + max as f64;
5699 nll += lse - lg[target] as f64;
5700 cnt += 1;
5701 if std::env::var("CMF_PPL_TRACE").is_ok() {
5702 let top = lg
5703 .iter()
5704 .enumerate()
5705 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5706 .map(|(i, _)| i)
5707 .unwrap_or(0);
5708 eprintln!(
5709 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
5710 pos + k0 + k,
5711 target,
5712 lse - lg[target] as f64,
5713 top,
5714 lg[target],
5715 lg[top]
5716 );
5717 }
5718 }
5719 k0 = k1;
5720 }
5721 pos = end;
5722 }
5723 return Ok((nll, cnt));
5724 }
5725 for pos in 0..ids.len().saturating_sub(1) {
5726 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
5727 self.nll_check_graph("serial forward", pos)?;
5728 let out_of_band = self.graph_logits.take();
5736 if pos < start {
5737 continue;
5738 }
5739 let logits = match out_of_band {
5740 Some(lg) => lg,
5741 None => {
5742 let normed = inference::rms_norm(
5743 &hidden,
5744 &self.weights.final_norm,
5745 self.rms_eps,
5746 self.norm_style,
5747 );
5748 self.lm_head_forward(&normed)
5752 }
5753 };
5754 let target = ids[pos + 1] as usize;
5755 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5756 let lse: f64 = logits
5757 .iter()
5758 .map(|&v| ((v - max) as f64).exp())
5759 .sum::<f64>()
5760 .ln()
5761 + max as f64;
5762 let tok_nll = lse - logits[target] as f64;
5763 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
5764 let top = logits
5765 .iter()
5766 .enumerate()
5767 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5768 .map(|(i, _)| i)
5769 .unwrap_or(0);
5770 eprintln!(
5771 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
5772 logits[target], logits[top]
5773 );
5774 }
5775 nll += tok_nll;
5776 cnt += 1;
5777 }
5778 Ok((nll, cnt))
5779 })();
5780 self.nll_end();
5781 result
5782 }
5783
5784 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
5789 let normed = inference::rms_norm(
5790 hidden,
5791 &self.weights.final_norm,
5792 self.rms_eps,
5793 self.norm_style,
5794 );
5795 let mut logits = self.lm_head_forward(&normed);
5798 let target = target as usize;
5799 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5800 let lse: f64 = logits
5801 .iter()
5802 .map(|&v| ((v - max) as f64).exp())
5803 .sum::<f64>()
5804 .ln()
5805 + max as f64;
5806 let tok_nll = lse - logits[target] as f64;
5807 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
5808 let top = logits
5809 .iter()
5810 .enumerate()
5811 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5812 .map(|(i, _)| i)
5813 .unwrap_or(0);
5814 eprintln!(
5815 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
5816 logits[target], logits[top]
5817 );
5818 }
5819 attention::recycle_buf(&mut logits);
5820 tok_nll
5821 }
5822
5823 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
5840 self.nll_begin()?;
5845 self.o1_begin();
5846 let n = ids.len().saturating_sub(1);
5847 let p = prefill.min(n);
5848 let mut pos = 0usize;
5850 if self.can_prefill_batched() {
5851 const CHUNK: usize = 128;
5852 while pos < p {
5853 let end = (pos + CHUNK).min(p);
5854 let _ = self.prefill_batch(&ids[pos..end], pos);
5855 if self
5856 .graph_failed
5857 .swap(false, std::sync::atomic::Ordering::Relaxed)
5858 {
5859 self.cancel
5860 .store(false, std::sync::atomic::Ordering::Relaxed);
5861 self.nll_end();
5862 return Err("GPU graph failed during O(1) NLL prefix".into());
5863 }
5864 pos = end;
5865 }
5866 } else {
5867 while pos < p {
5868 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
5869 if self
5870 .graph_failed
5871 .swap(false, std::sync::atomic::Ordering::Relaxed)
5872 {
5873 self.cancel
5874 .store(false, std::sync::atomic::Ordering::Relaxed);
5875 self.nll_end();
5876 return Err("GPU graph failed during O(1) NLL prefix".into());
5877 }
5878 pos += 1;
5879 }
5880 }
5881 self.o1_seal();
5882
5883 let mut nll = 0f64;
5884 let mut cnt = 0usize;
5885
5886 let batch_k = std::env::var("CMF_BATCH_K")
5895 .ok()
5896 .and_then(|v| v.parse::<usize>().ok())
5897 .unwrap_or(0);
5898 let batch_admitted = batch_k > 0
5899 && self.can_prefill_batched()
5900 && self.o1_active()
5901 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5902 && (0..self.num_layers).all(|li| {
5903 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5904 cache.o1.is_none() || cache.o1_views().is_some()
5905 });
5906 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5907 eprintln!(
5908 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
5909 batch_admitted,
5910 batch_k,
5911 n.saturating_sub(p),
5912 );
5913 }
5914 let mut batch_completed = false;
5915 if batch_admitted && p < n {
5916 let hs = self.hidden_size;
5917 let mut batch_pos = p;
5918 while batch_pos < n {
5919 let end = (batch_pos + batch_k).min(n);
5920 let bk = end - batch_pos;
5921 let mut hiddens = vec![0.0f32; bk * hs];
5922 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
5923 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
5924 }
5925 let positions: Vec<usize> = (batch_pos..end).collect();
5926 let t_batch = std::time::Instant::now();
5927 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5928 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5929 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
5930 eprintln!(
5931 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5932 batch_pos,
5933 end.saturating_sub(1),
5934 bk as f64 / (ms / 1000.0),
5935 );
5936 }
5937 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
5938 self.nll_end();
5939 return Err(err);
5940 }
5941 match outcome {
5942 crate::gpu::BatchGraphOutcome::Completed => {
5943 batch_completed = true;
5944 for row in 0..bk {
5945 nll += self.nll_from_hidden(
5946 &hiddens[row * hs..(row + 1) * hs],
5947 ids[batch_pos + row + 1],
5948 batch_pos + row,
5949 );
5950 cnt += 1;
5951 }
5952 batch_pos = end;
5953 }
5954 crate::gpu::BatchGraphOutcome::Declined => {
5955 if batch_completed {
5956 self.nll_end();
5957 return Err(format!(
5958 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
5959 ));
5960 }
5961 break;
5962 }
5963 crate::gpu::BatchGraphOutcome::Failed => {
5964 self.nll_end();
5965 return Err(format!(
5966 "O(1) NLL batch graph failed after admission at position {batch_pos}"
5967 ));
5968 }
5969 }
5970 }
5971 if batch_completed && cnt == n.saturating_sub(p) {
5972 self.nll_end();
5973 return Ok((nll, cnt));
5974 }
5975 }
5976
5977 for pos in p..n {
5982 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
5983 if self
5984 .graph_failed
5985 .swap(false, std::sync::atomic::Ordering::Relaxed)
5986 {
5987 self.cancel
5988 .store(false, std::sync::atomic::Ordering::Relaxed);
5989 self.nll_end();
5990 return Err(format!(
5991 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
5992 ));
5993 }
5994 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
5995 cnt += 1;
5996 }
5997 self.nll_end();
5998 Ok((nll, cnt))
5999 }
6000
6001 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
6009 self.clear_sequence_state();
6010 let n = ids.len().saturating_sub(1);
6011 let mut correct = Vec::with_capacity(n);
6012 let mut pmax = Vec::with_capacity(n);
6013 for pos in 0..n {
6014 let emb = self.embed_single(ids[pos]);
6015 let hidden = self.forward_layers(&emb, pos, None);
6016 let normed = inference::rms_norm(
6017 &hidden,
6018 &self.weights.final_norm,
6019 self.rms_eps,
6020 self.norm_style,
6021 );
6022 let logits = self.lm_head_forward(&normed);
6026 let target = ids[pos + 1] as usize;
6027 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
6028 for (i, &v) in logits.iter().enumerate() {
6029 if v > mval {
6030 mval = v;
6031 amax = i;
6032 }
6033 }
6034 correct.push(amax == target);
6035 let row: Vec<f32> = temps
6036 .iter()
6037 .map(|&t| {
6038 let tt = t.max(1e-3);
6039 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
6040 1.0 / s.max(1e-12) })
6042 .collect();
6043 pmax.push(row);
6044 }
6045 self.clear_sequence_state();
6046 (correct, pmax)
6047 }
6048
6049 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
6056 if self.dyn_router.is_none() {
6057 return Ok((self.ppl_ids(ids)?, 0));
6058 }
6059 self.nll_begin()?;
6060 let saved_active = self.dyn_active;
6061 let mut router = self
6062 .dyn_router
6063 .take()
6064 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
6065 router.reset();
6066 self.dyn_phi_seen = 0;
6067 let _ = self.set_active_skill(None);
6068
6069 let result: Result<(f64, usize), String> = (|| {
6070 let mut nll = 0f64;
6071 let mut cnt = 0usize;
6072 for pos in 0..ids.len().saturating_sub(1) {
6073 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
6074 self.nll_check_graph("dynamic serial forward", pos)?;
6075 let out_of_band = self.graph_logits.take();
6076 let mut logits = match out_of_band {
6077 Some(lg) => lg,
6078 None => {
6079 let normed = inference::rms_norm(
6080 &hidden,
6081 &self.weights.final_norm,
6082 self.rms_eps,
6083 self.norm_style,
6084 );
6085 self.lm_head_forward(&normed)
6089 }
6090 };
6091 let target = ids[pos + 1] as usize;
6092 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
6093 let lse: f64 = logits
6094 .iter()
6095 .map(|&v| ((v - max) as f64).exp())
6096 .sum::<f64>()
6097 .ln()
6098 + max as f64;
6099 let tok_nll = lse - logits[target] as f64;
6100 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
6101 let top = logits
6102 .iter()
6103 .enumerate()
6104 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6105 .map(|(i, _)| i)
6106 .unwrap_or(0);
6107 eprintln!(
6108 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
6109 logits[target], logits[top]
6110 );
6111 }
6112 nll += tok_nll;
6113 cnt += 1;
6114 attention::recycle_buf(&mut logits);
6115 let phi = self.dyn_phi_ema.clone();
6117 if let Some(new_active) = router.step(&phi, pos) {
6118 let _ = self.set_active_skill(new_active);
6119 }
6120 }
6121 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
6122 })();
6123
6124 let _ = self.set_active_skill(saved_active);
6127 self.dyn_router = Some(router);
6128 self.nll_end();
6129 result
6130 }
6131
6132 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
6134 self.clear_sequence_state();
6135 let mut acc = vec![0f32; self.hidden_size];
6136 for (pos, &id) in ids.iter().enumerate() {
6137 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
6138 for (a, v) in acc.iter_mut().zip(&h) {
6139 *a += v;
6140 }
6141 }
6142 let n = ids.len().max(1) as f32;
6143 for a in acc.iter_mut() {
6144 *a /= n;
6145 }
6146 self.clear_sequence_state();
6147 acc
6148 }
6149
6150 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
6156 self.prefill_batch_masked(ids, start_pos, None)
6157 }
6158
6159 fn prefill_batch_masked(
6165 &mut self,
6166 ids: &[u32],
6167 start_pos: usize,
6168 task_mask: Option<&TaskMask>,
6169 ) -> Vec<f32> {
6170 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
6171 }
6172
6173 fn prefill_batch_span(
6179 &mut self,
6180 input: PrefillIn<'_>,
6181 start_pos: usize,
6182 task_mask: Option<&TaskMask>,
6183 from: usize,
6184 upto_excl: usize,
6185 ) -> Vec<f32> {
6186 let hs = self.hidden_size;
6187 let b = match input {
6188 PrefillIn::Ids(ids) => ids.len(),
6189 PrefillIn::Hidden(hb) => hb.len() / hs,
6190 };
6191 let upto_excl = upto_excl.min(self.num_layers);
6192 let mut h: Vec<f32>;
6196 let mut h_ready;
6197 match input {
6198 PrefillIn::Ids(_) => {
6199 h = vec![0.0; b * hs];
6200 h_ready = false;
6201 }
6202 PrefillIn::Hidden(hb) => {
6203 h = hb.to_vec();
6204 h_ready = true;
6205 }
6206 }
6207 let fill_h = |h: &mut Vec<f32>, me: &Self| {
6208 if let PrefillIn::Ids(ids) = input {
6209 for (bi, &id) in ids.iter().enumerate() {
6210 let e = me.embed_single(id);
6211 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
6212 }
6213 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6214 if let Ok(t) = tp.parse::<usize>() {
6215 if t >= start_pos && t < start_pos + ids.len() {
6216 let bi = t - start_pos;
6217 let row = &h[bi * hs..(bi + 1) * hs];
6218 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6219 eprintln!(
6220 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
6221 ids[bi],
6222 row[0],
6223 row[1],
6224 ids.len(),
6225 &ids[..ids.len().min(8)]
6226 );
6227 }
6228 }
6229 }
6230 }
6231 };
6232 let (_nkv, _hd, _rd, eps) = (
6233 self.num_kv_heads,
6234 self.head_dim,
6235 self.rotary_dim,
6236 self.rms_eps,
6237 );
6238 let pool = self.pool.clone();
6239 let norm_style = self.norm_style;
6240 let automatic_gpu_prefix = self.automatic_gpu_prefix();
6241
6242 #[cfg(target_os = "macos")]
6243 let mut chunk_skip_until = 0usize;
6244 for li in from..upto_excl {
6245 let _capacity_tail = automatic_gpu_prefix
6246 .filter(|&prefix| li >= prefix)
6247 .map(|_| crate::gpu::enter_cpu_scope());
6248 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
6255 if task_mask.is_none() {
6256 if li < chunk_skip_until {
6257 continue;
6258 }
6259 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
6265 fill_h(&mut h, self);
6266 h_ready = true;
6267 }
6268 let ids_for_embed = match input {
6269 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
6270 PrefillIn::Hidden(_) => None,
6271 };
6272 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
6273 if end > li {
6274 h_ready = true;
6275 chunk_skip_until = end;
6276 if self.is_loop_end(end - 1) && end < self.num_layers {
6279 for bi in 0..b {
6280 let normed = inference::rms_norm(
6281 &h[bi * hs..(bi + 1) * hs],
6282 &self.weights.final_norm,
6283 eps,
6284 norm_style,
6285 );
6286 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6287 }
6288 }
6289 continue;
6290 }
6291 }
6292 if !h_ready {
6293 fill_h(&mut h, self);
6294 h_ready = true;
6295 }
6296 let lw = &self.weights.layers[self.phys_layer(li)];
6297 match &lw.attn {
6299 AttnKind::Kda(w) => {
6300 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
6302 let mut normed = vec![0.0f32; b * hs];
6303 for bi in 0..b {
6304 inference::rms_norm_into(
6305 &h[bi * hs..(bi + 1) * hs],
6306 &lw.input_norm,
6307 eps,
6308 norm_style,
6309 &mut normed[bi * hs..(bi + 1) * hs],
6310 );
6311 }
6312 let attn = crate::linear_core::kda_forward_batch(
6313 &normed,
6314 b,
6315 w,
6316 &cfg,
6317 &mut self.kv_cache.layers[li].linear_state,
6318 pool.as_deref(),
6319 );
6320 for (dst, &a) in h.iter_mut().zip(&attn) {
6321 *dst += a;
6322 }
6323 }
6324 AttnKind::LinearGdn(w) => {
6325 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
6327 let mut normed = vec![0.0f32; b * hs];
6328 for bi in 0..b {
6329 let r = inference::rms_norm(
6330 &h[bi * hs..(bi + 1) * hs],
6331 &lw.input_norm,
6332 eps,
6333 norm_style,
6334 );
6335 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6336 }
6337 let attn = crate::linear_core::gdn_forward_batch(
6338 &normed,
6339 b,
6340 w,
6341 &cfg,
6342 &mut self.kv_cache.layers[li].linear_state,
6343 pool.as_deref(),
6344 );
6345 for (dst, &a) in h.iter_mut().zip(&attn) {
6346 *dst += a;
6347 }
6348 }
6349 AttnKind::ShortConv(w) => {
6350 let cfg = self
6353 .short_conv_cfg
6354 .expect("short-conv layer without short_conv_cfg");
6355 let mut normed = vec![0.0f32; b * hs];
6356 for bi in 0..b {
6357 inference::rms_norm_into(
6358 &h[bi * hs..(bi + 1) * hs],
6359 &lw.input_norm,
6360 eps,
6361 norm_style,
6362 &mut normed[bi * hs..(bi + 1) * hs],
6363 );
6364 }
6365 let attn = short_conv_forward_batch(
6366 &normed,
6367 b,
6368 w,
6369 &cfg,
6370 &mut self.kv_cache.layers[li].linear_state,
6371 pool.as_deref(),
6372 );
6373 for (dst, &a) in h.iter_mut().zip(&attn) {
6374 *dst += a;
6375 }
6376 }
6377 AttnKind::Mla(w) => {
6378 let inv_freq_l = self.layer_inv_freq(li);
6381 let rs = self.layer_rope_scale(li);
6382 let mut normed = vec![0.0f32; hs];
6383 for bi in 0..b {
6384 inference::rms_norm_into(
6385 &h[bi * hs..(bi + 1) * hs],
6386 &lw.input_norm,
6387 eps,
6388 norm_style,
6389 &mut normed,
6390 );
6391 let ao = mla_attention(
6392 w,
6393 &normed,
6394 &mut self.kv_cache.layers[li],
6395 start_pos + bi,
6396 &inv_freq_l,
6397 rs,
6398 eps,
6399 pool.as_deref(),
6400 );
6401 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
6402 *dst += a;
6403 }
6404 }
6405 }
6406 AttnKind::Full {
6407 wq,
6408 wk,
6409 wv,
6410 wo,
6411 q_norm,
6412 k_norm,
6413 output_gate,
6414 softplus_gate,
6415 bias,
6416 } => {
6417 let mut normed = vec![0.0f32; b * hs];
6421 for bi in 0..b {
6422 inference::rms_norm_into(
6423 &h[bi * hs..(bi + 1) * hs],
6424 &lw.input_norm,
6425 eps,
6426 norm_style,
6427 &mut normed[bi * hs..(bi + 1) * hs],
6428 );
6429 }
6430 let inv_freq_l = self.layer_inv_freq(li);
6431 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
6432 let cfg = QwenAttnCfg {
6433 num_heads: self.layer_num_heads(li),
6434 num_kv_heads: nkv_l,
6435 head_dim: hd_l,
6436 hidden_size: hs,
6437 position: start_pos,
6438 inv_freq: &inv_freq_l,
6439 rotary_dim: rd_l,
6440 scale: self.attn_scale,
6441 softcap: self.attn_softcap,
6442 window: self.layer_window(li),
6443 v_norm: self.attn_v_norm,
6444 q_norm: q_norm.as_deref(),
6445 k_norm: k_norm.as_deref(),
6446 output_gate: *output_gate,
6447 softplus_gate: softplus_gate
6448 .as_ref()
6449 .map(|(gate, per_head)| (gate, *per_head)),
6450 rope_scale: self.layer_rope_scale(li),
6451 bias: bias
6452 .as_ref()
6453 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6454 rms_eps: eps,
6455 norm_style,
6456 pool: pool.as_deref(),
6457 };
6458 let mut attn = attention::qwen_attention_batch(
6459 &normed,
6460 b,
6461 wq,
6462 wk,
6463 wv,
6464 wo,
6465 &mut self.kv_cache.layers[li],
6466 &cfg,
6467 );
6468 if let Some(w) = &lw.attn_out_norm {
6469 for bi in 0..b {
6470 inference::rms_norm_into(
6471 &attn[bi * hs..(bi + 1) * hs],
6472 w,
6473 eps,
6474 norm_style,
6475 &mut normed[bi * hs..(bi + 1) * hs],
6476 );
6477 }
6478 attn.copy_from_slice(&normed);
6479 }
6480 for (dst, &a) in h.iter_mut().zip(&attn) {
6481 *dst += a;
6482 }
6483 }
6484 AttnKind::Linear(w) => {
6485 for bi in 0..b {
6486 let normed = inference::rms_norm(
6487 &h[bi * hs..(bi + 1) * hs],
6488 &lw.input_norm,
6489 eps,
6490 norm_style,
6491 );
6492 vmf_phase_forward(
6493 &normed,
6494 w,
6495 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
6496 &mut self.kv_cache.layers[li].linear_state,
6497 pool.as_deref(),
6498 )
6499 .iter()
6500 .enumerate()
6501 .for_each(|(i, &a)| h[bi * hs + i] += a);
6502 }
6503 }
6504 }
6505
6506 let lw = &self.weights.layers[self.phys_layer(li)];
6508 let mut post = vec![0.0f32; b * hs];
6509 for bi in 0..b {
6510 let r =
6511 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
6512 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6513 }
6514 let mask_row = task_mask
6517 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
6518 .and_then(|m| m.ffn_masks.get(li))
6519 .map(|v| v.as_slice());
6520 let mut ffn = match &lw.ffn {
6521 FfnKind::Dense(d) if !d.segs.is_empty() => {
6522 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
6523 }
6524 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
6525 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
6526 FfnKind::DenseMoe(dm) => {
6529 let mut out = vec![0.0f32; b * hs];
6530 for bi in 0..b {
6531 let r = dense_moe_ffn(
6532 dm,
6533 &post[bi * hs..(bi + 1) * hs],
6534 &h[bi * hs..(bi + 1) * hs],
6535 eps,
6536 norm_style,
6537 pool.as_deref(),
6538 );
6539 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
6540 }
6541 out
6542 }
6543 };
6544 if let Some(w) = &lw.ffn_out_norm {
6545 for bi in 0..b {
6546 inference::rms_norm_into(
6547 &ffn[bi * hs..(bi + 1) * hs],
6548 w,
6549 eps,
6550 norm_style,
6551 &mut post[bi * hs..(bi + 1) * hs],
6552 );
6553 }
6554 ffn.copy_from_slice(&post);
6555 }
6556 for (dst, &f) in h.iter_mut().zip(&ffn) {
6557 *dst += f;
6558 }
6559 if let Some(sc) = lw.layer_scale {
6560 for v in h.iter_mut() {
6561 *v *= sc;
6562 }
6563 }
6564 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6565 if let Ok(t) = tp.parse::<usize>() {
6566 if t >= start_pos && t < start_pos + b {
6567 let bi = t - start_pos;
6568 let row = &h[bi * hs..(bi + 1) * hs];
6569 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
6570 eprintln!(
6571 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
6572 row[0], row[1]
6573 );
6574 }
6575 }
6576 }
6577 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
6581 let row = &h[(b - 1) * hs..b * hs];
6582 let rms =
6583 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
6584 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
6585 eprintln!(
6586 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
6587 match &self.weights.layers[self.phys_layer(li)].attn {
6588 AttnKind::LinearGdn(_) => "gdn",
6589 AttnKind::Linear(_) => "vmf",
6590 AttnKind::ShortConv(_) => "conv",
6591 _ => "attn",
6592 },
6593 match &lw.ffn {
6594 FfnKind::Moe(_) => "moe",
6595 FfnKind::Dense(_) => "dense",
6596 FfnKind::DenseMoe(_) => "dense+moe",
6597 },
6598 );
6599 }
6600 if self.is_loop_end(li) && li + 1 < self.num_layers {
6602 for bi in 0..b {
6603 let normed = inference::rms_norm(
6604 &h[bi * hs..(bi + 1) * hs],
6605 &self.weights.final_norm,
6606 eps,
6607 norm_style,
6608 );
6609 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
6610 }
6611 }
6612 if std::env::var("CMF_TRACE_H").is_ok() {
6613 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
6614 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
6615 eprintln!(
6616 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
6617 lw.layer_scale
6618 );
6619 }
6620 }
6621 crate::gpu::set_layer(-1); h
6623 }
6624
6625 fn embed_single(&self, id: u32) -> Vec<f32> {
6627 let mut out = vec![0.0f32; self.hidden_size];
6628 if (id as usize) < self.weights.embed_tokens.rows() {
6629 self.weights.embed_tokens.row_f32(id as usize, &mut out);
6630 }
6631 if self.embed_multiplier != 1.0 {
6632 for v in out.iter_mut() {
6633 *v *= self.embed_multiplier;
6634 }
6635 }
6636 if self.dsv4.is_some() || self.qwen4_exp.is_some() {
6640 let mut v = vec![0.0f32; self.hidden_size.max(1)];
6641 v[0] = id as f32;
6642 return v;
6643 }
6644 if let Some(b) = &self.g3n {
6647 return b.0.extend_embedding(id, &out, self.pool.as_deref());
6648 }
6649 out
6650 }
6651
6652 #[cfg(target_os = "macos")]
6658 fn chunk_run_gpu(
6659 &mut self,
6660 li0: usize,
6661 h: &mut [f32],
6662 b: usize,
6663 pos0: usize,
6664 embed_ids: Option<&[u32]>,
6665 cap: usize,
6666 ) -> usize {
6667 if !crate::gpu::enabled_here()
6671 || std::env::var("CMF_GPU_CHUNK")
6672 .map(|v| v == "0")
6673 .unwrap_or(false)
6674 || b < 32
6675 || self.swa.is_some()
6676 || self.global_attn.is_some()
6677 || self.attn_v_norm
6678 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
6679 {
6680 return li0;
6681 }
6682 let Some(model) = self.model.clone() else {
6683 return li0;
6684 };
6685 let inv_freq = self.inv_freq.clone();
6686 let (nh, nkv, hd, hs) = (
6687 self.num_heads,
6688 self.num_kv_heads,
6689 self.head_dim,
6690 self.hidden_size,
6691 );
6692 let loop_end = if self.loop_final_norm {
6696 ((li0 / self.physical_layers) + 1) * self.physical_layers
6697 } else {
6698 self.num_layers
6699 };
6700 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
6701 let mut stored_at: Vec<usize> = Vec::new();
6702 for li in li0..self.num_layers.min(loop_end).min(cap) {
6703 let lw = &self.weights.layers[self.phys_layer(li)];
6704 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
6705 break;
6706 }
6707 let AttnKind::Full {
6708 wq,
6709 wk,
6710 wv,
6711 wo,
6712 q_norm,
6713 k_norm,
6714 output_gate: false,
6715 softplus_gate: None,
6716 bias,
6717 } = &lw.attn
6718 else {
6719 break;
6720 };
6721 let FfnKind::Dense(d) = &lw.ffn else { break };
6722 if d.act != Act::Silu || !d.segs.is_empty() {
6723 break;
6724 }
6725 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
6730 t.q8_row_parts()
6731 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
6732 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
6733 }
6734 let parts = (
6735 cw(wq),
6736 cw(wk),
6737 cw(wv),
6738 cw(wo),
6739 cw(&d.gate_proj),
6740 cw(&d.up_proj),
6741 cw(&d.down_proj),
6742 );
6743 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
6744 else {
6745 break;
6746 };
6747 let layer = &self.kv_cache.layers[li];
6748 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
6749 break;
6750 }
6751 stored_at.push(layer.head_len(0));
6752 layers.push(crate::gpu_metal::ChunkLayer {
6753 model: &model,
6754 kv_id: self.graph_kv_id,
6755 layer: li,
6756 wq: pq,
6757 wk: pk,
6758 wv: pv,
6759 wo: po,
6760 gate: pg,
6761 up: pu,
6762 down: pd,
6763 input_norm: &lw.input_norm,
6764 post_norm: &lw.post_norm,
6765 bias: bias
6766 .as_ref()
6767 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
6768 q_norm: q_norm.as_deref(),
6769 k_norm: k_norm.as_deref(),
6770 inv_freq: &inv_freq,
6771 rd: self.rotary_dim,
6772 nh,
6773 nkv,
6774 hd,
6775 hs,
6776 inter: d.gate_proj.rows(),
6777 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
6778 eps: self.rms_eps as f32,
6779 });
6780 }
6781 if layers.is_empty() {
6782 return li0;
6783 }
6784 let row = nkv * hd;
6785 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
6786 .iter()
6787 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
6788 .collect();
6789 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
6790 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
6791 let li = layers[i].layer;
6792 let layer = &self.kv_cache.layers[li];
6793 io.push(crate::gpu_metal::ChunkIo {
6794 cpu_stored: stored_at[i],
6795 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
6796 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
6797 out_k: ok,
6798 out_v: ov,
6799 imp: oi,
6800 });
6801 }
6802 let n_run = layers.len();
6803 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
6804 let ep = embed_ids.and_then(|ids| {
6807 self.weights
6808 .embed_tokens
6809 .q8_row_parts()
6810 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
6811 idx,
6812 rows,
6813 row_scale: rs,
6814 ids,
6815 mult: self.embed_multiplier,
6816 })
6817 });
6818 if embed_ids.is_some() && ep.is_none() {
6819 return li0;
6820 }
6821 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
6822 return li0;
6823 }
6824 drop(io);
6825 drop(layers);
6826 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
6829 let li = li0 + i;
6830 let layer = &mut self.kv_cache.layers[li];
6831 for bi in 0..b {
6832 layer.append(
6833 &ok[bi * row..(bi + 1) * row],
6834 &ov[bi * row..(bi + 1) * row],
6835 &[],
6836 );
6837 }
6838 layer.accumulate_imp(oi);
6839 }
6840 last
6841 }
6842
6843 fn layer_is_local(&self, li: usize) -> bool {
6846 if let Some(layers) = &self.sliding_layers {
6847 return layers.get(li).copied().unwrap_or(false);
6848 }
6849 match self.swa {
6850 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
6851 None => false,
6852 }
6853 }
6854
6855 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
6858 if self.layer_is_local(li) {
6859 if let Some(f) = &self.inv_freq_local {
6860 return f.clone();
6861 }
6862 } else if let Some(f) = &self.inv_freq_global {
6863 return f.clone();
6864 }
6865 self.inv_freq.clone()
6866 }
6867
6868 fn layer_window(&self, li: usize) -> Option<usize> {
6870 self.swa
6871 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
6872 }
6873
6874 fn layer_num_heads(&self, li: usize) -> usize {
6875 self.attention_heads_per_layer
6876 .as_ref()
6877 .and_then(|v| v.get(li).copied())
6878 .unwrap_or(self.num_heads)
6879 }
6880
6881 fn layer_rope_scale(&self, li: usize) -> f32 {
6882 if self.layer_is_local(li) {
6883 self.rope_scale_local
6884 } else {
6885 self.rope_scale
6886 }
6887 }
6888
6889 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
6892 if !self.layer_is_local(li) {
6893 if let Some((ghd, gkv)) = self.global_attn {
6894 return (gkv, ghd, ghd);
6895 }
6896 }
6897 (
6898 self.num_kv_heads,
6899 self.head_dim,
6900 if self.layer_is_local(li) {
6901 self.rotary_dim_local.unwrap_or(self.rotary_dim)
6902 } else {
6903 self.rotary_dim
6904 },
6905 )
6906 }
6907
6908 fn forward_layers(
6910 &mut self,
6911 hidden: &[f32],
6912 position: usize,
6913 task_mask: Option<&TaskMask>,
6914 ) -> Vec<f32> {
6915 self.forward_layers_upto(hidden, position, task_mask, None)
6916 }
6917
6918 pub fn embed_id(&self, id: u32) -> Vec<f32> {
6926 self.embed_single(id)
6927 }
6928
6929 pub fn split_supported(&self) -> Result<(), String> {
6933 if self.dsv4.is_some() {
6934 return Err(
6935 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
6936 );
6937 }
6938 if self.qwen4_exp.is_some() {
6939 return Err(
6940 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
6941 );
6942 }
6943 if self.g3n.is_some() {
6944 return Err(
6945 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
6946 );
6947 }
6948 Ok(())
6949 }
6950
6951 pub fn forward_span(
6956 &mut self,
6957 hidden: &[f32],
6958 position: usize,
6959 from: usize,
6960 upto: usize,
6961 task_mask: Option<&TaskMask>,
6962 ) -> Result<Vec<f32>, String> {
6963 self.split_supported()?;
6964 if from > upto || upto >= self.num_layers {
6965 return Err(format!(
6966 "forward_span: layer range {from}..={upto} outside 0..{}",
6967 self.num_layers
6968 ));
6969 }
6970 if hidden.len() != self.hidden_size {
6971 return Err(format!(
6972 "forward_span: hidden len {} ≠ hidden_size {}",
6973 hidden.len(),
6974 self.hidden_size
6975 ));
6976 }
6977 Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
6978 }
6979
6980 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
6983 let normed = inference::rms_norm(
6984 hidden,
6985 &self.weights.final_norm,
6986 self.rms_eps,
6987 self.norm_style,
6988 );
6989 self.lm_head_forward(&normed)
6990 }
6991
6992 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
6994 sampler::sample_with_scratch(
6995 logits,
6996 &self.sampler_config,
6997 past_tokens,
6998 &mut self.rng,
6999 &mut self.sampler_scratch,
7000 )
7001 }
7002
7003 pub fn reset_session(&mut self) {
7005 self.clear_sequence_state();
7006 }
7007
7008 pub fn prefill_span_ids(
7014 &mut self,
7015 ids: &[u32],
7016 start_pos: usize,
7017 upto: usize,
7018 task_mask: Option<&TaskMask>,
7019 ) -> Result<Vec<f32>, String> {
7020 self.split_supported()?;
7021 if upto >= self.num_layers {
7022 return Err(format!(
7023 "prefill_span_ids: upto {upto} outside 0..{}",
7024 self.num_layers
7025 ));
7026 }
7027 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7031 Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
7032 } else {
7033 let hs = self.hidden_size;
7034 let mut out = Vec::with_capacity(ids.len() * hs);
7035 for (i, &id) in ids.iter().enumerate() {
7036 let emb = self.embed_id(id);
7037 out.extend_from_slice(&self.forward_span(
7038 &emb,
7039 start_pos + i,
7040 0,
7041 upto,
7042 task_mask,
7043 )?);
7044 }
7045 Ok(out)
7046 }
7047 }
7048
7049 pub fn prefill_span_hidden(
7052 &mut self,
7053 hidden: &[f32],
7054 start_pos: usize,
7055 from: usize,
7056 upto: usize,
7057 task_mask: Option<&TaskMask>,
7058 ) -> Result<Vec<f32>, String> {
7059 self.split_supported()?;
7060 let hs = self.hidden_size;
7061 if hidden.is_empty() || hidden.len() % hs != 0 {
7062 return Err(format!(
7063 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
7064 hidden.len()
7065 ));
7066 }
7067 if from > upto || upto >= self.num_layers {
7068 return Err(format!(
7069 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
7070 self.num_layers
7071 ));
7072 }
7073 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
7074 Ok(self.prefill_batch_span(
7075 PrefillIn::Hidden(hidden),
7076 start_pos,
7077 task_mask,
7078 from,
7079 upto + 1,
7080 ))
7081 } else {
7082 let b = hidden.len() / hs;
7083 let mut out = Vec::with_capacity(hidden.len());
7084 for i in 0..b {
7085 let h = self.forward_span(
7086 &hidden[i * hs..(i + 1) * hs],
7087 start_pos + i,
7088 from,
7089 upto,
7090 task_mask,
7091 )?;
7092 out.extend_from_slice(&h);
7093 }
7094 Ok(out)
7095 }
7096 }
7097
7098 fn try_token_graph_wgpu(
7102 &self,
7103 hidden: &[f32],
7104 position: usize,
7105 logits_out: &mut Vec<f32>,
7106 layers_run: &mut usize,
7107 ) -> Option<Result<Vec<f32>, ()>> {
7108 self.try_token_graph_wgpu_steps(
7109 hidden,
7110 position,
7111 logits_out,
7112 1,
7113 None,
7114 Some(layers_run),
7115 0,
7116 self.num_layers,
7117 )
7118 }
7119
7120 fn try_token_graph_wgpu_span(
7124 &self,
7125 hidden: &[f32],
7126 position: usize,
7127 logits_out: &mut Vec<f32>,
7128 from: usize,
7129 upto_excl: usize,
7130 layers_run: &mut usize,
7131 ) -> Option<Result<Vec<f32>, ()>> {
7132 self.try_token_graph_wgpu_steps(
7133 hidden,
7134 position,
7135 logits_out,
7136 1,
7137 None,
7138 Some(layers_run),
7139 from,
7140 upto_excl,
7141 )
7142 }
7143
7144 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
7148 if self.o1_active() || self.attn_softcap > 0.0 {
7149 return None;
7150 }
7151 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
7152 if !graph_on || crate::gpu::graph_unsupported() {
7153 return None;
7160 }
7161 let emb = self.embed_single(t_next);
7162 let mut lg = Vec::new();
7163 let mut ids = Vec::new();
7164 match self.try_token_graph_wgpu_steps(
7165 &emb,
7166 position,
7167 &mut lg,
7168 k,
7169 Some(&mut ids),
7170 None,
7171 0,
7172 self.num_layers,
7173 ) {
7174 Some(Ok(_)) => {}
7175 Some(Err(())) => {
7176 self.graph_failed
7181 .store(true, std::sync::atomic::Ordering::Relaxed);
7182 return None;
7183 }
7184 None => return None,
7185 }
7186 (ids.len() == k).then_some(ids)
7187 }
7188
7189 fn try_token_graph_wgpu_steps(
7193 &self,
7194 hidden: &[f32],
7195 position: usize,
7196 logits_out: &mut Vec<f32>,
7197 steps: usize,
7198 ids_out: Option<&mut Vec<u32>>,
7199 layers_run: Option<&mut usize>,
7200 from: usize,
7201 upto_excl: usize,
7202 ) -> Option<Result<Vec<f32>, ()>> {
7203 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
7206 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
7207 return None;
7211 }
7212 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
7217 .map(|li| {
7218 if !o1_gpu {
7219 return None;
7220 }
7221 self.kv_cache.layers[self.phys_layer(li)].o1_views()
7222 })
7223 .collect();
7224 if self.o1_active() && o1_gpu {
7225 let want: usize = (from..upto_excl)
7228 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
7229 .count();
7230 let have = o1_views.iter().filter(|v| v.is_some()).count();
7231 if want == 0 || have != want {
7232 use std::sync::atomic::{AtomicUsize, Ordering};
7242 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
7243 let code = have * 1000 + want;
7244 if LAST.swap(code, Ordering::Relaxed) != code {
7245 tracing::warn!(
7246 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
7247 );
7248 }
7249 return None;
7250 }
7251 }
7252 let nh = self.num_heads;
7253 let (nkv, hd, rd) = self.layer_geom(0);
7254 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7255 let mut layers = Vec::with_capacity(upto_excl - from);
7256 let mut model = None;
7257 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
7258 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7259 if let Some((_, i, kind, rs)) = t.graph_weight() {
7260 return Some(crate::gpu::GraphW {
7261 idx: i,
7262 kind,
7263 row_scale: rs,
7264 data: &[],
7265 });
7266 }
7267 t.as_f32().map(|d| crate::gpu::GraphW {
7269 idx: 0,
7270 kind: 4,
7271 row_scale: &[],
7272 data: d,
7273 })
7274 }
7275 for li in from..upto_excl {
7276 let lw = &self.weights.layers[self.phys_layer(li)];
7277 if dbg {
7278 let ak = match &lw.attn {
7279 AttnKind::Mla(_) => "Mla".into(),
7280 AttnKind::Full {
7281 output_gate, bias, ..
7282 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
7283 AttnKind::LinearGdn(_) => "LinearGdn".into(),
7284 AttnKind::Kda(_) => "Kda".into(),
7285 AttnKind::Linear(_) => "Linear".into(),
7286 AttnKind::ShortConv(_) => "ShortConv".into(),
7287 };
7288 let fk = match &lw.ffn {
7289 FfnKind::Dense(_) => "Dense",
7290 FfnKind::Moe(_) => "Moe",
7291 FfnKind::DenseMoe(_) => "DenseMoe",
7292 };
7293 eprintln!("graph L{li}: attn={ak} ffn={fk}");
7294 }
7295 let gffn = match &lw.ffn {
7296 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7300 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7301 gate: gw(&d.gate_proj)?,
7302 up: gw(&d.up_proj)?,
7303 down: gw(&d.down_proj)?,
7304 },
7305 FfnKind::Moe(m) => {
7306 if m.route_tau.is_some()
7313 || m.mask.is_some()
7314 || (m.routed_scaling - 1.0).abs() > 1e-9
7315 {
7316 return None;
7317 }
7318 let shared = m.shared.as_ref();
7319 let has_shared = shared.is_some();
7320 let sgate = match shared {
7321 Some((_, sg)) => gw(sg.as_ref()?)?,
7322 None => gw(&m.router)?,
7325 };
7326 let router = gw(&m.router)?;
7327 let inter = m.experts.first()?.gate_proj.rows();
7328 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7329 let mut q4tp: Option<bool> = None;
7332 let mut gu_q2: Option<bool> = None;
7335 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
7336 if !matches!(e.act, Act::Silu)
7337 || e.gate_proj.rows() != inter
7338 || e.up_proj.rows() != inter
7339 {
7340 return None;
7341 }
7342 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7343 Some((mm, gi)) => (
7344 mm,
7345 gi,
7346 e.up_proj.mapped_q4t()?.1,
7347 e.down_proj.mapped_q4t()?.1,
7348 false,
7349 false,
7350 ),
7351 None => match e.gate_proj.mapped_q2tp() {
7352 Some((mm, gi)) => (
7353 mm,
7354 gi,
7355 e.up_proj.mapped_q2tp()?.1,
7356 e.down_proj.mapped_q4tp()?.1,
7357 true,
7358 true,
7359 ),
7360 None => {
7361 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7362 (
7363 mm,
7364 gi,
7365 e.up_proj.mapped_q4tp()?.1,
7366 e.down_proj.mapped_q4tp()?.1,
7367 true,
7368 false,
7369 )
7370 }
7371 },
7372 };
7373 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
7374 {
7375 tracing::warn!(
7381 "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."
7382 );
7383 return None;
7384 }
7385 model.get_or_insert_with(|| mm.clone());
7386 experts.push((gi, ui, di));
7387 }
7388 crate::gpu::GraphFfn::Moe {
7389 router,
7390 shared_gate: sgate,
7391 experts,
7392 n_exp: m.experts.len(),
7393 top_k: std::env::var("CMF_TOPK_PROBE")
7399 .ok()
7400 .and_then(|v| v.parse::<usize>().ok())
7401 .filter(|k| *k > 0 && *k <= m.top_k)
7402 .unwrap_or(m.top_k),
7403 inter,
7404 norm_topk: m.norm_topk_prob,
7405 q4tp: q4tp?,
7406 gu_q2: gu_q2.unwrap_or(false),
7407 sigmoid: m.router_sigmoid,
7408 bias: m.expert_bias.as_deref(),
7409 has_shared,
7410 }
7411 }
7412 };
7413 let attn = match &lw.attn {
7414 AttnKind::Full {
7415 wq,
7416 wk,
7417 wv,
7418 wo,
7419 q_norm,
7420 k_norm,
7421 output_gate,
7422 softplus_gate,
7423 bias,
7424 } => {
7425 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7426 return None;
7427 }
7428 let (m, _, _, _) = wq.graph_weight()?;
7429 model = Some(m.clone());
7430 crate::gpu::GraphAttn::Full {
7431 wq: gw(wq)?,
7432 wk: gw(wk)?,
7433 wv: gw(wv)?,
7434 wo: gw(wo)?,
7435 q_norm: q_norm.as_deref(),
7436 k_norm: k_norm.as_deref(),
7437 bias: bias
7438 .as_ref()
7439 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7440 output_gate: *output_gate,
7441 cpu_k: self.kv_cache.layers[li].k_heads(),
7442 cpu_v: self.kv_cache.layers[li].v_heads(),
7443 }
7444 }
7445 AttnKind::LinearGdn(w) => {
7446 let cfg = self.gdn_cfg?;
7447 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7448 model = Some(m.clone());
7449 crate::gpu::GraphAttn::Gdn {
7450 qkv: gw(&w.in_proj_qkv)?,
7451 z: gw(&w.in_proj_z)?,
7452 a: gw(&w.in_proj_a)?,
7453 b: gw(&w.in_proj_b)?,
7454 out: gw(&w.out_proj)?,
7455 conv1d: &w.conv1d,
7456 a_log: &w.a_log,
7457 dt_bias: &w.dt_bias,
7458 norm: &w.norm,
7459 nv: cfg.num_v_heads,
7460 nk: cfg.num_k_heads,
7461 dk: cfg.key_head_dim,
7462 dv: cfg.value_head_dim,
7463 kk: cfg.conv_kernel,
7464 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7465 }
7466 }
7467 AttnKind::ShortConv(w) => {
7468 let cfg = self.short_conv_cfg?;
7469 let (m, _, _, _) = w.in_proj.graph_weight()?;
7470 model = Some(m.clone());
7471 crate::gpu::GraphAttn::ShortConv {
7472 inp: gw(&w.in_proj)?,
7473 out: gw(&w.out_proj)?,
7474 taps: &w.conv,
7475 kernel: cfg.kernel,
7476 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7477 }
7478 }
7479 _ => return None,
7480 };
7481 layers.push(crate::gpu::GraphLayer {
7482 input_norm: &lw.input_norm,
7483 attn,
7484 post_norm: &lw.post_norm,
7485 ffn: gffn,
7486 });
7487 }
7488 let model = model?;
7489 let lm_gw = if upto_excl == self.num_layers
7495 && self.graph_want_logits
7496 && std::env::var("CMF_GPU_LMHEAD")
7497 .map(|v| v != "0")
7498 .unwrap_or(true)
7499 {
7500 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
7501 (
7502 crate::gpu::GraphW {
7503 idx: i,
7504 kind,
7505 row_scale: rs,
7506 data: &[],
7507 },
7508 self.weights.lm_head.rows(),
7509 )
7510 })
7511 } else {
7512 None
7513 };
7514 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
7515 let emb_gw = if steps > 1 {
7517 self.weights
7518 .embed_tokens
7519 .graph_weight()
7520 .map(|(_, i, kind, rs)| {
7521 (
7522 crate::gpu::GraphW {
7523 idx: i,
7524 kind,
7525 row_scale: rs,
7526 data: &[],
7527 },
7528 self.weights.embed_tokens.rows(),
7529 self.embed_multiplier,
7530 )
7531 })
7532 } else {
7533 None
7534 };
7535
7536 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
7542 (from..upto_excl.min(self.num_layers - 1))
7543 .filter(|&li| (li + 1) % self.physical_layers == 0)
7544 .map(|li| li - from)
7545 .collect()
7546 } else {
7547 Vec::new()
7548 };
7549 let mut h = hidden.to_vec();
7550 let outcome = crate::gpu::forward_token_graph(
7551 &model,
7552 self.graph_kv_id,
7553 &layers,
7554 &o1_views,
7555 self.o1_epoch,
7556 &self.inv_freq,
7557 &mut h,
7558 nh,
7559 nkv,
7560 hd,
7561 self.attn_scale,
7562 rd,
7563 self.hidden_size,
7564 self.intermediate_size,
7565 position,
7566 self.kv_cache.max_seq_len,
7567 gemma,
7568 self.rms_eps as f32,
7569 lm,
7570 &self.weights.final_norm,
7571 logits_out,
7572 &loop_norm_at,
7573 steps,
7574 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
7575 ids_out,
7576 layers_run,
7577 from,
7578 false,
7579 );
7580 match outcome {
7581 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
7582 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
7583 crate::gpu::TokenGraphOutcome::Declined => None,
7584 }
7585 }
7586
7587 #[cfg(target_os = "macos")]
7596 #[allow(clippy::type_complexity)]
7597 fn metal_rows_plan(
7598 &self,
7599 ) -> Option<(
7600 Vec<MetalRowsItem<'_>>,
7601 std::sync::Arc<cortiq_core::CmfModel>,
7602 Option<crate::gpu_metal::GdnGpuCfg>,
7603 )> {
7604 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
7605 if !crate::gpu::q1_force()
7606 || !crate::gpu::enabled_here()
7607 || std::env::var("CMF_GPU_BLOCK")
7608 .map(|v| v == "0")
7609 .unwrap_or(false)
7610 || self.attn_softcap > 0.0
7611 || self.o1_active()
7612 || self.swa.is_some()
7613 || self.global_attn.is_some()
7614 || self.attention_heads_per_layer.is_some()
7615 || self.attn_v_norm
7616 || self.loop_final_norm
7617 {
7618 return None;
7619 }
7620 let attend_contract = self.head_dim % 4 == 0
7621 && self.head_dim <= 256
7622 && self.rotary_dim >= 2
7623 && self.rotary_dim <= self.head_dim
7624 && (self.rotary_dim / 2) % 32 == 0
7625 && self.num_kv_heads > 0
7626 && self.num_heads % self.num_kv_heads == 0;
7627 if !attend_contract {
7628 return None;
7629 }
7630 let mut plan: Vec<MetalRowsItem> = Vec::new();
7631 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
7632 for li in 0..self.num_layers {
7633 let lw = &self.weights.layers[self.phys_layer(li)];
7634 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
7635 return None;
7636 }
7637 let ffn = match &lw.ffn {
7638 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
7639 let (Some(g), Some(u), Some(dn)) = (
7640 d.gate_proj.q1_parts(),
7641 d.up_proj.q1_parts(),
7642 d.down_proj.q1_parts(),
7643 ) else {
7644 return None;
7645 };
7646 MetalFfn::Dense {
7647 gate: g,
7648 up: u,
7649 down: dn,
7650 }
7651 }
7652 _ => return None,
7653 };
7654 match &lw.attn {
7655 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
7656 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
7657 w.in_proj_qkv.q1_parts(),
7658 w.in_proj_z.q1_parts(),
7659 w.in_proj_a.f32_parts(),
7660 w.in_proj_b.f32_parts(),
7661 w.out_proj.q1_parts(),
7662 ) else {
7663 return None;
7664 };
7665 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
7666 model_ref.get_or_insert_with(|| model.clone());
7667 }
7668 let gl = GdnGpuLayer {
7669 attn_norm: &lw.input_norm,
7670 post_norm: &lw.post_norm,
7671 qkv,
7672 z,
7673 a,
7674 b: bb,
7675 out,
7676 ffn,
7677 conv1d: &w.conv1d,
7678 a_log: &w.a_log,
7679 dt_bias: &w.dt_bias,
7680 gnorm: &w.norm,
7681 };
7682 match plan.last_mut() {
7683 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
7684 _ => plan.push(MetalRowsItem::Gdn {
7685 run: vec![gl],
7686 first: li,
7687 }),
7688 }
7689 }
7690 AttnKind::Full {
7691 wq,
7692 wk,
7693 wv,
7694 wo,
7695 q_norm,
7696 k_norm,
7697 output_gate,
7698 softplus_gate: None,
7699 bias: None,
7700 } => {
7701 let (Some(pq), Some(pk), Some(pv), Some(po)) =
7702 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
7703 else {
7704 return None;
7705 };
7706 if let QTensor::Mapped { model, .. } = wq {
7707 model_ref.get_or_insert_with(|| model.clone());
7708 }
7709 let cache = &self.kv_cache.layers[li];
7710 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
7711 return None;
7712 }
7713 plan.push(MetalRowsItem::Attn {
7714 l: AttnGpuLayer {
7715 attn_norm: &lw.input_norm,
7716 post_norm: &lw.post_norm,
7717 wq: pq,
7718 wk: pk,
7719 wv: pv,
7720 wo: po,
7721 ffn,
7722 },
7723 li,
7724 q_norm: q_norm.as_deref(),
7725 k_norm: k_norm.as_deref(),
7726 output_gate: *output_gate,
7727 });
7728 }
7729 _ => return None,
7730 }
7731 }
7732 let model = model_ref?;
7733 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
7734 nv: cfg.num_v_heads,
7735 nk: cfg.num_k_heads,
7736 dk: cfg.key_head_dim,
7737 dv: cfg.value_head_dim,
7738 kk: cfg.conv_kernel,
7739 hidden: self.hidden_size,
7740 inter: self.intermediate_size,
7741 c_dim: cfg.conv_dim(),
7742 eps: cfg.rms_eps as f32,
7743 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7744 });
7745 Some((plan, model, gcfg))
7746 }
7747
7748 #[cfg(target_os = "macos")]
7750 #[allow(clippy::too_many_arguments)]
7751 fn metal_attn_params<'a>(
7752 li: usize,
7753 cache: &'a crate::kv_cache::LayerKvCache,
7754 q_norm: Option<&'a [f32]>,
7755 k_norm: Option<&'a [f32]>,
7756 output_gate: bool,
7757 inv_freq: &'a [f32],
7758 geom: (usize, usize, usize, usize),
7759 pos0: usize,
7760 kv_id: u64,
7761 scale: f32,
7762 eps: f32,
7763 gemma: bool,
7764 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
7765 let (nh, nkv, hd, rd) = geom;
7766 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
7767 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
7768 let cpu_stored = cpu_k[0].len() / hd;
7769 (
7770 crate::gpu_metal::AttnDeviceParams {
7771 kv_id,
7772 layer: li,
7773 nh,
7774 nkv,
7775 hd,
7776 rd,
7777 position: pos0,
7778 scale,
7779 eps,
7780 gemma,
7781 output_gate,
7782 q_norm,
7783 k_norm,
7784 inv_freq,
7785 cpu_k,
7786 cpu_v,
7787 cpu_stored,
7788 o1: None,
7789 },
7790 cpu_stored,
7791 )
7792 }
7793
7794 #[cfg(target_os = "macos")]
7799 #[allow(clippy::type_complexity)]
7800 fn metal_rows_run(
7801 &mut self,
7802 hiddens: &mut [f32],
7803 pos0: usize,
7804 b: usize,
7805 prefill: bool,
7806 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
7807 ) -> Option<MetalVerifyPending> {
7808 use crate::gpu_metal::{GraphDims, VerifyGraph};
7809 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
7810 for l in &mut self.kv_cache.layers {
7811 if l.linear_state.len() != want && want > 0 {
7812 l.linear_state = vec![0f32; want];
7813 }
7814 }
7815 let (plan, model, gcfg) = self.metal_rows_plan()?;
7816 let dims = GraphDims {
7817 hidden: self.hidden_size,
7818 eps: self.rms_eps as f32,
7819 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7820 };
7821 let mut graph = if prefill {
7822 VerifyGraph::new_prefill(&model, dims, hiddens, b)?
7823 } else {
7824 VerifyGraph::new(&model, dims, hiddens, b)?
7825 };
7826 let geom = (
7827 self.num_heads,
7828 self.num_kv_heads,
7829 self.head_dim,
7830 self.rotary_dim,
7831 );
7832 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7833 let eps = self.rms_eps as f32;
7834 let kv_id = self.graph_kv_id;
7835 let inv_freq = self.inv_freq.clone();
7836 for item in &plan {
7837 let ok = match item {
7838 MetalRowsItem::Gdn { run, .. } => gcfg
7839 .as_ref()
7840 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
7841 .unwrap_or(false),
7842 MetalRowsItem::Attn {
7843 l,
7844 li,
7845 q_norm,
7846 k_norm,
7847 output_gate,
7848 } => {
7849 let (p, _) = Self::metal_attn_params(
7850 *li,
7851 &self.kv_cache.layers[*li],
7852 *q_norm,
7853 *k_norm,
7854 *output_gate,
7855 &inv_freq,
7856 geom,
7857 pos0,
7858 kv_id,
7859 self.attn_scale,
7860 eps,
7861 gemma,
7862 );
7863 graph.attn_ok(l, &p)
7864 }
7865 };
7866 if !ok {
7867 use std::sync::atomic::{AtomicBool, Ordering};
7868 static SAID: AtomicBool = AtomicBool::new(false);
7869 if !SAID.swap(true, Ordering::Relaxed) {
7870 tracing::warn!("metal rows graph: a layer failed preflight — declining");
7871 }
7872 return None;
7873 }
7874 }
7875 let lm = match &spec {
7876 Some((lm, _, _)) => {
7877 if !graph.lm_head_ok(*lm) {
7878 return None;
7879 }
7880 Some(*lm)
7881 }
7882 None => None,
7883 };
7884 let mut gdn_layers = Vec::new();
7885 let mut attn_layers = Vec::new();
7886 for item in &plan {
7887 match item {
7888 MetalRowsItem::Gdn { run, first } => {
7889 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
7890 .iter()
7891 .map(|l| l.linear_state.as_slice())
7892 .collect();
7893 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
7894 return None;
7895 }
7896 gdn_layers.extend(*first..*first + run.len());
7897 }
7898 MetalRowsItem::Attn {
7899 l,
7900 li,
7901 q_norm,
7902 k_norm,
7903 output_gate,
7904 } => {
7905 let (p, cpu_stored) = Self::metal_attn_params(
7906 *li,
7907 &self.kv_cache.layers[*li],
7908 *q_norm,
7909 *k_norm,
7910 *output_gate,
7911 &inv_freq,
7912 geom,
7913 pos0,
7914 kv_id,
7915 self.attn_scale,
7916 eps,
7917 gemma,
7918 );
7919 if !graph.encode_attn_b(l, &p) {
7920 return None;
7921 }
7922 attn_layers.push((*li, cpu_stored));
7923 }
7924 }
7925 }
7926 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
7927 if !graph.encode_lm_head_b(final_norm, lm) {
7928 return None;
7929 }
7930 }
7931 graph.sync();
7932 if let Some((lm, _, logits)) = spec {
7933 logits.resize(b * lm.1, 0.0);
7934 graph.read_logits(logits);
7935 }
7936 graph.read_hidden(hiddens);
7937 Some(MetalVerifyPending {
7938 graph,
7939 gdn_layers,
7940 attn_layers,
7941 })
7942 }
7943
7944 #[cfg(target_os = "macos")]
7950 fn try_batch_graph_metal(
7951 &mut self,
7952 hiddens: &mut [f32],
7953 positions: &[usize],
7954 b: usize,
7955 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
7956 ) -> crate::gpu::BatchGraphOutcome {
7957 let _t0 = std::time::Instant::now();
7958 if positions.len() != b
7959 || positions.windows(2).any(|w| w[1] != w[0] + 1)
7960 || hiddens.len() != b * self.hidden_size
7961 {
7962 return crate::gpu::BatchGraphOutcome::Declined;
7963 }
7964 let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
7965 return crate::gpu::BatchGraphOutcome::Declined;
7966 };
7967 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7968 eprintln!(
7969 "metal-verify: {:.1} ms | b={b}",
7970 _t0.elapsed().as_secs_f64() * 1e3
7971 );
7972 }
7973 self.metal_verify = Some(pending);
7974 crate::gpu::BatchGraphOutcome::Completed
7975 }
7976
7977 #[cfg(target_os = "macos")]
7982 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
7983 let b = ids.len();
7984 if b == 0 || b > 512 {
7985 return None;
7986 }
7987 let hs = self.hidden_size;
7988 let mut hiddens = vec![0f32; b * hs];
7989 for (j, &id) in ids.iter().enumerate() {
7990 let e = self.embed_single(id);
7991 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
7992 }
7993 let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
7994 let idxs = pending.gdn_layers.clone();
7996 let mut outs: Vec<&mut [f32]> = self
7997 .kv_cache
7998 .layers
7999 .iter_mut()
8000 .enumerate()
8001 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8002 .map(|(_, l)| l.linear_state.as_mut_slice())
8003 .collect();
8004 pending.graph.finish_states(&mut outs);
8005 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8006 let mut kbuf = vec![0f32; b * nkv * hd];
8007 let mut vbuf = vec![0f32; b * nkv * hd];
8008 for (li, cpu_stored) in &pending.attn_layers {
8009 if crate::gpu_metal::kv_mirror_read_rows(
8010 self.graph_kv_id,
8011 *li,
8012 nkv,
8013 hd,
8014 *cpu_stored,
8015 b,
8016 &mut kbuf,
8017 &mut vbuf,
8018 ) {
8019 let cache = &mut self.kv_cache.layers[*li];
8020 for r in 0..b {
8021 cache.append(
8022 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8023 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8024 &[],
8025 );
8026 }
8027 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
8028 }
8029 }
8030 Some(hiddens)
8031 }
8032
8033 #[cfg(target_os = "macos")]
8037 fn metal_verify_commit(&mut self, a: usize) -> bool {
8038 let Some(mut pending) = self.metal_verify.take() else {
8039 return false;
8040 };
8041 let n = a + 1;
8042 let idxs = pending.gdn_layers.clone();
8044 let mut outs: Vec<&mut [f32]> = self
8045 .kv_cache
8046 .layers
8047 .iter_mut()
8048 .enumerate()
8049 .filter(|(i, _)| idxs.binary_search(i).is_ok())
8050 .map(|(_, l)| l.linear_state.as_mut_slice())
8051 .collect();
8052 if !pending.graph.commit(n, &mut outs) {
8053 return false;
8054 }
8055 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8056 let mut kbuf = vec![0f32; n * nkv * hd];
8057 let mut vbuf = vec![0f32; n * nkv * hd];
8058 for (li, cpu_stored) in &pending.attn_layers {
8059 if crate::gpu_metal::kv_mirror_read_rows(
8060 self.graph_kv_id,
8061 *li,
8062 nkv,
8063 hd,
8064 *cpu_stored,
8065 n,
8066 &mut kbuf,
8067 &mut vbuf,
8068 ) {
8069 let cache = &mut self.kv_cache.layers[*li];
8070 for r in 0..n {
8071 cache.append(
8072 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8073 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8074 &[],
8075 );
8076 }
8077 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
8078 }
8079 }
8080 true
8081 }
8082
8083 #[cfg(target_os = "macos")]
8089 fn mtp_warm_batch_metal(
8090 &mut self,
8091 m: &mut MtpModule,
8092 pairs: &[(&[f32], u32)],
8093 first_pos: usize,
8094 ) -> bool {
8095 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
8096 let b = pairs.len();
8097 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
8098 return false;
8099 }
8100 let AttnKind::Full {
8101 wq,
8102 wk,
8103 wv,
8104 wo,
8105 q_norm,
8106 k_norm,
8107 output_gate,
8108 softplus_gate: None,
8109 bias: None,
8110 } = &m.layer.attn
8111 else {
8112 return false;
8113 };
8114 let FfnKind::Dense(d) = &m.layer.ffn else {
8115 return false;
8116 };
8117 if !d.segs.is_empty() {
8118 return false;
8119 }
8120 let (Some(pq), Some(pk), Some(pv), Some(po)) =
8121 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
8122 else {
8123 return false;
8124 };
8125 let (Some(g), Some(u), Some(dn)) = (
8126 d.gate_proj.q1_parts(),
8127 d.up_proj.q1_parts(),
8128 d.down_proj.q1_parts(),
8129 ) else {
8130 return false;
8131 };
8132 let Some(eh) = m.eh_proj.q1_parts() else {
8133 return false;
8134 };
8135 let QTensor::Mapped { model, .. } = wq else {
8136 return false;
8137 };
8138 let model = model.clone();
8139 let hs = self.hidden_size;
8140 let mut cat = vec![0f32; b * 2 * hs];
8142 for (j, (h, tok)) in pairs.iter().enumerate() {
8143 let e = self.embed_single(*tok);
8144 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
8145 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
8146 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
8147 }
8148 let dims = GraphDims {
8149 hidden: hs,
8150 eps: self.rms_eps as f32,
8151 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8152 };
8153 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
8154 return false;
8155 };
8156 let l = AttnGpuLayer {
8157 attn_norm: &m.layer.input_norm,
8158 post_norm: &m.layer.post_norm,
8159 wq: pq,
8160 wk: pk,
8161 wv: pv,
8162 wo: po,
8163 ffn: MetalFfn::Dense {
8164 gate: g,
8165 up: u,
8166 down: dn,
8167 },
8168 };
8169 let (nh, nkv, hd, rd) = (
8170 self.num_heads,
8171 self.num_kv_heads,
8172 self.head_dim,
8173 self.rotary_dim,
8174 );
8175 let inv_freq = self.inv_freq.clone();
8176 let cpu_stored;
8177 {
8178 let cache = &m.kv;
8179 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8180 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8181 cpu_stored = cpu_k[0].len() / hd;
8182 if cpu_stored != first_pos {
8183 return false;
8184 }
8185 let p = AttnDeviceParams {
8186 kv_id: self.mtp_kv_id(),
8187 layer: Self::MTP_LAYER_BASE,
8188 nh,
8189 nkv,
8190 hd,
8191 rd,
8192 position: first_pos,
8193 scale: self.attn_scale,
8194 eps: self.rms_eps as f32,
8195 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8196 output_gate: *output_gate,
8197 q_norm: q_norm.as_deref(),
8198 k_norm: k_norm.as_deref(),
8199 inv_freq: &inv_freq,
8200 cpu_k,
8201 cpu_v,
8202 cpu_stored,
8203 o1: None,
8204 };
8205 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
8206 return false;
8207 }
8208 }
8209 graph.sync();
8210 let mut kbuf = vec![0f32; b * nkv * hd];
8211 let mut vbuf = vec![0f32; b * nkv * hd];
8212 if !crate::gpu_metal::kv_mirror_read_rows(
8213 self.mtp_kv_id(),
8214 Self::MTP_LAYER_BASE,
8215 nkv,
8216 hd,
8217 cpu_stored,
8218 b,
8219 &mut kbuf,
8220 &mut vbuf,
8221 ) {
8222 return false;
8223 }
8224 for r in 0..b {
8225 m.kv.append(
8226 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
8227 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
8228 &[],
8229 );
8230 }
8231 crate::gpu_metal::kv_mirror_set_stored(
8232 self.mtp_kv_id(),
8233 Self::MTP_LAYER_BASE,
8234 cpu_stored + b,
8235 );
8236 true
8237 }
8238
8239 fn draft_vocab_rows(head_rows: usize) -> usize {
8242 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
8243 let n = *N.get_or_init(|| {
8244 std::env::var("CMF_DRAFT_VOCAB")
8245 .ok()
8246 .and_then(|v| v.parse().ok())
8247 .unwrap_or(65536)
8248 });
8249 if n == 0 { head_rows } else { n.min(head_rows) }
8250 }
8251
8252 #[cfg(target_os = "macos")]
8257 fn mtp_step_metal(
8258 &mut self,
8259 m: &mut MtpModule,
8260 hidden: &[f32],
8261 next_token: u32,
8262 position: usize,
8263 want_logits: bool,
8264 ) -> Option<(Vec<f32>, Vec<f32>)> {
8265 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
8266 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
8267 || !crate::gpu::q1_force()
8268 || !crate::gpu::enabled_here()
8269 || self.attn_softcap > 0.0
8270 || self.attention_heads_per_layer.is_some()
8271 || m.kv.mode != crate::kv_cache::KvMode::F32
8272 || m.kv.o1.is_some()
8273 {
8274 return None;
8275 }
8276 let AttnKind::Full {
8277 wq,
8278 wk,
8279 wv,
8280 wo,
8281 q_norm,
8282 k_norm,
8283 output_gate,
8284 softplus_gate: None,
8285 bias: None,
8286 } = &m.layer.attn
8287 else {
8288 return None;
8289 };
8290 let FfnKind::Dense(d) = &m.layer.ffn else {
8291 return None;
8292 };
8293 if d.act != Act::Silu || !d.segs.is_empty() {
8294 return None;
8295 }
8296 let (pq, pk, pv, po) = (
8297 wq.q1_parts()?,
8298 wk.q1_parts()?,
8299 wv.q1_parts()?,
8300 wo.q1_parts()?,
8301 );
8302 let (g, u, dn) = (
8303 d.gate_proj.q1_parts()?,
8304 d.up_proj.q1_parts()?,
8305 d.down_proj.q1_parts()?,
8306 );
8307 let QTensor::Mapped { model, .. } = wq else {
8308 return None;
8309 };
8310 let model = model.clone();
8311 let lm = if want_logits {
8312 Some(self.weights.lm_head.q1_parts()?)
8313 } else {
8314 None
8315 };
8316 let dims = GraphDims {
8317 hidden: self.hidden_size,
8318 eps: self.rms_eps as f32,
8319 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8320 };
8321 let hs = self.hidden_size;
8324 let mut x = vec![0f32; hs];
8325 let mut graph = TokenGraph::new(&model, dims, &x)?;
8326 let mut folded = false;
8327 if let Some(eh) = m.eh_proj.q1_parts() {
8328 let e = self.embed_single(next_token);
8329 let mut cat = vec![0.0f32; 2 * hs];
8330 let (cat_e, cat_h) = cat.split_at_mut(hs);
8331 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
8332 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
8333 folded = graph.encode_input_proj(eh, &cat);
8334 }
8335 if !folded {
8336 x = self.mtp_block_input(m, hidden, next_token);
8337 graph = TokenGraph::new(&model, dims, &x)?;
8338 }
8339 let l = AttnGpuLayer {
8340 attn_norm: &m.layer.input_norm,
8341 post_norm: &m.layer.post_norm,
8342 wq: pq,
8343 wk: pk,
8344 wv: pv,
8345 wo: po,
8346 ffn: MetalFfn::Dense {
8347 gate: g,
8348 up: u,
8349 down: dn,
8350 },
8351 };
8352 let (nh, nkv, hd, rd) = (
8353 self.num_heads,
8354 self.num_kv_heads,
8355 self.head_dim,
8356 self.rotary_dim,
8357 );
8358 let inv_freq = self.inv_freq.clone();
8359 {
8360 let cache = &m.kv;
8361 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
8362 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
8363 let cpu_stored = cpu_k[0].len() / hd;
8364 let p = AttnDeviceParams {
8365 kv_id: self.mtp_kv_id(),
8366 layer: Self::MTP_LAYER_BASE,
8367 nh,
8368 nkv,
8369 hd,
8370 rd,
8371 position,
8372 scale: self.attn_scale,
8373 eps: self.rms_eps as f32,
8374 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
8375 output_gate: *output_gate,
8376 q_norm: q_norm.as_deref(),
8377 k_norm: k_norm.as_deref(),
8378 inv_freq: &inv_freq,
8379 cpu_k,
8380 cpu_v,
8381 cpu_stored,
8382 o1: None,
8383 };
8384 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
8385 return None;
8386 }
8387 }
8388 let draft_rows = if let Some(lm) = lm {
8394 Self::draft_vocab_rows(lm.1)
8395 } else {
8396 0
8397 };
8398 if let Some(lm) = lm {
8399 if !graph.lm_head_ok(lm) {
8400 return None;
8401 }
8402 if draft_rows < lm.1 {
8403 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
8404 return None;
8405 }
8406 } else {
8407 graph.encode_lm_head(&m.final_norm, lm);
8408 }
8409 }
8410 graph.sync();
8411 let mut logits = Vec::new();
8412 if let Some(lm) = lm {
8413 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
8414 logits = attention::take_buf(n_read);
8415 graph.read_logits(&mut logits);
8416 logits.resize(self.vocab_size, f32::NEG_INFINITY);
8418 }
8419 graph.finish(&mut x);
8420 let mut krow = attention::take_buf(nkv * hd);
8421 let mut vrow = attention::take_buf(nkv * hd);
8422 if crate::gpu_metal::kv_mirror_read_last(
8423 self.mtp_kv_id(),
8424 Self::MTP_LAYER_BASE,
8425 nkv,
8426 hd,
8427 &mut krow,
8428 &mut vrow,
8429 ) {
8430 m.kv.append(&krow, &vrow, &[]);
8431 }
8432 attention::recycle_buf(&mut krow);
8433 attention::recycle_buf(&mut vrow);
8434 Some((logits, x))
8435 }
8436
8437 fn try_batch_graph_wgpu(
8438 &self,
8439 hiddens: &mut [f32],
8440 positions: &[usize],
8441 k: usize,
8442 spec: Option<crate::gpu::SpecTail<'_>>,
8443 ) -> crate::gpu::BatchGraphOutcome {
8444 let _tb = std::time::Instant::now();
8445 if self.attn_softcap > 0.0 {
8446 return crate::gpu::BatchGraphOutcome::Declined; }
8448 let nh = self.num_heads;
8449 let (nkv, hd, rd) = self.layer_geom(0);
8450 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8451 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
8452 if let Some((_, i, kind, rs)) = t.graph_weight() {
8453 return Some(crate::gpu::GraphW {
8454 idx: i,
8455 kind,
8456 row_scale: rs,
8457 data: &[],
8458 });
8459 }
8460 t.as_f32().map(|d| crate::gpu::GraphW {
8461 idx: 0,
8462 kind: 4,
8463 row_scale: &[],
8464 data: d,
8465 })
8466 }
8467 let built: Option<(
8468 Vec<crate::gpu::GraphLayer<'_>>,
8469 std::sync::Arc<cortiq_core::CmfModel>,
8470 )> = (|| {
8471 let mut layers = Vec::with_capacity(self.num_layers);
8472 let mut model = None;
8473 for li in 0..self.num_layers {
8474 let lw = &self.weights.layers[self.phys_layer(li)];
8475 let gffn = match &lw.ffn {
8482 FfnKind::Dense(d) if !d.segs.is_empty() => return None,
8483 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
8484 gate: gw(&d.gate_proj)?,
8485 up: gw(&d.up_proj)?,
8486 down: gw(&d.down_proj)?,
8487 },
8488 FfnKind::Moe(m) => {
8489 if m.router_sigmoid
8490 || m.expert_bias.is_some()
8491 || m.route_tau.is_some()
8492 || m.mask.is_some()
8493 {
8494 return None;
8495 }
8496 let (se, sg) = m.shared.as_ref()?;
8497 let sgate = gw(sg.as_ref()?)?;
8498 let router = gw(&m.router)?;
8499 let inter = m.experts.first()?.gate_proj.rows();
8500 let mut experts = Vec::with_capacity(m.experts.len() + 1);
8501 let mut q4tp: Option<bool> = None;
8502 let mut gu_q2: Option<bool> = None;
8503 for e in m.experts.iter().chain(std::iter::once(se)) {
8504 if !matches!(e.act, Act::Silu)
8505 || e.gate_proj.rows() != inter
8506 || e.up_proj.rows() != inter
8507 {
8508 return None;
8509 }
8510 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
8514 Some((mm, gi)) => (
8515 mm,
8516 gi,
8517 e.up_proj.mapped_q4t()?.1,
8518 e.down_proj.mapped_q4t()?.1,
8519 false,
8520 false,
8521 ),
8522 None => match e.gate_proj.mapped_q2tp() {
8523 Some((mm, gi)) => (
8524 mm,
8525 gi,
8526 e.up_proj.mapped_q2tp()?.1,
8527 e.down_proj.mapped_q4tp()?.1,
8528 true,
8529 true,
8530 ),
8531 None => {
8532 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
8533 (
8534 mm,
8535 gi,
8536 e.up_proj.mapped_q4tp()?.1,
8537 e.down_proj.mapped_q4tp()?.1,
8538 true,
8539 false,
8540 )
8541 }
8542 },
8543 };
8544 if *q4tp.get_or_insert(is_p) != is_p
8545 || *gu_q2.get_or_insert(is_q2) != is_q2
8546 {
8547 return None;
8548 }
8549 model.get_or_insert_with(|| mm.clone());
8550 experts.push((gi, ui, di));
8551 }
8552 crate::gpu::GraphFfn::Moe {
8553 router,
8554 shared_gate: sgate,
8555 experts,
8556 n_exp: m.experts.len(),
8557 top_k: m.top_k,
8558 inter,
8559 norm_topk: m.norm_topk_prob,
8560 q4tp: q4tp?,
8561 gu_q2: gu_q2.unwrap_or(false),
8562 sigmoid: false,
8563 bias: None,
8564 has_shared: true,
8565 }
8566 }
8567 _ => return None,
8568 };
8569 let attn = match &lw.attn {
8570 AttnKind::Full {
8571 wq,
8572 wk,
8573 wv,
8574 wo,
8575 q_norm,
8576 k_norm,
8577 output_gate,
8578 softplus_gate,
8579 bias,
8580 } => {
8581 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
8582 return None;
8583 }
8584 let (m, _, _, _) = wq.graph_weight()?;
8585 model = Some(m.clone());
8586 crate::gpu::GraphAttn::Full {
8587 wq: gw(wq)?,
8588 wk: gw(wk)?,
8589 wv: gw(wv)?,
8590 wo: gw(wo)?,
8591 q_norm: q_norm.as_deref(),
8592 k_norm: k_norm.as_deref(),
8593 bias: bias
8594 .as_ref()
8595 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8596 output_gate: *output_gate,
8597 cpu_k: self.kv_cache.layers[li].k_heads(),
8598 cpu_v: self.kv_cache.layers[li].v_heads(),
8599 }
8600 }
8601 AttnKind::LinearGdn(w) => {
8602 let cfg = self.gdn_cfg?;
8603 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
8604 model = Some(m.clone());
8605 crate::gpu::GraphAttn::Gdn {
8606 qkv: gw(&w.in_proj_qkv)?,
8607 z: gw(&w.in_proj_z)?,
8608 a: gw(&w.in_proj_a)?,
8609 b: gw(&w.in_proj_b)?,
8610 out: gw(&w.out_proj)?,
8611 conv1d: &w.conv1d,
8612 a_log: &w.a_log,
8613 dt_bias: &w.dt_bias,
8614 norm: &w.norm,
8615 nv: cfg.num_v_heads,
8616 nk: cfg.num_k_heads,
8617 dk: cfg.key_head_dim,
8618 dv: cfg.value_head_dim,
8619 kk: cfg.conv_kernel,
8620 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
8621 }
8622 }
8623 _ => return None,
8624 };
8625 layers.push(crate::gpu::GraphLayer {
8626 input_norm: &lw.input_norm,
8627 attn,
8628 post_norm: &lw.post_norm,
8629 ffn: gffn,
8630 });
8631 }
8632 Some((layers, model?))
8633 })();
8634 let Some((layers, model)) = built else {
8635 {
8636 use std::sync::atomic::{AtomicBool, Ordering};
8637 static SAID: AtomicBool = AtomicBool::new(false);
8638 if !SAID.swap(true, Ordering::Relaxed) {
8639 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
8640 }
8641 }
8642 return crate::gpu::BatchGraphOutcome::Declined;
8643 };
8644 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8645 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
8646 }
8647 crate::gpu::forward_batch_graph(
8648 &model,
8649 self.graph_kv_id,
8650 &layers,
8651 &self.inv_freq,
8652 hiddens,
8653 nh,
8654 nkv,
8655 hd,
8656 rd,
8657 self.hidden_size,
8658 self.intermediate_size,
8659 positions,
8660 self.kv_cache.max_seq_len,
8661 gemma,
8662 self.rms_eps as f32,
8663 self.attn_scale,
8664 k,
8665 &(0..self.num_layers)
8666 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
8667 .collect::<Vec<_>>(),
8668 self.o1_epoch,
8669 spec,
8670 )
8671 }
8672
8673 fn draft_probe() -> bool {
8677 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8678 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
8679 }
8680
8681 #[cfg(feature = "gpu")]
8693 fn dsv4_spec_on() -> bool {
8694 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8695 *ON.get_or_init(|| {
8696 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
8700 return v != "0";
8701 }
8702 std::env::var("CMF_DSV4_SPEC")
8709 .map(|v| v != "0")
8710 .unwrap_or_else(|_| {
8711 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
8712 })
8713 })
8714 }
8715
8716 #[cfg(feature = "gpu")]
8723 fn dsv4_spec_step(
8724 &mut self,
8725 tip_token: u32,
8726 t_next: u32,
8727 next_pos: usize,
8728 max_extra: usize,
8729 drafted: &mut usize,
8730 accepted_ctr: &mut usize,
8731 ) -> Option<(Vec<u32>, usize)> {
8732 let t_all = std::time::Instant::now();
8733 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
8734 thread_local! {
8735 static LAST: std::cell::Cell<Option<std::time::Instant>> =
8736 const { std::cell::Cell::new(None) };
8737 }
8738 LAST.with(|l| {
8739 if let Some(prev) = l.get() {
8740 eprintln!(
8741 "между раундами {:.1} мс",
8742 prev.elapsed().as_secs_f64() * 1e3
8743 );
8744 }
8745 l.set(Some(std::time::Instant::now()));
8746 });
8747 }
8748 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
8749 eprintln!("spec_step: вход pos={next_pos}");
8750 }
8751 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
8752 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
8753 if self.dspark.is_none() {
8755 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
8756 if t.is_empty() {
8757 return None;
8758 }
8759 crate::dsv4::dspark_arm(&t, cfg.dim);
8760 self.dspark = Some(crate::dsv4::DsparkState::new(
8761 self.dsv4_mtp.len(),
8762 &cfg,
8763 t.len(),
8764 ));
8765 }
8766 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
8767 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
8768 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
8769 eprintln!("spec_step: пак не построился (targets {targets:?})");
8770 }
8771 let pack = pack?;
8772 let block = crate::dsv4::dspark_block();
8773 let b_box = self.dsv4.as_mut()?;
8774 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
8775 let ds = self.dspark.as_mut()?;
8776 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
8779 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
8780 if dbg {
8781 eprintln!("spec_step: нет захвата");
8782 }
8783 return None;
8784 }
8785 ds.have_hidden = true;
8786 let tip_pos = next_pos.checked_sub(1)?;
8787 let draft_started = std::time::Instant::now();
8788 let mut conf = Vec::new();
8789 let props = crate::dsv4::dspark_draft_gpu(
8790 g,
8791 &self.dsv4_mtp,
8792 &cfg,
8793 ds,
8794 pack,
8795 st.kv_id,
8796 tip_token,
8797 tip_pos,
8798 self.pool.as_deref(),
8799 &mut conf,
8800 );
8801 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
8802 *drafted += block;
8803 if props.is_empty() || props[0] != t_next {
8804 if dbg {
8805 eprintln!(
8806 "spec_step: черновик {} (props0={:?} t_next={t_next})",
8807 if props.is_empty() {
8808 "пуст"
8809 } else {
8810 "мимо"
8811 },
8812 props.first()
8813 );
8814 }
8815 return None;
8816 }
8817 let mut k_verify = crate::dsv4::dspark_verify_k()
8824 .min(props.len())
8825 .min(max_extra.saturating_add(1));
8826 let conf_min = {
8832 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
8833 *M.get_or_init(|| {
8834 std::env::var("CMF_DSPARK_CONF_MIN")
8835 .ok()
8836 .and_then(|v| v.parse().ok())
8837 .unwrap_or(0.0)
8838 })
8839 };
8840 if conf_min > 0.0 && conf.len() >= props.len() {
8841 let mut keep = 1usize;
8842 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
8843 keep += 1;
8844 }
8845 k_verify = k_verify.min(keep.max(2));
8846 }
8847 if k_verify < 2 {
8848 return None;
8849 }
8850 let mut fed = Vec::with_capacity(k_verify);
8851 fed.push(t_next);
8852 fed.extend_from_slice(&props[1..k_verify]);
8853 let mut argmax = Vec::new();
8854 let mut logits_all = Vec::new();
8855 let mut walked = Vec::new();
8856 let txn = crate::dsv4::dsv4_verify_chunk(
8857 g,
8858 layers,
8859 &cfg,
8860 st,
8861 &fed,
8862 next_pos,
8863 &self.inv_freq,
8864 self.pool.as_deref(),
8865 &targets,
8866 &mut argmax,
8867 &mut logits_all,
8868 &mut walked,
8869 );
8870 if txn.is_none() && dbg {
8871 eprintln!("spec_step: verify отказал");
8872 }
8873 let txn = txn?;
8874 let spec_gpu_end = txn.gpu_end;
8875 let b = fed.len();
8876 let mut accepted = 1usize;
8877 while accepted < b && fed[accepted] == argmax[accepted - 1] {
8878 accepted += 1;
8879 }
8880 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
8885 accepted = 1;
8886 }
8887 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
8888 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
8889 }
8890 let t_fin = std::time::Instant::now();
8891 if !crate::dsv4::dsv4_spec_finish(
8892 g,
8893 layers,
8894 &cfg,
8895 st,
8896 txn,
8897 accepted,
8898 &fed,
8899 &self.inv_freq,
8900 self.pool.as_deref(),
8901 ) {
8902 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
8903 return None;
8904 }
8905 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
8906 eprintln!(
8907 "finish(k={accepted}): {:.1} мс",
8908 t_fin.elapsed().as_secs_f64() * 1e3
8909 );
8910 }
8911 *accepted_ctr += accepted - 1;
8912 let (hc, dim) = (cfg.hc_mult, cfg.dim);
8917 let dev_caps: Vec<usize> = targets
8922 .iter()
8923 .copied()
8924 .filter(|&t| t < spec_gpu_end)
8925 .collect();
8926 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
8927 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
8928 return None;
8929 }
8930 for t in 0..accepted {
8931 let tip = t + 1 == accepted;
8932 for (slot, &tl) in targets.iter().enumerate() {
8933 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
8934 let lo = (di * b + t) * hc * dim;
8935 crate::dsv4::dspark_capture(
8936 &caps_all[lo..lo + hc * dim],
8937 &cfg,
8938 slot,
8939 &mut ds.main_hidden,
8940 );
8941 } else if tip
8942 && crate::dsv4::dspark_peek_slot(slot, dim, {
8943 let lo = slot * dim;
8944 &mut ds.main_hidden[lo..lo + dim]
8945 })
8946 {
8947 } else {
8952 crate::dsv4::dspark_capture(
8956 &walked[t * hc * dim..(t + 1) * hc * dim],
8957 &cfg,
8958 slot,
8959 &mut ds.main_hidden,
8960 );
8961 }
8962 }
8963 crate::dsv4::dspark_ring_append(
8964 g,
8965 &self.dsv4_mtp,
8966 &cfg,
8967 ds,
8968 next_pos + t,
8969 self.pool.as_deref(),
8970 );
8971 }
8972 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
8973 self.graph_logits = Some(row);
8974 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
8979 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
8980 crate::dsv4::pick_tally_arm();
8981 }
8982 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
8983 eprintln!(
8984 "spec_step total {:.1} мс (k={accepted})",
8985 t_all.elapsed().as_secs_f64() * 1e3
8986 );
8987 }
8988 Some((fed[1..accepted].to_vec(), next_pos + accepted))
8989 }
8990
8991 fn dspark_probe(&mut self, position: usize, token_id: u32) {
8992 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
8993 return;
8994 }
8995 let trunk_now = crate::dsv4::pick_tally_take();
8997 crate::dsv4::trunk_freq_note(&trunk_now);
8998 if !trunk_now.is_empty() {
8999 self.dspark_trunk_picks.push(trunk_now);
9000 let keep = crate::dsv4::dspark_block();
9001 if self.dspark_trunk_picks.len() > keep {
9002 self.dspark_trunk_picks.remove(0);
9003 }
9004 }
9005 for p in std::mem::take(&mut self.dspark_pending) {
9008 let Some(i) = position.checked_sub(p.0 + 1) else {
9009 continue;
9010 };
9011 let mut p = p;
9012 if i < p.1.len() {
9013 if p.2 && p.1[i] == token_id {
9014 p.3 = i + 1;
9015 } else {
9016 p.2 = false;
9017 }
9018 if i + 1 < p.1.len() {
9019 self.dspark_pending.push(p);
9020 continue;
9021 }
9022 }
9023 self.dspark_hist.push(p.3);
9024 self.dspark_real.push(token_id);
9025 }
9026 let Some(b) = &mut self.dsv4 else { return };
9027 let (g, layers, cfg) = (&b.0, &b.1, b.2);
9028 let n_layers = layers.len();
9029 if self.dspark.is_none() {
9030 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
9031 if t.is_empty() {
9032 return;
9033 }
9034 eprintln!(
9035 "DSpark: захват со слоёв {t:?}, блок {}",
9036 crate::dsv4::dspark_block()
9037 );
9038 crate::dsv4::dspark_arm(&t, cfg.dim);
9039 self.dspark = Some(crate::dsv4::DsparkState::new(
9040 self.dsv4_mtp.len(),
9041 &cfg,
9042 t.len(),
9043 ));
9044 }
9045 let ds = self.dspark.as_mut().unwrap();
9046 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
9047 return; }
9049 let mut conf = Vec::new();
9050 crate::dsv4::pick_tally_arm();
9051 let draft_started = std::time::Instant::now();
9056 #[cfg(feature = "gpu")]
9057 let gpu_draft = crate::dsv4::dspark_gpu_on();
9058 #[cfg(not(feature = "gpu"))]
9059 let gpu_draft = false;
9060 let props = if gpu_draft {
9061 #[cfg(feature = "gpu")]
9062 {
9063 let kv_id = b.3.kv_id;
9064 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
9065 Some(pk) => crate::dsv4::dspark_draft_gpu(
9066 g,
9067 &self.dsv4_mtp,
9068 &cfg,
9069 ds,
9070 pk,
9071 kv_id,
9072 token_id,
9073 position,
9074 self.pool.as_deref(),
9075 &mut conf,
9076 ),
9077 None => Vec::new(),
9078 }
9079 }
9080 #[cfg(not(feature = "gpu"))]
9081 Vec::new()
9082 } else {
9083 crate::gpu::cpu_scope(|| {
9084 crate::dsv4::dspark_draft(
9085 g,
9086 &self.dsv4_mtp,
9087 &cfg,
9088 ds,
9089 token_id,
9090 position,
9091 self.pool.as_deref(),
9092 &mut conf,
9093 )
9094 })
9095 };
9096 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
9097 let draft_picks = crate::dsv4::pick_tally_take();
9098 crate::dsv4::dspark_freq_note(&draft_picks);
9099 crate::dsv4::pick_tally_arm();
9102 if !props.is_empty() {
9103 let (tu, tt) = {
9107 let flat: Vec<(usize, Vec<usize>)> = self
9108 .dspark_trunk_picks
9109 .iter()
9110 .flat_map(|v| v.iter().cloned())
9111 .collect();
9112 let mut per: std::collections::HashMap<usize, Vec<usize>> =
9114 std::collections::HashMap::new();
9115 for (li, picks) in flat {
9116 per.entry(li).or_default().extend(picks);
9117 }
9118 let n = per.len().max(1);
9119 let mut u = 0usize;
9120 let mut t = 0usize;
9121 for (_, v) in per {
9122 t += v.len();
9123 u += v.iter().collect::<std::collections::HashSet<_>>().len();
9124 }
9125 (u / n, t / n)
9126 };
9127 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
9128 self.dspark_exp.push((tu, tt, du, dt));
9129 self.dspark_pending.push((position, props, true, 0));
9130 }
9131 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
9132 let n = self.dspark_hist.len() as f32;
9133 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
9134 let block = crate::dsv4::dspark_block();
9135 let mut at = vec![0usize; block + 1];
9136 for &k in &self.dspark_hist {
9137 at[k] += 1;
9138 }
9139 let mut surv = Vec::with_capacity(block);
9141 for i in 1..=block {
9142 let k = at[i..].iter().sum::<usize>() as f32 / n;
9143 surv.push(format!("{k:.2}"));
9144 }
9145 let distinct = self
9146 .dspark_real
9147 .iter()
9148 .collect::<std::collections::HashSet<_>>()
9149 .len();
9150 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
9151 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
9152 });
9153 let m = self.dspark_exp.len().max(1);
9154 eprintln!(
9155 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
9156 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
9157 self.dspark_hist.len(),
9158 mean + 1.0,
9159 surv.join(" ")
9160 );
9161 eprintln!(
9162 "DSpark: разных токенов {distinct} из {} (вырожденность), \
9163 эксперты ствол {}/{} на слой за {block} токенов, \
9164 черновик {}/{} за блок, draft {:.2} мс/блок",
9165 self.dspark_real.len(),
9166 tu / m,
9167 tt / m,
9168 du / m,
9169 dt / m,
9170 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
9171 );
9172 }
9173 }
9174
9175 fn forward_layers_upto(
9176 &mut self,
9177 hidden: &[f32],
9178 position: usize,
9179 task_mask: Option<&TaskMask>,
9180 upto: Option<usize>,
9181 ) -> Vec<f32> {
9182 if let Some(plan) = self.gpu_plan.clone() {
9188 if upto.is_none() && plan.len() > 1 {
9189 let mut h = hidden.to_vec();
9190 for &(dev, from, upto_incl) in plan.iter() {
9191 h = crate::gpu::with_device(dev, || {
9192 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
9193 });
9194 }
9195 return h;
9196 }
9197 }
9198 self.forward_layers_span(hidden, position, task_mask, 0, upto)
9199 }
9200
9201 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
9206 self.set_gpu_plan_at(devices, None)
9207 }
9208
9209 pub fn set_gpu_plan_at(
9213 &mut self,
9214 devices: Option<&[usize]>,
9215 at: Option<usize>,
9216 ) -> Result<(), String> {
9217 let Some(devs) = devices.filter(|d| d.len() > 1) else {
9218 self.gpu_plan = None;
9219 return Ok(());
9220 };
9221 self.split_supported()?;
9222 let n = self.num_layers;
9223 if devs.len() > n {
9224 return Err(format!("{} devices for {n} layers", devs.len()));
9225 }
9226 if let Some(k) = at {
9227 if k == 0 || k >= n {
9228 return Err(format!("split at {k}: the model has {n} layers"));
9229 }
9230 if devs.len() == 2 {
9231 self.gpu_plan = Some(std::sync::Arc::new(vec![
9232 (devs[0], 0, k - 1),
9233 (devs[1], k, n - 1),
9234 ]));
9235 return Ok(());
9236 }
9237 return Err(format!(
9238 "an explicit split point takes exactly 2 devices, got {}",
9239 devs.len()
9240 ));
9241 }
9242 let per = n.div_ceil(devs.len());
9243 let mut plan = Vec::with_capacity(devs.len());
9244 let mut from = 0usize;
9245 for &d in devs {
9246 if from >= n {
9247 break;
9248 }
9249 let upto = (from + per - 1).min(n - 1);
9250 plan.push((d, from, upto));
9251 from = upto + 1;
9252 }
9253 self.gpu_plan = Some(std::sync::Arc::new(plan));
9254 Ok(())
9255 }
9256
9257 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
9259 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
9260 }
9261
9262 fn forward_layers_span(
9268 &mut self,
9269 hidden: &[f32],
9270 position: usize,
9271 task_mask: Option<&TaskMask>,
9272 from: usize,
9273 upto: Option<usize>,
9274 ) -> Vec<f32> {
9275 debug_assert!(
9276 from == 0 || (self.dsv4.is_none() && self.qwen4_exp.is_none() && self.g3n.is_none())
9277 );
9278 if let Some(b) = &mut self.qwen4_exp {
9279 let _ = (task_mask, upto);
9280 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9281 let mut logits = Vec::new();
9282 crate::qwen4_exp::forward_token(
9283 &b.0,
9284 &b.1,
9285 &b.2,
9286 &mut b.3,
9287 token_id,
9288 position,
9289 &self.inv_freq,
9290 self.pool.as_deref(),
9291 &mut logits,
9292 true,
9293 );
9294 self.graph_logits = Some(logits);
9295 return vec![0.0; self.hidden_size];
9296 }
9297 if let Some(b) = &mut self.dsv4 {
9303 let _ = (task_mask, upto);
9304 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
9305 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
9306 st.pos = position;
9307 let mut logits = Vec::new();
9308 crate::dsv4::forward_token(
9309 g,
9310 layers,
9311 &cfg,
9312 st,
9313 token_id,
9314 &self.inv_freq,
9315 self.pool.as_deref(),
9316 &mut logits,
9317 );
9318 self.graph_logits = Some(logits);
9319 self.dspark_probe(position, token_id);
9320 return vec![0.0; self.hidden_size];
9323 }
9324 if let Some(b) = &self.g3n {
9327 let _ = (task_mask, upto);
9328 return crate::g3n::g3n_forward(
9329 &b.0,
9330 &b.1,
9331 hidden,
9332 position,
9333 &mut self.kv_cache.layers,
9334 self.num_heads,
9335 self.num_kv_heads,
9336 self.head_dim,
9337 self.pool.as_deref(),
9338 );
9339 }
9340 let mut h = hidden.to_vec();
9341 let (nh, _nkv, _hd, hs, _rd, eps) = (
9344 self.num_heads,
9345 self.num_kv_heads,
9346 self.head_dim,
9347 self.hidden_size,
9348 self.rotary_dim,
9349 self.rms_eps,
9350 );
9351 let pool = self.pool.clone();
9352 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
9364 let graph_on = match graph_env.as_deref() {
9365 Some("0") => false,
9366 Some("prefill") => false, Some(_) => true,
9368 None => crate::gpu::wgpu_graph_default(),
9374 };
9375 let graph_trusted =
9376 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
9377 let race_eligible = graph_on
9378 && upto.is_none()
9379 && task_mask.is_none()
9380 && from == 0
9381 && !crate::gpu::graph_unsupported();
9382 let mut tail_start = 0usize;
9383 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
9384 let t_graph = std::time::Instant::now();
9385 let mut lg = Vec::new();
9386 let mut gl = 0usize;
9387 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
9388 let declined = built.is_none();
9389 let built = match built {
9390 Some(Ok(hh)) => Some(hh),
9391 Some(Err(())) => {
9392 self.clear_sequence_state();
9396 self.graph_failed
9397 .store(true, std::sync::atomic::Ordering::Relaxed);
9398 self.cancel
9399 .store(true, std::sync::atomic::Ordering::Relaxed);
9400 tracing::error!("token graph failed after admission; sequence state cleared");
9401 return vec![0.0; self.hidden_size];
9402 }
9403 None => None,
9404 };
9405 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
9410 crate::gpu::graph_mark_unsupported();
9411 }
9412 graph_note(built.is_some(), gl, self.num_layers);
9413 if let Some(hh) = built {
9414 let dur = t_graph.elapsed();
9415 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9416 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
9417 }
9418 if gl > 0 && gl < self.num_layers {
9419 h = hh;
9425 tail_start = gl;
9426 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
9427 if !graph_trusted {
9428 crate::gpu::graph_race_record(true, dur);
9429 }
9430 if !lg.is_empty() {
9431 lg.resize(self.vocab_size, 0.0);
9434 if let Some(c) = self.final_softcap {
9435 for l in lg.iter_mut() {
9436 *l = c * (*l / c).tanh();
9437 }
9438 }
9439 self.graph_logits = Some(lg);
9440 }
9441 return hh;
9442 }
9443 }
9449 }
9450 let span = from > 0 || upto.is_some();
9474 if span && graph_on && task_mask.is_none() && graph_trusted {
9475 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
9476 let mut lg = Vec::new();
9477 let mut gl = 0usize;
9478 let span_res =
9479 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
9480 let span_res = match span_res {
9481 Some(Ok(hh)) => Some(hh),
9482 Some(Err(())) => {
9483 self.clear_sequence_state();
9484 self.graph_failed
9485 .store(true, std::sync::atomic::Ordering::Relaxed);
9486 self.cancel
9487 .store(true, std::sync::atomic::Ordering::Relaxed);
9488 tracing::error!(
9489 "span token graph failed after admission; sequence state cleared"
9490 );
9491 return vec![0.0; self.hidden_size];
9492 }
9493 None => None,
9494 };
9495 graph_note(span_res.is_some(), gl, upto_excl - from);
9496 if std::env::var("CMF_GPU_DEBUG").is_ok() {
9497 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
9501 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
9502 eprintln!(
9503 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
9504 upto_excl - from,
9505 span_res.is_some()
9506 );
9507 }
9508 }
9509 if let Some(hh) = span_res {
9510 if gl == upto_excl - from {
9511 if !lg.is_empty() {
9512 lg.resize(self.vocab_size, 0.0);
9513 if let Some(c) = self.final_softcap {
9514 for l in lg.iter_mut() {
9515 *l = c * (*l / c).tanh();
9516 }
9517 }
9518 self.graph_logits = Some(lg);
9519 }
9520 crate::gpu::set_layer(-1);
9521 return hh;
9522 }
9523 h = hh;
9525 tail_start = from + gl;
9526 }
9527 }
9528 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
9529
9530 let _host_tail = (tail_start > from).then(crate::gpu::enter_cpu_scope);
9536 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9537
9538 #[cfg(target_os = "macos")]
9539 let mut gpu_skip_until = 0usize;
9540 for li in tail_start.max(from)..self.num_layers {
9541 let _capacity_tail = automatic_gpu_prefix
9542 .filter(|&prefix| li >= prefix)
9543 .map(|_| crate::gpu::enter_cpu_scope());
9544 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
9546 if li > u {
9547 break;
9548 }
9549 }
9550 if let Some(mask) = task_mask {
9551 if !mask.layer_alive(li) {
9552 continue; }
9554 }
9555 #[cfg(target_os = "macos")]
9559 {
9560 if li < gpu_skip_until {
9561 continue;
9562 }
9563 if task_mask.is_none() {
9564 let end = self.q1_graph_gpu(li, upto, position, &mut h);
9565 if end > li {
9566 gpu_skip_until = end;
9567 if self.is_loop_end(end - 1) && end < self.num_layers {
9570 h = inference::rms_norm(
9571 &h,
9572 &self.weights.final_norm,
9573 self.rms_eps,
9574 self.norm_style,
9575 );
9576 }
9577 continue;
9578 }
9579 }
9580 }
9581
9582 let lw = &self.weights.layers[self.phys_layer(li)];
9583 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9584 if tp.parse::<usize>().ok() == Some(position) {
9585 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
9586 eprintln!(
9587 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
9588 h[0], h[1]
9589 );
9590 }
9591 }
9592 inference::rms_norm_into(
9595 &h,
9596 &lw.input_norm,
9597 self.rms_eps,
9598 self.norm_style,
9599 &mut self.ws.n1,
9600 );
9601
9602 let attn_out = match &lw.attn {
9603 AttnKind::Mla(w) => {
9604 let inv_freq_l = self.layer_inv_freq(li);
9605 let rs = self.layer_rope_scale(li);
9606 let eps = self.rms_eps;
9607 let pool = self.pool.clone();
9608 mla_attention(
9609 w,
9610 &self.ws.n1,
9611 &mut self.kv_cache.layers[li],
9612 position,
9613 &inv_freq_l,
9614 rs,
9615 eps,
9616 pool.as_deref(),
9617 )
9618 }
9619 AttnKind::Linear(w) => {
9620 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
9621 vmf_phase_forward(
9622 &self.ws.n1,
9623 w,
9624 &cfg,
9625 &mut self.kv_cache.layers[li].linear_state,
9626 self.pool.as_deref(),
9627 )
9628 }
9629 AttnKind::Kda(w) => {
9630 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9631 crate::linear_core::kda_forward(
9632 &self.ws.n1,
9633 w,
9634 &cfg,
9635 &mut self.kv_cache.layers[li].linear_state,
9636 self.pool.as_deref(),
9637 )
9638 }
9639 AttnKind::LinearGdn(w) => {
9640 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9641 gdn_forward(
9642 &self.ws.n1,
9643 w,
9644 &cfg,
9645 &mut self.kv_cache.layers[li].linear_state,
9646 self.pool.as_deref(),
9647 )
9648 }
9649 AttnKind::ShortConv(w) => {
9650 let cfg = self
9651 .short_conv_cfg
9652 .expect("short-conv layer without short_conv_cfg");
9653 short_conv_forward(
9654 &self.ws.n1,
9655 w,
9656 &cfg,
9657 &mut self.kv_cache.layers[li].linear_state,
9658 self.pool.as_deref(),
9659 )
9660 }
9661 AttnKind::Full {
9662 wq,
9663 wk,
9664 wv,
9665 wo,
9666 q_norm,
9667 k_norm,
9668 output_gate,
9669 softplus_gate,
9670 bias,
9671 } if self.kv_cache.layers[li].o1_sealed() => {
9672 let inv_freq_l = self.layer_inv_freq(li);
9675 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9676 let cfg = QwenAttnCfg {
9677 num_heads: self.layer_num_heads(li),
9678 num_kv_heads: nkv_l,
9679 head_dim: hd_l,
9680 hidden_size: hs,
9681 position,
9682 inv_freq: &inv_freq_l,
9683 rotary_dim: rd_l,
9684 scale: self.attn_scale,
9685 softcap: self.attn_softcap,
9686 window: None,
9687 v_norm: self.attn_v_norm,
9688 q_norm: q_norm.as_deref(),
9689 k_norm: k_norm.as_deref(),
9690 output_gate: *output_gate,
9691 softplus_gate: softplus_gate
9692 .as_ref()
9693 .map(|(gate, per_head)| (gate, *per_head)),
9694 rope_scale: self.layer_rope_scale(li),
9695 bias: bias
9696 .as_ref()
9697 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9698 rms_eps: eps,
9699 norm_style: self.norm_style,
9700 pool: pool.as_deref(),
9701 };
9702 attention::qwen_attention_nystrom(
9703 &self.ws.n1,
9704 wq,
9705 wk,
9706 wv,
9707 wo,
9708 &mut self.kv_cache.layers[li],
9709 &cfg,
9710 )
9711 }
9712 AttnKind::Full {
9713 wq,
9714 wk,
9715 wv,
9716 wo,
9717 q_norm,
9718 k_norm,
9719 output_gate,
9720 softplus_gate,
9721 bias,
9722 } => 'attn: {
9723 if graph_on
9726 && !*output_gate
9727 && softplus_gate.is_none()
9728 && self.attention_heads_per_layer.is_none()
9729 && bias.is_none()
9730 && task_mask.is_none()
9731 {
9732 let inv_freq_l = self.layer_inv_freq(li);
9733 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9734 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9735 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
9736 wq.mapped_q1(),
9737 wk.mapped_q1(),
9738 wv.mapped_q1(),
9739 wo.mapped_q1(),
9740 ) {
9741 let gm = gm.clone();
9742 let mut out = vec![0f32; hs];
9743 let cache = &self.kv_cache.layers[li];
9744 if crate::gpu::attn_dropin(
9745 &gm,
9746 self.graph_kv_id,
9747 li,
9748 &self.ws.n1,
9749 qi,
9750 ki,
9751 vi,
9752 oi,
9753 q_norm.as_deref(),
9754 k_norm.as_deref(),
9755 &inv_freq_l,
9756 nh,
9757 nkv_l,
9758 hd_l,
9759 rd_l,
9760 hs,
9761 position,
9762 self.kv_cache.max_seq_len,
9763 gemma,
9764 eps as f32,
9765 cache.k_heads(),
9766 cache.v_heads(),
9767 &mut out,
9768 ) {
9769 break 'attn out;
9770 }
9771 }
9772 }
9773 let masked = task_mask
9774 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
9775 .unwrap_or(false);
9776 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
9777 match (masked, f32_view) {
9778 (true, (Some(q), Some(k), Some(v), Some(o))) => {
9781 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
9782 attention::multi_head_attention(
9783 &self.ws.n1,
9784 q,
9785 k,
9786 v,
9787 o,
9788 &mut self.kv_cache.layers[li],
9789 self.num_heads,
9790 self.num_kv_heads,
9791 self.head_dim,
9792 self.hidden_size,
9793 position,
9794 &active_heads,
9795 &self.inv_freq,
9796 )
9797 }
9798 (masked, _) => {
9799 if masked {
9800 tracing::warn!(
9801 "layer {li}: head mask on quantized weights not \
9802 supported yet — executing dense"
9803 );
9804 }
9805 let inv_freq_l = self.layer_inv_freq(li);
9806 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9807 let cfg = QwenAttnCfg {
9808 num_heads: self.layer_num_heads(li),
9809 num_kv_heads: nkv_l,
9810 head_dim: hd_l,
9811 hidden_size: hs,
9812 position,
9813 inv_freq: &inv_freq_l,
9814 rotary_dim: rd_l,
9815 scale: self.attn_scale,
9816 softcap: self.attn_softcap,
9817 window: self.layer_window(li),
9818 v_norm: self.attn_v_norm,
9819 q_norm: q_norm.as_deref(),
9820 k_norm: k_norm.as_deref(),
9821 output_gate: *output_gate,
9822 softplus_gate: softplus_gate
9823 .as_ref()
9824 .map(|(gate, per_head)| (gate, *per_head)),
9825 rope_scale: self.layer_rope_scale(li),
9826 bias: bias
9827 .as_ref()
9828 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9829 rms_eps: eps,
9830 norm_style: self.norm_style,
9831 pool: pool.as_deref(),
9832 };
9833 attention::qwen_attention(
9834 &self.ws.n1,
9835 wq,
9836 wk,
9837 wv,
9838 wo,
9839 &mut self.kv_cache.layers[li],
9840 &cfg,
9841 )
9842 }
9843 }
9844 }
9845 };
9846 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
9849 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
9850 None => attn_out,
9851 };
9852 let lw = &self.weights.layers[self.phys_layer(li)];
9853 inference::add_rmsnorm_fused_into(
9854 &mut h,
9855 &attn_out,
9856 &lw.post_norm,
9857 self.rms_eps,
9858 self.norm_style,
9859 &mut self.ws.p1,
9860 );
9861 let mut attn_out = attn_out;
9862 attention::recycle_buf(&mut attn_out);
9863 let post_normed = &self.ws.p1;
9864
9865 let ffn_masked = task_mask
9866 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
9867 .unwrap_or(false);
9868 let ffn_out = match (ffn_masked, &lw.ffn) {
9880 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
9884 let row = task_mask
9885 .and_then(|tm| tm.ffn_masks.get(li))
9886 .map(|v| v.as_slice());
9887 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
9888 }
9889 (true, FfnKind::Dense(d)) => {
9890 let tm = task_mask.unwrap();
9891 let alive = tm.ffn_active_count(li);
9892 let deep = alive * 2 <= self.intermediate_size;
9893 if deep && d.down_proj.sparse_col_ok() {
9894 let active = tm.ffn_active_indices(li);
9895 sparse_ffn_quant(
9896 d,
9897 post_normed,
9898 &active,
9899 self.hidden_size,
9900 self.pool.as_deref(),
9901 )
9902 } else if deep
9903 && let (Some(g), Some(u), Some(dn)) = (
9904 d.gate_proj.as_f32(),
9905 d.up_proj.as_f32(),
9906 d.down_proj.as_f32(),
9907 )
9908 {
9909 let active = tm.ffn_active_indices(li);
9910 inference::sparse_ffn_forward(
9911 post_normed,
9912 g,
9913 u,
9914 dn,
9915 self.hidden_size,
9916 self.intermediate_size,
9917 &active,
9918 self.pool.as_deref(),
9919 )
9920 } else {
9921 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
9922 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
9923 }
9924 }
9925 (true, FfnKind::Moe(m)) => {
9926 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
9930 ffn_forward(
9931 &lw.ffn,
9932 post_normed,
9933 self.pool.as_deref(),
9934 allowed.as_deref(),
9935 )
9936 }
9937 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
9938 dm,
9939 post_normed,
9940 &h,
9941 self.rms_eps,
9942 self.norm_style,
9943 self.pool.as_deref(),
9944 ),
9945 (false, _) => match &lw.ffn {
9946 FfnKind::DenseMoe(dm) => dense_moe_ffn(
9947 dm,
9948 post_normed,
9949 &h,
9950 self.rms_eps,
9951 self.norm_style,
9952 self.pool.as_deref(),
9953 ),
9954 _ => {
9955 let allowed = match (&lw.ffn, task_mask) {
9956 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
9957 _ => None,
9958 };
9959 ffn_forward(
9960 &lw.ffn,
9961 post_normed,
9962 self.pool.as_deref(),
9963 allowed.as_deref(),
9964 )
9965 }
9966 },
9967 };
9968 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9969 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
9970 None => ffn_out,
9971 };
9972 for (i, &f) in ffn_out.iter().enumerate() {
9973 h[i] += f;
9974 }
9975 let mut ffn_out = ffn_out;
9976 attention::recycle_buf(&mut ffn_out);
9977
9978 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9980 for v in h.iter_mut() {
9981 *v *= sc;
9982 }
9983 }
9984
9985 if self.is_loop_end(li) && li + 1 < self.num_layers {
9988 h = inference::rms_norm(
9989 &h,
9990 &self.weights.final_norm,
9991 self.rms_eps,
9992 self.norm_style,
9993 );
9994 }
9995
9996 if self.dyn_phi_layer == Some(li) {
10000 self.update_dyn_phi(&h);
10001 }
10002 }
10003 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
10005 crate::gpu::graph_race_record(false, t.elapsed());
10006 }
10007
10008 h
10009 }
10010
10011 fn update_dyn_phi(&mut self, h: &[f32]) {
10014 const A: f32 = 0.2;
10015 if self.dyn_phi_ema.len() != h.len() {
10016 self.dyn_phi_ema = vec![0.0; h.len()];
10017 self.dyn_phi_seen = 0;
10018 }
10019 if self.dyn_phi_seen == 0 {
10020 self.dyn_phi_ema.copy_from_slice(h);
10021 } else {
10022 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
10023 *e = (1.0 - A) * *e + A * v;
10024 }
10025 }
10026 self.dyn_phi_seen += 1;
10027 }
10028
10029 pub fn dyn_phi(&self) -> &[f32] {
10031 &self.dyn_phi_ema
10032 }
10033
10034 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
10036 self.dyn_phi_layer = layer;
10037 self.dyn_phi_ema.clear();
10038 self.dyn_phi_seen = 0;
10039 }
10040
10041 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
10043 let Some(model) = &self.model else {
10044 return Vec::new();
10045 };
10046 model
10047 .header
10048 .skills
10049 .iter()
10050 .enumerate()
10051 .filter_map(|(i, sk)| {
10052 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
10053 let sel = sk.selection.as_ref()?;
10054 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
10055 })
10056 .collect()
10057 }
10058
10059 pub fn active_skill(&self) -> Option<usize> {
10061 self.dyn_active
10062 }
10063
10064 pub fn enable_dynamic_routing(&mut self) -> usize {
10069 use crate::swarm::{DynRouter, RoutableSkill};
10070 let Some(model) = self.model.clone() else {
10071 return 0;
10072 };
10073 if self.dyn_blend_loaded {
10076 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
10077 return 0;
10078 }
10079 if let Some(a) = self.dyn_active {
10083 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
10084 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
10085 return 0;
10086 }
10087 }
10088 let hidden = self.hidden_size;
10089 let mut skills = Vec::new();
10090 for (idx, id, _phi) in self.dynamic_skills() {
10091 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
10092 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
10093 skills.push(rs);
10094 }
10095 }
10096 }
10097 if skills.is_empty() {
10098 return 0;
10099 }
10100 let phi = skills[0].phi_layer;
10102 if skills.iter().any(|s| s.phi_layer != phi) {
10103 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
10104 }
10105 let n = skills.len();
10106 self.set_dyn_phi_layer(Some(phi));
10107 self.dyn_router = Some(DynRouter::new(skills));
10108 n
10109 }
10110
10111 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
10113 self.dyn_router
10114 .as_ref()
10115 .map(|r| r.switches.clone())
10116 .unwrap_or_default()
10117 }
10118
10119 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
10122 let rows = self.weights.lm_head.rows();
10123 let mut logits = attention::take_buf(rows.min(self.vocab_size));
10124 self.weights
10125 .lm_head
10126 .matvec(hidden, &mut logits, self.pool.as_deref());
10127 logits.resize(self.vocab_size, 0.0);
10128 if let Some(m) = self.logit_multiplier {
10129 for l in logits.iter_mut() {
10130 *l *= m;
10131 }
10132 }
10133 if let Some(c) = self.final_softcap {
10134 for l in logits.iter_mut() {
10135 *l = c * (*l / c).tanh();
10136 }
10137 }
10138 if let Some(cm) = self.head_clusters.as_ref() {
10139 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
10140 }
10141 logits
10142 }
10143
10144 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
10147 let h = hidden.len();
10148 let ncl = cm.len() / h.max(1);
10149 if ncl == 0 || logits.len() % ncl != 0 {
10150 return;
10151 }
10152 let cs = logits.len() / ncl;
10153 let mut lc = vec![0.0f32; ncl];
10155 for c in 0..ncl {
10156 let row = &cm[c * h..(c + 1) * h];
10157 let mut s = 0.0f32;
10158 for j in 0..h {
10159 s += row[j] * hidden[j];
10160 }
10161 lc[c] = s;
10162 }
10163 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10164 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
10165 for c in 0..ncl {
10166 let blk = &mut logits[c * cs..(c + 1) * cs];
10167 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10168 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
10169 let add = lc[c] - lse - bl;
10170 for v in blk.iter_mut() {
10171 *v += add;
10172 }
10173 }
10174 }
10175
10176 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
10181 self.clear_sequence_state();
10182 crate::gpu::graph_race_begin_generation();
10186 self.o1_begin();
10187 let mut hidden = vec![0.0f32; self.hidden_size];
10188 for (pos, &id) in ids.iter().enumerate() {
10189 let emb = self.embed_single(id);
10190 hidden = self.forward_layers(&emb, pos, task_mask);
10191 }
10192 inference::rms_norm_into(
10193 &hidden,
10194 &self.weights.final_norm,
10195 self.rms_eps,
10196 self.norm_style,
10197 &mut self.ws.n1,
10198 );
10199 self.lm_head_forward(&self.ws.n1)
10200 }
10201}
10202
10203pub fn create_test_pipeline(
10205 hidden_size: usize,
10206 intermediate_size: usize,
10207 num_heads: usize,
10208 num_kv_heads: usize,
10209 head_dim: usize,
10210 num_layers: usize,
10211 vocab_size: usize,
10212) -> Pipeline {
10213 let synth = |n: usize, salt: usize| -> Vec<f32> {
10216 (0..n)
10217 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
10218 .collect()
10219 };
10220 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
10221 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
10222 };
10223 let layer_weights: Vec<LayerWeights> = (0..num_layers)
10224 .map(|li| LayerWeights {
10225 input_norm: vec![1.0; hidden_size],
10226 post_norm: vec![1.0; hidden_size],
10227 attn_out_norm: None,
10228 ffn_out_norm: None,
10229 layer_scale: None,
10230 ffn: FfnKind::Dense(DenseFfn {
10231 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
10232 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
10233 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
10234 act: Act::Silu,
10235 down_t: None,
10236 segs: Vec::new(),
10237 }),
10238 attn: AttnKind::Full {
10239 bias: None,
10240 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
10241 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
10242 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
10243 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
10244 q_norm: None,
10245 k_norm: None,
10246 output_gate: false,
10247 softplus_gate: None,
10248 },
10249 })
10250 .collect();
10251
10252 Pipeline::new(
10253 Tokenizer::byte_level(),
10254 PipelineWeights {
10255 embed_tokens: qt(vocab_size, hidden_size, 100),
10256 layers: layer_weights,
10257 lm_head: qt(vocab_size, hidden_size, 200),
10258 final_norm: vec![1.0; hidden_size],
10259 },
10260 hidden_size,
10261 intermediate_size,
10262 num_heads,
10263 num_kv_heads,
10264 head_dim,
10265 num_layers,
10266 num_layers, false, vocab_size,
10269 1e-6,
10270 10_000.0,
10271 NormStyle::Qwen,
10272 4096,
10273 SamplerConfig {
10274 seed: Some(42),
10275 ..Default::default()
10276 },
10277 )
10278}
10279
10280#[inline]
10285fn mask_bit(row: &[u8], j: usize) -> bool {
10286 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
10287}
10288
10289fn mask_gain() -> f32 {
10300 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
10301 *G.get_or_init(|| {
10302 std::env::var("CMF_FFN_MASK_GAIN")
10303 .ok()
10304 .and_then(|v| v.parse().ok())
10305 .unwrap_or(1.0)
10306 })
10307}
10308
10309fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
10310 let fill = meanfill().and_then(|(i, v)| {
10313 let li = crate::gpu::cur_layer();
10314 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
10315 });
10316 for r in 0..rows {
10317 let base = r * inter;
10318 for (bi, &byte) in row.iter().enumerate() {
10319 if byte == 0xFF {
10320 continue;
10321 }
10322 let j0 = bi * 8;
10323 for bit in 0..8 {
10324 let j = j0 + bit;
10325 if j < inter && byte & (1 << bit) == 0 {
10326 g[base + j] = fill.map_or(0.0, |f| f[j]);
10327 }
10328 }
10329 }
10330 }
10331 let gain = mask_gain();
10332 if gain != 1.0 {
10333 for v in g[..rows * inter].iter_mut() {
10334 *v *= gain;
10335 }
10336 }
10337}
10338
10339#[inline]
10341fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
10342 row.is_none_or(|r| mask_bit(r, i))
10343}
10344
10345fn all_bits_on(row: &[u8], n: usize) -> bool {
10348 (0..n).all(|i| mask_bit(row, i))
10349}
10350
10351fn tube_topk() -> usize {
10359 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10360 *K.get_or_init(|| {
10361 std::env::var("CMF_TUBE_TOPK")
10362 .ok()
10363 .and_then(|v| v.parse().ok())
10364 .unwrap_or(0)
10365 })
10366}
10367
10368fn tube_score_oracle() -> bool {
10369 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10370 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
10371}
10372
10373fn tube_ffn_routed(
10380 d: &DenseFfn,
10381 xs: &[f32],
10382 b: usize,
10383 pool: Option<&Pool>,
10384 mask_row: Option<&[u8]>,
10385 k: usize,
10386) -> Vec<f32> {
10387 let hidden = d.down_proj.rows();
10388 let core = d.gate_proj.rows();
10389 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
10390 let mut out = match (b, core_full, mask_row) {
10391 (1, true, _) => dense_ffn(d, xs, pool),
10392 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
10393 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
10394 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
10395 };
10396 let cand: Vec<usize> = (0..d.segs.len())
10397 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
10398 .collect();
10399 if cand.is_empty() {
10400 return out;
10401 }
10402 let oracle = tube_score_oracle();
10406 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
10407 let mut scores = vec![0f32; b * cand.len()];
10408 for (ci, &i) in cand.iter().enumerate() {
10409 let seg = &d.segs[i];
10410 let w = seg.width;
10411 let mut g = vec![0.0f32; b * w];
10412 if b == 1 {
10413 seg.gate.matvec(xs, &mut g, pool);
10414 } else {
10415 seg.gate.matmat(xs, b, &mut g, pool);
10416 }
10417 for v in g.iter_mut() {
10418 *v = Act::Silu.combine(*v, 1.0);
10419 }
10420 if !oracle {
10421 for t in 0..b {
10422 scores[t * cand.len() + ci] =
10423 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
10424 }
10425 }
10426 if oracle || b > 1 {
10427 let mut u = vec![0.0f32; b * w];
10428 if b == 1 {
10429 seg.up.matvec(xs, &mut u, pool);
10430 } else {
10431 seg.up.matmat(xs, b, &mut u, pool);
10432 }
10433 for (a, &v) in g.iter_mut().zip(u.iter()) {
10434 *a *= v;
10435 }
10436 if oracle {
10437 for t in 0..b {
10438 scores[t * cand.len() + ci] =
10439 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
10440 }
10441 }
10442 }
10443 acts.push(g);
10444 }
10445 let keep = k.min(cand.len());
10447 let mut scratch: Vec<f32> = Vec::new();
10448 for t in 0..b {
10449 let mut sc: Vec<(f32, usize)> = (0..cand.len())
10450 .map(|ci| (scores[t * cand.len() + ci], ci))
10451 .collect();
10452 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
10453 let mut alive = vec![false; cand.len()];
10454 for &(_, ci) in sc.iter().take(keep) {
10455 alive[ci] = true;
10456 }
10457 if b > 1 {
10458 for (ci, a) in acts.iter_mut().enumerate() {
10459 if !alive[ci] {
10460 let w = d.segs[cand[ci]].width;
10461 a[t * w..(t + 1) * w].fill(0.0);
10462 }
10463 }
10464 } else {
10465 for (ci, &i) in cand.iter().enumerate() {
10469 if !alive[ci] {
10470 continue;
10471 }
10472 let seg = &d.segs[i];
10473 let w = seg.width;
10474 let g = &mut acts[ci];
10475 if !tube_score_oracle() {
10476 scratch.clear();
10477 scratch.resize(w, 0.0);
10478 seg.up.matvec(xs, &mut scratch, pool);
10479 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
10480 *a *= v;
10481 }
10482 }
10483 let mut acc = vec![0.0f32; hidden];
10484 seg.down.matvec(g, &mut acc, pool);
10485 for (o, a) in out.iter_mut().zip(&acc) {
10486 *o += *a;
10487 }
10488 }
10489 }
10490 }
10491 if b > 1 {
10492 for (ci, &i) in cand.iter().enumerate() {
10493 let seg = &d.segs[i];
10494 let mut acc = vec![0.0f32; b * hidden];
10495 seg.down.matmat(&acts[ci], b, &mut acc, pool);
10496 for (o, a) in out.iter_mut().zip(&acc) {
10497 *o += *a;
10498 }
10499 }
10500 }
10501 out
10502}
10503
10504fn tube_ffn(
10510 d: &DenseFfn,
10511 xs: &[f32],
10512 b: usize,
10513 pool: Option<&Pool>,
10514 mask_row: Option<&[u8]>,
10515) -> Vec<f32> {
10516 if tube_topk() > 0 {
10517 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
10518 }
10519 let hidden = d.down_proj.rows();
10520 let core = d.gate_proj.rows();
10521 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
10522 let mut out = match (b, core_full, mask_row) {
10523 (1, true, _) => dense_ffn(d, xs, pool),
10524 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
10525 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
10526 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
10527 };
10528 TUBE_SCRATCH.with(|sc| {
10529 let mut sc = sc.borrow_mut();
10530 let [g, u, acc] = &mut *sc;
10531 for seg in &d.segs {
10532 if !tube_bit(mask_row, seg.start) {
10533 continue;
10534 }
10535 let w = seg.width;
10536 g.resize(b * w, 0.0);
10537 if b == 1
10538 && d.act == Act::Silu
10539 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
10540 {
10541 } else {
10543 u.resize(b * w, 0.0);
10544 if b == 1 {
10545 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
10546 } else {
10547 seg.gate.matmat(xs, b, g, pool);
10548 seg.up.matmat(xs, b, u, pool);
10549 }
10550 for i in 0..b * w {
10551 g[i] = d.act.combine(g[i], u[i]);
10552 }
10553 }
10554 acc.resize(b * hidden, 0.0);
10555 acc.fill(0.0);
10556 if b == 1 {
10557 seg.down.matvec(g, acc, pool);
10558 } else {
10559 seg.down.matmat(g, b, acc, pool);
10560 }
10561 for (o, a) in out.iter_mut().zip(acc.iter()) {
10562 *o += *a;
10563 }
10564 }
10565 out
10566 })
10567}
10568
10569thread_local! {
10570 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
10574 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
10575}
10576
10577fn dense_ffn_batch(
10578 d: &DenseFfn,
10579 xs: &[f32],
10580 b: usize,
10581 pool: Option<&Pool>,
10582 mask_row: Option<&[u8]>,
10583) -> Vec<f32> {
10584 let inter = d.gate_proj.rows();
10585 let hidden = d.down_proj.rows();
10586 if mask_row.is_none()
10594 && d.act == Act::Silu
10595 && b >= 32
10596 && crate::gpu::enabled_here()
10597 && !crate::gpu::mm_killed()
10598 && refit_dir().is_none()
10603 && !ffn_probe_active()
10608 {
10609 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
10610 d.gate_proj.mapped_q4t(),
10611 d.up_proj.mapped_q4t(),
10612 d.down_proj.mapped_q4t(),
10613 ) {
10614 let mut out = vec![0.0f32; b * hidden];
10615 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
10616 return out;
10617 }
10618 }
10619 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
10624 d.gate_proj.mapped_q4tp(),
10625 d.up_proj.mapped_q4tp(),
10626 d.down_proj.mapped_q4tp(),
10627 ) {
10628 let mut out = vec![0.0f32; b * hidden];
10629 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
10630 return out;
10631 }
10632 }
10633 }
10634 let mut g = vec![0.0f32; b * inter];
10635 d.gate_proj.matmat(xs, b, &mut g, pool);
10636 let mut u = vec![0.0f32; b * inter];
10637 d.up_proj.matmat(xs, b, &mut u, pool);
10638 if gate_topk() > 0 && d.act == Act::Silu {
10639 for t in 0..b {
10640 let row = &mut g[t * inter..(t + 1) * inter];
10641 for v in row.iter_mut() {
10642 *v = Act::Silu.combine(*v, 1.0);
10643 }
10644 keep_top_k(row, gate_topk());
10645 }
10646 for i in 0..b * inter {
10647 g[i] *= u[i];
10648 }
10649 } else {
10650 for i in 0..b * inter {
10651 g[i] = d.act.combine(g[i], u[i]);
10652 }
10653 }
10654 if let Some(row) = mask_row {
10655 zero_masked_cols(&mut g, b, inter, row);
10656 }
10657 if oracle_topk() > 0 {
10658 for t in 0..b {
10659 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
10660 }
10661 }
10662 let mut out = vec![0.0f32; b * hidden];
10663 d.down_proj.matmat(&g, b, &mut out, pool);
10664 if refit_dir().is_some() {
10665 let li = crate::gpu::cur_layer();
10666 if li >= 0 {
10667 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
10668 }
10669 }
10670 FFN_PROBE.with(|pr| {
10674 if let Some(acc) = pr.borrow_mut().as_mut() {
10675 let li = crate::gpu::cur_layer();
10676 if li < 0 {
10677 return;
10678 }
10679 let Some(row) = acc.get_mut(li as usize) else {
10680 return;
10681 };
10682 let sq = probe_sq();
10683 for t in 0..b {
10684 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
10685 *a += if sq {
10686 (v as f64) * (v as f64)
10687 } else {
10688 (v as f64).abs()
10689 };
10690 }
10691 }
10692 }
10693 });
10694 out
10695}
10696
10697fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
10702 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10703 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10704 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
10705 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
10706 if (!on && !dump) || b == 0 {
10707 return;
10708 }
10709 let hidden = xs.len() / b;
10710 if on {
10711 let mut acc = m.act_sq.borrow_mut();
10712 if acc.len() < hidden {
10713 acc.resize(hidden, 0.0);
10714 }
10715 for t in 0..b {
10716 let row = &xs[t * hidden..(t + 1) * hidden];
10717 for (a, &v) in acc.iter_mut().zip(row) {
10718 *a += (v as f64) * (v as f64);
10719 }
10720 }
10721 }
10722 if dump {
10723 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
10726 .ok()
10727 .and_then(|v| v.parse().ok())
10728 .unwrap_or(4096);
10729 let mut rows = m.act_rows.borrow_mut();
10730 if rows.len() < cap * hidden {
10731 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
10732 rows.extend_from_slice(&xs[..take * hidden]);
10733 }
10734 }
10735}
10736
10737#[derive(Clone, Copy)]
10740struct SendVecs(*mut Vec<f32>);
10741unsafe impl Send for SendVecs {}
10742unsafe impl Sync for SendVecs {}
10743impl SendVecs {
10744 #[inline]
10745 fn at(self, i: usize) -> *mut Vec<f32> {
10746 unsafe { self.0.add(i) }
10747 }
10748}
10749
10750fn moe_ffn_batch(
10751 m: &MoeFfn,
10752 xs: &[f32],
10753 b: usize,
10754 hidden: usize,
10755 pool: Option<&Pool>,
10756 allowed: Option<&[bool]>,
10757) -> Vec<f32> {
10758 accumulate_act(m, xs, b);
10759 let ne = m.experts.len();
10760 let mut logits = vec![0.0f32; b * ne];
10761 match &m.resonance {
10762 Some(r) => {
10763 let hdim = xs.len() / b.max(1);
10764 for bi in 0..b {
10765 r.scores(
10766 &xs[bi * hdim..(bi + 1) * hdim],
10767 &mut logits[bi * ne..(bi + 1) * ne],
10768 );
10769 }
10770 }
10771 None => m.router.matmat(xs, b, &mut logits, pool),
10772 }
10773
10774 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
10777 {
10778 let mut st = m.stats.borrow_mut();
10779 if st.len() < ne {
10780 st.resize(ne, 0);
10781 }
10782 for bi in 0..b {
10783 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
10784 for &e in &idx {
10785 st[e] += 1;
10786 assign[e].push((bi, p[e] / wsum));
10787 }
10788 }
10789 }
10790
10791 let mut out = vec![0.0f32; b * hidden];
10792 let cols = m.experts[0].gate_proj.cols();
10793 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
10794 let sb = list.len();
10795 let mut sub = vec![0.0f32; sb * cols];
10796 for (k, &(bi, _)) in list.iter().enumerate() {
10797 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
10798 }
10799 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
10800 for (k, &(bi, w)) in list.iter().enumerate() {
10801 for i in 0..hidden {
10802 out[bi * hidden + i] += w * eo[k * hidden + i];
10803 }
10804 }
10805 };
10806 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
10812 if pool.is_some() && active.len() >= 8 {
10813 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
10814 {
10815 let panel_ptr = SendVecs(panels.as_mut_ptr());
10816 let experts = &m.experts;
10819 let (active_r, assign_r) = (&active, &assign);
10820 let run = |start: usize, end: usize| {
10821 for ai in start..end {
10822 let e = active_r[ai];
10823 let list = &assign_r[e];
10824 let sb = list.len();
10825 let mut sub = vec![0.0f32; sb * cols];
10826 for (k, &(bi, _)) in list.iter().enumerate() {
10827 sub[k * cols..(k + 1) * cols]
10828 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
10829 }
10830 unsafe {
10832 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
10833 }
10834 }
10835 };
10836 match pool {
10837 Some(p) => p.run_rows(active.len(), &run),
10838 None => run(0, active.len()),
10839 }
10840 }
10841 for (ai, &e) in active.iter().enumerate() {
10842 for (k, &(bi, w)) in assign[e].iter().enumerate() {
10843 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
10844 for i in 0..hidden {
10845 out[bi * hidden + i] += w * eo[i];
10846 }
10847 }
10848 }
10849 } else {
10850 for &e in &active {
10851 run_expert(&m.experts[e], &assign[e], &mut out);
10852 }
10853 }
10854 if let Some((se, gate)) = &m.shared {
10855 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
10856 let mut gl = vec![0.0f32; b];
10857 gate.matmat(xs, b, &mut gl, pool);
10858 (0..b)
10859 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
10860 .collect()
10861 } else {
10862 (0..b).map(|bi| (bi, 1.0)).collect()
10863 };
10864 run_expert(se, &all, &mut out);
10865 }
10866 out
10867}
10868
10869thread_local! {
10870 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
10874 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
10875}
10876
10877fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
10879 if gate_topk() > 0
10882 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
10883 {
10884 return out;
10885 }
10886 if crate::gpu::enabled_here()
10897 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
10898 {
10899 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
10900 crate::gpu::ProbeArm::Gpu
10901 } else {
10902 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
10903 };
10904 match arm {
10905 crate::gpu::ProbeArm::Gpu => {
10906 let t0 = std::time::Instant::now();
10907 if let Some(out) = dense_ffn_gpu(d, x, pool) {
10908 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
10909 return out;
10910 }
10911 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
10915 }
10916 crate::gpu::ProbeArm::CpuTimed => {
10917 let t0 = std::time::Instant::now();
10918 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
10919 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
10920 return out;
10921 }
10922 crate::gpu::ProbeArm::Cpu => {
10923 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
10924 }
10925 }
10926 }
10927 dense_ffn_cpu(d, x, pool)
10928}
10929
10930fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
10932 let inter = d.gate_proj.rows();
10933 FFN_SCRATCH.with(|s| {
10934 let mut s = s.borrow_mut();
10935 let [g, u, ..] = &mut *s;
10936 g.resize(inter, 0.0);
10937 if gate_topk() > 0 {
10940 u.resize(inter, 0.0);
10944 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
10945 for i in 0..inter {
10946 g[i] = Act::Silu.combine(g[i], 1.0);
10947 }
10948 keep_top_k(g, gate_topk());
10949 for i in 0..inter {
10950 g[i] *= u[i];
10951 }
10952 } else if d.act == Act::Silu
10953 && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
10954 {
10955 } else {
10957 u.resize(inter, 0.0);
10958 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
10960 for i in 0..inter {
10961 g[i] = d.act.combine(g[i], u[i]);
10962 }
10963 }
10964 FFN_PROBE.with(|pr| {
10972 if let Some(acc) = pr.borrow_mut().as_mut() {
10973 let li = crate::gpu::cur_layer();
10974 if li >= 0 {
10975 if let Some(row) = acc.get_mut(li as usize) {
10976 match probe_topk() {
10977 0 if probe_sq() => {
10978 for (a, &v) in row.iter_mut().zip(g.iter()) {
10979 *a += (v as f64) * (v as f64);
10980 }
10981 }
10982 0 if probe_signed() => {
10983 for (a, &v) in row.iter_mut().zip(g.iter()) {
10984 *a += v as f64;
10985 }
10986 }
10987 0 => {
10988 for (a, &v) in row.iter_mut().zip(g.iter()) {
10989 *a += (v as f64).abs();
10990 }
10991 }
10992 k => {
10993 let n = g.len();
10994 let k = k.min(n);
10995 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
10996 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
10997 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10998 });
10999 let thr = *kth;
11000 for (a, &v) in row.iter_mut().zip(g.iter()) {
11001 if v.abs() >= thr {
11002 *a += 1.0;
11003 }
11004 }
11005 }
11006 }
11007 }
11008 }
11009 }
11010 });
11011 if oracle_topk() > 0 {
11012 keep_top_k(g, oracle_topk());
11013 }
11014 {
11015 let li = crate::gpu::cur_layer();
11016 if li >= 0 {
11017 adump_row(li as usize, g);
11018 }
11019 }
11020 let mut out = attention::take_buf(d.down_proj.rows());
11021 d.down_proj.matvec(g, &mut out, pool);
11022 out
11023 })
11024}
11025
11026pub struct RefitAcc {
11039 pub support: Vec<u32>,
11040 pub gss: Vec<f32>,
11041 pub ya: Vec<f32>,
11042 pub hidden: usize,
11043 pub tokens: u64,
11044 pub buf_g: Vec<f32>,
11050 pub buf_o: Vec<f32>,
11051 pub buf_t: usize,
11052}
11053
11054type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
11058
11059static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
11060 std::sync::OnceLock::new();
11061
11062fn ffn_probe_active() -> bool {
11065 FFN_PROBE.with(|p| p.borrow().is_some())
11066}
11067
11068fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
11069 REFIT
11070 .get_or_init(|| {
11071 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
11072 (
11073 d,
11074 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
11075 )
11076 })
11077 })
11078 .as_ref()
11079}
11080
11081fn refit_accumulate(
11083 li: usize,
11084 g: &[f32],
11085 b: usize,
11086 inter: usize,
11087 out: &[f32],
11088 hidden: usize,
11089 pool: Option<&Pool>,
11090) {
11091 let Some((dir, map)) = refit_dir() else {
11092 return;
11093 };
11094 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
11095 let (from, to) = *SPAN.get_or_init(|| {
11096 let g = |k: &str, d: usize| {
11097 std::env::var(k)
11098 .ok()
11099 .and_then(|v| v.parse().ok())
11100 .unwrap_or(d)
11101 };
11102 (
11103 g("CMF_FFN_REFIT_FROM", 0),
11104 g("CMF_FFN_REFIT_TO", usize::MAX),
11105 )
11106 });
11107 if li < from || li > to {
11108 return;
11109 }
11110 let mut guard = map.lock().unwrap();
11111 let (map, shared) = &mut *guard;
11112 let acc = match map.entry(li) {
11113 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
11114 std::collections::hash_map::Entry::Vacant(e) => {
11115 let path = format!("{dir}/support.{li}.u32");
11116 let Ok(bytes) = std::fs::read(&path) else {
11117 eprintln!("refit: no {path} — layer {li} skipped");
11118 return;
11119 };
11120 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
11121 let support: Vec<u32> = bytes[4..4 + n * 4]
11122 .chunks_exact(4)
11123 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11124 .collect();
11125 eprintln!(
11126 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
11127 (n * n + hidden * n) as f64 * 4.0 / 1e6
11128 );
11129 e.insert(RefitAcc {
11130 gss: vec![0.0; n * n],
11131 ya: vec![0.0; hidden * n],
11132 buf_g: Vec::new(),
11133 buf_o: Vec::new(),
11134 buf_t: 0,
11135 support,
11136 hidden,
11137 tokens: 0,
11138 })
11139 }
11140 };
11141 let ns = acc.support.len();
11142 let cap = refit_batch();
11144 if acc.buf_g.is_empty() {
11145 acc.buf_g = vec![0.0; ns * cap];
11146 acc.buf_o = vec![0.0; hidden * cap];
11147 }
11148 let take = b.min(cap - acc.buf_t);
11149 for t in 0..take {
11150 let col = acc.buf_t + t;
11151 for (j, &n) in acc.support.iter().enumerate() {
11152 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
11153 }
11154 for h in 0..hidden {
11155 acc.buf_o[h * cap + col] = out[t * hidden + h];
11156 }
11157 }
11158 acc.buf_t += take;
11159 acc.tokens += take as u64;
11160 if acc.buf_t < cap {
11161 return;
11162 }
11163 let bt = acc.buf_t;
11164 acc.buf_t = 0;
11165 let RefitAcc {
11175 gss,
11176 ya,
11177 buf_g,
11178 buf_o,
11179 ..
11180 } = acc;
11181 let need = (ns * ns).max(hidden * ns);
11182 if shared.len() < need {
11183 shared.resize(need, 0.0);
11184 }
11185 let scratch = &mut shared[..];
11186 let _ = bt;
11187 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
11188 add_into(gss, &scratch[..ns * ns], pool);
11189 if crate::gpu::gemm_nt_f32_transient(
11190 buf_o,
11191 buf_g,
11192 &mut scratch[..hidden * ns],
11193 hidden,
11194 cap,
11195 ns,
11196 ) {
11197 add_into(ya, &scratch[..hidden * ns], pool);
11198 } else {
11199 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
11200 }
11201 } else {
11202 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
11203 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
11204 }
11205 }
11209
11210fn refit_batch() -> usize {
11212 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11213 *B.get_or_init(|| {
11214 std::env::var("CMF_FFN_REFIT_BATCH")
11215 .ok()
11216 .and_then(|v| v.parse().ok())
11217 .unwrap_or(4096)
11218 })
11219}
11220
11221fn accum_outer_t(
11224 c: &mut [f32],
11225 m: usize,
11226 n: usize,
11227 b: usize,
11228 left: &[f32],
11229 right: &[f32],
11230 pool: Option<&Pool>,
11231) {
11232 let ptr = SendMut(c.as_mut_ptr());
11233 let body = |i: usize| {
11234 let ptr = &ptr;
11235 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
11236 for t in 0..b {
11237 let a = left[i * b + t];
11238 if a == 0.0 {
11239 continue;
11240 }
11241 for (j, o) in row.iter_mut().enumerate() {
11242 *o += a * right[j * b + t];
11243 }
11244 }
11245 };
11246 match pool {
11247 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
11248 for i in s..e {
11249 body(i);
11250 }
11251 }),
11252 _ => {
11253 for i in 0..m {
11254 body(i);
11255 }
11256 }
11257 }
11258}
11259
11260fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
11263 let n = dst.len().min(src.len());
11264 match pool {
11265 Some(p) if n >= 1 << 16 => {
11266 let ptr = SendMut(dst.as_mut_ptr());
11267 let f = |s: usize, e: usize| {
11268 let ptr = &ptr;
11269 for blk in s..e {
11270 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
11271 for i in a..b {
11272 unsafe { *ptr.0.add(i) += src[i] };
11273 }
11274 }
11275 };
11276 p.run_rows(n.div_ceil(4096), &f);
11277 }
11278 _ => {
11279 for (d, v) in dst.iter_mut().zip(&src[..n]) {
11280 *d += *v;
11281 }
11282 }
11283 }
11284}
11285
11286fn accum_outer(
11291 c: &mut [f32],
11292 m: usize,
11293 n: usize,
11294 b: usize,
11295 left: &[f32],
11296 right: &[f32],
11297 pool: Option<&Pool>,
11298) {
11299 const TILE: usize = 32;
11300 let tiles = m.div_ceil(TILE);
11301 let cp = SendMut(c.as_mut_ptr());
11302 let body = |ti: usize| {
11303 let cp = &cp;
11304 let i0 = ti * TILE;
11305 let i1 = (i0 + TILE).min(m);
11306 for t in 0..b {
11307 let r = &right[t * n..t * n + n];
11308 for i in i0..i1 {
11309 let a = left[i * b + t];
11310 if a == 0.0 {
11311 continue;
11312 }
11313 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
11315 for (o, v) in row.iter_mut().zip(r) {
11316 *o += a * *v;
11317 }
11318 }
11319 }
11320 };
11321 match pool {
11322 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
11323 for ti in s..e {
11324 body(ti);
11325 }
11326 }),
11327 _ => {
11328 for ti in 0..tiles {
11329 body(ti);
11330 }
11331 }
11332 }
11333}
11334
11335pub fn refit_flush() -> usize {
11337 let Some((dir, map)) = refit_dir() else {
11338 return 0;
11339 };
11340 let guard = map.lock().unwrap();
11341 let mut n = 0;
11342 for (li, acc) in guard.0.iter() {
11343 let w = |name: &str, v: &[f32]| {
11346 let path = format!("{dir}/{name}.{li}.f32");
11347 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
11348 match std::fs::write(&path, &bytes) {
11349 Ok(()) => {}
11350 Err(e) => eprintln!(
11351 "refit: FAILED to write {path} ({} MB): {e}",
11352 bytes.len() / 1_000_000
11353 ),
11354 }
11355 };
11356 w("gss", &acc.gss);
11357 w("ya", &acc.ya);
11358 println!(
11359 "refit L{li}: {} support, {} tokens, hidden {}",
11360 acc.support.len(),
11361 acc.tokens,
11362 acc.hidden
11363 );
11364 n += 1;
11365 }
11366 n
11367}
11368
11369fn adump_row(li: usize, g: &[f32]) {
11374 use std::io::Write as _;
11375 static FILES: std::sync::OnceLock<
11376 Option<(
11377 String,
11378 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
11379 )>,
11380 > = std::sync::OnceLock::new();
11381 let Some((prefix, map)) = FILES
11382 .get_or_init(|| {
11383 std::env::var("CMF_FFN_ADUMP")
11384 .ok()
11385 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
11386 })
11387 .as_ref()
11388 else {
11389 return;
11390 };
11391 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
11394 let (from, to) = *SPAN.get_or_init(|| {
11395 let g = |k: &str, d: usize| {
11396 std::env::var(k)
11397 .ok()
11398 .and_then(|v| v.parse().ok())
11399 .unwrap_or(d)
11400 };
11401 (
11402 g("CMF_FFN_ADUMP_FROM", 0),
11403 g("CMF_FFN_ADUMP_TO", usize::MAX),
11404 )
11405 });
11406 if li < from || li > to {
11407 return;
11408 }
11409 let mut map = map.lock().unwrap();
11410 let f = map.entry(li).or_insert_with(|| {
11411 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
11412 });
11413 let mut bytes = Vec::with_capacity(g.len() * 2);
11414 for v in g {
11415 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
11416 }
11417 let _ = f.write_all(&bytes);
11418}
11419
11420fn oracle_topk() -> usize {
11426 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11427 *K.get_or_init(|| {
11428 std::env::var("CMF_FFN_ORACLE_TOPK")
11429 .ok()
11430 .and_then(|v| v.parse().ok())
11431 .unwrap_or(0)
11432 })
11433}
11434
11435fn gate_topk() -> usize {
11441 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11442 *K.get_or_init(|| {
11443 std::env::var("CMF_FFN_GATE_TOPK")
11444 .ok()
11445 .and_then(|v| v.parse().ok())
11446 .unwrap_or(0)
11447 })
11448}
11449
11450fn gate_block() -> usize {
11457 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11458 *B.get_or_init(|| {
11459 std::env::var("CMF_FFN_GATE_BLOCK")
11460 .ok()
11461 .and_then(|v| v.parse().ok())
11462 .unwrap_or(1)
11463 })
11464}
11465
11466fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
11468 let n = g.len();
11469 let nb = n.div_ceil(block);
11470 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
11471 if kb >= nb {
11472 return;
11473 }
11474 let mut score: Vec<f32> = (0..nb)
11475 .map(|b| {
11476 g[b * block..((b + 1) * block).min(n)]
11477 .iter()
11478 .map(|v| v * v)
11479 .sum::<f32>()
11480 })
11481 .collect();
11482 let mut ord = score.clone();
11483 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
11484 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11485 });
11486 let thr = *kth;
11487 for b in 0..nb {
11488 if score[b] < thr {
11489 g[b * block..((b + 1) * block).min(n)].fill(0.0);
11490 }
11491 }
11492 score.clear();
11493}
11494
11495fn keep_top_k(g: &mut [f32], k: usize) {
11497 if gate_block() > 1 {
11498 return keep_top_blocks(g, k, gate_block());
11499 }
11500 let n = g.len();
11501 if k == 0 || k >= n {
11502 return;
11503 }
11504 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
11505 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11506 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11507 });
11508 let thr = *kth;
11509 for v in g.iter_mut() {
11510 if v.abs() < thr {
11511 *v = 0.0;
11512 }
11513 }
11514}
11515
11516fn probe_sq() -> bool {
11520 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11521 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
11522}
11523
11524fn probe_signed() -> bool {
11528 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11529 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
11530}
11531
11532fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
11540 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
11541 M.get_or_init(|| {
11542 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
11543 let b = std::fs::read(&p).ok()?;
11544 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
11545 let vals: Vec<f32> = b[8..]
11546 .chunks_exact(4)
11547 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11548 .collect();
11549 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
11550 Some((inter, vals))
11551 })
11552 .as_ref()
11553}
11554
11555fn probe_topk() -> usize {
11558 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
11559 *K.get_or_init(|| {
11560 std::env::var("CMF_FFN_PROBE_TOPK")
11561 .ok()
11562 .and_then(|v| v.parse().ok())
11563 .unwrap_or(0)
11564 })
11565}
11566
11567thread_local! {
11568 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
11571 const { std::cell::RefCell::new(None) };
11572}
11573
11574fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
11587 let dt = d.down_t.as_ref()?;
11588 let inter = d.gate_proj.rows();
11589 let hidden = dt.cols();
11590 if k == 0 || k >= inter || d.act != Act::Silu {
11591 return None;
11592 }
11593 DYN_SCRATCH.with(|sc| {
11594 let mut sc = sc.borrow_mut();
11595 let DynScratch {
11596 g,
11597 mag,
11598 live,
11599 parts,
11600 } = &mut *sc;
11601 g.resize(inter, 0.0);
11602 d.gate_proj.matvec(x, g, pool);
11603 for v in g.iter_mut() {
11604 *v = inference::silu(*v);
11605 }
11606 mag.clear();
11609 mag.extend(g.iter().map(|v| v.abs()));
11610 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
11611 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
11612 });
11613 let thr = *kth;
11614 live.clear();
11615 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
11616 let mut out = vec![0.0f32; hidden];
11617 match pool {
11618 Some(p) if live.len() >= 64 => {
11619 let nw = p.n_workers() + 1;
11620 parts.clear();
11621 parts.resize(nw * hidden, 0.0);
11622 let ptr = SendMut(parts.as_mut_ptr());
11623 let n = live.len();
11624 let live_ref: &[u32] = live;
11625 let g_ref: &[f32] = g;
11626 p.run(&|w, workers| {
11627 let chunk = n.div_ceil(workers);
11628 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
11629 if s >= e {
11630 return;
11631 }
11632 WORKER_SCRATCH.with(|ws| {
11633 let mut ws = ws.borrow_mut();
11634 let [scratch, acc] = &mut *ws;
11635 scratch.resize(hidden.max(x.len()), 0.0);
11636 acc.clear();
11637 acc.resize(hidden, 0.0);
11638 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
11639 if let Some(&nx) = live_ref[s..e].get(o + 1) {
11642 d.up_proj.prefetch_row(nx as usize);
11643 dt.prefetch_row(nx as usize);
11644 }
11645 let idx = nrm as usize;
11646 let up = d.up_proj.row_dot(idx, x, scratch);
11647 let a = g_ref[idx] * up;
11648 if a != 0.0 {
11649 dt.add_row_scaled(idx, a, acc, scratch);
11650 }
11651 }
11652 for (j, v) in acc.iter().enumerate() {
11653 unsafe { *ptr.at(w * hidden + j) = *v };
11654 }
11655 });
11656 });
11657 for w in 0..nw {
11658 for (j, o) in out.iter_mut().enumerate() {
11659 *o += parts[w * hidden + j];
11660 }
11661 }
11662 }
11663 _ => {
11664 WORKER_SCRATCH.with(|ws| {
11665 let mut ws = ws.borrow_mut();
11666 let [scratch, _acc] = &mut *ws;
11667 scratch.resize(hidden.max(x.len()), 0.0);
11668 for &nrm in live.iter() {
11669 let idx = nrm as usize;
11670 let up = d.up_proj.row_dot(idx, x, scratch);
11671 let a = g[idx] * up;
11672 if a != 0.0 {
11673 dt.add_row_scaled(idx, a, &mut out, scratch);
11674 }
11675 }
11676 });
11677 }
11678 }
11679 Some(out)
11680 })
11681}
11682
11683struct DynScratch {
11686 g: Vec<f32>,
11687 mag: Vec<f32>,
11688 live: Vec<u32>,
11689 parts: Vec<f32>,
11690}
11691
11692thread_local! {
11693 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
11694 std::cell::RefCell::new(DynScratch {
11695 g: Vec::new(),
11696 mag: Vec::new(),
11697 live: Vec::new(),
11698 parts: Vec::new(),
11699 })
11700 };
11701 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
11703 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
11704}
11705
11706fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
11711 let inter = d.gate_proj.rows();
11712 FFN_SCRATCH.with(|s| {
11713 let mut s = s.borrow_mut();
11714 let [g, u, ..] = &mut *s;
11715 g.resize(inter, 0.0);
11716 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
11717 } else {
11719 u.resize(inter, 0.0);
11720 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
11721 for i in 0..inter {
11722 g[i] = d.act.combine(g[i], u[i]);
11723 }
11724 }
11725 zero_masked_cols(g, 1, inter, mask_row);
11726 let mut out = attention::take_buf(d.down_proj.rows());
11727 d.down_proj.matvec(g, &mut out, pool);
11728 out
11729 })
11730}
11731
11732fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
11738 if d.act != Act::Silu {
11740 return None;
11741 }
11742 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
11745 return None;
11746 }
11747 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
11748 let mut model_ref = None;
11749 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
11750 let model = model_ref?;
11751 let hidden = jobs[0].down.1;
11752 let mut out = attention::take_buf(hidden);
11753 if crate::gpu::moe_block(&model, &jobs, &mut out) {
11754 Some(out)
11755 } else {
11756 let mut out = out;
11757 attention::recycle_buf(&mut out);
11758 None
11759 }
11760}
11761
11762#[allow(clippy::type_complexity)]
11767#[allow(clippy::type_complexity)]
11768pub(crate) fn moe_parts(
11769 t: &QTensor,
11770) -> Option<(
11771 &std::sync::Arc<cortiq_core::CmfModel>,
11772 usize,
11773 usize,
11774 usize,
11775 &[f32],
11776 &[f32],
11777 bool,
11778 bool,
11779 bool,
11780)> {
11781 match t {
11782 QTensor::Mapped {
11783 model,
11784 idx,
11785 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
11786 rows,
11787 cols,
11788 row_scale,
11789 col_field,
11790 ..
11791 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
11792 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
11793 )),
11794 QTensor::Mapped {
11796 model,
11797 idx,
11798 dtype: cortiq_core::TensorDtype::Q1,
11799 rows,
11800 cols,
11801 ..
11802 } => Some((
11803 model,
11804 *idx,
11805 *rows,
11806 *cols,
11807 &[][..],
11808 &[][..],
11809 true,
11810 false,
11811 false,
11812 )),
11813 QTensor::Mapped {
11815 model,
11816 idx,
11817 dtype: cortiq_core::TensorDtype::Q4Tiled,
11818 rows,
11819 cols,
11820 ..
11821 } => Some((
11822 model,
11823 *idx,
11824 *rows,
11825 *cols,
11826 &[][..],
11827 &[][..],
11828 false,
11829 true,
11830 false,
11831 )),
11832 QTensor::Mapped {
11834 model,
11835 idx,
11836 dtype: cortiq_core::TensorDtype::Q4TiledP,
11837 rows,
11838 cols,
11839 ..
11840 } => Some((
11841 model,
11842 *idx,
11843 *rows,
11844 *cols,
11845 &[][..],
11846 &[][..],
11847 false,
11848 true,
11849 false,
11850 )),
11851 QTensor::Mapped {
11855 model,
11856 idx,
11857 dtype: cortiq_core::TensorDtype::Q2TiledP,
11858 rows,
11859 cols,
11860 ..
11861 } => Some((
11862 model,
11863 *idx,
11864 *rows,
11865 *cols,
11866 &[][..],
11867 &[][..],
11868 false,
11869 true,
11870 true,
11871 )),
11872 _ => None,
11873 }
11874}
11875
11876#[cfg(target_os = "macos")]
11882fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
11883 if m.router_sigmoid
11884 || m.router_input_norm
11885 || m.expert_bias.is_some()
11886 || m.route_tau.is_some()
11887 || m.mask.is_some()
11888 || m.per_expert_scale.is_some()
11889 || m.experts.is_empty()
11890 || m.top_k == 0
11891 || m.resonance.is_some()
11892 {
11893 return None;
11894 }
11895 let (sh, sg) = match &m.shared {
11898 Some((sh, Some(sg))) => (sh, sg),
11899 _ => return None,
11900 };
11901 let (rf, rr, rc) = m.router.f32_parts()?;
11902 if rr != m.experts.len() || rc != hidden {
11903 return None;
11904 }
11905 let (sf, sr, sc) = sg.f32_parts()?;
11906 if sr * sc != hidden {
11907 return None;
11908 }
11909 let inter = m.experts[0].gate_proj.rows();
11910 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
11913 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
11914 if e.act != Act::Silu
11915 || e.gate_proj.rows() != inter
11916 || e.gate_proj.cols() != hidden
11917 || e.up_proj.rows() != inter
11918 || e.up_proj.cols() != hidden
11919 || e.down_proj.rows() != hidden
11920 || e.down_proj.cols() != inter
11921 {
11922 return None;
11923 }
11924 let pick = |t: &QTensor| -> Option<usize> {
11925 if gu_q2 {
11926 t.mapped_q2tp().map(|(_, i)| i)
11927 } else {
11928 t.mapped_q4tp().map(|(_, i)| i)
11929 }
11930 };
11931 Some((
11932 pick(&e.gate_proj)?,
11933 pick(&e.up_proj)?,
11934 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
11935 ))
11936 };
11937 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
11938 let shared = trio(sh)?;
11939 Some(crate::gpu::GpuMoe {
11940 router: rf,
11941 sgate: sf,
11942 experts,
11943 shared,
11944 n_exp: m.experts.len(),
11945 top_k: m.top_k,
11946 inter,
11947 norm_topk: m.norm_topk_prob,
11948 route_scale: m.routed_scaling,
11949 gu_q2,
11950 })
11951}
11952
11953pub(crate) fn moe_push_job_parts<'a>(
11957 gate: &'a QTensor,
11958 up: &'a QTensor,
11959 down: &'a QTensor,
11960 x: &[f32],
11961 w: f32,
11962 swiglu_limit: f32,
11963 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
11964 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
11965) -> Option<()> {
11966 use crate::qtensor::prescale;
11967 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
11968 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
11969 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
11970 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
11971 return None; }
11973 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
11976 return None;
11977 }
11978 if !gq2 && dq2 {
11979 return None;
11980 }
11981 model_ref.get_or_insert_with(|| gm.clone());
11982 let dt = |cf: &[f32]| {
11983 if cf.is_empty() {
11984 cortiq_core::TensorDtype::Q8Row
11985 } else {
11986 cortiq_core::TensorDtype::Q8_2f
11987 }
11988 };
11989 jobs.push(crate::gpu::MoeJob {
11990 gate: (gi, gr, gc, grs),
11991 up: (ui, ur, uc, urs),
11992 down: (di, dr, dc, drs),
11993 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
11994 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
11995 down_col: dcf,
11996 w,
11997 q1: gq1,
11998 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
11999 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
12000 gu_q2: gq2,
12001 swiglu_limit,
12002 });
12003 Some(())
12004}
12005
12006fn moe_push_job<'a>(
12008 d: &'a DenseFfn,
12009 x: &[f32],
12010 w: f32,
12011 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
12012 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
12013) -> Option<()> {
12014 use crate::qtensor::prescale;
12015 if d.act != Act::Silu {
12016 return None; }
12018 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
12019 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
12020 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
12021 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
12022 return None; }
12024 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
12025 return None;
12026 }
12027 if !gq2 && dq2 {
12028 return None;
12029 }
12030 model_ref.get_or_insert_with(|| gm.clone());
12031 let gdt = if gcf.is_empty() {
12032 cortiq_core::TensorDtype::Q8Row
12033 } else {
12034 cortiq_core::TensorDtype::Q8_2f
12035 };
12036 let udt = if ucf.is_empty() {
12037 cortiq_core::TensorDtype::Q8Row
12038 } else {
12039 cortiq_core::TensorDtype::Q8_2f
12040 };
12041 jobs.push(crate::gpu::MoeJob {
12042 gate: (gi, gr, gc, grs),
12043 up: (ui, ur, uc, urs),
12044 down: (di, dr, dc, drs),
12045 xs_gate: prescale(x, gcf, gdt).into_owned(),
12046 xs_up: prescale(x, ucf, udt).into_owned(),
12047 down_col: dcf,
12048 w,
12049 q1: gq1,
12050 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
12051 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
12052 gu_q2: gq2,
12053 swiglu_limit: 0.0,
12054 });
12055 Some(())
12056}
12057
12058fn sparse_ffn_quant(
12065 d: &DenseFfn,
12066 x: &[f32],
12067 active: &[u16],
12068 hidden: usize,
12069 pool: Option<&Pool>,
12070) -> Vec<f32> {
12071 let n = active.len();
12072 let inter = d.gate_proj.rows();
12073 let mut act = vec![0.0f32; n];
12074 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
12077 let compute = |ai: usize| -> f32 {
12078 let idx = active[ai] as usize;
12079 if idx >= inter {
12080 return 0.0; }
12082 let mut s = if need_scratch {
12083 vec![0.0f32; hidden]
12084 } else {
12085 Vec::new()
12086 };
12087 let gate = d.gate_proj.row_dot(idx, x, &mut s);
12088 let up = d.up_proj.row_dot(idx, x, &mut s);
12089 d.act.combine(gate, up)
12090 };
12091 match pool {
12092 Some(p) if n >= 256 => {
12093 let ptr = SendMut(act.as_mut_ptr());
12094 p.run(&|widx, nw| {
12095 let chunk = n.div_ceil(nw);
12096 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
12097 for ai in s..e {
12098 unsafe { *ptr.at(ai) = compute(ai) };
12099 }
12100 });
12101 }
12102 _ => {
12103 for (ai, a) in act.iter_mut().enumerate() {
12104 *a = compute(ai);
12105 }
12106 }
12107 }
12108 let mut out = vec![0.0f32; hidden];
12110 for (ai, &idx) in active.iter().enumerate() {
12111 let w = act[ai];
12112 if w.abs() >= 1e-12 && (idx as usize) < inter {
12113 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
12114 }
12115 }
12116 out
12117}
12118
12119#[doc(hidden)]
12121pub fn sparse_ffn_quant_for_test(
12122 d: &DenseFfn,
12123 x: &[f32],
12124 active: &[u16],
12125 hidden: usize,
12126) -> Vec<f32> {
12127 sparse_ffn_quant(d, x, active, hidden, None)
12128}
12129
12130fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
12134 let deq = |t: &QTensor| -> Vec<f32> {
12135 let (rows, cols) = (t.rows(), t.cols());
12136 let mut out = vec![0.0f32; rows * cols];
12137 for r in 0..rows {
12138 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
12139 }
12140 out
12141 };
12142 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
12143}
12144
12145struct SendMut(*mut f32);
12147unsafe impl Send for SendMut {}
12148unsafe impl Sync for SendMut {}
12149impl SendMut {
12150 #[inline]
12151 #[allow(clippy::mut_from_ref)]
12154 unsafe fn at(&self, i: usize) -> &mut f32 {
12155 unsafe { &mut *self.0.add(i) }
12156 }
12157}
12158
12159pub(crate) fn moe_route(
12169 logits: &[f32],
12170 m: &MoeFfn,
12171 allowed: Option<&[bool]>,
12172) -> (Vec<usize>, Vec<f32>, f32) {
12173 let ne = logits.len();
12174 let p: Vec<f32> = if m.router_sigmoid {
12175 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
12176 } else {
12177 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
12178 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
12179 let s: f32 = e.iter().sum();
12180 for v in &mut e {
12181 *v /= s;
12182 }
12183 e
12184 };
12185 let admit = |e: usize| {
12191 m.mask.as_ref().is_none_or(|mk| mk[e])
12192 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
12193 };
12194 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
12195 match &m.expert_bias {
12197 Some(b) => idx.sort_unstable_by(|&x, &y| {
12198 (p[y] + b[y])
12199 .partial_cmp(&(p[x] + b[x]))
12200 .unwrap()
12201 .then(x.cmp(&y))
12202 }),
12203 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
12204 }
12205 idx.truncate(m.top_k);
12206 if let Some(tau) = m.route_tau {
12210 let total: f32 = idx.iter().map(|&e| p[e]).sum();
12211 if total > 0.0 {
12212 let mut acc = 0.0f32;
12213 let mut keep = idx.len();
12214 for (i, &e) in idx.iter().enumerate() {
12215 acc += p[e];
12216 if acc >= tau * total {
12217 keep = i + 1;
12218 break;
12219 }
12220 }
12221 idx.truncate(keep);
12222 }
12223 }
12224 let wsum: f32 = if m.norm_topk_prob {
12225 let s: f32 = idx.iter().map(|&e| p[e]).sum();
12226 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
12229 } else {
12230 1.0 / m.routed_scaling
12231 };
12232 (idx, p, wsum)
12233}
12234
12235fn moe_trace(idx: &[usize]) {
12237 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
12238}
12239
12240pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
12243 use std::io::Write;
12244 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
12245 std::sync::OnceLock::new();
12246 let Some(f) = F.get_or_init(|| {
12247 let p = std::env::var("CMF_MOE_TRACE").ok()?;
12248 Some(std::sync::Mutex::new(
12249 std::fs::OpenOptions::new()
12250 .create(true)
12251 .append(true)
12252 .open(p)
12253 .ok()?,
12254 ))
12255 }) else {
12256 return;
12257 };
12258 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
12259 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
12260}
12261
12262pub(crate) fn moe_ffn(
12265 m: &MoeFfn,
12266 x: &[f32],
12267 pool: Option<&Pool>,
12268 allowed: Option<&[bool]>,
12269) -> Vec<f32> {
12270 accumulate_act(m, x, 1);
12271 let ne = m.experts.len();
12272 let mut logits = vec![0.0f32; ne];
12273 match &m.resonance {
12274 Some(r) => r.scores(x, &mut logits),
12275 None => m.router.matvec(x, &mut logits, pool),
12276 }
12277 let (idx, p, wsum) = moe_route(&logits, m, allowed);
12278 {
12279 let mut st = m.stats.borrow_mut();
12280 if st.len() < ne {
12281 st.resize(ne, 0);
12282 }
12283 for &e in &idx {
12284 st[e] += 1;
12285 }
12286 }
12287 moe_trace(&idx);
12293 if crate::gpu::enabled_here() {
12298 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
12299 crate::gpu::ProbeArm::Gpu => {
12300 let t0 = std::time::Instant::now();
12301 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
12302 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
12303 return out;
12304 }
12305 }
12306 crate::gpu::ProbeArm::CpuTimed => {
12307 let t0 = std::time::Instant::now();
12308 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
12309 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
12310 return out;
12311 }
12312 crate::gpu::ProbeArm::Cpu => {
12313 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
12314 }
12315 }
12316 }
12317 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
12318}
12319
12320fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
12325 use std::sync::atomic::{AtomicBool, Ordering};
12326 if built {
12327 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
12328 if total_layers > 0 && layers_run < total_layers {
12329 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
12330 } else {
12331 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
12332 }
12333 } else {
12334 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
12335 }
12336 static SAID: AtomicBool = AtomicBool::new(false);
12337 if !SAID.swap(true, Ordering::Relaxed) {
12338 if built {
12339 tracing::info!("wgpu whole-token graph: ACTIVE");
12340 } else {
12341 tracing::warn!("wgpu whole-token graph refused — per-op path");
12342 }
12343 }
12344}
12345
12346pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12350pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12351pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12355pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
12357
12358fn moe_batch_enabled() -> bool {
12361 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12362 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
12363}
12364
12365fn moe_ffn_cpu_batched(
12371 m: &MoeFfn,
12372 x: &[f32],
12373 idx: &[usize],
12374 p: &[f32],
12375 wsum: f32,
12376 pool: Option<&Pool>,
12377) -> Option<Vec<f32>> {
12378 if idx.is_empty() || !moe_batch_enabled() {
12379 return None;
12380 }
12381 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
12385 return None;
12386 }
12387 let n = idx.len() + usize::from(m.shared.is_some());
12388 let mut pairs = Vec::with_capacity(n);
12389 let mut downs = Vec::with_capacity(n);
12390 let mut ws = Vec::with_capacity(n);
12391 for &e in idx {
12392 let d = &m.experts[e];
12393 if d.act != Act::Silu {
12394 return None;
12395 }
12396 pairs.push((&d.gate_proj, &d.up_proj));
12397 downs.push(&d.down_proj);
12398 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
12399 }
12400 if let Some((se, gate)) = &m.shared {
12403 if se.act != Act::Silu {
12404 return None;
12405 }
12406 let g = gate.as_ref().map_or(1.0, |gate| {
12407 let mut gl = [0.0f32; 1];
12408 gate.matvec(x, &mut gl, pool);
12409 1.0 / (1.0 + (-gl[0]).exp())
12410 });
12411 pairs.push((&se.gate_proj, &se.up_proj));
12412 downs.push(&se.down_proj);
12413 ws.push(g);
12414 }
12415 let inter = pairs[0].0.rows();
12416 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
12417 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
12418 return None;
12419 }
12420 let mut out = attention::take_buf(x.len());
12421 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
12422 attention::recycle_buf(&mut out);
12423 return None;
12424 }
12425 Some(out)
12426}
12427
12428pub(crate) fn moe_cold_experts_cpu(
12434 experts: &[(&DenseFfn, f32)],
12435 x: &[f32],
12436 pool: Option<&Pool>,
12437) -> Vec<f32> {
12438 let mut out = attention::take_buf(x.len());
12439 if experts.is_empty() {
12440 return out;
12441 }
12442 let pairs: Vec<_> = experts
12443 .iter()
12444 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
12445 .collect();
12446 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
12447 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
12448 let inter = experts[0].0.gate_proj.rows();
12449 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
12450 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
12451 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
12452 {
12453 return out;
12454 }
12455 out.fill(0.0);
12456 for &(expert, weight) in experts {
12457 let mut one = dense_ffn(expert, x, pool);
12458 for (o, v) in out.iter_mut().zip(&one) {
12459 *o += weight * v;
12460 }
12461 attention::recycle_buf(&mut one);
12462 }
12463 out
12464}
12465
12466fn moe_ffn_cpu(
12468 m: &MoeFfn,
12469 x: &[f32],
12470 idx: &[usize],
12471 p: &[f32],
12472 wsum: f32,
12473 pool: Option<&Pool>,
12474) -> Vec<f32> {
12475 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
12476 return out;
12477 }
12478 let mut out = attention::take_buf(x.len());
12479 for &e in idx {
12480 let mut eo = dense_ffn(&m.experts[e], x, pool);
12481 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
12482 for i in 0..out.len() {
12483 out[i] += w * eo[i];
12484 }
12485 attention::recycle_buf(&mut eo);
12486 }
12487 if let Some((se, gate)) = &m.shared {
12488 let mut so = dense_ffn(se, x, pool);
12489 let g = gate.as_ref().map_or(1.0, |gate| {
12490 let mut gl = [0.0f32; 1];
12491 gate.matvec(x, &mut gl, pool);
12492 1.0 / (1.0 + (-gl[0]).exp())
12493 });
12494 for i in 0..out.len() {
12495 out[i] += g * so[i];
12496 }
12497 attention::recycle_buf(&mut so);
12498 }
12499 out
12500}
12501
12502#[allow(clippy::too_many_arguments)]
12510fn mla_attention(
12511 w: &MlaWeights,
12512 normed: &[f32],
12513 cache: &mut crate::kv_cache::LayerKvCache,
12514 position: usize,
12515 inv_freq: &[f32],
12516 rope_scale: f32,
12517 eps: f64,
12518 pool: Option<&Pool>,
12519) -> Vec<f32> {
12520 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
12521 let hd = dr + dn;
12522 let mut q = vec![0.0f32; nh * hd];
12523 match (&w.q_a, &w.q_a_norm) {
12524 (Some(qa), Some(qn)) => {
12525 let mut t = vec![0.0f32; qa.rows()];
12526 qa.matvec(normed, &mut t, pool);
12527 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
12528 w.q_proj.matvec(&tn, &mut q, pool);
12529 }
12530 _ => w.q_proj.matvec(normed, &mut q, pool),
12531 }
12532 let mut ca = vec![0.0f32; lora + dr];
12533 w.kv_a.matvec(normed, &mut ca, pool);
12534 let (c_lat, k_rope) = ca.split_at_mut(lora);
12535 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
12536 let mut kvb = vec![0.0f32; nh * (dn + dv)];
12537 w.kv_b.matvec(&latn, &mut kvb, pool);
12538 if !w.nope {
12539 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
12540 }
12541 for h in 0..nh {
12542 if !w.nope {
12543 attention::rope_rotate_scaled(
12544 &mut q[h * hd..h * hd + dr],
12545 position,
12546 inv_freq,
12547 rope_scale,
12548 );
12549 }
12550 }
12551 let mut k = vec![0.0f32; nh * hd];
12552 let mut v = vec![0.0f32; nh * hd];
12553 for h in 0..nh {
12554 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
12555 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
12556 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
12557 }
12558 cache.append(&k, &v, &vec![true; nh]);
12559 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
12560 attention::recycle_buf(&mut imp);
12561 let mut ov = vec![0.0f32; nh * dv];
12562 for h in 0..nh {
12563 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
12564 }
12565 let mut out = vec![0.0f32; w.o_proj.rows()];
12566 w.o_proj.matvec(&ov, &mut out, pool);
12567 out
12568}
12569
12570fn dense_moe_ffn(
12577 dm: &DenseMoeFfn,
12578 x_normed: &[f32],
12579 h_raw: &[f32],
12580 eps: f64,
12581 norm_style: NormStyle,
12582 pool: Option<&Pool>,
12583) -> Vec<f32> {
12584 let mut d = dense_ffn(&dm.dense, x_normed, pool);
12585 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
12586 let m = &dm.moe;
12587 let ne = m.experts.len();
12588 let mut logits = vec![0.0f32; ne];
12589 if m.router_input_norm {
12590 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
12591 let inv = 1.0 / (ss + eps as f32).sqrt();
12592 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
12593 m.router.matvec(&xr, &mut logits, pool);
12594 } else {
12595 m.router.matvec(h_raw, &mut logits, pool);
12596 }
12597 let (idx, p, wsum) = moe_route(&logits, m, None);
12598 {
12599 let mut st = m.stats.borrow_mut();
12600 if st.len() < ne {
12601 st.resize(ne, 0);
12602 }
12603 for &e in &idx {
12604 st[e] += 1;
12605 }
12606 }
12607 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
12608 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
12609 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
12610 for (di, mi) in d.iter_mut().zip(&mo) {
12611 *di += mi;
12612 }
12613 d
12614}
12615
12616fn moe_gpu_refused(why: &'static str) {
12623 use std::sync::atomic::{AtomicBool, Ordering};
12624 static SAID: AtomicBool = AtomicBool::new(false);
12625 if !SAID.swap(true, Ordering::Relaxed) {
12626 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
12627 }
12628}
12629
12630fn moe_ffn_gpu(
12631 m: &MoeFfn,
12632 x: &[f32],
12633 idx: &[usize],
12634 p: &[f32],
12635 wsum: f32,
12636 pool: Option<&Pool>,
12637) -> Option<Vec<f32>> {
12638 use crate::gpu::MoeJob;
12639
12640 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
12641 let mut model_ref = None;
12642 for &e in idx {
12643 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
12644 moe_gpu_refused("push_job(expert)");
12645 return None;
12646 }
12647 }
12648 if let Some((se, gate)) = &m.shared {
12649 let g = gate.as_ref().map_or(1.0, |gate| {
12650 let mut gl = [0.0f32; 1];
12651 gate.matvec(x, &mut gl, pool);
12652 1.0 / (1.0 + (-gl[0]).exp())
12653 });
12654 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
12655 moe_gpu_refused("push_job(shared)");
12656 return None;
12657 }
12658 }
12659 let Some(model) = model_ref else {
12660 moe_gpu_refused("no model_ref");
12661 return None;
12662 };
12663 let hidden = jobs[0].down.1;
12664 let mut out = vec![0.0f32; hidden];
12665 if crate::gpu::moe_block(&model, &jobs, &mut out) {
12666 Some(out)
12667 } else {
12668 moe_gpu_refused("gpu::moe_block");
12669 None
12670 }
12671}
12672
12673fn ffn_forward(
12675 ffn: &FfnKind,
12676 x: &[f32],
12677 pool: Option<&Pool>,
12678 experts_allowed: Option<&[bool]>,
12679) -> Vec<f32> {
12680 match ffn {
12681 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
12682 FfnKind::Dense(d) => dense_ffn(d, x, pool),
12683 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
12684 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
12688 }
12689}
12690
12691fn ffn_forward_pair(
12695 ffn: &FfnKind,
12696 x1: &[f32],
12697 x2: &[f32],
12698 pool: Option<&Pool>,
12699 experts_allowed: Option<&[bool]>,
12700) -> (Vec<f32>, Vec<f32>) {
12701 let d = match ffn {
12702 FfnKind::Dense(d) if !d.segs.is_empty() => {
12705 return (
12706 tube_ffn(d, x1, 1, pool, None),
12707 tube_ffn(d, x2, 1, pool, None),
12708 );
12709 }
12710 FfnKind::Dense(d) => d,
12711 FfnKind::Moe(m) => {
12712 return (
12713 moe_ffn(m, x1, pool, experts_allowed),
12714 moe_ffn(m, x2, pool, experts_allowed),
12715 );
12716 }
12717 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
12718 };
12719 let inter = d.gate_proj.rows();
12720 FFN_SCRATCH.with(|s| {
12721 let mut s = s.borrow_mut();
12722 let [g1, g2, u1, u2] = &mut *s;
12723 g1.resize(inter, 0.0);
12724 g2.resize(inter, 0.0);
12725 u1.resize(inter, 0.0);
12726 u2.resize(inter, 0.0);
12727 QTensor::matvec2_many(
12730 [&d.gate_proj, &d.up_proj],
12731 x1,
12732 x2,
12733 [g1.as_mut_slice(), u1.as_mut_slice()],
12734 [g2.as_mut_slice(), u2.as_mut_slice()],
12735 pool,
12736 );
12737 for i in 0..inter {
12738 g1[i] = d.act.combine(g1[i], u1[i]);
12739 g2[i] = d.act.combine(g2[i], u2[i]);
12740 }
12741 let mut o1 = attention::take_buf(d.down_proj.rows());
12742 let mut o2 = attention::take_buf(d.down_proj.rows());
12743 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
12744 (o1, o2)
12745 })
12746}
12747
12748#[cfg(test)]
12749mod tests {
12750
12751 #[test]
12752 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
12753 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
12754 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
12755 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
12756 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
12757 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
12758 }
12759
12760 #[test]
12761 fn cancel_flag_stops_generation() {
12762 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
12763 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
12766 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
12767 assert_eq!(r.finish_reason, "cancelled");
12768 assert!(
12769 r.token_ids.is_empty(),
12770 "no tokens after cancel: {:?}",
12771 r.token_ids
12772 );
12773 assert_eq!(p.kv_cache.seq_len(), 0);
12774 assert!(p.kv_history.is_empty());
12775 assert!(!p.graph_want_logits);
12776 assert!(p.graph_logits.is_none());
12777 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
12779 assert_ne!(r2.finish_reason, "cancelled");
12780 }
12781 use super::*;
12782
12783 #[test]
12791 fn dynamic_ffn_equals_the_zeroing_arm() {
12792 let (hidden, inter) = (8usize, 32usize);
12793 let synth = |n: usize, salt: usize| -> Vec<f32> {
12794 (0..n)
12795 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
12796 .collect()
12797 };
12798 let down = synth(hidden * inter, 3);
12799 let mut down_t = vec![0.0f32; inter * hidden];
12800 for r in 0..hidden {
12801 for c in 0..inter {
12802 down_t[c * hidden + r] = down[r * inter + c];
12803 }
12804 }
12805 let d = DenseFfn {
12806 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
12807 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
12808 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
12809 act: Act::Silu,
12810 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
12811 segs: Vec::new(),
12812 };
12813 let x = synth(hidden, 11);
12814 let k = 12usize;
12815 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
12816 let mut g = vec![0.0f32; inter];
12818 d.gate_proj.matvec(&x, &mut g, None);
12819 let mut u = vec![0.0f32; inter];
12820 d.up_proj.matvec(&x, &mut u, None);
12821 for v in g.iter_mut() {
12822 *v = inference::silu(*v);
12823 }
12824 keep_top_k(&mut g, k);
12825 for i in 0..inter {
12826 g[i] *= u[i];
12827 }
12828 let mut want = vec![0.0f32; hidden];
12829 d.down_proj.matvec(&g, &mut want, None);
12830 for (a, b) in want.iter().zip(&got) {
12831 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
12832 }
12833 }
12834
12835 #[test]
12841 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
12842 let (hidden, core, tube) = (8usize, 12usize, 8usize);
12843 let inter = core + tube;
12844 let synth = |n: usize, salt: usize| -> Vec<f32> {
12845 (0..n)
12846 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
12847 .collect()
12848 };
12849 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
12850 let d_all = synth(hidden * inter, 3);
12851 let dense = DenseFfn {
12853 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
12854 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
12855 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
12856 act: Act::Silu,
12857 down_t: None,
12858 segs: Vec::new(),
12859 };
12860 let rows =
12861 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
12862 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
12863 let mut o = Vec::with_capacity(hidden * (b - a));
12864 for r in 0..hidden {
12865 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
12866 }
12867 o
12868 };
12869 let tubed = DenseFfn {
12870 down_t: None,
12871 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
12872 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
12873 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
12874 act: Act::Silu,
12875 segs: vec![FfnSeg {
12876 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
12877 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
12878 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
12879 start: core,
12880 width: tube,
12881 }],
12882 };
12883 let x = synth(hidden, 7);
12884 let want = dense_ffn(&dense, &x, None);
12885 let got = tube_ffn(&tubed, &x, 1, None, None);
12886 for (a, b) in want.iter().zip(&got) {
12887 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
12888 }
12889 let mut bits = vec![0u8; inter.div_ceil(8)];
12891 for n in 0..core {
12892 bits[n / 8] |= 1 << (n % 8);
12893 }
12894 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
12895 let masked = dense_ffn_masked(&dense, &x, None, &bits);
12896 for (a, b) in masked.iter().zip(&closed) {
12897 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
12898 }
12899 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
12901 for (a, b) in closed.iter().zip(&batch) {
12902 assert_eq!(a, b, "batch arm disagrees with decode arm");
12903 }
12904 }
12905
12906 #[test]
12908 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
12909 let (hidden, inter) = (16usize, 40usize);
12910 let synth = |n: usize, salt: usize| -> Vec<f32> {
12911 (0..n)
12912 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
12913 .collect()
12914 };
12915 let d = DenseFfn {
12916 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
12917 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
12918 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
12919 act: Act::Silu,
12920 down_t: None,
12921 segs: Vec::new(),
12922 };
12923 let x = synth(hidden, 9);
12924 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
12926
12927 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
12928
12929 let mut g = vec![0.0f32; inter];
12931 d.gate_proj.matvec(&x, &mut g, None);
12932 let mut u = vec![0.0f32; inter];
12933 d.up_proj.matvec(&x, &mut u, None);
12934 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
12935 for i in 0..inter {
12936 g[i] = if act_set.contains(&(i as u16)) {
12937 inference::silu(g[i]) * u[i]
12938 } else {
12939 0.0
12940 };
12941 }
12942 let mut reference = vec![0.0f32; hidden];
12943 d.down_proj.matvec(&g, &mut reference, None);
12944
12945 let max_d = sparse
12946 .iter()
12947 .zip(&reference)
12948 .map(|(a, b)| (a - b).abs())
12949 .fold(0.0f32, f32::max);
12950 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
12951 }
12952
12953 fn attach_test_mtp(p: &mut Pipeline) {
12955 let (h, inter, heads, kv, hd) = (
12956 p.hidden_size,
12957 p.intermediate_size,
12958 p.num_heads,
12959 p.num_kv_heads,
12960 p.head_dim,
12961 );
12962 let synth = |n: usize, salt: usize| -> Vec<f32> {
12963 (0..n)
12964 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
12965 .collect()
12966 };
12967 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
12968 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
12969 };
12970 p.mtp = Some(MtpModule {
12971 enorm: vec![1.0; h],
12972 hnorm: vec![1.0; h],
12973 eh_proj: qt(h, 2 * h, 301),
12974 layer: LayerWeights {
12975 input_norm: vec![1.0; h],
12976 post_norm: vec![1.0; h],
12977 attn_out_norm: None,
12978 ffn_out_norm: None,
12979 layer_scale: None,
12980 ffn: FfnKind::Dense(DenseFfn {
12981 gate_proj: qt(inter, h, 315),
12982 up_proj: qt(inter, h, 316),
12983 down_proj: qt(h, inter, 317),
12984 act: Act::Silu,
12985 down_t: None,
12986 segs: Vec::new(),
12987 }),
12988 attn: AttnKind::Full {
12989 bias: None,
12990 wq: qt(heads * hd, h, 311),
12991 wk: qt(kv * hd, h, 312),
12992 wv: qt(kv * hd, h, 313),
12993 wo: qt(h, heads * hd, 314),
12994 q_norm: None,
12995 k_norm: None,
12996 output_gate: false,
12997 softplus_gate: None,
12998 },
12999 },
13000 final_norm: vec![1.0; h],
13001 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
13002 });
13003 }
13004
13005 #[test]
13006 fn speculative_equals_vanilla_greedy() {
13007 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
13011 let run = |spec: bool| {
13012 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13013 p.sampler_config.temperature = 0.0;
13014 attach_test_mtp(&mut p);
13015 p.speculative = spec;
13016 let r = p.generate("abcdef", 12, None, None).unwrap();
13017 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
13018 };
13019 let (vanilla, d0, _) = run(false);
13020 let (spec, d1, a1) = run(true);
13021 assert_eq!(d0, 0, "vanilla path must not draft");
13022 assert!(d1 > 0, "speculative path must draft");
13023 assert_eq!(
13024 vanilla, spec,
13025 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
13026 );
13027 }
13028
13029 #[test]
13030 fn speculative_accepts_constant_oracle() {
13031 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
13033 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13034 p.sampler_config.temperature = 0.0;
13035 p.sampler_config.repetition_penalty = 1.0;
13036 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
13039 attach_test_mtp(&mut p);
13040 p.speculative = true;
13041 let r = p.generate("abcd", 10, None, None).unwrap();
13042 assert!(r.mtp_drafted > 0);
13043 assert_eq!(
13044 r.mtp_accepted, r.mtp_drafted,
13045 "constant logits → every draft accepted"
13046 );
13047 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
13050 }
13051
13052 #[test]
13053 fn empty_prompt_is_an_error_not_a_panic() {
13054 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
13055 let r = p.generate("", 4, None, None);
13056 assert!(r.is_err(), "empty prompt must be a clean error");
13057 }
13058
13059 #[test]
13060 fn every_token_enters_kv_exactly_once() {
13061 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13062 p.sampler_config.temperature = 0.0;
13064 let r = p.generate("abc", 2, None, None).unwrap();
13065 assert_eq!(r.prompt_tokens, 3);
13066 assert_eq!(
13070 p.kv_cache.seq_len(),
13071 3 + r.tokens_generated - 1,
13072 "each token must be cached exactly once (v1 cached the last prompt token twice)"
13073 );
13074 }
13075
13076 #[test]
13077 fn generation_is_reproducible_with_seed() {
13078 let run = || {
13079 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13080 p.generate("hello", 8, None, None).unwrap().token_ids
13081 };
13082 assert_eq!(run(), run());
13083 }
13084
13085 #[test]
13086 fn resetting_sampler_restarts_the_seeded_stream() {
13087 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
13088 let config = SamplerConfig {
13089 seed: Some(1234),
13090 ..SamplerConfig::default()
13091 };
13092 p.set_sampler_config(config.clone());
13093 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
13094 p.set_sampler_config(config);
13095 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
13096 assert_eq!(first, second);
13097 }
13098
13099 #[test]
13100 fn eviction_bounds_the_cache() {
13101 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
13102 p.kv_cache.max_seq_len = 6;
13103 p.sampler_config.temperature = 0.0;
13104 let _ = p.generate("abcd", 12, None, None).unwrap();
13105 assert!(
13106 p.kv_cache.seq_len() <= 6 + 1,
13107 "cache must stay bounded by max_seq_len (got {})",
13108 p.kv_cache.seq_len()
13109 );
13110 }
13111
13112 #[test]
13113 fn confidence_matches_tokens_and_is_a_probability() {
13114 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13115 p.sampler_config.temperature = 0.0;
13116 p.sampler_config.repetition_penalty = 1.0;
13117 let r = p.generate("abcd", 10, None, None).unwrap();
13118 assert_eq!(
13119 r.token_confidence.len(),
13120 r.token_ids.len(),
13121 "one confidence per emitted token"
13122 );
13123 for &c in &r.token_confidence {
13124 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
13125 }
13126 let logits = [1.0f32, 3.0, 0.5, 3.0];
13128 let p0 = top1_prob_t(&logits, 1, 1.0);
13129 let p1 = top1_prob_t(&logits, 3, 1.0);
13130 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
13131 assert!(p0 > 0.0 && p0 < 1.0);
13132 let sharp = top1_prob_t(&logits, 1, 1.0);
13134 let soft = top1_prob_t(&logits, 1, 2.0);
13135 assert!(soft < sharp, "higher temperature lowers peak confidence");
13136 }
13137
13138 #[test]
13139 fn trace_is_opt_in_and_parallels_the_output() {
13140 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13142 p.sampler_config.temperature = 0.0;
13143 p.sampler_config.repetition_penalty = 1.0;
13144 let r = p.generate("abcd", 10, None, None).unwrap();
13145 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
13146
13147 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13149 p.sampler_config.temperature = 0.0;
13150 p.sampler_config.repetition_penalty = 1.0;
13151 p.set_trace(true);
13152 let r = p.generate("abcd", 10, None, None).unwrap();
13153 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
13154 for (i, tr) in r.traces.iter().enumerate() {
13155 assert_eq!(tr.t, i, "trace index is sequential");
13156 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
13157 assert_eq!(
13158 tr.confidence, r.token_confidence[i],
13159 "trace confidence matches the confidence channel"
13160 );
13161 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
13163 }
13164 }
13165
13166 #[test]
13167 fn explain_prefill_logits_match_greedy_first_token() {
13168 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13172 p.sampler_config.temperature = 0.0;
13173 p.sampler_config.repetition_penalty = 1.0;
13174 let ids = p.tokenizer.encode("abcd");
13175 let logits = p.prefill_next_logits(&ids, None);
13176 let argmax = logits
13177 .iter()
13178 .enumerate()
13179 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
13180 .unwrap()
13181 .0 as u32;
13182 let r = p.generate("abcd", 1, None, None).unwrap();
13183 assert_eq!(
13184 argmax, r.token_ids[0],
13185 "explain preview must match greedy emit"
13186 );
13187 }
13188
13189 #[test]
13190 fn laguna_shared_expert_is_unconditionally_added() {
13191 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
13192 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
13193 let zero_dense = || DenseFfn {
13194 gate_proj: matrix(vec![0.0; 4]),
13195 up_proj: matrix(vec![0.0; 4]),
13196 down_proj: matrix(vec![0.0; 4]),
13197 act: Act::Silu,
13198 down_t: None,
13199 segs: Vec::new(),
13200 };
13201 let shared = DenseFfn {
13202 gate_proj: identity(),
13203 up_proj: identity(),
13204 down_proj: identity(),
13205 act: Act::Silu,
13206 down_t: None,
13207 segs: Vec::new(),
13208 };
13209 let x = [1.0, 2.0];
13210 let expected = dense_ffn(&shared, &x, None);
13211 let moe = MoeFfn {
13212 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
13213 experts: vec![zero_dense()],
13214 top_k: 1,
13215 norm_topk_prob: true,
13216 router_sigmoid: true,
13217 expert_bias: None,
13218 routed_scaling: 1.0,
13219 route_tau: None,
13220 shared: Some((shared, None)),
13221 stats: std::cell::RefCell::new(Vec::new()),
13222 act_sq: std::cell::RefCell::new(Vec::new()),
13223 act_rows: std::cell::RefCell::new(Vec::new()),
13224 mask: None,
13225 per_expert_scale: None,
13226 router_input_norm: false,
13227 resonance: None,
13228 };
13229 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
13230 for (actual, expected) in actual.iter().zip(expected) {
13231 assert!((actual - expected).abs() < 1e-6);
13232 }
13233 }
13234
13235 #[test]
13236 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
13237 let ids = vec![1u32, 2, 3, 4, 5, 6];
13238 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13239 p.graph_logits = Some(vec![123.0]);
13240 p.graph_want_logits = true;
13241 p.graph_failed
13242 .store(true, std::sync::atomic::Ordering::Relaxed);
13243 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13244 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
13245 assert!(err.contains("before NLL"));
13246 assert!(p.graph_logits.is_none());
13247 assert!(!p.graph_want_logits);
13248 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13249 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13250
13251 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13252 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
13253 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
13254 assert_eq!(actual.1, expected.1);
13255 assert!((actual.0 - expected.0).abs() < 1e-9);
13256 }
13257
13258 #[test]
13259 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
13260 let ids = vec![1u32, 2, 3, 4, 5, 6];
13261 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13262 p.nll_test_fail_at = Some(1);
13263 let err = p
13264 .nll_ids_from(&ids, 0)
13265 .expect_err("one-shot forward failure");
13266 assert!(err.contains("forward") || err.contains("score row"));
13267 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13268 assert!(!p.graph_want_logits);
13269 assert!(p.graph_logits.is_none());
13270 assert!(p.kv_history.is_empty());
13271
13272 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13273 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
13274 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
13275 assert_eq!(actual.1, expected.1);
13276 assert!((actual.0 - expected.0).abs() < 1e-9);
13277 }
13278
13279 #[test]
13280 fn nll_serial_failure_before_first_row_is_reported() {
13281 let ids = vec![1u32, 2, 3, 4];
13282 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13283 p.nll_test_force_serial = true;
13284 p.nll_test_fail_at = Some(0);
13285 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
13286 assert!(err.contains("serial forward"));
13287 assert!(p.kv_history.is_empty());
13288 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13289 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13290 }
13291
13292 #[test]
13293 fn ffn_probe_failure_discards_recorder_and_state() {
13294 let ids = vec![1u32, 2, 3, 4];
13295 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13296 p.nll_test_fail_at = Some(0);
13297 let err = p
13298 .probe_ffn_mass_batch(&ids)
13299 .expect_err("probe forward failure");
13300 assert!(err.contains("NLL"));
13301 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
13302 assert!(p.kv_history.is_empty());
13303 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13304 }
13305
13306 #[test]
13307 fn nll_test_controls_are_pipeline_scoped() {
13308 let ids = vec![1u32, 2, 3, 4];
13309 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13310 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13311 failing.nll_test_force_serial = true;
13312 failing.nll_test_fail_at = Some(0);
13313
13314 assert!(!failing.can_prefill_batched());
13315 assert!(unaffected.can_prefill_batched());
13316 let expected = unaffected
13317 .nll_ids_from(&ids, 0)
13318 .expect("unaffected pipeline remains usable");
13319 let err = failing
13320 .nll_ids_from(&ids, 0)
13321 .expect_err("failure injection belongs to failing pipeline");
13322 assert!(err.contains("serial forward"));
13323 assert!(failing.nll_test_fail_at.is_none());
13324 assert!(unaffected.can_prefill_batched());
13325 let actual = unaffected
13326 .nll_ids_from(&ids, 0)
13327 .expect("unaffected pipeline remains reusable");
13328 assert_eq!(actual.1, expected.1);
13329 assert!((actual.0 - expected.0).abs() < 1e-9);
13330 }
13331
13332 #[test]
13333 fn forward_ids_failure_channel_is_terminal_and_reusable() {
13334 let ids = vec![1u32, 2, 3, 4, 5, 6];
13335 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
13336 p.graph_logits = Some(vec![123.0]);
13337 p.graph_want_logits = true;
13338 p.graph_failed
13339 .store(true, std::sync::atomic::Ordering::Relaxed);
13340 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
13341
13342 let err = p
13343 .forward_ids(&ids, None)
13344 .expect_err("a failed forward must not become a valid head result");
13345 assert!(err.contains("forward_ids setup"));
13346 assert!(p.graph_logits.is_none());
13347 assert!(!p.graph_want_logits);
13348 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
13349 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
13350 assert_eq!(p.kv_cache.seq_len(), 0);
13351
13352 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
13353 .forward_ids(&ids, None)
13354 .expect("fresh forward_ids");
13355 let actual = p
13356 .forward_ids(&ids, None)
13357 .expect("pipeline remains reusable after a failed forward");
13358 assert_eq!(actual.len(), expected.len());
13359 assert!(
13360 actual
13361 .iter()
13362 .zip(expected)
13363 .all(|(a, b)| (a - b).abs() < 1e-9)
13364 );
13365 assert_eq!(p.kv_cache.seq_len(), ids.len());
13366 }
13367}