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 attn_softcap: f32,
263 confidence_on: bool,
267}
268
269#[cfg(target_os = "macos")]
270impl Drop for Pipeline {
271 fn drop(&mut self) {
272 crate::gpu::kv_mirror_drop(self.graph_kv_id);
273 }
274}
275
276pub struct PipelineWeights {
281 pub embed_tokens: QTensor,
283 pub layers: Vec<LayerWeights>,
285 pub lm_head: QTensor,
287 pub final_norm: Vec<f32>,
289}
290
291pub struct LayerWeights {
293 pub input_norm: Vec<f32>,
294 pub post_norm: Vec<f32>,
297 pub attn_out_norm: Option<Vec<f32>>,
300 pub layer_scale: Option<f32>,
302 pub ffn_out_norm: Option<Vec<f32>>,
305 pub ffn: FfnKind,
306 pub attn: AttnKind,
307}
308
309#[derive(Clone, Copy, PartialEq, Debug, Default)]
312pub enum Act {
313 #[default]
314 Silu,
315 GeluTanh,
316 Situ {
319 beta: f32,
320 linear_beta: f32,
321 },
322}
323
324impl Act {
325 pub fn from_arch(name: &str) -> Self {
326 if name == "gelu_tanh" {
327 Self::GeluTanh
328 } else {
329 Self::Silu
330 }
331 }
332
333 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
335 match arch.hidden_act.as_str() {
336 "situ" => Self::Situ {
337 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
338 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
339 },
340 other => Self::from_arch(other),
341 }
342 }
343
344 #[inline]
345 pub fn apply(self, x: f32) -> f32 {
346 match self {
347 Self::Silu => inference::silu(x),
348 Self::GeluTanh => inference::gelu_tanh(x),
349 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
350 }
351 }
352
353 #[inline]
356 pub fn combine(self, g: f32, u: f32) -> f32 {
357 match self {
358 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
359 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
360 }
361 _ => self.apply(g) * u,
362 }
363 }
364}
365
366pub struct DenseFfn {
368 pub gate_proj: QTensor,
369 pub up_proj: QTensor,
370 pub down_proj: QTensor,
371 pub act: Act,
373}
374
375pub enum FfnKind {
378 Dense(DenseFfn),
379 Moe(MoeFfn),
383 DenseMoe(Box<DenseMoeFfn>),
390}
391
392pub struct DenseMoeFfn {
394 pub dense: DenseFfn,
395 pub moe: MoeFfn,
396 pub post_norm_1: Vec<f32>,
398 pub pre_norm_2: Vec<f32>,
401 pub post_norm_2: Vec<f32>,
403}
404
405pub struct MoeFfn {
406 pub router: QTensor,
408 pub experts: Vec<DenseFfn>,
409 pub top_k: usize,
410 pub norm_topk_prob: bool,
411 pub router_sigmoid: bool,
414 pub expert_bias: Option<Vec<f32>>,
418 pub routed_scaling: f32,
421 pub route_tau: Option<f32>,
427 pub shared: Option<(DenseFfn, Option<QTensor>)>,
430 pub stats: std::cell::RefCell<Vec<u64>>,
434 pub act_sq: std::cell::RefCell<Vec<f64>>,
441 pub act_rows: std::cell::RefCell<Vec<f32>>,
447 pub mask: Option<Vec<bool>>,
452 pub per_expert_scale: Option<Vec<f32>>,
455 pub router_input_norm: bool,
459}
460
461pub enum AttnKind {
464 Full {
466 wq: QTensor,
467 wk: QTensor,
468 wv: QTensor,
469 wo: QTensor,
470 q_norm: Option<Vec<f32>>,
471 k_norm: Option<Vec<f32>>,
472 output_gate: bool,
473 softplus_gate: Option<(QTensor, bool)>,
477 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
479 },
480 Linear(VmfPhaseWeights),
482 LinearGdn(GdnWeights),
484 ShortConv(ShortConvWeights),
487 Mla(Box<MlaWeights>),
495 Kda(Box<crate::linear_core::KdaWeights>),
499}
500
501pub struct MlaWeights {
503 pub q_proj: QTensor,
507 pub q_a: Option<QTensor>,
510 pub q_a_norm: Option<Vec<f32>>,
511 pub kv_a: QTensor,
513 pub kv_a_norm: Vec<f32>,
515 pub kv_b: QTensor,
517 pub o_proj: QTensor,
519 pub nh: usize,
520 pub qk_rope: usize,
521 pub qk_nope: usize,
522 pub v_dim: usize,
523 pub lora: usize,
524 pub scale: f32,
526 pub nope: bool,
528}
529
530pub struct MtpModule {
535 pub enorm: Vec<f32>,
536 pub hnorm: Vec<f32>,
537 pub eh_proj: QTensor,
539 pub layer: LayerWeights,
540 pub final_norm: Vec<f32>,
541 pub kv: crate::kv_cache::LayerKvCache,
542}
543
544#[cfg(target_os = "macos")]
551enum MetalRowsItem<'a> {
552 Gdn {
553 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
554 first: usize,
555 },
556 Attn {
557 l: crate::gpu_metal::AttnGpuLayer<'a>,
558 li: usize,
559 q_norm: Option<&'a [f32]>,
560 k_norm: Option<&'a [f32]>,
561 output_gate: bool,
562 },
563}
564
565#[cfg(target_os = "macos")]
566struct MetalVerifyPending {
567 graph: crate::gpu_metal::VerifyGraph,
568 gdn_layers: Vec<usize>,
569 attn_layers: Vec<(usize, usize)>,
570}
571
572#[derive(Clone, Copy)]
576enum SpecTrial {
577 Spec {
578 t0: std::time::Instant,
579 gen0: usize,
580 rounds: usize,
581 },
582 Plain {
583 t0: std::time::Instant,
584 gen0: usize,
585 },
586 Decided {
587 spec: bool,
588 recheck_at: usize,
589 },
590}
591
592#[derive(Default, Clone, Copy)]
603struct SpecMon {
604 round_ms: f64,
605 tokens: f64,
606 plain_ms: f64,
607 n: u32,
608 fails: u32,
609}
610
611impl SpecMon {
612 fn round(&mut self, dt_ms: f64, produced: usize) {
613 self.n += 1;
614 if self.n == 1 {
615 return; }
617 let a = if self.n == 2 { 1.0 } else { 0.3 };
618 self.round_ms += a * (dt_ms - self.round_ms);
619 self.tokens += a * (produced as f64 - self.tokens);
620 }
621 fn pays(&self) -> bool {
622 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
623 }
624}
625
626pub struct GenerateResult {
628 pub text: String,
629 pub token_ids: Vec<u32>,
630 pub prompt_tokens: usize,
631 pub tokens_generated: usize,
632 pub finish_reason: String,
633 pub mtp_drafted: usize,
635 pub mtp_accepted: usize,
636 pub token_confidence: Vec<f32>,
641 pub traces: Vec<TokenTrace>,
644}
645
646#[derive(Clone, Debug)]
651pub struct TokenTrace {
652 pub t: usize,
654 pub token_id: u32,
656 pub confidence: f32,
658 pub active_skill: Option<String>,
660 pub recon: Option<f32>,
664 pub switched: bool,
667}
668
669#[cfg_attr(not(test), allow(dead_code))]
674fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
675 let t = if temp > 1e-3 { temp } else { 1.0 };
676 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
677 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
678 if sum > 0.0 {
679 (((logits[id as usize] - max) / t).exp()) / sum
680 } else {
681 0.0
682 }
683}
684
685fn prefill_batched() -> bool {
688 std::env::var("CMF_PREFILL")
689 .map(|v| v != "seq")
690 .unwrap_or(true)
691}
692
693#[derive(Clone, Copy)]
697enum PrefillIn<'a> {
698 Ids(&'a [u32]),
699 Hidden(&'a [f32]),
700}
701
702impl Pipeline {
709 fn can_prefill_batched(&self) -> bool {
710 prefill_batched() && !self.weights.layers.is_empty()
711 }
712}
713
714pub fn prefill_chunk() -> usize {
721 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
722 .ok()
723 .and_then(|v| v.parse::<usize>().ok())
724 {
725 return n.max(1);
726 }
727 if cfg!(target_os = "macos") {
728 512
729 } else if cfg!(target_arch = "aarch64") {
730 256
733 } else {
734 48
735 }
736}
737
738pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
740
741impl Pipeline {
742 #[inline]
746 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
747 virtual_idx % self.physical_layers
748 }
749
750 #[inline]
753 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
754 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
755 }
756
757 #[allow(clippy::too_many_arguments)]
759
760 #[cfg(target_os = "macos")]
779 fn graph_prefill_preferred(&self) -> bool {
780 if !crate::gpu::enabled_here()
781 || !crate::gpu::q1_force()
782 || std::env::var("CMF_GPU_BLOCK")
783 .map(|v| v == "0")
784 .unwrap_or(false)
785 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
788 {
789 return false;
790 }
791 self.weights
792 .layers
793 .iter()
794 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
795 }
796
797 #[cfg(not(target_os = "macos"))]
798 fn graph_prefill_preferred(&self) -> bool {
799 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
807 if !graph_on || !crate::gpu::enabled_here() {
808 return false;
809 }
810 if self.o1_active() {
819 return false;
820 }
821 self.weights
822 .layers
823 .iter()
824 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
825 }
826
827 #[cfg(target_os = "macos")]
828 fn q1_graph_gpu(
829 &mut self,
830 start: usize,
831 upto: Option<usize>,
832 position: usize,
833 h: &mut [f32],
834 ) -> usize {
835 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
837 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
839 || !crate::gpu::q1_force()
840 || std::env::var("CMF_GPU_BLOCK")
841 .map(|v| v == "0")
842 .unwrap_or(false)
843 {
844 if std::env::var("CMF_GRAPH_DBG").is_ok() {
845 eprintln!(
846 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
847 self.attn_softcap > 0.0,
848 crate::gpu::enabled_here(),
849 crate::gpu::q1_force(),
850 );
851 }
852 return start;
853 }
854 if self.swa.is_some()
859 || self.global_attn.is_some()
860 || self.attention_heads_per_layer.is_some()
861 || self.attn_v_norm
862 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
863 || self.weights.layers.iter().any(|lw| {
864 lw.attn_out_norm.is_some()
865 || lw.ffn_out_norm.is_some()
866 || lw.layer_scale.is_some()
867 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
868 })
869 {
870 if std::env::var("CMF_GRAPH_DBG").is_ok() {
871 eprintln!(
872 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
873 self.swa.is_some(),
874 self.global_attn.is_some(),
875 self.attention_heads_per_layer.is_some(),
876 self.attn_v_norm,
877 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
878 );
879 }
880 return start;
881 }
882 let limit = upto
885 .map(|u| u + 1)
886 .unwrap_or(self.num_layers)
887 .min(self.num_layers);
888
889 enum Item<'a> {
890 Gdn {
891 run: Vec<GdnGpuLayer<'a>>,
892 first: usize,
893 },
894 Attn {
895 l: AttnGpuLayer<'a>,
896 li: usize,
897 q_norm: Option<&'a [f32]>,
898 k_norm: Option<&'a [f32]>,
899 output_gate: bool,
900 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
901 full_gpu: bool,
904 },
905 }
906
907 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
914 let attend_contract = attend_mode != "0"
915 && attend_mode != "off"
916 && self.head_dim % 4 == 0
917 && self.head_dim <= 256
918 && self.rotary_dim >= 2
919 && self.rotary_dim <= self.head_dim
920 && (self.rotary_dim / 2) % 32 == 0
921 && self.num_kv_heads > 0
922 && self.num_heads % self.num_kv_heads == 0;
923
924 let mut plan: Vec<Item> = Vec::new();
925 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
926 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
928 let mut scan = start;
929 while scan < limit {
930 let lw = &self.weights.layers[self.phys_layer(scan)];
931 let ffn = match &lw.ffn {
932 FfnKind::Dense(d) => {
933 let (Some(g), Some(u), Some(dn)) = (
934 d.gate_proj.q1_parts(),
935 d.up_proj.q1_parts(),
936 d.down_proj.q1_parts(),
937 ) else {
938 if block_diag {
939 eprintln!(
940 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
941 );
942 }
943 break;
944 };
945 MetalFfn::Dense {
946 gate: g,
947 up: u,
948 down: dn,
949 }
950 }
951 FfnKind::Moe(m) => {
952 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
953 if block_diag {
954 eprintln!(
955 "block-graph: L{scan} MoE outside the graph contract — run ends"
956 );
957 }
958 break;
959 };
960 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
961 model_ref.get_or_insert_with(|| model.clone());
962 }
963 MetalFfn::Moe(moe)
964 }
965 _ => {
966 if block_diag {
967 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
968 }
969 break;
970 }
971 };
972 match &lw.attn {
973 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
974 let parts = (
975 w.in_proj_qkv.q1_parts(),
976 w.in_proj_z.q1_parts(),
977 w.in_proj_a.f32_parts(),
978 w.in_proj_b.f32_parts(),
979 w.out_proj.q1_parts(),
980 );
981 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
982 if block_diag {
983 eprintln!(
984 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
985 w.in_proj_qkv.q1_parts().is_some(),
986 w.in_proj_z.q1_parts().is_some(),
987 w.in_proj_a.f32_parts().is_some(),
988 w.in_proj_b.f32_parts().is_some(),
989 w.out_proj.q1_parts().is_some(),
990 );
991 }
992 break;
993 };
994 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
995 model_ref.get_or_insert_with(|| model.clone());
996 }
997 let gl = GdnGpuLayer {
998 attn_norm: &lw.input_norm,
999 post_norm: &lw.post_norm,
1000 qkv,
1001 z,
1002 a,
1003 b,
1004 out,
1005 ffn,
1006 conv1d: &w.conv1d,
1007 a_log: &w.a_log,
1008 dt_bias: &w.dt_bias,
1009 gnorm: &w.norm,
1010 };
1011 match plan.last_mut() {
1012 Some(Item::Gdn { run, .. }) => run.push(gl),
1013 _ => plan.push(Item::Gdn {
1014 run: vec![gl],
1015 first: scan,
1016 }),
1017 }
1018 }
1019 AttnKind::Full {
1020 wq,
1021 wk,
1022 wv,
1023 wo,
1024 q_norm,
1025 k_norm,
1026 output_gate,
1027 softplus_gate: None,
1028 bias,
1029 } if !self.kv_cache.layers[scan].o1_sealed()
1030 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1035 {
1036 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1037 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1038 break;
1039 };
1040 if let QTensor::Mapped { model, .. } = wq {
1041 model_ref.get_or_insert_with(|| model.clone());
1042 }
1043 let cache = &self.kv_cache.layers[scan];
1044 let o1_metal = cache.o1.is_some()
1048 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1049 && cache.o1_views().is_some();
1050 let full_gpu = attend_contract
1051 && cache.mode == crate::kv_cache::KvMode::F32
1052 && (cache.o1.is_none() || o1_metal)
1053 && bias.is_none()
1054 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1055 && pk.1 == self.num_kv_heads * self.head_dim
1056 && pv.1 == self.num_kv_heads * self.head_dim
1057 && po.2 == self.num_heads * self.head_dim;
1058 plan.push(Item::Attn {
1059 l: AttnGpuLayer {
1060 attn_norm: &lw.input_norm,
1061 post_norm: &lw.post_norm,
1062 wq: pq,
1063 wk: pk,
1064 wv: pv,
1065 wo: po,
1066 ffn,
1067 },
1068 li: scan,
1069 q_norm: q_norm.as_deref(),
1070 k_norm: k_norm.as_deref(),
1071 output_gate: *output_gate,
1072 bias: bias
1073 .as_ref()
1074 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1075 full_gpu,
1076 });
1077 }
1078 _ => break,
1079 }
1080 scan += 1;
1081 }
1082 let Some(model) = model_ref else {
1083 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1084 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1085 }
1086 return start;
1087 };
1088 if plan.is_empty() {
1089 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1090 eprintln!("q1-graph: empty plan at layer {start}");
1091 }
1092 return start;
1093 }
1094 let has_moe = plan.iter().any(|it| match it {
1095 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1096 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1097 });
1098 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1099 let dev_attend = attend_contract
1100 && (self.head_dim <= 128
1101 || has_moe
1102 || (self.head_dim <= 256 && has_gdn)
1108 || attend_mode == "force"
1109 || attend_mode == "256");
1110 if !dev_attend {
1111 for it in &mut plan {
1112 if let Item::Attn { li, full_gpu, .. } = it {
1113 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1116 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1117 if !keep_o1 {
1118 *full_gpu = false;
1119 }
1120 }
1121 }
1122 }
1123 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1124 use std::sync::atomic::{AtomicBool, Ordering};
1125 static SAID: AtomicBool = AtomicBool::new(false);
1126 if !SAID.swap(true, Ordering::Relaxed) {
1127 let fg = plan
1128 .iter()
1129 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1130 .count();
1131 let att = plan
1132 .iter()
1133 .filter(|it| matches!(it, Item::Attn { .. }))
1134 .count();
1135 eprintln!(
1136 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1137 plan.len(),
1138 self.head_dim,
1139 self.rotary_dim,
1140 self.num_kv_heads,
1141 self.num_heads,
1142 );
1143 }
1144 }
1145 let dims = GraphDims {
1146 hidden: self.hidden_size,
1147 eps: self.rms_eps as f32,
1148 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1149 };
1150 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1151 return start;
1152 };
1153 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1154 nv: cfg.num_v_heads,
1155 nk: cfg.num_k_heads,
1156 dk: cfg.key_head_dim,
1157 dv: cfg.value_head_dim,
1158 kk: cfg.conv_kernel,
1159 hidden: self.hidden_size,
1160 inter: self.intermediate_size,
1161 c_dim: cfg.conv_dim(),
1162 eps: cfg.rms_eps as f32,
1163 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1164 });
1165 let mut valid = 0usize;
1169 let mut end = start;
1170 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1172 static ONCE: std::sync::Once = std::sync::Once::new();
1173 ONCE.call_once(|| {
1174 for it in &plan {
1175 match it {
1176 Item::Gdn { first, run } => {
1177 eprintln!("plan: Gdn first={first} len={}", run.len())
1178 }
1179 Item::Attn { li, full_gpu, .. } => {
1180 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1181 }
1182 }
1183 }
1184 });
1185 }
1186 for item in &plan {
1187 let ok = match item {
1188 Item::Gdn { run, .. } => gcfg
1189 .as_ref()
1190 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1191 .unwrap_or(false),
1192 Item::Attn { l, .. } => graph.attn_ok(l),
1193 };
1194 if !ok {
1195 if block_diag {
1196 eprintln!(
1197 "block-graph: plan item {} ({}) failed graph preflight",
1198 valid,
1199 match item {
1200 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1201 Item::Attn { li, .. } => format!("Attn L{li}"),
1202 }
1203 );
1204 }
1205 break;
1206 }
1207 valid += 1;
1208 end += match item {
1209 Item::Gdn { run, .. } => run.len(),
1210 Item::Attn { .. } => 1,
1211 };
1212 }
1213 plan.truncate(valid);
1214 if plan.is_empty() {
1215 return start;
1216 }
1217
1218 let inv_freq = self.inv_freq.clone();
1219 let pool = self.pool.clone();
1220 let (nh, nkv, hd, hs, rd, eps) = (
1221 self.num_heads,
1222 self.num_kv_heads,
1223 self.head_dim,
1224 self.hidden_size,
1225 self.rotary_dim,
1226 self.rms_eps,
1227 );
1228 let norm_style = self.norm_style;
1229 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1230 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1231 let kv_id = self.graph_kv_id;
1232 let mut pending: Vec<(usize, usize)> = Vec::new();
1235 let mut dev_attn: Vec<usize> = Vec::new();
1238 for item in &plan {
1239 let _xt0 = std::time::Instant::now();
1240 let _xkind: u32 = match item {
1241 Item::Gdn { .. } => 2,
1242 Item::Attn { .. } => 3,
1243 };
1244 if self.loop_final_norm {
1246 let item_start = match item {
1247 Item::Gdn { first, .. } => *first,
1248 Item::Attn { li, .. } => *li,
1249 };
1250 if item_start > start && self.is_loop_end(item_start - 1) {
1251 graph.encode_loop_norm(&self.weights.final_norm);
1252 }
1253 }
1254 match item {
1255 Item::Gdn { run, first } => {
1256 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1257 if l.linear_state.len() != want {
1258 l.linear_state = vec![0f32; want];
1259 }
1260 }
1261 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1262 .iter()
1263 .map(|l| l.linear_state.as_slice())
1264 .collect();
1265 let _ig = std::time::Instant::now();
1266 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1267 tracing::error!("q1 graph: GDN run refused after validation");
1269 return start;
1270 }
1271 graph.commit_kind = 2;
1274 graph.commit();
1275 crate::gpu::stageprof(0, _ig.elapsed());
1276 pending.push((*first, run.len()));
1277 }
1278 Item::Attn {
1279 l,
1280 li,
1281 q_norm,
1282 k_norm,
1283 output_gate,
1284 bias,
1285 full_gpu,
1286 } => {
1287 let _ia = std::time::Instant::now();
1288 if *full_gpu {
1290 let cache = &self.kv_cache.layers[*li];
1291 let o1p = if cache.o1.is_some() {
1292 match cache.o1_views() {
1293 Some(views) => Some(crate::gpu::O1AttnParams {
1294 views,
1295 epoch: self.o1_epoch,
1296 }),
1297 None => None,
1299 }
1300 } else {
1301 None
1302 };
1303 let o1_layer = cache.o1.is_some();
1304 if o1_layer && o1p.is_none() {
1305 }
1307 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1308 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1309 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1310 let p = crate::gpu::AttnDeviceParams {
1311 kv_id,
1312 layer: *li,
1313 nh,
1314 nkv,
1315 hd,
1316 rd,
1317 position,
1318 eps: eps as f32,
1319 gemma,
1320 output_gate: *output_gate,
1321 q_norm: *q_norm,
1322 k_norm: *k_norm,
1323 inv_freq: &inv_freq,
1324 cpu_k,
1325 cpu_v,
1326 cpu_stored,
1327 o1: o1p,
1328 };
1329 let o1_bad = o1_layer && p.o1.is_none();
1330 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1331 {
1332 if p.o1.is_none() {
1334 dev_attn.push(*li);
1335 }
1336 graph.commit_kind = 3;
1337 graph.commit();
1338 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1342 continue;
1343 }
1344 }
1346 graph.encode_attn_prefix(l);
1347 graph.sync();
1348 if !pending.is_empty() {
1349 let idxs: Vec<usize> =
1350 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1351 let mut outs: Vec<&mut [f32]> = self
1352 .kv_cache
1353 .layers
1354 .iter_mut()
1355 .enumerate()
1356 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1357 .map(|(_, s)| s.linear_state.as_mut_slice())
1358 .collect();
1359 graph.read_states(&mut outs);
1360 }
1361 let mut q_raw = attention::take_buf(l.wq.1);
1362 let mut k = attention::take_buf(l.wk.1);
1363 let mut v = attention::take_buf(l.wv.1);
1364 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1365 let cfg = QwenAttnCfg {
1366 num_heads: nh,
1367 num_kv_heads: nkv,
1368 head_dim: hd,
1369 hidden_size: hs,
1370 position,
1371 inv_freq: &inv_freq,
1372 rotary_dim: rd,
1373 scale: self.attn_scale,
1374 softcap: self.attn_softcap,
1375 window: None,
1376 v_norm: false,
1377 q_norm: *q_norm,
1378 k_norm: *k_norm,
1379 output_gate: *output_gate,
1380 softplus_gate: None,
1381 rope_scale: 1.0,
1382 bias: *bias,
1383 rms_eps: eps,
1384 norm_style,
1385 pool: pool.as_deref(),
1386 };
1387 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1");
1390 let _ = full_gpu;
1391 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
1392 let mut ao = attention::qwen_attention_core(
1393 q_raw,
1394 k,
1395 v,
1396 &mut self.kv_cache.layers[*li],
1397 &cfg,
1398 );
1399 if let Some((qr0, k0, v0)) = oracle_in {
1400 let (cq, _cg, ck, cv) = attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
1401 let mut h_now = vec![0f32; hs];
1402 graph.read_h(&mut h_now);
1403 let cache = &self.kv_cache.layers[*li];
1404 let n_after = cache.head_keys(0).len() / hd;
1405 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_keys(g)[..(n_after - 1) * hd]).collect();
1406 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| &cache.head_values(g)[..(n_after - 1) * hd]).collect();
1407 let p = crate::gpu::AttnDeviceParams {
1408 kv_id,
1409 layer: *li,
1410 nh,
1411 nkv,
1412 hd,
1413 rd,
1414 position,
1415 eps: eps as f32,
1416 gemma,
1417 output_gate: *output_gate,
1418 q_norm: *q_norm,
1419 k_norm: *k_norm,
1420 inv_freq: &inv_freq,
1421 cpu_k,
1422 cpu_v,
1423 cpu_stored: n_after - 1,
1424 o1: None,
1425 };
1426 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
1427 let md = |a: &[f32], b: &[f32]| a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
1428 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
1429 eprintln!(
1430 "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}",
1431 nn(&cq), md(&cq, &dq), nn(&ck), md(&ck, &dk), nn(&cv), md(&cv, &dv), nn(&ao), md(&ao, &dao)
1432 );
1433 } else {
1434 eprintln!("attn-oracle L{li}: device probe declined");
1435 }
1436 }
1437 graph.encode_attn_suffix(l, &ao);
1438 graph.commit();
1441 attention::recycle_buf(&mut ao);
1442 }
1443 }
1444
1445 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1446 }
1447 let mut lm_rows = None;
1452 if self.graph_want_logits
1453 && upto.is_none()
1454 && end == self.num_layers
1455 && std::env::var("CMF_GPU_LMHEAD")
1456 .map(|v| v != "0")
1457 .unwrap_or(true)
1458 {
1459 if let Some(lm) = self.weights.lm_head.q1_parts() {
1460 if graph.lm_head_ok(lm) {
1461 graph.encode_lm_head(&self.weights.final_norm, lm);
1462 lm_rows = Some(lm.1);
1463 }
1464 }
1465 }
1466 let _sy0 = std::time::Instant::now();
1467 graph.sync();
1468 let _rs0 = std::time::Instant::now();
1469 if !pending.is_empty() {
1470 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1471 let mut outs: Vec<&mut [f32]> = self
1472 .kv_cache
1473 .layers
1474 .iter_mut()
1475 .enumerate()
1476 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1477 .map(|(_, s)| s.linear_state.as_mut_slice())
1478 .collect();
1479 graph.read_states(&mut outs);
1480 }
1481 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1482 use std::sync::atomic::{AtomicU64, Ordering};
1483 static SY: AtomicU64 = AtomicU64::new(0);
1484 static RS: AtomicU64 = AtomicU64::new(0);
1485 static N: AtomicU64 = AtomicU64::new(0);
1486 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1487 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1488 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1489 if n % 100 == 0 {
1490 eprintln!(
1491 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1492 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1493 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1494 );
1495 }
1496 }
1497 if let Some(rows) = lm_rows {
1498 crate::gpu::hostprof_encode_done(_mt0);
1499 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1500 graph.read_logits(&mut lg);
1501 crate::gpu::hostprof_total(_mt0);
1502 lg.resize(self.vocab_size, 0.0);
1503 if let Some(c) = self.final_softcap {
1504 for l in lg.iter_mut() {
1505 *l = c * (*l / c).tanh();
1506 }
1507 }
1508 self.graph_logits = Some(lg);
1509 }
1510 graph.finish(h);
1511 for li in dev_attn {
1515 let mut krow = attention::take_buf(nkv * hd);
1516 let mut vrow = attention::take_buf(nkv * hd);
1517 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1518 let cache = &mut self.kv_cache.layers[li];
1519 cache.append(&krow, &vrow, &[]);
1520 let n = cache.seq_len;
1521 let mut imp = attention::take_buf(n);
1522 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1523 cache.accumulate_imp(&imp);
1524 attention::recycle_buf(&mut imp);
1525 }
1526 attention::recycle_buf(&mut krow);
1527 attention::recycle_buf(&mut vrow);
1528 }
1529 end
1530 }
1531
1532 pub fn new(
1533 tokenizer: Tokenizer,
1534 weights: PipelineWeights,
1535 hidden_size: usize,
1536 intermediate_size: usize,
1537 num_heads: usize,
1538 num_kv_heads: usize,
1539 head_dim: usize,
1540 num_layers: usize,
1541 physical_layers: usize,
1542 loop_final_norm: bool,
1543 vocab_size: usize,
1544 rms_eps: f64,
1545 rope_base: f32,
1546 norm_style: NormStyle,
1547 max_seq_len: usize,
1548 sampler_config: SamplerConfig,
1549 ) -> Self {
1550 let rng = match sampler_config.seed {
1551 Some(s) => SplitMix64::new(s),
1552 None => SplitMix64::from_entropy(),
1553 };
1554 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1555 let pool = Pool::from_env();
1556 if let Some(p) = &pool {
1557 tracing::info!("worker pool: {} threads", p.n_workers());
1558 }
1559 Self {
1560 gpu_plan: None,
1561 tokenizer: std::sync::Arc::new(tokenizer),
1562 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1563 sampler_config,
1564 weights,
1565 hidden_size,
1566 intermediate_size,
1567 num_heads,
1568 num_kv_heads,
1569 head_dim,
1570 num_layers,
1571 physical_layers,
1572 loop_final_norm,
1573 vocab_size,
1574 rms_eps,
1575 rope_base,
1576 norm_style,
1577 rotary_dim: head_dim,
1578 attention_heads_per_layer: None,
1579 vmf_cfg: None,
1580 gdn_cfg: None,
1581 kda_cfg: None,
1582 g3n: None,
1583 dsv4: None,
1584 dsv4_mtp: Vec::new(),
1585 dspark: None,
1586 dspark_pending: Vec::new(),
1587 dspark_hist: Vec::new(),
1588 dspark_real: Vec::new(),
1589 dspark_trunk_picks: Vec::new(),
1590 dspark_exp: Vec::new(),
1591 dspark_draft_ns: 0,
1592 logit_multiplier: None,
1593 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1594 kv_history: Vec::new(),
1595 short_conv_cfg: None,
1596 mtp: None,
1597 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1598 rng,
1599 sampler_scratch: SamplerScratch::default(),
1600 spec_forced: None,
1601 spec_q: Vec::new(),
1602 spec_p: Vec::new(),
1603 spec_res: Vec::new(),
1604 spec_qs: Vec::new(),
1605 spec_ps: Vec::new(),
1606 spec_ress: Vec::new(),
1607 mtp_graph_mode: None,
1608 #[cfg(target_os = "macos")]
1609 metal_verify: None,
1610 inv_freq,
1611 ws: ForwardScratch::new(hidden_size),
1612 pool,
1613 model: None,
1614 dyn_force_f32: false,
1615 dyn_skill_layers: Vec::new(),
1616 dyn_active: None,
1617 dyn_blend_loaded: false,
1618 dyn_phi_layer: None,
1619 dyn_phi_ema: Vec::new(),
1620 dyn_phi_seen: 0,
1621 dyn_router: None,
1622 o1_cfg: None,
1623 o1_epoch: 0,
1624 o1_flags: Vec::new(),
1625 trace: false,
1626 calib_temp: 1.0,
1627 confidence_on: true,
1628 embed_multiplier: 1.0,
1629 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1630 swa: None,
1631 sliding_layers: None,
1632 inv_freq_local: None,
1633 rotary_dim_local: None,
1634 rope_scale: 1.0,
1635 rope_scale_local: 1.0,
1636 global_attn: None,
1637 inv_freq_global: None,
1638 attn_v_norm: false,
1639 final_softcap: None,
1640 attn_softcap: 0.0,
1641 graph_want_logits: false,
1642 graph_logits: None,
1643 graph_kv_id: {
1644 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1645 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1646 },
1647 }
1648 }
1649
1650 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1657 self.o1_flags = match &cfg {
1658 Some(c) => {
1659 let mut flags = c.layer_flags(self.num_layers);
1660 for (li, f) in flags.iter_mut().enumerate() {
1661 if *f
1662 && !matches!(
1663 self.weights.layers[self.phys_layer(li)].attn,
1664 AttnKind::Full { .. }
1665 )
1666 {
1667 *f = false;
1668 }
1669 }
1670 flags
1671 }
1672 None => Vec::new(),
1673 };
1674 if let Some(c) = &cfg {
1675 let n = self.o1_flags.iter().filter(|&&f| f).count();
1676 tracing::info!(
1677 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1678 self.num_layers,
1679 c.m,
1680 c.w,
1681 c.sink,
1682 c.rect
1683 );
1684 }
1685 self.o1_cfg = cfg;
1686 }
1687
1688 pub fn o1_active(&self) -> bool {
1690 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1691 }
1692
1693 pub fn o1_begin(&mut self) {
1698 if let Some(c) = &self.o1_cfg {
1699 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1700 for (li, &f) in self.o1_flags.iter().enumerate() {
1701 if f {
1702 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1703 }
1704 }
1705 }
1706 }
1707
1708 pub fn o1_seal(&mut self) {
1712 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1713 if self.o1_cfg.is_none() {
1714 return;
1715 }
1716 for li in 0..self.num_layers {
1717 if self.o1_flags.get(li).copied().unwrap_or(false) {
1718 self.kv_cache.layers[li].o1_seal(self.num_heads);
1719 }
1720 }
1721 }
1722
1723 pub fn set_trace(&mut self, on: bool) {
1725 self.trace = on;
1726 }
1727
1728 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1731 self.rng = match config.seed {
1732 Some(seed) => SplitMix64::new(seed),
1733 None => SplitMix64::from_entropy(),
1734 };
1735 self.sampler_config = config;
1736 }
1737
1738 pub fn set_confidence(&mut self, on: bool) {
1743 self.confidence_on = on;
1744 }
1745
1746 pub fn set_calib_temp(&mut self, t: f32) {
1749 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1750 }
1751
1752 pub fn calib_temp(&self) -> f32 {
1754 self.calib_temp
1755 }
1756
1757 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1760 self.rotary_dim = rotary_dim.min(self.head_dim);
1761 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1762 }
1763
1764 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1765 QwenAttnCfg {
1766 num_heads: self.num_heads,
1767 num_kv_heads: self.num_kv_heads,
1768 head_dim: self.head_dim,
1769 hidden_size: self.hidden_size,
1770 position,
1771 inv_freq: &self.inv_freq,
1772 rotary_dim: self.rotary_dim,
1773 scale: self.attn_scale,
1774 softcap: self.attn_softcap,
1775 window: None,
1776 v_norm: false,
1777 q_norm: None,
1778 k_norm: None,
1779 output_gate: false,
1780 softplus_gate: None,
1781 rope_scale: self.rope_scale,
1782 bias: None,
1783 rms_eps: self.rms_eps,
1784 norm_style: self.norm_style,
1785 pool: self.pool.as_deref(),
1786 }
1787 }
1788
1789 pub fn generate(
1791 &mut self,
1792 prompt: &str,
1793 max_tokens: usize,
1794 task_mask: Option<&TaskMask>,
1795 on_token: Option<TokenCallback>,
1796 ) -> Result<GenerateResult, String> {
1797 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1798 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1799 }
1800
1801 pub fn generate_from_ids(
1809 &mut self,
1810 input_ids: &[u32],
1811 max_tokens: usize,
1812 task_mask: Option<&TaskMask>,
1813 mut on_token: Option<TokenCallback>,
1814 ) -> Result<GenerateResult, String> {
1815 if std::env::var("CMF_TRACE_H").is_ok() {
1816 eprintln!("input_ids: {input_ids:?}");
1817 }
1818 if input_ids.is_empty() {
1819 return Err("empty prompt: nothing to generate from".to_string());
1820 }
1821
1822 let reuse_from = {
1830 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1831 let h = &self.kv_history;
1832 if on
1833 && task_mask.is_none()
1834 && self.mtp.is_none()
1835 && self.o1_cfg.is_none()
1836 && !h.is_empty()
1837 && h.len() < input_ids.len()
1838 && input_ids[..h.len()] == h[..]
1839 {
1840 h.len()
1841 } else {
1842 0
1843 }
1844 };
1845 if reuse_from == 0 {
1846 self.kv_cache.clear();
1848 self.kv_history.clear();
1849 crate::gpu::graph_kv_reset(self.graph_kv_id);
1850 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1851 eprintln!(
1852 "kv-reuse: {} of {} prompt positions already cached",
1853 reuse_from,
1854 input_ids.len()
1855 );
1856 }
1857 crate::gpu::graph_race_begin_generation();
1858 self.o1_begin();
1859
1860 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
1866 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
1893 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
1894 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
1910 for lw in &self.weights.layers {
1911 if let FfnKind::Dense(d) = &lw.ffn {
1912 dense_n += 1;
1913 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
1914 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
1915 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
1916 {
1917 dense_q4tp += 1;
1918 }
1919 }
1920 }
1921 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
1922 let penalized = self.sampler_config.repetition_penalty != 1.0
1926 || self.sampler_config.presence_penalty != 0.0
1927 || !self.sampler_config.suppress_tokens.is_empty();
1928 #[cfg(feature = "gpu")]
1933 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
1934 #[cfg(not(feature = "gpu"))]
1935 let metal_wgpu = false;
1936 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
1937 let spec_wanted = match spec_env.as_deref() {
1938 Some("0") => false,
1939 Some(_) => {
1940 if metal_wgpu {
1941 tracing::warn!(
1942 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
1943 verified on this backend (garbage measured on Qwen3.5-0.8B)"
1944 );
1945 }
1946 true
1947 }
1948 None => spec_default_ok && !penalized && !metal_wgpu,
1949 };
1950 #[cfg(target_os = "macos")]
1953 let metal_graph = crate::gpu::q1_force()
1954 && crate::gpu::enabled_here()
1955 && std::env::var("CMF_GPU_BLOCK").map(|v| v != "0").unwrap_or(true);
1956 #[cfg(not(target_os = "macos"))]
1957 let metal_graph = false;
1958 let graph_spec = self.speculative
1959 && (graph_on || metal_graph)
1960 && self.mtp.is_some()
1961 && task_mask.is_none()
1962 && !self.o1_active()
1963 && spec_sampling_ok
1964 && spec_wanted;
1965 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
1972 let spec_active = self.speculative
1973 && self.mtp.is_some()
1974 && task_mask.is_none()
1975 && !self.o1_active()
1976 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
1977 let mut mtp = if spec_active { self.mtp.take() } else { None };
1980 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
1981 eprintln!(
1982 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
1983 mtp.is_some(),
1984 self.speculative,
1985 self.sampler_config.temperature < 1e-6,
1986 );
1987 }
1988 if let Some(m) = &mut mtp {
1989 m.kv.clear();
1990 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1992 self.mtp_graph_mode = None;
1993 }
1994 let mut router = if mtp.is_none() {
1998 self.dyn_router.take()
1999 } else {
2000 None
2001 };
2002 if let Some(r) = &mut router {
2003 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
2006 }
2007
2008 let mut all_ids = input_ids.to_vec();
2009 let mut generated = 0usize;
2010 let mut finish_reason = "max_tokens".to_string();
2011 let mut drafted = 0usize;
2012 let mut accepted = 0usize;
2013 let mut confidence: Vec<f32> = Vec::new();
2014 let trace_on = self.trace;
2015 let calib_temp = self.calib_temp;
2016 let mut traces: Vec<TokenTrace> = Vec::new();
2017
2018 let mut hidden = vec![0.0f32; self.hidden_size];
2024 let mut pos = reuse_from;
2025 let fuse_lm = mtp.is_none()
2034 && router.is_none()
2035 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
2036 self.graph_logits = None;
2037 self.graph_want_logits = false;
2038 let _tpf = std::time::Instant::now();
2039 let batch_k = std::env::var("CMF_BATCH_K")
2040 .ok()
2041 .and_then(|v| v.parse::<usize>().ok())
2042 .unwrap_or(0);
2043 while self.dsv4.is_some()
2054 && mtp.is_none()
2055 && pos < input_ids.len()
2056 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2057 {
2058 let end = (pos + prefill_chunk()).min(input_ids.len());
2059 let ids: Vec<u32> = input_ids[pos..end].to_vec();
2060 let mut lg = Vec::new();
2061 if let Some(b) = &mut self.dsv4 {
2062 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
2063 crate::dsv4::forward_chunk(
2064 g,
2065 layers,
2066 &cfg,
2067 st,
2068 &ids,
2069 pos,
2070 &self.inv_freq,
2071 self.pool.as_deref(),
2072 &mut lg,
2073 end == input_ids.len(),
2074 );
2075 }
2076 if end == input_ids.len() {
2077 self.graph_logits = Some(lg);
2078 }
2079 pos = end;
2080 hidden = vec![0.0; self.hidden_size];
2081 }
2082 let dyn_prefill = router.is_some();
2087 let graph_prefill = self.graph_prefill_preferred();
2093 #[cfg(target_os = "macos")]
2101 if task_mask.is_none()
2102 && !dyn_prefill
2103 && crate::gpu::q1_force()
2104 && crate::gpu::enabled_here()
2105 && self.gdn_cfg.is_some()
2106 && self.g3n.is_none()
2107 && input_ids.len() > 8
2108 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
2109 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
2110 {
2111 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
2112 .ok()
2113 .and_then(|v| v.parse().ok())
2114 .filter(|&v| (16..=512).contains(&v))
2115 .unwrap_or(256);
2116 let hs = self.hidden_size;
2117 let _tp = std::time::Instant::now();
2118 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2119 let end = (pos + chunk).min(input_ids.len());
2120 let Some(hb) = self.prefill_batch_metal(&input_ids[pos..end], pos) else {
2121 break;
2122 };
2123 if let Some(m) = &mut mtp {
2124 let n_pairs = if end < input_ids.len() { end - pos } else { end - pos - 1 };
2125 if n_pairs > 0 {
2126 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
2127 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
2128 .collect();
2129 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
2130 for (j, (h, t)) in pairs.iter().enumerate() {
2131 let h = h.to_vec();
2132 let _ = self.mtp_step(m, &h, *t, pos + j);
2133 }
2134 }
2135 }
2136 }
2137 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2138 pos = end;
2139 }
2140 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2141 eprintln!(
2142 "metal-prefill: {} of {} tokens in {:.1} ms",
2143 pos,
2144 input_ids.len(),
2145 _tp.elapsed().as_secs_f64() * 1e3
2146 );
2147 }
2148 }
2149 if task_mask.is_none()
2150 && !dyn_prefill
2151 && !graph_prefill
2152 && self.can_prefill_batched()
2153 && self.g3n.is_none()
2154 && input_ids.len() > 2
2155 {
2156 let chunk = prefill_chunk();
2162 let hs = self.hidden_size;
2163 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2164 let end = (pos + chunk).min(input_ids.len());
2165 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2166 if let Some(m) = &mut mtp {
2167 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2168 .ok()
2169 .and_then(|v| v.parse().ok())
2170 .unwrap_or(0);
2171 for p in pos..end {
2172 if p + 1 < input_ids.len() {
2173 if probe >= 1 && p + 2 < input_ids.len() {
2174 let (d1, mut hx) = self.mtp_step_h(
2178 m,
2179 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2180 input_ids[p + 1],
2181 p,
2182 );
2183 let mut ok = d1 == input_ids[p + 2];
2184 Self::chain_probe_note(0, ok);
2185 let mut d_prev = d1;
2186 let mut extra = 0usize;
2187 for j in 1..probe {
2188 if p + 2 + j >= input_ids.len() {
2189 break;
2190 }
2191 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2192 extra += 1;
2193 ok = ok && dj == input_ids[p + 2 + j];
2194 Self::chain_probe_note(j, ok);
2195 d_prev = dj;
2196 hx = hj;
2197 }
2198 m.kv.truncate_last(extra);
2199 } else {
2200 let _ = self.mtp_step(
2201 m,
2202 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2203 input_ids[p + 1],
2204 p,
2205 );
2206 }
2207 }
2208 }
2209 }
2210 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2211 pos = end;
2212 }
2213 }
2214 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2215 if task_mask.is_none()
2216 && !dyn_prefill
2217 && !graph_prefill
2218 && !pair_off
2219 && self.pair_supported()
2220 {
2221 while pos + 1 < input_ids.len()
2222 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2223 {
2224 let e1 = self.embed_single(input_ids[pos]);
2225 let e2 = self.embed_single(input_ids[pos + 1]);
2226 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2227 self.commit_linear_scratch();
2229 if let Some(m) = &mut mtp {
2230 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2231 if pos + 2 < input_ids.len() {
2232 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2233 .ok()
2234 .and_then(|v| v.parse().ok())
2235 .unwrap_or(0);
2236 if probe >= 1 && pos + 3 < input_ids.len() {
2237 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2241 let mut ok = d1 == input_ids[pos + 3];
2242 Self::chain_probe_note(0, ok);
2243 let mut d_prev = d1;
2244 let mut extra = 0usize;
2245 for j in 1..probe {
2246 if pos + 3 + j >= input_ids.len() {
2247 break;
2248 }
2249 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2250 extra += 1;
2251 ok = ok && dj == input_ids[pos + 3 + j];
2252 Self::chain_probe_note(j, ok);
2253 d_prev = dj;
2254 hx = hj;
2255 }
2256 m.kv.truncate_last(extra);
2257 } else {
2258 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2259 }
2260 }
2261 }
2262 hidden = h2;
2263 pos += 2;
2264 }
2265 }
2266 if batch_k > 0
2275 && graph_prefill
2276 && task_mask.is_none()
2277 && !self.o1_active()
2278 && mtp.is_none()
2279 && !dyn_prefill
2280 && pos + 1 < input_ids.len()
2281 {
2282 let hs = self.hidden_size;
2283 let chunk = batch_k;
2284 while pos < input_ids.len() {
2285 let end = (pos + chunk).min(input_ids.len());
2286 let bk = end - pos;
2287 let mut hiddens = vec![0f32; bk * hs];
2288 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2289 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2290 }
2291 let positions: Vec<usize> = (pos..end).collect();
2292 let t_chunk = std::time::Instant::now();
2293 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2294 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2295 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2296 eprintln!(
2297 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
2298 bk as f64 / (ms / 1000.0)
2299 );
2300 }
2301 {
2302 use std::sync::atomic::{AtomicBool, Ordering};
2303 static SAID: AtomicBool = AtomicBool::new(false);
2304 if !SAID.swap(true, Ordering::Relaxed) {
2305 if ok_b {
2306 tracing::info!("batched prefill: ACTIVE (k={bk})");
2307 } else {
2308 tracing::warn!("batched prefill declined — per-position graph");
2309 }
2310 }
2311 }
2312 if ok_b {
2313 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2314 pos = end;
2315 } else {
2316 break; }
2318 }
2319 }
2320 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2321 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2322 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2323 if let Some(m) = &mut mtp {
2324 if pos + 1 < input_ids.len() {
2325 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2331 .ok()
2332 .and_then(|v| v.parse().ok())
2333 .unwrap_or(0);
2334 if probe >= 1 && pos + 2 < input_ids.len() {
2335 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2336 let mut ok = d1 == input_ids[pos + 2];
2337 Self::chain_probe_note(0, ok);
2338 let mut d_prev = d1;
2339 let mut extra = 0usize;
2340 for j in 1..probe {
2341 if pos + 2 + j >= input_ids.len() {
2342 break;
2343 }
2344 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2345 extra += 1;
2346 ok = ok && dj == input_ids[pos + 2 + j];
2347 Self::chain_probe_note(j, ok);
2348 d_prev = dj;
2349 hx = hj;
2350 }
2351 m.kv.truncate_last(extra);
2354 } else {
2355 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2356 }
2357 }
2358 }
2359 pos += 1;
2360 }
2361 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2362 eprintln!(
2363 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2364 input_ids.len(),
2365 _tpf.elapsed().as_secs_f64() * 1000.0
2366 );
2367 }
2368 if self
2371 .cancel
2372 .swap(false, std::sync::atomic::Ordering::Relaxed)
2373 {
2374 self.kv_history.clear();
2375 if let Some(m) = mtp {
2376 self.mtp = Some(m);
2377 }
2378 return Ok(GenerateResult {
2379 text: String::new(),
2380 token_ids: Vec::new(),
2381 prompt_tokens: input_ids.len(),
2382 tokens_generated: 0,
2383 finish_reason: "cancelled".to_string(),
2384 mtp_drafted: 0,
2385 mtp_accepted: 0,
2386 token_confidence: Vec::new(),
2387 traces: Vec::new(),
2388 });
2389 }
2390
2391 self.o1_seal();
2394
2395 macro_rules! commit {
2397 ($id:expr) => {{
2398 all_ids.push($id);
2399 generated += 1;
2400 if self.tokenizer.is_eos($id) {
2401 finish_reason = "stop".to_string();
2402 false
2403 } else {
2404 let token_text = self.tokenizer.decode_token($id);
2405 let mut go = true;
2406 if let Some(ref mut cb) = on_token {
2407 if !cb(&token_text) {
2408 finish_reason = "cancelled".to_string();
2409 go = false;
2410 }
2411 }
2412 go
2413 }
2414 }};
2415 }
2416
2417 let mut spec_trial = SpecTrial::Spec {
2428 t0: std::time::Instant::now(),
2429 gen0: generated,
2430 rounds: 0,
2431 };
2432 let mut spec_mon = SpecMon::default();
2433 let mut spec_watchdog_off = false;
2434 let mut next_pos = input_ids.len();
2436 'decode: while generated < max_tokens {
2437 if self
2438 .cancel
2439 .swap(false, std::sync::atomic::Ordering::Relaxed)
2440 {
2441 finish_reason = "cancelled".to_string();
2442 break 'decode;
2443 }
2444 let forced = self.spec_forced.take();
2449 let mut logits = match (forced, self.graph_logits.take()) {
2450 (Some(_), _) => Vec::new(),
2451 (None, Some(lg)) => lg,
2452 (None, None) => {
2453 inference::rms_norm_into(
2454 &hidden,
2455 &self.weights.final_norm,
2456 self.rms_eps,
2457 self.norm_style,
2458 &mut self.ws.n1,
2459 );
2460 self.lm_head_forward(&self.ws.n1)
2461 }
2462 };
2463 if generated
2466 == std::env::var("CMF_LOGIT_DUMP_STEP")
2467 .ok()
2468 .and_then(|v| v.parse().ok())
2469 .unwrap_or(0)
2470 {
2471 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
2472 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
2473 for v in hidden.iter().chain(logits.iter()) {
2474 bytes.extend_from_slice(&v.to_le_bytes());
2475 }
2476 let _ = std::fs::write(&path, &bytes);
2477 }
2478 }
2479 let t_next = match forced {
2480 Some(c) => c,
2481 None => sampler::sample_with_scratch_pool(
2482 &logits,
2483 &self.sampler_config,
2484 &all_ids,
2485 &mut self.rng,
2486 &mut self.sampler_scratch,
2487 self.pool.as_deref(),
2488 ),
2489 };
2490 if self.confidence_on {
2491 confidence.push(if logits.is_empty() {
2492 0.0
2493 } else {
2494 sampler::top1_prob_pool(
2495 self.pool.as_deref(),
2496 &mut self.sampler_scratch,
2497 &logits,
2498 t_next,
2499 calib_temp,
2500 )
2501 });
2502 }
2503 if !logits.is_empty() {
2504 attention::recycle_buf(&mut logits);
2505 }
2506 if trace_on {
2507 let skill = router.as_ref().and_then(|r| r.active_id());
2511 traces.push(TokenTrace {
2512 t: generated,
2513 token_id: t_next,
2514 confidence: confidence.last().copied().unwrap_or(0.0),
2515 active_skill: skill,
2516 recon: None,
2517 switched: false,
2518 });
2519 }
2520 if !commit!(t_next) {
2521 break 'decode;
2522 }
2523 if generated >= max_tokens {
2524 break 'decode;
2525 }
2526
2527 if self.kv_cache.needs_eviction() {
2528 static SAID: std::sync::Once = std::sync::Once::new();
2534 SAID.call_once(|| {
2535 tracing::warn!(
2536 "KV cache full at {} positions — evicting half; quality \
2537 will degrade. Raise CMF_MAX_SEQ.",
2538 self.kv_cache.max_seq_len,
2539 );
2540 });
2541 let keep = (self.kv_cache.max_seq_len / 2).max(1);
2542 self.kv_cache.evict(keep);
2543 }
2544
2545 if graph_spec {
2548 match spec_trial {
2549 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
2550 spec_mon.plain_ms =
2551 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
2552 let keep = spec_mon.pays();
2553 tracing::info!(
2554 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2555 spec_mon.tokens,
2556 spec_mon.round_ms,
2557 spec_mon.plain_ms,
2558 if keep { "speculating" } else { "plain" }
2559 );
2560 spec_mon.fails = 0;
2561 spec_trial = SpecTrial::Decided {
2562 spec: keep,
2563 recheck_at: if keep { usize::MAX } else { generated + 128 },
2564 };
2565 }
2566 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
2567 spec_mon.n = 0;
2568 spec_trial = SpecTrial::Spec {
2569 t0: std::time::Instant::now(),
2570 gen0: generated,
2571 rounds: 0,
2572 };
2573 }
2574 _ => {}
2575 }
2576 spec_watchdog_off = matches!(
2577 spec_trial,
2578 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
2579 );
2580 }
2581 match &mut mtp {
2582 #[cfg(feature = "gpu")]
2584 Some(m)
2585 if graph_spec
2586 && !spec_watchdog_off
2587 && generated + 1 < max_tokens
2588 && next_pos > 0 =>
2589 {
2590 let t_round = std::time::Instant::now();
2591 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
2592 m,
2593 &hidden,
2594 t_next,
2595 next_pos,
2596 &mut drafted,
2597 &mut accepted,
2598 &mut all_ids,
2599 ) {
2600 next_pos = n_pos;
2601 hidden = new_h;
2602 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2603 eprintln!(
2604 "spec-round wall {:.1} ms → {} tokens",
2605 t_round.elapsed().as_secs_f64() * 1e3,
2606 extra.len() + 1
2607 );
2608 }
2609 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
2613 spec_trial = Self::spec_trial_round(
2616 spec_trial,
2617 &mut spec_mon,
2618 generated + extra.len() + 1,
2619 );
2620 let mut stopped = false;
2621 for &id in &extra {
2622 if self.confidence_on {
2623 confidence.push(0.0);
2624 }
2625 if !commit!(id) {
2626 stopped = true;
2627 break;
2628 }
2629 }
2630 if stopped {
2631 break 'decode;
2632 }
2633 continue 'decode;
2634 }
2635 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
2646 spec_mon.tokens = 0.0;
2647 spec_mon.fails = 3;
2648 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
2649 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2650 next_pos += 1;
2651 continue 'decode;
2652 }
2653 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2655 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2656 drafted += 1;
2657 let emb1 = self.embed_single(t_next);
2658 let emb2 = self.embed_single(draft);
2659 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2660
2661 inference::rms_norm_into(
2662 &h1,
2663 &self.weights.final_norm,
2664 self.rms_eps,
2665 self.norm_style,
2666 &mut self.ws.n1,
2667 );
2668 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2669 let t_after = sampler::sample_with_scratch_pool(
2670 &logits1,
2671 &self.sampler_config,
2672 &all_ids,
2673 &mut self.rng,
2674 &mut self.sampler_scratch,
2675 self.pool.as_deref(),
2676 );
2677 if self.confidence_on {
2678 confidence.push(sampler::top1_prob_pool(
2679 self.pool.as_deref(),
2680 &mut self.sampler_scratch,
2681 &logits1,
2682 t_after,
2683 calib_temp,
2684 ));
2685 }
2686 attention::recycle_buf(&mut logits1);
2687 if trace_on {
2688 traces.push(TokenTrace {
2691 t: generated,
2692 token_id: t_after,
2693 confidence: confidence.last().copied().unwrap_or(0.0),
2694 active_skill: None,
2695 recon: None,
2696 switched: false,
2697 });
2698 }
2699 let stop = !commit!(t_after);
2700
2701 if t_after == draft {
2702 accepted += 1;
2703 self.commit_linear_scratch();
2704 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2705 hidden = h2;
2706 next_pos += 2;
2707 } else {
2708 for layer in &mut self.kv_cache.layers {
2710 layer.truncate_last(1);
2711 }
2712 if !stop {
2713 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2714 hidden = self.forward_layers(
2715 &self.embed_single(t_after),
2716 next_pos + 1,
2717 None,
2718 );
2719 }
2720 next_pos += 2;
2721 }
2722 if stop {
2723 break 'decode;
2724 }
2725 }
2726 _ => {
2728 #[cfg(feature = "gpu")]
2733 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2734 static SAID: std::sync::Once = std::sync::Once::new();
2735 SAID.call_once(|| {
2736 eprintln!(
2737 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2738 !self.dsv4_mtp.is_empty(),
2739 task_mask.is_none(),
2740 router.is_none(),
2741 !trace_on,
2742 self.sampler_config.temperature < 1e-6,
2743 self.sampler_config.repetition_penalty == 1.0,
2744 );
2745 });
2746 }
2747 #[cfg(feature = "gpu")]
2748 if Self::dsv4_spec_on()
2749 && self.dsv4.is_some()
2750 && !self.dsv4_mtp.is_empty()
2751 && task_mask.is_none()
2752 && router.is_none()
2753 && !trace_on
2754 && self.sampler_config.temperature < 1e-6
2755 && self.sampler_config.repetition_penalty == 1.0
2756 && generated + 1 < max_tokens
2757 && all_ids.len() >= 2
2758 {
2759 let tip_token = all_ids[all_ids.len() - 2];
2760 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2761 tip_token,
2762 t_next,
2763 next_pos,
2764 &mut drafted,
2765 &mut accepted,
2766 ) {
2767 next_pos = n_pos;
2768 let mut stopped = false;
2769 for &id in &extra {
2770 if self.confidence_on {
2771 confidence.push(0.0);
2772 }
2773 if !commit!(id) {
2774 stopped = true;
2775 break;
2776 }
2777 }
2778 if stopped {
2779 break 'decode;
2780 }
2781 continue 'decode;
2782 }
2783 }
2784 self.graph_want_logits = fuse_lm;
2785 let mut t_fwd = t_next;
2791 let pure_greedy = self.sampler_config.temperature < 1e-6
2792 && self.sampler_config.repetition_penalty == 1.0
2793 && self.sampler_config.suppress_tokens.is_empty();
2794 let burst_k = std::env::var("CMF_MULTISTEP")
2799 .ok()
2800 .and_then(|v| v.parse::<usize>().ok())
2801 .unwrap_or(0);
2802 if pure_greedy
2803 && burst_k >= 1
2804 && fuse_lm
2805 && task_mask.is_none()
2806 && router.is_none()
2807 && !trace_on
2808 && !self.confidence_on
2809 {
2810 let mut stopped = false;
2811 loop {
2812 let room = max_tokens.saturating_sub(generated);
2813 if room <= 2 {
2814 break;
2815 }
2816 let k = burst_k.min(room - 1);
2817 if k < 1 {
2818 break;
2819 }
2820 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2821 break;
2822 };
2823 next_pos += k;
2824 for &id in &ids {
2825 if !commit!(id) {
2826 stopped = true;
2827 break;
2828 }
2829 }
2830 if stopped {
2831 break;
2832 }
2833 t_fwd = *ids.last().unwrap();
2834 }
2835 if stopped {
2836 break 'decode;
2837 }
2838 }
2839 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2840 next_pos += 1;
2841 if let Some(r) = &mut router {
2844 let phi = self.dyn_phi_ema.clone();
2845 let decision = r.step(&phi, generated);
2846 if let Some(new_active) = decision {
2847 let _ = self.set_active_skill(new_active);
2848 }
2849 if trace_on {
2852 if let Some(last) = traces.last_mut() {
2853 let e = r.last_best_e();
2854 last.recon = e.is_finite().then_some(e);
2855 last.switched = decision.is_some();
2856 }
2857 }
2858 }
2859 }
2860 }
2861 }
2862
2863 self.graph_want_logits = false;
2864 self.graph_logits = None;
2865 if router.is_some() {
2867 let _ = self.set_active_skill(None);
2868 }
2869 self.dyn_router = router.or(self.dyn_router.take());
2870 self.mtp = mtp.or(self.mtp.take());
2871
2872 let output_ids = &all_ids[input_ids.len()..];
2873 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2877 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2878 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2880 Ok(GenerateResult {
2881 text: self.tokenizer.decode(output_ids),
2882 token_ids: output_ids.to_vec(),
2883 prompt_tokens: input_ids.len(),
2884 tokens_generated: generated,
2885 finish_reason,
2886 mtp_drafted: drafted,
2887 mtp_accepted: accepted,
2888 token_confidence: confidence,
2889 traces,
2890 })
2891 }
2892
2893 fn mtp_step(
2897 &mut self,
2898 m: &mut MtpModule,
2899 hidden: &[f32],
2900 next_token: u32,
2901 position: usize,
2902 ) -> u32 {
2903 self.mtp_step_h(m, hidden, next_token, position).0
2904 }
2905
2906 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2910 use std::sync::Mutex;
2911 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2912 let mut t = T.lock().unwrap();
2913 if t.len() <= depth {
2914 t.resize(depth + 1, (0, 0));
2915 }
2916 t[depth].0 += 1;
2917 t[depth].1 += prefix_ok as u64;
2918 if depth == 0 && t[0].0 % 128 == 0 {
2919 let line: Vec<String> = t
2920 .iter()
2921 .enumerate()
2922 .map(|(d, (n, k))| {
2923 format!(
2924 "d{}={:.0}%({n})",
2925 d + 1,
2926 100.0 * *k as f64 / (*n).max(1) as f64
2927 )
2928 })
2929 .collect();
2930 eprintln!("mtp-chain: {}", line.join(" "));
2931 }
2932 }
2933
2934 fn mtp_step_hl(
2942 &mut self,
2943 m: &mut MtpModule,
2944 hidden: &[f32],
2945 next_token: u32,
2946 position: usize,
2947 ) -> (Vec<f32>, Vec<f32>) {
2948 #[cfg(target_os = "macos")]
2953 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
2954 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
2955 self.mtp_graph_mode = Some(true);
2956 return r;
2957 }
2958 self.mtp_graph_mode = Some(false);
2959 }
2960 #[cfg(feature = "gpu")]
2961 if self.mtp_graph_mode != Some(false) {
2962 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
2963 self.mtp_graph_mode = Some(true);
2964 return r;
2965 }
2966 if self.mtp_graph_mode == Some(true) {
2967 tracing::warn!("mtp graph declined mid-run — draft falls to the per-op path");
2972 }
2973 self.mtp_graph_mode = Some(false);
2974 }
2975 let e = self.embed_single(next_token);
2979 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2980 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2981 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2982 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2983 let mut x = vec![0.0f32; self.hidden_size];
2984 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2985
2986 let lw = &m.layer;
2988 inference::rms_norm_into(
2989 &x,
2990 &lw.input_norm,
2991 self.rms_eps,
2992 self.norm_style,
2993 &mut self.ws.n1,
2994 );
2995 let attn = match &lw.attn {
2996 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2998 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2999 AttnKind::Full {
3000 wq,
3001 wk,
3002 wv,
3003 wo,
3004 q_norm,
3005 k_norm,
3006 output_gate,
3007 softplus_gate,
3008 bias,
3009 } => {
3010 let mut cfg = self.attn_cfg(position);
3011 cfg.q_norm = q_norm.as_deref();
3012 cfg.k_norm = k_norm.as_deref();
3013 cfg.output_gate = *output_gate;
3014 cfg.softplus_gate = softplus_gate
3015 .as_ref()
3016 .map(|(gate, per_head)| (gate, *per_head));
3017 cfg.bias = bias
3018 .as_ref()
3019 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3020 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3021 }
3022 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
3023 unreachable!("MTP block is full attention")
3024 }
3025 };
3026 for (i, &a) in attn.iter().enumerate() {
3027 x[i] += a;
3028 }
3029 inference::rms_norm_into(
3030 &x,
3031 &lw.post_norm,
3032 self.rms_eps,
3033 self.norm_style,
3034 &mut self.ws.p1,
3035 );
3036 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
3037 for (i, &f) in ffn.iter().enumerate() {
3038 x[i] += f;
3039 }
3040
3041 inference::rms_norm_into(
3042 &x,
3043 &m.final_norm,
3044 self.rms_eps,
3045 self.norm_style,
3046 &mut self.ws.n1,
3047 );
3048 let lg = self.lm_head_forward(&self.ws.n1);
3049 (lg, x)
3050 }
3051
3052 fn mtp_step_h(
3054 &mut self,
3055 m: &mut MtpModule,
3056 hidden: &[f32],
3057 next_token: u32,
3058 position: usize,
3059 ) -> (u32, Vec<f32>) {
3060 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
3061 let draft = sampler::argmax(&lg);
3062 attention::recycle_buf(&mut lg);
3063 (draft, x)
3064 }
3065
3066 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
3072 match trial {
3073 SpecTrial::Spec { t0, gen0, rounds } => {
3074 let rounds = rounds + 1;
3075 if rounds >= 5 {
3076 if mon.plain_ms > 0.0 {
3077 let keep = mon.pays();
3078 mon.fails = 0;
3079 tracing::info!(
3080 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
3081 mon.tokens,
3082 mon.round_ms,
3083 mon.plain_ms,
3084 if keep { "speculating" } else { "plain" }
3085 );
3086 SpecTrial::Decided {
3087 spec: keep,
3088 recheck_at: if keep { usize::MAX } else { generated + 128 },
3089 }
3090 } else {
3091 SpecTrial::Plain {
3092 t0: std::time::Instant::now(),
3093 gen0: generated,
3094 }
3095 }
3096 } else {
3097 SpecTrial::Spec { t0, gen0, rounds }
3098 }
3099 }
3100 SpecTrial::Decided { spec: true, .. } => {
3101 if mon.pays() {
3102 mon.fails = 0;
3103 trial
3104 } else {
3105 mon.fails += 1;
3106 if mon.fails >= 4 {
3107 tracing::info!(
3108 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
3109 mon.tokens,
3110 mon.round_ms,
3111 mon.plain_ms
3112 );
3113 SpecTrial::Decided {
3114 spec: false,
3115 recheck_at: generated + 128,
3116 }
3117 } else {
3118 trial
3119 }
3120 }
3121 }
3122 other => other,
3123 }
3124 }
3125
3126 fn mtp_kv_id(&self) -> u64 {
3129 self.graph_kv_id | (1u64 << 40)
3130 }
3131
3132 const MTP_LAYER_BASE: usize = 0;
3137
3138 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
3141 let e = self.embed_single(next_token);
3142 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3143 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3144 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3145 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3146 let mut x = vec![0.0f32; self.hidden_size];
3147 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3148 x
3149 }
3150
3151 #[cfg(feature = "gpu")]
3154 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
3155 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
3156 return false;
3157 }
3158 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3159 || !crate::gpu::enabled_here()
3160 || self.attn_softcap > 0.0
3161 || self.attention_heads_per_layer.is_some()
3162 {
3163 return false;
3164 }
3165 matches!(
3166 &m.layer.attn,
3167 AttnKind::Full {
3168 softplus_gate: None,
3169 ..
3170 }
3171 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
3172 }
3173
3174 #[cfg(feature = "gpu")]
3180 fn mtp_step_graph(
3181 &mut self,
3182 m: &mut MtpModule,
3183 hidden: &[f32],
3184 next_token: u32,
3185 position: usize,
3186 ) -> Option<(Vec<f32>, Vec<f32>)> {
3187 if !self.mtp_graph_ok(m) {
3188 return None;
3189 }
3190 let lw = &m.layer;
3191 let AttnKind::Full {
3192 wq,
3193 wk,
3194 wv,
3195 wo,
3196 q_norm,
3197 k_norm,
3198 output_gate,
3199 softplus_gate,
3200 bias,
3201 } = &lw.attn
3202 else {
3203 return None;
3204 };
3205 if softplus_gate.is_some() {
3206 return None;
3207 }
3208 let FfnKind::Dense(d) = &lw.ffn else {
3209 return None;
3210 };
3211 let mut x = self.mtp_block_input(m, hidden, next_token);
3214 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3215 let (_, i, kind, rs) = t.graph_weight()?;
3216 Some(crate::gpu::GraphW {
3217 idx: i,
3218 kind,
3219 row_scale: rs,
3220 data: &[],
3221 })
3222 }
3223 let (model, _, _, _) = wq.graph_weight()?;
3224 let model = model.clone();
3225 let (lm_gw, lm_rows) = {
3226 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3227 (
3228 crate::gpu::GraphW {
3229 idx: i,
3230 kind,
3231 row_scale: rs,
3232 data: &[],
3233 },
3234 self.weights.lm_head.rows(),
3235 )
3236 };
3237 let layer = crate::gpu::GraphLayer {
3238 input_norm: &lw.input_norm,
3239 attn: crate::gpu::GraphAttn::Full {
3240 wq: gw(wq)?,
3241 wk: gw(wk)?,
3242 wv: gw(wv)?,
3243 wo: gw(wo)?,
3244 q_norm: q_norm.as_deref(),
3245 k_norm: k_norm.as_deref(),
3246 bias: bias
3247 .as_ref()
3248 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3249 output_gate: *output_gate,
3250 cpu_k: m.kv.k_heads(),
3251 cpu_v: m.kv.v_heads(),
3252 },
3253 post_norm: &lw.post_norm,
3254 ffn: crate::gpu::GraphFfn::Dense {
3255 gate: gw(&d.gate_proj)?,
3256 up: gw(&d.up_proj)?,
3257 down: gw(&d.down_proj)?,
3258 },
3259 };
3260 let nh = self.num_heads;
3261 let (nkv, hd, rd) = self.layer_geom(0);
3262 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3263 let mut logits = Vec::new();
3264 let ok = crate::gpu::forward_token_graph(
3265 &model,
3266 self.mtp_kv_id(),
3267 std::slice::from_ref(&layer),
3268 &[None],
3269 self.o1_epoch,
3270 &self.inv_freq,
3271 &mut x,
3272 nh,
3273 nkv,
3274 hd,
3275 rd,
3276 self.hidden_size,
3277 self.intermediate_size,
3278 position,
3279 self.kv_cache.max_seq_len,
3280 gemma,
3281 self.rms_eps as f32,
3282 Some((&lm_gw, lm_rows)),
3283 &m.final_norm,
3284 &mut logits,
3285 &[],
3286 1,
3287 None,
3288 None,
3289 None,
3290 Self::MTP_LAYER_BASE,
3291 true,
3292 );
3293 if !ok {
3294 return None;
3295 }
3296 logits.resize(self.vocab_size, 0.0);
3297 Some((logits, x))
3298 }
3299
3300 #[cfg(feature = "gpu")]
3307 fn mtp_warm_graph(
3308 &mut self,
3309 m: &mut MtpModule,
3310 pairs: &[(&[f32], u32)],
3311 first_pos: usize,
3312 ) -> bool {
3313 if pairs.is_empty() || !self.mtp_graph_ok(m) {
3314 return pairs.is_empty();
3315 }
3316 let hs = self.hidden_size;
3317 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
3320 for (h, t) in pairs {
3321 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
3322 }
3323 let lw = &m.layer;
3324 let AttnKind::Full {
3325 wq,
3326 wk,
3327 wv,
3328 wo,
3329 q_norm,
3330 k_norm,
3331 output_gate,
3332 bias,
3333 ..
3334 } = &lw.attn
3335 else {
3336 return false;
3337 };
3338 let FfnKind::Dense(d) = &lw.ffn else {
3339 return false;
3340 };
3341 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3342 let (_, i, kind, rs) = t.graph_weight()?;
3343 Some(crate::gpu::GraphW {
3344 idx: i,
3345 kind,
3346 row_scale: rs,
3347 data: &[],
3348 })
3349 }
3350 let Some((model, _, _, _)) = wq.graph_weight() else {
3351 return false;
3352 };
3353 let model = model.clone();
3354 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
3355 gw(wq),
3356 gw(wk),
3357 gw(wv),
3358 gw(wo),
3359 gw(&d.gate_proj),
3360 gw(&d.up_proj),
3361 gw(&d.down_proj),
3362 ) else {
3363 return false;
3364 };
3365 let layer = crate::gpu::GraphLayer {
3366 input_norm: &lw.input_norm,
3367 attn: crate::gpu::GraphAttn::Full {
3368 wq: gwq,
3369 wk: gwk,
3370 wv: gwv,
3371 wo: gwo,
3372 q_norm: q_norm.as_deref(),
3373 k_norm: k_norm.as_deref(),
3374 bias: bias
3375 .as_ref()
3376 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3377 output_gate: *output_gate,
3378 cpu_k: m.kv.k_heads(),
3379 cpu_v: m.kv.v_heads(),
3380 },
3381 post_norm: &lw.post_norm,
3382 ffn: crate::gpu::GraphFfn::Dense {
3383 gate: gg,
3384 up: gu,
3385 down: gd,
3386 },
3387 };
3388 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
3389 let nh = self.num_heads;
3390 let (nkv, hd, rd) = self.layer_geom(0);
3391 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3392 crate::gpu::forward_batch_graph(
3393 &model,
3394 self.mtp_kv_id(),
3395 std::slice::from_ref(&layer),
3396 &self.inv_freq,
3397 &mut hiddens,
3398 nh,
3399 nkv,
3400 hd,
3401 rd,
3402 hs,
3403 self.intermediate_size,
3404 &positions,
3405 self.kv_cache.max_seq_len,
3406 gemma,
3407 self.rms_eps as f32,
3408 pairs.len(),
3409 None,
3410 )
3411 }
3412
3413 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
3417 let e = self.embed_single(next_token);
3418 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3419 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3420 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3421 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3422 let mut x = vec![0.0f32; self.hidden_size];
3423 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3424 inference::rms_norm_into(
3425 &x,
3426 &m.layer.input_norm,
3427 self.rms_eps,
3428 self.norm_style,
3429 &mut self.ws.n1,
3430 );
3431 let attn = match &m.layer.attn {
3432 AttnKind::Full {
3433 wq,
3434 wk,
3435 wv,
3436 wo,
3437 q_norm,
3438 k_norm,
3439 output_gate,
3440 softplus_gate,
3441 bias,
3442 } => {
3443 let mut cfg = self.attn_cfg(position);
3444 cfg.q_norm = q_norm.as_deref();
3445 cfg.k_norm = k_norm.as_deref();
3446 cfg.output_gate = *output_gate;
3447 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
3448 cfg.bias = bias
3449 .as_ref()
3450 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3451 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3452 }
3453 _ => return,
3454 };
3455 let _ = attn;
3456 }
3457
3458 #[cfg(feature = "gpu")]
3465 #[allow(clippy::too_many_arguments)]
3466 fn graph_spec_step(
3467 &mut self,
3468 m: &mut MtpModule,
3469 hidden: &[f32],
3470 t_next: u32,
3471 next_pos: usize,
3472 drafted: &mut usize,
3473 accepted: &mut usize,
3474 all_ids: &mut Vec<u32>,
3478 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
3479 #[cfg(target_os = "macos")]
3490 let metal_native = crate::gpu::q1_force();
3491 #[cfg(not(target_os = "macos"))]
3492 let metal_native = false;
3493 #[cfg(feature = "gpu")]
3494 let k_default = if metal_native {
3495 7
3498 } else if crate::gpu_wgpu::verify_i8_on() {
3499 5
3500 } else {
3501 4
3502 };
3503 #[cfg(not(feature = "gpu"))]
3504 let k_default = 4;
3505 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
3506 .ok()
3507 .and_then(|v| v.parse().ok())
3508 .filter(|&v| (1..=8).contains(&v))
3509 .unwrap_or(k_default);
3510 if next_pos == 0 {
3511 return None;
3512 }
3513 let t_round = std::time::Instant::now();
3514 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
3530 let sub0 = subs();
3531 let cfg = self.sampler_config.clone();
3536 let penalized = !(cfg.repetition_penalty == 1.0
3537 && cfg.presence_penalty == 0.0
3538 && cfg.suppress_tokens.is_empty());
3539 let greedy_pen = cfg.temperature < 1e-6 && penalized;
3544 let sampling = cfg.temperature >= 1e-6;
3545 let sparse = sampling && sampler::sparse_ok(&cfg);
3551 let base_len = all_ids.len();
3552 if sampling && !sparse && self.spec_q.len() < k_spec {
3553 self.spec_q.resize_with(k_spec, Vec::new);
3554 }
3555 if sparse && self.spec_qs.len() < k_spec {
3556 self.spec_qs.resize_with(k_spec, Vec::new);
3557 }
3558 let mut drafts = Vec::with_capacity(k_spec);
3563 let mut hx = hidden.to_vec();
3564 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
3567 for j in 0..k_spec {
3568 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
3569 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
3570 if spec_dbg {
3571 let saved = self.mtp_graph_mode;
3572 self.mtp_graph_mode = Some(false);
3573 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3574 self.mtp_graph_mode = saved;
3575 m.kv.truncate_last(1);
3576 dbg_ref = Some(r);
3577 }
3578 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3579 if let Some((lg_cpu, h_cpu)) = dbg_ref {
3580 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
3581 let dl = lg.iter().zip(&lg_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3582 let dh = hj.iter().zip(&h_cpu).fold(0f32, |m, (a, b)| m.max((a - b).abs()));
3583 eprintln!(
3584 "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 {}",
3585 next_pos - 1 + j,
3586 sampler::argmax(&lg_cpu),
3587 sampler::argmax(&lg),
3588 n(&h_cpu),
3589 n(&hj),
3590 m.kv.seq_len
3591 );
3592 }
3593 let dj = if sparse {
3594 let mut q = std::mem::take(&mut self.spec_qs[j]);
3595 let ok = sampler::sparse_distribution_into(
3596 &lg,
3597 &cfg,
3598 all_ids,
3599 &mut self.sampler_scratch,
3600 self.pool.as_deref(),
3601 &mut q,
3602 );
3603 let d = if ok {
3604 sampler::draw_sparse(&q, &mut self.rng)
3605 } else {
3606 let t = sampler::argmax(&lg);
3608 q.clear();
3609 q.push((t, 1.0));
3610 t
3611 };
3612 self.spec_qs[j] = q;
3613 all_ids.push(d);
3614 d
3615 } else if sampling {
3616 let mut q = std::mem::take(&mut self.spec_q[j]);
3617 sampler::distribution_into(
3618 &lg,
3619 &cfg,
3620 all_ids,
3621 &mut self.sampler_scratch,
3622 self.pool.as_deref(),
3623 &mut q,
3624 );
3625 let d = sampler::draw(&q, &mut self.rng);
3626 self.spec_q[j] = q;
3627 all_ids.push(d); d
3629 } else if greedy_pen {
3630 let d = sampler::argmax_penalized(
3631 &lg,
3632 &cfg,
3633 all_ids,
3634 &mut self.sampler_scratch,
3635 self.pool.as_deref(),
3636 );
3637 all_ids.push(d);
3638 d
3639 } else {
3640 sampler::argmax(&lg)
3641 };
3642 attention::recycle_buf(&mut lg);
3643 drafts.push(dj);
3644 hx = hj;
3645 }
3646 all_ids.truncate(base_len);
3647 *drafted += k_spec;
3648 let t_draft = t_round.elapsed();
3649 let sub_draft = subs();
3650 let b = k_spec + 1;
3653 let mut hiddens = vec![0.0f32; b * self.hidden_size];
3654 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
3655 let e = self.embed_single(t);
3656 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
3657 }
3658 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
3659 let (lm_gw, lm_rows) = {
3660 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3661 (
3662 crate::gpu::GraphW {
3663 idx: i,
3664 kind,
3665 row_scale: rs,
3666 data: &[],
3667 },
3668 self.weights.lm_head.rows(),
3669 )
3670 };
3671 let mut logits = Vec::new();
3672 let final_norm = self.weights.final_norm.clone();
3673 #[cfg(target_os = "macos")]
3674 let ok = if metal_native {
3675 let lm = self.weights.lm_head.q1_parts()?;
3676 self.try_batch_graph_metal(&mut hiddens, &positions, b, Some((lm, &final_norm, &mut logits)))
3677 } else {
3678 self.try_batch_graph_wgpu(
3679 &mut hiddens,
3680 &positions,
3681 b,
3682 Some(crate::gpu::SpecTail {
3683 lm: lm_gw,
3684 lm_rows,
3685 final_norm: &final_norm,
3686 logits_out: &mut logits,
3687 }),
3688 )
3689 };
3690 #[cfg(not(target_os = "macos"))]
3691 let ok = self.try_batch_graph_wgpu(
3692 &mut hiddens,
3693 &positions,
3694 b,
3695 Some(crate::gpu::SpecTail {
3696 lm: lm_gw,
3697 lm_rows,
3698 final_norm: &final_norm,
3699 logits_out: &mut logits,
3700 }),
3701 );
3702 if !ok {
3703 m.kv.truncate_last(k_spec);
3706 return None;
3707 }
3708 #[cfg(target_os = "macos")]
3714 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
3715 let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3716 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
3717 let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
3718 let want_save = self.graph_want_logits;
3719 self.graph_want_logits = false;
3720 for (i, &t) in toks.iter().enumerate() {
3721 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
3722 let _ = self.graph_logits.take();
3723 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
3727 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
3728 }
3729 let ref_lg = self.logits_from_hidden(&hi);
3730 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
3731 let ra = sampler::argmax(&ref_lg);
3732 let va = sampler::argmax(row);
3733 let mut md = 0f32;
3734 let mut rms = 0f64;
3735 for j in 0..lm_rows.min(ref_lg.len()) {
3736 let d = (ref_lg[j] - row[j]).abs();
3737 md = md.max(d);
3738 rms += (d as f64) * (d as f64);
3739 }
3740 let mut hd = 0f32;
3741 for j in 0..self.hidden_size {
3742 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
3743 }
3744 eprintln!(
3745 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
3746 next_pos + i,
3747 if ra == va { "OK" } else { "MISMATCH" },
3748 (rms / lm_rows as f64).sqrt()
3749 );
3750 }
3751 self.graph_want_logits = want_save;
3752 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
3755 if l.linear_state.len() == st.len() {
3756 l.linear_state.copy_from_slice(&st);
3757 } else {
3758 l.linear_state = st;
3759 }
3760 }
3761 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
3762 let extra = l.seq_len.saturating_sub(n0);
3763 if extra > 0 {
3764 l.truncate_last(extra);
3765 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
3766 }
3767 }
3768 }
3769 let t_verify = t_round.elapsed();
3770 let sub_verify = subs();
3771 let mut a = 0usize;
3776 let mut forced: Option<u32> = None;
3777 let ids: Vec<u32> = if sparse {
3778 let mut p = std::mem::take(&mut self.spec_ps);
3779 let mut res = std::mem::take(&mut self.spec_ress);
3780 while a < k_spec {
3781 let ok = sampler::sparse_distribution_into(
3782 &logits[a * lm_rows..(a + 1) * lm_rows],
3783 &cfg,
3784 all_ids,
3785 &mut self.sampler_scratch,
3786 self.pool.as_deref(),
3787 &mut p,
3788 );
3789 if !ok {
3790 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
3791 p.clear();
3792 p.push((t, 1.0));
3793 }
3794 match sampler::spec_accept_or_correct_sparse(
3795 &p,
3796 &self.spec_qs[a],
3797 drafts[a],
3798 &mut self.rng,
3799 &mut res,
3800 ) {
3801 None => {
3802 all_ids.push(drafts[a]);
3803 a += 1;
3804 }
3805 Some(c) => {
3806 forced = Some(c);
3807 break;
3808 }
3809 }
3810 }
3811 all_ids.truncate(base_len);
3812 self.spec_ps = p;
3813 self.spec_ress = res;
3814 drafts.clone()
3815 } else if sampling {
3816 let mut p = std::mem::take(&mut self.spec_p);
3817 let mut res = std::mem::take(&mut self.spec_res);
3818 while a < k_spec {
3819 sampler::distribution_into(
3820 &logits[a * lm_rows..(a + 1) * lm_rows],
3821 &cfg,
3822 all_ids,
3823 &mut self.sampler_scratch,
3824 self.pool.as_deref(),
3825 &mut p,
3826 );
3827 match sampler::spec_accept_or_correct(
3828 &p,
3829 &self.spec_q[a],
3830 drafts[a],
3831 &mut self.rng,
3832 &mut res,
3833 self.pool.as_deref(),
3834 ) {
3835 None => {
3836 all_ids.push(drafts[a]);
3837 a += 1;
3838 }
3839 Some(c) => {
3840 forced = Some(c);
3841 break;
3842 }
3843 }
3844 }
3845 all_ids.truncate(base_len);
3846 self.spec_p = p;
3847 self.spec_res = res;
3848 drafts.clone()
3850 } else if greedy_pen {
3851 let mut ids: Vec<u32> = Vec::with_capacity(b);
3855 for i in 0..b {
3856 let t = sampler::argmax_penalized(
3857 &logits[i * lm_rows..(i + 1) * lm_rows],
3858 &cfg,
3859 all_ids,
3860 &mut self.sampler_scratch,
3861 self.pool.as_deref(),
3862 );
3863 ids.push(t);
3864 if i < k_spec && t == drafts[i] {
3865 all_ids.push(t);
3866 } else {
3867 break;
3868 }
3869 }
3870 all_ids.truncate(base_len);
3871 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
3872 a += 1;
3873 }
3874 ids
3877 } else {
3878 let ids: Vec<u32> = (0..b)
3879 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
3880 .collect();
3881 while a < k_spec && ids[a] == drafts[a] {
3882 a += 1;
3883 }
3884 ids
3885 };
3886 if spec_dbg {
3887 eprintln!("spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}", drafts, ids);
3888 }
3889 #[cfg(target_os = "macos")]
3893 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
3894 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
3895 {
3896 let snap: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3897 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
3898 let toks: Vec<u32> = std::iter::once(t_next).chain(drafts.iter().copied()).collect();
3899 let want_save = self.graph_want_logits;
3900 self.graph_want_logits = false;
3901 for (i, &t) in toks.iter().take(a + 1).enumerate() {
3902 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
3903 let _ = self.graph_logits.take();
3904 }
3905 self.graph_want_logits = want_save;
3906 let plain_states: Vec<Vec<f32>> = self.kv_cache.layers.iter().map(|l| l.linear_state.clone()).collect();
3907 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
3908 let mut rows = Vec::new();
3909 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens.iter()).enumerate() {
3910 let extra = l.seq_len.saturating_sub(*n0);
3911 if extra > 0 {
3912 let mut kk = Vec::new();
3913 let mut vv = Vec::new();
3914 for g in 0..nkv {
3915 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
3916 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
3917 }
3918 rows.push((li, kk, vv));
3919 l.truncate_last(extra);
3920 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
3921 }
3922 }
3923 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
3924 if l.linear_state.len() == st.len() {
3925 l.linear_state.copy_from_slice(&st);
3926 } else {
3927 l.linear_state = st;
3928 }
3929 }
3930 Some((plain_states, rows))
3931 } else {
3932 None
3933 };
3934 #[cfg(target_os = "macos")]
3936 if metal_native {
3937 self.metal_verify_commit(a);
3940 if let Some((plain_states, rows)) = commit_ref {
3941 crate::gpu_metal::queue_fence();
3942 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
3943 let mut worst_s = 0f32;
3944 let mut worst_li = 0usize;
3945 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
3946 if l.linear_state.len() != ps.len() || ps.is_empty() {
3947 continue;
3948 }
3949 let d = l.linear_state.iter().zip(ps).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
3950 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
3951 let rel = d / n.max(1e-6);
3952 if rel > worst_s {
3953 worst_s = rel;
3954 worst_li = li;
3955 }
3956 }
3957 let mut worst_k = 0f32;
3958 for (li, kk, vv) in &rows {
3959 let l = &self.kv_cache.layers[*li];
3960 let n0 = l.seq_len - (kk.len() / (nkv * hd));
3961 let mut ck = Vec::new();
3962 let mut cv = Vec::new();
3963 for g in 0..nkv {
3964 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
3965 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
3966 }
3967 if ck.len() == kk.len() {
3968 let dk = ck.iter().zip(kk).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
3969 let dv = cv.iter().zip(vv).fold(0f32, |m, (x, y)| m.max((x - y).abs()));
3970 worst_k = worst_k.max(dk).max(dv);
3971 } else {
3972 eprintln!("commit-check L{li}: kv row count mismatch {} vs {}", ck.len(), kk.len());
3973 }
3974 }
3975 eprintln!(
3976 "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}"
3977 );
3978 }
3979 } else if a + 1 < b {
3980 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
3981 }
3982 #[cfg(not(target_os = "macos"))]
3983 if a + 1 < b {
3984 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
3985 }
3986 *accepted += a;
3987 m.kv.truncate_last(k_spec.saturating_sub(1));
3998 #[cfg(target_os = "macos")]
3999 if metal_native && self.mtp_graph_mode == Some(true) {
4000 crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, m.kv.seq_len);
4003 }
4004 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
4005 if !warm_off && a > 0 {
4006 let mut warmed = false;
4009 #[cfg(target_os = "macos")]
4010 if metal_native && self.mtp_graph_mode == Some(true) {
4011 let pairs: Vec<(&[f32], u32)> = (0..a)
4015 .map(|j| (&hiddens[j * self.hidden_size..(j + 1) * self.hidden_size], ids[j]))
4016 .collect();
4017 warmed = self.mtp_warm_batch_metal(m, &pairs, next_pos);
4018 if !warmed {
4019 warmed = true;
4020 for j in 0..a {
4021 let row = hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
4022 if self.mtp_step_metal(m, &row, ids[j], next_pos + j, false).is_none() {
4023 warmed = false;
4024 break;
4025 }
4026 }
4027 }
4028 }
4029 if !warmed && self.mtp_graph_mode == Some(true) && !metal_native {
4030 let rows: Vec<Vec<f32>> = (0..a)
4031 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
4032 .collect();
4033 let pairs: Vec<(&[f32], u32)> = rows
4034 .iter()
4035 .zip(ids.iter())
4036 .map(|(r, &t)| (r.as_slice(), t))
4037 .collect();
4038 warmed = self.mtp_warm_graph(m, &pairs, next_pos);
4039 if !warmed {
4040 warmed = true;
4042 for j in 0..a {
4043 if self
4044 .mtp_step_graph(m, &rows[j], ids[j], next_pos + j)
4045 .is_none()
4046 {
4047 warmed = false;
4048 break;
4049 }
4050 }
4051 }
4052 }
4053 if !warmed {
4054 for j in 0..a {
4055 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
4056 let row = row.to_vec();
4057 self.mtp_warm(m, &row, ids[j], next_pos + j);
4058 }
4059 }
4060 }
4061 if let Some(c) = forced {
4065 self.spec_forced = Some(c);
4066 self.graph_logits = None;
4067 } else {
4068 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
4069 row.resize(self.vocab_size, 0.0);
4070 if let Some(c) = self.final_softcap {
4071 for l in row.iter_mut() {
4072 *l = c * (*l / c).tanh();
4073 }
4074 }
4075 self.graph_logits = Some(row);
4076 }
4077 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
4078 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4084 let end = subs();
4085 eprintln!(
4086 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
4087 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
4088 t_draft.as_secs_f64() * 1e3,
4089 sub_draft - sub0,
4090 (t_verify - t_draft).as_secs_f64() * 1e3,
4091 sub_verify - sub_draft,
4092 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
4093 end - sub_verify,
4094 );
4095 }
4096 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
4097 }
4098
4099 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
4108 if !self.pair_supported() {
4109 return (0.0, 0.0);
4110 }
4111 let emb1 = self.embed_single(1);
4112 let emb2 = self.embed_single(2);
4113 let pos = self.kv_cache.seq_len();
4114
4115 let t0 = std::time::Instant::now();
4116 for _ in 0..iters {
4117 let _ = self.forward_layers(&emb1, pos, None);
4118 let _ = self.forward_layers(&emb2, pos + 1, None);
4119 for l in &mut self.kv_cache.layers {
4120 l.truncate_last(2);
4121 }
4122 }
4123 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4124
4125 let t1 = std::time::Instant::now();
4126 for _ in 0..iters {
4127 let _ = self.forward_pair(&emb1, &emb2, pos);
4128 for l in &mut self.kv_cache.layers {
4129 l.truncate_last(2);
4130 }
4131 }
4132 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
4133 (singles_ms, pair_ms)
4134 }
4135
4136 fn pair_supported(&self) -> bool {
4144 !self.weights.layers.is_empty()
4151 && self.g3n.is_none()
4152 && !self
4153 .weights
4154 .layers
4155 .iter()
4156 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
4157 }
4158
4159 fn forward_pair(
4160 &mut self,
4161 emb1: &[f32],
4162 emb2: &[f32],
4163 position: usize,
4164 ) -> (Vec<f32>, Vec<f32>) {
4165 let mut h1 = emb1.to_vec();
4166 let mut h2 = emb2.to_vec();
4167 let (_nkv, _hd, hs, _rd, eps) = (
4168 self.num_kv_heads,
4169 self.head_dim,
4170 self.hidden_size,
4171 self.rotary_dim,
4172 self.rms_eps,
4173 );
4174 let pool = self.pool.clone();
4175
4176 for li in 0..self.num_layers {
4177 let lw = &self.weights.layers[self.phys_layer(li)];
4178 inference::rms_norm_into(
4181 &h1,
4182 &lw.input_norm,
4183 self.rms_eps,
4184 self.norm_style,
4185 &mut self.ws.n1,
4186 );
4187 inference::rms_norm_into(
4188 &h2,
4189 &lw.input_norm,
4190 self.rms_eps,
4191 self.norm_style,
4192 &mut self.ws.n2,
4193 );
4194
4195 let (a1, a2) = match &lw.attn {
4196 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
4197 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
4198 AttnKind::Linear(w) => {
4199 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
4200 let layer = &mut self.kv_cache.layers[li];
4201 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4202 vmf_phase_pair(
4203 &self.ws.n1,
4204 &self.ws.n2,
4205 w,
4206 &cfg,
4207 state,
4208 scratch,
4209 self.pool.as_deref(),
4210 )
4211 }
4212 AttnKind::LinearGdn(w) => {
4213 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4214 let layer = &mut self.kv_cache.layers[li];
4215 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4216 gdn_pair(
4217 &self.ws.n1,
4218 &self.ws.n2,
4219 w,
4220 &cfg,
4221 state,
4222 scratch,
4223 self.pool.as_deref(),
4224 )
4225 }
4226 AttnKind::ShortConv(w) => {
4227 let cfg = self
4228 .short_conv_cfg
4229 .expect("short-conv layer without short_conv_cfg");
4230 let layer = &mut self.kv_cache.layers[li];
4231 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
4232 short_conv_pair(
4233 &self.ws.n1,
4234 &self.ws.n2,
4235 w,
4236 &cfg,
4237 state,
4238 scratch,
4239 self.pool.as_deref(),
4240 )
4241 }
4242 AttnKind::Full {
4243 wq,
4244 wk,
4245 wv,
4246 wo,
4247 q_norm,
4248 k_norm,
4249 output_gate,
4250 softplus_gate,
4251 bias,
4252 } => {
4253 let inv_freq_l = self.layer_inv_freq(li);
4254 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4255 let cfg = QwenAttnCfg {
4256 num_heads: self.layer_num_heads(li),
4257 num_kv_heads: nkv_l,
4258 head_dim: hd_l,
4259 hidden_size: hs,
4260 position,
4261 inv_freq: &inv_freq_l,
4262 rotary_dim: rd_l,
4263 scale: self.attn_scale,
4264 softcap: self.attn_softcap,
4265 window: self.layer_window(li),
4266 v_norm: self.attn_v_norm,
4267 q_norm: q_norm.as_deref(),
4268 k_norm: k_norm.as_deref(),
4269 output_gate: *output_gate,
4270 softplus_gate: softplus_gate
4271 .as_ref()
4272 .map(|(gate, per_head)| (gate, *per_head)),
4273 rope_scale: self.layer_rope_scale(li),
4274 bias: bias
4275 .as_ref()
4276 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4277 rms_eps: eps,
4278 norm_style: self.norm_style,
4279 pool: pool.as_deref(),
4280 };
4281 attention::qwen_attention_pair(
4282 &self.ws.n1,
4283 &self.ws.n2,
4284 wq,
4285 wk,
4286 wv,
4287 wo,
4288 &mut self.kv_cache.layers[li],
4289 &cfg,
4290 )
4291 }
4292 };
4293 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
4294 Some(w) => (
4295 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
4296 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
4297 ),
4298 None => (a1, a2),
4299 };
4300 for i in 0..self.hidden_size {
4301 h1[i] += a1[i];
4302 h2[i] += a2[i];
4303 }
4304 let (mut a1, mut a2) = (a1, a2);
4305 attention::recycle_buf(&mut a1);
4306 attention::recycle_buf(&mut a2);
4307
4308 let lw = &self.weights.layers[self.phys_layer(li)];
4309 inference::rms_norm_into(
4310 &h1,
4311 &lw.post_norm,
4312 self.rms_eps,
4313 self.norm_style,
4314 &mut self.ws.p1,
4315 );
4316 inference::rms_norm_into(
4317 &h2,
4318 &lw.post_norm,
4319 self.rms_eps,
4320 self.norm_style,
4321 &mut self.ws.p2,
4322 );
4323 let (f1, f2) = match &lw.ffn {
4324 FfnKind::DenseMoe(dm) => (
4327 dense_moe_ffn(
4328 dm,
4329 &self.ws.p1,
4330 &h1,
4331 self.rms_eps,
4332 self.norm_style,
4333 self.pool.as_deref(),
4334 ),
4335 dense_moe_ffn(
4336 dm,
4337 &self.ws.p2,
4338 &h2,
4339 self.rms_eps,
4340 self.norm_style,
4341 self.pool.as_deref(),
4342 ),
4343 ),
4344 _ => ffn_forward_pair(
4345 &lw.ffn,
4346 &self.ws.p1,
4347 &self.ws.p2,
4348 self.pool.as_deref(),
4349 None,
4350 ),
4351 };
4352 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4353 Some(w) => (
4354 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
4355 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
4356 ),
4357 None => (f1, f2),
4358 };
4359 for i in 0..self.hidden_size {
4360 h1[i] += f1[i];
4361 h2[i] += f2[i];
4362 }
4363 let (mut f1, mut f2) = (f1, f2);
4364 attention::recycle_buf(&mut f1);
4365 attention::recycle_buf(&mut f2);
4366 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4367 for i in 0..self.hidden_size {
4368 h1[i] *= sc;
4369 h2[i] *= sc;
4370 }
4371 }
4372 if self.is_loop_end(li) && li + 1 < self.num_layers {
4374 h1 = inference::rms_norm(
4375 &h1,
4376 &self.weights.final_norm,
4377 self.rms_eps,
4378 self.norm_style,
4379 );
4380 h2 = inference::rms_norm(
4381 &h2,
4382 &self.weights.final_norm,
4383 self.rms_eps,
4384 self.norm_style,
4385 );
4386 }
4387 }
4388 (h1, h2)
4389 }
4390
4391 fn commit_linear_scratch(&mut self) {
4393 for layer in &mut self.kv_cache.layers {
4394 if !layer.linear_scratch.is_empty() {
4395 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
4396 layer.linear_scratch.clear();
4397 }
4398 }
4399 }
4400
4401 pub fn forward_ids(
4404 &mut self,
4405 ids: &[u32],
4406 task_mask: Option<&TaskMask>,
4407 ) -> Result<Vec<f32>, String> {
4408 if ids.is_empty() {
4409 return Err("empty id sequence".to_string());
4410 }
4411 self.kv_cache.clear();
4412 self.kv_history.clear();
4413 self.o1_begin();
4414 let mut hidden = vec![0.0f32; self.hidden_size];
4415 let mut pos = 0usize;
4416 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
4424 let chunk = prefill_chunk();
4428 let hs = self.hidden_size;
4429 while pos < ids.len() {
4430 let end = (pos + chunk).min(ids.len());
4431 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4432 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4433 pos = end;
4434 }
4435 }
4436 if task_mask.is_none()
4445 && !self.graph_prefill_preferred()
4446 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
4447 && self.pair_supported()
4448 {
4449 while pos + 1 < ids.len() {
4450 let e1 = self.embed_single(ids[pos]);
4451 let e2 = self.embed_single(ids[pos + 1]);
4452 let (_, h2) = self.forward_pair(&e1, &e2, pos);
4453 self.commit_linear_scratch();
4454 hidden = h2;
4455 pos += 2;
4456 }
4457 }
4458 while pos < ids.len() {
4459 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4460 pos += 1;
4461 }
4462 self.o1_seal();
4466 let normed = inference::rms_norm(
4467 &hidden,
4468 &self.weights.final_norm,
4469 self.rms_eps,
4470 self.norm_style,
4471 );
4472 Ok(self.lm_head_forward(&normed))
4473 }
4474
4475 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
4482 let (nll, cnt) = self.nll_ids_from(ids, 0);
4483 (nll / cnt.max(1) as f64).exp()
4484 }
4485
4486 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4491 self.kv_cache.clear();
4492 self.kv_history.clear();
4493 FFN_PROBE.with(|p| {
4494 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4495 });
4496 crate::gpu::cpu_scope(|| {
4497 for (pos, &id) in ids.iter().enumerate() {
4498 let emb = self.embed_single(id);
4499 let _ = self.forward_layers(&emb, pos, None);
4500 }
4501 });
4502 self.kv_cache.clear();
4503 self.kv_history.clear();
4504 FFN_PROBE
4505 .with(|p| p.borrow_mut().take())
4506 .unwrap_or_default()
4507 }
4508
4509 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
4513 self.kv_cache.clear();
4514 self.kv_history.clear();
4515 let mut nll = 0f64;
4516 let mut cnt = 0usize;
4517 let mut hidden = vec![0f32; self.hidden_size];
4518 for (pos, &id) in ids.iter().enumerate() {
4519 if pos > 0 {
4520 inference::rms_norm_into(
4521 &hidden,
4522 &self.weights.final_norm,
4523 self.rms_eps,
4524 self.norm_style,
4525 &mut self.ws.n1,
4526 );
4527 let mut logits = self.lm_head_forward(&self.ws.n1);
4528 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4529 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
4530 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
4531 nll -= p.max(1e-300).ln();
4532 cnt += 1;
4533 attention::recycle_buf(&mut logits);
4534 }
4535 let emb = self.embed_single(id);
4536 hidden = self.forward_layers(&emb, pos, Some(mask));
4537 }
4538 self.kv_cache.clear();
4539 self.kv_history.clear();
4540 (nll / cnt.max(1) as f64).exp()
4541 }
4542
4543 pub fn nll_ids_masked(
4562 &mut self,
4563 ids: &[u32],
4564 start: usize,
4565 task_mask: Option<&TaskMask>,
4566 ) -> (f64, usize) {
4567 self.nll_ids_inner(ids, start, task_mask)
4568 }
4569
4570 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
4571 self.nll_ids_inner(ids, start, None)
4572 }
4573
4574 fn nll_ids_inner(
4575 &mut self,
4576 ids: &[u32],
4577 start: usize,
4578 task_mask: Option<&TaskMask>,
4579 ) -> (f64, usize) {
4580 self.kv_cache.clear();
4581 self.kv_history.clear();
4582 let mut nll = 0f64;
4583 let mut cnt = 0usize;
4584 if self.can_prefill_batched() {
4585 const CHUNK: usize = 128;
4591 const LM_SUB: usize = 32;
4592 let n = ids.len().saturating_sub(1);
4593 let hs = self.hidden_size;
4594 let rows = self.weights.lm_head.rows();
4595 let mut pos = 0usize;
4596 while pos < n {
4597 let end = (pos + CHUNK).min(n);
4598 let bsz = end - pos;
4599 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4600 let mut k0 = 0usize;
4601 while k0 < bsz {
4602 let k1 = (k0 + LM_SUB).min(bsz);
4603 let sb = k1 - k0;
4604 if pos + k1 <= start {
4607 k0 = k1;
4608 continue;
4609 }
4610 let mut normed = vec![0.0f32; sb * hs];
4611 for k in 0..sb {
4612 let r = inference::rms_norm(
4613 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
4614 &self.weights.final_norm,
4615 self.rms_eps,
4616 self.norm_style,
4617 );
4618 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
4619 }
4620 let mut logits = vec![0.0f32; sb * rows];
4621 self.weights
4622 .lm_head
4623 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
4624 for k in 0..sb {
4625 if pos + k0 + k < start {
4626 continue;
4627 }
4628 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
4629 if let Some(mu) = self.logit_multiplier {
4630 for v in lg.iter_mut() {
4631 *v *= mu;
4632 }
4633 }
4634 if let Some(c) = self.final_softcap {
4638 for v in lg.iter_mut() {
4639 *v = c * (*v / c).tanh();
4640 }
4641 }
4642 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
4643 let target = ids[pos + k0 + k + 1] as usize;
4644 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4645 let lse: f64 = lg
4646 .iter()
4647 .map(|&v| ((v - max) as f64).exp())
4648 .sum::<f64>()
4649 .ln()
4650 + max as f64;
4651 nll += lse - lg[target] as f64;
4652 cnt += 1;
4653 if std::env::var("CMF_PPL_TRACE").is_ok() {
4654 let top = lg
4655 .iter()
4656 .enumerate()
4657 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4658 .map(|(i, _)| i)
4659 .unwrap_or(0);
4660 eprintln!(
4661 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
4662 pos + k0 + k,
4663 target,
4664 lse - lg[target] as f64,
4665 top,
4666 lg[target],
4667 lg[top]
4668 );
4669 }
4670 }
4671 k0 = k1;
4672 }
4673 pos = end;
4674 }
4675 self.kv_cache.clear();
4676 self.kv_history.clear();
4677 return (nll, cnt);
4678 }
4679 for pos in 0..ids.len().saturating_sub(1) {
4680 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4681 let out_of_band = self.graph_logits.take();
4689 if pos < start {
4690 continue;
4691 }
4692 let logits = match out_of_band {
4693 Some(lg) => lg,
4694 None => {
4695 let normed = inference::rms_norm(
4696 &hidden,
4697 &self.weights.final_norm,
4698 self.rms_eps,
4699 self.norm_style,
4700 );
4701 self.lm_head_forward(&normed)
4705 }
4706 };
4707 let target = ids[pos + 1] as usize;
4708 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4709 let lse: f64 = logits
4710 .iter()
4711 .map(|&v| ((v - max) as f64).exp())
4712 .sum::<f64>()
4713 .ln()
4714 + max as f64;
4715 let tok_nll = lse - logits[target] as f64;
4716 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4717 let top = logits
4718 .iter()
4719 .enumerate()
4720 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4721 .map(|(i, _)| i)
4722 .unwrap_or(0);
4723 eprintln!(
4724 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4725 logits[target], logits[top]
4726 );
4727 }
4728 nll += tok_nll;
4729 cnt += 1;
4730 }
4731 self.kv_cache.clear();
4732 self.kv_history.clear();
4733 (nll, cnt)
4734 }
4735
4736 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
4752 self.kv_cache.clear();
4753 self.kv_history.clear();
4754 self.o1_begin();
4755 let n = ids.len().saturating_sub(1);
4756 let p = prefill.min(n);
4757 let mut pos = 0usize;
4759 if self.can_prefill_batched() {
4760 const CHUNK: usize = 128;
4761 while pos < p {
4762 let end = (pos + CHUNK).min(p);
4763 let _ = self.prefill_batch(&ids[pos..end], pos);
4764 pos = end;
4765 }
4766 } else {
4767 while pos < p {
4768 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4769 pos += 1;
4770 }
4771 }
4772 self.o1_seal();
4773
4774 let mut nll = 0f64;
4775 let mut cnt = 0usize;
4776 for pos in p..n {
4777 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4778 let normed = inference::rms_norm(
4779 &hidden,
4780 &self.weights.final_norm,
4781 self.rms_eps,
4782 self.norm_style,
4783 );
4784 let logits = self.lm_head_forward(&normed);
4788 let target = ids[pos + 1] as usize;
4789 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4790 let lse: f64 = logits
4791 .iter()
4792 .map(|&v| ((v - max) as f64).exp())
4793 .sum::<f64>()
4794 .ln()
4795 + max as f64;
4796 let tok_nll = lse - logits[target] as f64;
4797 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4798 let top = logits
4799 .iter()
4800 .enumerate()
4801 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4802 .map(|(i, _)| i)
4803 .unwrap_or(0);
4804 eprintln!(
4805 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4806 logits[target], logits[top]
4807 );
4808 }
4809 nll += tok_nll;
4810 cnt += 1;
4811 }
4812 self.kv_cache.clear();
4813 self.kv_history.clear();
4814 (nll, cnt)
4815 }
4816
4817 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
4825 self.kv_cache.clear();
4826 self.kv_history.clear();
4827 let n = ids.len().saturating_sub(1);
4828 let mut correct = Vec::with_capacity(n);
4829 let mut pmax = Vec::with_capacity(n);
4830 for pos in 0..n {
4831 let emb = self.embed_single(ids[pos]);
4832 let hidden = self.forward_layers(&emb, pos, None);
4833 let normed = inference::rms_norm(
4834 &hidden,
4835 &self.weights.final_norm,
4836 self.rms_eps,
4837 self.norm_style,
4838 );
4839 let logits = self.lm_head_forward(&normed);
4843 let target = ids[pos + 1] as usize;
4844 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
4845 for (i, &v) in logits.iter().enumerate() {
4846 if v > mval {
4847 mval = v;
4848 amax = i;
4849 }
4850 }
4851 correct.push(amax == target);
4852 let row: Vec<f32> = temps
4853 .iter()
4854 .map(|&t| {
4855 let tt = t.max(1e-3);
4856 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
4857 1.0 / s.max(1e-12) })
4859 .collect();
4860 pmax.push(row);
4861 }
4862 self.kv_cache.clear();
4863 self.kv_history.clear();
4864 (correct, pmax)
4865 }
4866
4867 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
4874 let mut router = match self.dyn_router.take() {
4875 Some(r) => r,
4876 None => return (self.ppl_ids(ids), 0),
4877 };
4878 router.reset();
4879 self.dyn_phi_seen = 0;
4880 let _ = self.set_active_skill(None);
4881
4882 self.kv_cache.clear();
4883
4884 self.kv_history.clear();
4885 let mut nll = 0f64;
4886 let mut cnt = 0usize;
4887 for pos in 0..ids.len().saturating_sub(1) {
4888 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4889 let normed = inference::rms_norm(
4890 &hidden,
4891 &self.weights.final_norm,
4892 self.rms_eps,
4893 self.norm_style,
4894 );
4895 let logits = self.lm_head_forward(&normed);
4899 let target = ids[pos + 1] as usize;
4900 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4901 let lse: f64 = logits
4902 .iter()
4903 .map(|&v| ((v - max) as f64).exp())
4904 .sum::<f64>()
4905 .ln()
4906 + max as f64;
4907 let tok_nll = lse - logits[target] as f64;
4908 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4909 let top = logits
4910 .iter()
4911 .enumerate()
4912 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4913 .map(|(i, _)| i)
4914 .unwrap_or(0);
4915 eprintln!(
4916 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4917 logits[target], logits[top]
4918 );
4919 }
4920 nll += tok_nll;
4921 cnt += 1;
4922 let phi = self.dyn_phi_ema.clone();
4924 if let Some(new_active) = router.step(&phi, pos) {
4925 let _ = self.set_active_skill(new_active);
4926 }
4927 }
4928 let switches = router.switches.len();
4929 let _ = self.set_active_skill(None);
4930 self.dyn_router = Some(router);
4931 self.kv_cache.clear();
4932 self.kv_history.clear();
4933 ((nll / cnt.max(1) as f64).exp(), switches)
4934 }
4935
4936 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
4938 self.kv_cache.clear();
4939 self.kv_history.clear();
4940 let mut acc = vec![0f32; self.hidden_size];
4941 for (pos, &id) in ids.iter().enumerate() {
4942 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
4943 for (a, v) in acc.iter_mut().zip(&h) {
4944 *a += v;
4945 }
4946 }
4947 let n = ids.len().max(1) as f32;
4948 for a in acc.iter_mut() {
4949 *a /= n;
4950 }
4951 self.kv_cache.clear();
4952 self.kv_history.clear();
4953 acc
4954 }
4955
4956 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
4962 self.prefill_batch_masked(ids, start_pos, None)
4963 }
4964
4965 fn prefill_batch_masked(
4971 &mut self,
4972 ids: &[u32],
4973 start_pos: usize,
4974 task_mask: Option<&TaskMask>,
4975 ) -> Vec<f32> {
4976 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
4977 }
4978
4979 fn prefill_batch_span(
4985 &mut self,
4986 input: PrefillIn<'_>,
4987 start_pos: usize,
4988 task_mask: Option<&TaskMask>,
4989 from: usize,
4990 upto_excl: usize,
4991 ) -> Vec<f32> {
4992 let hs = self.hidden_size;
4993 let b = match input {
4994 PrefillIn::Ids(ids) => ids.len(),
4995 PrefillIn::Hidden(hb) => hb.len() / hs,
4996 };
4997 let upto_excl = upto_excl.min(self.num_layers);
4998 let mut h: Vec<f32>;
5002 let mut h_ready;
5003 match input {
5004 PrefillIn::Ids(_) => {
5005 h = vec![0.0; b * hs];
5006 h_ready = false;
5007 }
5008 PrefillIn::Hidden(hb) => {
5009 h = hb.to_vec();
5010 h_ready = true;
5011 }
5012 }
5013 let fill_h = |h: &mut Vec<f32>, me: &Self| {
5014 if let PrefillIn::Ids(ids) = input {
5015 for (bi, &id) in ids.iter().enumerate() {
5016 let e = me.embed_single(id);
5017 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
5018 }
5019 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5020 if let Some(t) = tp.parse::<usize>().ok() {
5021 if t >= start_pos && t < start_pos + ids.len() {
5022 let bi = t - start_pos;
5023 let row = &h[bi * hs..(bi + 1) * hs];
5024 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5025 eprintln!(
5026 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
5027 ids[bi], row[0], row[1], ids.len(), &ids[..ids.len().min(8)]
5028 );
5029 }
5030 }
5031 }
5032 }
5033 };
5034 let (_nkv, _hd, _rd, eps) = (
5035 self.num_kv_heads,
5036 self.head_dim,
5037 self.rotary_dim,
5038 self.rms_eps,
5039 );
5040 let pool = self.pool.clone();
5041 let norm_style = self.norm_style;
5042
5043 #[cfg(target_os = "macos")]
5044 let mut chunk_skip_until = 0usize;
5045 for li in from..upto_excl {
5046 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
5053 if task_mask.is_none() {
5054 if li < chunk_skip_until {
5055 continue;
5056 }
5057 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
5063 fill_h(&mut h, self);
5064 h_ready = true;
5065 }
5066 let ids_for_embed = match input {
5067 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
5068 PrefillIn::Hidden(_) => None,
5069 };
5070 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
5071 if end > li {
5072 h_ready = true;
5073 chunk_skip_until = end;
5074 if self.is_loop_end(end - 1) && end < self.num_layers {
5077 for bi in 0..b {
5078 let normed = inference::rms_norm(
5079 &h[bi * hs..(bi + 1) * hs],
5080 &self.weights.final_norm,
5081 eps,
5082 norm_style,
5083 );
5084 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5085 }
5086 }
5087 continue;
5088 }
5089 }
5090 if !h_ready {
5091 fill_h(&mut h, self);
5092 h_ready = true;
5093 }
5094 let lw = &self.weights.layers[self.phys_layer(li)];
5095 match &lw.attn {
5097 AttnKind::Kda(w) => {
5098 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5100 let mut normed = vec![0.0f32; b * hs];
5101 for bi in 0..b {
5102 inference::rms_norm_into(
5103 &h[bi * hs..(bi + 1) * hs],
5104 &lw.input_norm,
5105 eps,
5106 norm_style,
5107 &mut normed[bi * hs..(bi + 1) * hs],
5108 );
5109 }
5110 let attn = crate::linear_core::kda_forward_batch(
5111 &normed,
5112 b,
5113 w,
5114 &cfg,
5115 &mut self.kv_cache.layers[li].linear_state,
5116 pool.as_deref(),
5117 );
5118 for (dst, &a) in h.iter_mut().zip(&attn) {
5119 *dst += a;
5120 }
5121 }
5122 AttnKind::LinearGdn(w) => {
5123 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5125 let mut normed = vec![0.0f32; b * hs];
5126 for bi in 0..b {
5127 let r = inference::rms_norm(
5128 &h[bi * hs..(bi + 1) * hs],
5129 &lw.input_norm,
5130 eps,
5131 norm_style,
5132 );
5133 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5134 }
5135 let attn = crate::linear_core::gdn_forward_batch(
5136 &normed,
5137 b,
5138 w,
5139 &cfg,
5140 &mut self.kv_cache.layers[li].linear_state,
5141 pool.as_deref(),
5142 );
5143 for (dst, &a) in h.iter_mut().zip(&attn) {
5144 *dst += a;
5145 }
5146 }
5147 AttnKind::ShortConv(w) => {
5148 let cfg = self
5151 .short_conv_cfg
5152 .expect("short-conv layer without short_conv_cfg");
5153 let mut normed = vec![0.0f32; b * hs];
5154 for bi in 0..b {
5155 inference::rms_norm_into(
5156 &h[bi * hs..(bi + 1) * hs],
5157 &lw.input_norm,
5158 eps,
5159 norm_style,
5160 &mut normed[bi * hs..(bi + 1) * hs],
5161 );
5162 }
5163 let attn = short_conv_forward_batch(
5164 &normed,
5165 b,
5166 w,
5167 &cfg,
5168 &mut self.kv_cache.layers[li].linear_state,
5169 pool.as_deref(),
5170 );
5171 for (dst, &a) in h.iter_mut().zip(&attn) {
5172 *dst += a;
5173 }
5174 }
5175 AttnKind::Mla(w) => {
5176 let inv_freq_l = self.layer_inv_freq(li);
5179 let rs = self.layer_rope_scale(li);
5180 let mut normed = vec![0.0f32; hs];
5181 for bi in 0..b {
5182 inference::rms_norm_into(
5183 &h[bi * hs..(bi + 1) * hs],
5184 &lw.input_norm,
5185 eps,
5186 norm_style,
5187 &mut normed,
5188 );
5189 let ao = mla_attention(
5190 w,
5191 &normed,
5192 &mut self.kv_cache.layers[li],
5193 start_pos + bi,
5194 &inv_freq_l,
5195 rs,
5196 eps,
5197 pool.as_deref(),
5198 );
5199 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
5200 *dst += a;
5201 }
5202 }
5203 }
5204 AttnKind::Full {
5205 wq,
5206 wk,
5207 wv,
5208 wo,
5209 q_norm,
5210 k_norm,
5211 output_gate,
5212 softplus_gate,
5213 bias,
5214 } => {
5215 let mut normed = vec![0.0f32; b * hs];
5219 for bi in 0..b {
5220 inference::rms_norm_into(
5221 &h[bi * hs..(bi + 1) * hs],
5222 &lw.input_norm,
5223 eps,
5224 norm_style,
5225 &mut normed[bi * hs..(bi + 1) * hs],
5226 );
5227 }
5228 let inv_freq_l = self.layer_inv_freq(li);
5229 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5230 let cfg = QwenAttnCfg {
5231 num_heads: self.layer_num_heads(li),
5232 num_kv_heads: nkv_l,
5233 head_dim: hd_l,
5234 hidden_size: hs,
5235 position: start_pos,
5236 inv_freq: &inv_freq_l,
5237 rotary_dim: rd_l,
5238 scale: self.attn_scale,
5239 softcap: self.attn_softcap,
5240 window: self.layer_window(li),
5241 v_norm: self.attn_v_norm,
5242 q_norm: q_norm.as_deref(),
5243 k_norm: k_norm.as_deref(),
5244 output_gate: *output_gate,
5245 softplus_gate: softplus_gate
5246 .as_ref()
5247 .map(|(gate, per_head)| (gate, *per_head)),
5248 rope_scale: self.layer_rope_scale(li),
5249 bias: bias
5250 .as_ref()
5251 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5252 rms_eps: eps,
5253 norm_style,
5254 pool: pool.as_deref(),
5255 };
5256 let mut attn = attention::qwen_attention_batch(
5257 &normed,
5258 b,
5259 wq,
5260 wk,
5261 wv,
5262 wo,
5263 &mut self.kv_cache.layers[li],
5264 &cfg,
5265 );
5266 if let Some(w) = &lw.attn_out_norm {
5267 for bi in 0..b {
5268 inference::rms_norm_into(
5269 &attn[bi * hs..(bi + 1) * hs],
5270 w,
5271 eps,
5272 norm_style,
5273 &mut normed[bi * hs..(bi + 1) * hs],
5274 );
5275 }
5276 attn.copy_from_slice(&normed);
5277 }
5278 for (dst, &a) in h.iter_mut().zip(&attn) {
5279 *dst += a;
5280 }
5281 }
5282 AttnKind::Linear(w) => {
5283 for bi in 0..b {
5284 let normed = inference::rms_norm(
5285 &h[bi * hs..(bi + 1) * hs],
5286 &lw.input_norm,
5287 eps,
5288 norm_style,
5289 );
5290 vmf_phase_forward(
5291 &normed,
5292 w,
5293 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
5294 &mut self.kv_cache.layers[li].linear_state,
5295 pool.as_deref(),
5296 )
5297 .iter()
5298 .enumerate()
5299 .for_each(|(i, &a)| h[bi * hs + i] += a);
5300 }
5301 }
5302 }
5303
5304 let lw = &self.weights.layers[self.phys_layer(li)];
5306 let mut post = vec![0.0f32; b * hs];
5307 for bi in 0..b {
5308 let r =
5309 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
5310 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5311 }
5312 let mask_row = task_mask
5315 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
5316 .and_then(|m| m.ffn_masks.get(li))
5317 .map(|v| v.as_slice());
5318 let mut ffn = match &lw.ffn {
5319 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
5320 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
5321 FfnKind::DenseMoe(dm) => {
5324 let mut out = vec![0.0f32; b * hs];
5325 for bi in 0..b {
5326 let r = dense_moe_ffn(
5327 dm,
5328 &post[bi * hs..(bi + 1) * hs],
5329 &h[bi * hs..(bi + 1) * hs],
5330 eps,
5331 norm_style,
5332 pool.as_deref(),
5333 );
5334 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
5335 }
5336 out
5337 }
5338 };
5339 if let Some(w) = &lw.ffn_out_norm {
5340 for bi in 0..b {
5341 inference::rms_norm_into(
5342 &ffn[bi * hs..(bi + 1) * hs],
5343 w,
5344 eps,
5345 norm_style,
5346 &mut post[bi * hs..(bi + 1) * hs],
5347 );
5348 }
5349 ffn.copy_from_slice(&post);
5350 }
5351 for (dst, &f) in h.iter_mut().zip(&ffn) {
5352 *dst += f;
5353 }
5354 if let Some(sc) = lw.layer_scale {
5355 for v in h.iter_mut() {
5356 *v *= sc;
5357 }
5358 }
5359 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5360 if let Some(t) = tp.parse::<usize>().ok() {
5361 if t >= start_pos && t < start_pos + b {
5362 let bi = t - start_pos;
5363 let row = &h[bi * hs..(bi + 1) * hs];
5364 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
5365 eprintln!(
5366 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5367 row[0], row[1]
5368 );
5369 }
5370 }
5371 }
5372 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
5376 let row = &h[(b - 1) * hs..b * hs];
5377 let rms =
5378 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
5379 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
5380 eprintln!(
5381 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
5382 match &self.weights.layers[self.phys_layer(li)].attn {
5383 AttnKind::LinearGdn(_) => "gdn",
5384 AttnKind::Linear(_) => "vmf",
5385 AttnKind::ShortConv(_) => "conv",
5386 _ => "attn",
5387 },
5388 match &lw.ffn {
5389 FfnKind::Moe(_) => "moe",
5390 FfnKind::Dense(_) => "dense",
5391 FfnKind::DenseMoe(_) => "dense+moe",
5392 },
5393 );
5394 }
5395 if self.is_loop_end(li) && li + 1 < self.num_layers {
5397 for bi in 0..b {
5398 let normed = inference::rms_norm(
5399 &h[bi * hs..(bi + 1) * hs],
5400 &self.weights.final_norm,
5401 eps,
5402 norm_style,
5403 );
5404 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
5405 }
5406 }
5407 if std::env::var("CMF_TRACE_H").is_ok() {
5408 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
5409 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
5410 eprintln!(
5411 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
5412 lw.layer_scale
5413 );
5414 }
5415 }
5416 crate::gpu::set_layer(-1); h
5418 }
5419
5420 fn embed_single(&self, id: u32) -> Vec<f32> {
5422 let mut out = vec![0.0f32; self.hidden_size];
5423 if (id as usize) < self.weights.embed_tokens.rows() {
5424 self.weights.embed_tokens.row_f32(id as usize, &mut out);
5425 }
5426 if self.embed_multiplier != 1.0 {
5427 for v in out.iter_mut() {
5428 *v *= self.embed_multiplier;
5429 }
5430 }
5431 if self.dsv4.is_some() {
5435 let mut v = vec![0.0f32; self.hidden_size.max(1)];
5436 v[0] = id as f32;
5437 return v;
5438 }
5439 if let Some(b) = &self.g3n {
5442 return b.0.extend_embedding(id, &out, self.pool.as_deref());
5443 }
5444 out
5445 }
5446
5447 #[cfg(target_os = "macos")]
5453 fn chunk_run_gpu(
5454 &mut self,
5455 li0: usize,
5456 h: &mut [f32],
5457 b: usize,
5458 pos0: usize,
5459 embed_ids: Option<&[u32]>,
5460 cap: usize,
5461 ) -> usize {
5462 if !crate::gpu::enabled_here()
5466 || std::env::var("CMF_GPU_CHUNK")
5467 .map(|v| v == "0")
5468 .unwrap_or(false)
5469 || b < 32
5470 || self.swa.is_some()
5471 || self.global_attn.is_some()
5472 || self.attn_v_norm
5473 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
5474 {
5475 return li0;
5476 }
5477 let Some(model) = self.model.clone() else {
5478 return li0;
5479 };
5480 let inv_freq = self.inv_freq.clone();
5481 let (nh, nkv, hd, hs) = (
5482 self.num_heads,
5483 self.num_kv_heads,
5484 self.head_dim,
5485 self.hidden_size,
5486 );
5487 let loop_end = if self.loop_final_norm {
5491 ((li0 / self.physical_layers) + 1) * self.physical_layers
5492 } else {
5493 self.num_layers
5494 };
5495 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
5496 let mut stored_at: Vec<usize> = Vec::new();
5497 for li in li0..self.num_layers.min(loop_end).min(cap) {
5498 let lw = &self.weights.layers[self.phys_layer(li)];
5499 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
5500 break;
5501 }
5502 let AttnKind::Full {
5503 wq,
5504 wk,
5505 wv,
5506 wo,
5507 q_norm,
5508 k_norm,
5509 output_gate: false,
5510 softplus_gate: None,
5511 bias,
5512 } = &lw.attn
5513 else {
5514 break;
5515 };
5516 let FfnKind::Dense(d) = &lw.ffn else { break };
5517 if d.act != Act::Silu {
5518 break;
5519 }
5520 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
5525 t.q8_row_parts()
5526 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5527 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5528 }
5529 let parts = (
5530 cw(wq),
5531 cw(wk),
5532 cw(wv),
5533 cw(wo),
5534 cw(&d.gate_proj),
5535 cw(&d.up_proj),
5536 cw(&d.down_proj),
5537 );
5538 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
5539 else {
5540 break;
5541 };
5542 let layer = &self.kv_cache.layers[li];
5543 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
5544 break;
5545 }
5546 stored_at.push(layer.head_len(0));
5547 layers.push(crate::gpu_metal::ChunkLayer {
5548 model: &model,
5549 kv_id: self.graph_kv_id,
5550 layer: li,
5551 wq: pq,
5552 wk: pk,
5553 wv: pv,
5554 wo: po,
5555 gate: pg,
5556 up: pu,
5557 down: pd,
5558 input_norm: &lw.input_norm,
5559 post_norm: &lw.post_norm,
5560 bias: bias
5561 .as_ref()
5562 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
5563 q_norm: q_norm.as_deref(),
5564 k_norm: k_norm.as_deref(),
5565 inv_freq: &inv_freq,
5566 rd: self.rotary_dim,
5567 nh,
5568 nkv,
5569 hd,
5570 hs,
5571 inter: d.gate_proj.rows(),
5572 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
5573 eps: self.rms_eps as f32,
5574 });
5575 }
5576 if layers.is_empty() {
5577 return li0;
5578 }
5579 let row = nkv * hd;
5580 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
5581 .iter()
5582 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
5583 .collect();
5584 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
5585 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
5586 let li = layers[i].layer;
5587 let layer = &self.kv_cache.layers[li];
5588 io.push(crate::gpu_metal::ChunkIo {
5589 cpu_stored: stored_at[i],
5590 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
5591 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
5592 out_k: ok,
5593 out_v: ov,
5594 imp: oi,
5595 });
5596 }
5597 let n_run = layers.len();
5598 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
5599 let ep = embed_ids.and_then(|ids| {
5602 self.weights
5603 .embed_tokens
5604 .q8_row_parts()
5605 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
5606 idx,
5607 rows,
5608 row_scale: rs,
5609 ids,
5610 mult: self.embed_multiplier,
5611 })
5612 });
5613 if embed_ids.is_some() && ep.is_none() {
5614 return li0;
5615 }
5616 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
5617 return li0;
5618 }
5619 drop(io);
5620 drop(layers);
5621 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
5624 let li = li0 + i;
5625 let layer = &mut self.kv_cache.layers[li];
5626 for bi in 0..b {
5627 layer.append(
5628 &ok[bi * row..(bi + 1) * row],
5629 &ov[bi * row..(bi + 1) * row],
5630 &[],
5631 );
5632 }
5633 layer.accumulate_imp(oi);
5634 }
5635 last
5636 }
5637
5638 fn layer_is_local(&self, li: usize) -> bool {
5641 if let Some(layers) = &self.sliding_layers {
5642 return layers.get(li).copied().unwrap_or(false);
5643 }
5644 match self.swa {
5645 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
5646 None => false,
5647 }
5648 }
5649
5650 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
5653 if self.layer_is_local(li) {
5654 if let Some(f) = &self.inv_freq_local {
5655 return f.clone();
5656 }
5657 } else if let Some(f) = &self.inv_freq_global {
5658 return f.clone();
5659 }
5660 self.inv_freq.clone()
5661 }
5662
5663 fn layer_window(&self, li: usize) -> Option<usize> {
5665 self.swa
5666 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
5667 }
5668
5669 fn layer_num_heads(&self, li: usize) -> usize {
5670 self.attention_heads_per_layer
5671 .as_ref()
5672 .and_then(|v| v.get(li).copied())
5673 .unwrap_or(self.num_heads)
5674 }
5675
5676 fn layer_rope_scale(&self, li: usize) -> f32 {
5677 if self.layer_is_local(li) {
5678 self.rope_scale_local
5679 } else {
5680 self.rope_scale
5681 }
5682 }
5683
5684 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
5687 if !self.layer_is_local(li) {
5688 if let Some((ghd, gkv)) = self.global_attn {
5689 return (gkv, ghd, ghd);
5690 }
5691 }
5692 (
5693 self.num_kv_heads,
5694 self.head_dim,
5695 if self.layer_is_local(li) {
5696 self.rotary_dim_local.unwrap_or(self.rotary_dim)
5697 } else {
5698 self.rotary_dim
5699 },
5700 )
5701 }
5702
5703 fn forward_layers(
5705 &mut self,
5706 hidden: &[f32],
5707 position: usize,
5708 task_mask: Option<&TaskMask>,
5709 ) -> Vec<f32> {
5710 self.forward_layers_upto(hidden, position, task_mask, None)
5711 }
5712
5713 pub fn embed_id(&self, id: u32) -> Vec<f32> {
5721 self.embed_single(id)
5722 }
5723
5724 pub fn split_supported(&self) -> Result<(), String> {
5728 if self.dsv4.is_some() {
5729 return Err(
5730 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
5731 );
5732 }
5733 if self.g3n.is_some() {
5734 return Err(
5735 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
5736 );
5737 }
5738 Ok(())
5739 }
5740
5741 pub fn forward_span(
5746 &mut self,
5747 hidden: &[f32],
5748 position: usize,
5749 from: usize,
5750 upto: usize,
5751 task_mask: Option<&TaskMask>,
5752 ) -> Result<Vec<f32>, String> {
5753 self.split_supported()?;
5754 if from > upto || upto >= self.num_layers {
5755 return Err(format!(
5756 "forward_span: layer range {from}..={upto} outside 0..{}",
5757 self.num_layers
5758 ));
5759 }
5760 if hidden.len() != self.hidden_size {
5761 return Err(format!(
5762 "forward_span: hidden len {} ≠ hidden_size {}",
5763 hidden.len(),
5764 self.hidden_size
5765 ));
5766 }
5767 Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
5768 }
5769
5770 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
5773 let normed = inference::rms_norm(
5774 hidden,
5775 &self.weights.final_norm,
5776 self.rms_eps,
5777 self.norm_style,
5778 );
5779 self.lm_head_forward(&normed)
5780 }
5781
5782 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
5784 sampler::sample_with_scratch(
5785 logits,
5786 &self.sampler_config,
5787 past_tokens,
5788 &mut self.rng,
5789 &mut self.sampler_scratch,
5790 )
5791 }
5792
5793 pub fn reset_session(&mut self) {
5795 self.kv_cache.clear();
5796 self.kv_history.clear();
5797 crate::gpu::graph_kv_reset(self.graph_kv_id);
5798 }
5799
5800 pub fn prefill_span_ids(
5806 &mut self,
5807 ids: &[u32],
5808 start_pos: usize,
5809 upto: usize,
5810 task_mask: Option<&TaskMask>,
5811 ) -> Result<Vec<f32>, String> {
5812 self.split_supported()?;
5813 if upto >= self.num_layers {
5814 return Err(format!(
5815 "prefill_span_ids: upto {upto} outside 0..{}",
5816 self.num_layers
5817 ));
5818 }
5819 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5823 Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
5824 } else {
5825 let hs = self.hidden_size;
5826 let mut out = Vec::with_capacity(ids.len() * hs);
5827 for (i, &id) in ids.iter().enumerate() {
5828 let emb = self.embed_id(id);
5829 out.extend_from_slice(&self.forward_span(
5830 &emb,
5831 start_pos + i,
5832 0,
5833 upto,
5834 task_mask,
5835 )?);
5836 }
5837 Ok(out)
5838 }
5839 }
5840
5841 pub fn prefill_span_hidden(
5844 &mut self,
5845 hidden: &[f32],
5846 start_pos: usize,
5847 from: usize,
5848 upto: usize,
5849 task_mask: Option<&TaskMask>,
5850 ) -> Result<Vec<f32>, String> {
5851 self.split_supported()?;
5852 let hs = self.hidden_size;
5853 if hidden.is_empty() || hidden.len() % hs != 0 {
5854 return Err(format!(
5855 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
5856 hidden.len()
5857 ));
5858 }
5859 if from > upto || upto >= self.num_layers {
5860 return Err(format!(
5861 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
5862 self.num_layers
5863 ));
5864 }
5865 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5866 Ok(self.prefill_batch_span(
5867 PrefillIn::Hidden(hidden),
5868 start_pos,
5869 task_mask,
5870 from,
5871 upto + 1,
5872 ))
5873 } else {
5874 let b = hidden.len() / hs;
5875 let mut out = Vec::with_capacity(hidden.len());
5876 for i in 0..b {
5877 let h = self.forward_span(
5878 &hidden[i * hs..(i + 1) * hs],
5879 start_pos + i,
5880 from,
5881 upto,
5882 task_mask,
5883 )?;
5884 out.extend_from_slice(&h);
5885 }
5886 Ok(out)
5887 }
5888 }
5889
5890 fn try_token_graph_wgpu(
5894 &self,
5895 hidden: &[f32],
5896 position: usize,
5897 logits_out: &mut Vec<f32>,
5898 layers_run: &mut usize,
5899 ) -> Option<Vec<f32>> {
5900 self.try_token_graph_wgpu_steps(
5901 hidden,
5902 position,
5903 logits_out,
5904 1,
5905 None,
5906 Some(layers_run),
5907 0,
5908 self.num_layers,
5909 )
5910 }
5911
5912 fn try_token_graph_wgpu_span(
5916 &self,
5917 hidden: &[f32],
5918 position: usize,
5919 logits_out: &mut Vec<f32>,
5920 from: usize,
5921 upto_excl: usize,
5922 layers_run: &mut usize,
5923 ) -> Option<Vec<f32>> {
5924 self.try_token_graph_wgpu_steps(
5925 hidden,
5926 position,
5927 logits_out,
5928 1,
5929 None,
5930 Some(layers_run),
5931 from,
5932 upto_excl,
5933 )
5934 }
5935
5936 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
5940 if self.o1_active() || self.attn_softcap > 0.0 {
5941 return None;
5942 }
5943 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5944 if !graph_on || crate::gpu::graph_unsupported() {
5945 return None;
5952 }
5953 let emb = self.embed_single(t_next);
5954 let mut lg = Vec::new();
5955 let mut ids = Vec::new();
5956 self.try_token_graph_wgpu_steps(
5957 &emb,
5958 position,
5959 &mut lg,
5960 k,
5961 Some(&mut ids),
5962 None,
5963 0,
5964 self.num_layers,
5965 )?;
5966 (ids.len() == k).then_some(ids)
5967 }
5968
5969 fn try_token_graph_wgpu_steps(
5973 &self,
5974 hidden: &[f32],
5975 position: usize,
5976 logits_out: &mut Vec<f32>,
5977 steps: usize,
5978 ids_out: Option<&mut Vec<u32>>,
5979 layers_run: Option<&mut usize>,
5980 from: usize,
5981 upto_excl: usize,
5982 ) -> Option<Vec<f32>> {
5983 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
5986 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
5987 return None;
5991 }
5992 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
5997 .map(|li| {
5998 if !o1_gpu {
5999 return None;
6000 }
6001 self.kv_cache.layers[self.phys_layer(li)].o1_views()
6002 })
6003 .collect();
6004 if self.o1_active() && o1_gpu {
6005 let want: usize = (from..upto_excl)
6008 .filter(|li| !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None))
6009 .count();
6010 let have = o1_views.iter().filter(|v| v.is_some()).count();
6011 if want == 0 || have != want {
6012 use std::sync::atomic::{AtomicUsize, Ordering};
6022 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
6023 let code = have * 1000 + want;
6024 if LAST.swap(code, Ordering::Relaxed) != code {
6025 tracing::warn!(
6026 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
6027 );
6028 }
6029 return None;
6030 }
6031 }
6032 let nh = self.num_heads;
6033 let (nkv, hd, rd) = self.layer_geom(0);
6034 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6035 let mut layers = Vec::with_capacity(upto_excl - from);
6036 let mut model = None;
6037 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
6038 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6039 if let Some((_, i, kind, rs)) = t.graph_weight() {
6040 return Some(crate::gpu::GraphW {
6041 idx: i,
6042 kind,
6043 row_scale: rs,
6044 data: &[],
6045 });
6046 }
6047 t.as_f32().map(|d| crate::gpu::GraphW {
6049 idx: 0,
6050 kind: 4,
6051 row_scale: &[],
6052 data: d,
6053 })
6054 }
6055 for li in from..upto_excl {
6056 let lw = &self.weights.layers[self.phys_layer(li)];
6057 if dbg {
6058 let ak = match &lw.attn {
6059 AttnKind::Mla(_) => "Mla".into(),
6060 AttnKind::Full {
6061 output_gate, bias, ..
6062 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
6063 AttnKind::LinearGdn(_) => "LinearGdn".into(),
6064 AttnKind::Kda(_) => "Kda".into(),
6065 AttnKind::Linear(_) => "Linear".into(),
6066 AttnKind::ShortConv(_) => "ShortConv".into(),
6067 };
6068 let fk = match &lw.ffn {
6069 FfnKind::Dense(_) => "Dense",
6070 FfnKind::Moe(_) => "Moe",
6071 FfnKind::DenseMoe(_) => "DenseMoe",
6072 };
6073 eprintln!("graph L{li}: attn={ak} ffn={fk}");
6074 }
6075 let gffn = match &lw.ffn {
6076 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
6078 gate: gw(&d.gate_proj)?,
6079 up: gw(&d.up_proj)?,
6080 down: gw(&d.down_proj)?,
6081 },
6082 FfnKind::Moe(m) => {
6083 if m.router_sigmoid
6088 || m.expert_bias.is_some()
6089 || m.route_tau.is_some()
6090 || m.mask.is_some()
6091 {
6092 return None;
6093 }
6094 let (se, sg) = m.shared.as_ref()?;
6095 let sgate = gw(sg.as_ref()?)?;
6096 let router = gw(&m.router)?;
6097 let inter = m.experts.first()?.gate_proj.rows();
6098 let mut experts = Vec::with_capacity(m.experts.len() + 1);
6099 let mut q4tp: Option<bool> = None;
6102 let mut gu_q2: Option<bool> = None;
6105 for e in m.experts.iter().chain(std::iter::once(se)) {
6106 if !matches!(e.act, Act::Silu)
6107 || e.gate_proj.rows() != inter
6108 || e.up_proj.rows() != inter
6109 {
6110 return None;
6111 }
6112 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
6113 Some((mm, gi)) => (
6114 mm,
6115 gi,
6116 e.up_proj.mapped_q4t()?.1,
6117 e.down_proj.mapped_q4t()?.1,
6118 false,
6119 false,
6120 ),
6121 None => match e.gate_proj.mapped_q2tp() {
6122 Some((mm, gi)) => (
6123 mm,
6124 gi,
6125 e.up_proj.mapped_q2tp()?.1,
6126 e.down_proj.mapped_q4tp()?.1,
6127 true,
6128 true,
6129 ),
6130 None => {
6131 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
6132 (
6133 mm,
6134 gi,
6135 e.up_proj.mapped_q4tp()?.1,
6136 e.down_proj.mapped_q4tp()?.1,
6137 true,
6138 false,
6139 )
6140 }
6141 },
6142 };
6143 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
6144 {
6145 tracing::warn!(
6151 "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."
6152 );
6153 return None;
6154 }
6155 model.get_or_insert_with(|| mm.clone());
6156 experts.push((gi, ui, di));
6157 }
6158 crate::gpu::GraphFfn::Moe {
6159 router,
6160 shared_gate: sgate,
6161 experts,
6162 n_exp: m.experts.len(),
6163 top_k: std::env::var("CMF_TOPK_PROBE")
6169 .ok()
6170 .and_then(|v| v.parse::<usize>().ok())
6171 .filter(|k| *k > 0 && *k <= m.top_k)
6172 .unwrap_or(m.top_k),
6173 inter,
6174 norm_topk: m.norm_topk_prob,
6175 q4tp: q4tp?,
6176 gu_q2: gu_q2.unwrap_or(false),
6177 }
6178 }
6179 };
6180 let attn = match &lw.attn {
6181 AttnKind::Full {
6182 wq,
6183 wk,
6184 wv,
6185 wo,
6186 q_norm,
6187 k_norm,
6188 output_gate,
6189 softplus_gate,
6190 bias,
6191 } => {
6192 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
6193 return None;
6194 }
6195 let (m, _, _, _) = wq.graph_weight()?;
6196 model = Some(m.clone());
6197 crate::gpu::GraphAttn::Full {
6198 wq: gw(wq)?,
6199 wk: gw(wk)?,
6200 wv: gw(wv)?,
6201 wo: gw(wo)?,
6202 q_norm: q_norm.as_deref(),
6203 k_norm: k_norm.as_deref(),
6204 bias: bias
6205 .as_ref()
6206 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6207 output_gate: *output_gate,
6208 cpu_k: self.kv_cache.layers[li].k_heads(),
6209 cpu_v: self.kv_cache.layers[li].v_heads(),
6210 }
6211 }
6212 AttnKind::LinearGdn(w) => {
6213 let cfg = self.gdn_cfg?;
6214 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
6215 model = Some(m.clone());
6216 crate::gpu::GraphAttn::Gdn {
6217 qkv: gw(&w.in_proj_qkv)?,
6218 z: gw(&w.in_proj_z)?,
6219 a: gw(&w.in_proj_a)?,
6220 b: gw(&w.in_proj_b)?,
6221 out: gw(&w.out_proj)?,
6222 conv1d: &w.conv1d,
6223 a_log: &w.a_log,
6224 dt_bias: &w.dt_bias,
6225 norm: &w.norm,
6226 nv: cfg.num_v_heads,
6227 nk: cfg.num_k_heads,
6228 dk: cfg.key_head_dim,
6229 dv: cfg.value_head_dim,
6230 kk: cfg.conv_kernel,
6231 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6232 }
6233 }
6234 _ => return None,
6235 };
6236 layers.push(crate::gpu::GraphLayer {
6237 input_norm: &lw.input_norm,
6238 attn,
6239 post_norm: &lw.post_norm,
6240 ffn: gffn,
6241 });
6242 }
6243 let model = model?;
6244 let lm_gw = if upto_excl == self.num_layers
6250 && self.graph_want_logits
6251 && std::env::var("CMF_GPU_LMHEAD")
6252 .map(|v| v != "0")
6253 .unwrap_or(true)
6254 {
6255 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
6256 (
6257 crate::gpu::GraphW {
6258 idx: i,
6259 kind,
6260 row_scale: rs,
6261 data: &[],
6262 },
6263 self.weights.lm_head.rows(),
6264 )
6265 })
6266 } else {
6267 None
6268 };
6269 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
6270 let emb_gw = if steps > 1 {
6272 self.weights
6273 .embed_tokens
6274 .graph_weight()
6275 .map(|(_, i, kind, rs)| {
6276 (
6277 crate::gpu::GraphW {
6278 idx: i,
6279 kind,
6280 row_scale: rs,
6281 data: &[],
6282 },
6283 self.weights.embed_tokens.rows(),
6284 self.embed_multiplier as f32,
6285 )
6286 })
6287 } else {
6288 None
6289 };
6290
6291 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
6297 (from..upto_excl.min(self.num_layers - 1))
6298 .filter(|&li| (li + 1) % self.physical_layers == 0)
6299 .map(|li| li - from)
6300 .collect()
6301 } else {
6302 Vec::new()
6303 };
6304 let mut h = hidden.to_vec();
6305 crate::gpu::forward_token_graph(
6306 &model,
6307 self.graph_kv_id,
6308 &layers,
6309 &o1_views,
6310 self.o1_epoch,
6311 &self.inv_freq,
6312 &mut h,
6313 nh,
6314 nkv,
6315 hd,
6316 rd,
6317 self.hidden_size,
6318 self.intermediate_size,
6319 position,
6320 self.kv_cache.max_seq_len,
6321 gemma,
6322 self.rms_eps as f32,
6323 lm,
6324 &self.weights.final_norm,
6325 logits_out,
6326 &loop_norm_at,
6327 steps,
6328 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
6329 ids_out,
6330 layers_run,
6331 from,
6332 false,
6333 )
6334 .then_some(h)
6335 }
6336
6337 #[cfg(target_os = "macos")]
6346 #[allow(clippy::type_complexity)]
6347 fn metal_rows_plan(&self) -> Option<(Vec<MetalRowsItem<'_>>, std::sync::Arc<cortiq_core::CmfModel>, Option<crate::gpu_metal::GdnGpuCfg>)> {
6348 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
6349 if !crate::gpu::q1_force()
6350 || !crate::gpu::enabled_here()
6351 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
6352 || self.attn_softcap > 0.0
6353 || self.o1_active()
6354 || self.swa.is_some()
6355 || self.global_attn.is_some()
6356 || self.attention_heads_per_layer.is_some()
6357 || self.attn_v_norm
6358 || self.loop_final_norm
6359 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
6360 {
6361 return None;
6362 }
6363 let attend_contract = self.head_dim % 4 == 0
6364 && self.head_dim <= 256
6365 && self.rotary_dim >= 2
6366 && self.rotary_dim <= self.head_dim
6367 && (self.rotary_dim / 2) % 32 == 0
6368 && self.num_kv_heads > 0
6369 && self.num_heads % self.num_kv_heads == 0;
6370 if !attend_contract {
6371 return None;
6372 }
6373 let mut plan: Vec<MetalRowsItem> = Vec::new();
6374 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
6375 for li in 0..self.num_layers {
6376 let lw = &self.weights.layers[self.phys_layer(li)];
6377 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
6378 return None;
6379 }
6380 let ffn = match &lw.ffn {
6381 FfnKind::Dense(d) if d.act == Act::Silu => {
6382 let (Some(g), Some(u), Some(dn)) =
6383 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
6384 else {
6385 return None;
6386 };
6387 MetalFfn::Dense { gate: g, up: u, down: dn }
6388 }
6389 _ => return None,
6390 };
6391 match &lw.attn {
6392 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
6393 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
6394 w.in_proj_qkv.q1_parts(),
6395 w.in_proj_z.q1_parts(),
6396 w.in_proj_a.f32_parts(),
6397 w.in_proj_b.f32_parts(),
6398 w.out_proj.q1_parts(),
6399 ) else {
6400 return None;
6401 };
6402 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
6403 model_ref.get_or_insert_with(|| model.clone());
6404 }
6405 let gl = GdnGpuLayer {
6406 attn_norm: &lw.input_norm,
6407 post_norm: &lw.post_norm,
6408 qkv,
6409 z,
6410 a,
6411 b: bb,
6412 out,
6413 ffn,
6414 conv1d: &w.conv1d,
6415 a_log: &w.a_log,
6416 dt_bias: &w.dt_bias,
6417 gnorm: &w.norm,
6418 };
6419 match plan.last_mut() {
6420 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
6421 _ => plan.push(MetalRowsItem::Gdn { run: vec![gl], first: li }),
6422 }
6423 }
6424 AttnKind::Full {
6425 wq,
6426 wk,
6427 wv,
6428 wo,
6429 q_norm,
6430 k_norm,
6431 output_gate,
6432 softplus_gate: None,
6433 bias: None,
6434 } => {
6435 let (Some(pq), Some(pk), Some(pv), Some(po)) =
6436 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
6437 else {
6438 return None;
6439 };
6440 if let QTensor::Mapped { model, .. } = wq {
6441 model_ref.get_or_insert_with(|| model.clone());
6442 }
6443 let cache = &self.kv_cache.layers[li];
6444 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
6445 return None;
6446 }
6447 plan.push(MetalRowsItem::Attn {
6448 l: AttnGpuLayer {
6449 attn_norm: &lw.input_norm,
6450 post_norm: &lw.post_norm,
6451 wq: pq,
6452 wk: pk,
6453 wv: pv,
6454 wo: po,
6455 ffn,
6456 },
6457 li,
6458 q_norm: q_norm.as_deref(),
6459 k_norm: k_norm.as_deref(),
6460 output_gate: *output_gate,
6461 });
6462 }
6463 _ => return None,
6464 }
6465 }
6466 let model = model_ref?;
6467 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
6468 nv: cfg.num_v_heads,
6469 nk: cfg.num_k_heads,
6470 dk: cfg.key_head_dim,
6471 dv: cfg.value_head_dim,
6472 kk: cfg.conv_kernel,
6473 hidden: self.hidden_size,
6474 inter: self.intermediate_size,
6475 c_dim: cfg.conv_dim(),
6476 eps: cfg.rms_eps as f32,
6477 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6478 });
6479 Some((plan, model, gcfg))
6480 }
6481
6482 #[cfg(target_os = "macos")]
6484 #[allow(clippy::too_many_arguments)]
6485 fn metal_attn_params<'a>(
6486 li: usize,
6487 cache: &'a crate::kv_cache::LayerKvCache,
6488 q_norm: Option<&'a [f32]>,
6489 k_norm: Option<&'a [f32]>,
6490 output_gate: bool,
6491 inv_freq: &'a [f32],
6492 geom: (usize, usize, usize, usize),
6493 pos0: usize,
6494 kv_id: u64,
6495 eps: f32,
6496 gemma: bool,
6497 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
6498 let (nh, nkv, hd, rd) = geom;
6499 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6500 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6501 let cpu_stored = cpu_k[0].len() / hd;
6502 (
6503 crate::gpu_metal::AttnDeviceParams {
6504 kv_id,
6505 layer: li,
6506 nh,
6507 nkv,
6508 hd,
6509 rd,
6510 position: pos0,
6511 eps,
6512 gemma,
6513 output_gate,
6514 q_norm,
6515 k_norm,
6516 inv_freq,
6517 cpu_k,
6518 cpu_v,
6519 cpu_stored,
6520 o1: None,
6521 },
6522 cpu_stored,
6523 )
6524 }
6525
6526 #[cfg(target_os = "macos")]
6531 #[allow(clippy::type_complexity)]
6532 fn metal_rows_run(
6533 &mut self,
6534 hiddens: &mut [f32],
6535 pos0: usize,
6536 b: usize,
6537 prefill: bool,
6538 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6539 ) -> Option<MetalVerifyPending> {
6540 use crate::gpu_metal::{GraphDims, VerifyGraph};
6541 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
6542 for l in &mut self.kv_cache.layers {
6543 if l.linear_state.len() != want && want > 0 {
6544 l.linear_state = vec![0f32; want];
6545 }
6546 }
6547 let (plan, model, gcfg) = self.metal_rows_plan()?;
6548 let dims = GraphDims {
6549 hidden: self.hidden_size,
6550 eps: self.rms_eps as f32,
6551 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6552 };
6553 let mut graph = if prefill {
6554 VerifyGraph::new_prefill(&model, dims, hiddens, b)?
6555 } else {
6556 VerifyGraph::new(&model, dims, hiddens, b)?
6557 };
6558 let geom = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6559 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6560 let eps = self.rms_eps as f32;
6561 let kv_id = self.graph_kv_id;
6562 let inv_freq = self.inv_freq.clone();
6563 for item in &plan {
6564 let ok = match item {
6565 MetalRowsItem::Gdn { run, .. } => gcfg
6566 .as_ref()
6567 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
6568 .unwrap_or(false),
6569 MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6570 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);
6571 graph.attn_ok(l, &p)
6572 }
6573 };
6574 if !ok {
6575 use std::sync::atomic::{AtomicBool, Ordering};
6576 static SAID: AtomicBool = AtomicBool::new(false);
6577 if !SAID.swap(true, Ordering::Relaxed) {
6578 tracing::warn!("metal rows graph: a layer failed preflight — declining");
6579 }
6580 return None;
6581 }
6582 }
6583 let lm = match &spec {
6584 Some((lm, _, _)) => {
6585 if !graph.lm_head_ok(*lm) {
6586 return None;
6587 }
6588 Some(*lm)
6589 }
6590 None => None,
6591 };
6592 let mut gdn_layers = Vec::new();
6593 let mut attn_layers = Vec::new();
6594 for item in &plan {
6595 match item {
6596 MetalRowsItem::Gdn { run, first } => {
6597 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
6598 .iter()
6599 .map(|l| l.linear_state.as_slice())
6600 .collect();
6601 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
6602 return None;
6603 }
6604 gdn_layers.extend(*first..*first + run.len());
6605 }
6606 MetalRowsItem::Attn { l, li, q_norm, k_norm, output_gate } => {
6607 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);
6608 if !graph.encode_attn_b(l, &p) {
6609 return None;
6610 }
6611 attn_layers.push((*li, cpu_stored));
6612 }
6613 }
6614 }
6615 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
6616 if !graph.encode_lm_head_b(final_norm, lm) {
6617 return None;
6618 }
6619 }
6620 graph.sync();
6621 if let Some((lm, _, logits)) = spec {
6622 logits.resize(b * lm.1, 0.0);
6623 graph.read_logits(logits);
6624 }
6625 graph.read_hidden(hiddens);
6626 Some(MetalVerifyPending { graph, gdn_layers, attn_layers })
6627 }
6628
6629 #[cfg(target_os = "macos")]
6635 fn try_batch_graph_metal(
6636 &mut self,
6637 hiddens: &mut [f32],
6638 positions: &[usize],
6639 b: usize,
6640 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
6641 ) -> bool {
6642 let _t0 = std::time::Instant::now();
6643 if positions.len() != b
6644 || positions.windows(2).any(|w| w[1] != w[0] + 1)
6645 || hiddens.len() != b * self.hidden_size
6646 {
6647 return false;
6648 }
6649 let Some(pending) = self.metal_rows_run(hiddens, positions[0], b, false, spec) else {
6650 return false;
6651 };
6652 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6653 eprintln!("metal-verify: {:.1} ms | b={b}", _t0.elapsed().as_secs_f64() * 1e3);
6654 }
6655 self.metal_verify = Some(pending);
6656 true
6657 }
6658
6659 #[cfg(target_os = "macos")]
6664 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> Option<Vec<f32>> {
6665 let b = ids.len();
6666 if b == 0 || b > 512 {
6667 return None;
6668 }
6669 let hs = self.hidden_size;
6670 let mut hiddens = vec![0f32; b * hs];
6671 for (j, &id) in ids.iter().enumerate() {
6672 let e = self.embed_single(id);
6673 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
6674 }
6675 let mut pending = self.metal_rows_run(&mut hiddens, start_pos, b, true, None)?;
6676 let idxs = pending.gdn_layers.clone();
6678 let mut outs: Vec<&mut [f32]> = self
6679 .kv_cache
6680 .layers
6681 .iter_mut()
6682 .enumerate()
6683 .filter(|(i, _)| idxs.binary_search(i).is_ok())
6684 .map(|(_, l)| l.linear_state.as_mut_slice())
6685 .collect();
6686 pending.graph.finish_states(&mut outs);
6687 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6688 let mut kbuf = vec![0f32; b * nkv * hd];
6689 let mut vbuf = vec![0f32; b * nkv * hd];
6690 for (li, cpu_stored) in &pending.attn_layers {
6691 if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, b, &mut kbuf, &mut vbuf) {
6692 let cache = &mut self.kv_cache.layers[*li];
6693 for r in 0..b {
6694 cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6695 }
6696 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + b);
6697 }
6698 }
6699 Some(hiddens)
6700 }
6701
6702 #[cfg(target_os = "macos")]
6706 fn metal_verify_commit(&mut self, a: usize) -> bool {
6707 let Some(mut pending) = self.metal_verify.take() else {
6708 return false;
6709 };
6710 let n = a + 1;
6711 let idxs = pending.gdn_layers.clone();
6713 let mut outs: Vec<&mut [f32]> = self
6714 .kv_cache
6715 .layers
6716 .iter_mut()
6717 .enumerate()
6718 .filter(|(i, _)| idxs.binary_search(i).is_ok())
6719 .map(|(_, l)| l.linear_state.as_mut_slice())
6720 .collect();
6721 if !pending.graph.commit(n, &mut outs) {
6722 return false;
6723 }
6724 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
6725 let mut kbuf = vec![0f32; n * nkv * hd];
6726 let mut vbuf = vec![0f32; n * nkv * hd];
6727 for (li, cpu_stored) in &pending.attn_layers {
6728 if crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, *li, nkv, hd, *cpu_stored, n, &mut kbuf, &mut vbuf) {
6729 let cache = &mut self.kv_cache.layers[*li];
6730 for r in 0..n {
6731 cache.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6732 }
6733 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, *li, cpu_stored + n);
6734 }
6735 }
6736 true
6737 }
6738
6739 #[cfg(target_os = "macos")]
6745 fn mtp_warm_batch_metal(&mut self, m: &mut MtpModule, pairs: &[(&[f32], u32)], first_pos: usize) -> bool {
6746 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
6747 let b = pairs.len();
6748 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
6749 return false;
6750 }
6751 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate: None, bias: None } = &m.layer.attn else {
6752 return false;
6753 };
6754 let FfnKind::Dense(d) = &m.layer.ffn else { return false };
6755 let (Some(pq), Some(pk), Some(pv), Some(po)) = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts()) else {
6756 return false;
6757 };
6758 let (Some(g), Some(u), Some(dn)) = (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts()) else {
6759 return false;
6760 };
6761 let Some(eh) = m.eh_proj.q1_parts() else { return false };
6762 let QTensor::Mapped { model, .. } = wq else { return false };
6763 let model = model.clone();
6764 let hs = self.hidden_size;
6765 let mut cat = vec![0f32; b * 2 * hs];
6767 for (j, (h, tok)) in pairs.iter().enumerate() {
6768 let e = self.embed_single(*tok);
6769 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
6770 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
6771 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
6772 }
6773 let dims = GraphDims { hidden: hs, eps: self.rms_eps as f32, gemma: self.norm_style == cortiq_core::NormStyle::Gemma };
6774 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
6775 return false;
6776 };
6777 let l = AttnGpuLayer {
6778 attn_norm: &m.layer.input_norm,
6779 post_norm: &m.layer.post_norm,
6780 wq: pq,
6781 wk: pk,
6782 wv: pv,
6783 wo: po,
6784 ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
6785 };
6786 let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6787 let inv_freq = self.inv_freq.clone();
6788 let cpu_stored;
6789 {
6790 let cache = &m.kv;
6791 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6792 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6793 cpu_stored = cpu_k[0].len() / hd;
6794 if cpu_stored != first_pos {
6795 return false;
6796 }
6797 let p = AttnDeviceParams {
6798 kv_id: self.mtp_kv_id(),
6799 layer: Self::MTP_LAYER_BASE,
6800 nh,
6801 nkv,
6802 hd,
6803 rd,
6804 position: first_pos,
6805 eps: self.rms_eps as f32,
6806 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6807 output_gate: *output_gate,
6808 q_norm: q_norm.as_deref(),
6809 k_norm: k_norm.as_deref(),
6810 inv_freq: &inv_freq,
6811 cpu_k,
6812 cpu_v,
6813 cpu_stored,
6814 o1: None,
6815 };
6816 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
6817 return false;
6818 }
6819 }
6820 graph.sync();
6821 let mut kbuf = vec![0f32; b * nkv * hd];
6822 let mut vbuf = vec![0f32; b * nkv * hd];
6823 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) {
6824 return false;
6825 }
6826 for r in 0..b {
6827 m.kv.append(&kbuf[r * nkv * hd..(r + 1) * nkv * hd], &vbuf[r * nkv * hd..(r + 1) * nkv * hd], &[]);
6828 }
6829 crate::gpu_metal::kv_mirror_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, cpu_stored + b);
6830 true
6831 }
6832
6833 fn draft_vocab_rows(head_rows: usize) -> usize {
6836 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6837 let n = *N.get_or_init(|| {
6838 std::env::var("CMF_DRAFT_VOCAB")
6839 .ok()
6840 .and_then(|v| v.parse().ok())
6841 .unwrap_or(65536)
6842 });
6843 if n == 0 { head_rows } else { n.min(head_rows) }
6844 }
6845
6846 #[cfg(target_os = "macos")]
6851 fn mtp_step_metal(
6852 &mut self,
6853 m: &mut MtpModule,
6854 hidden: &[f32],
6855 next_token: u32,
6856 position: usize,
6857 want_logits: bool,
6858 ) -> Option<(Vec<f32>, Vec<f32>)> {
6859 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
6860 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
6861 || !crate::gpu::q1_force()
6862 || !crate::gpu::enabled_here()
6863 || self.attn_softcap > 0.0
6864 || self.attention_heads_per_layer.is_some()
6865 || m.kv.mode != crate::kv_cache::KvMode::F32
6866 || m.kv.o1.is_some()
6867 {
6868 return None;
6869 }
6870 let AttnKind::Full {
6871 wq,
6872 wk,
6873 wv,
6874 wo,
6875 q_norm,
6876 k_norm,
6877 output_gate,
6878 softplus_gate: None,
6879 bias: None,
6880 } = &m.layer.attn
6881 else {
6882 return None;
6883 };
6884 let FfnKind::Dense(d) = &m.layer.ffn else { return None };
6885 if d.act != Act::Silu {
6886 return None;
6887 }
6888 let (pq, pk, pv, po) = (wq.q1_parts()?, wk.q1_parts()?, wv.q1_parts()?, wo.q1_parts()?);
6889 let (g, u, dn) = (d.gate_proj.q1_parts()?, d.up_proj.q1_parts()?, d.down_proj.q1_parts()?);
6890 let QTensor::Mapped { model, .. } = wq else { return None };
6891 let model = model.clone();
6892 let lm = if want_logits { Some(self.weights.lm_head.q1_parts()?) } else { None };
6893 let dims = GraphDims {
6894 hidden: self.hidden_size,
6895 eps: self.rms_eps as f32,
6896 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6897 };
6898 let hs = self.hidden_size;
6901 let mut x = vec![0f32; hs];
6902 let mut graph = TokenGraph::new(&model, dims, &x)?;
6903 let mut folded = false;
6904 if let Some(eh) = m.eh_proj.q1_parts() {
6905 let e = self.embed_single(next_token);
6906 let mut cat = vec![0.0f32; 2 * hs];
6907 let (cat_e, cat_h) = cat.split_at_mut(hs);
6908 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6909 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6910 folded = graph.encode_input_proj(eh, &cat);
6911 }
6912 if !folded {
6913 x = self.mtp_block_input(m, hidden, next_token);
6914 graph = TokenGraph::new(&model, dims, &x)?;
6915 }
6916 let l = AttnGpuLayer {
6917 attn_norm: &m.layer.input_norm,
6918 post_norm: &m.layer.post_norm,
6919 wq: pq,
6920 wk: pk,
6921 wv: pv,
6922 wo: po,
6923 ffn: MetalFfn::Dense { gate: g, up: u, down: dn },
6924 };
6925 let (nh, nkv, hd, rd) = (self.num_heads, self.num_kv_heads, self.head_dim, self.rotary_dim);
6926 let inv_freq = self.inv_freq.clone();
6927 {
6928 let cache = &m.kv;
6929 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
6930 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
6931 let cpu_stored = cpu_k[0].len() / hd;
6932 let p = AttnDeviceParams {
6933 kv_id: self.mtp_kv_id(),
6934 layer: Self::MTP_LAYER_BASE,
6935 nh,
6936 nkv,
6937 hd,
6938 rd,
6939 position,
6940 eps: self.rms_eps as f32,
6941 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
6942 output_gate: *output_gate,
6943 q_norm: q_norm.as_deref(),
6944 k_norm: k_norm.as_deref(),
6945 inv_freq: &inv_freq,
6946 cpu_k,
6947 cpu_v,
6948 cpu_stored,
6949 o1: None,
6950 };
6951 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
6952 return None;
6953 }
6954 }
6955 let draft_rows = if let Some(lm) = lm { Self::draft_vocab_rows(lm.1) } else { 0 };
6961 if let Some(lm) = lm {
6962 if !graph.lm_head_ok(lm) {
6963 return None;
6964 }
6965 if draft_rows < lm.1 {
6966 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
6967 return None;
6968 }
6969 } else {
6970 graph.encode_lm_head(&m.final_norm, lm);
6971 }
6972 }
6973 graph.sync();
6974 let mut logits = Vec::new();
6975 if let Some(lm) = lm {
6976 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
6977 logits = attention::take_buf(n_read);
6978 graph.read_logits(&mut logits);
6979 logits.resize(self.vocab_size, f32::NEG_INFINITY);
6981 }
6982 graph.finish(&mut x);
6983 let mut krow = attention::take_buf(nkv * hd);
6984 let mut vrow = attention::take_buf(nkv * hd);
6985 if crate::gpu_metal::kv_mirror_read_last(self.mtp_kv_id(), Self::MTP_LAYER_BASE, nkv, hd, &mut krow, &mut vrow) {
6986 m.kv.append(&krow, &vrow, &[]);
6987 }
6988 attention::recycle_buf(&mut krow);
6989 attention::recycle_buf(&mut vrow);
6990 Some((logits, x))
6991 }
6992
6993 fn try_batch_graph_wgpu(
6994 &self,
6995 hiddens: &mut [f32],
6996 positions: &[usize],
6997 k: usize,
6998 spec: Option<crate::gpu::SpecTail<'_>>,
6999 ) -> bool {
7000 let _tb = std::time::Instant::now();
7001 if self.attn_softcap > 0.0 {
7002 return false; }
7004 if self.o1_active() {
7005 return false;
7006 }
7007 let nh = self.num_heads;
7008 let (nkv, hd, rd) = self.layer_geom(0);
7009 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7010 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7011 if let Some((_, i, kind, rs)) = t.graph_weight() {
7012 return Some(crate::gpu::GraphW {
7013 idx: i,
7014 kind,
7015 row_scale: rs,
7016 data: &[],
7017 });
7018 }
7019 t.as_f32().map(|d| crate::gpu::GraphW {
7020 idx: 0,
7021 kind: 4,
7022 row_scale: &[],
7023 data: d,
7024 })
7025 }
7026 let built: Option<(
7027 Vec<crate::gpu::GraphLayer<'_>>,
7028 std::sync::Arc<cortiq_core::CmfModel>,
7029 )> = (|| {
7030 let mut layers = Vec::with_capacity(self.num_layers);
7031 let mut model = None;
7032 for li in 0..self.num_layers {
7033 let lw = &self.weights.layers[self.phys_layer(li)];
7034 let gffn = match &lw.ffn {
7041 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
7042 gate: gw(&d.gate_proj)?,
7043 up: gw(&d.up_proj)?,
7044 down: gw(&d.down_proj)?,
7045 },
7046 FfnKind::Moe(m) => {
7047 if m.router_sigmoid
7048 || m.expert_bias.is_some()
7049 || m.route_tau.is_some()
7050 || m.mask.is_some()
7051 {
7052 return None;
7053 }
7054 let (se, sg) = m.shared.as_ref()?;
7055 let sgate = gw(sg.as_ref()?)?;
7056 let router = gw(&m.router)?;
7057 let inter = m.experts.first()?.gate_proj.rows();
7058 let mut experts = Vec::with_capacity(m.experts.len() + 1);
7059 let mut q4tp: Option<bool> = None;
7060 let mut gu_q2: Option<bool> = None;
7061 for e in m.experts.iter().chain(std::iter::once(se)) {
7062 if !matches!(e.act, Act::Silu)
7063 || e.gate_proj.rows() != inter
7064 || e.up_proj.rows() != inter
7065 {
7066 return None;
7067 }
7068 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
7072 Some((mm, gi)) => (
7073 mm,
7074 gi,
7075 e.up_proj.mapped_q4t()?.1,
7076 e.down_proj.mapped_q4t()?.1,
7077 false,
7078 false,
7079 ),
7080 None => match e.gate_proj.mapped_q2tp() {
7081 Some((mm, gi)) => (
7082 mm,
7083 gi,
7084 e.up_proj.mapped_q2tp()?.1,
7085 e.down_proj.mapped_q4tp()?.1,
7086 true,
7087 true,
7088 ),
7089 None => {
7090 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
7091 (
7092 mm,
7093 gi,
7094 e.up_proj.mapped_q4tp()?.1,
7095 e.down_proj.mapped_q4tp()?.1,
7096 true,
7097 false,
7098 )
7099 }
7100 },
7101 };
7102 if *q4tp.get_or_insert(is_p) != is_p
7103 || *gu_q2.get_or_insert(is_q2) != is_q2
7104 {
7105 return None;
7106 }
7107 model.get_or_insert_with(|| mm.clone());
7108 experts.push((gi, ui, di));
7109 }
7110 crate::gpu::GraphFfn::Moe {
7111 router,
7112 shared_gate: sgate,
7113 experts,
7114 n_exp: m.experts.len(),
7115 top_k: m.top_k,
7116 inter,
7117 norm_topk: m.norm_topk_prob,
7118 q4tp: q4tp?,
7119 gu_q2: gu_q2.unwrap_or(false),
7120 }
7121 }
7122 _ => return None,
7123 };
7124 let attn = match &lw.attn {
7125 AttnKind::Full {
7126 wq,
7127 wk,
7128 wv,
7129 wo,
7130 q_norm,
7131 k_norm,
7132 output_gate,
7133 softplus_gate,
7134 bias,
7135 } => {
7136 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
7137 return None;
7138 }
7139 let (m, _, _, _) = wq.graph_weight()?;
7140 model = Some(m.clone());
7141 crate::gpu::GraphAttn::Full {
7142 wq: gw(wq)?,
7143 wk: gw(wk)?,
7144 wv: gw(wv)?,
7145 wo: gw(wo)?,
7146 q_norm: q_norm.as_deref(),
7147 k_norm: k_norm.as_deref(),
7148 bias: bias
7149 .as_ref()
7150 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7151 output_gate: *output_gate,
7152 cpu_k: self.kv_cache.layers[li].k_heads(),
7153 cpu_v: self.kv_cache.layers[li].v_heads(),
7154 }
7155 }
7156 AttnKind::LinearGdn(w) => {
7157 let cfg = self.gdn_cfg?;
7158 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
7159 model = Some(m.clone());
7160 crate::gpu::GraphAttn::Gdn {
7161 qkv: gw(&w.in_proj_qkv)?,
7162 z: gw(&w.in_proj_z)?,
7163 a: gw(&w.in_proj_a)?,
7164 b: gw(&w.in_proj_b)?,
7165 out: gw(&w.out_proj)?,
7166 conv1d: &w.conv1d,
7167 a_log: &w.a_log,
7168 dt_bias: &w.dt_bias,
7169 norm: &w.norm,
7170 nv: cfg.num_v_heads,
7171 nk: cfg.num_k_heads,
7172 dk: cfg.key_head_dim,
7173 dv: cfg.value_head_dim,
7174 kk: cfg.conv_kernel,
7175 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
7176 }
7177 }
7178 _ => return None,
7179 };
7180 layers.push(crate::gpu::GraphLayer {
7181 input_norm: &lw.input_norm,
7182 attn,
7183 post_norm: &lw.post_norm,
7184 ffn: gffn,
7185 });
7186 }
7187 Some((layers, model?))
7188 })();
7189 let Some((layers, model)) = built else {
7190 {
7191 use std::sync::atomic::{AtomicBool, Ordering};
7192 static SAID: AtomicBool = AtomicBool::new(false);
7193 if !SAID.swap(true, Ordering::Relaxed) {
7194 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
7195 }
7196 }
7197 return false;
7198 };
7199 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7200 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
7201 }
7202 crate::gpu::forward_batch_graph(
7203 &model,
7204 self.graph_kv_id,
7205 &layers,
7206 &self.inv_freq,
7207 hiddens,
7208 nh,
7209 nkv,
7210 hd,
7211 rd,
7212 self.hidden_size,
7213 self.intermediate_size,
7214 positions,
7215 self.kv_cache.max_seq_len,
7216 gemma,
7217 self.rms_eps as f32,
7218 k,
7219 spec,
7220 )
7221 }
7222
7223 fn draft_probe() -> bool {
7227 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7228 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
7229 }
7230
7231 #[cfg(feature = "gpu")]
7243 fn dsv4_spec_on() -> bool {
7244 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7245 *ON.get_or_init(|| {
7246 std::env::var("CMF_DSV4_SPEC")
7247 .map(|v| v != "0")
7248 .unwrap_or(true)
7249 })
7250 }
7251
7252 #[cfg(feature = "gpu")]
7259 fn dsv4_spec_step(
7260 &mut self,
7261 tip_token: u32,
7262 t_next: u32,
7263 next_pos: usize,
7264 drafted: &mut usize,
7265 accepted_ctr: &mut usize,
7266 ) -> Option<(Vec<u32>, usize)> {
7267 let t_all = std::time::Instant::now();
7268 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7269 thread_local! {
7270 static LAST: std::cell::Cell<Option<std::time::Instant>> =
7271 const { std::cell::Cell::new(None) };
7272 }
7273 LAST.with(|l| {
7274 if let Some(prev) = l.get() {
7275 eprintln!(
7276 "между раундами {:.1} мс",
7277 prev.elapsed().as_secs_f64() * 1e3
7278 );
7279 }
7280 l.set(Some(std::time::Instant::now()));
7281 });
7282 }
7283 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7284 eprintln!("spec_step: вход pos={next_pos}");
7285 }
7286 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
7287 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
7288 if self.dspark.is_none() {
7290 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7291 if t.is_empty() {
7292 return None;
7293 }
7294 crate::dsv4::dspark_arm(&t, cfg.dim);
7295 self.dspark = Some(crate::dsv4::DsparkState::new(
7296 self.dsv4_mtp.len(),
7297 &cfg,
7298 t.len(),
7299 ));
7300 }
7301 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7302 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
7303 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
7304 eprintln!("spec_step: пак не построился (targets {targets:?})");
7305 }
7306 let pack = pack?;
7307 let block = crate::dsv4::dspark_block();
7308 let b_box = self.dsv4.as_mut()?;
7309 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
7310 let ds = self.dspark.as_mut()?;
7311 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
7314 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
7315 if dbg {
7316 eprintln!("spec_step: нет захвата");
7317 }
7318 return None;
7319 }
7320 ds.have_hidden = true;
7321 let tip_pos = next_pos.checked_sub(1)?;
7322 let draft_started = std::time::Instant::now();
7323 let mut conf = Vec::new();
7324 let props = crate::dsv4::dspark_draft_gpu(
7325 g,
7326 &self.dsv4_mtp,
7327 &cfg,
7328 ds,
7329 pack,
7330 st.kv_id,
7331 tip_token,
7332 tip_pos,
7333 self.pool.as_deref(),
7334 &mut conf,
7335 );
7336 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7337 *drafted += block;
7338 if props.is_empty() || props[0] != t_next {
7339 if dbg {
7340 eprintln!(
7341 "spec_step: черновик {} (props0={:?} t_next={t_next})",
7342 if props.is_empty() {
7343 "пуст"
7344 } else {
7345 "мимо"
7346 },
7347 props.first()
7348 );
7349 }
7350 return None;
7351 }
7352 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
7353 let conf_min = {
7359 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
7360 *M.get_or_init(|| {
7361 std::env::var("CMF_DSPARK_CONF_MIN")
7362 .ok()
7363 .and_then(|v| v.parse().ok())
7364 .unwrap_or(0.0)
7365 })
7366 };
7367 if conf_min > 0.0 && conf.len() >= props.len() {
7368 let mut keep = 1usize;
7369 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
7370 keep += 1;
7371 }
7372 k_verify = k_verify.min(keep.max(2));
7373 }
7374 if k_verify < 2 {
7375 return None;
7376 }
7377 let mut fed = Vec::with_capacity(k_verify);
7378 fed.push(t_next);
7379 fed.extend_from_slice(&props[1..k_verify]);
7380 let mut argmax = Vec::new();
7381 let mut logits_all = Vec::new();
7382 let mut walked = Vec::new();
7383 let txn = crate::dsv4::dsv4_verify_chunk(
7384 g,
7385 layers,
7386 &cfg,
7387 st,
7388 &fed,
7389 next_pos,
7390 &self.inv_freq,
7391 self.pool.as_deref(),
7392 &targets,
7393 &mut argmax,
7394 &mut logits_all,
7395 &mut walked,
7396 );
7397 if txn.is_none() && dbg {
7398 eprintln!("spec_step: verify отказал");
7399 }
7400 let txn = txn?;
7401 let b = fed.len();
7402 let mut accepted = 1usize;
7403 while accepted < b && fed[accepted] == argmax[accepted - 1] {
7404 accepted += 1;
7405 }
7406 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
7411 accepted = 1;
7412 }
7413 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
7414 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
7415 }
7416 let t_fin = std::time::Instant::now();
7417 if !crate::dsv4::dsv4_spec_finish(
7418 g,
7419 layers,
7420 &cfg,
7421 st,
7422 txn,
7423 accepted,
7424 &fed,
7425 &self.inv_freq,
7426 self.pool.as_deref(),
7427 ) {
7428 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
7429 return None;
7430 }
7431 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7432 eprintln!(
7433 "finish(k={accepted}): {:.1} мс",
7434 t_fin.elapsed().as_secs_f64() * 1e3
7435 );
7436 }
7437 *accepted_ctr += accepted - 1;
7438 let (hc, dim) = (cfg.hc_mult, cfg.dim);
7443 let dev_caps: Vec<usize> = targets
7450 .iter()
7451 .copied()
7452 .filter(|&t| {
7453 st.dev_set.get(t).copied().unwrap_or(false)
7454 && !st.partial_set.get(t).copied().unwrap_or(false)
7455 })
7456 .collect();
7457 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
7458 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
7459 return None;
7460 }
7461 for t in 0..accepted {
7462 let tip = t + 1 == accepted;
7463 for (slot, &tl) in targets.iter().enumerate() {
7464 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
7465 let lo = (di * b + t) * hc * dim;
7466 crate::dsv4::dspark_capture(
7467 &caps_all[lo..lo + hc * dim],
7468 &cfg,
7469 slot,
7470 &mut ds.main_hidden,
7471 );
7472 } else if tip
7473 && crate::dsv4::dspark_peek_slot(slot, dim, {
7474 let lo = slot * dim;
7475 &mut ds.main_hidden[lo..lo + dim]
7476 })
7477 {
7478 } else {
7483 crate::dsv4::dspark_capture(
7487 &walked[t * hc * dim..(t + 1) * hc * dim],
7488 &cfg,
7489 slot,
7490 &mut ds.main_hidden,
7491 );
7492 }
7493 }
7494 crate::dsv4::dspark_ring_append(
7495 g,
7496 &self.dsv4_mtp,
7497 &cfg,
7498 ds,
7499 next_pos + t,
7500 self.pool.as_deref(),
7501 );
7502 }
7503 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
7504 self.graph_logits = Some(row);
7505 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
7510 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
7511 crate::dsv4::pick_tally_arm();
7512 }
7513 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
7514 eprintln!(
7515 "spec_step total {:.1} мс (k={accepted})",
7516 t_all.elapsed().as_secs_f64() * 1e3
7517 );
7518 }
7519 Some((fed[1..accepted].to_vec(), next_pos + accepted))
7520 }
7521
7522 fn dspark_probe(&mut self, position: usize, token_id: u32) {
7523 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
7524 return;
7525 }
7526 let trunk_now = crate::dsv4::pick_tally_take();
7528 crate::dsv4::trunk_freq_note(&trunk_now);
7529 if !trunk_now.is_empty() {
7530 self.dspark_trunk_picks.push(trunk_now);
7531 let keep = crate::dsv4::dspark_block();
7532 if self.dspark_trunk_picks.len() > keep {
7533 self.dspark_trunk_picks.remove(0);
7534 }
7535 }
7536 for p in std::mem::take(&mut self.dspark_pending) {
7539 let Some(i) = position.checked_sub(p.0 + 1) else {
7540 continue;
7541 };
7542 let mut p = p;
7543 if i < p.1.len() {
7544 if p.2 && p.1[i] == token_id {
7545 p.3 = i + 1;
7546 } else {
7547 p.2 = false;
7548 }
7549 if i + 1 < p.1.len() {
7550 self.dspark_pending.push(p);
7551 continue;
7552 }
7553 }
7554 self.dspark_hist.push(p.3);
7555 self.dspark_real.push(token_id);
7556 }
7557 let Some(b) = &mut self.dsv4 else { return };
7558 let (g, layers, cfg) = (&b.0, &b.1, b.2);
7559 let n_layers = layers.len();
7560 if self.dspark.is_none() {
7561 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
7562 if t.is_empty() {
7563 return;
7564 }
7565 eprintln!(
7566 "DSpark: захват со слоёв {t:?}, блок {}",
7567 crate::dsv4::dspark_block()
7568 );
7569 crate::dsv4::dspark_arm(&t, cfg.dim);
7570 self.dspark = Some(crate::dsv4::DsparkState::new(
7571 self.dsv4_mtp.len(),
7572 &cfg,
7573 t.len(),
7574 ));
7575 }
7576 let ds = self.dspark.as_mut().unwrap();
7577 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
7578 return; }
7580 let mut conf = Vec::new();
7581 crate::dsv4::pick_tally_arm();
7582 let draft_started = std::time::Instant::now();
7587 #[cfg(feature = "gpu")]
7588 let gpu_draft = crate::dsv4::dspark_gpu_on();
7589 #[cfg(not(feature = "gpu"))]
7590 let gpu_draft = false;
7591 let props = if gpu_draft {
7592 #[cfg(feature = "gpu")]
7593 {
7594 let kv_id = b.3.kv_id;
7595 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
7596 Some(pk) => crate::dsv4::dspark_draft_gpu(
7597 g,
7598 &self.dsv4_mtp,
7599 &cfg,
7600 ds,
7601 pk,
7602 kv_id,
7603 token_id,
7604 position,
7605 self.pool.as_deref(),
7606 &mut conf,
7607 ),
7608 None => Vec::new(),
7609 }
7610 }
7611 #[cfg(not(feature = "gpu"))]
7612 Vec::new()
7613 } else {
7614 crate::gpu::cpu_scope(|| {
7615 crate::dsv4::dspark_draft(
7616 g,
7617 &self.dsv4_mtp,
7618 &cfg,
7619 ds,
7620 token_id,
7621 position,
7622 self.pool.as_deref(),
7623 &mut conf,
7624 )
7625 })
7626 };
7627 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
7628 let draft_picks = crate::dsv4::pick_tally_take();
7629 crate::dsv4::dspark_freq_note(&draft_picks);
7630 crate::dsv4::pick_tally_arm();
7633 if !props.is_empty() {
7634 let (tu, tt) = {
7638 let flat: Vec<(usize, Vec<usize>)> = self
7639 .dspark_trunk_picks
7640 .iter()
7641 .flat_map(|v| v.iter().cloned())
7642 .collect();
7643 let mut per: std::collections::HashMap<usize, Vec<usize>> =
7645 std::collections::HashMap::new();
7646 for (li, picks) in flat {
7647 per.entry(li).or_default().extend(picks);
7648 }
7649 let n = per.len().max(1);
7650 let mut u = 0usize;
7651 let mut t = 0usize;
7652 for (_, v) in per {
7653 t += v.len();
7654 u += v.iter().collect::<std::collections::HashSet<_>>().len();
7655 }
7656 (u / n, t / n)
7657 };
7658 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
7659 self.dspark_exp.push((tu, tt, du, dt));
7660 self.dspark_pending.push((position, props, true, 0));
7661 }
7662 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
7663 let n = self.dspark_hist.len() as f32;
7664 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
7665 let block = crate::dsv4::dspark_block();
7666 let mut at = vec![0usize; block + 1];
7667 for &k in &self.dspark_hist {
7668 at[k] += 1;
7669 }
7670 let mut surv = Vec::with_capacity(block);
7672 for i in 1..=block {
7673 let k = at[i..].iter().sum::<usize>() as f32 / n;
7674 surv.push(format!("{k:.2}"));
7675 }
7676 let distinct = self
7677 .dspark_real
7678 .iter()
7679 .collect::<std::collections::HashSet<_>>()
7680 .len();
7681 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
7682 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
7683 });
7684 let m = self.dspark_exp.len().max(1);
7685 eprintln!(
7686 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
7687 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
7688 self.dspark_hist.len(),
7689 mean + 1.0,
7690 surv.join(" ")
7691 );
7692 eprintln!(
7693 "DSpark: разных токенов {distinct} из {} (вырожденность), \
7694 эксперты ствол {}/{} на слой за {block} токенов, \
7695 черновик {}/{} за блок, draft {:.2} мс/блок",
7696 self.dspark_real.len(),
7697 tu / m,
7698 tt / m,
7699 du / m,
7700 dt / m,
7701 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
7702 );
7703 }
7704 }
7705
7706 fn forward_layers_upto(
7707 &mut self,
7708 hidden: &[f32],
7709 position: usize,
7710 task_mask: Option<&TaskMask>,
7711 upto: Option<usize>,
7712 ) -> Vec<f32> {
7713 if let Some(plan) = self.gpu_plan.clone() {
7719 if upto.is_none() && plan.len() > 1 {
7720 let mut h = hidden.to_vec();
7721 for &(dev, from, upto_incl) in plan.iter() {
7722 h = crate::gpu::with_device(dev, || {
7723 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
7724 });
7725 }
7726 return h;
7727 }
7728 }
7729 self.forward_layers_span(hidden, position, task_mask, 0, upto)
7730 }
7731
7732 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
7737 self.set_gpu_plan_at(devices, None)
7738 }
7739
7740 pub fn set_gpu_plan_at(
7744 &mut self,
7745 devices: Option<&[usize]>,
7746 at: Option<usize>,
7747 ) -> Result<(), String> {
7748 let Some(devs) = devices.filter(|d| d.len() > 1) else {
7749 self.gpu_plan = None;
7750 return Ok(());
7751 };
7752 self.split_supported()?;
7753 let n = self.num_layers;
7754 if devs.len() > n {
7755 return Err(format!("{} devices for {n} layers", devs.len()));
7756 }
7757 if let Some(k) = at {
7758 if k == 0 || k >= n {
7759 return Err(format!("split at {k}: the model has {n} layers"));
7760 }
7761 if devs.len() == 2 {
7762 self.gpu_plan = Some(std::sync::Arc::new(vec![
7763 (devs[0], 0, k - 1),
7764 (devs[1], k, n - 1),
7765 ]));
7766 return Ok(());
7767 }
7768 return Err(format!(
7769 "an explicit split point takes exactly 2 devices, got {}",
7770 devs.len()
7771 ));
7772 }
7773 let per = n.div_ceil(devs.len());
7774 let mut plan = Vec::with_capacity(devs.len());
7775 let mut from = 0usize;
7776 for &d in devs {
7777 if from >= n {
7778 break;
7779 }
7780 let upto = (from + per - 1).min(n - 1);
7781 plan.push((d, from, upto));
7782 from = upto + 1;
7783 }
7784 self.gpu_plan = Some(std::sync::Arc::new(plan));
7785 Ok(())
7786 }
7787
7788 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
7790 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
7791 }
7792
7793 fn forward_layers_span(
7799 &mut self,
7800 hidden: &[f32],
7801 position: usize,
7802 task_mask: Option<&TaskMask>,
7803 from: usize,
7804 upto: Option<usize>,
7805 ) -> Vec<f32> {
7806 debug_assert!(from == 0 || (self.dsv4.is_none() && self.g3n.is_none()));
7807 if let Some(b) = &mut self.dsv4 {
7813 let _ = (task_mask, upto);
7814 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
7815 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
7816 st.pos = position;
7817 let mut logits = Vec::new();
7818 crate::dsv4::forward_token(
7819 g,
7820 layers,
7821 &cfg,
7822 st,
7823 token_id,
7824 &self.inv_freq,
7825 self.pool.as_deref(),
7826 &mut logits,
7827 );
7828 self.graph_logits = Some(logits);
7829 self.dspark_probe(position, token_id);
7830 return vec![0.0; self.hidden_size];
7833 }
7834 if let Some(b) = &self.g3n {
7837 let _ = (task_mask, upto);
7838 return crate::g3n::g3n_forward(
7839 &b.0,
7840 &b.1,
7841 hidden,
7842 position,
7843 &mut self.kv_cache.layers,
7844 self.num_heads,
7845 self.num_kv_heads,
7846 self.head_dim,
7847 self.pool.as_deref(),
7848 );
7849 }
7850 let mut h = hidden.to_vec();
7851 let (nh, _nkv, _hd, hs, _rd, eps) = (
7854 self.num_heads,
7855 self.num_kv_heads,
7856 self.head_dim,
7857 self.hidden_size,
7858 self.rotary_dim,
7859 self.rms_eps,
7860 );
7861 let pool = self.pool.clone();
7862 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
7874 let graph_on = match graph_env.as_deref() {
7875 Some("0") => false,
7876 Some("prefill") => false, Some(_) => true,
7878 None => crate::gpu::wgpu_graph_default(),
7884 };
7885 let graph_trusted =
7886 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
7887 let race_eligible = graph_on
7888 && upto.is_none()
7889 && task_mask.is_none()
7890 && from == 0
7891 && !crate::gpu::graph_unsupported();
7892 let mut tail_start = 0usize;
7893 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
7894 let t_graph = std::time::Instant::now();
7895 let mut lg = Vec::new();
7896 let mut gl = 0usize;
7897 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
7898 if built.is_none() && !self.o1_active() && self.attn_softcap == 0.0 {
7903 crate::gpu::graph_mark_unsupported();
7904 }
7905 graph_note(built.is_some());
7906 if let Some(hh) = built {
7907 let dur = t_graph.elapsed();
7908 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7909 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
7910 }
7911 if gl > 0 && gl < self.num_layers {
7912 h = hh;
7918 tail_start = gl;
7919 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
7920 if !graph_trusted {
7921 crate::gpu::graph_race_record(true, dur);
7922 }
7923 if !lg.is_empty() {
7924 lg.resize(self.vocab_size, 0.0);
7927 if let Some(c) = self.final_softcap {
7928 for l in lg.iter_mut() {
7929 *l = c * (*l / c).tanh();
7930 }
7931 }
7932 self.graph_logits = Some(lg);
7933 }
7934 return hh;
7935 }
7936 }
7942 }
7943 let span = from > 0 || upto.is_some();
7967 if span && graph_on && task_mask.is_none() && graph_trusted {
7968 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
7969 let mut lg = Vec::new();
7970 let mut gl = 0usize;
7971 let span_res =
7972 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
7973 graph_note(span_res.is_some() && gl == upto_excl - from);
7974 if std::env::var("CMF_GPU_DEBUG").is_ok() {
7975 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
7979 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
7980 eprintln!(
7981 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
7982 upto_excl - from,
7983 span_res.is_some()
7984 );
7985 }
7986 }
7987 if let Some(hh) = span_res {
7988 if gl == upto_excl - from {
7989 if !lg.is_empty() {
7990 lg.resize(self.vocab_size, 0.0);
7991 if let Some(c) = self.final_softcap {
7992 for l in lg.iter_mut() {
7993 *l = c * (*l / c).tanh();
7994 }
7995 }
7996 self.graph_logits = Some(lg);
7997 }
7998 crate::gpu::set_layer(-1);
7999 return hh;
8000 }
8001 h = hh;
8003 tail_start = from + gl;
8004 }
8005 }
8006 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
8007
8008 #[cfg(target_os = "macos")]
8009 let mut gpu_skip_until = 0usize;
8010 for li in tail_start.max(from)..self.num_layers {
8011 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
8013 if li > u {
8014 break;
8015 }
8016 }
8017 if let Some(mask) = task_mask {
8018 if !mask.layer_alive(li) {
8019 continue; }
8021 }
8022 #[cfg(target_os = "macos")]
8026 {
8027 if li < gpu_skip_until {
8028 continue;
8029 }
8030 if task_mask.is_none() {
8031 let end = self.q1_graph_gpu(li, upto, position, &mut h);
8032 if end > li {
8033 gpu_skip_until = end;
8034 if self.is_loop_end(end - 1) && end < self.num_layers {
8037 h = inference::rms_norm(
8038 &h,
8039 &self.weights.final_norm,
8040 self.rms_eps,
8041 self.norm_style,
8042 );
8043 }
8044 continue;
8045 }
8046 }
8047 }
8048
8049 let lw = &self.weights.layers[self.phys_layer(li)];
8050 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
8051 if tp.parse::<usize>().ok() == Some(position) {
8052 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
8053 eprintln!(
8054 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
8055 h[0], h[1]
8056 );
8057 }
8058 }
8059 inference::rms_norm_into(
8062 &h,
8063 &lw.input_norm,
8064 self.rms_eps,
8065 self.norm_style,
8066 &mut self.ws.n1,
8067 );
8068
8069 let attn_out = match &lw.attn {
8070 AttnKind::Mla(w) => {
8071 let inv_freq_l = self.layer_inv_freq(li);
8072 let rs = self.layer_rope_scale(li);
8073 let eps = self.rms_eps;
8074 let pool = self.pool.clone();
8075 mla_attention(
8076 w,
8077 &self.ws.n1,
8078 &mut self.kv_cache.layers[li],
8079 position,
8080 &inv_freq_l,
8081 rs,
8082 eps,
8083 pool.as_deref(),
8084 )
8085 }
8086 AttnKind::Linear(w) => {
8087 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8088 vmf_phase_forward(
8089 &self.ws.n1,
8090 w,
8091 &cfg,
8092 &mut self.kv_cache.layers[li].linear_state,
8093 self.pool.as_deref(),
8094 )
8095 }
8096 AttnKind::Kda(w) => {
8097 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
8098 crate::linear_core::kda_forward(
8099 &self.ws.n1,
8100 w,
8101 &cfg,
8102 &mut self.kv_cache.layers[li].linear_state,
8103 self.pool.as_deref(),
8104 )
8105 }
8106 AttnKind::LinearGdn(w) => {
8107 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8108 gdn_forward(
8109 &self.ws.n1,
8110 w,
8111 &cfg,
8112 &mut self.kv_cache.layers[li].linear_state,
8113 self.pool.as_deref(),
8114 )
8115 }
8116 AttnKind::ShortConv(w) => {
8117 let cfg = self
8118 .short_conv_cfg
8119 .expect("short-conv layer without short_conv_cfg");
8120 short_conv_forward(
8121 &self.ws.n1,
8122 w,
8123 &cfg,
8124 &mut self.kv_cache.layers[li].linear_state,
8125 self.pool.as_deref(),
8126 )
8127 }
8128 AttnKind::Full {
8129 wq,
8130 wk,
8131 wv,
8132 wo,
8133 q_norm,
8134 k_norm,
8135 output_gate,
8136 softplus_gate,
8137 bias,
8138 } if self.kv_cache.layers[li].o1_sealed() => {
8139 let inv_freq_l = self.layer_inv_freq(li);
8142 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8143 let cfg = QwenAttnCfg {
8144 num_heads: self.layer_num_heads(li),
8145 num_kv_heads: nkv_l,
8146 head_dim: hd_l,
8147 hidden_size: hs,
8148 position,
8149 inv_freq: &inv_freq_l,
8150 rotary_dim: rd_l,
8151 scale: self.attn_scale,
8152 softcap: self.attn_softcap,
8153 window: None,
8154 v_norm: self.attn_v_norm,
8155 q_norm: q_norm.as_deref(),
8156 k_norm: k_norm.as_deref(),
8157 output_gate: *output_gate,
8158 softplus_gate: softplus_gate
8159 .as_ref()
8160 .map(|(gate, per_head)| (gate, *per_head)),
8161 rope_scale: self.layer_rope_scale(li),
8162 bias: bias
8163 .as_ref()
8164 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8165 rms_eps: eps,
8166 norm_style: self.norm_style,
8167 pool: pool.as_deref(),
8168 };
8169 attention::qwen_attention_nystrom(
8170 &self.ws.n1,
8171 wq,
8172 wk,
8173 wv,
8174 wo,
8175 &mut self.kv_cache.layers[li],
8176 &cfg,
8177 )
8178 }
8179 AttnKind::Full {
8180 wq,
8181 wk,
8182 wv,
8183 wo,
8184 q_norm,
8185 k_norm,
8186 output_gate,
8187 softplus_gate,
8188 bias,
8189 } => 'attn: {
8190 if graph_on
8193 && !*output_gate
8194 && softplus_gate.is_none()
8195 && self.attention_heads_per_layer.is_none()
8196 && bias.is_none()
8197 && task_mask.is_none()
8198 {
8199 let inv_freq_l = self.layer_inv_freq(li);
8200 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8201 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
8202 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
8203 wq.mapped_q1(),
8204 wk.mapped_q1(),
8205 wv.mapped_q1(),
8206 wo.mapped_q1(),
8207 ) {
8208 let gm = gm.clone();
8209 let mut out = vec![0f32; hs];
8210 let cache = &self.kv_cache.layers[li];
8211 if crate::gpu::attn_dropin(
8212 &gm,
8213 self.graph_kv_id,
8214 li,
8215 &self.ws.n1,
8216 qi,
8217 ki,
8218 vi,
8219 oi,
8220 q_norm.as_deref(),
8221 k_norm.as_deref(),
8222 &inv_freq_l,
8223 nh,
8224 nkv_l,
8225 hd_l,
8226 rd_l,
8227 hs,
8228 position,
8229 self.kv_cache.max_seq_len,
8230 gemma,
8231 eps as f32,
8232 cache.k_heads(),
8233 cache.v_heads(),
8234 &mut out,
8235 ) {
8236 break 'attn out;
8237 }
8238 }
8239 }
8240 let masked = task_mask
8241 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
8242 .unwrap_or(false);
8243 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
8244 match (masked, f32_view) {
8245 (true, (Some(q), Some(k), Some(v), Some(o))) => {
8248 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
8249 attention::multi_head_attention(
8250 &self.ws.n1,
8251 q,
8252 k,
8253 v,
8254 o,
8255 &mut self.kv_cache.layers[li],
8256 self.num_heads,
8257 self.num_kv_heads,
8258 self.head_dim,
8259 self.hidden_size,
8260 position,
8261 &active_heads,
8262 &self.inv_freq,
8263 )
8264 }
8265 (masked, _) => {
8266 if masked {
8267 tracing::warn!(
8268 "layer {li}: head mask on quantized weights not \
8269 supported yet — executing dense"
8270 );
8271 }
8272 let inv_freq_l = self.layer_inv_freq(li);
8273 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8274 let cfg = QwenAttnCfg {
8275 num_heads: self.layer_num_heads(li),
8276 num_kv_heads: nkv_l,
8277 head_dim: hd_l,
8278 hidden_size: hs,
8279 position,
8280 inv_freq: &inv_freq_l,
8281 rotary_dim: rd_l,
8282 scale: self.attn_scale,
8283 softcap: self.attn_softcap,
8284 window: self.layer_window(li),
8285 v_norm: self.attn_v_norm,
8286 q_norm: q_norm.as_deref(),
8287 k_norm: k_norm.as_deref(),
8288 output_gate: *output_gate,
8289 softplus_gate: softplus_gate
8290 .as_ref()
8291 .map(|(gate, per_head)| (gate, *per_head)),
8292 rope_scale: self.layer_rope_scale(li),
8293 bias: bias
8294 .as_ref()
8295 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8296 rms_eps: eps,
8297 norm_style: self.norm_style,
8298 pool: pool.as_deref(),
8299 };
8300 attention::qwen_attention(
8301 &self.ws.n1,
8302 wq,
8303 wk,
8304 wv,
8305 wo,
8306 &mut self.kv_cache.layers[li],
8307 &cfg,
8308 )
8309 }
8310 }
8311 }
8312 };
8313 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8316 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
8317 None => attn_out,
8318 };
8319 let lw = &self.weights.layers[self.phys_layer(li)];
8320 inference::add_rmsnorm_fused_into(
8321 &mut h,
8322 &attn_out,
8323 &lw.post_norm,
8324 self.rms_eps,
8325 self.norm_style,
8326 &mut self.ws.p1,
8327 );
8328 let mut attn_out = attn_out;
8329 attention::recycle_buf(&mut attn_out);
8330 let post_normed = &self.ws.p1;
8331
8332 let ffn_masked = task_mask
8333 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
8334 .unwrap_or(false);
8335 let ffn_out = match (ffn_masked, &lw.ffn) {
8347 (true, FfnKind::Dense(d)) => {
8348 let tm = task_mask.unwrap();
8349 let alive = tm.ffn_active_count(li);
8350 let deep = alive * 2 <= self.intermediate_size;
8351 if deep && d.down_proj.sparse_col_ok() {
8352 let active = tm.ffn_active_indices(li);
8353 sparse_ffn_quant(
8354 d,
8355 post_normed,
8356 &active,
8357 self.hidden_size,
8358 self.pool.as_deref(),
8359 )
8360 } else if deep
8361 && let (Some(g), Some(u), Some(dn)) = (
8362 d.gate_proj.as_f32(),
8363 d.up_proj.as_f32(),
8364 d.down_proj.as_f32(),
8365 )
8366 {
8367 let active = tm.ffn_active_indices(li);
8368 inference::sparse_ffn_forward(
8369 post_normed,
8370 g,
8371 u,
8372 dn,
8373 self.hidden_size,
8374 self.intermediate_size,
8375 &active,
8376 self.pool.as_deref(),
8377 )
8378 } else {
8379 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
8380 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
8381 }
8382 }
8383 (true, FfnKind::Moe(m)) => {
8384 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
8388 ffn_forward(
8389 &lw.ffn,
8390 post_normed,
8391 self.pool.as_deref(),
8392 allowed.as_deref(),
8393 )
8394 }
8395 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
8396 dm,
8397 post_normed,
8398 &h,
8399 self.rms_eps,
8400 self.norm_style,
8401 self.pool.as_deref(),
8402 ),
8403 (false, _) => match &lw.ffn {
8404 FfnKind::DenseMoe(dm) => dense_moe_ffn(
8405 dm,
8406 post_normed,
8407 &h,
8408 self.rms_eps,
8409 self.norm_style,
8410 self.pool.as_deref(),
8411 ),
8412 _ => {
8413 let allowed = match (&lw.ffn, task_mask) {
8414 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
8415 _ => None,
8416 };
8417 ffn_forward(
8418 &lw.ffn,
8419 post_normed,
8420 self.pool.as_deref(),
8421 allowed.as_deref(),
8422 )
8423 }
8424 },
8425 };
8426 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8427 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
8428 None => ffn_out,
8429 };
8430 for (i, &f) in ffn_out.iter().enumerate() {
8431 h[i] += f;
8432 }
8433 let mut ffn_out = ffn_out;
8434 attention::recycle_buf(&mut ffn_out);
8435
8436 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8438 for v in h.iter_mut() {
8439 *v *= sc;
8440 }
8441 }
8442
8443 if self.is_loop_end(li) && li + 1 < self.num_layers {
8446 h = inference::rms_norm(
8447 &h,
8448 &self.weights.final_norm,
8449 self.rms_eps,
8450 self.norm_style,
8451 );
8452 }
8453
8454 if self.dyn_phi_layer == Some(li) {
8458 self.update_dyn_phi(&h);
8459 }
8460 }
8461 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
8463 crate::gpu::graph_race_record(false, t.elapsed());
8464 }
8465
8466 h
8467 }
8468
8469 fn update_dyn_phi(&mut self, h: &[f32]) {
8472 const A: f32 = 0.2;
8473 if self.dyn_phi_ema.len() != h.len() {
8474 self.dyn_phi_ema = vec![0.0; h.len()];
8475 self.dyn_phi_seen = 0;
8476 }
8477 if self.dyn_phi_seen == 0 {
8478 self.dyn_phi_ema.copy_from_slice(h);
8479 } else {
8480 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
8481 *e = (1.0 - A) * *e + A * v;
8482 }
8483 }
8484 self.dyn_phi_seen += 1;
8485 }
8486
8487 pub fn dyn_phi(&self) -> &[f32] {
8489 &self.dyn_phi_ema
8490 }
8491
8492 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
8494 self.dyn_phi_layer = layer;
8495 self.dyn_phi_ema.clear();
8496 self.dyn_phi_seen = 0;
8497 }
8498
8499 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
8501 let Some(model) = &self.model else {
8502 return Vec::new();
8503 };
8504 model
8505 .header
8506 .skills
8507 .iter()
8508 .enumerate()
8509 .filter_map(|(i, sk)| {
8510 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
8511 let sel = sk.selection.as_ref()?;
8512 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
8513 })
8514 .collect()
8515 }
8516
8517 pub fn active_skill(&self) -> Option<usize> {
8519 self.dyn_active
8520 }
8521
8522 pub fn enable_dynamic_routing(&mut self) -> usize {
8527 use crate::swarm::{DynRouter, RoutableSkill};
8528 let Some(model) = self.model.clone() else {
8529 return 0;
8530 };
8531 if self.dyn_blend_loaded {
8534 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
8535 return 0;
8536 }
8537 if let Some(a) = self.dyn_active {
8541 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
8542 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
8543 return 0;
8544 }
8545 }
8546 let hidden = self.hidden_size;
8547 let mut skills = Vec::new();
8548 for (idx, id, _phi) in self.dynamic_skills() {
8549 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
8550 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
8551 skills.push(rs);
8552 }
8553 }
8554 }
8555 if skills.is_empty() {
8556 return 0;
8557 }
8558 let phi = skills[0].phi_layer;
8560 if skills.iter().any(|s| s.phi_layer != phi) {
8561 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
8562 }
8563 let n = skills.len();
8564 self.set_dyn_phi_layer(Some(phi));
8565 self.dyn_router = Some(DynRouter::new(skills));
8566 n
8567 }
8568
8569 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
8571 self.dyn_router
8572 .as_ref()
8573 .map(|r| r.switches.clone())
8574 .unwrap_or_default()
8575 }
8576
8577 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
8580 let rows = self.weights.lm_head.rows();
8581 let mut logits = attention::take_buf(rows.min(self.vocab_size));
8582 self.weights
8583 .lm_head
8584 .matvec(hidden, &mut logits, self.pool.as_deref());
8585 logits.resize(self.vocab_size, 0.0);
8586 if let Some(m) = self.logit_multiplier {
8587 for l in logits.iter_mut() {
8588 *l *= m;
8589 }
8590 }
8591 if let Some(c) = self.final_softcap {
8592 for l in logits.iter_mut() {
8593 *l = c * (*l / c).tanh();
8594 }
8595 }
8596 logits
8597 }
8598
8599 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
8604 self.kv_cache.clear();
8605 self.kv_history.clear();
8606 let mut hidden = vec![0.0f32; self.hidden_size];
8607 for (pos, &id) in ids.iter().enumerate() {
8608 let emb = self.embed_single(id);
8609 hidden = self.forward_layers(&emb, pos, task_mask);
8610 }
8611 inference::rms_norm_into(
8612 &hidden,
8613 &self.weights.final_norm,
8614 self.rms_eps,
8615 self.norm_style,
8616 &mut self.ws.n1,
8617 );
8618 self.lm_head_forward(&self.ws.n1)
8619 }
8620}
8621
8622pub fn create_test_pipeline(
8624 hidden_size: usize,
8625 intermediate_size: usize,
8626 num_heads: usize,
8627 num_kv_heads: usize,
8628 head_dim: usize,
8629 num_layers: usize,
8630 vocab_size: usize,
8631) -> Pipeline {
8632 let synth = |n: usize, salt: usize| -> Vec<f32> {
8635 (0..n)
8636 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
8637 .collect()
8638 };
8639 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
8640 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
8641 };
8642 let layer_weights: Vec<LayerWeights> = (0..num_layers)
8643 .map(|li| LayerWeights {
8644 input_norm: vec![1.0; hidden_size],
8645 post_norm: vec![1.0; hidden_size],
8646 attn_out_norm: None,
8647 ffn_out_norm: None,
8648 layer_scale: None,
8649 ffn: FfnKind::Dense(DenseFfn {
8650 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
8651 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
8652 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
8653 act: Act::Silu,
8654 }),
8655 attn: AttnKind::Full {
8656 bias: None,
8657 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
8658 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
8659 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
8660 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
8661 q_norm: None,
8662 k_norm: None,
8663 output_gate: false,
8664 softplus_gate: None,
8665 },
8666 })
8667 .collect();
8668
8669 Pipeline::new(
8670 Tokenizer::byte_level(),
8671 PipelineWeights {
8672 embed_tokens: qt(vocab_size, hidden_size, 100),
8673 layers: layer_weights,
8674 lm_head: qt(vocab_size, hidden_size, 200),
8675 final_norm: vec![1.0; hidden_size],
8676 },
8677 hidden_size,
8678 intermediate_size,
8679 num_heads,
8680 num_kv_heads,
8681 head_dim,
8682 num_layers,
8683 num_layers, false, vocab_size,
8686 1e-6,
8687 10_000.0,
8688 NormStyle::Qwen,
8689 4096,
8690 SamplerConfig {
8691 seed: Some(42),
8692 ..Default::default()
8693 },
8694 )
8695}
8696
8697#[inline]
8702fn mask_bit(row: &[u8], j: usize) -> bool {
8703 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
8704}
8705
8706fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
8712 for r in 0..rows {
8713 let base = r * inter;
8714 for (bi, &byte) in row.iter().enumerate() {
8715 if byte == 0xFF {
8716 continue;
8717 }
8718 let j0 = bi * 8;
8719 for bit in 0..8 {
8720 let j = j0 + bit;
8721 if j < inter && byte & (1 << bit) == 0 {
8722 g[base + j] = 0.0;
8723 }
8724 }
8725 }
8726 }
8727}
8728
8729fn dense_ffn_batch(
8730 d: &DenseFfn,
8731 xs: &[f32],
8732 b: usize,
8733 pool: Option<&Pool>,
8734 mask_row: Option<&[u8]>,
8735) -> Vec<f32> {
8736 let inter = d.gate_proj.rows();
8737 let hidden = d.down_proj.rows();
8738 if mask_row.is_none()
8746 && d.act == Act::Silu
8747 && b >= 32
8748 && crate::gpu::enabled_here()
8749 && !crate::gpu::mm_killed()
8750 {
8751 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
8752 d.gate_proj.mapped_q4t(),
8753 d.up_proj.mapped_q4t(),
8754 d.down_proj.mapped_q4t(),
8755 ) {
8756 let mut out = vec![0.0f32; b * hidden];
8757 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
8758 return out;
8759 }
8760 }
8761 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
8766 d.gate_proj.mapped_q4tp(),
8767 d.up_proj.mapped_q4tp(),
8768 d.down_proj.mapped_q4tp(),
8769 ) {
8770 let mut out = vec![0.0f32; b * hidden];
8771 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
8772 return out;
8773 }
8774 }
8775 }
8776 let mut g = vec![0.0f32; b * inter];
8777 d.gate_proj.matmat(xs, b, &mut g, pool);
8778 let mut u = vec![0.0f32; b * inter];
8779 d.up_proj.matmat(xs, b, &mut u, pool);
8780 for i in 0..b * inter {
8781 g[i] = d.act.combine(g[i], u[i]);
8782 }
8783 if let Some(row) = mask_row {
8784 zero_masked_cols(&mut g, b, inter, row);
8785 }
8786 let mut out = vec![0.0f32; b * hidden];
8787 d.down_proj.matmat(&g, b, &mut out, pool);
8788 out
8789}
8790
8791fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
8796 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8797 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8798 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
8799 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
8800 if (!on && !dump) || b == 0 {
8801 return;
8802 }
8803 let hidden = xs.len() / b;
8804 if on {
8805 let mut acc = m.act_sq.borrow_mut();
8806 if acc.len() < hidden {
8807 acc.resize(hidden, 0.0);
8808 }
8809 for t in 0..b {
8810 let row = &xs[t * hidden..(t + 1) * hidden];
8811 for (a, &v) in acc.iter_mut().zip(row) {
8812 *a += (v as f64) * (v as f64);
8813 }
8814 }
8815 }
8816 if dump {
8817 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
8820 .ok()
8821 .and_then(|v| v.parse().ok())
8822 .unwrap_or(4096);
8823 let mut rows = m.act_rows.borrow_mut();
8824 if rows.len() < cap * hidden {
8825 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
8826 rows.extend_from_slice(&xs[..take * hidden]);
8827 }
8828 }
8829}
8830
8831#[derive(Clone, Copy)]
8834struct SendVecs(*mut Vec<f32>);
8835unsafe impl Send for SendVecs {}
8836unsafe impl Sync for SendVecs {}
8837impl SendVecs {
8838 #[inline]
8839 fn at(self, i: usize) -> *mut Vec<f32> {
8840 unsafe { self.0.add(i) }
8841 }
8842}
8843
8844fn moe_ffn_batch(
8845 m: &MoeFfn,
8846 xs: &[f32],
8847 b: usize,
8848 hidden: usize,
8849 pool: Option<&Pool>,
8850 allowed: Option<&[bool]>,
8851) -> Vec<f32> {
8852 accumulate_act(m, xs, b);
8853 let ne = m.experts.len();
8854 let mut logits = vec![0.0f32; b * ne];
8855 m.router.matmat(xs, b, &mut logits, pool);
8856
8857 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
8860 {
8861 let mut st = m.stats.borrow_mut();
8862 if st.len() < ne {
8863 st.resize(ne, 0);
8864 }
8865 for bi in 0..b {
8866 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
8867 for &e in &idx {
8868 st[e] += 1;
8869 assign[e].push((bi, p[e] / wsum));
8870 }
8871 }
8872 }
8873
8874 let mut out = vec![0.0f32; b * hidden];
8875 let cols = m.experts[0].gate_proj.cols();
8876 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
8877 let sb = list.len();
8878 let mut sub = vec![0.0f32; sb * cols];
8879 for (k, &(bi, _)) in list.iter().enumerate() {
8880 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
8881 }
8882 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
8883 for (k, &(bi, w)) in list.iter().enumerate() {
8884 for i in 0..hidden {
8885 out[bi * hidden + i] += w * eo[k * hidden + i];
8886 }
8887 }
8888 };
8889 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
8895 if pool.is_some() && active.len() >= 8 {
8896 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
8897 {
8898 let panel_ptr = SendVecs(panels.as_mut_ptr());
8899 let experts = &m.experts;
8902 let (active_r, assign_r) = (&active, &assign);
8903 let run = |start: usize, end: usize| {
8904 for ai in start..end {
8905 let e = active_r[ai];
8906 let list = &assign_r[e];
8907 let sb = list.len();
8908 let mut sub = vec![0.0f32; sb * cols];
8909 for (k, &(bi, _)) in list.iter().enumerate() {
8910 sub[k * cols..(k + 1) * cols]
8911 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
8912 }
8913 unsafe {
8915 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
8916 }
8917 }
8918 };
8919 match pool {
8920 Some(p) => p.run_rows(active.len(), &run),
8921 None => run(0, active.len()),
8922 }
8923 }
8924 for (ai, &e) in active.iter().enumerate() {
8925 for (k, &(bi, w)) in assign[e].iter().enumerate() {
8926 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
8927 for i in 0..hidden {
8928 out[bi * hidden + i] += w * eo[i];
8929 }
8930 }
8931 }
8932 } else {
8933 for &e in &active {
8934 run_expert(&m.experts[e], &assign[e], &mut out);
8935 }
8936 }
8937 if let Some((se, gate)) = &m.shared {
8938 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
8939 let mut gl = vec![0.0f32; b];
8940 gate.matmat(xs, b, &mut gl, pool);
8941 (0..b)
8942 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
8943 .collect()
8944 } else {
8945 (0..b).map(|bi| (bi, 1.0)).collect()
8946 };
8947 run_expert(se, &all, &mut out);
8948 }
8949 out
8950}
8951
8952thread_local! {
8953 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
8957 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
8958}
8959
8960fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
8962 if crate::gpu::enabled_here()
8973 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
8974 {
8975 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
8976 crate::gpu::ProbeArm::Gpu
8977 } else {
8978 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
8979 };
8980 match arm {
8981 crate::gpu::ProbeArm::Gpu => {
8982 let t0 = std::time::Instant::now();
8983 if let Some(out) = dense_ffn_gpu(d, x, pool) {
8984 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
8985 return out;
8986 }
8987 }
8988 crate::gpu::ProbeArm::CpuTimed => {
8989 let t0 = std::time::Instant::now();
8990 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
8991 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
8992 return out;
8993 }
8994 crate::gpu::ProbeArm::Cpu => {
8995 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
8996 }
8997 }
8998 }
8999 dense_ffn_cpu(d, x, pool)
9000}
9001
9002fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
9004 let inter = d.gate_proj.rows();
9005 FFN_SCRATCH.with(|s| {
9006 let mut s = s.borrow_mut();
9007 let [g, u, ..] = &mut *s;
9008 g.resize(inter, 0.0);
9009 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
9012 } else {
9014 u.resize(inter, 0.0);
9015 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9017 for i in 0..inter {
9018 g[i] = d.act.combine(g[i], u[i]);
9019 }
9020 }
9021 FFN_PROBE.with(|pr| {
9024 if let Some(acc) = pr.borrow_mut().as_mut() {
9025 let li = crate::gpu::cur_layer();
9026 if li >= 0 {
9027 if let Some(row) = acc.get_mut(li as usize) {
9028 for (a, &v) in row.iter_mut().zip(g.iter()) {
9029 *a += (v as f64).abs();
9030 }
9031 }
9032 }
9033 }
9034 });
9035 let mut out = attention::take_buf(d.down_proj.rows());
9036 d.down_proj.matvec(g, &mut out, pool);
9037 out
9038 })
9039}
9040
9041thread_local! {
9042 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
9045 const { std::cell::RefCell::new(None) };
9046}
9047
9048fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
9053 let inter = d.gate_proj.rows();
9054 FFN_SCRATCH.with(|s| {
9055 let mut s = s.borrow_mut();
9056 let [g, u, ..] = &mut *s;
9057 g.resize(inter, 0.0);
9058 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
9059 } else {
9061 u.resize(inter, 0.0);
9062 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
9063 for i in 0..inter {
9064 g[i] = d.act.combine(g[i], u[i]);
9065 }
9066 }
9067 zero_masked_cols(g, 1, inter, mask_row);
9068 let mut out = attention::take_buf(d.down_proj.rows());
9069 d.down_proj.matvec(g, &mut out, pool);
9070 out
9071 })
9072}
9073
9074fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
9080 if d.act != Act::Silu {
9082 return None;
9083 }
9084 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
9087 return None;
9088 }
9089 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
9090 let mut model_ref = None;
9091 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
9092 let model = model_ref?;
9093 let hidden = jobs[0].down.1;
9094 let mut out = attention::take_buf(hidden);
9095 if crate::gpu::moe_block(&model, &jobs, &mut out) {
9096 Some(out)
9097 } else {
9098 let mut out = out;
9099 attention::recycle_buf(&mut out);
9100 None
9101 }
9102}
9103
9104#[allow(clippy::type_complexity)]
9109#[allow(clippy::type_complexity)]
9110pub(crate) fn moe_parts(
9111 t: &QTensor,
9112) -> Option<(
9113 &std::sync::Arc<cortiq_core::CmfModel>,
9114 usize,
9115 usize,
9116 usize,
9117 &[f32],
9118 &[f32],
9119 bool,
9120 bool,
9121 bool,
9122)> {
9123 match t {
9124 QTensor::Mapped {
9125 model,
9126 idx,
9127 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
9128 rows,
9129 cols,
9130 row_scale,
9131 col_field,
9132 ..
9133 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
9134 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
9135 )),
9136 QTensor::Mapped {
9138 model,
9139 idx,
9140 dtype: cortiq_core::TensorDtype::Q1,
9141 rows,
9142 cols,
9143 ..
9144 } => Some((
9145 model,
9146 *idx,
9147 *rows,
9148 *cols,
9149 &[][..],
9150 &[][..],
9151 true,
9152 false,
9153 false,
9154 )),
9155 QTensor::Mapped {
9157 model,
9158 idx,
9159 dtype: cortiq_core::TensorDtype::Q4Tiled,
9160 rows,
9161 cols,
9162 ..
9163 } => Some((
9164 model,
9165 *idx,
9166 *rows,
9167 *cols,
9168 &[][..],
9169 &[][..],
9170 false,
9171 true,
9172 false,
9173 )),
9174 QTensor::Mapped {
9176 model,
9177 idx,
9178 dtype: cortiq_core::TensorDtype::Q4TiledP,
9179 rows,
9180 cols,
9181 ..
9182 } => Some((
9183 model,
9184 *idx,
9185 *rows,
9186 *cols,
9187 &[][..],
9188 &[][..],
9189 false,
9190 true,
9191 false,
9192 )),
9193 QTensor::Mapped {
9197 model,
9198 idx,
9199 dtype: cortiq_core::TensorDtype::Q2TiledP,
9200 rows,
9201 cols,
9202 ..
9203 } => Some((
9204 model,
9205 *idx,
9206 *rows,
9207 *cols,
9208 &[][..],
9209 &[][..],
9210 false,
9211 true,
9212 true,
9213 )),
9214 _ => None,
9215 }
9216}
9217
9218#[cfg(target_os = "macos")]
9224fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
9225 if m.router_sigmoid
9226 || m.router_input_norm
9227 || m.expert_bias.is_some()
9228 || m.route_tau.is_some()
9229 || m.mask.is_some()
9230 || m.per_expert_scale.is_some()
9231 || m.experts.is_empty()
9232 || m.top_k == 0
9233 {
9234 return None;
9235 }
9236 let (sh, sg) = match &m.shared {
9239 Some((sh, Some(sg))) => (sh, sg),
9240 _ => return None,
9241 };
9242 let (rf, rr, rc) = m.router.f32_parts()?;
9243 if rr != m.experts.len() || rc != hidden {
9244 return None;
9245 }
9246 let (sf, sr, sc) = sg.f32_parts()?;
9247 if sr * sc != hidden {
9248 return None;
9249 }
9250 let inter = m.experts[0].gate_proj.rows();
9251 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
9254 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
9255 if e.act != Act::Silu
9256 || e.gate_proj.rows() != inter
9257 || e.gate_proj.cols() != hidden
9258 || e.up_proj.rows() != inter
9259 || e.up_proj.cols() != hidden
9260 || e.down_proj.rows() != hidden
9261 || e.down_proj.cols() != inter
9262 {
9263 return None;
9264 }
9265 let pick = |t: &QTensor| -> Option<usize> {
9266 if gu_q2 {
9267 t.mapped_q2tp().map(|(_, i)| i)
9268 } else {
9269 t.mapped_q4tp().map(|(_, i)| i)
9270 }
9271 };
9272 Some((
9273 pick(&e.gate_proj)?,
9274 pick(&e.up_proj)?,
9275 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
9276 ))
9277 };
9278 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
9279 let shared = trio(sh)?;
9280 Some(crate::gpu::GpuMoe {
9281 router: rf,
9282 sgate: sf,
9283 experts,
9284 shared,
9285 n_exp: m.experts.len(),
9286 top_k: m.top_k,
9287 inter,
9288 norm_topk: m.norm_topk_prob,
9289 route_scale: m.routed_scaling,
9290 gu_q2,
9291 })
9292}
9293
9294pub(crate) fn moe_push_job_parts<'a>(
9298 gate: &'a QTensor,
9299 up: &'a QTensor,
9300 down: &'a QTensor,
9301 x: &[f32],
9302 w: f32,
9303 swiglu_limit: f32,
9304 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
9305 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
9306) -> Option<()> {
9307 use crate::qtensor::prescale;
9308 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
9309 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
9310 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
9311 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
9312 return None; }
9314 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
9317 return None;
9318 }
9319 if !gq2 && dq2 {
9320 return None;
9321 }
9322 model_ref.get_or_insert_with(|| gm.clone());
9323 let dt = |cf: &[f32]| {
9324 if cf.is_empty() {
9325 cortiq_core::TensorDtype::Q8Row
9326 } else {
9327 cortiq_core::TensorDtype::Q8_2f
9328 }
9329 };
9330 jobs.push(crate::gpu::MoeJob {
9331 gate: (gi, gr, gc, grs),
9332 up: (ui, ur, uc, urs),
9333 down: (di, dr, dc, drs),
9334 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
9335 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
9336 down_col: dcf,
9337 w,
9338 q1: gq1,
9339 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
9340 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
9341 gu_q2: gq2,
9342 swiglu_limit,
9343 });
9344 Some(())
9345}
9346
9347fn moe_push_job<'a>(
9349 d: &'a DenseFfn,
9350 x: &[f32],
9351 w: f32,
9352 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
9353 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
9354) -> Option<()> {
9355 use crate::qtensor::prescale;
9356 if d.act != Act::Silu {
9357 return None; }
9359 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
9360 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
9361 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
9362 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
9363 return None; }
9365 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
9366 return None;
9367 }
9368 if !gq2 && dq2 {
9369 return None;
9370 }
9371 model_ref.get_or_insert_with(|| gm.clone());
9372 let gdt = if gcf.is_empty() {
9373 cortiq_core::TensorDtype::Q8Row
9374 } else {
9375 cortiq_core::TensorDtype::Q8_2f
9376 };
9377 let udt = if ucf.is_empty() {
9378 cortiq_core::TensorDtype::Q8Row
9379 } else {
9380 cortiq_core::TensorDtype::Q8_2f
9381 };
9382 jobs.push(crate::gpu::MoeJob {
9383 gate: (gi, gr, gc, grs),
9384 up: (ui, ur, uc, urs),
9385 down: (di, dr, dc, drs),
9386 xs_gate: prescale(x, gcf, gdt).into_owned(),
9387 xs_up: prescale(x, ucf, udt).into_owned(),
9388 down_col: dcf,
9389 w,
9390 q1: gq1,
9391 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
9392 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
9393 gu_q2: gq2,
9394 swiglu_limit: 0.0,
9395 });
9396 Some(())
9397}
9398
9399fn sparse_ffn_quant(
9406 d: &DenseFfn,
9407 x: &[f32],
9408 active: &[u16],
9409 hidden: usize,
9410 pool: Option<&Pool>,
9411) -> Vec<f32> {
9412 let n = active.len();
9413 let inter = d.gate_proj.rows();
9414 let mut act = vec![0.0f32; n];
9415 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
9418 let compute = |ai: usize| -> f32 {
9419 let idx = active[ai] as usize;
9420 if idx >= inter {
9421 return 0.0; }
9423 let mut s = if need_scratch {
9424 vec![0.0f32; hidden]
9425 } else {
9426 Vec::new()
9427 };
9428 let gate = d.gate_proj.row_dot(idx, x, &mut s);
9429 let up = d.up_proj.row_dot(idx, x, &mut s);
9430 d.act.combine(gate, up)
9431 };
9432 match pool {
9433 Some(p) if n >= 256 => {
9434 let ptr = SendMut(act.as_mut_ptr());
9435 p.run(&|widx, nw| {
9436 let chunk = n.div_ceil(nw);
9437 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
9438 for ai in s..e {
9439 unsafe { *ptr.at(ai) = compute(ai) };
9440 }
9441 });
9442 }
9443 _ => {
9444 for (ai, a) in act.iter_mut().enumerate() {
9445 *a = compute(ai);
9446 }
9447 }
9448 }
9449 let mut out = vec![0.0f32; hidden];
9451 for (ai, &idx) in active.iter().enumerate() {
9452 let w = act[ai];
9453 if w.abs() >= 1e-12 && (idx as usize) < inter {
9454 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
9455 }
9456 }
9457 out
9458}
9459
9460#[doc(hidden)]
9462pub fn sparse_ffn_quant_for_test(
9463 d: &DenseFfn,
9464 x: &[f32],
9465 active: &[u16],
9466 hidden: usize,
9467) -> Vec<f32> {
9468 sparse_ffn_quant(d, x, active, hidden, None)
9469}
9470
9471fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
9475 let deq = |t: &QTensor| -> Vec<f32> {
9476 let (rows, cols) = (t.rows(), t.cols());
9477 let mut out = vec![0.0f32; rows * cols];
9478 for r in 0..rows {
9479 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
9480 }
9481 out
9482 };
9483 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
9484}
9485
9486struct SendMut(*mut f32);
9488unsafe impl Send for SendMut {}
9489unsafe impl Sync for SendMut {}
9490impl SendMut {
9491 #[inline]
9492 #[allow(clippy::mut_from_ref)]
9495 unsafe fn at(&self, i: usize) -> &mut f32 {
9496 unsafe { &mut *self.0.add(i) }
9497 }
9498}
9499
9500fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
9510 let ne = logits.len();
9511 let p: Vec<f32> = if m.router_sigmoid {
9512 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
9513 } else {
9514 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9515 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
9516 let s: f32 = e.iter().sum();
9517 for v in &mut e {
9518 *v /= s;
9519 }
9520 e
9521 };
9522 let admit = |e: usize| {
9528 m.mask.as_ref().is_none_or(|mk| mk[e])
9529 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
9530 };
9531 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
9532 match &m.expert_bias {
9534 Some(b) => idx.sort_unstable_by(|&x, &y| {
9535 (p[y] + b[y])
9536 .partial_cmp(&(p[x] + b[x]))
9537 .unwrap()
9538 .then(x.cmp(&y))
9539 }),
9540 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
9541 }
9542 idx.truncate(m.top_k);
9543 if let Some(tau) = m.route_tau {
9547 let total: f32 = idx.iter().map(|&e| p[e]).sum();
9548 if total > 0.0 {
9549 let mut acc = 0.0f32;
9550 let mut keep = idx.len();
9551 for (i, &e) in idx.iter().enumerate() {
9552 acc += p[e];
9553 if acc >= tau * total {
9554 keep = i + 1;
9555 break;
9556 }
9557 }
9558 idx.truncate(keep);
9559 }
9560 }
9561 let wsum: f32 = if m.norm_topk_prob {
9562 let s: f32 = idx.iter().map(|&e| p[e]).sum();
9563 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
9566 } else {
9567 1.0 / m.routed_scaling
9568 };
9569 (idx, p, wsum)
9570}
9571
9572fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
9575 accumulate_act(m, x, 1);
9576 let ne = m.experts.len();
9577 let mut logits = vec![0.0f32; ne];
9578 m.router.matvec(x, &mut logits, pool);
9579 let (idx, p, wsum) = moe_route(&logits, m, allowed);
9580 {
9581 let mut st = m.stats.borrow_mut();
9582 if st.len() < ne {
9583 st.resize(ne, 0);
9584 }
9585 for &e in &idx {
9586 st[e] += 1;
9587 }
9588 }
9589 if crate::gpu::enabled_here() {
9594 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
9595 crate::gpu::ProbeArm::Gpu => {
9596 let t0 = std::time::Instant::now();
9597 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
9598 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
9599 return out;
9600 }
9601 }
9602 crate::gpu::ProbeArm::CpuTimed => {
9603 let t0 = std::time::Instant::now();
9604 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
9605 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
9606 return out;
9607 }
9608 crate::gpu::ProbeArm::Cpu => {
9609 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
9610 }
9611 }
9612 }
9613 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
9614}
9615
9616fn graph_note(built: bool) {
9620 use std::sync::atomic::{AtomicBool, Ordering};
9621 if built {
9622 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
9623 } else {
9624 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
9625 }
9626 static SAID: AtomicBool = AtomicBool::new(false);
9627 if !SAID.swap(true, Ordering::Relaxed) {
9628 if built {
9629 tracing::info!("wgpu whole-token graph: ACTIVE");
9630 } else {
9631 tracing::warn!("wgpu whole-token graph refused — per-op path");
9632 }
9633 }
9634}
9635
9636pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9640pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
9641
9642fn moe_batch_enabled() -> bool {
9645 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9646 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
9647}
9648
9649fn moe_ffn_cpu_batched(
9655 m: &MoeFfn,
9656 x: &[f32],
9657 idx: &[usize],
9658 p: &[f32],
9659 wsum: f32,
9660 pool: Option<&Pool>,
9661) -> Option<Vec<f32>> {
9662 if idx.is_empty() || !moe_batch_enabled() {
9663 return None;
9664 }
9665 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
9669 return None;
9670 }
9671 let n = idx.len() + usize::from(m.shared.is_some());
9672 let mut pairs = Vec::with_capacity(n);
9673 let mut downs = Vec::with_capacity(n);
9674 let mut ws = Vec::with_capacity(n);
9675 for &e in idx {
9676 let d = &m.experts[e];
9677 if d.act != Act::Silu {
9678 return None;
9679 }
9680 pairs.push((&d.gate_proj, &d.up_proj));
9681 downs.push(&d.down_proj);
9682 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
9683 }
9684 if let Some((se, gate)) = &m.shared {
9687 if se.act != Act::Silu {
9688 return None;
9689 }
9690 let g = gate.as_ref().map_or(1.0, |gate| {
9691 let mut gl = [0.0f32; 1];
9692 gate.matvec(x, &mut gl, pool);
9693 1.0 / (1.0 + (-gl[0]).exp())
9694 });
9695 pairs.push((&se.gate_proj, &se.up_proj));
9696 downs.push(&se.down_proj);
9697 ws.push(g);
9698 }
9699 let inter = pairs[0].0.rows();
9700 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
9701 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
9702 return None;
9703 }
9704 let mut out = attention::take_buf(x.len());
9705 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
9706 attention::recycle_buf(&mut out);
9707 return None;
9708 }
9709 Some(out)
9710}
9711
9712fn moe_ffn_cpu(
9714 m: &MoeFfn,
9715 x: &[f32],
9716 idx: &[usize],
9717 p: &[f32],
9718 wsum: f32,
9719 pool: Option<&Pool>,
9720) -> Vec<f32> {
9721 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
9722 return out;
9723 }
9724 let mut out = attention::take_buf(x.len());
9725 for &e in idx {
9726 let mut eo = dense_ffn(&m.experts[e], x, pool);
9727 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
9728 for i in 0..out.len() {
9729 out[i] += w * eo[i];
9730 }
9731 attention::recycle_buf(&mut eo);
9732 }
9733 if let Some((se, gate)) = &m.shared {
9734 let mut so = dense_ffn(se, x, pool);
9735 let g = gate.as_ref().map_or(1.0, |gate| {
9736 let mut gl = [0.0f32; 1];
9737 gate.matvec(x, &mut gl, pool);
9738 1.0 / (1.0 + (-gl[0]).exp())
9739 });
9740 for i in 0..out.len() {
9741 out[i] += g * so[i];
9742 }
9743 attention::recycle_buf(&mut so);
9744 }
9745 out
9746}
9747
9748#[allow(clippy::too_many_arguments)]
9756fn mla_attention(
9757 w: &MlaWeights,
9758 normed: &[f32],
9759 cache: &mut crate::kv_cache::LayerKvCache,
9760 position: usize,
9761 inv_freq: &[f32],
9762 rope_scale: f32,
9763 eps: f64,
9764 pool: Option<&Pool>,
9765) -> Vec<f32> {
9766 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
9767 let hd = dr + dn;
9768 let mut q = vec![0.0f32; nh * hd];
9769 match (&w.q_a, &w.q_a_norm) {
9770 (Some(qa), Some(qn)) => {
9771 let mut t = vec![0.0f32; qa.rows()];
9772 qa.matvec(normed, &mut t, pool);
9773 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
9774 w.q_proj.matvec(&tn, &mut q, pool);
9775 }
9776 _ => w.q_proj.matvec(normed, &mut q, pool),
9777 }
9778 let mut ca = vec![0.0f32; lora + dr];
9779 w.kv_a.matvec(normed, &mut ca, pool);
9780 let (c_lat, k_rope) = ca.split_at_mut(lora);
9781 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
9782 let mut kvb = vec![0.0f32; nh * (dn + dv)];
9783 w.kv_b.matvec(&latn, &mut kvb, pool);
9784 if !w.nope {
9785 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
9786 }
9787 for h in 0..nh {
9788 if !w.nope {
9789 attention::rope_rotate_scaled(
9790 &mut q[h * hd..h * hd + dr],
9791 position,
9792 inv_freq,
9793 rope_scale,
9794 );
9795 }
9796 }
9797 let mut k = vec![0.0f32; nh * hd];
9798 let mut v = vec![0.0f32; nh * hd];
9799 for h in 0..nh {
9800 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
9801 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
9802 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
9803 }
9804 cache.append(&k, &v, &vec![true; nh]);
9805 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
9806 attention::recycle_buf(&mut imp);
9807 let mut ov = vec![0.0f32; nh * dv];
9808 for h in 0..nh {
9809 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
9810 }
9811 let mut out = vec![0.0f32; w.o_proj.rows()];
9812 w.o_proj.matvec(&ov, &mut out, pool);
9813 out
9814}
9815
9816fn dense_moe_ffn(
9823 dm: &DenseMoeFfn,
9824 x_normed: &[f32],
9825 h_raw: &[f32],
9826 eps: f64,
9827 norm_style: NormStyle,
9828 pool: Option<&Pool>,
9829) -> Vec<f32> {
9830 let mut d = dense_ffn(&dm.dense, x_normed, pool);
9831 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
9832 let m = &dm.moe;
9833 let ne = m.experts.len();
9834 let mut logits = vec![0.0f32; ne];
9835 if m.router_input_norm {
9836 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
9837 let inv = 1.0 / (ss + eps as f32).sqrt();
9838 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
9839 m.router.matvec(&xr, &mut logits, pool);
9840 } else {
9841 m.router.matvec(h_raw, &mut logits, pool);
9842 }
9843 let (idx, p, wsum) = moe_route(&logits, m, None);
9844 {
9845 let mut st = m.stats.borrow_mut();
9846 if st.len() < ne {
9847 st.resize(ne, 0);
9848 }
9849 for &e in &idx {
9850 st[e] += 1;
9851 }
9852 }
9853 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
9854 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
9855 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
9856 for (di, mi) in d.iter_mut().zip(&mo) {
9857 *di += mi;
9858 }
9859 d
9860}
9861
9862fn moe_gpu_refused(why: &'static str) {
9869 use std::sync::atomic::{AtomicBool, Ordering};
9870 static SAID: AtomicBool = AtomicBool::new(false);
9871 if !SAID.swap(true, Ordering::Relaxed) {
9872 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
9873 }
9874}
9875
9876fn moe_ffn_gpu(
9877 m: &MoeFfn,
9878 x: &[f32],
9879 idx: &[usize],
9880 p: &[f32],
9881 wsum: f32,
9882 pool: Option<&Pool>,
9883) -> Option<Vec<f32>> {
9884 use crate::gpu::MoeJob;
9885
9886 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
9887 let mut model_ref = None;
9888 for &e in idx {
9889 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
9890 moe_gpu_refused("push_job(expert)");
9891 return None;
9892 }
9893 }
9894 if let Some((se, gate)) = &m.shared {
9895 let g = gate.as_ref().map_or(1.0, |gate| {
9896 let mut gl = [0.0f32; 1];
9897 gate.matvec(x, &mut gl, pool);
9898 1.0 / (1.0 + (-gl[0]).exp())
9899 });
9900 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
9901 moe_gpu_refused("push_job(shared)");
9902 return None;
9903 }
9904 }
9905 let Some(model) = model_ref else {
9906 moe_gpu_refused("no model_ref");
9907 return None;
9908 };
9909 let hidden = jobs[0].down.1;
9910 let mut out = vec![0.0f32; hidden];
9911 if crate::gpu::moe_block(&model, &jobs, &mut out) {
9912 Some(out)
9913 } else {
9914 moe_gpu_refused("gpu::moe_block");
9915 None
9916 }
9917}
9918
9919fn ffn_forward(
9921 ffn: &FfnKind,
9922 x: &[f32],
9923 pool: Option<&Pool>,
9924 experts_allowed: Option<&[bool]>,
9925) -> Vec<f32> {
9926 match ffn {
9927 FfnKind::Dense(d) => dense_ffn(d, x, pool),
9928 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
9929 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
9933 }
9934}
9935
9936fn ffn_forward_pair(
9940 ffn: &FfnKind,
9941 x1: &[f32],
9942 x2: &[f32],
9943 pool: Option<&Pool>,
9944 experts_allowed: Option<&[bool]>,
9945) -> (Vec<f32>, Vec<f32>) {
9946 let d = match ffn {
9947 FfnKind::Dense(d) => d,
9948 FfnKind::Moe(m) => {
9949 return (
9950 moe_ffn(m, x1, pool, experts_allowed),
9951 moe_ffn(m, x2, pool, experts_allowed),
9952 );
9953 }
9954 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
9955 };
9956 let inter = d.gate_proj.rows();
9957 FFN_SCRATCH.with(|s| {
9958 let mut s = s.borrow_mut();
9959 let [g1, g2, u1, u2] = &mut *s;
9960 g1.resize(inter, 0.0);
9961 g2.resize(inter, 0.0);
9962 u1.resize(inter, 0.0);
9963 u2.resize(inter, 0.0);
9964 QTensor::matvec2_many(
9967 [&d.gate_proj, &d.up_proj],
9968 x1,
9969 x2,
9970 [g1.as_mut_slice(), u1.as_mut_slice()],
9971 [g2.as_mut_slice(), u2.as_mut_slice()],
9972 pool,
9973 );
9974 for i in 0..inter {
9975 g1[i] = d.act.combine(g1[i], u1[i]);
9976 g2[i] = d.act.combine(g2[i], u2[i]);
9977 }
9978 let mut o1 = attention::take_buf(d.down_proj.rows());
9979 let mut o2 = attention::take_buf(d.down_proj.rows());
9980 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
9981 (o1, o2)
9982 })
9983}
9984
9985#[cfg(test)]
9986mod tests {
9987
9988 #[test]
9989 fn cancel_flag_stops_generation() {
9990 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
9991 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
9994 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
9995 assert_eq!(r.finish_reason, "cancelled");
9996 assert!(
9997 r.token_ids.is_empty(),
9998 "no tokens after cancel: {:?}",
9999 r.token_ids
10000 );
10001 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
10003 assert_ne!(r2.finish_reason, "cancelled");
10004 }
10005 use super::*;
10006
10007 #[test]
10013 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
10014 let (hidden, inter) = (16usize, 40usize);
10015 let synth = |n: usize, salt: usize| -> Vec<f32> {
10016 (0..n)
10017 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
10018 .collect()
10019 };
10020 let d = DenseFfn {
10021 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
10022 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
10023 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
10024 act: Act::Silu,
10025 };
10026 let x = synth(hidden, 9);
10027 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
10029
10030 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
10031
10032 let mut g = vec![0.0f32; inter];
10034 d.gate_proj.matvec(&x, &mut g, None);
10035 let mut u = vec![0.0f32; inter];
10036 d.up_proj.matvec(&x, &mut u, None);
10037 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
10038 for i in 0..inter {
10039 g[i] = if act_set.contains(&(i as u16)) {
10040 inference::silu(g[i]) * u[i]
10041 } else {
10042 0.0
10043 };
10044 }
10045 let mut reference = vec![0.0f32; hidden];
10046 d.down_proj.matvec(&g, &mut reference, None);
10047
10048 let max_d = sparse
10049 .iter()
10050 .zip(&reference)
10051 .map(|(a, b)| (a - b).abs())
10052 .fold(0.0f32, f32::max);
10053 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
10054 }
10055
10056 fn attach_test_mtp(p: &mut Pipeline) {
10058 let (h, inter, heads, kv, hd) = (
10059 p.hidden_size,
10060 p.intermediate_size,
10061 p.num_heads,
10062 p.num_kv_heads,
10063 p.head_dim,
10064 );
10065 let synth = |n: usize, salt: usize| -> Vec<f32> {
10066 (0..n)
10067 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
10068 .collect()
10069 };
10070 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
10071 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
10072 };
10073 p.mtp = Some(MtpModule {
10074 enorm: vec![1.0; h],
10075 hnorm: vec![1.0; h],
10076 eh_proj: qt(h, 2 * h, 301),
10077 layer: LayerWeights {
10078 input_norm: vec![1.0; h],
10079 post_norm: vec![1.0; h],
10080 attn_out_norm: None,
10081 ffn_out_norm: None,
10082 layer_scale: None,
10083 ffn: FfnKind::Dense(DenseFfn {
10084 gate_proj: qt(inter, h, 315),
10085 up_proj: qt(inter, h, 316),
10086 down_proj: qt(h, inter, 317),
10087 act: Act::Silu,
10088 }),
10089 attn: AttnKind::Full {
10090 bias: None,
10091 wq: qt(heads * hd, h, 311),
10092 wk: qt(kv * hd, h, 312),
10093 wv: qt(kv * hd, h, 313),
10094 wo: qt(h, heads * hd, 314),
10095 q_norm: None,
10096 k_norm: None,
10097 output_gate: false,
10098 softplus_gate: None,
10099 },
10100 },
10101 final_norm: vec![1.0; h],
10102 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
10103 });
10104 }
10105
10106 #[test]
10107 fn speculative_equals_vanilla_greedy() {
10108 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
10112 let run = |spec: bool| {
10113 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10114 p.sampler_config.temperature = 0.0;
10115 attach_test_mtp(&mut p);
10116 p.speculative = spec;
10117 let r = p.generate("abcdef", 12, None, None).unwrap();
10118 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
10119 };
10120 let (vanilla, d0, _) = run(false);
10121 let (spec, d1, a1) = run(true);
10122 assert_eq!(d0, 0, "vanilla path must not draft");
10123 assert!(d1 > 0, "speculative path must draft");
10124 assert_eq!(
10125 vanilla, spec,
10126 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
10127 );
10128 }
10129
10130 #[test]
10131 fn speculative_accepts_constant_oracle() {
10132 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
10134 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10135 p.sampler_config.temperature = 0.0;
10136 p.sampler_config.repetition_penalty = 1.0;
10137 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
10140 attach_test_mtp(&mut p);
10141 p.speculative = true;
10142 let r = p.generate("abcd", 10, None, None).unwrap();
10143 assert!(r.mtp_drafted > 0);
10144 assert_eq!(
10145 r.mtp_accepted, r.mtp_drafted,
10146 "constant logits → every draft accepted"
10147 );
10148 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
10151 }
10152
10153 #[test]
10154 fn empty_prompt_is_an_error_not_a_panic() {
10155 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
10156 let r = p.generate("", 4, None, None);
10157 assert!(r.is_err(), "empty prompt must be a clean error");
10158 }
10159
10160 #[test]
10161 fn every_token_enters_kv_exactly_once() {
10162 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10163 p.sampler_config.temperature = 0.0;
10165 let r = p.generate("abc", 2, None, None).unwrap();
10166 assert_eq!(r.prompt_tokens, 3);
10167 assert_eq!(
10171 p.kv_cache.seq_len(),
10172 3 + r.tokens_generated - 1,
10173 "each token must be cached exactly once (v1 cached the last prompt token twice)"
10174 );
10175 }
10176
10177 #[test]
10178 fn generation_is_reproducible_with_seed() {
10179 let run = || {
10180 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10181 p.generate("hello", 8, None, None).unwrap().token_ids
10182 };
10183 assert_eq!(run(), run());
10184 }
10185
10186 #[test]
10187 fn resetting_sampler_restarts_the_seeded_stream() {
10188 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
10189 let config = SamplerConfig {
10190 seed: Some(1234),
10191 ..SamplerConfig::default()
10192 };
10193 p.set_sampler_config(config.clone());
10194 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
10195 p.set_sampler_config(config);
10196 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
10197 assert_eq!(first, second);
10198 }
10199
10200 #[test]
10201 fn eviction_bounds_the_cache() {
10202 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
10203 p.kv_cache.max_seq_len = 6;
10204 p.sampler_config.temperature = 0.0;
10205 let _ = p.generate("abcd", 12, None, None).unwrap();
10206 assert!(
10207 p.kv_cache.seq_len() <= 6 + 1,
10208 "cache must stay bounded by max_seq_len (got {})",
10209 p.kv_cache.seq_len()
10210 );
10211 }
10212
10213 #[test]
10214 fn confidence_matches_tokens_and_is_a_probability() {
10215 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10216 p.sampler_config.temperature = 0.0;
10217 p.sampler_config.repetition_penalty = 1.0;
10218 let r = p.generate("abcd", 10, None, None).unwrap();
10219 assert_eq!(
10220 r.token_confidence.len(),
10221 r.token_ids.len(),
10222 "one confidence per emitted token"
10223 );
10224 for &c in &r.token_confidence {
10225 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
10226 }
10227 let logits = [1.0f32, 3.0, 0.5, 3.0];
10229 let p0 = top1_prob_t(&logits, 1, 1.0);
10230 let p1 = top1_prob_t(&logits, 3, 1.0);
10231 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
10232 assert!(p0 > 0.0 && p0 < 1.0);
10233 let sharp = top1_prob_t(&logits, 1, 1.0);
10235 let soft = top1_prob_t(&logits, 1, 2.0);
10236 assert!(soft < sharp, "higher temperature lowers peak confidence");
10237 }
10238
10239 #[test]
10240 fn trace_is_opt_in_and_parallels_the_output() {
10241 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10243 p.sampler_config.temperature = 0.0;
10244 p.sampler_config.repetition_penalty = 1.0;
10245 let r = p.generate("abcd", 10, None, None).unwrap();
10246 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
10247
10248 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10250 p.sampler_config.temperature = 0.0;
10251 p.sampler_config.repetition_penalty = 1.0;
10252 p.set_trace(true);
10253 let r = p.generate("abcd", 10, None, None).unwrap();
10254 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
10255 for (i, tr) in r.traces.iter().enumerate() {
10256 assert_eq!(tr.t, i, "trace index is sequential");
10257 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
10258 assert_eq!(
10259 tr.confidence, r.token_confidence[i],
10260 "trace confidence matches the confidence channel"
10261 );
10262 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
10264 }
10265 }
10266
10267 #[test]
10268 fn explain_prefill_logits_match_greedy_first_token() {
10269 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
10273 p.sampler_config.temperature = 0.0;
10274 p.sampler_config.repetition_penalty = 1.0;
10275 let ids = p.tokenizer.encode("abcd");
10276 let logits = p.prefill_next_logits(&ids, None);
10277 let argmax = logits
10278 .iter()
10279 .enumerate()
10280 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10281 .unwrap()
10282 .0 as u32;
10283 let r = p.generate("abcd", 1, None, None).unwrap();
10284 assert_eq!(
10285 argmax, r.token_ids[0],
10286 "explain preview must match greedy emit"
10287 );
10288 }
10289
10290 #[test]
10291 fn laguna_shared_expert_is_unconditionally_added() {
10292 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
10293 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
10294 let zero_dense = || DenseFfn {
10295 gate_proj: matrix(vec![0.0; 4]),
10296 up_proj: matrix(vec![0.0; 4]),
10297 down_proj: matrix(vec![0.0; 4]),
10298 act: Act::Silu,
10299 };
10300 let shared = DenseFfn {
10301 gate_proj: identity(),
10302 up_proj: identity(),
10303 down_proj: identity(),
10304 act: Act::Silu,
10305 };
10306 let x = [1.0, 2.0];
10307 let expected = dense_ffn(&shared, &x, None);
10308 let moe = MoeFfn {
10309 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
10310 experts: vec![zero_dense()],
10311 top_k: 1,
10312 norm_topk_prob: true,
10313 router_sigmoid: true,
10314 expert_bias: None,
10315 routed_scaling: 1.0,
10316 route_tau: None,
10317 shared: Some((shared, None)),
10318 stats: std::cell::RefCell::new(Vec::new()),
10319 act_sq: std::cell::RefCell::new(Vec::new()),
10320 act_rows: std::cell::RefCell::new(Vec::new()),
10321 mask: None,
10322 per_expert_scale: None,
10323 router_input_norm: false,
10324 };
10325 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
10326 for (actual, expected) in actual.iter().zip(expected) {
10327 assert!((actual - expected).abs() < 1e-6);
10328 }
10329 }
10330}