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 #[cfg(feature = "gpu")]
1853 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
1854 #[cfg(not(feature = "gpu"))]
1855 let metal_wgpu = false;
1856 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
1857 let spec_wanted = match spec_env.as_deref() {
1858 Some("0") => false,
1859 Some(_) => {
1860 if metal_wgpu {
1861 tracing::warn!(
1862 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
1863 verified on this backend (garbage measured on Qwen3.5-0.8B)"
1864 );
1865 }
1866 true
1867 }
1868 None => spec_default_ok && !penalized && !metal_wgpu,
1869 };
1870 let graph_spec = self.speculative
1871 && graph_on
1872 && self.mtp.is_some()
1873 && task_mask.is_none()
1874 && !self.o1_active()
1875 && spec_sampling_ok
1876 && spec_wanted;
1877 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
1884 let spec_active = self.speculative
1885 && self.mtp.is_some()
1886 && task_mask.is_none()
1887 && !self.o1_active()
1888 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
1889 let mut mtp = if spec_active { self.mtp.take() } else { None };
1892 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
1893 eprintln!(
1894 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
1895 mtp.is_some(),
1896 self.speculative,
1897 self.sampler_config.temperature < 1e-6,
1898 );
1899 }
1900 if let Some(m) = &mut mtp {
1901 m.kv.clear();
1902 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1904 self.mtp_graph_mode = None;
1905 }
1906 let mut router = if mtp.is_none() {
1910 self.dyn_router.take()
1911 } else {
1912 None
1913 };
1914 if let Some(r) = &mut router {
1915 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1918 }
1919
1920 let mut all_ids = input_ids.to_vec();
1921 let mut generated = 0usize;
1922 let mut finish_reason = "max_tokens".to_string();
1923 let mut drafted = 0usize;
1924 let mut accepted = 0usize;
1925 let mut confidence: Vec<f32> = Vec::new();
1926 let trace_on = self.trace;
1927 let calib_temp = self.calib_temp;
1928 let mut traces: Vec<TokenTrace> = Vec::new();
1929
1930 let mut hidden = vec![0.0f32; self.hidden_size];
1936 let mut pos = reuse_from;
1937 let fuse_lm = mtp.is_none()
1946 && router.is_none()
1947 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
1948 self.graph_logits = None;
1949 self.graph_want_logits = false;
1950 let _tpf = std::time::Instant::now();
1951 let batch_k = std::env::var("CMF_BATCH_K")
1952 .ok()
1953 .and_then(|v| v.parse::<usize>().ok())
1954 .unwrap_or(0);
1955 while self.dsv4.is_some()
1966 && mtp.is_none()
1967 && pos < input_ids.len()
1968 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1969 {
1970 let end = (pos + prefill_chunk()).min(input_ids.len());
1971 let ids: Vec<u32> = input_ids[pos..end].to_vec();
1972 let mut lg = Vec::new();
1973 if let Some(b) = &mut self.dsv4 {
1974 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
1975 crate::dsv4::forward_chunk(
1976 g,
1977 layers,
1978 &cfg,
1979 st,
1980 &ids,
1981 pos,
1982 &self.inv_freq,
1983 self.pool.as_deref(),
1984 &mut lg,
1985 end == input_ids.len(),
1986 );
1987 }
1988 if end == input_ids.len() {
1989 self.graph_logits = Some(lg);
1990 }
1991 pos = end;
1992 hidden = vec![0.0; self.hidden_size];
1993 }
1994 let dyn_prefill = router.is_some();
1999 let graph_prefill = self.graph_prefill_preferred();
2005 if task_mask.is_none()
2006 && !dyn_prefill
2007 && !graph_prefill
2008 && self.can_prefill_batched()
2009 && self.g3n.is_none()
2010 && input_ids.len() > 2
2011 {
2012 let chunk = prefill_chunk();
2018 let hs = self.hidden_size;
2019 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2020 let end = (pos + chunk).min(input_ids.len());
2021 let hb = self.prefill_batch(&input_ids[pos..end], pos);
2022 if let Some(m) = &mut mtp {
2023 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2024 .ok()
2025 .and_then(|v| v.parse().ok())
2026 .unwrap_or(0);
2027 for p in pos..end {
2028 if p + 1 < input_ids.len() {
2029 if probe >= 1 && p + 2 < input_ids.len() {
2030 let (d1, mut hx) = self.mtp_step_h(
2034 m,
2035 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2036 input_ids[p + 1],
2037 p,
2038 );
2039 let mut ok = d1 == input_ids[p + 2];
2040 Self::chain_probe_note(0, ok);
2041 let mut d_prev = d1;
2042 let mut extra = 0usize;
2043 for j in 1..probe {
2044 if p + 2 + j >= input_ids.len() {
2045 break;
2046 }
2047 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
2048 extra += 1;
2049 ok = ok && dj == input_ids[p + 2 + j];
2050 Self::chain_probe_note(j, ok);
2051 d_prev = dj;
2052 hx = hj;
2053 }
2054 m.kv.truncate_last(extra);
2055 } else {
2056 let _ = self.mtp_step(
2057 m,
2058 &hb[(p - pos) * hs..(p - pos + 1) * hs],
2059 input_ids[p + 1],
2060 p,
2061 );
2062 }
2063 }
2064 }
2065 }
2066 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2067 pos = end;
2068 }
2069 }
2070 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
2071 if task_mask.is_none()
2072 && !dyn_prefill
2073 && !graph_prefill
2074 && !pair_off
2075 && self.pair_supported()
2076 {
2077 while pos + 1 < input_ids.len()
2078 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
2079 {
2080 let e1 = self.embed_single(input_ids[pos]);
2081 let e2 = self.embed_single(input_ids[pos + 1]);
2082 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
2083 self.commit_linear_scratch();
2085 if let Some(m) = &mut mtp {
2086 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
2087 if pos + 2 < input_ids.len() {
2088 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2089 .ok()
2090 .and_then(|v| v.parse().ok())
2091 .unwrap_or(0);
2092 if probe >= 1 && pos + 3 < input_ids.len() {
2093 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
2097 let mut ok = d1 == input_ids[pos + 3];
2098 Self::chain_probe_note(0, ok);
2099 let mut d_prev = d1;
2100 let mut extra = 0usize;
2101 for j in 1..probe {
2102 if pos + 3 + j >= input_ids.len() {
2103 break;
2104 }
2105 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
2106 extra += 1;
2107 ok = ok && dj == input_ids[pos + 3 + j];
2108 Self::chain_probe_note(j, ok);
2109 d_prev = dj;
2110 hx = hj;
2111 }
2112 m.kv.truncate_last(extra);
2113 } else {
2114 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
2115 }
2116 }
2117 }
2118 hidden = h2;
2119 pos += 2;
2120 }
2121 }
2122 if batch_k > 0
2131 && graph_prefill
2132 && task_mask.is_none()
2133 && !self.o1_active()
2134 && mtp.is_none()
2135 && !dyn_prefill
2136 && pos + 1 < input_ids.len()
2137 {
2138 let hs = self.hidden_size;
2139 let chunk = batch_k;
2140 while pos < input_ids.len() {
2141 let end = (pos + chunk).min(input_ids.len());
2142 let bk = end - pos;
2143 let mut hiddens = vec![0f32; bk * hs];
2144 for (j, &id) in input_ids[pos..end].iter().enumerate() {
2145 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2146 }
2147 let positions: Vec<usize> = (pos..end).collect();
2148 let t_chunk = std::time::Instant::now();
2149 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2150 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2151 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2152 eprintln!(
2153 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
2154 bk as f64 / (ms / 1000.0)
2155 );
2156 }
2157 {
2158 use std::sync::atomic::{AtomicBool, Ordering};
2159 static SAID: AtomicBool = AtomicBool::new(false);
2160 if !SAID.swap(true, Ordering::Relaxed) {
2161 if ok_b {
2162 tracing::info!("batched prefill: ACTIVE (k={bk})");
2163 } else {
2164 tracing::warn!("batched prefill declined — per-position graph");
2165 }
2166 }
2167 }
2168 if ok_b {
2169 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
2170 pos = end;
2171 } else {
2172 break; }
2174 }
2175 }
2176 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2177 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
2178 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
2179 if let Some(m) = &mut mtp {
2180 if pos + 1 < input_ids.len() {
2181 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
2187 .ok()
2188 .and_then(|v| v.parse().ok())
2189 .unwrap_or(0);
2190 if probe >= 1 && pos + 2 < input_ids.len() {
2191 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
2192 let mut ok = d1 == input_ids[pos + 2];
2193 Self::chain_probe_note(0, ok);
2194 let mut d_prev = d1;
2195 let mut extra = 0usize;
2196 for j in 1..probe {
2197 if pos + 2 + j >= input_ids.len() {
2198 break;
2199 }
2200 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
2201 extra += 1;
2202 ok = ok && dj == input_ids[pos + 2 + j];
2203 Self::chain_probe_note(j, ok);
2204 d_prev = dj;
2205 hx = hj;
2206 }
2207 m.kv.truncate_last(extra);
2210 } else {
2211 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
2212 }
2213 }
2214 }
2215 pos += 1;
2216 }
2217 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2218 eprintln!(
2219 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
2220 input_ids.len(),
2221 _tpf.elapsed().as_secs_f64() * 1000.0
2222 );
2223 }
2224 if self
2227 .cancel
2228 .swap(false, std::sync::atomic::Ordering::Relaxed)
2229 {
2230 self.kv_history.clear();
2231 if let Some(m) = mtp {
2232 self.mtp = Some(m);
2233 }
2234 return Ok(GenerateResult {
2235 text: String::new(),
2236 token_ids: Vec::new(),
2237 prompt_tokens: input_ids.len(),
2238 tokens_generated: 0,
2239 finish_reason: "cancelled".to_string(),
2240 mtp_drafted: 0,
2241 mtp_accepted: 0,
2242 token_confidence: Vec::new(),
2243 traces: Vec::new(),
2244 });
2245 }
2246
2247 self.o1_seal();
2250
2251 macro_rules! commit {
2253 ($id:expr) => {{
2254 all_ids.push($id);
2255 generated += 1;
2256 if self.tokenizer.is_eos($id) {
2257 finish_reason = "stop".to_string();
2258 false
2259 } else {
2260 let token_text = self.tokenizer.decode_token($id);
2261 let mut go = true;
2262 if let Some(ref mut cb) = on_token {
2263 if !cb(&token_text) {
2264 finish_reason = "cancelled".to_string();
2265 go = false;
2266 }
2267 }
2268 go
2269 }
2270 }};
2271 }
2272
2273 let mut spec_trial = SpecTrial::Spec {
2284 t0: std::time::Instant::now(),
2285 gen0: generated,
2286 rounds: 0,
2287 };
2288 let mut spec_mon = SpecMon::default();
2289 let mut spec_watchdog_off = false;
2290 let mut next_pos = input_ids.len();
2292 'decode: while generated < max_tokens {
2293 if self
2294 .cancel
2295 .swap(false, std::sync::atomic::Ordering::Relaxed)
2296 {
2297 finish_reason = "cancelled".to_string();
2298 break 'decode;
2299 }
2300 let forced = self.spec_forced.take();
2305 let mut logits = match (forced, self.graph_logits.take()) {
2306 (Some(_), _) => Vec::new(),
2307 (None, Some(lg)) => lg,
2308 (None, None) => {
2309 inference::rms_norm_into(
2310 &hidden,
2311 &self.weights.final_norm,
2312 self.rms_eps,
2313 self.norm_style,
2314 &mut self.ws.n1,
2315 );
2316 self.lm_head_forward(&self.ws.n1)
2317 }
2318 };
2319 let t_next = match forced {
2320 Some(c) => c,
2321 None => sampler::sample_with_scratch_pool(
2322 &logits,
2323 &self.sampler_config,
2324 &all_ids,
2325 &mut self.rng,
2326 &mut self.sampler_scratch,
2327 self.pool.as_deref(),
2328 ),
2329 };
2330 if self.confidence_on {
2331 confidence.push(if logits.is_empty() {
2332 0.0
2333 } else {
2334 sampler::top1_prob_pool(
2335 self.pool.as_deref(),
2336 &mut self.sampler_scratch,
2337 &logits,
2338 t_next,
2339 calib_temp,
2340 )
2341 });
2342 }
2343 if !logits.is_empty() {
2344 attention::recycle_buf(&mut logits);
2345 }
2346 if trace_on {
2347 let skill = router.as_ref().and_then(|r| r.active_id());
2351 traces.push(TokenTrace {
2352 t: generated,
2353 token_id: t_next,
2354 confidence: confidence.last().copied().unwrap_or(0.0),
2355 active_skill: skill,
2356 recon: None,
2357 switched: false,
2358 });
2359 }
2360 if !commit!(t_next) {
2361 break 'decode;
2362 }
2363 if generated >= max_tokens {
2364 break 'decode;
2365 }
2366
2367 if self.kv_cache.needs_eviction() {
2368 static SAID: std::sync::Once = std::sync::Once::new();
2374 SAID.call_once(|| {
2375 tracing::warn!(
2376 "KV cache full at {} positions — evicting half; quality \
2377 will degrade. Raise CMF_MAX_SEQ.",
2378 self.kv_cache.max_seq_len,
2379 );
2380 });
2381 let keep = (self.kv_cache.max_seq_len / 2).max(1);
2382 self.kv_cache.evict(keep);
2383 }
2384
2385 if graph_spec {
2388 match spec_trial {
2389 SpecTrial::Plain { t0, gen0 } if generated >= gen0 + 8 => {
2390 spec_mon.plain_ms =
2391 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
2392 let keep = spec_mon.pays();
2393 tracing::info!(
2394 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2395 spec_mon.tokens,
2396 spec_mon.round_ms,
2397 spec_mon.plain_ms,
2398 if keep { "speculating" } else { "plain" }
2399 );
2400 spec_mon.fails = 0;
2401 spec_trial = SpecTrial::Decided {
2402 spec: keep,
2403 recheck_at: if keep { usize::MAX } else { generated + 128 },
2404 };
2405 }
2406 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
2407 spec_mon.n = 0;
2408 spec_trial = SpecTrial::Spec {
2409 t0: std::time::Instant::now(),
2410 gen0: generated,
2411 rounds: 0,
2412 };
2413 }
2414 _ => {}
2415 }
2416 spec_watchdog_off = matches!(
2417 spec_trial,
2418 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
2419 );
2420 }
2421 match &mut mtp {
2422 #[cfg(feature = "gpu")]
2424 Some(m)
2425 if graph_spec
2426 && !spec_watchdog_off
2427 && generated + 1 < max_tokens
2428 && next_pos > 0 =>
2429 {
2430 let t_round = std::time::Instant::now();
2431 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
2432 m,
2433 &hidden,
2434 t_next,
2435 next_pos,
2436 &mut drafted,
2437 &mut accepted,
2438 &mut all_ids,
2439 ) {
2440 next_pos = n_pos;
2441 hidden = new_h;
2442 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
2446 spec_trial = Self::spec_trial_round(
2449 spec_trial,
2450 &mut spec_mon,
2451 generated + extra.len() + 1,
2452 );
2453 let mut stopped = false;
2454 for &id in &extra {
2455 if self.confidence_on {
2456 confidence.push(0.0);
2457 }
2458 if !commit!(id) {
2459 stopped = true;
2460 break;
2461 }
2462 }
2463 if stopped {
2464 break 'decode;
2465 }
2466 continue 'decode;
2467 }
2468 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
2479 spec_mon.tokens = 0.0;
2480 spec_mon.fails = 3;
2481 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
2482 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2483 next_pos += 1;
2484 continue 'decode;
2485 }
2486 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2488 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2489 drafted += 1;
2490 let emb1 = self.embed_single(t_next);
2491 let emb2 = self.embed_single(draft);
2492 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2493
2494 inference::rms_norm_into(
2495 &h1,
2496 &self.weights.final_norm,
2497 self.rms_eps,
2498 self.norm_style,
2499 &mut self.ws.n1,
2500 );
2501 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2502 let t_after = sampler::sample_with_scratch_pool(
2503 &logits1,
2504 &self.sampler_config,
2505 &all_ids,
2506 &mut self.rng,
2507 &mut self.sampler_scratch,
2508 self.pool.as_deref(),
2509 );
2510 if self.confidence_on {
2511 confidence.push(sampler::top1_prob_pool(
2512 self.pool.as_deref(),
2513 &mut self.sampler_scratch,
2514 &logits1,
2515 t_after,
2516 calib_temp,
2517 ));
2518 }
2519 attention::recycle_buf(&mut logits1);
2520 if trace_on {
2521 traces.push(TokenTrace {
2524 t: generated,
2525 token_id: t_after,
2526 confidence: confidence.last().copied().unwrap_or(0.0),
2527 active_skill: None,
2528 recon: None,
2529 switched: false,
2530 });
2531 }
2532 let stop = !commit!(t_after);
2533
2534 if t_after == draft {
2535 accepted += 1;
2536 self.commit_linear_scratch();
2537 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2538 hidden = h2;
2539 next_pos += 2;
2540 } else {
2541 for layer in &mut self.kv_cache.layers {
2543 layer.truncate_last(1);
2544 }
2545 if !stop {
2546 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2547 hidden = self.forward_layers(
2548 &self.embed_single(t_after),
2549 next_pos + 1,
2550 None,
2551 );
2552 }
2553 next_pos += 2;
2554 }
2555 if stop {
2556 break 'decode;
2557 }
2558 }
2559 _ => {
2561 #[cfg(feature = "gpu")]
2566 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2567 static SAID: std::sync::Once = std::sync::Once::new();
2568 SAID.call_once(|| {
2569 eprintln!(
2570 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2571 !self.dsv4_mtp.is_empty(),
2572 task_mask.is_none(),
2573 router.is_none(),
2574 !trace_on,
2575 self.sampler_config.temperature < 1e-6,
2576 self.sampler_config.repetition_penalty == 1.0,
2577 );
2578 });
2579 }
2580 #[cfg(feature = "gpu")]
2581 if Self::dsv4_spec_on()
2582 && self.dsv4.is_some()
2583 && !self.dsv4_mtp.is_empty()
2584 && task_mask.is_none()
2585 && router.is_none()
2586 && !trace_on
2587 && self.sampler_config.temperature < 1e-6
2588 && self.sampler_config.repetition_penalty == 1.0
2589 && generated + 1 < max_tokens
2590 && all_ids.len() >= 2
2591 {
2592 let tip_token = all_ids[all_ids.len() - 2];
2593 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2594 tip_token,
2595 t_next,
2596 next_pos,
2597 &mut drafted,
2598 &mut accepted,
2599 ) {
2600 next_pos = n_pos;
2601 let mut stopped = false;
2602 for &id in &extra {
2603 if self.confidence_on {
2604 confidence.push(0.0);
2605 }
2606 if !commit!(id) {
2607 stopped = true;
2608 break;
2609 }
2610 }
2611 if stopped {
2612 break 'decode;
2613 }
2614 continue 'decode;
2615 }
2616 }
2617 self.graph_want_logits = fuse_lm;
2618 let mut t_fwd = t_next;
2624 let pure_greedy = self.sampler_config.temperature < 1e-6
2625 && self.sampler_config.repetition_penalty == 1.0
2626 && self.sampler_config.suppress_tokens.is_empty();
2627 let burst_k = std::env::var("CMF_MULTISTEP")
2632 .ok()
2633 .and_then(|v| v.parse::<usize>().ok())
2634 .unwrap_or(0);
2635 if pure_greedy
2636 && burst_k >= 1
2637 && fuse_lm
2638 && task_mask.is_none()
2639 && router.is_none()
2640 && !trace_on
2641 && !self.confidence_on
2642 {
2643 let mut stopped = false;
2644 loop {
2645 let room = max_tokens.saturating_sub(generated);
2646 if room <= 2 {
2647 break;
2648 }
2649 let k = burst_k.min(room - 1);
2650 if k < 1 {
2651 break;
2652 }
2653 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2654 break;
2655 };
2656 next_pos += k;
2657 for &id in &ids {
2658 if !commit!(id) {
2659 stopped = true;
2660 break;
2661 }
2662 }
2663 if stopped {
2664 break;
2665 }
2666 t_fwd = *ids.last().unwrap();
2667 }
2668 if stopped {
2669 break 'decode;
2670 }
2671 }
2672 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2673 next_pos += 1;
2674 if let Some(r) = &mut router {
2677 let phi = self.dyn_phi_ema.clone();
2678 let decision = r.step(&phi, generated);
2679 if let Some(new_active) = decision {
2680 let _ = self.set_active_skill(new_active);
2681 }
2682 if trace_on {
2685 if let Some(last) = traces.last_mut() {
2686 let e = r.last_best_e();
2687 last.recon = e.is_finite().then_some(e);
2688 last.switched = decision.is_some();
2689 }
2690 }
2691 }
2692 }
2693 }
2694 }
2695
2696 self.graph_want_logits = false;
2697 self.graph_logits = None;
2698 if router.is_some() {
2700 let _ = self.set_active_skill(None);
2701 }
2702 self.dyn_router = router.or(self.dyn_router.take());
2703 self.mtp = mtp.or(self.mtp.take());
2704
2705 let output_ids = &all_ids[input_ids.len()..];
2706 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2710 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2711 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2713 Ok(GenerateResult {
2714 text: self.tokenizer.decode(output_ids),
2715 token_ids: output_ids.to_vec(),
2716 prompt_tokens: input_ids.len(),
2717 tokens_generated: generated,
2718 finish_reason,
2719 mtp_drafted: drafted,
2720 mtp_accepted: accepted,
2721 token_confidence: confidence,
2722 traces,
2723 })
2724 }
2725
2726 fn mtp_step(
2730 &mut self,
2731 m: &mut MtpModule,
2732 hidden: &[f32],
2733 next_token: u32,
2734 position: usize,
2735 ) -> u32 {
2736 self.mtp_step_h(m, hidden, next_token, position).0
2737 }
2738
2739 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2743 use std::sync::Mutex;
2744 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2745 let mut t = T.lock().unwrap();
2746 if t.len() <= depth {
2747 t.resize(depth + 1, (0, 0));
2748 }
2749 t[depth].0 += 1;
2750 t[depth].1 += prefix_ok as u64;
2751 if depth == 0 && t[0].0 % 128 == 0 {
2752 let line: Vec<String> = t
2753 .iter()
2754 .enumerate()
2755 .map(|(d, (n, k))| {
2756 format!(
2757 "d{}={:.0}%({n})",
2758 d + 1,
2759 100.0 * *k as f64 / (*n).max(1) as f64
2760 )
2761 })
2762 .collect();
2763 eprintln!("mtp-chain: {}", line.join(" "));
2764 }
2765 }
2766
2767 fn mtp_step_hl(
2775 &mut self,
2776 m: &mut MtpModule,
2777 hidden: &[f32],
2778 next_token: u32,
2779 position: usize,
2780 ) -> (Vec<f32>, Vec<f32>) {
2781 #[cfg(feature = "gpu")]
2786 if self.mtp_graph_mode != Some(false) {
2787 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
2788 self.mtp_graph_mode = Some(true);
2789 return r;
2790 }
2791 if self.mtp_graph_mode == Some(true) {
2792 tracing::warn!("mtp graph declined mid-run — draft falls to the per-op path");
2797 }
2798 self.mtp_graph_mode = Some(false);
2799 }
2800 let e = self.embed_single(next_token);
2804 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2805 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2806 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2807 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2808 let mut x = vec![0.0f32; self.hidden_size];
2809 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2810
2811 let lw = &m.layer;
2813 inference::rms_norm_into(
2814 &x,
2815 &lw.input_norm,
2816 self.rms_eps,
2817 self.norm_style,
2818 &mut self.ws.n1,
2819 );
2820 let attn = match &lw.attn {
2821 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2823 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2824 AttnKind::Full {
2825 wq,
2826 wk,
2827 wv,
2828 wo,
2829 q_norm,
2830 k_norm,
2831 output_gate,
2832 softplus_gate,
2833 bias,
2834 } => {
2835 let mut cfg = self.attn_cfg(position);
2836 cfg.q_norm = q_norm.as_deref();
2837 cfg.k_norm = k_norm.as_deref();
2838 cfg.output_gate = *output_gate;
2839 cfg.softplus_gate = softplus_gate
2840 .as_ref()
2841 .map(|(gate, per_head)| (gate, *per_head));
2842 cfg.bias = bias
2843 .as_ref()
2844 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2845 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2846 }
2847 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
2848 unreachable!("MTP block is full attention")
2849 }
2850 };
2851 for (i, &a) in attn.iter().enumerate() {
2852 x[i] += a;
2853 }
2854 inference::rms_norm_into(
2855 &x,
2856 &lw.post_norm,
2857 self.rms_eps,
2858 self.norm_style,
2859 &mut self.ws.p1,
2860 );
2861 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
2862 for (i, &f) in ffn.iter().enumerate() {
2863 x[i] += f;
2864 }
2865
2866 inference::rms_norm_into(
2867 &x,
2868 &m.final_norm,
2869 self.rms_eps,
2870 self.norm_style,
2871 &mut self.ws.n1,
2872 );
2873 let lg = self.lm_head_forward(&self.ws.n1);
2874 (lg, x)
2875 }
2876
2877 fn mtp_step_h(
2879 &mut self,
2880 m: &mut MtpModule,
2881 hidden: &[f32],
2882 next_token: u32,
2883 position: usize,
2884 ) -> (u32, Vec<f32>) {
2885 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
2886 let draft = sampler::argmax(&lg);
2887 attention::recycle_buf(&mut lg);
2888 (draft, x)
2889 }
2890
2891 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
2897 match trial {
2898 SpecTrial::Spec { t0, gen0, rounds } => {
2899 let rounds = rounds + 1;
2900 if rounds >= 5 {
2901 if mon.plain_ms > 0.0 {
2902 let keep = mon.pays();
2903 mon.fails = 0;
2904 tracing::info!(
2905 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
2906 mon.tokens,
2907 mon.round_ms,
2908 mon.plain_ms,
2909 if keep { "speculating" } else { "plain" }
2910 );
2911 SpecTrial::Decided {
2912 spec: keep,
2913 recheck_at: if keep { usize::MAX } else { generated + 128 },
2914 }
2915 } else {
2916 SpecTrial::Plain {
2917 t0: std::time::Instant::now(),
2918 gen0: generated,
2919 }
2920 }
2921 } else {
2922 SpecTrial::Spec { t0, gen0, rounds }
2923 }
2924 }
2925 SpecTrial::Decided { spec: true, .. } => {
2926 if mon.pays() {
2927 mon.fails = 0;
2928 trial
2929 } else {
2930 mon.fails += 1;
2931 if mon.fails >= 4 {
2932 tracing::info!(
2933 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
2934 mon.tokens,
2935 mon.round_ms,
2936 mon.plain_ms
2937 );
2938 SpecTrial::Decided {
2939 spec: false,
2940 recheck_at: generated + 128,
2941 }
2942 } else {
2943 trial
2944 }
2945 }
2946 }
2947 other => other,
2948 }
2949 }
2950
2951 fn mtp_kv_id(&self) -> u64 {
2954 self.graph_kv_id | (1u64 << 40)
2955 }
2956
2957 const MTP_LAYER_BASE: usize = 0;
2962
2963 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
2966 let e = self.embed_single(next_token);
2967 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2968 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2969 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2970 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2971 let mut x = vec![0.0f32; self.hidden_size];
2972 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2973 x
2974 }
2975
2976 #[cfg(feature = "gpu")]
2979 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
2980 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
2981 return false;
2982 }
2983 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
2984 || !crate::gpu::enabled_here()
2985 || self.attn_softcap > 0.0
2986 || self.attention_heads_per_layer.is_some()
2987 {
2988 return false;
2989 }
2990 matches!(
2991 &m.layer.attn,
2992 AttnKind::Full {
2993 softplus_gate: None,
2994 ..
2995 }
2996 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
2997 }
2998
2999 #[cfg(feature = "gpu")]
3005 fn mtp_step_graph(
3006 &mut self,
3007 m: &mut MtpModule,
3008 hidden: &[f32],
3009 next_token: u32,
3010 position: usize,
3011 ) -> Option<(Vec<f32>, Vec<f32>)> {
3012 if !self.mtp_graph_ok(m) {
3013 return None;
3014 }
3015 let lw = &m.layer;
3016 let AttnKind::Full {
3017 wq,
3018 wk,
3019 wv,
3020 wo,
3021 q_norm,
3022 k_norm,
3023 output_gate,
3024 softplus_gate,
3025 bias,
3026 } = &lw.attn
3027 else {
3028 return None;
3029 };
3030 if softplus_gate.is_some() {
3031 return None;
3032 }
3033 let FfnKind::Dense(d) = &lw.ffn else {
3034 return None;
3035 };
3036 let mut x = self.mtp_block_input(m, hidden, next_token);
3039 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3040 let (_, i, kind, rs) = t.graph_weight()?;
3041 Some(crate::gpu::GraphW {
3042 idx: i,
3043 kind,
3044 row_scale: rs,
3045 data: &[],
3046 })
3047 }
3048 let (model, _, _, _) = wq.graph_weight()?;
3049 let model = model.clone();
3050 let (lm_gw, lm_rows) = {
3051 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3052 (
3053 crate::gpu::GraphW {
3054 idx: i,
3055 kind,
3056 row_scale: rs,
3057 data: &[],
3058 },
3059 self.weights.lm_head.rows(),
3060 )
3061 };
3062 let layer = crate::gpu::GraphLayer {
3063 input_norm: &lw.input_norm,
3064 attn: crate::gpu::GraphAttn::Full {
3065 wq: gw(wq)?,
3066 wk: gw(wk)?,
3067 wv: gw(wv)?,
3068 wo: gw(wo)?,
3069 q_norm: q_norm.as_deref(),
3070 k_norm: k_norm.as_deref(),
3071 bias: bias
3072 .as_ref()
3073 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3074 output_gate: *output_gate,
3075 cpu_k: m.kv.k_heads(),
3076 cpu_v: m.kv.v_heads(),
3077 },
3078 post_norm: &lw.post_norm,
3079 ffn: crate::gpu::GraphFfn::Dense {
3080 gate: gw(&d.gate_proj)?,
3081 up: gw(&d.up_proj)?,
3082 down: gw(&d.down_proj)?,
3083 },
3084 };
3085 let nh = self.num_heads;
3086 let (nkv, hd, rd) = self.layer_geom(0);
3087 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3088 let mut logits = Vec::new();
3089 let ok = crate::gpu::forward_token_graph(
3090 &model,
3091 self.mtp_kv_id(),
3092 std::slice::from_ref(&layer),
3093 &[None],
3094 self.o1_epoch,
3095 &self.inv_freq,
3096 &mut x,
3097 nh,
3098 nkv,
3099 hd,
3100 rd,
3101 self.hidden_size,
3102 self.intermediate_size,
3103 position,
3104 self.kv_cache.max_seq_len,
3105 gemma,
3106 self.rms_eps as f32,
3107 Some((&lm_gw, lm_rows)),
3108 &m.final_norm,
3109 &mut logits,
3110 &[],
3111 1,
3112 None,
3113 None,
3114 None,
3115 Self::MTP_LAYER_BASE,
3116 true,
3117 );
3118 if !ok {
3119 return None;
3120 }
3121 logits.resize(self.vocab_size, 0.0);
3122 Some((logits, x))
3123 }
3124
3125 #[cfg(feature = "gpu")]
3132 fn mtp_warm_graph(
3133 &mut self,
3134 m: &mut MtpModule,
3135 pairs: &[(&[f32], u32)],
3136 first_pos: usize,
3137 ) -> bool {
3138 if pairs.is_empty() || !self.mtp_graph_ok(m) {
3139 return pairs.is_empty();
3140 }
3141 let hs = self.hidden_size;
3142 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
3145 for (h, t) in pairs {
3146 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
3147 }
3148 let lw = &m.layer;
3149 let AttnKind::Full {
3150 wq,
3151 wk,
3152 wv,
3153 wo,
3154 q_norm,
3155 k_norm,
3156 output_gate,
3157 bias,
3158 ..
3159 } = &lw.attn
3160 else {
3161 return false;
3162 };
3163 let FfnKind::Dense(d) = &lw.ffn else {
3164 return false;
3165 };
3166 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3167 let (_, i, kind, rs) = t.graph_weight()?;
3168 Some(crate::gpu::GraphW {
3169 idx: i,
3170 kind,
3171 row_scale: rs,
3172 data: &[],
3173 })
3174 }
3175 let Some((model, _, _, _)) = wq.graph_weight() else {
3176 return false;
3177 };
3178 let model = model.clone();
3179 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
3180 gw(wq),
3181 gw(wk),
3182 gw(wv),
3183 gw(wo),
3184 gw(&d.gate_proj),
3185 gw(&d.up_proj),
3186 gw(&d.down_proj),
3187 ) else {
3188 return false;
3189 };
3190 let layer = crate::gpu::GraphLayer {
3191 input_norm: &lw.input_norm,
3192 attn: crate::gpu::GraphAttn::Full {
3193 wq: gwq,
3194 wk: gwk,
3195 wv: gwv,
3196 wo: gwo,
3197 q_norm: q_norm.as_deref(),
3198 k_norm: k_norm.as_deref(),
3199 bias: bias
3200 .as_ref()
3201 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3202 output_gate: *output_gate,
3203 cpu_k: m.kv.k_heads(),
3204 cpu_v: m.kv.v_heads(),
3205 },
3206 post_norm: &lw.post_norm,
3207 ffn: crate::gpu::GraphFfn::Dense {
3208 gate: gg,
3209 up: gu,
3210 down: gd,
3211 },
3212 };
3213 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
3214 let nh = self.num_heads;
3215 let (nkv, hd, rd) = self.layer_geom(0);
3216 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3217 crate::gpu::forward_batch_graph(
3218 &model,
3219 self.mtp_kv_id(),
3220 std::slice::from_ref(&layer),
3221 &self.inv_freq,
3222 &mut hiddens,
3223 nh,
3224 nkv,
3225 hd,
3226 rd,
3227 hs,
3228 self.intermediate_size,
3229 &positions,
3230 self.kv_cache.max_seq_len,
3231 gemma,
3232 self.rms_eps as f32,
3233 pairs.len(),
3234 None,
3235 )
3236 }
3237
3238 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
3242 let e = self.embed_single(next_token);
3243 let mut cat = vec![0.0f32; 2 * self.hidden_size];
3244 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
3245 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
3246 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
3247 let mut x = vec![0.0f32; self.hidden_size];
3248 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
3249 inference::rms_norm_into(
3250 &x,
3251 &m.layer.input_norm,
3252 self.rms_eps,
3253 self.norm_style,
3254 &mut self.ws.n1,
3255 );
3256 let attn = match &m.layer.attn {
3257 AttnKind::Full {
3258 wq,
3259 wk,
3260 wv,
3261 wo,
3262 q_norm,
3263 k_norm,
3264 output_gate,
3265 softplus_gate,
3266 bias,
3267 } => {
3268 let mut cfg = self.attn_cfg(position);
3269 cfg.q_norm = q_norm.as_deref();
3270 cfg.k_norm = k_norm.as_deref();
3271 cfg.output_gate = *output_gate;
3272 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
3273 cfg.bias = bias
3274 .as_ref()
3275 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
3276 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
3277 }
3278 _ => return,
3279 };
3280 let _ = attn;
3281 }
3282
3283 #[cfg(feature = "gpu")]
3290 #[allow(clippy::too_many_arguments)]
3291 fn graph_spec_step(
3292 &mut self,
3293 m: &mut MtpModule,
3294 hidden: &[f32],
3295 t_next: u32,
3296 next_pos: usize,
3297 drafted: &mut usize,
3298 accepted: &mut usize,
3299 all_ids: &mut Vec<u32>,
3303 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
3304 #[cfg(feature = "gpu")]
3315 let k_default = if crate::gpu_wgpu::verify_i8_on() { 5 } else { 4 };
3316 #[cfg(not(feature = "gpu"))]
3317 let k_default = 4;
3318 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
3319 .ok()
3320 .and_then(|v| v.parse().ok())
3321 .filter(|&v| (1..=8).contains(&v))
3322 .unwrap_or(k_default);
3323 if next_pos == 0 {
3324 return None;
3325 }
3326 let t_round = std::time::Instant::now();
3327 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
3343 let sub0 = subs();
3344 let cfg = self.sampler_config.clone();
3349 let penalized = !(cfg.repetition_penalty == 1.0
3350 && cfg.presence_penalty == 0.0
3351 && cfg.suppress_tokens.is_empty());
3352 let greedy_pen = cfg.temperature < 1e-6 && penalized;
3357 let sampling = cfg.temperature >= 1e-6;
3358 let sparse = sampling && sampler::sparse_ok(&cfg);
3364 let base_len = all_ids.len();
3365 if sampling && !sparse && self.spec_q.len() < k_spec {
3366 self.spec_q.resize_with(k_spec, Vec::new);
3367 }
3368 if sparse && self.spec_qs.len() < k_spec {
3369 self.spec_qs.resize_with(k_spec, Vec::new);
3370 }
3371 let mut drafts = Vec::with_capacity(k_spec);
3376 let mut hx = hidden.to_vec();
3377 for j in 0..k_spec {
3378 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
3379 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
3380 let dj = if sparse {
3381 let mut q = std::mem::take(&mut self.spec_qs[j]);
3382 let ok = sampler::sparse_distribution_into(
3383 &lg,
3384 &cfg,
3385 all_ids,
3386 &mut self.sampler_scratch,
3387 self.pool.as_deref(),
3388 &mut q,
3389 );
3390 let d = if ok {
3391 sampler::draw_sparse(&q, &mut self.rng)
3392 } else {
3393 let t = sampler::argmax(&lg);
3395 q.clear();
3396 q.push((t, 1.0));
3397 t
3398 };
3399 self.spec_qs[j] = q;
3400 all_ids.push(d);
3401 d
3402 } else if sampling {
3403 let mut q = std::mem::take(&mut self.spec_q[j]);
3404 sampler::distribution_into(
3405 &lg,
3406 &cfg,
3407 all_ids,
3408 &mut self.sampler_scratch,
3409 self.pool.as_deref(),
3410 &mut q,
3411 );
3412 let d = sampler::draw(&q, &mut self.rng);
3413 self.spec_q[j] = q;
3414 all_ids.push(d); d
3416 } else if greedy_pen {
3417 let d = sampler::argmax_penalized(
3418 &lg,
3419 &cfg,
3420 all_ids,
3421 &mut self.sampler_scratch,
3422 self.pool.as_deref(),
3423 );
3424 all_ids.push(d);
3425 d
3426 } else {
3427 sampler::argmax(&lg)
3428 };
3429 attention::recycle_buf(&mut lg);
3430 drafts.push(dj);
3431 hx = hj;
3432 }
3433 all_ids.truncate(base_len);
3434 *drafted += k_spec;
3435 let t_draft = t_round.elapsed();
3436 let sub_draft = subs();
3437 let b = k_spec + 1;
3440 let mut hiddens = vec![0.0f32; b * self.hidden_size];
3441 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
3442 let e = self.embed_single(t);
3443 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
3444 }
3445 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
3446 let (lm_gw, lm_rows) = {
3447 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
3448 (
3449 crate::gpu::GraphW {
3450 idx: i,
3451 kind,
3452 row_scale: rs,
3453 data: &[],
3454 },
3455 self.weights.lm_head.rows(),
3456 )
3457 };
3458 let mut logits = Vec::new();
3459 let final_norm = self.weights.final_norm.clone();
3460 let ok = self.try_batch_graph_wgpu(
3461 &mut hiddens,
3462 &positions,
3463 b,
3464 Some(crate::gpu::SpecTail {
3465 lm: lm_gw,
3466 lm_rows,
3467 final_norm: &final_norm,
3468 logits_out: &mut logits,
3469 }),
3470 );
3471 if !ok {
3472 m.kv.truncate_last(k_spec);
3475 return None;
3476 }
3477 let t_verify = t_round.elapsed();
3478 let sub_verify = subs();
3479 let mut a = 0usize;
3484 let mut forced: Option<u32> = None;
3485 let ids: Vec<u32> = if sparse {
3486 let mut p = std::mem::take(&mut self.spec_ps);
3487 let mut res = std::mem::take(&mut self.spec_ress);
3488 while a < k_spec {
3489 let ok = sampler::sparse_distribution_into(
3490 &logits[a * lm_rows..(a + 1) * lm_rows],
3491 &cfg,
3492 all_ids,
3493 &mut self.sampler_scratch,
3494 self.pool.as_deref(),
3495 &mut p,
3496 );
3497 if !ok {
3498 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
3499 p.clear();
3500 p.push((t, 1.0));
3501 }
3502 match sampler::spec_accept_or_correct_sparse(
3503 &p,
3504 &self.spec_qs[a],
3505 drafts[a],
3506 &mut self.rng,
3507 &mut res,
3508 ) {
3509 None => {
3510 all_ids.push(drafts[a]);
3511 a += 1;
3512 }
3513 Some(c) => {
3514 forced = Some(c);
3515 break;
3516 }
3517 }
3518 }
3519 all_ids.truncate(base_len);
3520 self.spec_ps = p;
3521 self.spec_ress = res;
3522 drafts.clone()
3523 } else if sampling {
3524 let mut p = std::mem::take(&mut self.spec_p);
3525 let mut res = std::mem::take(&mut self.spec_res);
3526 while a < k_spec {
3527 sampler::distribution_into(
3528 &logits[a * lm_rows..(a + 1) * lm_rows],
3529 &cfg,
3530 all_ids,
3531 &mut self.sampler_scratch,
3532 self.pool.as_deref(),
3533 &mut p,
3534 );
3535 match sampler::spec_accept_or_correct(
3536 &p,
3537 &self.spec_q[a],
3538 drafts[a],
3539 &mut self.rng,
3540 &mut res,
3541 self.pool.as_deref(),
3542 ) {
3543 None => {
3544 all_ids.push(drafts[a]);
3545 a += 1;
3546 }
3547 Some(c) => {
3548 forced = Some(c);
3549 break;
3550 }
3551 }
3552 }
3553 all_ids.truncate(base_len);
3554 self.spec_p = p;
3555 self.spec_res = res;
3556 drafts.clone()
3558 } else if greedy_pen {
3559 let mut ids: Vec<u32> = Vec::with_capacity(b);
3563 for i in 0..b {
3564 let t = sampler::argmax_penalized(
3565 &logits[i * lm_rows..(i + 1) * lm_rows],
3566 &cfg,
3567 all_ids,
3568 &mut self.sampler_scratch,
3569 self.pool.as_deref(),
3570 );
3571 ids.push(t);
3572 if i < k_spec && t == drafts[i] {
3573 all_ids.push(t);
3574 } else {
3575 break;
3576 }
3577 }
3578 all_ids.truncate(base_len);
3579 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
3580 a += 1;
3581 }
3582 ids
3585 } else {
3586 let ids: Vec<u32> = (0..b)
3587 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
3588 .collect();
3589 while a < k_spec && ids[a] == drafts[a] {
3590 a += 1;
3591 }
3592 ids
3593 };
3594 if a + 1 < b {
3596 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
3597 }
3598 *accepted += a;
3599 m.kv.truncate_last(k_spec.saturating_sub(1));
3610 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
3611 if !warm_off && a > 0 {
3612 let mut warmed = false;
3615 if self.mtp_graph_mode == Some(true) {
3616 let rows: Vec<Vec<f32>> = (0..a)
3617 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
3618 .collect();
3619 let pairs: Vec<(&[f32], u32)> = rows
3620 .iter()
3621 .zip(ids.iter())
3622 .map(|(r, &t)| (r.as_slice(), t))
3623 .collect();
3624 warmed = self.mtp_warm_graph(m, &pairs, next_pos);
3625 if !warmed {
3626 warmed = true;
3628 for j in 0..a {
3629 if self
3630 .mtp_step_graph(m, &rows[j], ids[j], next_pos + j)
3631 .is_none()
3632 {
3633 warmed = false;
3634 break;
3635 }
3636 }
3637 }
3638 }
3639 if !warmed {
3640 for j in 0..a {
3641 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
3642 let row = row.to_vec();
3643 self.mtp_warm(m, &row, ids[j], next_pos + j);
3644 }
3645 }
3646 }
3647 if let Some(c) = forced {
3651 self.spec_forced = Some(c);
3652 self.graph_logits = None;
3653 } else {
3654 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
3655 row.resize(self.vocab_size, 0.0);
3656 if let Some(c) = self.final_softcap {
3657 for l in row.iter_mut() {
3658 *l = c * (*l / c).tanh();
3659 }
3660 }
3661 self.graph_logits = Some(row);
3662 }
3663 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
3664 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
3670 let end = subs();
3671 eprintln!(
3672 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
3673 commit {:.1} ms/{} sub (accepted {a} of {k_spec})",
3674 t_draft.as_secs_f64() * 1e3,
3675 sub_draft - sub0,
3676 (t_verify - t_draft).as_secs_f64() * 1e3,
3677 sub_verify - sub_draft,
3678 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
3679 end - sub_verify,
3680 );
3681 }
3682 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
3683 }
3684
3685 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
3694 if !self.pair_supported() {
3695 return (0.0, 0.0);
3696 }
3697 let emb1 = self.embed_single(1);
3698 let emb2 = self.embed_single(2);
3699 let pos = self.kv_cache.seq_len();
3700
3701 let t0 = std::time::Instant::now();
3702 for _ in 0..iters {
3703 let _ = self.forward_layers(&emb1, pos, None);
3704 let _ = self.forward_layers(&emb2, pos + 1, None);
3705 for l in &mut self.kv_cache.layers {
3706 l.truncate_last(2);
3707 }
3708 }
3709 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
3710
3711 let t1 = std::time::Instant::now();
3712 for _ in 0..iters {
3713 let _ = self.forward_pair(&emb1, &emb2, pos);
3714 for l in &mut self.kv_cache.layers {
3715 l.truncate_last(2);
3716 }
3717 }
3718 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
3719 (singles_ms, pair_ms)
3720 }
3721
3722 fn pair_supported(&self) -> bool {
3730 !self.weights.layers.is_empty()
3737 && self.g3n.is_none()
3738 && !self
3739 .weights
3740 .layers
3741 .iter()
3742 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
3743 }
3744
3745 fn forward_pair(
3746 &mut self,
3747 emb1: &[f32],
3748 emb2: &[f32],
3749 position: usize,
3750 ) -> (Vec<f32>, Vec<f32>) {
3751 let mut h1 = emb1.to_vec();
3752 let mut h2 = emb2.to_vec();
3753 let (_nkv, _hd, hs, _rd, eps) = (
3754 self.num_kv_heads,
3755 self.head_dim,
3756 self.hidden_size,
3757 self.rotary_dim,
3758 self.rms_eps,
3759 );
3760 let pool = self.pool.clone();
3761
3762 for li in 0..self.num_layers {
3763 let lw = &self.weights.layers[self.phys_layer(li)];
3764 inference::rms_norm_into(
3767 &h1,
3768 &lw.input_norm,
3769 self.rms_eps,
3770 self.norm_style,
3771 &mut self.ws.n1,
3772 );
3773 inference::rms_norm_into(
3774 &h2,
3775 &lw.input_norm,
3776 self.rms_eps,
3777 self.norm_style,
3778 &mut self.ws.n2,
3779 );
3780
3781 let (a1, a2) = match &lw.attn {
3782 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
3783 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
3784 AttnKind::Linear(w) => {
3785 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3786 let layer = &mut self.kv_cache.layers[li];
3787 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
3788 vmf_phase_pair(
3789 &self.ws.n1,
3790 &self.ws.n2,
3791 w,
3792 &cfg,
3793 state,
3794 scratch,
3795 self.pool.as_deref(),
3796 )
3797 }
3798 AttnKind::LinearGdn(w) => {
3799 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3800 let layer = &mut self.kv_cache.layers[li];
3801 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
3802 gdn_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::ShortConv(w) => {
3813 let cfg = self
3814 .short_conv_cfg
3815 .expect("short-conv layer without short_conv_cfg");
3816 let layer = &mut self.kv_cache.layers[li];
3817 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
3818 short_conv_pair(
3819 &self.ws.n1,
3820 &self.ws.n2,
3821 w,
3822 &cfg,
3823 state,
3824 scratch,
3825 self.pool.as_deref(),
3826 )
3827 }
3828 AttnKind::Full {
3829 wq,
3830 wk,
3831 wv,
3832 wo,
3833 q_norm,
3834 k_norm,
3835 output_gate,
3836 softplus_gate,
3837 bias,
3838 } => {
3839 let inv_freq_l = self.layer_inv_freq(li);
3840 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3841 let cfg = QwenAttnCfg {
3842 num_heads: self.layer_num_heads(li),
3843 num_kv_heads: nkv_l,
3844 head_dim: hd_l,
3845 hidden_size: hs,
3846 position,
3847 inv_freq: &inv_freq_l,
3848 rotary_dim: rd_l,
3849 scale: self.attn_scale,
3850 softcap: self.attn_softcap,
3851 window: self.layer_window(li),
3852 v_norm: self.attn_v_norm,
3853 q_norm: q_norm.as_deref(),
3854 k_norm: k_norm.as_deref(),
3855 output_gate: *output_gate,
3856 softplus_gate: softplus_gate
3857 .as_ref()
3858 .map(|(gate, per_head)| (gate, *per_head)),
3859 rope_scale: self.layer_rope_scale(li),
3860 bias: bias
3861 .as_ref()
3862 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3863 rms_eps: eps,
3864 norm_style: self.norm_style,
3865 pool: pool.as_deref(),
3866 };
3867 attention::qwen_attention_pair(
3868 &self.ws.n1,
3869 &self.ws.n2,
3870 wq,
3871 wk,
3872 wv,
3873 wo,
3874 &mut self.kv_cache.layers[li],
3875 &cfg,
3876 )
3877 }
3878 };
3879 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
3880 Some(w) => (
3881 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
3882 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
3883 ),
3884 None => (a1, a2),
3885 };
3886 for i in 0..self.hidden_size {
3887 h1[i] += a1[i];
3888 h2[i] += a2[i];
3889 }
3890 let (mut a1, mut a2) = (a1, a2);
3891 attention::recycle_buf(&mut a1);
3892 attention::recycle_buf(&mut a2);
3893
3894 let lw = &self.weights.layers[self.phys_layer(li)];
3895 inference::rms_norm_into(
3896 &h1,
3897 &lw.post_norm,
3898 self.rms_eps,
3899 self.norm_style,
3900 &mut self.ws.p1,
3901 );
3902 inference::rms_norm_into(
3903 &h2,
3904 &lw.post_norm,
3905 self.rms_eps,
3906 self.norm_style,
3907 &mut self.ws.p2,
3908 );
3909 let (f1, f2) = match &lw.ffn {
3910 FfnKind::DenseMoe(dm) => (
3913 dense_moe_ffn(
3914 dm,
3915 &self.ws.p1,
3916 &h1,
3917 self.rms_eps,
3918 self.norm_style,
3919 self.pool.as_deref(),
3920 ),
3921 dense_moe_ffn(
3922 dm,
3923 &self.ws.p2,
3924 &h2,
3925 self.rms_eps,
3926 self.norm_style,
3927 self.pool.as_deref(),
3928 ),
3929 ),
3930 _ => ffn_forward_pair(
3931 &lw.ffn,
3932 &self.ws.p1,
3933 &self.ws.p2,
3934 self.pool.as_deref(),
3935 None,
3936 ),
3937 };
3938 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
3939 Some(w) => (
3940 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
3941 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
3942 ),
3943 None => (f1, f2),
3944 };
3945 for i in 0..self.hidden_size {
3946 h1[i] += f1[i];
3947 h2[i] += f2[i];
3948 }
3949 let (mut f1, mut f2) = (f1, f2);
3950 attention::recycle_buf(&mut f1);
3951 attention::recycle_buf(&mut f2);
3952 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
3953 for i in 0..self.hidden_size {
3954 h1[i] *= sc;
3955 h2[i] *= sc;
3956 }
3957 }
3958 if self.is_loop_end(li) && li + 1 < self.num_layers {
3960 h1 = inference::rms_norm(
3961 &h1,
3962 &self.weights.final_norm,
3963 self.rms_eps,
3964 self.norm_style,
3965 );
3966 h2 = inference::rms_norm(
3967 &h2,
3968 &self.weights.final_norm,
3969 self.rms_eps,
3970 self.norm_style,
3971 );
3972 }
3973 }
3974 (h1, h2)
3975 }
3976
3977 fn commit_linear_scratch(&mut self) {
3979 for layer in &mut self.kv_cache.layers {
3980 if !layer.linear_scratch.is_empty() {
3981 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
3982 layer.linear_scratch.clear();
3983 }
3984 }
3985 }
3986
3987 pub fn forward_ids(
3990 &mut self,
3991 ids: &[u32],
3992 task_mask: Option<&TaskMask>,
3993 ) -> Result<Vec<f32>, String> {
3994 if ids.is_empty() {
3995 return Err("empty id sequence".to_string());
3996 }
3997 self.kv_cache.clear();
3998 self.kv_history.clear();
3999 self.o1_begin();
4000 let mut hidden = vec![0.0f32; self.hidden_size];
4001 let mut pos = 0usize;
4002 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
4010 let chunk = prefill_chunk();
4014 let hs = self.hidden_size;
4015 while pos < ids.len() {
4016 let end = (pos + chunk).min(ids.len());
4017 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4018 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4019 pos = end;
4020 }
4021 }
4022 if task_mask.is_none()
4031 && !self.graph_prefill_preferred()
4032 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
4033 && self.pair_supported()
4034 {
4035 while pos + 1 < ids.len() {
4036 let e1 = self.embed_single(ids[pos]);
4037 let e2 = self.embed_single(ids[pos + 1]);
4038 let (_, h2) = self.forward_pair(&e1, &e2, pos);
4039 self.commit_linear_scratch();
4040 hidden = h2;
4041 pos += 2;
4042 }
4043 }
4044 while pos < ids.len() {
4045 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4046 pos += 1;
4047 }
4048 self.o1_seal();
4052 let normed = inference::rms_norm(
4053 &hidden,
4054 &self.weights.final_norm,
4055 self.rms_eps,
4056 self.norm_style,
4057 );
4058 Ok(self.lm_head_forward(&normed))
4059 }
4060
4061 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
4068 let (nll, cnt) = self.nll_ids_from(ids, 0);
4069 (nll / cnt.max(1) as f64).exp()
4070 }
4071
4072 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
4077 self.kv_cache.clear();
4078 self.kv_history.clear();
4079 FFN_PROBE.with(|p| {
4080 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
4081 });
4082 crate::gpu::cpu_scope(|| {
4083 for (pos, &id) in ids.iter().enumerate() {
4084 let emb = self.embed_single(id);
4085 let _ = self.forward_layers(&emb, pos, None);
4086 }
4087 });
4088 self.kv_cache.clear();
4089 self.kv_history.clear();
4090 FFN_PROBE
4091 .with(|p| p.borrow_mut().take())
4092 .unwrap_or_default()
4093 }
4094
4095 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
4099 self.kv_cache.clear();
4100 self.kv_history.clear();
4101 let mut nll = 0f64;
4102 let mut cnt = 0usize;
4103 let mut hidden = vec![0f32; self.hidden_size];
4104 for (pos, &id) in ids.iter().enumerate() {
4105 if pos > 0 {
4106 inference::rms_norm_into(
4107 &hidden,
4108 &self.weights.final_norm,
4109 self.rms_eps,
4110 self.norm_style,
4111 &mut self.ws.n1,
4112 );
4113 let mut logits = self.lm_head_forward(&self.ws.n1);
4114 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4115 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
4116 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
4117 nll -= p.max(1e-300).ln();
4118 cnt += 1;
4119 attention::recycle_buf(&mut logits);
4120 }
4121 let emb = self.embed_single(id);
4122 hidden = self.forward_layers(&emb, pos, Some(mask));
4123 }
4124 self.kv_cache.clear();
4125 self.kv_history.clear();
4126 (nll / cnt.max(1) as f64).exp()
4127 }
4128
4129 pub fn nll_ids_masked(
4148 &mut self,
4149 ids: &[u32],
4150 start: usize,
4151 task_mask: Option<&TaskMask>,
4152 ) -> (f64, usize) {
4153 self.nll_ids_inner(ids, start, task_mask)
4154 }
4155
4156 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
4157 self.nll_ids_inner(ids, start, None)
4158 }
4159
4160 fn nll_ids_inner(
4161 &mut self,
4162 ids: &[u32],
4163 start: usize,
4164 task_mask: Option<&TaskMask>,
4165 ) -> (f64, usize) {
4166 self.kv_cache.clear();
4167 self.kv_history.clear();
4168 let mut nll = 0f64;
4169 let mut cnt = 0usize;
4170 if self.can_prefill_batched() {
4171 const CHUNK: usize = 128;
4177 const LM_SUB: usize = 32;
4178 let n = ids.len().saturating_sub(1);
4179 let hs = self.hidden_size;
4180 let rows = self.weights.lm_head.rows();
4181 let mut pos = 0usize;
4182 while pos < n {
4183 let end = (pos + CHUNK).min(n);
4184 let bsz = end - pos;
4185 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
4186 let mut k0 = 0usize;
4187 while k0 < bsz {
4188 let k1 = (k0 + LM_SUB).min(bsz);
4189 let sb = k1 - k0;
4190 if pos + k1 <= start {
4193 k0 = k1;
4194 continue;
4195 }
4196 let mut normed = vec![0.0f32; sb * hs];
4197 for k in 0..sb {
4198 let r = inference::rms_norm(
4199 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
4200 &self.weights.final_norm,
4201 self.rms_eps,
4202 self.norm_style,
4203 );
4204 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
4205 }
4206 let mut logits = vec![0.0f32; sb * rows];
4207 self.weights
4208 .lm_head
4209 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
4210 for k in 0..sb {
4211 if pos + k0 + k < start {
4212 continue;
4213 }
4214 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
4215 if let Some(mu) = self.logit_multiplier {
4216 for v in lg.iter_mut() {
4217 *v *= mu;
4218 }
4219 }
4220 if let Some(c) = self.final_softcap {
4224 for v in lg.iter_mut() {
4225 *v = c * (*v / c).tanh();
4226 }
4227 }
4228 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
4229 let target = ids[pos + k0 + k + 1] as usize;
4230 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4231 let lse: f64 = lg
4232 .iter()
4233 .map(|&v| ((v - max) as f64).exp())
4234 .sum::<f64>()
4235 .ln()
4236 + max as f64;
4237 nll += lse - lg[target] as f64;
4238 cnt += 1;
4239 if std::env::var("CMF_PPL_TRACE").is_ok() {
4240 let top = lg
4241 .iter()
4242 .enumerate()
4243 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4244 .map(|(i, _)| i)
4245 .unwrap_or(0);
4246 eprintln!(
4247 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
4248 pos + k0 + k,
4249 target,
4250 lse - lg[target] as f64,
4251 top,
4252 lg[target],
4253 lg[top]
4254 );
4255 }
4256 }
4257 k0 = k1;
4258 }
4259 pos = end;
4260 }
4261 self.kv_cache.clear();
4262 self.kv_history.clear();
4263 return (nll, cnt);
4264 }
4265 for pos in 0..ids.len().saturating_sub(1) {
4266 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
4267 let out_of_band = self.graph_logits.take();
4275 if pos < start {
4276 continue;
4277 }
4278 let logits = match out_of_band {
4279 Some(lg) => lg,
4280 None => {
4281 let normed = inference::rms_norm(
4282 &hidden,
4283 &self.weights.final_norm,
4284 self.rms_eps,
4285 self.norm_style,
4286 );
4287 self.lm_head_forward(&normed)
4291 }
4292 };
4293 let target = ids[pos + 1] as usize;
4294 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4295 let lse: f64 = logits
4296 .iter()
4297 .map(|&v| ((v - max) as f64).exp())
4298 .sum::<f64>()
4299 .ln()
4300 + max as f64;
4301 let tok_nll = lse - logits[target] as f64;
4302 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4303 let top = logits
4304 .iter()
4305 .enumerate()
4306 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4307 .map(|(i, _)| i)
4308 .unwrap_or(0);
4309 eprintln!(
4310 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4311 logits[target], logits[top]
4312 );
4313 }
4314 nll += tok_nll;
4315 cnt += 1;
4316 }
4317 self.kv_cache.clear();
4318 self.kv_history.clear();
4319 (nll, cnt)
4320 }
4321
4322 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
4338 self.kv_cache.clear();
4339 self.kv_history.clear();
4340 self.o1_begin();
4341 let n = ids.len().saturating_sub(1);
4342 let p = prefill.min(n);
4343 let mut pos = 0usize;
4345 if self.can_prefill_batched() {
4346 const CHUNK: usize = 128;
4347 while pos < p {
4348 let end = (pos + CHUNK).min(p);
4349 let _ = self.prefill_batch(&ids[pos..end], pos);
4350 pos = end;
4351 }
4352 } else {
4353 while pos < p {
4354 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4355 pos += 1;
4356 }
4357 }
4358 self.o1_seal();
4359
4360 let mut nll = 0f64;
4361 let mut cnt = 0usize;
4362 for pos in p..n {
4363 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4364 let normed = inference::rms_norm(
4365 &hidden,
4366 &self.weights.final_norm,
4367 self.rms_eps,
4368 self.norm_style,
4369 );
4370 let logits = self.lm_head_forward(&normed);
4374 let target = ids[pos + 1] as usize;
4375 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4376 let lse: f64 = logits
4377 .iter()
4378 .map(|&v| ((v - max) as f64).exp())
4379 .sum::<f64>()
4380 .ln()
4381 + max as f64;
4382 let tok_nll = lse - logits[target] as f64;
4383 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4384 let top = logits
4385 .iter()
4386 .enumerate()
4387 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4388 .map(|(i, _)| i)
4389 .unwrap_or(0);
4390 eprintln!(
4391 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4392 logits[target], logits[top]
4393 );
4394 }
4395 nll += tok_nll;
4396 cnt += 1;
4397 }
4398 self.kv_cache.clear();
4399 self.kv_history.clear();
4400 (nll, cnt)
4401 }
4402
4403 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
4411 self.kv_cache.clear();
4412 self.kv_history.clear();
4413 let n = ids.len().saturating_sub(1);
4414 let mut correct = Vec::with_capacity(n);
4415 let mut pmax = Vec::with_capacity(n);
4416 for pos in 0..n {
4417 let emb = self.embed_single(ids[pos]);
4418 let hidden = self.forward_layers(&emb, pos, None);
4419 let normed = inference::rms_norm(
4420 &hidden,
4421 &self.weights.final_norm,
4422 self.rms_eps,
4423 self.norm_style,
4424 );
4425 let logits = self.lm_head_forward(&normed);
4429 let target = ids[pos + 1] as usize;
4430 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
4431 for (i, &v) in logits.iter().enumerate() {
4432 if v > mval {
4433 mval = v;
4434 amax = i;
4435 }
4436 }
4437 correct.push(amax == target);
4438 let row: Vec<f32> = temps
4439 .iter()
4440 .map(|&t| {
4441 let tt = t.max(1e-3);
4442 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
4443 1.0 / s.max(1e-12) })
4445 .collect();
4446 pmax.push(row);
4447 }
4448 self.kv_cache.clear();
4449 self.kv_history.clear();
4450 (correct, pmax)
4451 }
4452
4453 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
4460 let mut router = match self.dyn_router.take() {
4461 Some(r) => r,
4462 None => return (self.ppl_ids(ids), 0),
4463 };
4464 router.reset();
4465 self.dyn_phi_seen = 0;
4466 let _ = self.set_active_skill(None);
4467
4468 self.kv_cache.clear();
4469
4470 self.kv_history.clear();
4471 let mut nll = 0f64;
4472 let mut cnt = 0usize;
4473 for pos in 0..ids.len().saturating_sub(1) {
4474 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
4475 let normed = inference::rms_norm(
4476 &hidden,
4477 &self.weights.final_norm,
4478 self.rms_eps,
4479 self.norm_style,
4480 );
4481 let logits = self.lm_head_forward(&normed);
4485 let target = ids[pos + 1] as usize;
4486 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
4487 let lse: f64 = logits
4488 .iter()
4489 .map(|&v| ((v - max) as f64).exp())
4490 .sum::<f64>()
4491 .ln()
4492 + max as f64;
4493 let tok_nll = lse - logits[target] as f64;
4494 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
4495 let top = logits
4496 .iter()
4497 .enumerate()
4498 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4499 .map(|(i, _)| i)
4500 .unwrap_or(0);
4501 eprintln!(
4502 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
4503 logits[target], logits[top]
4504 );
4505 }
4506 nll += tok_nll;
4507 cnt += 1;
4508 let phi = self.dyn_phi_ema.clone();
4510 if let Some(new_active) = router.step(&phi, pos) {
4511 let _ = self.set_active_skill(new_active);
4512 }
4513 }
4514 let switches = router.switches.len();
4515 let _ = self.set_active_skill(None);
4516 self.dyn_router = Some(router);
4517 self.kv_cache.clear();
4518 self.kv_history.clear();
4519 ((nll / cnt.max(1) as f64).exp(), switches)
4520 }
4521
4522 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
4524 self.kv_cache.clear();
4525 self.kv_history.clear();
4526 let mut acc = vec![0f32; self.hidden_size];
4527 for (pos, &id) in ids.iter().enumerate() {
4528 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
4529 for (a, v) in acc.iter_mut().zip(&h) {
4530 *a += v;
4531 }
4532 }
4533 let n = ids.len().max(1) as f32;
4534 for a in acc.iter_mut() {
4535 *a /= n;
4536 }
4537 self.kv_cache.clear();
4538 self.kv_history.clear();
4539 acc
4540 }
4541
4542 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
4548 self.prefill_batch_masked(ids, start_pos, None)
4549 }
4550
4551 fn prefill_batch_masked(
4557 &mut self,
4558 ids: &[u32],
4559 start_pos: usize,
4560 task_mask: Option<&TaskMask>,
4561 ) -> Vec<f32> {
4562 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
4563 }
4564
4565 fn prefill_batch_span(
4571 &mut self,
4572 input: PrefillIn<'_>,
4573 start_pos: usize,
4574 task_mask: Option<&TaskMask>,
4575 from: usize,
4576 upto_excl: usize,
4577 ) -> Vec<f32> {
4578 let hs = self.hidden_size;
4579 let b = match input {
4580 PrefillIn::Ids(ids) => ids.len(),
4581 PrefillIn::Hidden(hb) => hb.len() / hs,
4582 };
4583 let upto_excl = upto_excl.min(self.num_layers);
4584 let mut h: Vec<f32>;
4588 let mut h_ready;
4589 match input {
4590 PrefillIn::Ids(_) => {
4591 h = vec![0.0; b * hs];
4592 h_ready = false;
4593 }
4594 PrefillIn::Hidden(hb) => {
4595 h = hb.to_vec();
4596 h_ready = true;
4597 }
4598 }
4599 let fill_h = |h: &mut Vec<f32>, me: &Self| {
4600 if let PrefillIn::Ids(ids) = input {
4601 for (bi, &id) in ids.iter().enumerate() {
4602 let e = me.embed_single(id);
4603 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
4604 }
4605 }
4606 };
4607 let (_nkv, _hd, _rd, eps) = (
4608 self.num_kv_heads,
4609 self.head_dim,
4610 self.rotary_dim,
4611 self.rms_eps,
4612 );
4613 let pool = self.pool.clone();
4614 let norm_style = self.norm_style;
4615
4616 #[cfg(target_os = "macos")]
4617 let mut chunk_skip_until = 0usize;
4618 for li in from..upto_excl {
4619 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
4626 if task_mask.is_none() {
4627 if li < chunk_skip_until {
4628 continue;
4629 }
4630 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
4636 fill_h(&mut h, self);
4637 h_ready = true;
4638 }
4639 let ids_for_embed = match input {
4640 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
4641 PrefillIn::Hidden(_) => None,
4642 };
4643 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
4644 if end > li {
4645 h_ready = true;
4646 chunk_skip_until = end;
4647 if self.is_loop_end(end - 1) && end < self.num_layers {
4650 for bi in 0..b {
4651 let normed = inference::rms_norm(
4652 &h[bi * hs..(bi + 1) * hs],
4653 &self.weights.final_norm,
4654 eps,
4655 norm_style,
4656 );
4657 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
4658 }
4659 }
4660 continue;
4661 }
4662 }
4663 if !h_ready {
4664 fill_h(&mut h, self);
4665 h_ready = true;
4666 }
4667 let lw = &self.weights.layers[self.phys_layer(li)];
4668 match &lw.attn {
4670 AttnKind::Kda(w) => {
4671 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
4673 let mut normed = vec![0.0f32; b * hs];
4674 for bi in 0..b {
4675 inference::rms_norm_into(
4676 &h[bi * hs..(bi + 1) * hs],
4677 &lw.input_norm,
4678 eps,
4679 norm_style,
4680 &mut normed[bi * hs..(bi + 1) * hs],
4681 );
4682 }
4683 let attn = crate::linear_core::kda_forward_batch(
4684 &normed,
4685 b,
4686 w,
4687 &cfg,
4688 &mut self.kv_cache.layers[li].linear_state,
4689 pool.as_deref(),
4690 );
4691 for (dst, &a) in h.iter_mut().zip(&attn) {
4692 *dst += a;
4693 }
4694 }
4695 AttnKind::LinearGdn(w) => {
4696 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4698 let mut normed = vec![0.0f32; b * hs];
4699 for bi in 0..b {
4700 let r = inference::rms_norm(
4701 &h[bi * hs..(bi + 1) * hs],
4702 &lw.input_norm,
4703 eps,
4704 norm_style,
4705 );
4706 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
4707 }
4708 let attn = crate::linear_core::gdn_forward_batch(
4709 &normed,
4710 b,
4711 w,
4712 &cfg,
4713 &mut self.kv_cache.layers[li].linear_state,
4714 pool.as_deref(),
4715 );
4716 for (dst, &a) in h.iter_mut().zip(&attn) {
4717 *dst += a;
4718 }
4719 }
4720 AttnKind::ShortConv(w) => {
4721 let cfg = self
4724 .short_conv_cfg
4725 .expect("short-conv layer without short_conv_cfg");
4726 let mut normed = vec![0.0f32; b * hs];
4727 for bi in 0..b {
4728 inference::rms_norm_into(
4729 &h[bi * hs..(bi + 1) * hs],
4730 &lw.input_norm,
4731 eps,
4732 norm_style,
4733 &mut normed[bi * hs..(bi + 1) * hs],
4734 );
4735 }
4736 let attn = short_conv_forward_batch(
4737 &normed,
4738 b,
4739 w,
4740 &cfg,
4741 &mut self.kv_cache.layers[li].linear_state,
4742 pool.as_deref(),
4743 );
4744 for (dst, &a) in h.iter_mut().zip(&attn) {
4745 *dst += a;
4746 }
4747 }
4748 AttnKind::Mla(w) => {
4749 let inv_freq_l = self.layer_inv_freq(li);
4752 let rs = self.layer_rope_scale(li);
4753 let mut normed = vec![0.0f32; hs];
4754 for bi in 0..b {
4755 inference::rms_norm_into(
4756 &h[bi * hs..(bi + 1) * hs],
4757 &lw.input_norm,
4758 eps,
4759 norm_style,
4760 &mut normed,
4761 );
4762 let ao = mla_attention(
4763 w,
4764 &normed,
4765 &mut self.kv_cache.layers[li],
4766 start_pos + bi,
4767 &inv_freq_l,
4768 rs,
4769 eps,
4770 pool.as_deref(),
4771 );
4772 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
4773 *dst += a;
4774 }
4775 }
4776 }
4777 AttnKind::Full {
4778 wq,
4779 wk,
4780 wv,
4781 wo,
4782 q_norm,
4783 k_norm,
4784 output_gate,
4785 softplus_gate,
4786 bias,
4787 } => {
4788 let mut normed = vec![0.0f32; b * hs];
4792 for bi in 0..b {
4793 inference::rms_norm_into(
4794 &h[bi * hs..(bi + 1) * hs],
4795 &lw.input_norm,
4796 eps,
4797 norm_style,
4798 &mut normed[bi * hs..(bi + 1) * hs],
4799 );
4800 }
4801 let inv_freq_l = self.layer_inv_freq(li);
4802 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4803 let cfg = QwenAttnCfg {
4804 num_heads: self.layer_num_heads(li),
4805 num_kv_heads: nkv_l,
4806 head_dim: hd_l,
4807 hidden_size: hs,
4808 position: start_pos,
4809 inv_freq: &inv_freq_l,
4810 rotary_dim: rd_l,
4811 scale: self.attn_scale,
4812 softcap: self.attn_softcap,
4813 window: self.layer_window(li),
4814 v_norm: self.attn_v_norm,
4815 q_norm: q_norm.as_deref(),
4816 k_norm: k_norm.as_deref(),
4817 output_gate: *output_gate,
4818 softplus_gate: softplus_gate
4819 .as_ref()
4820 .map(|(gate, per_head)| (gate, *per_head)),
4821 rope_scale: self.layer_rope_scale(li),
4822 bias: bias
4823 .as_ref()
4824 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4825 rms_eps: eps,
4826 norm_style,
4827 pool: pool.as_deref(),
4828 };
4829 let mut attn = attention::qwen_attention_batch(
4830 &normed,
4831 b,
4832 wq,
4833 wk,
4834 wv,
4835 wo,
4836 &mut self.kv_cache.layers[li],
4837 &cfg,
4838 );
4839 if let Some(w) = &lw.attn_out_norm {
4840 for bi in 0..b {
4841 inference::rms_norm_into(
4842 &attn[bi * hs..(bi + 1) * hs],
4843 w,
4844 eps,
4845 norm_style,
4846 &mut normed[bi * hs..(bi + 1) * hs],
4847 );
4848 }
4849 attn.copy_from_slice(&normed);
4850 }
4851 for (dst, &a) in h.iter_mut().zip(&attn) {
4852 *dst += a;
4853 }
4854 }
4855 AttnKind::Linear(w) => {
4856 for bi in 0..b {
4857 let normed = inference::rms_norm(
4858 &h[bi * hs..(bi + 1) * hs],
4859 &lw.input_norm,
4860 eps,
4861 norm_style,
4862 );
4863 vmf_phase_forward(
4864 &normed,
4865 w,
4866 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
4867 &mut self.kv_cache.layers[li].linear_state,
4868 pool.as_deref(),
4869 )
4870 .iter()
4871 .enumerate()
4872 .for_each(|(i, &a)| h[bi * hs + i] += a);
4873 }
4874 }
4875 }
4876
4877 let lw = &self.weights.layers[self.phys_layer(li)];
4879 let mut post = vec![0.0f32; b * hs];
4880 for bi in 0..b {
4881 let r =
4882 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
4883 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
4884 }
4885 let mask_row = task_mask
4888 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
4889 .and_then(|m| m.ffn_masks.get(li))
4890 .map(|v| v.as_slice());
4891 let mut ffn = match &lw.ffn {
4892 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
4893 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
4894 FfnKind::DenseMoe(dm) => {
4897 let mut out = vec![0.0f32; b * hs];
4898 for bi in 0..b {
4899 let r = dense_moe_ffn(
4900 dm,
4901 &post[bi * hs..(bi + 1) * hs],
4902 &h[bi * hs..(bi + 1) * hs],
4903 eps,
4904 norm_style,
4905 pool.as_deref(),
4906 );
4907 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
4908 }
4909 out
4910 }
4911 };
4912 if let Some(w) = &lw.ffn_out_norm {
4913 for bi in 0..b {
4914 inference::rms_norm_into(
4915 &ffn[bi * hs..(bi + 1) * hs],
4916 w,
4917 eps,
4918 norm_style,
4919 &mut post[bi * hs..(bi + 1) * hs],
4920 );
4921 }
4922 ffn.copy_from_slice(&post);
4923 }
4924 for (dst, &f) in h.iter_mut().zip(&ffn) {
4925 *dst += f;
4926 }
4927 if let Some(sc) = lw.layer_scale {
4928 for v in h.iter_mut() {
4929 *v *= sc;
4930 }
4931 }
4932 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
4933 if let Some(t) = tp.parse::<usize>().ok() {
4934 if t >= start_pos && t < start_pos + b {
4935 let bi = t - start_pos;
4936 let row = &h[bi * hs..(bi + 1) * hs];
4937 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
4938 eprintln!(
4939 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
4940 row[0], row[1]
4941 );
4942 }
4943 }
4944 }
4945 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
4949 let row = &h[(b - 1) * hs..b * hs];
4950 let rms =
4951 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
4952 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
4953 eprintln!(
4954 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
4955 match &self.weights.layers[self.phys_layer(li)].attn {
4956 AttnKind::LinearGdn(_) => "gdn",
4957 AttnKind::Linear(_) => "vmf",
4958 AttnKind::ShortConv(_) => "conv",
4959 _ => "attn",
4960 },
4961 match &lw.ffn {
4962 FfnKind::Moe(_) => "moe",
4963 FfnKind::Dense(_) => "dense",
4964 FfnKind::DenseMoe(_) => "dense+moe",
4965 },
4966 );
4967 }
4968 if self.is_loop_end(li) && li + 1 < self.num_layers {
4970 for bi in 0..b {
4971 let normed = inference::rms_norm(
4972 &h[bi * hs..(bi + 1) * hs],
4973 &self.weights.final_norm,
4974 eps,
4975 norm_style,
4976 );
4977 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
4978 }
4979 }
4980 if std::env::var("CMF_TRACE_H").is_ok() {
4981 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
4982 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
4983 eprintln!(
4984 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
4985 lw.layer_scale
4986 );
4987 }
4988 }
4989 crate::gpu::set_layer(-1); h
4991 }
4992
4993 fn embed_single(&self, id: u32) -> Vec<f32> {
4995 let mut out = vec![0.0f32; self.hidden_size];
4996 if (id as usize) < self.weights.embed_tokens.rows() {
4997 self.weights.embed_tokens.row_f32(id as usize, &mut out);
4998 }
4999 if self.embed_multiplier != 1.0 {
5000 for v in out.iter_mut() {
5001 *v *= self.embed_multiplier;
5002 }
5003 }
5004 if self.dsv4.is_some() {
5008 let mut v = vec![0.0f32; self.hidden_size.max(1)];
5009 v[0] = id as f32;
5010 return v;
5011 }
5012 if let Some(b) = &self.g3n {
5015 return b.0.extend_embedding(id, &out, self.pool.as_deref());
5016 }
5017 out
5018 }
5019
5020 #[cfg(target_os = "macos")]
5026 fn chunk_run_gpu(
5027 &mut self,
5028 li0: usize,
5029 h: &mut [f32],
5030 b: usize,
5031 pos0: usize,
5032 embed_ids: Option<&[u32]>,
5033 cap: usize,
5034 ) -> usize {
5035 if !crate::gpu::enabled_here()
5039 || std::env::var("CMF_GPU_CHUNK")
5040 .map(|v| v == "0")
5041 .unwrap_or(false)
5042 || b < 32
5043 || self.swa.is_some()
5044 || self.global_attn.is_some()
5045 || self.attn_v_norm
5046 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
5047 {
5048 return li0;
5049 }
5050 let Some(model) = self.model.clone() else {
5051 return li0;
5052 };
5053 let inv_freq = self.inv_freq.clone();
5054 let (nh, nkv, hd, hs) = (
5055 self.num_heads,
5056 self.num_kv_heads,
5057 self.head_dim,
5058 self.hidden_size,
5059 );
5060 let loop_end = if self.loop_final_norm {
5064 ((li0 / self.physical_layers) + 1) * self.physical_layers
5065 } else {
5066 self.num_layers
5067 };
5068 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
5069 let mut stored_at: Vec<usize> = Vec::new();
5070 for li in li0..self.num_layers.min(loop_end).min(cap) {
5071 let lw = &self.weights.layers[self.phys_layer(li)];
5072 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
5073 break;
5074 }
5075 let AttnKind::Full {
5076 wq,
5077 wk,
5078 wv,
5079 wo,
5080 q_norm,
5081 k_norm,
5082 output_gate: false,
5083 softplus_gate: None,
5084 bias,
5085 } = &lw.attn
5086 else {
5087 break;
5088 };
5089 let FfnKind::Dense(d) = &lw.ffn else { break };
5090 if d.act != Act::Silu {
5091 break;
5092 }
5093 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
5098 t.q8_row_parts()
5099 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5100 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
5101 }
5102 let parts = (
5103 cw(wq),
5104 cw(wk),
5105 cw(wv),
5106 cw(wo),
5107 cw(&d.gate_proj),
5108 cw(&d.up_proj),
5109 cw(&d.down_proj),
5110 );
5111 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
5112 else {
5113 break;
5114 };
5115 let layer = &self.kv_cache.layers[li];
5116 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
5117 break;
5118 }
5119 stored_at.push(layer.head_len(0));
5120 layers.push(crate::gpu_metal::ChunkLayer {
5121 model: &model,
5122 kv_id: self.graph_kv_id,
5123 layer: li,
5124 wq: pq,
5125 wk: pk,
5126 wv: pv,
5127 wo: po,
5128 gate: pg,
5129 up: pu,
5130 down: pd,
5131 input_norm: &lw.input_norm,
5132 post_norm: &lw.post_norm,
5133 bias: bias
5134 .as_ref()
5135 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
5136 q_norm: q_norm.as_deref(),
5137 k_norm: k_norm.as_deref(),
5138 inv_freq: &inv_freq,
5139 rd: self.rotary_dim,
5140 nh,
5141 nkv,
5142 hd,
5143 hs,
5144 inter: d.gate_proj.rows(),
5145 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
5146 eps: self.rms_eps as f32,
5147 });
5148 }
5149 if layers.is_empty() {
5150 return li0;
5151 }
5152 let row = nkv * hd;
5153 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
5154 .iter()
5155 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
5156 .collect();
5157 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
5158 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
5159 let li = layers[i].layer;
5160 let layer = &self.kv_cache.layers[li];
5161 io.push(crate::gpu_metal::ChunkIo {
5162 cpu_stored: stored_at[i],
5163 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
5164 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
5165 out_k: ok,
5166 out_v: ov,
5167 imp: oi,
5168 });
5169 }
5170 let n_run = layers.len();
5171 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
5172 let ep = embed_ids.and_then(|ids| {
5175 self.weights
5176 .embed_tokens
5177 .q8_row_parts()
5178 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
5179 idx,
5180 rows,
5181 row_scale: rs,
5182 ids,
5183 mult: self.embed_multiplier,
5184 })
5185 });
5186 if embed_ids.is_some() && ep.is_none() {
5187 return li0;
5188 }
5189 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
5190 return li0;
5191 }
5192 drop(io);
5193 drop(layers);
5194 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
5197 let li = li0 + i;
5198 let layer = &mut self.kv_cache.layers[li];
5199 for bi in 0..b {
5200 layer.append(
5201 &ok[bi * row..(bi + 1) * row],
5202 &ov[bi * row..(bi + 1) * row],
5203 &[],
5204 );
5205 }
5206 layer.accumulate_imp(oi);
5207 }
5208 last
5209 }
5210
5211 fn layer_is_local(&self, li: usize) -> bool {
5214 if let Some(layers) = &self.sliding_layers {
5215 return layers.get(li).copied().unwrap_or(false);
5216 }
5217 match self.swa {
5218 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
5219 None => false,
5220 }
5221 }
5222
5223 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
5226 if self.layer_is_local(li) {
5227 if let Some(f) = &self.inv_freq_local {
5228 return f.clone();
5229 }
5230 } else if let Some(f) = &self.inv_freq_global {
5231 return f.clone();
5232 }
5233 self.inv_freq.clone()
5234 }
5235
5236 fn layer_window(&self, li: usize) -> Option<usize> {
5238 self.swa
5239 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
5240 }
5241
5242 fn layer_num_heads(&self, li: usize) -> usize {
5243 self.attention_heads_per_layer
5244 .as_ref()
5245 .and_then(|v| v.get(li).copied())
5246 .unwrap_or(self.num_heads)
5247 }
5248
5249 fn layer_rope_scale(&self, li: usize) -> f32 {
5250 if self.layer_is_local(li) {
5251 self.rope_scale_local
5252 } else {
5253 self.rope_scale
5254 }
5255 }
5256
5257 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
5260 if !self.layer_is_local(li) {
5261 if let Some((ghd, gkv)) = self.global_attn {
5262 return (gkv, ghd, ghd);
5263 }
5264 }
5265 (
5266 self.num_kv_heads,
5267 self.head_dim,
5268 if self.layer_is_local(li) {
5269 self.rotary_dim_local.unwrap_or(self.rotary_dim)
5270 } else {
5271 self.rotary_dim
5272 },
5273 )
5274 }
5275
5276 fn forward_layers(
5278 &mut self,
5279 hidden: &[f32],
5280 position: usize,
5281 task_mask: Option<&TaskMask>,
5282 ) -> Vec<f32> {
5283 self.forward_layers_upto(hidden, position, task_mask, None)
5284 }
5285
5286 pub fn embed_id(&self, id: u32) -> Vec<f32> {
5294 self.embed_single(id)
5295 }
5296
5297 pub fn split_supported(&self) -> Result<(), String> {
5301 if self.dsv4.is_some() {
5302 return Err(
5303 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
5304 );
5305 }
5306 if self.g3n.is_some() {
5307 return Err(
5308 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
5309 );
5310 }
5311 Ok(())
5312 }
5313
5314 pub fn forward_span(
5319 &mut self,
5320 hidden: &[f32],
5321 position: usize,
5322 from: usize,
5323 upto: usize,
5324 task_mask: Option<&TaskMask>,
5325 ) -> Result<Vec<f32>, String> {
5326 self.split_supported()?;
5327 if from > upto || upto >= self.num_layers {
5328 return Err(format!(
5329 "forward_span: layer range {from}..={upto} outside 0..{}",
5330 self.num_layers
5331 ));
5332 }
5333 if hidden.len() != self.hidden_size {
5334 return Err(format!(
5335 "forward_span: hidden len {} ≠ hidden_size {}",
5336 hidden.len(),
5337 self.hidden_size
5338 ));
5339 }
5340 Ok(self.forward_layers_span(hidden, position, task_mask, from, Some(upto)))
5341 }
5342
5343 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
5346 let normed = inference::rms_norm(
5347 hidden,
5348 &self.weights.final_norm,
5349 self.rms_eps,
5350 self.norm_style,
5351 );
5352 self.lm_head_forward(&normed)
5353 }
5354
5355 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
5357 sampler::sample_with_scratch(
5358 logits,
5359 &self.sampler_config,
5360 past_tokens,
5361 &mut self.rng,
5362 &mut self.sampler_scratch,
5363 )
5364 }
5365
5366 pub fn reset_session(&mut self) {
5368 self.kv_cache.clear();
5369 self.kv_history.clear();
5370 crate::gpu::graph_kv_reset(self.graph_kv_id);
5371 }
5372
5373 pub fn prefill_span_ids(
5379 &mut self,
5380 ids: &[u32],
5381 start_pos: usize,
5382 upto: usize,
5383 task_mask: Option<&TaskMask>,
5384 ) -> Result<Vec<f32>, String> {
5385 self.split_supported()?;
5386 if upto >= self.num_layers {
5387 return Err(format!(
5388 "prefill_span_ids: upto {upto} outside 0..{}",
5389 self.num_layers
5390 ));
5391 }
5392 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5396 Ok(self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1))
5397 } else {
5398 let hs = self.hidden_size;
5399 let mut out = Vec::with_capacity(ids.len() * hs);
5400 for (i, &id) in ids.iter().enumerate() {
5401 let emb = self.embed_id(id);
5402 out.extend_from_slice(&self.forward_span(
5403 &emb,
5404 start_pos + i,
5405 0,
5406 upto,
5407 task_mask,
5408 )?);
5409 }
5410 Ok(out)
5411 }
5412 }
5413
5414 pub fn prefill_span_hidden(
5417 &mut self,
5418 hidden: &[f32],
5419 start_pos: usize,
5420 from: usize,
5421 upto: usize,
5422 task_mask: Option<&TaskMask>,
5423 ) -> Result<Vec<f32>, String> {
5424 self.split_supported()?;
5425 let hs = self.hidden_size;
5426 if hidden.is_empty() || hidden.len() % hs != 0 {
5427 return Err(format!(
5428 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
5429 hidden.len()
5430 ));
5431 }
5432 if from > upto || upto >= self.num_layers {
5433 return Err(format!(
5434 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
5435 self.num_layers
5436 ));
5437 }
5438 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
5439 Ok(self.prefill_batch_span(
5440 PrefillIn::Hidden(hidden),
5441 start_pos,
5442 task_mask,
5443 from,
5444 upto + 1,
5445 ))
5446 } else {
5447 let b = hidden.len() / hs;
5448 let mut out = Vec::with_capacity(hidden.len());
5449 for i in 0..b {
5450 let h = self.forward_span(
5451 &hidden[i * hs..(i + 1) * hs],
5452 start_pos + i,
5453 from,
5454 upto,
5455 task_mask,
5456 )?;
5457 out.extend_from_slice(&h);
5458 }
5459 Ok(out)
5460 }
5461 }
5462
5463 fn try_token_graph_wgpu(
5467 &self,
5468 hidden: &[f32],
5469 position: usize,
5470 logits_out: &mut Vec<f32>,
5471 layers_run: &mut usize,
5472 ) -> Option<Vec<f32>> {
5473 self.try_token_graph_wgpu_steps(
5474 hidden,
5475 position,
5476 logits_out,
5477 1,
5478 None,
5479 Some(layers_run),
5480 0,
5481 self.num_layers,
5482 )
5483 }
5484
5485 fn try_token_graph_wgpu_span(
5489 &self,
5490 hidden: &[f32],
5491 position: usize,
5492 logits_out: &mut Vec<f32>,
5493 from: usize,
5494 upto_excl: usize,
5495 layers_run: &mut usize,
5496 ) -> Option<Vec<f32>> {
5497 self.try_token_graph_wgpu_steps(
5498 hidden,
5499 position,
5500 logits_out,
5501 1,
5502 None,
5503 Some(layers_run),
5504 from,
5505 upto_excl,
5506 )
5507 }
5508
5509 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
5513 if self.o1_active() || self.attn_softcap > 0.0 {
5514 return None;
5515 }
5516 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5517 if !graph_on || crate::gpu::graph_unsupported() {
5518 return None;
5525 }
5526 let emb = self.embed_single(t_next);
5527 let mut lg = Vec::new();
5528 let mut ids = Vec::new();
5529 self.try_token_graph_wgpu_steps(
5530 &emb,
5531 position,
5532 &mut lg,
5533 k,
5534 Some(&mut ids),
5535 None,
5536 0,
5537 self.num_layers,
5538 )?;
5539 (ids.len() == k).then_some(ids)
5540 }
5541
5542 fn try_token_graph_wgpu_steps(
5546 &self,
5547 hidden: &[f32],
5548 position: usize,
5549 logits_out: &mut Vec<f32>,
5550 steps: usize,
5551 ids_out: Option<&mut Vec<u32>>,
5552 layers_run: Option<&mut usize>,
5553 from: usize,
5554 upto_excl: usize,
5555 ) -> Option<Vec<f32>> {
5556 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
5559 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
5560 return None;
5564 }
5565 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
5570 .map(|li| {
5571 if !o1_gpu {
5572 return None;
5573 }
5574 self.kv_cache.layers[self.phys_layer(li)].o1_views()
5575 })
5576 .collect();
5577 if self.o1_active() && o1_gpu {
5578 let want: usize = (from..upto_excl)
5581 .filter(|li| !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None))
5582 .count();
5583 let have = o1_views.iter().filter(|v| v.is_some()).count();
5584 if want == 0 || have != want {
5585 use std::sync::atomic::{AtomicUsize, Ordering};
5595 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
5596 let code = have * 1000 + want;
5597 if LAST.swap(code, Ordering::Relaxed) != code {
5598 tracing::warn!(
5599 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
5600 );
5601 }
5602 return None;
5603 }
5604 }
5605 let nh = self.num_heads;
5606 let (nkv, hd, rd) = self.layer_geom(0);
5607 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5608 let mut layers = Vec::with_capacity(upto_excl - from);
5609 let mut model = None;
5610 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
5611 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5612 if let Some((_, i, kind, rs)) = t.graph_weight() {
5613 return Some(crate::gpu::GraphW {
5614 idx: i,
5615 kind,
5616 row_scale: rs,
5617 data: &[],
5618 });
5619 }
5620 t.as_f32().map(|d| crate::gpu::GraphW {
5622 idx: 0,
5623 kind: 4,
5624 row_scale: &[],
5625 data: d,
5626 })
5627 }
5628 for li in from..upto_excl {
5629 let lw = &self.weights.layers[self.phys_layer(li)];
5630 if dbg {
5631 let ak = match &lw.attn {
5632 AttnKind::Mla(_) => "Mla".into(),
5633 AttnKind::Full {
5634 output_gate, bias, ..
5635 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
5636 AttnKind::LinearGdn(_) => "LinearGdn".into(),
5637 AttnKind::Kda(_) => "Kda".into(),
5638 AttnKind::Linear(_) => "Linear".into(),
5639 AttnKind::ShortConv(_) => "ShortConv".into(),
5640 };
5641 let fk = match &lw.ffn {
5642 FfnKind::Dense(_) => "Dense",
5643 FfnKind::Moe(_) => "Moe",
5644 FfnKind::DenseMoe(_) => "DenseMoe",
5645 };
5646 eprintln!("graph L{li}: attn={ak} ffn={fk}");
5647 }
5648 let gffn = match &lw.ffn {
5649 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
5651 gate: gw(&d.gate_proj)?,
5652 up: gw(&d.up_proj)?,
5653 down: gw(&d.down_proj)?,
5654 },
5655 FfnKind::Moe(m) => {
5656 if m.router_sigmoid
5661 || m.expert_bias.is_some()
5662 || m.route_tau.is_some()
5663 || m.mask.is_some()
5664 {
5665 return None;
5666 }
5667 let (se, sg) = m.shared.as_ref()?;
5668 let sgate = gw(sg.as_ref()?)?;
5669 let router = gw(&m.router)?;
5670 let inter = m.experts.first()?.gate_proj.rows();
5671 let mut experts = Vec::with_capacity(m.experts.len() + 1);
5672 let mut q4tp: Option<bool> = None;
5675 let mut gu_q2: Option<bool> = None;
5678 for e in m.experts.iter().chain(std::iter::once(se)) {
5679 if !matches!(e.act, Act::Silu)
5680 || e.gate_proj.rows() != inter
5681 || e.up_proj.rows() != inter
5682 {
5683 return None;
5684 }
5685 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
5686 Some((mm, gi)) => (
5687 mm,
5688 gi,
5689 e.up_proj.mapped_q4t()?.1,
5690 e.down_proj.mapped_q4t()?.1,
5691 false,
5692 false,
5693 ),
5694 None => match e.gate_proj.mapped_q2tp() {
5695 Some((mm, gi)) => (
5696 mm,
5697 gi,
5698 e.up_proj.mapped_q2tp()?.1,
5699 e.down_proj.mapped_q4tp()?.1,
5700 true,
5701 true,
5702 ),
5703 None => {
5704 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
5705 (
5706 mm,
5707 gi,
5708 e.up_proj.mapped_q4tp()?.1,
5709 e.down_proj.mapped_q4tp()?.1,
5710 true,
5711 false,
5712 )
5713 }
5714 },
5715 };
5716 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
5717 {
5718 tracing::warn!(
5724 "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."
5725 );
5726 return None;
5727 }
5728 model.get_or_insert_with(|| mm.clone());
5729 experts.push((gi, ui, di));
5730 }
5731 crate::gpu::GraphFfn::Moe {
5732 router,
5733 shared_gate: sgate,
5734 experts,
5735 n_exp: m.experts.len(),
5736 top_k: std::env::var("CMF_TOPK_PROBE")
5742 .ok()
5743 .and_then(|v| v.parse::<usize>().ok())
5744 .filter(|k| *k > 0 && *k <= m.top_k)
5745 .unwrap_or(m.top_k),
5746 inter,
5747 norm_topk: m.norm_topk_prob,
5748 q4tp: q4tp?,
5749 gu_q2: gu_q2.unwrap_or(false),
5750 }
5751 }
5752 };
5753 let attn = match &lw.attn {
5754 AttnKind::Full {
5755 wq,
5756 wk,
5757 wv,
5758 wo,
5759 q_norm,
5760 k_norm,
5761 output_gate,
5762 softplus_gate,
5763 bias,
5764 } => {
5765 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
5766 return None;
5767 }
5768 let (m, _, _, _) = wq.graph_weight()?;
5769 model = Some(m.clone());
5770 crate::gpu::GraphAttn::Full {
5771 wq: gw(wq)?,
5772 wk: gw(wk)?,
5773 wv: gw(wv)?,
5774 wo: gw(wo)?,
5775 q_norm: q_norm.as_deref(),
5776 k_norm: k_norm.as_deref(),
5777 bias: bias
5778 .as_ref()
5779 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5780 output_gate: *output_gate,
5781 cpu_k: self.kv_cache.layers[li].k_heads(),
5782 cpu_v: self.kv_cache.layers[li].v_heads(),
5783 }
5784 }
5785 AttnKind::LinearGdn(w) => {
5786 let cfg = self.gdn_cfg?;
5787 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
5788 model = Some(m.clone());
5789 crate::gpu::GraphAttn::Gdn {
5790 qkv: gw(&w.in_proj_qkv)?,
5791 z: gw(&w.in_proj_z)?,
5792 a: gw(&w.in_proj_a)?,
5793 b: gw(&w.in_proj_b)?,
5794 out: gw(&w.out_proj)?,
5795 conv1d: &w.conv1d,
5796 a_log: &w.a_log,
5797 dt_bias: &w.dt_bias,
5798 norm: &w.norm,
5799 nv: cfg.num_v_heads,
5800 nk: cfg.num_k_heads,
5801 dk: cfg.key_head_dim,
5802 dv: cfg.value_head_dim,
5803 kk: cfg.conv_kernel,
5804 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
5805 }
5806 }
5807 _ => return None,
5808 };
5809 layers.push(crate::gpu::GraphLayer {
5810 input_norm: &lw.input_norm,
5811 attn,
5812 post_norm: &lw.post_norm,
5813 ffn: gffn,
5814 });
5815 }
5816 let model = model?;
5817 let lm_gw = if upto_excl == self.num_layers
5823 && self.graph_want_logits
5824 && std::env::var("CMF_GPU_LMHEAD")
5825 .map(|v| v != "0")
5826 .unwrap_or(true)
5827 {
5828 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
5829 (
5830 crate::gpu::GraphW {
5831 idx: i,
5832 kind,
5833 row_scale: rs,
5834 data: &[],
5835 },
5836 self.weights.lm_head.rows(),
5837 )
5838 })
5839 } else {
5840 None
5841 };
5842 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
5843 let emb_gw = if steps > 1 {
5845 self.weights
5846 .embed_tokens
5847 .graph_weight()
5848 .map(|(_, i, kind, rs)| {
5849 (
5850 crate::gpu::GraphW {
5851 idx: i,
5852 kind,
5853 row_scale: rs,
5854 data: &[],
5855 },
5856 self.weights.embed_tokens.rows(),
5857 self.embed_multiplier as f32,
5858 )
5859 })
5860 } else {
5861 None
5862 };
5863
5864 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
5870 (from..upto_excl.min(self.num_layers - 1))
5871 .filter(|&li| (li + 1) % self.physical_layers == 0)
5872 .map(|li| li - from)
5873 .collect()
5874 } else {
5875 Vec::new()
5876 };
5877 let mut h = hidden.to_vec();
5878 crate::gpu::forward_token_graph(
5879 &model,
5880 self.graph_kv_id,
5881 &layers,
5882 &o1_views,
5883 self.o1_epoch,
5884 &self.inv_freq,
5885 &mut h,
5886 nh,
5887 nkv,
5888 hd,
5889 rd,
5890 self.hidden_size,
5891 self.intermediate_size,
5892 position,
5893 self.kv_cache.max_seq_len,
5894 gemma,
5895 self.rms_eps as f32,
5896 lm,
5897 &self.weights.final_norm,
5898 logits_out,
5899 &loop_norm_at,
5900 steps,
5901 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
5902 ids_out,
5903 layers_run,
5904 from,
5905 false,
5906 )
5907 .then_some(h)
5908 }
5909
5910 fn try_batch_graph_wgpu(
5915 &self,
5916 hiddens: &mut [f32],
5917 positions: &[usize],
5918 k: usize,
5919 spec: Option<crate::gpu::SpecTail<'_>>,
5920 ) -> bool {
5921 let _tb = std::time::Instant::now();
5922 if self.attn_softcap > 0.0 {
5923 return false; }
5925 if self.o1_active() {
5926 return false;
5927 }
5928 let nh = self.num_heads;
5929 let (nkv, hd, rd) = self.layer_geom(0);
5930 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5931 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
5932 if let Some((_, i, kind, rs)) = t.graph_weight() {
5933 return Some(crate::gpu::GraphW {
5934 idx: i,
5935 kind,
5936 row_scale: rs,
5937 data: &[],
5938 });
5939 }
5940 t.as_f32().map(|d| crate::gpu::GraphW {
5941 idx: 0,
5942 kind: 4,
5943 row_scale: &[],
5944 data: d,
5945 })
5946 }
5947 let built: Option<(
5948 Vec<crate::gpu::GraphLayer<'_>>,
5949 std::sync::Arc<cortiq_core::CmfModel>,
5950 )> = (|| {
5951 let mut layers = Vec::with_capacity(self.num_layers);
5952 let mut model = None;
5953 for li in 0..self.num_layers {
5954 let lw = &self.weights.layers[self.phys_layer(li)];
5955 let gffn = match &lw.ffn {
5962 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
5963 gate: gw(&d.gate_proj)?,
5964 up: gw(&d.up_proj)?,
5965 down: gw(&d.down_proj)?,
5966 },
5967 FfnKind::Moe(m) => {
5968 if m.router_sigmoid
5969 || m.expert_bias.is_some()
5970 || m.route_tau.is_some()
5971 || m.mask.is_some()
5972 {
5973 return None;
5974 }
5975 let (se, sg) = m.shared.as_ref()?;
5976 let sgate = gw(sg.as_ref()?)?;
5977 let router = gw(&m.router)?;
5978 let inter = m.experts.first()?.gate_proj.rows();
5979 let mut experts = Vec::with_capacity(m.experts.len() + 1);
5980 let mut q4tp: Option<bool> = None;
5981 let mut gu_q2: Option<bool> = None;
5982 for e in m.experts.iter().chain(std::iter::once(se)) {
5983 if !matches!(e.act, Act::Silu)
5984 || e.gate_proj.rows() != inter
5985 || e.up_proj.rows() != inter
5986 {
5987 return None;
5988 }
5989 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
5993 Some((mm, gi)) => (
5994 mm,
5995 gi,
5996 e.up_proj.mapped_q4t()?.1,
5997 e.down_proj.mapped_q4t()?.1,
5998 false,
5999 false,
6000 ),
6001 None => match e.gate_proj.mapped_q2tp() {
6002 Some((mm, gi)) => (
6003 mm,
6004 gi,
6005 e.up_proj.mapped_q2tp()?.1,
6006 e.down_proj.mapped_q4tp()?.1,
6007 true,
6008 true,
6009 ),
6010 None => {
6011 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
6012 (
6013 mm,
6014 gi,
6015 e.up_proj.mapped_q4tp()?.1,
6016 e.down_proj.mapped_q4tp()?.1,
6017 true,
6018 false,
6019 )
6020 }
6021 },
6022 };
6023 if *q4tp.get_or_insert(is_p) != is_p
6024 || *gu_q2.get_or_insert(is_q2) != is_q2
6025 {
6026 return None;
6027 }
6028 model.get_or_insert_with(|| mm.clone());
6029 experts.push((gi, ui, di));
6030 }
6031 crate::gpu::GraphFfn::Moe {
6032 router,
6033 shared_gate: sgate,
6034 experts,
6035 n_exp: m.experts.len(),
6036 top_k: m.top_k,
6037 inter,
6038 norm_topk: m.norm_topk_prob,
6039 q4tp: q4tp?,
6040 gu_q2: gu_q2.unwrap_or(false),
6041 }
6042 }
6043 _ => return None,
6044 };
6045 let attn = match &lw.attn {
6046 AttnKind::Full {
6047 wq,
6048 wk,
6049 wv,
6050 wo,
6051 q_norm,
6052 k_norm,
6053 output_gate,
6054 softplus_gate,
6055 bias,
6056 } => {
6057 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
6058 return None;
6059 }
6060 let (m, _, _, _) = wq.graph_weight()?;
6061 model = Some(m.clone());
6062 crate::gpu::GraphAttn::Full {
6063 wq: gw(wq)?,
6064 wk: gw(wk)?,
6065 wv: gw(wv)?,
6066 wo: gw(wo)?,
6067 q_norm: q_norm.as_deref(),
6068 k_norm: k_norm.as_deref(),
6069 bias: bias
6070 .as_ref()
6071 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6072 output_gate: *output_gate,
6073 cpu_k: self.kv_cache.layers[li].k_heads(),
6074 cpu_v: self.kv_cache.layers[li].v_heads(),
6075 }
6076 }
6077 AttnKind::LinearGdn(w) => {
6078 let cfg = self.gdn_cfg?;
6079 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
6080 model = Some(m.clone());
6081 crate::gpu::GraphAttn::Gdn {
6082 qkv: gw(&w.in_proj_qkv)?,
6083 z: gw(&w.in_proj_z)?,
6084 a: gw(&w.in_proj_a)?,
6085 b: gw(&w.in_proj_b)?,
6086 out: gw(&w.out_proj)?,
6087 conv1d: &w.conv1d,
6088 a_log: &w.a_log,
6089 dt_bias: &w.dt_bias,
6090 norm: &w.norm,
6091 nv: cfg.num_v_heads,
6092 nk: cfg.num_k_heads,
6093 dk: cfg.key_head_dim,
6094 dv: cfg.value_head_dim,
6095 kk: cfg.conv_kernel,
6096 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
6097 }
6098 }
6099 _ => return None,
6100 };
6101 layers.push(crate::gpu::GraphLayer {
6102 input_norm: &lw.input_norm,
6103 attn,
6104 post_norm: &lw.post_norm,
6105 ffn: gffn,
6106 });
6107 }
6108 Some((layers, model?))
6109 })();
6110 let Some((layers, model)) = built else {
6111 {
6112 use std::sync::atomic::{AtomicBool, Ordering};
6113 static SAID: AtomicBool = AtomicBool::new(false);
6114 if !SAID.swap(true, Ordering::Relaxed) {
6115 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
6116 }
6117 }
6118 return false;
6119 };
6120 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
6121 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
6122 }
6123 crate::gpu::forward_batch_graph(
6124 &model,
6125 self.graph_kv_id,
6126 &layers,
6127 &self.inv_freq,
6128 hiddens,
6129 nh,
6130 nkv,
6131 hd,
6132 rd,
6133 self.hidden_size,
6134 self.intermediate_size,
6135 positions,
6136 self.kv_cache.max_seq_len,
6137 gemma,
6138 self.rms_eps as f32,
6139 k,
6140 spec,
6141 )
6142 }
6143
6144 fn draft_probe() -> bool {
6148 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6149 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
6150 }
6151
6152 #[cfg(feature = "gpu")]
6164 fn dsv4_spec_on() -> bool {
6165 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6166 *ON.get_or_init(|| {
6167 std::env::var("CMF_DSV4_SPEC")
6168 .map(|v| v != "0")
6169 .unwrap_or(true)
6170 })
6171 }
6172
6173 #[cfg(feature = "gpu")]
6180 fn dsv4_spec_step(
6181 &mut self,
6182 tip_token: u32,
6183 t_next: u32,
6184 next_pos: usize,
6185 drafted: &mut usize,
6186 accepted_ctr: &mut usize,
6187 ) -> Option<(Vec<u32>, usize)> {
6188 let t_all = std::time::Instant::now();
6189 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
6190 thread_local! {
6191 static LAST: std::cell::Cell<Option<std::time::Instant>> =
6192 const { std::cell::Cell::new(None) };
6193 }
6194 LAST.with(|l| {
6195 if let Some(prev) = l.get() {
6196 eprintln!(
6197 "между раундами {:.1} мс",
6198 prev.elapsed().as_secs_f64() * 1e3
6199 );
6200 }
6201 l.set(Some(std::time::Instant::now()));
6202 });
6203 }
6204 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
6205 eprintln!("spec_step: вход pos={next_pos}");
6206 }
6207 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
6208 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
6209 if self.dspark.is_none() {
6211 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
6212 if t.is_empty() {
6213 return None;
6214 }
6215 crate::dsv4::dspark_arm(&t, cfg.dim);
6216 self.dspark = Some(crate::dsv4::DsparkState::new(
6217 self.dsv4_mtp.len(),
6218 &cfg,
6219 t.len(),
6220 ));
6221 }
6222 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
6223 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
6224 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
6225 eprintln!("spec_step: пак не построился (targets {targets:?})");
6226 }
6227 let pack = pack?;
6228 let block = crate::dsv4::dspark_block();
6229 let b_box = self.dsv4.as_mut()?;
6230 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
6231 let ds = self.dspark.as_mut()?;
6232 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
6235 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
6236 if dbg {
6237 eprintln!("spec_step: нет захвата");
6238 }
6239 return None;
6240 }
6241 ds.have_hidden = true;
6242 let tip_pos = next_pos.checked_sub(1)?;
6243 let draft_started = std::time::Instant::now();
6244 let mut conf = Vec::new();
6245 let props = crate::dsv4::dspark_draft_gpu(
6246 g,
6247 &self.dsv4_mtp,
6248 &cfg,
6249 ds,
6250 pack,
6251 st.kv_id,
6252 tip_token,
6253 tip_pos,
6254 self.pool.as_deref(),
6255 &mut conf,
6256 );
6257 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
6258 *drafted += block;
6259 if props.is_empty() || props[0] != t_next {
6260 if dbg {
6261 eprintln!(
6262 "spec_step: черновик {} (props0={:?} t_next={t_next})",
6263 if props.is_empty() {
6264 "пуст"
6265 } else {
6266 "мимо"
6267 },
6268 props.first()
6269 );
6270 }
6271 return None;
6272 }
6273 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
6274 let conf_min = {
6280 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
6281 *M.get_or_init(|| {
6282 std::env::var("CMF_DSPARK_CONF_MIN")
6283 .ok()
6284 .and_then(|v| v.parse().ok())
6285 .unwrap_or(0.0)
6286 })
6287 };
6288 if conf_min > 0.0 && conf.len() >= props.len() {
6289 let mut keep = 1usize;
6290 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
6291 keep += 1;
6292 }
6293 k_verify = k_verify.min(keep.max(2));
6294 }
6295 if k_verify < 2 {
6296 return None;
6297 }
6298 let mut fed = Vec::with_capacity(k_verify);
6299 fed.push(t_next);
6300 fed.extend_from_slice(&props[1..k_verify]);
6301 let mut argmax = Vec::new();
6302 let mut logits_all = Vec::new();
6303 let mut walked = Vec::new();
6304 let txn = crate::dsv4::dsv4_verify_chunk(
6305 g,
6306 layers,
6307 &cfg,
6308 st,
6309 &fed,
6310 next_pos,
6311 &self.inv_freq,
6312 self.pool.as_deref(),
6313 &targets,
6314 &mut argmax,
6315 &mut logits_all,
6316 &mut walked,
6317 );
6318 if txn.is_none() && dbg {
6319 eprintln!("spec_step: verify отказал");
6320 }
6321 let txn = txn?;
6322 let b = fed.len();
6323 let mut accepted = 1usize;
6324 while accepted < b && fed[accepted] == argmax[accepted - 1] {
6325 accepted += 1;
6326 }
6327 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
6332 accepted = 1;
6333 }
6334 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
6335 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
6336 }
6337 let t_fin = std::time::Instant::now();
6338 if !crate::dsv4::dsv4_spec_finish(
6339 g,
6340 layers,
6341 &cfg,
6342 st,
6343 txn,
6344 accepted,
6345 &fed,
6346 &self.inv_freq,
6347 self.pool.as_deref(),
6348 ) {
6349 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
6350 return None;
6351 }
6352 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
6353 eprintln!(
6354 "finish(k={accepted}): {:.1} мс",
6355 t_fin.elapsed().as_secs_f64() * 1e3
6356 );
6357 }
6358 *accepted_ctr += accepted - 1;
6359 let (hc, dim) = (cfg.hc_mult, cfg.dim);
6364 let dev_caps: Vec<usize> = targets
6371 .iter()
6372 .copied()
6373 .filter(|&t| {
6374 st.dev_set.get(t).copied().unwrap_or(false)
6375 && !st.partial_set.get(t).copied().unwrap_or(false)
6376 })
6377 .collect();
6378 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
6379 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
6380 return None;
6381 }
6382 for t in 0..accepted {
6383 let tip = t + 1 == accepted;
6384 for (slot, &tl) in targets.iter().enumerate() {
6385 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
6386 let lo = (di * b + t) * hc * dim;
6387 crate::dsv4::dspark_capture(
6388 &caps_all[lo..lo + hc * dim],
6389 &cfg,
6390 slot,
6391 &mut ds.main_hidden,
6392 );
6393 } else if tip
6394 && crate::dsv4::dspark_peek_slot(slot, dim, {
6395 let lo = slot * dim;
6396 &mut ds.main_hidden[lo..lo + dim]
6397 })
6398 {
6399 } else {
6404 crate::dsv4::dspark_capture(
6408 &walked[t * hc * dim..(t + 1) * hc * dim],
6409 &cfg,
6410 slot,
6411 &mut ds.main_hidden,
6412 );
6413 }
6414 }
6415 crate::dsv4::dspark_ring_append(
6416 g,
6417 &self.dsv4_mtp,
6418 &cfg,
6419 ds,
6420 next_pos + t,
6421 self.pool.as_deref(),
6422 );
6423 }
6424 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
6425 self.graph_logits = Some(row);
6426 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
6431 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
6432 crate::dsv4::pick_tally_arm();
6433 }
6434 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
6435 eprintln!(
6436 "spec_step total {:.1} мс (k={accepted})",
6437 t_all.elapsed().as_secs_f64() * 1e3
6438 );
6439 }
6440 Some((fed[1..accepted].to_vec(), next_pos + accepted))
6441 }
6442
6443 fn dspark_probe(&mut self, position: usize, token_id: u32) {
6444 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
6445 return;
6446 }
6447 let trunk_now = crate::dsv4::pick_tally_take();
6449 crate::dsv4::trunk_freq_note(&trunk_now);
6450 if !trunk_now.is_empty() {
6451 self.dspark_trunk_picks.push(trunk_now);
6452 let keep = crate::dsv4::dspark_block();
6453 if self.dspark_trunk_picks.len() > keep {
6454 self.dspark_trunk_picks.remove(0);
6455 }
6456 }
6457 for p in std::mem::take(&mut self.dspark_pending) {
6460 let Some(i) = position.checked_sub(p.0 + 1) else {
6461 continue;
6462 };
6463 let mut p = p;
6464 if i < p.1.len() {
6465 if p.2 && p.1[i] == token_id {
6466 p.3 = i + 1;
6467 } else {
6468 p.2 = false;
6469 }
6470 if i + 1 < p.1.len() {
6471 self.dspark_pending.push(p);
6472 continue;
6473 }
6474 }
6475 self.dspark_hist.push(p.3);
6476 self.dspark_real.push(token_id);
6477 }
6478 let Some(b) = &mut self.dsv4 else { return };
6479 let (g, layers, cfg) = (&b.0, &b.1, b.2);
6480 let n_layers = layers.len();
6481 if self.dspark.is_none() {
6482 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
6483 if t.is_empty() {
6484 return;
6485 }
6486 eprintln!(
6487 "DSpark: захват со слоёв {t:?}, блок {}",
6488 crate::dsv4::dspark_block()
6489 );
6490 crate::dsv4::dspark_arm(&t, cfg.dim);
6491 self.dspark = Some(crate::dsv4::DsparkState::new(
6492 self.dsv4_mtp.len(),
6493 &cfg,
6494 t.len(),
6495 ));
6496 }
6497 let ds = self.dspark.as_mut().unwrap();
6498 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
6499 return; }
6501 let mut conf = Vec::new();
6502 crate::dsv4::pick_tally_arm();
6503 let draft_started = std::time::Instant::now();
6508 #[cfg(feature = "gpu")]
6509 let gpu_draft = crate::dsv4::dspark_gpu_on();
6510 #[cfg(not(feature = "gpu"))]
6511 let gpu_draft = false;
6512 let props = if gpu_draft {
6513 #[cfg(feature = "gpu")]
6514 {
6515 let kv_id = b.3.kv_id;
6516 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
6517 Some(pk) => crate::dsv4::dspark_draft_gpu(
6518 g,
6519 &self.dsv4_mtp,
6520 &cfg,
6521 ds,
6522 pk,
6523 kv_id,
6524 token_id,
6525 position,
6526 self.pool.as_deref(),
6527 &mut conf,
6528 ),
6529 None => Vec::new(),
6530 }
6531 }
6532 #[cfg(not(feature = "gpu"))]
6533 Vec::new()
6534 } else {
6535 crate::gpu::cpu_scope(|| {
6536 crate::dsv4::dspark_draft(
6537 g,
6538 &self.dsv4_mtp,
6539 &cfg,
6540 ds,
6541 token_id,
6542 position,
6543 self.pool.as_deref(),
6544 &mut conf,
6545 )
6546 })
6547 };
6548 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
6549 let draft_picks = crate::dsv4::pick_tally_take();
6550 crate::dsv4::dspark_freq_note(&draft_picks);
6551 crate::dsv4::pick_tally_arm();
6554 if !props.is_empty() {
6555 let (tu, tt) = {
6559 let flat: Vec<(usize, Vec<usize>)> = self
6560 .dspark_trunk_picks
6561 .iter()
6562 .flat_map(|v| v.iter().cloned())
6563 .collect();
6564 let mut per: std::collections::HashMap<usize, Vec<usize>> =
6566 std::collections::HashMap::new();
6567 for (li, picks) in flat {
6568 per.entry(li).or_default().extend(picks);
6569 }
6570 let n = per.len().max(1);
6571 let mut u = 0usize;
6572 let mut t = 0usize;
6573 for (_, v) in per {
6574 t += v.len();
6575 u += v.iter().collect::<std::collections::HashSet<_>>().len();
6576 }
6577 (u / n, t / n)
6578 };
6579 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
6580 self.dspark_exp.push((tu, tt, du, dt));
6581 self.dspark_pending.push((position, props, true, 0));
6582 }
6583 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
6584 let n = self.dspark_hist.len() as f32;
6585 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
6586 let block = crate::dsv4::dspark_block();
6587 let mut at = vec![0usize; block + 1];
6588 for &k in &self.dspark_hist {
6589 at[k] += 1;
6590 }
6591 let mut surv = Vec::with_capacity(block);
6593 for i in 1..=block {
6594 let k = at[i..].iter().sum::<usize>() as f32 / n;
6595 surv.push(format!("{k:.2}"));
6596 }
6597 let distinct = self
6598 .dspark_real
6599 .iter()
6600 .collect::<std::collections::HashSet<_>>()
6601 .len();
6602 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
6603 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
6604 });
6605 let m = self.dspark_exp.len().max(1);
6606 eprintln!(
6607 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
6608 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
6609 self.dspark_hist.len(),
6610 mean + 1.0,
6611 surv.join(" ")
6612 );
6613 eprintln!(
6614 "DSpark: разных токенов {distinct} из {} (вырожденность), \
6615 эксперты ствол {}/{} на слой за {block} токенов, \
6616 черновик {}/{} за блок, draft {:.2} мс/блок",
6617 self.dspark_real.len(),
6618 tu / m,
6619 tt / m,
6620 du / m,
6621 dt / m,
6622 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
6623 );
6624 }
6625 }
6626
6627 fn forward_layers_upto(
6628 &mut self,
6629 hidden: &[f32],
6630 position: usize,
6631 task_mask: Option<&TaskMask>,
6632 upto: Option<usize>,
6633 ) -> Vec<f32> {
6634 if let Some(plan) = self.gpu_plan.clone() {
6640 if upto.is_none() && plan.len() > 1 {
6641 let mut h = hidden.to_vec();
6642 for &(dev, from, upto_incl) in plan.iter() {
6643 h = crate::gpu::with_device(dev, || {
6644 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
6645 });
6646 }
6647 return h;
6648 }
6649 }
6650 self.forward_layers_span(hidden, position, task_mask, 0, upto)
6651 }
6652
6653 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
6658 self.set_gpu_plan_at(devices, None)
6659 }
6660
6661 pub fn set_gpu_plan_at(
6665 &mut self,
6666 devices: Option<&[usize]>,
6667 at: Option<usize>,
6668 ) -> Result<(), String> {
6669 let Some(devs) = devices.filter(|d| d.len() > 1) else {
6670 self.gpu_plan = None;
6671 return Ok(());
6672 };
6673 self.split_supported()?;
6674 let n = self.num_layers;
6675 if devs.len() > n {
6676 return Err(format!("{} devices for {n} layers", devs.len()));
6677 }
6678 if let Some(k) = at {
6679 if k == 0 || k >= n {
6680 return Err(format!("split at {k}: the model has {n} layers"));
6681 }
6682 if devs.len() == 2 {
6683 self.gpu_plan = Some(std::sync::Arc::new(vec![
6684 (devs[0], 0, k - 1),
6685 (devs[1], k, n - 1),
6686 ]));
6687 return Ok(());
6688 }
6689 return Err(format!(
6690 "an explicit split point takes exactly 2 devices, got {}",
6691 devs.len()
6692 ));
6693 }
6694 let per = n.div_ceil(devs.len());
6695 let mut plan = Vec::with_capacity(devs.len());
6696 let mut from = 0usize;
6697 for &d in devs {
6698 if from >= n {
6699 break;
6700 }
6701 let upto = (from + per - 1).min(n - 1);
6702 plan.push((d, from, upto));
6703 from = upto + 1;
6704 }
6705 self.gpu_plan = Some(std::sync::Arc::new(plan));
6706 Ok(())
6707 }
6708
6709 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
6711 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
6712 }
6713
6714 fn forward_layers_span(
6720 &mut self,
6721 hidden: &[f32],
6722 position: usize,
6723 task_mask: Option<&TaskMask>,
6724 from: usize,
6725 upto: Option<usize>,
6726 ) -> Vec<f32> {
6727 debug_assert!(from == 0 || (self.dsv4.is_none() && self.g3n.is_none()));
6728 if let Some(b) = &mut self.dsv4 {
6734 let _ = (task_mask, upto);
6735 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
6736 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
6737 st.pos = position;
6738 let mut logits = Vec::new();
6739 crate::dsv4::forward_token(
6740 g,
6741 layers,
6742 &cfg,
6743 st,
6744 token_id,
6745 &self.inv_freq,
6746 self.pool.as_deref(),
6747 &mut logits,
6748 );
6749 self.graph_logits = Some(logits);
6750 self.dspark_probe(position, token_id);
6751 return vec![0.0; self.hidden_size];
6754 }
6755 if let Some(b) = &self.g3n {
6758 let _ = (task_mask, upto);
6759 return crate::g3n::g3n_forward(
6760 &b.0,
6761 &b.1,
6762 hidden,
6763 position,
6764 &mut self.kv_cache.layers,
6765 self.num_heads,
6766 self.num_kv_heads,
6767 self.head_dim,
6768 self.pool.as_deref(),
6769 );
6770 }
6771 let mut h = hidden.to_vec();
6772 let (nh, _nkv, _hd, hs, _rd, eps) = (
6775 self.num_heads,
6776 self.num_kv_heads,
6777 self.head_dim,
6778 self.hidden_size,
6779 self.rotary_dim,
6780 self.rms_eps,
6781 );
6782 let pool = self.pool.clone();
6783 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
6795 let graph_on = match graph_env.as_deref() {
6796 Some("0") => false,
6797 Some("prefill") => false, Some(_) => true,
6799 None => crate::gpu::wgpu_graph_default(),
6805 };
6806 let graph_trusted =
6807 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
6808 let race_eligible = graph_on
6809 && upto.is_none()
6810 && task_mask.is_none()
6811 && from == 0
6812 && !crate::gpu::graph_unsupported();
6813 let mut tail_start = 0usize;
6814 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
6815 let t_graph = std::time::Instant::now();
6816 let mut lg = Vec::new();
6817 let mut gl = 0usize;
6818 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
6819 if built.is_none() && !self.o1_active() && self.attn_softcap == 0.0 {
6824 crate::gpu::graph_mark_unsupported();
6825 }
6826 graph_note(built.is_some());
6827 if let Some(hh) = built {
6828 let dur = t_graph.elapsed();
6829 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6830 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
6831 }
6832 if gl > 0 && gl < self.num_layers {
6833 h = hh;
6839 tail_start = gl;
6840 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
6841 if !graph_trusted {
6842 crate::gpu::graph_race_record(true, dur);
6843 }
6844 if !lg.is_empty() {
6845 lg.resize(self.vocab_size, 0.0);
6848 if let Some(c) = self.final_softcap {
6849 for l in lg.iter_mut() {
6850 *l = c * (*l / c).tanh();
6851 }
6852 }
6853 self.graph_logits = Some(lg);
6854 }
6855 return hh;
6856 }
6857 }
6863 }
6864 let span = from > 0 || upto.is_some();
6888 if span && graph_on && task_mask.is_none() && graph_trusted {
6889 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
6890 let mut lg = Vec::new();
6891 let mut gl = 0usize;
6892 let span_res =
6893 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
6894 graph_note(span_res.is_some() && gl == upto_excl - from);
6895 if std::env::var("CMF_GPU_DEBUG").is_ok() {
6896 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
6900 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
6901 eprintln!(
6902 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
6903 upto_excl - from,
6904 span_res.is_some()
6905 );
6906 }
6907 }
6908 if let Some(hh) = span_res {
6909 if gl == upto_excl - from {
6910 if !lg.is_empty() {
6911 lg.resize(self.vocab_size, 0.0);
6912 if let Some(c) = self.final_softcap {
6913 for l in lg.iter_mut() {
6914 *l = c * (*l / c).tanh();
6915 }
6916 }
6917 self.graph_logits = Some(lg);
6918 }
6919 crate::gpu::set_layer(-1);
6920 return hh;
6921 }
6922 h = hh;
6924 tail_start = from + gl;
6925 }
6926 }
6927 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
6928
6929 #[cfg(target_os = "macos")]
6930 let mut gpu_skip_until = 0usize;
6931 for li in tail_start.max(from)..self.num_layers {
6932 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
6934 if li > u {
6935 break;
6936 }
6937 }
6938 if let Some(mask) = task_mask {
6939 if !mask.layer_alive(li) {
6940 continue; }
6942 }
6943 #[cfg(target_os = "macos")]
6947 {
6948 if li < gpu_skip_until {
6949 continue;
6950 }
6951 if task_mask.is_none() {
6952 let end = self.q1_graph_gpu(li, upto, position, &mut h);
6953 if end > li {
6954 gpu_skip_until = end;
6955 if self.is_loop_end(end - 1) && end < self.num_layers {
6958 h = inference::rms_norm(
6959 &h,
6960 &self.weights.final_norm,
6961 self.rms_eps,
6962 self.norm_style,
6963 );
6964 }
6965 continue;
6966 }
6967 }
6968 }
6969
6970 let lw = &self.weights.layers[self.phys_layer(li)];
6971 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
6972 if tp.parse::<usize>().ok() == Some(position) {
6973 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
6974 eprintln!(
6975 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
6976 h[0], h[1]
6977 );
6978 }
6979 }
6980 inference::rms_norm_into(
6983 &h,
6984 &lw.input_norm,
6985 self.rms_eps,
6986 self.norm_style,
6987 &mut self.ws.n1,
6988 );
6989
6990 let attn_out = match &lw.attn {
6991 AttnKind::Mla(w) => {
6992 let inv_freq_l = self.layer_inv_freq(li);
6993 let rs = self.layer_rope_scale(li);
6994 let eps = self.rms_eps;
6995 let pool = self.pool.clone();
6996 mla_attention(
6997 w,
6998 &self.ws.n1,
6999 &mut self.kv_cache.layers[li],
7000 position,
7001 &inv_freq_l,
7002 rs,
7003 eps,
7004 pool.as_deref(),
7005 )
7006 }
7007 AttnKind::Linear(w) => {
7008 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
7009 vmf_phase_forward(
7010 &self.ws.n1,
7011 w,
7012 &cfg,
7013 &mut self.kv_cache.layers[li].linear_state,
7014 self.pool.as_deref(),
7015 )
7016 }
7017 AttnKind::Kda(w) => {
7018 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
7019 crate::linear_core::kda_forward(
7020 &self.ws.n1,
7021 w,
7022 &cfg,
7023 &mut self.kv_cache.layers[li].linear_state,
7024 self.pool.as_deref(),
7025 )
7026 }
7027 AttnKind::LinearGdn(w) => {
7028 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7029 gdn_forward(
7030 &self.ws.n1,
7031 w,
7032 &cfg,
7033 &mut self.kv_cache.layers[li].linear_state,
7034 self.pool.as_deref(),
7035 )
7036 }
7037 AttnKind::ShortConv(w) => {
7038 let cfg = self
7039 .short_conv_cfg
7040 .expect("short-conv layer without short_conv_cfg");
7041 short_conv_forward(
7042 &self.ws.n1,
7043 w,
7044 &cfg,
7045 &mut self.kv_cache.layers[li].linear_state,
7046 self.pool.as_deref(),
7047 )
7048 }
7049 AttnKind::Full {
7050 wq,
7051 wk,
7052 wv,
7053 wo,
7054 q_norm,
7055 k_norm,
7056 output_gate,
7057 softplus_gate,
7058 bias,
7059 } if self.kv_cache.layers[li].o1_sealed() => {
7060 let inv_freq_l = self.layer_inv_freq(li);
7063 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7064 let cfg = QwenAttnCfg {
7065 num_heads: self.layer_num_heads(li),
7066 num_kv_heads: nkv_l,
7067 head_dim: hd_l,
7068 hidden_size: hs,
7069 position,
7070 inv_freq: &inv_freq_l,
7071 rotary_dim: rd_l,
7072 scale: self.attn_scale,
7073 softcap: self.attn_softcap,
7074 window: None,
7075 v_norm: self.attn_v_norm,
7076 q_norm: q_norm.as_deref(),
7077 k_norm: k_norm.as_deref(),
7078 output_gate: *output_gate,
7079 softplus_gate: softplus_gate
7080 .as_ref()
7081 .map(|(gate, per_head)| (gate, *per_head)),
7082 rope_scale: self.layer_rope_scale(li),
7083 bias: bias
7084 .as_ref()
7085 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7086 rms_eps: eps,
7087 norm_style: self.norm_style,
7088 pool: pool.as_deref(),
7089 };
7090 attention::qwen_attention_nystrom(
7091 &self.ws.n1,
7092 wq,
7093 wk,
7094 wv,
7095 wo,
7096 &mut self.kv_cache.layers[li],
7097 &cfg,
7098 )
7099 }
7100 AttnKind::Full {
7101 wq,
7102 wk,
7103 wv,
7104 wo,
7105 q_norm,
7106 k_norm,
7107 output_gate,
7108 softplus_gate,
7109 bias,
7110 } => 'attn: {
7111 if graph_on
7114 && !*output_gate
7115 && softplus_gate.is_none()
7116 && self.attention_heads_per_layer.is_none()
7117 && bias.is_none()
7118 && task_mask.is_none()
7119 {
7120 let inv_freq_l = self.layer_inv_freq(li);
7121 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7122 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7123 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
7124 wq.mapped_q1(),
7125 wk.mapped_q1(),
7126 wv.mapped_q1(),
7127 wo.mapped_q1(),
7128 ) {
7129 let gm = gm.clone();
7130 let mut out = vec![0f32; hs];
7131 let cache = &self.kv_cache.layers[li];
7132 if crate::gpu::attn_dropin(
7133 &gm,
7134 self.graph_kv_id,
7135 li,
7136 &self.ws.n1,
7137 qi,
7138 ki,
7139 vi,
7140 oi,
7141 q_norm.as_deref(),
7142 k_norm.as_deref(),
7143 &inv_freq_l,
7144 nh,
7145 nkv_l,
7146 hd_l,
7147 rd_l,
7148 hs,
7149 position,
7150 self.kv_cache.max_seq_len,
7151 gemma,
7152 eps as f32,
7153 cache.k_heads(),
7154 cache.v_heads(),
7155 &mut out,
7156 ) {
7157 break 'attn out;
7158 }
7159 }
7160 }
7161 let masked = task_mask
7162 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
7163 .unwrap_or(false);
7164 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
7165 match (masked, f32_view) {
7166 (true, (Some(q), Some(k), Some(v), Some(o))) => {
7169 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
7170 attention::multi_head_attention(
7171 &self.ws.n1,
7172 q,
7173 k,
7174 v,
7175 o,
7176 &mut self.kv_cache.layers[li],
7177 self.num_heads,
7178 self.num_kv_heads,
7179 self.head_dim,
7180 self.hidden_size,
7181 position,
7182 &active_heads,
7183 &self.inv_freq,
7184 )
7185 }
7186 (masked, _) => {
7187 if masked {
7188 tracing::warn!(
7189 "layer {li}: head mask on quantized weights not \
7190 supported yet — executing dense"
7191 );
7192 }
7193 let inv_freq_l = self.layer_inv_freq(li);
7194 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7195 let cfg = QwenAttnCfg {
7196 num_heads: self.layer_num_heads(li),
7197 num_kv_heads: nkv_l,
7198 head_dim: hd_l,
7199 hidden_size: hs,
7200 position,
7201 inv_freq: &inv_freq_l,
7202 rotary_dim: rd_l,
7203 scale: self.attn_scale,
7204 softcap: self.attn_softcap,
7205 window: self.layer_window(li),
7206 v_norm: self.attn_v_norm,
7207 q_norm: q_norm.as_deref(),
7208 k_norm: k_norm.as_deref(),
7209 output_gate: *output_gate,
7210 softplus_gate: softplus_gate
7211 .as_ref()
7212 .map(|(gate, per_head)| (gate, *per_head)),
7213 rope_scale: self.layer_rope_scale(li),
7214 bias: bias
7215 .as_ref()
7216 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7217 rms_eps: eps,
7218 norm_style: self.norm_style,
7219 pool: pool.as_deref(),
7220 };
7221 attention::qwen_attention(
7222 &self.ws.n1,
7223 wq,
7224 wk,
7225 wv,
7226 wo,
7227 &mut self.kv_cache.layers[li],
7228 &cfg,
7229 )
7230 }
7231 }
7232 }
7233 };
7234 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
7237 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
7238 None => attn_out,
7239 };
7240 let lw = &self.weights.layers[self.phys_layer(li)];
7241 inference::add_rmsnorm_fused_into(
7242 &mut h,
7243 &attn_out,
7244 &lw.post_norm,
7245 self.rms_eps,
7246 self.norm_style,
7247 &mut self.ws.p1,
7248 );
7249 let mut attn_out = attn_out;
7250 attention::recycle_buf(&mut attn_out);
7251 let post_normed = &self.ws.p1;
7252
7253 let ffn_masked = task_mask
7254 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
7255 .unwrap_or(false);
7256 let ffn_out = match (ffn_masked, &lw.ffn) {
7268 (true, FfnKind::Dense(d)) => {
7269 let tm = task_mask.unwrap();
7270 let alive = tm.ffn_active_count(li);
7271 let deep = alive * 2 <= self.intermediate_size;
7272 if deep && d.down_proj.sparse_col_ok() {
7273 let active = tm.ffn_active_indices(li);
7274 sparse_ffn_quant(
7275 d,
7276 post_normed,
7277 &active,
7278 self.hidden_size,
7279 self.pool.as_deref(),
7280 )
7281 } else if deep
7282 && let (Some(g), Some(u), Some(dn)) = (
7283 d.gate_proj.as_f32(),
7284 d.up_proj.as_f32(),
7285 d.down_proj.as_f32(),
7286 )
7287 {
7288 let active = tm.ffn_active_indices(li);
7289 inference::sparse_ffn_forward(
7290 post_normed,
7291 g,
7292 u,
7293 dn,
7294 self.hidden_size,
7295 self.intermediate_size,
7296 &active,
7297 self.pool.as_deref(),
7298 )
7299 } else {
7300 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
7301 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
7302 }
7303 }
7304 (true, FfnKind::Moe(m)) => {
7305 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
7309 ffn_forward(
7310 &lw.ffn,
7311 post_normed,
7312 self.pool.as_deref(),
7313 allowed.as_deref(),
7314 )
7315 }
7316 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
7317 dm,
7318 post_normed,
7319 &h,
7320 self.rms_eps,
7321 self.norm_style,
7322 self.pool.as_deref(),
7323 ),
7324 (false, _) => match &lw.ffn {
7325 FfnKind::DenseMoe(dm) => dense_moe_ffn(
7326 dm,
7327 post_normed,
7328 &h,
7329 self.rms_eps,
7330 self.norm_style,
7331 self.pool.as_deref(),
7332 ),
7333 _ => {
7334 let allowed = match (&lw.ffn, task_mask) {
7335 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
7336 _ => None,
7337 };
7338 ffn_forward(
7339 &lw.ffn,
7340 post_normed,
7341 self.pool.as_deref(),
7342 allowed.as_deref(),
7343 )
7344 }
7345 },
7346 };
7347 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
7348 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
7349 None => ffn_out,
7350 };
7351 for (i, &f) in ffn_out.iter().enumerate() {
7352 h[i] += f;
7353 }
7354 let mut ffn_out = ffn_out;
7355 attention::recycle_buf(&mut ffn_out);
7356
7357 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
7359 for v in h.iter_mut() {
7360 *v *= sc;
7361 }
7362 }
7363
7364 if self.is_loop_end(li) && li + 1 < self.num_layers {
7367 h = inference::rms_norm(
7368 &h,
7369 &self.weights.final_norm,
7370 self.rms_eps,
7371 self.norm_style,
7372 );
7373 }
7374
7375 if self.dyn_phi_layer == Some(li) {
7379 self.update_dyn_phi(&h);
7380 }
7381 }
7382 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
7384 crate::gpu::graph_race_record(false, t.elapsed());
7385 }
7386
7387 h
7388 }
7389
7390 fn update_dyn_phi(&mut self, h: &[f32]) {
7393 const A: f32 = 0.2;
7394 if self.dyn_phi_ema.len() != h.len() {
7395 self.dyn_phi_ema = vec![0.0; h.len()];
7396 self.dyn_phi_seen = 0;
7397 }
7398 if self.dyn_phi_seen == 0 {
7399 self.dyn_phi_ema.copy_from_slice(h);
7400 } else {
7401 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
7402 *e = (1.0 - A) * *e + A * v;
7403 }
7404 }
7405 self.dyn_phi_seen += 1;
7406 }
7407
7408 pub fn dyn_phi(&self) -> &[f32] {
7410 &self.dyn_phi_ema
7411 }
7412
7413 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
7415 self.dyn_phi_layer = layer;
7416 self.dyn_phi_ema.clear();
7417 self.dyn_phi_seen = 0;
7418 }
7419
7420 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
7422 let Some(model) = &self.model else {
7423 return Vec::new();
7424 };
7425 model
7426 .header
7427 .skills
7428 .iter()
7429 .enumerate()
7430 .filter_map(|(i, sk)| {
7431 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
7432 let sel = sk.selection.as_ref()?;
7433 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
7434 })
7435 .collect()
7436 }
7437
7438 pub fn active_skill(&self) -> Option<usize> {
7440 self.dyn_active
7441 }
7442
7443 pub fn enable_dynamic_routing(&mut self) -> usize {
7448 use crate::swarm::{DynRouter, RoutableSkill};
7449 let Some(model) = self.model.clone() else {
7450 return 0;
7451 };
7452 if self.dyn_blend_loaded {
7455 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
7456 return 0;
7457 }
7458 if let Some(a) = self.dyn_active {
7462 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
7463 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
7464 return 0;
7465 }
7466 }
7467 let hidden = self.hidden_size;
7468 let mut skills = Vec::new();
7469 for (idx, id, _phi) in self.dynamic_skills() {
7470 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
7471 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
7472 skills.push(rs);
7473 }
7474 }
7475 }
7476 if skills.is_empty() {
7477 return 0;
7478 }
7479 let phi = skills[0].phi_layer;
7481 if skills.iter().any(|s| s.phi_layer != phi) {
7482 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
7483 }
7484 let n = skills.len();
7485 self.set_dyn_phi_layer(Some(phi));
7486 self.dyn_router = Some(DynRouter::new(skills));
7487 n
7488 }
7489
7490 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
7492 self.dyn_router
7493 .as_ref()
7494 .map(|r| r.switches.clone())
7495 .unwrap_or_default()
7496 }
7497
7498 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
7501 let rows = self.weights.lm_head.rows();
7502 let mut logits = attention::take_buf(rows.min(self.vocab_size));
7503 self.weights
7504 .lm_head
7505 .matvec(hidden, &mut logits, self.pool.as_deref());
7506 logits.resize(self.vocab_size, 0.0);
7507 if let Some(m) = self.logit_multiplier {
7508 for l in logits.iter_mut() {
7509 *l *= m;
7510 }
7511 }
7512 if let Some(c) = self.final_softcap {
7513 for l in logits.iter_mut() {
7514 *l = c * (*l / c).tanh();
7515 }
7516 }
7517 logits
7518 }
7519
7520 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
7525 self.kv_cache.clear();
7526 self.kv_history.clear();
7527 let mut hidden = vec![0.0f32; self.hidden_size];
7528 for (pos, &id) in ids.iter().enumerate() {
7529 let emb = self.embed_single(id);
7530 hidden = self.forward_layers(&emb, pos, task_mask);
7531 }
7532 inference::rms_norm_into(
7533 &hidden,
7534 &self.weights.final_norm,
7535 self.rms_eps,
7536 self.norm_style,
7537 &mut self.ws.n1,
7538 );
7539 self.lm_head_forward(&self.ws.n1)
7540 }
7541}
7542
7543pub fn create_test_pipeline(
7545 hidden_size: usize,
7546 intermediate_size: usize,
7547 num_heads: usize,
7548 num_kv_heads: usize,
7549 head_dim: usize,
7550 num_layers: usize,
7551 vocab_size: usize,
7552) -> Pipeline {
7553 let synth = |n: usize, salt: usize| -> Vec<f32> {
7556 (0..n)
7557 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
7558 .collect()
7559 };
7560 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
7561 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
7562 };
7563 let layer_weights: Vec<LayerWeights> = (0..num_layers)
7564 .map(|li| LayerWeights {
7565 input_norm: vec![1.0; hidden_size],
7566 post_norm: vec![1.0; hidden_size],
7567 attn_out_norm: None,
7568 ffn_out_norm: None,
7569 layer_scale: None,
7570 ffn: FfnKind::Dense(DenseFfn {
7571 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
7572 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
7573 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
7574 act: Act::Silu,
7575 }),
7576 attn: AttnKind::Full {
7577 bias: None,
7578 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
7579 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
7580 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
7581 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
7582 q_norm: None,
7583 k_norm: None,
7584 output_gate: false,
7585 softplus_gate: None,
7586 },
7587 })
7588 .collect();
7589
7590 Pipeline::new(
7591 Tokenizer::byte_level(),
7592 PipelineWeights {
7593 embed_tokens: qt(vocab_size, hidden_size, 100),
7594 layers: layer_weights,
7595 lm_head: qt(vocab_size, hidden_size, 200),
7596 final_norm: vec![1.0; hidden_size],
7597 },
7598 hidden_size,
7599 intermediate_size,
7600 num_heads,
7601 num_kv_heads,
7602 head_dim,
7603 num_layers,
7604 num_layers, false, vocab_size,
7607 1e-6,
7608 10_000.0,
7609 NormStyle::Qwen,
7610 4096,
7611 SamplerConfig {
7612 seed: Some(42),
7613 ..Default::default()
7614 },
7615 )
7616}
7617
7618#[inline]
7623fn mask_bit(row: &[u8], j: usize) -> bool {
7624 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
7625}
7626
7627fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
7633 for r in 0..rows {
7634 let base = r * inter;
7635 for (bi, &byte) in row.iter().enumerate() {
7636 if byte == 0xFF {
7637 continue;
7638 }
7639 let j0 = bi * 8;
7640 for bit in 0..8 {
7641 let j = j0 + bit;
7642 if j < inter && byte & (1 << bit) == 0 {
7643 g[base + j] = 0.0;
7644 }
7645 }
7646 }
7647 }
7648}
7649
7650fn dense_ffn_batch(
7651 d: &DenseFfn,
7652 xs: &[f32],
7653 b: usize,
7654 pool: Option<&Pool>,
7655 mask_row: Option<&[u8]>,
7656) -> Vec<f32> {
7657 let inter = d.gate_proj.rows();
7658 let hidden = d.down_proj.rows();
7659 if mask_row.is_none()
7667 && d.act == Act::Silu
7668 && b >= 32
7669 && crate::gpu::enabled_here()
7670 && !crate::gpu::mm_killed()
7671 {
7672 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
7673 d.gate_proj.mapped_q4t(),
7674 d.up_proj.mapped_q4t(),
7675 d.down_proj.mapped_q4t(),
7676 ) {
7677 let mut out = vec![0.0f32; b * hidden];
7678 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
7679 return out;
7680 }
7681 }
7682 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
7687 d.gate_proj.mapped_q4tp(),
7688 d.up_proj.mapped_q4tp(),
7689 d.down_proj.mapped_q4tp(),
7690 ) {
7691 let mut out = vec![0.0f32; b * hidden];
7692 if crate::gpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
7693 return out;
7694 }
7695 }
7696 }
7697 let mut g = vec![0.0f32; b * inter];
7698 d.gate_proj.matmat(xs, b, &mut g, pool);
7699 let mut u = vec![0.0f32; b * inter];
7700 d.up_proj.matmat(xs, b, &mut u, pool);
7701 for i in 0..b * inter {
7702 g[i] = d.act.combine(g[i], u[i]);
7703 }
7704 if let Some(row) = mask_row {
7705 zero_masked_cols(&mut g, b, inter, row);
7706 }
7707 let mut out = vec![0.0f32; b * hidden];
7708 d.down_proj.matmat(&g, b, &mut out, pool);
7709 out
7710}
7711
7712fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
7717 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7718 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
7719 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
7720 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
7721 if (!on && !dump) || b == 0 {
7722 return;
7723 }
7724 let hidden = xs.len() / b;
7725 if on {
7726 let mut acc = m.act_sq.borrow_mut();
7727 if acc.len() < hidden {
7728 acc.resize(hidden, 0.0);
7729 }
7730 for t in 0..b {
7731 let row = &xs[t * hidden..(t + 1) * hidden];
7732 for (a, &v) in acc.iter_mut().zip(row) {
7733 *a += (v as f64) * (v as f64);
7734 }
7735 }
7736 }
7737 if dump {
7738 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
7741 .ok()
7742 .and_then(|v| v.parse().ok())
7743 .unwrap_or(4096);
7744 let mut rows = m.act_rows.borrow_mut();
7745 if rows.len() < cap * hidden {
7746 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
7747 rows.extend_from_slice(&xs[..take * hidden]);
7748 }
7749 }
7750}
7751
7752#[derive(Clone, Copy)]
7755struct SendVecs(*mut Vec<f32>);
7756unsafe impl Send for SendVecs {}
7757unsafe impl Sync for SendVecs {}
7758impl SendVecs {
7759 #[inline]
7760 fn at(self, i: usize) -> *mut Vec<f32> {
7761 unsafe { self.0.add(i) }
7762 }
7763}
7764
7765fn moe_ffn_batch(
7766 m: &MoeFfn,
7767 xs: &[f32],
7768 b: usize,
7769 hidden: usize,
7770 pool: Option<&Pool>,
7771 allowed: Option<&[bool]>,
7772) -> Vec<f32> {
7773 accumulate_act(m, xs, b);
7774 let ne = m.experts.len();
7775 let mut logits = vec![0.0f32; b * ne];
7776 m.router.matmat(xs, b, &mut logits, pool);
7777
7778 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
7781 {
7782 let mut st = m.stats.borrow_mut();
7783 if st.len() < ne {
7784 st.resize(ne, 0);
7785 }
7786 for bi in 0..b {
7787 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
7788 for &e in &idx {
7789 st[e] += 1;
7790 assign[e].push((bi, p[e] / wsum));
7791 }
7792 }
7793 }
7794
7795 let mut out = vec![0.0f32; b * hidden];
7796 let cols = m.experts[0].gate_proj.cols();
7797 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
7798 let sb = list.len();
7799 let mut sub = vec![0.0f32; sb * cols];
7800 for (k, &(bi, _)) in list.iter().enumerate() {
7801 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
7802 }
7803 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
7804 for (k, &(bi, w)) in list.iter().enumerate() {
7805 for i in 0..hidden {
7806 out[bi * hidden + i] += w * eo[k * hidden + i];
7807 }
7808 }
7809 };
7810 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
7816 if pool.is_some() && active.len() >= 8 {
7817 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
7818 {
7819 let panel_ptr = SendVecs(panels.as_mut_ptr());
7820 let experts = &m.experts;
7823 let (active_r, assign_r) = (&active, &assign);
7824 let run = |start: usize, end: usize| {
7825 for ai in start..end {
7826 let e = active_r[ai];
7827 let list = &assign_r[e];
7828 let sb = list.len();
7829 let mut sub = vec![0.0f32; sb * cols];
7830 for (k, &(bi, _)) in list.iter().enumerate() {
7831 sub[k * cols..(k + 1) * cols]
7832 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
7833 }
7834 unsafe {
7836 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
7837 }
7838 }
7839 };
7840 match pool {
7841 Some(p) => p.run_rows(active.len(), &run),
7842 None => run(0, active.len()),
7843 }
7844 }
7845 for (ai, &e) in active.iter().enumerate() {
7846 for (k, &(bi, w)) in assign[e].iter().enumerate() {
7847 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
7848 for i in 0..hidden {
7849 out[bi * hidden + i] += w * eo[i];
7850 }
7851 }
7852 }
7853 } else {
7854 for &e in &active {
7855 run_expert(&m.experts[e], &assign[e], &mut out);
7856 }
7857 }
7858 if let Some((se, gate)) = &m.shared {
7859 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
7860 let mut gl = vec![0.0f32; b];
7861 gate.matmat(xs, b, &mut gl, pool);
7862 (0..b)
7863 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
7864 .collect()
7865 } else {
7866 (0..b).map(|bi| (bi, 1.0)).collect()
7867 };
7868 run_expert(se, &all, &mut out);
7869 }
7870 out
7871}
7872
7873thread_local! {
7874 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
7878 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
7879}
7880
7881fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
7883 if crate::gpu::enabled_here()
7894 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
7895 {
7896 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
7897 crate::gpu::ProbeArm::Gpu
7898 } else {
7899 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
7900 };
7901 match arm {
7902 crate::gpu::ProbeArm::Gpu => {
7903 let t0 = std::time::Instant::now();
7904 if let Some(out) = dense_ffn_gpu(d, x, pool) {
7905 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
7906 return out;
7907 }
7908 }
7909 crate::gpu::ProbeArm::CpuTimed => {
7910 let t0 = std::time::Instant::now();
7911 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
7912 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
7913 return out;
7914 }
7915 crate::gpu::ProbeArm::Cpu => {
7916 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
7917 }
7918 }
7919 }
7920 dense_ffn_cpu(d, x, pool)
7921}
7922
7923fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
7925 let inter = d.gate_proj.rows();
7926 FFN_SCRATCH.with(|s| {
7927 let mut s = s.borrow_mut();
7928 let [g, u, ..] = &mut *s;
7929 g.resize(inter, 0.0);
7930 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
7933 } else {
7935 u.resize(inter, 0.0);
7936 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
7938 for i in 0..inter {
7939 g[i] = d.act.combine(g[i], u[i]);
7940 }
7941 }
7942 FFN_PROBE.with(|pr| {
7945 if let Some(acc) = pr.borrow_mut().as_mut() {
7946 let li = crate::gpu::cur_layer();
7947 if li >= 0 {
7948 if let Some(row) = acc.get_mut(li as usize) {
7949 for (a, &v) in row.iter_mut().zip(g.iter()) {
7950 *a += (v as f64).abs();
7951 }
7952 }
7953 }
7954 }
7955 });
7956 let mut out = attention::take_buf(d.down_proj.rows());
7957 d.down_proj.matvec(g, &mut out, pool);
7958 out
7959 })
7960}
7961
7962thread_local! {
7963 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
7966 const { std::cell::RefCell::new(None) };
7967}
7968
7969fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
7974 let inter = d.gate_proj.rows();
7975 FFN_SCRATCH.with(|s| {
7976 let mut s = s.borrow_mut();
7977 let [g, u, ..] = &mut *s;
7978 g.resize(inter, 0.0);
7979 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
7980 } else {
7982 u.resize(inter, 0.0);
7983 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
7984 for i in 0..inter {
7985 g[i] = d.act.combine(g[i], u[i]);
7986 }
7987 }
7988 zero_masked_cols(g, 1, inter, mask_row);
7989 let mut out = attention::take_buf(d.down_proj.rows());
7990 d.down_proj.matvec(g, &mut out, pool);
7991 out
7992 })
7993}
7994
7995fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
8001 if d.act != Act::Silu {
8003 return None;
8004 }
8005 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
8008 return None;
8009 }
8010 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
8011 let mut model_ref = None;
8012 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
8013 let model = model_ref?;
8014 let hidden = jobs[0].down.1;
8015 let mut out = attention::take_buf(hidden);
8016 if crate::gpu::moe_block(&model, &jobs, &mut out) {
8017 Some(out)
8018 } else {
8019 let mut out = out;
8020 attention::recycle_buf(&mut out);
8021 None
8022 }
8023}
8024
8025#[allow(clippy::type_complexity)]
8030#[allow(clippy::type_complexity)]
8031pub(crate) fn moe_parts(
8032 t: &QTensor,
8033) -> Option<(
8034 &std::sync::Arc<cortiq_core::CmfModel>,
8035 usize,
8036 usize,
8037 usize,
8038 &[f32],
8039 &[f32],
8040 bool,
8041 bool,
8042 bool,
8043)> {
8044 match t {
8045 QTensor::Mapped {
8046 model,
8047 idx,
8048 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
8049 rows,
8050 cols,
8051 row_scale,
8052 col_field,
8053 ..
8054 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
8055 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
8056 )),
8057 QTensor::Mapped {
8059 model,
8060 idx,
8061 dtype: cortiq_core::TensorDtype::Q1,
8062 rows,
8063 cols,
8064 ..
8065 } => Some((
8066 model,
8067 *idx,
8068 *rows,
8069 *cols,
8070 &[][..],
8071 &[][..],
8072 true,
8073 false,
8074 false,
8075 )),
8076 QTensor::Mapped {
8078 model,
8079 idx,
8080 dtype: cortiq_core::TensorDtype::Q4Tiled,
8081 rows,
8082 cols,
8083 ..
8084 } => Some((
8085 model,
8086 *idx,
8087 *rows,
8088 *cols,
8089 &[][..],
8090 &[][..],
8091 false,
8092 true,
8093 false,
8094 )),
8095 QTensor::Mapped {
8097 model,
8098 idx,
8099 dtype: cortiq_core::TensorDtype::Q4TiledP,
8100 rows,
8101 cols,
8102 ..
8103 } => Some((
8104 model,
8105 *idx,
8106 *rows,
8107 *cols,
8108 &[][..],
8109 &[][..],
8110 false,
8111 true,
8112 false,
8113 )),
8114 QTensor::Mapped {
8118 model,
8119 idx,
8120 dtype: cortiq_core::TensorDtype::Q2TiledP,
8121 rows,
8122 cols,
8123 ..
8124 } => Some((
8125 model,
8126 *idx,
8127 *rows,
8128 *cols,
8129 &[][..],
8130 &[][..],
8131 false,
8132 true,
8133 true,
8134 )),
8135 _ => None,
8136 }
8137}
8138
8139#[cfg(target_os = "macos")]
8145fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
8146 if m.router_sigmoid
8147 || m.router_input_norm
8148 || m.expert_bias.is_some()
8149 || m.route_tau.is_some()
8150 || m.mask.is_some()
8151 || m.per_expert_scale.is_some()
8152 || m.experts.is_empty()
8153 || m.top_k == 0
8154 {
8155 return None;
8156 }
8157 let (sh, sg) = match &m.shared {
8160 Some((sh, Some(sg))) => (sh, sg),
8161 _ => return None,
8162 };
8163 let (rf, rr, rc) = m.router.f32_parts()?;
8164 if rr != m.experts.len() || rc != hidden {
8165 return None;
8166 }
8167 let (sf, sr, sc) = sg.f32_parts()?;
8168 if sr * sc != hidden {
8169 return None;
8170 }
8171 let inter = m.experts[0].gate_proj.rows();
8172 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
8175 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
8176 if e.act != Act::Silu
8177 || e.gate_proj.rows() != inter
8178 || e.gate_proj.cols() != hidden
8179 || e.up_proj.rows() != inter
8180 || e.up_proj.cols() != hidden
8181 || e.down_proj.rows() != hidden
8182 || e.down_proj.cols() != inter
8183 {
8184 return None;
8185 }
8186 let pick = |t: &QTensor| -> Option<usize> {
8187 if gu_q2 {
8188 t.mapped_q2tp().map(|(_, i)| i)
8189 } else {
8190 t.mapped_q4tp().map(|(_, i)| i)
8191 }
8192 };
8193 Some((
8194 pick(&e.gate_proj)?,
8195 pick(&e.up_proj)?,
8196 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
8197 ))
8198 };
8199 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
8200 let shared = trio(sh)?;
8201 Some(crate::gpu::GpuMoe {
8202 router: rf,
8203 sgate: sf,
8204 experts,
8205 shared,
8206 n_exp: m.experts.len(),
8207 top_k: m.top_k,
8208 inter,
8209 norm_topk: m.norm_topk_prob,
8210 route_scale: m.routed_scaling,
8211 gu_q2,
8212 })
8213}
8214
8215pub(crate) fn moe_push_job_parts<'a>(
8219 gate: &'a QTensor,
8220 up: &'a QTensor,
8221 down: &'a QTensor,
8222 x: &[f32],
8223 w: f32,
8224 swiglu_limit: f32,
8225 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
8226 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
8227) -> Option<()> {
8228 use crate::qtensor::prescale;
8229 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
8230 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
8231 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
8232 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
8233 return None; }
8235 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
8238 return None;
8239 }
8240 if !gq2 && dq2 {
8241 return None;
8242 }
8243 model_ref.get_or_insert_with(|| gm.clone());
8244 let dt = |cf: &[f32]| {
8245 if cf.is_empty() {
8246 cortiq_core::TensorDtype::Q8Row
8247 } else {
8248 cortiq_core::TensorDtype::Q8_2f
8249 }
8250 };
8251 jobs.push(crate::gpu::MoeJob {
8252 gate: (gi, gr, gc, grs),
8253 up: (ui, ur, uc, urs),
8254 down: (di, dr, dc, drs),
8255 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
8256 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
8257 down_col: dcf,
8258 w,
8259 q1: gq1,
8260 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
8261 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
8262 gu_q2: gq2,
8263 swiglu_limit,
8264 });
8265 Some(())
8266}
8267
8268fn moe_push_job<'a>(
8270 d: &'a DenseFfn,
8271 x: &[f32],
8272 w: f32,
8273 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
8274 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
8275) -> Option<()> {
8276 use crate::qtensor::prescale;
8277 if d.act != Act::Silu {
8278 return None; }
8280 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
8281 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
8282 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
8283 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
8284 return None; }
8286 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
8287 return None;
8288 }
8289 if !gq2 && dq2 {
8290 return None;
8291 }
8292 model_ref.get_or_insert_with(|| gm.clone());
8293 let gdt = if gcf.is_empty() {
8294 cortiq_core::TensorDtype::Q8Row
8295 } else {
8296 cortiq_core::TensorDtype::Q8_2f
8297 };
8298 let udt = if ucf.is_empty() {
8299 cortiq_core::TensorDtype::Q8Row
8300 } else {
8301 cortiq_core::TensorDtype::Q8_2f
8302 };
8303 jobs.push(crate::gpu::MoeJob {
8304 gate: (gi, gr, gc, grs),
8305 up: (ui, ur, uc, urs),
8306 down: (di, dr, dc, drs),
8307 xs_gate: prescale(x, gcf, gdt).into_owned(),
8308 xs_up: prescale(x, ucf, udt).into_owned(),
8309 down_col: dcf,
8310 w,
8311 q1: gq1,
8312 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
8313 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
8314 gu_q2: gq2,
8315 swiglu_limit: 0.0,
8316 });
8317 Some(())
8318}
8319
8320fn sparse_ffn_quant(
8327 d: &DenseFfn,
8328 x: &[f32],
8329 active: &[u16],
8330 hidden: usize,
8331 pool: Option<&Pool>,
8332) -> Vec<f32> {
8333 let n = active.len();
8334 let inter = d.gate_proj.rows();
8335 let mut act = vec![0.0f32; n];
8336 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
8339 let compute = |ai: usize| -> f32 {
8340 let idx = active[ai] as usize;
8341 if idx >= inter {
8342 return 0.0; }
8344 let mut s = if need_scratch {
8345 vec![0.0f32; hidden]
8346 } else {
8347 Vec::new()
8348 };
8349 let gate = d.gate_proj.row_dot(idx, x, &mut s);
8350 let up = d.up_proj.row_dot(idx, x, &mut s);
8351 d.act.combine(gate, up)
8352 };
8353 match pool {
8354 Some(p) if n >= 256 => {
8355 let ptr = SendMut(act.as_mut_ptr());
8356 p.run(&|widx, nw| {
8357 let chunk = n.div_ceil(nw);
8358 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
8359 for ai in s..e {
8360 unsafe { *ptr.at(ai) = compute(ai) };
8361 }
8362 });
8363 }
8364 _ => {
8365 for (ai, a) in act.iter_mut().enumerate() {
8366 *a = compute(ai);
8367 }
8368 }
8369 }
8370 let mut out = vec![0.0f32; hidden];
8372 for (ai, &idx) in active.iter().enumerate() {
8373 let w = act[ai];
8374 if w.abs() >= 1e-12 && (idx as usize) < inter {
8375 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
8376 }
8377 }
8378 out
8379}
8380
8381#[doc(hidden)]
8383pub fn sparse_ffn_quant_for_test(
8384 d: &DenseFfn,
8385 x: &[f32],
8386 active: &[u16],
8387 hidden: usize,
8388) -> Vec<f32> {
8389 sparse_ffn_quant(d, x, active, hidden, None)
8390}
8391
8392fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
8396 let deq = |t: &QTensor| -> Vec<f32> {
8397 let (rows, cols) = (t.rows(), t.cols());
8398 let mut out = vec![0.0f32; rows * cols];
8399 for r in 0..rows {
8400 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
8401 }
8402 out
8403 };
8404 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
8405}
8406
8407struct SendMut(*mut f32);
8409unsafe impl Send for SendMut {}
8410unsafe impl Sync for SendMut {}
8411impl SendMut {
8412 #[inline]
8413 #[allow(clippy::mut_from_ref)]
8416 unsafe fn at(&self, i: usize) -> &mut f32 {
8417 unsafe { &mut *self.0.add(i) }
8418 }
8419}
8420
8421fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
8431 let ne = logits.len();
8432 let p: Vec<f32> = if m.router_sigmoid {
8433 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
8434 } else {
8435 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8436 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
8437 let s: f32 = e.iter().sum();
8438 for v in &mut e {
8439 *v /= s;
8440 }
8441 e
8442 };
8443 let admit = |e: usize| {
8449 m.mask.as_ref().is_none_or(|mk| mk[e])
8450 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
8451 };
8452 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
8453 match &m.expert_bias {
8455 Some(b) => idx.sort_unstable_by(|&x, &y| {
8456 (p[y] + b[y])
8457 .partial_cmp(&(p[x] + b[x]))
8458 .unwrap()
8459 .then(x.cmp(&y))
8460 }),
8461 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
8462 }
8463 idx.truncate(m.top_k);
8464 if let Some(tau) = m.route_tau {
8468 let total: f32 = idx.iter().map(|&e| p[e]).sum();
8469 if total > 0.0 {
8470 let mut acc = 0.0f32;
8471 let mut keep = idx.len();
8472 for (i, &e) in idx.iter().enumerate() {
8473 acc += p[e];
8474 if acc >= tau * total {
8475 keep = i + 1;
8476 break;
8477 }
8478 }
8479 idx.truncate(keep);
8480 }
8481 }
8482 let wsum: f32 = if m.norm_topk_prob {
8483 let s: f32 = idx.iter().map(|&e| p[e]).sum();
8484 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
8487 } else {
8488 1.0 / m.routed_scaling
8489 };
8490 (idx, p, wsum)
8491}
8492
8493fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
8496 accumulate_act(m, x, 1);
8497 let ne = m.experts.len();
8498 let mut logits = vec![0.0f32; ne];
8499 m.router.matvec(x, &mut logits, pool);
8500 let (idx, p, wsum) = moe_route(&logits, m, allowed);
8501 {
8502 let mut st = m.stats.borrow_mut();
8503 if st.len() < ne {
8504 st.resize(ne, 0);
8505 }
8506 for &e in &idx {
8507 st[e] += 1;
8508 }
8509 }
8510 if crate::gpu::enabled_here() {
8515 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
8516 crate::gpu::ProbeArm::Gpu => {
8517 let t0 = std::time::Instant::now();
8518 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
8519 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
8520 return out;
8521 }
8522 }
8523 crate::gpu::ProbeArm::CpuTimed => {
8524 let t0 = std::time::Instant::now();
8525 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
8526 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
8527 return out;
8528 }
8529 crate::gpu::ProbeArm::Cpu => {
8530 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
8531 }
8532 }
8533 }
8534 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
8535}
8536
8537fn graph_note(built: bool) {
8541 use std::sync::atomic::{AtomicBool, Ordering};
8542 if built {
8543 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
8544 } else {
8545 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
8546 }
8547 static SAID: AtomicBool = AtomicBool::new(false);
8548 if !SAID.swap(true, Ordering::Relaxed) {
8549 if built {
8550 tracing::info!("wgpu whole-token graph: ACTIVE");
8551 } else {
8552 tracing::warn!("wgpu whole-token graph refused — per-op path");
8553 }
8554 }
8555}
8556
8557pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
8561pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
8562
8563fn moe_batch_enabled() -> bool {
8566 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8567 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
8568}
8569
8570fn moe_ffn_cpu_batched(
8576 m: &MoeFfn,
8577 x: &[f32],
8578 idx: &[usize],
8579 p: &[f32],
8580 wsum: f32,
8581 pool: Option<&Pool>,
8582) -> Option<Vec<f32>> {
8583 if idx.is_empty() || !moe_batch_enabled() {
8584 return None;
8585 }
8586 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
8590 return None;
8591 }
8592 let n = idx.len() + usize::from(m.shared.is_some());
8593 let mut pairs = Vec::with_capacity(n);
8594 let mut downs = Vec::with_capacity(n);
8595 let mut ws = Vec::with_capacity(n);
8596 for &e in idx {
8597 let d = &m.experts[e];
8598 if d.act != Act::Silu {
8599 return None;
8600 }
8601 pairs.push((&d.gate_proj, &d.up_proj));
8602 downs.push(&d.down_proj);
8603 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
8604 }
8605 if let Some((se, gate)) = &m.shared {
8608 if se.act != Act::Silu {
8609 return None;
8610 }
8611 let g = gate.as_ref().map_or(1.0, |gate| {
8612 let mut gl = [0.0f32; 1];
8613 gate.matvec(x, &mut gl, pool);
8614 1.0 / (1.0 + (-gl[0]).exp())
8615 });
8616 pairs.push((&se.gate_proj, &se.up_proj));
8617 downs.push(&se.down_proj);
8618 ws.push(g);
8619 }
8620 let inter = pairs[0].0.rows();
8621 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
8622 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
8623 return None;
8624 }
8625 let mut out = attention::take_buf(x.len());
8626 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
8627 attention::recycle_buf(&mut out);
8628 return None;
8629 }
8630 Some(out)
8631}
8632
8633fn moe_ffn_cpu(
8635 m: &MoeFfn,
8636 x: &[f32],
8637 idx: &[usize],
8638 p: &[f32],
8639 wsum: f32,
8640 pool: Option<&Pool>,
8641) -> Vec<f32> {
8642 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
8643 return out;
8644 }
8645 let mut out = attention::take_buf(x.len());
8646 for &e in idx {
8647 let mut eo = dense_ffn(&m.experts[e], x, pool);
8648 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
8649 for i in 0..out.len() {
8650 out[i] += w * eo[i];
8651 }
8652 attention::recycle_buf(&mut eo);
8653 }
8654 if let Some((se, gate)) = &m.shared {
8655 let mut so = dense_ffn(se, x, pool);
8656 let g = gate.as_ref().map_or(1.0, |gate| {
8657 let mut gl = [0.0f32; 1];
8658 gate.matvec(x, &mut gl, pool);
8659 1.0 / (1.0 + (-gl[0]).exp())
8660 });
8661 for i in 0..out.len() {
8662 out[i] += g * so[i];
8663 }
8664 attention::recycle_buf(&mut so);
8665 }
8666 out
8667}
8668
8669#[allow(clippy::too_many_arguments)]
8677fn mla_attention(
8678 w: &MlaWeights,
8679 normed: &[f32],
8680 cache: &mut crate::kv_cache::LayerKvCache,
8681 position: usize,
8682 inv_freq: &[f32],
8683 rope_scale: f32,
8684 eps: f64,
8685 pool: Option<&Pool>,
8686) -> Vec<f32> {
8687 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
8688 let hd = dr + dn;
8689 let mut q = vec![0.0f32; nh * hd];
8690 match (&w.q_a, &w.q_a_norm) {
8691 (Some(qa), Some(qn)) => {
8692 let mut t = vec![0.0f32; qa.rows()];
8693 qa.matvec(normed, &mut t, pool);
8694 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
8695 w.q_proj.matvec(&tn, &mut q, pool);
8696 }
8697 _ => w.q_proj.matvec(normed, &mut q, pool),
8698 }
8699 let mut ca = vec![0.0f32; lora + dr];
8700 w.kv_a.matvec(normed, &mut ca, pool);
8701 let (c_lat, k_rope) = ca.split_at_mut(lora);
8702 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
8703 let mut kvb = vec![0.0f32; nh * (dn + dv)];
8704 w.kv_b.matvec(&latn, &mut kvb, pool);
8705 if !w.nope {
8706 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
8707 }
8708 for h in 0..nh {
8709 if !w.nope {
8710 attention::rope_rotate_scaled(
8711 &mut q[h * hd..h * hd + dr],
8712 position,
8713 inv_freq,
8714 rope_scale,
8715 );
8716 }
8717 }
8718 let mut k = vec![0.0f32; nh * hd];
8719 let mut v = vec![0.0f32; nh * hd];
8720 for h in 0..nh {
8721 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
8722 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
8723 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
8724 }
8725 cache.append(&k, &v, &vec![true; nh]);
8726 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
8727 attention::recycle_buf(&mut imp);
8728 let mut ov = vec![0.0f32; nh * dv];
8729 for h in 0..nh {
8730 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
8731 }
8732 let mut out = vec![0.0f32; w.o_proj.rows()];
8733 w.o_proj.matvec(&ov, &mut out, pool);
8734 out
8735}
8736
8737fn dense_moe_ffn(
8744 dm: &DenseMoeFfn,
8745 x_normed: &[f32],
8746 h_raw: &[f32],
8747 eps: f64,
8748 norm_style: NormStyle,
8749 pool: Option<&Pool>,
8750) -> Vec<f32> {
8751 let mut d = dense_ffn(&dm.dense, x_normed, pool);
8752 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
8753 let m = &dm.moe;
8754 let ne = m.experts.len();
8755 let mut logits = vec![0.0f32; ne];
8756 if m.router_input_norm {
8757 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
8758 let inv = 1.0 / (ss + eps as f32).sqrt();
8759 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
8760 m.router.matvec(&xr, &mut logits, pool);
8761 } else {
8762 m.router.matvec(h_raw, &mut logits, pool);
8763 }
8764 let (idx, p, wsum) = moe_route(&logits, m, None);
8765 {
8766 let mut st = m.stats.borrow_mut();
8767 if st.len() < ne {
8768 st.resize(ne, 0);
8769 }
8770 for &e in &idx {
8771 st[e] += 1;
8772 }
8773 }
8774 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
8775 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
8776 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
8777 for (di, mi) in d.iter_mut().zip(&mo) {
8778 *di += mi;
8779 }
8780 d
8781}
8782
8783fn moe_gpu_refused(why: &'static str) {
8790 use std::sync::atomic::{AtomicBool, Ordering};
8791 static SAID: AtomicBool = AtomicBool::new(false);
8792 if !SAID.swap(true, Ordering::Relaxed) {
8793 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
8794 }
8795}
8796
8797fn moe_ffn_gpu(
8798 m: &MoeFfn,
8799 x: &[f32],
8800 idx: &[usize],
8801 p: &[f32],
8802 wsum: f32,
8803 pool: Option<&Pool>,
8804) -> Option<Vec<f32>> {
8805 use crate::gpu::MoeJob;
8806
8807 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
8808 let mut model_ref = None;
8809 for &e in idx {
8810 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
8811 moe_gpu_refused("push_job(expert)");
8812 return None;
8813 }
8814 }
8815 if let Some((se, gate)) = &m.shared {
8816 let g = gate.as_ref().map_or(1.0, |gate| {
8817 let mut gl = [0.0f32; 1];
8818 gate.matvec(x, &mut gl, pool);
8819 1.0 / (1.0 + (-gl[0]).exp())
8820 });
8821 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
8822 moe_gpu_refused("push_job(shared)");
8823 return None;
8824 }
8825 }
8826 let Some(model) = model_ref else {
8827 moe_gpu_refused("no model_ref");
8828 return None;
8829 };
8830 let hidden = jobs[0].down.1;
8831 let mut out = vec![0.0f32; hidden];
8832 if crate::gpu::moe_block(&model, &jobs, &mut out) {
8833 Some(out)
8834 } else {
8835 moe_gpu_refused("gpu::moe_block");
8836 None
8837 }
8838}
8839
8840fn ffn_forward(
8842 ffn: &FfnKind,
8843 x: &[f32],
8844 pool: Option<&Pool>,
8845 experts_allowed: Option<&[bool]>,
8846) -> Vec<f32> {
8847 match ffn {
8848 FfnKind::Dense(d) => dense_ffn(d, x, pool),
8849 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
8850 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
8854 }
8855}
8856
8857fn ffn_forward_pair(
8861 ffn: &FfnKind,
8862 x1: &[f32],
8863 x2: &[f32],
8864 pool: Option<&Pool>,
8865 experts_allowed: Option<&[bool]>,
8866) -> (Vec<f32>, Vec<f32>) {
8867 let d = match ffn {
8868 FfnKind::Dense(d) => d,
8869 FfnKind::Moe(m) => {
8870 return (
8871 moe_ffn(m, x1, pool, experts_allowed),
8872 moe_ffn(m, x2, pool, experts_allowed),
8873 );
8874 }
8875 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
8876 };
8877 let inter = d.gate_proj.rows();
8878 FFN_SCRATCH.with(|s| {
8879 let mut s = s.borrow_mut();
8880 let [g1, g2, u1, u2] = &mut *s;
8881 g1.resize(inter, 0.0);
8882 g2.resize(inter, 0.0);
8883 u1.resize(inter, 0.0);
8884 u2.resize(inter, 0.0);
8885 QTensor::matvec2_many(
8888 [&d.gate_proj, &d.up_proj],
8889 x1,
8890 x2,
8891 [g1.as_mut_slice(), u1.as_mut_slice()],
8892 [g2.as_mut_slice(), u2.as_mut_slice()],
8893 pool,
8894 );
8895 for i in 0..inter {
8896 g1[i] = d.act.combine(g1[i], u1[i]);
8897 g2[i] = d.act.combine(g2[i], u2[i]);
8898 }
8899 let mut o1 = attention::take_buf(d.down_proj.rows());
8900 let mut o2 = attention::take_buf(d.down_proj.rows());
8901 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
8902 (o1, o2)
8903 })
8904}
8905
8906#[cfg(test)]
8907mod tests {
8908
8909 #[test]
8910 fn cancel_flag_stops_generation() {
8911 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
8912 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
8915 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
8916 assert_eq!(r.finish_reason, "cancelled");
8917 assert!(
8918 r.token_ids.is_empty(),
8919 "no tokens after cancel: {:?}",
8920 r.token_ids
8921 );
8922 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
8924 assert_ne!(r2.finish_reason, "cancelled");
8925 }
8926 use super::*;
8927
8928 #[test]
8934 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
8935 let (hidden, inter) = (16usize, 40usize);
8936 let synth = |n: usize, salt: usize| -> Vec<f32> {
8937 (0..n)
8938 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
8939 .collect()
8940 };
8941 let d = DenseFfn {
8942 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
8943 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
8944 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
8945 act: Act::Silu,
8946 };
8947 let x = synth(hidden, 9);
8948 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
8950
8951 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
8952
8953 let mut g = vec![0.0f32; inter];
8955 d.gate_proj.matvec(&x, &mut g, None);
8956 let mut u = vec![0.0f32; inter];
8957 d.up_proj.matvec(&x, &mut u, None);
8958 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
8959 for i in 0..inter {
8960 g[i] = if act_set.contains(&(i as u16)) {
8961 inference::silu(g[i]) * u[i]
8962 } else {
8963 0.0
8964 };
8965 }
8966 let mut reference = vec![0.0f32; hidden];
8967 d.down_proj.matvec(&g, &mut reference, None);
8968
8969 let max_d = sparse
8970 .iter()
8971 .zip(&reference)
8972 .map(|(a, b)| (a - b).abs())
8973 .fold(0.0f32, f32::max);
8974 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
8975 }
8976
8977 fn attach_test_mtp(p: &mut Pipeline) {
8979 let (h, inter, heads, kv, hd) = (
8980 p.hidden_size,
8981 p.intermediate_size,
8982 p.num_heads,
8983 p.num_kv_heads,
8984 p.head_dim,
8985 );
8986 let synth = |n: usize, salt: usize| -> Vec<f32> {
8987 (0..n)
8988 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
8989 .collect()
8990 };
8991 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
8992 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
8993 };
8994 p.mtp = Some(MtpModule {
8995 enorm: vec![1.0; h],
8996 hnorm: vec![1.0; h],
8997 eh_proj: qt(h, 2 * h, 301),
8998 layer: LayerWeights {
8999 input_norm: vec![1.0; h],
9000 post_norm: vec![1.0; h],
9001 attn_out_norm: None,
9002 ffn_out_norm: None,
9003 layer_scale: None,
9004 ffn: FfnKind::Dense(DenseFfn {
9005 gate_proj: qt(inter, h, 315),
9006 up_proj: qt(inter, h, 316),
9007 down_proj: qt(h, inter, 317),
9008 act: Act::Silu,
9009 }),
9010 attn: AttnKind::Full {
9011 bias: None,
9012 wq: qt(heads * hd, h, 311),
9013 wk: qt(kv * hd, h, 312),
9014 wv: qt(kv * hd, h, 313),
9015 wo: qt(h, heads * hd, 314),
9016 q_norm: None,
9017 k_norm: None,
9018 output_gate: false,
9019 softplus_gate: None,
9020 },
9021 },
9022 final_norm: vec![1.0; h],
9023 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
9024 });
9025 }
9026
9027 #[test]
9028 fn speculative_equals_vanilla_greedy() {
9029 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
9033 let run = |spec: bool| {
9034 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9035 p.sampler_config.temperature = 0.0;
9036 attach_test_mtp(&mut p);
9037 p.speculative = spec;
9038 let r = p.generate("abcdef", 12, None, None).unwrap();
9039 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
9040 };
9041 let (vanilla, d0, _) = run(false);
9042 let (spec, d1, a1) = run(true);
9043 assert_eq!(d0, 0, "vanilla path must not draft");
9044 assert!(d1 > 0, "speculative path must draft");
9045 assert_eq!(
9046 vanilla, spec,
9047 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
9048 );
9049 }
9050
9051 #[test]
9052 fn speculative_accepts_constant_oracle() {
9053 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
9055 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9056 p.sampler_config.temperature = 0.0;
9057 p.sampler_config.repetition_penalty = 1.0;
9058 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
9061 attach_test_mtp(&mut p);
9062 p.speculative = true;
9063 let r = p.generate("abcd", 10, None, None).unwrap();
9064 assert!(r.mtp_drafted > 0);
9065 assert_eq!(
9066 r.mtp_accepted, r.mtp_drafted,
9067 "constant logits → every draft accepted"
9068 );
9069 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
9072 }
9073
9074 #[test]
9075 fn empty_prompt_is_an_error_not_a_panic() {
9076 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
9077 let r = p.generate("", 4, None, None);
9078 assert!(r.is_err(), "empty prompt must be a clean error");
9079 }
9080
9081 #[test]
9082 fn every_token_enters_kv_exactly_once() {
9083 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9084 p.sampler_config.temperature = 0.0;
9086 let r = p.generate("abc", 2, None, None).unwrap();
9087 assert_eq!(r.prompt_tokens, 3);
9088 assert_eq!(
9092 p.kv_cache.seq_len(),
9093 3 + r.tokens_generated - 1,
9094 "each token must be cached exactly once (v1 cached the last prompt token twice)"
9095 );
9096 }
9097
9098 #[test]
9099 fn generation_is_reproducible_with_seed() {
9100 let run = || {
9101 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9102 p.generate("hello", 8, None, None).unwrap().token_ids
9103 };
9104 assert_eq!(run(), run());
9105 }
9106
9107 #[test]
9108 fn resetting_sampler_restarts_the_seeded_stream() {
9109 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
9110 let config = SamplerConfig {
9111 seed: Some(1234),
9112 ..SamplerConfig::default()
9113 };
9114 p.set_sampler_config(config.clone());
9115 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
9116 p.set_sampler_config(config);
9117 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
9118 assert_eq!(first, second);
9119 }
9120
9121 #[test]
9122 fn eviction_bounds_the_cache() {
9123 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
9124 p.kv_cache.max_seq_len = 6;
9125 p.sampler_config.temperature = 0.0;
9126 let _ = p.generate("abcd", 12, None, None).unwrap();
9127 assert!(
9128 p.kv_cache.seq_len() <= 6 + 1,
9129 "cache must stay bounded by max_seq_len (got {})",
9130 p.kv_cache.seq_len()
9131 );
9132 }
9133
9134 #[test]
9135 fn confidence_matches_tokens_and_is_a_probability() {
9136 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9137 p.sampler_config.temperature = 0.0;
9138 p.sampler_config.repetition_penalty = 1.0;
9139 let r = p.generate("abcd", 10, None, None).unwrap();
9140 assert_eq!(
9141 r.token_confidence.len(),
9142 r.token_ids.len(),
9143 "one confidence per emitted token"
9144 );
9145 for &c in &r.token_confidence {
9146 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
9147 }
9148 let logits = [1.0f32, 3.0, 0.5, 3.0];
9150 let p0 = top1_prob_t(&logits, 1, 1.0);
9151 let p1 = top1_prob_t(&logits, 3, 1.0);
9152 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
9153 assert!(p0 > 0.0 && p0 < 1.0);
9154 let sharp = top1_prob_t(&logits, 1, 1.0);
9156 let soft = top1_prob_t(&logits, 1, 2.0);
9157 assert!(soft < sharp, "higher temperature lowers peak confidence");
9158 }
9159
9160 #[test]
9161 fn trace_is_opt_in_and_parallels_the_output() {
9162 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9164 p.sampler_config.temperature = 0.0;
9165 p.sampler_config.repetition_penalty = 1.0;
9166 let r = p.generate("abcd", 10, None, None).unwrap();
9167 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
9168
9169 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9171 p.sampler_config.temperature = 0.0;
9172 p.sampler_config.repetition_penalty = 1.0;
9173 p.set_trace(true);
9174 let r = p.generate("abcd", 10, None, None).unwrap();
9175 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
9176 for (i, tr) in r.traces.iter().enumerate() {
9177 assert_eq!(tr.t, i, "trace index is sequential");
9178 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
9179 assert_eq!(
9180 tr.confidence, r.token_confidence[i],
9181 "trace confidence matches the confidence channel"
9182 );
9183 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
9185 }
9186 }
9187
9188 #[test]
9189 fn explain_prefill_logits_match_greedy_first_token() {
9190 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
9194 p.sampler_config.temperature = 0.0;
9195 p.sampler_config.repetition_penalty = 1.0;
9196 let ids = p.tokenizer.encode("abcd");
9197 let logits = p.prefill_next_logits(&ids, None);
9198 let argmax = logits
9199 .iter()
9200 .enumerate()
9201 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9202 .unwrap()
9203 .0 as u32;
9204 let r = p.generate("abcd", 1, None, None).unwrap();
9205 assert_eq!(
9206 argmax, r.token_ids[0],
9207 "explain preview must match greedy emit"
9208 );
9209 }
9210
9211 #[test]
9212 fn laguna_shared_expert_is_unconditionally_added() {
9213 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
9214 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
9215 let zero_dense = || DenseFfn {
9216 gate_proj: matrix(vec![0.0; 4]),
9217 up_proj: matrix(vec![0.0; 4]),
9218 down_proj: matrix(vec![0.0; 4]),
9219 act: Act::Silu,
9220 };
9221 let shared = DenseFfn {
9222 gate_proj: identity(),
9223 up_proj: identity(),
9224 down_proj: identity(),
9225 act: Act::Silu,
9226 };
9227 let x = [1.0, 2.0];
9228 let expected = dense_ffn(&shared, &x, None);
9229 let moe = MoeFfn {
9230 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
9231 experts: vec![zero_dense()],
9232 top_k: 1,
9233 norm_topk_prob: true,
9234 router_sigmoid: true,
9235 expert_bias: None,
9236 routed_scaling: 1.0,
9237 route_tau: None,
9238 shared: Some((shared, None)),
9239 stats: std::cell::RefCell::new(Vec::new()),
9240 act_sq: std::cell::RefCell::new(Vec::new()),
9241 act_rows: std::cell::RefCell::new(Vec::new()),
9242 mask: None,
9243 per_expert_scale: None,
9244 router_input_norm: false,
9245 };
9246 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
9247 for (actual, expected) in actual.iter().zip(expected) {
9248 assert!((actual - expected).abs() < 1e-6);
9249 }
9250 }
9251}