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 pub kv_history: Vec<u32>,
96 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
98 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
101 pub dsv4: Option<
105 Box<(
106 crate::dsv4::Dsv4Globals,
107 Vec<crate::dsv4::Dsv4Layer>,
108 crate::dsv4::Dsv4Cfg,
109 crate::dsv4::Dsv4State,
110 )>,
111 >,
112 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
116 pub dspark: Option<crate::dsv4::DsparkState>,
118 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
121 pub dspark_hist: Vec<usize>,
123 pub dspark_real: Vec<u32>,
127 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
131 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
133 pub dspark_draft_ns: u128,
137 pub short_conv_cfg: Option<ShortConvCfg>,
140 pub mtp: Option<MtpModule>,
142 pub speculative: bool,
144 rng: SplitMix64,
145 sampler_scratch: SamplerScratch,
146 spec_forced: Option<u32>,
152 spec_q: Vec<Vec<f32>>,
153 spec_p: Vec<f32>,
154 spec_res: Vec<f32>,
155 spec_qs: Vec<sampler::Sparse>,
157 spec_ps: sampler::Sparse,
158 spec_ress: sampler::Sparse,
159 mtp_graph_mode: Option<bool>,
166 #[cfg(target_os = "macos")]
169 metal_verify: Option<MetalVerifyPending>,
170 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
174 ws: ForwardScratch,
178 pool: Option<std::sync::Arc<Pool>>,
180 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
184 pub(crate) dyn_force_f32: bool,
186 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
191 pub(crate) dyn_active: Option<usize>,
197 pub(crate) dyn_blend_loaded: bool,
201 pub(crate) dyn_phi_layer: Option<usize>,
204 dyn_phi_ema: Vec<f32>,
206 dyn_phi_seen: usize,
207 pub dyn_router: Option<crate::swarm::DynRouter>,
210 o1_cfg: Option<crate::nystrom::O1Cfg>,
213 o1_epoch: u64,
216 o1_flags: Vec<bool>,
218 trace: bool,
221 calib_temp: f32,
224 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
226 graph_kv_id: u64,
227 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
230 graph_want_logits: bool,
231 graph_logits: Option<Vec<f32>>,
234 pub embed_multiplier: f32,
236 pub attn_scale: f32,
239 pub swa: Option<(usize, usize)>,
242 pub sliding_layers: Option<Vec<bool>>,
245 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
248 pub rotary_dim_local: Option<usize>,
249 pub rope_scale: f32,
250 pub rope_scale_local: f32,
251 pub global_attn: Option<(usize, usize)>,
254 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
257 pub attn_v_norm: bool,
259 pub final_softcap: Option<f32>,
261 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
265 pub attn_softcap: f32,
267 confidence_on: bool,
271}
272
273#[cfg(target_os = "macos")]
274impl Drop for Pipeline {
275 fn drop(&mut self) {
276 crate::gpu::kv_mirror_drop(self.graph_kv_id);
277 }
278}
279
280pub struct PipelineWeights {
285 pub embed_tokens: QTensor,
287 pub layers: Vec<LayerWeights>,
289 pub lm_head: QTensor,
291 pub final_norm: Vec<f32>,
293}
294
295pub struct LayerWeights {
297 pub input_norm: Vec<f32>,
298 pub post_norm: Vec<f32>,
301 pub attn_out_norm: Option<Vec<f32>>,
304 pub layer_scale: Option<f32>,
306 pub ffn_out_norm: Option<Vec<f32>>,
309 pub ffn: FfnKind,
310 pub attn: AttnKind,
311}
312
313#[derive(Clone, Copy, PartialEq, Debug, Default)]
316pub enum Act {
317 #[default]
318 Silu,
319 GeluTanh,
320 Situ {
323 beta: f32,
324 linear_beta: f32,
325 },
326}
327
328impl Act {
329 pub fn from_arch(name: &str) -> Self {
330 if name == "gelu_tanh" {
331 Self::GeluTanh
332 } else {
333 Self::Silu
334 }
335 }
336
337 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
339 match arch.hidden_act.as_str() {
340 "situ" => Self::Situ {
341 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
342 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
343 },
344 other => Self::from_arch(other),
345 }
346 }
347
348 #[inline]
349 pub fn apply(self, x: f32) -> f32 {
350 match self {
351 Self::Silu => inference::silu(x),
352 Self::GeluTanh => inference::gelu_tanh(x),
353 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
354 }
355 }
356
357 #[inline]
360 pub fn combine(self, g: f32, u: f32) -> f32 {
361 match self {
362 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
363 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
364 }
365 _ => self.apply(g) * u,
366 }
367 }
368}
369
370pub struct DenseFfn {
372 pub gate_proj: QTensor,
373 pub up_proj: QTensor,
374 pub down_proj: QTensor,
375 pub act: Act,
377}
378
379pub enum FfnKind {
382 Dense(DenseFfn),
383 Moe(MoeFfn),
387 DenseMoe(Box<DenseMoeFfn>),
394}
395
396pub struct DenseMoeFfn {
398 pub dense: DenseFfn,
399 pub moe: MoeFfn,
400 pub post_norm_1: Vec<f32>,
402 pub pre_norm_2: Vec<f32>,
405 pub post_norm_2: Vec<f32>,
407}
408
409pub struct MoeFfn {
410 pub router: QTensor,
412 pub experts: Vec<DenseFfn>,
413 pub top_k: usize,
414 pub norm_topk_prob: bool,
415 pub router_sigmoid: bool,
418 pub expert_bias: Option<Vec<f32>>,
422 pub routed_scaling: f32,
425 pub route_tau: Option<f32>,
431 pub shared: Option<(DenseFfn, Option<QTensor>)>,
434 pub stats: std::cell::RefCell<Vec<u64>>,
438 pub act_sq: std::cell::RefCell<Vec<f64>>,
445 pub act_rows: std::cell::RefCell<Vec<f32>>,
451 pub mask: Option<Vec<bool>>,
456 pub per_expert_scale: Option<Vec<f32>>,
459 pub router_input_norm: bool,
463 pub resonance: Option<Resonance>,
467}
468
469pub struct Resonance {
471 pub mu: Vec<f32>,
473 pub u: Vec<f32>,
475 pub k: usize,
476 pub bias: Vec<f32>,
478}
479
480impl Resonance {
481 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
483 let h = x.len();
484 let ne = out.len();
485 for e in 0..ne {
486 let mu = &self.mu[e * h..(e + 1) * h];
487 let mut d2 = 0.0f32;
488 for j in 0..h {
489 let d = x[j] - mu[j];
490 d2 += d * d;
491 }
492 let mut proj = 0.0f32;
493 for i in 0..self.k {
494 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
495 let mut p = 0.0f32;
496 for j in 0..h {
497 p += (x[j] - mu[j]) * u[j];
498 }
499 proj += p * p;
500 }
501 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
502 }
503 }
504}
505
506pub enum AttnKind {
509 Full {
511 wq: QTensor,
512 wk: QTensor,
513 wv: QTensor,
514 wo: QTensor,
515 q_norm: Option<Vec<f32>>,
516 k_norm: Option<Vec<f32>>,
517 output_gate: bool,
518 softplus_gate: Option<(QTensor, bool)>,
522 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
524 },
525 Linear(VmfPhaseWeights),
527 LinearGdn(GdnWeights),
529 ShortConv(ShortConvWeights),
532 Mla(Box<MlaWeights>),
540 Kda(Box<crate::linear_core::KdaWeights>),
544}
545
546pub struct MlaWeights {
548 pub q_proj: QTensor,
552 pub q_a: Option<QTensor>,
555 pub q_a_norm: Option<Vec<f32>>,
556 pub kv_a: QTensor,
558 pub kv_a_norm: Vec<f32>,
560 pub kv_b: QTensor,
562 pub o_proj: QTensor,
564 pub nh: usize,
565 pub qk_rope: usize,
566 pub qk_nope: usize,
567 pub v_dim: usize,
568 pub lora: usize,
569 pub scale: f32,
571 pub nope: bool,
573}
574
575pub struct MtpModule {
580 pub enorm: Vec<f32>,
581 pub hnorm: Vec<f32>,
582 pub eh_proj: QTensor,
584 pub layer: LayerWeights,
585 pub final_norm: Vec<f32>,
586 pub kv: crate::kv_cache::LayerKvCache,
587}
588
589#[cfg(target_os = "macos")]
596enum MetalRowsItem<'a> {
597 Gdn {
598 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
599 first: usize,
600 },
601 Attn {
602 l: crate::gpu_metal::AttnGpuLayer<'a>,
603 li: usize,
604 q_norm: Option<&'a [f32]>,
605 k_norm: Option<&'a [f32]>,
606 output_gate: bool,
607 },
608}
609
610#[cfg(target_os = "macos")]
611struct MetalVerifyPending {
612 graph: crate::gpu_metal::VerifyGraph,
613 gdn_layers: Vec<usize>,
614 attn_layers: Vec<(usize, usize)>,
615}
616
617#[derive(Clone, Copy)]
621enum SpecTrial {
622 Spec {
623 t0: std::time::Instant,
624 gen0: usize,
625 rounds: usize,
626 },
627 Plain {
628 t0: std::time::Instant,
629 gen0: usize,
630 },
631 Decided {
632 spec: bool,
633 recheck_at: usize,
634 },
635}
636
637#[derive(Default, Clone, Copy)]
648struct SpecMon {
649 round_ms: f64,
650 tokens: f64,
651 plain_ms: f64,
652 n: u32,
653 fails: u32,
654}
655
656impl SpecMon {
657 fn round(&mut self, dt_ms: f64, produced: usize) {
658 self.n += 1;
659 if self.n == 1 {
660 return; }
662 let a = if self.n == 2 { 1.0 } else { 0.3 };
663 self.round_ms += a * (dt_ms - self.round_ms);
664 self.tokens += a * (produced as f64 - self.tokens);
665 }
666 fn pays(&self) -> bool {
667 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
668 }
669}
670
671pub struct GenerateResult {
673 pub text: String,
674 pub token_ids: Vec<u32>,
675 pub prompt_tokens: usize,
676 pub tokens_generated: usize,
677 pub finish_reason: String,
678 pub mtp_drafted: usize,
680 pub mtp_accepted: usize,
681 pub token_confidence: Vec<f32>,
686 pub traces: Vec<TokenTrace>,
689}
690
691#[derive(Clone, Debug)]
696pub struct TokenTrace {
697 pub t: usize,
699 pub token_id: u32,
701 pub confidence: f32,
703 pub active_skill: Option<String>,
705 pub recon: Option<f32>,
709 pub switched: bool,
712}
713
714#[cfg_attr(not(test), allow(dead_code))]
719fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
720 let t = if temp > 1e-3 { temp } else { 1.0 };
721 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
722 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
723 if sum > 0.0 {
724 (((logits[id as usize] - max) / t).exp()) / sum
725 } else {
726 0.0
727 }
728}
729
730fn prefill_batched() -> bool {
733 std::env::var("CMF_PREFILL")
734 .map(|v| v != "seq")
735 .unwrap_or(true)
736}
737
738#[derive(Clone, Copy)]
742enum PrefillIn<'a> {
743 Ids(&'a [u32]),
744 Hidden(&'a [f32]),
745}
746
747impl Pipeline {
754 fn can_prefill_batched(&self) -> bool {
755 prefill_batched() && !self.weights.layers.is_empty()
756 }
757}
758
759pub fn prefill_chunk() -> usize {
766 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
767 .ok()
768 .and_then(|v| v.parse::<usize>().ok())
769 {
770 return n.max(1);
771 }
772 if cfg!(target_os = "macos") {
773 512
774 } else if cfg!(target_arch = "aarch64") {
775 256
778 } else {
779 48
780 }
781}
782
783pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
785
786impl Pipeline {
787 #[inline]
791 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
792 virtual_idx % self.physical_layers
793 }
794
795 #[inline]
798 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
799 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
800 }
801
802 #[allow(clippy::too_many_arguments)]
804
805 #[cfg(target_os = "macos")]
824 fn graph_prefill_preferred(&self) -> bool {
825 if !crate::gpu::enabled_here()
826 || !crate::gpu::q1_force()
827 || std::env::var("CMF_GPU_BLOCK")
828 .map(|v| v == "0")
829 .unwrap_or(false)
830 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
833 {
834 return false;
835 }
836 self.weights
837 .layers
838 .iter()
839 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
840 }
841
842 #[cfg(not(target_os = "macos"))]
843 fn graph_prefill_preferred(&self) -> bool {
844 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
852 if !graph_on || !crate::gpu::enabled_here() {
853 return false;
854 }
855 if self.o1_active() {
864 return false;
865 }
866 self.weights
867 .layers
868 .iter()
869 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
870 }
871
872 #[cfg(target_os = "macos")]
873 fn q1_graph_gpu(
874 &mut self,
875 start: usize,
876 upto: Option<usize>,
877 position: usize,
878 h: &mut [f32],
879 ) -> usize {
880 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
882 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
884 || !crate::gpu::q1_force()
885 || std::env::var("CMF_GPU_BLOCK")
886 .map(|v| v == "0")
887 .unwrap_or(false)
888 {
889 if std::env::var("CMF_GRAPH_DBG").is_ok() {
890 eprintln!(
891 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
892 self.attn_softcap > 0.0,
893 crate::gpu::enabled_here(),
894 crate::gpu::q1_force(),
895 );
896 }
897 return start;
898 }
899 if self.swa.is_some()
904 || self.global_attn.is_some()
905 || self.attention_heads_per_layer.is_some()
906 || self.attn_v_norm
907 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
908 || self.weights.layers.iter().any(|lw| {
909 lw.attn_out_norm.is_some()
910 || lw.ffn_out_norm.is_some()
911 || lw.layer_scale.is_some()
912 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
913 })
914 {
915 if std::env::var("CMF_GRAPH_DBG").is_ok() {
916 eprintln!(
917 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
918 self.swa.is_some(),
919 self.global_attn.is_some(),
920 self.attention_heads_per_layer.is_some(),
921 self.attn_v_norm,
922 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
923 );
924 }
925 return start;
926 }
927 let limit = upto
930 .map(|u| u + 1)
931 .unwrap_or(self.num_layers)
932 .min(self.num_layers);
933
934 enum Item<'a> {
935 Gdn {
936 run: Vec<GdnGpuLayer<'a>>,
937 first: usize,
938 },
939 Attn {
940 l: AttnGpuLayer<'a>,
941 li: usize,
942 q_norm: Option<&'a [f32]>,
943 k_norm: Option<&'a [f32]>,
944 output_gate: bool,
945 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
946 full_gpu: bool,
949 },
950 }
951
952 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
959 let attend_contract = attend_mode != "0"
960 && attend_mode != "off"
961 && self.head_dim % 4 == 0
962 && self.head_dim <= 256
963 && self.rotary_dim >= 2
964 && self.rotary_dim <= self.head_dim
965 && (self.rotary_dim / 2) % 32 == 0
966 && self.num_kv_heads > 0
967 && self.num_heads % self.num_kv_heads == 0;
968
969 let mut plan: Vec<Item> = Vec::new();
970 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
971 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
973 let mut scan = start;
974 while scan < limit {
975 let lw = &self.weights.layers[self.phys_layer(scan)];
976 let ffn = match &lw.ffn {
977 FfnKind::Dense(d) => {
978 let (Some(g), Some(u), Some(dn)) = (
979 d.gate_proj.q1_parts(),
980 d.up_proj.q1_parts(),
981 d.down_proj.q1_parts(),
982 ) else {
983 if block_diag {
984 eprintln!(
985 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
986 );
987 }
988 break;
989 };
990 MetalFfn::Dense {
991 gate: g,
992 up: u,
993 down: dn,
994 }
995 }
996 FfnKind::Moe(m) => {
997 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
998 if block_diag {
999 eprintln!(
1000 "block-graph: L{scan} MoE outside the graph contract — run ends"
1001 );
1002 }
1003 break;
1004 };
1005 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1006 model_ref.get_or_insert_with(|| model.clone());
1007 }
1008 MetalFfn::Moe(moe)
1009 }
1010 _ => {
1011 if block_diag {
1012 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1013 }
1014 break;
1015 }
1016 };
1017 match &lw.attn {
1018 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1019 let parts = (
1020 w.in_proj_qkv.q1_parts(),
1021 w.in_proj_z.q1_parts(),
1022 w.in_proj_a.f32_parts(),
1023 w.in_proj_b.f32_parts(),
1024 w.out_proj.q1_parts(),
1025 );
1026 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1027 if block_diag {
1028 eprintln!(
1029 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1030 w.in_proj_qkv.q1_parts().is_some(),
1031 w.in_proj_z.q1_parts().is_some(),
1032 w.in_proj_a.f32_parts().is_some(),
1033 w.in_proj_b.f32_parts().is_some(),
1034 w.out_proj.q1_parts().is_some(),
1035 );
1036 }
1037 break;
1038 };
1039 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1040 model_ref.get_or_insert_with(|| model.clone());
1041 }
1042 let gl = GdnGpuLayer {
1043 attn_norm: &lw.input_norm,
1044 post_norm: &lw.post_norm,
1045 qkv,
1046 z,
1047 a,
1048 b,
1049 out,
1050 ffn,
1051 conv1d: &w.conv1d,
1052 a_log: &w.a_log,
1053 dt_bias: &w.dt_bias,
1054 gnorm: &w.norm,
1055 };
1056 match plan.last_mut() {
1057 Some(Item::Gdn { run, .. }) => run.push(gl),
1058 _ => plan.push(Item::Gdn {
1059 run: vec![gl],
1060 first: scan,
1061 }),
1062 }
1063 }
1064 AttnKind::Full {
1065 wq,
1066 wk,
1067 wv,
1068 wo,
1069 q_norm,
1070 k_norm,
1071 output_gate,
1072 softplus_gate: None,
1073 bias,
1074 } if !self.kv_cache.layers[scan].o1_sealed()
1075 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1080 {
1081 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1082 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1083 break;
1084 };
1085 if let QTensor::Mapped { model, .. } = wq {
1086 model_ref.get_or_insert_with(|| model.clone());
1087 }
1088 let cache = &self.kv_cache.layers[scan];
1089 let o1_metal = cache.o1.is_some()
1093 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1094 && cache.o1_views().is_some();
1095 let full_gpu = attend_contract
1096 && cache.mode == crate::kv_cache::KvMode::F32
1097 && (cache.o1.is_none() || o1_metal)
1098 && bias.is_none()
1099 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1100 && pk.1 == self.num_kv_heads * self.head_dim
1101 && pv.1 == self.num_kv_heads * self.head_dim
1102 && po.2 == self.num_heads * self.head_dim;
1103 plan.push(Item::Attn {
1104 l: AttnGpuLayer {
1105 attn_norm: &lw.input_norm,
1106 post_norm: &lw.post_norm,
1107 wq: pq,
1108 wk: pk,
1109 wv: pv,
1110 wo: po,
1111 ffn,
1112 },
1113 li: scan,
1114 q_norm: q_norm.as_deref(),
1115 k_norm: k_norm.as_deref(),
1116 output_gate: *output_gate,
1117 bias: bias
1118 .as_ref()
1119 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1120 full_gpu,
1121 });
1122 }
1123 _ => break,
1124 }
1125 scan += 1;
1126 }
1127 let Some(model) = model_ref else {
1128 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1129 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1130 }
1131 return start;
1132 };
1133 if plan.is_empty() {
1134 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1135 eprintln!("q1-graph: empty plan at layer {start}");
1136 }
1137 return start;
1138 }
1139 let has_moe = plan.iter().any(|it| match it {
1140 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1141 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1142 });
1143 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1144 let dev_attend = attend_contract
1145 && (self.head_dim <= 128
1146 || has_moe
1147 || (self.head_dim <= 256 && has_gdn)
1153 || attend_mode == "force"
1154 || attend_mode == "256");
1155 if !dev_attend {
1156 for it in &mut plan {
1157 if let Item::Attn { li, full_gpu, .. } = it {
1158 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1161 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1162 if !keep_o1 {
1163 *full_gpu = false;
1164 }
1165 }
1166 }
1167 }
1168 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1169 use std::sync::atomic::{AtomicBool, Ordering};
1170 static SAID: AtomicBool = AtomicBool::new(false);
1171 if !SAID.swap(true, Ordering::Relaxed) {
1172 let fg = plan
1173 .iter()
1174 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1175 .count();
1176 let att = plan
1177 .iter()
1178 .filter(|it| matches!(it, Item::Attn { .. }))
1179 .count();
1180 eprintln!(
1181 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1182 plan.len(),
1183 self.head_dim,
1184 self.rotary_dim,
1185 self.num_kv_heads,
1186 self.num_heads,
1187 );
1188 }
1189 }
1190 let dims = GraphDims {
1191 hidden: self.hidden_size,
1192 eps: self.rms_eps as f32,
1193 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1194 };
1195 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1196 return start;
1197 };
1198 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1199 nv: cfg.num_v_heads,
1200 nk: cfg.num_k_heads,
1201 dk: cfg.key_head_dim,
1202 dv: cfg.value_head_dim,
1203 kk: cfg.conv_kernel,
1204 hidden: self.hidden_size,
1205 inter: self.intermediate_size,
1206 c_dim: cfg.conv_dim(),
1207 eps: cfg.rms_eps as f32,
1208 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1209 });
1210 let mut valid = 0usize;
1214 let mut end = start;
1215 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1217 static ONCE: std::sync::Once = std::sync::Once::new();
1218 ONCE.call_once(|| {
1219 for it in &plan {
1220 match it {
1221 Item::Gdn { first, run } => {
1222 eprintln!("plan: Gdn first={first} len={}", run.len())
1223 }
1224 Item::Attn { li, full_gpu, .. } => {
1225 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1226 }
1227 }
1228 }
1229 });
1230 }
1231 for item in &plan {
1232 let ok = match item {
1233 Item::Gdn { run, .. } => gcfg
1234 .as_ref()
1235 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1236 .unwrap_or(false),
1237 Item::Attn { l, .. } => graph.attn_ok(l),
1238 };
1239 if !ok {
1240 if block_diag {
1241 eprintln!(
1242 "block-graph: plan item {} ({}) failed graph preflight",
1243 valid,
1244 match item {
1245 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1246 Item::Attn { li, .. } => format!("Attn L{li}"),
1247 }
1248 );
1249 }
1250 break;
1251 }
1252 valid += 1;
1253 end += match item {
1254 Item::Gdn { run, .. } => run.len(),
1255 Item::Attn { .. } => 1,
1256 };
1257 }
1258 plan.truncate(valid);
1259 if plan.is_empty() {
1260 return start;
1261 }
1262
1263 let inv_freq = self.inv_freq.clone();
1264 let pool = self.pool.clone();
1265 let (nh, nkv, hd, hs, rd, eps) = (
1266 self.num_heads,
1267 self.num_kv_heads,
1268 self.head_dim,
1269 self.hidden_size,
1270 self.rotary_dim,
1271 self.rms_eps,
1272 );
1273 let norm_style = self.norm_style;
1274 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1275 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1276 let kv_id = self.graph_kv_id;
1277 let mut pending: Vec<(usize, usize)> = Vec::new();
1280 let mut dev_attn: Vec<usize> = Vec::new();
1283 for item in &plan {
1284 let _xt0 = std::time::Instant::now();
1285 let _xkind: u32 = match item {
1286 Item::Gdn { .. } => 2,
1287 Item::Attn { .. } => 3,
1288 };
1289 if self.loop_final_norm {
1291 let item_start = match item {
1292 Item::Gdn { first, .. } => *first,
1293 Item::Attn { li, .. } => *li,
1294 };
1295 if item_start > start && self.is_loop_end(item_start - 1) {
1296 graph.encode_loop_norm(&self.weights.final_norm);
1297 }
1298 }
1299 match item {
1300 Item::Gdn { run, first } => {
1301 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1302 if l.linear_state.len() != want {
1303 l.linear_state = vec![0f32; want];
1304 }
1305 }
1306 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1307 .iter()
1308 .map(|l| l.linear_state.as_slice())
1309 .collect();
1310 let _ig = std::time::Instant::now();
1311 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1312 tracing::error!("q1 graph: GDN run refused after validation");
1314 return start;
1315 }
1316 graph.commit_kind = 2;
1319 graph.commit();
1320 crate::gpu::stageprof(0, _ig.elapsed());
1321 pending.push((*first, run.len()));
1322 }
1323 Item::Attn {
1324 l,
1325 li,
1326 q_norm,
1327 k_norm,
1328 output_gate,
1329 bias,
1330 full_gpu,
1331 } => {
1332 let _ia = std::time::Instant::now();
1333 if *full_gpu {
1335 let cache = &self.kv_cache.layers[*li];
1336 let o1p = if cache.o1.is_some() {
1337 match cache.o1_views() {
1338 Some(views) => Some(crate::gpu::O1AttnParams {
1339 views,
1340 epoch: self.o1_epoch,
1341 }),
1342 None => None,
1344 }
1345 } else {
1346 None
1347 };
1348 let o1_layer = cache.o1.is_some();
1349 if o1_layer && o1p.is_none() {
1350 }
1352 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1353 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1354 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1355 let p = crate::gpu::AttnDeviceParams {
1356 kv_id,
1357 layer: *li,
1358 nh,
1359 nkv,
1360 hd,
1361 rd,
1362 position,
1363 eps: eps as f32,
1364 gemma,
1365 output_gate: *output_gate,
1366 q_norm: *q_norm,
1367 k_norm: *k_norm,
1368 inv_freq: &inv_freq,
1369 cpu_k,
1370 cpu_v,
1371 cpu_stored,
1372 o1: o1p,
1373 };
1374 let o1_bad = o1_layer && p.o1.is_none();
1375 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1376 {
1377 if p.o1.is_none() {
1379 dev_attn.push(*li);
1380 }
1381 graph.commit_kind = 3;
1382 graph.commit();
1383 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1387 continue;
1388 }
1389 }
1391 graph.encode_attn_prefix(l);
1392 graph.sync();
1393 if !pending.is_empty() {
1394 let idxs: Vec<usize> =
1395 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1396 let mut outs: Vec<&mut [f32]> = self
1397 .kv_cache
1398 .layers
1399 .iter_mut()
1400 .enumerate()
1401 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1402 .map(|(_, s)| s.linear_state.as_mut_slice())
1403 .collect();
1404 graph.read_states(&mut outs);
1405 }
1406 let mut q_raw = attention::take_buf(l.wq.1);
1407 let mut k = attention::take_buf(l.wk.1);
1408 let mut v = attention::take_buf(l.wv.1);
1409 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1410 let cfg = QwenAttnCfg {
1411 num_heads: nh,
1412 num_kv_heads: nkv,
1413 head_dim: hd,
1414 hidden_size: hs,
1415 position,
1416 inv_freq: &inv_freq,
1417 rotary_dim: rd,
1418 scale: self.attn_scale,
1419 softcap: self.attn_softcap,
1420 window: None,
1421 v_norm: false,
1422 q_norm: *q_norm,
1423 k_norm: *k_norm,
1424 output_gate: *output_gate,
1425 softplus_gate: None,
1426 rope_scale: 1.0,
1427 bias: *bias,
1428 rms_eps: eps,
1429 norm_style,
1430 pool: pool.as_deref(),
1431 };
1432 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1435 || std::env::var("CMF_ATTN_DUMP").is_ok();
1436 let _ = full_gpu;
1437 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1438 let mut ao = attention::qwen_attention_core(
1439 q_raw,
1440 k,
1441 v,
1442 &mut self.kv_cache.layers[*li],
1443 &cfg,
1444 );
1445 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1449 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1450 let (cq, _cg, _ck, _cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1451 let cache = &self.kv_cache.layers[*li];
1452 let n = cache.head_keys(0).len() / hd;
1453 let mut bytes: Vec<u8> = Vec::new();
1454 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1455 bytes.extend_from_slice(&v.to_le_bytes());
1456 }
1457 for v in &cq {
1458 bytes.extend_from_slice(&v.to_le_bytes());
1459 }
1460 for g in 0..nkv {
1461 for v in cache.head_keys(g) {
1462 bytes.extend_from_slice(&v.to_le_bytes());
1463 }
1464 }
1465 for g in 0..nkv {
1466 for v in cache.head_values(g) {
1467 bytes.extend_from_slice(&v.to_le_bytes());
1468 }
1469 }
1470 let _ = std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1471 }
1472 }
1473 if let Some((qr0, k0, v0)) = oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")) {
1474 let (cq, _cg, ck, cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1475 let mut h_now = vec![0f32; hs];
1476 graph.read_h(&mut h_now);
1477 let cache = &self.kv_cache.layers[*li];
1478 let n_after = cache.head_keys(0).len() / hd;
1479 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_keys(g)[..(n_after - 1) * hd]).collect();
1480 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_values(g)[..(n_after - 1) * hd]).collect();
1481 let p = crate::gpu::AttnDeviceParams {
1482 kv_id,
1483 layer: *li,
1484 nh,
1485 nkv,
1486 hd,
1487 rd,
1488 position,
1489 eps: eps as f32,
1490 gemma,
1491 output_gate: *output_gate,
1492 q_norm: *q_norm,
1493 k_norm: *k_norm,
1494 inv_freq: &inv_freq,
1495 cpu_k,
1496 cpu_v,
1497 cpu_stored: n_after - 1,
1498 o1: None,
1499 };
1500 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1501 let md = |a: &[f32], b: &[f32]| a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
1502 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1503 eprintln!(
1504 "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}",
1505 nn(&cq), md(&cq, &dq), nn(&ck), md(&ck, &dk), nn(&cv), md(&cv, &dv), nn(&ao), md(&ao, &dao)
1506 );
1507 } else {
1508 eprintln!("attn-oracle L{li}: device probe declined");
1509 }
1510 }
1511 graph.encode_attn_suffix(l, &ao);
1512 graph.commit();
1515 attention::recycle_buf(&mut ao);
1516 }
1517 }
1518
1519 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1520 }
1521 let mut lm_rows = None;
1526 if self.graph_want_logits
1527 && upto.is_none()
1528 && end == self.num_layers
1529 && std::env::var("CMF_GPU_LMHEAD")
1530 .map(|v| v != "0")
1531 .unwrap_or(true)
1532 {
1533 if let Some(lm) = self.weights.lm_head.q1_parts() {
1534 if graph.lm_head_ok(lm) {
1535 graph.encode_lm_head(&self.weights.final_norm, lm);
1536 lm_rows = Some(lm.1);
1537 }
1538 }
1539 }
1540 let _sy0 = std::time::Instant::now();
1541 graph.sync();
1542 let _rs0 = std::time::Instant::now();
1543 if !pending.is_empty() {
1544 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1545 let mut outs: Vec<&mut [f32]> = self
1546 .kv_cache
1547 .layers
1548 .iter_mut()
1549 .enumerate()
1550 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1551 .map(|(_, s)| s.linear_state.as_mut_slice())
1552 .collect();
1553 graph.read_states(&mut outs);
1554 }
1555 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1556 use std::sync::atomic::{AtomicU64, Ordering};
1557 static SY: AtomicU64 = AtomicU64::new(0);
1558 static RS: AtomicU64 = AtomicU64::new(0);
1559 static N: AtomicU64 = AtomicU64::new(0);
1560 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1561 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1562 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1563 if n % 100 == 0 {
1564 eprintln!(
1565 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1566 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1567 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1568 );
1569 }
1570 }
1571 if let Some(rows) = lm_rows {
1572 crate::gpu::hostprof_encode_done(_mt0);
1573 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1574 graph.read_logits(&mut lg);
1575 crate::gpu::hostprof_total(_mt0);
1576 lg.resize(self.vocab_size, 0.0);
1577 if let Some(c) = self.final_softcap {
1578 for l in lg.iter_mut() {
1579 *l = c * (*l / c).tanh();
1580 }
1581 }
1582 self.graph_logits = Some(lg);
1583 }
1584 graph.finish(h);
1585 for li in dev_attn {
1589 let mut krow = attention::take_buf(nkv * hd);
1590 let mut vrow = attention::take_buf(nkv * hd);
1591 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1592 let cache = &mut self.kv_cache.layers[li];
1593 cache.append(&krow, &vrow, &[]);
1594 let n = cache.seq_len;
1595 let mut imp = attention::take_buf(n);
1596 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1597 cache.accumulate_imp(&imp);
1598 attention::recycle_buf(&mut imp);
1599 }
1600 attention::recycle_buf(&mut krow);
1601 attention::recycle_buf(&mut vrow);
1602 }
1603 end
1604 }
1605
1606 pub fn new(
1607 tokenizer: Tokenizer,
1608 weights: PipelineWeights,
1609 hidden_size: usize,
1610 intermediate_size: usize,
1611 num_heads: usize,
1612 num_kv_heads: usize,
1613 head_dim: usize,
1614 num_layers: usize,
1615 physical_layers: usize,
1616 loop_final_norm: bool,
1617 vocab_size: usize,
1618 rms_eps: f64,
1619 rope_base: f32,
1620 norm_style: NormStyle,
1621 max_seq_len: usize,
1622 sampler_config: SamplerConfig,
1623 ) -> Self {
1624 let rng = match sampler_config.seed {
1625 Some(s) => SplitMix64::new(s),
1626 None => SplitMix64::from_entropy(),
1627 };
1628 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1629 let pool = Pool::from_env();
1630 if let Some(p) = &pool {
1631 tracing::info!("worker pool: {} threads", p.n_workers());
1632 }
1633 Self {
1634 gpu_plan: None,
1635 tokenizer: std::sync::Arc::new(tokenizer),
1636 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1637 sampler_config,
1638 weights,
1639 hidden_size,
1640 intermediate_size,
1641 num_heads,
1642 num_kv_heads,
1643 head_dim,
1644 num_layers,
1645 physical_layers,
1646 loop_final_norm,
1647 vocab_size,
1648 rms_eps,
1649 rope_base,
1650 norm_style,
1651 rotary_dim: head_dim,
1652 attention_heads_per_layer: None,
1653 vmf_cfg: None,
1654 gdn_cfg: None,
1655 kda_cfg: None,
1656 g3n: None,
1657 dsv4: None,
1658 dsv4_mtp: Vec::new(),
1659 dspark: None,
1660 dspark_pending: Vec::new(),
1661 dspark_hist: Vec::new(),
1662 dspark_real: Vec::new(),
1663 dspark_trunk_picks: Vec::new(),
1664 dspark_exp: Vec::new(),
1665 dspark_draft_ns: 0,
1666 logit_multiplier: None,
1667 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1668 kv_history: Vec::new(),
1669 short_conv_cfg: None,
1670 mtp: None,
1671 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1672 rng,
1673 sampler_scratch: SamplerScratch::default(),
1674 spec_forced: None,
1675 spec_q: Vec::new(),
1676 spec_p: Vec::new(),
1677 spec_res: Vec::new(),
1678 spec_qs: Vec::new(),
1679 spec_ps: Vec::new(),
1680 spec_ress: Vec::new(),
1681 mtp_graph_mode: None,
1682 #[cfg(target_os = "macos")]
1683 metal_verify: None,
1684 inv_freq,
1685 ws: ForwardScratch::new(hidden_size),
1686 pool,
1687 model: None,
1688 dyn_force_f32: false,
1689 dyn_skill_layers: Vec::new(),
1690 dyn_active: None,
1691 dyn_blend_loaded: false,
1692 dyn_phi_layer: None,
1693 dyn_phi_ema: Vec::new(),
1694 dyn_phi_seen: 0,
1695 dyn_router: None,
1696 o1_cfg: None,
1697 o1_epoch: 0,
1698 o1_flags: Vec::new(),
1699 trace: false,
1700 calib_temp: 1.0,
1701 confidence_on: true,
1702 embed_multiplier: 1.0,
1703 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1704 swa: None,
1705 sliding_layers: None,
1706 inv_freq_local: None,
1707 rotary_dim_local: None,
1708 rope_scale: 1.0,
1709 rope_scale_local: 1.0,
1710 global_attn: None,
1711 inv_freq_global: None,
1712 attn_v_norm: false,
1713 final_softcap: None,
1714 head_clusters: None,
1715 attn_softcap: 0.0,
1716 graph_want_logits: false,
1717 graph_logits: None,
1718 graph_kv_id: {
1719 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1720 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1721 },
1722 }
1723 }
1724
1725 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1732 self.o1_flags = match &cfg {
1733 Some(c) => {
1734 let mut flags = c.layer_flags(self.num_layers);
1735 for (li, f) in flags.iter_mut().enumerate() {
1736 if *f
1737 && !matches!(
1738 self.weights.layers[self.phys_layer(li)].attn,
1739 AttnKind::Full { .. }
1740 )
1741 {
1742 *f = false;
1743 }
1744 }
1745 flags
1746 }
1747 None => Vec::new(),
1748 };
1749 if let Some(c) = &cfg {
1750 let n = self.o1_flags.iter().filter(|&&f| f).count();
1751 tracing::info!(
1752 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1753 self.num_layers,
1754 c.m,
1755 c.w,
1756 c.sink,
1757 c.rect
1758 );
1759 }
1760 self.o1_cfg = cfg;
1761 }
1762
1763 pub fn o1_active(&self) -> bool {
1765 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1766 }
1767
1768 pub fn o1_begin(&mut self) {
1773 if let Some(c) = &self.o1_cfg {
1774 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1775 for (li, &f) in self.o1_flags.iter().enumerate() {
1776 if f {
1777 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1778 }
1779 }
1780 }
1781 }
1782
1783 pub fn o1_seal(&mut self) {
1787 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1788 if self.o1_cfg.is_none() {
1789 return;
1790 }
1791 for li in 0..self.num_layers {
1792 if self.o1_flags.get(li).copied().unwrap_or(false) {
1793 self.kv_cache.layers[li].o1_seal(self.num_heads);
1794 }
1795 }
1796 }
1797
1798 pub fn set_trace(&mut self, on: bool) {
1800 self.trace = on;
1801 }
1802
1803 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1806 self.rng = match config.seed {
1807 Some(seed) => SplitMix64::new(seed),
1808 None => SplitMix64::from_entropy(),
1809 };
1810 self.sampler_config = config;
1811 }
1812
1813 pub fn set_confidence(&mut self, on: bool) {
1818 self.confidence_on = on;
1819 }
1820
1821 pub fn set_calib_temp(&mut self, t: f32) {
1824 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1825 }
1826
1827 pub fn calib_temp(&self) -> f32 {
1829 self.calib_temp
1830 }
1831
1832 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1835 self.rotary_dim = rotary_dim.min(self.head_dim);
1836 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1837 }
1838
1839 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1840 QwenAttnCfg {
1841 num_heads: self.num_heads,
1842 num_kv_heads: self.num_kv_heads,
1843 head_dim: self.head_dim,
1844 hidden_size: self.hidden_size,
1845 position,
1846 inv_freq: &self.inv_freq,
1847 rotary_dim: self.rotary_dim,
1848 scale: self.attn_scale,
1849 softcap: self.attn_softcap,
1850 window: None,
1851 v_norm: false,
1852 q_norm: None,
1853 k_norm: None,
1854 output_gate: false,
1855 softplus_gate: None,
1856 rope_scale: self.rope_scale,
1857 bias: None,
1858 rms_eps: self.rms_eps,
1859 norm_style: self.norm_style,
1860 pool: self.pool.as_deref(),
1861 }
1862 }
1863
1864 pub fn generate(
1866 &mut self,
1867 prompt: &str,
1868 max_tokens: usize,
1869 task_mask: Option<&TaskMask>,
1870 on_token: Option<TokenCallback>,
1871 ) -> Result<GenerateResult, String> {
1872 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1873 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1874 }
1875
1876 pub fn generate_from_ids(
1884 &mut self,
1885 input_ids: &[u32],
1886 max_tokens: usize,
1887 task_mask: Option<&TaskMask>,
1888 mut on_token: Option<TokenCallback>,
1889 ) -> Result<GenerateResult, String> {
1890 if std::env::var("CMF_TRACE_H").is_ok() {
1891 eprintln!("input_ids: {input_ids:?}");
1892 }
1893 if input_ids.is_empty() {
1894 return Err("empty prompt: nothing to generate from".to_string());
1895 }
1896
1897 let reuse_from = {
1905 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1906 let h = &self.kv_history;
1907 if on
1908 && task_mask.is_none()
1909 && self.mtp.is_none()
1910 && self.o1_cfg.is_none()
1911 && !h.is_empty()
1912 && h.len() < input_ids.len()
1913 && input_ids[..h.len()] == h[..]
1914 {
1915 h.len()
1916 } else {
1917 0
1918 }
1919 };
1920 if reuse_from == 0 {
1921 self.kv_cache.clear();
1923 self.kv_history.clear();
1924 crate::gpu::graph_kv_reset(self.graph_kv_id);
1925 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1926 eprintln!(
1927 "kv-reuse: {} of {} prompt positions already cached",
1928 reuse_from,
1929 input_ids.len()
1930 );
1931 }
1932 crate::gpu::graph_race_begin_generation();
1933 self.o1_begin();
1934
1935 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
1941 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
1968 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
1969 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
1985 for lw in &self.weights.layers {
1986 if let FfnKind::Dense(d) = &lw.ffn {
1987 dense_n += 1;
1988 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
1989 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
1990 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
1991 {
1992 dense_q4tp += 1;
1993 }
1994 }
1995 }
1996 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
1997 let penalized = self.sampler_config.repetition_penalty != 1.0
2001 || self.sampler_config.presence_penalty != 0.0
2002 || !self.sampler_config.suppress_tokens.is_empty();
2003 #[cfg(feature = "gpu")]
2008 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2009 #[cfg(not(feature = "gpu"))]
2010 let metal_wgpu = false;
2011 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2012 let spec_wanted = match spec_env.as_deref() {
2013 Some("0") => false,
2014 Some(_) => {
2015 if metal_wgpu {
2016 tracing::warn!(
2017 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2018 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2019 );
2020 }
2021 true
2022 }
2023 None => spec_default_ok && !penalized && !metal_wgpu,
2024 };
2025 #[cfg(target_os = "macos")]
2028 let metal_graph = crate::gpu::q1_force()
2029 && crate::gpu::enabled_here()
2030 && std::env::var("CMF_GPU_BLOCK").map(|v| v != "0").unwrap_or(true);
2031 #[cfg(not(target_os = "macos"))]
2032 let metal_graph = false;
2033 let graph_spec = self.speculative
2034 && (graph_on || metal_graph)
2035 && self.mtp.is_some()
2036 && task_mask.is_none()
2037 && !self.o1_active()
2038 && spec_sampling_ok
2039 && spec_wanted;
2040 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2047 let spec_active = self.speculative
2048 && self.mtp.is_some()
2049 && task_mask.is_none()
2050 && !self.o1_active()
2051 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2052 let mut mtp = if spec_active { self.mtp.take() } else { None };
2055 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2056 eprintln!(
2057 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2058 mtp.is_some(),
2059 self.speculative,
2060 self.sampler_config.temperature < 1e-6,
2061 );
2062 }
2063 if let Some(m) = &mut mtp {
2064 m.kv.clear();
2065 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2067 self.mtp_graph_mode = None;
2068 }
2069 let mut router = if mtp.is_none() {
2073 self.dyn_router.take()
2074 } else {
2075 None
2076 };
2077 if let Some(r) = &mut router {
2078 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2081 }
2082
2083 let mut all_ids = input_ids.to_vec();
2084 let mut generated = 0usize;
2085 let mut finish_reason = "max_tokens".to_string();
2086 let mut drafted = 0usize;
2087 let mut accepted = 0usize;
2088 let mut confidence: Vec<f32> = Vec::new();
2089 let trace_on = self.trace;
2090 let calib_temp = self.calib_temp;
2091 let mut traces: Vec<TokenTrace> = Vec::new();
2092
2093 let mut hidden = vec![0.0f32; self.hidden_size];
2099 let mut pos = reuse_from;
2100 let fuse_lm = mtp.is_none()
2109 && router.is_none()
2110 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2111 self.graph_logits = None;
2112 self.graph_want_logits = false;
2113 let _tpf = std::time::Instant::now();
2114 let batch_k = std::env::var("CMF_BATCH_K")
2115 .ok()
2116 .and_then(|v| v.parse::<usize>().ok())
2117 .unwrap_or(0);
2118 while self.dsv4.is_some()
2129 && mtp.is_none()
2130 && pos < input_ids.len()
2131 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2132 {
2133 let end = (pos + prefill_chunk()).min(input_ids.len());
2134 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2135 let mut lg = Vec::new();
2136 if let Some(b) = &mut self.dsv4 {
2137 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2138 crate::dsv4::forward_chunk(
2139 g,
2140 layers,
2141 &cfg,
2142 st,
2143 &ids,
2144 pos,
2145 &self.inv_freq,
2146 self.pool.as_deref(),
2147 &mut lg,
2148 end == input_ids.len(),
2149 );
2150 }
2151 if end == input_ids.len() {
2152 self.graph_logits = Some(lg);
2153 }
2154 pos = end;
2155 hidden = vec![0.0; self.hidden_size];
2156 }
2157 let dyn_prefill = router.is_some();
2162 let graph_prefill = self.graph_prefill_preferred();
2168 #[cfg(target_os = "macos")]
2176 if task_mask.is_none()
2177 && !dyn_prefill
2178 && crate::gpu::q1_force()
2179 && crate::gpu::enabled_here()
2180 && self.gdn_cfg.is_some()
2181 && self.g3n.is_none()
2182 && input_ids.len() > 8
2183 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2184 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2185 {
2186 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2187 .ok()
2188 .and_then(|v| v.parse().ok())
2189 .filter(|&v| (16..=512).contains(&v))
2190 .unwrap_or(256);
2191 let hs = self.hidden_size;
2192 let _tp = std::time::Instant::now();
2193 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2194 let end = (pos + chunk).min(input_ids.len());
2195 let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2196 break;
2197 };
2198 if let Some(m) = &mut mtp {
2199 let n_pairs = if end < input_ids.len() { end - pos } else { end - pos - 1 };
2200 if n_pairs > 0 {
2201 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2202 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2203 .collect();
2204 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2205 for (j, (h, t)) in pairs.iter().enumerate() {
2206 let h = h.to_vec();
2207 let _ = self.mtp_step(m, &h, *t, pos + j);
2208 }
2209 }
2210 }
2211 }
2212 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2213 pos = end;
2214 }
2215 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2216 eprintln!(
2217 "metal-prefill: {} of {} tokens in {:.1} ms",
2218 pos,
2219 input_ids.len(),
2220 _tp.elapsed().as_secs_f64() * 1e3
2221 );
2222 }
2223 }
2224 if task_mask.is_none()
2225 && !dyn_prefill
2226 && !graph_prefill
2227 && self.can_prefill_batched()
2228 && self.g3n.is_none()
2229 && input_ids.len() > 2
2230 {
2231 let chunk = prefill_chunk();
2237 let hs = self.hidden_size;
2238 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2239 let end = (pos + chunk).min(input_ids.len());
2240 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2241 if let Some(m) = &mut mtp {
2242 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2243 .ok()
2244 .and_then(|v| v.parse().ok())
2245 .unwrap_or(0);
2246 for p in pos..end {
2247 if p + 1 < input_ids.len() {
2248 if probe >= 1 && p + 2 < input_ids.len() {
2249 let (d1, mut hx) = self.mtp_step_h(
2253 m,
2254 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2255 input_ids[p + 1],
2256 p,
2257 );
2258 let mut ok = d1 == input_ids[p + 2];
2259 Self::chain_probe_note(0, ok);
2260 let mut d_prev = d1;
2261 let mut extra = 0usize;
2262 for j in 1..probe {
2263 if p + 2 + j >= input_ids.len() {
2264 break;
2265 }
2266 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2267 extra += 1;
2268 ok = ok && dj == input_ids[p + 2 + j];
2269 Self::chain_probe_note(j, ok);
2270 d_prev = dj;
2271 hx = hj;
2272 }
2273 m.kv.truncate_last(extra);
2274 } else {
2275 let _ = self.mtp_step(
2276 m,
2277 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2278 input_ids[p + 1],
2279 p,
2280 );
2281 }
2282 }
2283 }
2284 }
2285 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2286 pos = end;
2287 }
2288 }
2289 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2290 if task_mask.is_none()
2291 && !dyn_prefill
2292 && !graph_prefill
2293 && !pair_off
2294 && self.pair_supported()
2295 {
2296 while pos + 1 < input_ids.len()
2297 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2298 {
2299 let e1 = self.embed_single(input_ids[pos]);
2300 let e2 = self.embed_single(input_ids[pos + 1]);
2301 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2302 self.commit_linear_scratch();
2304 if let Some(m) = &mut mtp {
2305 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2306 if pos + 2 < input_ids.len() {
2307 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2308 .ok()
2309 .and_then(|v| v.parse().ok())
2310 .unwrap_or(0);
2311 if probe >= 1 && pos + 3 < input_ids.len() {
2312 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2316 let mut ok = d1 == input_ids[pos + 3];
2317 Self::chain_probe_note(0, ok);
2318 let mut d_prev = d1;
2319 let mut extra = 0usize;
2320 for j in 1..probe {
2321 if pos + 3 + j >= input_ids.len() {
2322 break;
2323 }
2324 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2325 extra += 1;
2326 ok = ok && dj == input_ids[pos + 3 + j];
2327 Self::chain_probe_note(j, ok);
2328 d_prev = dj;
2329 hx = hj;
2330 }
2331 m.kv.truncate_last(extra);
2332 } else {
2333 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2334 }
2335 }
2336 }
2337 hidden = h2;
2338 pos += 2;
2339 }
2340 }
2341 if batch_k > 0
2350 && graph_prefill
2351 && task_mask.is_none()
2352 && !self.o1_active()
2353 && mtp.is_none()
2354 && !dyn_prefill
2355 && pos + 1 < input_ids.len()
2356 {
2357 let hs = self.hidden_size;
2358 let chunk = batch_k;
2359 while pos < input_ids.len() {
2360 let end = (pos + chunk).min(input_ids.len());
2361 let bk = end - pos;
2362 let mut hiddens = vec![0f32; bk * hs];
2363 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2364 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2365 }
2366 let positions: Vec<usize> = (pos..end).collect();
2367 let t_chunk = std::time::Instant::now();
2368 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2369 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2370 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2371 eprintln!(
2372 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
2373 bk as f64 / (ms / 1000.0)
2374 );
2375 }
2376 {
2377 use std::sync::atomic::{AtomicBool, Ordering};
2378 static SAID: AtomicBool = AtomicBool::new(false);
2379 if !SAID.swap(true, Ordering::Relaxed) {
2380 if ok_b {
2381 tracing::info!("batched prefill: ACTIVE (k={bk})");
2382 } else {
2383 tracing::warn!("batched prefill declined — per-position graph");
2384 }
2385 }
2386 }
2387 if ok_b {
2388 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2389 pos = end;
2390 } else {
2391 break; }
2393 }
2394 }
2395 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2396 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2397 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2398 if let Some(m) = &mut mtp {
2399 if pos + 1 < input_ids.len() {
2400 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2406 .ok()
2407 .and_then(|v| v.parse().ok())
2408 .unwrap_or(0);
2409 if probe >= 1 && pos + 2 < input_ids.len() {
2410 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2411 let mut ok = d1 == input_ids[pos + 2];
2412 Self::chain_probe_note(0, ok);
2413 let mut d_prev = d1;
2414 let mut extra = 0usize;
2415 for j in 1..probe {
2416 if pos + 2 + j >= input_ids.len() {
2417 break;
2418 }
2419 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2420 extra += 1;
2421 ok = ok && dj == input_ids[pos + 2 + j];
2422 Self::chain_probe_note(j, ok);
2423 d_prev = dj;
2424 hx = hj;
2425 }
2426 m.kv.truncate_last(extra);
2429 } else {
2430 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2431 }
2432 }
2433 }
2434 pos += 1;
2435 }
2436 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2437 eprintln!(
2438 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2439 input_ids.len(),
2440 _tpf.elapsed().as_secs_f64() * 1000.0
2441 );
2442 }
2443 if self
2446 .cancel
2447 .swap(false, std::sync::atomic::Ordering::Relaxed)
2448 {
2449 self.kv_history.clear();
2450 if let Some(m) = mtp {
2451 self.mtp = Some(m);
2452 }
2453 return Ok(GenerateResult {
2454 text: String::new(),
2455 token_ids: Vec::new(),
2456 prompt_tokens: input_ids.len(),
2457 tokens_generated: 0,
2458 finish_reason: "cancelled".to_string(),
2459 mtp_drafted: 0,
2460 mtp_accepted: 0,
2461 token_confidence: Vec::new(),
2462 traces: Vec::new(),
2463 });
2464 }
2465
2466 self.o1_seal();
2469
2470 macro_rules! commit {
2472 ($id:expr) => {{
2473 all_ids.push($id);
2474 generated += 1;
2475 if self.tokenizer.is_eos($id) {
2476 finish_reason = "stop".to_string();
2477 false
2478 } else {
2479 let token_text = self.tokenizer.decode_token($id);
2480 let mut go = true;
2481 if let Some(ref mut cb) = on_token {
2482 if !cb(&token_text) {
2483 finish_reason = "cancelled".to_string();
2484 go = false;
2485 }
2486 }
2487 go
2488 }
2489 }};
2490 }
2491
2492 let mut spec_trial = SpecTrial::Spec {
2503 t0: std::time::Instant::now(),
2504 gen0: generated,
2505 rounds: 0,
2506 };
2507 let mut spec_mon = SpecMon::default();
2508 let mut spec_watchdog_off = false;
2509 let mut next_pos = input_ids.len();
2511 'decode: while generated < max_tokens {
2512 if self
2513 .cancel
2514 .swap(false, std::sync::atomic::Ordering::Relaxed)
2515 {
2516 finish_reason = "cancelled".to_string();
2517 break 'decode;
2518 }
2519 let forced = self.spec_forced.take();
2524 let mut logits = match (forced, self.graph_logits.take()) {
2525 (Some(_), _) => Vec::new(),
2526 (None, Some(lg)) => lg,
2527 (None, None) => {
2528 inference::rms_norm_into(
2529 &hidden,
2530 &self.weights.final_norm,
2531 self.rms_eps,
2532 self.norm_style,
2533 &mut self.ws.n1,
2534 );
2535 self.lm_head_forward(&self.ws.n1)
2536 }
2537 };
2538 if generated
2541 == std::env::var("CMF_LOGIT_DUMP_STEP")
2542 .ok()
2543 .and_then(|v| v.parse().ok())
2544 .unwrap_or(0)
2545 {
2546 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
2547 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
2548 for v in hidden.iter().chain(logits.iter()) {
2549 bytes.extend_from_slice(&v.to_le_bytes());
2550 }
2551 let _ = std::fs::write(&path, &bytes);
2552 }
2553 }
2554 let t_next = match forced {
2555 Some(c) => c,
2556 None => sampler::sample_with_scratch_pool(
2557 &logits,
2558 &self.sampler_config,
2559 &all_ids,
2560 &mut self.rng,
2561 &mut self.sampler_scratch,
2562 self.pool.as_deref(),
2563 ),
2564 };
2565 if self.confidence_on {
2566 confidence.push(if logits.is_empty() {
2567 0.0
2568 } else {
2569 sampler::top1_prob_pool(
2570 self.pool.as_deref(),
2571 &mut self.sampler_scratch,
2572 &logits,
2573 t_next,
2574 calib_temp,
2575 )
2576 });
2577 }
2578 if !logits.is_empty() {
2579 attention::recycle_buf(&mut logits);
2580 }
2581 if trace_on {
2582 let skill = router.as_ref().and_then(|r| r.active_id());
2586 traces.push(TokenTrace {
2587 t: generated,
2588 token_id: t_next,
2589 confidence: confidence.last().copied().unwrap_or(0.0),
2590 active_skill: skill,
2591 recon: None,
2592 switched: false,
2593 });
2594 }
2595 if !commit!(t_next) {
2596 break 'decode;
2597 }
2598 if generated >= max_tokens {
2599 break 'decode;
2600 }
2601
2602 if self.kv_cache.needs_eviction() {
2603 static SAID: std::sync::Once = std::sync::Once::new();
2609 SAID.call_once(|| {
2610 tracing::warn!(
2611 "KV cache full at {} positions — evicting half; quality \
2612 will degrade. Raise CMF_MAX_SEQ.",
2613 self.kv_cache.max_seq_len,
2614 );
2615 });
2616 let keep = (self.kv_cache.max_seq_len / 2).max(1);
2617 self.kv_cache.evict(keep);
2618 }
2619
2620 if graph_spec {
2623 match spec_trial {
2624 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
2625 spec_mon.plain_ms =
2626 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
2627 let keep = spec_mon.pays();
2628 tracing::info!(
2629 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2630 spec_mon.tokens,
2631 spec_mon.round_ms,
2632 spec_mon.plain_ms,
2633 if keep { "speculating" } else { "plain" }
2634 );
2635 spec_mon.fails = 0;
2636 spec_trial = SpecTrial::Decided {
2637 spec: keep,
2638 recheck_at: if keep { usize::MAX } else { generated + 128 },
2639 };
2640 }
2641 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
2642 spec_mon.n = 0;
2643 spec_trial = SpecTrial::Spec {
2644 t0: std::time::Instant::now(),
2645 gen0: generated,
2646 rounds: 0,
2647 };
2648 }
2649 _ => {}
2650 }
2651 spec_watchdog_off = matches!(
2652 spec_trial,
2653 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
2654 );
2655 }
2656 match &mut mtp {
2657 #[cfg(feature = "gpu")]
2659 Some(m)
2660 if graph_spec
2661 && !spec_watchdog_off
2662 && generated + 1 < max_tokens
2663 && next_pos > 0 =>
2664 {
2665 let t_round = std::time::Instant::now();
2666 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
2667 m,
2668 &hidden,
2669 t_next,
2670 next_pos,
2671 &mut drafted,
2672 &mut accepted,
2673 &mut all_ids,
2674 ) {
2675 next_pos = n_pos;
2676 hidden = new_h;
2677 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2678 eprintln!(
2679 "spec-round wall {:.1} ms → {} tokens",
2680 t_round.elapsed().as_secs_f64() * 1e3,
2681 extra.len() + 1
2682 );
2683 }
2684 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
2688 spec_trial = Self::spec_trial_round(
2691 spec_trial,
2692 &mut spec_mon,
2693 generated + extra.len() + 1,
2694 );
2695 let mut stopped = false;
2696 for &id in &extra {
2697 if self.confidence_on {
2698 confidence.push(0.0);
2699 }
2700 if !commit!(id) {
2701 stopped = true;
2702 break;
2703 }
2704 }
2705 if stopped {
2706 break 'decode;
2707 }
2708 continue 'decode;
2709 }
2710 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
2721 spec_mon.tokens = 0.0;
2722 spec_mon.fails = 3;
2723 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
2724 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2725 next_pos += 1;
2726 continue 'decode;
2727 }
2728 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2730 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2731 drafted += 1;
2732 let emb1 = self.embed_single(t_next);
2733 let emb2 = self.embed_single(draft);
2734 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2735
2736 inference::rms_norm_into(
2737 &h1,
2738 &self.weights.final_norm,
2739 self.rms_eps,
2740 self.norm_style,
2741 &mut self.ws.n1,
2742 );
2743 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2744 let t_after = sampler::sample_with_scratch_pool(
2745 &logits1,
2746 &self.sampler_config,
2747 &all_ids,
2748 &mut self.rng,
2749 &mut self.sampler_scratch,
2750 self.pool.as_deref(),
2751 );
2752 if self.confidence_on {
2753 confidence.push(sampler::top1_prob_pool(
2754 self.pool.as_deref(),
2755 &mut self.sampler_scratch,
2756 &logits1,
2757 t_after,
2758 calib_temp,
2759 ));
2760 }
2761 attention::recycle_buf(&mut logits1);
2762 if trace_on {
2763 traces.push(TokenTrace {
2766 t: generated,
2767 token_id: t_after,
2768 confidence: confidence.last().copied().unwrap_or(0.0),
2769 active_skill: None,
2770 recon: None,
2771 switched: false,
2772 });
2773 }
2774 let stop = !commit!(t_after);
2775
2776 if t_after == draft {
2777 accepted += 1;
2778 self.commit_linear_scratch();
2779 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2780 hidden = h2;
2781 next_pos += 2;
2782 } else {
2783 for layer in &mut self.kv_cache.layers {
2785 layer.truncate_last(1);
2786 }
2787 if !stop {
2788 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2789 hidden = self.forward_layers(
2790 &self.embed_single(t_after),
2791 next_pos + 1,
2792 None,
2793 );
2794 }
2795 next_pos += 2;
2796 }
2797 if stop {
2798 break 'decode;
2799 }
2800 }
2801 _ => {
2803 #[cfg(feature = "gpu")]
2808 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2809 static SAID: std::sync::Once = std::sync::Once::new();
2810 SAID.call_once(|| {
2811 eprintln!(
2812 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2813 !self.dsv4_mtp.is_empty(),
2814 task_mask.is_none(),
2815 router.is_none(),
2816 !trace_on,
2817 self.sampler_config.temperature < 1e-6,
2818 self.sampler_config.repetition_penalty == 1.0,
2819 );
2820 });
2821 }
2822 #[cfg(feature = "gpu")]
2823 if Self::dsv4_spec_on()
2824 && self.dsv4.is_some()
2825 && !self.dsv4_mtp.is_empty()
2826 && task_mask.is_none()
2827 && router.is_none()
2828 && !trace_on
2829 && self.sampler_config.temperature < 1e-6
2830 && self.sampler_config.repetition_penalty == 1.0
2831 && generated + 1 < max_tokens
2832 && all_ids.len() >= 2
2833 {
2834 let tip_token = all_ids[all_ids.len() - 2];
2835 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2836 tip_token,
2837 t_next,
2838 next_pos,
2839 &mut drafted,
2840 &mut accepted,
2841 ) {
2842 next_pos = n_pos;
2843 let mut stopped = false;
2844 for &id in &extra {
2845 if self.confidence_on {
2846 confidence.push(0.0);
2847 }
2848 if !commit!(id) {
2849 stopped = true;
2850 break;
2851 }
2852 }
2853 if stopped {
2854 break 'decode;
2855 }
2856 continue 'decode;
2857 }
2858 }
2859 self.graph_want_logits = fuse_lm;
2860 let mut t_fwd = t_next;
2866 let pure_greedy = self.sampler_config.temperature < 1e-6
2867 && self.sampler_config.repetition_penalty == 1.0
2868 && self.sampler_config.suppress_tokens.is_empty();
2869 let burst_k = std::env::var("CMF_MULTISTEP")
2874 .ok()
2875 .and_then(|v| v.parse::<usize>().ok())
2876 .unwrap_or(0);
2877 if pure_greedy
2878 && burst_k >= 1
2879 && fuse_lm
2880 && task_mask.is_none()
2881 && router.is_none()
2882 && !trace_on
2883 && !self.confidence_on
2884 {
2885 let mut stopped = false;
2886 loop {
2887 let room = max_tokens.saturating_sub(generated);
2888 if room <= 2 {
2889 break;
2890 }
2891 let k = burst_k.min(room - 1);
2892 if k < 1 {
2893 break;
2894 }
2895 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2896 break;
2897 };
2898 next_pos += k;
2899 for &id in &ids {
2900 if !commit!(id) {
2901 stopped = true;
2902 break;
2903 }
2904 }
2905 if stopped {
2906 break;
2907 }
2908 t_fwd = *ids.last().unwrap();
2909 }
2910 if stopped {
2911 break 'decode;
2912 }
2913 }
2914 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2915 next_pos += 1;
2916 if let Some(r) = &mut router {
2919 let phi = self.dyn_phi_ema.clone();
2920 let decision = r.step(&phi, generated);
2921 if let Some(new_active) = decision {
2922 let _ = self.set_active_skill(new_active);
2923 }
2924 if trace_on {
2927 if let Some(last) = traces.last_mut() {
2928 let e = r.last_best_e();
2929 last.recon = e.is_finite().then_some(e);
2930 last.switched = decision.is_some();
2931 }
2932 }
2933 }
2934 }
2935 }
2936 }
2937
2938 self.graph_want_logits = false;
2939 self.graph_logits = None;
2940 if router.is_some() {
2942 let _ = self.set_active_skill(None);
2943 }
2944 self.dyn_router = router.or(self.dyn_router.take());
2945 self.mtp = mtp.or(self.mtp.take());
2946
2947 let output_ids = &all_ids[input_ids.len()..];
2948 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2952 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2953 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2955 Ok(GenerateResult {
2956 text: self.tokenizer.decode(output_ids),
2957 token_ids: output_ids.to_vec(),
2958 prompt_tokens: input_ids.len(),
2959 tokens_generated: generated,
2960 finish_reason,
2961 mtp_drafted: drafted,
2962 mtp_accepted: accepted,
2963 token_confidence: confidence,
2964 traces,
2965 })
2966 }
2967
2968 fn mtp_step(
2972 &mut self,
2973 m: &mut MtpModule,
2974 hidden: &[f32],
2975 next_token: u32,
2976 position: usize,
2977 ) -> u32 {
2978 self.mtp_step_h(m, hidden, next_token, position).0
2979 }
2980
2981 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2985 use std::sync::Mutex;
2986 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2987 let mut t = T.lock().unwrap();
2988 if t.len() <= depth {
2989 t.resize(depth + 1, (0, 0));
2990 }
2991 t[depth].0 += 1;
2992 t[depth].1 += prefix_ok as u64;
2993 if depth == 0 && t[0].0 % 128 == 0 {
2994 let line: Vec<String> = t
2995 .iter()
2996 .enumerate()
2997 .map(|(d, (n, k))| {
2998 format!(
2999 "d{}={:.0}%({n})",
3000 d + 1,
3001 100.0 * *k as f64 / (*n).max(1) as f64
3002 )
3003 })
3004 .collect();
3005 eprintln!("mtp-chain: {}", line.join(" "));
3006 }
3007 }
3008
3009 fn mtp_step_hl(
3017 &mut self,
3018 m: &mut MtpModule,
3019 hidden: &[f32],
3020 next_token: u32,
3021 position: usize,
3022 ) -> (Vec<f32>, Vec<f32>) {
3023 #[cfg(target_os = "macos")]
3028 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3029 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3030 self.mtp_graph_mode = Some(true);
3031 return r;
3032 }
3033 self.mtp_graph_mode = Some(false);
3034 }
3035 #[cfg(feature = "gpu")]
3036 if self.mtp_graph_mode != Some(false) {
3037 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
3038 self.mtp_graph_mode = Some(true);
3039 return r;
3040 }
3041 if self.mtp_graph_mode == Some(true) {
3042 tracing::warn!("mtp graph declined mid-run — draft falls to the per-op path");
3047 }
3048 self.mtp_graph_mode = Some(false);
3049 }
3050 let e = self.embed_single(next_token);
3054 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3055 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3056 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3057 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3058 let mut x = vec![0.0f32; self.hidden_size];
3059 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3060
3061 let lw = &m.layer;
3063 inference::rms_norm_into(
3064 &x,
3065 &lw.input_norm,
3066 self.rms_eps,
3067 self.norm_style,
3068 &mut self.ws.n1,
3069 );
3070 let attn = match &lw.attn {
3071 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3073 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3074 AttnKind::Full {
3075 wq,
3076 wk,
3077 wv,
3078 wo,
3079 q_norm,
3080 k_norm,
3081 output_gate,
3082 softplus_gate,
3083 bias,
3084 } => {
3085 let mut cfg = self.attn_cfg(position);
3086 cfg.q_norm = q_norm.as_deref();
3087 cfg.k_norm = k_norm.as_deref();
3088 cfg.output_gate = *output_gate;
3089 cfg.softplus_gate = softplus_gate
3090 .as_ref()
3091 .map(|(gate, per_head)| (gate, *per_head));
3092 cfg.bias = bias
3093 .as_ref()
3094 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3095 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3096 }
3097 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3098 unreachable!("MTP block is full attention")
3099 }
3100 };
3101 for (i, &a) in attn.iter().enumerate() {
3102 x[i] += a;
3103 }
3104 inference::rms_norm_into(
3105 &x,
3106 &lw.post_norm,
3107 self.rms_eps,
3108 self.norm_style,
3109 &mut self.ws.p1,
3110 );
3111 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3112 for (i, &f) in ffn.iter().enumerate() {
3113 x[i] += f;
3114 }
3115
3116 inference::rms_norm_into(
3117 &x,
3118 &m.final_norm,
3119 self.rms_eps,
3120 self.norm_style,
3121 &mut self.ws.n1,
3122 );
3123 let lg = self.lm_head_forward(&self.ws.n1);
3124 (lg, x)
3125 }
3126
3127 fn mtp_step_h(
3129 &mut self,
3130 m: &mut MtpModule,
3131 hidden: &[f32],
3132 next_token: u32,
3133 position: usize,
3134 ) -> (u32, Vec<f32>) {
3135 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
3136 let draft = sampler::argmax(&lg);
3137 attention::recycle_buf(&mut lg);
3138 (draft, x)
3139 }
3140
3141 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
3147 match trial {
3148 SpecTrial::Spec { t0, gen0, rounds } => {
3149 let rounds = rounds + 1;
3150 if rounds >= 5 {
3151 if mon.plain_ms > 0.0 {
3152 let keep = mon.pays();
3153 mon.fails = 0;
3154 tracing::info!(
3155 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3156 mon.tokens,
3157 mon.round_ms,
3158 mon.plain_ms,
3159 if keep { "speculating" } else { "plain" }
3160 );
3161 SpecTrial::Decided {
3162 spec: keep,
3163 recheck_at: if keep { usize::MAX } else { generated + 128 },
3164 }
3165 } else {
3166 SpecTrial::Plain {
3167 t0: std::time::Instant::now(),
3168 gen0: generated,
3169 }
3170 }
3171 } else {
3172 SpecTrial::Spec { t0, gen0, rounds }
3173 }
3174 }
3175 SpecTrial::Decided { spec: true, .. } => {
3176 if mon.pays() {
3177 mon.fails = 0;
3178 trial
3179 } else {
3180 mon.fails += 1;
3181 if mon.fails >= 4 {
3182 tracing::info!(
3183 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
3184 mon.tokens,
3185 mon.round_ms,
3186 mon.plain_ms
3187 );
3188 SpecTrial::Decided {
3189 spec: false,
3190 recheck_at: generated + 128,
3191 }
3192 } else {
3193 trial
3194 }
3195 }
3196 }
3197 other => other,
3198 }
3199 }
3200
3201 fn mtp_kv_id(&self) -> u64 {
3204 self.graph_kv_id | (1u64 << 40)
3205 }
3206
3207 const MTP_LAYER_BASE: usize = 0;
3212
3213 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
3216 let e = self.embed_single(next_token);
3217 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3218 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3219 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3220 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3221 let mut x = vec![0.0f32; self.hidden_size];
3222 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3223 x
3224 }
3225
3226 #[cfg(feature = "gpu")]
3229 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
3230 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
3231 return false;
3232 }
3233 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3234 || !crate::gpu::enabled_here()
3235 || self.attn_softcap > 0.0
3236 || self.attention_heads_per_layer.is_some()
3237 {
3238 return false;
3239 }
3240 matches!(
3241 &m.layer.attn,
3242 AttnKind::Full {
3243 softplus_gate: None,
3244 ..
3245 }
3246 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
3247 }
3248
3249 #[cfg(feature = "gpu")]
3255 fn mtp_step_graph(
3256 &mut self,
3257 m: &mut MtpModule,
3258 hidden: &[f32],
3259 next_token: u32,
3260 position: usize,
3261 ) -> Option<(Vec<f32>, Vec<f32>)> {
3262 if !self.mtp_graph_ok(m) {
3263 return None;
3264 }
3265 let lw = &m.layer;
3266 let AttnKind::Full {
3267 wq,
3268 wk,
3269 wv,
3270 wo,
3271 q_norm,
3272 k_norm,
3273 output_gate,
3274 softplus_gate,
3275 bias,
3276 } = &lw.attn
3277 else {
3278 return None;
3279 };
3280 if softplus_gate.is_some() {
3281 return None;
3282 }
3283 let FfnKind::Dense(d) = &lw.ffn else {
3284 return None;
3285 };
3286 let mut x = self.mtp_block_input(m, hidden, next_token);
3289 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3290 let (_, i, kind, rs) = t.graph_weight()?;
3291 Some(crate::gpu::GraphW {
3292 idx: i,
3293 kind,
3294 row_scale: rs,
3295 data: &[],
3296 })
3297 }
3298 let (model, _, _, _) = wq.graph_weight()?;
3299 let model = model.clone();
3300 let (lm_gw, lm_rows) = {
3301 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3302 (
3303 crate::gpu::GraphW {
3304 idx: i,
3305 kind,
3306 row_scale: rs,
3307 data: &[],
3308 },
3309 self.weights.lm_head.rows(),
3310 )
3311 };
3312 let layer = crate::gpu::GraphLayer {
3313 input_norm: &lw.input_norm,
3314 attn: crate::gpu::GraphAttn::Full {
3315 wq: gw(wq)?,
3316 wk: gw(wk)?,
3317 wv: gw(wv)?,
3318 wo: gw(wo)?,
3319 q_norm: q_norm.as_deref(),
3320 k_norm: k_norm.as_deref(),
3321 bias: bias
3322 .as_ref()
3323 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3324 output_gate: *output_gate,
3325 cpu_k: m.kv.k_heads(),
3326 cpu_v: m.kv.v_heads(),
3327 },
3328 post_norm: &lw.post_norm,
3329 ffn: crate::gpu::GraphFfn::Dense {
3330 gate: gw(&d.gate_proj)?,
3331 up: gw(&d.up_proj)?,
3332 down: gw(&d.down_proj)?,
3333 },
3334 };
3335 let nh = self.num_heads;
3336 let (nkv, hd, rd) = self.layer_geom(0);
3337 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3338 let mut logits = Vec::new();
3339 let ok = crate::gpu::forward_token_graph(
3340 &model,
3341 self.mtp_kv_id(),
3342 std::slice::from_ref(&layer),
3343 &[None],
3344 self.o1_epoch,
3345 &self.inv_freq,
3346 &mut x,
3347 nh,
3348 nkv,
3349 hd,
3350 rd,
3351 self.hidden_size,
3352 self.intermediate_size,
3353 position,
3354 self.kv_cache.max_seq_len,
3355 gemma,
3356 self.rms_eps as f32,
3357 Some((&lm_gw, lm_rows)),
3358 &m.final_norm,
3359 &mut logits,
3360 &[],
3361 1,
3362 None,
3363 None,
3364 None,
3365 Self::MTP_LAYER_BASE,
3366 true,
3367 );
3368 if !ok {
3369 return None;
3370 }
3371 logits.resize(self.vocab_size, 0.0);
3372 Some((logits, x))
3373 }
3374
3375 #[cfg(feature = "gpu")]
3382 fn mtp_warm_graph(
3383 &mut self,
3384 m: &mut MtpModule,
3385 pairs: &[(&[f32], u32)],
3386 first_pos: usize,
3387 ) -> bool {
3388 if pairs.is_empty() || !self.mtp_graph_ok(m) {
3389 return pairs.is_empty();
3390 }
3391 let hs = self.hidden_size;
3392 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
3395 for (h, t) in pairs {
3396 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
3397 }
3398 let lw = &m.layer;
3399 let AttnKind::Full {
3400 wq,
3401 wk,
3402 wv,
3403 wo,
3404 q_norm,
3405 k_norm,
3406 output_gate,
3407 bias,
3408 ..
3409 } = &lw.attn
3410 else {
3411 return false;
3412 };
3413 let FfnKind::Dense(d) = &lw.ffn else {
3414 return false;
3415 };
3416 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3417 let (_, i, kind, rs) = t.graph_weight()?;
3418 Some(crate::gpu::GraphW {
3419 idx: i,
3420 kind,
3421 row_scale: rs,
3422 data: &[],
3423 })
3424 }
3425 let Some((model, _, _, _)) = wq.graph_weight() else {
3426 return false;
3427 };
3428 let model = model.clone();
3429 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
3430 gw(wq),
3431 gw(wk),
3432 gw(wv),
3433 gw(wo),
3434 gw(&d.gate_proj),
3435 gw(&d.up_proj),
3436 gw(&d.down_proj),
3437 ) else {
3438 return false;
3439 };
3440 let layer = crate::gpu::GraphLayer {
3441 input_norm: &lw.input_norm,
3442 attn: crate::gpu::GraphAttn::Full {
3443 wq: gwq,
3444 wk: gwk,
3445 wv: gwv,
3446 wo: gwo,
3447 q_norm: q_norm.as_deref(),
3448 k_norm: k_norm.as_deref(),
3449 bias: bias
3450 .as_ref()
3451 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3452 output_gate: *output_gate,
3453 cpu_k: m.kv.k_heads(),
3454 cpu_v: m.kv.v_heads(),
3455 },
3456 post_norm: &lw.post_norm,
3457 ffn: crate::gpu::GraphFfn::Dense {
3458 gate: gg,
3459 up: gu,
3460 down: gd,
3461 },
3462 };
3463 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
3464 let nh = self.num_heads;
3465 let (nkv, hd, rd) = self.layer_geom(0);
3466 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3467 crate::gpu::forward_batch_graph(
3468 &model,
3469 self.mtp_kv_id(),
3470 std::slice::from_ref(&layer),
3471 &self.inv_freq,
3472 &mut hiddens,
3473 nh,
3474 nkv,
3475 hd,
3476 rd,
3477 hs,
3478 self.intermediate_size,
3479 &positions,
3480 self.kv_cache.max_seq_len,
3481 gemma,
3482 self.rms_eps as f32,
3483 pairs.len(),
3484 None,
3485 )
3486 }
3487
3488 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
3492 let e = self.embed_single(next_token);
3493 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3494 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3495 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3496 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3497 let mut x = vec![0.0f32; self.hidden_size];
3498 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3499 inference::rms_norm_into(
3500 &x,
3501 &m.layer.input_norm,
3502 self.rms_eps,
3503 self.norm_style,
3504 &mut self.ws.n1,
3505 );
3506 let attn = match &m.layer.attn {
3507 AttnKind::Full {
3508 wq,
3509 wk,
3510 wv,
3511 wo,
3512 q_norm,
3513 k_norm,
3514 output_gate,
3515 softplus_gate,
3516 bias,
3517 } => {
3518 let mut cfg = self.attn_cfg(position);
3519 cfg.q_norm = q_norm.as_deref();
3520 cfg.k_norm = k_norm.as_deref();
3521 cfg.output_gate = *output_gate;
3522 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
3523 cfg.bias = bias
3524 .as_ref()
3525 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3526 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3527 }
3528 _ => return,
3529 };
3530 let _ = attn;
3531 }
3532
3533 #[cfg(feature = "gpu")]
3540 #[allow(clippy::too_many_arguments)]
3541 fn graph_spec_step(
3542 &mut self,
3543 m: &mut MtpModule,
3544 hidden: &[f32],
3545 t_next: u32,
3546 next_pos: usize,
3547 drafted: &mut usize,
3548 accepted: &mut usize,
3549 all_ids: &mut Vec<u32>,
3553 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
3554 #[cfg(target_os = "macos")]
3565 let metal_native = crate::gpu::q1_force();
3566 #[cfg(not(target_os = "macos"))]
3567 let metal_native = false;
3568 #[cfg(feature = "gpu")]
3569 let k_default = if metal_native {
3570 7
3573 } else if crate::gpu_wgpu::verify_i8_on() {
3574 5
3575 } else {
3576 4
3577 };
3578 #[cfg(not(feature = "gpu"))]
3579 let k_default = 4;
3580 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
3581 .ok()
3582 .and_then(|v| v.parse().ok())
3583 .filter(|&v| (1..=8).contains(&v))
3584 .unwrap_or(k_default);
3585 if next_pos == 0 {
3586 return None;
3587 }
3588 let t_round = std::time::Instant::now();
3589 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
3605 let sub0 = subs();
3606 let cfg = self.sampler_config.clone();
3611 let penalized = !(cfg.repetition_penalty == 1.0
3612 && cfg.presence_penalty == 0.0
3613 && cfg.suppress_tokens.is_empty());
3614 let greedy_pen = cfg.temperature < 1e-6 && penalized;
3619 let sampling = cfg.temperature >= 1e-6;
3620 let sparse = sampling && sampler::sparse_ok(&cfg);
3626 let base_len = all_ids.len();
3627 if sampling && !sparse && self.spec_q.len() < k_spec {
3628 self.spec_q.resize_with(k_spec, Vec::new);
3629 }
3630 if sparse && self.spec_qs.len() < k_spec {
3631 self.spec_qs.resize_with(k_spec, Vec::new);
3632 }
3633 let mut drafts = Vec::with_capacity(k_spec);
3638 let mut hx = hidden.to_vec();
3639 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
3642 for j in 0..k_spec {
3643 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
3644 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
3645 if spec_dbg {
3646 let saved = self.mtp_graph_mode;
3647 self.mtp_graph_mode = Some(false);
3648 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3649 self.mtp_graph_mode = saved;
3650 m.kv.truncate_last(1);
3651 dbg_ref = Some(r);
3652 }
3653 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3654 if let Some((lg_cpu, h_cpu)) = dbg_ref {
3655 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
3656 let dl = lg.iter().zip(&lg_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3657 let dh = hj.iter().zip(&h_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3658 eprintln!(
3659 "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 {}",
3660 next_pos - 1 + j,
3661 sampler::argmax(&lg_cpu),
3662 sampler::argmax(&lg),
3663 n(&h_cpu),
3664 n(&hj),
3665 m.kv.seq_len
3666 );
3667 }
3668 let dj = if sparse {
3669 let mut q = std::mem::take(&mut self.spec_qs[j]);
3670 let ok = sampler::sparse_distribution_into(
3671 &lg,
3672 &cfg,
3673 all_ids,
3674 &mut self.sampler_scratch,
3675 self.pool.as_deref(),
3676 &mut q,
3677 );
3678 let d = if ok {
3679 sampler::draw_sparse(&q, &mut self.rng)
3680 } else {
3681 let t = sampler::argmax(&lg);
3683 q.clear();
3684 q.push((t, 1.0));
3685 t
3686 };
3687 self.spec_qs[j] = q;
3688 all_ids.push(d);
3689 d
3690 } else if sampling {
3691 let mut q = std::mem::take(&mut self.spec_q[j]);
3692 sampler::distribution_into(
3693 &lg,
3694 &cfg,
3695 all_ids,
3696 &mut self.sampler_scratch,
3697 self.pool.as_deref(),
3698 &mut q,
3699 );
3700 let d = sampler::draw(&q, &mut self.rng);
3701 self.spec_q[j] = q;
3702 all_ids.push(d); d
3704 } else if greedy_pen {
3705 let d = sampler::argmax_penalized(
3706 &lg,
3707 &cfg,
3708 all_ids,
3709 &mut self.sampler_scratch,
3710 self.pool.as_deref(),
3711 );
3712 all_ids.push(d);
3713 d
3714 } else {
3715 sampler::argmax(&lg)
3716 };
3717 attention::recycle_buf(&mut lg);
3718 drafts.push(dj);
3719 hx = hj;
3720 }
3721 all_ids.truncate(base_len);
3722 *drafted += k_spec;
3723 let t_draft = t_round.elapsed();
3724 let sub_draft = subs();
3725 let b = k_spec + 1;
3728 let mut hiddens = vec![0.0f32; b * self.hidden_size];
3729 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
3730 let e = self.embed_single(t);
3731 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
3732 }
3733 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
3734 let (lm_gw, lm_rows) = {
3735 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3736 (
3737 crate::gpu::GraphW {
3738 idx: i,
3739 kind,
3740 row_scale: rs,
3741 data: &[],
3742 },
3743 self.weights.lm_head.rows(),
3744 )
3745 };
3746 let mut logits = Vec::new();
3747 let final_norm = self.weights.final_norm.clone();
3748 #[cfg(target_os = "macos")]
3749 let ok = if metal_native {
3750 let lm = self.weights.lm_head.q1_parts()?;
3751 self.try_batch_graph_metal(&mut hiddens, &positions, b, Some((lm, &final_norm, &mut logits)))
3752 } else {
3753 self.try_batch_graph_wgpu(
3754 &mut hiddens,
3755 &positions,
3756 b,
3757 Some(crate::gpu::SpecTail {
3758 lm: lm_gw,
3759 lm_rows,
3760 final_norm: &final_norm,
3761 logits_out: &mut logits,
3762 }),
3763 )
3764 };
3765 #[cfg(not(target_os = "macos"))]
3766 let ok = self.try_batch_graph_wgpu(
3767 &mut hiddens,
3768 &positions,
3769 b,
3770 Some(crate::gpu::SpecTail {
3771 lm: lm_gw,
3772 lm_rows,
3773 final_norm: &final_norm,
3774 logits_out: &mut logits,
3775 }),
3776 );
3777 if !ok {
3778 m.kv.truncate_last(k_spec);
3781 return None;
3782 }
3783 #[cfg(target_os = "macos")]
3789 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
3790 let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3791 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
3792 let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
3793 let want_save = self.graph_want_logits;
3794 self.graph_want_logits = false;
3795 for (i, &t) in toks.iter().enumerate() {
3796 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
3797 let _ = self.graph_logits.take();
3798 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
3802 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
3803 }
3804 let ref_lg = self.logits_from_hidden(&hi);
3805 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
3806 let ra = sampler::argmax(&ref_lg);
3807 let va = sampler::argmax(row);
3808 let mut md = 0f32;
3809 let mut rms = 0f64;
3810 for j in 0..lm_rows.min(ref_lg.len()) {
3811 let d = (ref_lg[j] - row[j]).abs();
3812 md = md.max(d);
3813 rms += (d as f64) * (d as f64);
3814 }
3815 let mut hd = 0f32;
3816 for j in 0..self.hidden_size {
3817 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
3818 }
3819 eprintln!(
3820 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
3821 next_pos + i,
3822 if ra == va { "OK" } else { "MISMATCH" },
3823 (rms / lm_rows as f64).sqrt()
3824 );
3825 }
3826 self.graph_want_logits = want_save;
3827 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
3830 if l.linear_state.len() == st.len() {
3831 l.linear_state.copy_from_slice(&st);
3832 } else {
3833 l.linear_state = st;
3834 }
3835 }
3836 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
3837 let extra = l.seq_len.saturating_sub(n0);
3838 if extra > 0 {
3839 l.truncate_last(extra);
3840 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
3841 }
3842 }
3843 }
3844 let t_verify = t_round.elapsed();
3845 let sub_verify = subs();
3846 let mut a = 0usize;
3851 let mut forced: Option<u32> = None;
3852 let ids: Vec<u32> = if sparse {
3853 let mut p = std::mem::take(&mut self.spec_ps);
3854 let mut res = std::mem::take(&mut self.spec_ress);
3855 while a < k_spec {
3856 let ok = sampler::sparse_distribution_into(
3857 &logits[a * lm_rows..(a + 1) * lm_rows],
3858 &cfg,
3859 all_ids,
3860 &mut self.sampler_scratch,
3861 self.pool.as_deref(),
3862 &mut p,
3863 );
3864 if !ok {
3865 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
3866 p.clear();
3867 p.push((t, 1.0));
3868 }
3869 match sampler::spec_accept_or_correct_sparse(
3870 &p,
3871 &self.spec_qs[a],
3872 drafts[a],
3873 &mut self.rng,
3874 &mut res,
3875 ) {
3876 None => {
3877 all_ids.push(drafts[a]);
3878 a += 1;
3879 }
3880 Some(c) => {
3881 forced = Some(c);
3882 break;
3883 }
3884 }
3885 }
3886 all_ids.truncate(base_len);
3887 self.spec_ps = p;
3888 self.spec_ress = res;
3889 drafts.clone()
3890 } else if sampling {
3891 let mut p = std::mem::take(&mut self.spec_p);
3892 let mut res = std::mem::take(&mut self.spec_res);
3893 while a < k_spec {
3894 sampler::distribution_into(
3895 &logits[a * lm_rows..(a + 1) * lm_rows],
3896 &cfg,
3897 all_ids,
3898 &mut self.sampler_scratch,
3899 self.pool.as_deref(),
3900 &mut p,
3901 );
3902 match sampler::spec_accept_or_correct(
3903 &p,
3904 &self.spec_q[a],
3905 drafts[a],
3906 &mut self.rng,
3907 &mut res,
3908 self.pool.as_deref(),
3909 ) {
3910 None => {
3911 all_ids.push(drafts[a]);
3912 a += 1;
3913 }
3914 Some(c) => {
3915 forced = Some(c);
3916 break;
3917 }
3918 }
3919 }
3920 all_ids.truncate(base_len);
3921 self.spec_p = p;
3922 self.spec_res = res;
3923 drafts.clone()
3925 } else if greedy_pen {
3926 let mut ids: Vec<u32> = Vec::with_capacity(b);
3930 for i in 0..b {
3931 let t = sampler::argmax_penalized(
3932 &logits[i * lm_rows..(i + 1) * lm_rows],
3933 &cfg,
3934 all_ids,
3935 &mut self.sampler_scratch,
3936 self.pool.as_deref(),
3937 );
3938 ids.push(t);
3939 if i < k_spec && t == drafts[i] {
3940 all_ids.push(t);
3941 } else {
3942 break;
3943 }
3944 }
3945 all_ids.truncate(base_len);
3946 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
3947 a += 1;
3948 }
3949 ids
3952 } else {
3953 let ids: Vec<u32> = (0..b)
3954 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
3955 .collect();
3956 while a < k_spec && ids[a] == drafts[a] {
3957 a += 1;
3958 }
3959 ids
3960 };
3961 if spec_dbg {
3962 eprintln!("spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}", drafts, ids);
3963 }
3964 #[cfg(target_os = "macos")]
3968 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
3969 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
3970 {
3971 let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3972 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
3973 let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
3974 let want_save = self.graph_want_logits;
3975 self.graph_want_logits = false;
3976 for (i, &t) in toks.iter().take(a + 1).enumerate() {
3977 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
3978 let _ = self.graph_logits.take();
3979 }
3980 self.graph_want_logits = want_save;
3981 let plain_states: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3982 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
3983 let mut rows = Vec::new();
3984 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens.iter()).enumerate() {
3985 let extra = l.seq_len.saturating_sub(*n0);
3986 if extra > 0 {
3987 let mut kk = Vec::new();
3988 let mut vv = Vec::new();
3989 for g in 0..nkv {
3990 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
3991 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
3992 }
3993 rows.push((li, kk, vv));
3994 l.truncate_last(extra);
3995 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
3996 }
3997 }
3998 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
3999 if l.linear_state.len() == st.len() {
4000 l.linear_state.copy_from_slice(&st);
4001 } else {
4002 l.linear_state = st;
4003 }
4004 }
4005 Some((plain_states, rows))
4006 } else {
4007 None
4008 };
4009 #[cfg(target_os = "macos")]
4011 if metal_native {
4012 self.metal_verify_commit(a);
4015 if let Some((plain_states, rows)) = commit_ref {
4016 crate::gpu_metal::queue_fence();
4017 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4018 let mut worst_s = 0f32;
4019 let mut worst_li = 0usize;
4020 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
4021 if l.linear_state.len() != ps.len() || ps.is_empty() {
4022 continue;
4023 }
4024 let d = l.linear_state.iter().zip(ps).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4025 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
4026 let rel = d / n.max(1e-6);
4027 if rel > worst_s {
4028 worst_s = rel;
4029 worst_li = li;
4030 }
4031 }
4032 let mut worst_k = 0f32;
4033 for (li, kk, vv) in &rows {
4034 let l = &self.kv_cache.layers[*li];
4035 let n0 = l.seq_len - (kk.len() / (nkv * hd));
4036 let mut ck = Vec::new();
4037 let mut cv = Vec::new();
4038 for g in 0..nkv {
4039 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4040 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4041 }
4042 if ck.len() == kk.len() {
4043 let dk = ck.iter().zip(kk).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4044 let dv = cv.iter().zip(vv).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4045 worst_k = worst_k.max(dk).max(dv);
4046 } else {
4047 eprintln!("commit-check L{li}: kv row count mismatch {} vs {}", ck.len(), kk.len());
4048 }
4049 }
4050 eprintln!(
4051 "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}"
4052 );
4053 }
4054 } else if a + 1 < b {
4055 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
4056 }
4057 #[cfg(not(target_os = "macos"))]
4058 if a + 1 < b {
4059 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
4060 }
4061 *accepted += a;
4062 m.kv.truncate_last(k_spec.saturating_sub(1));
4073 #[cfg(target_os = "macos")]
4074 if metal_native && self.mtp_graph_mode == Some(true) {
4075 crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, m.kv.seq_len);
4078 }
4079 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
4080 if !warm_off && a > 0 {
4081 let mut warmed = false;
4084 #[cfg(target_os = "macos")]
4085 if metal_native && self.mtp_graph_mode == Some(true) {
4086 let pairs: Vec<(&[f32], u32)> = (0..a)
4090 .map(|j| (&hiddens[j * self.hidden_size..(j + 1) * self.hidden_size], ids[j]))
4091 .collect();
4092 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
4093 if !warmed {
4094 warmed = true;
4095 for j in 0..a {
4096 let row = hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
4097 if self.mtp_step_metal(m, &row, ids[j], next_pos + j, false).is_none() {
4098 warmed = false;
4099 break;
4100 }
4101 }
4102 }
4103 }
4104 if !warmed && self.mtp_graph_mode == Some(true) && !metal_native {
4105 let rows: Vec<Vec<f32>> = (0..a)
4106 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
4107 .collect();
4108 let pairs: Vec<(&[f32], u32)> = rows
4109 .iter()
4110 .zip(ids.iter())
4111 .map(|(r, &t)| (r.as_slice(), t))
4112 .collect();
4113 warmed = self.mtp_warm_graph(m, &pairs, next_pos);
4114 if !warmed {
4115 warmed = true;
4117 for j in 0..a {
4118 if self
4119 .mtp_step_graph(m, &rows[j], ids[j], next_pos + j)
4120 .is_none()
4121 {
4122 warmed = false;
4123 break;
4124 }
4125 }
4126 }
4127 }
4128 if !warmed {
4129 for j in 0..a {
4130 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
4131 let row = row.to_vec();
4132 self.mtp_warm(m, &row, ids[j], next_pos + j);
4133 }
4134 }
4135 }
4136 if let Some(c) = forced {
4140 self.spec_forced = Some(c);
4141 self.graph_logits = None;
4142 } else {
4143 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
4144 row.resize(self.vocab_size, 0.0);
4145 if let Some(c) = self.final_softcap {
4146 for l in row.iter_mut() {
4147 *l = c * (*l / c).tanh();
4148 }
4149 }
4150 self.graph_logits = Some(row);
4151 }
4152 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
4153 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4159 let end = subs();
4160 eprintln!(
4161 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
4162 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
4163 t_draft.as_secs_f64() * 1e3,
4164 sub_draft - sub0,
4165 (t_verify - t_draft).as_secs_f64() * 1e3,
4166 sub_verify - sub_draft,
4167 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
4168 end - sub_verify,
4169 );
4170 }
4171 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
4172 }
4173
4174 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
4183 if !self.pair_supported() {
4184 return (0.0, 0.0);
4185 }
4186 let emb1 = self.embed_single(1);
4187 let emb2 = self.embed_single(2);
4188 let pos = self.kv_cache.seq_len();
4189
4190 let t0 = std::time::Instant::now();
4191 for _ in 0..iters {
4192 let _ = self.forward_layers(&emb1, pos, None);
4193 let _ = self.forward_layers(&emb2, pos + 1, None);
4194 for l in &mut self.kv_cache.layers {
4195 l.truncate_last(2);
4196 }
4197 }
4198 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4199
4200 let t1 = std::time::Instant::now();
4201 for _ in 0..iters {
4202 let _ = self.forward_pair(&emb1, &emb2, pos);
4203 for l in &mut self.kv_cache.layers {
4204 l.truncate_last(2);
4205 }
4206 }
4207 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4208 (singles_ms, pair_ms)
4209 }
4210
4211 fn pair_supported(&self) -> bool {
4219 !self.weights.layers.is_empty()
4226 && self.g3n.is_none()
4227 && !self
4228 .weights
4229 .layers
4230 .iter()
4231 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
4232 }
4233
4234 fn forward_pair(
4235 &mut self,
4236 emb1: &[f32],
4237 emb2: &[f32],
4238 position: usize,
4239 ) -> (Vec<f32>, Vec<f32>) {
4240 let mut h1 = emb1.to_vec();
4241 let mut h2 = emb2.to_vec();
4242 let (_nkv, _hd, hs, _rd, eps) = (
4243 self.num_kv_heads,
4244 self.head_dim,
4245 self.hidden_size,
4246 self.rotary_dim,
4247 self.rms_eps,
4248 );
4249 let pool = self.pool.clone();
4250
4251 for li in 0..self.num_layers {
4252 let lw = &self.weights.layers[self.phys_layer(li)];
4253 inference::rms_norm_into(
4256 &h1,
4257 &lw.input_norm,
4258 self.rms_eps,
4259 self.norm_style,
4260 &mut self.ws.n1,
4261 );
4262 inference::rms_norm_into(
4263 &h2,
4264 &lw.input_norm,
4265 self.rms_eps,
4266 self.norm_style,
4267 &mut self.ws.n2,
4268 );
4269
4270 let (a1, a2) = match &lw.attn {
4271 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4272 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4273 AttnKind::Linear(w) => {
4274 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
4275 let layer = &mut self.kv_cache.layers[li];
4276 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4277 vmf_phase_pair(
4278 &self.ws.n1,
4279 &self.ws.n2,
4280 w,
4281 &cfg,
4282 state,
4283 scratch,
4284 self.pool.as_deref(),
4285 )
4286 }
4287 AttnKind::LinearGdn(w) => {
4288 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4289 let layer = &mut self.kv_cache.layers[li];
4290 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4291 gdn_pair(
4292 &self.ws.n1,
4293 &self.ws.n2,
4294 w,
4295 &cfg,
4296 state,
4297 scratch,
4298 self.pool.as_deref(),
4299 )
4300 }
4301 AttnKind::ShortConv(w) => {
4302 let cfg = self
4303 .short_conv_cfg
4304 .expect("short-conv layer without short_conv_cfg");
4305 let layer = &mut self.kv_cache.layers[li];
4306 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4307 short_conv_pair(
4308 &self.ws.n1,
4309 &self.ws.n2,
4310 w,
4311 &cfg,
4312 state,
4313 scratch,
4314 self.pool.as_deref(),
4315 )
4316 }
4317 AttnKind::Full {
4318 wq,
4319 wk,
4320 wv,
4321 wo,
4322 q_norm,
4323 k_norm,
4324 output_gate,
4325 softplus_gate,
4326 bias,
4327 } => {
4328 let inv_freq_l = self.layer_inv_freq(li);
4329 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4330 let cfg = QwenAttnCfg {
4331 num_heads: self.layer_num_heads(li),
4332 num_kv_heads: nkv_l,
4333 head_dim: hd_l,
4334 hidden_size: hs,
4335 position,
4336 inv_freq: &inv_freq_l,
4337 rotary_dim: rd_l,
4338 scale: self.attn_scale,
4339 softcap: self.attn_softcap,
4340 window: self.layer_window(li),
4341 v_norm: self.attn_v_norm,
4342 q_norm: q_norm.as_deref(),
4343 k_norm: k_norm.as_deref(),
4344 output_gate: *output_gate,
4345 softplus_gate: softplus_gate
4346 .as_ref()
4347 .map(|(gate, per_head)| (gate, *per_head)),
4348 rope_scale: self.layer_rope_scale(li),
4349 bias: bias
4350 .as_ref()
4351 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4352 rms_eps: eps,
4353 norm_style: self.norm_style,
4354 pool: pool.as_deref(),
4355 };
4356 attention::qwen_attention_pair(
4357 &self.ws.n1,
4358 &self.ws.n2,
4359 wq,
4360 wk,
4361 wv,
4362 wo,
4363 &mut self.kv_cache.layers[li],
4364 &cfg,
4365 )
4366 }
4367 };
4368 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
4369 Some(w) => (
4370 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
4371 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
4372 ),
4373 None => (a1, a2),
4374 };
4375 for i in 0..self.hidden_size {
4376 h1[i] += a1[i];
4377 h2[i] += a2[i];
4378 }
4379 let (mut a1, mut a2) = (a1, a2);
4380 attention::recycle_buf(&mut a1);
4381 attention::recycle_buf(&mut a2);
4382
4383 let lw = &self.weights.layers[self.phys_layer(li)];
4384 inference::rms_norm_into(
4385 &h1,
4386 &lw.post_norm,
4387 self.rms_eps,
4388 self.norm_style,
4389 &mut self.ws.p1,
4390 );
4391 inference::rms_norm_into(
4392 &h2,
4393 &lw.post_norm,
4394 self.rms_eps,
4395 self.norm_style,
4396 &mut self.ws.p2,
4397 );
4398 let (f1, f2) = match &lw.ffn {
4399 FfnKind::DenseMoe(dm) => (
4402 dense_moe_ffn(
4403 dm,
4404 &self.ws.p1,
4405 &h1,
4406 self.rms_eps,
4407 self.norm_style,
4408 self.pool.as_deref(),
4409 ),
4410 dense_moe_ffn(
4411 dm,
4412 &self.ws.p2,
4413 &h2,
4414 self.rms_eps,
4415 self.norm_style,
4416 self.pool.as_deref(),
4417 ),
4418 ),
4419 _ => ffn_forward_pair(
4420 &lw.ffn,
4421 &self.ws.p1,
4422 &self.ws.p2,
4423 self.pool.as_deref(),
4424 None,
4425 ),
4426 };
4427 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4428 Some(w) => (
4429 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
4430 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
4431 ),
4432 None => (f1, f2),
4433 };
4434 for i in 0..self.hidden_size {
4435 h1[i] += f1[i];
4436 h2[i] += f2[i];
4437 }
4438 let (mut f1, mut f2) = (f1, f2);
4439 attention::recycle_buf(&mut f1);
4440 attention::recycle_buf(&mut f2);
4441 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4442 for i in 0..self.hidden_size {
4443 h1[i] *= sc;
4444 h2[i] *= sc;
4445 }
4446 }
4447 if self.is_loop_end(li) && li + 1 < self.num_layers {
4449 h1 = inference::rms_norm(
4450 &h1,
4451 &self.weights.final_norm,
4452 self.rms_eps,
4453 self.norm_style,
4454 );
4455 h2 = inference::rms_norm(
4456 &h2,
4457 &self.weights.final_norm,
4458 self.rms_eps,
4459 self.norm_style,
4460 );
4461 }
4462 }
4463 (h1, h2)
4464 }
4465
4466 fn commit_linear_scratch(&mut self) {
4468 for layer in &mut self.kv_cache.layers {
4469 if !layer.linear_scratch.is_empty() {
4470 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
4471 layer.linear_scratch.clear();
4472 }
4473 }
4474 }
4475
4476 pub fn forward_ids(
4479 &mut self,
4480 ids: &[u32],
4481 task_mask: Option<&TaskMask>,
4482 ) -> Result<Vec<f32>, String> {
4483 if ids.is_empty() {
4484 return Err("empty id sequence".to_string());
4485 }
4486 self.kv_cache.clear();
4487 self.kv_history.clear();
4488 self.o1_begin();
4489 let mut hidden = vec![0.0f32; self.hidden_size];
4490 let mut pos = 0usize;
4491 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
4499 let chunk = prefill_chunk();
4503 let hs = self.hidden_size;
4504 while pos < ids.len() {
4505 let end = (pos + chunk).min(ids.len());
4506 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4507 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4508 pos = end;
4509 }
4510 }
4511 if task_mask.is_none()
4520 && !self.graph_prefill_preferred()
4521 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
4522 && self.pair_supported()
4523 {
4524 while pos + 1 < ids.len() {
4525 let e1 = self.embed_single(ids[pos]);
4526 let e2 = self.embed_single(ids[pos + 1]);
4527 let (_, h2) = self.forward_pair(&e1, &e2, pos);
4528 self.commit_linear_scratch();
4529 hidden = h2;
4530 pos += 2;
4531 }
4532 }
4533 while pos < ids.len() {
4534 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4535 pos += 1;
4536 }
4537 self.o1_seal();
4541 let normed = inference::rms_norm(
4542 &hidden,
4543 &self.weights.final_norm,
4544 self.rms_eps,
4545 self.norm_style,
4546 );
4547 Ok(self.lm_head_forward(&normed))
4548 }
4549
4550 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
4557 let (nll, cnt) = self.nll_ids_from(ids, 0);
4558 (nll / cnt.max(1) as f64).exp()
4559 }
4560
4561 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4566 self.kv_cache.clear();
4567 self.kv_history.clear();
4568 FFN_PROBE.with(|p| {
4569 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4570 });
4571 crate::gpu::cpu_scope(|| {
4572 for (pos, &id) in ids.iter().enumerate() {
4573 let emb = self.embed_single(id);
4574 let _ = self.forward_layers(&emb, pos, None);
4575 }
4576 });
4577 self.kv_cache.clear();
4578 self.kv_history.clear();
4579 FFN_PROBE
4580 .with(|p| p.borrow_mut().take())
4581 .unwrap_or_default()
4582 }
4583
4584 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
4588 self.kv_cache.clear();
4589 self.kv_history.clear();
4590 let mut nll = 0f64;
4591 let mut cnt = 0usize;
4592 let mut hidden = vec![0f32; self.hidden_size];
4593 for (pos, &id) in ids.iter().enumerate() {
4594 if pos > 0 {
4595 inference::rms_norm_into(
4596 &hidden,
4597 &self.weights.final_norm,
4598 self.rms_eps,
4599 self.norm_style,
4600 &mut self.ws.n1,
4601 );
4602 let mut logits = self.lm_head_forward(&self.ws.n1);
4603 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4604 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
4605 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
4606 nll -= p.max(1e-300).ln();
4607 cnt += 1;
4608 attention::recycle_buf(&mut logits);
4609 }
4610 let emb = self.embed_single(id);
4611 hidden = self.forward_layers(&emb, pos, Some(mask));
4612 }
4613 self.kv_cache.clear();
4614 self.kv_history.clear();
4615 (nll / cnt.max(1) as f64).exp()
4616 }
4617
4618 pub fn nll_ids_masked(
4637 &mut self,
4638 ids: &[u32],
4639 start: usize,
4640 task_mask: Option<&TaskMask>,
4641 ) -> (f64, usize) {
4642 self.nll_ids_inner(ids, start, task_mask)
4643 }
4644
4645 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
4646 self.nll_ids_inner(ids, start, None)
4647 }
4648
4649 fn nll_ids_inner(
4650 &mut self,
4651 ids: &[u32],
4652 start: usize,
4653 task_mask: Option<&TaskMask>,
4654 ) -> (f64, usize) {
4655 self.kv_cache.clear();
4656 self.kv_history.clear();
4657 let mut nll = 0f64;
4658 let mut cnt = 0usize;
4659 if self.can_prefill_batched() {
4660 const CHUNK: usize = 128;
4666 const LM_SUB: usize = 32;
4667 let n = ids.len().saturating_sub(1);
4668 let hs = self.hidden_size;
4669 let rows = self.weights.lm_head.rows();
4670 let mut pos = 0usize;
4671 while pos < n {
4672 let end = (pos + CHUNK).min(n);
4673 let bsz = end - pos;
4674 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4675 let mut k0 = 0usize;
4676 while k0 < bsz {
4677 let k1 = (k0 + LM_SUB).min(bsz);
4678 let sb = k1 - k0;
4679 if pos + k1 <= start {
4682 k0 = k1;
4683 continue;
4684 }
4685 let mut normed = vec![0.0f32; sb * hs];
4686 for k in 0..sb {
4687 let r = inference::rms_norm(
4688 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
4689 &self.weights.final_norm,
4690 self.rms_eps,
4691 self.norm_style,
4692 );
4693 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
4694 }
4695 let mut logits = vec![0.0f32; sb * rows];
4696 self.weights
4697 .lm_head
4698 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
4699 for k in 0..sb {
4700 if pos + k0 + k < start {
4701 continue;
4702 }
4703 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
4704 if let Some(mu) = self.logit_multiplier {
4705 for v in lg.iter_mut() {
4706 *v *= mu;
4707 }
4708 }
4709 if let Some(c) = self.final_softcap {
4713 for v in lg.iter_mut() {
4714 *v = c * (*v / c).tanh();
4715 }
4716 }
4717 if let Some(cm) = self.head_clusters.clone() {
4720 self.hierarchical_head_logprobs(&normed[k * hs..(k + 1) * hs], &cm, lg);
4721 }
4722 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
4723 let target = ids[pos + k0 + k + 1] as usize;
4724 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4725 let lse: f64 = lg
4726 .iter()
4727 .map(|&v| ((v - max) as f64).exp())
4728 .sum::<f64>()
4729 .ln()
4730 + max as f64;
4731 nll += lse - lg[target] as f64;
4732 cnt += 1;
4733 if std::env::var("CMF_PPL_TRACE").is_ok() {
4734 let top = lg
4735 .iter()
4736 .enumerate()
4737 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4738 .map(|(i, _)| i)
4739 .unwrap_or(0);
4740 eprintln!(
4741 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
4742 pos + k0 + k,
4743 target,
4744 lse - lg[target] as f64,
4745 top,
4746 lg[target],
4747 lg[top]
4748 );
4749 }
4750 }
4751 k0 = k1;
4752 }
4753 pos = end;
4754 }
4755 self.kv_cache.clear();
4756 self.kv_history.clear();
4757 return (nll, cnt);
4758 }
4759 for pos in 0..ids.len().saturating_sub(1) {
4760 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4761 let out_of_band = self.graph_logits.take();
4769 if pos < start {
4770 continue;
4771 }
4772 let logits = match out_of_band {
4773 Some(lg) => lg,
4774 None => {
4775 let normed = inference::rms_norm(
4776 &hidden,
4777 &self.weights.final_norm,
4778 self.rms_eps,
4779 self.norm_style,
4780 );
4781 self.lm_head_forward(&normed)
4785 }
4786 };
4787 let target = ids[pos + 1] as usize;
4788 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4789 let lse: f64 = logits
4790 .iter()
4791 .map(|&v| ((v - max) as f64).exp())
4792 .sum::<f64>()
4793 .ln()
4794 + max as f64;
4795 let tok_nll = lse - logits[target] as f64;
4796 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4797 let top = logits
4798 .iter()
4799 .enumerate()
4800 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4801 .map(|(i, _)| i)
4802 .unwrap_or(0);
4803 eprintln!(
4804 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4805 logits[target], logits[top]
4806 );
4807 }
4808 nll += tok_nll;
4809 cnt += 1;
4810 }
4811 self.kv_cache.clear();
4812 self.kv_history.clear();
4813 (nll, cnt)
4814 }
4815
4816 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
4832 self.kv_cache.clear();
4833 self.kv_history.clear();
4834 self.o1_begin();
4835 let n = ids.len().saturating_sub(1);
4836 let p = prefill.min(n);
4837 let mut pos = 0usize;
4839 if self.can_prefill_batched() {
4840 const CHUNK: usize = 128;
4841 while pos < p {
4842 let end = (pos + CHUNK).min(p);
4843 let _ = self.prefill_batch(&ids[pos..end], pos);
4844 pos = end;
4845 }
4846 } else {
4847 while pos < p {
4848 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4849 pos += 1;
4850 }
4851 }
4852 self.o1_seal();
4853
4854 let mut nll = 0f64;
4855 let mut cnt = 0usize;
4856 for pos in p..n {
4857 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4858 let normed = inference::rms_norm(
4859 &hidden,
4860 &self.weights.final_norm,
4861 self.rms_eps,
4862 self.norm_style,
4863 );
4864 let logits = self.lm_head_forward(&normed);
4868 let target = ids[pos + 1] as usize;
4869 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4870 let lse: f64 = logits
4871 .iter()
4872 .map(|&v| ((v - max) as f64).exp())
4873 .sum::<f64>()
4874 .ln()
4875 + max as f64;
4876 let tok_nll = lse - logits[target] as f64;
4877 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4878 let top = logits
4879 .iter()
4880 .enumerate()
4881 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4882 .map(|(i, _)| i)
4883 .unwrap_or(0);
4884 eprintln!(
4885 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4886 logits[target], logits[top]
4887 );
4888 }
4889 nll += tok_nll;
4890 cnt += 1;
4891 }
4892 self.kv_cache.clear();
4893 self.kv_history.clear();
4894 (nll, cnt)
4895 }
4896
4897 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
4905 self.kv_cache.clear();
4906 self.kv_history.clear();
4907 let n = ids.len().saturating_sub(1);
4908 let mut correct = Vec::with_capacity(n);
4909 let mut pmax = Vec::with_capacity(n);
4910 for pos in 0..n {
4911 let emb = self.embed_single(ids[pos]);
4912 let hidden = self.forward_layers(&emb, pos, None);
4913 let normed = inference::rms_norm(
4914 &hidden,
4915 &self.weights.final_norm,
4916 self.rms_eps,
4917 self.norm_style,
4918 );
4919 let logits = self.lm_head_forward(&normed);
4923 let target = ids[pos + 1] as usize;
4924 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
4925 for (i, &v) in logits.iter().enumerate() {
4926 if v > mval {
4927 mval = v;
4928 amax = i;
4929 }
4930 }
4931 correct.push(amax == target);
4932 let row: Vec<f32> = temps
4933 .iter()
4934 .map(|&t| {
4935 let tt = t.max(1e-3);
4936 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
4937 1.0 / s.max(1e-12) })
4939 .collect();
4940 pmax.push(row);
4941 }
4942 self.kv_cache.clear();
4943 self.kv_history.clear();
4944 (correct, pmax)
4945 }
4946
4947 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
4954 let mut router = match self.dyn_router.take() {
4955 Some(r) => r,
4956 None => return (self.ppl_ids(ids), 0),
4957 };
4958 router.reset();
4959 self.dyn_phi_seen = 0;
4960 let _ = self.set_active_skill(None);
4961
4962 self.kv_cache.clear();
4963
4964 self.kv_history.clear();
4965 let mut nll = 0f64;
4966 let mut cnt = 0usize;
4967 for pos in 0..ids.len().saturating_sub(1) {
4968 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4969 let normed = inference::rms_norm(
4970 &hidden,
4971 &self.weights.final_norm,
4972 self.rms_eps,
4973 self.norm_style,
4974 );
4975 let logits = self.lm_head_forward(&normed);
4979 let target = ids[pos + 1] as usize;
4980 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4981 let lse: f64 = logits
4982 .iter()
4983 .map(|&v| ((v - max) as f64).exp())
4984 .sum::<f64>()
4985 .ln()
4986 + max as f64;
4987 let tok_nll = lse - logits[target] as f64;
4988 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4989 let top = logits
4990 .iter()
4991 .enumerate()
4992 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4993 .map(|(i, _)| i)
4994 .unwrap_or(0);
4995 eprintln!(
4996 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4997 logits[target], logits[top]
4998 );
4999 }
5000 nll += tok_nll;
5001 cnt += 1;
5002 let phi = self.dyn_phi_ema.clone();
5004 if let Some(new_active) = router.step(&phi, pos) {
5005 let _ = self.set_active_skill(new_active);
5006 }
5007 }
5008 let switches = router.switches.len();
5009 let _ = self.set_active_skill(None);
5010 self.dyn_router = Some(router);
5011 self.kv_cache.clear();
5012 self.kv_history.clear();
5013 ((nll / cnt.max(1) as f64).exp(), switches)
5014 }
5015
5016 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
5018 self.kv_cache.clear();
5019 self.kv_history.clear();
5020 let mut acc = vec![0f32; self.hidden_size];
5021 for (pos, &id) in ids.iter().enumerate() {
5022 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
5023 for (a, v) in acc.iter_mut().zip(&h) {
5024 *a += v;
5025 }
5026 }
5027 let n = ids.len().max(1) as f32;
5028 for a in acc.iter_mut() {
5029 *a /= n;
5030 }
5031 self.kv_cache.clear();
5032 self.kv_history.clear();
5033 acc
5034 }
5035
5036 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
5042 self.prefill_batch_masked(ids, start_pos, None)
5043 }
5044
5045 fn prefill_batch_masked(
5051 &mut self,
5052 ids: &[u32],
5053 start_pos: usize,
5054 task_mask: Option<&TaskMask>,
5055 ) -> Vec<f32> {
5056 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
5057 }
5058
5059 fn prefill_batch_span(
5065 &mut self,
5066 input: PrefillIn<'_>,
5067 start_pos: usize,
5068 task_mask: Option<&TaskMask>,
5069 from: usize,
5070 upto_excl: usize,
5071 ) -> Vec<f32> {
5072 let hs = self.hidden_size;
5073 let b = match input {
5074 PrefillIn::Ids(ids) => ids.len(),
5075 PrefillIn::Hidden(hb) => hb.len() / hs,
5076 };
5077 let upto_excl = upto_excl.min(self.num_layers);
5078 let mut h: Vec<f32>;
5082 let mut h_ready;
5083 match input {
5084 PrefillIn::Ids(_) => {
5085 h = vec![0.0; b * hs];
5086 h_ready = false;
5087 }
5088 PrefillIn::Hidden(hb) => {
5089 h = hb.to_vec();
5090 h_ready = true;
5091 }
5092 }
5093 let fill_h = |h: &mut Vec<f32>, me: &Self| {
5094 if let PrefillIn::Ids(ids) = input {
5095 for (bi, &id) in ids.iter().enumerate() {
5096 let e = me.embed_single(id);
5097 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
5098 }
5099 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5100 if let Ok(t) = tp.parse::<usize>() {
5101 if t >= start_pos && t < start_pos + ids.len() {
5102 let bi = t - start_pos;
5103 let row = &h[bi * hs..(bi + 1) * hs];
5104 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5105 eprintln!(
5106 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
5107 ids[bi], row[0], row[1], ids.len(), &ids[..ids.len().min(8)]
5108 );
5109 }
5110 }
5111 }
5112 }
5113 };
5114 let (_nkv, _hd, _rd, eps) = (
5115 self.num_kv_heads,
5116 self.head_dim,
5117 self.rotary_dim,
5118 self.rms_eps,
5119 );
5120 let pool = self.pool.clone();
5121 let norm_style = self.norm_style;
5122
5123 #[cfg(target_os = "macos")]
5124 let mut chunk_skip_until = 0usize;
5125 for li in from..upto_excl {
5126 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
5133 if task_mask.is_none() {
5134 if li < chunk_skip_until {
5135 continue;
5136 }
5137 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
5143 fill_h(&mut h, self);
5144 h_ready = true;
5145 }
5146 let ids_for_embed = match input {
5147 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
5148 PrefillIn::Hidden(_) => None,
5149 };
5150 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
5151 if end > li {
5152 h_ready = true;
5153 chunk_skip_until = end;
5154 if self.is_loop_end(end - 1) && end < self.num_layers {
5157 for bi in 0..b {
5158 let normed = inference::rms_norm(
5159 &h[bi * hs..(bi + 1) * hs],
5160 &self.weights.final_norm,
5161 eps,
5162 norm_style,
5163 );
5164 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5165 }
5166 }
5167 continue;
5168 }
5169 }
5170 if !h_ready {
5171 fill_h(&mut h, self);
5172 h_ready = true;
5173 }
5174 let lw = &self.weights.layers[self.phys_layer(li)];
5175 match &lw.attn {
5177 AttnKind::Kda(w) => {
5178 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5180 let mut normed = vec![0.0f32; b * hs];
5181 for bi in 0..b {
5182 inference::rms_norm_into(
5183 &h[bi * hs..(bi + 1) * hs],
5184 &lw.input_norm,
5185 eps,
5186 norm_style,
5187 &mut normed[bi * hs..(bi + 1) * hs],
5188 );
5189 }
5190 let attn = crate::linear_core::kda_forward_batch(
5191 &normed,
5192 b,
5193 w,
5194 &cfg,
5195 &mut self.kv_cache.layers[li].linear_state,
5196 pool.as_deref(),
5197 );
5198 for (dst, &a) in h.iter_mut().zip(&attn) {
5199 *dst += a;
5200 }
5201 }
5202 AttnKind::LinearGdn(w) => {
5203 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5205 let mut normed = vec![0.0f32; b * hs];
5206 for bi in 0..b {
5207 let r = inference::rms_norm(
5208 &h[bi * hs..(bi + 1) * hs],
5209 &lw.input_norm,
5210 eps,
5211 norm_style,
5212 );
5213 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5214 }
5215 let attn = crate::linear_core::gdn_forward_batch(
5216 &normed,
5217 b,
5218 w,
5219 &cfg,
5220 &mut self.kv_cache.layers[li].linear_state,
5221 pool.as_deref(),
5222 );
5223 for (dst, &a) in h.iter_mut().zip(&attn) {
5224 *dst += a;
5225 }
5226 }
5227 AttnKind::ShortConv(w) => {
5228 let cfg = self
5231 .short_conv_cfg
5232 .expect("short-conv layer without short_conv_cfg");
5233 let mut normed = vec![0.0f32; b * hs];
5234 for bi in 0..b {
5235 inference::rms_norm_into(
5236 &h[bi * hs..(bi + 1) * hs],
5237 &lw.input_norm,
5238 eps,
5239 norm_style,
5240 &mut normed[bi * hs..(bi + 1) * hs],
5241 );
5242 }
5243 let attn = short_conv_forward_batch(
5244 &normed,
5245 b,
5246 w,
5247 &cfg,
5248 &mut self.kv_cache.layers[li].linear_state,
5249 pool.as_deref(),
5250 );
5251 for (dst, &a) in h.iter_mut().zip(&attn) {
5252 *dst += a;
5253 }
5254 }
5255 AttnKind::Mla(w) => {
5256 let inv_freq_l = self.layer_inv_freq(li);
5259 let rs = self.layer_rope_scale(li);
5260 let mut normed = vec![0.0f32; hs];
5261 for bi in 0..b {
5262 inference::rms_norm_into(
5263 &h[bi * hs..(bi + 1) * hs],
5264 &lw.input_norm,
5265 eps,
5266 norm_style,
5267 &mut normed,
5268 );
5269 let ao = mla_attention(
5270 w,
5271 &normed,
5272 &mut self.kv_cache.layers[li],
5273 start_pos + bi,
5274 &inv_freq_l,
5275 rs,
5276 eps,
5277 pool.as_deref(),
5278 );
5279 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
5280 *dst += a;
5281 }
5282 }
5283 }
5284 AttnKind::Full {
5285 wq,
5286 wk,
5287 wv,
5288 wo,
5289 q_norm,
5290 k_norm,
5291 output_gate,
5292 softplus_gate,
5293 bias,
5294 } => {
5295 let mut normed = vec![0.0f32; b * hs];
5299 for bi in 0..b {
5300 inference::rms_norm_into(
5301 &h[bi * hs..(bi + 1) * hs],
5302 &lw.input_norm,
5303 eps,
5304 norm_style,
5305 &mut normed[bi * hs..(bi + 1) * hs],
5306 );
5307 }
5308 let inv_freq_l = self.layer_inv_freq(li);
5309 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5310 let cfg = QwenAttnCfg {
5311 num_heads: self.layer_num_heads(li),
5312 num_kv_heads: nkv_l,
5313 head_dim: hd_l,
5314 hidden_size: hs,
5315 position: start_pos,
5316 inv_freq: &inv_freq_l,
5317 rotary_dim: rd_l,
5318 scale: self.attn_scale,
5319 softcap: self.attn_softcap,
5320 window: self.layer_window(li),
5321 v_norm: self.attn_v_norm,
5322 q_norm: q_norm.as_deref(),
5323 k_norm: k_norm.as_deref(),
5324 output_gate: *output_gate,
5325 softplus_gate: softplus_gate
5326 .as_ref()
5327 .map(|(gate, per_head)| (gate, *per_head)),
5328 rope_scale: self.layer_rope_scale(li),
5329 bias: bias
5330 .as_ref()
5331 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5332 rms_eps: eps,
5333 norm_style,
5334 pool: pool.as_deref(),
5335 };
5336 let mut attn = attention::qwen_attention_batch(
5337 &normed,
5338 b,
5339 wq,
5340 wk,
5341 wv,
5342 wo,
5343 &mut self.kv_cache.layers[li],
5344 &cfg,
5345 );
5346 if let Some(w) = &lw.attn_out_norm {
5347 for bi in 0..b {
5348 inference::rms_norm_into(
5349 &attn[bi * hs..(bi + 1) * hs],
5350 w,
5351 eps,
5352 norm_style,
5353 &mut normed[bi * hs..(bi + 1) * hs],
5354 );
5355 }
5356 attn.copy_from_slice(&normed);
5357 }
5358 for (dst, &a) in h.iter_mut().zip(&attn) {
5359 *dst += a;
5360 }
5361 }
5362 AttnKind::Linear(w) => {
5363 for bi in 0..b {
5364 let normed = inference::rms_norm(
5365 &h[bi * hs..(bi + 1) * hs],
5366 &lw.input_norm,
5367 eps,
5368 norm_style,
5369 );
5370 vmf_phase_forward(
5371 &normed,
5372 w,
5373 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
5374 &mut self.kv_cache.layers[li].linear_state,
5375 pool.as_deref(),
5376 )
5377 .iter()
5378 .enumerate()
5379 .for_each(|(i, &a)| h[bi * hs + i] += a);
5380 }
5381 }
5382 }
5383
5384 let lw = &self.weights.layers[self.phys_layer(li)];
5386 let mut post = vec![0.0f32; b * hs];
5387 for bi in 0..b {
5388 let r =
5389 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
5390 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5391 }
5392 let mask_row = task_mask
5395 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
5396 .and_then(|m| m.ffn_masks.get(li))
5397 .map(|v| v.as_slice());
5398 let mut ffn = match &lw.ffn {
5399 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
5400 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
5401 FfnKind::DenseMoe(dm) => {
5404 let mut out = vec![0.0f32; b * hs];
5405 for bi in 0..b {
5406 let r = dense_moe_ffn(
5407 dm,
5408 &post[bi * hs..(bi + 1) * hs],
5409 &h[bi * hs..(bi + 1) * hs],
5410 eps,
5411 norm_style,
5412 pool.as_deref(),
5413 );
5414 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5415 }
5416 out
5417 }
5418 };
5419 if let Some(w) = &lw.ffn_out_norm {
5420 for bi in 0..b {
5421 inference::rms_norm_into(
5422 &ffn[bi * hs..(bi + 1) * hs],
5423 w,
5424 eps,
5425 norm_style,
5426 &mut post[bi * hs..(bi + 1) * hs],
5427 );
5428 }
5429 ffn.copy_from_slice(&post);
5430 }
5431 for (dst, &f) in h.iter_mut().zip(&ffn) {
5432 *dst += f;
5433 }
5434 if let Some(sc) = lw.layer_scale {
5435 for v in h.iter_mut() {
5436 *v *= sc;
5437 }
5438 }
5439 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5440 if let Ok(t) = tp.parse::<usize>() {
5441 if t >= start_pos && t < start_pos + b {
5442 let bi = t - start_pos;
5443 let row = &h[bi * hs..(bi + 1) * hs];
5444 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5445 eprintln!(
5446 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5447 row[0], row[1]
5448 );
5449 }
5450 }
5451 }
5452 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
5456 let row = &h[(b - 1) * hs..b * hs];
5457 let rms =
5458 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
5459 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
5460 eprintln!(
5461 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
5462 match &self.weights.layers[self.phys_layer(li)].attn {
5463 AttnKind::LinearGdn(_) => "gdn",
5464 AttnKind::Linear(_) => "vmf",
5465 AttnKind::ShortConv(_) => "conv",
5466 _ => "attn",
5467 },
5468 match &lw.ffn {
5469 FfnKind::Moe(_) => "moe",
5470 FfnKind::Dense(_) => "dense",
5471 FfnKind::DenseMoe(_) => "dense+moe",
5472 },
5473 );
5474 }
5475 if self.is_loop_end(li) && li + 1 < self.num_layers {
5477 for bi in 0..b {
5478 let normed = inference::rms_norm(
5479 &h[bi * hs..(bi + 1) * hs],
5480 &self.weights.final_norm,
5481 eps,
5482 norm_style,
5483 );
5484 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5485 }
5486 }
5487 if std::env::var("CMF_TRACE_H").is_ok() {
5488 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
5489 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
5490 eprintln!(
5491 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
5492 lw.layer_scale
5493 );
5494 }
5495 }
5496 crate::gpu::set_layer(-1); h
5498 }
5499
5500 fn embed_single(&self, id: u32) -> Vec<f32> {
5502 let mut out = vec![0.0f32; self.hidden_size];
5503 if (id as usize) < self.weights.embed_tokens.rows() {
5504 self.weights.embed_tokens.row_f32(id as usize, &mut out);
5505 }
5506 if self.embed_multiplier != 1.0 {
5507 for v in out.iter_mut() {
5508 *v *= self.embed_multiplier;
5509 }
5510 }
5511 if self.dsv4.is_some() {
5515 let mut v = vec![0.0f32; self.hidden_size.max(1)];
5516 v[0] = id as f32;
5517 return v;
5518 }
5519 if let Some(b) = &self.g3n {
5522 return b.0.extend_embedding(id, &out, self.pool.as_deref());
5523 }
5524 out
5525 }
5526
5527 #[cfg(target_os = "macos")]
5533 fn chunk_run_gpu(
5534 &mut self,
5535 li0: usize,
5536 h: &mut [f32],
5537 b: usize,
5538 pos0: usize,
5539 embed_ids: Option<&[u32]>,
5540 cap: usize,
5541 ) -> usize {
5542 if !crate::gpu::enabled_here()
5546 || std::env::var("CMF_GPU_CHUNK")
5547 .map(|v| v == "0")
5548 .unwrap_or(false)
5549 || b < 32
5550 || self.swa.is_some()
5551 || self.global_attn.is_some()
5552 || self.attn_v_norm
5553 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
5554 {
5555 return li0;
5556 }
5557 let Some(model) = self.model.clone() else {
5558 return li0;
5559 };
5560 let inv_freq = self.inv_freq.clone();
5561 let (nh, nkv, hd, hs) = (
5562 self.num_heads,
5563 self.num_kv_heads,
5564 self.head_dim,
5565 self.hidden_size,
5566 );
5567 let loop_end = if self.loop_final_norm {
5571 ((li0 / self.physical_layers) + 1) * self.physical_layers
5572 } else {
5573 self.num_layers
5574 };
5575 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
5576 let mut stored_at: Vec<usize> = Vec::new();
5577 for li in li0..self.num_layers.min(loop_end).min(cap) {
5578 let lw = &self.weights.layers[self.phys_layer(li)];
5579 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
5580 break;
5581 }
5582 let AttnKind::Full {
5583 wq,
5584 wk,
5585 wv,
5586 wo,
5587 q_norm,
5588 k_norm,
5589 output_gate: false,
5590 softplus_gate: None,
5591 bias,
5592 } = &lw.attn
5593 else {
5594 break;
5595 };
5596 let FfnKind::Dense(d) = &lw.ffn else { break };
5597 if d.act != Act::Silu {
5598 break;
5599 }
5600 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
5605 t.q8_row_parts()
5606 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5607 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5608 }
5609 let parts = (
5610 cw(wq),
5611 cw(wk),
5612 cw(wv),
5613 cw(wo),
5614 cw(&d.gate_proj),
5615 cw(&d.up_proj),
5616 cw(&d.down_proj),
5617 );
5618 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
5619 else {
5620 break;
5621 };
5622 let layer = &self.kv_cache.layers[li];
5623 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
5624 break;
5625 }
5626 stored_at.push(layer.head_len(0));
5627 layers.push(crate::gpu_metal::ChunkLayer {
5628 model: &model,
5629 kv_id: self.graph_kv_id,
5630 layer: li,
5631 wq: pq,
5632 wk: pk,
5633 wv: pv,
5634 wo: po,
5635 gate: pg,
5636 up: pu,
5637 down: pd,
5638 input_norm: &lw.input_norm,
5639 post_norm: &lw.post_norm,
5640 bias: bias
5641 .as_ref()
5642 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
5643 q_norm: q_norm.as_deref(),
5644 k_norm: k_norm.as_deref(),
5645 inv_freq: &inv_freq,
5646 rd: self.rotary_dim,
5647 nh,
5648 nkv,
5649 hd,
5650 hs,
5651 inter: d.gate_proj.rows(),
5652 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
5653 eps: self.rms_eps as f32,
5654 });
5655 }
5656 if layers.is_empty() {
5657 return li0;
5658 }
5659 let row = nkv * hd;
5660 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
5661 .iter()
5662 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
5663 .collect();
5664 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
5665 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
5666 let li = layers[i].layer;
5667 let layer = &self.kv_cache.layers[li];
5668 io.push(crate::gpu_metal::ChunkIo {
5669 cpu_stored: stored_at[i],
5670 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
5671 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
5672 out_k: ok,
5673 out_v: ov,
5674 imp: oi,
5675 });
5676 }
5677 let n_run = layers.len();
5678 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
5679 let ep = embed_ids.and_then(|ids| {
5682 self.weights
5683 .embed_tokens
5684 .q8_row_parts()
5685 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
5686 idx,
5687 rows,
5688 row_scale: rs,
5689 ids,
5690 mult: self.embed_multiplier,
5691 })
5692 });
5693 if embed_ids.is_some() && ep.is_none() {
5694 return li0;
5695 }
5696 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
5697 return li0;
5698 }
5699 drop(io);
5700 drop(layers);
5701 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
5704 let li = li0 + i;
5705 let layer = &mut self.kv_cache.layers[li];
5706 for bi in 0..b {
5707 layer.append(
5708 &ok[bi * row..(bi + 1) * row],
5709 &ov[bi * row..(bi + 1) * row],
5710 &[],
5711 );
5712 }
5713 layer.accumulate_imp(oi);
5714 }
5715 last
5716 }
5717
5718 fn layer_is_local(&self, li: usize) -> bool {
5721 if let Some(layers) = &self.sliding_layers {
5722 return layers.get(li).copied().unwrap_or(false);
5723 }
5724 match self.swa {
5725 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
5726 None => false,
5727 }
5728 }
5729
5730 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
5733 if self.layer_is_local(li) {
5734 if let Some(f) = &self.inv_freq_local {
5735 return f.clone();
5736 }
5737 } else if let Some(f) = &self.inv_freq_global {
5738 return f.clone();
5739 }
5740 self.inv_freq.clone()
5741 }
5742
5743 fn layer_window(&self, li: usize) -> Option<usize> {
5745 self.swa
5746 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
5747 }
5748
5749 fn layer_num_heads(&self, li: usize) -> usize {
5750 self.attention_heads_per_layer
5751 .as_ref()
5752 .and_then(|v| v.get(li).copied())
5753 .unwrap_or(self.num_heads)
5754 }
5755
5756 fn layer_rope_scale(&self, li: usize) -> f32 {
5757 if self.layer_is_local(li) {
5758 self.rope_scale_local
5759 } else {
5760 self.rope_scale
5761 }
5762 }
5763
5764 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
5767 if !self.layer_is_local(li) {
5768 if let Some((ghd, gkv)) = self.global_attn {
5769 return (gkv, ghd, ghd);
5770 }
5771 }
5772 (
5773 self.num_kv_heads,
5774 self.head_dim,
5775 if self.layer_is_local(li) {
5776 self.rotary_dim_local.unwrap_or(self.rotary_dim)
5777 } else {
5778 self.rotary_dim
5779 },
5780 )
5781 }
5782
5783 fn forward_layers(
5785 &mut self,
5786 hidden: &[f32],
5787 position: usize,
5788 task_mask: Option<&TaskMask>,
5789 ) -> Vec<f32> {
5790 self.forward_layers_upto(hidden, position, task_mask, None)
5791 }
5792
5793 pub fn embed_id(&self, id: u32) -> Vec<f32> {
5801 self.embed_single(id)
5802 }
5803
5804 pub fn split_supported(&self) -> Result<(), String> {
5808 if self.dsv4.is_some() {
5809 return Err(
5810 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
5811 );
5812 }
5813 if self.g3n.is_some() {
5814 return Err(
5815 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
5816 );
5817 }
5818 Ok(())
5819 }
5820
5821 pub fn forward_span(
5826 &mut self,
5827 hidden: &[f32],
5828 position: usize,
5829 from: usize,
5830 upto: usize,
5831 task_mask: Option<&TaskMask>,
5832 ) -> Result<Vec<f32>, String> {
5833 self.split_supported()?;
5834 if from > upto || upto >= self.num_layers {
5835 return Err(format!(
5836 "forward_span: layer range {from}..={upto} outside 0..{}",
5837 self.num_layers
5838 ));
5839 }
5840 if hidden.len() != self.hidden_size {
5841 return Err(format!(
5842 "forward_span: hidden len {} ≠ hidden_size {}",
5843 hidden.len(),
5844 self.hidden_size
5845 ));
5846 }
5847 Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
5848 }
5849
5850 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
5853 let normed = inference::rms_norm(
5854 hidden,
5855 &self.weights.final_norm,
5856 self.rms_eps,
5857 self.norm_style,
5858 );
5859 self.lm_head_forward(&normed)
5860 }
5861
5862 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
5864 sampler::sample_with_scratch(
5865 logits,
5866 &self.sampler_config,
5867 past_tokens,
5868 &mut self.rng,
5869 &mut self.sampler_scratch,
5870 )
5871 }
5872
5873 pub fn reset_session(&mut self) {
5875 self.kv_cache.clear();
5876 self.kv_history.clear();
5877 crate::gpu::graph_kv_reset(self.graph_kv_id);
5878 }
5879
5880 pub fn prefill_span_ids(
5886 &mut self,
5887 ids: &[u32],
5888 start_pos: usize,
5889 upto: usize,
5890 task_mask: Option<&TaskMask>,
5891 ) -> Result<Vec<f32>, String> {
5892 self.split_supported()?;
5893 if upto >= self.num_layers {
5894 return Err(format!(
5895 "prefill_span_ids: upto {upto} outside 0..{}",
5896 self.num_layers
5897 ));
5898 }
5899 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5903 Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
5904 } else {
5905 let hs = self.hidden_size;
5906 let mut out = Vec::with_capacity(ids.len() * hs);
5907 for (i, &id) in ids.iter().enumerate() {
5908 let emb = self.embed_id(id);
5909 out.extend_from_slice(&self.forward_span(
5910 &emb,
5911 start_pos + i,
5912 0,
5913 upto,
5914 task_mask,
5915 )?);
5916 }
5917 Ok(out)
5918 }
5919 }
5920
5921 pub fn prefill_span_hidden(
5924 &mut self,
5925 hidden: &[f32],
5926 start_pos: usize,
5927 from: usize,
5928 upto: usize,
5929 task_mask: Option<&TaskMask>,
5930 ) -> Result<Vec<f32>, String> {
5931 self.split_supported()?;
5932 let hs = self.hidden_size;
5933 if hidden.is_empty() || hidden.len() % hs != 0 {
5934 return Err(format!(
5935 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
5936 hidden.len()
5937 ));
5938 }
5939 if from > upto || upto >= self.num_layers {
5940 return Err(format!(
5941 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
5942 self.num_layers
5943 ));
5944 }
5945 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5946 Ok(self.prefill_batch_span(
5947 PrefillIn::Hidden(hidden),
5948 start_pos,
5949 task_mask,
5950 from,
5951 upto + 1,
5952 ))
5953 } else {
5954 let b = hidden.len() / hs;
5955 let mut out = Vec::with_capacity(hidden.len());
5956 for i in 0..b {
5957 let h = self.forward_span(
5958 &hidden[i * hs..(i + 1) * hs],
5959 start_pos + i,
5960 from,
5961 upto,
5962 task_mask,
5963 )?;
5964 out.extend_from_slice(&h);
5965 }
5966 Ok(out)
5967 }
5968 }
5969
5970 fn try_token_graph_wgpu(
5974 &self,
5975 hidden: &[f32],
5976 position: usize,
5977 logits_out: &mut Vec<f32>,
5978 layers_run: &mut usize,
5979 ) -> Option<Vec<f32>> {
5980 self.try_token_graph_wgpu_steps(
5981 hidden,
5982 position,
5983 logits_out,
5984 1,
5985 None,
5986 Some(layers_run),
5987 0,
5988 self.num_layers,
5989 )
5990 }
5991
5992 fn try_token_graph_wgpu_span(
5996 &self,
5997 hidden: &[f32],
5998 position: usize,
5999 logits_out: &mut Vec<f32>,
6000 from: usize,
6001 upto_excl: usize,
6002 layers_run: &mut usize,
6003 ) -> Option<Vec<f32>> {
6004 self.try_token_graph_wgpu_steps(
6005 hidden,
6006 position,
6007 logits_out,
6008 1,
6009 None,
6010 Some(layers_run),
6011 from,
6012 upto_excl,
6013 )
6014 }
6015
6016 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
6020 if self.o1_active() || self.attn_softcap > 0.0 {
6021 return None;
6022 }
6023 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
6024 if !graph_on || crate::gpu::graph_unsupported() {
6025 return None;
6032 }
6033 let emb = self.embed_single(t_next);
6034 let mut lg = Vec::new();
6035 let mut ids = Vec::new();
6036 self.try_token_graph_wgpu_steps(
6037 &emb,
6038 position,
6039 &mut lg,
6040 k,
6041 Some(&mut ids),
6042 None,
6043 0,
6044 self.num_layers,
6045 )?;
6046 (ids.len() == k).then_some(ids)
6047 }
6048
6049 fn try_token_graph_wgpu_steps(
6053 &self,
6054 hidden: &[f32],
6055 position: usize,
6056 logits_out: &mut Vec<f32>,
6057 steps: usize,
6058 ids_out: Option<&mut Vec<u32>>,
6059 layers_run: Option<&mut usize>,
6060 from: usize,
6061 upto_excl: usize,
6062 ) -> Option<Vec<f32>> {
6063 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
6066 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
6067 return None;
6071 }
6072 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
6077 .map(|li| {
6078 if !o1_gpu {
6079 return None;
6080 }
6081 self.kv_cache.layers[self.phys_layer(li)].o1_views()
6082 })
6083 .collect();
6084 if self.o1_active() && o1_gpu {
6085 let want: usize = (from..upto_excl)
6088 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
6089 .count();
6090 let have = o1_views.iter().filter(|v| v.is_some()).count();
6091 if want == 0 || have != want {
6092 use std::sync::atomic::{AtomicUsize, Ordering};
6102 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
6103 let code = have * 1000 + want;
6104 if LAST.swap(code, Ordering::Relaxed) != code {
6105 tracing::warn!(
6106 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
6107 );
6108 }
6109 return None;
6110 }
6111 }
6112 let nh = self.num_heads;
6113 let (nkv, hd, rd) = self.layer_geom(0);
6114 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6115 let mut layers = Vec::with_capacity(upto_excl - from);
6116 let mut model = None;
6117 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
6118 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6119 if let Some((_, i, kind, rs)) = t.graph_weight() {
6120 return Some(crate::gpu::GraphW {
6121 idx: i,
6122 kind,
6123 row_scale: rs,
6124 data: &[],
6125 });
6126 }
6127 t.as_f32().map(|d| crate::gpu::GraphW {
6129 idx: 0,
6130 kind: 4,
6131 row_scale: &[],
6132 data: d,
6133 })
6134 }
6135 for li in from..upto_excl {
6136 let lw = &self.weights.layers[self.phys_layer(li)];
6137 if dbg {
6138 let ak = match &lw.attn {
6139 AttnKind::Mla(_) => "Mla".into(),
6140 AttnKind::Full {
6141 output_gate, bias, ..
6142 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
6143 AttnKind::LinearGdn(_) => "LinearGdn".into(),
6144 AttnKind::Kda(_) => "Kda".into(),
6145 AttnKind::Linear(_) => "Linear".into(),
6146 AttnKind::ShortConv(_) => "ShortConv".into(),
6147 };
6148 let fk = match &lw.ffn {
6149 FfnKind::Dense(_) => "Dense",
6150 FfnKind::Moe(_) => "Moe",
6151 FfnKind::DenseMoe(_) => "DenseMoe",
6152 };
6153 eprintln!("graph L{li}: attn={ak} ffn={fk}");
6154 }
6155 let gffn = match &lw.ffn {
6156 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
6158 gate: gw(&d.gate_proj)?,
6159 up: gw(&d.up_proj)?,
6160 down: gw(&d.down_proj)?,
6161 },
6162 FfnKind::Moe(m) => {
6163 if m.router_sigmoid
6168 || m.expert_bias.is_some()
6169 || m.route_tau.is_some()
6170 || m.mask.is_some()
6171 {
6172 return None;
6173 }
6174 let (se, sg) = m.shared.as_ref()?;
6175 let sgate = gw(sg.as_ref()?)?;
6176 let router = gw(&m.router)?;
6177 let inter = m.experts.first()?.gate_proj.rows();
6178 let mut experts = Vec::with_capacity(m.experts.len() + 1);
6179 let mut q4tp: Option<bool> = None;
6182 let mut gu_q2: Option<bool> = None;
6185 for e in m.experts.iter().chain(std::iter::once(se)) {
6186 if !matches!(e.act, Act::Silu)
6187 || e.gate_proj.rows() != inter
6188 || e.up_proj.rows() != inter
6189 {
6190 return None;
6191 }
6192 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
6193 Some((mm, gi)) => (
6194 mm,
6195 gi,
6196 e.up_proj.mapped_q4t()?.1,
6197 e.down_proj.mapped_q4t()?.1,
6198 false,
6199 false,
6200 ),
6201 None => match e.gate_proj.mapped_q2tp() {
6202 Some((mm, gi)) => (
6203 mm,
6204 gi,
6205 e.up_proj.mapped_q2tp()?.1,
6206 e.down_proj.mapped_q4tp()?.1,
6207 true,
6208 true,
6209 ),
6210 None => {
6211 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
6212 (
6213 mm,
6214 gi,
6215 e.up_proj.mapped_q4tp()?.1,
6216 e.down_proj.mapped_q4tp()?.1,
6217 true,
6218 false,
6219 )
6220 }
6221 },
6222 };
6223 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
6224 {
6225 tracing::warn!(
6231 "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."
6232 );
6233 return None;
6234 }
6235 model.get_or_insert_with(|| mm.clone());
6236 experts.push((gi, ui, di));
6237 }
6238 crate::gpu::GraphFfn::Moe {
6239 router,
6240 shared_gate: sgate,
6241 experts,
6242 n_exp: m.experts.len(),
6243 top_k: std::env::var("CMF_TOPK_PROBE")
6249 .ok()
6250 .and_then(|v| v.parse::<usize>().ok())
6251 .filter(|k| *k > 0 && *k <= m.top_k)
6252 .unwrap_or(m.top_k),
6253 inter,
6254 norm_topk: m.norm_topk_prob,
6255 q4tp: q4tp?,
6256 gu_q2: gu_q2.unwrap_or(false),
6257 }
6258 }
6259 };
6260 let attn = match &lw.attn {
6261 AttnKind::Full {
6262 wq,
6263 wk,
6264 wv,
6265 wo,
6266 q_norm,
6267 k_norm,
6268 output_gate,
6269 softplus_gate,
6270 bias,
6271 } => {
6272 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
6273 return None;
6274 }
6275 let (m, _, _, _) = wq.graph_weight()?;
6276 model = Some(m.clone());
6277 crate::gpu::GraphAttn::Full {
6278 wq: gw(wq)?,
6279 wk: gw(wk)?,
6280 wv: gw(wv)?,
6281 wo: gw(wo)?,
6282 q_norm: q_norm.as_deref(),
6283 k_norm: k_norm.as_deref(),
6284 bias: bias
6285 .as_ref()
6286 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6287 output_gate: *output_gate,
6288 cpu_k: self.kv_cache.layers[li].k_heads(),
6289 cpu_v: self.kv_cache.layers[li].v_heads(),
6290 }
6291 }
6292 AttnKind::LinearGdn(w) => {
6293 let cfg = self.gdn_cfg?;
6294 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
6295 model = Some(m.clone());
6296 crate::gpu::GraphAttn::Gdn {
6297 qkv: gw(&w.in_proj_qkv)?,
6298 z: gw(&w.in_proj_z)?,
6299 a: gw(&w.in_proj_a)?,
6300 b: gw(&w.in_proj_b)?,
6301 out: gw(&w.out_proj)?,
6302 conv1d: &w.conv1d,
6303 a_log: &w.a_log,
6304 dt_bias: &w.dt_bias,
6305 norm: &w.norm,
6306 nv: cfg.num_v_heads,
6307 nk: cfg.num_k_heads,
6308 dk: cfg.key_head_dim,
6309 dv: cfg.value_head_dim,
6310 kk: cfg.conv_kernel,
6311 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6312 }
6313 }
6314 _ => return None,
6315 };
6316 layers.push(crate::gpu::GraphLayer {
6317 input_norm: &lw.input_norm,
6318 attn,
6319 post_norm: &lw.post_norm,
6320 ffn: gffn,
6321 });
6322 }
6323 let model = model?;
6324 let lm_gw = if upto_excl == self.num_layers
6330 && self.graph_want_logits
6331 && std::env::var("CMF_GPU_LMHEAD")
6332 .map(|v| v != "0")
6333 .unwrap_or(true)
6334 {
6335 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
6336 (
6337 crate::gpu::GraphW {
6338 idx: i,
6339 kind,
6340 row_scale: rs,
6341 data: &[],
6342 },
6343 self.weights.lm_head.rows(),
6344 )
6345 })
6346 } else {
6347 None
6348 };
6349 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
6350 let emb_gw = if steps > 1 {
6352 self.weights
6353 .embed_tokens
6354 .graph_weight()
6355 .map(|(_, i, kind, rs)| {
6356 (
6357 crate::gpu::GraphW {
6358 idx: i,
6359 kind,
6360 row_scale: rs,
6361 data: &[],
6362 },
6363 self.weights.embed_tokens.rows(),
6364 self.embed_multiplier,
6365 )
6366 })
6367 } else {
6368 None
6369 };
6370
6371 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
6377 (from..upto_excl.min(self.num_layers - 1))
6378 .filter(|&li| (li + 1) % self.physical_layers == 0)
6379 .map(|li| li - from)
6380 .collect()
6381 } else {
6382 Vec::new()
6383 };
6384 let mut h = hidden.to_vec();
6385 crate::gpu::forward_token_graph(
6386 &model,
6387 self.graph_kv_id,
6388 &layers,
6389 &o1_views,
6390 self.o1_epoch,
6391 &self.inv_freq,
6392 &mut h,
6393 nh,
6394 nkv,
6395 hd,
6396 rd,
6397 self.hidden_size,
6398 self.intermediate_size,
6399 position,
6400 self.kv_cache.max_seq_len,
6401 gemma,
6402 self.rms_eps as f32,
6403 lm,
6404 &self.weights.final_norm,
6405 logits_out,
6406 &loop_norm_at,
6407 steps,
6408 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
6409 ids_out,
6410 layers_run,
6411 from,
6412 false,
6413 )
6414 .then_some(h)
6415 }
6416
6417 #[cfg(target_os = "macos")]
6426 #[allow(clippy::type_complexity)]
6427 fn metal_rows_plan(&self) -> Option<(Vec<MetalRowsItem<'_>>, std::sync::Arc<cortiq_core::CmfModel>, Option<crate::gpu_metal::GdnGpuCfg>)> {
6428 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
6429 if !crate::gpu::q1_force()
6430 || !crate::gpu::enabled_here()
6431 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
6432 || self.attn_softcap > 0.0
6433 || self.o1_active()
6434 || self.swa.is_some()
6435 || self.global_attn.is_some()
6436 || self.attention_heads_per_layer.is_some()
6437 || self.attn_v_norm
6438 || self.loop_final_norm
6439 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
6440 {
6441 return None;
6442 }
6443 let attend_contract = self.head_dim % 4 == 0
6444 && self.head_dim <= 256
6445 && self.rotary_dim >= 2
6446 && self.rotary_dim <= self.head_dim
6447 && (self.rotary_dim / 2) % 32 == 0
6448 && self.num_kv_heads > 0
6449 && self.num_heads % self.num_kv_heads == 0;
6450 if !attend_contract {
6451 return None;
6452 }
6453 let mut plan: Vec<MetalRowsItem> = Vec::new();
6454 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
6455 for li in 0..self.num_layers {
6456 let lw = &self.weights.layers[self.phys_layer(li)];
6457 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
6458 return None;
6459 }
6460 let ffn = match &lw.ffn {
6461 FfnKind::Dense(d) if d.act == Act::Silu => {
6462 let (Some(g), Some(u), Some(dn)) =
6463 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
6464 else {
6465 return None;
6466 };
6467 MetalFfn::Dense { gate: g, up: u, down: dn }
6468 }
6469 _ => return None,
6470 };
6471 match &lw.attn {
6472 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
6473 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
6474 w.in_proj_qkv.q1_parts(),
6475 w.in_proj_z.q1_parts(),
6476 w.in_proj_a.f32_parts(),
6477 w.in_proj_b.f32_parts(),
6478 w.out_proj.q1_parts(),
6479 ) else {
6480 return None;
6481 };
6482 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
6483 model_ref.get_or_insert_with(|| model.clone());
6484 }
6485 let gl = GdnGpuLayer {
6486 attn_norm: &lw.input_norm,
6487 post_norm: &lw.post_norm,
6488 qkv,
6489 z,
6490 a,
6491 b: bb,
6492 out,
6493 ffn,
6494 conv1d: &w.conv1d,
6495 a_log: &w.a_log,
6496 dt_bias: &w.dt_bias,
6497 gnorm: &w.norm,
6498 };
6499 match plan.last_mut() {
6500 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
6501 _ => plan.push(MetalRowsItem::Gdn { run: vec![gl], first: li }),
6502 }
6503 }
6504 AttnKind::Full {
6505 wq,
6506 wk,
6507 wv,
6508 wo,
6509 q_norm,
6510 k_norm,
6511 output_gate,
6512 softplus_gate: None,
6513 bias: None,
6514 } => {
6515 let (Some(pq), Some(pk), Some(pv), Some(po)) =
6516 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
6517 else {
6518 return None;
6519 };
6520 if let QTensor::Mapped { model, .. } = wq {
6521 model_ref.get_or_insert_with(|| model.clone());
6522 }
6523 let cache = &self.kv_cache.layers[li];
6524 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
6525 return None;
6526 }
6527 plan.push(MetalRowsItem::Attn {
6528 l: AttnGpuLayer {
6529 attn_norm: &lw.input_norm,
6530 post_norm: &lw.post_norm,
6531 wq: pq,
6532 wk: pk,
6533 wv: pv,
6534 wo: po,
6535 ffn,
6536 },
6537 li,
6538 q_norm: q_norm.as_deref(),
6539 k_norm: k_norm.as_deref(),
6540 output_gate: *output_gate,
6541 });
6542 }
6543 _ => return None,
6544 }
6545 }
6546 let model = model_ref?;
6547 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
6548 nv: cfg.num_v_heads,
6549 nk: cfg.num_k_heads,
6550 dk: cfg.key_head_dim,
6551 dv: cfg.value_head_dim,
6552 kk: cfg.conv_kernel,
6553 hidden: self.hidden_size,
6554 inter: self.intermediate_size,
6555 c_dim: cfg.conv_dim(),
6556 eps: cfg.rms_eps as f32,
6557 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6558 });
6559 Some((plan, model, gcfg))
6560 }
6561
6562 #[cfg(target_os = "macos")]
6564 #[allow(clippy::too_many_arguments)]
6565 fn metal_attn_params<'a>(
6566 li: usize,
6567 cache: &'a crate::kv_cache::LayerKvCache,
6568 q_norm: Option<&'a [f32]>,
6569 k_norm: Option<&'a [f32]>,
6570 output_gate: bool,
6571 inv_freq: &'a [f32],
6572 geom: (usize, usize, usize, usize),
6573 pos0: usize,
6574 kv_id: u64,
6575 eps: f32,
6576 gemma: bool,
6577 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
6578 let (nh, nkv, hd, rd) = geom;
6579 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6580 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6581 let cpu_stored = cpu_k[0].len() / hd;
6582 (
6583 crate::gpu_metal::AttnDeviceParams {
6584 kv_id,
6585 layer: li,
6586 nh,
6587 nkv,
6588 hd,
6589 rd,
6590 position: pos0,
6591 eps,
6592 gemma,
6593 output_gate,
6594 q_norm,
6595 k_norm,
6596 inv_freq,
6597 cpu_k,
6598 cpu_v,
6599 cpu_stored,
6600 o1: None,
6601 },
6602 cpu_stored,
6603 )
6604 }
6605
6606 #[cfg(target_os = "macos")]
6611 #[allow(clippy::type_complexity)]
6612 fn metal_rows_run(
6613 &mut self,
6614 hiddens: &mut [f32],
6615 pos0: usize,
6616 b: usize,
6617 prefill: bool,
6618 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6619 ) -> Option<MetalVerifyPending> {
6620 use crate::gpu_metal::{GraphDims, VerifyGraph};
6621 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
6622 for l in &mut self.kv_cache.layers {
6623 if l.linear_state.len() != want && want > 0 {
6624 l.linear_state = vec![0f32; want];
6625 }
6626 }
6627 let (plan, model, gcfg) = self.metal_rows_plan()?;
6628 let dims = GraphDims {
6629 hidden: self.hidden_size,
6630 eps: self.rms_eps as f32,
6631 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6632 };
6633 let mut graph = if prefill {
6634 VerifyGraph::new_prefill(&model, dims, hiddens, b)?
6635 } else {
6636 VerifyGraph::new(&model, dims, hiddens, b)?
6637 };
6638 let geom = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6639 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6640 let eps = self.rms_eps as f32;
6641 let kv_id = self.graph_kv_id;
6642 let inv_freq = self.inv_freq.clone();
6643 for item in &plan {
6644 let ok = match item {
6645 MetalRowsItem::Gdn { run, .. } => gcfg
6646 .as_ref()
6647 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
6648 .unwrap_or(false),
6649 MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6650 let (p, _) = Self::metal_attn_params(*li, &self.kv_cache.layers[*li], *q_norm, *k_norm, *output_gate, &inv_freq, geom, pos0, kv_id, eps, gemma);
6651 graph.attn_ok(l, &p)
6652 }
6653 };
6654 if !ok {
6655 use std::sync::atomic::{AtomicBool, Ordering};
6656 static SAID: AtomicBool = AtomicBool::new(false);
6657 if !SAID.swap(true, Ordering::Relaxed) {
6658 tracing::warn!("metal rows graph: a layer failed preflight — declining");
6659 }
6660 return None;
6661 }
6662 }
6663 let lm = match &spec {
6664 Some((lm, _, _)) => {
6665 if !graph.lm_head_ok(*lm) {
6666 return None;
6667 }
6668 Some(*lm)
6669 }
6670 None => None,
6671 };
6672 let mut gdn_layers = Vec::new();
6673 let mut attn_layers = Vec::new();
6674 for item in &plan {
6675 match item {
6676 MetalRowsItem::Gdn { run, first } => {
6677 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
6678 .iter()
6679 .map(|l| l.linear_state.as_slice())
6680 .collect();
6681 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
6682 return None;
6683 }
6684 gdn_layers.extend(*first..*first + run.len());
6685 }
6686 MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6687 let (p, cpu_stored) = Self::metal_attn_params(*li, &self.kv_cache.layers[*li], *q_norm, *k_norm, *output_gate, &inv_freq, geom, pos0, kv_id, eps, gemma);
6688 if !graph.encode_attn_b(l, &p) {
6689 return None;
6690 }
6691 attn_layers.push((*li, cpu_stored));
6692 }
6693 }
6694 }
6695 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
6696 if !graph.encode_lm_head_b(final_norm, lm) {
6697 return None;
6698 }
6699 }
6700 graph.sync();
6701 if let Some((lm, _, logits)) = spec {
6702 logits.resize(b * lm.1, 0.0);
6703 graph.read_logits(logits);
6704 }
6705 graph.read_hidden(hiddens);
6706 Some(MetalVerifyPending { graph, gdn_layers, attn_layers })
6707 }
6708
6709 #[cfg(target_os = "macos")]
6715 fn try_batch_graph_metal(
6716 &mut self,
6717 hiddens: &mut [f32],
6718 positions: &[usize],
6719 b: usize,
6720 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6721 ) -> bool {
6722 let _t0 = std::time::Instant::now();
6723 if positions.len() != b
6724 || positions.windows(2).any(|w| w[1] != w[0] + 1)
6725 || hiddens.len() != b * self.hidden_size
6726 {
6727 return false;
6728 }
6729 let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
6730 return false;
6731 };
6732 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6733 eprintln!("metal-verify: {:.1} ms | b={b}", _t0.elapsed().as_secs_f64() * 1e3);
6734 }
6735 self.metal_verify = Some(pending);
6736 true
6737 }
6738
6739 #[cfg(target_os = "macos")]
6744 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
6745 let b = ids.len();
6746 if b == 0 || b > 512 {
6747 return None;
6748 }
6749 let hs = self.hidden_size;
6750 let mut hiddens = vec![0f32; b * hs];
6751 for (j, &id) in ids.iter().enumerate() {
6752 let e = self.embed_single(id);
6753 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
6754 }
6755 let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
6756 let idxs = pending.gdn_layers.clone();
6758 let mut outs: Vec<&mut [f32]> = self
6759 .kv_cache
6760 .layers
6761 .iter_mut()
6762 .enumerate()
6763 .filter(|(i, _)| idxs.binary_search(i).is_ok())
6764 .map(|(_, l)| l.linear_state.as_mut_slice())
6765 .collect();
6766 pending.graph.finish_states(&mut outs);
6767 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6768 let mut kbuf = vec![0f32; b * nkv * hd];
6769 let mut vbuf = vec![0f32; b * nkv * hd];
6770 for (li, cpu_stored) in &pending.attn_layers {
6771 if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, b, &mut kbuf, &mut vbuf) {
6772 let cache = &mut self.kv_cache.layers[*li];
6773 for r in 0..b {
6774 cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6775 }
6776 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
6777 }
6778 }
6779 Some(hiddens)
6780 }
6781
6782 #[cfg(target_os = "macos")]
6786 fn metal_verify_commit(&mut self, a: usize) -> bool {
6787 let Some(mut pending) = self.metal_verify.take() else {
6788 return false;
6789 };
6790 let n = a + 1;
6791 let idxs = pending.gdn_layers.clone();
6793 let mut outs: Vec<&mut [f32]> = self
6794 .kv_cache
6795 .layers
6796 .iter_mut()
6797 .enumerate()
6798 .filter(|(i, _)| idxs.binary_search(i).is_ok())
6799 .map(|(_, l)| l.linear_state.as_mut_slice())
6800 .collect();
6801 if !pending.graph.commit(n, &mut outs) {
6802 return false;
6803 }
6804 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6805 let mut kbuf = vec![0f32; n * nkv * hd];
6806 let mut vbuf = vec![0f32; n * nkv * hd];
6807 for (li, cpu_stored) in &pending.attn_layers {
6808 if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, n, &mut kbuf, &mut vbuf) {
6809 let cache = &mut self.kv_cache.layers[*li];
6810 for r in 0..n {
6811 cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6812 }
6813 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
6814 }
6815 }
6816 true
6817 }
6818
6819 #[cfg(target_os = "macos")]
6825 fn mtp_warm_batch_metal(&mut self, m: &mut MtpModule, pairs: &[(&[f32], u32)], first_pos: usize) -> bool {
6826 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
6827 let b = pairs.len();
6828 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
6829 return false;
6830 }
6831 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate: None, bias: None } = &m.layer.attn else {
6832 return false;
6833 };
6834 let FfnKind::Dense(d) = &m.layer.ffn else { return false };
6835 let (Some(pq), Some(pk), Some(pv), Some(po)) = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts()) else {
6836 return false;
6837 };
6838 let (Some(g), Some(u), Some(dn)) = (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts()) else {
6839 return false;
6840 };
6841 let Some(eh) = m.eh_proj.q1_parts() else { return false };
6842 let QTensor::Mapped { model, .. } = wq else { return false };
6843 let model = model.clone();
6844 let hs = self.hidden_size;
6845 let mut cat = vec![0f32; b * 2 * hs];
6847 for (j, (h, tok)) in pairs.iter().enumerate() {
6848 let e = self.embed_single(*tok);
6849 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
6850 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
6851 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
6852 }
6853 let dims = GraphDims { hidden: hs, eps: self.rms_eps as f32, gemma: self.norm_style == cortiq_core::NormStyle::Gemma };
6854 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
6855 return false;
6856 };
6857 let l = AttnGpuLayer {
6858 attn_norm: &m.layer.input_norm,
6859 post_norm: &m.layer.post_norm,
6860 wq: pq,
6861 wk: pk,
6862 wv: pv,
6863 wo: po,
6864 ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
6865 };
6866 let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6867 let inv_freq = self.inv_freq.clone();
6868 let cpu_stored;
6869 {
6870 let cache = &m.kv;
6871 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6872 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6873 cpu_stored = cpu_k[0].len() / hd;
6874 if cpu_stored != first_pos {
6875 return false;
6876 }
6877 let p = AttnDeviceParams {
6878 kv_id: self.mtp_kv_id(),
6879 layer: Self::MTP_LAYER_BASE,
6880 nh,
6881 nkv,
6882 hd,
6883 rd,
6884 position: first_pos,
6885 eps: self.rms_eps as f32,
6886 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6887 output_gate: *output_gate,
6888 q_norm: q_norm.as_deref(),
6889 k_norm: k_norm.as_deref(),
6890 inv_freq: &inv_freq,
6891 cpu_k,
6892 cpu_v,
6893 cpu_stored,
6894 o1: None,
6895 };
6896 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
6897 return false;
6898 }
6899 }
6900 graph.sync();
6901 let mut kbuf = vec![0f32; b * nkv * hd];
6902 let mut vbuf = vec![0f32; b * nkv * hd];
6903 if !crate::gpu_metal::kv_mirror_read_rows(self.mtp_kv_id(), Self::MTP_LAYER_BASE, nkv, hd, cpu_stored, b, &mut kbuf, &mut vbuf) {
6904 return false;
6905 }
6906 for r in 0..b {
6907 m.kv.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6908 }
6909 crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, cpu_stored + b);
6910 true
6911 }
6912
6913 fn draft_vocab_rows(head_rows: usize) -> usize {
6916 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6917 let n = *N.get_or_init(|| {
6918 std::env::var("CMF_DRAFT_VOCAB")
6919 .ok()
6920 .and_then(|v| v.parse().ok())
6921 .unwrap_or(65536)
6922 });
6923 if n == 0 { head_rows } else { n.min(head_rows) }
6924 }
6925
6926 #[cfg(target_os = "macos")]
6931 fn mtp_step_metal(
6932 &mut self,
6933 m: &mut MtpModule,
6934 hidden: &[f32],
6935 next_token: u32,
6936 position: usize,
6937 want_logits: bool,
6938 ) -> Option<(Vec<f32>, Vec<f32>)> {
6939 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
6940 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
6941 || !crate::gpu::q1_force()
6942 || !crate::gpu::enabled_here()
6943 || self.attn_softcap > 0.0
6944 || self.attention_heads_per_layer.is_some()
6945 || m.kv.mode != crate::kv_cache::KvMode::F32
6946 || m.kv.o1.is_some()
6947 {
6948 return None;
6949 }
6950 let AttnKind::Full {
6951 wq,
6952 wk,
6953 wv,
6954 wo,
6955 q_norm,
6956 k_norm,
6957 output_gate,
6958 softplus_gate: None,
6959 bias: None,
6960 } = &m.layer.attn
6961 else {
6962 return None;
6963 };
6964 let FfnKind::Dense(d) = &m.layer.ffn else { return None };
6965 if d.act != Act::Silu {
6966 return None;
6967 }
6968 let (pq, pk, pv, po) = (wq.q1_parts()?, wk.q1_parts()?, wv.q1_parts()?, wo.q1_parts()?);
6969 let (g, u, dn) = (d.gate_proj.q1_parts()?, d.up_proj.q1_parts()?, d.down_proj.q1_parts()?);
6970 let QTensor::Mapped { model, .. } = wq else { return None };
6971 let model = model.clone();
6972 let lm = if want_logits { Some(self.weights.lm_head.q1_parts()?) } else { None };
6973 let dims = GraphDims {
6974 hidden: self.hidden_size,
6975 eps: self.rms_eps as f32,
6976 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6977 };
6978 let hs = self.hidden_size;
6981 let mut x = vec![0f32; hs];
6982 let mut graph = TokenGraph::new(&model, dims, &x)?;
6983 let mut folded = false;
6984 if let Some(eh) = m.eh_proj.q1_parts() {
6985 let e = self.embed_single(next_token);
6986 let mut cat = vec![0.0f32; 2 * hs];
6987 let (cat_e, cat_h) = cat.split_at_mut(hs);
6988 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6989 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6990 folded = graph.encode_input_proj(eh, &cat);
6991 }
6992 if !folded {
6993 x = self.mtp_block_input(m, hidden, next_token);
6994 graph = TokenGraph::new(&model, dims, &x)?;
6995 }
6996 let l = AttnGpuLayer {
6997 attn_norm: &m.layer.input_norm,
6998 post_norm: &m.layer.post_norm,
6999 wq: pq,
7000 wk: pk,
7001 wv: pv,
7002 wo: po,
7003 ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
7004 };
7005 let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
7006 let inv_freq = self.inv_freq.clone();
7007 {
7008 let cache = &m.kv;
7009 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
7010 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
7011 let cpu_stored = cpu_k[0].len() / hd;
7012 let p = AttnDeviceParams {
7013 kv_id: self.mtp_kv_id(),
7014 layer: Self::MTP_LAYER_BASE,
7015 nh,
7016 nkv,
7017 hd,
7018 rd,
7019 position,
7020 eps: self.rms_eps as f32,
7021 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7022 output_gate: *output_gate,
7023 q_norm: q_norm.as_deref(),
7024 k_norm: k_norm.as_deref(),
7025 inv_freq: &inv_freq,
7026 cpu_k,
7027 cpu_v,
7028 cpu_stored,
7029 o1: None,
7030 };
7031 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
7032 return None;
7033 }
7034 }
7035 let draft_rows = if let Some(lm) = lm { Self::draft_vocab_rows(lm.1) } else { 0 };
7041 if let Some(lm) = lm {
7042 if !graph.lm_head_ok(lm) {
7043 return None;
7044 }
7045 if draft_rows < lm.1 {
7046 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
7047 return None;
7048 }
7049 } else {
7050 graph.encode_lm_head(&m.final_norm, lm);
7051 }
7052 }
7053 graph.sync();
7054 let mut logits = Vec::new();
7055 if let Some(lm) = lm {
7056 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
7057 logits = attention::take_buf(n_read);
7058 graph.read_logits(&mut logits);
7059 logits.resize(self.vocab_size, f32::NEG_INFINITY);
7061 }
7062 graph.finish(&mut x);
7063 let mut krow = attention::take_buf(nkv * hd);
7064 let mut vrow = attention::take_buf(nkv * hd);
7065 if crate::gpu_metal::kv_mirror_read_last(self.mtp_kv_id(), Self::MTP_LAYER_BASE, nkv, hd, &mut krow, &mut vrow) {
7066 m.kv.append(&krow, &vrow, &[]);
7067 }
7068 attention::recycle_buf(&mut krow);
7069 attention::recycle_buf(&mut vrow);
7070 Some((logits, x))
7071 }
7072
7073 fn try_batch_graph_wgpu(
7074 &self,
7075 hiddens: &mut [f32],
7076 positions: &[usize],
7077 k: usize,
7078 spec: Option<crate::gpu::SpecTail<'_>>,
7079 ) -> bool {
7080 let _tb = std::time::Instant::now();
7081 if self.attn_softcap > 0.0 {
7082 return false; }
7084 if self.o1_active() {
7085 return false;
7086 }
7087 let nh = self.num_heads;
7088 let (nkv, hd, rd) = self.layer_geom(0);
7089 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7090 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7091 if let Some((_, i, kind, rs)) = t.graph_weight() {
7092 return Some(crate::gpu::GraphW {
7093 idx: i,
7094 kind,
7095 row_scale: rs,
7096 data: &[],
7097 });
7098 }
7099 t.as_f32().map(|d| crate::gpu::GraphW {
7100 idx: 0,
7101 kind: 4,
7102 row_scale: &[],
7103 data: d,
7104 })
7105 }
7106 let built: Option<(
7107 Vec<crate::gpu::GraphLayer<'_>>,
7108 std::sync::Arc<cortiq_core::CmfModel>,
7109 )> = (|| {
7110 let mut layers = Vec::with_capacity(self.num_layers);
7111 let mut model = None;
7112 for li in 0..self.num_layers {
7113 let lw = &self.weights.layers[self.phys_layer(li)];
7114 let gffn = match &lw.ffn {
7121 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7122 gate: gw(&d.gate_proj)?,
7123 up: gw(&d.up_proj)?,
7124 down: gw(&d.down_proj)?,
7125 },
7126 FfnKind::Moe(m) => {
7127 if m.router_sigmoid
7128 || m.expert_bias.is_some()
7129 || m.route_tau.is_some()
7130 || m.mask.is_some()
7131 {
7132 return None;
7133 }
7134 let (se, sg) = m.shared.as_ref()?;
7135 let sgate = gw(sg.as_ref()?)?;
7136 let router = gw(&m.router)?;
7137 let inter = m.experts.first()?.gate_proj.rows();
7138 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7139 let mut q4tp: Option<bool> = None;
7140 let mut gu_q2: Option<bool> = None;
7141 for e in m.experts.iter().chain(std::iter::once(se)) {
7142 if !matches!(e.act, Act::Silu)
7143 || e.gate_proj.rows() != inter
7144 || e.up_proj.rows() != inter
7145 {
7146 return None;
7147 }
7148 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7152 Some((mm, gi)) => (
7153 mm,
7154 gi,
7155 e.up_proj.mapped_q4t()?.1,
7156 e.down_proj.mapped_q4t()?.1,
7157 false,
7158 false,
7159 ),
7160 None => match e.gate_proj.mapped_q2tp() {
7161 Some((mm, gi)) => (
7162 mm,
7163 gi,
7164 e.up_proj.mapped_q2tp()?.1,
7165 e.down_proj.mapped_q4tp()?.1,
7166 true,
7167 true,
7168 ),
7169 None => {
7170 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7171 (
7172 mm,
7173 gi,
7174 e.up_proj.mapped_q4tp()?.1,
7175 e.down_proj.mapped_q4tp()?.1,
7176 true,
7177 false,
7178 )
7179 }
7180 },
7181 };
7182 if *q4tp.get_or_insert(is_p) != is_p
7183 || *gu_q2.get_or_insert(is_q2) != is_q2
7184 {
7185 return None;
7186 }
7187 model.get_or_insert_with(|| mm.clone());
7188 experts.push((gi, ui, di));
7189 }
7190 crate::gpu::GraphFfn::Moe {
7191 router,
7192 shared_gate: sgate,
7193 experts,
7194 n_exp: m.experts.len(),
7195 top_k: m.top_k,
7196 inter,
7197 norm_topk: m.norm_topk_prob,
7198 q4tp: q4tp?,
7199 gu_q2: gu_q2.unwrap_or(false),
7200 }
7201 }
7202 _ => return None,
7203 };
7204 let attn = match &lw.attn {
7205 AttnKind::Full {
7206 wq,
7207 wk,
7208 wv,
7209 wo,
7210 q_norm,
7211 k_norm,
7212 output_gate,
7213 softplus_gate,
7214 bias,
7215 } => {
7216 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7217 return None;
7218 }
7219 let (m, _, _, _) = wq.graph_weight()?;
7220 model = Some(m.clone());
7221 crate::gpu::GraphAttn::Full {
7222 wq: gw(wq)?,
7223 wk: gw(wk)?,
7224 wv: gw(wv)?,
7225 wo: gw(wo)?,
7226 q_norm: q_norm.as_deref(),
7227 k_norm: k_norm.as_deref(),
7228 bias: bias
7229 .as_ref()
7230 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7231 output_gate: *output_gate,
7232 cpu_k: self.kv_cache.layers[li].k_heads(),
7233 cpu_v: self.kv_cache.layers[li].v_heads(),
7234 }
7235 }
7236 AttnKind::LinearGdn(w) => {
7237 let cfg = self.gdn_cfg?;
7238 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7239 model = Some(m.clone());
7240 crate::gpu::GraphAttn::Gdn {
7241 qkv: gw(&w.in_proj_qkv)?,
7242 z: gw(&w.in_proj_z)?,
7243 a: gw(&w.in_proj_a)?,
7244 b: gw(&w.in_proj_b)?,
7245 out: gw(&w.out_proj)?,
7246 conv1d: &w.conv1d,
7247 a_log: &w.a_log,
7248 dt_bias: &w.dt_bias,
7249 norm: &w.norm,
7250 nv: cfg.num_v_heads,
7251 nk: cfg.num_k_heads,
7252 dk: cfg.key_head_dim,
7253 dv: cfg.value_head_dim,
7254 kk: cfg.conv_kernel,
7255 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7256 }
7257 }
7258 _ => return None,
7259 };
7260 layers.push(crate::gpu::GraphLayer {
7261 input_norm: &lw.input_norm,
7262 attn,
7263 post_norm: &lw.post_norm,
7264 ffn: gffn,
7265 });
7266 }
7267 Some((layers, model?))
7268 })();
7269 let Some((layers, model)) = built else {
7270 {
7271 use std::sync::atomic::{AtomicBool, Ordering};
7272 static SAID: AtomicBool = AtomicBool::new(false);
7273 if !SAID.swap(true, Ordering::Relaxed) {
7274 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
7275 }
7276 }
7277 return false;
7278 };
7279 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7280 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
7281 }
7282 crate::gpu::forward_batch_graph(
7283 &model,
7284 self.graph_kv_id,
7285 &layers,
7286 &self.inv_freq,
7287 hiddens,
7288 nh,
7289 nkv,
7290 hd,
7291 rd,
7292 self.hidden_size,
7293 self.intermediate_size,
7294 positions,
7295 self.kv_cache.max_seq_len,
7296 gemma,
7297 self.rms_eps as f32,
7298 k,
7299 spec,
7300 )
7301 }
7302
7303 fn draft_probe() -> bool {
7307 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7308 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
7309 }
7310
7311 #[cfg(feature = "gpu")]
7323 fn dsv4_spec_on() -> bool {
7324 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7325 *ON.get_or_init(|| {
7326 std::env::var("CMF_DSV4_SPEC")
7327 .map(|v| v != "0")
7328 .unwrap_or(true)
7329 })
7330 }
7331
7332 #[cfg(feature = "gpu")]
7339 fn dsv4_spec_step(
7340 &mut self,
7341 tip_token: u32,
7342 t_next: u32,
7343 next_pos: usize,
7344 drafted: &mut usize,
7345 accepted_ctr: &mut usize,
7346 ) -> Option<(Vec<u32>, usize)> {
7347 let t_all = std::time::Instant::now();
7348 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7349 thread_local! {
7350 static LAST: std::cell::Cell<Option<std::time::Instant>> =
7351 const { std::cell::Cell::new(None) };
7352 }
7353 LAST.with(|l| {
7354 if let Some(prev) = l.get() {
7355 eprintln!(
7356 "между раундами {:.1} мс",
7357 prev.elapsed().as_secs_f64() * 1e3
7358 );
7359 }
7360 l.set(Some(std::time::Instant::now()));
7361 });
7362 }
7363 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7364 eprintln!("spec_step: вход pos={next_pos}");
7365 }
7366 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
7367 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
7368 if self.dspark.is_none() {
7370 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7371 if t.is_empty() {
7372 return None;
7373 }
7374 crate::dsv4::dspark_arm(&t, cfg.dim);
7375 self.dspark = Some(crate::dsv4::DsparkState::new(
7376 self.dsv4_mtp.len(),
7377 &cfg,
7378 t.len(),
7379 ));
7380 }
7381 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7382 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
7383 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7384 eprintln!("spec_step: пак не построился (targets {targets:?})");
7385 }
7386 let pack = pack?;
7387 let block = crate::dsv4::dspark_block();
7388 let b_box = self.dsv4.as_mut()?;
7389 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
7390 let ds = self.dspark.as_mut()?;
7391 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
7394 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
7395 if dbg {
7396 eprintln!("spec_step: нет захвата");
7397 }
7398 return None;
7399 }
7400 ds.have_hidden = true;
7401 let tip_pos = next_pos.checked_sub(1)?;
7402 let draft_started = std::time::Instant::now();
7403 let mut conf = Vec::new();
7404 let props = crate::dsv4::dspark_draft_gpu(
7405 g,
7406 &self.dsv4_mtp,
7407 &cfg,
7408 ds,
7409 pack,
7410 st.kv_id,
7411 tip_token,
7412 tip_pos,
7413 self.pool.as_deref(),
7414 &mut conf,
7415 );
7416 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7417 *drafted += block;
7418 if props.is_empty() || props[0] != t_next {
7419 if dbg {
7420 eprintln!(
7421 "spec_step: черновик {} (props0={:?} t_next={t_next})",
7422 if props.is_empty() {
7423 "пуст"
7424 } else {
7425 "мимо"
7426 },
7427 props.first()
7428 );
7429 }
7430 return None;
7431 }
7432 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
7433 let conf_min = {
7439 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
7440 *M.get_or_init(|| {
7441 std::env::var("CMF_DSPARK_CONF_MIN")
7442 .ok()
7443 .and_then(|v| v.parse().ok())
7444 .unwrap_or(0.0)
7445 })
7446 };
7447 if conf_min > 0.0 && conf.len() >= props.len() {
7448 let mut keep = 1usize;
7449 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
7450 keep += 1;
7451 }
7452 k_verify = k_verify.min(keep.max(2));
7453 }
7454 if k_verify < 2 {
7455 return None;
7456 }
7457 let mut fed = Vec::with_capacity(k_verify);
7458 fed.push(t_next);
7459 fed.extend_from_slice(&props[1..k_verify]);
7460 let mut argmax = Vec::new();
7461 let mut logits_all = Vec::new();
7462 let mut walked = Vec::new();
7463 let txn = crate::dsv4::dsv4_verify_chunk(
7464 g,
7465 layers,
7466 &cfg,
7467 st,
7468 &fed,
7469 next_pos,
7470 &self.inv_freq,
7471 self.pool.as_deref(),
7472 &targets,
7473 &mut argmax,
7474 &mut logits_all,
7475 &mut walked,
7476 );
7477 if txn.is_none() && dbg {
7478 eprintln!("spec_step: verify отказал");
7479 }
7480 let txn = txn?;
7481 let b = fed.len();
7482 let mut accepted = 1usize;
7483 while accepted < b && fed[accepted] == argmax[accepted - 1] {
7484 accepted += 1;
7485 }
7486 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
7491 accepted = 1;
7492 }
7493 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
7494 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
7495 }
7496 let t_fin = std::time::Instant::now();
7497 if !crate::dsv4::dsv4_spec_finish(
7498 g,
7499 layers,
7500 &cfg,
7501 st,
7502 txn,
7503 accepted,
7504 &fed,
7505 &self.inv_freq,
7506 self.pool.as_deref(),
7507 ) {
7508 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
7509 return None;
7510 }
7511 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7512 eprintln!(
7513 "finish(k={accepted}): {:.1} мс",
7514 t_fin.elapsed().as_secs_f64() * 1e3
7515 );
7516 }
7517 *accepted_ctr += accepted - 1;
7518 let (hc, dim) = (cfg.hc_mult, cfg.dim);
7523 let dev_caps: Vec<usize> = targets
7530 .iter()
7531 .copied()
7532 .filter(|&t| {
7533 st.dev_set.get(t).copied().unwrap_or(false)
7534 && !st.partial_set.get(t).copied().unwrap_or(false)
7535 })
7536 .collect();
7537 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
7538 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
7539 return None;
7540 }
7541 for t in 0..accepted {
7542 let tip = t + 1 == accepted;
7543 for (slot, &tl) in targets.iter().enumerate() {
7544 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
7545 let lo = (di * b + t) * hc * dim;
7546 crate::dsv4::dspark_capture(
7547 &caps_all[lo..lo + hc * dim],
7548 &cfg,
7549 slot,
7550 &mut ds.main_hidden,
7551 );
7552 } else if tip
7553 && crate::dsv4::dspark_peek_slot(slot, dim, {
7554 let lo = slot * dim;
7555 &mut ds.main_hidden[lo..lo + dim]
7556 })
7557 {
7558 } else {
7563 crate::dsv4::dspark_capture(
7567 &walked[t * hc * dim..(t + 1) * hc * dim],
7568 &cfg,
7569 slot,
7570 &mut ds.main_hidden,
7571 );
7572 }
7573 }
7574 crate::dsv4::dspark_ring_append(
7575 g,
7576 &self.dsv4_mtp,
7577 &cfg,
7578 ds,
7579 next_pos + t,
7580 self.pool.as_deref(),
7581 );
7582 }
7583 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
7584 self.graph_logits = Some(row);
7585 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
7590 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
7591 crate::dsv4::pick_tally_arm();
7592 }
7593 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7594 eprintln!(
7595 "spec_step total {:.1} мс (k={accepted})",
7596 t_all.elapsed().as_secs_f64() * 1e3
7597 );
7598 }
7599 Some((fed[1..accepted].to_vec(), next_pos + accepted))
7600 }
7601
7602 fn dspark_probe(&mut self, position: usize, token_id: u32) {
7603 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
7604 return;
7605 }
7606 let trunk_now = crate::dsv4::pick_tally_take();
7608 crate::dsv4::trunk_freq_note(&trunk_now);
7609 if !trunk_now.is_empty() {
7610 self.dspark_trunk_picks.push(trunk_now);
7611 let keep = crate::dsv4::dspark_block();
7612 if self.dspark_trunk_picks.len() > keep {
7613 self.dspark_trunk_picks.remove(0);
7614 }
7615 }
7616 for p in std::mem::take(&mut self.dspark_pending) {
7619 let Some(i) = position.checked_sub(p.0 + 1) else {
7620 continue;
7621 };
7622 let mut p = p;
7623 if i < p.1.len() {
7624 if p.2 && p.1[i] == token_id {
7625 p.3 = i + 1;
7626 } else {
7627 p.2 = false;
7628 }
7629 if i + 1 < p.1.len() {
7630 self.dspark_pending.push(p);
7631 continue;
7632 }
7633 }
7634 self.dspark_hist.push(p.3);
7635 self.dspark_real.push(token_id);
7636 }
7637 let Some(b) = &mut self.dsv4 else { return };
7638 let (g, layers, cfg) = (&b.0, &b.1, b.2);
7639 let n_layers = layers.len();
7640 if self.dspark.is_none() {
7641 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7642 if t.is_empty() {
7643 return;
7644 }
7645 eprintln!(
7646 "DSpark: захват со слоёв {t:?}, блок {}",
7647 crate::dsv4::dspark_block()
7648 );
7649 crate::dsv4::dspark_arm(&t, cfg.dim);
7650 self.dspark = Some(crate::dsv4::DsparkState::new(
7651 self.dsv4_mtp.len(),
7652 &cfg,
7653 t.len(),
7654 ));
7655 }
7656 let ds = self.dspark.as_mut().unwrap();
7657 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
7658 return; }
7660 let mut conf = Vec::new();
7661 crate::dsv4::pick_tally_arm();
7662 let draft_started = std::time::Instant::now();
7667 #[cfg(feature = "gpu")]
7668 let gpu_draft = crate::dsv4::dspark_gpu_on();
7669 #[cfg(not(feature = "gpu"))]
7670 let gpu_draft = false;
7671 let props = if gpu_draft {
7672 #[cfg(feature = "gpu")]
7673 {
7674 let kv_id = b.3.kv_id;
7675 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
7676 Some(pk) => crate::dsv4::dspark_draft_gpu(
7677 g,
7678 &self.dsv4_mtp,
7679 &cfg,
7680 ds,
7681 pk,
7682 kv_id,
7683 token_id,
7684 position,
7685 self.pool.as_deref(),
7686 &mut conf,
7687 ),
7688 None => Vec::new(),
7689 }
7690 }
7691 #[cfg(not(feature = "gpu"))]
7692 Vec::new()
7693 } else {
7694 crate::gpu::cpu_scope(|| {
7695 crate::dsv4::dspark_draft(
7696 g,
7697 &self.dsv4_mtp,
7698 &cfg,
7699 ds,
7700 token_id,
7701 position,
7702 self.pool.as_deref(),
7703 &mut conf,
7704 )
7705 })
7706 };
7707 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7708 let draft_picks = crate::dsv4::pick_tally_take();
7709 crate::dsv4::dspark_freq_note(&draft_picks);
7710 crate::dsv4::pick_tally_arm();
7713 if !props.is_empty() {
7714 let (tu, tt) = {
7718 let flat: Vec<(usize, Vec<usize>)> = self
7719 .dspark_trunk_picks
7720 .iter()
7721 .flat_map(|v| v.iter().cloned())
7722 .collect();
7723 let mut per: std::collections::HashMap<usize, Vec<usize>> =
7725 std::collections::HashMap::new();
7726 for (li, picks) in flat {
7727 per.entry(li).or_default().extend(picks);
7728 }
7729 let n = per.len().max(1);
7730 let mut u = 0usize;
7731 let mut t = 0usize;
7732 for (_, v) in per {
7733 t += v.len();
7734 u += v.iter().collect::<std::collections::HashSet<_>>().len();
7735 }
7736 (u / n, t / n)
7737 };
7738 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
7739 self.dspark_exp.push((tu, tt, du, dt));
7740 self.dspark_pending.push((position, props, true, 0));
7741 }
7742 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
7743 let n = self.dspark_hist.len() as f32;
7744 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
7745 let block = crate::dsv4::dspark_block();
7746 let mut at = vec![0usize; block + 1];
7747 for &k in &self.dspark_hist {
7748 at[k] += 1;
7749 }
7750 let mut surv = Vec::with_capacity(block);
7752 for i in 1..=block {
7753 let k = at[i..].iter().sum::<usize>() as f32 / n;
7754 surv.push(format!("{k:.2}"));
7755 }
7756 let distinct = self
7757 .dspark_real
7758 .iter()
7759 .collect::<std::collections::HashSet<_>>()
7760 .len();
7761 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
7762 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
7763 });
7764 let m = self.dspark_exp.len().max(1);
7765 eprintln!(
7766 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
7767 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
7768 self.dspark_hist.len(),
7769 mean + 1.0,
7770 surv.join(" ")
7771 );
7772 eprintln!(
7773 "DSpark: разных токенов {distinct} из {} (вырожденность), \
7774 эксперты ствол {}/{} на слой за {block} токенов, \
7775 черновик {}/{} за блок, draft {:.2} мс/блок",
7776 self.dspark_real.len(),
7777 tu / m,
7778 tt / m,
7779 du / m,
7780 dt / m,
7781 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
7782 );
7783 }
7784 }
7785
7786 fn forward_layers_upto(
7787 &mut self,
7788 hidden: &[f32],
7789 position: usize,
7790 task_mask: Option<&TaskMask>,
7791 upto: Option<usize>,
7792 ) -> Vec<f32> {
7793 if let Some(plan) = self.gpu_plan.clone() {
7799 if upto.is_none() && plan.len() > 1 {
7800 let mut h = hidden.to_vec();
7801 for &(dev, from, upto_incl) in plan.iter() {
7802 h = crate::gpu::with_device(dev, || {
7803 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
7804 });
7805 }
7806 return h;
7807 }
7808 }
7809 self.forward_layers_span(hidden, position, task_mask, 0, upto)
7810 }
7811
7812 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
7817 self.set_gpu_plan_at(devices, None)
7818 }
7819
7820 pub fn set_gpu_plan_at(
7824 &mut self,
7825 devices: Option<&[usize]>,
7826 at: Option<usize>,
7827 ) -> Result<(), String> {
7828 let Some(devs) = devices.filter(|d| d.len() > 1) else {
7829 self.gpu_plan = None;
7830 return Ok(());
7831 };
7832 self.split_supported()?;
7833 let n = self.num_layers;
7834 if devs.len() > n {
7835 return Err(format!("{} devices for {n} layers", devs.len()));
7836 }
7837 if let Some(k) = at {
7838 if k == 0 || k >= n {
7839 return Err(format!("split at {k}: the model has {n} layers"));
7840 }
7841 if devs.len() == 2 {
7842 self.gpu_plan = Some(std::sync::Arc::new(vec![
7843 (devs[0], 0, k - 1),
7844 (devs[1], k, n - 1),
7845 ]));
7846 return Ok(());
7847 }
7848 return Err(format!(
7849 "an explicit split point takes exactly 2 devices, got {}",
7850 devs.len()
7851 ));
7852 }
7853 let per = n.div_ceil(devs.len());
7854 let mut plan = Vec::with_capacity(devs.len());
7855 let mut from = 0usize;
7856 for &d in devs {
7857 if from >= n {
7858 break;
7859 }
7860 let upto = (from + per - 1).min(n - 1);
7861 plan.push((d, from, upto));
7862 from = upto + 1;
7863 }
7864 self.gpu_plan = Some(std::sync::Arc::new(plan));
7865 Ok(())
7866 }
7867
7868 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
7870 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
7871 }
7872
7873 fn forward_layers_span(
7879 &mut self,
7880 hidden: &[f32],
7881 position: usize,
7882 task_mask: Option<&TaskMask>,
7883 from: usize,
7884 upto: Option<usize>,
7885 ) -> Vec<f32> {
7886 debug_assert!(from == 0 || (self.dsv4.is_none() && self.g3n.is_none()));
7887 if let Some(b) = &mut self.dsv4 {
7893 let _ = (task_mask, upto);
7894 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
7895 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
7896 st.pos = position;
7897 let mut logits = Vec::new();
7898 crate::dsv4::forward_token(
7899 g,
7900 layers,
7901 &cfg,
7902 st,
7903 token_id,
7904 &self.inv_freq,
7905 self.pool.as_deref(),
7906 &mut logits,
7907 );
7908 self.graph_logits = Some(logits);
7909 self.dspark_probe(position, token_id);
7910 return vec![0.0; self.hidden_size];
7913 }
7914 if let Some(b) = &self.g3n {
7917 let _ = (task_mask, upto);
7918 return crate::g3n::g3n_forward(
7919 &b.0,
7920 &b.1,
7921 hidden,
7922 position,
7923 &mut self.kv_cache.layers,
7924 self.num_heads,
7925 self.num_kv_heads,
7926 self.head_dim,
7927 self.pool.as_deref(),
7928 );
7929 }
7930 let mut h = hidden.to_vec();
7931 let (nh, _nkv, _hd, hs, _rd, eps) = (
7934 self.num_heads,
7935 self.num_kv_heads,
7936 self.head_dim,
7937 self.hidden_size,
7938 self.rotary_dim,
7939 self.rms_eps,
7940 );
7941 let pool = self.pool.clone();
7942 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
7954 let graph_on = match graph_env.as_deref() {
7955 Some("0") => false,
7956 Some("prefill") => false, Some(_) => true,
7958 None => crate::gpu::wgpu_graph_default(),
7964 };
7965 let graph_trusted =
7966 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
7967 let race_eligible = graph_on
7968 && upto.is_none()
7969 && task_mask.is_none()
7970 && from == 0
7971 && !crate::gpu::graph_unsupported();
7972 let mut tail_start = 0usize;
7973 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
7974 let t_graph = std::time::Instant::now();
7975 let mut lg = Vec::new();
7976 let mut gl = 0usize;
7977 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
7978 if built.is_none() && !self.o1_active() && self.attn_softcap == 0.0 {
7983 crate::gpu::graph_mark_unsupported();
7984 }
7985 graph_note(built.is_some());
7986 if let Some(hh) = built {
7987 let dur = t_graph.elapsed();
7988 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7989 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
7990 }
7991 if gl > 0 && gl < self.num_layers {
7992 h = hh;
7998 tail_start = gl;
7999 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
8000 if !graph_trusted {
8001 crate::gpu::graph_race_record(true, dur);
8002 }
8003 if !lg.is_empty() {
8004 lg.resize(self.vocab_size, 0.0);
8007 if let Some(c) = self.final_softcap {
8008 for l in lg.iter_mut() {
8009 *l = c * (*l / c).tanh();
8010 }
8011 }
8012 self.graph_logits = Some(lg);
8013 }
8014 return hh;
8015 }
8016 }
8022 }
8023 let span = from > 0 || upto.is_some();
8047 if span && graph_on && task_mask.is_none() && graph_trusted {
8048 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
8049 let mut lg = Vec::new();
8050 let mut gl = 0usize;
8051 let span_res =
8052 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
8053 graph_note(span_res.is_some() && gl == upto_excl - from);
8054 if std::env::var("CMF_GPU_DEBUG").is_ok() {
8055 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
8059 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
8060 eprintln!(
8061 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
8062 upto_excl - from,
8063 span_res.is_some()
8064 );
8065 }
8066 }
8067 if let Some(hh) = span_res {
8068 if gl == upto_excl - from {
8069 if !lg.is_empty() {
8070 lg.resize(self.vocab_size, 0.0);
8071 if let Some(c) = self.final_softcap {
8072 for l in lg.iter_mut() {
8073 *l = c * (*l / c).tanh();
8074 }
8075 }
8076 self.graph_logits = Some(lg);
8077 }
8078 crate::gpu::set_layer(-1);
8079 return hh;
8080 }
8081 h = hh;
8083 tail_start = from + gl;
8084 }
8085 }
8086 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
8087
8088 #[cfg(target_os = "macos")]
8089 let mut gpu_skip_until = 0usize;
8090 for li in tail_start.max(from)..self.num_layers {
8091 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
8093 if li > u {
8094 break;
8095 }
8096 }
8097 if let Some(mask) = task_mask {
8098 if !mask.layer_alive(li) {
8099 continue; }
8101 }
8102 #[cfg(target_os = "macos")]
8106 {
8107 if li < gpu_skip_until {
8108 continue;
8109 }
8110 if task_mask.is_none() {
8111 let end = self.q1_graph_gpu(li, upto, position, &mut h);
8112 if end > li {
8113 gpu_skip_until = end;
8114 if self.is_loop_end(end - 1) && end < self.num_layers {
8117 h = inference::rms_norm(
8118 &h,
8119 &self.weights.final_norm,
8120 self.rms_eps,
8121 self.norm_style,
8122 );
8123 }
8124 continue;
8125 }
8126 }
8127 }
8128
8129 let lw = &self.weights.layers[self.phys_layer(li)];
8130 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8131 if tp.parse::<usize>().ok() == Some(position) {
8132 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
8133 eprintln!(
8134 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
8135 h[0], h[1]
8136 );
8137 }
8138 }
8139 inference::rms_norm_into(
8142 &h,
8143 &lw.input_norm,
8144 self.rms_eps,
8145 self.norm_style,
8146 &mut self.ws.n1,
8147 );
8148
8149 let attn_out = match &lw.attn {
8150 AttnKind::Mla(w) => {
8151 let inv_freq_l = self.layer_inv_freq(li);
8152 let rs = self.layer_rope_scale(li);
8153 let eps = self.rms_eps;
8154 let pool = self.pool.clone();
8155 mla_attention(
8156 w,
8157 &self.ws.n1,
8158 &mut self.kv_cache.layers[li],
8159 position,
8160 &inv_freq_l,
8161 rs,
8162 eps,
8163 pool.as_deref(),
8164 )
8165 }
8166 AttnKind::Linear(w) => {
8167 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8168 vmf_phase_forward(
8169 &self.ws.n1,
8170 w,
8171 &cfg,
8172 &mut self.kv_cache.layers[li].linear_state,
8173 self.pool.as_deref(),
8174 )
8175 }
8176 AttnKind::Kda(w) => {
8177 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
8178 crate::linear_core::kda_forward(
8179 &self.ws.n1,
8180 w,
8181 &cfg,
8182 &mut self.kv_cache.layers[li].linear_state,
8183 self.pool.as_deref(),
8184 )
8185 }
8186 AttnKind::LinearGdn(w) => {
8187 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8188 gdn_forward(
8189 &self.ws.n1,
8190 w,
8191 &cfg,
8192 &mut self.kv_cache.layers[li].linear_state,
8193 self.pool.as_deref(),
8194 )
8195 }
8196 AttnKind::ShortConv(w) => {
8197 let cfg = self
8198 .short_conv_cfg
8199 .expect("short-conv layer without short_conv_cfg");
8200 short_conv_forward(
8201 &self.ws.n1,
8202 w,
8203 &cfg,
8204 &mut self.kv_cache.layers[li].linear_state,
8205 self.pool.as_deref(),
8206 )
8207 }
8208 AttnKind::Full {
8209 wq,
8210 wk,
8211 wv,
8212 wo,
8213 q_norm,
8214 k_norm,
8215 output_gate,
8216 softplus_gate,
8217 bias,
8218 } if self.kv_cache.layers[li].o1_sealed() => {
8219 let inv_freq_l = self.layer_inv_freq(li);
8222 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8223 let cfg = QwenAttnCfg {
8224 num_heads: self.layer_num_heads(li),
8225 num_kv_heads: nkv_l,
8226 head_dim: hd_l,
8227 hidden_size: hs,
8228 position,
8229 inv_freq: &inv_freq_l,
8230 rotary_dim: rd_l,
8231 scale: self.attn_scale,
8232 softcap: self.attn_softcap,
8233 window: None,
8234 v_norm: self.attn_v_norm,
8235 q_norm: q_norm.as_deref(),
8236 k_norm: k_norm.as_deref(),
8237 output_gate: *output_gate,
8238 softplus_gate: softplus_gate
8239 .as_ref()
8240 .map(|(gate, per_head)| (gate, *per_head)),
8241 rope_scale: self.layer_rope_scale(li),
8242 bias: bias
8243 .as_ref()
8244 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8245 rms_eps: eps,
8246 norm_style: self.norm_style,
8247 pool: pool.as_deref(),
8248 };
8249 attention::qwen_attention_nystrom(
8250 &self.ws.n1,
8251 wq,
8252 wk,
8253 wv,
8254 wo,
8255 &mut self.kv_cache.layers[li],
8256 &cfg,
8257 )
8258 }
8259 AttnKind::Full {
8260 wq,
8261 wk,
8262 wv,
8263 wo,
8264 q_norm,
8265 k_norm,
8266 output_gate,
8267 softplus_gate,
8268 bias,
8269 } => 'attn: {
8270 if graph_on
8273 && !*output_gate
8274 && softplus_gate.is_none()
8275 && self.attention_heads_per_layer.is_none()
8276 && bias.is_none()
8277 && task_mask.is_none()
8278 {
8279 let inv_freq_l = self.layer_inv_freq(li);
8280 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8281 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8282 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
8283 wq.mapped_q1(),
8284 wk.mapped_q1(),
8285 wv.mapped_q1(),
8286 wo.mapped_q1(),
8287 ) {
8288 let gm = gm.clone();
8289 let mut out = vec![0f32; hs];
8290 let cache = &self.kv_cache.layers[li];
8291 if crate::gpu::attn_dropin(
8292 &gm,
8293 self.graph_kv_id,
8294 li,
8295 &self.ws.n1,
8296 qi,
8297 ki,
8298 vi,
8299 oi,
8300 q_norm.as_deref(),
8301 k_norm.as_deref(),
8302 &inv_freq_l,
8303 nh,
8304 nkv_l,
8305 hd_l,
8306 rd_l,
8307 hs,
8308 position,
8309 self.kv_cache.max_seq_len,
8310 gemma,
8311 eps as f32,
8312 cache.k_heads(),
8313 cache.v_heads(),
8314 &mut out,
8315 ) {
8316 break 'attn out;
8317 }
8318 }
8319 }
8320 let masked = task_mask
8321 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
8322 .unwrap_or(false);
8323 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
8324 match (masked, f32_view) {
8325 (true, (Some(q), Some(k), Some(v), Some(o))) => {
8328 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
8329 attention::multi_head_attention(
8330 &self.ws.n1,
8331 q,
8332 k,
8333 v,
8334 o,
8335 &mut self.kv_cache.layers[li],
8336 self.num_heads,
8337 self.num_kv_heads,
8338 self.head_dim,
8339 self.hidden_size,
8340 position,
8341 &active_heads,
8342 &self.inv_freq,
8343 )
8344 }
8345 (masked, _) => {
8346 if masked {
8347 tracing::warn!(
8348 "layer {li}: head mask on quantized weights not \
8349 supported yet — executing dense"
8350 );
8351 }
8352 let inv_freq_l = self.layer_inv_freq(li);
8353 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8354 let cfg = QwenAttnCfg {
8355 num_heads: self.layer_num_heads(li),
8356 num_kv_heads: nkv_l,
8357 head_dim: hd_l,
8358 hidden_size: hs,
8359 position,
8360 inv_freq: &inv_freq_l,
8361 rotary_dim: rd_l,
8362 scale: self.attn_scale,
8363 softcap: self.attn_softcap,
8364 window: self.layer_window(li),
8365 v_norm: self.attn_v_norm,
8366 q_norm: q_norm.as_deref(),
8367 k_norm: k_norm.as_deref(),
8368 output_gate: *output_gate,
8369 softplus_gate: softplus_gate
8370 .as_ref()
8371 .map(|(gate, per_head)| (gate, *per_head)),
8372 rope_scale: self.layer_rope_scale(li),
8373 bias: bias
8374 .as_ref()
8375 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8376 rms_eps: eps,
8377 norm_style: self.norm_style,
8378 pool: pool.as_deref(),
8379 };
8380 attention::qwen_attention(
8381 &self.ws.n1,
8382 wq,
8383 wk,
8384 wv,
8385 wo,
8386 &mut self.kv_cache.layers[li],
8387 &cfg,
8388 )
8389 }
8390 }
8391 }
8392 };
8393 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8396 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
8397 None => attn_out,
8398 };
8399 let lw = &self.weights.layers[self.phys_layer(li)];
8400 inference::add_rmsnorm_fused_into(
8401 &mut h,
8402 &attn_out,
8403 &lw.post_norm,
8404 self.rms_eps,
8405 self.norm_style,
8406 &mut self.ws.p1,
8407 );
8408 let mut attn_out = attn_out;
8409 attention::recycle_buf(&mut attn_out);
8410 let post_normed = &self.ws.p1;
8411
8412 let ffn_masked = task_mask
8413 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
8414 .unwrap_or(false);
8415 let ffn_out = match (ffn_masked, &lw.ffn) {
8427 (true, FfnKind::Dense(d)) => {
8428 let tm = task_mask.unwrap();
8429 let alive = tm.ffn_active_count(li);
8430 let deep = alive * 2 <= self.intermediate_size;
8431 if deep && d.down_proj.sparse_col_ok() {
8432 let active = tm.ffn_active_indices(li);
8433 sparse_ffn_quant(
8434 d,
8435 post_normed,
8436 &active,
8437 self.hidden_size,
8438 self.pool.as_deref(),
8439 )
8440 } else if deep
8441 && let (Some(g), Some(u), Some(dn)) = (
8442 d.gate_proj.as_f32(),
8443 d.up_proj.as_f32(),
8444 d.down_proj.as_f32(),
8445 )
8446 {
8447 let active = tm.ffn_active_indices(li);
8448 inference::sparse_ffn_forward(
8449 post_normed,
8450 g,
8451 u,
8452 dn,
8453 self.hidden_size,
8454 self.intermediate_size,
8455 &active,
8456 self.pool.as_deref(),
8457 )
8458 } else {
8459 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
8460 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
8461 }
8462 }
8463 (true, FfnKind::Moe(m)) => {
8464 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
8468 ffn_forward(
8469 &lw.ffn,
8470 post_normed,
8471 self.pool.as_deref(),
8472 allowed.as_deref(),
8473 )
8474 }
8475 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
8476 dm,
8477 post_normed,
8478 &h,
8479 self.rms_eps,
8480 self.norm_style,
8481 self.pool.as_deref(),
8482 ),
8483 (false, _) => match &lw.ffn {
8484 FfnKind::DenseMoe(dm) => dense_moe_ffn(
8485 dm,
8486 post_normed,
8487 &h,
8488 self.rms_eps,
8489 self.norm_style,
8490 self.pool.as_deref(),
8491 ),
8492 _ => {
8493 let allowed = match (&lw.ffn, task_mask) {
8494 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
8495 _ => None,
8496 };
8497 ffn_forward(
8498 &lw.ffn,
8499 post_normed,
8500 self.pool.as_deref(),
8501 allowed.as_deref(),
8502 )
8503 }
8504 },
8505 };
8506 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8507 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
8508 None => ffn_out,
8509 };
8510 for (i, &f) in ffn_out.iter().enumerate() {
8511 h[i] += f;
8512 }
8513 let mut ffn_out = ffn_out;
8514 attention::recycle_buf(&mut ffn_out);
8515
8516 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8518 for v in h.iter_mut() {
8519 *v *= sc;
8520 }
8521 }
8522
8523 if self.is_loop_end(li) && li + 1 < self.num_layers {
8526 h = inference::rms_norm(
8527 &h,
8528 &self.weights.final_norm,
8529 self.rms_eps,
8530 self.norm_style,
8531 );
8532 }
8533
8534 if self.dyn_phi_layer == Some(li) {
8538 self.update_dyn_phi(&h);
8539 }
8540 }
8541 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
8543 crate::gpu::graph_race_record(false, t.elapsed());
8544 }
8545
8546 h
8547 }
8548
8549 fn update_dyn_phi(&mut self, h: &[f32]) {
8552 const A: f32 = 0.2;
8553 if self.dyn_phi_ema.len() != h.len() {
8554 self.dyn_phi_ema = vec![0.0; h.len()];
8555 self.dyn_phi_seen = 0;
8556 }
8557 if self.dyn_phi_seen == 0 {
8558 self.dyn_phi_ema.copy_from_slice(h);
8559 } else {
8560 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
8561 *e = (1.0 - A) * *e + A * v;
8562 }
8563 }
8564 self.dyn_phi_seen += 1;
8565 }
8566
8567 pub fn dyn_phi(&self) -> &[f32] {
8569 &self.dyn_phi_ema
8570 }
8571
8572 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
8574 self.dyn_phi_layer = layer;
8575 self.dyn_phi_ema.clear();
8576 self.dyn_phi_seen = 0;
8577 }
8578
8579 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
8581 let Some(model) = &self.model else {
8582 return Vec::new();
8583 };
8584 model
8585 .header
8586 .skills
8587 .iter()
8588 .enumerate()
8589 .filter_map(|(i, sk)| {
8590 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
8591 let sel = sk.selection.as_ref()?;
8592 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
8593 })
8594 .collect()
8595 }
8596
8597 pub fn active_skill(&self) -> Option<usize> {
8599 self.dyn_active
8600 }
8601
8602 pub fn enable_dynamic_routing(&mut self) -> usize {
8607 use crate::swarm::{DynRouter, RoutableSkill};
8608 let Some(model) = self.model.clone() else {
8609 return 0;
8610 };
8611 if self.dyn_blend_loaded {
8614 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
8615 return 0;
8616 }
8617 if let Some(a) = self.dyn_active {
8621 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
8622 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
8623 return 0;
8624 }
8625 }
8626 let hidden = self.hidden_size;
8627 let mut skills = Vec::new();
8628 for (idx, id, _phi) in self.dynamic_skills() {
8629 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
8630 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
8631 skills.push(rs);
8632 }
8633 }
8634 }
8635 if skills.is_empty() {
8636 return 0;
8637 }
8638 let phi = skills[0].phi_layer;
8640 if skills.iter().any(|s| s.phi_layer != phi) {
8641 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
8642 }
8643 let n = skills.len();
8644 self.set_dyn_phi_layer(Some(phi));
8645 self.dyn_router = Some(DynRouter::new(skills));
8646 n
8647 }
8648
8649 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
8651 self.dyn_router
8652 .as_ref()
8653 .map(|r| r.switches.clone())
8654 .unwrap_or_default()
8655 }
8656
8657 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
8660 let rows = self.weights.lm_head.rows();
8661 let mut logits = attention::take_buf(rows.min(self.vocab_size));
8662 self.weights
8663 .lm_head
8664 .matvec(hidden, &mut logits, self.pool.as_deref());
8665 logits.resize(self.vocab_size, 0.0);
8666 if let Some(m) = self.logit_multiplier {
8667 for l in logits.iter_mut() {
8668 *l *= m;
8669 }
8670 }
8671 if let Some(c) = self.final_softcap {
8672 for l in logits.iter_mut() {
8673 *l = c * (*l / c).tanh();
8674 }
8675 }
8676 if let Some(cm) = self.head_clusters.as_ref() {
8677 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
8678 }
8679 logits
8680 }
8681
8682 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
8685 let h = hidden.len();
8686 let ncl = cm.len() / h.max(1);
8687 if ncl == 0 || logits.len() % ncl != 0 {
8688 return;
8689 }
8690 let cs = logits.len() / ncl;
8691 let mut lc = vec![0.0f32; ncl];
8693 for c in 0..ncl {
8694 let row = &cm[c * h..(c + 1) * h];
8695 let mut s = 0.0f32;
8696 for j in 0..h {
8697 s += row[j] * hidden[j];
8698 }
8699 lc[c] = s;
8700 }
8701 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8702 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
8703 for c in 0..ncl {
8704 let blk = &mut logits[c * cs..(c + 1) * cs];
8705 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8706 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
8707 let add = lc[c] - lse - bl;
8708 for v in blk.iter_mut() {
8709 *v += add;
8710 }
8711 }
8712 }
8713
8714 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
8719 self.kv_cache.clear();
8720 self.kv_history.clear();
8721 let mut hidden = vec![0.0f32; self.hidden_size];
8722 for (pos, &id) in ids.iter().enumerate() {
8723 let emb = self.embed_single(id);
8724 hidden = self.forward_layers(&emb, pos, task_mask);
8725 }
8726 inference::rms_norm_into(
8727 &hidden,
8728 &self.weights.final_norm,
8729 self.rms_eps,
8730 self.norm_style,
8731 &mut self.ws.n1,
8732 );
8733 self.lm_head_forward(&self.ws.n1)
8734 }
8735}
8736
8737pub fn create_test_pipeline(
8739 hidden_size: usize,
8740 intermediate_size: usize,
8741 num_heads: usize,
8742 num_kv_heads: usize,
8743 head_dim: usize,
8744 num_layers: usize,
8745 vocab_size: usize,
8746) -> Pipeline {
8747 let synth = |n: usize, salt: usize| -> Vec<f32> {
8750 (0..n)
8751 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
8752 .collect()
8753 };
8754 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
8755 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
8756 };
8757 let layer_weights: Vec<LayerWeights> = (0..num_layers)
8758 .map(|li| LayerWeights {
8759 input_norm: vec![1.0; hidden_size],
8760 post_norm: vec![1.0; hidden_size],
8761 attn_out_norm: None,
8762 ffn_out_norm: None,
8763 layer_scale: None,
8764 ffn: FfnKind::Dense(DenseFfn {
8765 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
8766 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
8767 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
8768 act: Act::Silu,
8769 }),
8770 attn: AttnKind::Full {
8771 bias: None,
8772 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
8773 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
8774 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
8775 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
8776 q_norm: None,
8777 k_norm: None,
8778 output_gate: false,
8779 softplus_gate: None,
8780 },
8781 })
8782 .collect();
8783
8784 Pipeline::new(
8785 Tokenizer::byte_level(),
8786 PipelineWeights {
8787 embed_tokens: qt(vocab_size, hidden_size, 100),
8788 layers: layer_weights,
8789 lm_head: qt(vocab_size, hidden_size, 200),
8790 final_norm: vec![1.0; hidden_size],
8791 },
8792 hidden_size,
8793 intermediate_size,
8794 num_heads,
8795 num_kv_heads,
8796 head_dim,
8797 num_layers,
8798 num_layers, false, vocab_size,
8801 1e-6,
8802 10_000.0,
8803 NormStyle::Qwen,
8804 4096,
8805 SamplerConfig {
8806 seed: Some(42),
8807 ..Default::default()
8808 },
8809 )
8810}
8811
8812#[inline]
8817fn mask_bit(row: &[u8], j: usize) -> bool {
8818 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
8819}
8820
8821fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
8827 for r in 0..rows {
8828 let base = r * inter;
8829 for (bi, &byte) in row.iter().enumerate() {
8830 if byte == 0xFF {
8831 continue;
8832 }
8833 let j0 = bi * 8;
8834 for bit in 0..8 {
8835 let j = j0 + bit;
8836 if j < inter && byte & (1 << bit) == 0 {
8837 g[base + j] = 0.0;
8838 }
8839 }
8840 }
8841 }
8842}
8843
8844fn dense_ffn_batch(
8845 d: &DenseFfn,
8846 xs: &[f32],
8847 b: usize,
8848 pool: Option<&Pool>,
8849 mask_row: Option<&[u8]>,
8850) -> Vec<f32> {
8851 let inter = d.gate_proj.rows();
8852 let hidden = d.down_proj.rows();
8853 if mask_row.is_none()
8861 && d.act == Act::Silu
8862 && b >= 32
8863 && crate::gpu::enabled_here()
8864 && !crate::gpu::mm_killed()
8865 {
8866 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
8867 d.gate_proj.mapped_q4t(),
8868 d.up_proj.mapped_q4t(),
8869 d.down_proj.mapped_q4t(),
8870 ) {
8871 let mut out = vec![0.0f32; b * hidden];
8872 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
8873 return out;
8874 }
8875 }
8876 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
8881 d.gate_proj.mapped_q4tp(),
8882 d.up_proj.mapped_q4tp(),
8883 d.down_proj.mapped_q4tp(),
8884 ) {
8885 let mut out = vec![0.0f32; b * hidden];
8886 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
8887 return out;
8888 }
8889 }
8890 }
8891 let mut g = vec![0.0f32; b * inter];
8892 d.gate_proj.matmat(xs, b, &mut g, pool);
8893 let mut u = vec![0.0f32; b * inter];
8894 d.up_proj.matmat(xs, b, &mut u, pool);
8895 for i in 0..b * inter {
8896 g[i] = d.act.combine(g[i], u[i]);
8897 }
8898 if let Some(row) = mask_row {
8899 zero_masked_cols(&mut g, b, inter, row);
8900 }
8901 let mut out = vec![0.0f32; b * hidden];
8902 d.down_proj.matmat(&g, b, &mut out, pool);
8903 out
8904}
8905
8906fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
8911 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8912 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8913 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
8914 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
8915 if (!on && !dump) || b == 0 {
8916 return;
8917 }
8918 let hidden = xs.len() / b;
8919 if on {
8920 let mut acc = m.act_sq.borrow_mut();
8921 if acc.len() < hidden {
8922 acc.resize(hidden, 0.0);
8923 }
8924 for t in 0..b {
8925 let row = &xs[t * hidden..(t + 1) * hidden];
8926 for (a, &v) in acc.iter_mut().zip(row) {
8927 *a += (v as f64) * (v as f64);
8928 }
8929 }
8930 }
8931 if dump {
8932 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
8935 .ok()
8936 .and_then(|v| v.parse().ok())
8937 .unwrap_or(4096);
8938 let mut rows = m.act_rows.borrow_mut();
8939 if rows.len() < cap * hidden {
8940 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
8941 rows.extend_from_slice(&xs[..take * hidden]);
8942 }
8943 }
8944}
8945
8946#[derive(Clone, Copy)]
8949struct SendVecs(*mut Vec<f32>);
8950unsafe impl Send for SendVecs {}
8951unsafe impl Sync for SendVecs {}
8952impl SendVecs {
8953 #[inline]
8954 fn at(self, i: usize) -> *mut Vec<f32> {
8955 unsafe { self.0.add(i) }
8956 }
8957}
8958
8959fn moe_ffn_batch(
8960 m: &MoeFfn,
8961 xs: &[f32],
8962 b: usize,
8963 hidden: usize,
8964 pool: Option<&Pool>,
8965 allowed: Option<&[bool]>,
8966) -> Vec<f32> {
8967 accumulate_act(m, xs, b);
8968 let ne = m.experts.len();
8969 let mut logits = vec![0.0f32; b * ne];
8970 match &m.resonance {
8971 Some(r) => {
8972 let hdim = xs.len() / b.max(1);
8973 for bi in 0..b {
8974 r.scores(&xs[bi * hdim..(bi + 1) * hdim], &mut logits[bi * ne..(bi + 1) * ne]);
8975 }
8976 }
8977 None => m.router.matmat(xs, b, &mut logits, pool),
8978 }
8979
8980 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
8983 {
8984 let mut st = m.stats.borrow_mut();
8985 if st.len() < ne {
8986 st.resize(ne, 0);
8987 }
8988 for bi in 0..b {
8989 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
8990 for &e in &idx {
8991 st[e] += 1;
8992 assign[e].push((bi, p[e] / wsum));
8993 }
8994 }
8995 }
8996
8997 let mut out = vec![0.0f32; b * hidden];
8998 let cols = m.experts[0].gate_proj.cols();
8999 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
9000 let sb = list.len();
9001 let mut sub = vec![0.0f32; sb * cols];
9002 for (k, &(bi, _)) in list.iter().enumerate() {
9003 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
9004 }
9005 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
9006 for (k, &(bi, w)) in list.iter().enumerate() {
9007 for i in 0..hidden {
9008 out[bi * hidden + i] += w * eo[k * hidden + i];
9009 }
9010 }
9011 };
9012 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
9018 if pool.is_some() && active.len() >= 8 {
9019 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
9020 {
9021 let panel_ptr = SendVecs(panels.as_mut_ptr());
9022 let experts = &m.experts;
9025 let (active_r, assign_r) = (&active, &assign);
9026 let run = |start: usize, end: usize| {
9027 for ai in start..end {
9028 let e = active_r[ai];
9029 let list = &assign_r[e];
9030 let sb = list.len();
9031 let mut sub = vec![0.0f32; sb * cols];
9032 for (k, &(bi, _)) in list.iter().enumerate() {
9033 sub[k * cols..(k + 1) * cols]
9034 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
9035 }
9036 unsafe {
9038 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
9039 }
9040 }
9041 };
9042 match pool {
9043 Some(p) => p.run_rows(active.len(), &run),
9044 None => run(0, active.len()),
9045 }
9046 }
9047 for (ai, &e) in active.iter().enumerate() {
9048 for (k, &(bi, w)) in assign[e].iter().enumerate() {
9049 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
9050 for i in 0..hidden {
9051 out[bi * hidden + i] += w * eo[i];
9052 }
9053 }
9054 }
9055 } else {
9056 for &e in &active {
9057 run_expert(&m.experts[e], &assign[e], &mut out);
9058 }
9059 }
9060 if let Some((se, gate)) = &m.shared {
9061 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
9062 let mut gl = vec![0.0f32; b];
9063 gate.matmat(xs, b, &mut gl, pool);
9064 (0..b)
9065 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
9066 .collect()
9067 } else {
9068 (0..b).map(|bi| (bi, 1.0)).collect()
9069 };
9070 run_expert(se, &all, &mut out);
9071 }
9072 out
9073}
9074
9075thread_local! {
9076 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
9080 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
9081}
9082
9083fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9085 if crate::gpu::enabled_here()
9096 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
9097 {
9098 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
9099 crate::gpu::ProbeArm::Gpu
9100 } else {
9101 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
9102 };
9103 match arm {
9104 crate::gpu::ProbeArm::Gpu => {
9105 let t0 = std::time::Instant::now();
9106 if let Some(out) = dense_ffn_gpu(d, x, pool) {
9107 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
9108 return out;
9109 }
9110 }
9111 crate::gpu::ProbeArm::CpuTimed => {
9112 let t0 = std::time::Instant::now();
9113 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
9114 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
9115 return out;
9116 }
9117 crate::gpu::ProbeArm::Cpu => {
9118 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
9119 }
9120 }
9121 }
9122 dense_ffn_cpu(d, x, pool)
9123}
9124
9125fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9127 let inter = d.gate_proj.rows();
9128 FFN_SCRATCH.with(|s| {
9129 let mut s = s.borrow_mut();
9130 let [g, u, ..] = &mut *s;
9131 g.resize(inter, 0.0);
9132 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
9135 } else {
9137 u.resize(inter, 0.0);
9138 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9140 for i in 0..inter {
9141 g[i] = d.act.combine(g[i], u[i]);
9142 }
9143 }
9144 FFN_PROBE.with(|pr| {
9147 if let Some(acc) = pr.borrow_mut().as_mut() {
9148 let li = crate::gpu::cur_layer();
9149 if li >= 0 {
9150 if let Some(row) = acc.get_mut(li as usize) {
9151 for (a, &v) in row.iter_mut().zip(g.iter()) {
9152 *a += (v as f64).abs();
9153 }
9154 }
9155 }
9156 }
9157 });
9158 let mut out = attention::take_buf(d.down_proj.rows());
9159 d.down_proj.matvec(g, &mut out, pool);
9160 out
9161 })
9162}
9163
9164thread_local! {
9165 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
9168 const { std::cell::RefCell::new(None) };
9169}
9170
9171fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
9176 let inter = d.gate_proj.rows();
9177 FFN_SCRATCH.with(|s| {
9178 let mut s = s.borrow_mut();
9179 let [g, u, ..] = &mut *s;
9180 g.resize(inter, 0.0);
9181 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
9182 } else {
9184 u.resize(inter, 0.0);
9185 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9186 for i in 0..inter {
9187 g[i] = d.act.combine(g[i], u[i]);
9188 }
9189 }
9190 zero_masked_cols(g, 1, inter, mask_row);
9191 let mut out = attention::take_buf(d.down_proj.rows());
9192 d.down_proj.matvec(g, &mut out, pool);
9193 out
9194 })
9195}
9196
9197fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
9203 if d.act != Act::Silu {
9205 return None;
9206 }
9207 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
9210 return None;
9211 }
9212 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
9213 let mut model_ref = None;
9214 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
9215 let model = model_ref?;
9216 let hidden = jobs[0].down.1;
9217 let mut out = attention::take_buf(hidden);
9218 if crate::gpu::moe_block(&model, &jobs, &mut out) {
9219 Some(out)
9220 } else {
9221 let mut out = out;
9222 attention::recycle_buf(&mut out);
9223 None
9224 }
9225}
9226
9227#[allow(clippy::type_complexity)]
9232#[allow(clippy::type_complexity)]
9233pub(crate) fn moe_parts(
9234 t: &QTensor,
9235) -> Option<(
9236 &std::sync::Arc<cortiq_core::CmfModel>,
9237 usize,
9238 usize,
9239 usize,
9240 &[f32],
9241 &[f32],
9242 bool,
9243 bool,
9244 bool,
9245)> {
9246 match t {
9247 QTensor::Mapped {
9248 model,
9249 idx,
9250 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
9251 rows,
9252 cols,
9253 row_scale,
9254 col_field,
9255 ..
9256 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
9257 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
9258 )),
9259 QTensor::Mapped {
9261 model,
9262 idx,
9263 dtype: cortiq_core::TensorDtype::Q1,
9264 rows,
9265 cols,
9266 ..
9267 } => Some((
9268 model,
9269 *idx,
9270 *rows,
9271 *cols,
9272 &[][..],
9273 &[][..],
9274 true,
9275 false,
9276 false,
9277 )),
9278 QTensor::Mapped {
9280 model,
9281 idx,
9282 dtype: cortiq_core::TensorDtype::Q4Tiled,
9283 rows,
9284 cols,
9285 ..
9286 } => Some((
9287 model,
9288 *idx,
9289 *rows,
9290 *cols,
9291 &[][..],
9292 &[][..],
9293 false,
9294 true,
9295 false,
9296 )),
9297 QTensor::Mapped {
9299 model,
9300 idx,
9301 dtype: cortiq_core::TensorDtype::Q4TiledP,
9302 rows,
9303 cols,
9304 ..
9305 } => Some((
9306 model,
9307 *idx,
9308 *rows,
9309 *cols,
9310 &[][..],
9311 &[][..],
9312 false,
9313 true,
9314 false,
9315 )),
9316 QTensor::Mapped {
9320 model,
9321 idx,
9322 dtype: cortiq_core::TensorDtype::Q2TiledP,
9323 rows,
9324 cols,
9325 ..
9326 } => Some((
9327 model,
9328 *idx,
9329 *rows,
9330 *cols,
9331 &[][..],
9332 &[][..],
9333 false,
9334 true,
9335 true,
9336 )),
9337 _ => None,
9338 }
9339}
9340
9341#[cfg(target_os = "macos")]
9347fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
9348 if m.router_sigmoid
9349 || m.router_input_norm
9350 || m.expert_bias.is_some()
9351 || m.route_tau.is_some()
9352 || m.mask.is_some()
9353 || m.per_expert_scale.is_some()
9354 || m.experts.is_empty()
9355 || m.top_k == 0
9356 || m.resonance.is_some()
9357 {
9358 return None;
9359 }
9360 let (sh, sg) = match &m.shared {
9363 Some((sh, Some(sg))) => (sh, sg),
9364 _ => return None,
9365 };
9366 let (rf, rr, rc) = m.router.f32_parts()?;
9367 if rr != m.experts.len() || rc != hidden {
9368 return None;
9369 }
9370 let (sf, sr, sc) = sg.f32_parts()?;
9371 if sr * sc != hidden {
9372 return None;
9373 }
9374 let inter = m.experts[0].gate_proj.rows();
9375 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
9378 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
9379 if e.act != Act::Silu
9380 || e.gate_proj.rows() != inter
9381 || e.gate_proj.cols() != hidden
9382 || e.up_proj.rows() != inter
9383 || e.up_proj.cols() != hidden
9384 || e.down_proj.rows() != hidden
9385 || e.down_proj.cols() != inter
9386 {
9387 return None;
9388 }
9389 let pick = |t: &QTensor| -> Option<usize> {
9390 if gu_q2 {
9391 t.mapped_q2tp().map(|(_, i)| i)
9392 } else {
9393 t.mapped_q4tp().map(|(_, i)| i)
9394 }
9395 };
9396 Some((
9397 pick(&e.gate_proj)?,
9398 pick(&e.up_proj)?,
9399 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
9400 ))
9401 };
9402 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
9403 let shared = trio(sh)?;
9404 Some(crate::gpu::GpuMoe {
9405 router: rf,
9406 sgate: sf,
9407 experts,
9408 shared,
9409 n_exp: m.experts.len(),
9410 top_k: m.top_k,
9411 inter,
9412 norm_topk: m.norm_topk_prob,
9413 route_scale: m.routed_scaling,
9414 gu_q2,
9415 })
9416}
9417
9418pub(crate) fn moe_push_job_parts<'a>(
9422 gate: &'a QTensor,
9423 up: &'a QTensor,
9424 down: &'a QTensor,
9425 x: &[f32],
9426 w: f32,
9427 swiglu_limit: f32,
9428 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
9429 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
9430) -> Option<()> {
9431 use crate::qtensor::prescale;
9432 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
9433 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
9434 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
9435 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
9436 return None; }
9438 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
9441 return None;
9442 }
9443 if !gq2 && dq2 {
9444 return None;
9445 }
9446 model_ref.get_or_insert_with(|| gm.clone());
9447 let dt = |cf: &[f32]| {
9448 if cf.is_empty() {
9449 cortiq_core::TensorDtype::Q8Row
9450 } else {
9451 cortiq_core::TensorDtype::Q8_2f
9452 }
9453 };
9454 jobs.push(crate::gpu::MoeJob {
9455 gate: (gi, gr, gc, grs),
9456 up: (ui, ur, uc, urs),
9457 down: (di, dr, dc, drs),
9458 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
9459 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
9460 down_col: dcf,
9461 w,
9462 q1: gq1,
9463 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
9464 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
9465 gu_q2: gq2,
9466 swiglu_limit,
9467 });
9468 Some(())
9469}
9470
9471fn moe_push_job<'a>(
9473 d: &'a DenseFfn,
9474 x: &[f32],
9475 w: f32,
9476 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
9477 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
9478) -> Option<()> {
9479 use crate::qtensor::prescale;
9480 if d.act != Act::Silu {
9481 return None; }
9483 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
9484 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
9485 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
9486 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
9487 return None; }
9489 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
9490 return None;
9491 }
9492 if !gq2 && dq2 {
9493 return None;
9494 }
9495 model_ref.get_or_insert_with(|| gm.clone());
9496 let gdt = if gcf.is_empty() {
9497 cortiq_core::TensorDtype::Q8Row
9498 } else {
9499 cortiq_core::TensorDtype::Q8_2f
9500 };
9501 let udt = if ucf.is_empty() {
9502 cortiq_core::TensorDtype::Q8Row
9503 } else {
9504 cortiq_core::TensorDtype::Q8_2f
9505 };
9506 jobs.push(crate::gpu::MoeJob {
9507 gate: (gi, gr, gc, grs),
9508 up: (ui, ur, uc, urs),
9509 down: (di, dr, dc, drs),
9510 xs_gate: prescale(x, gcf, gdt).into_owned(),
9511 xs_up: prescale(x, ucf, udt).into_owned(),
9512 down_col: dcf,
9513 w,
9514 q1: gq1,
9515 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
9516 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
9517 gu_q2: gq2,
9518 swiglu_limit: 0.0,
9519 });
9520 Some(())
9521}
9522
9523fn sparse_ffn_quant(
9530 d: &DenseFfn,
9531 x: &[f32],
9532 active: &[u16],
9533 hidden: usize,
9534 pool: Option<&Pool>,
9535) -> Vec<f32> {
9536 let n = active.len();
9537 let inter = d.gate_proj.rows();
9538 let mut act = vec![0.0f32; n];
9539 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
9542 let compute = |ai: usize| -> f32 {
9543 let idx = active[ai] as usize;
9544 if idx >= inter {
9545 return 0.0; }
9547 let mut s = if need_scratch {
9548 vec![0.0f32; hidden]
9549 } else {
9550 Vec::new()
9551 };
9552 let gate = d.gate_proj.row_dot(idx, x, &mut s);
9553 let up = d.up_proj.row_dot(idx, x, &mut s);
9554 d.act.combine(gate, up)
9555 };
9556 match pool {
9557 Some(p) if n >= 256 => {
9558 let ptr = SendMut(act.as_mut_ptr());
9559 p.run(&|widx, nw| {
9560 let chunk = n.div_ceil(nw);
9561 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
9562 for ai in s..e {
9563 unsafe { *ptr.at(ai) = compute(ai) };
9564 }
9565 });
9566 }
9567 _ => {
9568 for (ai, a) in act.iter_mut().enumerate() {
9569 *a = compute(ai);
9570 }
9571 }
9572 }
9573 let mut out = vec![0.0f32; hidden];
9575 for (ai, &idx) in active.iter().enumerate() {
9576 let w = act[ai];
9577 if w.abs() >= 1e-12 && (idx as usize) < inter {
9578 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
9579 }
9580 }
9581 out
9582}
9583
9584#[doc(hidden)]
9586pub fn sparse_ffn_quant_for_test(
9587 d: &DenseFfn,
9588 x: &[f32],
9589 active: &[u16],
9590 hidden: usize,
9591) -> Vec<f32> {
9592 sparse_ffn_quant(d, x, active, hidden, None)
9593}
9594
9595fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
9599 let deq = |t: &QTensor| -> Vec<f32> {
9600 let (rows, cols) = (t.rows(), t.cols());
9601 let mut out = vec![0.0f32; rows * cols];
9602 for r in 0..rows {
9603 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
9604 }
9605 out
9606 };
9607 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
9608}
9609
9610struct SendMut(*mut f32);
9612unsafe impl Send for SendMut {}
9613unsafe impl Sync for SendMut {}
9614impl SendMut {
9615 #[inline]
9616 #[allow(clippy::mut_from_ref)]
9619 unsafe fn at(&self, i: usize) -> &mut f32 {
9620 unsafe { &mut *self.0.add(i) }
9621 }
9622}
9623
9624fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
9634 let ne = logits.len();
9635 let p: Vec<f32> = if m.router_sigmoid {
9636 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
9637 } else {
9638 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9639 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
9640 let s: f32 = e.iter().sum();
9641 for v in &mut e {
9642 *v /= s;
9643 }
9644 e
9645 };
9646 let admit = |e: usize| {
9652 m.mask.as_ref().is_none_or(|mk| mk[e])
9653 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
9654 };
9655 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
9656 match &m.expert_bias {
9658 Some(b) => idx.sort_unstable_by(|&x, &y| {
9659 (p[y] + b[y])
9660 .partial_cmp(&(p[x] + b[x]))
9661 .unwrap()
9662 .then(x.cmp(&y))
9663 }),
9664 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
9665 }
9666 idx.truncate(m.top_k);
9667 if let Some(tau) = m.route_tau {
9671 let total: f32 = idx.iter().map(|&e| p[e]).sum();
9672 if total > 0.0 {
9673 let mut acc = 0.0f32;
9674 let mut keep = idx.len();
9675 for (i, &e) in idx.iter().enumerate() {
9676 acc += p[e];
9677 if acc >= tau * total {
9678 keep = i + 1;
9679 break;
9680 }
9681 }
9682 idx.truncate(keep);
9683 }
9684 }
9685 let wsum: f32 = if m.norm_topk_prob {
9686 let s: f32 = idx.iter().map(|&e| p[e]).sum();
9687 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
9690 } else {
9691 1.0 / m.routed_scaling
9692 };
9693 (idx, p, wsum)
9694}
9695
9696fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
9699 accumulate_act(m, x, 1);
9700 let ne = m.experts.len();
9701 let mut logits = vec![0.0f32; ne];
9702 match &m.resonance {
9703 Some(r) => r.scores(x, &mut logits),
9704 None => m.router.matvec(x, &mut logits, pool),
9705 }
9706 let (idx, p, wsum) = moe_route(&logits, m, allowed);
9707 {
9708 let mut st = m.stats.borrow_mut();
9709 if st.len() < ne {
9710 st.resize(ne, 0);
9711 }
9712 for &e in &idx {
9713 st[e] += 1;
9714 }
9715 }
9716 if crate::gpu::enabled_here() {
9721 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
9722 crate::gpu::ProbeArm::Gpu => {
9723 let t0 = std::time::Instant::now();
9724 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
9725 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
9726 return out;
9727 }
9728 }
9729 crate::gpu::ProbeArm::CpuTimed => {
9730 let t0 = std::time::Instant::now();
9731 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
9732 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
9733 return out;
9734 }
9735 crate::gpu::ProbeArm::Cpu => {
9736 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
9737 }
9738 }
9739 }
9740 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
9741}
9742
9743fn graph_note(built: bool) {
9747 use std::sync::atomic::{AtomicBool, Ordering};
9748 if built {
9749 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
9750 } else {
9751 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
9752 }
9753 static SAID: AtomicBool = AtomicBool::new(false);
9754 if !SAID.swap(true, Ordering::Relaxed) {
9755 if built {
9756 tracing::info!("wgpu whole-token graph: ACTIVE");
9757 } else {
9758 tracing::warn!("wgpu whole-token graph refused — per-op path");
9759 }
9760 }
9761}
9762
9763pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9767pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9768
9769fn moe_batch_enabled() -> bool {
9772 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9773 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
9774}
9775
9776fn moe_ffn_cpu_batched(
9782 m: &MoeFfn,
9783 x: &[f32],
9784 idx: &[usize],
9785 p: &[f32],
9786 wsum: f32,
9787 pool: Option<&Pool>,
9788) -> Option<Vec<f32>> {
9789 if idx.is_empty() || !moe_batch_enabled() {
9790 return None;
9791 }
9792 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
9796 return None;
9797 }
9798 let n = idx.len() + usize::from(m.shared.is_some());
9799 let mut pairs = Vec::with_capacity(n);
9800 let mut downs = Vec::with_capacity(n);
9801 let mut ws = Vec::with_capacity(n);
9802 for &e in idx {
9803 let d = &m.experts[e];
9804 if d.act != Act::Silu {
9805 return None;
9806 }
9807 pairs.push((&d.gate_proj, &d.up_proj));
9808 downs.push(&d.down_proj);
9809 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
9810 }
9811 if let Some((se, gate)) = &m.shared {
9814 if se.act != Act::Silu {
9815 return None;
9816 }
9817 let g = gate.as_ref().map_or(1.0, |gate| {
9818 let mut gl = [0.0f32; 1];
9819 gate.matvec(x, &mut gl, pool);
9820 1.0 / (1.0 + (-gl[0]).exp())
9821 });
9822 pairs.push((&se.gate_proj, &se.up_proj));
9823 downs.push(&se.down_proj);
9824 ws.push(g);
9825 }
9826 let inter = pairs[0].0.rows();
9827 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
9828 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
9829 return None;
9830 }
9831 let mut out = attention::take_buf(x.len());
9832 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
9833 attention::recycle_buf(&mut out);
9834 return None;
9835 }
9836 Some(out)
9837}
9838
9839fn moe_ffn_cpu(
9841 m: &MoeFfn,
9842 x: &[f32],
9843 idx: &[usize],
9844 p: &[f32],
9845 wsum: f32,
9846 pool: Option<&Pool>,
9847) -> Vec<f32> {
9848 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
9849 return out;
9850 }
9851 let mut out = attention::take_buf(x.len());
9852 for &e in idx {
9853 let mut eo = dense_ffn(&m.experts[e], x, pool);
9854 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
9855 for i in 0..out.len() {
9856 out[i] += w * eo[i];
9857 }
9858 attention::recycle_buf(&mut eo);
9859 }
9860 if let Some((se, gate)) = &m.shared {
9861 let mut so = dense_ffn(se, x, pool);
9862 let g = gate.as_ref().map_or(1.0, |gate| {
9863 let mut gl = [0.0f32; 1];
9864 gate.matvec(x, &mut gl, pool);
9865 1.0 / (1.0 + (-gl[0]).exp())
9866 });
9867 for i in 0..out.len() {
9868 out[i] += g * so[i];
9869 }
9870 attention::recycle_buf(&mut so);
9871 }
9872 out
9873}
9874
9875#[allow(clippy::too_many_arguments)]
9883fn mla_attention(
9884 w: &MlaWeights,
9885 normed: &[f32],
9886 cache: &mut crate::kv_cache::LayerKvCache,
9887 position: usize,
9888 inv_freq: &[f32],
9889 rope_scale: f32,
9890 eps: f64,
9891 pool: Option<&Pool>,
9892) -> Vec<f32> {
9893 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
9894 let hd = dr + dn;
9895 let mut q = vec![0.0f32; nh * hd];
9896 match (&w.q_a, &w.q_a_norm) {
9897 (Some(qa), Some(qn)) => {
9898 let mut t = vec![0.0f32; qa.rows()];
9899 qa.matvec(normed, &mut t, pool);
9900 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
9901 w.q_proj.matvec(&tn, &mut q, pool);
9902 }
9903 _ => w.q_proj.matvec(normed, &mut q, pool),
9904 }
9905 let mut ca = vec![0.0f32; lora + dr];
9906 w.kv_a.matvec(normed, &mut ca, pool);
9907 let (c_lat, k_rope) = ca.split_at_mut(lora);
9908 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
9909 let mut kvb = vec![0.0f32; nh * (dn + dv)];
9910 w.kv_b.matvec(&latn, &mut kvb, pool);
9911 if !w.nope {
9912 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
9913 }
9914 for h in 0..nh {
9915 if !w.nope {
9916 attention::rope_rotate_scaled(
9917 &mut q[h * hd..h * hd + dr],
9918 position,
9919 inv_freq,
9920 rope_scale,
9921 );
9922 }
9923 }
9924 let mut k = vec![0.0f32; nh * hd];
9925 let mut v = vec![0.0f32; nh * hd];
9926 for h in 0..nh {
9927 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
9928 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
9929 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
9930 }
9931 cache.append(&k, &v, &vec![true; nh]);
9932 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
9933 attention::recycle_buf(&mut imp);
9934 let mut ov = vec![0.0f32; nh * dv];
9935 for h in 0..nh {
9936 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
9937 }
9938 let mut out = vec![0.0f32; w.o_proj.rows()];
9939 w.o_proj.matvec(&ov, &mut out, pool);
9940 out
9941}
9942
9943fn dense_moe_ffn(
9950 dm: &DenseMoeFfn,
9951 x_normed: &[f32],
9952 h_raw: &[f32],
9953 eps: f64,
9954 norm_style: NormStyle,
9955 pool: Option<&Pool>,
9956) -> Vec<f32> {
9957 let mut d = dense_ffn(&dm.dense, x_normed, pool);
9958 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
9959 let m = &dm.moe;
9960 let ne = m.experts.len();
9961 let mut logits = vec![0.0f32; ne];
9962 if m.router_input_norm {
9963 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
9964 let inv = 1.0 / (ss + eps as f32).sqrt();
9965 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
9966 m.router.matvec(&xr, &mut logits, pool);
9967 } else {
9968 m.router.matvec(h_raw, &mut logits, pool);
9969 }
9970 let (idx, p, wsum) = moe_route(&logits, m, None);
9971 {
9972 let mut st = m.stats.borrow_mut();
9973 if st.len() < ne {
9974 st.resize(ne, 0);
9975 }
9976 for &e in &idx {
9977 st[e] += 1;
9978 }
9979 }
9980 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
9981 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
9982 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
9983 for (di, mi) in d.iter_mut().zip(&mo) {
9984 *di += mi;
9985 }
9986 d
9987}
9988
9989fn moe_gpu_refused(why: &'static str) {
9996 use std::sync::atomic::{AtomicBool, Ordering};
9997 static SAID: AtomicBool = AtomicBool::new(false);
9998 if !SAID.swap(true, Ordering::Relaxed) {
9999 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
10000 }
10001}
10002
10003fn moe_ffn_gpu(
10004 m: &MoeFfn,
10005 x: &[f32],
10006 idx: &[usize],
10007 p: &[f32],
10008 wsum: f32,
10009 pool: Option<&Pool>,
10010) -> Option<Vec<f32>> {
10011 use crate::gpu::MoeJob;
10012
10013 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
10014 let mut model_ref = None;
10015 for &e in idx {
10016 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
10017 moe_gpu_refused("push_job(expert)");
10018 return None;
10019 }
10020 }
10021 if let Some((se, gate)) = &m.shared {
10022 let g = gate.as_ref().map_or(1.0, |gate| {
10023 let mut gl = [0.0f32; 1];
10024 gate.matvec(x, &mut gl, pool);
10025 1.0 / (1.0 + (-gl[0]).exp())
10026 });
10027 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
10028 moe_gpu_refused("push_job(shared)");
10029 return None;
10030 }
10031 }
10032 let Some(model) = model_ref else {
10033 moe_gpu_refused("no model_ref");
10034 return None;
10035 };
10036 let hidden = jobs[0].down.1;
10037 let mut out = vec![0.0f32; hidden];
10038 if crate::gpu::moe_block(&model, &jobs, &mut out) {
10039 Some(out)
10040 } else {
10041 moe_gpu_refused("gpu::moe_block");
10042 None
10043 }
10044}
10045
10046fn ffn_forward(
10048 ffn: &FfnKind,
10049 x: &[f32],
10050 pool: Option<&Pool>,
10051 experts_allowed: Option<&[bool]>,
10052) -> Vec<f32> {
10053 match ffn {
10054 FfnKind::Dense(d) => dense_ffn(d, x, pool),
10055 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
10056 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
10060 }
10061}
10062
10063fn ffn_forward_pair(
10067 ffn: &FfnKind,
10068 x1: &[f32],
10069 x2: &[f32],
10070 pool: Option<&Pool>,
10071 experts_allowed: Option<&[bool]>,
10072) -> (Vec<f32>, Vec<f32>) {
10073 let d = match ffn {
10074 FfnKind::Dense(d) => d,
10075 FfnKind::Moe(m) => {
10076 return (
10077 moe_ffn(m, x1, pool, experts_allowed),
10078 moe_ffn(m, x2, pool, experts_allowed),
10079 );
10080 }
10081 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
10082 };
10083 let inter = d.gate_proj.rows();
10084 FFN_SCRATCH.with(|s| {
10085 let mut s = s.borrow_mut();
10086 let [g1, g2, u1, u2] = &mut *s;
10087 g1.resize(inter, 0.0);
10088 g2.resize(inter, 0.0);
10089 u1.resize(inter, 0.0);
10090 u2.resize(inter, 0.0);
10091 QTensor::matvec2_many(
10094 [&d.gate_proj, &d.up_proj],
10095 x1,
10096 x2,
10097 [g1.as_mut_slice(), u1.as_mut_slice()],
10098 [g2.as_mut_slice(), u2.as_mut_slice()],
10099 pool,
10100 );
10101 for i in 0..inter {
10102 g1[i] = d.act.combine(g1[i], u1[i]);
10103 g2[i] = d.act.combine(g2[i], u2[i]);
10104 }
10105 let mut o1 = attention::take_buf(d.down_proj.rows());
10106 let mut o2 = attention::take_buf(d.down_proj.rows());
10107 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
10108 (o1, o2)
10109 })
10110}
10111
10112#[cfg(test)]
10113mod tests {
10114
10115 #[test]
10116 fn cancel_flag_stops_generation() {
10117 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
10118 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
10121 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
10122 assert_eq!(r.finish_reason, "cancelled");
10123 assert!(
10124 r.token_ids.is_empty(),
10125 "no tokens after cancel: {:?}",
10126 r.token_ids
10127 );
10128 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
10130 assert_ne!(r2.finish_reason, "cancelled");
10131 }
10132 use super::*;
10133
10134 #[test]
10140 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
10141 let (hidden, inter) = (16usize, 40usize);
10142 let synth = |n: usize, salt: usize| -> Vec<f32> {
10143 (0..n)
10144 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
10145 .collect()
10146 };
10147 let d = DenseFfn {
10148 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
10149 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
10150 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
10151 act: Act::Silu,
10152 };
10153 let x = synth(hidden, 9);
10154 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
10156
10157 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
10158
10159 let mut g = vec![0.0f32; inter];
10161 d.gate_proj.matvec(&x, &mut g, None);
10162 let mut u = vec![0.0f32; inter];
10163 d.up_proj.matvec(&x, &mut u, None);
10164 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
10165 for i in 0..inter {
10166 g[i] = if act_set.contains(&(i as u16)) {
10167 inference::silu(g[i]) * u[i]
10168 } else {
10169 0.0
10170 };
10171 }
10172 let mut reference = vec![0.0f32; hidden];
10173 d.down_proj.matvec(&g, &mut reference, None);
10174
10175 let max_d = sparse
10176 .iter()
10177 .zip(&reference)
10178 .map(|(a, b)| (a - b).abs())
10179 .fold(0.0f32, f32::max);
10180 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
10181 }
10182
10183 fn attach_test_mtp(p: &mut Pipeline) {
10185 let (h, inter, heads, kv, hd) = (
10186 p.hidden_size,
10187 p.intermediate_size,
10188 p.num_heads,
10189 p.num_kv_heads,
10190 p.head_dim,
10191 );
10192 let synth = |n: usize, salt: usize| -> Vec<f32> {
10193 (0..n)
10194 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
10195 .collect()
10196 };
10197 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
10198 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
10199 };
10200 p.mtp = Some(MtpModule {
10201 enorm: vec![1.0; h],
10202 hnorm: vec![1.0; h],
10203 eh_proj: qt(h, 2 * h, 301),
10204 layer: LayerWeights {
10205 input_norm: vec![1.0; h],
10206 post_norm: vec![1.0; h],
10207 attn_out_norm: None,
10208 ffn_out_norm: None,
10209 layer_scale: None,
10210 ffn: FfnKind::Dense(DenseFfn {
10211 gate_proj: qt(inter, h, 315),
10212 up_proj: qt(inter, h, 316),
10213 down_proj: qt(h, inter, 317),
10214 act: Act::Silu,
10215 }),
10216 attn: AttnKind::Full {
10217 bias: None,
10218 wq: qt(heads * hd, h, 311),
10219 wk: qt(kv * hd, h, 312),
10220 wv: qt(kv * hd, h, 313),
10221 wo: qt(h, heads * hd, 314),
10222 q_norm: None,
10223 k_norm: None,
10224 output_gate: false,
10225 softplus_gate: None,
10226 },
10227 },
10228 final_norm: vec![1.0; h],
10229 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
10230 });
10231 }
10232
10233 #[test]
10234 fn speculative_equals_vanilla_greedy() {
10235 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
10239 let run = |spec: bool| {
10240 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10241 p.sampler_config.temperature = 0.0;
10242 attach_test_mtp(&mut p);
10243 p.speculative = spec;
10244 let r = p.generate("abcdef", 12, None, None).unwrap();
10245 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
10246 };
10247 let (vanilla, d0, _) = run(false);
10248 let (spec, d1, a1) = run(true);
10249 assert_eq!(d0, 0, "vanilla path must not draft");
10250 assert!(d1 > 0, "speculative path must draft");
10251 assert_eq!(
10252 vanilla, spec,
10253 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
10254 );
10255 }
10256
10257 #[test]
10258 fn speculative_accepts_constant_oracle() {
10259 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
10261 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10262 p.sampler_config.temperature = 0.0;
10263 p.sampler_config.repetition_penalty = 1.0;
10264 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
10267 attach_test_mtp(&mut p);
10268 p.speculative = true;
10269 let r = p.generate("abcd", 10, None, None).unwrap();
10270 assert!(r.mtp_drafted > 0);
10271 assert_eq!(
10272 r.mtp_accepted, r.mtp_drafted,
10273 "constant logits → every draft accepted"
10274 );
10275 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
10278 }
10279
10280 #[test]
10281 fn empty_prompt_is_an_error_not_a_panic() {
10282 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
10283 let r = p.generate("", 4, None, None);
10284 assert!(r.is_err(), "empty prompt must be a clean error");
10285 }
10286
10287 #[test]
10288 fn every_token_enters_kv_exactly_once() {
10289 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10290 p.sampler_config.temperature = 0.0;
10292 let r = p.generate("abc", 2, None, None).unwrap();
10293 assert_eq!(r.prompt_tokens, 3);
10294 assert_eq!(
10298 p.kv_cache.seq_len(),
10299 3 + r.tokens_generated - 1,
10300 "each token must be cached exactly once (v1 cached the last prompt token twice)"
10301 );
10302 }
10303
10304 #[test]
10305 fn generation_is_reproducible_with_seed() {
10306 let run = || {
10307 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10308 p.generate("hello", 8, None, None).unwrap().token_ids
10309 };
10310 assert_eq!(run(), run());
10311 }
10312
10313 #[test]
10314 fn resetting_sampler_restarts_the_seeded_stream() {
10315 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10316 let config = SamplerConfig {
10317 seed: Some(1234),
10318 ..SamplerConfig::default()
10319 };
10320 p.set_sampler_config(config.clone());
10321 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
10322 p.set_sampler_config(config);
10323 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
10324 assert_eq!(first, second);
10325 }
10326
10327 #[test]
10328 fn eviction_bounds_the_cache() {
10329 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
10330 p.kv_cache.max_seq_len = 6;
10331 p.sampler_config.temperature = 0.0;
10332 let _ = p.generate("abcd", 12, None, None).unwrap();
10333 assert!(
10334 p.kv_cache.seq_len() <= 6 + 1,
10335 "cache must stay bounded by max_seq_len (got {})",
10336 p.kv_cache.seq_len()
10337 );
10338 }
10339
10340 #[test]
10341 fn confidence_matches_tokens_and_is_a_probability() {
10342 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10343 p.sampler_config.temperature = 0.0;
10344 p.sampler_config.repetition_penalty = 1.0;
10345 let r = p.generate("abcd", 10, None, None).unwrap();
10346 assert_eq!(
10347 r.token_confidence.len(),
10348 r.token_ids.len(),
10349 "one confidence per emitted token"
10350 );
10351 for &c in &r.token_confidence {
10352 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
10353 }
10354 let logits = [1.0f32, 3.0, 0.5, 3.0];
10356 let p0 = top1_prob_t(&logits, 1, 1.0);
10357 let p1 = top1_prob_t(&logits, 3, 1.0);
10358 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
10359 assert!(p0 > 0.0 && p0 < 1.0);
10360 let sharp = top1_prob_t(&logits, 1, 1.0);
10362 let soft = top1_prob_t(&logits, 1, 2.0);
10363 assert!(soft < sharp, "higher temperature lowers peak confidence");
10364 }
10365
10366 #[test]
10367 fn trace_is_opt_in_and_parallels_the_output() {
10368 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10370 p.sampler_config.temperature = 0.0;
10371 p.sampler_config.repetition_penalty = 1.0;
10372 let r = p.generate("abcd", 10, None, None).unwrap();
10373 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
10374
10375 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10377 p.sampler_config.temperature = 0.0;
10378 p.sampler_config.repetition_penalty = 1.0;
10379 p.set_trace(true);
10380 let r = p.generate("abcd", 10, None, None).unwrap();
10381 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
10382 for (i, tr) in r.traces.iter().enumerate() {
10383 assert_eq!(tr.t, i, "trace index is sequential");
10384 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
10385 assert_eq!(
10386 tr.confidence, r.token_confidence[i],
10387 "trace confidence matches the confidence channel"
10388 );
10389 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
10391 }
10392 }
10393
10394 #[test]
10395 fn explain_prefill_logits_match_greedy_first_token() {
10396 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10400 p.sampler_config.temperature = 0.0;
10401 p.sampler_config.repetition_penalty = 1.0;
10402 let ids = p.tokenizer.encode("abcd");
10403 let logits = p.prefill_next_logits(&ids, None);
10404 let argmax = logits
10405 .iter()
10406 .enumerate()
10407 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10408 .unwrap()
10409 .0 as u32;
10410 let r = p.generate("abcd", 1, None, None).unwrap();
10411 assert_eq!(
10412 argmax, r.token_ids[0],
10413 "explain preview must match greedy emit"
10414 );
10415 }
10416
10417 #[test]
10418 fn laguna_shared_expert_is_unconditionally_added() {
10419 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
10420 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
10421 let zero_dense = || DenseFfn {
10422 gate_proj: matrix(vec![0.0; 4]),
10423 up_proj: matrix(vec![0.0; 4]),
10424 down_proj: matrix(vec![0.0; 4]),
10425 act: Act::Silu,
10426 };
10427 let shared = DenseFfn {
10428 gate_proj: identity(),
10429 up_proj: identity(),
10430 down_proj: identity(),
10431 act: Act::Silu,
10432 };
10433 let x = [1.0, 2.0];
10434 let expected = dense_ffn(&shared, &x, None);
10435 let moe = MoeFfn {
10436 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
10437 experts: vec![zero_dense()],
10438 top_k: 1,
10439 norm_topk_prob: true,
10440 router_sigmoid: true,
10441 expert_bias: None,
10442 routed_scaling: 1.0,
10443 route_tau: None,
10444 shared: Some((shared, None)),
10445 stats: std::cell::RefCell::new(Vec::new()),
10446 act_sq: std::cell::RefCell::new(Vec::new()),
10447 act_rows: std::cell::RefCell::new(Vec::new()),
10448 mask: None,
10449 per_expert_scale: None,
10450 router_input_norm: false,
10451 resonance: None,
10452 };
10453 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
10454 for (actual, expected) in actual.iter().zip(expected) {
10455 assert!((actual - expected).abs() < 1e-6);
10456 }
10457 }
10458}