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 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
170 ws: ForwardScratch,
174 pool: Option<std::sync::Arc<Pool>>,
176 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
180 pub(crate) dyn_force_f32: bool,
182 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
187 pub(crate) dyn_active: Option<usize>,
193 pub(crate) dyn_blend_loaded: bool,
197 pub(crate) dyn_phi_layer: Option<usize>,
200 dyn_phi_ema: Vec<f32>,
202 dyn_phi_seen: usize,
203 pub dyn_router: Option<crate::swarm::DynRouter>,
206 o1_cfg: Option<crate::nystrom::O1Cfg>,
209 o1_epoch: u64,
212 o1_flags: Vec<bool>,
214 trace: bool,
217 calib_temp: f32,
220 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
222 graph_kv_id: u64,
223 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
226 graph_want_logits: bool,
227 graph_logits: Option<Vec<f32>>,
230 pub embed_multiplier: f32,
232 pub attn_scale: f32,
235 pub swa: Option<(usize, usize)>,
238 pub sliding_layers: Option<Vec<bool>>,
241 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
244 pub rotary_dim_local: Option<usize>,
245 pub rope_scale: f32,
246 pub rope_scale_local: f32,
247 pub global_attn: Option<(usize, usize)>,
250 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
253 pub attn_v_norm: bool,
255 pub final_softcap: Option<f32>,
257 pub attn_softcap: f32,
259 confidence_on: bool,
263}
264
265#[cfg(target_os = "macos")]
266impl Drop for Pipeline {
267 fn drop(&mut self) {
268 crate::gpu::kv_mirror_drop(self.graph_kv_id);
269 }
270}
271
272pub struct PipelineWeights {
277 pub embed_tokens: QTensor,
279 pub layers: Vec<LayerWeights>,
281 pub lm_head: QTensor,
283 pub final_norm: Vec<f32>,
285}
286
287pub struct LayerWeights {
289 pub input_norm: Vec<f32>,
290 pub post_norm: Vec<f32>,
293 pub attn_out_norm: Option<Vec<f32>>,
296 pub layer_scale: Option<f32>,
298 pub ffn_out_norm: Option<Vec<f32>>,
301 pub ffn: FfnKind,
302 pub attn: AttnKind,
303}
304
305#[derive(Clone, Copy, PartialEq, Debug, Default)]
308pub enum Act {
309 #[default]
310 Silu,
311 GeluTanh,
312 Situ {
315 beta: f32,
316 linear_beta: f32,
317 },
318}
319
320impl Act {
321 pub fn from_arch(name: &str) -> Self {
322 if name == "gelu_tanh" {
323 Self::GeluTanh
324 } else {
325 Self::Silu
326 }
327 }
328
329 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
331 match arch.hidden_act.as_str() {
332 "situ" => Self::Situ {
333 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
334 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
335 },
336 other => Self::from_arch(other),
337 }
338 }
339
340 #[inline]
341 pub fn apply(self, x: f32) -> f32 {
342 match self {
343 Self::Silu => inference::silu(x),
344 Self::GeluTanh => inference::gelu_tanh(x),
345 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
346 }
347 }
348
349 #[inline]
352 pub fn combine(self, g: f32, u: f32) -> f32 {
353 match self {
354 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
355 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
356 }
357 _ => self.apply(g) * u,
358 }
359 }
360}
361
362pub struct DenseFfn {
364 pub gate_proj: QTensor,
365 pub up_proj: QTensor,
366 pub down_proj: QTensor,
367 pub act: Act,
369}
370
371pub enum FfnKind {
374 Dense(DenseFfn),
375 Moe(MoeFfn),
379 DenseMoe(Box<DenseMoeFfn>),
386}
387
388pub struct DenseMoeFfn {
390 pub dense: DenseFfn,
391 pub moe: MoeFfn,
392 pub post_norm_1: Vec<f32>,
394 pub pre_norm_2: Vec<f32>,
397 pub post_norm_2: Vec<f32>,
399}
400
401pub struct MoeFfn {
402 pub router: QTensor,
404 pub experts: Vec<DenseFfn>,
405 pub top_k: usize,
406 pub norm_topk_prob: bool,
407 pub router_sigmoid: bool,
410 pub expert_bias: Option<Vec<f32>>,
414 pub routed_scaling: f32,
417 pub route_tau: Option<f32>,
423 pub shared: Option<(DenseFfn, Option<QTensor>)>,
426 pub stats: std::cell::RefCell<Vec<u64>>,
430 pub act_sq: std::cell::RefCell<Vec<f64>>,
437 pub act_rows: std::cell::RefCell<Vec<f32>>,
443 pub mask: Option<Vec<bool>>,
448 pub per_expert_scale: Option<Vec<f32>>,
451 pub router_input_norm: bool,
455}
456
457pub enum AttnKind {
460 Full {
462 wq: QTensor,
463 wk: QTensor,
464 wv: QTensor,
465 wo: QTensor,
466 q_norm: Option<Vec<f32>>,
467 k_norm: Option<Vec<f32>>,
468 output_gate: bool,
469 softplus_gate: Option<(QTensor, bool)>,
473 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
475 },
476 Linear(VmfPhaseWeights),
478 LinearGdn(GdnWeights),
480 ShortConv(ShortConvWeights),
483 Mla(Box<MlaWeights>),
491 Kda(Box<crate::linear_core::KdaWeights>),
495}
496
497pub struct MlaWeights {
499 pub q_proj: QTensor,
503 pub q_a: Option<QTensor>,
506 pub q_a_norm: Option<Vec<f32>>,
507 pub kv_a: QTensor,
509 pub kv_a_norm: Vec<f32>,
511 pub kv_b: QTensor,
513 pub o_proj: QTensor,
515 pub nh: usize,
516 pub qk_rope: usize,
517 pub qk_nope: usize,
518 pub v_dim: usize,
519 pub lora: usize,
520 pub scale: f32,
522 pub nope: bool,
524}
525
526pub struct MtpModule {
531 pub enorm: Vec<f32>,
532 pub hnorm: Vec<f32>,
533 pub eh_proj: QTensor,
535 pub layer: LayerWeights,
536 pub final_norm: Vec<f32>,
537 pub kv: crate::kv_cache::LayerKvCache,
538}
539
540#[derive(Clone, Copy)]
544enum SpecTrial {
545 Spec {
546 t0: std::time::Instant,
547 gen0: usize,
548 rounds: usize,
549 },
550 Plain {
551 t0: std::time::Instant,
552 gen0: usize,
553 },
554 Decided {
555 spec: bool,
556 recheck_at: usize,
557 },
558}
559
560#[derive(Default, Clone, Copy)]
571struct SpecMon {
572 round_ms: f64,
573 tokens: f64,
574 plain_ms: f64,
575 n: u32,
576 fails: u32,
577}
578
579impl SpecMon {
580 fn round(&mut self, dt_ms: f64, produced: usize) {
581 self.n += 1;
582 if self.n == 1 {
583 return; }
585 let a = if self.n == 2 { 1.0 } else { 0.3 };
586 self.round_ms += a * (dt_ms - self.round_ms);
587 self.tokens += a * (produced as f64 - self.tokens);
588 }
589 fn pays(&self) -> bool {
590 self.plain_ms > 0.0 && self.tokens * self.plain_ms > self.round_ms * 1.03
591 }
592}
593
594pub struct GenerateResult {
596 pub text: String,
597 pub token_ids: Vec<u32>,
598 pub prompt_tokens: usize,
599 pub tokens_generated: usize,
600 pub finish_reason: String,
601 pub mtp_drafted: usize,
603 pub mtp_accepted: usize,
604 pub token_confidence: Vec<f32>,
609 pub traces: Vec<TokenTrace>,
612}
613
614#[derive(Clone, Debug)]
619pub struct TokenTrace {
620 pub t: usize,
622 pub token_id: u32,
624 pub confidence: f32,
626 pub active_skill: Option<String>,
628 pub recon: Option<f32>,
632 pub switched: bool,
635}
636
637#[cfg_attr(not(test), allow(dead_code))]
642fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
643 let t = if temp > 1e-3 { temp } else { 1.0 };
644 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
645 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
646 if sum > 0.0 {
647 (((logits[id as usize] - max) / t).exp()) / sum
648 } else {
649 0.0
650 }
651}
652
653fn prefill_batched() -> bool {
656 std::env::var("CMF_PREFILL")
657 .map(|v| v != "seq")
658 .unwrap_or(true)
659}
660
661#[derive(Clone, Copy)]
665enum PrefillIn<'a> {
666 Ids(&'a [u32]),
667 Hidden(&'a [f32]),
668}
669
670impl Pipeline {
677 fn can_prefill_batched(&self) -> bool {
678 prefill_batched() && !self.weights.layers.is_empty()
679 }
680}
681
682pub fn prefill_chunk() -> usize {
689 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
690 .ok()
691 .and_then(|v| v.parse::<usize>().ok())
692 {
693 return n.max(1);
694 }
695 if cfg!(target_os = "macos") {
696 512
697 } else if cfg!(target_arch = "aarch64") {
698 256
701 } else {
702 48
703 }
704}
705
706pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
708
709impl Pipeline {
710 #[inline]
714 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
715 virtual_idx % self.physical_layers
716 }
717
718 #[inline]
721 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
722 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
723 }
724
725 #[allow(clippy::too_many_arguments)]
727
728 #[cfg(target_os = "macos")]
747 fn graph_prefill_preferred(&self) -> bool {
748 if !crate::gpu::enabled_here()
749 || !crate::gpu::q1_force()
750 || std::env::var("CMF_GPU_BLOCK")
751 .map(|v| v == "0")
752 .unwrap_or(false)
753 {
754 return false;
755 }
756 self.weights
757 .layers
758 .iter()
759 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
760 }
761
762 #[cfg(not(target_os = "macos"))]
763 fn graph_prefill_preferred(&self) -> bool {
764 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
772 if !graph_on || !crate::gpu::enabled_here() {
773 return false;
774 }
775 if self.o1_active() {
784 return false;
785 }
786 self.weights
787 .layers
788 .iter()
789 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
790 }
791
792 #[cfg(target_os = "macos")]
793 fn q1_graph_gpu(
794 &mut self,
795 start: usize,
796 upto: Option<usize>,
797 position: usize,
798 h: &mut [f32],
799 ) -> usize {
800 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
802 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
804 || !crate::gpu::q1_force()
805 || std::env::var("CMF_GPU_BLOCK")
806 .map(|v| v == "0")
807 .unwrap_or(false)
808 {
809 if std::env::var("CMF_GRAPH_DBG").is_ok() {
810 eprintln!(
811 "block-graph: front gate (softcap={} enabled_here={} q1_force={})",
812 self.attn_softcap > 0.0,
813 crate::gpu::enabled_here(),
814 crate::gpu::q1_force(),
815 );
816 }
817 return start;
818 }
819 if self.swa.is_some()
824 || self.global_attn.is_some()
825 || self.attention_heads_per_layer.is_some()
826 || self.attn_v_norm
827 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
828 || self.weights.layers.iter().any(|lw| {
829 lw.attn_out_norm.is_some()
830 || lw.ffn_out_norm.is_some()
831 || lw.layer_scale.is_some()
832 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
833 })
834 {
835 if std::env::var("CMF_GRAPH_DBG").is_ok() {
836 eprintln!(
837 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
838 self.swa.is_some(),
839 self.global_attn.is_some(),
840 self.attention_heads_per_layer.is_some(),
841 self.attn_v_norm,
842 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
843 );
844 }
845 return start;
846 }
847 let limit = upto
850 .map(|u| u + 1)
851 .unwrap_or(self.num_layers)
852 .min(self.num_layers);
853
854 enum Item<'a> {
855 Gdn {
856 run: Vec<GdnGpuLayer<'a>>,
857 first: usize,
858 },
859 Attn {
860 l: AttnGpuLayer<'a>,
861 li: usize,
862 q_norm: Option<&'a [f32]>,
863 k_norm: Option<&'a [f32]>,
864 output_gate: bool,
865 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
866 full_gpu: bool,
869 },
870 }
871
872 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
879 let attend_contract = attend_mode != "0"
880 && attend_mode != "off"
881 && self.head_dim % 4 == 0
882 && self.head_dim <= 256
883 && self.rotary_dim >= 2
884 && self.rotary_dim <= self.head_dim
885 && (self.rotary_dim / 2) % 32 == 0
886 && self.num_kv_heads > 0
887 && self.num_heads % self.num_kv_heads == 0;
888
889 let mut plan: Vec<Item> = Vec::new();
890 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
891 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
893 let mut scan = start;
894 while scan < limit {
895 let lw = &self.weights.layers[self.phys_layer(scan)];
896 let ffn = match &lw.ffn {
897 FfnKind::Dense(d) => {
898 let (Some(g), Some(u), Some(dn)) = (
899 d.gate_proj.q1_parts(),
900 d.up_proj.q1_parts(),
901 d.down_proj.q1_parts(),
902 ) else {
903 if block_diag {
904 eprintln!(
905 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
906 );
907 }
908 break;
909 };
910 MetalFfn::Dense {
911 gate: g,
912 up: u,
913 down: dn,
914 }
915 }
916 FfnKind::Moe(m) => {
917 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
918 if block_diag {
919 eprintln!(
920 "block-graph: L{scan} MoE outside the graph contract — run ends"
921 );
922 }
923 break;
924 };
925 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
926 model_ref.get_or_insert_with(|| model.clone());
927 }
928 MetalFfn::Moe(moe)
929 }
930 _ => {
931 if block_diag {
932 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
933 }
934 break;
935 }
936 };
937 match &lw.attn {
938 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
939 let parts = (
940 w.in_proj_qkv.q1_parts(),
941 w.in_proj_z.q1_parts(),
942 w.in_proj_a.f32_parts(),
943 w.in_proj_b.f32_parts(),
944 w.out_proj.q1_parts(),
945 );
946 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
947 if block_diag {
948 eprintln!(
949 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
950 w.in_proj_qkv.q1_parts().is_some(),
951 w.in_proj_z.q1_parts().is_some(),
952 w.in_proj_a.f32_parts().is_some(),
953 w.in_proj_b.f32_parts().is_some(),
954 w.out_proj.q1_parts().is_some(),
955 );
956 }
957 break;
958 };
959 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
960 model_ref.get_or_insert_with(|| model.clone());
961 }
962 let gl = GdnGpuLayer {
963 attn_norm: &lw.input_norm,
964 post_norm: &lw.post_norm,
965 qkv,
966 z,
967 a,
968 b,
969 out,
970 ffn,
971 conv1d: &w.conv1d,
972 a_log: &w.a_log,
973 dt_bias: &w.dt_bias,
974 gnorm: &w.norm,
975 };
976 match plan.last_mut() {
977 Some(Item::Gdn { run, .. }) => run.push(gl),
978 _ => plan.push(Item::Gdn {
979 run: vec![gl],
980 first: scan,
981 }),
982 }
983 }
984 AttnKind::Full {
985 wq,
986 wk,
987 wv,
988 wo,
989 q_norm,
990 k_norm,
991 output_gate,
992 softplus_gate: None,
993 bias,
994 } if !self.kv_cache.layers[scan].o1_sealed()
995 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1") =>
1000 {
1001 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
1002 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
1003 break;
1004 };
1005 if let QTensor::Mapped { model, .. } = wq {
1006 model_ref.get_or_insert_with(|| model.clone());
1007 }
1008 let cache = &self.kv_cache.layers[scan];
1009 let o1_metal = cache.o1.is_some()
1013 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
1014 && cache.o1_views().is_some();
1015 let full_gpu = attend_contract
1016 && cache.mode == crate::kv_cache::KvMode::F32
1017 && (cache.o1.is_none() || o1_metal)
1018 && bias.is_none()
1019 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
1020 && pk.1 == self.num_kv_heads * self.head_dim
1021 && pv.1 == self.num_kv_heads * self.head_dim
1022 && po.2 == self.num_heads * self.head_dim;
1023 plan.push(Item::Attn {
1024 l: AttnGpuLayer {
1025 attn_norm: &lw.input_norm,
1026 post_norm: &lw.post_norm,
1027 wq: pq,
1028 wk: pk,
1029 wv: pv,
1030 wo: po,
1031 ffn,
1032 },
1033 li: scan,
1034 q_norm: q_norm.as_deref(),
1035 k_norm: k_norm.as_deref(),
1036 output_gate: *output_gate,
1037 bias: bias
1038 .as_ref()
1039 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1040 full_gpu,
1041 });
1042 }
1043 _ => break,
1044 }
1045 scan += 1;
1046 }
1047 let Some(model) = model_ref else {
1048 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1049 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
1050 }
1051 return start;
1052 };
1053 if plan.is_empty() {
1054 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1055 eprintln!("q1-graph: empty plan at layer {start}");
1056 }
1057 return start;
1058 }
1059 let has_moe = plan.iter().any(|it| match it {
1060 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
1061 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
1062 });
1063 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
1064 let dev_attend = attend_contract
1065 && (self.head_dim <= 128
1066 || has_moe
1067 || (self.head_dim <= 256 && has_gdn)
1073 || attend_mode == "force"
1074 || attend_mode == "256");
1075 if !dev_attend {
1076 for it in &mut plan {
1077 if let Item::Attn { li, full_gpu, .. } = it {
1078 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
1081 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
1082 if !keep_o1 {
1083 *full_gpu = false;
1084 }
1085 }
1086 }
1087 }
1088 if std::env::var("CMF_GRAPH_DBG").is_ok() {
1089 use std::sync::atomic::{AtomicBool, Ordering};
1090 static SAID: AtomicBool = AtomicBool::new(false);
1091 if !SAID.swap(true, Ordering::Relaxed) {
1092 let fg = plan
1093 .iter()
1094 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
1095 .count();
1096 let att = plan
1097 .iter()
1098 .filter(|it| matches!(it, Item::Attn { .. }))
1099 .count();
1100 eprintln!(
1101 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
1102 plan.len(),
1103 self.head_dim,
1104 self.rotary_dim,
1105 self.num_kv_heads,
1106 self.num_heads,
1107 );
1108 }
1109 }
1110 let dims = GraphDims {
1111 hidden: self.hidden_size,
1112 eps: self.rms_eps as f32,
1113 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1114 };
1115 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
1116 return start;
1117 };
1118 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
1119 nv: cfg.num_v_heads,
1120 nk: cfg.num_k_heads,
1121 dk: cfg.key_head_dim,
1122 dv: cfg.value_head_dim,
1123 kk: cfg.conv_kernel,
1124 hidden: self.hidden_size,
1125 inter: self.intermediate_size,
1126 c_dim: cfg.conv_dim(),
1127 eps: cfg.rms_eps as f32,
1128 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
1129 });
1130 let mut valid = 0usize;
1134 let mut end = start;
1135 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
1137 static ONCE: std::sync::Once = std::sync::Once::new();
1138 ONCE.call_once(|| {
1139 for it in &plan {
1140 match it {
1141 Item::Gdn { first, run } => {
1142 eprintln!("plan: Gdn first={first} len={}", run.len())
1143 }
1144 Item::Attn { li, full_gpu, .. } => {
1145 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
1146 }
1147 }
1148 }
1149 });
1150 }
1151 for item in &plan {
1152 let ok = match item {
1153 Item::Gdn { run, .. } => gcfg
1154 .as_ref()
1155 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
1156 .unwrap_or(false),
1157 Item::Attn { l, .. } => graph.attn_ok(l),
1158 };
1159 if !ok {
1160 if block_diag {
1161 eprintln!(
1162 "block-graph: plan item {} ({}) failed graph preflight",
1163 valid,
1164 match item {
1165 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
1166 Item::Attn { li, .. } => format!("Attn L{li}"),
1167 }
1168 );
1169 }
1170 break;
1171 }
1172 valid += 1;
1173 end += match item {
1174 Item::Gdn { run, .. } => run.len(),
1175 Item::Attn { .. } => 1,
1176 };
1177 }
1178 plan.truncate(valid);
1179 if plan.is_empty() {
1180 return start;
1181 }
1182
1183 let inv_freq = self.inv_freq.clone();
1184 let pool = self.pool.clone();
1185 let (nh, nkv, hd, hs, rd, eps) = (
1186 self.num_heads,
1187 self.num_kv_heads,
1188 self.head_dim,
1189 self.hidden_size,
1190 self.rotary_dim,
1191 self.rms_eps,
1192 );
1193 let norm_style = self.norm_style;
1194 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
1195 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
1196 let kv_id = self.graph_kv_id;
1197 let mut pending: Vec<(usize, usize)> = Vec::new();
1200 let mut dev_attn: Vec<usize> = Vec::new();
1203 for item in &plan {
1204 let _xt0 = std::time::Instant::now();
1205 let _xkind: u32 = match item {
1206 Item::Gdn { .. } => 2,
1207 Item::Attn { .. } => 3,
1208 };
1209 if self.loop_final_norm {
1211 let item_start = match item {
1212 Item::Gdn { first, .. } => *first,
1213 Item::Attn { li, .. } => *li,
1214 };
1215 if item_start > start && self.is_loop_end(item_start - 1) {
1216 graph.encode_loop_norm(&self.weights.final_norm);
1217 }
1218 }
1219 match item {
1220 Item::Gdn { run, first } => {
1221 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
1222 if l.linear_state.len() != want {
1223 l.linear_state = vec![0f32; want];
1224 }
1225 }
1226 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
1227 .iter()
1228 .map(|l| l.linear_state.as_slice())
1229 .collect();
1230 let _ig = std::time::Instant::now();
1231 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1232 tracing::error!("q1 graph: GDN run refused after validation");
1234 return start;
1235 }
1236 graph.commit_kind = 2;
1239 graph.commit();
1240 crate::gpu::stageprof(0, _ig.elapsed());
1241 pending.push((*first, run.len()));
1242 }
1243 Item::Attn {
1244 l,
1245 li,
1246 q_norm,
1247 k_norm,
1248 output_gate,
1249 bias,
1250 full_gpu,
1251 } => {
1252 let _ia = std::time::Instant::now();
1253 if *full_gpu {
1255 let cache = &self.kv_cache.layers[*li];
1256 let o1p = if cache.o1.is_some() {
1257 match cache.o1_views() {
1258 Some(views) => Some(crate::gpu::O1AttnParams {
1259 views,
1260 epoch: self.o1_epoch,
1261 }),
1262 None => None,
1264 }
1265 } else {
1266 None
1267 };
1268 let o1_layer = cache.o1.is_some();
1269 if o1_layer && o1p.is_none() {
1270 }
1272 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1273 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1274 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
1275 let p = crate::gpu::AttnDeviceParams {
1276 kv_id,
1277 layer: *li,
1278 nh,
1279 nkv,
1280 hd,
1281 rd,
1282 position,
1283 eps: eps as f32,
1284 gemma,
1285 output_gate: *output_gate,
1286 q_norm: *q_norm,
1287 k_norm: *k_norm,
1288 inv_freq: &inv_freq,
1289 cpu_k,
1290 cpu_v,
1291 cpu_stored,
1292 o1: o1p,
1293 };
1294 let o1_bad = o1_layer && p.o1.is_none();
1295 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
1296 {
1297 if p.o1.is_none() {
1299 dev_attn.push(*li);
1300 }
1301 graph.commit_kind = 3;
1302 graph.commit();
1303 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1307 continue;
1308 }
1309 }
1311 graph.encode_attn_prefix(l);
1312 graph.sync();
1313 if !pending.is_empty() {
1314 let idxs: Vec<usize> =
1315 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1316 let mut outs: Vec<&mut [f32]> = self
1317 .kv_cache
1318 .layers
1319 .iter_mut()
1320 .enumerate()
1321 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1322 .map(|(_, s)| s.linear_state.as_mut_slice())
1323 .collect();
1324 graph.read_states(&mut outs);
1325 }
1326 let mut q_raw = attention::take_buf(l.wq.1);
1327 let mut k = attention::take_buf(l.wk.1);
1328 let mut v = attention::take_buf(l.wv.1);
1329 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1330 let cfg = QwenAttnCfg {
1331 num_heads: nh,
1332 num_kv_heads: nkv,
1333 head_dim: hd,
1334 hidden_size: hs,
1335 position,
1336 inv_freq: &inv_freq,
1337 rotary_dim: rd,
1338 scale: self.attn_scale,
1339 softcap: self.attn_softcap,
1340 window: None,
1341 v_norm: false,
1342 q_norm: *q_norm,
1343 k_norm: *k_norm,
1344 output_gate: *output_gate,
1345 softplus_gate: None,
1346 rope_scale: 1.0,
1347 bias: *bias,
1348 rms_eps: eps,
1349 norm_style,
1350 pool: pool.as_deref(),
1351 };
1352 let mut ao = attention::qwen_attention_core(
1353 q_raw,
1354 k,
1355 v,
1356 &mut self.kv_cache.layers[*li],
1357 &cfg,
1358 );
1359 graph.encode_attn_suffix(l, &ao);
1360 graph.commit();
1363 attention::recycle_buf(&mut ao);
1364 }
1365 }
1366
1367 crate::gpu::stageprof(_xkind, _xt0.elapsed());
1368 }
1369 let mut lm_rows = None;
1374 if self.graph_want_logits
1375 && upto.is_none()
1376 && end == self.num_layers
1377 && std::env::var("CMF_GPU_LMHEAD")
1378 .map(|v| v != "0")
1379 .unwrap_or(true)
1380 {
1381 if let Some(lm) = self.weights.lm_head.q1_parts() {
1382 if graph.lm_head_ok(lm) {
1383 graph.encode_lm_head(&self.weights.final_norm, lm);
1384 lm_rows = Some(lm.1);
1385 }
1386 }
1387 }
1388 let _sy0 = std::time::Instant::now();
1389 graph.sync();
1390 let _rs0 = std::time::Instant::now();
1391 if !pending.is_empty() {
1392 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1393 let mut outs: Vec<&mut [f32]> = self
1394 .kv_cache
1395 .layers
1396 .iter_mut()
1397 .enumerate()
1398 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1399 .map(|(_, s)| s.linear_state.as_mut_slice())
1400 .collect();
1401 graph.read_states(&mut outs);
1402 }
1403 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
1404 use std::sync::atomic::{AtomicU64, Ordering};
1405 static SY: AtomicU64 = AtomicU64::new(0);
1406 static RS: AtomicU64 = AtomicU64::new(0);
1407 static N: AtomicU64 = AtomicU64::new(0);
1408 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
1409 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
1410 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
1411 if n % 100 == 0 {
1412 eprintln!(
1413 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
1414 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
1415 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
1416 );
1417 }
1418 }
1419 if let Some(rows) = lm_rows {
1420 crate::gpu::hostprof_encode_done(_mt0);
1421 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1422 graph.read_logits(&mut lg);
1423 crate::gpu::hostprof_total(_mt0);
1424 lg.resize(self.vocab_size, 0.0);
1425 if let Some(c) = self.final_softcap {
1426 for l in lg.iter_mut() {
1427 *l = c * (*l / c).tanh();
1428 }
1429 }
1430 self.graph_logits = Some(lg);
1431 }
1432 graph.finish(h);
1433 for li in dev_attn {
1437 let mut krow = attention::take_buf(nkv * hd);
1438 let mut vrow = attention::take_buf(nkv * hd);
1439 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1440 let cache = &mut self.kv_cache.layers[li];
1441 cache.append(&krow, &vrow, &[]);
1442 let n = cache.seq_len;
1443 let mut imp = attention::take_buf(n);
1444 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1445 cache.accumulate_imp(&imp);
1446 attention::recycle_buf(&mut imp);
1447 }
1448 attention::recycle_buf(&mut krow);
1449 attention::recycle_buf(&mut vrow);
1450 }
1451 end
1452 }
1453
1454 pub fn new(
1455 tokenizer: Tokenizer,
1456 weights: PipelineWeights,
1457 hidden_size: usize,
1458 intermediate_size: usize,
1459 num_heads: usize,
1460 num_kv_heads: usize,
1461 head_dim: usize,
1462 num_layers: usize,
1463 physical_layers: usize,
1464 loop_final_norm: bool,
1465 vocab_size: usize,
1466 rms_eps: f64,
1467 rope_base: f32,
1468 norm_style: NormStyle,
1469 max_seq_len: usize,
1470 sampler_config: SamplerConfig,
1471 ) -> Self {
1472 let rng = match sampler_config.seed {
1473 Some(s) => SplitMix64::new(s),
1474 None => SplitMix64::from_entropy(),
1475 };
1476 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1477 let pool = Pool::from_env();
1478 if let Some(p) = &pool {
1479 tracing::info!("worker pool: {} threads", p.n_workers());
1480 }
1481 Self {
1482 gpu_plan: None,
1483 tokenizer: std::sync::Arc::new(tokenizer),
1484 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1485 sampler_config,
1486 weights,
1487 hidden_size,
1488 intermediate_size,
1489 num_heads,
1490 num_kv_heads,
1491 head_dim,
1492 num_layers,
1493 physical_layers,
1494 loop_final_norm,
1495 vocab_size,
1496 rms_eps,
1497 rope_base,
1498 norm_style,
1499 rotary_dim: head_dim,
1500 attention_heads_per_layer: None,
1501 vmf_cfg: None,
1502 gdn_cfg: None,
1503 kda_cfg: None,
1504 g3n: None,
1505 dsv4: None,
1506 dsv4_mtp: Vec::new(),
1507 dspark: None,
1508 dspark_pending: Vec::new(),
1509 dspark_hist: Vec::new(),
1510 dspark_real: Vec::new(),
1511 dspark_trunk_picks: Vec::new(),
1512 dspark_exp: Vec::new(),
1513 dspark_draft_ns: 0,
1514 logit_multiplier: None,
1515 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1516 kv_history: Vec::new(),
1517 short_conv_cfg: None,
1518 mtp: None,
1519 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1520 rng,
1521 sampler_scratch: SamplerScratch::default(),
1522 spec_forced: None,
1523 spec_q: Vec::new(),
1524 spec_p: Vec::new(),
1525 spec_res: Vec::new(),
1526 spec_qs: Vec::new(),
1527 spec_ps: Vec::new(),
1528 spec_ress: Vec::new(),
1529 mtp_graph_mode: None,
1530 inv_freq,
1531 ws: ForwardScratch::new(hidden_size),
1532 pool,
1533 model: None,
1534 dyn_force_f32: false,
1535 dyn_skill_layers: Vec::new(),
1536 dyn_active: None,
1537 dyn_blend_loaded: false,
1538 dyn_phi_layer: None,
1539 dyn_phi_ema: Vec::new(),
1540 dyn_phi_seen: 0,
1541 dyn_router: None,
1542 o1_cfg: None,
1543 o1_epoch: 0,
1544 o1_flags: Vec::new(),
1545 trace: false,
1546 calib_temp: 1.0,
1547 confidence_on: true,
1548 embed_multiplier: 1.0,
1549 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1550 swa: None,
1551 sliding_layers: None,
1552 inv_freq_local: None,
1553 rotary_dim_local: None,
1554 rope_scale: 1.0,
1555 rope_scale_local: 1.0,
1556 global_attn: None,
1557 inv_freq_global: None,
1558 attn_v_norm: false,
1559 final_softcap: None,
1560 attn_softcap: 0.0,
1561 graph_want_logits: false,
1562 graph_logits: None,
1563 graph_kv_id: {
1564 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1565 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1566 },
1567 }
1568 }
1569
1570 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1577 self.o1_flags = match &cfg {
1578 Some(c) => {
1579 let mut flags = c.layer_flags(self.num_layers);
1580 for (li, f) in flags.iter_mut().enumerate() {
1581 if *f
1582 && !matches!(
1583 self.weights.layers[self.phys_layer(li)].attn,
1584 AttnKind::Full { .. }
1585 )
1586 {
1587 *f = false;
1588 }
1589 }
1590 flags
1591 }
1592 None => Vec::new(),
1593 };
1594 if let Some(c) = &cfg {
1595 let n = self.o1_flags.iter().filter(|&&f| f).count();
1596 tracing::info!(
1597 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1598 self.num_layers,
1599 c.m,
1600 c.w,
1601 c.sink,
1602 c.rect
1603 );
1604 }
1605 self.o1_cfg = cfg;
1606 }
1607
1608 pub fn o1_active(&self) -> bool {
1610 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1611 }
1612
1613 pub fn o1_begin(&mut self) {
1618 if let Some(c) = &self.o1_cfg {
1619 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1620 for (li, &f) in self.o1_flags.iter().enumerate() {
1621 if f {
1622 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1623 }
1624 }
1625 }
1626 }
1627
1628 pub fn o1_seal(&mut self) {
1632 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1633 if self.o1_cfg.is_none() {
1634 return;
1635 }
1636 for li in 0..self.num_layers {
1637 if self.o1_flags.get(li).copied().unwrap_or(false) {
1638 self.kv_cache.layers[li].o1_seal(self.num_heads);
1639 }
1640 }
1641 }
1642
1643 pub fn set_trace(&mut self, on: bool) {
1645 self.trace = on;
1646 }
1647
1648 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1651 self.rng = match config.seed {
1652 Some(seed) => SplitMix64::new(seed),
1653 None => SplitMix64::from_entropy(),
1654 };
1655 self.sampler_config = config;
1656 }
1657
1658 pub fn set_confidence(&mut self, on: bool) {
1663 self.confidence_on = on;
1664 }
1665
1666 pub fn set_calib_temp(&mut self, t: f32) {
1669 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1670 }
1671
1672 pub fn calib_temp(&self) -> f32 {
1674 self.calib_temp
1675 }
1676
1677 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1680 self.rotary_dim = rotary_dim.min(self.head_dim);
1681 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1682 }
1683
1684 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1685 QwenAttnCfg {
1686 num_heads: self.num_heads,
1687 num_kv_heads: self.num_kv_heads,
1688 head_dim: self.head_dim,
1689 hidden_size: self.hidden_size,
1690 position,
1691 inv_freq: &self.inv_freq,
1692 rotary_dim: self.rotary_dim,
1693 scale: self.attn_scale,
1694 softcap: self.attn_softcap,
1695 window: None,
1696 v_norm: false,
1697 q_norm: None,
1698 k_norm: None,
1699 output_gate: false,
1700 softplus_gate: None,
1701 rope_scale: self.rope_scale,
1702 bias: None,
1703 rms_eps: self.rms_eps,
1704 norm_style: self.norm_style,
1705 pool: self.pool.as_deref(),
1706 }
1707 }
1708
1709 pub fn generate(
1711 &mut self,
1712 prompt: &str,
1713 max_tokens: usize,
1714 task_mask: Option<&TaskMask>,
1715 on_token: Option<TokenCallback>,
1716 ) -> Result<GenerateResult, String> {
1717 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1718 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1719 }
1720
1721 pub fn generate_from_ids(
1729 &mut self,
1730 input_ids: &[u32],
1731 max_tokens: usize,
1732 task_mask: Option<&TaskMask>,
1733 mut on_token: Option<TokenCallback>,
1734 ) -> Result<GenerateResult, String> {
1735 if std::env::var("CMF_TRACE_H").is_ok() {
1736 eprintln!("input_ids: {input_ids:?}");
1737 }
1738 if input_ids.is_empty() {
1739 return Err("empty prompt: nothing to generate from".to_string());
1740 }
1741
1742 let reuse_from = {
1750 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1751 let h = &self.kv_history;
1752 if on
1753 && task_mask.is_none()
1754 && self.mtp.is_none()
1755 && self.o1_cfg.is_none()
1756 && !h.is_empty()
1757 && h.len() < input_ids.len()
1758 && input_ids[..h.len()] == h[..]
1759 {
1760 h.len()
1761 } else {
1762 0
1763 }
1764 };
1765 if reuse_from == 0 {
1766 self.kv_cache.clear();
1768 self.kv_history.clear();
1769 crate::gpu::graph_kv_reset(self.graph_kv_id);
1770 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1771 eprintln!(
1772 "kv-reuse: {} of {} prompt positions already cached",
1773 reuse_from,
1774 input_ids.len()
1775 );
1776 }
1777 crate::gpu::graph_race_begin_generation();
1778 self.o1_begin();
1779
1780 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
1786 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
1813 || std::env::var("CMF_GRAPH_SPEC_SAMPLE").as_deref() == Ok("1");
1814 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
1830 for lw in &self.weights.layers {
1831 if let FfnKind::Dense(d) = &lw.ffn {
1832 dense_n += 1;
1833 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
1834 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
1835 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
1836 {
1837 dense_q4tp += 1;
1838 }
1839 }
1840 }
1841 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
1842 let penalized = self.sampler_config.repetition_penalty != 1.0
1846 || self.sampler_config.presence_penalty != 0.0
1847 || !self.sampler_config.suppress_tokens.is_empty();
1848 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
1849 let spec_wanted = match spec_env.as_deref() {
1850 Some("0") => false,
1851 Some(_) => true,
1852 None => spec_default_ok && !penalized,
1853 };
1854 let graph_spec = self.speculative
1855 && graph_on
1856 && self.mtp.is_some()
1857 && task_mask.is_none()
1858 && !self.o1_active()
1859 && spec_sampling_ok
1860 && spec_wanted;
1861 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
1868 let spec_active = self.speculative
1869 && self.mtp.is_some()
1870 && task_mask.is_none()
1871 && !self.o1_active()
1872 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
1873 let mut mtp = if spec_active { self.mtp.take() } else { None };
1876 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
1877 eprintln!(
1878 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
1879 mtp.is_some(),
1880 self.speculative,
1881 self.sampler_config.temperature < 1e-6,
1882 );
1883 }
1884 if let Some(m) = &mut mtp {
1885 m.kv.clear();
1886 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1888 self.mtp_graph_mode = None;
1889 }
1890 let mut router = if mtp.is_none() {
1894 self.dyn_router.take()
1895 } else {
1896 None
1897 };
1898 if let Some(r) = &mut router {
1899 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1902 }
1903
1904 let mut all_ids = input_ids.to_vec();
1905 let mut generated = 0usize;
1906 let mut finish_reason = "max_tokens".to_string();
1907 let mut drafted = 0usize;
1908 let mut accepted = 0usize;
1909 let mut confidence: Vec<f32> = Vec::new();
1910 let trace_on = self.trace;
1911 let calib_temp = self.calib_temp;
1912 let mut traces: Vec<TokenTrace> = Vec::new();
1913
1914 let mut hidden = vec![0.0f32; self.hidden_size];
1920 let mut pos = reuse_from;
1921 let fuse_lm = mtp.is_none()
1930 && router.is_none()
1931 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
1932 self.graph_logits = None;
1933 self.graph_want_logits = false;
1934 let _tpf = std::time::Instant::now();
1935 let batch_k = std::env::var("CMF_BATCH_K")
1936 .ok()
1937 .and_then(|v| v.parse::<usize>().ok())
1938 .unwrap_or(0);
1939 while self.dsv4.is_some()
1950 && mtp.is_none()
1951 && pos < input_ids.len()
1952 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1953 {
1954 let end = (pos + prefill_chunk()).min(input_ids.len());
1955 let ids: Vec<u32> = input_ids[pos..end].to_vec();
1956 let mut lg = Vec::new();
1957 if let Some(b) = &mut self.dsv4 {
1958 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
1959 crate::dsv4::forward_chunk(
1960 g,
1961 layers,
1962 &cfg,
1963 st,
1964 &ids,
1965 pos,
1966 &self.inv_freq,
1967 self.pool.as_deref(),
1968 &mut lg,
1969 end == input_ids.len(),
1970 );
1971 }
1972 if end == input_ids.len() {
1973 self.graph_logits = Some(lg);
1974 }
1975 pos = end;
1976 hidden = vec![0.0; self.hidden_size];
1977 }
1978 let dyn_prefill = router.is_some();
1983 let graph_prefill = self.graph_prefill_preferred();
1989 if task_mask.is_none()
1990 && !dyn_prefill
1991 && !graph_prefill
1992 && self.can_prefill_batched()
1993 && self.g3n.is_none()
1994 && input_ids.len() > 2
1995 {
1996 let chunk = prefill_chunk();
2002 let hs = self.hidden_size;
2003 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2004 let end = (pos + chunk).min(input_ids.len());
2005 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2006 if let Some(m) = &mut mtp {
2007 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2008 .ok()
2009 .and_then(|v| v.parse().ok())
2010 .unwrap_or(0);
2011 for p in pos..end {
2012 if p + 1 < input_ids.len() {
2013 if probe >= 1 && p + 2 < input_ids.len() {
2014 let (d1, mut hx) = self.mtp_step_h(
2018 m,
2019 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2020 input_ids[p + 1],
2021 p,
2022 );
2023 let mut ok = d1 == input_ids[p + 2];
2024 Self::chain_probe_note(0, ok);
2025 let mut d_prev = d1;
2026 let mut extra = 0usize;
2027 for j in 1..probe {
2028 if p + 2 + j >= input_ids.len() {
2029 break;
2030 }
2031 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2032 extra += 1;
2033 ok = ok && dj == input_ids[p + 2 + j];
2034 Self::chain_probe_note(j, ok);
2035 d_prev = dj;
2036 hx = hj;
2037 }
2038 m.kv.truncate_last(extra);
2039 } else {
2040 let _ = self.mtp_step(
2041 m,
2042 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2043 input_ids[p + 1],
2044 p,
2045 );
2046 }
2047 }
2048 }
2049 }
2050 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2051 pos = end;
2052 }
2053 }
2054 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2055 if task_mask.is_none()
2056 && !dyn_prefill
2057 && !graph_prefill
2058 && !pair_off
2059 && self.pair_supported()
2060 {
2061 while pos + 1 < input_ids.len()
2062 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2063 {
2064 let e1 = self.embed_single(input_ids[pos]);
2065 let e2 = self.embed_single(input_ids[pos + 1]);
2066 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2067 self.commit_linear_scratch();
2069 if let Some(m) = &mut mtp {
2070 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2071 if pos + 2 < input_ids.len() {
2072 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2073 .ok()
2074 .and_then(|v| v.parse().ok())
2075 .unwrap_or(0);
2076 if probe >= 1 && pos + 3 < input_ids.len() {
2077 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2081 let mut ok = d1 == input_ids[pos + 3];
2082 Self::chain_probe_note(0, ok);
2083 let mut d_prev = d1;
2084 let mut extra = 0usize;
2085 for j in 1..probe {
2086 if pos + 3 + j >= input_ids.len() {
2087 break;
2088 }
2089 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2090 extra += 1;
2091 ok = ok && dj == input_ids[pos + 3 + j];
2092 Self::chain_probe_note(j, ok);
2093 d_prev = dj;
2094 hx = hj;
2095 }
2096 m.kv.truncate_last(extra);
2097 } else {
2098 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2099 }
2100 }
2101 }
2102 hidden = h2;
2103 pos += 2;
2104 }
2105 }
2106 if batch_k > 0
2115 && graph_prefill
2116 && task_mask.is_none()
2117 && !self.o1_active()
2118 && mtp.is_none()
2119 && !dyn_prefill
2120 && pos + 1 < input_ids.len()
2121 {
2122 let hs = self.hidden_size;
2123 let chunk = batch_k;
2124 while pos < input_ids.len() {
2125 let end = (pos + chunk).min(input_ids.len());
2126 let bk = end - pos;
2127 let mut hiddens = vec![0f32; bk * hs];
2128 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2129 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2130 }
2131 let positions: Vec<usize> = (pos..end).collect();
2132 let t_chunk = std::time::Instant::now();
2133 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2134 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2135 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2136 eprintln!(
2137 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
2138 bk as f64 / (ms / 1000.0)
2139 );
2140 }
2141 {
2142 use std::sync::atomic::{AtomicBool, Ordering};
2143 static SAID: AtomicBool = AtomicBool::new(false);
2144 if !SAID.swap(true, Ordering::Relaxed) {
2145 if ok_b {
2146 tracing::info!("batched prefill: ACTIVE (k={bk})");
2147 } else {
2148 tracing::warn!("batched prefill declined — per-position graph");
2149 }
2150 }
2151 }
2152 if ok_b {
2153 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2154 pos = end;
2155 } else {
2156 break; }
2158 }
2159 }
2160 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2161 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2162 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2163 if let Some(m) = &mut mtp {
2164 if pos + 1 < input_ids.len() {
2165 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2171 .ok()
2172 .and_then(|v| v.parse().ok())
2173 .unwrap_or(0);
2174 if probe >= 1 && pos + 2 < input_ids.len() {
2175 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2176 let mut ok = d1 == input_ids[pos + 2];
2177 Self::chain_probe_note(0, ok);
2178 let mut d_prev = d1;
2179 let mut extra = 0usize;
2180 for j in 1..probe {
2181 if pos + 2 + j >= input_ids.len() {
2182 break;
2183 }
2184 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2185 extra += 1;
2186 ok = ok && dj == input_ids[pos + 2 + j];
2187 Self::chain_probe_note(j, ok);
2188 d_prev = dj;
2189 hx = hj;
2190 }
2191 m.kv.truncate_last(extra);
2194 } else {
2195 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2196 }
2197 }
2198 }
2199 pos += 1;
2200 }
2201 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2202 eprintln!(
2203 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2204 input_ids.len(),
2205 _tpf.elapsed().as_secs_f64() * 1000.0
2206 );
2207 }
2208 if self
2211 .cancel
2212 .swap(false, std::sync::atomic::Ordering::Relaxed)
2213 {
2214 self.kv_history.clear();
2215 if let Some(m) = mtp {
2216 self.mtp = Some(m);
2217 }
2218 return Ok(GenerateResult {
2219 text: String::new(),
2220 token_ids: Vec::new(),
2221 prompt_tokens: input_ids.len(),
2222 tokens_generated: 0,
2223 finish_reason: "cancelled".to_string(),
2224 mtp_drafted: 0,
2225 mtp_accepted: 0,
2226 token_confidence: Vec::new(),
2227 traces: Vec::new(),
2228 });
2229 }
2230
2231 self.o1_seal();
2234
2235 macro_rules! commit {
2237 ($id:expr) => {{
2238 all_ids.push($id);
2239 generated += 1;
2240 if self.tokenizer.is_eos($id) {
2241 finish_reason = "stop".to_string();
2242 false
2243 } else {
2244 let token_text = self.tokenizer.decode_token($id);
2245 let mut go = true;
2246 if let Some(ref mut cb) = on_token {
2247 if !cb(&token_text) {
2248 finish_reason = "cancelled".to_string();
2249 go = false;
2250 }
2251 }
2252 go
2253 }
2254 }};
2255 }
2256
2257 let mut spec_trial = SpecTrial::Spec {
2268 t0: std::time::Instant::now(),
2269 gen0: generated,
2270 rounds: 0,
2271 };
2272 let mut spec_mon = SpecMon::default();
2273 let mut spec_watchdog_off = false;
2274 let mut next_pos = input_ids.len();
2276 'decode: while generated < max_tokens {
2277 if self
2278 .cancel
2279 .swap(false, std::sync::atomic::Ordering::Relaxed)
2280 {
2281 finish_reason = "cancelled".to_string();
2282 break 'decode;
2283 }
2284 let forced = self.spec_forced.take();
2289 let mut logits = match (forced, self.graph_logits.take()) {
2290 (Some(_), _) => Vec::new(),
2291 (None, Some(lg)) => lg,
2292 (None, None) => {
2293 inference::rms_norm_into(
2294 &hidden,
2295 &self.weights.final_norm,
2296 self.rms_eps,
2297 self.norm_style,
2298 &mut self.ws.n1,
2299 );
2300 self.lm_head_forward(&self.ws.n1)
2301 }
2302 };
2303 let t_next = match forced {
2304 Some(c) => c,
2305 None => sampler::sample_with_scratch_pool(
2306 &logits,
2307 &self.sampler_config,
2308 &all_ids,
2309 &mut self.rng,
2310 &mut self.sampler_scratch,
2311 self.pool.as_deref(),
2312 ),
2313 };
2314 if self.confidence_on {
2315 confidence.push(if logits.is_empty() {
2316 0.0
2317 } else {
2318 sampler::top1_prob_pool(
2319 self.pool.as_deref(),
2320 &mut self.sampler_scratch,
2321 &logits,
2322 t_next,
2323 calib_temp,
2324 )
2325 });
2326 }
2327 if !logits.is_empty() {
2328 attention::recycle_buf(&mut logits);
2329 }
2330 if trace_on {
2331 let skill = router.as_ref().and_then(|r| r.active_id());
2335 traces.push(TokenTrace {
2336 t: generated,
2337 token_id: t_next,
2338 confidence: confidence.last().copied().unwrap_or(0.0),
2339 active_skill: skill,
2340 recon: None,
2341 switched: false,
2342 });
2343 }
2344 if !commit!(t_next) {
2345 break 'decode;
2346 }
2347 if generated >= max_tokens {
2348 break 'decode;
2349 }
2350
2351 if self.kv_cache.needs_eviction() {
2352 static SAID: std::sync::Once = std::sync::Once::new();
2358 SAID.call_once(|| {
2359 tracing::warn!(
2360 "KV cache full at {} positions — evicting half; quality \
2361 will degrade. Raise CMF_MAX_SEQ.",
2362 self.kv_cache.max_seq_len,
2363 );
2364 });
2365 let keep = (self.kv_cache.max_seq_len / 2).max(1);
2366 self.kv_cache.evict(keep);
2367 }
2368
2369 if graph_spec {
2372 match spec_trial {
2373 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
2374 spec_mon.plain_ms =
2375 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
2376 let keep = spec_mon.pays();
2377 tracing::info!(
2378 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2379 spec_mon.tokens,
2380 spec_mon.round_ms,
2381 spec_mon.plain_ms,
2382 if keep { "speculating" } else { "plain" }
2383 );
2384 spec_mon.fails = 0;
2385 spec_trial = SpecTrial::Decided {
2386 spec: keep,
2387 recheck_at: if keep { usize::MAX } else { generated + 128 },
2388 };
2389 }
2390 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
2391 spec_mon.n = 0;
2392 spec_trial = SpecTrial::Spec {
2393 t0: std::time::Instant::now(),
2394 gen0: generated,
2395 rounds: 0,
2396 };
2397 }
2398 _ => {}
2399 }
2400 spec_watchdog_off = matches!(
2401 spec_trial,
2402 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
2403 );
2404 }
2405 match &mut mtp {
2406 #[cfg(feature = "gpu")]
2408 Some(m)
2409 if graph_spec
2410 && !spec_watchdog_off
2411 && generated + 1 < max_tokens
2412 && next_pos > 0 =>
2413 {
2414 let t_round = std::time::Instant::now();
2415 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
2416 m,
2417 &hidden,
2418 t_next,
2419 next_pos,
2420 &mut drafted,
2421 &mut accepted,
2422 &mut all_ids,
2423 ) {
2424 next_pos = n_pos;
2425 hidden = new_h;
2426 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
2430 spec_trial = Self::spec_trial_round(
2433 spec_trial,
2434 &mut spec_mon,
2435 generated + extra.len() + 1,
2436 );
2437 let mut stopped = false;
2438 for &id in &extra {
2439 if self.confidence_on {
2440 confidence.push(0.0);
2441 }
2442 if !commit!(id) {
2443 stopped = true;
2444 break;
2445 }
2446 }
2447 if stopped {
2448 break 'decode;
2449 }
2450 continue 'decode;
2451 }
2452 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
2463 spec_mon.tokens = 0.0;
2464 spec_mon.fails = 3;
2465 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
2466 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2467 next_pos += 1;
2468 continue 'decode;
2469 }
2470 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2472 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2473 drafted += 1;
2474 let emb1 = self.embed_single(t_next);
2475 let emb2 = self.embed_single(draft);
2476 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2477
2478 inference::rms_norm_into(
2479 &h1,
2480 &self.weights.final_norm,
2481 self.rms_eps,
2482 self.norm_style,
2483 &mut self.ws.n1,
2484 );
2485 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2486 let t_after = sampler::sample_with_scratch_pool(
2487 &logits1,
2488 &self.sampler_config,
2489 &all_ids,
2490 &mut self.rng,
2491 &mut self.sampler_scratch,
2492 self.pool.as_deref(),
2493 );
2494 if self.confidence_on {
2495 confidence.push(sampler::top1_prob_pool(
2496 self.pool.as_deref(),
2497 &mut self.sampler_scratch,
2498 &logits1,
2499 t_after,
2500 calib_temp,
2501 ));
2502 }
2503 attention::recycle_buf(&mut logits1);
2504 if trace_on {
2505 traces.push(TokenTrace {
2508 t: generated,
2509 token_id: t_after,
2510 confidence: confidence.last().copied().unwrap_or(0.0),
2511 active_skill: None,
2512 recon: None,
2513 switched: false,
2514 });
2515 }
2516 let stop = !commit!(t_after);
2517
2518 if t_after == draft {
2519 accepted += 1;
2520 self.commit_linear_scratch();
2521 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2522 hidden = h2;
2523 next_pos += 2;
2524 } else {
2525 for layer in &mut self.kv_cache.layers {
2527 layer.truncate_last(1);
2528 }
2529 if !stop {
2530 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2531 hidden = self.forward_layers(
2532 &self.embed_single(t_after),
2533 next_pos + 1,
2534 None,
2535 );
2536 }
2537 next_pos += 2;
2538 }
2539 if stop {
2540 break 'decode;
2541 }
2542 }
2543 _ => {
2545 #[cfg(feature = "gpu")]
2550 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2551 static SAID: std::sync::Once = std::sync::Once::new();
2552 SAID.call_once(|| {
2553 eprintln!(
2554 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2555 !self.dsv4_mtp.is_empty(),
2556 task_mask.is_none(),
2557 router.is_none(),
2558 !trace_on,
2559 self.sampler_config.temperature < 1e-6,
2560 self.sampler_config.repetition_penalty == 1.0,
2561 );
2562 });
2563 }
2564 #[cfg(feature = "gpu")]
2565 if Self::dsv4_spec_on()
2566 && self.dsv4.is_some()
2567 && !self.dsv4_mtp.is_empty()
2568 && task_mask.is_none()
2569 && router.is_none()
2570 && !trace_on
2571 && self.sampler_config.temperature < 1e-6
2572 && self.sampler_config.repetition_penalty == 1.0
2573 && generated + 1 < max_tokens
2574 && all_ids.len() >= 2
2575 {
2576 let tip_token = all_ids[all_ids.len() - 2];
2577 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2578 tip_token,
2579 t_next,
2580 next_pos,
2581 &mut drafted,
2582 &mut accepted,
2583 ) {
2584 next_pos = n_pos;
2585 let mut stopped = false;
2586 for &id in &extra {
2587 if self.confidence_on {
2588 confidence.push(0.0);
2589 }
2590 if !commit!(id) {
2591 stopped = true;
2592 break;
2593 }
2594 }
2595 if stopped {
2596 break 'decode;
2597 }
2598 continue 'decode;
2599 }
2600 }
2601 self.graph_want_logits = fuse_lm;
2602 let mut t_fwd = t_next;
2608 let pure_greedy = self.sampler_config.temperature < 1e-6
2609 && self.sampler_config.repetition_penalty == 1.0
2610 && self.sampler_config.suppress_tokens.is_empty();
2611 let burst_k = std::env::var("CMF_MULTISTEP")
2616 .ok()
2617 .and_then(|v| v.parse::<usize>().ok())
2618 .unwrap_or(0);
2619 if pure_greedy
2620 && burst_k >= 1
2621 && fuse_lm
2622 && task_mask.is_none()
2623 && router.is_none()
2624 && !trace_on
2625 && !self.confidence_on
2626 {
2627 let mut stopped = false;
2628 loop {
2629 let room = max_tokens.saturating_sub(generated);
2630 if room <= 2 {
2631 break;
2632 }
2633 let k = burst_k.min(room - 1);
2634 if k < 1 {
2635 break;
2636 }
2637 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2638 break;
2639 };
2640 next_pos += k;
2641 for &id in &ids {
2642 if !commit!(id) {
2643 stopped = true;
2644 break;
2645 }
2646 }
2647 if stopped {
2648 break;
2649 }
2650 t_fwd = *ids.last().unwrap();
2651 }
2652 if stopped {
2653 break 'decode;
2654 }
2655 }
2656 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2657 next_pos += 1;
2658 if let Some(r) = &mut router {
2661 let phi = self.dyn_phi_ema.clone();
2662 let decision = r.step(&phi, generated);
2663 if let Some(new_active) = decision {
2664 let _ = self.set_active_skill(new_active);
2665 }
2666 if trace_on {
2669 if let Some(last) = traces.last_mut() {
2670 let e = r.last_best_e();
2671 last.recon = e.is_finite().then_some(e);
2672 last.switched = decision.is_some();
2673 }
2674 }
2675 }
2676 }
2677 }
2678 }
2679
2680 self.graph_want_logits = false;
2681 self.graph_logits = None;
2682 if router.is_some() {
2684 let _ = self.set_active_skill(None);
2685 }
2686 self.dyn_router = router.or(self.dyn_router.take());
2687 self.mtp = mtp.or(self.mtp.take());
2688
2689 let output_ids = &all_ids[input_ids.len()..];
2690 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2694 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2695 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2697 Ok(GenerateResult {
2698 text: self.tokenizer.decode(output_ids),
2699 token_ids: output_ids.to_vec(),
2700 prompt_tokens: input_ids.len(),
2701 tokens_generated: generated,
2702 finish_reason,
2703 mtp_drafted: drafted,
2704 mtp_accepted: accepted,
2705 token_confidence: confidence,
2706 traces,
2707 })
2708 }
2709
2710 fn mtp_step(
2714 &mut self,
2715 m: &mut MtpModule,
2716 hidden: &[f32],
2717 next_token: u32,
2718 position: usize,
2719 ) -> u32 {
2720 self.mtp_step_h(m, hidden, next_token, position).0
2721 }
2722
2723 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2727 use std::sync::Mutex;
2728 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2729 let mut t = T.lock().unwrap();
2730 if t.len() <= depth {
2731 t.resize(depth + 1, (0, 0));
2732 }
2733 t[depth].0 += 1;
2734 t[depth].1 += prefix_ok as u64;
2735 if depth == 0 && t[0].0 % 128 == 0 {
2736 let line: Vec<String> = t
2737 .iter()
2738 .enumerate()
2739 .map(|(d, (n, k))| {
2740 format!(
2741 "d{}={:.0}%({n})",
2742 d + 1,
2743 100.0 * *k as f64 / (*n).max(1) as f64
2744 )
2745 })
2746 .collect();
2747 eprintln!("mtp-chain: {}", line.join(" "));
2748 }
2749 }
2750
2751 fn mtp_step_hl(
2759 &mut self,
2760 m: &mut MtpModule,
2761 hidden: &[f32],
2762 next_token: u32,
2763 position: usize,
2764 ) -> (Vec<f32>, Vec<f32>) {
2765 #[cfg(feature = "gpu")]
2770 if self.mtp_graph_mode != Some(false) {
2771 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
2772 self.mtp_graph_mode = Some(true);
2773 return r;
2774 }
2775 if self.mtp_graph_mode == Some(true) {
2776 tracing::warn!("mtp graph declined mid-run — draft falls to the per-op path");
2781 }
2782 self.mtp_graph_mode = Some(false);
2783 }
2784 let e = self.embed_single(next_token);
2788 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2789 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2790 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2791 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2792 let mut x = vec![0.0f32; self.hidden_size];
2793 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2794
2795 let lw = &m.layer;
2797 inference::rms_norm_into(
2798 &x,
2799 &lw.input_norm,
2800 self.rms_eps,
2801 self.norm_style,
2802 &mut self.ws.n1,
2803 );
2804 let attn = match &lw.attn {
2805 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2807 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2808 AttnKind::Full {
2809 wq,
2810 wk,
2811 wv,
2812 wo,
2813 q_norm,
2814 k_norm,
2815 output_gate,
2816 softplus_gate,
2817 bias,
2818 } => {
2819 let mut cfg = self.attn_cfg(position);
2820 cfg.q_norm = q_norm.as_deref();
2821 cfg.k_norm = k_norm.as_deref();
2822 cfg.output_gate = *output_gate;
2823 cfg.softplus_gate = softplus_gate
2824 .as_ref()
2825 .map(|(gate, per_head)| (gate, *per_head));
2826 cfg.bias = bias
2827 .as_ref()
2828 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2829 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2830 }
2831 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
2832 unreachable!("MTP block is full attention")
2833 }
2834 };
2835 for (i, &a) in attn.iter().enumerate() {
2836 x[i] += a;
2837 }
2838 inference::rms_norm_into(
2839 &x,
2840 &lw.post_norm,
2841 self.rms_eps,
2842 self.norm_style,
2843 &mut self.ws.p1,
2844 );
2845 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
2846 for (i, &f) in ffn.iter().enumerate() {
2847 x[i] += f;
2848 }
2849
2850 inference::rms_norm_into(
2851 &x,
2852 &m.final_norm,
2853 self.rms_eps,
2854 self.norm_style,
2855 &mut self.ws.n1,
2856 );
2857 let lg = self.lm_head_forward(&self.ws.n1);
2858 (lg, x)
2859 }
2860
2861 fn mtp_step_h(
2863 &mut self,
2864 m: &mut MtpModule,
2865 hidden: &[f32],
2866 next_token: u32,
2867 position: usize,
2868 ) -> (u32, Vec<f32>) {
2869 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
2870 let draft = sampler::argmax(&lg);
2871 attention::recycle_buf(&mut lg);
2872 (draft, x)
2873 }
2874
2875 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
2881 match trial {
2882 SpecTrial::Spec { t0, gen0, rounds } => {
2883 let rounds = rounds + 1;
2884 if rounds >= 5 {
2885 if mon.plain_ms > 0.0 {
2886 let keep = mon.pays();
2887 mon.fails = 0;
2888 tracing::info!(
2889 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2890 mon.tokens,
2891 mon.round_ms,
2892 mon.plain_ms,
2893 if keep { "speculating" } else { "plain" }
2894 );
2895 SpecTrial::Decided {
2896 spec: keep,
2897 recheck_at: if keep { usize::MAX } else { generated + 128 },
2898 }
2899 } else {
2900 SpecTrial::Plain {
2901 t0: std::time::Instant::now(),
2902 gen0: generated,
2903 }
2904 }
2905 } else {
2906 SpecTrial::Spec { t0, gen0, rounds }
2907 }
2908 }
2909 SpecTrial::Decided { spec: true, .. } => {
2910 if mon.pays() {
2911 mon.fails = 0;
2912 trial
2913 } else {
2914 mon.fails += 1;
2915 if mon.fails >= 4 {
2916 tracing::info!(
2917 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
2918 mon.tokens,
2919 mon.round_ms,
2920 mon.plain_ms
2921 );
2922 SpecTrial::Decided {
2923 spec: false,
2924 recheck_at: generated + 128,
2925 }
2926 } else {
2927 trial
2928 }
2929 }
2930 }
2931 other => other,
2932 }
2933 }
2934
2935 fn mtp_kv_id(&self) -> u64 {
2938 self.graph_kv_id | (1u64 << 40)
2939 }
2940
2941 const MTP_LAYER_BASE: usize = 0;
2946
2947 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
2950 let e = self.embed_single(next_token);
2951 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2952 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2953 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2954 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2955 let mut x = vec![0.0f32; self.hidden_size];
2956 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2957 x
2958 }
2959
2960 #[cfg(feature = "gpu")]
2963 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
2964 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
2965 return false;
2966 }
2967 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
2968 || !crate::gpu::enabled_here()
2969 || self.attn_softcap > 0.0
2970 || self.attention_heads_per_layer.is_some()
2971 {
2972 return false;
2973 }
2974 matches!(
2975 &m.layer.attn,
2976 AttnKind::Full {
2977 softplus_gate: None,
2978 ..
2979 }
2980 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
2981 }
2982
2983 #[cfg(feature = "gpu")]
2989 fn mtp_step_graph(
2990 &mut self,
2991 m: &mut MtpModule,
2992 hidden: &[f32],
2993 next_token: u32,
2994 position: usize,
2995 ) -> Option<(Vec<f32>, Vec<f32>)> {
2996 if !self.mtp_graph_ok(m) {
2997 return None;
2998 }
2999 let lw = &m.layer;
3000 let AttnKind::Full {
3001 wq,
3002 wk,
3003 wv,
3004 wo,
3005 q_norm,
3006 k_norm,
3007 output_gate,
3008 softplus_gate,
3009 bias,
3010 } = &lw.attn
3011 else {
3012 return None;
3013 };
3014 if softplus_gate.is_some() {
3015 return None;
3016 }
3017 let FfnKind::Dense(d) = &lw.ffn else {
3018 return None;
3019 };
3020 let mut x = self.mtp_block_input(m, hidden, next_token);
3023 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3024 let (_, i, kind, rs) = t.graph_weight()?;
3025 Some(crate::gpu::GraphW {
3026 idx: i,
3027 kind,
3028 row_scale: rs,
3029 data: &[],
3030 })
3031 }
3032 let (model, _, _, _) = wq.graph_weight()?;
3033 let model = model.clone();
3034 let (lm_gw, lm_rows) = {
3035 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3036 (
3037 crate::gpu::GraphW {
3038 idx: i,
3039 kind,
3040 row_scale: rs,
3041 data: &[],
3042 },
3043 self.weights.lm_head.rows(),
3044 )
3045 };
3046 let layer = crate::gpu::GraphLayer {
3047 input_norm: &lw.input_norm,
3048 attn: crate::gpu::GraphAttn::Full {
3049 wq: gw(wq)?,
3050 wk: gw(wk)?,
3051 wv: gw(wv)?,
3052 wo: gw(wo)?,
3053 q_norm: q_norm.as_deref(),
3054 k_norm: k_norm.as_deref(),
3055 bias: bias
3056 .as_ref()
3057 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3058 output_gate: *output_gate,
3059 cpu_k: m.kv.k_heads(),
3060 cpu_v: m.kv.v_heads(),
3061 },
3062 post_norm: &lw.post_norm,
3063 ffn: crate::gpu::GraphFfn::Dense {
3064 gate: gw(&d.gate_proj)?,
3065 up: gw(&d.up_proj)?,
3066 down: gw(&d.down_proj)?,
3067 },
3068 };
3069 let nh = self.num_heads;
3070 let (nkv, hd, rd) = self.layer_geom(0);
3071 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3072 let mut logits = Vec::new();
3073 let ok = crate::gpu::forward_token_graph(
3074 &model,
3075 self.mtp_kv_id(),
3076 std::slice::from_ref(&layer),
3077 &[None],
3078 self.o1_epoch,
3079 &self.inv_freq,
3080 &mut x,
3081 nh,
3082 nkv,
3083 hd,
3084 rd,
3085 self.hidden_size,
3086 self.intermediate_size,
3087 position,
3088 self.kv_cache.max_seq_len,
3089 gemma,
3090 self.rms_eps as f32,
3091 Some((&lm_gw, lm_rows)),
3092 &m.final_norm,
3093 &mut logits,
3094 &[],
3095 1,
3096 None,
3097 None,
3098 None,
3099 Self::MTP_LAYER_BASE,
3100 true,
3101 );
3102 if !ok {
3103 return None;
3104 }
3105 logits.resize(self.vocab_size, 0.0);
3106 Some((logits, x))
3107 }
3108
3109 #[cfg(feature = "gpu")]
3116 fn mtp_warm_graph(
3117 &mut self,
3118 m: &mut MtpModule,
3119 pairs: &[(&[f32], u32)],
3120 first_pos: usize,
3121 ) -> bool {
3122 if pairs.is_empty() || !self.mtp_graph_ok(m) {
3123 return pairs.is_empty();
3124 }
3125 let hs = self.hidden_size;
3126 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
3129 for (h, t) in pairs {
3130 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
3131 }
3132 let lw = &m.layer;
3133 let AttnKind::Full {
3134 wq,
3135 wk,
3136 wv,
3137 wo,
3138 q_norm,
3139 k_norm,
3140 output_gate,
3141 bias,
3142 ..
3143 } = &lw.attn
3144 else {
3145 return false;
3146 };
3147 let FfnKind::Dense(d) = &lw.ffn else {
3148 return false;
3149 };
3150 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3151 let (_, i, kind, rs) = t.graph_weight()?;
3152 Some(crate::gpu::GraphW {
3153 idx: i,
3154 kind,
3155 row_scale: rs,
3156 data: &[],
3157 })
3158 }
3159 let Some((model, _, _, _)) = wq.graph_weight() else {
3160 return false;
3161 };
3162 let model = model.clone();
3163 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
3164 gw(wq),
3165 gw(wk),
3166 gw(wv),
3167 gw(wo),
3168 gw(&d.gate_proj),
3169 gw(&d.up_proj),
3170 gw(&d.down_proj),
3171 ) else {
3172 return false;
3173 };
3174 let layer = crate::gpu::GraphLayer {
3175 input_norm: &lw.input_norm,
3176 attn: crate::gpu::GraphAttn::Full {
3177 wq: gwq,
3178 wk: gwk,
3179 wv: gwv,
3180 wo: gwo,
3181 q_norm: q_norm.as_deref(),
3182 k_norm: k_norm.as_deref(),
3183 bias: bias
3184 .as_ref()
3185 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3186 output_gate: *output_gate,
3187 cpu_k: m.kv.k_heads(),
3188 cpu_v: m.kv.v_heads(),
3189 },
3190 post_norm: &lw.post_norm,
3191 ffn: crate::gpu::GraphFfn::Dense {
3192 gate: gg,
3193 up: gu,
3194 down: gd,
3195 },
3196 };
3197 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
3198 let nh = self.num_heads;
3199 let (nkv, hd, rd) = self.layer_geom(0);
3200 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3201 crate::gpu::forward_batch_graph(
3202 &model,
3203 self.mtp_kv_id(),
3204 std::slice::from_ref(&layer),
3205 &self.inv_freq,
3206 &mut hiddens,
3207 nh,
3208 nkv,
3209 hd,
3210 rd,
3211 hs,
3212 self.intermediate_size,
3213 &positions,
3214 self.kv_cache.max_seq_len,
3215 gemma,
3216 self.rms_eps as f32,
3217 pairs.len(),
3218 None,
3219 )
3220 }
3221
3222 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
3226 let e = self.embed_single(next_token);
3227 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3228 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3229 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3230 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3231 let mut x = vec![0.0f32; self.hidden_size];
3232 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3233 inference::rms_norm_into(
3234 &x,
3235 &m.layer.input_norm,
3236 self.rms_eps,
3237 self.norm_style,
3238 &mut self.ws.n1,
3239 );
3240 let attn = match &m.layer.attn {
3241 AttnKind::Full {
3242 wq,
3243 wk,
3244 wv,
3245 wo,
3246 q_norm,
3247 k_norm,
3248 output_gate,
3249 softplus_gate,
3250 bias,
3251 } => {
3252 let mut cfg = self.attn_cfg(position);
3253 cfg.q_norm = q_norm.as_deref();
3254 cfg.k_norm = k_norm.as_deref();
3255 cfg.output_gate = *output_gate;
3256 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
3257 cfg.bias = bias
3258 .as_ref()
3259 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3260 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3261 }
3262 _ => return,
3263 };
3264 let _ = attn;
3265 }
3266
3267 #[cfg(feature = "gpu")]
3274 #[allow(clippy::too_many_arguments)]
3275 fn graph_spec_step(
3276 &mut self,
3277 m: &mut MtpModule,
3278 hidden: &[f32],
3279 t_next: u32,
3280 next_pos: usize,
3281 drafted: &mut usize,
3282 accepted: &mut usize,
3283 all_ids: &mut Vec<u32>,
3287 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
3288 #[cfg(feature = "gpu")]
3299 let k_default = if crate::gpu_wgpu::verify_i8_on() { 5 } else { 4 };
3300 #[cfg(not(feature = "gpu"))]
3301 let k_default = 4;
3302 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
3303 .ok()
3304 .and_then(|v| v.parse().ok())
3305 .filter(|&v| (1..=8).contains(&v))
3306 .unwrap_or(k_default);
3307 if next_pos == 0 {
3308 return None;
3309 }
3310 let t_round = std::time::Instant::now();
3311 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
3327 let sub0 = subs();
3328 let cfg = self.sampler_config.clone();
3333 let penalized = !(cfg.repetition_penalty == 1.0
3334 && cfg.presence_penalty == 0.0
3335 && cfg.suppress_tokens.is_empty());
3336 let greedy_pen = cfg.temperature < 1e-6 && penalized;
3341 let sampling = cfg.temperature >= 1e-6;
3342 let sparse = sampling && sampler::sparse_ok(&cfg);
3348 let base_len = all_ids.len();
3349 if sampling && !sparse && self.spec_q.len() < k_spec {
3350 self.spec_q.resize_with(k_spec, Vec::new);
3351 }
3352 if sparse && self.spec_qs.len() < k_spec {
3353 self.spec_qs.resize_with(k_spec, Vec::new);
3354 }
3355 let mut drafts = Vec::with_capacity(k_spec);
3360 let mut hx = hidden.to_vec();
3361 for j in 0..k_spec {
3362 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
3363 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3364 let dj = if sparse {
3365 let mut q = std::mem::take(&mut self.spec_qs[j]);
3366 let ok = sampler::sparse_distribution_into(
3367 &lg,
3368 &cfg,
3369 all_ids,
3370 &mut self.sampler_scratch,
3371 self.pool.as_deref(),
3372 &mut q,
3373 );
3374 let d = if ok {
3375 sampler::draw_sparse(&q, &mut self.rng)
3376 } else {
3377 let t = sampler::argmax(&lg);
3379 q.clear();
3380 q.push((t, 1.0));
3381 t
3382 };
3383 self.spec_qs[j] = q;
3384 all_ids.push(d);
3385 d
3386 } else if sampling {
3387 let mut q = std::mem::take(&mut self.spec_q[j]);
3388 sampler::distribution_into(
3389 &lg,
3390 &cfg,
3391 all_ids,
3392 &mut self.sampler_scratch,
3393 self.pool.as_deref(),
3394 &mut q,
3395 );
3396 let d = sampler::draw(&q, &mut self.rng);
3397 self.spec_q[j] = q;
3398 all_ids.push(d); d
3400 } else if greedy_pen {
3401 let d = sampler::argmax_penalized(
3402 &lg,
3403 &cfg,
3404 all_ids,
3405 &mut self.sampler_scratch,
3406 self.pool.as_deref(),
3407 );
3408 all_ids.push(d);
3409 d
3410 } else {
3411 sampler::argmax(&lg)
3412 };
3413 attention::recycle_buf(&mut lg);
3414 drafts.push(dj);
3415 hx = hj;
3416 }
3417 all_ids.truncate(base_len);
3418 *drafted += k_spec;
3419 let t_draft = t_round.elapsed();
3420 let sub_draft = subs();
3421 let b = k_spec + 1;
3424 let mut hiddens = vec![0.0f32; b * self.hidden_size];
3425 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
3426 let e = self.embed_single(t);
3427 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
3428 }
3429 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
3430 let (lm_gw, lm_rows) = {
3431 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3432 (
3433 crate::gpu::GraphW {
3434 idx: i,
3435 kind,
3436 row_scale: rs,
3437 data: &[],
3438 },
3439 self.weights.lm_head.rows(),
3440 )
3441 };
3442 let mut logits = Vec::new();
3443 let final_norm = self.weights.final_norm.clone();
3444 let ok = self.try_batch_graph_wgpu(
3445 &mut hiddens,
3446 &positions,
3447 b,
3448 Some(crate::gpu::SpecTail {
3449 lm: lm_gw,
3450 lm_rows,
3451 final_norm: &final_norm,
3452 logits_out: &mut logits,
3453 }),
3454 );
3455 if !ok {
3456 m.kv.truncate_last(k_spec);
3459 return None;
3460 }
3461 let t_verify = t_round.elapsed();
3462 let sub_verify = subs();
3463 let mut a = 0usize;
3468 let mut forced: Option<u32> = None;
3469 let ids: Vec<u32> = if sparse {
3470 let mut p = std::mem::take(&mut self.spec_ps);
3471 let mut res = std::mem::take(&mut self.spec_ress);
3472 while a < k_spec {
3473 let ok = sampler::sparse_distribution_into(
3474 &logits[a * lm_rows..(a + 1) * lm_rows],
3475 &cfg,
3476 all_ids,
3477 &mut self.sampler_scratch,
3478 self.pool.as_deref(),
3479 &mut p,
3480 );
3481 if !ok {
3482 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
3483 p.clear();
3484 p.push((t, 1.0));
3485 }
3486 match sampler::spec_accept_or_correct_sparse(
3487 &p,
3488 &self.spec_qs[a],
3489 drafts[a],
3490 &mut self.rng,
3491 &mut res,
3492 ) {
3493 None => {
3494 all_ids.push(drafts[a]);
3495 a += 1;
3496 }
3497 Some(c) => {
3498 forced = Some(c);
3499 break;
3500 }
3501 }
3502 }
3503 all_ids.truncate(base_len);
3504 self.spec_ps = p;
3505 self.spec_ress = res;
3506 drafts.clone()
3507 } else if sampling {
3508 let mut p = std::mem::take(&mut self.spec_p);
3509 let mut res = std::mem::take(&mut self.spec_res);
3510 while a < k_spec {
3511 sampler::distribution_into(
3512 &logits[a * lm_rows..(a + 1) * lm_rows],
3513 &cfg,
3514 all_ids,
3515 &mut self.sampler_scratch,
3516 self.pool.as_deref(),
3517 &mut p,
3518 );
3519 match sampler::spec_accept_or_correct(
3520 &p,
3521 &self.spec_q[a],
3522 drafts[a],
3523 &mut self.rng,
3524 &mut res,
3525 self.pool.as_deref(),
3526 ) {
3527 None => {
3528 all_ids.push(drafts[a]);
3529 a += 1;
3530 }
3531 Some(c) => {
3532 forced = Some(c);
3533 break;
3534 }
3535 }
3536 }
3537 all_ids.truncate(base_len);
3538 self.spec_p = p;
3539 self.spec_res = res;
3540 drafts.clone()
3542 } else if greedy_pen {
3543 let mut ids: Vec<u32> = Vec::with_capacity(b);
3547 for i in 0..b {
3548 let t = sampler::argmax_penalized(
3549 &logits[i * lm_rows..(i + 1) * lm_rows],
3550 &cfg,
3551 all_ids,
3552 &mut self.sampler_scratch,
3553 self.pool.as_deref(),
3554 );
3555 ids.push(t);
3556 if i < k_spec && t == drafts[i] {
3557 all_ids.push(t);
3558 } else {
3559 break;
3560 }
3561 }
3562 all_ids.truncate(base_len);
3563 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
3564 a += 1;
3565 }
3566 ids
3569 } else {
3570 let ids: Vec<u32> = (0..b)
3571 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
3572 .collect();
3573 while a < k_spec && ids[a] == drafts[a] {
3574 a += 1;
3575 }
3576 ids
3577 };
3578 if a + 1 < b {
3580 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
3581 }
3582 *accepted += a;
3583 m.kv.truncate_last(k_spec.saturating_sub(1));
3594 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
3595 if !warm_off && a > 0 {
3596 let mut warmed = false;
3599 if self.mtp_graph_mode == Some(true) {
3600 let rows: Vec<Vec<f32>> = (0..a)
3601 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
3602 .collect();
3603 let pairs: Vec<(&[f32], u32)> = rows
3604 .iter()
3605 .zip(ids.iter())
3606 .map(|(r, &t)| (r.as_slice(), t))
3607 .collect();
3608 warmed = self.mtp_warm_graph(m, &pairs, next_pos);
3609 if !warmed {
3610 warmed = true;
3612 for j in 0..a {
3613 if self
3614 .mtp_step_graph(m, &rows[j], ids[j], next_pos + j)
3615 .is_none()
3616 {
3617 warmed = false;
3618 break;
3619 }
3620 }
3621 }
3622 }
3623 if !warmed {
3624 for j in 0..a {
3625 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
3626 let row = row.to_vec();
3627 self.mtp_warm(m, &row, ids[j], next_pos + j);
3628 }
3629 }
3630 }
3631 if let Some(c) = forced {
3635 self.spec_forced = Some(c);
3636 self.graph_logits = None;
3637 } else {
3638 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
3639 row.resize(self.vocab_size, 0.0);
3640 if let Some(c) = self.final_softcap {
3641 for l in row.iter_mut() {
3642 *l = c * (*l / c).tanh();
3643 }
3644 }
3645 self.graph_logits = Some(row);
3646 }
3647 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
3648 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3654 let end = subs();
3655 eprintln!(
3656 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
3657 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
3658 t_draft.as_secs_f64() * 1e3,
3659 sub_draft - sub0,
3660 (t_verify - t_draft).as_secs_f64() * 1e3,
3661 sub_verify - sub_draft,
3662 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
3663 end - sub_verify,
3664 );
3665 }
3666 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
3667 }
3668
3669 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
3678 if !self.pair_supported() {
3679 return (0.0, 0.0);
3680 }
3681 let emb1 = self.embed_single(1);
3682 let emb2 = self.embed_single(2);
3683 let pos = self.kv_cache.seq_len();
3684
3685 let t0 = std::time::Instant::now();
3686 for _ in 0..iters {
3687 let _ = self.forward_layers(&emb1, pos, None);
3688 let _ = self.forward_layers(&emb2, pos + 1, None);
3689 for l in &mut self.kv_cache.layers {
3690 l.truncate_last(2);
3691 }
3692 }
3693 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
3694
3695 let t1 = std::time::Instant::now();
3696 for _ in 0..iters {
3697 let _ = self.forward_pair(&emb1, &emb2, pos);
3698 for l in &mut self.kv_cache.layers {
3699 l.truncate_last(2);
3700 }
3701 }
3702 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
3703 (singles_ms, pair_ms)
3704 }
3705
3706 fn pair_supported(&self) -> bool {
3714 !self.weights.layers.is_empty()
3721 && self.g3n.is_none()
3722 && !self
3723 .weights
3724 .layers
3725 .iter()
3726 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
3727 }
3728
3729 fn forward_pair(
3730 &mut self,
3731 emb1: &[f32],
3732 emb2: &[f32],
3733 position: usize,
3734 ) -> (Vec<f32>, Vec<f32>) {
3735 let mut h1 = emb1.to_vec();
3736 let mut h2 = emb2.to_vec();
3737 let (_nkv, _hd, hs, _rd, eps) = (
3738 self.num_kv_heads,
3739 self.head_dim,
3740 self.hidden_size,
3741 self.rotary_dim,
3742 self.rms_eps,
3743 );
3744 let pool = self.pool.clone();
3745
3746 for li in 0..self.num_layers {
3747 let lw = &self.weights.layers[self.phys_layer(li)];
3748 inference::rms_norm_into(
3751 &h1,
3752 &lw.input_norm,
3753 self.rms_eps,
3754 self.norm_style,
3755 &mut self.ws.n1,
3756 );
3757 inference::rms_norm_into(
3758 &h2,
3759 &lw.input_norm,
3760 self.rms_eps,
3761 self.norm_style,
3762 &mut self.ws.n2,
3763 );
3764
3765 let (a1, a2) = match &lw.attn {
3766 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3767 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3768 AttnKind::Linear(w) => {
3769 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3770 let layer = &mut self.kv_cache.layers[li];
3771 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
3772 vmf_phase_pair(
3773 &self.ws.n1,
3774 &self.ws.n2,
3775 w,
3776 &cfg,
3777 state,
3778 scratch,
3779 self.pool.as_deref(),
3780 )
3781 }
3782 AttnKind::LinearGdn(w) => {
3783 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3784 let layer = &mut self.kv_cache.layers[li];
3785 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
3786 gdn_pair(
3787 &self.ws.n1,
3788 &self.ws.n2,
3789 w,
3790 &cfg,
3791 state,
3792 scratch,
3793 self.pool.as_deref(),
3794 )
3795 }
3796 AttnKind::ShortConv(w) => {
3797 let cfg = self
3798 .short_conv_cfg
3799 .expect("short-conv layer without short_conv_cfg");
3800 let layer = &mut self.kv_cache.layers[li];
3801 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
3802 short_conv_pair(
3803 &self.ws.n1,
3804 &self.ws.n2,
3805 w,
3806 &cfg,
3807 state,
3808 scratch,
3809 self.pool.as_deref(),
3810 )
3811 }
3812 AttnKind::Full {
3813 wq,
3814 wk,
3815 wv,
3816 wo,
3817 q_norm,
3818 k_norm,
3819 output_gate,
3820 softplus_gate,
3821 bias,
3822 } => {
3823 let inv_freq_l = self.layer_inv_freq(li);
3824 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3825 let cfg = QwenAttnCfg {
3826 num_heads: self.layer_num_heads(li),
3827 num_kv_heads: nkv_l,
3828 head_dim: hd_l,
3829 hidden_size: hs,
3830 position,
3831 inv_freq: &inv_freq_l,
3832 rotary_dim: rd_l,
3833 scale: self.attn_scale,
3834 softcap: self.attn_softcap,
3835 window: self.layer_window(li),
3836 v_norm: self.attn_v_norm,
3837 q_norm: q_norm.as_deref(),
3838 k_norm: k_norm.as_deref(),
3839 output_gate: *output_gate,
3840 softplus_gate: softplus_gate
3841 .as_ref()
3842 .map(|(gate, per_head)| (gate, *per_head)),
3843 rope_scale: self.layer_rope_scale(li),
3844 bias: bias
3845 .as_ref()
3846 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3847 rms_eps: eps,
3848 norm_style: self.norm_style,
3849 pool: pool.as_deref(),
3850 };
3851 attention::qwen_attention_pair(
3852 &self.ws.n1,
3853 &self.ws.n2,
3854 wq,
3855 wk,
3856 wv,
3857 wo,
3858 &mut self.kv_cache.layers[li],
3859 &cfg,
3860 )
3861 }
3862 };
3863 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
3864 Some(w) => (
3865 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
3866 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
3867 ),
3868 None => (a1, a2),
3869 };
3870 for i in 0..self.hidden_size {
3871 h1[i] += a1[i];
3872 h2[i] += a2[i];
3873 }
3874 let (mut a1, mut a2) = (a1, a2);
3875 attention::recycle_buf(&mut a1);
3876 attention::recycle_buf(&mut a2);
3877
3878 let lw = &self.weights.layers[self.phys_layer(li)];
3879 inference::rms_norm_into(
3880 &h1,
3881 &lw.post_norm,
3882 self.rms_eps,
3883 self.norm_style,
3884 &mut self.ws.p1,
3885 );
3886 inference::rms_norm_into(
3887 &h2,
3888 &lw.post_norm,
3889 self.rms_eps,
3890 self.norm_style,
3891 &mut self.ws.p2,
3892 );
3893 let (f1, f2) = match &lw.ffn {
3894 FfnKind::DenseMoe(dm) => (
3897 dense_moe_ffn(
3898 dm,
3899 &self.ws.p1,
3900 &h1,
3901 self.rms_eps,
3902 self.norm_style,
3903 self.pool.as_deref(),
3904 ),
3905 dense_moe_ffn(
3906 dm,
3907 &self.ws.p2,
3908 &h2,
3909 self.rms_eps,
3910 self.norm_style,
3911 self.pool.as_deref(),
3912 ),
3913 ),
3914 _ => ffn_forward_pair(
3915 &lw.ffn,
3916 &self.ws.p1,
3917 &self.ws.p2,
3918 self.pool.as_deref(),
3919 None,
3920 ),
3921 };
3922 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
3923 Some(w) => (
3924 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
3925 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
3926 ),
3927 None => (f1, f2),
3928 };
3929 for i in 0..self.hidden_size {
3930 h1[i] += f1[i];
3931 h2[i] += f2[i];
3932 }
3933 let (mut f1, mut f2) = (f1, f2);
3934 attention::recycle_buf(&mut f1);
3935 attention::recycle_buf(&mut f2);
3936 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
3937 for i in 0..self.hidden_size {
3938 h1[i] *= sc;
3939 h2[i] *= sc;
3940 }
3941 }
3942 if self.is_loop_end(li) && li + 1 < self.num_layers {
3944 h1 = inference::rms_norm(
3945 &h1,
3946 &self.weights.final_norm,
3947 self.rms_eps,
3948 self.norm_style,
3949 );
3950 h2 = inference::rms_norm(
3951 &h2,
3952 &self.weights.final_norm,
3953 self.rms_eps,
3954 self.norm_style,
3955 );
3956 }
3957 }
3958 (h1, h2)
3959 }
3960
3961 fn commit_linear_scratch(&mut self) {
3963 for layer in &mut self.kv_cache.layers {
3964 if !layer.linear_scratch.is_empty() {
3965 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
3966 layer.linear_scratch.clear();
3967 }
3968 }
3969 }
3970
3971 pub fn forward_ids(
3974 &mut self,
3975 ids: &[u32],
3976 task_mask: Option<&TaskMask>,
3977 ) -> Result<Vec<f32>, String> {
3978 if ids.is_empty() {
3979 return Err("empty id sequence".to_string());
3980 }
3981 self.kv_cache.clear();
3982 self.kv_history.clear();
3983 self.o1_begin();
3984 let mut hidden = vec![0.0f32; self.hidden_size];
3985 let mut pos = 0usize;
3986 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
3994 let chunk = prefill_chunk();
3998 let hs = self.hidden_size;
3999 while pos < ids.len() {
4000 let end = (pos + chunk).min(ids.len());
4001 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4002 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4003 pos = end;
4004 }
4005 }
4006 if task_mask.is_none()
4015 && !self.graph_prefill_preferred()
4016 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
4017 && self.pair_supported()
4018 {
4019 while pos + 1 < ids.len() {
4020 let e1 = self.embed_single(ids[pos]);
4021 let e2 = self.embed_single(ids[pos + 1]);
4022 let (_, h2) = self.forward_pair(&e1, &e2, pos);
4023 self.commit_linear_scratch();
4024 hidden = h2;
4025 pos += 2;
4026 }
4027 }
4028 while pos < ids.len() {
4029 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4030 pos += 1;
4031 }
4032 self.o1_seal();
4036 let normed = inference::rms_norm(
4037 &hidden,
4038 &self.weights.final_norm,
4039 self.rms_eps,
4040 self.norm_style,
4041 );
4042 Ok(self.lm_head_forward(&normed))
4043 }
4044
4045 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
4052 let (nll, cnt) = self.nll_ids_from(ids, 0);
4053 (nll / cnt.max(1) as f64).exp()
4054 }
4055
4056 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4061 self.kv_cache.clear();
4062 self.kv_history.clear();
4063 FFN_PROBE.with(|p| {
4064 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4065 });
4066 crate::gpu::cpu_scope(|| {
4067 for (pos, &id) in ids.iter().enumerate() {
4068 let emb = self.embed_single(id);
4069 let _ = self.forward_layers(&emb, pos, None);
4070 }
4071 });
4072 self.kv_cache.clear();
4073 self.kv_history.clear();
4074 FFN_PROBE
4075 .with(|p| p.borrow_mut().take())
4076 .unwrap_or_default()
4077 }
4078
4079 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
4083 self.kv_cache.clear();
4084 self.kv_history.clear();
4085 let mut nll = 0f64;
4086 let mut cnt = 0usize;
4087 let mut hidden = vec![0f32; self.hidden_size];
4088 for (pos, &id) in ids.iter().enumerate() {
4089 if pos > 0 {
4090 inference::rms_norm_into(
4091 &hidden,
4092 &self.weights.final_norm,
4093 self.rms_eps,
4094 self.norm_style,
4095 &mut self.ws.n1,
4096 );
4097 let mut logits = self.lm_head_forward(&self.ws.n1);
4098 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4099 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
4100 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
4101 nll -= p.max(1e-300).ln();
4102 cnt += 1;
4103 attention::recycle_buf(&mut logits);
4104 }
4105 let emb = self.embed_single(id);
4106 hidden = self.forward_layers(&emb, pos, Some(mask));
4107 }
4108 self.kv_cache.clear();
4109 self.kv_history.clear();
4110 (nll / cnt.max(1) as f64).exp()
4111 }
4112
4113 pub fn nll_ids_masked(
4132 &mut self,
4133 ids: &[u32],
4134 start: usize,
4135 task_mask: Option<&TaskMask>,
4136 ) -> (f64, usize) {
4137 self.nll_ids_inner(ids, start, task_mask)
4138 }
4139
4140 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
4141 self.nll_ids_inner(ids, start, None)
4142 }
4143
4144 fn nll_ids_inner(
4145 &mut self,
4146 ids: &[u32],
4147 start: usize,
4148 task_mask: Option<&TaskMask>,
4149 ) -> (f64, usize) {
4150 self.kv_cache.clear();
4151 self.kv_history.clear();
4152 let mut nll = 0f64;
4153 let mut cnt = 0usize;
4154 if self.can_prefill_batched() {
4155 const CHUNK: usize = 128;
4161 const LM_SUB: usize = 32;
4162 let n = ids.len().saturating_sub(1);
4163 let hs = self.hidden_size;
4164 let rows = self.weights.lm_head.rows();
4165 let mut pos = 0usize;
4166 while pos < n {
4167 let end = (pos + CHUNK).min(n);
4168 let bsz = end - pos;
4169 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4170 let mut k0 = 0usize;
4171 while k0 < bsz {
4172 let k1 = (k0 + LM_SUB).min(bsz);
4173 let sb = k1 - k0;
4174 if pos + k1 <= start {
4177 k0 = k1;
4178 continue;
4179 }
4180 let mut normed = vec![0.0f32; sb * hs];
4181 for k in 0..sb {
4182 let r = inference::rms_norm(
4183 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
4184 &self.weights.final_norm,
4185 self.rms_eps,
4186 self.norm_style,
4187 );
4188 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
4189 }
4190 let mut logits = vec![0.0f32; sb * rows];
4191 self.weights
4192 .lm_head
4193 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
4194 for k in 0..sb {
4195 if pos + k0 + k < start {
4196 continue;
4197 }
4198 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
4199 if let Some(mu) = self.logit_multiplier {
4200 for v in lg.iter_mut() {
4201 *v *= mu;
4202 }
4203 }
4204 if let Some(c) = self.final_softcap {
4208 for v in lg.iter_mut() {
4209 *v = c * (*v / c).tanh();
4210 }
4211 }
4212 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
4213 let target = ids[pos + k0 + k + 1] as usize;
4214 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4215 let lse: f64 = lg
4216 .iter()
4217 .map(|&v| ((v - max) as f64).exp())
4218 .sum::<f64>()
4219 .ln()
4220 + max as f64;
4221 nll += lse - lg[target] as f64;
4222 cnt += 1;
4223 if std::env::var("CMF_PPL_TRACE").is_ok() {
4224 let top = lg
4225 .iter()
4226 .enumerate()
4227 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4228 .map(|(i, _)| i)
4229 .unwrap_or(0);
4230 eprintln!(
4231 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
4232 pos + k0 + k,
4233 target,
4234 lse - lg[target] as f64,
4235 top,
4236 lg[target],
4237 lg[top]
4238 );
4239 }
4240 }
4241 k0 = k1;
4242 }
4243 pos = end;
4244 }
4245 self.kv_cache.clear();
4246 self.kv_history.clear();
4247 return (nll, cnt);
4248 }
4249 for pos in 0..ids.len().saturating_sub(1) {
4250 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4251 let out_of_band = self.graph_logits.take();
4259 if pos < start {
4260 continue;
4261 }
4262 let logits = match out_of_band {
4263 Some(lg) => lg,
4264 None => {
4265 let normed = inference::rms_norm(
4266 &hidden,
4267 &self.weights.final_norm,
4268 self.rms_eps,
4269 self.norm_style,
4270 );
4271 self.lm_head_forward(&normed)
4275 }
4276 };
4277 let target = ids[pos + 1] as usize;
4278 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4279 let lse: f64 = logits
4280 .iter()
4281 .map(|&v| ((v - max) as f64).exp())
4282 .sum::<f64>()
4283 .ln()
4284 + max as f64;
4285 let tok_nll = lse - logits[target] as f64;
4286 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4287 let top = logits
4288 .iter()
4289 .enumerate()
4290 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4291 .map(|(i, _)| i)
4292 .unwrap_or(0);
4293 eprintln!(
4294 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4295 logits[target], logits[top]
4296 );
4297 }
4298 nll += tok_nll;
4299 cnt += 1;
4300 }
4301 self.kv_cache.clear();
4302 self.kv_history.clear();
4303 (nll, cnt)
4304 }
4305
4306 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
4322 self.kv_cache.clear();
4323 self.kv_history.clear();
4324 self.o1_begin();
4325 let n = ids.len().saturating_sub(1);
4326 let p = prefill.min(n);
4327 let mut pos = 0usize;
4329 if self.can_prefill_batched() {
4330 const CHUNK: usize = 128;
4331 while pos < p {
4332 let end = (pos + CHUNK).min(p);
4333 let _ = self.prefill_batch(&ids[pos..end], pos);
4334 pos = end;
4335 }
4336 } else {
4337 while pos < p {
4338 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4339 pos += 1;
4340 }
4341 }
4342 self.o1_seal();
4343
4344 let mut nll = 0f64;
4345 let mut cnt = 0usize;
4346 for pos in p..n {
4347 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4348 let normed = inference::rms_norm(
4349 &hidden,
4350 &self.weights.final_norm,
4351 self.rms_eps,
4352 self.norm_style,
4353 );
4354 let logits = self.lm_head_forward(&normed);
4358 let target = ids[pos + 1] as usize;
4359 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4360 let lse: f64 = logits
4361 .iter()
4362 .map(|&v| ((v - max) as f64).exp())
4363 .sum::<f64>()
4364 .ln()
4365 + max as f64;
4366 let tok_nll = lse - logits[target] as f64;
4367 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4368 let top = logits
4369 .iter()
4370 .enumerate()
4371 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4372 .map(|(i, _)| i)
4373 .unwrap_or(0);
4374 eprintln!(
4375 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4376 logits[target], logits[top]
4377 );
4378 }
4379 nll += tok_nll;
4380 cnt += 1;
4381 }
4382 self.kv_cache.clear();
4383 self.kv_history.clear();
4384 (nll, cnt)
4385 }
4386
4387 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
4395 self.kv_cache.clear();
4396 self.kv_history.clear();
4397 let n = ids.len().saturating_sub(1);
4398 let mut correct = Vec::with_capacity(n);
4399 let mut pmax = Vec::with_capacity(n);
4400 for pos in 0..n {
4401 let emb = self.embed_single(ids[pos]);
4402 let hidden = self.forward_layers(&emb, pos, None);
4403 let normed = inference::rms_norm(
4404 &hidden,
4405 &self.weights.final_norm,
4406 self.rms_eps,
4407 self.norm_style,
4408 );
4409 let logits = self.lm_head_forward(&normed);
4413 let target = ids[pos + 1] as usize;
4414 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
4415 for (i, &v) in logits.iter().enumerate() {
4416 if v > mval {
4417 mval = v;
4418 amax = i;
4419 }
4420 }
4421 correct.push(amax == target);
4422 let row: Vec<f32> = temps
4423 .iter()
4424 .map(|&t| {
4425 let tt = t.max(1e-3);
4426 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
4427 1.0 / s.max(1e-12) })
4429 .collect();
4430 pmax.push(row);
4431 }
4432 self.kv_cache.clear();
4433 self.kv_history.clear();
4434 (correct, pmax)
4435 }
4436
4437 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
4444 let mut router = match self.dyn_router.take() {
4445 Some(r) => r,
4446 None => return (self.ppl_ids(ids), 0),
4447 };
4448 router.reset();
4449 self.dyn_phi_seen = 0;
4450 let _ = self.set_active_skill(None);
4451
4452 self.kv_cache.clear();
4453
4454 self.kv_history.clear();
4455 let mut nll = 0f64;
4456 let mut cnt = 0usize;
4457 for pos in 0..ids.len().saturating_sub(1) {
4458 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4459 let normed = inference::rms_norm(
4460 &hidden,
4461 &self.weights.final_norm,
4462 self.rms_eps,
4463 self.norm_style,
4464 );
4465 let logits = self.lm_head_forward(&normed);
4469 let target = ids[pos + 1] as usize;
4470 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4471 let lse: f64 = logits
4472 .iter()
4473 .map(|&v| ((v - max) as f64).exp())
4474 .sum::<f64>()
4475 .ln()
4476 + max as f64;
4477 let tok_nll = lse - logits[target] as f64;
4478 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4479 let top = logits
4480 .iter()
4481 .enumerate()
4482 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4483 .map(|(i, _)| i)
4484 .unwrap_or(0);
4485 eprintln!(
4486 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4487 logits[target], logits[top]
4488 );
4489 }
4490 nll += tok_nll;
4491 cnt += 1;
4492 let phi = self.dyn_phi_ema.clone();
4494 if let Some(new_active) = router.step(&phi, pos) {
4495 let _ = self.set_active_skill(new_active);
4496 }
4497 }
4498 let switches = router.switches.len();
4499 let _ = self.set_active_skill(None);
4500 self.dyn_router = Some(router);
4501 self.kv_cache.clear();
4502 self.kv_history.clear();
4503 ((nll / cnt.max(1) as f64).exp(), switches)
4504 }
4505
4506 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
4508 self.kv_cache.clear();
4509 self.kv_history.clear();
4510 let mut acc = vec![0f32; self.hidden_size];
4511 for (pos, &id) in ids.iter().enumerate() {
4512 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
4513 for (a, v) in acc.iter_mut().zip(&h) {
4514 *a += v;
4515 }
4516 }
4517 let n = ids.len().max(1) as f32;
4518 for a in acc.iter_mut() {
4519 *a /= n;
4520 }
4521 self.kv_cache.clear();
4522 self.kv_history.clear();
4523 acc
4524 }
4525
4526 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
4532 self.prefill_batch_masked(ids, start_pos, None)
4533 }
4534
4535 fn prefill_batch_masked(
4541 &mut self,
4542 ids: &[u32],
4543 start_pos: usize,
4544 task_mask: Option<&TaskMask>,
4545 ) -> Vec<f32> {
4546 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
4547 }
4548
4549 fn prefill_batch_span(
4555 &mut self,
4556 input: PrefillIn<'_>,
4557 start_pos: usize,
4558 task_mask: Option<&TaskMask>,
4559 from: usize,
4560 upto_excl: usize,
4561 ) -> Vec<f32> {
4562 let hs = self.hidden_size;
4563 let b = match input {
4564 PrefillIn::Ids(ids) => ids.len(),
4565 PrefillIn::Hidden(hb) => hb.len() / hs,
4566 };
4567 let upto_excl = upto_excl.min(self.num_layers);
4568 let mut h: Vec<f32>;
4572 let mut h_ready;
4573 match input {
4574 PrefillIn::Ids(_) => {
4575 h = vec![0.0; b * hs];
4576 h_ready = false;
4577 }
4578 PrefillIn::Hidden(hb) => {
4579 h = hb.to_vec();
4580 h_ready = true;
4581 }
4582 }
4583 let fill_h = |h: &mut Vec<f32>, me: &Self| {
4584 if let PrefillIn::Ids(ids) = input {
4585 for (bi, &id) in ids.iter().enumerate() {
4586 let e = me.embed_single(id);
4587 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
4588 }
4589 }
4590 };
4591 let (_nkv, _hd, _rd, eps) = (
4592 self.num_kv_heads,
4593 self.head_dim,
4594 self.rotary_dim,
4595 self.rms_eps,
4596 );
4597 let pool = self.pool.clone();
4598 let norm_style = self.norm_style;
4599
4600 #[cfg(target_os = "macos")]
4601 let mut chunk_skip_until = 0usize;
4602 for li in from..upto_excl {
4603 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
4610 if task_mask.is_none() {
4611 if li < chunk_skip_until {
4612 continue;
4613 }
4614 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
4620 fill_h(&mut h, self);
4621 h_ready = true;
4622 }
4623 let ids_for_embed = match input {
4624 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
4625 PrefillIn::Hidden(_) => None,
4626 };
4627 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
4628 if end > li {
4629 h_ready = true;
4630 chunk_skip_until = end;
4631 if self.is_loop_end(end - 1) && end < self.num_layers {
4634 for bi in 0..b {
4635 let normed = inference::rms_norm(
4636 &h[bi * hs..(bi + 1) * hs],
4637 &self.weights.final_norm,
4638 eps,
4639 norm_style,
4640 );
4641 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
4642 }
4643 }
4644 continue;
4645 }
4646 }
4647 if !h_ready {
4648 fill_h(&mut h, self);
4649 h_ready = true;
4650 }
4651 let lw = &self.weights.layers[self.phys_layer(li)];
4652 match &lw.attn {
4654 AttnKind::Kda(w) => {
4655 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
4657 let mut normed = vec![0.0f32; b * hs];
4658 for bi in 0..b {
4659 inference::rms_norm_into(
4660 &h[bi * hs..(bi + 1) * hs],
4661 &lw.input_norm,
4662 eps,
4663 norm_style,
4664 &mut normed[bi * hs..(bi + 1) * hs],
4665 );
4666 }
4667 let attn = crate::linear_core::kda_forward_batch(
4668 &normed,
4669 b,
4670 w,
4671 &cfg,
4672 &mut self.kv_cache.layers[li].linear_state,
4673 pool.as_deref(),
4674 );
4675 for (dst, &a) in h.iter_mut().zip(&attn) {
4676 *dst += a;
4677 }
4678 }
4679 AttnKind::LinearGdn(w) => {
4680 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4682 let mut normed = vec![0.0f32; b * hs];
4683 for bi in 0..b {
4684 let r = inference::rms_norm(
4685 &h[bi * hs..(bi + 1) * hs],
4686 &lw.input_norm,
4687 eps,
4688 norm_style,
4689 );
4690 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
4691 }
4692 let attn = crate::linear_core::gdn_forward_batch(
4693 &normed,
4694 b,
4695 w,
4696 &cfg,
4697 &mut self.kv_cache.layers[li].linear_state,
4698 pool.as_deref(),
4699 );
4700 for (dst, &a) in h.iter_mut().zip(&attn) {
4701 *dst += a;
4702 }
4703 }
4704 AttnKind::ShortConv(w) => {
4705 let cfg = self
4708 .short_conv_cfg
4709 .expect("short-conv layer without short_conv_cfg");
4710 let mut normed = vec![0.0f32; b * hs];
4711 for bi in 0..b {
4712 inference::rms_norm_into(
4713 &h[bi * hs..(bi + 1) * hs],
4714 &lw.input_norm,
4715 eps,
4716 norm_style,
4717 &mut normed[bi * hs..(bi + 1) * hs],
4718 );
4719 }
4720 let attn = short_conv_forward_batch(
4721 &normed,
4722 b,
4723 w,
4724 &cfg,
4725 &mut self.kv_cache.layers[li].linear_state,
4726 pool.as_deref(),
4727 );
4728 for (dst, &a) in h.iter_mut().zip(&attn) {
4729 *dst += a;
4730 }
4731 }
4732 AttnKind::Mla(w) => {
4733 let inv_freq_l = self.layer_inv_freq(li);
4736 let rs = self.layer_rope_scale(li);
4737 let mut normed = vec![0.0f32; hs];
4738 for bi in 0..b {
4739 inference::rms_norm_into(
4740 &h[bi * hs..(bi + 1) * hs],
4741 &lw.input_norm,
4742 eps,
4743 norm_style,
4744 &mut normed,
4745 );
4746 let ao = mla_attention(
4747 w,
4748 &normed,
4749 &mut self.kv_cache.layers[li],
4750 start_pos + bi,
4751 &inv_freq_l,
4752 rs,
4753 eps,
4754 pool.as_deref(),
4755 );
4756 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
4757 *dst += a;
4758 }
4759 }
4760 }
4761 AttnKind::Full {
4762 wq,
4763 wk,
4764 wv,
4765 wo,
4766 q_norm,
4767 k_norm,
4768 output_gate,
4769 softplus_gate,
4770 bias,
4771 } => {
4772 let mut normed = vec![0.0f32; b * hs];
4776 for bi in 0..b {
4777 inference::rms_norm_into(
4778 &h[bi * hs..(bi + 1) * hs],
4779 &lw.input_norm,
4780 eps,
4781 norm_style,
4782 &mut normed[bi * hs..(bi + 1) * hs],
4783 );
4784 }
4785 let inv_freq_l = self.layer_inv_freq(li);
4786 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4787 let cfg = QwenAttnCfg {
4788 num_heads: self.layer_num_heads(li),
4789 num_kv_heads: nkv_l,
4790 head_dim: hd_l,
4791 hidden_size: hs,
4792 position: start_pos,
4793 inv_freq: &inv_freq_l,
4794 rotary_dim: rd_l,
4795 scale: self.attn_scale,
4796 softcap: self.attn_softcap,
4797 window: self.layer_window(li),
4798 v_norm: self.attn_v_norm,
4799 q_norm: q_norm.as_deref(),
4800 k_norm: k_norm.as_deref(),
4801 output_gate: *output_gate,
4802 softplus_gate: softplus_gate
4803 .as_ref()
4804 .map(|(gate, per_head)| (gate, *per_head)),
4805 rope_scale: self.layer_rope_scale(li),
4806 bias: bias
4807 .as_ref()
4808 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4809 rms_eps: eps,
4810 norm_style,
4811 pool: pool.as_deref(),
4812 };
4813 let mut attn = attention::qwen_attention_batch(
4814 &normed,
4815 b,
4816 wq,
4817 wk,
4818 wv,
4819 wo,
4820 &mut self.kv_cache.layers[li],
4821 &cfg,
4822 );
4823 if let Some(w) = &lw.attn_out_norm {
4824 for bi in 0..b {
4825 inference::rms_norm_into(
4826 &attn[bi * hs..(bi + 1) * hs],
4827 w,
4828 eps,
4829 norm_style,
4830 &mut normed[bi * hs..(bi + 1) * hs],
4831 );
4832 }
4833 attn.copy_from_slice(&normed);
4834 }
4835 for (dst, &a) in h.iter_mut().zip(&attn) {
4836 *dst += a;
4837 }
4838 }
4839 AttnKind::Linear(w) => {
4840 for bi in 0..b {
4841 let normed = inference::rms_norm(
4842 &h[bi * hs..(bi + 1) * hs],
4843 &lw.input_norm,
4844 eps,
4845 norm_style,
4846 );
4847 vmf_phase_forward(
4848 &normed,
4849 w,
4850 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
4851 &mut self.kv_cache.layers[li].linear_state,
4852 pool.as_deref(),
4853 )
4854 .iter()
4855 .enumerate()
4856 .for_each(|(i, &a)| h[bi * hs + i] += a);
4857 }
4858 }
4859 }
4860
4861 let lw = &self.weights.layers[self.phys_layer(li)];
4863 let mut post = vec![0.0f32; b * hs];
4864 for bi in 0..b {
4865 let r =
4866 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
4867 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
4868 }
4869 let mask_row = task_mask
4872 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
4873 .and_then(|m| m.ffn_masks.get(li))
4874 .map(|v| v.as_slice());
4875 let mut ffn = match &lw.ffn {
4876 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
4877 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
4878 FfnKind::DenseMoe(dm) => {
4881 let mut out = vec![0.0f32; b * hs];
4882 for bi in 0..b {
4883 let r = dense_moe_ffn(
4884 dm,
4885 &post[bi * hs..(bi + 1) * hs],
4886 &h[bi * hs..(bi + 1) * hs],
4887 eps,
4888 norm_style,
4889 pool.as_deref(),
4890 );
4891 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
4892 }
4893 out
4894 }
4895 };
4896 if let Some(w) = &lw.ffn_out_norm {
4897 for bi in 0..b {
4898 inference::rms_norm_into(
4899 &ffn[bi * hs..(bi + 1) * hs],
4900 w,
4901 eps,
4902 norm_style,
4903 &mut post[bi * hs..(bi + 1) * hs],
4904 );
4905 }
4906 ffn.copy_from_slice(&post);
4907 }
4908 for (dst, &f) in h.iter_mut().zip(&ffn) {
4909 *dst += f;
4910 }
4911 if let Some(sc) = lw.layer_scale {
4912 for v in h.iter_mut() {
4913 *v *= sc;
4914 }
4915 }
4916 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
4917 if let Some(t) = tp.parse::<usize>().ok() {
4918 if t >= start_pos && t < start_pos + b {
4919 let bi = t - start_pos;
4920 let row = &h[bi * hs..(bi + 1) * hs];
4921 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
4922 eprintln!(
4923 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
4924 row[0], row[1]
4925 );
4926 }
4927 }
4928 }
4929 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
4933 let row = &h[(b - 1) * hs..b * hs];
4934 let rms =
4935 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
4936 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
4937 eprintln!(
4938 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
4939 match &self.weights.layers[self.phys_layer(li)].attn {
4940 AttnKind::LinearGdn(_) => "gdn",
4941 AttnKind::Linear(_) => "vmf",
4942 AttnKind::ShortConv(_) => "conv",
4943 _ => "attn",
4944 },
4945 match &lw.ffn {
4946 FfnKind::Moe(_) => "moe",
4947 FfnKind::Dense(_) => "dense",
4948 FfnKind::DenseMoe(_) => "dense+moe",
4949 },
4950 );
4951 }
4952 if self.is_loop_end(li) && li + 1 < self.num_layers {
4954 for bi in 0..b {
4955 let normed = inference::rms_norm(
4956 &h[bi * hs..(bi + 1) * hs],
4957 &self.weights.final_norm,
4958 eps,
4959 norm_style,
4960 );
4961 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
4962 }
4963 }
4964 if std::env::var("CMF_TRACE_H").is_ok() {
4965 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
4966 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
4967 eprintln!(
4968 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
4969 lw.layer_scale
4970 );
4971 }
4972 }
4973 crate::gpu::set_layer(-1); h
4975 }
4976
4977 fn embed_single(&self, id: u32) -> Vec<f32> {
4979 let mut out = vec![0.0f32; self.hidden_size];
4980 if (id as usize) < self.weights.embed_tokens.rows() {
4981 self.weights.embed_tokens.row_f32(id as usize, &mut out);
4982 }
4983 if self.embed_multiplier != 1.0 {
4984 for v in out.iter_mut() {
4985 *v *= self.embed_multiplier;
4986 }
4987 }
4988 if self.dsv4.is_some() {
4992 let mut v = vec![0.0f32; self.hidden_size.max(1)];
4993 v[0] = id as f32;
4994 return v;
4995 }
4996 if let Some(b) = &self.g3n {
4999 return b.0.extend_embedding(id, &out, self.pool.as_deref());
5000 }
5001 out
5002 }
5003
5004 #[cfg(target_os = "macos")]
5010 fn chunk_run_gpu(
5011 &mut self,
5012 li0: usize,
5013 h: &mut [f32],
5014 b: usize,
5015 pos0: usize,
5016 embed_ids: Option<&[u32]>,
5017 cap: usize,
5018 ) -> usize {
5019 if !crate::gpu::enabled_here()
5023 || std::env::var("CMF_GPU_CHUNK")
5024 .map(|v| v == "0")
5025 .unwrap_or(false)
5026 || b < 32
5027 || self.swa.is_some()
5028 || self.global_attn.is_some()
5029 || self.attn_v_norm
5030 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
5031 {
5032 return li0;
5033 }
5034 let Some(model) = self.model.clone() else {
5035 return li0;
5036 };
5037 let inv_freq = self.inv_freq.clone();
5038 let (nh, nkv, hd, hs) = (
5039 self.num_heads,
5040 self.num_kv_heads,
5041 self.head_dim,
5042 self.hidden_size,
5043 );
5044 let loop_end = if self.loop_final_norm {
5048 ((li0 / self.physical_layers) + 1) * self.physical_layers
5049 } else {
5050 self.num_layers
5051 };
5052 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
5053 let mut stored_at: Vec<usize> = Vec::new();
5054 for li in li0..self.num_layers.min(loop_end).min(cap) {
5055 let lw = &self.weights.layers[self.phys_layer(li)];
5056 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
5057 break;
5058 }
5059 let AttnKind::Full {
5060 wq,
5061 wk,
5062 wv,
5063 wo,
5064 q_norm,
5065 k_norm,
5066 output_gate: false,
5067 softplus_gate: None,
5068 bias,
5069 } = &lw.attn
5070 else {
5071 break;
5072 };
5073 let FfnKind::Dense(d) = &lw.ffn else { break };
5074 if d.act != Act::Silu {
5075 break;
5076 }
5077 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
5082 t.q8_row_parts()
5083 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5084 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5085 }
5086 let parts = (
5087 cw(wq),
5088 cw(wk),
5089 cw(wv),
5090 cw(wo),
5091 cw(&d.gate_proj),
5092 cw(&d.up_proj),
5093 cw(&d.down_proj),
5094 );
5095 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
5096 else {
5097 break;
5098 };
5099 let layer = &self.kv_cache.layers[li];
5100 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
5101 break;
5102 }
5103 stored_at.push(layer.head_len(0));
5104 layers.push(crate::gpu_metal::ChunkLayer {
5105 model: &model,
5106 kv_id: self.graph_kv_id,
5107 layer: li,
5108 wq: pq,
5109 wk: pk,
5110 wv: pv,
5111 wo: po,
5112 gate: pg,
5113 up: pu,
5114 down: pd,
5115 input_norm: &lw.input_norm,
5116 post_norm: &lw.post_norm,
5117 bias: bias
5118 .as_ref()
5119 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
5120 q_norm: q_norm.as_deref(),
5121 k_norm: k_norm.as_deref(),
5122 inv_freq: &inv_freq,
5123 rd: self.rotary_dim,
5124 nh,
5125 nkv,
5126 hd,
5127 hs,
5128 inter: d.gate_proj.rows(),
5129 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
5130 eps: self.rms_eps as f32,
5131 });
5132 }
5133 if layers.is_empty() {
5134 return li0;
5135 }
5136 let row = nkv * hd;
5137 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
5138 .iter()
5139 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
5140 .collect();
5141 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
5142 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
5143 let li = layers[i].layer;
5144 let layer = &self.kv_cache.layers[li];
5145 io.push(crate::gpu_metal::ChunkIo {
5146 cpu_stored: stored_at[i],
5147 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
5148 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
5149 out_k: ok,
5150 out_v: ov,
5151 imp: oi,
5152 });
5153 }
5154 let n_run = layers.len();
5155 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
5156 let ep = embed_ids.and_then(|ids| {
5159 self.weights
5160 .embed_tokens
5161 .q8_row_parts()
5162 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
5163 idx,
5164 rows,
5165 row_scale: rs,
5166 ids,
5167 mult: self.embed_multiplier,
5168 })
5169 });
5170 if embed_ids.is_some() && ep.is_none() {
5171 return li0;
5172 }
5173 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
5174 return li0;
5175 }
5176 drop(io);
5177 drop(layers);
5178 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
5181 let li = li0 + i;
5182 let layer = &mut self.kv_cache.layers[li];
5183 for bi in 0..b {
5184 layer.append(
5185 &ok[bi * row..(bi + 1) * row],
5186 &ov[bi * row..(bi + 1) * row],
5187 &[],
5188 );
5189 }
5190 layer.accumulate_imp(oi);
5191 }
5192 last
5193 }
5194
5195 fn layer_is_local(&self, li: usize) -> bool {
5198 if let Some(layers) = &self.sliding_layers {
5199 return layers.get(li).copied().unwrap_or(false);
5200 }
5201 match self.swa {
5202 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
5203 None => false,
5204 }
5205 }
5206
5207 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
5210 if self.layer_is_local(li) {
5211 if let Some(f) = &self.inv_freq_local {
5212 return f.clone();
5213 }
5214 } else if let Some(f) = &self.inv_freq_global {
5215 return f.clone();
5216 }
5217 self.inv_freq.clone()
5218 }
5219
5220 fn layer_window(&self, li: usize) -> Option<usize> {
5222 self.swa
5223 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
5224 }
5225
5226 fn layer_num_heads(&self, li: usize) -> usize {
5227 self.attention_heads_per_layer
5228 .as_ref()
5229 .and_then(|v| v.get(li).copied())
5230 .unwrap_or(self.num_heads)
5231 }
5232
5233 fn layer_rope_scale(&self, li: usize) -> f32 {
5234 if self.layer_is_local(li) {
5235 self.rope_scale_local
5236 } else {
5237 self.rope_scale
5238 }
5239 }
5240
5241 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
5244 if !self.layer_is_local(li) {
5245 if let Some((ghd, gkv)) = self.global_attn {
5246 return (gkv, ghd, ghd);
5247 }
5248 }
5249 (
5250 self.num_kv_heads,
5251 self.head_dim,
5252 if self.layer_is_local(li) {
5253 self.rotary_dim_local.unwrap_or(self.rotary_dim)
5254 } else {
5255 self.rotary_dim
5256 },
5257 )
5258 }
5259
5260 fn forward_layers(
5262 &mut self,
5263 hidden: &[f32],
5264 position: usize,
5265 task_mask: Option<&TaskMask>,
5266 ) -> Vec<f32> {
5267 self.forward_layers_upto(hidden, position, task_mask, None)
5268 }
5269
5270 pub fn embed_id(&self, id: u32) -> Vec<f32> {
5278 self.embed_single(id)
5279 }
5280
5281 pub fn split_supported(&self) -> Result<(), String> {
5285 if self.dsv4.is_some() {
5286 return Err(
5287 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
5288 );
5289 }
5290 if self.g3n.is_some() {
5291 return Err(
5292 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
5293 );
5294 }
5295 Ok(())
5296 }
5297
5298 pub fn forward_span(
5303 &mut self,
5304 hidden: &[f32],
5305 position: usize,
5306 from: usize,
5307 upto: usize,
5308 task_mask: Option<&TaskMask>,
5309 ) -> Result<Vec<f32>, String> {
5310 self.split_supported()?;
5311 if from > upto || upto >= self.num_layers {
5312 return Err(format!(
5313 "forward_span: layer range {from}..={upto} outside 0..{}",
5314 self.num_layers
5315 ));
5316 }
5317 if hidden.len() != self.hidden_size {
5318 return Err(format!(
5319 "forward_span: hidden len {} ≠ hidden_size {}",
5320 hidden.len(),
5321 self.hidden_size
5322 ));
5323 }
5324 Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
5325 }
5326
5327 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
5330 let normed = inference::rms_norm(
5331 hidden,
5332 &self.weights.final_norm,
5333 self.rms_eps,
5334 self.norm_style,
5335 );
5336 self.lm_head_forward(&normed)
5337 }
5338
5339 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
5341 sampler::sample_with_scratch(
5342 logits,
5343 &self.sampler_config,
5344 past_tokens,
5345 &mut self.rng,
5346 &mut self.sampler_scratch,
5347 )
5348 }
5349
5350 pub fn reset_session(&mut self) {
5352 self.kv_cache.clear();
5353 self.kv_history.clear();
5354 crate::gpu::graph_kv_reset(self.graph_kv_id);
5355 }
5356
5357 pub fn prefill_span_ids(
5363 &mut self,
5364 ids: &[u32],
5365 start_pos: usize,
5366 upto: usize,
5367 task_mask: Option<&TaskMask>,
5368 ) -> Result<Vec<f32>, String> {
5369 self.split_supported()?;
5370 if upto >= self.num_layers {
5371 return Err(format!(
5372 "prefill_span_ids: upto {upto} outside 0..{}",
5373 self.num_layers
5374 ));
5375 }
5376 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5380 Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
5381 } else {
5382 let hs = self.hidden_size;
5383 let mut out = Vec::with_capacity(ids.len() * hs);
5384 for (i, &id) in ids.iter().enumerate() {
5385 let emb = self.embed_id(id);
5386 out.extend_from_slice(&self.forward_span(
5387 &emb,
5388 start_pos + i,
5389 0,
5390 upto,
5391 task_mask,
5392 )?);
5393 }
5394 Ok(out)
5395 }
5396 }
5397
5398 pub fn prefill_span_hidden(
5401 &mut self,
5402 hidden: &[f32],
5403 start_pos: usize,
5404 from: usize,
5405 upto: usize,
5406 task_mask: Option<&TaskMask>,
5407 ) -> Result<Vec<f32>, String> {
5408 self.split_supported()?;
5409 let hs = self.hidden_size;
5410 if hidden.is_empty() || hidden.len() % hs != 0 {
5411 return Err(format!(
5412 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
5413 hidden.len()
5414 ));
5415 }
5416 if from > upto || upto >= self.num_layers {
5417 return Err(format!(
5418 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
5419 self.num_layers
5420 ));
5421 }
5422 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5423 Ok(self.prefill_batch_span(
5424 PrefillIn::Hidden(hidden),
5425 start_pos,
5426 task_mask,
5427 from,
5428 upto + 1,
5429 ))
5430 } else {
5431 let b = hidden.len() / hs;
5432 let mut out = Vec::with_capacity(hidden.len());
5433 for i in 0..b {
5434 let h = self.forward_span(
5435 &hidden[i * hs..(i + 1) * hs],
5436 start_pos + i,
5437 from,
5438 upto,
5439 task_mask,
5440 )?;
5441 out.extend_from_slice(&h);
5442 }
5443 Ok(out)
5444 }
5445 }
5446
5447 fn try_token_graph_wgpu(
5451 &self,
5452 hidden: &[f32],
5453 position: usize,
5454 logits_out: &mut Vec<f32>,
5455 layers_run: &mut usize,
5456 ) -> Option<Vec<f32>> {
5457 self.try_token_graph_wgpu_steps(
5458 hidden,
5459 position,
5460 logits_out,
5461 1,
5462 None,
5463 Some(layers_run),
5464 0,
5465 self.num_layers,
5466 )
5467 }
5468
5469 fn try_token_graph_wgpu_span(
5473 &self,
5474 hidden: &[f32],
5475 position: usize,
5476 logits_out: &mut Vec<f32>,
5477 from: usize,
5478 upto_excl: usize,
5479 layers_run: &mut usize,
5480 ) -> Option<Vec<f32>> {
5481 self.try_token_graph_wgpu_steps(
5482 hidden,
5483 position,
5484 logits_out,
5485 1,
5486 None,
5487 Some(layers_run),
5488 from,
5489 upto_excl,
5490 )
5491 }
5492
5493 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
5497 if self.o1_active() || self.attn_softcap > 0.0 {
5498 return None;
5499 }
5500 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5501 if !graph_on || crate::gpu::graph_unsupported() {
5502 return None;
5509 }
5510 let emb = self.embed_single(t_next);
5511 let mut lg = Vec::new();
5512 let mut ids = Vec::new();
5513 self.try_token_graph_wgpu_steps(
5514 &emb,
5515 position,
5516 &mut lg,
5517 k,
5518 Some(&mut ids),
5519 None,
5520 0,
5521 self.num_layers,
5522 )?;
5523 (ids.len() == k).then_some(ids)
5524 }
5525
5526 fn try_token_graph_wgpu_steps(
5530 &self,
5531 hidden: &[f32],
5532 position: usize,
5533 logits_out: &mut Vec<f32>,
5534 steps: usize,
5535 ids_out: Option<&mut Vec<u32>>,
5536 layers_run: Option<&mut usize>,
5537 from: usize,
5538 upto_excl: usize,
5539 ) -> Option<Vec<f32>> {
5540 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
5543 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
5544 return None;
5548 }
5549 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
5554 .map(|li| {
5555 if !o1_gpu {
5556 return None;
5557 }
5558 self.kv_cache.layers[self.phys_layer(li)].o1_views()
5559 })
5560 .collect();
5561 if self.o1_active() && o1_gpu {
5562 let want: usize = (from..upto_excl)
5565 .filter(|li| !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None))
5566 .count();
5567 let have = o1_views.iter().filter(|v| v.is_some()).count();
5568 if want == 0 || have != want {
5569 use std::sync::atomic::{AtomicUsize, Ordering};
5579 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
5580 let code = have * 1000 + want;
5581 if LAST.swap(code, Ordering::Relaxed) != code {
5582 tracing::warn!(
5583 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
5584 );
5585 }
5586 return None;
5587 }
5588 }
5589 let nh = self.num_heads;
5590 let (nkv, hd, rd) = self.layer_geom(0);
5591 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5592 let mut layers = Vec::with_capacity(upto_excl - from);
5593 let mut model = None;
5594 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
5595 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5596 if let Some((_, i, kind, rs)) = t.graph_weight() {
5597 return Some(crate::gpu::GraphW {
5598 idx: i,
5599 kind,
5600 row_scale: rs,
5601 data: &[],
5602 });
5603 }
5604 t.as_f32().map(|d| crate::gpu::GraphW {
5606 idx: 0,
5607 kind: 4,
5608 row_scale: &[],
5609 data: d,
5610 })
5611 }
5612 for li in from..upto_excl {
5613 let lw = &self.weights.layers[self.phys_layer(li)];
5614 if dbg {
5615 let ak = match &lw.attn {
5616 AttnKind::Mla(_) => "Mla".into(),
5617 AttnKind::Full {
5618 output_gate, bias, ..
5619 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
5620 AttnKind::LinearGdn(_) => "LinearGdn".into(),
5621 AttnKind::Kda(_) => "Kda".into(),
5622 AttnKind::Linear(_) => "Linear".into(),
5623 AttnKind::ShortConv(_) => "ShortConv".into(),
5624 };
5625 let fk = match &lw.ffn {
5626 FfnKind::Dense(_) => "Dense",
5627 FfnKind::Moe(_) => "Moe",
5628 FfnKind::DenseMoe(_) => "DenseMoe",
5629 };
5630 eprintln!("graph L{li}: attn={ak} ffn={fk}");
5631 }
5632 let gffn = match &lw.ffn {
5633 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
5635 gate: gw(&d.gate_proj)?,
5636 up: gw(&d.up_proj)?,
5637 down: gw(&d.down_proj)?,
5638 },
5639 FfnKind::Moe(m) => {
5640 if m.router_sigmoid
5645 || m.expert_bias.is_some()
5646 || m.route_tau.is_some()
5647 || m.mask.is_some()
5648 {
5649 return None;
5650 }
5651 let (se, sg) = m.shared.as_ref()?;
5652 let sgate = gw(sg.as_ref()?)?;
5653 let router = gw(&m.router)?;
5654 let inter = m.experts.first()?.gate_proj.rows();
5655 let mut experts = Vec::with_capacity(m.experts.len() + 1);
5656 let mut q4tp: Option<bool> = None;
5659 let mut gu_q2: Option<bool> = None;
5662 for e in m.experts.iter().chain(std::iter::once(se)) {
5663 if !matches!(e.act, Act::Silu)
5664 || e.gate_proj.rows() != inter
5665 || e.up_proj.rows() != inter
5666 {
5667 return None;
5668 }
5669 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
5670 Some((mm, gi)) => (
5671 mm,
5672 gi,
5673 e.up_proj.mapped_q4t()?.1,
5674 e.down_proj.mapped_q4t()?.1,
5675 false,
5676 false,
5677 ),
5678 None => match e.gate_proj.mapped_q2tp() {
5679 Some((mm, gi)) => (
5680 mm,
5681 gi,
5682 e.up_proj.mapped_q2tp()?.1,
5683 e.down_proj.mapped_q4tp()?.1,
5684 true,
5685 true,
5686 ),
5687 None => {
5688 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
5689 (
5690 mm,
5691 gi,
5692 e.up_proj.mapped_q4tp()?.1,
5693 e.down_proj.mapped_q4tp()?.1,
5694 true,
5695 false,
5696 )
5697 }
5698 },
5699 };
5700 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
5701 {
5702 tracing::warn!(
5708 "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."
5709 );
5710 return None;
5711 }
5712 model.get_or_insert_with(|| mm.clone());
5713 experts.push((gi, ui, di));
5714 }
5715 crate::gpu::GraphFfn::Moe {
5716 router,
5717 shared_gate: sgate,
5718 experts,
5719 n_exp: m.experts.len(),
5720 top_k: std::env::var("CMF_TOPK_PROBE")
5726 .ok()
5727 .and_then(|v| v.parse::<usize>().ok())
5728 .filter(|k| *k > 0 && *k <= m.top_k)
5729 .unwrap_or(m.top_k),
5730 inter,
5731 norm_topk: m.norm_topk_prob,
5732 q4tp: q4tp?,
5733 gu_q2: gu_q2.unwrap_or(false),
5734 }
5735 }
5736 };
5737 let attn = match &lw.attn {
5738 AttnKind::Full {
5739 wq,
5740 wk,
5741 wv,
5742 wo,
5743 q_norm,
5744 k_norm,
5745 output_gate,
5746 softplus_gate,
5747 bias,
5748 } => {
5749 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
5750 return None;
5751 }
5752 let (m, _, _, _) = wq.graph_weight()?;
5753 model = Some(m.clone());
5754 crate::gpu::GraphAttn::Full {
5755 wq: gw(wq)?,
5756 wk: gw(wk)?,
5757 wv: gw(wv)?,
5758 wo: gw(wo)?,
5759 q_norm: q_norm.as_deref(),
5760 k_norm: k_norm.as_deref(),
5761 bias: bias
5762 .as_ref()
5763 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5764 output_gate: *output_gate,
5765 cpu_k: self.kv_cache.layers[li].k_heads(),
5766 cpu_v: self.kv_cache.layers[li].v_heads(),
5767 }
5768 }
5769 AttnKind::LinearGdn(w) => {
5770 let cfg = self.gdn_cfg?;
5771 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
5772 model = Some(m.clone());
5773 crate::gpu::GraphAttn::Gdn {
5774 qkv: gw(&w.in_proj_qkv)?,
5775 z: gw(&w.in_proj_z)?,
5776 a: gw(&w.in_proj_a)?,
5777 b: gw(&w.in_proj_b)?,
5778 out: gw(&w.out_proj)?,
5779 conv1d: &w.conv1d,
5780 a_log: &w.a_log,
5781 dt_bias: &w.dt_bias,
5782 norm: &w.norm,
5783 nv: cfg.num_v_heads,
5784 nk: cfg.num_k_heads,
5785 dk: cfg.key_head_dim,
5786 dv: cfg.value_head_dim,
5787 kk: cfg.conv_kernel,
5788 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
5789 }
5790 }
5791 _ => return None,
5792 };
5793 layers.push(crate::gpu::GraphLayer {
5794 input_norm: &lw.input_norm,
5795 attn,
5796 post_norm: &lw.post_norm,
5797 ffn: gffn,
5798 });
5799 }
5800 let model = model?;
5801 let lm_gw = if upto_excl == self.num_layers
5807 && self.graph_want_logits
5808 && std::env::var("CMF_GPU_LMHEAD")
5809 .map(|v| v != "0")
5810 .unwrap_or(true)
5811 {
5812 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
5813 (
5814 crate::gpu::GraphW {
5815 idx: i,
5816 kind,
5817 row_scale: rs,
5818 data: &[],
5819 },
5820 self.weights.lm_head.rows(),
5821 )
5822 })
5823 } else {
5824 None
5825 };
5826 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
5827 let emb_gw = if steps > 1 {
5829 self.weights
5830 .embed_tokens
5831 .graph_weight()
5832 .map(|(_, i, kind, rs)| {
5833 (
5834 crate::gpu::GraphW {
5835 idx: i,
5836 kind,
5837 row_scale: rs,
5838 data: &[],
5839 },
5840 self.weights.embed_tokens.rows(),
5841 self.embed_multiplier as f32,
5842 )
5843 })
5844 } else {
5845 None
5846 };
5847
5848 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
5854 (from..upto_excl.min(self.num_layers - 1))
5855 .filter(|&li| (li + 1) % self.physical_layers == 0)
5856 .map(|li| li - from)
5857 .collect()
5858 } else {
5859 Vec::new()
5860 };
5861 let mut h = hidden.to_vec();
5862 crate::gpu::forward_token_graph(
5863 &model,
5864 self.graph_kv_id,
5865 &layers,
5866 &o1_views,
5867 self.o1_epoch,
5868 &self.inv_freq,
5869 &mut h,
5870 nh,
5871 nkv,
5872 hd,
5873 rd,
5874 self.hidden_size,
5875 self.intermediate_size,
5876 position,
5877 self.kv_cache.max_seq_len,
5878 gemma,
5879 self.rms_eps as f32,
5880 lm,
5881 &self.weights.final_norm,
5882 logits_out,
5883 &loop_norm_at,
5884 steps,
5885 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
5886 ids_out,
5887 layers_run,
5888 from,
5889 false,
5890 )
5891 .then_some(h)
5892 }
5893
5894 fn try_batch_graph_wgpu(
5899 &self,
5900 hiddens: &mut [f32],
5901 positions: &[usize],
5902 k: usize,
5903 spec: Option<crate::gpu::SpecTail<'_>>,
5904 ) -> bool {
5905 let _tb = std::time::Instant::now();
5906 if self.attn_softcap > 0.0 {
5907 return false; }
5909 if self.o1_active() {
5910 return false;
5911 }
5912 let nh = self.num_heads;
5913 let (nkv, hd, rd) = self.layer_geom(0);
5914 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5915 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5916 if let Some((_, i, kind, rs)) = t.graph_weight() {
5917 return Some(crate::gpu::GraphW {
5918 idx: i,
5919 kind,
5920 row_scale: rs,
5921 data: &[],
5922 });
5923 }
5924 t.as_f32().map(|d| crate::gpu::GraphW {
5925 idx: 0,
5926 kind: 4,
5927 row_scale: &[],
5928 data: d,
5929 })
5930 }
5931 let built: Option<(
5932 Vec<crate::gpu::GraphLayer<'_>>,
5933 std::sync::Arc<cortiq_core::CmfModel>,
5934 )> = (|| {
5935 let mut layers = Vec::with_capacity(self.num_layers);
5936 let mut model = None;
5937 for li in 0..self.num_layers {
5938 let lw = &self.weights.layers[self.phys_layer(li)];
5939 let gffn = match &lw.ffn {
5946 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
5947 gate: gw(&d.gate_proj)?,
5948 up: gw(&d.up_proj)?,
5949 down: gw(&d.down_proj)?,
5950 },
5951 FfnKind::Moe(m) => {
5952 if m.router_sigmoid
5953 || m.expert_bias.is_some()
5954 || m.route_tau.is_some()
5955 || m.mask.is_some()
5956 {
5957 return None;
5958 }
5959 let (se, sg) = m.shared.as_ref()?;
5960 let sgate = gw(sg.as_ref()?)?;
5961 let router = gw(&m.router)?;
5962 let inter = m.experts.first()?.gate_proj.rows();
5963 let mut experts = Vec::with_capacity(m.experts.len() + 1);
5964 let mut q4tp: Option<bool> = None;
5965 let mut gu_q2: Option<bool> = None;
5966 for e in m.experts.iter().chain(std::iter::once(se)) {
5967 if !matches!(e.act, Act::Silu)
5968 || e.gate_proj.rows() != inter
5969 || e.up_proj.rows() != inter
5970 {
5971 return None;
5972 }
5973 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
5977 Some((mm, gi)) => (
5978 mm,
5979 gi,
5980 e.up_proj.mapped_q4t()?.1,
5981 e.down_proj.mapped_q4t()?.1,
5982 false,
5983 false,
5984 ),
5985 None => match e.gate_proj.mapped_q2tp() {
5986 Some((mm, gi)) => (
5987 mm,
5988 gi,
5989 e.up_proj.mapped_q2tp()?.1,
5990 e.down_proj.mapped_q4tp()?.1,
5991 true,
5992 true,
5993 ),
5994 None => {
5995 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
5996 (
5997 mm,
5998 gi,
5999 e.up_proj.mapped_q4tp()?.1,
6000 e.down_proj.mapped_q4tp()?.1,
6001 true,
6002 false,
6003 )
6004 }
6005 },
6006 };
6007 if *q4tp.get_or_insert(is_p) != is_p
6008 || *gu_q2.get_or_insert(is_q2) != is_q2
6009 {
6010 return None;
6011 }
6012 model.get_or_insert_with(|| mm.clone());
6013 experts.push((gi, ui, di));
6014 }
6015 crate::gpu::GraphFfn::Moe {
6016 router,
6017 shared_gate: sgate,
6018 experts,
6019 n_exp: m.experts.len(),
6020 top_k: m.top_k,
6021 inter,
6022 norm_topk: m.norm_topk_prob,
6023 q4tp: q4tp?,
6024 gu_q2: gu_q2.unwrap_or(false),
6025 }
6026 }
6027 _ => return None,
6028 };
6029 let attn = match &lw.attn {
6030 AttnKind::Full {
6031 wq,
6032 wk,
6033 wv,
6034 wo,
6035 q_norm,
6036 k_norm,
6037 output_gate,
6038 softplus_gate,
6039 bias,
6040 } => {
6041 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
6042 return None;
6043 }
6044 let (m, _, _, _) = wq.graph_weight()?;
6045 model = Some(m.clone());
6046 crate::gpu::GraphAttn::Full {
6047 wq: gw(wq)?,
6048 wk: gw(wk)?,
6049 wv: gw(wv)?,
6050 wo: gw(wo)?,
6051 q_norm: q_norm.as_deref(),
6052 k_norm: k_norm.as_deref(),
6053 bias: bias
6054 .as_ref()
6055 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6056 output_gate: *output_gate,
6057 cpu_k: self.kv_cache.layers[li].k_heads(),
6058 cpu_v: self.kv_cache.layers[li].v_heads(),
6059 }
6060 }
6061 AttnKind::LinearGdn(w) => {
6062 let cfg = self.gdn_cfg?;
6063 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
6064 model = Some(m.clone());
6065 crate::gpu::GraphAttn::Gdn {
6066 qkv: gw(&w.in_proj_qkv)?,
6067 z: gw(&w.in_proj_z)?,
6068 a: gw(&w.in_proj_a)?,
6069 b: gw(&w.in_proj_b)?,
6070 out: gw(&w.out_proj)?,
6071 conv1d: &w.conv1d,
6072 a_log: &w.a_log,
6073 dt_bias: &w.dt_bias,
6074 norm: &w.norm,
6075 nv: cfg.num_v_heads,
6076 nk: cfg.num_k_heads,
6077 dk: cfg.key_head_dim,
6078 dv: cfg.value_head_dim,
6079 kk: cfg.conv_kernel,
6080 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6081 }
6082 }
6083 _ => return None,
6084 };
6085 layers.push(crate::gpu::GraphLayer {
6086 input_norm: &lw.input_norm,
6087 attn,
6088 post_norm: &lw.post_norm,
6089 ffn: gffn,
6090 });
6091 }
6092 Some((layers, model?))
6093 })();
6094 let Some((layers, model)) = built else {
6095 {
6096 use std::sync::atomic::{AtomicBool, Ordering};
6097 static SAID: AtomicBool = AtomicBool::new(false);
6098 if !SAID.swap(true, Ordering::Relaxed) {
6099 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
6100 }
6101 }
6102 return false;
6103 };
6104 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6105 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
6106 }
6107 crate::gpu::forward_batch_graph(
6108 &model,
6109 self.graph_kv_id,
6110 &layers,
6111 &self.inv_freq,
6112 hiddens,
6113 nh,
6114 nkv,
6115 hd,
6116 rd,
6117 self.hidden_size,
6118 self.intermediate_size,
6119 positions,
6120 self.kv_cache.max_seq_len,
6121 gemma,
6122 self.rms_eps as f32,
6123 k,
6124 spec,
6125 )
6126 }
6127
6128 fn draft_probe() -> bool {
6132 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6133 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
6134 }
6135
6136 #[cfg(feature = "gpu")]
6148 fn dsv4_spec_on() -> bool {
6149 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6150 *ON.get_or_init(|| {
6151 std::env::var("CMF_DSV4_SPEC")
6152 .map(|v| v != "0")
6153 .unwrap_or(true)
6154 })
6155 }
6156
6157 #[cfg(feature = "gpu")]
6164 fn dsv4_spec_step(
6165 &mut self,
6166 tip_token: u32,
6167 t_next: u32,
6168 next_pos: usize,
6169 drafted: &mut usize,
6170 accepted_ctr: &mut usize,
6171 ) -> Option<(Vec<u32>, usize)> {
6172 let t_all = std::time::Instant::now();
6173 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
6174 thread_local! {
6175 static LAST: std::cell::Cell<Option<std::time::Instant>> =
6176 const { std::cell::Cell::new(None) };
6177 }
6178 LAST.with(|l| {
6179 if let Some(prev) = l.get() {
6180 eprintln!(
6181 "между раундами {:.1} мс",
6182 prev.elapsed().as_secs_f64() * 1e3
6183 );
6184 }
6185 l.set(Some(std::time::Instant::now()));
6186 });
6187 }
6188 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
6189 eprintln!("spec_step: вход pos={next_pos}");
6190 }
6191 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
6192 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
6193 if self.dspark.is_none() {
6195 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
6196 if t.is_empty() {
6197 return None;
6198 }
6199 crate::dsv4::dspark_arm(&t, cfg.dim);
6200 self.dspark = Some(crate::dsv4::DsparkState::new(
6201 self.dsv4_mtp.len(),
6202 &cfg,
6203 t.len(),
6204 ));
6205 }
6206 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
6207 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
6208 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
6209 eprintln!("spec_step: пак не построился (targets {targets:?})");
6210 }
6211 let pack = pack?;
6212 let block = crate::dsv4::dspark_block();
6213 let b_box = self.dsv4.as_mut()?;
6214 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
6215 let ds = self.dspark.as_mut()?;
6216 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
6219 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
6220 if dbg {
6221 eprintln!("spec_step: нет захвата");
6222 }
6223 return None;
6224 }
6225 ds.have_hidden = true;
6226 let tip_pos = next_pos.checked_sub(1)?;
6227 let draft_started = std::time::Instant::now();
6228 let mut conf = Vec::new();
6229 let props = crate::dsv4::dspark_draft_gpu(
6230 g,
6231 &self.dsv4_mtp,
6232 &cfg,
6233 ds,
6234 pack,
6235 st.kv_id,
6236 tip_token,
6237 tip_pos,
6238 self.pool.as_deref(),
6239 &mut conf,
6240 );
6241 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
6242 *drafted += block;
6243 if props.is_empty() || props[0] != t_next {
6244 if dbg {
6245 eprintln!(
6246 "spec_step: черновик {} (props0={:?} t_next={t_next})",
6247 if props.is_empty() {
6248 "пуст"
6249 } else {
6250 "мимо"
6251 },
6252 props.first()
6253 );
6254 }
6255 return None;
6256 }
6257 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
6258 let conf_min = {
6264 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
6265 *M.get_or_init(|| {
6266 std::env::var("CMF_DSPARK_CONF_MIN")
6267 .ok()
6268 .and_then(|v| v.parse().ok())
6269 .unwrap_or(0.0)
6270 })
6271 };
6272 if conf_min > 0.0 && conf.len() >= props.len() {
6273 let mut keep = 1usize;
6274 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
6275 keep += 1;
6276 }
6277 k_verify = k_verify.min(keep.max(2));
6278 }
6279 if k_verify < 2 {
6280 return None;
6281 }
6282 let mut fed = Vec::with_capacity(k_verify);
6283 fed.push(t_next);
6284 fed.extend_from_slice(&props[1..k_verify]);
6285 let mut argmax = Vec::new();
6286 let mut logits_all = Vec::new();
6287 let mut walked = Vec::new();
6288 let txn = crate::dsv4::dsv4_verify_chunk(
6289 g,
6290 layers,
6291 &cfg,
6292 st,
6293 &fed,
6294 next_pos,
6295 &self.inv_freq,
6296 self.pool.as_deref(),
6297 &targets,
6298 &mut argmax,
6299 &mut logits_all,
6300 &mut walked,
6301 );
6302 if txn.is_none() && dbg {
6303 eprintln!("spec_step: verify отказал");
6304 }
6305 let txn = txn?;
6306 let b = fed.len();
6307 let mut accepted = 1usize;
6308 while accepted < b && fed[accepted] == argmax[accepted - 1] {
6309 accepted += 1;
6310 }
6311 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
6316 accepted = 1;
6317 }
6318 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
6319 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
6320 }
6321 let t_fin = std::time::Instant::now();
6322 if !crate::dsv4::dsv4_spec_finish(
6323 g,
6324 layers,
6325 &cfg,
6326 st,
6327 txn,
6328 accepted,
6329 &fed,
6330 &self.inv_freq,
6331 self.pool.as_deref(),
6332 ) {
6333 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
6334 return None;
6335 }
6336 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
6337 eprintln!(
6338 "finish(k={accepted}): {:.1} мс",
6339 t_fin.elapsed().as_secs_f64() * 1e3
6340 );
6341 }
6342 *accepted_ctr += accepted - 1;
6343 let (hc, dim) = (cfg.hc_mult, cfg.dim);
6348 let dev_caps: Vec<usize> = targets
6355 .iter()
6356 .copied()
6357 .filter(|&t| {
6358 st.dev_set.get(t).copied().unwrap_or(false)
6359 && !st.partial_set.get(t).copied().unwrap_or(false)
6360 })
6361 .collect();
6362 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
6363 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
6364 return None;
6365 }
6366 for t in 0..accepted {
6367 let tip = t + 1 == accepted;
6368 for (slot, &tl) in targets.iter().enumerate() {
6369 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
6370 let lo = (di * b + t) * hc * dim;
6371 crate::dsv4::dspark_capture(
6372 &caps_all[lo..lo + hc * dim],
6373 &cfg,
6374 slot,
6375 &mut ds.main_hidden,
6376 );
6377 } else if tip
6378 && crate::dsv4::dspark_peek_slot(slot, dim, {
6379 let lo = slot * dim;
6380 &mut ds.main_hidden[lo..lo + dim]
6381 })
6382 {
6383 } else {
6388 crate::dsv4::dspark_capture(
6392 &walked[t * hc * dim..(t + 1) * hc * dim],
6393 &cfg,
6394 slot,
6395 &mut ds.main_hidden,
6396 );
6397 }
6398 }
6399 crate::dsv4::dspark_ring_append(
6400 g,
6401 &self.dsv4_mtp,
6402 &cfg,
6403 ds,
6404 next_pos + t,
6405 self.pool.as_deref(),
6406 );
6407 }
6408 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
6409 self.graph_logits = Some(row);
6410 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
6415 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
6416 crate::dsv4::pick_tally_arm();
6417 }
6418 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
6419 eprintln!(
6420 "spec_step total {:.1} мс (k={accepted})",
6421 t_all.elapsed().as_secs_f64() * 1e3
6422 );
6423 }
6424 Some((fed[1..accepted].to_vec(), next_pos + accepted))
6425 }
6426
6427 fn dspark_probe(&mut self, position: usize, token_id: u32) {
6428 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
6429 return;
6430 }
6431 let trunk_now = crate::dsv4::pick_tally_take();
6433 crate::dsv4::trunk_freq_note(&trunk_now);
6434 if !trunk_now.is_empty() {
6435 self.dspark_trunk_picks.push(trunk_now);
6436 let keep = crate::dsv4::dspark_block();
6437 if self.dspark_trunk_picks.len() > keep {
6438 self.dspark_trunk_picks.remove(0);
6439 }
6440 }
6441 for p in std::mem::take(&mut self.dspark_pending) {
6444 let Some(i) = position.checked_sub(p.0 + 1) else {
6445 continue;
6446 };
6447 let mut p = p;
6448 if i < p.1.len() {
6449 if p.2 && p.1[i] == token_id {
6450 p.3 = i + 1;
6451 } else {
6452 p.2 = false;
6453 }
6454 if i + 1 < p.1.len() {
6455 self.dspark_pending.push(p);
6456 continue;
6457 }
6458 }
6459 self.dspark_hist.push(p.3);
6460 self.dspark_real.push(token_id);
6461 }
6462 let Some(b) = &mut self.dsv4 else { return };
6463 let (g, layers, cfg) = (&b.0, &b.1, b.2);
6464 let n_layers = layers.len();
6465 if self.dspark.is_none() {
6466 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
6467 if t.is_empty() {
6468 return;
6469 }
6470 eprintln!(
6471 "DSpark: захват со слоёв {t:?}, блок {}",
6472 crate::dsv4::dspark_block()
6473 );
6474 crate::dsv4::dspark_arm(&t, cfg.dim);
6475 self.dspark = Some(crate::dsv4::DsparkState::new(
6476 self.dsv4_mtp.len(),
6477 &cfg,
6478 t.len(),
6479 ));
6480 }
6481 let ds = self.dspark.as_mut().unwrap();
6482 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
6483 return; }
6485 let mut conf = Vec::new();
6486 crate::dsv4::pick_tally_arm();
6487 let draft_started = std::time::Instant::now();
6492 #[cfg(feature = "gpu")]
6493 let gpu_draft = crate::dsv4::dspark_gpu_on();
6494 #[cfg(not(feature = "gpu"))]
6495 let gpu_draft = false;
6496 let props = if gpu_draft {
6497 #[cfg(feature = "gpu")]
6498 {
6499 let kv_id = b.3.kv_id;
6500 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
6501 Some(pk) => crate::dsv4::dspark_draft_gpu(
6502 g,
6503 &self.dsv4_mtp,
6504 &cfg,
6505 ds,
6506 pk,
6507 kv_id,
6508 token_id,
6509 position,
6510 self.pool.as_deref(),
6511 &mut conf,
6512 ),
6513 None => Vec::new(),
6514 }
6515 }
6516 #[cfg(not(feature = "gpu"))]
6517 Vec::new()
6518 } else {
6519 crate::gpu::cpu_scope(|| {
6520 crate::dsv4::dspark_draft(
6521 g,
6522 &self.dsv4_mtp,
6523 &cfg,
6524 ds,
6525 token_id,
6526 position,
6527 self.pool.as_deref(),
6528 &mut conf,
6529 )
6530 })
6531 };
6532 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
6533 let draft_picks = crate::dsv4::pick_tally_take();
6534 crate::dsv4::dspark_freq_note(&draft_picks);
6535 crate::dsv4::pick_tally_arm();
6538 if !props.is_empty() {
6539 let (tu, tt) = {
6543 let flat: Vec<(usize, Vec<usize>)> = self
6544 .dspark_trunk_picks
6545 .iter()
6546 .flat_map(|v| v.iter().cloned())
6547 .collect();
6548 let mut per: std::collections::HashMap<usize, Vec<usize>> =
6550 std::collections::HashMap::new();
6551 for (li, picks) in flat {
6552 per.entry(li).or_default().extend(picks);
6553 }
6554 let n = per.len().max(1);
6555 let mut u = 0usize;
6556 let mut t = 0usize;
6557 for (_, v) in per {
6558 t += v.len();
6559 u += v.iter().collect::<std::collections::HashSet<_>>().len();
6560 }
6561 (u / n, t / n)
6562 };
6563 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
6564 self.dspark_exp.push((tu, tt, du, dt));
6565 self.dspark_pending.push((position, props, true, 0));
6566 }
6567 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
6568 let n = self.dspark_hist.len() as f32;
6569 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
6570 let block = crate::dsv4::dspark_block();
6571 let mut at = vec![0usize; block + 1];
6572 for &k in &self.dspark_hist {
6573 at[k] += 1;
6574 }
6575 let mut surv = Vec::with_capacity(block);
6577 for i in 1..=block {
6578 let k = at[i..].iter().sum::<usize>() as f32 / n;
6579 surv.push(format!("{k:.2}"));
6580 }
6581 let distinct = self
6582 .dspark_real
6583 .iter()
6584 .collect::<std::collections::HashSet<_>>()
6585 .len();
6586 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
6587 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
6588 });
6589 let m = self.dspark_exp.len().max(1);
6590 eprintln!(
6591 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
6592 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
6593 self.dspark_hist.len(),
6594 mean + 1.0,
6595 surv.join(" ")
6596 );
6597 eprintln!(
6598 "DSpark: разных токенов {distinct} из {} (вырожденность), \
6599 эксперты ствол {}/{} на слой за {block} токенов, \
6600 черновик {}/{} за блок, draft {:.2} мс/блок",
6601 self.dspark_real.len(),
6602 tu / m,
6603 tt / m,
6604 du / m,
6605 dt / m,
6606 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
6607 );
6608 }
6609 }
6610
6611 fn forward_layers_upto(
6612 &mut self,
6613 hidden: &[f32],
6614 position: usize,
6615 task_mask: Option<&TaskMask>,
6616 upto: Option<usize>,
6617 ) -> Vec<f32> {
6618 if let Some(plan) = self.gpu_plan.clone() {
6624 if upto.is_none() && plan.len() > 1 {
6625 let mut h = hidden.to_vec();
6626 for &(dev, from, upto_incl) in plan.iter() {
6627 h = crate::gpu::with_device(dev, || {
6628 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
6629 });
6630 }
6631 return h;
6632 }
6633 }
6634 self.forward_layers_span(hidden, position, task_mask, 0, upto)
6635 }
6636
6637 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
6642 self.set_gpu_plan_at(devices, None)
6643 }
6644
6645 pub fn set_gpu_plan_at(
6649 &mut self,
6650 devices: Option<&[usize]>,
6651 at: Option<usize>,
6652 ) -> Result<(), String> {
6653 let Some(devs) = devices.filter(|d| d.len() > 1) else {
6654 self.gpu_plan = None;
6655 return Ok(());
6656 };
6657 self.split_supported()?;
6658 let n = self.num_layers;
6659 if devs.len() > n {
6660 return Err(format!("{} devices for {n} layers", devs.len()));
6661 }
6662 if let Some(k) = at {
6663 if k == 0 || k >= n {
6664 return Err(format!("split at {k}: the model has {n} layers"));
6665 }
6666 if devs.len() == 2 {
6667 self.gpu_plan = Some(std::sync::Arc::new(vec![
6668 (devs[0], 0, k - 1),
6669 (devs[1], k, n - 1),
6670 ]));
6671 return Ok(());
6672 }
6673 return Err(format!(
6674 "an explicit split point takes exactly 2 devices, got {}",
6675 devs.len()
6676 ));
6677 }
6678 let per = n.div_ceil(devs.len());
6679 let mut plan = Vec::with_capacity(devs.len());
6680 let mut from = 0usize;
6681 for &d in devs {
6682 if from >= n {
6683 break;
6684 }
6685 let upto = (from + per - 1).min(n - 1);
6686 plan.push((d, from, upto));
6687 from = upto + 1;
6688 }
6689 self.gpu_plan = Some(std::sync::Arc::new(plan));
6690 Ok(())
6691 }
6692
6693 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
6695 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
6696 }
6697
6698 fn forward_layers_span(
6704 &mut self,
6705 hidden: &[f32],
6706 position: usize,
6707 task_mask: Option<&TaskMask>,
6708 from: usize,
6709 upto: Option<usize>,
6710 ) -> Vec<f32> {
6711 debug_assert!(from == 0 || (self.dsv4.is_none() && self.g3n.is_none()));
6712 if let Some(b) = &mut self.dsv4 {
6718 let _ = (task_mask, upto);
6719 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
6720 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
6721 st.pos = position;
6722 let mut logits = Vec::new();
6723 crate::dsv4::forward_token(
6724 g,
6725 layers,
6726 &cfg,
6727 st,
6728 token_id,
6729 &self.inv_freq,
6730 self.pool.as_deref(),
6731 &mut logits,
6732 );
6733 self.graph_logits = Some(logits);
6734 self.dspark_probe(position, token_id);
6735 return vec![0.0; self.hidden_size];
6738 }
6739 if let Some(b) = &self.g3n {
6742 let _ = (task_mask, upto);
6743 return crate::g3n::g3n_forward(
6744 &b.0,
6745 &b.1,
6746 hidden,
6747 position,
6748 &mut self.kv_cache.layers,
6749 self.num_heads,
6750 self.num_kv_heads,
6751 self.head_dim,
6752 self.pool.as_deref(),
6753 );
6754 }
6755 let mut h = hidden.to_vec();
6756 let (nh, _nkv, _hd, hs, _rd, eps) = (
6759 self.num_heads,
6760 self.num_kv_heads,
6761 self.head_dim,
6762 self.hidden_size,
6763 self.rotary_dim,
6764 self.rms_eps,
6765 );
6766 let pool = self.pool.clone();
6767 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
6779 let graph_on = match graph_env.as_deref() {
6780 Some("0") => false,
6781 Some("prefill") => false, Some(_) => true,
6783 None => crate::gpu::wgpu_graph_default(),
6789 };
6790 let graph_trusted =
6791 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
6792 let race_eligible = graph_on
6793 && upto.is_none()
6794 && task_mask.is_none()
6795 && from == 0
6796 && !crate::gpu::graph_unsupported();
6797 let mut tail_start = 0usize;
6798 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
6799 let t_graph = std::time::Instant::now();
6800 let mut lg = Vec::new();
6801 let mut gl = 0usize;
6802 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
6803 if built.is_none() && !self.o1_active() && self.attn_softcap == 0.0 {
6808 crate::gpu::graph_mark_unsupported();
6809 }
6810 graph_note(built.is_some());
6811 if let Some(hh) = built {
6812 let dur = t_graph.elapsed();
6813 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6814 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
6815 }
6816 if gl > 0 && gl < self.num_layers {
6817 h = hh;
6823 tail_start = gl;
6824 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
6825 if !graph_trusted {
6826 crate::gpu::graph_race_record(true, dur);
6827 }
6828 if !lg.is_empty() {
6829 lg.resize(self.vocab_size, 0.0);
6832 if let Some(c) = self.final_softcap {
6833 for l in lg.iter_mut() {
6834 *l = c * (*l / c).tanh();
6835 }
6836 }
6837 self.graph_logits = Some(lg);
6838 }
6839 return hh;
6840 }
6841 }
6847 }
6848 let span = from > 0 || upto.is_some();
6872 if span && graph_on && task_mask.is_none() && graph_trusted {
6873 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
6874 let mut lg = Vec::new();
6875 let mut gl = 0usize;
6876 let span_res =
6877 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
6878 graph_note(span_res.is_some() && gl == upto_excl - from);
6879 if std::env::var("CMF_GPU_DEBUG").is_ok() {
6880 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
6884 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
6885 eprintln!(
6886 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
6887 upto_excl - from,
6888 span_res.is_some()
6889 );
6890 }
6891 }
6892 if let Some(hh) = span_res {
6893 if gl == upto_excl - from {
6894 if !lg.is_empty() {
6895 lg.resize(self.vocab_size, 0.0);
6896 if let Some(c) = self.final_softcap {
6897 for l in lg.iter_mut() {
6898 *l = c * (*l / c).tanh();
6899 }
6900 }
6901 self.graph_logits = Some(lg);
6902 }
6903 crate::gpu::set_layer(-1);
6904 return hh;
6905 }
6906 h = hh;
6908 tail_start = from + gl;
6909 }
6910 }
6911 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
6912
6913 #[cfg(target_os = "macos")]
6914 let mut gpu_skip_until = 0usize;
6915 for li in tail_start.max(from)..self.num_layers {
6916 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
6918 if li > u {
6919 break;
6920 }
6921 }
6922 if let Some(mask) = task_mask {
6923 if !mask.layer_alive(li) {
6924 continue; }
6926 }
6927 #[cfg(target_os = "macos")]
6931 {
6932 if li < gpu_skip_until {
6933 continue;
6934 }
6935 if task_mask.is_none() {
6936 let end = self.q1_graph_gpu(li, upto, position, &mut h);
6937 if end > li {
6938 gpu_skip_until = end;
6939 if self.is_loop_end(end - 1) && end < self.num_layers {
6942 h = inference::rms_norm(
6943 &h,
6944 &self.weights.final_norm,
6945 self.rms_eps,
6946 self.norm_style,
6947 );
6948 }
6949 continue;
6950 }
6951 }
6952 }
6953
6954 let lw = &self.weights.layers[self.phys_layer(li)];
6955 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6956 if tp.parse::<usize>().ok() == Some(position) {
6957 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
6958 eprintln!(
6959 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
6960 h[0], h[1]
6961 );
6962 }
6963 }
6964 inference::rms_norm_into(
6967 &h,
6968 &lw.input_norm,
6969 self.rms_eps,
6970 self.norm_style,
6971 &mut self.ws.n1,
6972 );
6973
6974 let attn_out = match &lw.attn {
6975 AttnKind::Mla(w) => {
6976 let inv_freq_l = self.layer_inv_freq(li);
6977 let rs = self.layer_rope_scale(li);
6978 let eps = self.rms_eps;
6979 let pool = self.pool.clone();
6980 mla_attention(
6981 w,
6982 &self.ws.n1,
6983 &mut self.kv_cache.layers[li],
6984 position,
6985 &inv_freq_l,
6986 rs,
6987 eps,
6988 pool.as_deref(),
6989 )
6990 }
6991 AttnKind::Linear(w) => {
6992 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
6993 vmf_phase_forward(
6994 &self.ws.n1,
6995 w,
6996 &cfg,
6997 &mut self.kv_cache.layers[li].linear_state,
6998 self.pool.as_deref(),
6999 )
7000 }
7001 AttnKind::Kda(w) => {
7002 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
7003 crate::linear_core::kda_forward(
7004 &self.ws.n1,
7005 w,
7006 &cfg,
7007 &mut self.kv_cache.layers[li].linear_state,
7008 self.pool.as_deref(),
7009 )
7010 }
7011 AttnKind::LinearGdn(w) => {
7012 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7013 gdn_forward(
7014 &self.ws.n1,
7015 w,
7016 &cfg,
7017 &mut self.kv_cache.layers[li].linear_state,
7018 self.pool.as_deref(),
7019 )
7020 }
7021 AttnKind::ShortConv(w) => {
7022 let cfg = self
7023 .short_conv_cfg
7024 .expect("short-conv layer without short_conv_cfg");
7025 short_conv_forward(
7026 &self.ws.n1,
7027 w,
7028 &cfg,
7029 &mut self.kv_cache.layers[li].linear_state,
7030 self.pool.as_deref(),
7031 )
7032 }
7033 AttnKind::Full {
7034 wq,
7035 wk,
7036 wv,
7037 wo,
7038 q_norm,
7039 k_norm,
7040 output_gate,
7041 softplus_gate,
7042 bias,
7043 } if self.kv_cache.layers[li].o1_sealed() => {
7044 let inv_freq_l = self.layer_inv_freq(li);
7047 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7048 let cfg = QwenAttnCfg {
7049 num_heads: self.layer_num_heads(li),
7050 num_kv_heads: nkv_l,
7051 head_dim: hd_l,
7052 hidden_size: hs,
7053 position,
7054 inv_freq: &inv_freq_l,
7055 rotary_dim: rd_l,
7056 scale: self.attn_scale,
7057 softcap: self.attn_softcap,
7058 window: None,
7059 v_norm: self.attn_v_norm,
7060 q_norm: q_norm.as_deref(),
7061 k_norm: k_norm.as_deref(),
7062 output_gate: *output_gate,
7063 softplus_gate: softplus_gate
7064 .as_ref()
7065 .map(|(gate, per_head)| (gate, *per_head)),
7066 rope_scale: self.layer_rope_scale(li),
7067 bias: bias
7068 .as_ref()
7069 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7070 rms_eps: eps,
7071 norm_style: self.norm_style,
7072 pool: pool.as_deref(),
7073 };
7074 attention::qwen_attention_nystrom(
7075 &self.ws.n1,
7076 wq,
7077 wk,
7078 wv,
7079 wo,
7080 &mut self.kv_cache.layers[li],
7081 &cfg,
7082 )
7083 }
7084 AttnKind::Full {
7085 wq,
7086 wk,
7087 wv,
7088 wo,
7089 q_norm,
7090 k_norm,
7091 output_gate,
7092 softplus_gate,
7093 bias,
7094 } => 'attn: {
7095 if graph_on
7098 && !*output_gate
7099 && softplus_gate.is_none()
7100 && self.attention_heads_per_layer.is_none()
7101 && bias.is_none()
7102 && task_mask.is_none()
7103 {
7104 let inv_freq_l = self.layer_inv_freq(li);
7105 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7106 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7107 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
7108 wq.mapped_q1(),
7109 wk.mapped_q1(),
7110 wv.mapped_q1(),
7111 wo.mapped_q1(),
7112 ) {
7113 let gm = gm.clone();
7114 let mut out = vec![0f32; hs];
7115 let cache = &self.kv_cache.layers[li];
7116 if crate::gpu::attn_dropin(
7117 &gm,
7118 self.graph_kv_id,
7119 li,
7120 &self.ws.n1,
7121 qi,
7122 ki,
7123 vi,
7124 oi,
7125 q_norm.as_deref(),
7126 k_norm.as_deref(),
7127 &inv_freq_l,
7128 nh,
7129 nkv_l,
7130 hd_l,
7131 rd_l,
7132 hs,
7133 position,
7134 self.kv_cache.max_seq_len,
7135 gemma,
7136 eps as f32,
7137 cache.k_heads(),
7138 cache.v_heads(),
7139 &mut out,
7140 ) {
7141 break 'attn out;
7142 }
7143 }
7144 }
7145 let masked = task_mask
7146 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
7147 .unwrap_or(false);
7148 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
7149 match (masked, f32_view) {
7150 (true, (Some(q), Some(k), Some(v), Some(o))) => {
7153 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
7154 attention::multi_head_attention(
7155 &self.ws.n1,
7156 q,
7157 k,
7158 v,
7159 o,
7160 &mut self.kv_cache.layers[li],
7161 self.num_heads,
7162 self.num_kv_heads,
7163 self.head_dim,
7164 self.hidden_size,
7165 position,
7166 &active_heads,
7167 &self.inv_freq,
7168 )
7169 }
7170 (masked, _) => {
7171 if masked {
7172 tracing::warn!(
7173 "layer {li}: head mask on quantized weights not \
7174 supported yet — executing dense"
7175 );
7176 }
7177 let inv_freq_l = self.layer_inv_freq(li);
7178 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7179 let cfg = QwenAttnCfg {
7180 num_heads: self.layer_num_heads(li),
7181 num_kv_heads: nkv_l,
7182 head_dim: hd_l,
7183 hidden_size: hs,
7184 position,
7185 inv_freq: &inv_freq_l,
7186 rotary_dim: rd_l,
7187 scale: self.attn_scale,
7188 softcap: self.attn_softcap,
7189 window: self.layer_window(li),
7190 v_norm: self.attn_v_norm,
7191 q_norm: q_norm.as_deref(),
7192 k_norm: k_norm.as_deref(),
7193 output_gate: *output_gate,
7194 softplus_gate: softplus_gate
7195 .as_ref()
7196 .map(|(gate, per_head)| (gate, *per_head)),
7197 rope_scale: self.layer_rope_scale(li),
7198 bias: bias
7199 .as_ref()
7200 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7201 rms_eps: eps,
7202 norm_style: self.norm_style,
7203 pool: pool.as_deref(),
7204 };
7205 attention::qwen_attention(
7206 &self.ws.n1,
7207 wq,
7208 wk,
7209 wv,
7210 wo,
7211 &mut self.kv_cache.layers[li],
7212 &cfg,
7213 )
7214 }
7215 }
7216 }
7217 };
7218 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
7221 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
7222 None => attn_out,
7223 };
7224 let lw = &self.weights.layers[self.phys_layer(li)];
7225 inference::add_rmsnorm_fused_into(
7226 &mut h,
7227 &attn_out,
7228 &lw.post_norm,
7229 self.rms_eps,
7230 self.norm_style,
7231 &mut self.ws.p1,
7232 );
7233 let mut attn_out = attn_out;
7234 attention::recycle_buf(&mut attn_out);
7235 let post_normed = &self.ws.p1;
7236
7237 let ffn_masked = task_mask
7238 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
7239 .unwrap_or(false);
7240 let ffn_out = match (ffn_masked, &lw.ffn) {
7252 (true, FfnKind::Dense(d)) => {
7253 let tm = task_mask.unwrap();
7254 let alive = tm.ffn_active_count(li);
7255 let deep = alive * 2 <= self.intermediate_size;
7256 if deep && d.down_proj.sparse_col_ok() {
7257 let active = tm.ffn_active_indices(li);
7258 sparse_ffn_quant(
7259 d,
7260 post_normed,
7261 &active,
7262 self.hidden_size,
7263 self.pool.as_deref(),
7264 )
7265 } else if deep
7266 && let (Some(g), Some(u), Some(dn)) = (
7267 d.gate_proj.as_f32(),
7268 d.up_proj.as_f32(),
7269 d.down_proj.as_f32(),
7270 )
7271 {
7272 let active = tm.ffn_active_indices(li);
7273 inference::sparse_ffn_forward(
7274 post_normed,
7275 g,
7276 u,
7277 dn,
7278 self.hidden_size,
7279 self.intermediate_size,
7280 &active,
7281 self.pool.as_deref(),
7282 )
7283 } else {
7284 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
7285 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
7286 }
7287 }
7288 (true, FfnKind::Moe(m)) => {
7289 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
7293 ffn_forward(
7294 &lw.ffn,
7295 post_normed,
7296 self.pool.as_deref(),
7297 allowed.as_deref(),
7298 )
7299 }
7300 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
7301 dm,
7302 post_normed,
7303 &h,
7304 self.rms_eps,
7305 self.norm_style,
7306 self.pool.as_deref(),
7307 ),
7308 (false, _) => match &lw.ffn {
7309 FfnKind::DenseMoe(dm) => dense_moe_ffn(
7310 dm,
7311 post_normed,
7312 &h,
7313 self.rms_eps,
7314 self.norm_style,
7315 self.pool.as_deref(),
7316 ),
7317 _ => {
7318 let allowed = match (&lw.ffn, task_mask) {
7319 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
7320 _ => None,
7321 };
7322 ffn_forward(
7323 &lw.ffn,
7324 post_normed,
7325 self.pool.as_deref(),
7326 allowed.as_deref(),
7327 )
7328 }
7329 },
7330 };
7331 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
7332 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
7333 None => ffn_out,
7334 };
7335 for (i, &f) in ffn_out.iter().enumerate() {
7336 h[i] += f;
7337 }
7338 let mut ffn_out = ffn_out;
7339 attention::recycle_buf(&mut ffn_out);
7340
7341 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
7343 for v in h.iter_mut() {
7344 *v *= sc;
7345 }
7346 }
7347
7348 if self.is_loop_end(li) && li + 1 < self.num_layers {
7351 h = inference::rms_norm(
7352 &h,
7353 &self.weights.final_norm,
7354 self.rms_eps,
7355 self.norm_style,
7356 );
7357 }
7358
7359 if self.dyn_phi_layer == Some(li) {
7363 self.update_dyn_phi(&h);
7364 }
7365 }
7366 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
7368 crate::gpu::graph_race_record(false, t.elapsed());
7369 }
7370
7371 h
7372 }
7373
7374 fn update_dyn_phi(&mut self, h: &[f32]) {
7377 const A: f32 = 0.2;
7378 if self.dyn_phi_ema.len() != h.len() {
7379 self.dyn_phi_ema = vec![0.0; h.len()];
7380 self.dyn_phi_seen = 0;
7381 }
7382 if self.dyn_phi_seen == 0 {
7383 self.dyn_phi_ema.copy_from_slice(h);
7384 } else {
7385 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
7386 *e = (1.0 - A) * *e + A * v;
7387 }
7388 }
7389 self.dyn_phi_seen += 1;
7390 }
7391
7392 pub fn dyn_phi(&self) -> &[f32] {
7394 &self.dyn_phi_ema
7395 }
7396
7397 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
7399 self.dyn_phi_layer = layer;
7400 self.dyn_phi_ema.clear();
7401 self.dyn_phi_seen = 0;
7402 }
7403
7404 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
7406 let Some(model) = &self.model else {
7407 return Vec::new();
7408 };
7409 model
7410 .header
7411 .skills
7412 .iter()
7413 .enumerate()
7414 .filter_map(|(i, sk)| {
7415 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
7416 let sel = sk.selection.as_ref()?;
7417 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
7418 })
7419 .collect()
7420 }
7421
7422 pub fn active_skill(&self) -> Option<usize> {
7424 self.dyn_active
7425 }
7426
7427 pub fn enable_dynamic_routing(&mut self) -> usize {
7432 use crate::swarm::{DynRouter, RoutableSkill};
7433 let Some(model) = self.model.clone() else {
7434 return 0;
7435 };
7436 if self.dyn_blend_loaded {
7439 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
7440 return 0;
7441 }
7442 if let Some(a) = self.dyn_active {
7446 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
7447 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
7448 return 0;
7449 }
7450 }
7451 let hidden = self.hidden_size;
7452 let mut skills = Vec::new();
7453 for (idx, id, _phi) in self.dynamic_skills() {
7454 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
7455 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
7456 skills.push(rs);
7457 }
7458 }
7459 }
7460 if skills.is_empty() {
7461 return 0;
7462 }
7463 let phi = skills[0].phi_layer;
7465 if skills.iter().any(|s| s.phi_layer != phi) {
7466 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
7467 }
7468 let n = skills.len();
7469 self.set_dyn_phi_layer(Some(phi));
7470 self.dyn_router = Some(DynRouter::new(skills));
7471 n
7472 }
7473
7474 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
7476 self.dyn_router
7477 .as_ref()
7478 .map(|r| r.switches.clone())
7479 .unwrap_or_default()
7480 }
7481
7482 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
7485 let rows = self.weights.lm_head.rows();
7486 let mut logits = attention::take_buf(rows.min(self.vocab_size));
7487 self.weights
7488 .lm_head
7489 .matvec(hidden, &mut logits, self.pool.as_deref());
7490 logits.resize(self.vocab_size, 0.0);
7491 if let Some(m) = self.logit_multiplier {
7492 for l in logits.iter_mut() {
7493 *l *= m;
7494 }
7495 }
7496 if let Some(c) = self.final_softcap {
7497 for l in logits.iter_mut() {
7498 *l = c * (*l / c).tanh();
7499 }
7500 }
7501 logits
7502 }
7503
7504 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
7509 self.kv_cache.clear();
7510 self.kv_history.clear();
7511 let mut hidden = vec![0.0f32; self.hidden_size];
7512 for (pos, &id) in ids.iter().enumerate() {
7513 let emb = self.embed_single(id);
7514 hidden = self.forward_layers(&emb, pos, task_mask);
7515 }
7516 inference::rms_norm_into(
7517 &hidden,
7518 &self.weights.final_norm,
7519 self.rms_eps,
7520 self.norm_style,
7521 &mut self.ws.n1,
7522 );
7523 self.lm_head_forward(&self.ws.n1)
7524 }
7525}
7526
7527pub fn create_test_pipeline(
7529 hidden_size: usize,
7530 intermediate_size: usize,
7531 num_heads: usize,
7532 num_kv_heads: usize,
7533 head_dim: usize,
7534 num_layers: usize,
7535 vocab_size: usize,
7536) -> Pipeline {
7537 let synth = |n: usize, salt: usize| -> Vec<f32> {
7540 (0..n)
7541 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
7542 .collect()
7543 };
7544 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
7545 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
7546 };
7547 let layer_weights: Vec<LayerWeights> = (0..num_layers)
7548 .map(|li| LayerWeights {
7549 input_norm: vec![1.0; hidden_size],
7550 post_norm: vec![1.0; hidden_size],
7551 attn_out_norm: None,
7552 ffn_out_norm: None,
7553 layer_scale: None,
7554 ffn: FfnKind::Dense(DenseFfn {
7555 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
7556 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
7557 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
7558 act: Act::Silu,
7559 }),
7560 attn: AttnKind::Full {
7561 bias: None,
7562 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
7563 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
7564 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
7565 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
7566 q_norm: None,
7567 k_norm: None,
7568 output_gate: false,
7569 softplus_gate: None,
7570 },
7571 })
7572 .collect();
7573
7574 Pipeline::new(
7575 Tokenizer::byte_level(),
7576 PipelineWeights {
7577 embed_tokens: qt(vocab_size, hidden_size, 100),
7578 layers: layer_weights,
7579 lm_head: qt(vocab_size, hidden_size, 200),
7580 final_norm: vec![1.0; hidden_size],
7581 },
7582 hidden_size,
7583 intermediate_size,
7584 num_heads,
7585 num_kv_heads,
7586 head_dim,
7587 num_layers,
7588 num_layers, false, vocab_size,
7591 1e-6,
7592 10_000.0,
7593 NormStyle::Qwen,
7594 4096,
7595 SamplerConfig {
7596 seed: Some(42),
7597 ..Default::default()
7598 },
7599 )
7600}
7601
7602#[inline]
7607fn mask_bit(row: &[u8], j: usize) -> bool {
7608 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
7609}
7610
7611fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
7617 for r in 0..rows {
7618 let base = r * inter;
7619 for (bi, &byte) in row.iter().enumerate() {
7620 if byte == 0xFF {
7621 continue;
7622 }
7623 let j0 = bi * 8;
7624 for bit in 0..8 {
7625 let j = j0 + bit;
7626 if j < inter && byte & (1 << bit) == 0 {
7627 g[base + j] = 0.0;
7628 }
7629 }
7630 }
7631 }
7632}
7633
7634fn dense_ffn_batch(
7635 d: &DenseFfn,
7636 xs: &[f32],
7637 b: usize,
7638 pool: Option<&Pool>,
7639 mask_row: Option<&[u8]>,
7640) -> Vec<f32> {
7641 let inter = d.gate_proj.rows();
7642 let hidden = d.down_proj.rows();
7643 if mask_row.is_none()
7651 && d.act == Act::Silu
7652 && b >= 32
7653 && crate::gpu::enabled_here()
7654 && !crate::gpu::mm_killed()
7655 {
7656 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
7657 d.gate_proj.mapped_q4t(),
7658 d.up_proj.mapped_q4t(),
7659 d.down_proj.mapped_q4t(),
7660 ) {
7661 let mut out = vec![0.0f32; b * hidden];
7662 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
7663 return out;
7664 }
7665 }
7666 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
7671 d.gate_proj.mapped_q4tp(),
7672 d.up_proj.mapped_q4tp(),
7673 d.down_proj.mapped_q4tp(),
7674 ) {
7675 let mut out = vec![0.0f32; b * hidden];
7676 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
7677 return out;
7678 }
7679 }
7680 }
7681 let mut g = vec![0.0f32; b * inter];
7682 d.gate_proj.matmat(xs, b, &mut g, pool);
7683 let mut u = vec![0.0f32; b * inter];
7684 d.up_proj.matmat(xs, b, &mut u, pool);
7685 for i in 0..b * inter {
7686 g[i] = d.act.combine(g[i], u[i]);
7687 }
7688 if let Some(row) = mask_row {
7689 zero_masked_cols(&mut g, b, inter, row);
7690 }
7691 let mut out = vec![0.0f32; b * hidden];
7692 d.down_proj.matmat(&g, b, &mut out, pool);
7693 out
7694}
7695
7696fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
7701 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7702 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7703 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
7704 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
7705 if (!on && !dump) || b == 0 {
7706 return;
7707 }
7708 let hidden = xs.len() / b;
7709 if on {
7710 let mut acc = m.act_sq.borrow_mut();
7711 if acc.len() < hidden {
7712 acc.resize(hidden, 0.0);
7713 }
7714 for t in 0..b {
7715 let row = &xs[t * hidden..(t + 1) * hidden];
7716 for (a, &v) in acc.iter_mut().zip(row) {
7717 *a += (v as f64) * (v as f64);
7718 }
7719 }
7720 }
7721 if dump {
7722 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
7725 .ok()
7726 .and_then(|v| v.parse().ok())
7727 .unwrap_or(4096);
7728 let mut rows = m.act_rows.borrow_mut();
7729 if rows.len() < cap * hidden {
7730 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
7731 rows.extend_from_slice(&xs[..take * hidden]);
7732 }
7733 }
7734}
7735
7736#[derive(Clone, Copy)]
7739struct SendVecs(*mut Vec<f32>);
7740unsafe impl Send for SendVecs {}
7741unsafe impl Sync for SendVecs {}
7742impl SendVecs {
7743 #[inline]
7744 fn at(self, i: usize) -> *mut Vec<f32> {
7745 unsafe { self.0.add(i) }
7746 }
7747}
7748
7749fn moe_ffn_batch(
7750 m: &MoeFfn,
7751 xs: &[f32],
7752 b: usize,
7753 hidden: usize,
7754 pool: Option<&Pool>,
7755 allowed: Option<&[bool]>,
7756) -> Vec<f32> {
7757 accumulate_act(m, xs, b);
7758 let ne = m.experts.len();
7759 let mut logits = vec![0.0f32; b * ne];
7760 m.router.matmat(xs, b, &mut logits, pool);
7761
7762 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
7765 {
7766 let mut st = m.stats.borrow_mut();
7767 if st.len() < ne {
7768 st.resize(ne, 0);
7769 }
7770 for bi in 0..b {
7771 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
7772 for &e in &idx {
7773 st[e] += 1;
7774 assign[e].push((bi, p[e] / wsum));
7775 }
7776 }
7777 }
7778
7779 let mut out = vec![0.0f32; b * hidden];
7780 let cols = m.experts[0].gate_proj.cols();
7781 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
7782 let sb = list.len();
7783 let mut sub = vec![0.0f32; sb * cols];
7784 for (k, &(bi, _)) in list.iter().enumerate() {
7785 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
7786 }
7787 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
7788 for (k, &(bi, w)) in list.iter().enumerate() {
7789 for i in 0..hidden {
7790 out[bi * hidden + i] += w * eo[k * hidden + i];
7791 }
7792 }
7793 };
7794 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
7800 if pool.is_some() && active.len() >= 8 {
7801 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
7802 {
7803 let panel_ptr = SendVecs(panels.as_mut_ptr());
7804 let experts = &m.experts;
7807 let (active_r, assign_r) = (&active, &assign);
7808 let run = |start: usize, end: usize| {
7809 for ai in start..end {
7810 let e = active_r[ai];
7811 let list = &assign_r[e];
7812 let sb = list.len();
7813 let mut sub = vec![0.0f32; sb * cols];
7814 for (k, &(bi, _)) in list.iter().enumerate() {
7815 sub[k * cols..(k + 1) * cols]
7816 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
7817 }
7818 unsafe {
7820 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
7821 }
7822 }
7823 };
7824 match pool {
7825 Some(p) => p.run_rows(active.len(), &run),
7826 None => run(0, active.len()),
7827 }
7828 }
7829 for (ai, &e) in active.iter().enumerate() {
7830 for (k, &(bi, w)) in assign[e].iter().enumerate() {
7831 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
7832 for i in 0..hidden {
7833 out[bi * hidden + i] += w * eo[i];
7834 }
7835 }
7836 }
7837 } else {
7838 for &e in &active {
7839 run_expert(&m.experts[e], &assign[e], &mut out);
7840 }
7841 }
7842 if let Some((se, gate)) = &m.shared {
7843 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
7844 let mut gl = vec![0.0f32; b];
7845 gate.matmat(xs, b, &mut gl, pool);
7846 (0..b)
7847 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
7848 .collect()
7849 } else {
7850 (0..b).map(|bi| (bi, 1.0)).collect()
7851 };
7852 run_expert(se, &all, &mut out);
7853 }
7854 out
7855}
7856
7857thread_local! {
7858 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
7862 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
7863}
7864
7865fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
7867 if crate::gpu::enabled_here()
7878 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
7879 {
7880 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
7881 crate::gpu::ProbeArm::Gpu
7882 } else {
7883 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
7884 };
7885 match arm {
7886 crate::gpu::ProbeArm::Gpu => {
7887 let t0 = std::time::Instant::now();
7888 if let Some(out) = dense_ffn_gpu(d, x, pool) {
7889 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
7890 return out;
7891 }
7892 }
7893 crate::gpu::ProbeArm::CpuTimed => {
7894 let t0 = std::time::Instant::now();
7895 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
7896 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
7897 return out;
7898 }
7899 crate::gpu::ProbeArm::Cpu => {
7900 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
7901 }
7902 }
7903 }
7904 dense_ffn_cpu(d, x, pool)
7905}
7906
7907fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
7909 let inter = d.gate_proj.rows();
7910 FFN_SCRATCH.with(|s| {
7911 let mut s = s.borrow_mut();
7912 let [g, u, ..] = &mut *s;
7913 g.resize(inter, 0.0);
7914 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
7917 } else {
7919 u.resize(inter, 0.0);
7920 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
7922 for i in 0..inter {
7923 g[i] = d.act.combine(g[i], u[i]);
7924 }
7925 }
7926 FFN_PROBE.with(|pr| {
7929 if let Some(acc) = pr.borrow_mut().as_mut() {
7930 let li = crate::gpu::cur_layer();
7931 if li >= 0 {
7932 if let Some(row) = acc.get_mut(li as usize) {
7933 for (a, &v) in row.iter_mut().zip(g.iter()) {
7934 *a += (v as f64).abs();
7935 }
7936 }
7937 }
7938 }
7939 });
7940 let mut out = attention::take_buf(d.down_proj.rows());
7941 d.down_proj.matvec(g, &mut out, pool);
7942 out
7943 })
7944}
7945
7946thread_local! {
7947 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
7950 const { std::cell::RefCell::new(None) };
7951}
7952
7953fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
7958 let inter = d.gate_proj.rows();
7959 FFN_SCRATCH.with(|s| {
7960 let mut s = s.borrow_mut();
7961 let [g, u, ..] = &mut *s;
7962 g.resize(inter, 0.0);
7963 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
7964 } else {
7966 u.resize(inter, 0.0);
7967 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
7968 for i in 0..inter {
7969 g[i] = d.act.combine(g[i], u[i]);
7970 }
7971 }
7972 zero_masked_cols(g, 1, inter, mask_row);
7973 let mut out = attention::take_buf(d.down_proj.rows());
7974 d.down_proj.matvec(g, &mut out, pool);
7975 out
7976 })
7977}
7978
7979fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
7985 if d.act != Act::Silu {
7987 return None;
7988 }
7989 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
7992 return None;
7993 }
7994 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
7995 let mut model_ref = None;
7996 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
7997 let model = model_ref?;
7998 let hidden = jobs[0].down.1;
7999 let mut out = attention::take_buf(hidden);
8000 if crate::gpu::moe_block(&model, &jobs, &mut out) {
8001 Some(out)
8002 } else {
8003 let mut out = out;
8004 attention::recycle_buf(&mut out);
8005 None
8006 }
8007}
8008
8009#[allow(clippy::type_complexity)]
8014#[allow(clippy::type_complexity)]
8015pub(crate) fn moe_parts(
8016 t: &QTensor,
8017) -> Option<(
8018 &std::sync::Arc<cortiq_core::CmfModel>,
8019 usize,
8020 usize,
8021 usize,
8022 &[f32],
8023 &[f32],
8024 bool,
8025 bool,
8026 bool,
8027)> {
8028 match t {
8029 QTensor::Mapped {
8030 model,
8031 idx,
8032 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
8033 rows,
8034 cols,
8035 row_scale,
8036 col_field,
8037 ..
8038 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
8039 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
8040 )),
8041 QTensor::Mapped {
8043 model,
8044 idx,
8045 dtype: cortiq_core::TensorDtype::Q1,
8046 rows,
8047 cols,
8048 ..
8049 } => Some((
8050 model,
8051 *idx,
8052 *rows,
8053 *cols,
8054 &[][..],
8055 &[][..],
8056 true,
8057 false,
8058 false,
8059 )),
8060 QTensor::Mapped {
8062 model,
8063 idx,
8064 dtype: cortiq_core::TensorDtype::Q4Tiled,
8065 rows,
8066 cols,
8067 ..
8068 } => Some((
8069 model,
8070 *idx,
8071 *rows,
8072 *cols,
8073 &[][..],
8074 &[][..],
8075 false,
8076 true,
8077 false,
8078 )),
8079 QTensor::Mapped {
8081 model,
8082 idx,
8083 dtype: cortiq_core::TensorDtype::Q4TiledP,
8084 rows,
8085 cols,
8086 ..
8087 } => Some((
8088 model,
8089 *idx,
8090 *rows,
8091 *cols,
8092 &[][..],
8093 &[][..],
8094 false,
8095 true,
8096 false,
8097 )),
8098 QTensor::Mapped {
8102 model,
8103 idx,
8104 dtype: cortiq_core::TensorDtype::Q2TiledP,
8105 rows,
8106 cols,
8107 ..
8108 } => Some((
8109 model,
8110 *idx,
8111 *rows,
8112 *cols,
8113 &[][..],
8114 &[][..],
8115 false,
8116 true,
8117 true,
8118 )),
8119 _ => None,
8120 }
8121}
8122
8123#[cfg(target_os = "macos")]
8129fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
8130 if m.router_sigmoid
8131 || m.router_input_norm
8132 || m.expert_bias.is_some()
8133 || m.route_tau.is_some()
8134 || m.mask.is_some()
8135 || m.per_expert_scale.is_some()
8136 || m.experts.is_empty()
8137 || m.top_k == 0
8138 {
8139 return None;
8140 }
8141 let (sh, sg) = match &m.shared {
8144 Some((sh, Some(sg))) => (sh, sg),
8145 _ => return None,
8146 };
8147 let (rf, rr, rc) = m.router.f32_parts()?;
8148 if rr != m.experts.len() || rc != hidden {
8149 return None;
8150 }
8151 let (sf, sr, sc) = sg.f32_parts()?;
8152 if sr * sc != hidden {
8153 return None;
8154 }
8155 let inter = m.experts[0].gate_proj.rows();
8156 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
8159 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
8160 if e.act != Act::Silu
8161 || e.gate_proj.rows() != inter
8162 || e.gate_proj.cols() != hidden
8163 || e.up_proj.rows() != inter
8164 || e.up_proj.cols() != hidden
8165 || e.down_proj.rows() != hidden
8166 || e.down_proj.cols() != inter
8167 {
8168 return None;
8169 }
8170 let pick = |t: &QTensor| -> Option<usize> {
8171 if gu_q2 {
8172 t.mapped_q2tp().map(|(_, i)| i)
8173 } else {
8174 t.mapped_q4tp().map(|(_, i)| i)
8175 }
8176 };
8177 Some((
8178 pick(&e.gate_proj)?,
8179 pick(&e.up_proj)?,
8180 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
8181 ))
8182 };
8183 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
8184 let shared = trio(sh)?;
8185 Some(crate::gpu::GpuMoe {
8186 router: rf,
8187 sgate: sf,
8188 experts,
8189 shared,
8190 n_exp: m.experts.len(),
8191 top_k: m.top_k,
8192 inter,
8193 norm_topk: m.norm_topk_prob,
8194 route_scale: m.routed_scaling,
8195 gu_q2,
8196 })
8197}
8198
8199pub(crate) fn moe_push_job_parts<'a>(
8203 gate: &'a QTensor,
8204 up: &'a QTensor,
8205 down: &'a QTensor,
8206 x: &[f32],
8207 w: f32,
8208 swiglu_limit: f32,
8209 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
8210 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
8211) -> Option<()> {
8212 use crate::qtensor::prescale;
8213 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
8214 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
8215 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
8216 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
8217 return None; }
8219 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
8222 return None;
8223 }
8224 if !gq2 && dq2 {
8225 return None;
8226 }
8227 model_ref.get_or_insert_with(|| gm.clone());
8228 let dt = |cf: &[f32]| {
8229 if cf.is_empty() {
8230 cortiq_core::TensorDtype::Q8Row
8231 } else {
8232 cortiq_core::TensorDtype::Q8_2f
8233 }
8234 };
8235 jobs.push(crate::gpu::MoeJob {
8236 gate: (gi, gr, gc, grs),
8237 up: (ui, ur, uc, urs),
8238 down: (di, dr, dc, drs),
8239 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
8240 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
8241 down_col: dcf,
8242 w,
8243 q1: gq1,
8244 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
8245 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
8246 gu_q2: gq2,
8247 swiglu_limit,
8248 });
8249 Some(())
8250}
8251
8252fn moe_push_job<'a>(
8254 d: &'a DenseFfn,
8255 x: &[f32],
8256 w: f32,
8257 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
8258 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
8259) -> Option<()> {
8260 use crate::qtensor::prescale;
8261 if d.act != Act::Silu {
8262 return None; }
8264 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
8265 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
8266 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
8267 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
8268 return None; }
8270 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
8271 return None;
8272 }
8273 if !gq2 && dq2 {
8274 return None;
8275 }
8276 model_ref.get_or_insert_with(|| gm.clone());
8277 let gdt = if gcf.is_empty() {
8278 cortiq_core::TensorDtype::Q8Row
8279 } else {
8280 cortiq_core::TensorDtype::Q8_2f
8281 };
8282 let udt = if ucf.is_empty() {
8283 cortiq_core::TensorDtype::Q8Row
8284 } else {
8285 cortiq_core::TensorDtype::Q8_2f
8286 };
8287 jobs.push(crate::gpu::MoeJob {
8288 gate: (gi, gr, gc, grs),
8289 up: (ui, ur, uc, urs),
8290 down: (di, dr, dc, drs),
8291 xs_gate: prescale(x, gcf, gdt).into_owned(),
8292 xs_up: prescale(x, ucf, udt).into_owned(),
8293 down_col: dcf,
8294 w,
8295 q1: gq1,
8296 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
8297 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
8298 gu_q2: gq2,
8299 swiglu_limit: 0.0,
8300 });
8301 Some(())
8302}
8303
8304fn sparse_ffn_quant(
8311 d: &DenseFfn,
8312 x: &[f32],
8313 active: &[u16],
8314 hidden: usize,
8315 pool: Option<&Pool>,
8316) -> Vec<f32> {
8317 let n = active.len();
8318 let inter = d.gate_proj.rows();
8319 let mut act = vec![0.0f32; n];
8320 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
8323 let compute = |ai: usize| -> f32 {
8324 let idx = active[ai] as usize;
8325 if idx >= inter {
8326 return 0.0; }
8328 let mut s = if need_scratch {
8329 vec![0.0f32; hidden]
8330 } else {
8331 Vec::new()
8332 };
8333 let gate = d.gate_proj.row_dot(idx, x, &mut s);
8334 let up = d.up_proj.row_dot(idx, x, &mut s);
8335 d.act.combine(gate, up)
8336 };
8337 match pool {
8338 Some(p) if n >= 256 => {
8339 let ptr = SendMut(act.as_mut_ptr());
8340 p.run(&|widx, nw| {
8341 let chunk = n.div_ceil(nw);
8342 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
8343 for ai in s..e {
8344 unsafe { *ptr.at(ai) = compute(ai) };
8345 }
8346 });
8347 }
8348 _ => {
8349 for (ai, a) in act.iter_mut().enumerate() {
8350 *a = compute(ai);
8351 }
8352 }
8353 }
8354 let mut out = vec![0.0f32; hidden];
8356 for (ai, &idx) in active.iter().enumerate() {
8357 let w = act[ai];
8358 if w.abs() >= 1e-12 && (idx as usize) < inter {
8359 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
8360 }
8361 }
8362 out
8363}
8364
8365#[doc(hidden)]
8367pub fn sparse_ffn_quant_for_test(
8368 d: &DenseFfn,
8369 x: &[f32],
8370 active: &[u16],
8371 hidden: usize,
8372) -> Vec<f32> {
8373 sparse_ffn_quant(d, x, active, hidden, None)
8374}
8375
8376fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
8380 let deq = |t: &QTensor| -> Vec<f32> {
8381 let (rows, cols) = (t.rows(), t.cols());
8382 let mut out = vec![0.0f32; rows * cols];
8383 for r in 0..rows {
8384 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
8385 }
8386 out
8387 };
8388 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
8389}
8390
8391struct SendMut(*mut f32);
8393unsafe impl Send for SendMut {}
8394unsafe impl Sync for SendMut {}
8395impl SendMut {
8396 #[inline]
8397 #[allow(clippy::mut_from_ref)]
8400 unsafe fn at(&self, i: usize) -> &mut f32 {
8401 unsafe { &mut *self.0.add(i) }
8402 }
8403}
8404
8405fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
8415 let ne = logits.len();
8416 let p: Vec<f32> = if m.router_sigmoid {
8417 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
8418 } else {
8419 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8420 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
8421 let s: f32 = e.iter().sum();
8422 for v in &mut e {
8423 *v /= s;
8424 }
8425 e
8426 };
8427 let admit = |e: usize| {
8433 m.mask.as_ref().is_none_or(|mk| mk[e])
8434 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
8435 };
8436 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
8437 match &m.expert_bias {
8439 Some(b) => idx.sort_unstable_by(|&x, &y| {
8440 (p[y] + b[y])
8441 .partial_cmp(&(p[x] + b[x]))
8442 .unwrap()
8443 .then(x.cmp(&y))
8444 }),
8445 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
8446 }
8447 idx.truncate(m.top_k);
8448 if let Some(tau) = m.route_tau {
8452 let total: f32 = idx.iter().map(|&e| p[e]).sum();
8453 if total > 0.0 {
8454 let mut acc = 0.0f32;
8455 let mut keep = idx.len();
8456 for (i, &e) in idx.iter().enumerate() {
8457 acc += p[e];
8458 if acc >= tau * total {
8459 keep = i + 1;
8460 break;
8461 }
8462 }
8463 idx.truncate(keep);
8464 }
8465 }
8466 let wsum: f32 = if m.norm_topk_prob {
8467 let s: f32 = idx.iter().map(|&e| p[e]).sum();
8468 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
8471 } else {
8472 1.0 / m.routed_scaling
8473 };
8474 (idx, p, wsum)
8475}
8476
8477fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
8480 accumulate_act(m, x, 1);
8481 let ne = m.experts.len();
8482 let mut logits = vec![0.0f32; ne];
8483 m.router.matvec(x, &mut logits, pool);
8484 let (idx, p, wsum) = moe_route(&logits, m, allowed);
8485 {
8486 let mut st = m.stats.borrow_mut();
8487 if st.len() < ne {
8488 st.resize(ne, 0);
8489 }
8490 for &e in &idx {
8491 st[e] += 1;
8492 }
8493 }
8494 if crate::gpu::enabled_here() {
8499 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
8500 crate::gpu::ProbeArm::Gpu => {
8501 let t0 = std::time::Instant::now();
8502 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
8503 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
8504 return out;
8505 }
8506 }
8507 crate::gpu::ProbeArm::CpuTimed => {
8508 let t0 = std::time::Instant::now();
8509 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
8510 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
8511 return out;
8512 }
8513 crate::gpu::ProbeArm::Cpu => {
8514 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
8515 }
8516 }
8517 }
8518 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
8519}
8520
8521fn graph_note(built: bool) {
8525 use std::sync::atomic::{AtomicBool, Ordering};
8526 if built {
8527 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
8528 } else {
8529 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
8530 }
8531 static SAID: AtomicBool = AtomicBool::new(false);
8532 if !SAID.swap(true, Ordering::Relaxed) {
8533 if built {
8534 tracing::info!("wgpu whole-token graph: ACTIVE");
8535 } else {
8536 tracing::warn!("wgpu whole-token graph refused — per-op path");
8537 }
8538 }
8539}
8540
8541pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
8545pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
8546
8547fn moe_batch_enabled() -> bool {
8550 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8551 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
8552}
8553
8554fn moe_ffn_cpu_batched(
8560 m: &MoeFfn,
8561 x: &[f32],
8562 idx: &[usize],
8563 p: &[f32],
8564 wsum: f32,
8565 pool: Option<&Pool>,
8566) -> Option<Vec<f32>> {
8567 if idx.is_empty() || !moe_batch_enabled() {
8568 return None;
8569 }
8570 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
8574 return None;
8575 }
8576 let n = idx.len() + usize::from(m.shared.is_some());
8577 let mut pairs = Vec::with_capacity(n);
8578 let mut downs = Vec::with_capacity(n);
8579 let mut ws = Vec::with_capacity(n);
8580 for &e in idx {
8581 let d = &m.experts[e];
8582 if d.act != Act::Silu {
8583 return None;
8584 }
8585 pairs.push((&d.gate_proj, &d.up_proj));
8586 downs.push(&d.down_proj);
8587 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
8588 }
8589 if let Some((se, gate)) = &m.shared {
8592 if se.act != Act::Silu {
8593 return None;
8594 }
8595 let g = gate.as_ref().map_or(1.0, |gate| {
8596 let mut gl = [0.0f32; 1];
8597 gate.matvec(x, &mut gl, pool);
8598 1.0 / (1.0 + (-gl[0]).exp())
8599 });
8600 pairs.push((&se.gate_proj, &se.up_proj));
8601 downs.push(&se.down_proj);
8602 ws.push(g);
8603 }
8604 let inter = pairs[0].0.rows();
8605 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
8606 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
8607 return None;
8608 }
8609 let mut out = attention::take_buf(x.len());
8610 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
8611 attention::recycle_buf(&mut out);
8612 return None;
8613 }
8614 Some(out)
8615}
8616
8617fn moe_ffn_cpu(
8619 m: &MoeFfn,
8620 x: &[f32],
8621 idx: &[usize],
8622 p: &[f32],
8623 wsum: f32,
8624 pool: Option<&Pool>,
8625) -> Vec<f32> {
8626 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
8627 return out;
8628 }
8629 let mut out = attention::take_buf(x.len());
8630 for &e in idx {
8631 let mut eo = dense_ffn(&m.experts[e], x, pool);
8632 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
8633 for i in 0..out.len() {
8634 out[i] += w * eo[i];
8635 }
8636 attention::recycle_buf(&mut eo);
8637 }
8638 if let Some((se, gate)) = &m.shared {
8639 let mut so = dense_ffn(se, x, pool);
8640 let g = gate.as_ref().map_or(1.0, |gate| {
8641 let mut gl = [0.0f32; 1];
8642 gate.matvec(x, &mut gl, pool);
8643 1.0 / (1.0 + (-gl[0]).exp())
8644 });
8645 for i in 0..out.len() {
8646 out[i] += g * so[i];
8647 }
8648 attention::recycle_buf(&mut so);
8649 }
8650 out
8651}
8652
8653#[allow(clippy::too_many_arguments)]
8661fn mla_attention(
8662 w: &MlaWeights,
8663 normed: &[f32],
8664 cache: &mut crate::kv_cache::LayerKvCache,
8665 position: usize,
8666 inv_freq: &[f32],
8667 rope_scale: f32,
8668 eps: f64,
8669 pool: Option<&Pool>,
8670) -> Vec<f32> {
8671 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
8672 let hd = dr + dn;
8673 let mut q = vec![0.0f32; nh * hd];
8674 match (&w.q_a, &w.q_a_norm) {
8675 (Some(qa), Some(qn)) => {
8676 let mut t = vec![0.0f32; qa.rows()];
8677 qa.matvec(normed, &mut t, pool);
8678 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
8679 w.q_proj.matvec(&tn, &mut q, pool);
8680 }
8681 _ => w.q_proj.matvec(normed, &mut q, pool),
8682 }
8683 let mut ca = vec![0.0f32; lora + dr];
8684 w.kv_a.matvec(normed, &mut ca, pool);
8685 let (c_lat, k_rope) = ca.split_at_mut(lora);
8686 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
8687 let mut kvb = vec![0.0f32; nh * (dn + dv)];
8688 w.kv_b.matvec(&latn, &mut kvb, pool);
8689 if !w.nope {
8690 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
8691 }
8692 for h in 0..nh {
8693 if !w.nope {
8694 attention::rope_rotate_scaled(
8695 &mut q[h * hd..h * hd + dr],
8696 position,
8697 inv_freq,
8698 rope_scale,
8699 );
8700 }
8701 }
8702 let mut k = vec![0.0f32; nh * hd];
8703 let mut v = vec![0.0f32; nh * hd];
8704 for h in 0..nh {
8705 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
8706 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
8707 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
8708 }
8709 cache.append(&k, &v, &vec![true; nh]);
8710 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
8711 attention::recycle_buf(&mut imp);
8712 let mut ov = vec![0.0f32; nh * dv];
8713 for h in 0..nh {
8714 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
8715 }
8716 let mut out = vec![0.0f32; w.o_proj.rows()];
8717 w.o_proj.matvec(&ov, &mut out, pool);
8718 out
8719}
8720
8721fn dense_moe_ffn(
8728 dm: &DenseMoeFfn,
8729 x_normed: &[f32],
8730 h_raw: &[f32],
8731 eps: f64,
8732 norm_style: NormStyle,
8733 pool: Option<&Pool>,
8734) -> Vec<f32> {
8735 let mut d = dense_ffn(&dm.dense, x_normed, pool);
8736 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
8737 let m = &dm.moe;
8738 let ne = m.experts.len();
8739 let mut logits = vec![0.0f32; ne];
8740 if m.router_input_norm {
8741 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
8742 let inv = 1.0 / (ss + eps as f32).sqrt();
8743 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
8744 m.router.matvec(&xr, &mut logits, pool);
8745 } else {
8746 m.router.matvec(h_raw, &mut logits, pool);
8747 }
8748 let (idx, p, wsum) = moe_route(&logits, m, None);
8749 {
8750 let mut st = m.stats.borrow_mut();
8751 if st.len() < ne {
8752 st.resize(ne, 0);
8753 }
8754 for &e in &idx {
8755 st[e] += 1;
8756 }
8757 }
8758 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
8759 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
8760 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
8761 for (di, mi) in d.iter_mut().zip(&mo) {
8762 *di += mi;
8763 }
8764 d
8765}
8766
8767fn moe_gpu_refused(why: &'static str) {
8774 use std::sync::atomic::{AtomicBool, Ordering};
8775 static SAID: AtomicBool = AtomicBool::new(false);
8776 if !SAID.swap(true, Ordering::Relaxed) {
8777 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
8778 }
8779}
8780
8781fn moe_ffn_gpu(
8782 m: &MoeFfn,
8783 x: &[f32],
8784 idx: &[usize],
8785 p: &[f32],
8786 wsum: f32,
8787 pool: Option<&Pool>,
8788) -> Option<Vec<f32>> {
8789 use crate::gpu::MoeJob;
8790
8791 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
8792 let mut model_ref = None;
8793 for &e in idx {
8794 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
8795 moe_gpu_refused("push_job(expert)");
8796 return None;
8797 }
8798 }
8799 if let Some((se, gate)) = &m.shared {
8800 let g = gate.as_ref().map_or(1.0, |gate| {
8801 let mut gl = [0.0f32; 1];
8802 gate.matvec(x, &mut gl, pool);
8803 1.0 / (1.0 + (-gl[0]).exp())
8804 });
8805 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
8806 moe_gpu_refused("push_job(shared)");
8807 return None;
8808 }
8809 }
8810 let Some(model) = model_ref else {
8811 moe_gpu_refused("no model_ref");
8812 return None;
8813 };
8814 let hidden = jobs[0].down.1;
8815 let mut out = vec![0.0f32; hidden];
8816 if crate::gpu::moe_block(&model, &jobs, &mut out) {
8817 Some(out)
8818 } else {
8819 moe_gpu_refused("gpu::moe_block");
8820 None
8821 }
8822}
8823
8824fn ffn_forward(
8826 ffn: &FfnKind,
8827 x: &[f32],
8828 pool: Option<&Pool>,
8829 experts_allowed: Option<&[bool]>,
8830) -> Vec<f32> {
8831 match ffn {
8832 FfnKind::Dense(d) => dense_ffn(d, x, pool),
8833 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
8834 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
8838 }
8839}
8840
8841fn ffn_forward_pair(
8845 ffn: &FfnKind,
8846 x1: &[f32],
8847 x2: &[f32],
8848 pool: Option<&Pool>,
8849 experts_allowed: Option<&[bool]>,
8850) -> (Vec<f32>, Vec<f32>) {
8851 let d = match ffn {
8852 FfnKind::Dense(d) => d,
8853 FfnKind::Moe(m) => {
8854 return (
8855 moe_ffn(m, x1, pool, experts_allowed),
8856 moe_ffn(m, x2, pool, experts_allowed),
8857 );
8858 }
8859 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
8860 };
8861 let inter = d.gate_proj.rows();
8862 FFN_SCRATCH.with(|s| {
8863 let mut s = s.borrow_mut();
8864 let [g1, g2, u1, u2] = &mut *s;
8865 g1.resize(inter, 0.0);
8866 g2.resize(inter, 0.0);
8867 u1.resize(inter, 0.0);
8868 u2.resize(inter, 0.0);
8869 QTensor::matvec2_many(
8872 [&d.gate_proj, &d.up_proj],
8873 x1,
8874 x2,
8875 [g1.as_mut_slice(), u1.as_mut_slice()],
8876 [g2.as_mut_slice(), u2.as_mut_slice()],
8877 pool,
8878 );
8879 for i in 0..inter {
8880 g1[i] = d.act.combine(g1[i], u1[i]);
8881 g2[i] = d.act.combine(g2[i], u2[i]);
8882 }
8883 let mut o1 = attention::take_buf(d.down_proj.rows());
8884 let mut o2 = attention::take_buf(d.down_proj.rows());
8885 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
8886 (o1, o2)
8887 })
8888}
8889
8890#[cfg(test)]
8891mod tests {
8892
8893 #[test]
8894 fn cancel_flag_stops_generation() {
8895 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
8896 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
8899 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
8900 assert_eq!(r.finish_reason, "cancelled");
8901 assert!(
8902 r.token_ids.is_empty(),
8903 "no tokens after cancel: {:?}",
8904 r.token_ids
8905 );
8906 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
8908 assert_ne!(r2.finish_reason, "cancelled");
8909 }
8910 use super::*;
8911
8912 #[test]
8918 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
8919 let (hidden, inter) = (16usize, 40usize);
8920 let synth = |n: usize, salt: usize| -> Vec<f32> {
8921 (0..n)
8922 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
8923 .collect()
8924 };
8925 let d = DenseFfn {
8926 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
8927 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
8928 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
8929 act: Act::Silu,
8930 };
8931 let x = synth(hidden, 9);
8932 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
8934
8935 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
8936
8937 let mut g = vec![0.0f32; inter];
8939 d.gate_proj.matvec(&x, &mut g, None);
8940 let mut u = vec![0.0f32; inter];
8941 d.up_proj.matvec(&x, &mut u, None);
8942 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
8943 for i in 0..inter {
8944 g[i] = if act_set.contains(&(i as u16)) {
8945 inference::silu(g[i]) * u[i]
8946 } else {
8947 0.0
8948 };
8949 }
8950 let mut reference = vec![0.0f32; hidden];
8951 d.down_proj.matvec(&g, &mut reference, None);
8952
8953 let max_d = sparse
8954 .iter()
8955 .zip(&reference)
8956 .map(|(a, b)| (a - b).abs())
8957 .fold(0.0f32, f32::max);
8958 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
8959 }
8960
8961 fn attach_test_mtp(p: &mut Pipeline) {
8963 let (h, inter, heads, kv, hd) = (
8964 p.hidden_size,
8965 p.intermediate_size,
8966 p.num_heads,
8967 p.num_kv_heads,
8968 p.head_dim,
8969 );
8970 let synth = |n: usize, salt: usize| -> Vec<f32> {
8971 (0..n)
8972 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
8973 .collect()
8974 };
8975 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
8976 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
8977 };
8978 p.mtp = Some(MtpModule {
8979 enorm: vec![1.0; h],
8980 hnorm: vec![1.0; h],
8981 eh_proj: qt(h, 2 * h, 301),
8982 layer: LayerWeights {
8983 input_norm: vec![1.0; h],
8984 post_norm: vec![1.0; h],
8985 attn_out_norm: None,
8986 ffn_out_norm: None,
8987 layer_scale: None,
8988 ffn: FfnKind::Dense(DenseFfn {
8989 gate_proj: qt(inter, h, 315),
8990 up_proj: qt(inter, h, 316),
8991 down_proj: qt(h, inter, 317),
8992 act: Act::Silu,
8993 }),
8994 attn: AttnKind::Full {
8995 bias: None,
8996 wq: qt(heads * hd, h, 311),
8997 wk: qt(kv * hd, h, 312),
8998 wv: qt(kv * hd, h, 313),
8999 wo: qt(h, heads * hd, 314),
9000 q_norm: None,
9001 k_norm: None,
9002 output_gate: false,
9003 softplus_gate: None,
9004 },
9005 },
9006 final_norm: vec![1.0; h],
9007 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
9008 });
9009 }
9010
9011 #[test]
9012 fn speculative_equals_vanilla_greedy() {
9013 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
9017 let run = |spec: bool| {
9018 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9019 p.sampler_config.temperature = 0.0;
9020 attach_test_mtp(&mut p);
9021 p.speculative = spec;
9022 let r = p.generate("abcdef", 12, None, None).unwrap();
9023 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
9024 };
9025 let (vanilla, d0, _) = run(false);
9026 let (spec, d1, a1) = run(true);
9027 assert_eq!(d0, 0, "vanilla path must not draft");
9028 assert!(d1 > 0, "speculative path must draft");
9029 assert_eq!(
9030 vanilla, spec,
9031 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
9032 );
9033 }
9034
9035 #[test]
9036 fn speculative_accepts_constant_oracle() {
9037 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
9039 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9040 p.sampler_config.temperature = 0.0;
9041 p.sampler_config.repetition_penalty = 1.0;
9042 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
9045 attach_test_mtp(&mut p);
9046 p.speculative = true;
9047 let r = p.generate("abcd", 10, None, None).unwrap();
9048 assert!(r.mtp_drafted > 0);
9049 assert_eq!(
9050 r.mtp_accepted, r.mtp_drafted,
9051 "constant logits → every draft accepted"
9052 );
9053 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
9056 }
9057
9058 #[test]
9059 fn empty_prompt_is_an_error_not_a_panic() {
9060 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
9061 let r = p.generate("", 4, None, None);
9062 assert!(r.is_err(), "empty prompt must be a clean error");
9063 }
9064
9065 #[test]
9066 fn every_token_enters_kv_exactly_once() {
9067 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9068 p.sampler_config.temperature = 0.0;
9070 let r = p.generate("abc", 2, None, None).unwrap();
9071 assert_eq!(r.prompt_tokens, 3);
9072 assert_eq!(
9076 p.kv_cache.seq_len(),
9077 3 + r.tokens_generated - 1,
9078 "each token must be cached exactly once (v1 cached the last prompt token twice)"
9079 );
9080 }
9081
9082 #[test]
9083 fn generation_is_reproducible_with_seed() {
9084 let run = || {
9085 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9086 p.generate("hello", 8, None, None).unwrap().token_ids
9087 };
9088 assert_eq!(run(), run());
9089 }
9090
9091 #[test]
9092 fn resetting_sampler_restarts_the_seeded_stream() {
9093 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9094 let config = SamplerConfig {
9095 seed: Some(1234),
9096 ..SamplerConfig::default()
9097 };
9098 p.set_sampler_config(config.clone());
9099 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
9100 p.set_sampler_config(config);
9101 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
9102 assert_eq!(first, second);
9103 }
9104
9105 #[test]
9106 fn eviction_bounds_the_cache() {
9107 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
9108 p.kv_cache.max_seq_len = 6;
9109 p.sampler_config.temperature = 0.0;
9110 let _ = p.generate("abcd", 12, None, None).unwrap();
9111 assert!(
9112 p.kv_cache.seq_len() <= 6 + 1,
9113 "cache must stay bounded by max_seq_len (got {})",
9114 p.kv_cache.seq_len()
9115 );
9116 }
9117
9118 #[test]
9119 fn confidence_matches_tokens_and_is_a_probability() {
9120 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9121 p.sampler_config.temperature = 0.0;
9122 p.sampler_config.repetition_penalty = 1.0;
9123 let r = p.generate("abcd", 10, None, None).unwrap();
9124 assert_eq!(
9125 r.token_confidence.len(),
9126 r.token_ids.len(),
9127 "one confidence per emitted token"
9128 );
9129 for &c in &r.token_confidence {
9130 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
9131 }
9132 let logits = [1.0f32, 3.0, 0.5, 3.0];
9134 let p0 = top1_prob_t(&logits, 1, 1.0);
9135 let p1 = top1_prob_t(&logits, 3, 1.0);
9136 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
9137 assert!(p0 > 0.0 && p0 < 1.0);
9138 let sharp = top1_prob_t(&logits, 1, 1.0);
9140 let soft = top1_prob_t(&logits, 1, 2.0);
9141 assert!(soft < sharp, "higher temperature lowers peak confidence");
9142 }
9143
9144 #[test]
9145 fn trace_is_opt_in_and_parallels_the_output() {
9146 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9148 p.sampler_config.temperature = 0.0;
9149 p.sampler_config.repetition_penalty = 1.0;
9150 let r = p.generate("abcd", 10, None, None).unwrap();
9151 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
9152
9153 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9155 p.sampler_config.temperature = 0.0;
9156 p.sampler_config.repetition_penalty = 1.0;
9157 p.set_trace(true);
9158 let r = p.generate("abcd", 10, None, None).unwrap();
9159 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
9160 for (i, tr) in r.traces.iter().enumerate() {
9161 assert_eq!(tr.t, i, "trace index is sequential");
9162 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
9163 assert_eq!(
9164 tr.confidence, r.token_confidence[i],
9165 "trace confidence matches the confidence channel"
9166 );
9167 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
9169 }
9170 }
9171
9172 #[test]
9173 fn explain_prefill_logits_match_greedy_first_token() {
9174 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9178 p.sampler_config.temperature = 0.0;
9179 p.sampler_config.repetition_penalty = 1.0;
9180 let ids = p.tokenizer.encode("abcd");
9181 let logits = p.prefill_next_logits(&ids, None);
9182 let argmax = logits
9183 .iter()
9184 .enumerate()
9185 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9186 .unwrap()
9187 .0 as u32;
9188 let r = p.generate("abcd", 1, None, None).unwrap();
9189 assert_eq!(
9190 argmax, r.token_ids[0],
9191 "explain preview must match greedy emit"
9192 );
9193 }
9194
9195 #[test]
9196 fn laguna_shared_expert_is_unconditionally_added() {
9197 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
9198 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
9199 let zero_dense = || DenseFfn {
9200 gate_proj: matrix(vec![0.0; 4]),
9201 up_proj: matrix(vec![0.0; 4]),
9202 down_proj: matrix(vec![0.0; 4]),
9203 act: Act::Silu,
9204 };
9205 let shared = DenseFfn {
9206 gate_proj: identity(),
9207 up_proj: identity(),
9208 down_proj: identity(),
9209 act: Act::Silu,
9210 };
9211 let x = [1.0, 2.0];
9212 let expected = dense_ffn(&shared, &x, None);
9213 let moe = MoeFfn {
9214 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
9215 experts: vec![zero_dense()],
9216 top_k: 1,
9217 norm_topk_prob: true,
9218 router_sigmoid: true,
9219 expert_bias: None,
9220 routed_scaling: 1.0,
9221 route_tau: None,
9222 shared: Some((shared, None)),
9223 stats: std::cell::RefCell::new(Vec::new()),
9224 act_sq: std::cell::RefCell::new(Vec::new()),
9225 act_rows: std::cell::RefCell::new(Vec::new()),
9226 mask: None,
9227 per_expert_scale: None,
9228 router_input_norm: false,
9229 };
9230 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
9231 for (actual, expected) in actual.iter().zip(expected) {
9232 assert!((actual - expected).abs() < 1e-6);
9233 }
9234 }
9235}