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 pub down_t: Option<QTensor>,
383 pub segs: Vec<FfnSeg>,
390}
391
392pub struct FfnSeg {
397 pub gate: QTensor,
398 pub up: QTensor,
399 pub down: QTensor,
400 pub start: usize,
401 pub width: usize,
402}
403
404pub enum FfnKind {
407 Dense(DenseFfn),
408 Moe(MoeFfn),
412 DenseMoe(Box<DenseMoeFfn>),
419}
420
421pub struct DenseMoeFfn {
423 pub dense: DenseFfn,
424 pub moe: MoeFfn,
425 pub post_norm_1: Vec<f32>,
427 pub pre_norm_2: Vec<f32>,
430 pub post_norm_2: Vec<f32>,
432}
433
434pub struct MoeFfn {
435 pub router: QTensor,
437 pub experts: Vec<DenseFfn>,
438 pub top_k: usize,
439 pub norm_topk_prob: bool,
440 pub router_sigmoid: bool,
443 pub expert_bias: Option<Vec<f32>>,
447 pub routed_scaling: f32,
450 pub route_tau: Option<f32>,
456 pub shared: Option<(DenseFfn, Option<QTensor>)>,
459 pub stats: std::cell::RefCell<Vec<u64>>,
463 pub act_sq: std::cell::RefCell<Vec<f64>>,
470 pub act_rows: std::cell::RefCell<Vec<f32>>,
476 pub mask: Option<Vec<bool>>,
481 pub per_expert_scale: Option<Vec<f32>>,
484 pub router_input_norm: bool,
488 pub resonance: Option<Resonance>,
492}
493
494pub struct Resonance {
496 pub mu: Vec<f32>,
498 pub u: Vec<f32>,
500 pub k: usize,
501 pub bias: Vec<f32>,
503}
504
505impl Resonance {
506 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
508 let h = x.len();
509 let ne = out.len();
510 for e in 0..ne {
511 let mu = &self.mu[e * h..(e + 1) * h];
512 let mut d2 = 0.0f32;
513 for j in 0..h {
514 let d = x[j] - mu[j];
515 d2 += d * d;
516 }
517 let mut proj = 0.0f32;
518 for i in 0..self.k {
519 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
520 let mut p = 0.0f32;
521 for j in 0..h {
522 p += (x[j] - mu[j]) * u[j];
523 }
524 proj += p * p;
525 }
526 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - (d2 - proj);
527 }
528 }
529}
530
531pub enum AttnKind {
534 Full {
536 wq: QTensor,
537 wk: QTensor,
538 wv: QTensor,
539 wo: QTensor,
540 q_norm: Option<Vec<f32>>,
541 k_norm: Option<Vec<f32>>,
542 output_gate: bool,
543 softplus_gate: Option<(QTensor, bool)>,
547 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
549 },
550 Linear(VmfPhaseWeights),
552 LinearGdn(GdnWeights),
554 ShortConv(ShortConvWeights),
557 Mla(Box<MlaWeights>),
565 Kda(Box<crate::linear_core::KdaWeights>),
569}
570
571pub struct MlaWeights {
573 pub q_proj: QTensor,
577 pub q_a: Option<QTensor>,
580 pub q_a_norm: Option<Vec<f32>>,
581 pub kv_a: QTensor,
583 pub kv_a_norm: Vec<f32>,
585 pub kv_b: QTensor,
587 pub o_proj: QTensor,
589 pub nh: usize,
590 pub qk_rope: usize,
591 pub qk_nope: usize,
592 pub v_dim: usize,
593 pub lora: usize,
594 pub scale: f32,
596 pub nope: bool,
598}
599
600pub struct MtpModule {
605 pub enorm: Vec<f32>,
606 pub hnorm: Vec<f32>,
607 pub eh_proj: QTensor,
609 pub layer: LayerWeights,
610 pub final_norm: Vec<f32>,
611 pub kv: crate::kv_cache::LayerKvCache,
612}
613
614#[cfg(target_os = "macos")]
621enum MetalRowsItem<'a> {
622 Gdn {
623 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
624 first: usize,
625 },
626 Attn {
627 l: crate::gpu_metal::AttnGpuLayer<'a>,
628 li: usize,
629 q_norm: Option<&'a [f32]>,
630 k_norm: Option<&'a [f32]>,
631 output_gate: bool,
632 },
633}
634
635#[cfg(target_os = "macos")]
636struct MetalVerifyPending {
637 graph: crate::gpu_metal::VerifyGraph,
638 gdn_layers: Vec<usize>,
639 attn_layers: Vec<(usize, usize)>,
640}
641
642#[derive(Clone, Copy)]
646enum SpecTrial {
647 Spec {
648 t0: std::time::Instant,
649 gen0: usize,
650 rounds: usize,
651 },
652 Plain {
653 t0: std::time::Instant,
654 gen0: usize,
655 },
656 Decided {
657 spec: bool,
658 recheck_at: usize,
659 },
660}
661
662#[derive(Default, Clone, Copy)]
673struct SpecMon {
674 round_ms: f64,
675 tokens: f64,
676 plain_ms: f64,
677 n: u32,
678 fails: u32,
679}
680
681impl SpecMon {
682 fn round(&mut self, dt_ms: f64, produced: usize) {
683 self.n += 1;
684 if self.n == 1 {
685 return; }
687 let a = if self.n == 2 { 1.0 } else { 0.3 };
688 self.round_ms += a * (dt_ms - self.round_ms);
689 self.tokens += a * (produced as f64 - self.tokens);
690 }
691 fn pays(&self) -> bool {
692 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
693 }
694}
695
696pub struct GenerateResult {
698 pub text: String,
699 pub token_ids: Vec<u32>,
700 pub prompt_tokens: usize,
701 pub tokens_generated: usize,
702 pub finish_reason: String,
703 pub mtp_drafted: usize,
705 pub mtp_accepted: usize,
706 pub token_confidence: Vec<f32>,
711 pub traces: Vec<TokenTrace>,
714}
715
716#[derive(Clone, Debug)]
721pub struct TokenTrace {
722 pub t: usize,
724 pub token_id: u32,
726 pub confidence: f32,
728 pub active_skill: Option<String>,
730 pub recon: Option<f32>,
734 pub switched: bool,
737}
738
739#[cfg_attr(not(test), allow(dead_code))]
744fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
745 let t = if temp > 1e-3 { temp } else { 1.0 };
746 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
747 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
748 if sum > 0.0 {
749 (((logits[id as usize] - max) / t).exp()) / sum
750 } else {
751 0.0
752 }
753}
754
755fn prefill_batched() -> bool {
758 std::env::var("CMF_PREFILL")
759 .map(|v| v != "seq")
760 .unwrap_or(true)
761}
762
763#[derive(Clone, Copy)]
767enum PrefillIn<'a> {
768 Ids(&'a [u32]),
769 Hidden(&'a [f32]),
770}
771
772impl Pipeline {
779 fn can_prefill_batched(&self) -> bool {
780 prefill_batched() && !self.weights.layers.is_empty()
781 }
782}
783
784pub fn prefill_chunk() -> usize {
791 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
792 .ok()
793 .and_then(|v| v.parse::<usize>().ok())
794 {
795 return n.max(1);
796 }
797 if cfg!(target_os = "macos") {
798 512
799 } else if cfg!(target_arch = "aarch64") {
800 256
803 } else {
804 48
805 }
806}
807
808pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
810
811impl Pipeline {
812 #[inline]
816 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
817 virtual_idx % self.physical_layers
818 }
819
820 #[inline]
823 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
824 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
825 }
826
827 #[allow(clippy::too_many_arguments)]
829
830 #[cfg(target_os = "macos")]
849 fn graph_prefill_preferred(&self) -> bool {
850 if !crate::gpu::enabled_here()
851 || !crate::gpu::q1_force()
852 || std::env::var("CMF_GPU_BLOCK")
853 .map(|v| v == "0")
854 .unwrap_or(false)
855 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
858 {
859 return false;
860 }
861 self.weights
862 .layers
863 .iter()
864 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
865 }
866
867 #[cfg(not(target_os = "macos"))]
868 fn graph_prefill_preferred(&self) -> bool {
869 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
877 if !graph_on || !crate::gpu::enabled_here() {
878 return false;
879 }
880 if self.o1_active() {
889 return false;
890 }
891 self.weights
892 .layers
893 .iter()
894 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
895 }
896
897 #[cfg(target_os = "macos")]
898 fn q1_graph_gpu(
899 &mut self,
900 start: usize,
901 upto: Option<usize>,
902 position: usize,
903 h: &mut [f32],
904 ) -> usize {
905 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
907 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
909 || !crate::gpu::q1_force()
910 || std::env::var("CMF_GPU_BLOCK")
911 .map(|v| v == "0")
912 .unwrap_or(false)
913 {
914 if std::env::var("CMF_GRAPH_DBG").is_ok() {
915 eprintln!(
916 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
917 self.attn_softcap > 0.0,
918 crate::gpu::enabled_here(),
919 crate::gpu::q1_force(),
920 );
921 }
922 return start;
923 }
924 if self.swa.is_some()
929 || self.global_attn.is_some()
930 || self.attention_heads_per_layer.is_some()
931 || self.attn_v_norm
932 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
933 || self.weights.layers.iter().any(|lw| {
934 lw.attn_out_norm.is_some()
935 || lw.ffn_out_norm.is_some()
936 || lw.layer_scale.is_some()
937 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
938 })
939 {
940 if std::env::var("CMF_GRAPH_DBG").is_ok() {
941 eprintln!(
942 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
943 self.swa.is_some(),
944 self.global_attn.is_some(),
945 self.attention_heads_per_layer.is_some(),
946 self.attn_v_norm,
947 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
948 );
949 }
950 return start;
951 }
952 let limit = upto
955 .map(|u| u + 1)
956 .unwrap_or(self.num_layers)
957 .min(self.num_layers);
958
959 enum Item<'a> {
960 Gdn {
961 run: Vec<GdnGpuLayer<'a>>,
962 first: usize,
963 },
964 Attn {
965 l: AttnGpuLayer<'a>,
966 li: usize,
967 q_norm: Option<&'a [f32]>,
968 k_norm: Option<&'a [f32]>,
969 output_gate: bool,
970 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
971 full_gpu: bool,
974 },
975 }
976
977 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
984 let attend_contract = attend_mode != "0"
985 && attend_mode != "off"
986 && self.head_dim % 4 == 0
987 && self.head_dim <= 256
988 && self.rotary_dim >= 2
989 && self.rotary_dim <= self.head_dim
990 && (self.rotary_dim / 2) % 32 == 0
991 && self.num_kv_heads > 0
992 && self.num_heads % self.num_kv_heads == 0;
993
994 let mut plan: Vec<Item> = Vec::new();
995 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
996 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
998 let mut scan = start;
999 while scan < limit {
1000 let lw = &self.weights.layers[self.phys_layer(scan)];
1001 let ffn = match &lw.ffn {
1002 FfnKind::Dense(d) if d.segs.is_empty() => {
1003 let (Some(g), Some(u), Some(dn)) = (
1004 d.gate_proj.q1_parts(),
1005 d.up_proj.q1_parts(),
1006 d.down_proj.q1_parts(),
1007 ) else {
1008 if block_diag {
1009 eprintln!(
1010 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
1011 );
1012 }
1013 break;
1014 };
1015 MetalFfn::Dense {
1016 gate: g,
1017 up: u,
1018 down: dn,
1019 }
1020 }
1021 FfnKind::Moe(m) => {
1022 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
1023 if block_diag {
1024 eprintln!(
1025 "block-graph: L{scan} MoE outside the graph contract — run ends"
1026 );
1027 }
1028 break;
1029 };
1030 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
1031 model_ref.get_or_insert_with(|| model.clone());
1032 }
1033 MetalFfn::Moe(moe)
1034 }
1035 _ => {
1036 if block_diag {
1037 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
1038 }
1039 break;
1040 }
1041 };
1042 match &lw.attn {
1043 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
1044 let parts = (
1045 w.in_proj_qkv.q1_parts(),
1046 w.in_proj_z.q1_parts(),
1047 w.in_proj_a.f32_parts(),
1048 w.in_proj_b.f32_parts(),
1049 w.out_proj.q1_parts(),
1050 );
1051 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
1052 if block_diag {
1053 eprintln!(
1054 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
1055 w.in_proj_qkv.q1_parts().is_some(),
1056 w.in_proj_z.q1_parts().is_some(),
1057 w.in_proj_a.f32_parts().is_some(),
1058 w.in_proj_b.f32_parts().is_some(),
1059 w.out_proj.q1_parts().is_some(),
1060 );
1061 }
1062 break;
1063 };
1064 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
1065 model_ref.get_or_insert_with(|| model.clone());
1066 }
1067 let gl = GdnGpuLayer {
1068 attn_norm: &lw.input_norm,
1069 post_norm: &lw.post_norm,
1070 qkv,
1071 z,
1072 a,
1073 b,
1074 out,
1075 ffn,
1076 conv1d: &w.conv1d,
1077 a_log: &w.a_log,
1078 dt_bias: &w.dt_bias,
1079 gnorm: &w.norm,
1080 };
1081 match plan.last_mut() {
1082 Some(Item::Gdn { run, .. }) => run.push(gl),
1083 _ => plan.push(Item::Gdn {
1084 run: vec![gl],
1085 first: scan,
1086 }),
1087 }
1088 }
1089 AttnKind::Full {
1090 wq,
1091 wk,
1092 wv,
1093 wo,
1094 q_norm,
1095 k_norm,
1096 output_gate,
1097 softplus_gate: None,
1098 bias,
1099 } if !self.kv_cache.layers[scan].o1_sealed()
1100 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1105 {
1106 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1107 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1108 break;
1109 };
1110 if let QTensor::Mapped { model, .. } = wq {
1111 model_ref.get_or_insert_with(|| model.clone());
1112 }
1113 let cache = &self.kv_cache.layers[scan];
1114 let o1_metal = cache.o1.is_some()
1118 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1119 && cache.o1_views().is_some();
1120 let full_gpu = attend_contract
1121 && cache.mode == crate::kv_cache::KvMode::F32
1122 && (cache.o1.is_none() || o1_metal)
1123 && bias.is_none()
1124 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1125 && pk.1 == self.num_kv_heads * self.head_dim
1126 && pv.1 == self.num_kv_heads * self.head_dim
1127 && po.2 == self.num_heads * self.head_dim;
1128 plan.push(Item::Attn {
1129 l: AttnGpuLayer {
1130 attn_norm: &lw.input_norm,
1131 post_norm: &lw.post_norm,
1132 wq: pq,
1133 wk: pk,
1134 wv: pv,
1135 wo: po,
1136 ffn,
1137 },
1138 li: scan,
1139 q_norm: q_norm.as_deref(),
1140 k_norm: k_norm.as_deref(),
1141 output_gate: *output_gate,
1142 bias: bias
1143 .as_ref()
1144 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1145 full_gpu,
1146 });
1147 }
1148 _ => break,
1149 }
1150 scan += 1;
1151 }
1152 let Some(model) = model_ref else {
1153 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1154 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1155 }
1156 return start;
1157 };
1158 if plan.is_empty() {
1159 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1160 eprintln!("q1-graph: empty plan at layer {start}");
1161 }
1162 return start;
1163 }
1164 let has_moe = plan.iter().any(|it| match it {
1165 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1166 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1167 });
1168 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1169 let dev_attend = attend_contract
1170 && (self.head_dim <= 128
1171 || has_moe
1172 || (self.head_dim <= 256 && has_gdn)
1178 || attend_mode == "force"
1179 || attend_mode == "256");
1180 if !dev_attend {
1181 for it in &mut plan {
1182 if let Item::Attn { li, full_gpu, .. } = it {
1183 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1186 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1187 if !keep_o1 {
1188 *full_gpu = false;
1189 }
1190 }
1191 }
1192 }
1193 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1194 use std::sync::atomic::{AtomicBool, Ordering};
1195 static SAID: AtomicBool = AtomicBool::new(false);
1196 if !SAID.swap(true, Ordering::Relaxed) {
1197 let fg = plan
1198 .iter()
1199 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1200 .count();
1201 let att = plan
1202 .iter()
1203 .filter(|it| matches!(it, Item::Attn { .. }))
1204 .count();
1205 eprintln!(
1206 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1207 plan.len(),
1208 self.head_dim,
1209 self.rotary_dim,
1210 self.num_kv_heads,
1211 self.num_heads,
1212 );
1213 }
1214 }
1215 let dims = GraphDims {
1216 hidden: self.hidden_size,
1217 eps: self.rms_eps as f32,
1218 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1219 };
1220 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1221 return start;
1222 };
1223 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1224 nv: cfg.num_v_heads,
1225 nk: cfg.num_k_heads,
1226 dk: cfg.key_head_dim,
1227 dv: cfg.value_head_dim,
1228 kk: cfg.conv_kernel,
1229 hidden: self.hidden_size,
1230 inter: self.intermediate_size,
1231 c_dim: cfg.conv_dim(),
1232 eps: cfg.rms_eps as f32,
1233 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1234 });
1235 let mut valid = 0usize;
1239 let mut end = start;
1240 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1242 static ONCE: std::sync::Once = std::sync::Once::new();
1243 ONCE.call_once(|| {
1244 for it in &plan {
1245 match it {
1246 Item::Gdn { first, run } => {
1247 eprintln!("plan: Gdn first={first} len={}", run.len())
1248 }
1249 Item::Attn { li, full_gpu, .. } => {
1250 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1251 }
1252 }
1253 }
1254 });
1255 }
1256 for item in &plan {
1257 let ok = match item {
1258 Item::Gdn { run, .. } => gcfg
1259 .as_ref()
1260 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1261 .unwrap_or(false),
1262 Item::Attn { l, .. } => graph.attn_ok(l),
1263 };
1264 if !ok {
1265 if block_diag {
1266 eprintln!(
1267 "block-graph: plan item {} ({}) failed graph preflight",
1268 valid,
1269 match item {
1270 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1271 Item::Attn { li, .. } => format!("Attn L{li}"),
1272 }
1273 );
1274 }
1275 break;
1276 }
1277 valid += 1;
1278 end += match item {
1279 Item::Gdn { run, .. } => run.len(),
1280 Item::Attn { .. } => 1,
1281 };
1282 }
1283 plan.truncate(valid);
1284 if plan.is_empty() {
1285 return start;
1286 }
1287
1288 let inv_freq = self.inv_freq.clone();
1289 let pool = self.pool.clone();
1290 let (nh, nkv, hd, hs, rd, eps) = (
1291 self.num_heads,
1292 self.num_kv_heads,
1293 self.head_dim,
1294 self.hidden_size,
1295 self.rotary_dim,
1296 self.rms_eps,
1297 );
1298 let norm_style = self.norm_style;
1299 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1300 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1301 let kv_id = self.graph_kv_id;
1302 let mut pending: Vec<(usize, usize)> = Vec::new();
1305 let mut dev_attn: Vec<usize> = Vec::new();
1308 for item in &plan {
1309 let _xt0 = std::time::Instant::now();
1310 let _xkind: u32 = match item {
1311 Item::Gdn { .. } => 2,
1312 Item::Attn { .. } => 3,
1313 };
1314 if self.loop_final_norm {
1316 let item_start = match item {
1317 Item::Gdn { first, .. } => *first,
1318 Item::Attn { li, .. } => *li,
1319 };
1320 if item_start > start && self.is_loop_end(item_start - 1) {
1321 graph.encode_loop_norm(&self.weights.final_norm);
1322 }
1323 }
1324 match item {
1325 Item::Gdn { run, first } => {
1326 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1327 if l.linear_state.len() != want {
1328 l.linear_state = vec![0f32; want];
1329 }
1330 }
1331 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1332 .iter()
1333 .map(|l| l.linear_state.as_slice())
1334 .collect();
1335 let _ig = std::time::Instant::now();
1336 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1337 tracing::error!("q1 graph: GDN run refused after validation");
1339 return start;
1340 }
1341 graph.commit_kind = 2;
1344 graph.commit();
1345 crate::gpu::stageprof(0, _ig.elapsed());
1346 pending.push((*first, run.len()));
1347 }
1348 Item::Attn {
1349 l,
1350 li,
1351 q_norm,
1352 k_norm,
1353 output_gate,
1354 bias,
1355 full_gpu,
1356 } => {
1357 let _ia = std::time::Instant::now();
1358 if *full_gpu {
1360 let cache = &self.kv_cache.layers[*li];
1361 let o1p = if cache.o1.is_some() {
1362 match cache.o1_views() {
1363 Some(views) => Some(crate::gpu::O1AttnParams {
1364 views,
1365 epoch: self.o1_epoch,
1366 }),
1367 None => None,
1369 }
1370 } else {
1371 None
1372 };
1373 let o1_layer = cache.o1.is_some();
1374 if o1_layer && o1p.is_none() {
1375 }
1377 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1378 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1379 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1380 let p = crate::gpu::AttnDeviceParams {
1381 kv_id,
1382 layer: *li,
1383 nh,
1384 nkv,
1385 hd,
1386 rd,
1387 position,
1388 eps: eps as f32,
1389 gemma,
1390 output_gate: *output_gate,
1391 q_norm: *q_norm,
1392 k_norm: *k_norm,
1393 inv_freq: &inv_freq,
1394 cpu_k,
1395 cpu_v,
1396 cpu_stored,
1397 o1: o1p,
1398 };
1399 let o1_bad = o1_layer && p.o1.is_none();
1400 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1401 {
1402 if p.o1.is_none() {
1404 dev_attn.push(*li);
1405 }
1406 graph.commit_kind = 3;
1407 graph.commit();
1408 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1412 continue;
1413 }
1414 }
1416 graph.encode_attn_prefix(l);
1417 graph.sync();
1418 if !pending.is_empty() {
1419 let idxs: Vec<usize> =
1420 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1421 let mut outs: Vec<&mut [f32]> = self
1422 .kv_cache
1423 .layers
1424 .iter_mut()
1425 .enumerate()
1426 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1427 .map(|(_, s)| s.linear_state.as_mut_slice())
1428 .collect();
1429 graph.read_states(&mut outs);
1430 }
1431 let mut q_raw = attention::take_buf(l.wq.1);
1432 let mut k = attention::take_buf(l.wk.1);
1433 let mut v = attention::take_buf(l.wv.1);
1434 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1435 let cfg = QwenAttnCfg {
1436 num_heads: nh,
1437 num_kv_heads: nkv,
1438 head_dim: hd,
1439 hidden_size: hs,
1440 position,
1441 inv_freq: &inv_freq,
1442 rotary_dim: rd,
1443 scale: self.attn_scale,
1444 softcap: self.attn_softcap,
1445 window: None,
1446 v_norm: false,
1447 q_norm: *q_norm,
1448 k_norm: *k_norm,
1449 output_gate: *output_gate,
1450 softplus_gate: None,
1451 rope_scale: 1.0,
1452 bias: *bias,
1453 rms_eps: eps,
1454 norm_style,
1455 pool: pool.as_deref(),
1456 };
1457 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
1460 || std::env::var("CMF_ATTN_DUMP").is_ok();
1461 let _ = full_gpu;
1462 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1463 let mut ao = attention::qwen_attention_core(
1464 q_raw,
1465 k,
1466 v,
1467 &mut self.kv_cache.layers[*li],
1468 &cfg,
1469 );
1470 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
1474 if let Some((qr0, k0, v0)) = oracle_in.clone() {
1475 let (cq, _cg, _ck, _cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1476 let cache = &self.kv_cache.layers[*li];
1477 let n = cache.head_keys(0).len() / hd;
1478 let mut bytes: Vec<u8> = Vec::new();
1479 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
1480 bytes.extend_from_slice(&v.to_le_bytes());
1481 }
1482 for v in &cq {
1483 bytes.extend_from_slice(&v.to_le_bytes());
1484 }
1485 for g in 0..nkv {
1486 for v in cache.head_keys(g) {
1487 bytes.extend_from_slice(&v.to_le_bytes());
1488 }
1489 }
1490 for g in 0..nkv {
1491 for v in cache.head_values(g) {
1492 bytes.extend_from_slice(&v.to_le_bytes());
1493 }
1494 }
1495 let _ = std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
1496 }
1497 }
1498 if let Some((qr0, k0, v0)) = oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")) {
1499 let (cq, _cg, ck, cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1500 let mut h_now = vec![0f32; hs];
1501 graph.read_h(&mut h_now);
1502 let cache = &self.kv_cache.layers[*li];
1503 let n_after = cache.head_keys(0).len() / hd;
1504 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_keys(g)[..(n_after - 1) * hd]).collect();
1505 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_values(g)[..(n_after - 1) * hd]).collect();
1506 let p = crate::gpu::AttnDeviceParams {
1507 kv_id,
1508 layer: *li,
1509 nh,
1510 nkv,
1511 hd,
1512 rd,
1513 position,
1514 eps: eps as f32,
1515 gemma,
1516 output_gate: *output_gate,
1517 q_norm: *q_norm,
1518 k_norm: *k_norm,
1519 inv_freq: &inv_freq,
1520 cpu_k,
1521 cpu_v,
1522 cpu_stored: n_after - 1,
1523 o1: None,
1524 };
1525 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1526 let md = |a: &[f32], b: &[f32]| a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
1527 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1528 eprintln!(
1529 "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}",
1530 nn(&cq), md(&cq, &dq), nn(&ck), md(&ck, &dk), nn(&cv), md(&cv, &dv), nn(&ao), md(&ao, &dao)
1531 );
1532 } else {
1533 eprintln!("attn-oracle L{li}: device probe declined");
1534 }
1535 }
1536 graph.encode_attn_suffix(l, &ao);
1537 graph.commit();
1540 attention::recycle_buf(&mut ao);
1541 }
1542 }
1543
1544 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1545 }
1546 let mut lm_rows = None;
1551 if self.graph_want_logits
1552 && upto.is_none()
1553 && end == self.num_layers
1554 && std::env::var("CMF_GPU_LMHEAD")
1555 .map(|v| v != "0")
1556 .unwrap_or(true)
1557 {
1558 if let Some(lm) = self.weights.lm_head.q1_parts() {
1559 if graph.lm_head_ok(lm) {
1560 graph.encode_lm_head(&self.weights.final_norm, lm);
1561 lm_rows = Some(lm.1);
1562 }
1563 }
1564 }
1565 let _sy0 = std::time::Instant::now();
1566 graph.sync();
1567 let _rs0 = std::time::Instant::now();
1568 if !pending.is_empty() {
1569 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1570 let mut outs: Vec<&mut [f32]> = self
1571 .kv_cache
1572 .layers
1573 .iter_mut()
1574 .enumerate()
1575 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1576 .map(|(_, s)| s.linear_state.as_mut_slice())
1577 .collect();
1578 graph.read_states(&mut outs);
1579 }
1580 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1581 use std::sync::atomic::{AtomicU64, Ordering};
1582 static SY: AtomicU64 = AtomicU64::new(0);
1583 static RS: AtomicU64 = AtomicU64::new(0);
1584 static N: AtomicU64 = AtomicU64::new(0);
1585 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1586 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1587 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1588 if n % 100 == 0 {
1589 eprintln!(
1590 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1591 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1592 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1593 );
1594 }
1595 }
1596 if let Some(rows) = lm_rows {
1597 crate::gpu::hostprof_encode_done(_mt0);
1598 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1599 graph.read_logits(&mut lg);
1600 crate::gpu::hostprof_total(_mt0);
1601 lg.resize(self.vocab_size, 0.0);
1602 if let Some(c) = self.final_softcap {
1603 for l in lg.iter_mut() {
1604 *l = c * (*l / c).tanh();
1605 }
1606 }
1607 self.graph_logits = Some(lg);
1608 }
1609 graph.finish(h);
1610 for li in dev_attn {
1614 let mut krow = attention::take_buf(nkv * hd);
1615 let mut vrow = attention::take_buf(nkv * hd);
1616 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1617 let cache = &mut self.kv_cache.layers[li];
1618 cache.append(&krow, &vrow, &[]);
1619 let n = cache.seq_len;
1620 let mut imp = attention::take_buf(n);
1621 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1622 cache.accumulate_imp(&imp);
1623 attention::recycle_buf(&mut imp);
1624 }
1625 attention::recycle_buf(&mut krow);
1626 attention::recycle_buf(&mut vrow);
1627 }
1628 end
1629 }
1630
1631 pub fn new(
1632 tokenizer: Tokenizer,
1633 weights: PipelineWeights,
1634 hidden_size: usize,
1635 intermediate_size: usize,
1636 num_heads: usize,
1637 num_kv_heads: usize,
1638 head_dim: usize,
1639 num_layers: usize,
1640 physical_layers: usize,
1641 loop_final_norm: bool,
1642 vocab_size: usize,
1643 rms_eps: f64,
1644 rope_base: f32,
1645 norm_style: NormStyle,
1646 max_seq_len: usize,
1647 sampler_config: SamplerConfig,
1648 ) -> Self {
1649 let rng = match sampler_config.seed {
1650 Some(s) => SplitMix64::new(s),
1651 None => SplitMix64::from_entropy(),
1652 };
1653 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1654 let pool = Pool::from_env();
1655 if let Some(p) = &pool {
1656 tracing::info!("worker pool: {} threads", p.n_workers());
1657 }
1658 Self {
1659 gpu_plan: None,
1660 tokenizer: std::sync::Arc::new(tokenizer),
1661 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1662 sampler_config,
1663 weights,
1664 hidden_size,
1665 intermediate_size,
1666 num_heads,
1667 num_kv_heads,
1668 head_dim,
1669 num_layers,
1670 physical_layers,
1671 loop_final_norm,
1672 vocab_size,
1673 rms_eps,
1674 rope_base,
1675 norm_style,
1676 rotary_dim: head_dim,
1677 attention_heads_per_layer: None,
1678 vmf_cfg: None,
1679 gdn_cfg: None,
1680 kda_cfg: None,
1681 g3n: None,
1682 dsv4: None,
1683 dsv4_mtp: Vec::new(),
1684 dspark: None,
1685 dspark_pending: Vec::new(),
1686 dspark_hist: Vec::new(),
1687 dspark_real: Vec::new(),
1688 dspark_trunk_picks: Vec::new(),
1689 dspark_exp: Vec::new(),
1690 dspark_draft_ns: 0,
1691 logit_multiplier: None,
1692 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1693 kv_history: Vec::new(),
1694 short_conv_cfg: None,
1695 mtp: None,
1696 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1697 rng,
1698 sampler_scratch: SamplerScratch::default(),
1699 spec_forced: None,
1700 spec_q: Vec::new(),
1701 spec_p: Vec::new(),
1702 spec_res: Vec::new(),
1703 spec_qs: Vec::new(),
1704 spec_ps: Vec::new(),
1705 spec_ress: Vec::new(),
1706 mtp_graph_mode: None,
1707 #[cfg(target_os = "macos")]
1708 metal_verify: None,
1709 inv_freq,
1710 ws: ForwardScratch::new(hidden_size),
1711 pool,
1712 model: None,
1713 dyn_force_f32: false,
1714 dyn_skill_layers: Vec::new(),
1715 dyn_active: None,
1716 dyn_blend_loaded: false,
1717 dyn_phi_layer: None,
1718 dyn_phi_ema: Vec::new(),
1719 dyn_phi_seen: 0,
1720 dyn_router: None,
1721 o1_cfg: None,
1722 o1_epoch: 0,
1723 o1_flags: Vec::new(),
1724 trace: false,
1725 calib_temp: 1.0,
1726 confidence_on: true,
1727 embed_multiplier: 1.0,
1728 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1729 swa: None,
1730 sliding_layers: None,
1731 inv_freq_local: None,
1732 rotary_dim_local: None,
1733 rope_scale: 1.0,
1734 rope_scale_local: 1.0,
1735 global_attn: None,
1736 inv_freq_global: None,
1737 attn_v_norm: false,
1738 final_softcap: None,
1739 head_clusters: None,
1740 attn_softcap: 0.0,
1741 graph_want_logits: false,
1742 graph_logits: None,
1743 graph_kv_id: {
1744 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1745 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1746 },
1747 }
1748 }
1749
1750 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1757 self.o1_flags = match &cfg {
1758 Some(c) => {
1759 let mut flags = c.layer_flags(self.num_layers);
1760 for (li, f) in flags.iter_mut().enumerate() {
1761 if *f
1762 && !matches!(
1763 self.weights.layers[self.phys_layer(li)].attn,
1764 AttnKind::Full { .. }
1765 )
1766 {
1767 *f = false;
1768 }
1769 }
1770 flags
1771 }
1772 None => Vec::new(),
1773 };
1774 if let Some(c) = &cfg {
1775 let n = self.o1_flags.iter().filter(|&&f| f).count();
1776 tracing::info!(
1777 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1778 self.num_layers,
1779 c.m,
1780 c.w,
1781 c.sink,
1782 c.rect
1783 );
1784 }
1785 self.o1_cfg = cfg;
1786 }
1787
1788 pub fn o1_active(&self) -> bool {
1790 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1791 }
1792
1793 pub fn o1_begin(&mut self) {
1798 if let Some(c) = &self.o1_cfg {
1799 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1800 for (li, &f) in self.o1_flags.iter().enumerate() {
1801 if f {
1802 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1803 }
1804 }
1805 }
1806 }
1807
1808 pub fn o1_seal(&mut self) {
1812 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1813 if self.o1_cfg.is_none() {
1814 return;
1815 }
1816 for li in 0..self.num_layers {
1817 if self.o1_flags.get(li).copied().unwrap_or(false) {
1818 self.kv_cache.layers[li].o1_seal(self.num_heads);
1819 }
1820 }
1821 }
1822
1823 pub fn set_trace(&mut self, on: bool) {
1825 self.trace = on;
1826 }
1827
1828 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1831 self.rng = match config.seed {
1832 Some(seed) => SplitMix64::new(seed),
1833 None => SplitMix64::from_entropy(),
1834 };
1835 self.sampler_config = config;
1836 }
1837
1838 pub fn set_confidence(&mut self, on: bool) {
1843 self.confidence_on = on;
1844 }
1845
1846 pub fn set_calib_temp(&mut self, t: f32) {
1849 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1850 }
1851
1852 pub fn calib_temp(&self) -> f32 {
1854 self.calib_temp
1855 }
1856
1857 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1860 self.rotary_dim = rotary_dim.min(self.head_dim);
1861 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1862 }
1863
1864 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1865 QwenAttnCfg {
1866 num_heads: self.num_heads,
1867 num_kv_heads: self.num_kv_heads,
1868 head_dim: self.head_dim,
1869 hidden_size: self.hidden_size,
1870 position,
1871 inv_freq: &self.inv_freq,
1872 rotary_dim: self.rotary_dim,
1873 scale: self.attn_scale,
1874 softcap: self.attn_softcap,
1875 window: None,
1876 v_norm: false,
1877 q_norm: None,
1878 k_norm: None,
1879 output_gate: false,
1880 softplus_gate: None,
1881 rope_scale: self.rope_scale,
1882 bias: None,
1883 rms_eps: self.rms_eps,
1884 norm_style: self.norm_style,
1885 pool: self.pool.as_deref(),
1886 }
1887 }
1888
1889 pub fn generate(
1891 &mut self,
1892 prompt: &str,
1893 max_tokens: usize,
1894 task_mask: Option<&TaskMask>,
1895 on_token: Option<TokenCallback>,
1896 ) -> Result<GenerateResult, String> {
1897 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1898 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1899 }
1900
1901 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
1903 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
1904 }
1905
1906 pub fn generate_from_ids(
1914 &mut self,
1915 input_ids: &[u32],
1916 max_tokens: usize,
1917 task_mask: Option<&TaskMask>,
1918 mut on_token: Option<TokenCallback>,
1919 ) -> Result<GenerateResult, String> {
1920 if std::env::var("CMF_TRACE_H").is_ok() {
1921 eprintln!("input_ids: {input_ids:?}");
1922 }
1923 if input_ids.is_empty() {
1924 return Err("empty prompt: nothing to generate from".to_string());
1925 }
1926 let task_mask = self.drop_open_mask(task_mask);
1931
1932 let reuse_from = {
1940 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1941 let h = &self.kv_history;
1942 if on
1943 && task_mask.is_none()
1944 && self.mtp.is_none()
1945 && self.o1_cfg.is_none()
1946 && !h.is_empty()
1947 && h.len() < input_ids.len()
1948 && input_ids[..h.len()] == h[..]
1949 {
1950 h.len()
1951 } else {
1952 0
1953 }
1954 };
1955 if reuse_from == 0 {
1956 self.kv_cache.clear();
1958 self.kv_history.clear();
1959 crate::gpu::graph_kv_reset(self.graph_kv_id);
1960 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1961 eprintln!(
1962 "kv-reuse: {} of {} prompt positions already cached",
1963 reuse_from,
1964 input_ids.len()
1965 );
1966 }
1967 crate::gpu::graph_race_begin_generation();
1968 self.o1_begin();
1969
1970 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
1976 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
2003 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
2004 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
2020 for lw in &self.weights.layers {
2021 if let FfnKind::Dense(d) = &lw.ffn {
2022 dense_n += 1;
2023 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
2024 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
2025 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
2026 {
2027 dense_q4tp += 1;
2028 }
2029 }
2030 }
2031 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
2032 let penalized = self.sampler_config.repetition_penalty != 1.0
2036 || self.sampler_config.presence_penalty != 0.0
2037 || !self.sampler_config.suppress_tokens.is_empty();
2038 #[cfg(feature = "gpu")]
2043 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
2044 #[cfg(not(feature = "gpu"))]
2045 let metal_wgpu = false;
2046 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
2047 let spec_wanted = match spec_env.as_deref() {
2048 Some("0") => false,
2049 Some(_) => {
2050 if metal_wgpu {
2051 tracing::warn!(
2052 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
2053 verified on this backend (garbage measured on Qwen3.5-0.8B)"
2054 );
2055 }
2056 true
2057 }
2058 None => spec_default_ok && !penalized && !metal_wgpu,
2059 };
2060 #[cfg(target_os = "macos")]
2063 let metal_graph = crate::gpu::q1_force()
2064 && crate::gpu::enabled_here()
2065 && std::env::var("CMF_GPU_BLOCK").map(|v| v != "0").unwrap_or(true);
2066 #[cfg(not(target_os = "macos"))]
2067 let metal_graph = false;
2068 let graph_spec = self.speculative
2069 && (graph_on || metal_graph)
2070 && self.mtp.is_some()
2071 && task_mask.is_none()
2072 && !self.o1_active()
2073 && spec_sampling_ok
2074 && spec_wanted;
2075 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
2082 let spec_active = self.speculative
2083 && self.mtp.is_some()
2084 && task_mask.is_none()
2085 && !self.o1_active()
2086 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
2087 let mut mtp = if spec_active { self.mtp.take() } else { None };
2090 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
2091 eprintln!(
2092 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
2093 mtp.is_some(),
2094 self.speculative,
2095 self.sampler_config.temperature < 1e-6,
2096 );
2097 }
2098 if let Some(m) = &mut mtp {
2099 m.kv.clear();
2100 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2102 self.mtp_graph_mode = None;
2103 }
2104 let mut router = if mtp.is_none() {
2108 self.dyn_router.take()
2109 } else {
2110 None
2111 };
2112 if let Some(r) = &mut router {
2113 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2116 }
2117
2118 let mut all_ids = input_ids.to_vec();
2119 let mut generated = 0usize;
2120 let mut finish_reason = "max_tokens".to_string();
2121 let mut drafted = 0usize;
2122 let mut accepted = 0usize;
2123 let mut dsv4_spec_bad = 0usize;
2130 let mut dsv4_spec_retry_at = 0usize;
2131 let mut confidence: Vec<f32> = Vec::new();
2132 let trace_on = self.trace;
2133 let calib_temp = self.calib_temp;
2134 let mut traces: Vec<TokenTrace> = Vec::new();
2135
2136 let mut hidden = vec![0.0f32; self.hidden_size];
2142 let mut pos = reuse_from;
2143 let fuse_lm = mtp.is_none()
2152 && router.is_none()
2153 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2154 self.graph_logits = None;
2155 self.graph_want_logits = false;
2156 let _tpf = std::time::Instant::now();
2157 let batch_k = std::env::var("CMF_BATCH_K")
2158 .ok()
2159 .and_then(|v| v.parse::<usize>().ok())
2160 .unwrap_or(0);
2161 while self.dsv4.is_some()
2172 && mtp.is_none()
2173 && pos < input_ids.len()
2174 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2175 {
2176 let end = (pos + prefill_chunk()).min(input_ids.len());
2177 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2178 let mut lg = Vec::new();
2179 if let Some(b) = &mut self.dsv4 {
2180 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2181 crate::dsv4::forward_chunk(
2182 g,
2183 layers,
2184 &cfg,
2185 st,
2186 &ids,
2187 pos,
2188 &self.inv_freq,
2189 self.pool.as_deref(),
2190 &mut lg,
2191 end == input_ids.len(),
2192 );
2193 }
2194 if end == input_ids.len() {
2195 self.graph_logits = Some(lg);
2196 }
2197 pos = end;
2198 hidden = vec![0.0; self.hidden_size];
2199 }
2200 let dyn_prefill = router.is_some();
2205 let graph_prefill = self.graph_prefill_preferred();
2211 #[cfg(target_os = "macos")]
2219 if task_mask.is_none()
2220 && !dyn_prefill
2221 && crate::gpu::q1_force()
2222 && crate::gpu::enabled_here()
2223 && self.gdn_cfg.is_some()
2224 && self.g3n.is_none()
2225 && input_ids.len() > 8
2226 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2227 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2228 {
2229 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2230 .ok()
2231 .and_then(|v| v.parse().ok())
2232 .filter(|&v| (16..=512).contains(&v))
2233 .unwrap_or(256);
2234 let hs = self.hidden_size;
2235 let _tp = std::time::Instant::now();
2236 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2237 let end = (pos + chunk).min(input_ids.len());
2238 let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2239 break;
2240 };
2241 if let Some(m) = &mut mtp {
2242 let n_pairs = if end < input_ids.len() { end - pos } else { end - pos - 1 };
2243 if n_pairs > 0 {
2244 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2245 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2246 .collect();
2247 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2248 for (j, (h, t)) in pairs.iter().enumerate() {
2249 let h = h.to_vec();
2250 let _ = self.mtp_step(m, &h, *t, pos + j);
2251 }
2252 }
2253 }
2254 }
2255 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2256 pos = end;
2257 }
2258 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2259 eprintln!(
2260 "metal-prefill: {} of {} tokens in {:.1} ms",
2261 pos,
2262 input_ids.len(),
2263 _tp.elapsed().as_secs_f64() * 1e3
2264 );
2265 }
2266 }
2267 if task_mask.is_none()
2268 && !dyn_prefill
2269 && !graph_prefill
2270 && self.can_prefill_batched()
2271 && self.g3n.is_none()
2272 && input_ids.len() > 2
2273 {
2274 let chunk = prefill_chunk();
2280 let hs = self.hidden_size;
2281 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2282 let end = (pos + chunk).min(input_ids.len());
2283 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2284 if let Some(m) = &mut mtp {
2285 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2286 .ok()
2287 .and_then(|v| v.parse().ok())
2288 .unwrap_or(0);
2289 for p in pos..end {
2290 if p + 1 < input_ids.len() {
2291 if probe >= 1 && p + 2 < input_ids.len() {
2292 let (d1, mut hx) = self.mtp_step_h(
2296 m,
2297 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2298 input_ids[p + 1],
2299 p,
2300 );
2301 let mut ok = d1 == input_ids[p + 2];
2302 Self::chain_probe_note(0, ok);
2303 let mut d_prev = d1;
2304 let mut extra = 0usize;
2305 for j in 1..probe {
2306 if p + 2 + j >= input_ids.len() {
2307 break;
2308 }
2309 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2310 extra += 1;
2311 ok = ok && dj == input_ids[p + 2 + j];
2312 Self::chain_probe_note(j, ok);
2313 d_prev = dj;
2314 hx = hj;
2315 }
2316 m.kv.truncate_last(extra);
2317 } else {
2318 let _ = self.mtp_step(
2319 m,
2320 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2321 input_ids[p + 1],
2322 p,
2323 );
2324 }
2325 }
2326 }
2327 }
2328 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2329 pos = end;
2330 }
2331 }
2332 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2333 if task_mask.is_none()
2334 && !dyn_prefill
2335 && !graph_prefill
2336 && !pair_off
2337 && self.pair_supported()
2338 {
2339 while pos + 1 < input_ids.len()
2340 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2341 {
2342 let e1 = self.embed_single(input_ids[pos]);
2343 let e2 = self.embed_single(input_ids[pos + 1]);
2344 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2345 self.commit_linear_scratch();
2347 if let Some(m) = &mut mtp {
2348 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2349 if pos + 2 < input_ids.len() {
2350 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2351 .ok()
2352 .and_then(|v| v.parse().ok())
2353 .unwrap_or(0);
2354 if probe >= 1 && pos + 3 < input_ids.len() {
2355 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2359 let mut ok = d1 == input_ids[pos + 3];
2360 Self::chain_probe_note(0, ok);
2361 let mut d_prev = d1;
2362 let mut extra = 0usize;
2363 for j in 1..probe {
2364 if pos + 3 + j >= input_ids.len() {
2365 break;
2366 }
2367 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2368 extra += 1;
2369 ok = ok && dj == input_ids[pos + 3 + j];
2370 Self::chain_probe_note(j, ok);
2371 d_prev = dj;
2372 hx = hj;
2373 }
2374 m.kv.truncate_last(extra);
2375 } else {
2376 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2377 }
2378 }
2379 }
2380 hidden = h2;
2381 pos += 2;
2382 }
2383 }
2384 if batch_k > 0
2393 && graph_prefill
2394 && task_mask.is_none()
2395 && !self.o1_active()
2396 && mtp.is_none()
2397 && !dyn_prefill
2398 && pos + 1 < input_ids.len()
2399 {
2400 let hs = self.hidden_size;
2401 let chunk = batch_k;
2402 while pos < input_ids.len() {
2403 let end = (pos + chunk).min(input_ids.len());
2404 let bk = end - pos;
2405 let mut hiddens = vec![0f32; bk * hs];
2406 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2407 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2408 }
2409 let positions: Vec<usize> = (pos..end).collect();
2410 let t_chunk = std::time::Instant::now();
2411 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2412 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2413 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2414 eprintln!(
2415 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
2416 bk as f64 / (ms / 1000.0)
2417 );
2418 }
2419 {
2420 use std::sync::atomic::{AtomicBool, Ordering};
2421 static SAID: AtomicBool = AtomicBool::new(false);
2422 if !SAID.swap(true, Ordering::Relaxed) {
2423 if ok_b {
2424 tracing::info!("batched prefill: ACTIVE (k={bk})");
2425 } else {
2426 tracing::warn!("batched prefill declined — per-position graph");
2427 }
2428 }
2429 }
2430 if ok_b {
2431 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2432 pos = end;
2433 } else {
2434 break; }
2436 }
2437 }
2438 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2439 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2440 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2441 if let Some(m) = &mut mtp {
2442 if pos + 1 < input_ids.len() {
2443 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2449 .ok()
2450 .and_then(|v| v.parse().ok())
2451 .unwrap_or(0);
2452 if probe >= 1 && pos + 2 < input_ids.len() {
2453 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2454 let mut ok = d1 == input_ids[pos + 2];
2455 Self::chain_probe_note(0, ok);
2456 let mut d_prev = d1;
2457 let mut extra = 0usize;
2458 for j in 1..probe {
2459 if pos + 2 + j >= input_ids.len() {
2460 break;
2461 }
2462 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2463 extra += 1;
2464 ok = ok && dj == input_ids[pos + 2 + j];
2465 Self::chain_probe_note(j, ok);
2466 d_prev = dj;
2467 hx = hj;
2468 }
2469 m.kv.truncate_last(extra);
2472 } else {
2473 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2474 }
2475 }
2476 }
2477 pos += 1;
2478 }
2479 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2480 eprintln!(
2481 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2482 input_ids.len(),
2483 _tpf.elapsed().as_secs_f64() * 1000.0
2484 );
2485 }
2486 if self
2489 .cancel
2490 .swap(false, std::sync::atomic::Ordering::Relaxed)
2491 {
2492 self.kv_history.clear();
2493 if let Some(m) = mtp {
2494 self.mtp = Some(m);
2495 }
2496 return Ok(GenerateResult {
2497 text: String::new(),
2498 token_ids: Vec::new(),
2499 prompt_tokens: input_ids.len(),
2500 tokens_generated: 0,
2501 finish_reason: "cancelled".to_string(),
2502 mtp_drafted: 0,
2503 mtp_accepted: 0,
2504 token_confidence: Vec::new(),
2505 traces: Vec::new(),
2506 });
2507 }
2508
2509 self.o1_seal();
2512
2513 macro_rules! commit {
2515 ($id:expr) => {{
2516 all_ids.push($id);
2517 generated += 1;
2518 if self.tokenizer.is_eos($id) {
2519 finish_reason = "stop".to_string();
2520 false
2521 } else {
2522 let token_text = self.tokenizer.decode_token($id);
2523 let mut go = true;
2524 if let Some(ref mut cb) = on_token {
2525 if !cb(&token_text) {
2526 finish_reason = "cancelled".to_string();
2527 go = false;
2528 }
2529 }
2530 go
2531 }
2532 }};
2533 }
2534
2535 let mut spec_trial = SpecTrial::Spec {
2546 t0: std::time::Instant::now(),
2547 gen0: generated,
2548 rounds: 0,
2549 };
2550 let mut spec_mon = SpecMon::default();
2551 let mut spec_watchdog_off = false;
2552 let mut next_pos = input_ids.len();
2554 'decode: while generated < max_tokens {
2555 if self
2556 .cancel
2557 .swap(false, std::sync::atomic::Ordering::Relaxed)
2558 {
2559 finish_reason = "cancelled".to_string();
2560 break 'decode;
2561 }
2562 let forced = self.spec_forced.take();
2567 let mut logits = match (forced, self.graph_logits.take()) {
2568 (Some(_), _) => Vec::new(),
2569 (None, Some(lg)) => lg,
2570 (None, None) => {
2571 inference::rms_norm_into(
2572 &hidden,
2573 &self.weights.final_norm,
2574 self.rms_eps,
2575 self.norm_style,
2576 &mut self.ws.n1,
2577 );
2578 self.lm_head_forward(&self.ws.n1)
2579 }
2580 };
2581 if generated
2584 == std::env::var("CMF_LOGIT_DUMP_STEP")
2585 .ok()
2586 .and_then(|v| v.parse().ok())
2587 .unwrap_or(0)
2588 {
2589 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
2590 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
2591 for v in hidden.iter().chain(logits.iter()) {
2592 bytes.extend_from_slice(&v.to_le_bytes());
2593 }
2594 let _ = std::fs::write(&path, &bytes);
2595 }
2596 }
2597 let t_next = match forced {
2598 Some(c) => c,
2599 None => sampler::sample_with_scratch_pool(
2600 &logits,
2601 &self.sampler_config,
2602 &all_ids,
2603 &mut self.rng,
2604 &mut self.sampler_scratch,
2605 self.pool.as_deref(),
2606 ),
2607 };
2608 if self.confidence_on {
2609 confidence.push(if logits.is_empty() {
2610 0.0
2611 } else {
2612 sampler::top1_prob_pool(
2613 self.pool.as_deref(),
2614 &mut self.sampler_scratch,
2615 &logits,
2616 t_next,
2617 calib_temp,
2618 )
2619 });
2620 }
2621 if !logits.is_empty() {
2622 attention::recycle_buf(&mut logits);
2623 }
2624 if trace_on {
2625 let skill = router.as_ref().and_then(|r| r.active_id());
2629 traces.push(TokenTrace {
2630 t: generated,
2631 token_id: t_next,
2632 confidence: confidence.last().copied().unwrap_or(0.0),
2633 active_skill: skill,
2634 recon: None,
2635 switched: false,
2636 });
2637 }
2638 if !commit!(t_next) {
2639 break 'decode;
2640 }
2641 if generated >= max_tokens {
2642 break 'decode;
2643 }
2644
2645 if self.kv_cache.needs_eviction() {
2646 static SAID: std::sync::Once = std::sync::Once::new();
2652 SAID.call_once(|| {
2653 tracing::warn!(
2654 "KV cache full at {} positions — evicting half; quality \
2655 will degrade. Raise CMF_MAX_SEQ.",
2656 self.kv_cache.max_seq_len,
2657 );
2658 });
2659 let keep = (self.kv_cache.max_seq_len / 2).max(1);
2660 self.kv_cache.evict(keep);
2661 }
2662
2663 if graph_spec {
2666 match spec_trial {
2667 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
2668 spec_mon.plain_ms =
2669 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
2670 let keep = spec_mon.pays();
2671 tracing::info!(
2672 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2673 spec_mon.tokens,
2674 spec_mon.round_ms,
2675 spec_mon.plain_ms,
2676 if keep { "speculating" } else { "plain" }
2677 );
2678 spec_mon.fails = 0;
2679 spec_trial = SpecTrial::Decided {
2680 spec: keep,
2681 recheck_at: if keep { usize::MAX } else { generated + 128 },
2682 };
2683 }
2684 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
2685 spec_mon.n = 0;
2686 spec_trial = SpecTrial::Spec {
2687 t0: std::time::Instant::now(),
2688 gen0: generated,
2689 rounds: 0,
2690 };
2691 }
2692 _ => {}
2693 }
2694 spec_watchdog_off = matches!(
2695 spec_trial,
2696 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
2697 );
2698 }
2699 match &mut mtp {
2700 #[cfg(feature = "gpu")]
2702 Some(m)
2703 if graph_spec
2704 && !spec_watchdog_off
2705 && generated + 1 < max_tokens
2706 && next_pos > 0 =>
2707 {
2708 let t_round = std::time::Instant::now();
2709 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
2710 m,
2711 &hidden,
2712 t_next,
2713 next_pos,
2714 &mut drafted,
2715 &mut accepted,
2716 &mut all_ids,
2717 ) {
2718 next_pos = n_pos;
2719 hidden = new_h;
2720 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2721 eprintln!(
2722 "spec-round wall {:.1} ms → {} tokens",
2723 t_round.elapsed().as_secs_f64() * 1e3,
2724 extra.len() + 1
2725 );
2726 }
2727 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
2731 spec_trial = Self::spec_trial_round(
2734 spec_trial,
2735 &mut spec_mon,
2736 generated + extra.len() + 1,
2737 );
2738 let mut stopped = false;
2739 for &id in &extra {
2740 if self.confidence_on {
2741 confidence.push(0.0);
2742 }
2743 if !commit!(id) {
2744 stopped = true;
2745 break;
2746 }
2747 }
2748 if stopped {
2749 break 'decode;
2750 }
2751 continue 'decode;
2752 }
2753 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
2764 spec_mon.tokens = 0.0;
2765 spec_mon.fails = 3;
2766 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
2767 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2768 next_pos += 1;
2769 continue 'decode;
2770 }
2771 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2773 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2774 drafted += 1;
2775 let emb1 = self.embed_single(t_next);
2776 let emb2 = self.embed_single(draft);
2777 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2778
2779 inference::rms_norm_into(
2780 &h1,
2781 &self.weights.final_norm,
2782 self.rms_eps,
2783 self.norm_style,
2784 &mut self.ws.n1,
2785 );
2786 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2787 let t_after = sampler::sample_with_scratch_pool(
2788 &logits1,
2789 &self.sampler_config,
2790 &all_ids,
2791 &mut self.rng,
2792 &mut self.sampler_scratch,
2793 self.pool.as_deref(),
2794 );
2795 if self.confidence_on {
2796 confidence.push(sampler::top1_prob_pool(
2797 self.pool.as_deref(),
2798 &mut self.sampler_scratch,
2799 &logits1,
2800 t_after,
2801 calib_temp,
2802 ));
2803 }
2804 attention::recycle_buf(&mut logits1);
2805 if trace_on {
2806 traces.push(TokenTrace {
2809 t: generated,
2810 token_id: t_after,
2811 confidence: confidence.last().copied().unwrap_or(0.0),
2812 active_skill: None,
2813 recon: None,
2814 switched: false,
2815 });
2816 }
2817 let stop = !commit!(t_after);
2818
2819 if t_after == draft {
2820 accepted += 1;
2821 self.commit_linear_scratch();
2822 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2823 hidden = h2;
2824 next_pos += 2;
2825 } else {
2826 for layer in &mut self.kv_cache.layers {
2828 layer.truncate_last(1);
2829 }
2830 if !stop {
2831 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2832 hidden = self.forward_layers(
2833 &self.embed_single(t_after),
2834 next_pos + 1,
2835 None,
2836 );
2837 }
2838 next_pos += 2;
2839 }
2840 if stop {
2841 break 'decode;
2842 }
2843 }
2844 _ => {
2846 #[cfg(feature = "gpu")]
2851 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2852 static SAID: std::sync::Once = std::sync::Once::new();
2853 SAID.call_once(|| {
2854 eprintln!(
2855 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2856 !self.dsv4_mtp.is_empty(),
2857 task_mask.is_none(),
2858 router.is_none(),
2859 !trace_on,
2860 self.sampler_config.temperature < 1e-6,
2861 self.sampler_config.repetition_penalty == 1.0,
2862 );
2863 });
2864 }
2865 #[cfg(feature = "gpu")]
2866 if Self::dsv4_spec_on()
2867 && self.dsv4.is_some()
2868 && !self.dsv4_mtp.is_empty()
2869 && task_mask.is_none()
2870 && router.is_none()
2871 && !trace_on
2872 && self.sampler_config.temperature < 1e-6
2873 && self.sampler_config.repetition_penalty == 1.0
2874 && generated + 1 < max_tokens
2875 && all_ids.len() >= 2
2876 && generated >= dsv4_spec_retry_at
2877 {
2878 let tip_token = all_ids[all_ids.len() - 2];
2879 let drafted0 = drafted;
2880 let round = self.dsv4_spec_step(
2881 tip_token,
2882 t_next,
2883 next_pos,
2884 max_tokens.saturating_sub(generated),
2885 &mut drafted,
2886 &mut accepted,
2887 );
2888 if drafted > drafted0 {
2889 let useful = round
2890 .as_ref()
2891 .is_some_and(|(extra, _)| !extra.is_empty());
2892 if useful {
2893 dsv4_spec_bad = 0;
2894 } else {
2895 dsv4_spec_bad += 1;
2896 if dsv4_spec_bad >= 2 {
2897 dsv4_spec_bad = 0;
2898 dsv4_spec_retry_at = generated.saturating_add(32);
2899 tracing::info!(
2900 "dsv4: draft не окупился дважды — точный walk на 32 токена"
2901 );
2902 }
2903 }
2904 }
2905 if let Some((extra, n_pos)) = round {
2906 next_pos = n_pos;
2907 let mut stopped = false;
2908 for &id in &extra {
2909 if self.confidence_on {
2910 confidence.push(0.0);
2911 }
2912 if !commit!(id) {
2913 stopped = true;
2914 break;
2915 }
2916 }
2917 if stopped {
2918 break 'decode;
2919 }
2920 continue 'decode;
2921 }
2922 }
2923 self.graph_want_logits = fuse_lm;
2924 let mut t_fwd = t_next;
2930 let pure_greedy = self.sampler_config.temperature < 1e-6
2931 && self.sampler_config.repetition_penalty == 1.0
2932 && self.sampler_config.suppress_tokens.is_empty();
2933 let burst_k = std::env::var("CMF_MULTISTEP")
2938 .ok()
2939 .and_then(|v| v.parse::<usize>().ok())
2940 .unwrap_or(0);
2941 if pure_greedy
2942 && burst_k >= 1
2943 && fuse_lm
2944 && task_mask.is_none()
2945 && router.is_none()
2946 && !trace_on
2947 && !self.confidence_on
2948 {
2949 let mut stopped = false;
2950 loop {
2951 let room = max_tokens.saturating_sub(generated);
2952 if room <= 2 {
2953 break;
2954 }
2955 let k = burst_k.min(room - 1);
2956 if k < 1 {
2957 break;
2958 }
2959 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2960 break;
2961 };
2962 next_pos += k;
2963 for &id in &ids {
2964 if !commit!(id) {
2965 stopped = true;
2966 break;
2967 }
2968 }
2969 if stopped {
2970 break;
2971 }
2972 t_fwd = *ids.last().unwrap();
2973 }
2974 if stopped {
2975 break 'decode;
2976 }
2977 }
2978 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2979 next_pos += 1;
2980 if let Some(r) = &mut router {
2983 let phi = self.dyn_phi_ema.clone();
2984 let decision = r.step(&phi, generated);
2985 if let Some(new_active) = decision {
2986 let _ = self.set_active_skill(new_active);
2987 }
2988 if trace_on {
2991 if let Some(last) = traces.last_mut() {
2992 let e = r.last_best_e();
2993 last.recon = e.is_finite().then_some(e);
2994 last.switched = decision.is_some();
2995 }
2996 }
2997 }
2998 }
2999 }
3000 }
3001
3002 self.graph_want_logits = false;
3003 self.graph_logits = None;
3004 if router.is_some() {
3006 let _ = self.set_active_skill(None);
3007 }
3008 self.dyn_router = router.or(self.dyn_router.take());
3009 self.mtp = mtp.or(self.mtp.take());
3010
3011 let output_ids = &all_ids[input_ids.len()..];
3012 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
3016 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
3017 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
3019 Ok(GenerateResult {
3020 text: self.tokenizer.decode(output_ids),
3021 token_ids: output_ids.to_vec(),
3022 prompt_tokens: input_ids.len(),
3023 tokens_generated: generated,
3024 finish_reason,
3025 mtp_drafted: drafted,
3026 mtp_accepted: accepted,
3027 token_confidence: confidence,
3028 traces,
3029 })
3030 }
3031
3032 fn mtp_step(
3036 &mut self,
3037 m: &mut MtpModule,
3038 hidden: &[f32],
3039 next_token: u32,
3040 position: usize,
3041 ) -> u32 {
3042 self.mtp_step_h(m, hidden, next_token, position).0
3043 }
3044
3045 fn chain_probe_note(depth: usize, prefix_ok: bool) {
3049 use std::sync::Mutex;
3050 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
3051 let mut t = T.lock().unwrap();
3052 if t.len() <= depth {
3053 t.resize(depth + 1, (0, 0));
3054 }
3055 t[depth].0 += 1;
3056 t[depth].1 += prefix_ok as u64;
3057 if depth == 0 && t[0].0 % 128 == 0 {
3058 let line: Vec<String> = t
3059 .iter()
3060 .enumerate()
3061 .map(|(d, (n, k))| {
3062 format!(
3063 "d{}={:.0}%({n})",
3064 d + 1,
3065 100.0 * *k as f64 / (*n).max(1) as f64
3066 )
3067 })
3068 .collect();
3069 eprintln!("mtp-chain: {}", line.join(" "));
3070 }
3071 }
3072
3073 fn mtp_step_hl(
3081 &mut self,
3082 m: &mut MtpModule,
3083 hidden: &[f32],
3084 next_token: u32,
3085 position: usize,
3086 ) -> (Vec<f32>, Vec<f32>) {
3087 #[cfg(target_os = "macos")]
3092 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
3093 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
3094 self.mtp_graph_mode = Some(true);
3095 return r;
3096 }
3097 self.mtp_graph_mode = Some(false);
3098 }
3099 #[cfg(feature = "gpu")]
3100 if self.mtp_graph_mode != Some(false) {
3101 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
3102 self.mtp_graph_mode = Some(true);
3103 return r;
3104 }
3105 if self.mtp_graph_mode == Some(true) {
3106 tracing::warn!("mtp graph declined mid-run — draft falls to the per-op path");
3111 }
3112 self.mtp_graph_mode = Some(false);
3113 }
3114 let e = self.embed_single(next_token);
3118 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3119 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3120 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3121 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3122 let mut x = vec![0.0f32; self.hidden_size];
3123 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3124
3125 let lw = &m.layer;
3127 inference::rms_norm_into(
3128 &x,
3129 &lw.input_norm,
3130 self.rms_eps,
3131 self.norm_style,
3132 &mut self.ws.n1,
3133 );
3134 let attn = match &lw.attn {
3135 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3137 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3138 AttnKind::Full {
3139 wq,
3140 wk,
3141 wv,
3142 wo,
3143 q_norm,
3144 k_norm,
3145 output_gate,
3146 softplus_gate,
3147 bias,
3148 } => {
3149 let mut cfg = self.attn_cfg(position);
3150 cfg.q_norm = q_norm.as_deref();
3151 cfg.k_norm = k_norm.as_deref();
3152 cfg.output_gate = *output_gate;
3153 cfg.softplus_gate = softplus_gate
3154 .as_ref()
3155 .map(|(gate, per_head)| (gate, *per_head));
3156 cfg.bias = bias
3157 .as_ref()
3158 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3159 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3160 }
3161 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3162 unreachable!("MTP block is full attention")
3163 }
3164 };
3165 for (i, &a) in attn.iter().enumerate() {
3166 x[i] += a;
3167 }
3168 inference::rms_norm_into(
3169 &x,
3170 &lw.post_norm,
3171 self.rms_eps,
3172 self.norm_style,
3173 &mut self.ws.p1,
3174 );
3175 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3176 for (i, &f) in ffn.iter().enumerate() {
3177 x[i] += f;
3178 }
3179
3180 inference::rms_norm_into(
3181 &x,
3182 &m.final_norm,
3183 self.rms_eps,
3184 self.norm_style,
3185 &mut self.ws.n1,
3186 );
3187 let lg = self.lm_head_forward(&self.ws.n1);
3188 (lg, x)
3189 }
3190
3191 fn mtp_step_h(
3193 &mut self,
3194 m: &mut MtpModule,
3195 hidden: &[f32],
3196 next_token: u32,
3197 position: usize,
3198 ) -> (u32, Vec<f32>) {
3199 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
3200 let draft = sampler::argmax(&lg);
3201 attention::recycle_buf(&mut lg);
3202 (draft, x)
3203 }
3204
3205 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
3211 match trial {
3212 SpecTrial::Spec { t0, gen0, rounds } => {
3213 let rounds = rounds + 1;
3214 if rounds >= 5 {
3215 if mon.plain_ms > 0.0 {
3216 let keep = mon.pays();
3217 mon.fails = 0;
3218 tracing::info!(
3219 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3220 mon.tokens,
3221 mon.round_ms,
3222 mon.plain_ms,
3223 if keep { "speculating" } else { "plain" }
3224 );
3225 SpecTrial::Decided {
3226 spec: keep,
3227 recheck_at: if keep { usize::MAX } else { generated + 128 },
3228 }
3229 } else {
3230 SpecTrial::Plain {
3231 t0: std::time::Instant::now(),
3232 gen0: generated,
3233 }
3234 }
3235 } else {
3236 SpecTrial::Spec { t0, gen0, rounds }
3237 }
3238 }
3239 SpecTrial::Decided { spec: true, .. } => {
3240 if mon.pays() {
3241 mon.fails = 0;
3242 trial
3243 } else {
3244 mon.fails += 1;
3245 if mon.fails >= 4 {
3246 tracing::info!(
3247 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
3248 mon.tokens,
3249 mon.round_ms,
3250 mon.plain_ms
3251 );
3252 SpecTrial::Decided {
3253 spec: false,
3254 recheck_at: generated + 128,
3255 }
3256 } else {
3257 trial
3258 }
3259 }
3260 }
3261 other => other,
3262 }
3263 }
3264
3265 fn mtp_kv_id(&self) -> u64 {
3268 self.graph_kv_id | (1u64 << 40)
3269 }
3270
3271 const MTP_LAYER_BASE: usize = 0;
3276
3277 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
3280 let e = self.embed_single(next_token);
3281 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3282 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3283 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3284 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3285 let mut x = vec![0.0f32; self.hidden_size];
3286 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3287 x
3288 }
3289
3290 #[cfg(feature = "gpu")]
3293 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
3294 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
3295 return false;
3296 }
3297 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3298 || !crate::gpu::enabled_here()
3299 || self.attn_softcap > 0.0
3300 || self.attention_heads_per_layer.is_some()
3301 {
3302 return false;
3303 }
3304 matches!(
3305 &m.layer.attn,
3306 AttnKind::Full {
3307 softplus_gate: None,
3308 ..
3309 }
3310 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
3311 }
3312
3313 #[cfg(feature = "gpu")]
3319 fn mtp_step_graph(
3320 &mut self,
3321 m: &mut MtpModule,
3322 hidden: &[f32],
3323 next_token: u32,
3324 position: usize,
3325 ) -> Option<(Vec<f32>, Vec<f32>)> {
3326 if !self.mtp_graph_ok(m) {
3327 return None;
3328 }
3329 let lw = &m.layer;
3330 let AttnKind::Full {
3331 wq,
3332 wk,
3333 wv,
3334 wo,
3335 q_norm,
3336 k_norm,
3337 output_gate,
3338 softplus_gate,
3339 bias,
3340 } = &lw.attn
3341 else {
3342 return None;
3343 };
3344 if softplus_gate.is_some() {
3345 return None;
3346 }
3347 let FfnKind::Dense(d) = &lw.ffn else {
3348 return None;
3349 };
3350 if !d.segs.is_empty() {
3351 return None; }
3353 let mut x = self.mtp_block_input(m, hidden, next_token);
3356 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3357 let (_, i, kind, rs) = t.graph_weight()?;
3358 Some(crate::gpu::GraphW {
3359 idx: i,
3360 kind,
3361 row_scale: rs,
3362 data: &[],
3363 })
3364 }
3365 let (model, _, _, _) = wq.graph_weight()?;
3366 let model = model.clone();
3367 let (lm_gw, lm_rows) = {
3368 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3369 (
3370 crate::gpu::GraphW {
3371 idx: i,
3372 kind,
3373 row_scale: rs,
3374 data: &[],
3375 },
3376 self.weights.lm_head.rows(),
3377 )
3378 };
3379 let layer = crate::gpu::GraphLayer {
3380 input_norm: &lw.input_norm,
3381 attn: crate::gpu::GraphAttn::Full {
3382 wq: gw(wq)?,
3383 wk: gw(wk)?,
3384 wv: gw(wv)?,
3385 wo: gw(wo)?,
3386 q_norm: q_norm.as_deref(),
3387 k_norm: k_norm.as_deref(),
3388 bias: bias
3389 .as_ref()
3390 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3391 output_gate: *output_gate,
3392 cpu_k: m.kv.k_heads(),
3393 cpu_v: m.kv.v_heads(),
3394 },
3395 post_norm: &lw.post_norm,
3396 ffn: crate::gpu::GraphFfn::Dense {
3397 gate: gw(&d.gate_proj)?,
3398 up: gw(&d.up_proj)?,
3399 down: gw(&d.down_proj)?,
3400 },
3401 };
3402 let nh = self.num_heads;
3403 let (nkv, hd, rd) = self.layer_geom(0);
3404 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3405 let mut logits = Vec::new();
3406 let ok = crate::gpu::forward_token_graph(
3407 &model,
3408 self.mtp_kv_id(),
3409 std::slice::from_ref(&layer),
3410 &[None],
3411 self.o1_epoch,
3412 &self.inv_freq,
3413 &mut x,
3414 nh,
3415 nkv,
3416 hd,
3417 rd,
3418 self.hidden_size,
3419 self.intermediate_size,
3420 position,
3421 self.kv_cache.max_seq_len,
3422 gemma,
3423 self.rms_eps as f32,
3424 Some((&lm_gw, lm_rows)),
3425 &m.final_norm,
3426 &mut logits,
3427 &[],
3428 1,
3429 None,
3430 None,
3431 None,
3432 Self::MTP_LAYER_BASE,
3433 true,
3434 );
3435 if !ok {
3436 return None;
3437 }
3438 logits.resize(self.vocab_size, 0.0);
3439 Some((logits, x))
3440 }
3441
3442 #[cfg(feature = "gpu")]
3449 fn mtp_warm_graph(
3450 &mut self,
3451 m: &mut MtpModule,
3452 pairs: &[(&[f32], u32)],
3453 first_pos: usize,
3454 ) -> bool {
3455 if pairs.is_empty() || !self.mtp_graph_ok(m) {
3456 return pairs.is_empty();
3457 }
3458 let hs = self.hidden_size;
3459 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
3462 for (h, t) in pairs {
3463 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
3464 }
3465 let lw = &m.layer;
3466 let AttnKind::Full {
3467 wq,
3468 wk,
3469 wv,
3470 wo,
3471 q_norm,
3472 k_norm,
3473 output_gate,
3474 bias,
3475 ..
3476 } = &lw.attn
3477 else {
3478 return false;
3479 };
3480 let FfnKind::Dense(d) = &lw.ffn else {
3481 return false;
3482 };
3483 if !d.segs.is_empty() {
3484 return false; }
3486 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3487 let (_, i, kind, rs) = t.graph_weight()?;
3488 Some(crate::gpu::GraphW {
3489 idx: i,
3490 kind,
3491 row_scale: rs,
3492 data: &[],
3493 })
3494 }
3495 let Some((model, _, _, _)) = wq.graph_weight() else {
3496 return false;
3497 };
3498 let model = model.clone();
3499 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
3500 gw(wq),
3501 gw(wk),
3502 gw(wv),
3503 gw(wo),
3504 gw(&d.gate_proj),
3505 gw(&d.up_proj),
3506 gw(&d.down_proj),
3507 ) else {
3508 return false;
3509 };
3510 let layer = crate::gpu::GraphLayer {
3511 input_norm: &lw.input_norm,
3512 attn: crate::gpu::GraphAttn::Full {
3513 wq: gwq,
3514 wk: gwk,
3515 wv: gwv,
3516 wo: gwo,
3517 q_norm: q_norm.as_deref(),
3518 k_norm: k_norm.as_deref(),
3519 bias: bias
3520 .as_ref()
3521 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3522 output_gate: *output_gate,
3523 cpu_k: m.kv.k_heads(),
3524 cpu_v: m.kv.v_heads(),
3525 },
3526 post_norm: &lw.post_norm,
3527 ffn: crate::gpu::GraphFfn::Dense {
3528 gate: gg,
3529 up: gu,
3530 down: gd,
3531 },
3532 };
3533 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
3534 let nh = self.num_heads;
3535 let (nkv, hd, rd) = self.layer_geom(0);
3536 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3537 crate::gpu::forward_batch_graph(
3538 &model,
3539 self.mtp_kv_id(),
3540 std::slice::from_ref(&layer),
3541 &self.inv_freq,
3542 &mut hiddens,
3543 nh,
3544 nkv,
3545 hd,
3546 rd,
3547 hs,
3548 self.intermediate_size,
3549 &positions,
3550 self.kv_cache.max_seq_len,
3551 gemma,
3552 self.rms_eps as f32,
3553 pairs.len(),
3554 None,
3555 )
3556 }
3557
3558 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
3562 let e = self.embed_single(next_token);
3563 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3564 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3565 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3566 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3567 let mut x = vec![0.0f32; self.hidden_size];
3568 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3569 inference::rms_norm_into(
3570 &x,
3571 &m.layer.input_norm,
3572 self.rms_eps,
3573 self.norm_style,
3574 &mut self.ws.n1,
3575 );
3576 let attn = match &m.layer.attn {
3577 AttnKind::Full {
3578 wq,
3579 wk,
3580 wv,
3581 wo,
3582 q_norm,
3583 k_norm,
3584 output_gate,
3585 softplus_gate,
3586 bias,
3587 } => {
3588 let mut cfg = self.attn_cfg(position);
3589 cfg.q_norm = q_norm.as_deref();
3590 cfg.k_norm = k_norm.as_deref();
3591 cfg.output_gate = *output_gate;
3592 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
3593 cfg.bias = bias
3594 .as_ref()
3595 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3596 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3597 }
3598 _ => return,
3599 };
3600 let _ = attn;
3601 }
3602
3603 #[cfg(feature = "gpu")]
3610 #[allow(clippy::too_many_arguments)]
3611 fn graph_spec_step(
3612 &mut self,
3613 m: &mut MtpModule,
3614 hidden: &[f32],
3615 t_next: u32,
3616 next_pos: usize,
3617 drafted: &mut usize,
3618 accepted: &mut usize,
3619 all_ids: &mut Vec<u32>,
3623 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
3624 #[cfg(target_os = "macos")]
3635 let metal_native = crate::gpu::q1_force();
3636 #[cfg(not(target_os = "macos"))]
3637 let metal_native = false;
3638 #[cfg(feature = "gpu")]
3639 let k_default = if metal_native {
3640 7
3643 } else if crate::gpu_wgpu::verify_i8_on() {
3644 5
3645 } else {
3646 4
3647 };
3648 #[cfg(not(feature = "gpu"))]
3649 let k_default = 4;
3650 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
3651 .ok()
3652 .and_then(|v| v.parse().ok())
3653 .filter(|&v| (1..=8).contains(&v))
3654 .unwrap_or(k_default);
3655 if next_pos == 0 {
3656 return None;
3657 }
3658 let t_round = std::time::Instant::now();
3659 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
3675 let sub0 = subs();
3676 let cfg = self.sampler_config.clone();
3681 let penalized = !(cfg.repetition_penalty == 1.0
3682 && cfg.presence_penalty == 0.0
3683 && cfg.suppress_tokens.is_empty());
3684 let greedy_pen = cfg.temperature < 1e-6 && penalized;
3689 let sampling = cfg.temperature >= 1e-6;
3690 let sparse = sampling && sampler::sparse_ok(&cfg);
3696 let base_len = all_ids.len();
3697 if sampling && !sparse && self.spec_q.len() < k_spec {
3698 self.spec_q.resize_with(k_spec, Vec::new);
3699 }
3700 if sparse && self.spec_qs.len() < k_spec {
3701 self.spec_qs.resize_with(k_spec, Vec::new);
3702 }
3703 let mut drafts = Vec::with_capacity(k_spec);
3708 let mut hx = hidden.to_vec();
3709 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
3712 for j in 0..k_spec {
3713 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
3714 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
3715 if spec_dbg {
3716 let saved = self.mtp_graph_mode;
3717 self.mtp_graph_mode = Some(false);
3718 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3719 self.mtp_graph_mode = saved;
3720 m.kv.truncate_last(1);
3721 dbg_ref = Some(r);
3722 }
3723 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3724 if let Some((lg_cpu, h_cpu)) = dbg_ref {
3725 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
3726 let dl = lg.iter().zip(&lg_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3727 let dh = hj.iter().zip(&h_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3728 eprintln!(
3729 "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 {}",
3730 next_pos - 1 + j,
3731 sampler::argmax(&lg_cpu),
3732 sampler::argmax(&lg),
3733 n(&h_cpu),
3734 n(&hj),
3735 m.kv.seq_len
3736 );
3737 }
3738 let dj = if sparse {
3739 let mut q = std::mem::take(&mut self.spec_qs[j]);
3740 let ok = sampler::sparse_distribution_into(
3741 &lg,
3742 &cfg,
3743 all_ids,
3744 &mut self.sampler_scratch,
3745 self.pool.as_deref(),
3746 &mut q,
3747 );
3748 let d = if ok {
3749 sampler::draw_sparse(&q, &mut self.rng)
3750 } else {
3751 let t = sampler::argmax(&lg);
3753 q.clear();
3754 q.push((t, 1.0));
3755 t
3756 };
3757 self.spec_qs[j] = q;
3758 all_ids.push(d);
3759 d
3760 } else if sampling {
3761 let mut q = std::mem::take(&mut self.spec_q[j]);
3762 sampler::distribution_into(
3763 &lg,
3764 &cfg,
3765 all_ids,
3766 &mut self.sampler_scratch,
3767 self.pool.as_deref(),
3768 &mut q,
3769 );
3770 let d = sampler::draw(&q, &mut self.rng);
3771 self.spec_q[j] = q;
3772 all_ids.push(d); d
3774 } else if greedy_pen {
3775 let d = sampler::argmax_penalized(
3776 &lg,
3777 &cfg,
3778 all_ids,
3779 &mut self.sampler_scratch,
3780 self.pool.as_deref(),
3781 );
3782 all_ids.push(d);
3783 d
3784 } else {
3785 sampler::argmax(&lg)
3786 };
3787 attention::recycle_buf(&mut lg);
3788 drafts.push(dj);
3789 hx = hj;
3790 }
3791 all_ids.truncate(base_len);
3792 *drafted += k_spec;
3793 let t_draft = t_round.elapsed();
3794 let sub_draft = subs();
3795 let b = k_spec + 1;
3798 let mut hiddens = vec![0.0f32; b * self.hidden_size];
3799 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
3800 let e = self.embed_single(t);
3801 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
3802 }
3803 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
3804 let (lm_gw, lm_rows) = {
3805 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3806 (
3807 crate::gpu::GraphW {
3808 idx: i,
3809 kind,
3810 row_scale: rs,
3811 data: &[],
3812 },
3813 self.weights.lm_head.rows(),
3814 )
3815 };
3816 let mut logits = Vec::new();
3817 let final_norm = self.weights.final_norm.clone();
3818 #[cfg(target_os = "macos")]
3819 let ok = if metal_native {
3820 let lm = self.weights.lm_head.q1_parts()?;
3821 self.try_batch_graph_metal(&mut hiddens, &positions, b, Some((lm, &final_norm, &mut logits)))
3822 } else {
3823 self.try_batch_graph_wgpu(
3824 &mut hiddens,
3825 &positions,
3826 b,
3827 Some(crate::gpu::SpecTail {
3828 lm: lm_gw,
3829 lm_rows,
3830 final_norm: &final_norm,
3831 logits_out: &mut logits,
3832 }),
3833 )
3834 };
3835 #[cfg(not(target_os = "macos"))]
3836 let ok = self.try_batch_graph_wgpu(
3837 &mut hiddens,
3838 &positions,
3839 b,
3840 Some(crate::gpu::SpecTail {
3841 lm: lm_gw,
3842 lm_rows,
3843 final_norm: &final_norm,
3844 logits_out: &mut logits,
3845 }),
3846 );
3847 if !ok {
3848 m.kv.truncate_last(k_spec);
3851 return None;
3852 }
3853 #[cfg(target_os = "macos")]
3859 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
3860 let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3861 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
3862 let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
3863 let want_save = self.graph_want_logits;
3864 self.graph_want_logits = false;
3865 for (i, &t) in toks.iter().enumerate() {
3866 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
3867 let _ = self.graph_logits.take();
3868 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
3872 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
3873 }
3874 let ref_lg = self.logits_from_hidden(&hi);
3875 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
3876 let ra = sampler::argmax(&ref_lg);
3877 let va = sampler::argmax(row);
3878 let mut md = 0f32;
3879 let mut rms = 0f64;
3880 for j in 0..lm_rows.min(ref_lg.len()) {
3881 let d = (ref_lg[j] - row[j]).abs();
3882 md = md.max(d);
3883 rms += (d as f64) * (d as f64);
3884 }
3885 let mut hd = 0f32;
3886 for j in 0..self.hidden_size {
3887 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
3888 }
3889 eprintln!(
3890 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
3891 next_pos + i,
3892 if ra == va { "OK" } else { "MISMATCH" },
3893 (rms / lm_rows as f64).sqrt()
3894 );
3895 }
3896 self.graph_want_logits = want_save;
3897 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
3900 if l.linear_state.len() == st.len() {
3901 l.linear_state.copy_from_slice(&st);
3902 } else {
3903 l.linear_state = st;
3904 }
3905 }
3906 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
3907 let extra = l.seq_len.saturating_sub(n0);
3908 if extra > 0 {
3909 l.truncate_last(extra);
3910 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
3911 }
3912 }
3913 }
3914 let t_verify = t_round.elapsed();
3915 let sub_verify = subs();
3916 let mut a = 0usize;
3921 let mut forced: Option<u32> = None;
3922 let ids: Vec<u32> = if sparse {
3923 let mut p = std::mem::take(&mut self.spec_ps);
3924 let mut res = std::mem::take(&mut self.spec_ress);
3925 while a < k_spec {
3926 let ok = sampler::sparse_distribution_into(
3927 &logits[a * lm_rows..(a + 1) * lm_rows],
3928 &cfg,
3929 all_ids,
3930 &mut self.sampler_scratch,
3931 self.pool.as_deref(),
3932 &mut p,
3933 );
3934 if !ok {
3935 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
3936 p.clear();
3937 p.push((t, 1.0));
3938 }
3939 match sampler::spec_accept_or_correct_sparse(
3940 &p,
3941 &self.spec_qs[a],
3942 drafts[a],
3943 &mut self.rng,
3944 &mut res,
3945 ) {
3946 None => {
3947 all_ids.push(drafts[a]);
3948 a += 1;
3949 }
3950 Some(c) => {
3951 forced = Some(c);
3952 break;
3953 }
3954 }
3955 }
3956 all_ids.truncate(base_len);
3957 self.spec_ps = p;
3958 self.spec_ress = res;
3959 drafts.clone()
3960 } else if sampling {
3961 let mut p = std::mem::take(&mut self.spec_p);
3962 let mut res = std::mem::take(&mut self.spec_res);
3963 while a < k_spec {
3964 sampler::distribution_into(
3965 &logits[a * lm_rows..(a + 1) * lm_rows],
3966 &cfg,
3967 all_ids,
3968 &mut self.sampler_scratch,
3969 self.pool.as_deref(),
3970 &mut p,
3971 );
3972 match sampler::spec_accept_or_correct(
3973 &p,
3974 &self.spec_q[a],
3975 drafts[a],
3976 &mut self.rng,
3977 &mut res,
3978 self.pool.as_deref(),
3979 ) {
3980 None => {
3981 all_ids.push(drafts[a]);
3982 a += 1;
3983 }
3984 Some(c) => {
3985 forced = Some(c);
3986 break;
3987 }
3988 }
3989 }
3990 all_ids.truncate(base_len);
3991 self.spec_p = p;
3992 self.spec_res = res;
3993 drafts.clone()
3995 } else if greedy_pen {
3996 let mut ids: Vec<u32> = Vec::with_capacity(b);
4000 for i in 0..b {
4001 let t = sampler::argmax_penalized(
4002 &logits[i * lm_rows..(i + 1) * lm_rows],
4003 &cfg,
4004 all_ids,
4005 &mut self.sampler_scratch,
4006 self.pool.as_deref(),
4007 );
4008 ids.push(t);
4009 if i < k_spec && t == drafts[i] {
4010 all_ids.push(t);
4011 } else {
4012 break;
4013 }
4014 }
4015 all_ids.truncate(base_len);
4016 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
4017 a += 1;
4018 }
4019 ids
4022 } else {
4023 let ids: Vec<u32> = (0..b)
4024 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
4025 .collect();
4026 while a < k_spec && ids[a] == drafts[a] {
4027 a += 1;
4028 }
4029 ids
4030 };
4031 if spec_dbg {
4032 eprintln!("spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}", drafts, ids);
4033 }
4034 #[cfg(target_os = "macos")]
4038 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
4039 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
4040 {
4041 let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
4042 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
4043 let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
4044 let want_save = self.graph_want_logits;
4045 self.graph_want_logits = false;
4046 for (i, &t) in toks.iter().take(a + 1).enumerate() {
4047 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
4048 let _ = self.graph_logits.take();
4049 }
4050 self.graph_want_logits = want_save;
4051 let plain_states: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
4052 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4053 let mut rows = Vec::new();
4054 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens.iter()).enumerate() {
4055 let extra = l.seq_len.saturating_sub(*n0);
4056 if extra > 0 {
4057 let mut kk = Vec::new();
4058 let mut vv = Vec::new();
4059 for g in 0..nkv {
4060 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4061 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4062 }
4063 rows.push((li, kk, vv));
4064 l.truncate_last(extra);
4065 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
4066 }
4067 }
4068 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
4069 if l.linear_state.len() == st.len() {
4070 l.linear_state.copy_from_slice(&st);
4071 } else {
4072 l.linear_state = st;
4073 }
4074 }
4075 Some((plain_states, rows))
4076 } else {
4077 None
4078 };
4079 #[cfg(target_os = "macos")]
4081 if metal_native {
4082 self.metal_verify_commit(a);
4085 if let Some((plain_states, rows)) = commit_ref {
4086 crate::gpu_metal::queue_fence();
4087 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
4088 let mut worst_s = 0f32;
4089 let mut worst_li = 0usize;
4090 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
4091 if l.linear_state.len() != ps.len() || ps.is_empty() {
4092 continue;
4093 }
4094 let d = l.linear_state.iter().zip(ps).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4095 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
4096 let rel = d / n.max(1e-6);
4097 if rel > worst_s {
4098 worst_s = rel;
4099 worst_li = li;
4100 }
4101 }
4102 let mut worst_k = 0f32;
4103 for (li, kk, vv) in &rows {
4104 let l = &self.kv_cache.layers[*li];
4105 let n0 = l.seq_len - (kk.len() / (nkv * hd));
4106 let mut ck = Vec::new();
4107 let mut cv = Vec::new();
4108 for g in 0..nkv {
4109 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
4110 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
4111 }
4112 if ck.len() == kk.len() {
4113 let dk = ck.iter().zip(kk).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4114 let dv = cv.iter().zip(vv).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
4115 worst_k = worst_k.max(dk).max(dv);
4116 } else {
4117 eprintln!("commit-check L{li}: kv row count mismatch {} vs {}", ck.len(), kk.len());
4118 }
4119 }
4120 eprintln!(
4121 "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}"
4122 );
4123 }
4124 } else if a + 1 < b {
4125 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
4126 }
4127 #[cfg(not(target_os = "macos"))]
4128 if a + 1 < b {
4129 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
4130 }
4131 *accepted += a;
4132 m.kv.truncate_last(k_spec.saturating_sub(1));
4143 #[cfg(target_os = "macos")]
4144 if metal_native && self.mtp_graph_mode == Some(true) {
4145 crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, m.kv.seq_len);
4148 }
4149 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
4150 if !warm_off && a > 0 {
4151 let mut warmed = false;
4154 #[cfg(target_os = "macos")]
4155 if metal_native && self.mtp_graph_mode == Some(true) {
4156 let pairs: Vec<(&[f32], u32)> = (0..a)
4160 .map(|j| (&hiddens[j * self.hidden_size..(j + 1) * self.hidden_size], ids[j]))
4161 .collect();
4162 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
4163 if !warmed {
4164 warmed = true;
4165 for j in 0..a {
4166 let row = hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
4167 if self.mtp_step_metal(m, &row, ids[j], next_pos + j, false).is_none() {
4168 warmed = false;
4169 break;
4170 }
4171 }
4172 }
4173 }
4174 if !warmed && self.mtp_graph_mode == Some(true) && !metal_native {
4175 let rows: Vec<Vec<f32>> = (0..a)
4176 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
4177 .collect();
4178 let pairs: Vec<(&[f32], u32)> = rows
4179 .iter()
4180 .zip(ids.iter())
4181 .map(|(r, &t)| (r.as_slice(), t))
4182 .collect();
4183 warmed = self.mtp_warm_graph(m, &pairs, next_pos);
4184 if !warmed {
4185 warmed = true;
4187 for j in 0..a {
4188 if self
4189 .mtp_step_graph(m, &rows[j], ids[j], next_pos + j)
4190 .is_none()
4191 {
4192 warmed = false;
4193 break;
4194 }
4195 }
4196 }
4197 }
4198 if !warmed {
4199 for j in 0..a {
4200 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
4201 let row = row.to_vec();
4202 self.mtp_warm(m, &row, ids[j], next_pos + j);
4203 }
4204 }
4205 }
4206 if let Some(c) = forced {
4210 self.spec_forced = Some(c);
4211 self.graph_logits = None;
4212 } else {
4213 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
4214 row.resize(self.vocab_size, 0.0);
4215 if let Some(c) = self.final_softcap {
4216 for l in row.iter_mut() {
4217 *l = c * (*l / c).tanh();
4218 }
4219 }
4220 self.graph_logits = Some(row);
4221 }
4222 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
4223 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4229 let end = subs();
4230 eprintln!(
4231 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
4232 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
4233 t_draft.as_secs_f64() * 1e3,
4234 sub_draft - sub0,
4235 (t_verify - t_draft).as_secs_f64() * 1e3,
4236 sub_verify - sub_draft,
4237 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
4238 end - sub_verify,
4239 );
4240 }
4241 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
4242 }
4243
4244 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
4253 if !self.pair_supported() {
4254 return (0.0, 0.0);
4255 }
4256 let emb1 = self.embed_single(1);
4257 let emb2 = self.embed_single(2);
4258 let pos = self.kv_cache.seq_len();
4259
4260 let t0 = std::time::Instant::now();
4261 for _ in 0..iters {
4262 let _ = self.forward_layers(&emb1, pos, None);
4263 let _ = self.forward_layers(&emb2, pos + 1, None);
4264 for l in &mut self.kv_cache.layers {
4265 l.truncate_last(2);
4266 }
4267 }
4268 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4269
4270 let t1 = std::time::Instant::now();
4271 for _ in 0..iters {
4272 let _ = self.forward_pair(&emb1, &emb2, pos);
4273 for l in &mut self.kv_cache.layers {
4274 l.truncate_last(2);
4275 }
4276 }
4277 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4278 (singles_ms, pair_ms)
4279 }
4280
4281 fn pair_supported(&self) -> bool {
4289 !self.weights.layers.is_empty()
4296 && self.g3n.is_none()
4297 && !self
4298 .weights
4299 .layers
4300 .iter()
4301 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
4302 }
4303
4304 fn forward_pair(
4305 &mut self,
4306 emb1: &[f32],
4307 emb2: &[f32],
4308 position: usize,
4309 ) -> (Vec<f32>, Vec<f32>) {
4310 let mut h1 = emb1.to_vec();
4311 let mut h2 = emb2.to_vec();
4312 let (_nkv, _hd, hs, _rd, eps) = (
4313 self.num_kv_heads,
4314 self.head_dim,
4315 self.hidden_size,
4316 self.rotary_dim,
4317 self.rms_eps,
4318 );
4319 let pool = self.pool.clone();
4320
4321 for li in 0..self.num_layers {
4322 let lw = &self.weights.layers[self.phys_layer(li)];
4323 inference::rms_norm_into(
4326 &h1,
4327 &lw.input_norm,
4328 self.rms_eps,
4329 self.norm_style,
4330 &mut self.ws.n1,
4331 );
4332 inference::rms_norm_into(
4333 &h2,
4334 &lw.input_norm,
4335 self.rms_eps,
4336 self.norm_style,
4337 &mut self.ws.n2,
4338 );
4339
4340 let (a1, a2) = match &lw.attn {
4341 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4342 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4343 AttnKind::Linear(w) => {
4344 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
4345 let layer = &mut self.kv_cache.layers[li];
4346 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4347 vmf_phase_pair(
4348 &self.ws.n1,
4349 &self.ws.n2,
4350 w,
4351 &cfg,
4352 state,
4353 scratch,
4354 self.pool.as_deref(),
4355 )
4356 }
4357 AttnKind::LinearGdn(w) => {
4358 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4359 let layer = &mut self.kv_cache.layers[li];
4360 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4361 gdn_pair(
4362 &self.ws.n1,
4363 &self.ws.n2,
4364 w,
4365 &cfg,
4366 state,
4367 scratch,
4368 self.pool.as_deref(),
4369 )
4370 }
4371 AttnKind::ShortConv(w) => {
4372 let cfg = self
4373 .short_conv_cfg
4374 .expect("short-conv layer without short_conv_cfg");
4375 let layer = &mut self.kv_cache.layers[li];
4376 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4377 short_conv_pair(
4378 &self.ws.n1,
4379 &self.ws.n2,
4380 w,
4381 &cfg,
4382 state,
4383 scratch,
4384 self.pool.as_deref(),
4385 )
4386 }
4387 AttnKind::Full {
4388 wq,
4389 wk,
4390 wv,
4391 wo,
4392 q_norm,
4393 k_norm,
4394 output_gate,
4395 softplus_gate,
4396 bias,
4397 } => {
4398 let inv_freq_l = self.layer_inv_freq(li);
4399 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4400 let cfg = QwenAttnCfg {
4401 num_heads: self.layer_num_heads(li),
4402 num_kv_heads: nkv_l,
4403 head_dim: hd_l,
4404 hidden_size: hs,
4405 position,
4406 inv_freq: &inv_freq_l,
4407 rotary_dim: rd_l,
4408 scale: self.attn_scale,
4409 softcap: self.attn_softcap,
4410 window: self.layer_window(li),
4411 v_norm: self.attn_v_norm,
4412 q_norm: q_norm.as_deref(),
4413 k_norm: k_norm.as_deref(),
4414 output_gate: *output_gate,
4415 softplus_gate: softplus_gate
4416 .as_ref()
4417 .map(|(gate, per_head)| (gate, *per_head)),
4418 rope_scale: self.layer_rope_scale(li),
4419 bias: bias
4420 .as_ref()
4421 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4422 rms_eps: eps,
4423 norm_style: self.norm_style,
4424 pool: pool.as_deref(),
4425 };
4426 attention::qwen_attention_pair(
4427 &self.ws.n1,
4428 &self.ws.n2,
4429 wq,
4430 wk,
4431 wv,
4432 wo,
4433 &mut self.kv_cache.layers[li],
4434 &cfg,
4435 )
4436 }
4437 };
4438 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
4439 Some(w) => (
4440 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
4441 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
4442 ),
4443 None => (a1, a2),
4444 };
4445 for i in 0..self.hidden_size {
4446 h1[i] += a1[i];
4447 h2[i] += a2[i];
4448 }
4449 let (mut a1, mut a2) = (a1, a2);
4450 attention::recycle_buf(&mut a1);
4451 attention::recycle_buf(&mut a2);
4452
4453 let lw = &self.weights.layers[self.phys_layer(li)];
4454 inference::rms_norm_into(
4455 &h1,
4456 &lw.post_norm,
4457 self.rms_eps,
4458 self.norm_style,
4459 &mut self.ws.p1,
4460 );
4461 inference::rms_norm_into(
4462 &h2,
4463 &lw.post_norm,
4464 self.rms_eps,
4465 self.norm_style,
4466 &mut self.ws.p2,
4467 );
4468 let (f1, f2) = match &lw.ffn {
4469 FfnKind::DenseMoe(dm) => (
4472 dense_moe_ffn(
4473 dm,
4474 &self.ws.p1,
4475 &h1,
4476 self.rms_eps,
4477 self.norm_style,
4478 self.pool.as_deref(),
4479 ),
4480 dense_moe_ffn(
4481 dm,
4482 &self.ws.p2,
4483 &h2,
4484 self.rms_eps,
4485 self.norm_style,
4486 self.pool.as_deref(),
4487 ),
4488 ),
4489 _ => ffn_forward_pair(
4490 &lw.ffn,
4491 &self.ws.p1,
4492 &self.ws.p2,
4493 self.pool.as_deref(),
4494 None,
4495 ),
4496 };
4497 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4498 Some(w) => (
4499 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
4500 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
4501 ),
4502 None => (f1, f2),
4503 };
4504 for i in 0..self.hidden_size {
4505 h1[i] += f1[i];
4506 h2[i] += f2[i];
4507 }
4508 let (mut f1, mut f2) = (f1, f2);
4509 attention::recycle_buf(&mut f1);
4510 attention::recycle_buf(&mut f2);
4511 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4512 for i in 0..self.hidden_size {
4513 h1[i] *= sc;
4514 h2[i] *= sc;
4515 }
4516 }
4517 if self.is_loop_end(li) && li + 1 < self.num_layers {
4519 h1 = inference::rms_norm(
4520 &h1,
4521 &self.weights.final_norm,
4522 self.rms_eps,
4523 self.norm_style,
4524 );
4525 h2 = inference::rms_norm(
4526 &h2,
4527 &self.weights.final_norm,
4528 self.rms_eps,
4529 self.norm_style,
4530 );
4531 }
4532 }
4533 (h1, h2)
4534 }
4535
4536 fn commit_linear_scratch(&mut self) {
4538 for layer in &mut self.kv_cache.layers {
4539 if !layer.linear_scratch.is_empty() {
4540 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
4541 layer.linear_scratch.clear();
4542 }
4543 }
4544 }
4545
4546 pub fn forward_ids(
4549 &mut self,
4550 ids: &[u32],
4551 task_mask: Option<&TaskMask>,
4552 ) -> Result<Vec<f32>, String> {
4553 if ids.is_empty() {
4554 return Err("empty id sequence".to_string());
4555 }
4556 self.kv_cache.clear();
4557 self.kv_history.clear();
4558 self.o1_begin();
4559 let mut hidden = vec![0.0f32; self.hidden_size];
4560 let mut pos = 0usize;
4561 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
4569 let chunk = prefill_chunk();
4573 let hs = self.hidden_size;
4574 while pos < ids.len() {
4575 let end = (pos + chunk).min(ids.len());
4576 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4577 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4578 pos = end;
4579 }
4580 }
4581 if task_mask.is_none()
4590 && !self.graph_prefill_preferred()
4591 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
4592 && self.pair_supported()
4593 {
4594 while pos + 1 < ids.len() {
4595 let e1 = self.embed_single(ids[pos]);
4596 let e2 = self.embed_single(ids[pos + 1]);
4597 let (_, h2) = self.forward_pair(&e1, &e2, pos);
4598 self.commit_linear_scratch();
4599 hidden = h2;
4600 pos += 2;
4601 }
4602 }
4603 while pos < ids.len() {
4604 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4605 pos += 1;
4606 }
4607 self.o1_seal();
4611 let normed = inference::rms_norm(
4612 &hidden,
4613 &self.weights.final_norm,
4614 self.rms_eps,
4615 self.norm_style,
4616 );
4617 Ok(self.lm_head_forward(&normed))
4618 }
4619
4620 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
4627 let (nll, cnt) = self.nll_ids_from(ids, 0);
4628 (nll / cnt.max(1) as f64).exp()
4629 }
4630
4631 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4636 self.kv_cache.clear();
4637 self.kv_history.clear();
4638 FFN_PROBE.with(|p| {
4639 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4640 });
4641 crate::gpu::cpu_scope(|| {
4642 for (pos, &id) in ids.iter().enumerate() {
4643 let emb = self.embed_single(id);
4644 let _ = self.forward_layers(&emb, pos, None);
4645 }
4646 });
4647 self.kv_cache.clear();
4648 self.kv_history.clear();
4649 FFN_PROBE
4650 .with(|p| p.borrow_mut().take())
4651 .unwrap_or_default()
4652 }
4653
4654 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4658 self.kv_cache.clear();
4659 self.kv_history.clear();
4660 FFN_PROBE.with(|p| {
4661 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4662 });
4663 for chunk in ids.chunks(256) {
4664 if chunk.len() < 2 {
4665 continue;
4666 }
4667 let _ = self.nll_ids_masked(chunk, 0, None);
4668 }
4669 self.kv_cache.clear();
4670 self.kv_history.clear();
4671 FFN_PROBE
4672 .with(|p| p.borrow_mut().take())
4673 .unwrap_or_default()
4674 }
4675
4676 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
4680 self.kv_cache.clear();
4681 self.kv_history.clear();
4682 let mut nll = 0f64;
4683 let mut cnt = 0usize;
4684 let mut hidden = vec![0f32; self.hidden_size];
4685 for (pos, &id) in ids.iter().enumerate() {
4686 if pos > 0 {
4687 inference::rms_norm_into(
4688 &hidden,
4689 &self.weights.final_norm,
4690 self.rms_eps,
4691 self.norm_style,
4692 &mut self.ws.n1,
4693 );
4694 let mut logits = self.lm_head_forward(&self.ws.n1);
4695 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4696 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
4697 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
4698 nll -= p.max(1e-300).ln();
4699 cnt += 1;
4700 attention::recycle_buf(&mut logits);
4701 }
4702 let emb = self.embed_single(id);
4703 hidden = self.forward_layers(&emb, pos, Some(mask));
4704 }
4705 self.kv_cache.clear();
4706 self.kv_history.clear();
4707 (nll / cnt.max(1) as f64).exp()
4708 }
4709
4710 pub fn nll_ids_masked(
4729 &mut self,
4730 ids: &[u32],
4731 start: usize,
4732 task_mask: Option<&TaskMask>,
4733 ) -> (f64, usize) {
4734 let task_mask = self.drop_open_mask(task_mask);
4735 self.nll_ids_inner(ids, start, task_mask)
4736 }
4737
4738 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
4739 self.nll_ids_inner(ids, start, None)
4740 }
4741
4742 fn nll_ids_inner(
4743 &mut self,
4744 ids: &[u32],
4745 start: usize,
4746 task_mask: Option<&TaskMask>,
4747 ) -> (f64, usize) {
4748 self.kv_cache.clear();
4749 self.kv_history.clear();
4750 let mut nll = 0f64;
4751 let mut cnt = 0usize;
4752 if self.can_prefill_batched() {
4753 const CHUNK: usize = 128;
4759 const LM_SUB: usize = 32;
4760 let n = ids.len().saturating_sub(1);
4761 let hs = self.hidden_size;
4762 let rows = self.weights.lm_head.rows();
4763 let mut pos = 0usize;
4764 while pos < n {
4765 let end = (pos + CHUNK).min(n);
4766 let bsz = end - pos;
4767 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4768 let mut k0 = 0usize;
4769 while k0 < bsz {
4770 let k1 = (k0 + LM_SUB).min(bsz);
4771 let sb = k1 - k0;
4772 if pos + k1 <= start {
4775 k0 = k1;
4776 continue;
4777 }
4778 let mut normed = vec![0.0f32; sb * hs];
4779 for k in 0..sb {
4780 let r = inference::rms_norm(
4781 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
4782 &self.weights.final_norm,
4783 self.rms_eps,
4784 self.norm_style,
4785 );
4786 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
4787 }
4788 let mut logits = vec![0.0f32; sb * rows];
4789 self.weights
4790 .lm_head
4791 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
4792 for k in 0..sb {
4793 if pos + k0 + k < start {
4794 continue;
4795 }
4796 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
4797 if let Some(mu) = self.logit_multiplier {
4798 for v in lg.iter_mut() {
4799 *v *= mu;
4800 }
4801 }
4802 if let Some(c) = self.final_softcap {
4806 for v in lg.iter_mut() {
4807 *v = c * (*v / c).tanh();
4808 }
4809 }
4810 if let Some(cm) = self.head_clusters.clone() {
4813 self.hierarchical_head_logprobs(&normed[k * hs..(k + 1) * hs], &cm, lg);
4814 }
4815 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
4816 let target = ids[pos + k0 + k + 1] as usize;
4817 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4818 let lse: f64 = lg
4819 .iter()
4820 .map(|&v| ((v - max) as f64).exp())
4821 .sum::<f64>()
4822 .ln()
4823 + max as f64;
4824 nll += lse - lg[target] as f64;
4825 cnt += 1;
4826 if std::env::var("CMF_PPL_TRACE").is_ok() {
4827 let top = lg
4828 .iter()
4829 .enumerate()
4830 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4831 .map(|(i, _)| i)
4832 .unwrap_or(0);
4833 eprintln!(
4834 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
4835 pos + k0 + k,
4836 target,
4837 lse - lg[target] as f64,
4838 top,
4839 lg[target],
4840 lg[top]
4841 );
4842 }
4843 }
4844 k0 = k1;
4845 }
4846 pos = end;
4847 }
4848 self.kv_cache.clear();
4849 self.kv_history.clear();
4850 return (nll, cnt);
4851 }
4852 for pos in 0..ids.len().saturating_sub(1) {
4853 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4854 let out_of_band = self.graph_logits.take();
4862 if pos < start {
4863 continue;
4864 }
4865 let logits = match out_of_band {
4866 Some(lg) => lg,
4867 None => {
4868 let normed = inference::rms_norm(
4869 &hidden,
4870 &self.weights.final_norm,
4871 self.rms_eps,
4872 self.norm_style,
4873 );
4874 self.lm_head_forward(&normed)
4878 }
4879 };
4880 let target = ids[pos + 1] as usize;
4881 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4882 let lse: f64 = logits
4883 .iter()
4884 .map(|&v| ((v - max) as f64).exp())
4885 .sum::<f64>()
4886 .ln()
4887 + max as f64;
4888 let tok_nll = lse - logits[target] as f64;
4889 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4890 let top = logits
4891 .iter()
4892 .enumerate()
4893 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4894 .map(|(i, _)| i)
4895 .unwrap_or(0);
4896 eprintln!(
4897 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4898 logits[target], logits[top]
4899 );
4900 }
4901 nll += tok_nll;
4902 cnt += 1;
4903 }
4904 self.kv_cache.clear();
4905 self.kv_history.clear();
4906 (nll, cnt)
4907 }
4908
4909 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
4925 self.kv_cache.clear();
4926 self.kv_history.clear();
4927 self.o1_begin();
4928 let n = ids.len().saturating_sub(1);
4929 let p = prefill.min(n);
4930 let mut pos = 0usize;
4932 if self.can_prefill_batched() {
4933 const CHUNK: usize = 128;
4934 while pos < p {
4935 let end = (pos + CHUNK).min(p);
4936 let _ = self.prefill_batch(&ids[pos..end], pos);
4937 pos = end;
4938 }
4939 } else {
4940 while pos < p {
4941 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4942 pos += 1;
4943 }
4944 }
4945 self.o1_seal();
4946
4947 let mut nll = 0f64;
4948 let mut cnt = 0usize;
4949 for pos in p..n {
4950 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4951 let normed = inference::rms_norm(
4952 &hidden,
4953 &self.weights.final_norm,
4954 self.rms_eps,
4955 self.norm_style,
4956 );
4957 let logits = self.lm_head_forward(&normed);
4961 let target = ids[pos + 1] as usize;
4962 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4963 let lse: f64 = logits
4964 .iter()
4965 .map(|&v| ((v - max) as f64).exp())
4966 .sum::<f64>()
4967 .ln()
4968 + max as f64;
4969 let tok_nll = lse - logits[target] as f64;
4970 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4971 let top = logits
4972 .iter()
4973 .enumerate()
4974 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4975 .map(|(i, _)| i)
4976 .unwrap_or(0);
4977 eprintln!(
4978 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4979 logits[target], logits[top]
4980 );
4981 }
4982 nll += tok_nll;
4983 cnt += 1;
4984 }
4985 self.kv_cache.clear();
4986 self.kv_history.clear();
4987 (nll, cnt)
4988 }
4989
4990 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
4998 self.kv_cache.clear();
4999 self.kv_history.clear();
5000 let n = ids.len().saturating_sub(1);
5001 let mut correct = Vec::with_capacity(n);
5002 let mut pmax = Vec::with_capacity(n);
5003 for pos in 0..n {
5004 let emb = self.embed_single(ids[pos]);
5005 let hidden = self.forward_layers(&emb, pos, None);
5006 let normed = inference::rms_norm(
5007 &hidden,
5008 &self.weights.final_norm,
5009 self.rms_eps,
5010 self.norm_style,
5011 );
5012 let logits = self.lm_head_forward(&normed);
5016 let target = ids[pos + 1] as usize;
5017 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
5018 for (i, &v) in logits.iter().enumerate() {
5019 if v > mval {
5020 mval = v;
5021 amax = i;
5022 }
5023 }
5024 correct.push(amax == target);
5025 let row: Vec<f32> = temps
5026 .iter()
5027 .map(|&t| {
5028 let tt = t.max(1e-3);
5029 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
5030 1.0 / s.max(1e-12) })
5032 .collect();
5033 pmax.push(row);
5034 }
5035 self.kv_cache.clear();
5036 self.kv_history.clear();
5037 (correct, pmax)
5038 }
5039
5040 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
5047 let mut router = match self.dyn_router.take() {
5048 Some(r) => r,
5049 None => return (self.ppl_ids(ids), 0),
5050 };
5051 router.reset();
5052 self.dyn_phi_seen = 0;
5053 let _ = self.set_active_skill(None);
5054
5055 self.kv_cache.clear();
5056
5057 self.kv_history.clear();
5058 let mut nll = 0f64;
5059 let mut cnt = 0usize;
5060 for pos in 0..ids.len().saturating_sub(1) {
5061 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
5062 let normed = inference::rms_norm(
5063 &hidden,
5064 &self.weights.final_norm,
5065 self.rms_eps,
5066 self.norm_style,
5067 );
5068 let logits = self.lm_head_forward(&normed);
5072 let target = ids[pos + 1] as usize;
5073 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
5074 let lse: f64 = logits
5075 .iter()
5076 .map(|&v| ((v - max) as f64).exp())
5077 .sum::<f64>()
5078 .ln()
5079 + max as f64;
5080 let tok_nll = lse - logits[target] as f64;
5081 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
5082 let top = logits
5083 .iter()
5084 .enumerate()
5085 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5086 .map(|(i, _)| i)
5087 .unwrap_or(0);
5088 eprintln!(
5089 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
5090 logits[target], logits[top]
5091 );
5092 }
5093 nll += tok_nll;
5094 cnt += 1;
5095 let phi = self.dyn_phi_ema.clone();
5097 if let Some(new_active) = router.step(&phi, pos) {
5098 let _ = self.set_active_skill(new_active);
5099 }
5100 }
5101 let switches = router.switches.len();
5102 let _ = self.set_active_skill(None);
5103 self.dyn_router = Some(router);
5104 self.kv_cache.clear();
5105 self.kv_history.clear();
5106 ((nll / cnt.max(1) as f64).exp(), switches)
5107 }
5108
5109 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
5111 self.kv_cache.clear();
5112 self.kv_history.clear();
5113 let mut acc = vec![0f32; self.hidden_size];
5114 for (pos, &id) in ids.iter().enumerate() {
5115 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
5116 for (a, v) in acc.iter_mut().zip(&h) {
5117 *a += v;
5118 }
5119 }
5120 let n = ids.len().max(1) as f32;
5121 for a in acc.iter_mut() {
5122 *a /= n;
5123 }
5124 self.kv_cache.clear();
5125 self.kv_history.clear();
5126 acc
5127 }
5128
5129 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
5135 self.prefill_batch_masked(ids, start_pos, None)
5136 }
5137
5138 fn prefill_batch_masked(
5144 &mut self,
5145 ids: &[u32],
5146 start_pos: usize,
5147 task_mask: Option<&TaskMask>,
5148 ) -> Vec<f32> {
5149 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
5150 }
5151
5152 fn prefill_batch_span(
5158 &mut self,
5159 input: PrefillIn<'_>,
5160 start_pos: usize,
5161 task_mask: Option<&TaskMask>,
5162 from: usize,
5163 upto_excl: usize,
5164 ) -> Vec<f32> {
5165 let hs = self.hidden_size;
5166 let b = match input {
5167 PrefillIn::Ids(ids) => ids.len(),
5168 PrefillIn::Hidden(hb) => hb.len() / hs,
5169 };
5170 let upto_excl = upto_excl.min(self.num_layers);
5171 let mut h: Vec<f32>;
5175 let mut h_ready;
5176 match input {
5177 PrefillIn::Ids(_) => {
5178 h = vec![0.0; b * hs];
5179 h_ready = false;
5180 }
5181 PrefillIn::Hidden(hb) => {
5182 h = hb.to_vec();
5183 h_ready = true;
5184 }
5185 }
5186 let fill_h = |h: &mut Vec<f32>, me: &Self| {
5187 if let PrefillIn::Ids(ids) = input {
5188 for (bi, &id) in ids.iter().enumerate() {
5189 let e = me.embed_single(id);
5190 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
5191 }
5192 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5193 if let Ok(t) = tp.parse::<usize>() {
5194 if t >= start_pos && t < start_pos + ids.len() {
5195 let bi = t - start_pos;
5196 let row = &h[bi * hs..(bi + 1) * hs];
5197 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5198 eprintln!(
5199 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
5200 ids[bi], row[0], row[1], ids.len(), &ids[..ids.len().min(8)]
5201 );
5202 }
5203 }
5204 }
5205 }
5206 };
5207 let (_nkv, _hd, _rd, eps) = (
5208 self.num_kv_heads,
5209 self.head_dim,
5210 self.rotary_dim,
5211 self.rms_eps,
5212 );
5213 let pool = self.pool.clone();
5214 let norm_style = self.norm_style;
5215
5216 #[cfg(target_os = "macos")]
5217 let mut chunk_skip_until = 0usize;
5218 for li in from..upto_excl {
5219 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
5226 if task_mask.is_none() {
5227 if li < chunk_skip_until {
5228 continue;
5229 }
5230 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
5236 fill_h(&mut h, self);
5237 h_ready = true;
5238 }
5239 let ids_for_embed = match input {
5240 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
5241 PrefillIn::Hidden(_) => None,
5242 };
5243 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
5244 if end > li {
5245 h_ready = true;
5246 chunk_skip_until = end;
5247 if self.is_loop_end(end - 1) && end < self.num_layers {
5250 for bi in 0..b {
5251 let normed = inference::rms_norm(
5252 &h[bi * hs..(bi + 1) * hs],
5253 &self.weights.final_norm,
5254 eps,
5255 norm_style,
5256 );
5257 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5258 }
5259 }
5260 continue;
5261 }
5262 }
5263 if !h_ready {
5264 fill_h(&mut h, self);
5265 h_ready = true;
5266 }
5267 let lw = &self.weights.layers[self.phys_layer(li)];
5268 match &lw.attn {
5270 AttnKind::Kda(w) => {
5271 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5273 let mut normed = vec![0.0f32; b * hs];
5274 for bi in 0..b {
5275 inference::rms_norm_into(
5276 &h[bi * hs..(bi + 1) * hs],
5277 &lw.input_norm,
5278 eps,
5279 norm_style,
5280 &mut normed[bi * hs..(bi + 1) * hs],
5281 );
5282 }
5283 let attn = crate::linear_core::kda_forward_batch(
5284 &normed,
5285 b,
5286 w,
5287 &cfg,
5288 &mut self.kv_cache.layers[li].linear_state,
5289 pool.as_deref(),
5290 );
5291 for (dst, &a) in h.iter_mut().zip(&attn) {
5292 *dst += a;
5293 }
5294 }
5295 AttnKind::LinearGdn(w) => {
5296 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5298 let mut normed = vec![0.0f32; b * hs];
5299 for bi in 0..b {
5300 let r = inference::rms_norm(
5301 &h[bi * hs..(bi + 1) * hs],
5302 &lw.input_norm,
5303 eps,
5304 norm_style,
5305 );
5306 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5307 }
5308 let attn = crate::linear_core::gdn_forward_batch(
5309 &normed,
5310 b,
5311 w,
5312 &cfg,
5313 &mut self.kv_cache.layers[li].linear_state,
5314 pool.as_deref(),
5315 );
5316 for (dst, &a) in h.iter_mut().zip(&attn) {
5317 *dst += a;
5318 }
5319 }
5320 AttnKind::ShortConv(w) => {
5321 let cfg = self
5324 .short_conv_cfg
5325 .expect("short-conv layer without short_conv_cfg");
5326 let mut normed = vec![0.0f32; b * hs];
5327 for bi in 0..b {
5328 inference::rms_norm_into(
5329 &h[bi * hs..(bi + 1) * hs],
5330 &lw.input_norm,
5331 eps,
5332 norm_style,
5333 &mut normed[bi * hs..(bi + 1) * hs],
5334 );
5335 }
5336 let attn = short_conv_forward_batch(
5337 &normed,
5338 b,
5339 w,
5340 &cfg,
5341 &mut self.kv_cache.layers[li].linear_state,
5342 pool.as_deref(),
5343 );
5344 for (dst, &a) in h.iter_mut().zip(&attn) {
5345 *dst += a;
5346 }
5347 }
5348 AttnKind::Mla(w) => {
5349 let inv_freq_l = self.layer_inv_freq(li);
5352 let rs = self.layer_rope_scale(li);
5353 let mut normed = vec![0.0f32; hs];
5354 for bi in 0..b {
5355 inference::rms_norm_into(
5356 &h[bi * hs..(bi + 1) * hs],
5357 &lw.input_norm,
5358 eps,
5359 norm_style,
5360 &mut normed,
5361 );
5362 let ao = mla_attention(
5363 w,
5364 &normed,
5365 &mut self.kv_cache.layers[li],
5366 start_pos + bi,
5367 &inv_freq_l,
5368 rs,
5369 eps,
5370 pool.as_deref(),
5371 );
5372 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
5373 *dst += a;
5374 }
5375 }
5376 }
5377 AttnKind::Full {
5378 wq,
5379 wk,
5380 wv,
5381 wo,
5382 q_norm,
5383 k_norm,
5384 output_gate,
5385 softplus_gate,
5386 bias,
5387 } => {
5388 let mut normed = vec![0.0f32; b * hs];
5392 for bi in 0..b {
5393 inference::rms_norm_into(
5394 &h[bi * hs..(bi + 1) * hs],
5395 &lw.input_norm,
5396 eps,
5397 norm_style,
5398 &mut normed[bi * hs..(bi + 1) * hs],
5399 );
5400 }
5401 let inv_freq_l = self.layer_inv_freq(li);
5402 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5403 let cfg = QwenAttnCfg {
5404 num_heads: self.layer_num_heads(li),
5405 num_kv_heads: nkv_l,
5406 head_dim: hd_l,
5407 hidden_size: hs,
5408 position: start_pos,
5409 inv_freq: &inv_freq_l,
5410 rotary_dim: rd_l,
5411 scale: self.attn_scale,
5412 softcap: self.attn_softcap,
5413 window: self.layer_window(li),
5414 v_norm: self.attn_v_norm,
5415 q_norm: q_norm.as_deref(),
5416 k_norm: k_norm.as_deref(),
5417 output_gate: *output_gate,
5418 softplus_gate: softplus_gate
5419 .as_ref()
5420 .map(|(gate, per_head)| (gate, *per_head)),
5421 rope_scale: self.layer_rope_scale(li),
5422 bias: bias
5423 .as_ref()
5424 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5425 rms_eps: eps,
5426 norm_style,
5427 pool: pool.as_deref(),
5428 };
5429 let mut attn = attention::qwen_attention_batch(
5430 &normed,
5431 b,
5432 wq,
5433 wk,
5434 wv,
5435 wo,
5436 &mut self.kv_cache.layers[li],
5437 &cfg,
5438 );
5439 if let Some(w) = &lw.attn_out_norm {
5440 for bi in 0..b {
5441 inference::rms_norm_into(
5442 &attn[bi * hs..(bi + 1) * hs],
5443 w,
5444 eps,
5445 norm_style,
5446 &mut normed[bi * hs..(bi + 1) * hs],
5447 );
5448 }
5449 attn.copy_from_slice(&normed);
5450 }
5451 for (dst, &a) in h.iter_mut().zip(&attn) {
5452 *dst += a;
5453 }
5454 }
5455 AttnKind::Linear(w) => {
5456 for bi in 0..b {
5457 let normed = inference::rms_norm(
5458 &h[bi * hs..(bi + 1) * hs],
5459 &lw.input_norm,
5460 eps,
5461 norm_style,
5462 );
5463 vmf_phase_forward(
5464 &normed,
5465 w,
5466 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
5467 &mut self.kv_cache.layers[li].linear_state,
5468 pool.as_deref(),
5469 )
5470 .iter()
5471 .enumerate()
5472 .for_each(|(i, &a)| h[bi * hs + i] += a);
5473 }
5474 }
5475 }
5476
5477 let lw = &self.weights.layers[self.phys_layer(li)];
5479 let mut post = vec![0.0f32; b * hs];
5480 for bi in 0..b {
5481 let r =
5482 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
5483 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5484 }
5485 let mask_row = task_mask
5488 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
5489 .and_then(|m| m.ffn_masks.get(li))
5490 .map(|v| v.as_slice());
5491 let mut ffn = match &lw.ffn {
5492 FfnKind::Dense(d) if !d.segs.is_empty() => {
5493 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
5494 }
5495 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
5496 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
5497 FfnKind::DenseMoe(dm) => {
5500 let mut out = vec![0.0f32; b * hs];
5501 for bi in 0..b {
5502 let r = dense_moe_ffn(
5503 dm,
5504 &post[bi * hs..(bi + 1) * hs],
5505 &h[bi * hs..(bi + 1) * hs],
5506 eps,
5507 norm_style,
5508 pool.as_deref(),
5509 );
5510 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5511 }
5512 out
5513 }
5514 };
5515 if let Some(w) = &lw.ffn_out_norm {
5516 for bi in 0..b {
5517 inference::rms_norm_into(
5518 &ffn[bi * hs..(bi + 1) * hs],
5519 w,
5520 eps,
5521 norm_style,
5522 &mut post[bi * hs..(bi + 1) * hs],
5523 );
5524 }
5525 ffn.copy_from_slice(&post);
5526 }
5527 for (dst, &f) in h.iter_mut().zip(&ffn) {
5528 *dst += f;
5529 }
5530 if let Some(sc) = lw.layer_scale {
5531 for v in h.iter_mut() {
5532 *v *= sc;
5533 }
5534 }
5535 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5536 if let Ok(t) = tp.parse::<usize>() {
5537 if t >= start_pos && t < start_pos + b {
5538 let bi = t - start_pos;
5539 let row = &h[bi * hs..(bi + 1) * hs];
5540 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5541 eprintln!(
5542 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5543 row[0], row[1]
5544 );
5545 }
5546 }
5547 }
5548 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
5552 let row = &h[(b - 1) * hs..b * hs];
5553 let rms =
5554 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
5555 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
5556 eprintln!(
5557 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
5558 match &self.weights.layers[self.phys_layer(li)].attn {
5559 AttnKind::LinearGdn(_) => "gdn",
5560 AttnKind::Linear(_) => "vmf",
5561 AttnKind::ShortConv(_) => "conv",
5562 _ => "attn",
5563 },
5564 match &lw.ffn {
5565 FfnKind::Moe(_) => "moe",
5566 FfnKind::Dense(_) => "dense",
5567 FfnKind::DenseMoe(_) => "dense+moe",
5568 },
5569 );
5570 }
5571 if self.is_loop_end(li) && li + 1 < self.num_layers {
5573 for bi in 0..b {
5574 let normed = inference::rms_norm(
5575 &h[bi * hs..(bi + 1) * hs],
5576 &self.weights.final_norm,
5577 eps,
5578 norm_style,
5579 );
5580 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5581 }
5582 }
5583 if std::env::var("CMF_TRACE_H").is_ok() {
5584 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
5585 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
5586 eprintln!(
5587 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
5588 lw.layer_scale
5589 );
5590 }
5591 }
5592 crate::gpu::set_layer(-1); h
5594 }
5595
5596 fn embed_single(&self, id: u32) -> Vec<f32> {
5598 let mut out = vec![0.0f32; self.hidden_size];
5599 if (id as usize) < self.weights.embed_tokens.rows() {
5600 self.weights.embed_tokens.row_f32(id as usize, &mut out);
5601 }
5602 if self.embed_multiplier != 1.0 {
5603 for v in out.iter_mut() {
5604 *v *= self.embed_multiplier;
5605 }
5606 }
5607 if self.dsv4.is_some() {
5611 let mut v = vec![0.0f32; self.hidden_size.max(1)];
5612 v[0] = id as f32;
5613 return v;
5614 }
5615 if let Some(b) = &self.g3n {
5618 return b.0.extend_embedding(id, &out, self.pool.as_deref());
5619 }
5620 out
5621 }
5622
5623 #[cfg(target_os = "macos")]
5629 fn chunk_run_gpu(
5630 &mut self,
5631 li0: usize,
5632 h: &mut [f32],
5633 b: usize,
5634 pos0: usize,
5635 embed_ids: Option<&[u32]>,
5636 cap: usize,
5637 ) -> usize {
5638 if !crate::gpu::enabled_here()
5642 || std::env::var("CMF_GPU_CHUNK")
5643 .map(|v| v == "0")
5644 .unwrap_or(false)
5645 || b < 32
5646 || self.swa.is_some()
5647 || self.global_attn.is_some()
5648 || self.attn_v_norm
5649 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
5650 {
5651 return li0;
5652 }
5653 let Some(model) = self.model.clone() else {
5654 return li0;
5655 };
5656 let inv_freq = self.inv_freq.clone();
5657 let (nh, nkv, hd, hs) = (
5658 self.num_heads,
5659 self.num_kv_heads,
5660 self.head_dim,
5661 self.hidden_size,
5662 );
5663 let loop_end = if self.loop_final_norm {
5667 ((li0 / self.physical_layers) + 1) * self.physical_layers
5668 } else {
5669 self.num_layers
5670 };
5671 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
5672 let mut stored_at: Vec<usize> = Vec::new();
5673 for li in li0..self.num_layers.min(loop_end).min(cap) {
5674 let lw = &self.weights.layers[self.phys_layer(li)];
5675 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
5676 break;
5677 }
5678 let AttnKind::Full {
5679 wq,
5680 wk,
5681 wv,
5682 wo,
5683 q_norm,
5684 k_norm,
5685 output_gate: false,
5686 softplus_gate: None,
5687 bias,
5688 } = &lw.attn
5689 else {
5690 break;
5691 };
5692 let FfnKind::Dense(d) = &lw.ffn else { break };
5693 if d.act != Act::Silu || !d.segs.is_empty() {
5694 break;
5695 }
5696 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
5701 t.q8_row_parts()
5702 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5703 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5704 }
5705 let parts = (
5706 cw(wq),
5707 cw(wk),
5708 cw(wv),
5709 cw(wo),
5710 cw(&d.gate_proj),
5711 cw(&d.up_proj),
5712 cw(&d.down_proj),
5713 );
5714 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
5715 else {
5716 break;
5717 };
5718 let layer = &self.kv_cache.layers[li];
5719 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
5720 break;
5721 }
5722 stored_at.push(layer.head_len(0));
5723 layers.push(crate::gpu_metal::ChunkLayer {
5724 model: &model,
5725 kv_id: self.graph_kv_id,
5726 layer: li,
5727 wq: pq,
5728 wk: pk,
5729 wv: pv,
5730 wo: po,
5731 gate: pg,
5732 up: pu,
5733 down: pd,
5734 input_norm: &lw.input_norm,
5735 post_norm: &lw.post_norm,
5736 bias: bias
5737 .as_ref()
5738 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
5739 q_norm: q_norm.as_deref(),
5740 k_norm: k_norm.as_deref(),
5741 inv_freq: &inv_freq,
5742 rd: self.rotary_dim,
5743 nh,
5744 nkv,
5745 hd,
5746 hs,
5747 inter: d.gate_proj.rows(),
5748 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
5749 eps: self.rms_eps as f32,
5750 });
5751 }
5752 if layers.is_empty() {
5753 return li0;
5754 }
5755 let row = nkv * hd;
5756 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
5757 .iter()
5758 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
5759 .collect();
5760 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
5761 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
5762 let li = layers[i].layer;
5763 let layer = &self.kv_cache.layers[li];
5764 io.push(crate::gpu_metal::ChunkIo {
5765 cpu_stored: stored_at[i],
5766 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
5767 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
5768 out_k: ok,
5769 out_v: ov,
5770 imp: oi,
5771 });
5772 }
5773 let n_run = layers.len();
5774 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
5775 let ep = embed_ids.and_then(|ids| {
5778 self.weights
5779 .embed_tokens
5780 .q8_row_parts()
5781 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
5782 idx,
5783 rows,
5784 row_scale: rs,
5785 ids,
5786 mult: self.embed_multiplier,
5787 })
5788 });
5789 if embed_ids.is_some() && ep.is_none() {
5790 return li0;
5791 }
5792 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
5793 return li0;
5794 }
5795 drop(io);
5796 drop(layers);
5797 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
5800 let li = li0 + i;
5801 let layer = &mut self.kv_cache.layers[li];
5802 for bi in 0..b {
5803 layer.append(
5804 &ok[bi * row..(bi + 1) * row],
5805 &ov[bi * row..(bi + 1) * row],
5806 &[],
5807 );
5808 }
5809 layer.accumulate_imp(oi);
5810 }
5811 last
5812 }
5813
5814 fn layer_is_local(&self, li: usize) -> bool {
5817 if let Some(layers) = &self.sliding_layers {
5818 return layers.get(li).copied().unwrap_or(false);
5819 }
5820 match self.swa {
5821 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
5822 None => false,
5823 }
5824 }
5825
5826 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
5829 if self.layer_is_local(li) {
5830 if let Some(f) = &self.inv_freq_local {
5831 return f.clone();
5832 }
5833 } else if let Some(f) = &self.inv_freq_global {
5834 return f.clone();
5835 }
5836 self.inv_freq.clone()
5837 }
5838
5839 fn layer_window(&self, li: usize) -> Option<usize> {
5841 self.swa
5842 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
5843 }
5844
5845 fn layer_num_heads(&self, li: usize) -> usize {
5846 self.attention_heads_per_layer
5847 .as_ref()
5848 .and_then(|v| v.get(li).copied())
5849 .unwrap_or(self.num_heads)
5850 }
5851
5852 fn layer_rope_scale(&self, li: usize) -> f32 {
5853 if self.layer_is_local(li) {
5854 self.rope_scale_local
5855 } else {
5856 self.rope_scale
5857 }
5858 }
5859
5860 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
5863 if !self.layer_is_local(li) {
5864 if let Some((ghd, gkv)) = self.global_attn {
5865 return (gkv, ghd, ghd);
5866 }
5867 }
5868 (
5869 self.num_kv_heads,
5870 self.head_dim,
5871 if self.layer_is_local(li) {
5872 self.rotary_dim_local.unwrap_or(self.rotary_dim)
5873 } else {
5874 self.rotary_dim
5875 },
5876 )
5877 }
5878
5879 fn forward_layers(
5881 &mut self,
5882 hidden: &[f32],
5883 position: usize,
5884 task_mask: Option<&TaskMask>,
5885 ) -> Vec<f32> {
5886 self.forward_layers_upto(hidden, position, task_mask, None)
5887 }
5888
5889 pub fn embed_id(&self, id: u32) -> Vec<f32> {
5897 self.embed_single(id)
5898 }
5899
5900 pub fn split_supported(&self) -> Result<(), String> {
5904 if self.dsv4.is_some() {
5905 return Err(
5906 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
5907 );
5908 }
5909 if self.g3n.is_some() {
5910 return Err(
5911 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
5912 );
5913 }
5914 Ok(())
5915 }
5916
5917 pub fn forward_span(
5922 &mut self,
5923 hidden: &[f32],
5924 position: usize,
5925 from: usize,
5926 upto: usize,
5927 task_mask: Option<&TaskMask>,
5928 ) -> Result<Vec<f32>, String> {
5929 self.split_supported()?;
5930 if from > upto || upto >= self.num_layers {
5931 return Err(format!(
5932 "forward_span: layer range {from}..={upto} outside 0..{}",
5933 self.num_layers
5934 ));
5935 }
5936 if hidden.len() != self.hidden_size {
5937 return Err(format!(
5938 "forward_span: hidden len {} ≠ hidden_size {}",
5939 hidden.len(),
5940 self.hidden_size
5941 ));
5942 }
5943 Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
5944 }
5945
5946 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
5949 let normed = inference::rms_norm(
5950 hidden,
5951 &self.weights.final_norm,
5952 self.rms_eps,
5953 self.norm_style,
5954 );
5955 self.lm_head_forward(&normed)
5956 }
5957
5958 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
5960 sampler::sample_with_scratch(
5961 logits,
5962 &self.sampler_config,
5963 past_tokens,
5964 &mut self.rng,
5965 &mut self.sampler_scratch,
5966 )
5967 }
5968
5969 pub fn reset_session(&mut self) {
5971 self.kv_cache.clear();
5972 self.kv_history.clear();
5973 crate::gpu::graph_kv_reset(self.graph_kv_id);
5974 }
5975
5976 pub fn prefill_span_ids(
5982 &mut self,
5983 ids: &[u32],
5984 start_pos: usize,
5985 upto: usize,
5986 task_mask: Option<&TaskMask>,
5987 ) -> Result<Vec<f32>, String> {
5988 self.split_supported()?;
5989 if upto >= self.num_layers {
5990 return Err(format!(
5991 "prefill_span_ids: upto {upto} outside 0..{}",
5992 self.num_layers
5993 ));
5994 }
5995 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5999 Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
6000 } else {
6001 let hs = self.hidden_size;
6002 let mut out = Vec::with_capacity(ids.len() * hs);
6003 for (i, &id) in ids.iter().enumerate() {
6004 let emb = self.embed_id(id);
6005 out.extend_from_slice(&self.forward_span(
6006 &emb,
6007 start_pos + i,
6008 0,
6009 upto,
6010 task_mask,
6011 )?);
6012 }
6013 Ok(out)
6014 }
6015 }
6016
6017 pub fn prefill_span_hidden(
6020 &mut self,
6021 hidden: &[f32],
6022 start_pos: usize,
6023 from: usize,
6024 upto: usize,
6025 task_mask: Option<&TaskMask>,
6026 ) -> Result<Vec<f32>, String> {
6027 self.split_supported()?;
6028 let hs = self.hidden_size;
6029 if hidden.is_empty() || hidden.len() % hs != 0 {
6030 return Err(format!(
6031 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
6032 hidden.len()
6033 ));
6034 }
6035 if from > upto || upto >= self.num_layers {
6036 return Err(format!(
6037 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
6038 self.num_layers
6039 ));
6040 }
6041 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
6042 Ok(self.prefill_batch_span(
6043 PrefillIn::Hidden(hidden),
6044 start_pos,
6045 task_mask,
6046 from,
6047 upto + 1,
6048 ))
6049 } else {
6050 let b = hidden.len() / hs;
6051 let mut out = Vec::with_capacity(hidden.len());
6052 for i in 0..b {
6053 let h = self.forward_span(
6054 &hidden[i * hs..(i + 1) * hs],
6055 start_pos + i,
6056 from,
6057 upto,
6058 task_mask,
6059 )?;
6060 out.extend_from_slice(&h);
6061 }
6062 Ok(out)
6063 }
6064 }
6065
6066 fn try_token_graph_wgpu(
6070 &self,
6071 hidden: &[f32],
6072 position: usize,
6073 logits_out: &mut Vec<f32>,
6074 layers_run: &mut usize,
6075 ) -> Option<Vec<f32>> {
6076 self.try_token_graph_wgpu_steps(
6077 hidden,
6078 position,
6079 logits_out,
6080 1,
6081 None,
6082 Some(layers_run),
6083 0,
6084 self.num_layers,
6085 )
6086 }
6087
6088 fn try_token_graph_wgpu_span(
6092 &self,
6093 hidden: &[f32],
6094 position: usize,
6095 logits_out: &mut Vec<f32>,
6096 from: usize,
6097 upto_excl: usize,
6098 layers_run: &mut usize,
6099 ) -> Option<Vec<f32>> {
6100 self.try_token_graph_wgpu_steps(
6101 hidden,
6102 position,
6103 logits_out,
6104 1,
6105 None,
6106 Some(layers_run),
6107 from,
6108 upto_excl,
6109 )
6110 }
6111
6112 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
6116 if self.o1_active() || self.attn_softcap > 0.0 {
6117 return None;
6118 }
6119 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
6120 if !graph_on || crate::gpu::graph_unsupported() {
6121 return None;
6128 }
6129 let emb = self.embed_single(t_next);
6130 let mut lg = Vec::new();
6131 let mut ids = Vec::new();
6132 self.try_token_graph_wgpu_steps(
6133 &emb,
6134 position,
6135 &mut lg,
6136 k,
6137 Some(&mut ids),
6138 None,
6139 0,
6140 self.num_layers,
6141 )?;
6142 (ids.len() == k).then_some(ids)
6143 }
6144
6145 fn try_token_graph_wgpu_steps(
6149 &self,
6150 hidden: &[f32],
6151 position: usize,
6152 logits_out: &mut Vec<f32>,
6153 steps: usize,
6154 ids_out: Option<&mut Vec<u32>>,
6155 layers_run: Option<&mut usize>,
6156 from: usize,
6157 upto_excl: usize,
6158 ) -> Option<Vec<f32>> {
6159 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
6162 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
6163 return None;
6167 }
6168 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
6173 .map(|li| {
6174 if !o1_gpu {
6175 return None;
6176 }
6177 self.kv_cache.layers[self.phys_layer(li)].o1_views()
6178 })
6179 .collect();
6180 if self.o1_active() && o1_gpu {
6181 let want: usize = (from..upto_excl)
6184 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
6185 .count();
6186 let have = o1_views.iter().filter(|v| v.is_some()).count();
6187 if want == 0 || have != want {
6188 use std::sync::atomic::{AtomicUsize, Ordering};
6198 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
6199 let code = have * 1000 + want;
6200 if LAST.swap(code, Ordering::Relaxed) != code {
6201 tracing::warn!(
6202 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
6203 );
6204 }
6205 return None;
6206 }
6207 }
6208 let nh = self.num_heads;
6209 let (nkv, hd, rd) = self.layer_geom(0);
6210 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6211 let mut layers = Vec::with_capacity(upto_excl - from);
6212 let mut model = None;
6213 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
6214 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6215 if let Some((_, i, kind, rs)) = t.graph_weight() {
6216 return Some(crate::gpu::GraphW {
6217 idx: i,
6218 kind,
6219 row_scale: rs,
6220 data: &[],
6221 });
6222 }
6223 t.as_f32().map(|d| crate::gpu::GraphW {
6225 idx: 0,
6226 kind: 4,
6227 row_scale: &[],
6228 data: d,
6229 })
6230 }
6231 for li in from..upto_excl {
6232 let lw = &self.weights.layers[self.phys_layer(li)];
6233 if dbg {
6234 let ak = match &lw.attn {
6235 AttnKind::Mla(_) => "Mla".into(),
6236 AttnKind::Full {
6237 output_gate, bias, ..
6238 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
6239 AttnKind::LinearGdn(_) => "LinearGdn".into(),
6240 AttnKind::Kda(_) => "Kda".into(),
6241 AttnKind::Linear(_) => "Linear".into(),
6242 AttnKind::ShortConv(_) => "ShortConv".into(),
6243 };
6244 let fk = match &lw.ffn {
6245 FfnKind::Dense(_) => "Dense",
6246 FfnKind::Moe(_) => "Moe",
6247 FfnKind::DenseMoe(_) => "DenseMoe",
6248 };
6249 eprintln!("graph L{li}: attn={ak} ffn={fk}");
6250 }
6251 let gffn = match &lw.ffn {
6252 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
6256 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
6257 gate: gw(&d.gate_proj)?,
6258 up: gw(&d.up_proj)?,
6259 down: gw(&d.down_proj)?,
6260 },
6261 FfnKind::Moe(m) => {
6262 if m.route_tau.is_some()
6269 || m.mask.is_some()
6270 || (m.routed_scaling - 1.0).abs() > 1e-9
6271 {
6272 return None;
6273 }
6274 let shared = m.shared.as_ref();
6275 let has_shared = shared.is_some();
6276 let sgate = match shared {
6277 Some((_, sg)) => gw(sg.as_ref()?)?,
6278 None => gw(&m.router)?,
6281 };
6282 let router = gw(&m.router)?;
6283 let inter = m.experts.first()?.gate_proj.rows();
6284 let mut experts = Vec::with_capacity(m.experts.len() + 1);
6285 let mut q4tp: Option<bool> = None;
6288 let mut gu_q2: Option<bool> = None;
6291 for e in m
6292 .experts
6293 .iter()
6294 .chain(shared.map(|(se, _)| se))
6295 {
6296 if !matches!(e.act, Act::Silu)
6297 || e.gate_proj.rows() != inter
6298 || e.up_proj.rows() != inter
6299 {
6300 return None;
6301 }
6302 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
6303 Some((mm, gi)) => (
6304 mm,
6305 gi,
6306 e.up_proj.mapped_q4t()?.1,
6307 e.down_proj.mapped_q4t()?.1,
6308 false,
6309 false,
6310 ),
6311 None => match e.gate_proj.mapped_q2tp() {
6312 Some((mm, gi)) => (
6313 mm,
6314 gi,
6315 e.up_proj.mapped_q2tp()?.1,
6316 e.down_proj.mapped_q4tp()?.1,
6317 true,
6318 true,
6319 ),
6320 None => {
6321 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
6322 (
6323 mm,
6324 gi,
6325 e.up_proj.mapped_q4tp()?.1,
6326 e.down_proj.mapped_q4tp()?.1,
6327 true,
6328 false,
6329 )
6330 }
6331 },
6332 };
6333 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
6334 {
6335 tracing::warn!(
6341 "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."
6342 );
6343 return None;
6344 }
6345 model.get_or_insert_with(|| mm.clone());
6346 experts.push((gi, ui, di));
6347 }
6348 crate::gpu::GraphFfn::Moe {
6349 router,
6350 shared_gate: sgate,
6351 experts,
6352 n_exp: m.experts.len(),
6353 top_k: std::env::var("CMF_TOPK_PROBE")
6359 .ok()
6360 .and_then(|v| v.parse::<usize>().ok())
6361 .filter(|k| *k > 0 && *k <= m.top_k)
6362 .unwrap_or(m.top_k),
6363 inter,
6364 norm_topk: m.norm_topk_prob,
6365 q4tp: q4tp?,
6366 gu_q2: gu_q2.unwrap_or(false),
6367 sigmoid: m.router_sigmoid,
6368 bias: m.expert_bias.as_deref(),
6369 has_shared,
6370 }
6371 }
6372 };
6373 let attn = match &lw.attn {
6374 AttnKind::Full {
6375 wq,
6376 wk,
6377 wv,
6378 wo,
6379 q_norm,
6380 k_norm,
6381 output_gate,
6382 softplus_gate,
6383 bias,
6384 } => {
6385 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
6386 return None;
6387 }
6388 let (m, _, _, _) = wq.graph_weight()?;
6389 model = Some(m.clone());
6390 crate::gpu::GraphAttn::Full {
6391 wq: gw(wq)?,
6392 wk: gw(wk)?,
6393 wv: gw(wv)?,
6394 wo: gw(wo)?,
6395 q_norm: q_norm.as_deref(),
6396 k_norm: k_norm.as_deref(),
6397 bias: bias
6398 .as_ref()
6399 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6400 output_gate: *output_gate,
6401 cpu_k: self.kv_cache.layers[li].k_heads(),
6402 cpu_v: self.kv_cache.layers[li].v_heads(),
6403 }
6404 }
6405 AttnKind::LinearGdn(w) => {
6406 let cfg = self.gdn_cfg?;
6407 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
6408 model = Some(m.clone());
6409 crate::gpu::GraphAttn::Gdn {
6410 qkv: gw(&w.in_proj_qkv)?,
6411 z: gw(&w.in_proj_z)?,
6412 a: gw(&w.in_proj_a)?,
6413 b: gw(&w.in_proj_b)?,
6414 out: gw(&w.out_proj)?,
6415 conv1d: &w.conv1d,
6416 a_log: &w.a_log,
6417 dt_bias: &w.dt_bias,
6418 norm: &w.norm,
6419 nv: cfg.num_v_heads,
6420 nk: cfg.num_k_heads,
6421 dk: cfg.key_head_dim,
6422 dv: cfg.value_head_dim,
6423 kk: cfg.conv_kernel,
6424 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6425 }
6426 }
6427 AttnKind::ShortConv(w) => {
6428 let cfg = self.short_conv_cfg?;
6429 let (m, _, _, _) = w.in_proj.graph_weight()?;
6430 model = Some(m.clone());
6431 crate::gpu::GraphAttn::ShortConv {
6432 inp: gw(&w.in_proj)?,
6433 out: gw(&w.out_proj)?,
6434 taps: &w.conv,
6435 kernel: cfg.kernel,
6436 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6437 }
6438 }
6439 _ => return None,
6440 };
6441 layers.push(crate::gpu::GraphLayer {
6442 input_norm: &lw.input_norm,
6443 attn,
6444 post_norm: &lw.post_norm,
6445 ffn: gffn,
6446 });
6447 }
6448 let model = model?;
6449 let lm_gw = if upto_excl == self.num_layers
6455 && self.graph_want_logits
6456 && std::env::var("CMF_GPU_LMHEAD")
6457 .map(|v| v != "0")
6458 .unwrap_or(true)
6459 {
6460 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
6461 (
6462 crate::gpu::GraphW {
6463 idx: i,
6464 kind,
6465 row_scale: rs,
6466 data: &[],
6467 },
6468 self.weights.lm_head.rows(),
6469 )
6470 })
6471 } else {
6472 None
6473 };
6474 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
6475 let emb_gw = if steps > 1 {
6477 self.weights
6478 .embed_tokens
6479 .graph_weight()
6480 .map(|(_, i, kind, rs)| {
6481 (
6482 crate::gpu::GraphW {
6483 idx: i,
6484 kind,
6485 row_scale: rs,
6486 data: &[],
6487 },
6488 self.weights.embed_tokens.rows(),
6489 self.embed_multiplier,
6490 )
6491 })
6492 } else {
6493 None
6494 };
6495
6496 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
6502 (from..upto_excl.min(self.num_layers - 1))
6503 .filter(|&li| (li + 1) % self.physical_layers == 0)
6504 .map(|li| li - from)
6505 .collect()
6506 } else {
6507 Vec::new()
6508 };
6509 let mut h = hidden.to_vec();
6510 crate::gpu::forward_token_graph(
6511 &model,
6512 self.graph_kv_id,
6513 &layers,
6514 &o1_views,
6515 self.o1_epoch,
6516 &self.inv_freq,
6517 &mut h,
6518 nh,
6519 nkv,
6520 hd,
6521 rd,
6522 self.hidden_size,
6523 self.intermediate_size,
6524 position,
6525 self.kv_cache.max_seq_len,
6526 gemma,
6527 self.rms_eps as f32,
6528 lm,
6529 &self.weights.final_norm,
6530 logits_out,
6531 &loop_norm_at,
6532 steps,
6533 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
6534 ids_out,
6535 layers_run,
6536 from,
6537 false,
6538 )
6539 .then_some(h)
6540 }
6541
6542 #[cfg(target_os = "macos")]
6551 #[allow(clippy::type_complexity)]
6552 fn metal_rows_plan(&self) -> Option<(Vec<MetalRowsItem<'_>>, std::sync::Arc<cortiq_core::CmfModel>, Option<crate::gpu_metal::GdnGpuCfg>)> {
6553 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
6554 if !crate::gpu::q1_force()
6555 || !crate::gpu::enabled_here()
6556 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
6557 || self.attn_softcap > 0.0
6558 || self.o1_active()
6559 || self.swa.is_some()
6560 || self.global_attn.is_some()
6561 || self.attention_heads_per_layer.is_some()
6562 || self.attn_v_norm
6563 || self.loop_final_norm
6564 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
6565 {
6566 return None;
6567 }
6568 let attend_contract = self.head_dim % 4 == 0
6569 && self.head_dim <= 256
6570 && self.rotary_dim >= 2
6571 && self.rotary_dim <= self.head_dim
6572 && (self.rotary_dim / 2) % 32 == 0
6573 && self.num_kv_heads > 0
6574 && self.num_heads % self.num_kv_heads == 0;
6575 if !attend_contract {
6576 return None;
6577 }
6578 let mut plan: Vec<MetalRowsItem> = Vec::new();
6579 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
6580 for li in 0..self.num_layers {
6581 let lw = &self.weights.layers[self.phys_layer(li)];
6582 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
6583 return None;
6584 }
6585 let ffn = match &lw.ffn {
6586 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
6587 let (Some(g), Some(u), Some(dn)) =
6588 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
6589 else {
6590 return None;
6591 };
6592 MetalFfn::Dense { gate: g, up: u, down: dn }
6593 }
6594 _ => return None,
6595 };
6596 match &lw.attn {
6597 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
6598 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
6599 w.in_proj_qkv.q1_parts(),
6600 w.in_proj_z.q1_parts(),
6601 w.in_proj_a.f32_parts(),
6602 w.in_proj_b.f32_parts(),
6603 w.out_proj.q1_parts(),
6604 ) else {
6605 return None;
6606 };
6607 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
6608 model_ref.get_or_insert_with(|| model.clone());
6609 }
6610 let gl = GdnGpuLayer {
6611 attn_norm: &lw.input_norm,
6612 post_norm: &lw.post_norm,
6613 qkv,
6614 z,
6615 a,
6616 b: bb,
6617 out,
6618 ffn,
6619 conv1d: &w.conv1d,
6620 a_log: &w.a_log,
6621 dt_bias: &w.dt_bias,
6622 gnorm: &w.norm,
6623 };
6624 match plan.last_mut() {
6625 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
6626 _ => plan.push(MetalRowsItem::Gdn { run: vec![gl], first: li }),
6627 }
6628 }
6629 AttnKind::Full {
6630 wq,
6631 wk,
6632 wv,
6633 wo,
6634 q_norm,
6635 k_norm,
6636 output_gate,
6637 softplus_gate: None,
6638 bias: None,
6639 } => {
6640 let (Some(pq), Some(pk), Some(pv), Some(po)) =
6641 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
6642 else {
6643 return None;
6644 };
6645 if let QTensor::Mapped { model, .. } = wq {
6646 model_ref.get_or_insert_with(|| model.clone());
6647 }
6648 let cache = &self.kv_cache.layers[li];
6649 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
6650 return None;
6651 }
6652 plan.push(MetalRowsItem::Attn {
6653 l: AttnGpuLayer {
6654 attn_norm: &lw.input_norm,
6655 post_norm: &lw.post_norm,
6656 wq: pq,
6657 wk: pk,
6658 wv: pv,
6659 wo: po,
6660 ffn,
6661 },
6662 li,
6663 q_norm: q_norm.as_deref(),
6664 k_norm: k_norm.as_deref(),
6665 output_gate: *output_gate,
6666 });
6667 }
6668 _ => return None,
6669 }
6670 }
6671 let model = model_ref?;
6672 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
6673 nv: cfg.num_v_heads,
6674 nk: cfg.num_k_heads,
6675 dk: cfg.key_head_dim,
6676 dv: cfg.value_head_dim,
6677 kk: cfg.conv_kernel,
6678 hidden: self.hidden_size,
6679 inter: self.intermediate_size,
6680 c_dim: cfg.conv_dim(),
6681 eps: cfg.rms_eps as f32,
6682 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6683 });
6684 Some((plan, model, gcfg))
6685 }
6686
6687 #[cfg(target_os = "macos")]
6689 #[allow(clippy::too_many_arguments)]
6690 fn metal_attn_params<'a>(
6691 li: usize,
6692 cache: &'a crate::kv_cache::LayerKvCache,
6693 q_norm: Option<&'a [f32]>,
6694 k_norm: Option<&'a [f32]>,
6695 output_gate: bool,
6696 inv_freq: &'a [f32],
6697 geom: (usize, usize, usize, usize),
6698 pos0: usize,
6699 kv_id: u64,
6700 eps: f32,
6701 gemma: bool,
6702 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
6703 let (nh, nkv, hd, rd) = geom;
6704 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6705 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6706 let cpu_stored = cpu_k[0].len() / hd;
6707 (
6708 crate::gpu_metal::AttnDeviceParams {
6709 kv_id,
6710 layer: li,
6711 nh,
6712 nkv,
6713 hd,
6714 rd,
6715 position: pos0,
6716 eps,
6717 gemma,
6718 output_gate,
6719 q_norm,
6720 k_norm,
6721 inv_freq,
6722 cpu_k,
6723 cpu_v,
6724 cpu_stored,
6725 o1: None,
6726 },
6727 cpu_stored,
6728 )
6729 }
6730
6731 #[cfg(target_os = "macos")]
6736 #[allow(clippy::type_complexity)]
6737 fn metal_rows_run(
6738 &mut self,
6739 hiddens: &mut [f32],
6740 pos0: usize,
6741 b: usize,
6742 prefill: bool,
6743 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6744 ) -> Option<MetalVerifyPending> {
6745 use crate::gpu_metal::{GraphDims, VerifyGraph};
6746 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
6747 for l in &mut self.kv_cache.layers {
6748 if l.linear_state.len() != want && want > 0 {
6749 l.linear_state = vec![0f32; want];
6750 }
6751 }
6752 let (plan, model, gcfg) = self.metal_rows_plan()?;
6753 let dims = GraphDims {
6754 hidden: self.hidden_size,
6755 eps: self.rms_eps as f32,
6756 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6757 };
6758 let mut graph = if prefill {
6759 VerifyGraph::new_prefill(&model, dims, hiddens, b)?
6760 } else {
6761 VerifyGraph::new(&model, dims, hiddens, b)?
6762 };
6763 let geom = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6764 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6765 let eps = self.rms_eps as f32;
6766 let kv_id = self.graph_kv_id;
6767 let inv_freq = self.inv_freq.clone();
6768 for item in &plan {
6769 let ok = match item {
6770 MetalRowsItem::Gdn { run, .. } => gcfg
6771 .as_ref()
6772 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
6773 .unwrap_or(false),
6774 MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6775 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);
6776 graph.attn_ok(l, &p)
6777 }
6778 };
6779 if !ok {
6780 use std::sync::atomic::{AtomicBool, Ordering};
6781 static SAID: AtomicBool = AtomicBool::new(false);
6782 if !SAID.swap(true, Ordering::Relaxed) {
6783 tracing::warn!("metal rows graph: a layer failed preflight — declining");
6784 }
6785 return None;
6786 }
6787 }
6788 let lm = match &spec {
6789 Some((lm, _, _)) => {
6790 if !graph.lm_head_ok(*lm) {
6791 return None;
6792 }
6793 Some(*lm)
6794 }
6795 None => None,
6796 };
6797 let mut gdn_layers = Vec::new();
6798 let mut attn_layers = Vec::new();
6799 for item in &plan {
6800 match item {
6801 MetalRowsItem::Gdn { run, first } => {
6802 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
6803 .iter()
6804 .map(|l| l.linear_state.as_slice())
6805 .collect();
6806 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
6807 return None;
6808 }
6809 gdn_layers.extend(*first..*first + run.len());
6810 }
6811 MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6812 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);
6813 if !graph.encode_attn_b(l, &p) {
6814 return None;
6815 }
6816 attn_layers.push((*li, cpu_stored));
6817 }
6818 }
6819 }
6820 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
6821 if !graph.encode_lm_head_b(final_norm, lm) {
6822 return None;
6823 }
6824 }
6825 graph.sync();
6826 if let Some((lm, _, logits)) = spec {
6827 logits.resize(b * lm.1, 0.0);
6828 graph.read_logits(logits);
6829 }
6830 graph.read_hidden(hiddens);
6831 Some(MetalVerifyPending { graph, gdn_layers, attn_layers })
6832 }
6833
6834 #[cfg(target_os = "macos")]
6840 fn try_batch_graph_metal(
6841 &mut self,
6842 hiddens: &mut [f32],
6843 positions: &[usize],
6844 b: usize,
6845 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6846 ) -> bool {
6847 let _t0 = std::time::Instant::now();
6848 if positions.len() != b
6849 || positions.windows(2).any(|w| w[1] != w[0] + 1)
6850 || hiddens.len() != b * self.hidden_size
6851 {
6852 return false;
6853 }
6854 let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
6855 return false;
6856 };
6857 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6858 eprintln!("metal-verify: {:.1} ms | b={b}", _t0.elapsed().as_secs_f64() * 1e3);
6859 }
6860 self.metal_verify = Some(pending);
6861 true
6862 }
6863
6864 #[cfg(target_os = "macos")]
6869 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
6870 let b = ids.len();
6871 if b == 0 || b > 512 {
6872 return None;
6873 }
6874 let hs = self.hidden_size;
6875 let mut hiddens = vec![0f32; b * hs];
6876 for (j, &id) in ids.iter().enumerate() {
6877 let e = self.embed_single(id);
6878 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
6879 }
6880 let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
6881 let idxs = pending.gdn_layers.clone();
6883 let mut outs: Vec<&mut [f32]> = self
6884 .kv_cache
6885 .layers
6886 .iter_mut()
6887 .enumerate()
6888 .filter(|(i, _)| idxs.binary_search(i).is_ok())
6889 .map(|(_, l)| l.linear_state.as_mut_slice())
6890 .collect();
6891 pending.graph.finish_states(&mut outs);
6892 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6893 let mut kbuf = vec![0f32; b * nkv * hd];
6894 let mut vbuf = vec![0f32; b * nkv * hd];
6895 for (li, cpu_stored) in &pending.attn_layers {
6896 if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, b, &mut kbuf, &mut vbuf) {
6897 let cache = &mut self.kv_cache.layers[*li];
6898 for r in 0..b {
6899 cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6900 }
6901 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
6902 }
6903 }
6904 Some(hiddens)
6905 }
6906
6907 #[cfg(target_os = "macos")]
6911 fn metal_verify_commit(&mut self, a: usize) -> bool {
6912 let Some(mut pending) = self.metal_verify.take() else {
6913 return false;
6914 };
6915 let n = a + 1;
6916 let idxs = pending.gdn_layers.clone();
6918 let mut outs: Vec<&mut [f32]> = self
6919 .kv_cache
6920 .layers
6921 .iter_mut()
6922 .enumerate()
6923 .filter(|(i, _)| idxs.binary_search(i).is_ok())
6924 .map(|(_, l)| l.linear_state.as_mut_slice())
6925 .collect();
6926 if !pending.graph.commit(n, &mut outs) {
6927 return false;
6928 }
6929 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6930 let mut kbuf = vec![0f32; n * nkv * hd];
6931 let mut vbuf = vec![0f32; n * nkv * hd];
6932 for (li, cpu_stored) in &pending.attn_layers {
6933 if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, n, &mut kbuf, &mut vbuf) {
6934 let cache = &mut self.kv_cache.layers[*li];
6935 for r in 0..n {
6936 cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6937 }
6938 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
6939 }
6940 }
6941 true
6942 }
6943
6944 #[cfg(target_os = "macos")]
6950 fn mtp_warm_batch_metal(&mut self, m: &mut MtpModule, pairs: &[(&[f32], u32)], first_pos: usize) -> bool {
6951 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
6952 let b = pairs.len();
6953 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
6954 return false;
6955 }
6956 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate: None, bias: None } = &m.layer.attn else {
6957 return false;
6958 };
6959 let FfnKind::Dense(d) = &m.layer.ffn else { return false };
6960 if !d.segs.is_empty() {
6961 return false;
6962 }
6963 let (Some(pq), Some(pk), Some(pv), Some(po)) = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts()) else {
6964 return false;
6965 };
6966 let (Some(g), Some(u), Some(dn)) = (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts()) else {
6967 return false;
6968 };
6969 let Some(eh) = m.eh_proj.q1_parts() else { return false };
6970 let QTensor::Mapped { model, .. } = wq else { return false };
6971 let model = model.clone();
6972 let hs = self.hidden_size;
6973 let mut cat = vec![0f32; b * 2 * hs];
6975 for (j, (h, tok)) in pairs.iter().enumerate() {
6976 let e = self.embed_single(*tok);
6977 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
6978 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
6979 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
6980 }
6981 let dims = GraphDims { hidden: hs, eps: self.rms_eps as f32, gemma: self.norm_style == cortiq_core::NormStyle::Gemma };
6982 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
6983 return false;
6984 };
6985 let l = AttnGpuLayer {
6986 attn_norm: &m.layer.input_norm,
6987 post_norm: &m.layer.post_norm,
6988 wq: pq,
6989 wk: pk,
6990 wv: pv,
6991 wo: po,
6992 ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
6993 };
6994 let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6995 let inv_freq = self.inv_freq.clone();
6996 let cpu_stored;
6997 {
6998 let cache = &m.kv;
6999 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
7000 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
7001 cpu_stored = cpu_k[0].len() / hd;
7002 if cpu_stored != first_pos {
7003 return false;
7004 }
7005 let p = AttnDeviceParams {
7006 kv_id: self.mtp_kv_id(),
7007 layer: Self::MTP_LAYER_BASE,
7008 nh,
7009 nkv,
7010 hd,
7011 rd,
7012 position: first_pos,
7013 eps: self.rms_eps as f32,
7014 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7015 output_gate: *output_gate,
7016 q_norm: q_norm.as_deref(),
7017 k_norm: k_norm.as_deref(),
7018 inv_freq: &inv_freq,
7019 cpu_k,
7020 cpu_v,
7021 cpu_stored,
7022 o1: None,
7023 };
7024 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
7025 return false;
7026 }
7027 }
7028 graph.sync();
7029 let mut kbuf = vec![0f32; b * nkv * hd];
7030 let mut vbuf = vec![0f32; b * nkv * hd];
7031 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) {
7032 return false;
7033 }
7034 for r in 0..b {
7035 m.kv.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
7036 }
7037 crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, cpu_stored + b);
7038 true
7039 }
7040
7041 fn draft_vocab_rows(head_rows: usize) -> usize {
7044 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
7045 let n = *N.get_or_init(|| {
7046 std::env::var("CMF_DRAFT_VOCAB")
7047 .ok()
7048 .and_then(|v| v.parse().ok())
7049 .unwrap_or(65536)
7050 });
7051 if n == 0 { head_rows } else { n.min(head_rows) }
7052 }
7053
7054 #[cfg(target_os = "macos")]
7059 fn mtp_step_metal(
7060 &mut self,
7061 m: &mut MtpModule,
7062 hidden: &[f32],
7063 next_token: u32,
7064 position: usize,
7065 want_logits: bool,
7066 ) -> Option<(Vec<f32>, Vec<f32>)> {
7067 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
7068 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
7069 || !crate::gpu::q1_force()
7070 || !crate::gpu::enabled_here()
7071 || self.attn_softcap > 0.0
7072 || self.attention_heads_per_layer.is_some()
7073 || m.kv.mode != crate::kv_cache::KvMode::F32
7074 || m.kv.o1.is_some()
7075 {
7076 return None;
7077 }
7078 let AttnKind::Full {
7079 wq,
7080 wk,
7081 wv,
7082 wo,
7083 q_norm,
7084 k_norm,
7085 output_gate,
7086 softplus_gate: None,
7087 bias: None,
7088 } = &m.layer.attn
7089 else {
7090 return None;
7091 };
7092 let FfnKind::Dense(d) = &m.layer.ffn else { return None };
7093 if d.act != Act::Silu || !d.segs.is_empty() {
7094 return None;
7095 }
7096 let (pq, pk, pv, po) = (wq.q1_parts()?, wk.q1_parts()?, wv.q1_parts()?, wo.q1_parts()?);
7097 let (g, u, dn) = (d.gate_proj.q1_parts()?, d.up_proj.q1_parts()?, d.down_proj.q1_parts()?);
7098 let QTensor::Mapped { model, .. } = wq else { return None };
7099 let model = model.clone();
7100 let lm = if want_logits { Some(self.weights.lm_head.q1_parts()?) } else { None };
7101 let dims = GraphDims {
7102 hidden: self.hidden_size,
7103 eps: self.rms_eps as f32,
7104 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7105 };
7106 let hs = self.hidden_size;
7109 let mut x = vec![0f32; hs];
7110 let mut graph = TokenGraph::new(&model, dims, &x)?;
7111 let mut folded = false;
7112 if let Some(eh) = m.eh_proj.q1_parts() {
7113 let e = self.embed_single(next_token);
7114 let mut cat = vec![0.0f32; 2 * hs];
7115 let (cat_e, cat_h) = cat.split_at_mut(hs);
7116 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7117 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7118 folded = graph.encode_input_proj(eh, &cat);
7119 }
7120 if !folded {
7121 x = self.mtp_block_input(m, hidden, next_token);
7122 graph = TokenGraph::new(&model, dims, &x)?;
7123 }
7124 let l = AttnGpuLayer {
7125 attn_norm: &m.layer.input_norm,
7126 post_norm: &m.layer.post_norm,
7127 wq: pq,
7128 wk: pk,
7129 wv: pv,
7130 wo: po,
7131 ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
7132 };
7133 let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
7134 let inv_freq = self.inv_freq.clone();
7135 {
7136 let cache = &m.kv;
7137 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
7138 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
7139 let cpu_stored = cpu_k[0].len() / hd;
7140 let p = AttnDeviceParams {
7141 kv_id: self.mtp_kv_id(),
7142 layer: Self::MTP_LAYER_BASE,
7143 nh,
7144 nkv,
7145 hd,
7146 rd,
7147 position,
7148 eps: self.rms_eps as f32,
7149 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
7150 output_gate: *output_gate,
7151 q_norm: q_norm.as_deref(),
7152 k_norm: k_norm.as_deref(),
7153 inv_freq: &inv_freq,
7154 cpu_k,
7155 cpu_v,
7156 cpu_stored,
7157 o1: None,
7158 };
7159 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
7160 return None;
7161 }
7162 }
7163 let draft_rows = if let Some(lm) = lm { Self::draft_vocab_rows(lm.1) } else { 0 };
7169 if let Some(lm) = lm {
7170 if !graph.lm_head_ok(lm) {
7171 return None;
7172 }
7173 if draft_rows < lm.1 {
7174 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
7175 return None;
7176 }
7177 } else {
7178 graph.encode_lm_head(&m.final_norm, lm);
7179 }
7180 }
7181 graph.sync();
7182 let mut logits = Vec::new();
7183 if let Some(lm) = lm {
7184 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
7185 logits = attention::take_buf(n_read);
7186 graph.read_logits(&mut logits);
7187 logits.resize(self.vocab_size, f32::NEG_INFINITY);
7189 }
7190 graph.finish(&mut x);
7191 let mut krow = attention::take_buf(nkv * hd);
7192 let mut vrow = attention::take_buf(nkv * hd);
7193 if crate::gpu_metal::kv_mirror_read_last(self.mtp_kv_id(), Self::MTP_LAYER_BASE, nkv, hd, &mut krow, &mut vrow) {
7194 m.kv.append(&krow, &vrow, &[]);
7195 }
7196 attention::recycle_buf(&mut krow);
7197 attention::recycle_buf(&mut vrow);
7198 Some((logits, x))
7199 }
7200
7201 fn try_batch_graph_wgpu(
7202 &self,
7203 hiddens: &mut [f32],
7204 positions: &[usize],
7205 k: usize,
7206 spec: Option<crate::gpu::SpecTail<'_>>,
7207 ) -> bool {
7208 let _tb = std::time::Instant::now();
7209 if self.attn_softcap > 0.0 {
7210 return false; }
7212 if self.o1_active() {
7213 return false;
7214 }
7215 let nh = self.num_heads;
7216 let (nkv, hd, rd) = self.layer_geom(0);
7217 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7218 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7219 if let Some((_, i, kind, rs)) = t.graph_weight() {
7220 return Some(crate::gpu::GraphW {
7221 idx: i,
7222 kind,
7223 row_scale: rs,
7224 data: &[],
7225 });
7226 }
7227 t.as_f32().map(|d| crate::gpu::GraphW {
7228 idx: 0,
7229 kind: 4,
7230 row_scale: &[],
7231 data: d,
7232 })
7233 }
7234 let built: Option<(
7235 Vec<crate::gpu::GraphLayer<'_>>,
7236 std::sync::Arc<cortiq_core::CmfModel>,
7237 )> = (|| {
7238 let mut layers = Vec::with_capacity(self.num_layers);
7239 let mut model = None;
7240 for li in 0..self.num_layers {
7241 let lw = &self.weights.layers[self.phys_layer(li)];
7242 let gffn = match &lw.ffn {
7249 FfnKind::Dense(d) if !d.segs.is_empty() => return None,
7250 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7251 gate: gw(&d.gate_proj)?,
7252 up: gw(&d.up_proj)?,
7253 down: gw(&d.down_proj)?,
7254 },
7255 FfnKind::Moe(m) => {
7256 if m.router_sigmoid
7257 || m.expert_bias.is_some()
7258 || m.route_tau.is_some()
7259 || m.mask.is_some()
7260 {
7261 return None;
7262 }
7263 let (se, sg) = m.shared.as_ref()?;
7264 let sgate = gw(sg.as_ref()?)?;
7265 let router = gw(&m.router)?;
7266 let inter = m.experts.first()?.gate_proj.rows();
7267 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7268 let mut q4tp: Option<bool> = None;
7269 let mut gu_q2: Option<bool> = None;
7270 for e in m.experts.iter().chain(std::iter::once(se)) {
7271 if !matches!(e.act, Act::Silu)
7272 || e.gate_proj.rows() != inter
7273 || e.up_proj.rows() != inter
7274 {
7275 return None;
7276 }
7277 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7281 Some((mm, gi)) => (
7282 mm,
7283 gi,
7284 e.up_proj.mapped_q4t()?.1,
7285 e.down_proj.mapped_q4t()?.1,
7286 false,
7287 false,
7288 ),
7289 None => match e.gate_proj.mapped_q2tp() {
7290 Some((mm, gi)) => (
7291 mm,
7292 gi,
7293 e.up_proj.mapped_q2tp()?.1,
7294 e.down_proj.mapped_q4tp()?.1,
7295 true,
7296 true,
7297 ),
7298 None => {
7299 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7300 (
7301 mm,
7302 gi,
7303 e.up_proj.mapped_q4tp()?.1,
7304 e.down_proj.mapped_q4tp()?.1,
7305 true,
7306 false,
7307 )
7308 }
7309 },
7310 };
7311 if *q4tp.get_or_insert(is_p) != is_p
7312 || *gu_q2.get_or_insert(is_q2) != is_q2
7313 {
7314 return None;
7315 }
7316 model.get_or_insert_with(|| mm.clone());
7317 experts.push((gi, ui, di));
7318 }
7319 crate::gpu::GraphFfn::Moe {
7320 router,
7321 shared_gate: sgate,
7322 experts,
7323 n_exp: m.experts.len(),
7324 top_k: m.top_k,
7325 inter,
7326 norm_topk: m.norm_topk_prob,
7327 q4tp: q4tp?,
7328 gu_q2: gu_q2.unwrap_or(false),
7329 sigmoid: false,
7330 bias: None,
7331 has_shared: true,
7332 }
7333 }
7334 _ => return None,
7335 };
7336 let attn = match &lw.attn {
7337 AttnKind::Full {
7338 wq,
7339 wk,
7340 wv,
7341 wo,
7342 q_norm,
7343 k_norm,
7344 output_gate,
7345 softplus_gate,
7346 bias,
7347 } => {
7348 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7349 return None;
7350 }
7351 let (m, _, _, _) = wq.graph_weight()?;
7352 model = Some(m.clone());
7353 crate::gpu::GraphAttn::Full {
7354 wq: gw(wq)?,
7355 wk: gw(wk)?,
7356 wv: gw(wv)?,
7357 wo: gw(wo)?,
7358 q_norm: q_norm.as_deref(),
7359 k_norm: k_norm.as_deref(),
7360 bias: bias
7361 .as_ref()
7362 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7363 output_gate: *output_gate,
7364 cpu_k: self.kv_cache.layers[li].k_heads(),
7365 cpu_v: self.kv_cache.layers[li].v_heads(),
7366 }
7367 }
7368 AttnKind::LinearGdn(w) => {
7369 let cfg = self.gdn_cfg?;
7370 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7371 model = Some(m.clone());
7372 crate::gpu::GraphAttn::Gdn {
7373 qkv: gw(&w.in_proj_qkv)?,
7374 z: gw(&w.in_proj_z)?,
7375 a: gw(&w.in_proj_a)?,
7376 b: gw(&w.in_proj_b)?,
7377 out: gw(&w.out_proj)?,
7378 conv1d: &w.conv1d,
7379 a_log: &w.a_log,
7380 dt_bias: &w.dt_bias,
7381 norm: &w.norm,
7382 nv: cfg.num_v_heads,
7383 nk: cfg.num_k_heads,
7384 dk: cfg.key_head_dim,
7385 dv: cfg.value_head_dim,
7386 kk: cfg.conv_kernel,
7387 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7388 }
7389 }
7390 _ => return None,
7391 };
7392 layers.push(crate::gpu::GraphLayer {
7393 input_norm: &lw.input_norm,
7394 attn,
7395 post_norm: &lw.post_norm,
7396 ffn: gffn,
7397 });
7398 }
7399 Some((layers, model?))
7400 })();
7401 let Some((layers, model)) = built else {
7402 {
7403 use std::sync::atomic::{AtomicBool, Ordering};
7404 static SAID: AtomicBool = AtomicBool::new(false);
7405 if !SAID.swap(true, Ordering::Relaxed) {
7406 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
7407 }
7408 }
7409 return false;
7410 };
7411 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7412 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
7413 }
7414 crate::gpu::forward_batch_graph(
7415 &model,
7416 self.graph_kv_id,
7417 &layers,
7418 &self.inv_freq,
7419 hiddens,
7420 nh,
7421 nkv,
7422 hd,
7423 rd,
7424 self.hidden_size,
7425 self.intermediate_size,
7426 positions,
7427 self.kv_cache.max_seq_len,
7428 gemma,
7429 self.rms_eps as f32,
7430 k,
7431 spec,
7432 )
7433 }
7434
7435 fn draft_probe() -> bool {
7439 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7440 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
7441 }
7442
7443 #[cfg(feature = "gpu")]
7455 fn dsv4_spec_on() -> bool {
7456 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7457 *ON.get_or_init(|| {
7458 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
7462 return v != "0";
7463 }
7464 std::env::var("CMF_DSV4_SPEC")
7471 .map(|v| v != "0")
7472 .unwrap_or_else(|_| {
7473 crate::gpu_wgpu::DRAFT_RESERVE
7474 .load(std::sync::atomic::Ordering::Relaxed)
7475 > 0
7476 })
7477 })
7478 }
7479
7480 #[cfg(feature = "gpu")]
7487 fn dsv4_spec_step(
7488 &mut self,
7489 tip_token: u32,
7490 t_next: u32,
7491 next_pos: usize,
7492 max_extra: usize,
7493 drafted: &mut usize,
7494 accepted_ctr: &mut usize,
7495 ) -> Option<(Vec<u32>, usize)> {
7496 let t_all = std::time::Instant::now();
7497 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7498 thread_local! {
7499 static LAST: std::cell::Cell<Option<std::time::Instant>> =
7500 const { std::cell::Cell::new(None) };
7501 }
7502 LAST.with(|l| {
7503 if let Some(prev) = l.get() {
7504 eprintln!(
7505 "между раундами {:.1} мс",
7506 prev.elapsed().as_secs_f64() * 1e3
7507 );
7508 }
7509 l.set(Some(std::time::Instant::now()));
7510 });
7511 }
7512 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7513 eprintln!("spec_step: вход pos={next_pos}");
7514 }
7515 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
7516 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
7517 if self.dspark.is_none() {
7519 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7520 if t.is_empty() {
7521 return None;
7522 }
7523 crate::dsv4::dspark_arm(&t, cfg.dim);
7524 self.dspark = Some(crate::dsv4::DsparkState::new(
7525 self.dsv4_mtp.len(),
7526 &cfg,
7527 t.len(),
7528 ));
7529 }
7530 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7531 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
7532 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7533 eprintln!("spec_step: пак не построился (targets {targets:?})");
7534 }
7535 let pack = pack?;
7536 let block = crate::dsv4::dspark_block();
7537 let b_box = self.dsv4.as_mut()?;
7538 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
7539 let ds = self.dspark.as_mut()?;
7540 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
7543 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
7544 if dbg {
7545 eprintln!("spec_step: нет захвата");
7546 }
7547 return None;
7548 }
7549 ds.have_hidden = true;
7550 let tip_pos = next_pos.checked_sub(1)?;
7551 let draft_started = std::time::Instant::now();
7552 let mut conf = Vec::new();
7553 let props = crate::dsv4::dspark_draft_gpu(
7554 g,
7555 &self.dsv4_mtp,
7556 &cfg,
7557 ds,
7558 pack,
7559 st.kv_id,
7560 tip_token,
7561 tip_pos,
7562 self.pool.as_deref(),
7563 &mut conf,
7564 );
7565 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7566 *drafted += block;
7567 if props.is_empty() || props[0] != t_next {
7568 if dbg {
7569 eprintln!(
7570 "spec_step: черновик {} (props0={:?} t_next={t_next})",
7571 if props.is_empty() {
7572 "пуст"
7573 } else {
7574 "мимо"
7575 },
7576 props.first()
7577 );
7578 }
7579 return None;
7580 }
7581 let mut k_verify = crate::dsv4::dspark_verify_k()
7588 .min(props.len())
7589 .min(max_extra.saturating_add(1));
7590 let conf_min = {
7596 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
7597 *M.get_or_init(|| {
7598 std::env::var("CMF_DSPARK_CONF_MIN")
7599 .ok()
7600 .and_then(|v| v.parse().ok())
7601 .unwrap_or(0.0)
7602 })
7603 };
7604 if conf_min > 0.0 && conf.len() >= props.len() {
7605 let mut keep = 1usize;
7606 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
7607 keep += 1;
7608 }
7609 k_verify = k_verify.min(keep.max(2));
7610 }
7611 if k_verify < 2 {
7612 return None;
7613 }
7614 let mut fed = Vec::with_capacity(k_verify);
7615 fed.push(t_next);
7616 fed.extend_from_slice(&props[1..k_verify]);
7617 let mut argmax = Vec::new();
7618 let mut logits_all = Vec::new();
7619 let mut walked = Vec::new();
7620 let txn = crate::dsv4::dsv4_verify_chunk(
7621 g,
7622 layers,
7623 &cfg,
7624 st,
7625 &fed,
7626 next_pos,
7627 &self.inv_freq,
7628 self.pool.as_deref(),
7629 &targets,
7630 &mut argmax,
7631 &mut logits_all,
7632 &mut walked,
7633 );
7634 if txn.is_none() && dbg {
7635 eprintln!("spec_step: verify отказал");
7636 }
7637 let txn = txn?;
7638 let spec_gpu_end = txn.gpu_end;
7639 let b = fed.len();
7640 let mut accepted = 1usize;
7641 while accepted < b && fed[accepted] == argmax[accepted - 1] {
7642 accepted += 1;
7643 }
7644 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
7649 accepted = 1;
7650 }
7651 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
7652 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
7653 }
7654 let t_fin = std::time::Instant::now();
7655 if !crate::dsv4::dsv4_spec_finish(
7656 g,
7657 layers,
7658 &cfg,
7659 st,
7660 txn,
7661 accepted,
7662 &fed,
7663 &self.inv_freq,
7664 self.pool.as_deref(),
7665 ) {
7666 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
7667 return None;
7668 }
7669 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7670 eprintln!(
7671 "finish(k={accepted}): {:.1} мс",
7672 t_fin.elapsed().as_secs_f64() * 1e3
7673 );
7674 }
7675 *accepted_ctr += accepted - 1;
7676 let (hc, dim) = (cfg.hc_mult, cfg.dim);
7681 let dev_caps: Vec<usize> = targets
7686 .iter()
7687 .copied()
7688 .filter(|&t| t < spec_gpu_end)
7689 .collect();
7690 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
7691 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
7692 return None;
7693 }
7694 for t in 0..accepted {
7695 let tip = t + 1 == accepted;
7696 for (slot, &tl) in targets.iter().enumerate() {
7697 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
7698 let lo = (di * b + t) * hc * dim;
7699 crate::dsv4::dspark_capture(
7700 &caps_all[lo..lo + hc * dim],
7701 &cfg,
7702 slot,
7703 &mut ds.main_hidden,
7704 );
7705 } else if tip
7706 && crate::dsv4::dspark_peek_slot(slot, dim, {
7707 let lo = slot * dim;
7708 &mut ds.main_hidden[lo..lo + dim]
7709 })
7710 {
7711 } else {
7716 crate::dsv4::dspark_capture(
7720 &walked[t * hc * dim..(t + 1) * hc * dim],
7721 &cfg,
7722 slot,
7723 &mut ds.main_hidden,
7724 );
7725 }
7726 }
7727 crate::dsv4::dspark_ring_append(
7728 g,
7729 &self.dsv4_mtp,
7730 &cfg,
7731 ds,
7732 next_pos + t,
7733 self.pool.as_deref(),
7734 );
7735 }
7736 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
7737 self.graph_logits = Some(row);
7738 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
7743 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
7744 crate::dsv4::pick_tally_arm();
7745 }
7746 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7747 eprintln!(
7748 "spec_step total {:.1} мс (k={accepted})",
7749 t_all.elapsed().as_secs_f64() * 1e3
7750 );
7751 }
7752 Some((fed[1..accepted].to_vec(), next_pos + accepted))
7753 }
7754
7755 fn dspark_probe(&mut self, position: usize, token_id: u32) {
7756 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
7757 return;
7758 }
7759 let trunk_now = crate::dsv4::pick_tally_take();
7761 crate::dsv4::trunk_freq_note(&trunk_now);
7762 if !trunk_now.is_empty() {
7763 self.dspark_trunk_picks.push(trunk_now);
7764 let keep = crate::dsv4::dspark_block();
7765 if self.dspark_trunk_picks.len() > keep {
7766 self.dspark_trunk_picks.remove(0);
7767 }
7768 }
7769 for p in std::mem::take(&mut self.dspark_pending) {
7772 let Some(i) = position.checked_sub(p.0 + 1) else {
7773 continue;
7774 };
7775 let mut p = p;
7776 if i < p.1.len() {
7777 if p.2 && p.1[i] == token_id {
7778 p.3 = i + 1;
7779 } else {
7780 p.2 = false;
7781 }
7782 if i + 1 < p.1.len() {
7783 self.dspark_pending.push(p);
7784 continue;
7785 }
7786 }
7787 self.dspark_hist.push(p.3);
7788 self.dspark_real.push(token_id);
7789 }
7790 let Some(b) = &mut self.dsv4 else { return };
7791 let (g, layers, cfg) = (&b.0, &b.1, b.2);
7792 let n_layers = layers.len();
7793 if self.dspark.is_none() {
7794 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7795 if t.is_empty() {
7796 return;
7797 }
7798 eprintln!(
7799 "DSpark: захват со слоёв {t:?}, блок {}",
7800 crate::dsv4::dspark_block()
7801 );
7802 crate::dsv4::dspark_arm(&t, cfg.dim);
7803 self.dspark = Some(crate::dsv4::DsparkState::new(
7804 self.dsv4_mtp.len(),
7805 &cfg,
7806 t.len(),
7807 ));
7808 }
7809 let ds = self.dspark.as_mut().unwrap();
7810 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
7811 return; }
7813 let mut conf = Vec::new();
7814 crate::dsv4::pick_tally_arm();
7815 let draft_started = std::time::Instant::now();
7820 #[cfg(feature = "gpu")]
7821 let gpu_draft = crate::dsv4::dspark_gpu_on();
7822 #[cfg(not(feature = "gpu"))]
7823 let gpu_draft = false;
7824 let props = if gpu_draft {
7825 #[cfg(feature = "gpu")]
7826 {
7827 let kv_id = b.3.kv_id;
7828 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
7829 Some(pk) => crate::dsv4::dspark_draft_gpu(
7830 g,
7831 &self.dsv4_mtp,
7832 &cfg,
7833 ds,
7834 pk,
7835 kv_id,
7836 token_id,
7837 position,
7838 self.pool.as_deref(),
7839 &mut conf,
7840 ),
7841 None => Vec::new(),
7842 }
7843 }
7844 #[cfg(not(feature = "gpu"))]
7845 Vec::new()
7846 } else {
7847 crate::gpu::cpu_scope(|| {
7848 crate::dsv4::dspark_draft(
7849 g,
7850 &self.dsv4_mtp,
7851 &cfg,
7852 ds,
7853 token_id,
7854 position,
7855 self.pool.as_deref(),
7856 &mut conf,
7857 )
7858 })
7859 };
7860 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7861 let draft_picks = crate::dsv4::pick_tally_take();
7862 crate::dsv4::dspark_freq_note(&draft_picks);
7863 crate::dsv4::pick_tally_arm();
7866 if !props.is_empty() {
7867 let (tu, tt) = {
7871 let flat: Vec<(usize, Vec<usize>)> = self
7872 .dspark_trunk_picks
7873 .iter()
7874 .flat_map(|v| v.iter().cloned())
7875 .collect();
7876 let mut per: std::collections::HashMap<usize, Vec<usize>> =
7878 std::collections::HashMap::new();
7879 for (li, picks) in flat {
7880 per.entry(li).or_default().extend(picks);
7881 }
7882 let n = per.len().max(1);
7883 let mut u = 0usize;
7884 let mut t = 0usize;
7885 for (_, v) in per {
7886 t += v.len();
7887 u += v.iter().collect::<std::collections::HashSet<_>>().len();
7888 }
7889 (u / n, t / n)
7890 };
7891 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
7892 self.dspark_exp.push((tu, tt, du, dt));
7893 self.dspark_pending.push((position, props, true, 0));
7894 }
7895 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
7896 let n = self.dspark_hist.len() as f32;
7897 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
7898 let block = crate::dsv4::dspark_block();
7899 let mut at = vec![0usize; block + 1];
7900 for &k in &self.dspark_hist {
7901 at[k] += 1;
7902 }
7903 let mut surv = Vec::with_capacity(block);
7905 for i in 1..=block {
7906 let k = at[i..].iter().sum::<usize>() as f32 / n;
7907 surv.push(format!("{k:.2}"));
7908 }
7909 let distinct = self
7910 .dspark_real
7911 .iter()
7912 .collect::<std::collections::HashSet<_>>()
7913 .len();
7914 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
7915 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
7916 });
7917 let m = self.dspark_exp.len().max(1);
7918 eprintln!(
7919 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
7920 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
7921 self.dspark_hist.len(),
7922 mean + 1.0,
7923 surv.join(" ")
7924 );
7925 eprintln!(
7926 "DSpark: разных токенов {distinct} из {} (вырожденность), \
7927 эксперты ствол {}/{} на слой за {block} токенов, \
7928 черновик {}/{} за блок, draft {:.2} мс/блок",
7929 self.dspark_real.len(),
7930 tu / m,
7931 tt / m,
7932 du / m,
7933 dt / m,
7934 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
7935 );
7936 }
7937 }
7938
7939 fn forward_layers_upto(
7940 &mut self,
7941 hidden: &[f32],
7942 position: usize,
7943 task_mask: Option<&TaskMask>,
7944 upto: Option<usize>,
7945 ) -> Vec<f32> {
7946 if let Some(plan) = self.gpu_plan.clone() {
7952 if upto.is_none() && plan.len() > 1 {
7953 let mut h = hidden.to_vec();
7954 for &(dev, from, upto_incl) in plan.iter() {
7955 h = crate::gpu::with_device(dev, || {
7956 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
7957 });
7958 }
7959 return h;
7960 }
7961 }
7962 self.forward_layers_span(hidden, position, task_mask, 0, upto)
7963 }
7964
7965 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
7970 self.set_gpu_plan_at(devices, None)
7971 }
7972
7973 pub fn set_gpu_plan_at(
7977 &mut self,
7978 devices: Option<&[usize]>,
7979 at: Option<usize>,
7980 ) -> Result<(), String> {
7981 let Some(devs) = devices.filter(|d| d.len() > 1) else {
7982 self.gpu_plan = None;
7983 return Ok(());
7984 };
7985 self.split_supported()?;
7986 let n = self.num_layers;
7987 if devs.len() > n {
7988 return Err(format!("{} devices for {n} layers", devs.len()));
7989 }
7990 if let Some(k) = at {
7991 if k == 0 || k >= n {
7992 return Err(format!("split at {k}: the model has {n} layers"));
7993 }
7994 if devs.len() == 2 {
7995 self.gpu_plan = Some(std::sync::Arc::new(vec![
7996 (devs[0], 0, k - 1),
7997 (devs[1], k, n - 1),
7998 ]));
7999 return Ok(());
8000 }
8001 return Err(format!(
8002 "an explicit split point takes exactly 2 devices, got {}",
8003 devs.len()
8004 ));
8005 }
8006 let per = n.div_ceil(devs.len());
8007 let mut plan = Vec::with_capacity(devs.len());
8008 let mut from = 0usize;
8009 for &d in devs {
8010 if from >= n {
8011 break;
8012 }
8013 let upto = (from + per - 1).min(n - 1);
8014 plan.push((d, from, upto));
8015 from = upto + 1;
8016 }
8017 self.gpu_plan = Some(std::sync::Arc::new(plan));
8018 Ok(())
8019 }
8020
8021 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
8023 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
8024 }
8025
8026 fn forward_layers_span(
8032 &mut self,
8033 hidden: &[f32],
8034 position: usize,
8035 task_mask: Option<&TaskMask>,
8036 from: usize,
8037 upto: Option<usize>,
8038 ) -> Vec<f32> {
8039 debug_assert!(from == 0 || (self.dsv4.is_none() && self.g3n.is_none()));
8040 if let Some(b) = &mut self.dsv4 {
8046 let _ = (task_mask, upto);
8047 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
8048 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
8049 st.pos = position;
8050 let mut logits = Vec::new();
8051 crate::dsv4::forward_token(
8052 g,
8053 layers,
8054 &cfg,
8055 st,
8056 token_id,
8057 &self.inv_freq,
8058 self.pool.as_deref(),
8059 &mut logits,
8060 );
8061 self.graph_logits = Some(logits);
8062 self.dspark_probe(position, token_id);
8063 return vec![0.0; self.hidden_size];
8066 }
8067 if let Some(b) = &self.g3n {
8070 let _ = (task_mask, upto);
8071 return crate::g3n::g3n_forward(
8072 &b.0,
8073 &b.1,
8074 hidden,
8075 position,
8076 &mut self.kv_cache.layers,
8077 self.num_heads,
8078 self.num_kv_heads,
8079 self.head_dim,
8080 self.pool.as_deref(),
8081 );
8082 }
8083 let mut h = hidden.to_vec();
8084 let (nh, _nkv, _hd, hs, _rd, eps) = (
8087 self.num_heads,
8088 self.num_kv_heads,
8089 self.head_dim,
8090 self.hidden_size,
8091 self.rotary_dim,
8092 self.rms_eps,
8093 );
8094 let pool = self.pool.clone();
8095 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
8107 let graph_on = match graph_env.as_deref() {
8108 Some("0") => false,
8109 Some("prefill") => false, Some(_) => true,
8111 None => crate::gpu::wgpu_graph_default(),
8117 };
8118 let graph_trusted =
8119 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
8120 let race_eligible = graph_on
8121 && upto.is_none()
8122 && task_mask.is_none()
8123 && from == 0
8124 && !crate::gpu::graph_unsupported();
8125 let mut tail_start = 0usize;
8126 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
8127 let t_graph = std::time::Instant::now();
8128 let mut lg = Vec::new();
8129 let mut gl = 0usize;
8130 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
8131 if built.is_none() && !self.o1_active() && self.attn_softcap == 0.0 {
8136 crate::gpu::graph_mark_unsupported();
8137 }
8138 graph_note(built.is_some());
8139 if let Some(hh) = built {
8140 let dur = t_graph.elapsed();
8141 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8142 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
8143 }
8144 if gl > 0 && gl < self.num_layers {
8145 h = hh;
8151 tail_start = gl;
8152 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
8153 if !graph_trusted {
8154 crate::gpu::graph_race_record(true, dur);
8155 }
8156 if !lg.is_empty() {
8157 lg.resize(self.vocab_size, 0.0);
8160 if let Some(c) = self.final_softcap {
8161 for l in lg.iter_mut() {
8162 *l = c * (*l / c).tanh();
8163 }
8164 }
8165 self.graph_logits = Some(lg);
8166 }
8167 return hh;
8168 }
8169 }
8175 }
8176 let span = from > 0 || upto.is_some();
8200 if span && graph_on && task_mask.is_none() && graph_trusted {
8201 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
8202 let mut lg = Vec::new();
8203 let mut gl = 0usize;
8204 let span_res =
8205 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
8206 graph_note(span_res.is_some() && gl == upto_excl - from);
8207 if std::env::var("CMF_GPU_DEBUG").is_ok() {
8208 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
8212 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
8213 eprintln!(
8214 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
8215 upto_excl - from,
8216 span_res.is_some()
8217 );
8218 }
8219 }
8220 if let Some(hh) = span_res {
8221 if gl == upto_excl - from {
8222 if !lg.is_empty() {
8223 lg.resize(self.vocab_size, 0.0);
8224 if let Some(c) = self.final_softcap {
8225 for l in lg.iter_mut() {
8226 *l = c * (*l / c).tanh();
8227 }
8228 }
8229 self.graph_logits = Some(lg);
8230 }
8231 crate::gpu::set_layer(-1);
8232 return hh;
8233 }
8234 h = hh;
8236 tail_start = from + gl;
8237 }
8238 }
8239 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
8240
8241 #[cfg(target_os = "macos")]
8242 let mut gpu_skip_until = 0usize;
8243 for li in tail_start.max(from)..self.num_layers {
8244 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
8246 if li > u {
8247 break;
8248 }
8249 }
8250 if let Some(mask) = task_mask {
8251 if !mask.layer_alive(li) {
8252 continue; }
8254 }
8255 #[cfg(target_os = "macos")]
8259 {
8260 if li < gpu_skip_until {
8261 continue;
8262 }
8263 if task_mask.is_none() {
8264 let end = self.q1_graph_gpu(li, upto, position, &mut h);
8265 if end > li {
8266 gpu_skip_until = end;
8267 if self.is_loop_end(end - 1) && end < self.num_layers {
8270 h = inference::rms_norm(
8271 &h,
8272 &self.weights.final_norm,
8273 self.rms_eps,
8274 self.norm_style,
8275 );
8276 }
8277 continue;
8278 }
8279 }
8280 }
8281
8282 let lw = &self.weights.layers[self.phys_layer(li)];
8283 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8284 if tp.parse::<usize>().ok() == Some(position) {
8285 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
8286 eprintln!(
8287 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
8288 h[0], h[1]
8289 );
8290 }
8291 }
8292 inference::rms_norm_into(
8295 &h,
8296 &lw.input_norm,
8297 self.rms_eps,
8298 self.norm_style,
8299 &mut self.ws.n1,
8300 );
8301
8302 let attn_out = match &lw.attn {
8303 AttnKind::Mla(w) => {
8304 let inv_freq_l = self.layer_inv_freq(li);
8305 let rs = self.layer_rope_scale(li);
8306 let eps = self.rms_eps;
8307 let pool = self.pool.clone();
8308 mla_attention(
8309 w,
8310 &self.ws.n1,
8311 &mut self.kv_cache.layers[li],
8312 position,
8313 &inv_freq_l,
8314 rs,
8315 eps,
8316 pool.as_deref(),
8317 )
8318 }
8319 AttnKind::Linear(w) => {
8320 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8321 vmf_phase_forward(
8322 &self.ws.n1,
8323 w,
8324 &cfg,
8325 &mut self.kv_cache.layers[li].linear_state,
8326 self.pool.as_deref(),
8327 )
8328 }
8329 AttnKind::Kda(w) => {
8330 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
8331 crate::linear_core::kda_forward(
8332 &self.ws.n1,
8333 w,
8334 &cfg,
8335 &mut self.kv_cache.layers[li].linear_state,
8336 self.pool.as_deref(),
8337 )
8338 }
8339 AttnKind::LinearGdn(w) => {
8340 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8341 gdn_forward(
8342 &self.ws.n1,
8343 w,
8344 &cfg,
8345 &mut self.kv_cache.layers[li].linear_state,
8346 self.pool.as_deref(),
8347 )
8348 }
8349 AttnKind::ShortConv(w) => {
8350 let cfg = self
8351 .short_conv_cfg
8352 .expect("short-conv layer without short_conv_cfg");
8353 short_conv_forward(
8354 &self.ws.n1,
8355 w,
8356 &cfg,
8357 &mut self.kv_cache.layers[li].linear_state,
8358 self.pool.as_deref(),
8359 )
8360 }
8361 AttnKind::Full {
8362 wq,
8363 wk,
8364 wv,
8365 wo,
8366 q_norm,
8367 k_norm,
8368 output_gate,
8369 softplus_gate,
8370 bias,
8371 } if self.kv_cache.layers[li].o1_sealed() => {
8372 let inv_freq_l = self.layer_inv_freq(li);
8375 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8376 let cfg = QwenAttnCfg {
8377 num_heads: self.layer_num_heads(li),
8378 num_kv_heads: nkv_l,
8379 head_dim: hd_l,
8380 hidden_size: hs,
8381 position,
8382 inv_freq: &inv_freq_l,
8383 rotary_dim: rd_l,
8384 scale: self.attn_scale,
8385 softcap: self.attn_softcap,
8386 window: None,
8387 v_norm: self.attn_v_norm,
8388 q_norm: q_norm.as_deref(),
8389 k_norm: k_norm.as_deref(),
8390 output_gate: *output_gate,
8391 softplus_gate: softplus_gate
8392 .as_ref()
8393 .map(|(gate, per_head)| (gate, *per_head)),
8394 rope_scale: self.layer_rope_scale(li),
8395 bias: bias
8396 .as_ref()
8397 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8398 rms_eps: eps,
8399 norm_style: self.norm_style,
8400 pool: pool.as_deref(),
8401 };
8402 attention::qwen_attention_nystrom(
8403 &self.ws.n1,
8404 wq,
8405 wk,
8406 wv,
8407 wo,
8408 &mut self.kv_cache.layers[li],
8409 &cfg,
8410 )
8411 }
8412 AttnKind::Full {
8413 wq,
8414 wk,
8415 wv,
8416 wo,
8417 q_norm,
8418 k_norm,
8419 output_gate,
8420 softplus_gate,
8421 bias,
8422 } => 'attn: {
8423 if graph_on
8426 && !*output_gate
8427 && softplus_gate.is_none()
8428 && self.attention_heads_per_layer.is_none()
8429 && bias.is_none()
8430 && task_mask.is_none()
8431 {
8432 let inv_freq_l = self.layer_inv_freq(li);
8433 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8434 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8435 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
8436 wq.mapped_q1(),
8437 wk.mapped_q1(),
8438 wv.mapped_q1(),
8439 wo.mapped_q1(),
8440 ) {
8441 let gm = gm.clone();
8442 let mut out = vec![0f32; hs];
8443 let cache = &self.kv_cache.layers[li];
8444 if crate::gpu::attn_dropin(
8445 &gm,
8446 self.graph_kv_id,
8447 li,
8448 &self.ws.n1,
8449 qi,
8450 ki,
8451 vi,
8452 oi,
8453 q_norm.as_deref(),
8454 k_norm.as_deref(),
8455 &inv_freq_l,
8456 nh,
8457 nkv_l,
8458 hd_l,
8459 rd_l,
8460 hs,
8461 position,
8462 self.kv_cache.max_seq_len,
8463 gemma,
8464 eps as f32,
8465 cache.k_heads(),
8466 cache.v_heads(),
8467 &mut out,
8468 ) {
8469 break 'attn out;
8470 }
8471 }
8472 }
8473 let masked = task_mask
8474 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
8475 .unwrap_or(false);
8476 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
8477 match (masked, f32_view) {
8478 (true, (Some(q), Some(k), Some(v), Some(o))) => {
8481 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
8482 attention::multi_head_attention(
8483 &self.ws.n1,
8484 q,
8485 k,
8486 v,
8487 o,
8488 &mut self.kv_cache.layers[li],
8489 self.num_heads,
8490 self.num_kv_heads,
8491 self.head_dim,
8492 self.hidden_size,
8493 position,
8494 &active_heads,
8495 &self.inv_freq,
8496 )
8497 }
8498 (masked, _) => {
8499 if masked {
8500 tracing::warn!(
8501 "layer {li}: head mask on quantized weights not \
8502 supported yet — executing dense"
8503 );
8504 }
8505 let inv_freq_l = self.layer_inv_freq(li);
8506 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8507 let cfg = QwenAttnCfg {
8508 num_heads: self.layer_num_heads(li),
8509 num_kv_heads: nkv_l,
8510 head_dim: hd_l,
8511 hidden_size: hs,
8512 position,
8513 inv_freq: &inv_freq_l,
8514 rotary_dim: rd_l,
8515 scale: self.attn_scale,
8516 softcap: self.attn_softcap,
8517 window: self.layer_window(li),
8518 v_norm: self.attn_v_norm,
8519 q_norm: q_norm.as_deref(),
8520 k_norm: k_norm.as_deref(),
8521 output_gate: *output_gate,
8522 softplus_gate: softplus_gate
8523 .as_ref()
8524 .map(|(gate, per_head)| (gate, *per_head)),
8525 rope_scale: self.layer_rope_scale(li),
8526 bias: bias
8527 .as_ref()
8528 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8529 rms_eps: eps,
8530 norm_style: self.norm_style,
8531 pool: pool.as_deref(),
8532 };
8533 attention::qwen_attention(
8534 &self.ws.n1,
8535 wq,
8536 wk,
8537 wv,
8538 wo,
8539 &mut self.kv_cache.layers[li],
8540 &cfg,
8541 )
8542 }
8543 }
8544 }
8545 };
8546 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8549 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
8550 None => attn_out,
8551 };
8552 let lw = &self.weights.layers[self.phys_layer(li)];
8553 inference::add_rmsnorm_fused_into(
8554 &mut h,
8555 &attn_out,
8556 &lw.post_norm,
8557 self.rms_eps,
8558 self.norm_style,
8559 &mut self.ws.p1,
8560 );
8561 let mut attn_out = attn_out;
8562 attention::recycle_buf(&mut attn_out);
8563 let post_normed = &self.ws.p1;
8564
8565 let ffn_masked = task_mask
8566 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
8567 .unwrap_or(false);
8568 let ffn_out = match (ffn_masked, &lw.ffn) {
8580 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
8584 let row = task_mask
8585 .and_then(|tm| tm.ffn_masks.get(li))
8586 .map(|v| v.as_slice());
8587 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
8588 }
8589 (true, FfnKind::Dense(d)) => {
8590 let tm = task_mask.unwrap();
8591 let alive = tm.ffn_active_count(li);
8592 let deep = alive * 2 <= self.intermediate_size;
8593 if deep && d.down_proj.sparse_col_ok() {
8594 let active = tm.ffn_active_indices(li);
8595 sparse_ffn_quant(
8596 d,
8597 post_normed,
8598 &active,
8599 self.hidden_size,
8600 self.pool.as_deref(),
8601 )
8602 } else if deep
8603 && let (Some(g), Some(u), Some(dn)) = (
8604 d.gate_proj.as_f32(),
8605 d.up_proj.as_f32(),
8606 d.down_proj.as_f32(),
8607 )
8608 {
8609 let active = tm.ffn_active_indices(li);
8610 inference::sparse_ffn_forward(
8611 post_normed,
8612 g,
8613 u,
8614 dn,
8615 self.hidden_size,
8616 self.intermediate_size,
8617 &active,
8618 self.pool.as_deref(),
8619 )
8620 } else {
8621 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
8622 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
8623 }
8624 }
8625 (true, FfnKind::Moe(m)) => {
8626 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
8630 ffn_forward(
8631 &lw.ffn,
8632 post_normed,
8633 self.pool.as_deref(),
8634 allowed.as_deref(),
8635 )
8636 }
8637 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
8638 dm,
8639 post_normed,
8640 &h,
8641 self.rms_eps,
8642 self.norm_style,
8643 self.pool.as_deref(),
8644 ),
8645 (false, _) => match &lw.ffn {
8646 FfnKind::DenseMoe(dm) => dense_moe_ffn(
8647 dm,
8648 post_normed,
8649 &h,
8650 self.rms_eps,
8651 self.norm_style,
8652 self.pool.as_deref(),
8653 ),
8654 _ => {
8655 let allowed = match (&lw.ffn, task_mask) {
8656 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
8657 _ => None,
8658 };
8659 ffn_forward(
8660 &lw.ffn,
8661 post_normed,
8662 self.pool.as_deref(),
8663 allowed.as_deref(),
8664 )
8665 }
8666 },
8667 };
8668 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8669 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
8670 None => ffn_out,
8671 };
8672 for (i, &f) in ffn_out.iter().enumerate() {
8673 h[i] += f;
8674 }
8675 let mut ffn_out = ffn_out;
8676 attention::recycle_buf(&mut ffn_out);
8677
8678 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8680 for v in h.iter_mut() {
8681 *v *= sc;
8682 }
8683 }
8684
8685 if self.is_loop_end(li) && li + 1 < self.num_layers {
8688 h = inference::rms_norm(
8689 &h,
8690 &self.weights.final_norm,
8691 self.rms_eps,
8692 self.norm_style,
8693 );
8694 }
8695
8696 if self.dyn_phi_layer == Some(li) {
8700 self.update_dyn_phi(&h);
8701 }
8702 }
8703 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
8705 crate::gpu::graph_race_record(false, t.elapsed());
8706 }
8707
8708 h
8709 }
8710
8711 fn update_dyn_phi(&mut self, h: &[f32]) {
8714 const A: f32 = 0.2;
8715 if self.dyn_phi_ema.len() != h.len() {
8716 self.dyn_phi_ema = vec![0.0; h.len()];
8717 self.dyn_phi_seen = 0;
8718 }
8719 if self.dyn_phi_seen == 0 {
8720 self.dyn_phi_ema.copy_from_slice(h);
8721 } else {
8722 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
8723 *e = (1.0 - A) * *e + A * v;
8724 }
8725 }
8726 self.dyn_phi_seen += 1;
8727 }
8728
8729 pub fn dyn_phi(&self) -> &[f32] {
8731 &self.dyn_phi_ema
8732 }
8733
8734 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
8736 self.dyn_phi_layer = layer;
8737 self.dyn_phi_ema.clear();
8738 self.dyn_phi_seen = 0;
8739 }
8740
8741 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
8743 let Some(model) = &self.model else {
8744 return Vec::new();
8745 };
8746 model
8747 .header
8748 .skills
8749 .iter()
8750 .enumerate()
8751 .filter_map(|(i, sk)| {
8752 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
8753 let sel = sk.selection.as_ref()?;
8754 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
8755 })
8756 .collect()
8757 }
8758
8759 pub fn active_skill(&self) -> Option<usize> {
8761 self.dyn_active
8762 }
8763
8764 pub fn enable_dynamic_routing(&mut self) -> usize {
8769 use crate::swarm::{DynRouter, RoutableSkill};
8770 let Some(model) = self.model.clone() else {
8771 return 0;
8772 };
8773 if self.dyn_blend_loaded {
8776 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
8777 return 0;
8778 }
8779 if let Some(a) = self.dyn_active {
8783 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
8784 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
8785 return 0;
8786 }
8787 }
8788 let hidden = self.hidden_size;
8789 let mut skills = Vec::new();
8790 for (idx, id, _phi) in self.dynamic_skills() {
8791 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
8792 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
8793 skills.push(rs);
8794 }
8795 }
8796 }
8797 if skills.is_empty() {
8798 return 0;
8799 }
8800 let phi = skills[0].phi_layer;
8802 if skills.iter().any(|s| s.phi_layer != phi) {
8803 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
8804 }
8805 let n = skills.len();
8806 self.set_dyn_phi_layer(Some(phi));
8807 self.dyn_router = Some(DynRouter::new(skills));
8808 n
8809 }
8810
8811 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
8813 self.dyn_router
8814 .as_ref()
8815 .map(|r| r.switches.clone())
8816 .unwrap_or_default()
8817 }
8818
8819 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
8822 let rows = self.weights.lm_head.rows();
8823 let mut logits = attention::take_buf(rows.min(self.vocab_size));
8824 self.weights
8825 .lm_head
8826 .matvec(hidden, &mut logits, self.pool.as_deref());
8827 logits.resize(self.vocab_size, 0.0);
8828 if let Some(m) = self.logit_multiplier {
8829 for l in logits.iter_mut() {
8830 *l *= m;
8831 }
8832 }
8833 if let Some(c) = self.final_softcap {
8834 for l in logits.iter_mut() {
8835 *l = c * (*l / c).tanh();
8836 }
8837 }
8838 if let Some(cm) = self.head_clusters.as_ref() {
8839 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
8840 }
8841 logits
8842 }
8843
8844 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
8847 let h = hidden.len();
8848 let ncl = cm.len() / h.max(1);
8849 if ncl == 0 || logits.len() % ncl != 0 {
8850 return;
8851 }
8852 let cs = logits.len() / ncl;
8853 let mut lc = vec![0.0f32; ncl];
8855 for c in 0..ncl {
8856 let row = &cm[c * h..(c + 1) * h];
8857 let mut s = 0.0f32;
8858 for j in 0..h {
8859 s += row[j] * hidden[j];
8860 }
8861 lc[c] = s;
8862 }
8863 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8864 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
8865 for c in 0..ncl {
8866 let blk = &mut logits[c * cs..(c + 1) * cs];
8867 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8868 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
8869 let add = lc[c] - lse - bl;
8870 for v in blk.iter_mut() {
8871 *v += add;
8872 }
8873 }
8874 }
8875
8876 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
8881 self.kv_cache.clear();
8882 self.kv_history.clear();
8883 let mut hidden = vec![0.0f32; self.hidden_size];
8884 for (pos, &id) in ids.iter().enumerate() {
8885 let emb = self.embed_single(id);
8886 hidden = self.forward_layers(&emb, pos, task_mask);
8887 }
8888 inference::rms_norm_into(
8889 &hidden,
8890 &self.weights.final_norm,
8891 self.rms_eps,
8892 self.norm_style,
8893 &mut self.ws.n1,
8894 );
8895 self.lm_head_forward(&self.ws.n1)
8896 }
8897}
8898
8899pub fn create_test_pipeline(
8901 hidden_size: usize,
8902 intermediate_size: usize,
8903 num_heads: usize,
8904 num_kv_heads: usize,
8905 head_dim: usize,
8906 num_layers: usize,
8907 vocab_size: usize,
8908) -> Pipeline {
8909 let synth = |n: usize, salt: usize| -> Vec<f32> {
8912 (0..n)
8913 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
8914 .collect()
8915 };
8916 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
8917 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
8918 };
8919 let layer_weights: Vec<LayerWeights> = (0..num_layers)
8920 .map(|li| LayerWeights {
8921 input_norm: vec![1.0; hidden_size],
8922 post_norm: vec![1.0; hidden_size],
8923 attn_out_norm: None,
8924 ffn_out_norm: None,
8925 layer_scale: None,
8926 ffn: FfnKind::Dense(DenseFfn {
8927 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
8928 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
8929 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
8930 act: Act::Silu,
8931 down_t: None,
8932 segs: Vec::new(),
8933 }),
8934 attn: AttnKind::Full {
8935 bias: None,
8936 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
8937 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
8938 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
8939 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
8940 q_norm: None,
8941 k_norm: None,
8942 output_gate: false,
8943 softplus_gate: None,
8944 },
8945 })
8946 .collect();
8947
8948 Pipeline::new(
8949 Tokenizer::byte_level(),
8950 PipelineWeights {
8951 embed_tokens: qt(vocab_size, hidden_size, 100),
8952 layers: layer_weights,
8953 lm_head: qt(vocab_size, hidden_size, 200),
8954 final_norm: vec![1.0; hidden_size],
8955 },
8956 hidden_size,
8957 intermediate_size,
8958 num_heads,
8959 num_kv_heads,
8960 head_dim,
8961 num_layers,
8962 num_layers, false, vocab_size,
8965 1e-6,
8966 10_000.0,
8967 NormStyle::Qwen,
8968 4096,
8969 SamplerConfig {
8970 seed: Some(42),
8971 ..Default::default()
8972 },
8973 )
8974}
8975
8976#[inline]
8981fn mask_bit(row: &[u8], j: usize) -> bool {
8982 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
8983}
8984
8985fn mask_gain() -> f32 {
8996 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
8997 *G.get_or_init(|| {
8998 std::env::var("CMF_FFN_MASK_GAIN")
8999 .ok()
9000 .and_then(|v| v.parse().ok())
9001 .unwrap_or(1.0)
9002 })
9003}
9004
9005fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
9006 let fill = meanfill().and_then(|(i, v)| {
9009 let li = crate::gpu::cur_layer();
9010 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
9011 });
9012 for r in 0..rows {
9013 let base = r * inter;
9014 for (bi, &byte) in row.iter().enumerate() {
9015 if byte == 0xFF {
9016 continue;
9017 }
9018 let j0 = bi * 8;
9019 for bit in 0..8 {
9020 let j = j0 + bit;
9021 if j < inter && byte & (1 << bit) == 0 {
9022 g[base + j] = fill.map_or(0.0, |f| f[j]);
9023 }
9024 }
9025 }
9026 }
9027 let gain = mask_gain();
9028 if gain != 1.0 {
9029 for v in g[..rows * inter].iter_mut() {
9030 *v *= gain;
9031 }
9032 }
9033}
9034
9035#[inline]
9037fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
9038 row.is_none_or(|r| mask_bit(r, i))
9039}
9040
9041fn all_bits_on(row: &[u8], n: usize) -> bool {
9044 (0..n).all(|i| mask_bit(row, i))
9045}
9046
9047fn tube_topk() -> usize {
9055 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9056 *K.get_or_init(|| {
9057 std::env::var("CMF_TUBE_TOPK")
9058 .ok()
9059 .and_then(|v| v.parse().ok())
9060 .unwrap_or(0)
9061 })
9062}
9063
9064fn tube_score_oracle() -> bool {
9065 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9066 *O.get_or_init(|| {
9067 std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle")
9068 })
9069}
9070
9071fn tube_ffn_routed(
9078 d: &DenseFfn,
9079 xs: &[f32],
9080 b: usize,
9081 pool: Option<&Pool>,
9082 mask_row: Option<&[u8]>,
9083 k: usize,
9084) -> Vec<f32> {
9085 let hidden = d.down_proj.rows();
9086 let core = d.gate_proj.rows();
9087 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
9088 let mut out = match (b, core_full, mask_row) {
9089 (1, true, _) => dense_ffn(d, xs, pool),
9090 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
9091 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
9092 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
9093 };
9094 let cand: Vec<usize> = (0..d.segs.len())
9095 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
9096 .collect();
9097 if cand.is_empty() {
9098 return out;
9099 }
9100 let oracle = tube_score_oracle();
9104 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
9105 let mut scores = vec![0f32; b * cand.len()];
9106 for (ci, &i) in cand.iter().enumerate() {
9107 let seg = &d.segs[i];
9108 let w = seg.width;
9109 let mut g = vec![0.0f32; b * w];
9110 if b == 1 {
9111 seg.gate.matvec(xs, &mut g, pool);
9112 } else {
9113 seg.gate.matmat(xs, b, &mut g, pool);
9114 }
9115 for v in g.iter_mut() {
9116 *v = Act::Silu.combine(*v, 1.0);
9117 }
9118 if !oracle {
9119 for t in 0..b {
9120 scores[t * cand.len() + ci] =
9121 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
9122 }
9123 }
9124 if oracle || b > 1 {
9125 let mut u = vec![0.0f32; b * w];
9126 if b == 1 {
9127 seg.up.matvec(xs, &mut u, pool);
9128 } else {
9129 seg.up.matmat(xs, b, &mut u, pool);
9130 }
9131 for (a, &v) in g.iter_mut().zip(u.iter()) {
9132 *a *= v;
9133 }
9134 if oracle {
9135 for t in 0..b {
9136 scores[t * cand.len() + ci] =
9137 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
9138 }
9139 }
9140 }
9141 acts.push(g);
9142 }
9143 let keep = k.min(cand.len());
9145 let mut scratch: Vec<f32> = Vec::new();
9146 for t in 0..b {
9147 let mut sc: Vec<(f32, usize)> = (0..cand.len())
9148 .map(|ci| (scores[t * cand.len() + ci], ci))
9149 .collect();
9150 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
9151 let mut alive = vec![false; cand.len()];
9152 for &(_, ci) in sc.iter().take(keep) {
9153 alive[ci] = true;
9154 }
9155 if b > 1 {
9156 for (ci, a) in acts.iter_mut().enumerate() {
9157 if !alive[ci] {
9158 let w = d.segs[cand[ci]].width;
9159 a[t * w..(t + 1) * w].fill(0.0);
9160 }
9161 }
9162 } else {
9163 for (ci, &i) in cand.iter().enumerate() {
9167 if !alive[ci] {
9168 continue;
9169 }
9170 let seg = &d.segs[i];
9171 let w = seg.width;
9172 let g = &mut acts[ci];
9173 if !tube_score_oracle() {
9174 scratch.clear();
9175 scratch.resize(w, 0.0);
9176 seg.up.matvec(xs, &mut scratch, pool);
9177 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
9178 *a *= v;
9179 }
9180 }
9181 let mut acc = vec![0.0f32; hidden];
9182 seg.down.matvec(g, &mut acc, pool);
9183 for (o, a) in out.iter_mut().zip(&acc) {
9184 *o += *a;
9185 }
9186 }
9187 }
9188 }
9189 if b > 1 {
9190 for (ci, &i) in cand.iter().enumerate() {
9191 let seg = &d.segs[i];
9192 let mut acc = vec![0.0f32; b * hidden];
9193 seg.down.matmat(&acts[ci], b, &mut acc, pool);
9194 for (o, a) in out.iter_mut().zip(&acc) {
9195 *o += *a;
9196 }
9197 }
9198 }
9199 out
9200}
9201
9202fn tube_ffn(
9208 d: &DenseFfn,
9209 xs: &[f32],
9210 b: usize,
9211 pool: Option<&Pool>,
9212 mask_row: Option<&[u8]>,
9213) -> Vec<f32> {
9214 if tube_topk() > 0 {
9215 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
9216 }
9217 let hidden = d.down_proj.rows();
9218 let core = d.gate_proj.rows();
9219 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
9220 let mut out = match (b, core_full, mask_row) {
9221 (1, true, _) => dense_ffn(d, xs, pool),
9222 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
9223 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
9224 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
9225 };
9226 TUBE_SCRATCH.with(|sc| {
9227 let mut sc = sc.borrow_mut();
9228 let [g, u, acc] = &mut *sc;
9229 for seg in &d.segs {
9230 if !tube_bit(mask_row, seg.start) {
9231 continue;
9232 }
9233 let w = seg.width;
9234 g.resize(b * w, 0.0);
9235 if b == 1 && d.act == Act::Silu && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
9236 {
9237 } else {
9239 u.resize(b * w, 0.0);
9240 if b == 1 {
9241 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
9242 } else {
9243 seg.gate.matmat(xs, b, g, pool);
9244 seg.up.matmat(xs, b, u, pool);
9245 }
9246 for i in 0..b * w {
9247 g[i] = d.act.combine(g[i], u[i]);
9248 }
9249 }
9250 acc.resize(b * hidden, 0.0);
9251 acc.fill(0.0);
9252 if b == 1 {
9253 seg.down.matvec(g, acc, pool);
9254 } else {
9255 seg.down.matmat(g, b, acc, pool);
9256 }
9257 for (o, a) in out.iter_mut().zip(acc.iter()) {
9258 *o += *a;
9259 }
9260 }
9261 out
9262 })
9263}
9264
9265thread_local! {
9266 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
9270 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
9271}
9272
9273fn dense_ffn_batch(
9274 d: &DenseFfn,
9275 xs: &[f32],
9276 b: usize,
9277 pool: Option<&Pool>,
9278 mask_row: Option<&[u8]>,
9279) -> Vec<f32> {
9280 let inter = d.gate_proj.rows();
9281 let hidden = d.down_proj.rows();
9282 if mask_row.is_none()
9290 && d.act == Act::Silu
9291 && b >= 32
9292 && crate::gpu::enabled_here()
9293 && !crate::gpu::mm_killed()
9294 && refit_dir().is_none()
9299 && !ffn_probe_active()
9304 {
9305 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
9306 d.gate_proj.mapped_q4t(),
9307 d.up_proj.mapped_q4t(),
9308 d.down_proj.mapped_q4t(),
9309 ) {
9310 let mut out = vec![0.0f32; b * hidden];
9311 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
9312 return out;
9313 }
9314 }
9315 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
9320 d.gate_proj.mapped_q4tp(),
9321 d.up_proj.mapped_q4tp(),
9322 d.down_proj.mapped_q4tp(),
9323 ) {
9324 let mut out = vec![0.0f32; b * hidden];
9325 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
9326 return out;
9327 }
9328 }
9329 }
9330 let mut g = vec![0.0f32; b * inter];
9331 d.gate_proj.matmat(xs, b, &mut g, pool);
9332 let mut u = vec![0.0f32; b * inter];
9333 d.up_proj.matmat(xs, b, &mut u, pool);
9334 if gate_topk() > 0 && d.act == Act::Silu {
9335 for t in 0..b {
9336 let row = &mut g[t * inter..(t + 1) * inter];
9337 for v in row.iter_mut() {
9338 *v = Act::Silu.combine(*v, 1.0);
9339 }
9340 keep_top_k(row, gate_topk());
9341 }
9342 for i in 0..b * inter {
9343 g[i] *= u[i];
9344 }
9345 } else {
9346 for i in 0..b * inter {
9347 g[i] = d.act.combine(g[i], u[i]);
9348 }
9349 }
9350 if let Some(row) = mask_row {
9351 zero_masked_cols(&mut g, b, inter, row);
9352 }
9353 if oracle_topk() > 0 {
9354 for t in 0..b {
9355 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
9356 }
9357 }
9358 let mut out = vec![0.0f32; b * hidden];
9359 d.down_proj.matmat(&g, b, &mut out, pool);
9360 if refit_dir().is_some() {
9361 let li = crate::gpu::cur_layer();
9362 if li >= 0 {
9363 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
9364 }
9365 }
9366 FFN_PROBE.with(|pr| {
9370 if let Some(acc) = pr.borrow_mut().as_mut() {
9371 let li = crate::gpu::cur_layer();
9372 if li < 0 {
9373 return;
9374 }
9375 let Some(row) = acc.get_mut(li as usize) else {
9376 return;
9377 };
9378 let sq = probe_sq();
9379 for t in 0..b {
9380 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
9381 *a += if sq { (v as f64) * (v as f64) } else { (v as f64).abs() };
9382 }
9383 }
9384 }
9385 });
9386 out
9387}
9388
9389fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
9394 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9395 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9396 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
9397 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
9398 if (!on && !dump) || b == 0 {
9399 return;
9400 }
9401 let hidden = xs.len() / b;
9402 if on {
9403 let mut acc = m.act_sq.borrow_mut();
9404 if acc.len() < hidden {
9405 acc.resize(hidden, 0.0);
9406 }
9407 for t in 0..b {
9408 let row = &xs[t * hidden..(t + 1) * hidden];
9409 for (a, &v) in acc.iter_mut().zip(row) {
9410 *a += (v as f64) * (v as f64);
9411 }
9412 }
9413 }
9414 if dump {
9415 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
9418 .ok()
9419 .and_then(|v| v.parse().ok())
9420 .unwrap_or(4096);
9421 let mut rows = m.act_rows.borrow_mut();
9422 if rows.len() < cap * hidden {
9423 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
9424 rows.extend_from_slice(&xs[..take * hidden]);
9425 }
9426 }
9427}
9428
9429#[derive(Clone, Copy)]
9432struct SendVecs(*mut Vec<f32>);
9433unsafe impl Send for SendVecs {}
9434unsafe impl Sync for SendVecs {}
9435impl SendVecs {
9436 #[inline]
9437 fn at(self, i: usize) -> *mut Vec<f32> {
9438 unsafe { self.0.add(i) }
9439 }
9440}
9441
9442fn moe_ffn_batch(
9443 m: &MoeFfn,
9444 xs: &[f32],
9445 b: usize,
9446 hidden: usize,
9447 pool: Option<&Pool>,
9448 allowed: Option<&[bool]>,
9449) -> Vec<f32> {
9450 accumulate_act(m, xs, b);
9451 let ne = m.experts.len();
9452 let mut logits = vec![0.0f32; b * ne];
9453 match &m.resonance {
9454 Some(r) => {
9455 let hdim = xs.len() / b.max(1);
9456 for bi in 0..b {
9457 r.scores(&xs[bi * hdim..(bi + 1) * hdim], &mut logits[bi * ne..(bi + 1) * ne]);
9458 }
9459 }
9460 None => m.router.matmat(xs, b, &mut logits, pool),
9461 }
9462
9463 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
9466 {
9467 let mut st = m.stats.borrow_mut();
9468 if st.len() < ne {
9469 st.resize(ne, 0);
9470 }
9471 for bi in 0..b {
9472 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
9473 for &e in &idx {
9474 st[e] += 1;
9475 assign[e].push((bi, p[e] / wsum));
9476 }
9477 }
9478 }
9479
9480 let mut out = vec![0.0f32; b * hidden];
9481 let cols = m.experts[0].gate_proj.cols();
9482 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
9483 let sb = list.len();
9484 let mut sub = vec![0.0f32; sb * cols];
9485 for (k, &(bi, _)) in list.iter().enumerate() {
9486 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
9487 }
9488 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
9489 for (k, &(bi, w)) in list.iter().enumerate() {
9490 for i in 0..hidden {
9491 out[bi * hidden + i] += w * eo[k * hidden + i];
9492 }
9493 }
9494 };
9495 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
9501 if pool.is_some() && active.len() >= 8 {
9502 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
9503 {
9504 let panel_ptr = SendVecs(panels.as_mut_ptr());
9505 let experts = &m.experts;
9508 let (active_r, assign_r) = (&active, &assign);
9509 let run = |start: usize, end: usize| {
9510 for ai in start..end {
9511 let e = active_r[ai];
9512 let list = &assign_r[e];
9513 let sb = list.len();
9514 let mut sub = vec![0.0f32; sb * cols];
9515 for (k, &(bi, _)) in list.iter().enumerate() {
9516 sub[k * cols..(k + 1) * cols]
9517 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
9518 }
9519 unsafe {
9521 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
9522 }
9523 }
9524 };
9525 match pool {
9526 Some(p) => p.run_rows(active.len(), &run),
9527 None => run(0, active.len()),
9528 }
9529 }
9530 for (ai, &e) in active.iter().enumerate() {
9531 for (k, &(bi, w)) in assign[e].iter().enumerate() {
9532 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
9533 for i in 0..hidden {
9534 out[bi * hidden + i] += w * eo[i];
9535 }
9536 }
9537 }
9538 } else {
9539 for &e in &active {
9540 run_expert(&m.experts[e], &assign[e], &mut out);
9541 }
9542 }
9543 if let Some((se, gate)) = &m.shared {
9544 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
9545 let mut gl = vec![0.0f32; b];
9546 gate.matmat(xs, b, &mut gl, pool);
9547 (0..b)
9548 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
9549 .collect()
9550 } else {
9551 (0..b).map(|bi| (bi, 1.0)).collect()
9552 };
9553 run_expert(se, &all, &mut out);
9554 }
9555 out
9556}
9557
9558thread_local! {
9559 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
9563 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
9564}
9565
9566fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9568 if gate_topk() > 0
9571 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
9572 {
9573 return out;
9574 }
9575 if crate::gpu::enabled_here()
9586 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
9587 {
9588 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
9589 crate::gpu::ProbeArm::Gpu
9590 } else {
9591 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
9592 };
9593 match arm {
9594 crate::gpu::ProbeArm::Gpu => {
9595 let t0 = std::time::Instant::now();
9596 if let Some(out) = dense_ffn_gpu(d, x, pool) {
9597 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
9598 return out;
9599 }
9600 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
9604 }
9605 crate::gpu::ProbeArm::CpuTimed => {
9606 let t0 = std::time::Instant::now();
9607 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
9608 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
9609 return out;
9610 }
9611 crate::gpu::ProbeArm::Cpu => {
9612 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
9613 }
9614 }
9615 }
9616 dense_ffn_cpu(d, x, pool)
9617}
9618
9619fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9621 let inter = d.gate_proj.rows();
9622 FFN_SCRATCH.with(|s| {
9623 let mut s = s.borrow_mut();
9624 let [g, u, ..] = &mut *s;
9625 g.resize(inter, 0.0);
9626 if gate_topk() > 0 {
9629 u.resize(inter, 0.0);
9633 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9634 for i in 0..inter {
9635 g[i] = Act::Silu.combine(g[i], 1.0);
9636 }
9637 keep_top_k(g, gate_topk());
9638 for i in 0..inter {
9639 g[i] *= u[i];
9640 }
9641 } else if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
9642 } else {
9644 u.resize(inter, 0.0);
9645 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9647 for i in 0..inter {
9648 g[i] = d.act.combine(g[i], u[i]);
9649 }
9650 }
9651 FFN_PROBE.with(|pr| {
9659 if let Some(acc) = pr.borrow_mut().as_mut() {
9660 let li = crate::gpu::cur_layer();
9661 if li >= 0 {
9662 if let Some(row) = acc.get_mut(li as usize) {
9663 match probe_topk() {
9664 0 if probe_sq() => {
9665 for (a, &v) in row.iter_mut().zip(g.iter()) {
9666 *a += (v as f64) * (v as f64);
9667 }
9668 }
9669 0 if probe_signed() => {
9670 for (a, &v) in row.iter_mut().zip(g.iter()) {
9671 *a += v as f64;
9672 }
9673 }
9674 0 => {
9675 for (a, &v) in row.iter_mut().zip(g.iter()) {
9676 *a += (v as f64).abs();
9677 }
9678 }
9679 k => {
9680 let n = g.len();
9681 let k = k.min(n);
9682 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
9683 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
9684 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
9685 });
9686 let thr = *kth;
9687 for (a, &v) in row.iter_mut().zip(g.iter()) {
9688 if v.abs() >= thr {
9689 *a += 1.0;
9690 }
9691 }
9692 }
9693 }
9694 }
9695 }
9696 }
9697 });
9698 if oracle_topk() > 0 {
9699 keep_top_k(g, oracle_topk());
9700 }
9701 {
9702 let li = crate::gpu::cur_layer();
9703 if li >= 0 {
9704 adump_row(li as usize, g);
9705 }
9706 }
9707 let mut out = attention::take_buf(d.down_proj.rows());
9708 d.down_proj.matvec(g, &mut out, pool);
9709 out
9710 })
9711}
9712
9713pub struct RefitAcc {
9726 pub support: Vec<u32>,
9727 pub gss: Vec<f32>,
9728 pub ya: Vec<f32>,
9729 pub hidden: usize,
9730 pub tokens: u64,
9731 pub buf_g: Vec<f32>,
9737 pub buf_o: Vec<f32>,
9738 pub buf_t: usize,
9739}
9740
9741type RefitState = (
9745 std::collections::HashMap<usize, RefitAcc>,
9746 Vec<f32>,
9747);
9748
9749static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
9750 std::sync::OnceLock::new();
9751
9752fn ffn_probe_active() -> bool {
9755 FFN_PROBE.with(|p| p.borrow().is_some())
9756}
9757
9758fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
9759 REFIT
9760 .get_or_init(|| {
9761 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
9762 (
9763 d,
9764 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
9765 )
9766 })
9767 })
9768 .as_ref()
9769}
9770
9771fn refit_accumulate(
9773 li: usize,
9774 g: &[f32],
9775 b: usize,
9776 inter: usize,
9777 out: &[f32],
9778 hidden: usize,
9779 pool: Option<&Pool>,
9780) {
9781 let Some((dir, map)) = refit_dir() else {
9782 return;
9783 };
9784 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
9785 let (from, to) = *SPAN.get_or_init(|| {
9786 let g = |k: &str, d: usize| {
9787 std::env::var(k)
9788 .ok()
9789 .and_then(|v| v.parse().ok())
9790 .unwrap_or(d)
9791 };
9792 (g("CMF_FFN_REFIT_FROM", 0), g("CMF_FFN_REFIT_TO", usize::MAX))
9793 });
9794 if li < from || li > to {
9795 return;
9796 }
9797 let mut guard = map.lock().unwrap();
9798 let (map, shared) = &mut *guard;
9799 let acc = match map.entry(li) {
9800 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
9801 std::collections::hash_map::Entry::Vacant(e) => {
9802 let path = format!("{dir}/support.{li}.u32");
9803 let Ok(bytes) = std::fs::read(&path) else {
9804 eprintln!("refit: no {path} — layer {li} skipped");
9805 return;
9806 };
9807 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
9808 let support: Vec<u32> = bytes[4..4 + n * 4]
9809 .chunks_exact(4)
9810 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
9811 .collect();
9812 eprintln!(
9813 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
9814 (n * n + hidden * n) as f64 * 4.0 / 1e6
9815 );
9816 e.insert(RefitAcc {
9817 gss: vec![0.0; n * n],
9818 ya: vec![0.0; hidden * n],
9819 buf_g: Vec::new(),
9820 buf_o: Vec::new(),
9821 buf_t: 0,
9822 support,
9823 hidden,
9824 tokens: 0,
9825 })
9826 }
9827 };
9828 let ns = acc.support.len();
9829 let cap = refit_batch();
9831 if acc.buf_g.is_empty() {
9832 acc.buf_g = vec![0.0; ns * cap];
9833 acc.buf_o = vec![0.0; hidden * cap];
9834 }
9835 let take = b.min(cap - acc.buf_t);
9836 for t in 0..take {
9837 let col = acc.buf_t + t;
9838 for (j, &n) in acc.support.iter().enumerate() {
9839 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
9840 }
9841 for h in 0..hidden {
9842 acc.buf_o[h * cap + col] = out[t * hidden + h];
9843 }
9844 }
9845 acc.buf_t += take;
9846 acc.tokens += take as u64;
9847 if acc.buf_t < cap {
9848 return;
9849 }
9850 let bt = acc.buf_t;
9851 acc.buf_t = 0;
9852 let RefitAcc {
9862 gss, ya, buf_g, buf_o, ..
9863 } = acc;
9864 let need = (ns * ns).max(hidden * ns);
9865 if shared.len() < need {
9866 shared.resize(need, 0.0);
9867 }
9868 let scratch = &mut shared[..];
9869 let _ = bt;
9870 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
9871 add_into(gss, &scratch[..ns * ns], pool);
9872 if crate::gpu::gemm_nt_f32_transient(buf_o, buf_g, &mut scratch[..hidden * ns], hidden, cap, ns) {
9873 add_into(ya, &scratch[..hidden * ns], pool);
9874 } else {
9875 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
9876 }
9877 } else {
9878 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
9879 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
9880 }
9881 }
9885
9886fn refit_batch() -> usize {
9888 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
9889 *B.get_or_init(|| {
9890 std::env::var("CMF_FFN_REFIT_BATCH")
9891 .ok()
9892 .and_then(|v| v.parse().ok())
9893 .unwrap_or(4096)
9894 })
9895}
9896
9897fn accum_outer_t(
9900 c: &mut [f32],
9901 m: usize,
9902 n: usize,
9903 b: usize,
9904 left: &[f32],
9905 right: &[f32],
9906 pool: Option<&Pool>,
9907) {
9908 let ptr = SendMut(c.as_mut_ptr());
9909 let body = |i: usize| {
9910 let ptr = &ptr;
9911 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
9912 for t in 0..b {
9913 let a = left[i * b + t];
9914 if a == 0.0 {
9915 continue;
9916 }
9917 for (j, o) in row.iter_mut().enumerate() {
9918 *o += a * right[j * b + t];
9919 }
9920 }
9921 };
9922 match pool {
9923 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
9924 for i in s..e {
9925 body(i);
9926 }
9927 }),
9928 _ => {
9929 for i in 0..m {
9930 body(i);
9931 }
9932 }
9933 }
9934}
9935
9936fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
9939 let n = dst.len().min(src.len());
9940 match pool {
9941 Some(p) if n >= 1 << 16 => {
9942 let ptr = SendMut(dst.as_mut_ptr());
9943 let f = |s: usize, e: usize| {
9944 let ptr = &ptr;
9945 for blk in s..e {
9946 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
9947 for i in a..b {
9948 unsafe { *ptr.0.add(i) += src[i] };
9949 }
9950 }
9951 };
9952 p.run_rows(n.div_ceil(4096), &f);
9953 }
9954 _ => {
9955 for (d, v) in dst.iter_mut().zip(&src[..n]) {
9956 *d += *v;
9957 }
9958 }
9959 }
9960}
9961
9962fn accum_outer(
9967 c: &mut [f32],
9968 m: usize,
9969 n: usize,
9970 b: usize,
9971 left: &[f32],
9972 right: &[f32],
9973 pool: Option<&Pool>,
9974) {
9975 const TILE: usize = 32;
9976 let tiles = m.div_ceil(TILE);
9977 let cp = SendMut(c.as_mut_ptr());
9978 let body = |ti: usize| {
9979 let cp = &cp;
9980 let i0 = ti * TILE;
9981 let i1 = (i0 + TILE).min(m);
9982 for t in 0..b {
9983 let r = &right[t * n..t * n + n];
9984 for i in i0..i1 {
9985 let a = left[i * b + t];
9986 if a == 0.0 {
9987 continue;
9988 }
9989 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
9991 for (o, v) in row.iter_mut().zip(r) {
9992 *o += a * *v;
9993 }
9994 }
9995 }
9996 };
9997 match pool {
9998 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
9999 for ti in s..e {
10000 body(ti);
10001 }
10002 }),
10003 _ => {
10004 for ti in 0..tiles {
10005 body(ti);
10006 }
10007 }
10008 }
10009}
10010
10011pub fn refit_flush() -> usize {
10013 let Some((dir, map)) = refit_dir() else {
10014 return 0;
10015 };
10016 let guard = map.lock().unwrap();
10017 let mut n = 0;
10018 for (li, acc) in guard.0.iter() {
10019 let w = |name: &str, v: &[f32]| {
10022 let path = format!("{dir}/{name}.{li}.f32");
10023 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
10024 match std::fs::write(&path, &bytes) {
10025 Ok(()) => {}
10026 Err(e) => eprintln!("refit: FAILED to write {path} ({} MB): {e}", bytes.len() / 1_000_000),
10027 }
10028 };
10029 w("gss", &acc.gss);
10030 w("ya", &acc.ya);
10031 println!(
10032 "refit L{li}: {} support, {} tokens, hidden {}",
10033 acc.support.len(),
10034 acc.tokens,
10035 acc.hidden
10036 );
10037 n += 1;
10038 }
10039 n
10040}
10041
10042fn adump_row(li: usize, g: &[f32]) {
10047 use std::io::Write as _;
10048 static FILES: std::sync::OnceLock<
10049 Option<(String, std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>)>,
10050 > = std::sync::OnceLock::new();
10051 let Some((prefix, map)) = FILES
10052 .get_or_init(|| {
10053 std::env::var("CMF_FFN_ADUMP")
10054 .ok()
10055 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
10056 })
10057 .as_ref()
10058 else {
10059 return;
10060 };
10061 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
10064 let (from, to) = *SPAN.get_or_init(|| {
10065 let g = |k: &str, d: usize| {
10066 std::env::var(k)
10067 .ok()
10068 .and_then(|v| v.parse().ok())
10069 .unwrap_or(d)
10070 };
10071 (g("CMF_FFN_ADUMP_FROM", 0), g("CMF_FFN_ADUMP_TO", usize::MAX))
10072 });
10073 if li < from || li > to {
10074 return;
10075 }
10076 let mut map = map.lock().unwrap();
10077 let f = map.entry(li).or_insert_with(|| {
10078 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
10079 });
10080 let mut bytes = Vec::with_capacity(g.len() * 2);
10081 for v in g {
10082 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
10083 }
10084 let _ = f.write_all(&bytes);
10085}
10086
10087fn oracle_topk() -> usize {
10093 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10094 *K.get_or_init(|| {
10095 std::env::var("CMF_FFN_ORACLE_TOPK")
10096 .ok()
10097 .and_then(|v| v.parse().ok())
10098 .unwrap_or(0)
10099 })
10100}
10101
10102fn gate_topk() -> usize {
10108 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10109 *K.get_or_init(|| {
10110 std::env::var("CMF_FFN_GATE_TOPK")
10111 .ok()
10112 .and_then(|v| v.parse().ok())
10113 .unwrap_or(0)
10114 })
10115}
10116
10117fn gate_block() -> usize {
10124 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10125 *B.get_or_init(|| {
10126 std::env::var("CMF_FFN_GATE_BLOCK")
10127 .ok()
10128 .and_then(|v| v.parse().ok())
10129 .unwrap_or(1)
10130 })
10131}
10132
10133fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
10135 let n = g.len();
10136 let nb = n.div_ceil(block);
10137 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
10138 if kb >= nb {
10139 return;
10140 }
10141 let mut score: Vec<f32> = (0..nb)
10142 .map(|b| {
10143 g[b * block..((b + 1) * block).min(n)]
10144 .iter()
10145 .map(|v| v * v)
10146 .sum::<f32>()
10147 })
10148 .collect();
10149 let mut ord = score.clone();
10150 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
10151 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10152 });
10153 let thr = *kth;
10154 for b in 0..nb {
10155 if score[b] < thr {
10156 g[b * block..((b + 1) * block).min(n)].fill(0.0);
10157 }
10158 }
10159 score.clear();
10160}
10161
10162fn keep_top_k(g: &mut [f32], k: usize) {
10164 if gate_block() > 1 {
10165 return keep_top_blocks(g, k, gate_block());
10166 }
10167 let n = g.len();
10168 if k == 0 || k >= n {
10169 return;
10170 }
10171 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
10172 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
10173 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10174 });
10175 let thr = *kth;
10176 for v in g.iter_mut() {
10177 if v.abs() < thr {
10178 *v = 0.0;
10179 }
10180 }
10181}
10182
10183fn probe_sq() -> bool {
10187 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10188 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
10189}
10190
10191fn probe_signed() -> bool {
10195 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10196 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
10197}
10198
10199fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
10207 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
10208 M.get_or_init(|| {
10209 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
10210 let b = std::fs::read(&p).ok()?;
10211 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
10212 let vals: Vec<f32> = b[8..]
10213 .chunks_exact(4)
10214 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
10215 .collect();
10216 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
10217 Some((inter, vals))
10218 })
10219 .as_ref()
10220}
10221
10222fn probe_topk() -> usize {
10225 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
10226 *K.get_or_init(|| {
10227 std::env::var("CMF_FFN_PROBE_TOPK")
10228 .ok()
10229 .and_then(|v| v.parse().ok())
10230 .unwrap_or(0)
10231 })
10232}
10233
10234thread_local! {
10235 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
10238 const { std::cell::RefCell::new(None) };
10239}
10240
10241fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
10254 let dt = d.down_t.as_ref()?;
10255 let inter = d.gate_proj.rows();
10256 let hidden = dt.cols();
10257 if k == 0 || k >= inter || d.act != Act::Silu {
10258 return None;
10259 }
10260 DYN_SCRATCH.with(|sc| {
10261 let mut sc = sc.borrow_mut();
10262 let DynScratch { g, mag, live, parts } = &mut *sc;
10263 g.resize(inter, 0.0);
10264 d.gate_proj.matvec(x, g, pool);
10265 for v in g.iter_mut() {
10266 *v = inference::silu(*v);
10267 }
10268 mag.clear();
10271 mag.extend(g.iter().map(|v| v.abs()));
10272 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
10273 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
10274 });
10275 let thr = *kth;
10276 live.clear();
10277 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
10278 let mut out = vec![0.0f32; hidden];
10279 match pool {
10280 Some(p) if live.len() >= 64 => {
10281 let nw = p.n_workers() + 1;
10282 parts.clear();
10283 parts.resize(nw * hidden, 0.0);
10284 let ptr = SendMut(parts.as_mut_ptr());
10285 let n = live.len();
10286 let live_ref: &[u32] = live;
10287 let g_ref: &[f32] = g;
10288 p.run(&|w, workers| {
10289 let chunk = n.div_ceil(workers);
10290 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
10291 if s >= e {
10292 return;
10293 }
10294 WORKER_SCRATCH.with(|ws| {
10295 let mut ws = ws.borrow_mut();
10296 let [scratch, acc] = &mut *ws;
10297 scratch.resize(hidden.max(x.len()), 0.0);
10298 acc.clear();
10299 acc.resize(hidden, 0.0);
10300 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
10301 if let Some(&nx) = live_ref[s..e].get(o + 1) {
10304 d.up_proj.prefetch_row(nx as usize);
10305 dt.prefetch_row(nx as usize);
10306 }
10307 let idx = nrm as usize;
10308 let up = d.up_proj.row_dot(idx, x, scratch);
10309 let a = g_ref[idx] * up;
10310 if a != 0.0 {
10311 dt.add_row_scaled(idx, a, acc, scratch);
10312 }
10313 }
10314 for (j, v) in acc.iter().enumerate() {
10315 unsafe { *ptr.at(w * hidden + j) = *v };
10316 }
10317 });
10318 });
10319 for w in 0..nw {
10320 for (j, o) in out.iter_mut().enumerate() {
10321 *o += parts[w * hidden + j];
10322 }
10323 }
10324 }
10325 _ => {
10326 WORKER_SCRATCH.with(|ws| {
10327 let mut ws = ws.borrow_mut();
10328 let [scratch, _acc] = &mut *ws;
10329 scratch.resize(hidden.max(x.len()), 0.0);
10330 for &nrm in live.iter() {
10331 let idx = nrm as usize;
10332 let up = d.up_proj.row_dot(idx, x, scratch);
10333 let a = g[idx] * up;
10334 if a != 0.0 {
10335 dt.add_row_scaled(idx, a, &mut out, scratch);
10336 }
10337 }
10338 });
10339 }
10340 }
10341 Some(out)
10342 })
10343}
10344
10345struct DynScratch {
10348 g: Vec<f32>,
10349 mag: Vec<f32>,
10350 live: Vec<u32>,
10351 parts: Vec<f32>,
10352}
10353
10354thread_local! {
10355 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
10356 std::cell::RefCell::new(DynScratch {
10357 g: Vec::new(),
10358 mag: Vec::new(),
10359 live: Vec::new(),
10360 parts: Vec::new(),
10361 })
10362 };
10363 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
10365 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
10366}
10367
10368fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
10373 let inter = d.gate_proj.rows();
10374 FFN_SCRATCH.with(|s| {
10375 let mut s = s.borrow_mut();
10376 let [g, u, ..] = &mut *s;
10377 g.resize(inter, 0.0);
10378 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
10379 } else {
10381 u.resize(inter, 0.0);
10382 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
10383 for i in 0..inter {
10384 g[i] = d.act.combine(g[i], u[i]);
10385 }
10386 }
10387 zero_masked_cols(g, 1, inter, mask_row);
10388 let mut out = attention::take_buf(d.down_proj.rows());
10389 d.down_proj.matvec(g, &mut out, pool);
10390 out
10391 })
10392}
10393
10394fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
10400 if d.act != Act::Silu {
10402 return None;
10403 }
10404 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
10407 return None;
10408 }
10409 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
10410 let mut model_ref = None;
10411 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
10412 let model = model_ref?;
10413 let hidden = jobs[0].down.1;
10414 let mut out = attention::take_buf(hidden);
10415 if crate::gpu::moe_block(&model, &jobs, &mut out) {
10416 Some(out)
10417 } else {
10418 let mut out = out;
10419 attention::recycle_buf(&mut out);
10420 None
10421 }
10422}
10423
10424#[allow(clippy::type_complexity)]
10429#[allow(clippy::type_complexity)]
10430pub(crate) fn moe_parts(
10431 t: &QTensor,
10432) -> Option<(
10433 &std::sync::Arc<cortiq_core::CmfModel>,
10434 usize,
10435 usize,
10436 usize,
10437 &[f32],
10438 &[f32],
10439 bool,
10440 bool,
10441 bool,
10442)> {
10443 match t {
10444 QTensor::Mapped {
10445 model,
10446 idx,
10447 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
10448 rows,
10449 cols,
10450 row_scale,
10451 col_field,
10452 ..
10453 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
10454 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
10455 )),
10456 QTensor::Mapped {
10458 model,
10459 idx,
10460 dtype: cortiq_core::TensorDtype::Q1,
10461 rows,
10462 cols,
10463 ..
10464 } => Some((
10465 model,
10466 *idx,
10467 *rows,
10468 *cols,
10469 &[][..],
10470 &[][..],
10471 true,
10472 false,
10473 false,
10474 )),
10475 QTensor::Mapped {
10477 model,
10478 idx,
10479 dtype: cortiq_core::TensorDtype::Q4Tiled,
10480 rows,
10481 cols,
10482 ..
10483 } => Some((
10484 model,
10485 *idx,
10486 *rows,
10487 *cols,
10488 &[][..],
10489 &[][..],
10490 false,
10491 true,
10492 false,
10493 )),
10494 QTensor::Mapped {
10496 model,
10497 idx,
10498 dtype: cortiq_core::TensorDtype::Q4TiledP,
10499 rows,
10500 cols,
10501 ..
10502 } => Some((
10503 model,
10504 *idx,
10505 *rows,
10506 *cols,
10507 &[][..],
10508 &[][..],
10509 false,
10510 true,
10511 false,
10512 )),
10513 QTensor::Mapped {
10517 model,
10518 idx,
10519 dtype: cortiq_core::TensorDtype::Q2TiledP,
10520 rows,
10521 cols,
10522 ..
10523 } => Some((
10524 model,
10525 *idx,
10526 *rows,
10527 *cols,
10528 &[][..],
10529 &[][..],
10530 false,
10531 true,
10532 true,
10533 )),
10534 _ => None,
10535 }
10536}
10537
10538#[cfg(target_os = "macos")]
10544fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
10545 if m.router_sigmoid
10546 || m.router_input_norm
10547 || m.expert_bias.is_some()
10548 || m.route_tau.is_some()
10549 || m.mask.is_some()
10550 || m.per_expert_scale.is_some()
10551 || m.experts.is_empty()
10552 || m.top_k == 0
10553 || m.resonance.is_some()
10554 {
10555 return None;
10556 }
10557 let (sh, sg) = match &m.shared {
10560 Some((sh, Some(sg))) => (sh, sg),
10561 _ => return None,
10562 };
10563 let (rf, rr, rc) = m.router.f32_parts()?;
10564 if rr != m.experts.len() || rc != hidden {
10565 return None;
10566 }
10567 let (sf, sr, sc) = sg.f32_parts()?;
10568 if sr * sc != hidden {
10569 return None;
10570 }
10571 let inter = m.experts[0].gate_proj.rows();
10572 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
10575 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
10576 if e.act != Act::Silu
10577 || e.gate_proj.rows() != inter
10578 || e.gate_proj.cols() != hidden
10579 || e.up_proj.rows() != inter
10580 || e.up_proj.cols() != hidden
10581 || e.down_proj.rows() != hidden
10582 || e.down_proj.cols() != inter
10583 {
10584 return None;
10585 }
10586 let pick = |t: &QTensor| -> Option<usize> {
10587 if gu_q2 {
10588 t.mapped_q2tp().map(|(_, i)| i)
10589 } else {
10590 t.mapped_q4tp().map(|(_, i)| i)
10591 }
10592 };
10593 Some((
10594 pick(&e.gate_proj)?,
10595 pick(&e.up_proj)?,
10596 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
10597 ))
10598 };
10599 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
10600 let shared = trio(sh)?;
10601 Some(crate::gpu::GpuMoe {
10602 router: rf,
10603 sgate: sf,
10604 experts,
10605 shared,
10606 n_exp: m.experts.len(),
10607 top_k: m.top_k,
10608 inter,
10609 norm_topk: m.norm_topk_prob,
10610 route_scale: m.routed_scaling,
10611 gu_q2,
10612 })
10613}
10614
10615pub(crate) fn moe_push_job_parts<'a>(
10619 gate: &'a QTensor,
10620 up: &'a QTensor,
10621 down: &'a QTensor,
10622 x: &[f32],
10623 w: f32,
10624 swiglu_limit: f32,
10625 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
10626 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
10627) -> Option<()> {
10628 use crate::qtensor::prescale;
10629 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
10630 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
10631 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
10632 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
10633 return None; }
10635 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
10638 return None;
10639 }
10640 if !gq2 && dq2 {
10641 return None;
10642 }
10643 model_ref.get_or_insert_with(|| gm.clone());
10644 let dt = |cf: &[f32]| {
10645 if cf.is_empty() {
10646 cortiq_core::TensorDtype::Q8Row
10647 } else {
10648 cortiq_core::TensorDtype::Q8_2f
10649 }
10650 };
10651 jobs.push(crate::gpu::MoeJob {
10652 gate: (gi, gr, gc, grs),
10653 up: (ui, ur, uc, urs),
10654 down: (di, dr, dc, drs),
10655 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
10656 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
10657 down_col: dcf,
10658 w,
10659 q1: gq1,
10660 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
10661 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
10662 gu_q2: gq2,
10663 swiglu_limit,
10664 });
10665 Some(())
10666}
10667
10668fn moe_push_job<'a>(
10670 d: &'a DenseFfn,
10671 x: &[f32],
10672 w: f32,
10673 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
10674 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
10675) -> Option<()> {
10676 use crate::qtensor::prescale;
10677 if d.act != Act::Silu {
10678 return None; }
10680 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
10681 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
10682 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
10683 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
10684 return None; }
10686 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
10687 return None;
10688 }
10689 if !gq2 && dq2 {
10690 return None;
10691 }
10692 model_ref.get_or_insert_with(|| gm.clone());
10693 let gdt = if gcf.is_empty() {
10694 cortiq_core::TensorDtype::Q8Row
10695 } else {
10696 cortiq_core::TensorDtype::Q8_2f
10697 };
10698 let udt = if ucf.is_empty() {
10699 cortiq_core::TensorDtype::Q8Row
10700 } else {
10701 cortiq_core::TensorDtype::Q8_2f
10702 };
10703 jobs.push(crate::gpu::MoeJob {
10704 gate: (gi, gr, gc, grs),
10705 up: (ui, ur, uc, urs),
10706 down: (di, dr, dc, drs),
10707 xs_gate: prescale(x, gcf, gdt).into_owned(),
10708 xs_up: prescale(x, ucf, udt).into_owned(),
10709 down_col: dcf,
10710 w,
10711 q1: gq1,
10712 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
10713 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
10714 gu_q2: gq2,
10715 swiglu_limit: 0.0,
10716 });
10717 Some(())
10718}
10719
10720fn sparse_ffn_quant(
10727 d: &DenseFfn,
10728 x: &[f32],
10729 active: &[u16],
10730 hidden: usize,
10731 pool: Option<&Pool>,
10732) -> Vec<f32> {
10733 let n = active.len();
10734 let inter = d.gate_proj.rows();
10735 let mut act = vec![0.0f32; n];
10736 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
10739 let compute = |ai: usize| -> f32 {
10740 let idx = active[ai] as usize;
10741 if idx >= inter {
10742 return 0.0; }
10744 let mut s = if need_scratch {
10745 vec![0.0f32; hidden]
10746 } else {
10747 Vec::new()
10748 };
10749 let gate = d.gate_proj.row_dot(idx, x, &mut s);
10750 let up = d.up_proj.row_dot(idx, x, &mut s);
10751 d.act.combine(gate, up)
10752 };
10753 match pool {
10754 Some(p) if n >= 256 => {
10755 let ptr = SendMut(act.as_mut_ptr());
10756 p.run(&|widx, nw| {
10757 let chunk = n.div_ceil(nw);
10758 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
10759 for ai in s..e {
10760 unsafe { *ptr.at(ai) = compute(ai) };
10761 }
10762 });
10763 }
10764 _ => {
10765 for (ai, a) in act.iter_mut().enumerate() {
10766 *a = compute(ai);
10767 }
10768 }
10769 }
10770 let mut out = vec![0.0f32; hidden];
10772 for (ai, &idx) in active.iter().enumerate() {
10773 let w = act[ai];
10774 if w.abs() >= 1e-12 && (idx as usize) < inter {
10775 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
10776 }
10777 }
10778 out
10779}
10780
10781#[doc(hidden)]
10783pub fn sparse_ffn_quant_for_test(
10784 d: &DenseFfn,
10785 x: &[f32],
10786 active: &[u16],
10787 hidden: usize,
10788) -> Vec<f32> {
10789 sparse_ffn_quant(d, x, active, hidden, None)
10790}
10791
10792fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
10796 let deq = |t: &QTensor| -> Vec<f32> {
10797 let (rows, cols) = (t.rows(), t.cols());
10798 let mut out = vec![0.0f32; rows * cols];
10799 for r in 0..rows {
10800 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
10801 }
10802 out
10803 };
10804 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
10805}
10806
10807struct SendMut(*mut f32);
10809unsafe impl Send for SendMut {}
10810unsafe impl Sync for SendMut {}
10811impl SendMut {
10812 #[inline]
10813 #[allow(clippy::mut_from_ref)]
10816 unsafe fn at(&self, i: usize) -> &mut f32 {
10817 unsafe { &mut *self.0.add(i) }
10818 }
10819}
10820
10821fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
10831 let ne = logits.len();
10832 let p: Vec<f32> = if m.router_sigmoid {
10833 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
10834 } else {
10835 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10836 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
10837 let s: f32 = e.iter().sum();
10838 for v in &mut e {
10839 *v /= s;
10840 }
10841 e
10842 };
10843 let admit = |e: usize| {
10849 m.mask.as_ref().is_none_or(|mk| mk[e])
10850 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
10851 };
10852 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
10853 match &m.expert_bias {
10855 Some(b) => idx.sort_unstable_by(|&x, &y| {
10856 (p[y] + b[y])
10857 .partial_cmp(&(p[x] + b[x]))
10858 .unwrap()
10859 .then(x.cmp(&y))
10860 }),
10861 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
10862 }
10863 idx.truncate(m.top_k);
10864 if let Some(tau) = m.route_tau {
10868 let total: f32 = idx.iter().map(|&e| p[e]).sum();
10869 if total > 0.0 {
10870 let mut acc = 0.0f32;
10871 let mut keep = idx.len();
10872 for (i, &e) in idx.iter().enumerate() {
10873 acc += p[e];
10874 if acc >= tau * total {
10875 keep = i + 1;
10876 break;
10877 }
10878 }
10879 idx.truncate(keep);
10880 }
10881 }
10882 let wsum: f32 = if m.norm_topk_prob {
10883 let s: f32 = idx.iter().map(|&e| p[e]).sum();
10884 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
10887 } else {
10888 1.0 / m.routed_scaling
10889 };
10890 (idx, p, wsum)
10891}
10892
10893fn moe_trace(idx: &[usize]) {
10895 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
10896}
10897
10898pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
10901 use std::io::Write;
10902 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
10903 std::sync::OnceLock::new();
10904 let Some(f) = F.get_or_init(|| {
10905 let p = std::env::var("CMF_MOE_TRACE").ok()?;
10906 Some(std::sync::Mutex::new(
10907 std::fs::OpenOptions::new().create(true).append(true).open(p).ok()?,
10908 ))
10909 }) else {
10910 return;
10911 };
10912 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
10913 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
10914}
10915
10916fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
10919 accumulate_act(m, x, 1);
10920 let ne = m.experts.len();
10921 let mut logits = vec![0.0f32; ne];
10922 match &m.resonance {
10923 Some(r) => r.scores(x, &mut logits),
10924 None => m.router.matvec(x, &mut logits, pool),
10925 }
10926 let (idx, p, wsum) = moe_route(&logits, m, allowed);
10927 {
10928 let mut st = m.stats.borrow_mut();
10929 if st.len() < ne {
10930 st.resize(ne, 0);
10931 }
10932 for &e in &idx {
10933 st[e] += 1;
10934 }
10935 }
10936 moe_trace(&idx);
10942 if crate::gpu::enabled_here() {
10947 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
10948 crate::gpu::ProbeArm::Gpu => {
10949 let t0 = std::time::Instant::now();
10950 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
10951 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
10952 return out;
10953 }
10954 }
10955 crate::gpu::ProbeArm::CpuTimed => {
10956 let t0 = std::time::Instant::now();
10957 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
10958 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
10959 return out;
10960 }
10961 crate::gpu::ProbeArm::Cpu => {
10962 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
10963 }
10964 }
10965 }
10966 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
10967}
10968
10969fn graph_note(built: bool) {
10973 use std::sync::atomic::{AtomicBool, Ordering};
10974 if built {
10975 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
10976 } else {
10977 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
10978 }
10979 static SAID: AtomicBool = AtomicBool::new(false);
10980 if !SAID.swap(true, Ordering::Relaxed) {
10981 if built {
10982 tracing::info!("wgpu whole-token graph: ACTIVE");
10983 } else {
10984 tracing::warn!("wgpu whole-token graph refused — per-op path");
10985 }
10986 }
10987}
10988
10989pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
10993pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
10994
10995fn moe_batch_enabled() -> bool {
10998 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
10999 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
11000}
11001
11002fn moe_ffn_cpu_batched(
11008 m: &MoeFfn,
11009 x: &[f32],
11010 idx: &[usize],
11011 p: &[f32],
11012 wsum: f32,
11013 pool: Option<&Pool>,
11014) -> Option<Vec<f32>> {
11015 if idx.is_empty() || !moe_batch_enabled() {
11016 return None;
11017 }
11018 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
11022 return None;
11023 }
11024 let n = idx.len() + usize::from(m.shared.is_some());
11025 let mut pairs = Vec::with_capacity(n);
11026 let mut downs = Vec::with_capacity(n);
11027 let mut ws = Vec::with_capacity(n);
11028 for &e in idx {
11029 let d = &m.experts[e];
11030 if d.act != Act::Silu {
11031 return None;
11032 }
11033 pairs.push((&d.gate_proj, &d.up_proj));
11034 downs.push(&d.down_proj);
11035 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
11036 }
11037 if let Some((se, gate)) = &m.shared {
11040 if se.act != Act::Silu {
11041 return None;
11042 }
11043 let g = gate.as_ref().map_or(1.0, |gate| {
11044 let mut gl = [0.0f32; 1];
11045 gate.matvec(x, &mut gl, pool);
11046 1.0 / (1.0 + (-gl[0]).exp())
11047 });
11048 pairs.push((&se.gate_proj, &se.up_proj));
11049 downs.push(&se.down_proj);
11050 ws.push(g);
11051 }
11052 let inter = pairs[0].0.rows();
11053 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
11054 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
11055 return None;
11056 }
11057 let mut out = attention::take_buf(x.len());
11058 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
11059 attention::recycle_buf(&mut out);
11060 return None;
11061 }
11062 Some(out)
11063}
11064
11065fn moe_ffn_cpu(
11067 m: &MoeFfn,
11068 x: &[f32],
11069 idx: &[usize],
11070 p: &[f32],
11071 wsum: f32,
11072 pool: Option<&Pool>,
11073) -> Vec<f32> {
11074 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
11075 return out;
11076 }
11077 let mut out = attention::take_buf(x.len());
11078 for &e in idx {
11079 let mut eo = dense_ffn(&m.experts[e], x, pool);
11080 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
11081 for i in 0..out.len() {
11082 out[i] += w * eo[i];
11083 }
11084 attention::recycle_buf(&mut eo);
11085 }
11086 if let Some((se, gate)) = &m.shared {
11087 let mut so = dense_ffn(se, x, pool);
11088 let g = gate.as_ref().map_or(1.0, |gate| {
11089 let mut gl = [0.0f32; 1];
11090 gate.matvec(x, &mut gl, pool);
11091 1.0 / (1.0 + (-gl[0]).exp())
11092 });
11093 for i in 0..out.len() {
11094 out[i] += g * so[i];
11095 }
11096 attention::recycle_buf(&mut so);
11097 }
11098 out
11099}
11100
11101#[allow(clippy::too_many_arguments)]
11109fn mla_attention(
11110 w: &MlaWeights,
11111 normed: &[f32],
11112 cache: &mut crate::kv_cache::LayerKvCache,
11113 position: usize,
11114 inv_freq: &[f32],
11115 rope_scale: f32,
11116 eps: f64,
11117 pool: Option<&Pool>,
11118) -> Vec<f32> {
11119 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
11120 let hd = dr + dn;
11121 let mut q = vec![0.0f32; nh * hd];
11122 match (&w.q_a, &w.q_a_norm) {
11123 (Some(qa), Some(qn)) => {
11124 let mut t = vec![0.0f32; qa.rows()];
11125 qa.matvec(normed, &mut t, pool);
11126 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
11127 w.q_proj.matvec(&tn, &mut q, pool);
11128 }
11129 _ => w.q_proj.matvec(normed, &mut q, pool),
11130 }
11131 let mut ca = vec![0.0f32; lora + dr];
11132 w.kv_a.matvec(normed, &mut ca, pool);
11133 let (c_lat, k_rope) = ca.split_at_mut(lora);
11134 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
11135 let mut kvb = vec![0.0f32; nh * (dn + dv)];
11136 w.kv_b.matvec(&latn, &mut kvb, pool);
11137 if !w.nope {
11138 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
11139 }
11140 for h in 0..nh {
11141 if !w.nope {
11142 attention::rope_rotate_scaled(
11143 &mut q[h * hd..h * hd + dr],
11144 position,
11145 inv_freq,
11146 rope_scale,
11147 );
11148 }
11149 }
11150 let mut k = vec![0.0f32; nh * hd];
11151 let mut v = vec![0.0f32; nh * hd];
11152 for h in 0..nh {
11153 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
11154 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
11155 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
11156 }
11157 cache.append(&k, &v, &vec![true; nh]);
11158 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
11159 attention::recycle_buf(&mut imp);
11160 let mut ov = vec![0.0f32; nh * dv];
11161 for h in 0..nh {
11162 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
11163 }
11164 let mut out = vec![0.0f32; w.o_proj.rows()];
11165 w.o_proj.matvec(&ov, &mut out, pool);
11166 out
11167}
11168
11169fn dense_moe_ffn(
11176 dm: &DenseMoeFfn,
11177 x_normed: &[f32],
11178 h_raw: &[f32],
11179 eps: f64,
11180 norm_style: NormStyle,
11181 pool: Option<&Pool>,
11182) -> Vec<f32> {
11183 let mut d = dense_ffn(&dm.dense, x_normed, pool);
11184 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
11185 let m = &dm.moe;
11186 let ne = m.experts.len();
11187 let mut logits = vec![0.0f32; ne];
11188 if m.router_input_norm {
11189 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
11190 let inv = 1.0 / (ss + eps as f32).sqrt();
11191 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
11192 m.router.matvec(&xr, &mut logits, pool);
11193 } else {
11194 m.router.matvec(h_raw, &mut logits, pool);
11195 }
11196 let (idx, p, wsum) = moe_route(&logits, m, None);
11197 {
11198 let mut st = m.stats.borrow_mut();
11199 if st.len() < ne {
11200 st.resize(ne, 0);
11201 }
11202 for &e in &idx {
11203 st[e] += 1;
11204 }
11205 }
11206 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
11207 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
11208 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
11209 for (di, mi) in d.iter_mut().zip(&mo) {
11210 *di += mi;
11211 }
11212 d
11213}
11214
11215fn moe_gpu_refused(why: &'static str) {
11222 use std::sync::atomic::{AtomicBool, Ordering};
11223 static SAID: AtomicBool = AtomicBool::new(false);
11224 if !SAID.swap(true, Ordering::Relaxed) {
11225 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
11226 }
11227}
11228
11229fn moe_ffn_gpu(
11230 m: &MoeFfn,
11231 x: &[f32],
11232 idx: &[usize],
11233 p: &[f32],
11234 wsum: f32,
11235 pool: Option<&Pool>,
11236) -> Option<Vec<f32>> {
11237 use crate::gpu::MoeJob;
11238
11239 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
11240 let mut model_ref = None;
11241 for &e in idx {
11242 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
11243 moe_gpu_refused("push_job(expert)");
11244 return None;
11245 }
11246 }
11247 if let Some((se, gate)) = &m.shared {
11248 let g = gate.as_ref().map_or(1.0, |gate| {
11249 let mut gl = [0.0f32; 1];
11250 gate.matvec(x, &mut gl, pool);
11251 1.0 / (1.0 + (-gl[0]).exp())
11252 });
11253 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
11254 moe_gpu_refused("push_job(shared)");
11255 return None;
11256 }
11257 }
11258 let Some(model) = model_ref else {
11259 moe_gpu_refused("no model_ref");
11260 return None;
11261 };
11262 let hidden = jobs[0].down.1;
11263 let mut out = vec![0.0f32; hidden];
11264 if crate::gpu::moe_block(&model, &jobs, &mut out) {
11265 Some(out)
11266 } else {
11267 moe_gpu_refused("gpu::moe_block");
11268 None
11269 }
11270}
11271
11272fn ffn_forward(
11274 ffn: &FfnKind,
11275 x: &[f32],
11276 pool: Option<&Pool>,
11277 experts_allowed: Option<&[bool]>,
11278) -> Vec<f32> {
11279 match ffn {
11280 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
11281 FfnKind::Dense(d) => dense_ffn(d, x, pool),
11282 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
11283 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
11287 }
11288}
11289
11290fn ffn_forward_pair(
11294 ffn: &FfnKind,
11295 x1: &[f32],
11296 x2: &[f32],
11297 pool: Option<&Pool>,
11298 experts_allowed: Option<&[bool]>,
11299) -> (Vec<f32>, Vec<f32>) {
11300 let d = match ffn {
11301 FfnKind::Dense(d) if !d.segs.is_empty() => {
11304 return (
11305 tube_ffn(d, x1, 1, pool, None),
11306 tube_ffn(d, x2, 1, pool, None),
11307 );
11308 }
11309 FfnKind::Dense(d) => d,
11310 FfnKind::Moe(m) => {
11311 return (
11312 moe_ffn(m, x1, pool, experts_allowed),
11313 moe_ffn(m, x2, pool, experts_allowed),
11314 );
11315 }
11316 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
11317 };
11318 let inter = d.gate_proj.rows();
11319 FFN_SCRATCH.with(|s| {
11320 let mut s = s.borrow_mut();
11321 let [g1, g2, u1, u2] = &mut *s;
11322 g1.resize(inter, 0.0);
11323 g2.resize(inter, 0.0);
11324 u1.resize(inter, 0.0);
11325 u2.resize(inter, 0.0);
11326 QTensor::matvec2_many(
11329 [&d.gate_proj, &d.up_proj],
11330 x1,
11331 x2,
11332 [g1.as_mut_slice(), u1.as_mut_slice()],
11333 [g2.as_mut_slice(), u2.as_mut_slice()],
11334 pool,
11335 );
11336 for i in 0..inter {
11337 g1[i] = d.act.combine(g1[i], u1[i]);
11338 g2[i] = d.act.combine(g2[i], u2[i]);
11339 }
11340 let mut o1 = attention::take_buf(d.down_proj.rows());
11341 let mut o2 = attention::take_buf(d.down_proj.rows());
11342 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
11343 (o1, o2)
11344 })
11345}
11346
11347#[cfg(test)]
11348mod tests {
11349
11350 #[test]
11351 fn cancel_flag_stops_generation() {
11352 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
11353 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
11356 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
11357 assert_eq!(r.finish_reason, "cancelled");
11358 assert!(
11359 r.token_ids.is_empty(),
11360 "no tokens after cancel: {:?}",
11361 r.token_ids
11362 );
11363 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
11365 assert_ne!(r2.finish_reason, "cancelled");
11366 }
11367 use super::*;
11368
11369 #[test]
11377 fn dynamic_ffn_equals_the_zeroing_arm() {
11378 let (hidden, inter) = (8usize, 32usize);
11379 let synth = |n: usize, salt: usize| -> Vec<f32> {
11380 (0..n)
11381 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
11382 .collect()
11383 };
11384 let down = synth(hidden * inter, 3);
11385 let mut down_t = vec![0.0f32; inter * hidden];
11386 for r in 0..hidden {
11387 for c in 0..inter {
11388 down_t[c * hidden + r] = down[r * inter + c];
11389 }
11390 }
11391 let d = DenseFfn {
11392 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
11393 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
11394 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
11395 act: Act::Silu,
11396 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
11397 segs: Vec::new(),
11398 };
11399 let x = synth(hidden, 11);
11400 let k = 12usize;
11401 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
11402 let mut g = vec![0.0f32; inter];
11404 d.gate_proj.matvec(&x, &mut g, None);
11405 let mut u = vec![0.0f32; inter];
11406 d.up_proj.matvec(&x, &mut u, None);
11407 for v in g.iter_mut() {
11408 *v = inference::silu(*v);
11409 }
11410 keep_top_k(&mut g, k);
11411 for i in 0..inter {
11412 g[i] *= u[i];
11413 }
11414 let mut want = vec![0.0f32; hidden];
11415 d.down_proj.matvec(&g, &mut want, None);
11416 for (a, b) in want.iter().zip(&got) {
11417 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
11418 }
11419 }
11420
11421 #[test]
11427 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
11428 let (hidden, core, tube) = (8usize, 12usize, 8usize);
11429 let inter = core + tube;
11430 let synth = |n: usize, salt: usize| -> Vec<f32> {
11431 (0..n)
11432 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
11433 .collect()
11434 };
11435 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
11436 let d_all = synth(hidden * inter, 3);
11437 let dense = DenseFfn {
11439 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
11440 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
11441 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
11442 act: Act::Silu,
11443 down_t: None,
11444 segs: Vec::new(),
11445 };
11446 let rows = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
11447 v[a * hidden..b * hidden].to_vec()
11448 };
11449 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
11450 let mut o = Vec::with_capacity(hidden * (b - a));
11451 for r in 0..hidden {
11452 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
11453 }
11454 o
11455 };
11456 let tubed = DenseFfn {
11457 down_t: None,
11458 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
11459 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
11460 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
11461 act: Act::Silu,
11462 segs: vec![FfnSeg {
11463 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
11464 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
11465 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
11466 start: core,
11467 width: tube,
11468 }],
11469 };
11470 let x = synth(hidden, 7);
11471 let want = dense_ffn(&dense, &x, None);
11472 let got = tube_ffn(&tubed, &x, 1, None, None);
11473 for (a, b) in want.iter().zip(&got) {
11474 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
11475 }
11476 let mut bits = vec![0u8; inter.div_ceil(8)];
11478 for n in 0..core {
11479 bits[n / 8] |= 1 << (n % 8);
11480 }
11481 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
11482 let masked = dense_ffn_masked(&dense, &x, None, &bits);
11483 for (a, b) in masked.iter().zip(&closed) {
11484 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
11485 }
11486 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
11488 for (a, b) in closed.iter().zip(&batch) {
11489 assert_eq!(a, b, "batch arm disagrees with decode arm");
11490 }
11491 }
11492
11493 #[test]
11495 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
11496 let (hidden, inter) = (16usize, 40usize);
11497 let synth = |n: usize, salt: usize| -> Vec<f32> {
11498 (0..n)
11499 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
11500 .collect()
11501 };
11502 let d = DenseFfn {
11503 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
11504 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
11505 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
11506 act: Act::Silu,
11507 down_t: None,
11508 segs: Vec::new(),
11509 };
11510 let x = synth(hidden, 9);
11511 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
11513
11514 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
11515
11516 let mut g = vec![0.0f32; inter];
11518 d.gate_proj.matvec(&x, &mut g, None);
11519 let mut u = vec![0.0f32; inter];
11520 d.up_proj.matvec(&x, &mut u, None);
11521 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
11522 for i in 0..inter {
11523 g[i] = if act_set.contains(&(i as u16)) {
11524 inference::silu(g[i]) * u[i]
11525 } else {
11526 0.0
11527 };
11528 }
11529 let mut reference = vec![0.0f32; hidden];
11530 d.down_proj.matvec(&g, &mut reference, None);
11531
11532 let max_d = sparse
11533 .iter()
11534 .zip(&reference)
11535 .map(|(a, b)| (a - b).abs())
11536 .fold(0.0f32, f32::max);
11537 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
11538 }
11539
11540 fn attach_test_mtp(p: &mut Pipeline) {
11542 let (h, inter, heads, kv, hd) = (
11543 p.hidden_size,
11544 p.intermediate_size,
11545 p.num_heads,
11546 p.num_kv_heads,
11547 p.head_dim,
11548 );
11549 let synth = |n: usize, salt: usize| -> Vec<f32> {
11550 (0..n)
11551 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
11552 .collect()
11553 };
11554 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
11555 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
11556 };
11557 p.mtp = Some(MtpModule {
11558 enorm: vec![1.0; h],
11559 hnorm: vec![1.0; h],
11560 eh_proj: qt(h, 2 * h, 301),
11561 layer: LayerWeights {
11562 input_norm: vec![1.0; h],
11563 post_norm: vec![1.0; h],
11564 attn_out_norm: None,
11565 ffn_out_norm: None,
11566 layer_scale: None,
11567 ffn: FfnKind::Dense(DenseFfn {
11568 gate_proj: qt(inter, h, 315),
11569 up_proj: qt(inter, h, 316),
11570 down_proj: qt(h, inter, 317),
11571 act: Act::Silu,
11572 down_t: None,
11573 segs: Vec::new(),
11574 }),
11575 attn: AttnKind::Full {
11576 bias: None,
11577 wq: qt(heads * hd, h, 311),
11578 wk: qt(kv * hd, h, 312),
11579 wv: qt(kv * hd, h, 313),
11580 wo: qt(h, heads * hd, 314),
11581 q_norm: None,
11582 k_norm: None,
11583 output_gate: false,
11584 softplus_gate: None,
11585 },
11586 },
11587 final_norm: vec![1.0; h],
11588 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
11589 });
11590 }
11591
11592 #[test]
11593 fn speculative_equals_vanilla_greedy() {
11594 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
11598 let run = |spec: bool| {
11599 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11600 p.sampler_config.temperature = 0.0;
11601 attach_test_mtp(&mut p);
11602 p.speculative = spec;
11603 let r = p.generate("abcdef", 12, None, None).unwrap();
11604 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
11605 };
11606 let (vanilla, d0, _) = run(false);
11607 let (spec, d1, a1) = run(true);
11608 assert_eq!(d0, 0, "vanilla path must not draft");
11609 assert!(d1 > 0, "speculative path must draft");
11610 assert_eq!(
11611 vanilla, spec,
11612 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
11613 );
11614 }
11615
11616 #[test]
11617 fn speculative_accepts_constant_oracle() {
11618 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
11620 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11621 p.sampler_config.temperature = 0.0;
11622 p.sampler_config.repetition_penalty = 1.0;
11623 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
11626 attach_test_mtp(&mut p);
11627 p.speculative = true;
11628 let r = p.generate("abcd", 10, None, None).unwrap();
11629 assert!(r.mtp_drafted > 0);
11630 assert_eq!(
11631 r.mtp_accepted, r.mtp_drafted,
11632 "constant logits → every draft accepted"
11633 );
11634 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
11637 }
11638
11639 #[test]
11640 fn empty_prompt_is_an_error_not_a_panic() {
11641 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
11642 let r = p.generate("", 4, None, None);
11643 assert!(r.is_err(), "empty prompt must be a clean error");
11644 }
11645
11646 #[test]
11647 fn every_token_enters_kv_exactly_once() {
11648 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11649 p.sampler_config.temperature = 0.0;
11651 let r = p.generate("abc", 2, None, None).unwrap();
11652 assert_eq!(r.prompt_tokens, 3);
11653 assert_eq!(
11657 p.kv_cache.seq_len(),
11658 3 + r.tokens_generated - 1,
11659 "each token must be cached exactly once (v1 cached the last prompt token twice)"
11660 );
11661 }
11662
11663 #[test]
11664 fn generation_is_reproducible_with_seed() {
11665 let run = || {
11666 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11667 p.generate("hello", 8, None, None).unwrap().token_ids
11668 };
11669 assert_eq!(run(), run());
11670 }
11671
11672 #[test]
11673 fn resetting_sampler_restarts_the_seeded_stream() {
11674 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
11675 let config = SamplerConfig {
11676 seed: Some(1234),
11677 ..SamplerConfig::default()
11678 };
11679 p.set_sampler_config(config.clone());
11680 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
11681 p.set_sampler_config(config);
11682 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
11683 assert_eq!(first, second);
11684 }
11685
11686 #[test]
11687 fn eviction_bounds_the_cache() {
11688 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
11689 p.kv_cache.max_seq_len = 6;
11690 p.sampler_config.temperature = 0.0;
11691 let _ = p.generate("abcd", 12, None, None).unwrap();
11692 assert!(
11693 p.kv_cache.seq_len() <= 6 + 1,
11694 "cache must stay bounded by max_seq_len (got {})",
11695 p.kv_cache.seq_len()
11696 );
11697 }
11698
11699 #[test]
11700 fn confidence_matches_tokens_and_is_a_probability() {
11701 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11702 p.sampler_config.temperature = 0.0;
11703 p.sampler_config.repetition_penalty = 1.0;
11704 let r = p.generate("abcd", 10, None, None).unwrap();
11705 assert_eq!(
11706 r.token_confidence.len(),
11707 r.token_ids.len(),
11708 "one confidence per emitted token"
11709 );
11710 for &c in &r.token_confidence {
11711 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
11712 }
11713 let logits = [1.0f32, 3.0, 0.5, 3.0];
11715 let p0 = top1_prob_t(&logits, 1, 1.0);
11716 let p1 = top1_prob_t(&logits, 3, 1.0);
11717 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
11718 assert!(p0 > 0.0 && p0 < 1.0);
11719 let sharp = top1_prob_t(&logits, 1, 1.0);
11721 let soft = top1_prob_t(&logits, 1, 2.0);
11722 assert!(soft < sharp, "higher temperature lowers peak confidence");
11723 }
11724
11725 #[test]
11726 fn trace_is_opt_in_and_parallels_the_output() {
11727 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11729 p.sampler_config.temperature = 0.0;
11730 p.sampler_config.repetition_penalty = 1.0;
11731 let r = p.generate("abcd", 10, None, None).unwrap();
11732 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
11733
11734 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11736 p.sampler_config.temperature = 0.0;
11737 p.sampler_config.repetition_penalty = 1.0;
11738 p.set_trace(true);
11739 let r = p.generate("abcd", 10, None, None).unwrap();
11740 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
11741 for (i, tr) in r.traces.iter().enumerate() {
11742 assert_eq!(tr.t, i, "trace index is sequential");
11743 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
11744 assert_eq!(
11745 tr.confidence, r.token_confidence[i],
11746 "trace confidence matches the confidence channel"
11747 );
11748 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
11750 }
11751 }
11752
11753 #[test]
11754 fn explain_prefill_logits_match_greedy_first_token() {
11755 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
11759 p.sampler_config.temperature = 0.0;
11760 p.sampler_config.repetition_penalty = 1.0;
11761 let ids = p.tokenizer.encode("abcd");
11762 let logits = p.prefill_next_logits(&ids, None);
11763 let argmax = logits
11764 .iter()
11765 .enumerate()
11766 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11767 .unwrap()
11768 .0 as u32;
11769 let r = p.generate("abcd", 1, None, None).unwrap();
11770 assert_eq!(
11771 argmax, r.token_ids[0],
11772 "explain preview must match greedy emit"
11773 );
11774 }
11775
11776 #[test]
11777 fn laguna_shared_expert_is_unconditionally_added() {
11778 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
11779 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
11780 let zero_dense = || DenseFfn {
11781 gate_proj: matrix(vec![0.0; 4]),
11782 up_proj: matrix(vec![0.0; 4]),
11783 down_proj: matrix(vec![0.0; 4]),
11784 act: Act::Silu,
11785 down_t: None,
11786 segs: Vec::new(),
11787 };
11788 let shared = DenseFfn {
11789 gate_proj: identity(),
11790 up_proj: identity(),
11791 down_proj: identity(),
11792 act: Act::Silu,
11793 down_t: None,
11794 segs: Vec::new(),
11795 };
11796 let x = [1.0, 2.0];
11797 let expected = dense_ffn(&shared, &x, None);
11798 let moe = MoeFfn {
11799 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
11800 experts: vec![zero_dense()],
11801 top_k: 1,
11802 norm_topk_prob: true,
11803 router_sigmoid: true,
11804 expert_bias: None,
11805 routed_scaling: 1.0,
11806 route_tau: None,
11807 shared: Some((shared, None)),
11808 stats: std::cell::RefCell::new(Vec::new()),
11809 act_sq: std::cell::RefCell::new(Vec::new()),
11810 act_rows: std::cell::RefCell::new(Vec::new()),
11811 mask: None,
11812 per_expert_scale: None,
11813 router_input_norm: false,
11814 resonance: None,
11815 };
11816 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
11817 for (actual, expected) in actual.iter().zip(expected) {
11818 assert!((actual - expected).abs() < 1e-6);
11819 }
11820 }
11821}