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 pub tokenizer: std::sync::Arc<Tokenizer>,
53 pub kv_cache: KvCache,
54 pub sampler_config: SamplerConfig,
55 pub weights: PipelineWeights,
56 pub hidden_size: usize,
57 pub intermediate_size: usize,
58 pub num_heads: usize,
59 pub num_kv_heads: usize,
60 pub head_dim: usize,
61 pub num_layers: usize,
63 pub physical_layers: usize,
65 pub loop_final_norm: bool,
67 pub vocab_size: usize,
68 pub rms_eps: f64,
69 pub rope_base: f32,
70 pub norm_style: NormStyle,
71 pub rotary_dim: usize,
73 pub attention_heads_per_layer: Option<Vec<usize>>,
75 pub vmf_cfg: Option<VmfPhaseCfg>,
77 pub gdn_cfg: Option<GdnCfg>,
79 pub logit_multiplier: Option<f32>,
81 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
86 pub kv_history: Vec<u32>,
91 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
93 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
96 pub dsv4: Option<
100 Box<(
101 crate::dsv4::Dsv4Globals,
102 Vec<crate::dsv4::Dsv4Layer>,
103 crate::dsv4::Dsv4Cfg,
104 crate::dsv4::Dsv4State,
105 )>,
106 >,
107 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
111 pub dspark: Option<crate::dsv4::DsparkState>,
113 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
116 pub dspark_hist: Vec<usize>,
118 pub dspark_real: Vec<u32>,
122 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
126 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
128 pub dspark_draft_ns: u128,
132 pub short_conv_cfg: Option<ShortConvCfg>,
135 pub mtp: Option<MtpModule>,
137 pub speculative: bool,
139 rng: SplitMix64,
140 sampler_scratch: SamplerScratch,
141 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
145 ws: ForwardScratch,
149 pool: Option<std::sync::Arc<Pool>>,
151 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
155 pub(crate) dyn_force_f32: bool,
157 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
162 pub(crate) dyn_active: Option<usize>,
168 pub(crate) dyn_blend_loaded: bool,
172 pub(crate) dyn_phi_layer: Option<usize>,
175 dyn_phi_ema: Vec<f32>,
177 dyn_phi_seen: usize,
178 pub dyn_router: Option<crate::swarm::DynRouter>,
181 o1_cfg: Option<crate::nystrom::O1Cfg>,
184 o1_epoch: u64,
187 o1_flags: Vec<bool>,
189 trace: bool,
192 calib_temp: f32,
195 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
197 graph_kv_id: u64,
198 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
201 graph_want_logits: bool,
202 graph_logits: Option<Vec<f32>>,
205 pub embed_multiplier: f32,
207 pub attn_scale: f32,
210 pub swa: Option<(usize, usize)>,
213 pub sliding_layers: Option<Vec<bool>>,
216 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
219 pub rotary_dim_local: Option<usize>,
220 pub rope_scale: f32,
221 pub rope_scale_local: f32,
222 pub global_attn: Option<(usize, usize)>,
225 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
228 pub attn_v_norm: bool,
230 pub final_softcap: Option<f32>,
232 pub attn_softcap: f32,
234 confidence_on: bool,
238}
239
240#[cfg(target_os = "macos")]
241impl Drop for Pipeline {
242 fn drop(&mut self) {
243 crate::gpu::kv_mirror_drop(self.graph_kv_id);
244 }
245}
246
247pub struct PipelineWeights {
252 pub embed_tokens: QTensor,
254 pub layers: Vec<LayerWeights>,
256 pub lm_head: QTensor,
258 pub final_norm: Vec<f32>,
260}
261
262pub struct LayerWeights {
264 pub input_norm: Vec<f32>,
265 pub post_norm: Vec<f32>,
268 pub attn_out_norm: Option<Vec<f32>>,
271 pub layer_scale: Option<f32>,
273 pub ffn_out_norm: Option<Vec<f32>>,
276 pub ffn: FfnKind,
277 pub attn: AttnKind,
278}
279
280#[derive(Clone, Copy, PartialEq, Debug, Default)]
283pub enum Act {
284 #[default]
285 Silu,
286 GeluTanh,
287 Situ {
290 beta: f32,
291 linear_beta: f32,
292 },
293}
294
295impl Act {
296 pub fn from_arch(name: &str) -> Self {
297 if name == "gelu_tanh" {
298 Self::GeluTanh
299 } else {
300 Self::Silu
301 }
302 }
303
304 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
306 match arch.hidden_act.as_str() {
307 "situ" => Self::Situ {
308 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
309 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
310 },
311 other => Self::from_arch(other),
312 }
313 }
314
315 #[inline]
316 pub fn apply(self, x: f32) -> f32 {
317 match self {
318 Self::Silu => inference::silu(x),
319 Self::GeluTanh => inference::gelu_tanh(x),
320 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
321 }
322 }
323
324 #[inline]
327 pub fn combine(self, g: f32, u: f32) -> f32 {
328 match self {
329 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
330 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
331 }
332 _ => self.apply(g) * u,
333 }
334 }
335}
336
337pub struct DenseFfn {
339 pub gate_proj: QTensor,
340 pub up_proj: QTensor,
341 pub down_proj: QTensor,
342 pub act: Act,
344}
345
346pub enum FfnKind {
349 Dense(DenseFfn),
350 Moe(MoeFfn),
354 DenseMoe(Box<DenseMoeFfn>),
361}
362
363pub struct DenseMoeFfn {
365 pub dense: DenseFfn,
366 pub moe: MoeFfn,
367 pub post_norm_1: Vec<f32>,
369 pub pre_norm_2: Vec<f32>,
372 pub post_norm_2: Vec<f32>,
374}
375
376pub struct MoeFfn {
377 pub router: QTensor,
379 pub experts: Vec<DenseFfn>,
380 pub top_k: usize,
381 pub norm_topk_prob: bool,
382 pub router_sigmoid: bool,
385 pub expert_bias: Option<Vec<f32>>,
389 pub routed_scaling: f32,
392 pub route_tau: Option<f32>,
398 pub shared: Option<(DenseFfn, Option<QTensor>)>,
401 pub stats: std::cell::RefCell<Vec<u64>>,
405 pub act_sq: std::cell::RefCell<Vec<f64>>,
412 pub act_rows: std::cell::RefCell<Vec<f32>>,
418 pub mask: Option<Vec<bool>>,
423 pub per_expert_scale: Option<Vec<f32>>,
426 pub router_input_norm: bool,
430}
431
432pub enum AttnKind {
435 Full {
437 wq: QTensor,
438 wk: QTensor,
439 wv: QTensor,
440 wo: QTensor,
441 q_norm: Option<Vec<f32>>,
442 k_norm: Option<Vec<f32>>,
443 output_gate: bool,
444 softplus_gate: Option<(QTensor, bool)>,
448 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
450 },
451 Linear(VmfPhaseWeights),
453 LinearGdn(GdnWeights),
455 ShortConv(ShortConvWeights),
458 Mla(Box<MlaWeights>),
466 Kda(Box<crate::linear_core::KdaWeights>),
470}
471
472pub struct MlaWeights {
474 pub q_proj: QTensor,
478 pub q_a: Option<QTensor>,
481 pub q_a_norm: Option<Vec<f32>>,
482 pub kv_a: QTensor,
484 pub kv_a_norm: Vec<f32>,
486 pub kv_b: QTensor,
488 pub o_proj: QTensor,
490 pub nh: usize,
491 pub qk_rope: usize,
492 pub qk_nope: usize,
493 pub v_dim: usize,
494 pub lora: usize,
495 pub scale: f32,
497 pub nope: bool,
499}
500
501pub struct MtpModule {
506 pub enorm: Vec<f32>,
507 pub hnorm: Vec<f32>,
508 pub eh_proj: QTensor,
510 pub layer: LayerWeights,
511 pub final_norm: Vec<f32>,
512 pub kv: crate::kv_cache::LayerKvCache,
513}
514
515pub struct GenerateResult {
517 pub text: String,
518 pub token_ids: Vec<u32>,
519 pub prompt_tokens: usize,
520 pub tokens_generated: usize,
521 pub finish_reason: String,
522 pub mtp_drafted: usize,
524 pub mtp_accepted: usize,
525 pub token_confidence: Vec<f32>,
530 pub traces: Vec<TokenTrace>,
533}
534
535#[derive(Clone, Debug)]
540pub struct TokenTrace {
541 pub t: usize,
543 pub token_id: u32,
545 pub confidence: f32,
547 pub active_skill: Option<String>,
549 pub recon: Option<f32>,
553 pub switched: bool,
556}
557
558fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
563 let t = if temp > 1e-3 { temp } else { 1.0 };
564 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
565 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
566 if sum > 0.0 {
567 (((logits[id as usize] - max) / t).exp()) / sum
568 } else {
569 0.0
570 }
571}
572
573fn prefill_batched() -> bool {
576 std::env::var("CMF_PREFILL")
577 .map(|v| v != "seq")
578 .unwrap_or(true)
579}
580
581impl Pipeline {
588 fn can_prefill_batched(&self) -> bool {
589 prefill_batched() && !self.weights.layers.is_empty()
590 }
591}
592
593fn prefill_chunk() -> usize {
598 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
599 .ok()
600 .and_then(|v| v.parse::<usize>().ok())
601 {
602 return n.max(1);
603 }
604 if cfg!(target_os = "macos") {
605 512
606 } else if cfg!(target_arch = "aarch64") {
607 256
610 } else {
611 48
612 }
613}
614
615pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
617
618impl Pipeline {
619 #[inline]
623 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
624 virtual_idx % self.physical_layers
625 }
626
627 #[inline]
630 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
631 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
632 }
633
634 #[allow(clippy::too_many_arguments)]
636
637 #[cfg(target_os = "macos")]
656 fn graph_prefill_preferred(&self) -> bool {
657 if !crate::gpu::enabled_here()
658 || !crate::gpu::q1_force()
659 || std::env::var("CMF_GPU_BLOCK")
660 .map(|v| v == "0")
661 .unwrap_or(false)
662 {
663 return false;
664 }
665 self.weights
666 .layers
667 .iter()
668 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
669 }
670
671 #[cfg(not(target_os = "macos"))]
672 fn graph_prefill_preferred(&self) -> bool {
673 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
681 .map(|v| v != "0")
682 .unwrap_or_else(|_| {
683 crate::gpu::wgpu_graph_default()
687 });
688 if !graph_on || !crate::gpu::enabled_here() {
689 return false;
690 }
691 self.weights
692 .layers
693 .iter()
694 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
695 }
696
697 #[cfg(target_os = "macos")]
698 fn q1_graph_gpu(
699 &mut self,
700 start: usize,
701 upto: Option<usize>,
702 position: usize,
703 h: &mut [f32],
704 ) -> usize {
705 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
706 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
708 || !crate::gpu::q1_force()
709 || std::env::var("CMF_GPU_BLOCK")
710 .map(|v| v == "0")
711 .unwrap_or(false)
712 {
713 return start;
714 }
715 if self.swa.is_some()
720 || self.global_attn.is_some()
721 || self.attention_heads_per_layer.is_some()
722 || self.attn_v_norm
723 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
724 || self.weights.layers.iter().any(|lw| {
725 lw.attn_out_norm.is_some()
726 || lw.ffn_out_norm.is_some()
727 || lw.layer_scale.is_some()
728 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
729 })
730 {
731 return start;
732 }
733 let limit = upto
736 .map(|u| u + 1)
737 .unwrap_or(self.num_layers)
738 .min(self.num_layers);
739
740 enum Item<'a> {
741 Gdn {
742 run: Vec<GdnGpuLayer<'a>>,
743 first: usize,
744 },
745 Attn {
746 l: AttnGpuLayer<'a>,
747 li: usize,
748 q_norm: Option<&'a [f32]>,
749 k_norm: Option<&'a [f32]>,
750 output_gate: bool,
751 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
752 full_gpu: bool,
755 },
756 }
757
758 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
760 let dev_attend = attend_mode != "0"
761 && attend_mode != "off"
762 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
766 && self.head_dim % 4 == 0
767 && self.head_dim <= 256
768 && self.rotary_dim >= 2
769 && self.rotary_dim <= self.head_dim
770 && (self.rotary_dim / 2) % 32 == 0
771 && self.num_kv_heads > 0
772 && self.num_heads % self.num_kv_heads == 0;
773
774 let mut plan: Vec<Item> = Vec::new();
775 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
776 let mut scan = start;
777 while scan < limit {
778 let lw = &self.weights.layers[self.phys_layer(scan)];
779 let FfnKind::Dense(d) = &lw.ffn else { break };
780 let (Some(g), Some(u), Some(dn)) = (
781 d.gate_proj.q1_parts(),
782 d.up_proj.q1_parts(),
783 d.down_proj.q1_parts(),
784 ) else {
785 break;
786 };
787 match &lw.attn {
788 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
789 let parts = (
790 w.in_proj_qkv.q1_parts(),
791 w.in_proj_z.q1_parts(),
792 w.in_proj_a.f32_parts(),
793 w.in_proj_b.f32_parts(),
794 w.out_proj.q1_parts(),
795 );
796 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
797 break;
798 };
799 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
800 model_ref.get_or_insert_with(|| model.clone());
801 }
802 let gl = GdnGpuLayer {
803 attn_norm: &lw.input_norm,
804 post_norm: &lw.post_norm,
805 qkv,
806 z,
807 a,
808 b,
809 out,
810 gate: g,
811 up: u,
812 down: dn,
813 conv1d: &w.conv1d,
814 a_log: &w.a_log,
815 dt_bias: &w.dt_bias,
816 gnorm: &w.norm,
817 };
818 match plan.last_mut() {
819 Some(Item::Gdn { run, .. }) => run.push(gl),
820 _ => plan.push(Item::Gdn {
821 run: vec![gl],
822 first: scan,
823 }),
824 }
825 }
826 AttnKind::Full {
827 wq,
828 wk,
829 wv,
830 wo,
831 q_norm,
832 k_norm,
833 output_gate,
834 softplus_gate: None,
835 bias,
836 } if !self.kv_cache.layers[scan].o1_sealed() => {
837 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
838 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
839 break;
840 };
841 if let QTensor::Mapped { model, .. } = wq {
842 model_ref.get_or_insert_with(|| model.clone());
843 }
844 let cache = &self.kv_cache.layers[scan];
845 let full_gpu = dev_attend
846 && cache.mode == crate::kv_cache::KvMode::F32
847 && cache.o1.is_none()
848 && bias.is_none()
849 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
850 && pk.1 == self.num_kv_heads * self.head_dim
851 && pv.1 == self.num_kv_heads * self.head_dim
852 && po.2 == self.num_heads * self.head_dim;
853 plan.push(Item::Attn {
854 l: AttnGpuLayer {
855 attn_norm: &lw.input_norm,
856 post_norm: &lw.post_norm,
857 wq: pq,
858 wk: pk,
859 wv: pv,
860 wo: po,
861 gate: g,
862 up: u,
863 down: dn,
864 },
865 li: scan,
866 q_norm: q_norm.as_deref(),
867 k_norm: k_norm.as_deref(),
868 output_gate: *output_gate,
869 bias: bias
870 .as_ref()
871 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
872 full_gpu,
873 });
874 }
875 _ => break,
876 }
877 scan += 1;
878 }
879 let Some(model) = model_ref else {
880 if std::env::var("CMF_GRAPH_DBG").is_ok() {
881 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
882 }
883 return start;
884 };
885 if plan.is_empty() {
886 if std::env::var("CMF_GRAPH_DBG").is_ok() {
887 eprintln!("q1-graph: empty plan at layer {start}");
888 }
889 return start;
890 }
891 if std::env::var("CMF_GRAPH_DBG").is_ok() {
892 use std::sync::atomic::{AtomicBool, Ordering};
893 static SAID: AtomicBool = AtomicBool::new(false);
894 if !SAID.swap(true, Ordering::Relaxed) {
895 let fg = plan
896 .iter()
897 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
898 .count();
899 let att = plan
900 .iter()
901 .filter(|it| matches!(it, Item::Attn { .. }))
902 .count();
903 eprintln!(
904 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
905 plan.len(),
906 self.head_dim,
907 self.rotary_dim,
908 self.num_kv_heads,
909 self.num_heads,
910 );
911 }
912 }
913 let dims = GraphDims {
914 hidden: self.hidden_size,
915 eps: self.rms_eps as f32,
916 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
917 };
918 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
919 return start;
920 };
921 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
922 nv: cfg.num_v_heads,
923 nk: cfg.num_k_heads,
924 dk: cfg.key_head_dim,
925 dv: cfg.value_head_dim,
926 kk: cfg.conv_kernel,
927 hidden: self.hidden_size,
928 inter: self.intermediate_size,
929 c_dim: cfg.conv_dim(),
930 eps: cfg.rms_eps as f32,
931 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
932 });
933 let mut valid = 0usize;
937 let mut end = start;
938 for item in &plan {
939 let ok = match item {
940 Item::Gdn { run, .. } => gcfg
941 .as_ref()
942 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
943 .unwrap_or(false),
944 Item::Attn { l, .. } => graph.attn_ok(l),
945 };
946 if !ok {
947 break;
948 }
949 valid += 1;
950 end += match item {
951 Item::Gdn { run, .. } => run.len(),
952 Item::Attn { .. } => 1,
953 };
954 }
955 plan.truncate(valid);
956 if plan.is_empty() {
957 return start;
958 }
959
960 let inv_freq = self.inv_freq.clone();
961 let pool = self.pool.clone();
962 let (nh, nkv, hd, hs, rd, eps) = (
963 self.num_heads,
964 self.num_kv_heads,
965 self.head_dim,
966 self.hidden_size,
967 self.rotary_dim,
968 self.rms_eps,
969 );
970 let norm_style = self.norm_style;
971 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
972 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
973 let kv_id = self.graph_kv_id;
974 let mut pending: Vec<(usize, usize)> = Vec::new();
977 let mut dev_attn: Vec<usize> = Vec::new();
980 for item in &plan {
981 if self.loop_final_norm {
983 let item_start = match item {
984 Item::Gdn { first, .. } => *first,
985 Item::Attn { li, .. } => *li,
986 };
987 if item_start > start && self.is_loop_end(item_start - 1) {
988 graph.encode_loop_norm(&self.weights.final_norm);
989 }
990 }
991 match item {
992 Item::Gdn { run, first } => {
993 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
994 if l.linear_state.len() != want {
995 l.linear_state = vec![0f32; want];
996 }
997 }
998 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
999 .iter()
1000 .map(|l| l.linear_state.as_slice())
1001 .collect();
1002 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1003 tracing::error!("q1 graph: GDN run refused after validation");
1005 return start;
1006 }
1007 graph.commit();
1010 pending.push((*first, run.len()));
1011 }
1012 Item::Attn {
1013 l,
1014 li,
1015 q_norm,
1016 k_norm,
1017 output_gate,
1018 bias,
1019 full_gpu,
1020 } => {
1021 if *full_gpu {
1023 let cache = &self.kv_cache.layers[*li];
1024 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1025 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1026 let cpu_stored = cpu_k[0].len() / hd;
1027 let p = crate::gpu::AttnDeviceParams {
1028 kv_id,
1029 layer: *li,
1030 nh,
1031 nkv,
1032 hd,
1033 rd,
1034 position,
1035 eps: eps as f32,
1036 gemma,
1037 output_gate: *output_gate,
1038 q_norm: *q_norm,
1039 k_norm: *k_norm,
1040 inv_freq: &inv_freq,
1041 cpu_k,
1042 cpu_v,
1043 cpu_stored,
1044 };
1045 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
1046 graph.commit();
1047 dev_attn.push(*li);
1048 continue;
1049 }
1050 }
1052 graph.encode_attn_prefix(l);
1053 graph.sync();
1054 if !pending.is_empty() {
1055 let idxs: Vec<usize> =
1056 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1057 let mut outs: Vec<&mut [f32]> = self
1058 .kv_cache
1059 .layers
1060 .iter_mut()
1061 .enumerate()
1062 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1063 .map(|(_, s)| s.linear_state.as_mut_slice())
1064 .collect();
1065 graph.read_states(&mut outs);
1066 }
1067 let mut q_raw = attention::take_buf(l.wq.1);
1068 let mut k = attention::take_buf(l.wk.1);
1069 let mut v = attention::take_buf(l.wv.1);
1070 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1071 let cfg = QwenAttnCfg {
1072 num_heads: nh,
1073 num_kv_heads: nkv,
1074 head_dim: hd,
1075 hidden_size: hs,
1076 position,
1077 inv_freq: &inv_freq,
1078 rotary_dim: rd,
1079 scale: self.attn_scale,
1080 softcap: self.attn_softcap,
1081 window: None,
1082 v_norm: false,
1083 q_norm: *q_norm,
1084 k_norm: *k_norm,
1085 output_gate: *output_gate,
1086 softplus_gate: None,
1087 rope_scale: 1.0,
1088 bias: *bias,
1089 rms_eps: eps,
1090 norm_style,
1091 pool: pool.as_deref(),
1092 };
1093 let mut ao = attention::qwen_attention_core(
1094 q_raw,
1095 k,
1096 v,
1097 &mut self.kv_cache.layers[*li],
1098 &cfg,
1099 );
1100 graph.encode_attn_suffix(l, &ao);
1101 graph.commit();
1104 attention::recycle_buf(&mut ao);
1105 }
1106 }
1107 }
1108 let mut lm_rows = None;
1113 if self.graph_want_logits
1114 && upto.is_none()
1115 && end == self.num_layers
1116 && std::env::var("CMF_GPU_LMHEAD")
1117 .map(|v| v != "0")
1118 .unwrap_or(true)
1119 {
1120 if let Some(lm) = self.weights.lm_head.q1_parts() {
1121 if graph.lm_head_ok(lm) {
1122 graph.encode_lm_head(&self.weights.final_norm, lm);
1123 lm_rows = Some(lm.1);
1124 }
1125 }
1126 }
1127 graph.sync();
1128 if !pending.is_empty() {
1129 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1130 let mut outs: Vec<&mut [f32]> = self
1131 .kv_cache
1132 .layers
1133 .iter_mut()
1134 .enumerate()
1135 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1136 .map(|(_, s)| s.linear_state.as_mut_slice())
1137 .collect();
1138 graph.read_states(&mut outs);
1139 }
1140 if let Some(rows) = lm_rows {
1141 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1142 graph.read_logits(&mut lg);
1143 lg.resize(self.vocab_size, 0.0);
1144 if let Some(c) = self.final_softcap {
1145 for l in lg.iter_mut() {
1146 *l = c * (*l / c).tanh();
1147 }
1148 }
1149 self.graph_logits = Some(lg);
1150 }
1151 graph.finish(h);
1152 for li in dev_attn {
1156 let mut krow = attention::take_buf(nkv * hd);
1157 let mut vrow = attention::take_buf(nkv * hd);
1158 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1159 let cache = &mut self.kv_cache.layers[li];
1160 cache.append(&krow, &vrow, &[]);
1161 let n = cache.seq_len;
1162 let mut imp = attention::take_buf(n);
1163 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1164 cache.accumulate_imp(&imp);
1165 attention::recycle_buf(&mut imp);
1166 }
1167 attention::recycle_buf(&mut krow);
1168 attention::recycle_buf(&mut vrow);
1169 }
1170 end
1171 }
1172
1173 pub fn new(
1174 tokenizer: Tokenizer,
1175 weights: PipelineWeights,
1176 hidden_size: usize,
1177 intermediate_size: usize,
1178 num_heads: usize,
1179 num_kv_heads: usize,
1180 head_dim: usize,
1181 num_layers: usize,
1182 physical_layers: usize,
1183 loop_final_norm: bool,
1184 vocab_size: usize,
1185 rms_eps: f64,
1186 rope_base: f32,
1187 norm_style: NormStyle,
1188 max_seq_len: usize,
1189 sampler_config: SamplerConfig,
1190 ) -> Self {
1191 let rng = match sampler_config.seed {
1192 Some(s) => SplitMix64::new(s),
1193 None => SplitMix64::from_entropy(),
1194 };
1195 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1196 let pool = Pool::from_env();
1197 if let Some(p) = &pool {
1198 tracing::info!("worker pool: {} threads", p.n_workers());
1199 }
1200 Self {
1201 tokenizer: std::sync::Arc::new(tokenizer),
1202 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1203 sampler_config,
1204 weights,
1205 hidden_size,
1206 intermediate_size,
1207 num_heads,
1208 num_kv_heads,
1209 head_dim,
1210 num_layers,
1211 physical_layers,
1212 loop_final_norm,
1213 vocab_size,
1214 rms_eps,
1215 rope_base,
1216 norm_style,
1217 rotary_dim: head_dim,
1218 attention_heads_per_layer: None,
1219 vmf_cfg: None,
1220 gdn_cfg: None,
1221 kda_cfg: None,
1222 g3n: None,
1223 dsv4: None,
1224 dsv4_mtp: Vec::new(),
1225 dspark: None,
1226 dspark_pending: Vec::new(),
1227 dspark_hist: Vec::new(),
1228 dspark_real: Vec::new(),
1229 dspark_trunk_picks: Vec::new(),
1230 dspark_exp: Vec::new(),
1231 dspark_draft_ns: 0,
1232 logit_multiplier: None,
1233 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1234 kv_history: Vec::new(),
1235 short_conv_cfg: None,
1236 mtp: None,
1237 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1238 rng,
1239 sampler_scratch: SamplerScratch::default(),
1240 inv_freq,
1241 ws: ForwardScratch::new(hidden_size),
1242 pool,
1243 model: None,
1244 dyn_force_f32: false,
1245 dyn_skill_layers: Vec::new(),
1246 dyn_active: None,
1247 dyn_blend_loaded: false,
1248 dyn_phi_layer: None,
1249 dyn_phi_ema: Vec::new(),
1250 dyn_phi_seen: 0,
1251 dyn_router: None,
1252 o1_cfg: None,
1253 o1_epoch: 0,
1254 o1_flags: Vec::new(),
1255 trace: false,
1256 calib_temp: 1.0,
1257 confidence_on: true,
1258 embed_multiplier: 1.0,
1259 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1260 swa: None,
1261 sliding_layers: None,
1262 inv_freq_local: None,
1263 rotary_dim_local: None,
1264 rope_scale: 1.0,
1265 rope_scale_local: 1.0,
1266 global_attn: None,
1267 inv_freq_global: None,
1268 attn_v_norm: false,
1269 final_softcap: None,
1270 attn_softcap: 0.0,
1271 graph_want_logits: false,
1272 graph_logits: None,
1273 graph_kv_id: {
1274 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1275 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1276 },
1277 }
1278 }
1279
1280 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1287 self.o1_flags = match &cfg {
1288 Some(c) => {
1289 let mut flags = c.layer_flags(self.num_layers);
1290 for (li, f) in flags.iter_mut().enumerate() {
1291 if *f
1292 && !matches!(
1293 self.weights.layers[self.phys_layer(li)].attn,
1294 AttnKind::Full { .. }
1295 )
1296 {
1297 *f = false;
1298 }
1299 }
1300 flags
1301 }
1302 None => Vec::new(),
1303 };
1304 if let Some(c) = &cfg {
1305 let n = self.o1_flags.iter().filter(|&&f| f).count();
1306 tracing::info!(
1307 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1308 self.num_layers,
1309 c.m,
1310 c.w,
1311 c.sink,
1312 c.rect
1313 );
1314 }
1315 self.o1_cfg = cfg;
1316 }
1317
1318 pub fn o1_active(&self) -> bool {
1320 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1321 }
1322
1323 fn o1_begin(&mut self) {
1325 if let Some(c) = &self.o1_cfg {
1326 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1327 for (li, &f) in self.o1_flags.iter().enumerate() {
1328 if f {
1329 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1330 }
1331 }
1332 }
1333 }
1334
1335 fn o1_seal(&mut self) {
1338 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1339 if self.o1_cfg.is_none() {
1340 return;
1341 }
1342 for li in 0..self.num_layers {
1343 if self.o1_flags.get(li).copied().unwrap_or(false) {
1344 self.kv_cache.layers[li].o1_seal(self.num_heads);
1345 }
1346 }
1347 }
1348
1349 pub fn set_trace(&mut self, on: bool) {
1351 self.trace = on;
1352 }
1353
1354 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1357 self.rng = match config.seed {
1358 Some(seed) => SplitMix64::new(seed),
1359 None => SplitMix64::from_entropy(),
1360 };
1361 self.sampler_config = config;
1362 }
1363
1364 pub fn set_confidence(&mut self, on: bool) {
1369 self.confidence_on = on;
1370 }
1371
1372 pub fn set_calib_temp(&mut self, t: f32) {
1375 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1376 }
1377
1378 pub fn calib_temp(&self) -> f32 {
1380 self.calib_temp
1381 }
1382
1383 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1386 self.rotary_dim = rotary_dim.min(self.head_dim);
1387 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1388 }
1389
1390 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1391 QwenAttnCfg {
1392 num_heads: self.num_heads,
1393 num_kv_heads: self.num_kv_heads,
1394 head_dim: self.head_dim,
1395 hidden_size: self.hidden_size,
1396 position,
1397 inv_freq: &self.inv_freq,
1398 rotary_dim: self.rotary_dim,
1399 scale: self.attn_scale,
1400 softcap: self.attn_softcap,
1401 window: None,
1402 v_norm: false,
1403 q_norm: None,
1404 k_norm: None,
1405 output_gate: false,
1406 softplus_gate: None,
1407 rope_scale: self.rope_scale,
1408 bias: None,
1409 rms_eps: self.rms_eps,
1410 norm_style: self.norm_style,
1411 pool: self.pool.as_deref(),
1412 }
1413 }
1414
1415 pub fn generate(
1417 &mut self,
1418 prompt: &str,
1419 max_tokens: usize,
1420 task_mask: Option<&TaskMask>,
1421 on_token: Option<TokenCallback>,
1422 ) -> Result<GenerateResult, String> {
1423 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1424 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1425 }
1426
1427 pub fn generate_from_ids(
1435 &mut self,
1436 input_ids: &[u32],
1437 max_tokens: usize,
1438 task_mask: Option<&TaskMask>,
1439 mut on_token: Option<TokenCallback>,
1440 ) -> Result<GenerateResult, String> {
1441 if std::env::var("CMF_TRACE_H").is_ok() {
1442 eprintln!("input_ids: {input_ids:?}");
1443 }
1444 if input_ids.is_empty() {
1445 return Err("empty prompt: nothing to generate from".to_string());
1446 }
1447
1448 let reuse_from = {
1456 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1457 let h = &self.kv_history;
1458 if on
1459 && task_mask.is_none()
1460 && self.mtp.is_none()
1461 && self.o1_cfg.is_none()
1462 && !h.is_empty()
1463 && h.len() < input_ids.len()
1464 && input_ids[..h.len()] == h[..]
1465 {
1466 h.len()
1467 } else {
1468 0
1469 }
1470 };
1471 if reuse_from == 0 {
1472 self.kv_cache.clear();
1474 self.kv_history.clear();
1475 crate::gpu::graph_kv_reset(self.graph_kv_id);
1476 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1477 eprintln!(
1478 "kv-reuse: {} of {} prompt positions already cached",
1479 reuse_from,
1480 input_ids.len()
1481 );
1482 }
1483 crate::gpu::graph_race_begin_generation();
1484 self.o1_begin();
1485
1486 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1492 .map(|v| v != "0")
1493 .unwrap_or_else(|_| {
1494 crate::gpu::wgpu_graph_default()
1498 });
1499 let graph_spec = self.speculative
1506 && graph_on
1507 && self.mtp.is_some()
1508 && task_mask.is_none()
1509 && !self.o1_active()
1510 && self.sampler_config.temperature < 1e-6
1511 && self.sampler_config.repetition_penalty == 1.0
1512 && std::env::var("CMF_GRAPH_SPEC").is_ok_and(|v| v != "0");
1513 let pair_pays = self.gdn_cfg.is_none()
1520 || std::env::var("CMF_MTP").as_deref() == Ok("1");
1521 let spec_active = self.speculative
1522 && self.mtp.is_some()
1523 && task_mask.is_none()
1524 && !self.o1_active()
1525 && ((!graph_on && pair_pays) || graph_spec)
1526 && self.sampler_config.temperature < 1e-6;
1527 let mut mtp = if spec_active { self.mtp.take() } else { None };
1530 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
1531 eprintln!(
1532 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
1533 mtp.is_some(),
1534 self.speculative,
1535 self.sampler_config.temperature < 1e-6,
1536 );
1537 }
1538 if let Some(m) = &mut mtp {
1539 m.kv.clear();
1540 }
1541 let mut router = if mtp.is_none() {
1545 self.dyn_router.take()
1546 } else {
1547 None
1548 };
1549 if let Some(r) = &mut router {
1550 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1553 }
1554
1555 let mut all_ids = input_ids.to_vec();
1556 let mut generated = 0usize;
1557 let mut finish_reason = "max_tokens".to_string();
1558 let mut drafted = 0usize;
1559 let mut accepted = 0usize;
1560 let mut confidence: Vec<f32> = Vec::new();
1561 let trace_on = self.trace;
1562 let calib_temp = self.calib_temp;
1563 let mut traces: Vec<TokenTrace> = Vec::new();
1564
1565 let mut hidden = vec![0.0f32; self.hidden_size];
1571 let mut pos = reuse_from;
1572 let fuse_lm = mtp.is_none()
1581 && router.is_none()
1582 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
1583 self.graph_logits = None;
1584 self.graph_want_logits = false;
1585 let _tpf = std::time::Instant::now();
1586 let batch_k = std::env::var("CMF_BATCH_K")
1587 .ok()
1588 .and_then(|v| v.parse::<usize>().ok())
1589 .unwrap_or(0);
1590 while self.dsv4.is_some()
1601 && mtp.is_none()
1602 && pos < input_ids.len()
1603 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1604 {
1605 let end = (pos + prefill_chunk()).min(input_ids.len());
1606 let ids: Vec<u32> = input_ids[pos..end].to_vec();
1607 let mut lg = Vec::new();
1608 if let Some(b) = &mut self.dsv4 {
1609 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
1610 crate::dsv4::forward_chunk(
1611 g,
1612 layers,
1613 &cfg,
1614 st,
1615 &ids,
1616 pos,
1617 &self.inv_freq,
1618 self.pool.as_deref(),
1619 &mut lg,
1620 end == input_ids.len(),
1621 );
1622 }
1623 if end == input_ids.len() {
1624 self.graph_logits = Some(lg);
1625 }
1626 pos = end;
1627 hidden = vec![0.0; self.hidden_size];
1628 }
1629 let dyn_prefill = router.is_some();
1634 let graph_prefill = self.graph_prefill_preferred();
1640 if task_mask.is_none()
1641 && !dyn_prefill
1642 && !graph_prefill
1643 && self.can_prefill_batched()
1644 && self.g3n.is_none()
1645 && input_ids.len() > 2
1646 {
1647 let chunk = prefill_chunk();
1653 let hs = self.hidden_size;
1654 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1655 let end = (pos + chunk).min(input_ids.len());
1656 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1657 if let Some(m) = &mut mtp {
1658 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1659 .ok()
1660 .and_then(|v| v.parse().ok())
1661 .unwrap_or(0);
1662 for p in pos..end {
1663 if p + 1 < input_ids.len() {
1664 if probe >= 1 && p + 2 < input_ids.len() {
1665 let (d1, mut hx) = self.mtp_step_h(
1669 m,
1670 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1671 input_ids[p + 1],
1672 p,
1673 );
1674 let mut ok = d1 == input_ids[p + 2];
1675 Self::chain_probe_note(0, ok);
1676 let mut d_prev = d1;
1677 let mut extra = 0usize;
1678 for j in 1..probe {
1679 if p + 2 + j >= input_ids.len() {
1680 break;
1681 }
1682 let (dj, hj) =
1683 self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
1684 extra += 1;
1685 ok = ok && dj == input_ids[p + 2 + j];
1686 Self::chain_probe_note(j, ok);
1687 d_prev = dj;
1688 hx = hj;
1689 }
1690 m.kv.truncate_last(extra);
1691 } else {
1692 let _ = self.mtp_step(
1693 m,
1694 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1695 input_ids[p + 1],
1696 p,
1697 );
1698 }
1699 }
1700 }
1701 }
1702 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1703 pos = end;
1704 }
1705 }
1706 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
1707 if task_mask.is_none()
1708 && !dyn_prefill
1709 && !graph_prefill
1710 && !pair_off
1711 && self.pair_supported()
1712 {
1713 while pos + 1 < input_ids.len()
1714 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1715 {
1716 let e1 = self.embed_single(input_ids[pos]);
1717 let e2 = self.embed_single(input_ids[pos + 1]);
1718 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1719 self.commit_linear_scratch();
1721 if let Some(m) = &mut mtp {
1722 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1723 if pos + 2 < input_ids.len() {
1724 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1725 .ok()
1726 .and_then(|v| v.parse().ok())
1727 .unwrap_or(0);
1728 if probe >= 1 && pos + 3 < input_ids.len() {
1729 let (d1, mut hx) =
1733 self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
1734 let mut ok = d1 == input_ids[pos + 3];
1735 Self::chain_probe_note(0, ok);
1736 let mut d_prev = d1;
1737 let mut extra = 0usize;
1738 for j in 1..probe {
1739 if pos + 3 + j >= input_ids.len() {
1740 break;
1741 }
1742 let (dj, hj) =
1743 self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
1744 extra += 1;
1745 ok = ok && dj == input_ids[pos + 3 + j];
1746 Self::chain_probe_note(j, ok);
1747 d_prev = dj;
1748 hx = hj;
1749 }
1750 m.kv.truncate_last(extra);
1751 } else {
1752 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1753 }
1754 }
1755 }
1756 hidden = h2;
1757 pos += 2;
1758 }
1759 }
1760 if batch_k > 0
1769 && graph_prefill
1770 && task_mask.is_none()
1771 && !self.o1_active()
1772 && mtp.is_none()
1773 && !dyn_prefill
1774 && pos + 1 < input_ids.len()
1775 {
1776 let hs = self.hidden_size;
1777 let chunk = batch_k;
1778 while pos < input_ids.len() {
1779 let end = (pos + chunk).min(input_ids.len());
1780 let bk = end - pos;
1781 let mut hiddens = vec![0f32; bk * hs];
1782 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1783 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1784 }
1785 let positions: Vec<usize> = (pos..end).collect();
1786 let t_chunk = std::time::Instant::now();
1787 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
1788 if std::env::var("CMF_GRAPH_PROF").is_ok() {
1789 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
1790 eprintln!(
1791 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
1792 bk as f64 / (ms / 1000.0)
1793 );
1794 }
1795 {
1796 use std::sync::atomic::{AtomicBool, Ordering};
1797 static SAID: AtomicBool = AtomicBool::new(false);
1798 if !SAID.swap(true, Ordering::Relaxed) {
1799 if ok_b {
1800 tracing::info!("batched prefill: ACTIVE (k={bk})");
1801 } else {
1802 tracing::warn!("batched prefill declined — per-position graph");
1803 }
1804 }
1805 }
1806 if ok_b {
1807 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1808 pos = end;
1809 } else {
1810 break; }
1812 }
1813 }
1814 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1815 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1816 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1817 if let Some(m) = &mut mtp {
1818 if pos + 1 < input_ids.len() {
1819 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1825 .ok()
1826 .and_then(|v| v.parse().ok())
1827 .unwrap_or(0);
1828 if probe >= 1 && pos + 2 < input_ids.len() {
1829 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
1830 let mut ok = d1 == input_ids[pos + 2];
1831 Self::chain_probe_note(0, ok);
1832 let mut d_prev = d1;
1833 let mut extra = 0usize;
1834 for j in 1..probe {
1835 if pos + 2 + j >= input_ids.len() {
1836 break;
1837 }
1838 let (dj, hj) =
1839 self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
1840 extra += 1;
1841 ok = ok && dj == input_ids[pos + 2 + j];
1842 Self::chain_probe_note(j, ok);
1843 d_prev = dj;
1844 hx = hj;
1845 }
1846 m.kv.truncate_last(extra);
1849 } else {
1850 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1851 }
1852 }
1853 }
1854 pos += 1;
1855 }
1856 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1857 eprintln!(
1858 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1859 input_ids.len(),
1860 _tpf.elapsed().as_secs_f64() * 1000.0
1861 );
1862 }
1863 if self
1866 .cancel
1867 .swap(false, std::sync::atomic::Ordering::Relaxed)
1868 {
1869 self.kv_history.clear();
1870 if let Some(m) = mtp {
1871 self.mtp = Some(m);
1872 }
1873 return Ok(GenerateResult {
1874 text: String::new(),
1875 token_ids: Vec::new(),
1876 prompt_tokens: input_ids.len(),
1877 tokens_generated: 0,
1878 finish_reason: "cancelled".to_string(),
1879 mtp_drafted: 0,
1880 mtp_accepted: 0,
1881 token_confidence: Vec::new(),
1882 traces: Vec::new(),
1883 });
1884 }
1885
1886 self.o1_seal();
1889
1890 macro_rules! commit {
1892 ($id:expr) => {{
1893 all_ids.push($id);
1894 generated += 1;
1895 if self.tokenizer.is_eos($id) {
1896 finish_reason = "stop".to_string();
1897 false
1898 } else {
1899 let token_text = self.tokenizer.decode_token($id);
1900 let mut go = true;
1901 if let Some(ref mut cb) = on_token {
1902 if !cb(&token_text) {
1903 finish_reason = "cancelled".to_string();
1904 go = false;
1905 }
1906 }
1907 go
1908 }
1909 }};
1910 }
1911
1912 let mut next_pos = input_ids.len();
1914 'decode: while generated < max_tokens {
1915 if self
1916 .cancel
1917 .swap(false, std::sync::atomic::Ordering::Relaxed)
1918 {
1919 finish_reason = "cancelled".to_string();
1920 break 'decode;
1921 }
1922 let mut logits = match self.graph_logits.take() {
1923 Some(lg) => lg,
1924 None => {
1925 inference::rms_norm_into(
1926 &hidden,
1927 &self.weights.final_norm,
1928 self.rms_eps,
1929 self.norm_style,
1930 &mut self.ws.n1,
1931 );
1932 self.lm_head_forward(&self.ws.n1)
1933 }
1934 };
1935 let t_next = sampler::sample_with_scratch(
1936 &logits,
1937 &self.sampler_config,
1938 &all_ids,
1939 &mut self.rng,
1940 &mut self.sampler_scratch,
1941 );
1942 if self.confidence_on {
1943 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1944 }
1945 attention::recycle_buf(&mut logits);
1946 if trace_on {
1947 let skill = router.as_ref().and_then(|r| r.active_id());
1951 traces.push(TokenTrace {
1952 t: generated,
1953 token_id: t_next,
1954 confidence: confidence.last().copied().unwrap_or(0.0),
1955 active_skill: skill,
1956 recon: None,
1957 switched: false,
1958 });
1959 }
1960 if !commit!(t_next) {
1961 break 'decode;
1962 }
1963 if generated >= max_tokens {
1964 break 'decode;
1965 }
1966
1967 if self.kv_cache.needs_eviction() {
1968 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1969 self.kv_cache.evict(keep);
1970 }
1971
1972 match &mut mtp {
1973 #[cfg(feature = "gpu")]
1975 Some(m) if graph_spec && generated + 1 < max_tokens && next_pos > 0 => {
1976 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
1977 m,
1978 &hidden,
1979 t_next,
1980 next_pos,
1981 &mut drafted,
1982 &mut accepted,
1983 ) {
1984 next_pos = n_pos;
1985 hidden = new_h;
1986 let mut stopped = false;
1987 for &id in &extra {
1988 if self.confidence_on {
1989 confidence.push(0.0);
1990 }
1991 if !commit!(id) {
1992 stopped = true;
1993 break;
1994 }
1995 }
1996 if stopped {
1997 break 'decode;
1998 }
1999 continue 'decode;
2000 }
2001 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2003 next_pos += 1;
2004 continue 'decode;
2005 }
2006 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2008 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2009 drafted += 1;
2010 let emb1 = self.embed_single(t_next);
2011 let emb2 = self.embed_single(draft);
2012 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2013
2014 inference::rms_norm_into(
2015 &h1,
2016 &self.weights.final_norm,
2017 self.rms_eps,
2018 self.norm_style,
2019 &mut self.ws.n1,
2020 );
2021 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2022 let t_after = sampler::sample_with_scratch(
2023 &logits1,
2024 &self.sampler_config,
2025 &all_ids,
2026 &mut self.rng,
2027 &mut self.sampler_scratch,
2028 );
2029 if self.confidence_on {
2030 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
2031 }
2032 attention::recycle_buf(&mut logits1);
2033 if trace_on {
2034 traces.push(TokenTrace {
2037 t: generated,
2038 token_id: t_after,
2039 confidence: confidence.last().copied().unwrap_or(0.0),
2040 active_skill: None,
2041 recon: None,
2042 switched: false,
2043 });
2044 }
2045 let stop = !commit!(t_after);
2046
2047 if t_after == draft {
2048 accepted += 1;
2049 self.commit_linear_scratch();
2050 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2051 hidden = h2;
2052 next_pos += 2;
2053 } else {
2054 for layer in &mut self.kv_cache.layers {
2056 layer.truncate_last(1);
2057 }
2058 if !stop {
2059 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2060 hidden = self.forward_layers(
2061 &self.embed_single(t_after),
2062 next_pos + 1,
2063 None,
2064 );
2065 }
2066 next_pos += 2;
2067 }
2068 if stop {
2069 break 'decode;
2070 }
2071 }
2072 _ => {
2074 #[cfg(feature = "gpu")]
2079 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2080 static SAID: std::sync::Once = std::sync::Once::new();
2081 SAID.call_once(|| {
2082 eprintln!(
2083 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2084 !self.dsv4_mtp.is_empty(),
2085 task_mask.is_none(),
2086 router.is_none(),
2087 !trace_on,
2088 self.sampler_config.temperature < 1e-6,
2089 self.sampler_config.repetition_penalty == 1.0,
2090 );
2091 });
2092 }
2093 #[cfg(feature = "gpu")]
2094 if Self::dsv4_spec_on()
2095 && self.dsv4.is_some()
2096 && !self.dsv4_mtp.is_empty()
2097 && task_mask.is_none()
2098 && router.is_none()
2099 && !trace_on
2100 && self.sampler_config.temperature < 1e-6
2101 && self.sampler_config.repetition_penalty == 1.0
2102 && generated + 1 < max_tokens
2103 && all_ids.len() >= 2
2104 {
2105 let tip_token = all_ids[all_ids.len() - 2];
2106 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2107 tip_token,
2108 t_next,
2109 next_pos,
2110 &mut drafted,
2111 &mut accepted,
2112 ) {
2113 next_pos = n_pos;
2114 let mut stopped = false;
2115 for &id in &extra {
2116 if self.confidence_on {
2117 confidence.push(0.0);
2118 }
2119 if !commit!(id) {
2120 stopped = true;
2121 break;
2122 }
2123 }
2124 if stopped {
2125 break 'decode;
2126 }
2127 continue 'decode;
2128 }
2129 }
2130 self.graph_want_logits = fuse_lm;
2131 let mut t_fwd = t_next;
2137 let pure_greedy = self.sampler_config.temperature < 1e-6
2138 && self.sampler_config.repetition_penalty == 1.0
2139 && self.sampler_config.suppress_tokens.is_empty();
2140 let burst_k = std::env::var("CMF_MULTISTEP")
2145 .ok()
2146 .and_then(|v| v.parse::<usize>().ok())
2147 .unwrap_or(0);
2148 if pure_greedy
2149 && burst_k >= 1
2150 && fuse_lm
2151 && task_mask.is_none()
2152 && router.is_none()
2153 && !trace_on
2154 && !self.confidence_on
2155 {
2156 let mut stopped = false;
2157 loop {
2158 let room = max_tokens.saturating_sub(generated);
2159 if room <= 2 {
2160 break;
2161 }
2162 let k = burst_k.min(room - 1);
2163 if k < 1 {
2164 break;
2165 }
2166 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2167 break;
2168 };
2169 next_pos += k;
2170 for &id in &ids {
2171 if !commit!(id) {
2172 stopped = true;
2173 break;
2174 }
2175 }
2176 if stopped {
2177 break;
2178 }
2179 t_fwd = *ids.last().unwrap();
2180 }
2181 if stopped {
2182 break 'decode;
2183 }
2184 }
2185 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2186 next_pos += 1;
2187 if let Some(r) = &mut router {
2190 let phi = self.dyn_phi_ema.clone();
2191 let decision = r.step(&phi, generated);
2192 if let Some(new_active) = decision {
2193 let _ = self.set_active_skill(new_active);
2194 }
2195 if trace_on {
2198 if let Some(last) = traces.last_mut() {
2199 let e = r.last_best_e();
2200 last.recon = e.is_finite().then_some(e);
2201 last.switched = decision.is_some();
2202 }
2203 }
2204 }
2205 }
2206 }
2207 }
2208
2209 self.graph_want_logits = false;
2210 self.graph_logits = None;
2211 if router.is_some() {
2213 let _ = self.set_active_skill(None);
2214 }
2215 self.dyn_router = router.or(self.dyn_router.take());
2216 self.mtp = mtp.or(self.mtp.take());
2217
2218 let output_ids = &all_ids[input_ids.len()..];
2219 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2223 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2224 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2226 Ok(GenerateResult {
2227 text: self.tokenizer.decode(output_ids),
2228 token_ids: output_ids.to_vec(),
2229 prompt_tokens: input_ids.len(),
2230 tokens_generated: generated,
2231 finish_reason,
2232 mtp_drafted: drafted,
2233 mtp_accepted: accepted,
2234 token_confidence: confidence,
2235 traces,
2236 })
2237 }
2238
2239 fn mtp_step(
2243 &mut self,
2244 m: &mut MtpModule,
2245 hidden: &[f32],
2246 next_token: u32,
2247 position: usize,
2248 ) -> u32 {
2249 self.mtp_step_h(m, hidden, next_token, position).0
2250 }
2251
2252 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2256 use std::sync::Mutex;
2257 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2258 let mut t = T.lock().unwrap();
2259 if t.len() <= depth {
2260 t.resize(depth + 1, (0, 0));
2261 }
2262 t[depth].0 += 1;
2263 t[depth].1 += prefix_ok as u64;
2264 if depth == 0 && t[0].0 % 128 == 0 {
2265 let line: Vec<String> = t
2266 .iter()
2267 .enumerate()
2268 .map(|(d, (n, k))| format!("d{}={:.0}%({n})", d + 1, 100.0 * *k as f64 / (*n).max(1) as f64))
2269 .collect();
2270 eprintln!("mtp-chain: {}", line.join(" "));
2271 }
2272 }
2273
2274 fn mtp_step_h(
2278 &mut self,
2279 m: &mut MtpModule,
2280 hidden: &[f32],
2281 next_token: u32,
2282 position: usize,
2283 ) -> (u32, Vec<f32>) {
2284 let e = self.embed_single(next_token);
2288 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2289 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2290 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2291 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2292 let mut x = vec![0.0f32; self.hidden_size];
2293 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2294
2295 let lw = &m.layer;
2297 inference::rms_norm_into(
2298 &x,
2299 &lw.input_norm,
2300 self.rms_eps,
2301 self.norm_style,
2302 &mut self.ws.n1,
2303 );
2304 let attn = match &lw.attn {
2305 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2307 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2308 AttnKind::Full {
2309 wq,
2310 wk,
2311 wv,
2312 wo,
2313 q_norm,
2314 k_norm,
2315 output_gate,
2316 softplus_gate,
2317 bias,
2318 } => {
2319 let mut cfg = self.attn_cfg(position);
2320 cfg.q_norm = q_norm.as_deref();
2321 cfg.k_norm = k_norm.as_deref();
2322 cfg.output_gate = *output_gate;
2323 cfg.softplus_gate = softplus_gate
2324 .as_ref()
2325 .map(|(gate, per_head)| (gate, *per_head));
2326 cfg.bias = bias
2327 .as_ref()
2328 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2329 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2330 }
2331 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
2332 unreachable!("MTP block is full attention")
2333 }
2334 };
2335 for (i, &a) in attn.iter().enumerate() {
2336 x[i] += a;
2337 }
2338 inference::rms_norm_into(
2339 &x,
2340 &lw.post_norm,
2341 self.rms_eps,
2342 self.norm_style,
2343 &mut self.ws.p1,
2344 );
2345 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
2346 for (i, &f) in ffn.iter().enumerate() {
2347 x[i] += f;
2348 }
2349
2350 inference::rms_norm_into(
2351 &x,
2352 &m.final_norm,
2353 self.rms_eps,
2354 self.norm_style,
2355 &mut self.ws.n1,
2356 );
2357 let mut lg = self.lm_head_forward(&self.ws.n1);
2358 let draft = sampler::argmax(&lg);
2359 attention::recycle_buf(&mut lg);
2360 (draft, x)
2361 }
2362
2363 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
2367 let e = self.embed_single(next_token);
2368 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2369 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2370 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2371 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2372 let mut x = vec![0.0f32; self.hidden_size];
2373 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2374 inference::rms_norm_into(&x, &m.layer.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
2375 let attn = match &m.layer.attn {
2376 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate, bias } => {
2377 let mut cfg = self.attn_cfg(position);
2378 cfg.q_norm = q_norm.as_deref();
2379 cfg.k_norm = k_norm.as_deref();
2380 cfg.output_gate = *output_gate;
2381 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
2382 cfg.bias = bias.as_ref().map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2383 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2384 }
2385 _ => return,
2386 };
2387 let _ = attn;
2388 }
2389
2390 #[cfg(feature = "gpu")]
2397 #[allow(clippy::too_many_arguments)]
2398 fn graph_spec_step(
2399 &mut self,
2400 m: &mut MtpModule,
2401 hidden: &[f32],
2402 t_next: u32,
2403 next_pos: usize,
2404 drafted: &mut usize,
2405 accepted: &mut usize,
2406 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
2407 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
2408 .ok()
2409 .and_then(|v| v.parse().ok())
2410 .filter(|&v| (1..=8).contains(&v))
2411 .unwrap_or(2);
2412 if next_pos == 0 {
2413 return None;
2414 }
2415 let t_round = std::time::Instant::now();
2416 let mut drafts = Vec::with_capacity(k_spec);
2421 let (d1, mut hx) = self.mtp_step_h(m, hidden, t_next, next_pos - 1);
2422 drafts.push(d1);
2423 for j in 1..k_spec {
2424 let (dj, hj) = self.mtp_step_h(m, &hx, drafts[j - 1], next_pos - 1 + j);
2425 drafts.push(dj);
2426 hx = hj;
2427 }
2428 *drafted += k_spec;
2429 let t_draft = t_round.elapsed();
2430 let b = k_spec + 1;
2433 let mut hiddens = vec![0.0f32; b * self.hidden_size];
2434 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
2435 let e = self.embed_single(t);
2436 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
2437 }
2438 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
2439 let (lm_gw, lm_rows) = {
2440 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
2441 (
2442 crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[] },
2443 self.weights.lm_head.rows(),
2444 )
2445 };
2446 let mut logits = Vec::new();
2447 let final_norm = self.weights.final_norm.clone();
2448 let ok = self.try_batch_graph_wgpu(
2449 &mut hiddens,
2450 &positions,
2451 b,
2452 Some(crate::gpu::SpecTail {
2453 lm: lm_gw,
2454 lm_rows,
2455 final_norm: &final_norm,
2456 logits_out: &mut logits,
2457 }),
2458 );
2459 if !ok {
2460 m.kv.truncate_last(k_spec);
2463 return None;
2464 }
2465 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2466 eprintln!(
2467 "spec-round: draft {:.1} ms | verify {:.1} ms",
2468 t_draft.as_secs_f64() * 1e3,
2469 (t_round.elapsed() - t_draft).as_secs_f64() * 1e3,
2470 );
2471 }
2472 let ids: Vec<u32> = (0..b)
2474 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
2475 .collect();
2476 let mut a = 0usize;
2477 while a < k_spec && ids[a] == drafts[a] {
2478 a += 1;
2479 }
2480 if a + 1 < b {
2482 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
2483 }
2484 *accepted += a;
2485 m.kv.truncate_last(k_spec.saturating_sub(1));
2488 for j in 0..a {
2489 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
2490 let row = row.to_vec();
2491 self.mtp_warm(m, &row, ids[j], next_pos + j);
2492 }
2493 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
2495 row.resize(self.vocab_size, 0.0);
2496 if let Some(c) = self.final_softcap {
2497 for l in row.iter_mut() {
2498 *l = c * (*l / c).tanh();
2499 }
2500 }
2501 self.graph_logits = Some(row);
2502 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
2503 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
2504 }
2505
2506 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
2515 if !self.pair_supported() {
2516 return (0.0, 0.0);
2517 }
2518 let emb1 = self.embed_single(1);
2519 let emb2 = self.embed_single(2);
2520 let pos = self.kv_cache.seq_len();
2521
2522 let t0 = std::time::Instant::now();
2523 for _ in 0..iters {
2524 let _ = self.forward_layers(&emb1, pos, None);
2525 let _ = self.forward_layers(&emb2, pos + 1, None);
2526 for l in &mut self.kv_cache.layers {
2527 l.truncate_last(2);
2528 }
2529 }
2530 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2531
2532 let t1 = std::time::Instant::now();
2533 for _ in 0..iters {
2534 let _ = self.forward_pair(&emb1, &emb2, pos);
2535 for l in &mut self.kv_cache.layers {
2536 l.truncate_last(2);
2537 }
2538 }
2539 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2540 (singles_ms, pair_ms)
2541 }
2542
2543 fn pair_supported(&self) -> bool {
2551 !self.weights.layers.is_empty()
2558 && self.g3n.is_none()
2559 && !self
2560 .weights
2561 .layers
2562 .iter()
2563 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
2564 }
2565
2566 fn forward_pair(
2567 &mut self,
2568 emb1: &[f32],
2569 emb2: &[f32],
2570 position: usize,
2571 ) -> (Vec<f32>, Vec<f32>) {
2572 let mut h1 = emb1.to_vec();
2573 let mut h2 = emb2.to_vec();
2574 let (_nkv, _hd, hs, _rd, eps) = (
2575 self.num_kv_heads,
2576 self.head_dim,
2577 self.hidden_size,
2578 self.rotary_dim,
2579 self.rms_eps,
2580 );
2581 let pool = self.pool.clone();
2582
2583 for li in 0..self.num_layers {
2584 let lw = &self.weights.layers[self.phys_layer(li)];
2585 inference::rms_norm_into(
2588 &h1,
2589 &lw.input_norm,
2590 self.rms_eps,
2591 self.norm_style,
2592 &mut self.ws.n1,
2593 );
2594 inference::rms_norm_into(
2595 &h2,
2596 &lw.input_norm,
2597 self.rms_eps,
2598 self.norm_style,
2599 &mut self.ws.n2,
2600 );
2601
2602 let (a1, a2) = match &lw.attn {
2603 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2604 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2605 AttnKind::Linear(w) => {
2606 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2607 let layer = &mut self.kv_cache.layers[li];
2608 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2609 vmf_phase_pair(
2610 &self.ws.n1,
2611 &self.ws.n2,
2612 w,
2613 &cfg,
2614 state,
2615 scratch,
2616 self.pool.as_deref(),
2617 )
2618 }
2619 AttnKind::LinearGdn(w) => {
2620 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2621 let layer = &mut self.kv_cache.layers[li];
2622 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2623 gdn_pair(
2624 &self.ws.n1,
2625 &self.ws.n2,
2626 w,
2627 &cfg,
2628 state,
2629 scratch,
2630 self.pool.as_deref(),
2631 )
2632 }
2633 AttnKind::ShortConv(w) => {
2634 let cfg = self
2635 .short_conv_cfg
2636 .expect("short-conv layer without short_conv_cfg");
2637 let layer = &mut self.kv_cache.layers[li];
2638 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2639 short_conv_pair(
2640 &self.ws.n1,
2641 &self.ws.n2,
2642 w,
2643 &cfg,
2644 state,
2645 scratch,
2646 self.pool.as_deref(),
2647 )
2648 }
2649 AttnKind::Full {
2650 wq,
2651 wk,
2652 wv,
2653 wo,
2654 q_norm,
2655 k_norm,
2656 output_gate,
2657 softplus_gate,
2658 bias,
2659 } => {
2660 let inv_freq_l = self.layer_inv_freq(li);
2661 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2662 let cfg = QwenAttnCfg {
2663 num_heads: self.layer_num_heads(li),
2664 num_kv_heads: nkv_l,
2665 head_dim: hd_l,
2666 hidden_size: hs,
2667 position,
2668 inv_freq: &inv_freq_l,
2669 rotary_dim: rd_l,
2670 scale: self.attn_scale,
2671 softcap: self.attn_softcap,
2672 window: self.layer_window(li),
2673 v_norm: self.attn_v_norm,
2674 q_norm: q_norm.as_deref(),
2675 k_norm: k_norm.as_deref(),
2676 output_gate: *output_gate,
2677 softplus_gate: softplus_gate
2678 .as_ref()
2679 .map(|(gate, per_head)| (gate, *per_head)),
2680 rope_scale: self.layer_rope_scale(li),
2681 bias: bias
2682 .as_ref()
2683 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2684 rms_eps: eps,
2685 norm_style: self.norm_style,
2686 pool: pool.as_deref(),
2687 };
2688 attention::qwen_attention_pair(
2689 &self.ws.n1,
2690 &self.ws.n2,
2691 wq,
2692 wk,
2693 wv,
2694 wo,
2695 &mut self.kv_cache.layers[li],
2696 &cfg,
2697 )
2698 }
2699 };
2700 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
2701 Some(w) => (
2702 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
2703 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
2704 ),
2705 None => (a1, a2),
2706 };
2707 for i in 0..self.hidden_size {
2708 h1[i] += a1[i];
2709 h2[i] += a2[i];
2710 }
2711 let (mut a1, mut a2) = (a1, a2);
2712 attention::recycle_buf(&mut a1);
2713 attention::recycle_buf(&mut a2);
2714
2715 let lw = &self.weights.layers[self.phys_layer(li)];
2716 inference::rms_norm_into(
2717 &h1,
2718 &lw.post_norm,
2719 self.rms_eps,
2720 self.norm_style,
2721 &mut self.ws.p1,
2722 );
2723 inference::rms_norm_into(
2724 &h2,
2725 &lw.post_norm,
2726 self.rms_eps,
2727 self.norm_style,
2728 &mut self.ws.p2,
2729 );
2730 let (f1, f2) = match &lw.ffn {
2731 FfnKind::DenseMoe(dm) => (
2734 dense_moe_ffn(
2735 dm,
2736 &self.ws.p1,
2737 &h1,
2738 self.rms_eps,
2739 self.norm_style,
2740 self.pool.as_deref(),
2741 ),
2742 dense_moe_ffn(
2743 dm,
2744 &self.ws.p2,
2745 &h2,
2746 self.rms_eps,
2747 self.norm_style,
2748 self.pool.as_deref(),
2749 ),
2750 ),
2751 _ => ffn_forward_pair(
2752 &lw.ffn,
2753 &self.ws.p1,
2754 &self.ws.p2,
2755 self.pool.as_deref(),
2756 None,
2757 ),
2758 };
2759 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
2760 Some(w) => (
2761 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
2762 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
2763 ),
2764 None => (f1, f2),
2765 };
2766 for i in 0..self.hidden_size {
2767 h1[i] += f1[i];
2768 h2[i] += f2[i];
2769 }
2770 let (mut f1, mut f2) = (f1, f2);
2771 attention::recycle_buf(&mut f1);
2772 attention::recycle_buf(&mut f2);
2773 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
2774 for i in 0..self.hidden_size {
2775 h1[i] *= sc;
2776 h2[i] *= sc;
2777 }
2778 }
2779 if self.is_loop_end(li) && li + 1 < self.num_layers {
2781 h1 = inference::rms_norm(
2782 &h1,
2783 &self.weights.final_norm,
2784 self.rms_eps,
2785 self.norm_style,
2786 );
2787 h2 = inference::rms_norm(
2788 &h2,
2789 &self.weights.final_norm,
2790 self.rms_eps,
2791 self.norm_style,
2792 );
2793 }
2794 }
2795 (h1, h2)
2796 }
2797
2798 fn commit_linear_scratch(&mut self) {
2800 for layer in &mut self.kv_cache.layers {
2801 if !layer.linear_scratch.is_empty() {
2802 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
2803 layer.linear_scratch.clear();
2804 }
2805 }
2806 }
2807
2808 pub fn forward_ids(
2811 &mut self,
2812 ids: &[u32],
2813 task_mask: Option<&TaskMask>,
2814 ) -> Result<Vec<f32>, String> {
2815 if ids.is_empty() {
2816 return Err("empty id sequence".to_string());
2817 }
2818 self.kv_cache.clear();
2819 self.kv_history.clear();
2820 self.o1_begin();
2821 let mut hidden = vec![0.0f32; self.hidden_size];
2822 let mut pos = 0usize;
2823 if task_mask.is_none() && self.can_prefill_batched() && ids.len() > 2 {
2824 let chunk = prefill_chunk();
2828 let hs = self.hidden_size;
2829 while pos < ids.len() {
2830 let end = (pos + chunk).min(ids.len());
2831 let hb = self.prefill_batch(&ids[pos..end], pos);
2832 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2833 pos = end;
2834 }
2835 }
2836 if task_mask.is_none()
2840 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
2841 && self.pair_supported()
2842 {
2843 while pos + 1 < ids.len() {
2844 let e1 = self.embed_single(ids[pos]);
2845 let e2 = self.embed_single(ids[pos + 1]);
2846 let (_, h2) = self.forward_pair(&e1, &e2, pos);
2847 self.commit_linear_scratch();
2848 hidden = h2;
2849 pos += 2;
2850 }
2851 }
2852 while pos < ids.len() {
2853 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2854 pos += 1;
2855 }
2856 self.o1_seal();
2860 let normed = inference::rms_norm(
2861 &hidden,
2862 &self.weights.final_norm,
2863 self.rms_eps,
2864 self.norm_style,
2865 );
2866 Ok(self.lm_head_forward(&normed))
2867 }
2868
2869 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
2876 let (nll, cnt) = self.nll_ids_from(ids, 0);
2877 (nll / cnt.max(1) as f64).exp()
2878 }
2879
2880 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2885 self.kv_cache.clear();
2886 self.kv_history.clear();
2887 FFN_PROBE.with(|p| {
2888 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2889 });
2890 crate::gpu::cpu_scope(|| {
2891 for (pos, &id) in ids.iter().enumerate() {
2892 let emb = self.embed_single(id);
2893 let _ = self.forward_layers(&emb, pos, None);
2894 }
2895 });
2896 self.kv_cache.clear();
2897 self.kv_history.clear();
2898 FFN_PROBE
2899 .with(|p| p.borrow_mut().take())
2900 .unwrap_or_default()
2901 }
2902
2903 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2907 self.kv_cache.clear();
2908 self.kv_history.clear();
2909 let mut nll = 0f64;
2910 let mut cnt = 0usize;
2911 let mut hidden = vec![0f32; self.hidden_size];
2912 for (pos, &id) in ids.iter().enumerate() {
2913 if pos > 0 {
2914 inference::rms_norm_into(
2915 &hidden,
2916 &self.weights.final_norm,
2917 self.rms_eps,
2918 self.norm_style,
2919 &mut self.ws.n1,
2920 );
2921 let mut logits = self.lm_head_forward(&self.ws.n1);
2922 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2923 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2924 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2925 nll -= p.max(1e-300).ln();
2926 cnt += 1;
2927 attention::recycle_buf(&mut logits);
2928 }
2929 let emb = self.embed_single(id);
2930 hidden = self.forward_layers(&emb, pos, Some(mask));
2931 }
2932 self.kv_cache.clear();
2933 self.kv_history.clear();
2934 (nll / cnt.max(1) as f64).exp()
2935 }
2936
2937 pub fn nll_ids_masked(
2952 &mut self,
2953 ids: &[u32],
2954 start: usize,
2955 task_mask: Option<&TaskMask>,
2956 ) -> (f64, usize) {
2957 if task_mask.is_none() {
2958 return self.nll_ids_from(ids, start);
2959 }
2960 self.kv_cache.clear();
2961 self.kv_history.clear();
2962 let mut nll = 0f64;
2963 let mut cnt = 0usize;
2964 let n = ids.len().saturating_sub(1);
2965 let hs = self.hidden_size;
2966 let rows = self.weights.lm_head.rows();
2967 let voc = self.vocab_size.min(rows);
2968 for pos in 0..n {
2969 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2970 if pos < start {
2971 continue;
2972 }
2973 let normed = inference::rms_norm(
2974 &hidden[..hs],
2975 &self.weights.final_norm,
2976 self.rms_eps,
2977 self.norm_style,
2978 );
2979 let mut logits = vec![0.0f32; rows];
2980 self.weights
2981 .lm_head
2982 .matmat(&normed, 1, &mut logits, self.pool.as_deref());
2983 let lg = &mut logits[..voc];
2984 if let Some(mu) = self.logit_multiplier {
2985 for v in lg.iter_mut() {
2986 *v *= mu;
2987 }
2988 }
2989 if let Some(c) = self.final_softcap {
2990 for v in lg.iter_mut() {
2991 *v = c * (*v / c).tanh();
2992 }
2993 }
2994 let target = ids[pos + 1] as usize;
2995 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2996 let lse: f64 = lg.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
2997 + max as f64;
2998 nll += lse - lg[target.min(voc - 1)] as f64;
2999 cnt += 1;
3000 }
3001 (nll, cnt)
3002 }
3003
3004 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
3005 self.kv_cache.clear();
3006 self.kv_history.clear();
3007 let mut nll = 0f64;
3008 let mut cnt = 0usize;
3009 if self.can_prefill_batched() {
3010 const CHUNK: usize = 128;
3016 const LM_SUB: usize = 32;
3017 let n = ids.len().saturating_sub(1);
3018 let hs = self.hidden_size;
3019 let rows = self.weights.lm_head.rows();
3020 let mut pos = 0usize;
3021 while pos < n {
3022 let end = (pos + CHUNK).min(n);
3023 let bsz = end - pos;
3024 let hb = self.prefill_batch(&ids[pos..end], pos);
3025 let mut k0 = 0usize;
3026 while k0 < bsz {
3027 let k1 = (k0 + LM_SUB).min(bsz);
3028 let sb = k1 - k0;
3029 if pos + k1 <= start {
3032 k0 = k1;
3033 continue;
3034 }
3035 let mut normed = vec![0.0f32; sb * hs];
3036 for k in 0..sb {
3037 let r = inference::rms_norm(
3038 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
3039 &self.weights.final_norm,
3040 self.rms_eps,
3041 self.norm_style,
3042 );
3043 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
3044 }
3045 let mut logits = vec![0.0f32; sb * rows];
3046 self.weights
3047 .lm_head
3048 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
3049 for k in 0..sb {
3050 if pos + k0 + k < start {
3051 continue;
3052 }
3053 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
3054 if let Some(mu) = self.logit_multiplier {
3055 for v in lg.iter_mut() {
3056 *v *= mu;
3057 }
3058 }
3059 if let Some(c) = self.final_softcap {
3063 for v in lg.iter_mut() {
3064 *v = c * (*v / c).tanh();
3065 }
3066 }
3067 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
3068 let target = ids[pos + k0 + k + 1] as usize;
3069 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3070 let lse: f64 = lg
3071 .iter()
3072 .map(|&v| ((v - max) as f64).exp())
3073 .sum::<f64>()
3074 .ln()
3075 + max as f64;
3076 nll += lse - lg[target] as f64;
3077 cnt += 1;
3078 if std::env::var("CMF_PPL_TRACE").is_ok() {
3079 let top = lg
3080 .iter()
3081 .enumerate()
3082 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3083 .map(|(i, _)| i)
3084 .unwrap_or(0);
3085 eprintln!(
3086 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
3087 pos + k0 + k,
3088 target,
3089 lse - lg[target] as f64,
3090 top,
3091 lg[target],
3092 lg[top]
3093 );
3094 }
3095 }
3096 k0 = k1;
3097 }
3098 pos = end;
3099 }
3100 self.kv_cache.clear();
3101 self.kv_history.clear();
3102 return (nll, cnt);
3103 }
3104 for pos in 0..ids.len().saturating_sub(1) {
3105 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3106 let out_of_band = self.graph_logits.take();
3114 if pos < start {
3115 continue;
3116 }
3117 let logits = match out_of_band {
3118 Some(lg) => lg,
3119 None => {
3120 let normed = inference::rms_norm(
3121 &hidden,
3122 &self.weights.final_norm,
3123 self.rms_eps,
3124 self.norm_style,
3125 );
3126 self.lm_head_forward(&normed)
3130 }
3131 };
3132 let target = ids[pos + 1] as usize;
3133 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3134 let lse: f64 = logits
3135 .iter()
3136 .map(|&v| ((v - max) as f64).exp())
3137 .sum::<f64>()
3138 .ln()
3139 + max as f64;
3140 let tok_nll = lse - logits[target] as f64;
3141 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3142 let top = logits
3143 .iter()
3144 .enumerate()
3145 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3146 .map(|(i, _)| i)
3147 .unwrap_or(0);
3148 eprintln!(
3149 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3150 logits[target], logits[top]
3151 );
3152 }
3153 nll += tok_nll;
3154 cnt += 1;
3155 }
3156 self.kv_cache.clear();
3157 self.kv_history.clear();
3158 (nll, cnt)
3159 }
3160
3161 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
3177 self.kv_cache.clear();
3178 self.kv_history.clear();
3179 self.o1_begin();
3180 let n = ids.len().saturating_sub(1);
3181 let p = prefill.min(n);
3182 let mut pos = 0usize;
3184 if self.can_prefill_batched() {
3185 const CHUNK: usize = 128;
3186 while pos < p {
3187 let end = (pos + CHUNK).min(p);
3188 let _ = self.prefill_batch(&ids[pos..end], pos);
3189 pos = end;
3190 }
3191 } else {
3192 while pos < p {
3193 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3194 pos += 1;
3195 }
3196 }
3197 self.o1_seal();
3198
3199 let mut nll = 0f64;
3200 let mut cnt = 0usize;
3201 for pos in p..n {
3202 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3203 let normed = inference::rms_norm(
3204 &hidden,
3205 &self.weights.final_norm,
3206 self.rms_eps,
3207 self.norm_style,
3208 );
3209 let logits = self.lm_head_forward(&normed);
3213 let target = ids[pos + 1] as usize;
3214 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3215 let lse: f64 = logits
3216 .iter()
3217 .map(|&v| ((v - max) as f64).exp())
3218 .sum::<f64>()
3219 .ln()
3220 + max as f64;
3221 let tok_nll = lse - logits[target] as f64;
3222 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3223 let top = logits
3224 .iter()
3225 .enumerate()
3226 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3227 .map(|(i, _)| i)
3228 .unwrap_or(0);
3229 eprintln!(
3230 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3231 logits[target], logits[top]
3232 );
3233 }
3234 nll += tok_nll;
3235 cnt += 1;
3236 }
3237 self.kv_cache.clear();
3238 self.kv_history.clear();
3239 (nll, cnt)
3240 }
3241
3242 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
3250 self.kv_cache.clear();
3251 self.kv_history.clear();
3252 let n = ids.len().saturating_sub(1);
3253 let mut correct = Vec::with_capacity(n);
3254 let mut pmax = Vec::with_capacity(n);
3255 for pos in 0..n {
3256 let emb = self.embed_single(ids[pos]);
3257 let hidden = self.forward_layers(&emb, pos, None);
3258 let normed = inference::rms_norm(
3259 &hidden,
3260 &self.weights.final_norm,
3261 self.rms_eps,
3262 self.norm_style,
3263 );
3264 let logits = self.lm_head_forward(&normed);
3268 let target = ids[pos + 1] as usize;
3269 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
3270 for (i, &v) in logits.iter().enumerate() {
3271 if v > mval {
3272 mval = v;
3273 amax = i;
3274 }
3275 }
3276 correct.push(amax == target);
3277 let row: Vec<f32> = temps
3278 .iter()
3279 .map(|&t| {
3280 let tt = t.max(1e-3);
3281 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
3282 1.0 / s.max(1e-12) })
3284 .collect();
3285 pmax.push(row);
3286 }
3287 self.kv_cache.clear();
3288 self.kv_history.clear();
3289 (correct, pmax)
3290 }
3291
3292 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
3299 let mut router = match self.dyn_router.take() {
3300 Some(r) => r,
3301 None => return (self.ppl_ids(ids), 0),
3302 };
3303 router.reset();
3304 self.dyn_phi_seen = 0;
3305 let _ = self.set_active_skill(None);
3306
3307 self.kv_cache.clear();
3308
3309 self.kv_history.clear();
3310 let mut nll = 0f64;
3311 let mut cnt = 0usize;
3312 for pos in 0..ids.len().saturating_sub(1) {
3313 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3314 let normed = inference::rms_norm(
3315 &hidden,
3316 &self.weights.final_norm,
3317 self.rms_eps,
3318 self.norm_style,
3319 );
3320 let logits = self.lm_head_forward(&normed);
3324 let target = ids[pos + 1] as usize;
3325 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3326 let lse: f64 = logits
3327 .iter()
3328 .map(|&v| ((v - max) as f64).exp())
3329 .sum::<f64>()
3330 .ln()
3331 + max as f64;
3332 let tok_nll = lse - logits[target] as f64;
3333 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3334 let top = logits
3335 .iter()
3336 .enumerate()
3337 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3338 .map(|(i, _)| i)
3339 .unwrap_or(0);
3340 eprintln!(
3341 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3342 logits[target], logits[top]
3343 );
3344 }
3345 nll += tok_nll;
3346 cnt += 1;
3347 let phi = self.dyn_phi_ema.clone();
3349 if let Some(new_active) = router.step(&phi, pos) {
3350 let _ = self.set_active_skill(new_active);
3351 }
3352 }
3353 let switches = router.switches.len();
3354 let _ = self.set_active_skill(None);
3355 self.dyn_router = Some(router);
3356 self.kv_cache.clear();
3357 self.kv_history.clear();
3358 ((nll / cnt.max(1) as f64).exp(), switches)
3359 }
3360
3361 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
3363 self.kv_cache.clear();
3364 self.kv_history.clear();
3365 let mut acc = vec![0f32; self.hidden_size];
3366 for (pos, &id) in ids.iter().enumerate() {
3367 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
3368 for (a, v) in acc.iter_mut().zip(&h) {
3369 *a += v;
3370 }
3371 }
3372 let n = ids.len().max(1) as f32;
3373 for a in acc.iter_mut() {
3374 *a /= n;
3375 }
3376 self.kv_cache.clear();
3377 self.kv_history.clear();
3378 acc
3379 }
3380
3381 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
3387 let b = ids.len();
3388 let hs = self.hidden_size;
3389 let mut h: Vec<f32> = vec![0.0; b * hs];
3392 let mut h_ready = false;
3393 let fill_h = |h: &mut Vec<f32>, me: &Self| {
3394 for (bi, &id) in ids.iter().enumerate() {
3395 let e = me.embed_single(id);
3396 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
3397 }
3398 };
3399 let (_nkv, _hd, _rd, eps) = (
3400 self.num_kv_heads,
3401 self.head_dim,
3402 self.rotary_dim,
3403 self.rms_eps,
3404 );
3405 let pool = self.pool.clone();
3406 let norm_style = self.norm_style;
3407
3408 #[cfg(target_os = "macos")]
3409 let mut chunk_skip_until = 0usize;
3410 for li in 0..self.num_layers {
3411 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
3418 {
3419 if li < chunk_skip_until {
3420 continue;
3421 }
3422 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
3428 fill_h(&mut h, self);
3429 h_ready = true;
3430 }
3431 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
3432 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
3433 if end > li {
3434 h_ready = true;
3435 chunk_skip_until = end;
3436 if self.is_loop_end(end - 1) && end < self.num_layers {
3439 for bi in 0..b {
3440 let normed = inference::rms_norm(
3441 &h[bi * hs..(bi + 1) * hs],
3442 &self.weights.final_norm,
3443 eps,
3444 norm_style,
3445 );
3446 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3447 }
3448 }
3449 continue;
3450 }
3451 }
3452 if !h_ready {
3453 fill_h(&mut h, self);
3454 h_ready = true;
3455 }
3456 let lw = &self.weights.layers[self.phys_layer(li)];
3457 match &lw.attn {
3459 AttnKind::Kda(w) => {
3460 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
3462 let mut normed = vec![0.0f32; b * hs];
3463 for bi in 0..b {
3464 inference::rms_norm_into(
3465 &h[bi * hs..(bi + 1) * hs],
3466 &lw.input_norm,
3467 eps,
3468 norm_style,
3469 &mut normed[bi * hs..(bi + 1) * hs],
3470 );
3471 }
3472 let attn = crate::linear_core::kda_forward_batch(
3473 &normed,
3474 b,
3475 w,
3476 &cfg,
3477 &mut self.kv_cache.layers[li].linear_state,
3478 pool.as_deref(),
3479 );
3480 for (dst, &a) in h.iter_mut().zip(&attn) {
3481 *dst += a;
3482 }
3483 }
3484 AttnKind::LinearGdn(w) => {
3485 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3487 let mut normed = vec![0.0f32; b * hs];
3488 for bi in 0..b {
3489 let r = inference::rms_norm(
3490 &h[bi * hs..(bi + 1) * hs],
3491 &lw.input_norm,
3492 eps,
3493 norm_style,
3494 );
3495 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3496 }
3497 let attn = crate::linear_core::gdn_forward_batch(
3498 &normed,
3499 b,
3500 w,
3501 &cfg,
3502 &mut self.kv_cache.layers[li].linear_state,
3503 pool.as_deref(),
3504 );
3505 for (dst, &a) in h.iter_mut().zip(&attn) {
3506 *dst += a;
3507 }
3508 }
3509 AttnKind::ShortConv(w) => {
3510 let cfg = self
3513 .short_conv_cfg
3514 .expect("short-conv layer without short_conv_cfg");
3515 let mut normed = vec![0.0f32; b * hs];
3516 for bi in 0..b {
3517 inference::rms_norm_into(
3518 &h[bi * hs..(bi + 1) * hs],
3519 &lw.input_norm,
3520 eps,
3521 norm_style,
3522 &mut normed[bi * hs..(bi + 1) * hs],
3523 );
3524 }
3525 let attn = short_conv_forward_batch(
3526 &normed,
3527 b,
3528 w,
3529 &cfg,
3530 &mut self.kv_cache.layers[li].linear_state,
3531 pool.as_deref(),
3532 );
3533 for (dst, &a) in h.iter_mut().zip(&attn) {
3534 *dst += a;
3535 }
3536 }
3537 AttnKind::Mla(w) => {
3538 let inv_freq_l = self.layer_inv_freq(li);
3541 let rs = self.layer_rope_scale(li);
3542 let mut normed = vec![0.0f32; hs];
3543 for bi in 0..b {
3544 inference::rms_norm_into(
3545 &h[bi * hs..(bi + 1) * hs],
3546 &lw.input_norm,
3547 eps,
3548 norm_style,
3549 &mut normed,
3550 );
3551 let ao = mla_attention(
3552 w,
3553 &normed,
3554 &mut self.kv_cache.layers[li],
3555 start_pos + bi,
3556 &inv_freq_l,
3557 rs,
3558 eps,
3559 pool.as_deref(),
3560 );
3561 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
3562 *dst += a;
3563 }
3564 }
3565 }
3566 AttnKind::Full {
3567 wq,
3568 wk,
3569 wv,
3570 wo,
3571 q_norm,
3572 k_norm,
3573 output_gate,
3574 softplus_gate,
3575 bias,
3576 } => {
3577 let mut normed = vec![0.0f32; b * hs];
3581 for bi in 0..b {
3582 inference::rms_norm_into(
3583 &h[bi * hs..(bi + 1) * hs],
3584 &lw.input_norm,
3585 eps,
3586 norm_style,
3587 &mut normed[bi * hs..(bi + 1) * hs],
3588 );
3589 }
3590 let inv_freq_l = self.layer_inv_freq(li);
3591 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3592 let cfg = QwenAttnCfg {
3593 num_heads: self.layer_num_heads(li),
3594 num_kv_heads: nkv_l,
3595 head_dim: hd_l,
3596 hidden_size: hs,
3597 position: start_pos,
3598 inv_freq: &inv_freq_l,
3599 rotary_dim: rd_l,
3600 scale: self.attn_scale,
3601 softcap: self.attn_softcap,
3602 window: self.layer_window(li),
3603 v_norm: self.attn_v_norm,
3604 q_norm: q_norm.as_deref(),
3605 k_norm: k_norm.as_deref(),
3606 output_gate: *output_gate,
3607 softplus_gate: softplus_gate
3608 .as_ref()
3609 .map(|(gate, per_head)| (gate, *per_head)),
3610 rope_scale: self.layer_rope_scale(li),
3611 bias: bias
3612 .as_ref()
3613 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3614 rms_eps: eps,
3615 norm_style,
3616 pool: pool.as_deref(),
3617 };
3618 let mut attn = attention::qwen_attention_batch(
3619 &normed,
3620 b,
3621 wq,
3622 wk,
3623 wv,
3624 wo,
3625 &mut self.kv_cache.layers[li],
3626 &cfg,
3627 );
3628 if let Some(w) = &lw.attn_out_norm {
3629 for bi in 0..b {
3630 inference::rms_norm_into(
3631 &attn[bi * hs..(bi + 1) * hs],
3632 w,
3633 eps,
3634 norm_style,
3635 &mut normed[bi * hs..(bi + 1) * hs],
3636 );
3637 }
3638 attn.copy_from_slice(&normed);
3639 }
3640 for (dst, &a) in h.iter_mut().zip(&attn) {
3641 *dst += a;
3642 }
3643 }
3644 AttnKind::Linear(w) => {
3645 for bi in 0..b {
3646 let normed = inference::rms_norm(
3647 &h[bi * hs..(bi + 1) * hs],
3648 &lw.input_norm,
3649 eps,
3650 norm_style,
3651 );
3652 vmf_phase_forward(
3653 &normed,
3654 w,
3655 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
3656 &mut self.kv_cache.layers[li].linear_state,
3657 pool.as_deref(),
3658 )
3659 .iter()
3660 .enumerate()
3661 .for_each(|(i, &a)| h[bi * hs + i] += a);
3662 }
3663 }
3664 }
3665
3666 let lw = &self.weights.layers[self.phys_layer(li)];
3668 let mut post = vec![0.0f32; b * hs];
3669 for bi in 0..b {
3670 let r =
3671 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
3672 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3673 }
3674 let mut ffn = match &lw.ffn {
3675 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
3676 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
3677 FfnKind::DenseMoe(dm) => {
3680 let mut out = vec![0.0f32; b * hs];
3681 for bi in 0..b {
3682 let r = dense_moe_ffn(
3683 dm,
3684 &post[bi * hs..(bi + 1) * hs],
3685 &h[bi * hs..(bi + 1) * hs],
3686 eps,
3687 norm_style,
3688 pool.as_deref(),
3689 );
3690 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3691 }
3692 out
3693 }
3694 };
3695 if let Some(w) = &lw.ffn_out_norm {
3696 for bi in 0..b {
3697 inference::rms_norm_into(
3698 &ffn[bi * hs..(bi + 1) * hs],
3699 w,
3700 eps,
3701 norm_style,
3702 &mut post[bi * hs..(bi + 1) * hs],
3703 );
3704 }
3705 ffn.copy_from_slice(&post);
3706 }
3707 for (dst, &f) in h.iter_mut().zip(&ffn) {
3708 *dst += f;
3709 }
3710 if let Some(sc) = lw.layer_scale {
3711 for v in h.iter_mut() {
3712 *v *= sc;
3713 }
3714 }
3715 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
3716 if let Some(t) = tp.parse::<usize>().ok() {
3717 if t >= start_pos && t < start_pos + b {
3718 let bi = t - start_pos;
3719 let row = &h[bi * hs..(bi + 1) * hs];
3720 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
3721 eprintln!(
3722 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
3723 row[0], row[1]
3724 );
3725 }
3726 }
3727 }
3728 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
3732 let row = &h[(b - 1) * hs..b * hs];
3733 let rms =
3734 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
3735 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
3736 eprintln!(
3737 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
3738 match &self.weights.layers[self.phys_layer(li)].attn {
3739 AttnKind::LinearGdn(_) => "gdn",
3740 AttnKind::Linear(_) => "vmf",
3741 AttnKind::ShortConv(_) => "conv",
3742 _ => "attn",
3743 },
3744 match &lw.ffn {
3745 FfnKind::Moe(_) => "moe",
3746 FfnKind::Dense(_) => "dense",
3747 FfnKind::DenseMoe(_) => "dense+moe",
3748 },
3749 );
3750 }
3751 if self.is_loop_end(li) && li + 1 < self.num_layers {
3753 for bi in 0..b {
3754 let normed = inference::rms_norm(
3755 &h[bi * hs..(bi + 1) * hs],
3756 &self.weights.final_norm,
3757 eps,
3758 norm_style,
3759 );
3760 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3761 }
3762 }
3763 if std::env::var("CMF_TRACE_H").is_ok() {
3764 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
3765 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
3766 eprintln!(
3767 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
3768 lw.layer_scale
3769 );
3770 }
3771 }
3772 crate::gpu::set_layer(-1); h
3774 }
3775
3776 fn embed_single(&self, id: u32) -> Vec<f32> {
3778 let mut out = vec![0.0f32; self.hidden_size];
3779 if (id as usize) < self.weights.embed_tokens.rows() {
3780 self.weights.embed_tokens.row_f32(id as usize, &mut out);
3781 }
3782 if self.embed_multiplier != 1.0 {
3783 for v in out.iter_mut() {
3784 *v *= self.embed_multiplier;
3785 }
3786 }
3787 if self.dsv4.is_some() {
3791 let mut v = vec![0.0f32; self.hidden_size.max(1)];
3792 v[0] = id as f32;
3793 return v;
3794 }
3795 if let Some(b) = &self.g3n {
3798 return b.0.extend_embedding(id, &out, self.pool.as_deref());
3799 }
3800 out
3801 }
3802
3803 #[cfg(target_os = "macos")]
3809 fn chunk_run_gpu(
3810 &mut self,
3811 li0: usize,
3812 h: &mut [f32],
3813 b: usize,
3814 pos0: usize,
3815 embed_ids: Option<&[u32]>,
3816 ) -> usize {
3817 if !crate::gpu::enabled_here()
3821 || std::env::var("CMF_GPU_CHUNK")
3822 .map(|v| v == "0")
3823 .unwrap_or(false)
3824 || b < 32
3825 || self.swa.is_some()
3826 || self.global_attn.is_some()
3827 || self.attn_v_norm
3828 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
3829 {
3830 return li0;
3831 }
3832 let Some(model) = self.model.clone() else {
3833 return li0;
3834 };
3835 let inv_freq = self.inv_freq.clone();
3836 let (nh, nkv, hd, hs) = (
3837 self.num_heads,
3838 self.num_kv_heads,
3839 self.head_dim,
3840 self.hidden_size,
3841 );
3842 let loop_end = if self.loop_final_norm {
3846 ((li0 / self.physical_layers) + 1) * self.physical_layers
3847 } else {
3848 self.num_layers
3849 };
3850 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
3851 let mut stored_at: Vec<usize> = Vec::new();
3852 for li in li0..self.num_layers.min(loop_end) {
3853 let lw = &self.weights.layers[self.phys_layer(li)];
3854 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
3855 break;
3856 }
3857 let AttnKind::Full {
3858 wq,
3859 wk,
3860 wv,
3861 wo,
3862 q_norm,
3863 k_norm,
3864 output_gate: false,
3865 softplus_gate: None,
3866 bias,
3867 } = &lw.attn
3868 else {
3869 break;
3870 };
3871 let FfnKind::Dense(d) = &lw.ffn else { break };
3872 if d.act != Act::Silu {
3873 break;
3874 }
3875 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
3880 t.q8_row_parts()
3881 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3882 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3883 }
3884 let parts = (
3885 cw(wq),
3886 cw(wk),
3887 cw(wv),
3888 cw(wo),
3889 cw(&d.gate_proj),
3890 cw(&d.up_proj),
3891 cw(&d.down_proj),
3892 );
3893 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
3894 else {
3895 break;
3896 };
3897 let layer = &self.kv_cache.layers[li];
3898 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
3899 break;
3900 }
3901 stored_at.push(layer.head_len(0));
3902 layers.push(crate::gpu_metal::ChunkLayer {
3903 model: &model,
3904 kv_id: self.graph_kv_id,
3905 layer: li,
3906 wq: pq,
3907 wk: pk,
3908 wv: pv,
3909 wo: po,
3910 gate: pg,
3911 up: pu,
3912 down: pd,
3913 input_norm: &lw.input_norm,
3914 post_norm: &lw.post_norm,
3915 bias: bias
3916 .as_ref()
3917 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
3918 q_norm: q_norm.as_deref(),
3919 k_norm: k_norm.as_deref(),
3920 inv_freq: &inv_freq,
3921 rd: self.rotary_dim,
3922 nh,
3923 nkv,
3924 hd,
3925 hs,
3926 inter: d.gate_proj.rows(),
3927 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
3928 eps: self.rms_eps as f32,
3929 });
3930 }
3931 if layers.is_empty() {
3932 return li0;
3933 }
3934 let row = nkv * hd;
3935 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
3936 .iter()
3937 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
3938 .collect();
3939 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
3940 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
3941 let li = layers[i].layer;
3942 let layer = &self.kv_cache.layers[li];
3943 io.push(crate::gpu_metal::ChunkIo {
3944 cpu_stored: stored_at[i],
3945 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
3946 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
3947 out_k: ok,
3948 out_v: ov,
3949 imp: oi,
3950 });
3951 }
3952 let n_run = layers.len();
3953 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
3954 let ep = embed_ids.and_then(|ids| {
3957 self.weights
3958 .embed_tokens
3959 .q8_row_parts()
3960 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
3961 idx,
3962 rows,
3963 row_scale: rs,
3964 ids,
3965 mult: self.embed_multiplier,
3966 })
3967 });
3968 if embed_ids.is_some() && ep.is_none() {
3969 return li0;
3970 }
3971 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
3972 return li0;
3973 }
3974 drop(io);
3975 drop(layers);
3976 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
3979 let li = li0 + i;
3980 let layer = &mut self.kv_cache.layers[li];
3981 for bi in 0..b {
3982 layer.append(
3983 &ok[bi * row..(bi + 1) * row],
3984 &ov[bi * row..(bi + 1) * row],
3985 &[],
3986 );
3987 }
3988 layer.accumulate_imp(oi);
3989 }
3990 last
3991 }
3992
3993 fn layer_is_local(&self, li: usize) -> bool {
3996 if let Some(layers) = &self.sliding_layers {
3997 return layers.get(li).copied().unwrap_or(false);
3998 }
3999 match self.swa {
4000 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
4001 None => false,
4002 }
4003 }
4004
4005 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
4008 if self.layer_is_local(li) {
4009 if let Some(f) = &self.inv_freq_local {
4010 return f.clone();
4011 }
4012 } else if let Some(f) = &self.inv_freq_global {
4013 return f.clone();
4014 }
4015 self.inv_freq.clone()
4016 }
4017
4018 fn layer_window(&self, li: usize) -> Option<usize> {
4020 self.swa
4021 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
4022 }
4023
4024 fn layer_num_heads(&self, li: usize) -> usize {
4025 self.attention_heads_per_layer
4026 .as_ref()
4027 .and_then(|v| v.get(li).copied())
4028 .unwrap_or(self.num_heads)
4029 }
4030
4031 fn layer_rope_scale(&self, li: usize) -> f32 {
4032 if self.layer_is_local(li) {
4033 self.rope_scale_local
4034 } else {
4035 self.rope_scale
4036 }
4037 }
4038
4039 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
4042 if !self.layer_is_local(li) {
4043 if let Some((ghd, gkv)) = self.global_attn {
4044 return (gkv, ghd, ghd);
4045 }
4046 }
4047 (
4048 self.num_kv_heads,
4049 self.head_dim,
4050 if self.layer_is_local(li) {
4051 self.rotary_dim_local.unwrap_or(self.rotary_dim)
4052 } else {
4053 self.rotary_dim
4054 },
4055 )
4056 }
4057
4058 fn forward_layers(
4060 &mut self,
4061 hidden: &[f32],
4062 position: usize,
4063 task_mask: Option<&TaskMask>,
4064 ) -> Vec<f32> {
4065 self.forward_layers_upto(hidden, position, task_mask, None)
4066 }
4067
4068 fn try_token_graph_wgpu(
4072 &self,
4073 hidden: &[f32],
4074 position: usize,
4075 logits_out: &mut Vec<f32>,
4076 layers_run: &mut usize,
4077 ) -> Option<Vec<f32>> {
4078 self.try_token_graph_wgpu_steps(hidden, position, logits_out, 1, None, Some(layers_run))
4079 }
4080
4081 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
4085 if self.o1_active() || self.attn_softcap > 0.0 {
4086 return None;
4087 }
4088 let graph_on = match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
4089 Some("0") => return None,
4090 Some(_) => true,
4091 None => crate::gpu::wgpu_graph_default(),
4092 };
4093 if !graph_on {
4094 return None;
4095 }
4096 let emb = self.embed_single(t_next);
4097 let mut lg = Vec::new();
4098 let mut ids = Vec::new();
4099 self.try_token_graph_wgpu_steps(&emb, position, &mut lg, k, Some(&mut ids), None)?;
4100 (ids.len() == k).then_some(ids)
4101 }
4102
4103 fn try_token_graph_wgpu_steps(
4107 &self,
4108 hidden: &[f32],
4109 position: usize,
4110 logits_out: &mut Vec<f32>,
4111 steps: usize,
4112 ids_out: Option<&mut Vec<u32>>,
4113 layers_run: Option<&mut usize>,
4114 ) -> Option<Vec<f32>> {
4115 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
4118 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
4119 return None;
4123 }
4124 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (0..self.num_layers)
4129 .map(|li| {
4130 if !o1_gpu {
4131 return None;
4132 }
4133 self.kv_cache.layers[self.phys_layer(li)].o1_views()
4134 })
4135 .collect();
4136 if self.o1_active() && o1_gpu {
4137 let want: usize = (0..self.num_layers)
4140 .filter(|li| !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None))
4141 .count();
4142 let have = o1_views.iter().filter(|v| v.is_some()).count();
4143 if want == 0 || have != want {
4144 return None;
4145 }
4146 }
4147 let nh = self.num_heads;
4148 let (nkv, hd, rd) = self.layer_geom(0);
4149 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4150 let mut layers = Vec::with_capacity(self.num_layers);
4151 let mut model = None;
4152 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
4153 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4154 if let Some((_, i, kind, rs)) = t.graph_weight() {
4155 return Some(crate::gpu::GraphW {
4156 idx: i,
4157 kind,
4158 row_scale: rs,
4159 data: &[],
4160 });
4161 }
4162 t.as_f32().map(|d| crate::gpu::GraphW {
4164 idx: 0,
4165 kind: 4,
4166 row_scale: &[],
4167 data: d,
4168 })
4169 }
4170 for li in 0..self.num_layers {
4171 let lw = &self.weights.layers[self.phys_layer(li)];
4172 if dbg {
4173 let ak = match &lw.attn {
4174 AttnKind::Mla(_) => "Mla".into(),
4175 AttnKind::Full {
4176 output_gate, bias, ..
4177 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
4178 AttnKind::LinearGdn(_) => "LinearGdn".into(),
4179 AttnKind::Kda(_) => "Kda".into(),
4180 AttnKind::Linear(_) => "Linear".into(),
4181 AttnKind::ShortConv(_) => "ShortConv".into(),
4182 };
4183 let fk = match &lw.ffn {
4184 FfnKind::Dense(_) => "Dense",
4185 FfnKind::Moe(_) => "Moe",
4186 FfnKind::DenseMoe(_) => "DenseMoe",
4187 };
4188 eprintln!("graph L{li}: attn={ak} ffn={fk}");
4189 }
4190 let gffn = match &lw.ffn {
4191 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
4193 gate: gw(&d.gate_proj)?,
4194 up: gw(&d.up_proj)?,
4195 down: gw(&d.down_proj)?,
4196 },
4197 FfnKind::Moe(m) => {
4198 if m.router_sigmoid
4203 || m.expert_bias.is_some()
4204 || m.route_tau.is_some()
4205 || m.mask.is_some()
4206 {
4207 return None;
4208 }
4209 let (se, sg) = m.shared.as_ref()?;
4210 let sgate = gw(sg.as_ref()?)?;
4211 let router = gw(&m.router)?;
4212 let inter = m.experts.first()?.gate_proj.rows();
4213 let mut experts = Vec::with_capacity(m.experts.len() + 1);
4214 let mut q4tp: Option<bool> = None;
4217 let mut gu_q2: Option<bool> = None;
4220 for e in m.experts.iter().chain(std::iter::once(se)) {
4221 if !matches!(e.act, Act::Silu)
4222 || e.gate_proj.rows() != inter
4223 || e.up_proj.rows() != inter
4224 {
4225 return None;
4226 }
4227 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
4228 Some((mm, gi)) => (
4229 mm,
4230 gi,
4231 e.up_proj.mapped_q4t()?.1,
4232 e.down_proj.mapped_q4t()?.1,
4233 false,
4234 false,
4235 ),
4236 None => match e.gate_proj.mapped_q2tp() {
4237 Some((mm, gi)) => (
4238 mm,
4239 gi,
4240 e.up_proj.mapped_q2tp()?.1,
4241 e.down_proj.mapped_q4tp()?.1,
4242 true,
4243 true,
4244 ),
4245 None => {
4246 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
4247 (
4248 mm,
4249 gi,
4250 e.up_proj.mapped_q4tp()?.1,
4251 e.down_proj.mapped_q4tp()?.1,
4252 true,
4253 false,
4254 )
4255 }
4256 },
4257 };
4258 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
4259 {
4260 tracing::warn!(
4266 "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."
4267 );
4268 return None;
4269 }
4270 model.get_or_insert_with(|| mm.clone());
4271 experts.push((gi, ui, di));
4272 }
4273 crate::gpu::GraphFfn::Moe {
4274 router,
4275 shared_gate: sgate,
4276 experts,
4277 n_exp: m.experts.len(),
4278 top_k: std::env::var("CMF_TOPK_PROBE")
4284 .ok()
4285 .and_then(|v| v.parse::<usize>().ok())
4286 .filter(|k| *k > 0 && *k <= m.top_k)
4287 .unwrap_or(m.top_k),
4288 inter,
4289 norm_topk: m.norm_topk_prob,
4290 q4tp: q4tp?,
4291 gu_q2: gu_q2.unwrap_or(false),
4292 }
4293 }
4294 };
4295 let attn = match &lw.attn {
4296 AttnKind::Full {
4297 wq,
4298 wk,
4299 wv,
4300 wo,
4301 q_norm,
4302 k_norm,
4303 output_gate,
4304 softplus_gate,
4305 bias,
4306 } => {
4307 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
4308 return None;
4309 }
4310 let (m, _, _, _) = wq.graph_weight()?;
4311 model = Some(m.clone());
4312 crate::gpu::GraphAttn::Full {
4313 wq: gw(wq)?,
4314 wk: gw(wk)?,
4315 wv: gw(wv)?,
4316 wo: gw(wo)?,
4317 q_norm: q_norm.as_deref(),
4318 k_norm: k_norm.as_deref(),
4319 bias: bias
4320 .as_ref()
4321 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4322 output_gate: *output_gate,
4323 cpu_k: self.kv_cache.layers[li].k_heads(),
4324 cpu_v: self.kv_cache.layers[li].v_heads(),
4325 }
4326 }
4327 AttnKind::LinearGdn(w) => {
4328 let cfg = self.gdn_cfg?;
4329 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
4330 model = Some(m.clone());
4331 crate::gpu::GraphAttn::Gdn {
4332 qkv: gw(&w.in_proj_qkv)?,
4333 z: gw(&w.in_proj_z)?,
4334 a: gw(&w.in_proj_a)?,
4335 b: gw(&w.in_proj_b)?,
4336 out: gw(&w.out_proj)?,
4337 conv1d: &w.conv1d,
4338 a_log: &w.a_log,
4339 dt_bias: &w.dt_bias,
4340 norm: &w.norm,
4341 nv: cfg.num_v_heads,
4342 nk: cfg.num_k_heads,
4343 dk: cfg.key_head_dim,
4344 dv: cfg.value_head_dim,
4345 kk: cfg.conv_kernel,
4346 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
4347 }
4348 }
4349 _ => return None,
4350 };
4351 layers.push(crate::gpu::GraphLayer {
4352 input_norm: &lw.input_norm,
4353 attn,
4354 post_norm: &lw.post_norm,
4355 ffn: gffn,
4356 });
4357 }
4358 let model = model?;
4359 let lm_gw = if self.graph_want_logits
4365 && std::env::var("CMF_GPU_LMHEAD")
4366 .map(|v| v != "0")
4367 .unwrap_or(true)
4368 {
4369 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
4370 (
4371 crate::gpu::GraphW {
4372 idx: i,
4373 kind,
4374 row_scale: rs,
4375 data: &[],
4376 },
4377 self.weights.lm_head.rows(),
4378 )
4379 })
4380 } else {
4381 None
4382 };
4383 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
4384 let emb_gw = if steps > 1 {
4386 self.weights
4387 .embed_tokens
4388 .graph_weight()
4389 .map(|(_, i, kind, rs)| {
4390 (
4391 crate::gpu::GraphW {
4392 idx: i,
4393 kind,
4394 row_scale: rs,
4395 data: &[],
4396 },
4397 self.weights.embed_tokens.rows(),
4398 self.embed_multiplier as f32,
4399 )
4400 })
4401 } else {
4402 None
4403 };
4404
4405 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
4408 (0..self.num_layers - 1)
4409 .filter(|&li| (li + 1) % self.physical_layers == 0)
4410 .collect()
4411 } else {
4412 Vec::new()
4413 };
4414 let mut h = hidden.to_vec();
4415 crate::gpu::forward_token_graph(
4416 &model,
4417 self.graph_kv_id,
4418 &layers,
4419 &o1_views,
4420 self.o1_epoch,
4421 &self.inv_freq,
4422 &mut h,
4423 nh,
4424 nkv,
4425 hd,
4426 rd,
4427 self.hidden_size,
4428 self.intermediate_size,
4429 position,
4430 self.kv_cache.max_seq_len,
4431 gemma,
4432 self.rms_eps as f32,
4433 lm,
4434 &self.weights.final_norm,
4435 logits_out,
4436 &loop_norm_at,
4437 steps,
4438 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
4439 ids_out,
4440 layers_run,
4441 )
4442 .then_some(h)
4443 }
4444
4445 fn try_batch_graph_wgpu(
4450 &self,
4451 hiddens: &mut [f32],
4452 positions: &[usize],
4453 k: usize,
4454 spec: Option<crate::gpu::SpecTail<'_>>,
4455 ) -> bool {
4456 let _tb = std::time::Instant::now();
4457 if self.attn_softcap > 0.0 {
4458 return false; }
4460 if self.o1_active() {
4461 return false;
4462 }
4463 let nh = self.num_heads;
4464 let (nkv, hd, rd) = self.layer_geom(0);
4465 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4466 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4467 if let Some((_, i, kind, rs)) = t.graph_weight() {
4468 return Some(crate::gpu::GraphW {
4469 idx: i,
4470 kind,
4471 row_scale: rs,
4472 data: &[],
4473 });
4474 }
4475 t.as_f32().map(|d| crate::gpu::GraphW {
4476 idx: 0,
4477 kind: 4,
4478 row_scale: &[],
4479 data: d,
4480 })
4481 }
4482 let built: Option<(
4483 Vec<crate::gpu::GraphLayer<'_>>,
4484 std::sync::Arc<cortiq_core::CmfModel>,
4485 )> = (|| {
4486 let mut layers = Vec::with_capacity(self.num_layers);
4487 let mut model = None;
4488 for li in 0..self.num_layers {
4489 let lw = &self.weights.layers[self.phys_layer(li)];
4490 let gffn = match &lw.ffn {
4497 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
4498 gate: gw(&d.gate_proj)?,
4499 up: gw(&d.up_proj)?,
4500 down: gw(&d.down_proj)?,
4501 },
4502 FfnKind::Moe(m) => {
4503 if m.router_sigmoid
4504 || m.expert_bias.is_some()
4505 || m.route_tau.is_some()
4506 || m.mask.is_some()
4507 {
4508 return None;
4509 }
4510 let (se, sg) = m.shared.as_ref()?;
4511 let sgate = gw(sg.as_ref()?)?;
4512 let router = gw(&m.router)?;
4513 let inter = m.experts.first()?.gate_proj.rows();
4514 let mut experts = Vec::with_capacity(m.experts.len() + 1);
4515 let mut q4tp: Option<bool> = None;
4516 for e in m.experts.iter().chain(std::iter::once(se)) {
4517 if !matches!(e.act, Act::Silu)
4518 || e.gate_proj.rows() != inter
4519 || e.up_proj.rows() != inter
4520 {
4521 return None;
4522 }
4523 let (mm, gi, ui, di, is_p) = match e.gate_proj.mapped_q4t() {
4524 Some((mm, gi)) => (
4525 mm,
4526 gi,
4527 e.up_proj.mapped_q4t()?.1,
4528 e.down_proj.mapped_q4t()?.1,
4529 false,
4530 ),
4531 None => {
4532 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
4533 (
4534 mm,
4535 gi,
4536 e.up_proj.mapped_q4tp()?.1,
4537 e.down_proj.mapped_q4tp()?.1,
4538 true,
4539 )
4540 }
4541 };
4542 if *q4tp.get_or_insert(is_p) != is_p {
4543 return None;
4544 }
4545 model.get_or_insert_with(|| mm.clone());
4546 experts.push((gi, ui, di));
4547 }
4548 crate::gpu::GraphFfn::Moe {
4549 router,
4550 shared_gate: sgate,
4551 experts,
4552 n_exp: m.experts.len(),
4553 top_k: m.top_k,
4554 inter,
4555 norm_topk: m.norm_topk_prob,
4556 q4tp: q4tp?,
4557 gu_q2: false,
4560 }
4561 }
4562 _ => return None,
4563 };
4564 let attn = match &lw.attn {
4565 AttnKind::Full {
4566 wq,
4567 wk,
4568 wv,
4569 wo,
4570 q_norm,
4571 k_norm,
4572 output_gate,
4573 softplus_gate,
4574 bias,
4575 } => {
4576 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
4577 return None;
4578 }
4579 let (m, _, _, _) = wq.graph_weight()?;
4580 model = Some(m.clone());
4581 crate::gpu::GraphAttn::Full {
4582 wq: gw(wq)?,
4583 wk: gw(wk)?,
4584 wv: gw(wv)?,
4585 wo: gw(wo)?,
4586 q_norm: q_norm.as_deref(),
4587 k_norm: k_norm.as_deref(),
4588 bias: bias
4589 .as_ref()
4590 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4591 output_gate: *output_gate,
4592 cpu_k: self.kv_cache.layers[li].k_heads(),
4593 cpu_v: self.kv_cache.layers[li].v_heads(),
4594 }
4595 }
4596 AttnKind::LinearGdn(w) => {
4597 let cfg = self.gdn_cfg?;
4598 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
4599 model = Some(m.clone());
4600 crate::gpu::GraphAttn::Gdn {
4601 qkv: gw(&w.in_proj_qkv)?,
4602 z: gw(&w.in_proj_z)?,
4603 a: gw(&w.in_proj_a)?,
4604 b: gw(&w.in_proj_b)?,
4605 out: gw(&w.out_proj)?,
4606 conv1d: &w.conv1d,
4607 a_log: &w.a_log,
4608 dt_bias: &w.dt_bias,
4609 norm: &w.norm,
4610 nv: cfg.num_v_heads,
4611 nk: cfg.num_k_heads,
4612 dk: cfg.key_head_dim,
4613 dv: cfg.value_head_dim,
4614 kk: cfg.conv_kernel,
4615 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
4616 }
4617 }
4618 _ => return None,
4619 };
4620 layers.push(crate::gpu::GraphLayer {
4621 input_norm: &lw.input_norm,
4622 attn,
4623 post_norm: &lw.post_norm,
4624 ffn: gffn,
4625 });
4626 }
4627 Some((layers, model?))
4628 })();
4629 let Some((layers, model)) = built else {
4630 {
4631 use std::sync::atomic::{AtomicBool, Ordering};
4632 static SAID: AtomicBool = AtomicBool::new(false);
4633 if !SAID.swap(true, Ordering::Relaxed) {
4634 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
4635 }
4636 }
4637 return false;
4638 };
4639 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4640 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
4641 }
4642 crate::gpu::forward_batch_graph(
4643 &model,
4644 self.graph_kv_id,
4645 &layers,
4646 &self.inv_freq,
4647 hiddens,
4648 nh,
4649 nkv,
4650 hd,
4651 rd,
4652 self.hidden_size,
4653 self.intermediate_size,
4654 positions,
4655 self.kv_cache.max_seq_len,
4656 gemma,
4657 self.rms_eps as f32,
4658 k,
4659 spec,
4660 )
4661 }
4662
4663 fn draft_probe() -> bool {
4667 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4668 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
4669}
4670
4671 #[cfg(feature = "gpu")]
4683 fn dsv4_spec_on() -> bool {
4684 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4685 *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC").map(|v| v != "0").unwrap_or(true))
4686 }
4687
4688 #[cfg(feature = "gpu")]
4695 fn dsv4_spec_step(
4696 &mut self,
4697 tip_token: u32,
4698 t_next: u32,
4699 next_pos: usize,
4700 drafted: &mut usize,
4701 accepted_ctr: &mut usize,
4702 ) -> Option<(Vec<u32>, usize)> {
4703 let t_all = std::time::Instant::now();
4704 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4705 thread_local! {
4706 static LAST: std::cell::Cell<Option<std::time::Instant>> =
4707 const { std::cell::Cell::new(None) };
4708 }
4709 LAST.with(|l| {
4710 if let Some(prev) = l.get() {
4711 eprintln!("между раундами {:.1} мс", prev.elapsed().as_secs_f64() * 1e3);
4712 }
4713 l.set(Some(std::time::Instant::now()));
4714 });
4715 }
4716 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4717 eprintln!("spec_step: вход pos={next_pos}");
4718 }
4719 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
4720 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
4721 if self.dspark.is_none() {
4723 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4724 if t.is_empty() {
4725 return None;
4726 }
4727 crate::dsv4::dspark_arm(&t, cfg.dim);
4728 self.dspark = Some(crate::dsv4::DsparkState::new(
4729 self.dsv4_mtp.len(),
4730 &cfg,
4731 t.len(),
4732 ));
4733 }
4734 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4735 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
4736 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4737 eprintln!("spec_step: пак не построился (targets {targets:?})");
4738 }
4739 let pack = pack?;
4740 let block = crate::dsv4::dspark_block();
4741 let b_box = self.dsv4.as_mut()?;
4742 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
4743 let ds = self.dspark.as_mut()?;
4744 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
4747 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
4748 if dbg {
4749 eprintln!("spec_step: нет захвата");
4750 }
4751 return None;
4752 }
4753 ds.have_hidden = true;
4754 let tip_pos = next_pos.checked_sub(1)?;
4755 let draft_started = std::time::Instant::now();
4756 let mut conf = Vec::new();
4757 let props = crate::dsv4::dspark_draft_gpu(
4758 g,
4759 &self.dsv4_mtp,
4760 &cfg,
4761 ds,
4762 pack,
4763 st.kv_id,
4764 tip_token,
4765 tip_pos,
4766 self.pool.as_deref(),
4767 &mut conf,
4768 );
4769 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
4770 *drafted += block;
4771 if props.is_empty() || props[0] != t_next {
4772 if dbg {
4773 eprintln!(
4774 "spec_step: черновик {} (props0={:?} t_next={t_next})",
4775 if props.is_empty() { "пуст" } else { "мимо" },
4776 props.first()
4777 );
4778 }
4779 return None;
4780 }
4781 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
4782 let conf_min = {
4788 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
4789 *M.get_or_init(|| {
4790 std::env::var("CMF_DSPARK_CONF_MIN")
4791 .ok()
4792 .and_then(|v| v.parse().ok())
4793 .unwrap_or(0.0)
4794 })
4795 };
4796 if conf_min > 0.0 && conf.len() >= props.len() {
4797 let mut keep = 1usize;
4798 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
4799 keep += 1;
4800 }
4801 k_verify = k_verify.min(keep.max(2));
4802 }
4803 if k_verify < 2 {
4804 return None;
4805 }
4806 let mut fed = Vec::with_capacity(k_verify);
4807 fed.push(t_next);
4808 fed.extend_from_slice(&props[1..k_verify]);
4809 let mut argmax = Vec::new();
4810 let mut logits_all = Vec::new();
4811 let mut walked = Vec::new();
4812 let txn = crate::dsv4::dsv4_verify_chunk(
4813 g,
4814 layers,
4815 &cfg,
4816 st,
4817 &fed,
4818 next_pos,
4819 &self.inv_freq,
4820 self.pool.as_deref(),
4821 &targets,
4822 &mut argmax,
4823 &mut logits_all,
4824 &mut walked,
4825 );
4826 if txn.is_none() && dbg {
4827 eprintln!("spec_step: verify отказал");
4828 }
4829 let txn = txn?;
4830 let b = fed.len();
4831 let mut accepted = 1usize;
4832 while accepted < b && fed[accepted] == argmax[accepted - 1] {
4833 accepted += 1;
4834 }
4835 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
4840 accepted = 1;
4841 }
4842 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
4843 eprintln!(
4844 "spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}"
4845 );
4846 }
4847 let t_fin = std::time::Instant::now();
4848 if !crate::dsv4::dsv4_spec_finish(
4849 g,
4850 layers,
4851 &cfg,
4852 st,
4853 txn,
4854 accepted,
4855 &fed,
4856 &self.inv_freq,
4857 self.pool.as_deref(),
4858 ) {
4859 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
4860 return None;
4861 }
4862 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4863 eprintln!("finish(k={accepted}): {:.1} мс", t_fin.elapsed().as_secs_f64() * 1e3);
4864 }
4865 *accepted_ctr += accepted - 1;
4866 let (hc, dim) = (cfg.hc_mult, cfg.dim);
4871 let dev_caps: Vec<usize> = targets
4878 .iter()
4879 .copied()
4880 .filter(|&t| {
4881 st.dev_set.get(t).copied().unwrap_or(false)
4882 && !st.partial_set.get(t).copied().unwrap_or(false)
4883 })
4884 .collect();
4885 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
4886 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
4887 return None;
4888 }
4889 for t in 0..accepted {
4890 let tip = t + 1 == accepted;
4891 for (slot, &tl) in targets.iter().enumerate() {
4892 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
4893 let lo = (di * b + t) * hc * dim;
4894 crate::dsv4::dspark_capture(
4895 &caps_all[lo..lo + hc * dim],
4896 &cfg,
4897 slot,
4898 &mut ds.main_hidden,
4899 );
4900 } else if tip
4901 && crate::dsv4::dspark_peek_slot(slot, dim, {
4902 let lo = slot * dim;
4903 &mut ds.main_hidden[lo..lo + dim]
4904 })
4905 {
4906 } else {
4911 crate::dsv4::dspark_capture(
4915 &walked[t * hc * dim..(t + 1) * hc * dim],
4916 &cfg,
4917 slot,
4918 &mut ds.main_hidden,
4919 );
4920 }
4921 }
4922 crate::dsv4::dspark_ring_append(g, &self.dsv4_mtp, &cfg, ds, next_pos + t, self.pool.as_deref());
4923 }
4924 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
4925 self.graph_logits = Some(row);
4926 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
4931 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
4932 crate::dsv4::pick_tally_arm();
4933 }
4934 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4935 eprintln!("spec_step total {:.1} мс (k={accepted})", t_all.elapsed().as_secs_f64() * 1e3);
4936 }
4937 Some((fed[1..accepted].to_vec(), next_pos + accepted))
4938 }
4939
4940 fn dspark_probe(&mut self, position: usize, token_id: u32) {
4941 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
4942 return;
4943 }
4944 let trunk_now = crate::dsv4::pick_tally_take();
4946 crate::dsv4::trunk_freq_note(&trunk_now);
4947 if !trunk_now.is_empty() {
4948 self.dspark_trunk_picks.push(trunk_now);
4949 let keep = crate::dsv4::dspark_block();
4950 if self.dspark_trunk_picks.len() > keep {
4951 self.dspark_trunk_picks.remove(0);
4952 }
4953 }
4954 for p in std::mem::take(&mut self.dspark_pending) {
4957 let Some(i) = position.checked_sub(p.0 + 1) else {
4958 continue;
4959 };
4960 let mut p = p;
4961 if i < p.1.len() {
4962 if p.2 && p.1[i] == token_id {
4963 p.3 = i + 1;
4964 } else {
4965 p.2 = false;
4966 }
4967 if i + 1 < p.1.len() {
4968 self.dspark_pending.push(p);
4969 continue;
4970 }
4971 }
4972 self.dspark_hist.push(p.3);
4973 self.dspark_real.push(token_id);
4974 }
4975 let Some(b) = &mut self.dsv4 else { return };
4976 let (g, layers, cfg) = (&b.0, &b.1, b.2);
4977 let n_layers = layers.len();
4978 if self.dspark.is_none() {
4979 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4980 if t.is_empty() {
4981 return;
4982 }
4983 eprintln!("DSpark: захват со слоёв {t:?}, блок {}", crate::dsv4::dspark_block());
4984 crate::dsv4::dspark_arm(&t, cfg.dim);
4985 self.dspark = Some(crate::dsv4::DsparkState::new(
4986 self.dsv4_mtp.len(),
4987 &cfg,
4988 t.len(),
4989 ));
4990 }
4991 let ds = self.dspark.as_mut().unwrap();
4992 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
4993 return; }
4995 let mut conf = Vec::new();
4996 crate::dsv4::pick_tally_arm();
4997 let draft_started = std::time::Instant::now();
5002 #[cfg(feature = "gpu")]
5003 let gpu_draft = crate::dsv4::dspark_gpu_on();
5004 #[cfg(not(feature = "gpu"))]
5005 let gpu_draft = false;
5006 let props = if gpu_draft {
5007 #[cfg(feature = "gpu")]
5008 {
5009 let kv_id = b.3.kv_id;
5010 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
5011 Some(pk) => crate::dsv4::dspark_draft_gpu(
5012 g,
5013 &self.dsv4_mtp,
5014 &cfg,
5015 ds,
5016 pk,
5017 kv_id,
5018 token_id,
5019 position,
5020 self.pool.as_deref(),
5021 &mut conf,
5022 ),
5023 None => Vec::new(),
5024 }
5025 }
5026 #[cfg(not(feature = "gpu"))]
5027 Vec::new()
5028 } else {
5029 crate::gpu::cpu_scope(|| {
5030 crate::dsv4::dspark_draft(
5031 g,
5032 &self.dsv4_mtp,
5033 &cfg,
5034 ds,
5035 token_id,
5036 position,
5037 self.pool.as_deref(),
5038 &mut conf,
5039 )
5040 })
5041 };
5042 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
5043 let draft_picks = crate::dsv4::pick_tally_take();
5044 crate::dsv4::dspark_freq_note(&draft_picks);
5045 crate::dsv4::pick_tally_arm();
5048 if !props.is_empty() {
5049 let (tu, tt) = {
5053 let flat: Vec<(usize, Vec<usize>)> = self
5054 .dspark_trunk_picks
5055 .iter()
5056 .flat_map(|v| v.iter().cloned())
5057 .collect();
5058 let mut per: std::collections::HashMap<usize, Vec<usize>> =
5060 std::collections::HashMap::new();
5061 for (li, picks) in flat {
5062 per.entry(li).or_default().extend(picks);
5063 }
5064 let n = per.len().max(1);
5065 let mut u = 0usize;
5066 let mut t = 0usize;
5067 for (_, v) in per {
5068 t += v.len();
5069 u += v.iter().collect::<std::collections::HashSet<_>>().len();
5070 }
5071 (u / n, t / n)
5072 };
5073 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
5074 self.dspark_exp.push((tu, tt, du, dt));
5075 self.dspark_pending.push((position, props, true, 0));
5076 }
5077 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
5078 let n = self.dspark_hist.len() as f32;
5079 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
5080 let block = crate::dsv4::dspark_block();
5081 let mut at = vec![0usize; block + 1];
5082 for &k in &self.dspark_hist {
5083 at[k] += 1;
5084 }
5085 let mut surv = Vec::with_capacity(block);
5087 for i in 1..=block {
5088 let k = at[i..].iter().sum::<usize>() as f32 / n;
5089 surv.push(format!("{k:.2}"));
5090 }
5091 let distinct = self
5092 .dspark_real
5093 .iter()
5094 .collect::<std::collections::HashSet<_>>()
5095 .len();
5096 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
5097 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
5098 });
5099 let m = self.dspark_exp.len().max(1);
5100 eprintln!(
5101 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
5102 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
5103 self.dspark_hist.len(),
5104 mean + 1.0,
5105 surv.join(" ")
5106 );
5107 eprintln!(
5108 "DSpark: разных токенов {distinct} из {} (вырожденность), \
5109 эксперты ствол {}/{} на слой за {block} токенов, \
5110 черновик {}/{} за блок, draft {:.2} мс/блок",
5111 self.dspark_real.len(),
5112 tu / m,
5113 tt / m,
5114 du / m,
5115 dt / m,
5116 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
5117 );
5118 }
5119 }
5120
5121 fn forward_layers_upto(
5122 &mut self,
5123 hidden: &[f32],
5124 position: usize,
5125 task_mask: Option<&TaskMask>,
5126 upto: Option<usize>,
5127 ) -> Vec<f32> {
5128 if let Some(b) = &mut self.dsv4 {
5134 let _ = (task_mask, upto);
5135 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
5136 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5137 st.pos = position;
5138 let mut logits = Vec::new();
5139 crate::dsv4::forward_token(
5140 g,
5141 layers,
5142 &cfg,
5143 st,
5144 token_id,
5145 &self.inv_freq,
5146 self.pool.as_deref(),
5147 &mut logits,
5148 );
5149 self.graph_logits = Some(logits);
5150 self.dspark_probe(position, token_id);
5151 return vec![0.0; self.hidden_size];
5154 }
5155 if let Some(b) = &self.g3n {
5158 let _ = (task_mask, upto);
5159 return crate::g3n::g3n_forward(
5160 &b.0,
5161 &b.1,
5162 hidden,
5163 position,
5164 &mut self.kv_cache.layers,
5165 self.num_heads,
5166 self.num_kv_heads,
5167 self.head_dim,
5168 self.pool.as_deref(),
5169 );
5170 }
5171 let mut h = hidden.to_vec();
5172 let (nh, _nkv, _hd, hs, _rd, eps) = (
5175 self.num_heads,
5176 self.num_kv_heads,
5177 self.head_dim,
5178 self.hidden_size,
5179 self.rotary_dim,
5180 self.rms_eps,
5181 );
5182 let pool = self.pool.clone();
5183 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
5195 let graph_on = match graph_env.as_deref() {
5196 Some("0") => false,
5197 Some(_) => true,
5198 None => crate::gpu::wgpu_graph_default(),
5204 };
5205 let graph_trusted =
5206 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
5207 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
5208 let mut tail_start = 0usize;
5209 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
5210 let t_graph = std::time::Instant::now();
5211 let mut lg = Vec::new();
5212 let mut gl = 0usize;
5213 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
5214 graph_note(built.is_some());
5215 if let Some(hh) = built {
5216 let dur = t_graph.elapsed();
5217 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5218 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
5219 }
5220 if gl > 0 && gl < self.num_layers {
5221 h = hh;
5227 tail_start = gl;
5228 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
5229 if !graph_trusted {
5230 crate::gpu::graph_race_record(true, dur);
5231 }
5232 if !lg.is_empty() {
5233 lg.resize(self.vocab_size, 0.0);
5236 if let Some(c) = self.final_softcap {
5237 for l in lg.iter_mut() {
5238 *l = c * (*l / c).tanh();
5239 }
5240 }
5241 self.graph_logits = Some(lg);
5242 }
5243 return hh;
5244 }
5245 }
5251 }
5252 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
5253
5254 #[cfg(target_os = "macos")]
5255 let mut gpu_skip_until = 0usize;
5256 for li in tail_start..self.num_layers {
5257 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
5259 if li > u {
5260 break;
5261 }
5262 }
5263 if let Some(mask) = task_mask {
5264 if !mask.layer_alive(li) {
5265 continue; }
5267 }
5268 #[cfg(target_os = "macos")]
5272 {
5273 if li < gpu_skip_until {
5274 continue;
5275 }
5276 if task_mask.is_none() {
5277 let end = self.q1_graph_gpu(li, upto, position, &mut h);
5278 if end > li {
5279 gpu_skip_until = end;
5280 if self.is_loop_end(end - 1) && end < self.num_layers {
5283 h = inference::rms_norm(
5284 &h,
5285 &self.weights.final_norm,
5286 self.rms_eps,
5287 self.norm_style,
5288 );
5289 }
5290 continue;
5291 }
5292 }
5293 }
5294
5295 let lw = &self.weights.layers[self.phys_layer(li)];
5296 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5297 if tp.parse::<usize>().ok() == Some(position) {
5298 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
5299 eprintln!(
5300 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5301 h[0], h[1]
5302 );
5303 }
5304 }
5305 inference::rms_norm_into(
5308 &h,
5309 &lw.input_norm,
5310 self.rms_eps,
5311 self.norm_style,
5312 &mut self.ws.n1,
5313 );
5314
5315 let attn_out = match &lw.attn {
5316 AttnKind::Mla(w) => {
5317 let inv_freq_l = self.layer_inv_freq(li);
5318 let rs = self.layer_rope_scale(li);
5319 let eps = self.rms_eps;
5320 let pool = self.pool.clone();
5321 mla_attention(
5322 w,
5323 &self.ws.n1,
5324 &mut self.kv_cache.layers[li],
5325 position,
5326 &inv_freq_l,
5327 rs,
5328 eps,
5329 pool.as_deref(),
5330 )
5331 }
5332 AttnKind::Linear(w) => {
5333 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5334 vmf_phase_forward(
5335 &self.ws.n1,
5336 w,
5337 &cfg,
5338 &mut self.kv_cache.layers[li].linear_state,
5339 self.pool.as_deref(),
5340 )
5341 }
5342 AttnKind::Kda(w) => {
5343 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5344 crate::linear_core::kda_forward(
5345 &self.ws.n1,
5346 w,
5347 &cfg,
5348 &mut self.kv_cache.layers[li].linear_state,
5349 self.pool.as_deref(),
5350 )
5351 }
5352 AttnKind::LinearGdn(w) => {
5353 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5354 gdn_forward(
5355 &self.ws.n1,
5356 w,
5357 &cfg,
5358 &mut self.kv_cache.layers[li].linear_state,
5359 self.pool.as_deref(),
5360 )
5361 }
5362 AttnKind::ShortConv(w) => {
5363 let cfg = self
5364 .short_conv_cfg
5365 .expect("short-conv layer without short_conv_cfg");
5366 short_conv_forward(
5367 &self.ws.n1,
5368 w,
5369 &cfg,
5370 &mut self.kv_cache.layers[li].linear_state,
5371 self.pool.as_deref(),
5372 )
5373 }
5374 AttnKind::Full {
5375 wq,
5376 wk,
5377 wv,
5378 wo,
5379 q_norm,
5380 k_norm,
5381 output_gate,
5382 softplus_gate,
5383 bias,
5384 } if self.kv_cache.layers[li].o1_sealed() => {
5385 let inv_freq_l = self.layer_inv_freq(li);
5388 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5389 let cfg = QwenAttnCfg {
5390 num_heads: self.layer_num_heads(li),
5391 num_kv_heads: nkv_l,
5392 head_dim: hd_l,
5393 hidden_size: hs,
5394 position,
5395 inv_freq: &inv_freq_l,
5396 rotary_dim: rd_l,
5397 scale: self.attn_scale,
5398 softcap: self.attn_softcap,
5399 window: None,
5400 v_norm: self.attn_v_norm,
5401 q_norm: q_norm.as_deref(),
5402 k_norm: k_norm.as_deref(),
5403 output_gate: *output_gate,
5404 softplus_gate: softplus_gate
5405 .as_ref()
5406 .map(|(gate, per_head)| (gate, *per_head)),
5407 rope_scale: self.layer_rope_scale(li),
5408 bias: bias
5409 .as_ref()
5410 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5411 rms_eps: eps,
5412 norm_style: self.norm_style,
5413 pool: pool.as_deref(),
5414 };
5415 attention::qwen_attention_nystrom(
5416 &self.ws.n1,
5417 wq,
5418 wk,
5419 wv,
5420 wo,
5421 &mut self.kv_cache.layers[li],
5422 &cfg,
5423 )
5424 }
5425 AttnKind::Full {
5426 wq,
5427 wk,
5428 wv,
5429 wo,
5430 q_norm,
5431 k_norm,
5432 output_gate,
5433 softplus_gate,
5434 bias,
5435 } => 'attn: {
5436 if graph_on
5439 && !*output_gate
5440 && softplus_gate.is_none()
5441 && self.attention_heads_per_layer.is_none()
5442 && bias.is_none()
5443 && task_mask.is_none()
5444 {
5445 let inv_freq_l = self.layer_inv_freq(li);
5446 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5447 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5448 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
5449 wq.mapped_q1(),
5450 wk.mapped_q1(),
5451 wv.mapped_q1(),
5452 wo.mapped_q1(),
5453 ) {
5454 let gm = gm.clone();
5455 let mut out = vec![0f32; hs];
5456 let cache = &self.kv_cache.layers[li];
5457 if crate::gpu::attn_dropin(
5458 &gm,
5459 self.graph_kv_id,
5460 li,
5461 &self.ws.n1,
5462 qi,
5463 ki,
5464 vi,
5465 oi,
5466 q_norm.as_deref(),
5467 k_norm.as_deref(),
5468 &inv_freq_l,
5469 nh,
5470 nkv_l,
5471 hd_l,
5472 rd_l,
5473 hs,
5474 position,
5475 self.kv_cache.max_seq_len,
5476 gemma,
5477 eps as f32,
5478 cache.k_heads(),
5479 cache.v_heads(),
5480 &mut out,
5481 ) {
5482 break 'attn out;
5483 }
5484 }
5485 }
5486 let masked = task_mask
5487 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
5488 .unwrap_or(false);
5489 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
5490 match (masked, f32_view) {
5491 (true, (Some(q), Some(k), Some(v), Some(o))) => {
5494 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
5495 attention::multi_head_attention(
5496 &self.ws.n1,
5497 q,
5498 k,
5499 v,
5500 o,
5501 &mut self.kv_cache.layers[li],
5502 self.num_heads,
5503 self.num_kv_heads,
5504 self.head_dim,
5505 self.hidden_size,
5506 position,
5507 &active_heads,
5508 &self.inv_freq,
5509 )
5510 }
5511 (masked, _) => {
5512 if masked {
5513 tracing::warn!(
5514 "layer {li}: head mask on quantized weights not \
5515 supported yet — executing dense"
5516 );
5517 }
5518 let inv_freq_l = self.layer_inv_freq(li);
5519 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5520 let cfg = QwenAttnCfg {
5521 num_heads: self.layer_num_heads(li),
5522 num_kv_heads: nkv_l,
5523 head_dim: hd_l,
5524 hidden_size: hs,
5525 position,
5526 inv_freq: &inv_freq_l,
5527 rotary_dim: rd_l,
5528 scale: self.attn_scale,
5529 softcap: self.attn_softcap,
5530 window: self.layer_window(li),
5531 v_norm: self.attn_v_norm,
5532 q_norm: q_norm.as_deref(),
5533 k_norm: k_norm.as_deref(),
5534 output_gate: *output_gate,
5535 softplus_gate: softplus_gate
5536 .as_ref()
5537 .map(|(gate, per_head)| (gate, *per_head)),
5538 rope_scale: self.layer_rope_scale(li),
5539 bias: bias
5540 .as_ref()
5541 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5542 rms_eps: eps,
5543 norm_style: self.norm_style,
5544 pool: pool.as_deref(),
5545 };
5546 attention::qwen_attention(
5547 &self.ws.n1,
5548 wq,
5549 wk,
5550 wv,
5551 wo,
5552 &mut self.kv_cache.layers[li],
5553 &cfg,
5554 )
5555 }
5556 }
5557 }
5558 };
5559 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5562 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
5563 None => attn_out,
5564 };
5565 let lw = &self.weights.layers[self.phys_layer(li)];
5566 inference::add_rmsnorm_fused_into(
5567 &mut h,
5568 &attn_out,
5569 &lw.post_norm,
5570 self.rms_eps,
5571 self.norm_style,
5572 &mut self.ws.p1,
5573 );
5574 let mut attn_out = attn_out;
5575 attention::recycle_buf(&mut attn_out);
5576 let post_normed = &self.ws.p1;
5577
5578 let ffn_masked = task_mask
5579 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
5580 .unwrap_or(false);
5581 let f32_ffn = match &lw.ffn {
5584 FfnKind::Dense(d) => (
5585 d.gate_proj.as_f32(),
5586 d.up_proj.as_f32(),
5587 d.down_proj.as_f32(),
5588 ),
5589 FfnKind::Moe(_) | FfnKind::DenseMoe(_) => (None, None, None),
5590 };
5591 let ffn_out = match (ffn_masked, f32_ffn) {
5592 (true, (Some(g), Some(u), Some(d))) => {
5593 let active = task_mask.unwrap().ffn_active_indices(li);
5594 inference::sparse_ffn_forward(
5595 post_normed,
5596 g,
5597 u,
5598 d,
5599 self.hidden_size,
5600 self.intermediate_size,
5601 &active,
5602 self.pool.as_deref(),
5603 )
5604 }
5605 (true, _) => match &lw.ffn {
5609 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
5610 let active = task_mask.unwrap().ffn_active_indices(li);
5611 sparse_ffn_quant(
5612 d,
5613 post_normed,
5614 &active,
5615 self.hidden_size,
5616 self.pool.as_deref(),
5617 )
5618 }
5619 FfnKind::Dense(d) => {
5624 let active = task_mask.unwrap().ffn_active_indices(li);
5625 let (gf, uf, df) = dequant_dense_f32(d);
5626 inference::sparse_ffn_forward(
5627 post_normed,
5628 &gf,
5629 &uf,
5630 &df,
5631 self.hidden_size,
5632 self.intermediate_size,
5633 &active,
5634 self.pool.as_deref(),
5635 )
5636 }
5637 FfnKind::Moe(m) => {
5638 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
5642 ffn_forward(
5643 &lw.ffn,
5644 post_normed,
5645 self.pool.as_deref(),
5646 allowed.as_deref(),
5647 )
5648 }
5649 FfnKind::DenseMoe(dm) => dense_moe_ffn(
5650 dm,
5651 post_normed,
5652 &h,
5653 self.rms_eps,
5654 self.norm_style,
5655 self.pool.as_deref(),
5656 ),
5657 },
5658 (false, _) => match &lw.ffn {
5659 FfnKind::DenseMoe(dm) => dense_moe_ffn(
5660 dm,
5661 post_normed,
5662 &h,
5663 self.rms_eps,
5664 self.norm_style,
5665 self.pool.as_deref(),
5666 ),
5667 _ => {
5668 let allowed = match (&lw.ffn, task_mask) {
5669 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
5670 _ => None,
5671 };
5672 ffn_forward(
5673 &lw.ffn,
5674 post_normed,
5675 self.pool.as_deref(),
5676 allowed.as_deref(),
5677 )
5678 }
5679 },
5680 };
5681 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5682 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
5683 None => ffn_out,
5684 };
5685 for (i, &f) in ffn_out.iter().enumerate() {
5686 h[i] += f;
5687 }
5688 let mut ffn_out = ffn_out;
5689 attention::recycle_buf(&mut ffn_out);
5690
5691 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5693 for v in h.iter_mut() {
5694 *v *= sc;
5695 }
5696 }
5697
5698 if self.is_loop_end(li) && li + 1 < self.num_layers {
5701 h = inference::rms_norm(
5702 &h,
5703 &self.weights.final_norm,
5704 self.rms_eps,
5705 self.norm_style,
5706 );
5707 }
5708
5709 if self.dyn_phi_layer == Some(li) {
5713 self.update_dyn_phi(&h);
5714 }
5715 }
5716 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
5718 crate::gpu::graph_race_record(false, t.elapsed());
5719 }
5720
5721 h
5722 }
5723
5724 fn update_dyn_phi(&mut self, h: &[f32]) {
5727 const A: f32 = 0.2;
5728 if self.dyn_phi_ema.len() != h.len() {
5729 self.dyn_phi_ema = vec![0.0; h.len()];
5730 self.dyn_phi_seen = 0;
5731 }
5732 if self.dyn_phi_seen == 0 {
5733 self.dyn_phi_ema.copy_from_slice(h);
5734 } else {
5735 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
5736 *e = (1.0 - A) * *e + A * v;
5737 }
5738 }
5739 self.dyn_phi_seen += 1;
5740 }
5741
5742 pub fn dyn_phi(&self) -> &[f32] {
5744 &self.dyn_phi_ema
5745 }
5746
5747 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
5749 self.dyn_phi_layer = layer;
5750 self.dyn_phi_ema.clear();
5751 self.dyn_phi_seen = 0;
5752 }
5753
5754 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
5756 let Some(model) = &self.model else {
5757 return Vec::new();
5758 };
5759 model
5760 .header
5761 .skills
5762 .iter()
5763 .enumerate()
5764 .filter_map(|(i, sk)| {
5765 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
5766 let sel = sk.selection.as_ref()?;
5767 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
5768 })
5769 .collect()
5770 }
5771
5772 pub fn active_skill(&self) -> Option<usize> {
5774 self.dyn_active
5775 }
5776
5777 pub fn enable_dynamic_routing(&mut self) -> usize {
5782 use crate::swarm::{DynRouter, RoutableSkill};
5783 let Some(model) = self.model.clone() else {
5784 return 0;
5785 };
5786 if self.dyn_blend_loaded {
5789 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
5790 return 0;
5791 }
5792 if let Some(a) = self.dyn_active {
5796 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
5797 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
5798 return 0;
5799 }
5800 }
5801 let hidden = self.hidden_size;
5802 let mut skills = Vec::new();
5803 for (idx, id, _phi) in self.dynamic_skills() {
5804 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
5805 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
5806 skills.push(rs);
5807 }
5808 }
5809 }
5810 if skills.is_empty() {
5811 return 0;
5812 }
5813 let phi = skills[0].phi_layer;
5815 if skills.iter().any(|s| s.phi_layer != phi) {
5816 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
5817 }
5818 let n = skills.len();
5819 self.set_dyn_phi_layer(Some(phi));
5820 self.dyn_router = Some(DynRouter::new(skills));
5821 n
5822 }
5823
5824 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
5826 self.dyn_router
5827 .as_ref()
5828 .map(|r| r.switches.clone())
5829 .unwrap_or_default()
5830 }
5831
5832 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
5835 let rows = self.weights.lm_head.rows();
5836 let mut logits = attention::take_buf(rows.min(self.vocab_size));
5837 self.weights
5838 .lm_head
5839 .matvec(hidden, &mut logits, self.pool.as_deref());
5840 logits.resize(self.vocab_size, 0.0);
5841 if let Some(m) = self.logit_multiplier {
5842 for l in logits.iter_mut() {
5843 *l *= m;
5844 }
5845 }
5846 if let Some(c) = self.final_softcap {
5847 for l in logits.iter_mut() {
5848 *l = c * (*l / c).tanh();
5849 }
5850 }
5851 logits
5852 }
5853
5854 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
5859 self.kv_cache.clear();
5860 self.kv_history.clear();
5861 let mut hidden = vec![0.0f32; self.hidden_size];
5862 for (pos, &id) in ids.iter().enumerate() {
5863 let emb = self.embed_single(id);
5864 hidden = self.forward_layers(&emb, pos, task_mask);
5865 }
5866 inference::rms_norm_into(
5867 &hidden,
5868 &self.weights.final_norm,
5869 self.rms_eps,
5870 self.norm_style,
5871 &mut self.ws.n1,
5872 );
5873 self.lm_head_forward(&self.ws.n1)
5874 }
5875}
5876
5877pub fn create_test_pipeline(
5879 hidden_size: usize,
5880 intermediate_size: usize,
5881 num_heads: usize,
5882 num_kv_heads: usize,
5883 head_dim: usize,
5884 num_layers: usize,
5885 vocab_size: usize,
5886) -> Pipeline {
5887 let synth = |n: usize, salt: usize| -> Vec<f32> {
5890 (0..n)
5891 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
5892 .collect()
5893 };
5894 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
5895 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
5896 };
5897 let layer_weights: Vec<LayerWeights> = (0..num_layers)
5898 .map(|li| LayerWeights {
5899 input_norm: vec![1.0; hidden_size],
5900 post_norm: vec![1.0; hidden_size],
5901 attn_out_norm: None,
5902 ffn_out_norm: None,
5903 layer_scale: None,
5904 ffn: FfnKind::Dense(DenseFfn {
5905 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
5906 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
5907 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
5908 act: Act::Silu,
5909 }),
5910 attn: AttnKind::Full {
5911 bias: None,
5912 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
5913 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
5914 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
5915 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
5916 q_norm: None,
5917 k_norm: None,
5918 output_gate: false,
5919 softplus_gate: None,
5920 },
5921 })
5922 .collect();
5923
5924 Pipeline::new(
5925 Tokenizer::byte_level(),
5926 PipelineWeights {
5927 embed_tokens: qt(vocab_size, hidden_size, 100),
5928 layers: layer_weights,
5929 lm_head: qt(vocab_size, hidden_size, 200),
5930 final_norm: vec![1.0; hidden_size],
5931 },
5932 hidden_size,
5933 intermediate_size,
5934 num_heads,
5935 num_kv_heads,
5936 head_dim,
5937 num_layers,
5938 num_layers, false, vocab_size,
5941 1e-6,
5942 10_000.0,
5943 NormStyle::Qwen,
5944 4096,
5945 SamplerConfig {
5946 seed: Some(42),
5947 ..Default::default()
5948 },
5949 )
5950}
5951
5952fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
5955 let inter = d.gate_proj.rows();
5956 let hidden = d.down_proj.rows();
5957 if d.act == Act::Silu && b >= 32 && crate::gpu::enabled_here() && !crate::gpu::mm_killed() {
5963 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
5964 d.gate_proj.mapped_q4t(),
5965 d.up_proj.mapped_q4t(),
5966 d.down_proj.mapped_q4t(),
5967 ) {
5968 let mut out = vec![0.0f32; b * hidden];
5969 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
5970 return out;
5971 }
5972 }
5973 }
5974 let mut g = vec![0.0f32; b * inter];
5975 d.gate_proj.matmat(xs, b, &mut g, pool);
5976 let mut u = vec![0.0f32; b * inter];
5977 d.up_proj.matmat(xs, b, &mut u, pool);
5978 for i in 0..b * inter {
5979 g[i] = d.act.combine(g[i], u[i]);
5980 }
5981 let mut out = vec![0.0f32; b * hidden];
5982 d.down_proj.matmat(&g, b, &mut out, pool);
5983 out
5984}
5985
5986fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
5991 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5992 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5993 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
5994 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
5995 if (!on && !dump) || b == 0 {
5996 return;
5997 }
5998 let hidden = xs.len() / b;
5999 if on {
6000 let mut acc = m.act_sq.borrow_mut();
6001 if acc.len() < hidden {
6002 acc.resize(hidden, 0.0);
6003 }
6004 for t in 0..b {
6005 let row = &xs[t * hidden..(t + 1) * hidden];
6006 for (a, &v) in acc.iter_mut().zip(row) {
6007 *a += (v as f64) * (v as f64);
6008 }
6009 }
6010 }
6011 if dump {
6012 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
6015 .ok()
6016 .and_then(|v| v.parse().ok())
6017 .unwrap_or(4096);
6018 let mut rows = m.act_rows.borrow_mut();
6019 if rows.len() < cap * hidden {
6020 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
6021 rows.extend_from_slice(&xs[..take * hidden]);
6022 }
6023 }
6024}
6025
6026fn moe_ffn_batch(
6027 m: &MoeFfn,
6028 xs: &[f32],
6029 b: usize,
6030 hidden: usize,
6031 pool: Option<&Pool>,
6032 allowed: Option<&[bool]>,
6033) -> Vec<f32> {
6034 accumulate_act(m, xs, b);
6035 let ne = m.experts.len();
6036 let mut logits = vec![0.0f32; b * ne];
6037 m.router.matmat(xs, b, &mut logits, pool);
6038
6039 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
6042 {
6043 let mut st = m.stats.borrow_mut();
6044 if st.len() < ne {
6045 st.resize(ne, 0);
6046 }
6047 for bi in 0..b {
6048 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
6049 for &e in &idx {
6050 st[e] += 1;
6051 assign[e].push((bi, p[e] / wsum));
6052 }
6053 }
6054 }
6055
6056 let mut out = vec![0.0f32; b * hidden];
6057 let cols = m.experts[0].gate_proj.cols();
6058 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
6059 let sb = list.len();
6060 let mut sub = vec![0.0f32; sb * cols];
6061 for (k, &(bi, _)) in list.iter().enumerate() {
6062 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
6063 }
6064 let eo = dense_ffn_batch(d, &sub, sb, pool);
6065 for (k, &(bi, w)) in list.iter().enumerate() {
6066 for i in 0..hidden {
6067 out[bi * hidden + i] += w * eo[k * hidden + i];
6068 }
6069 }
6070 };
6071 for (e, a) in assign.iter().enumerate().take(ne) {
6072 if !a.is_empty() {
6073 run_expert(&m.experts[e], a);
6074 }
6075 }
6076 if let Some((se, gate)) = &m.shared {
6077 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
6078 let mut gl = vec![0.0f32; b];
6079 gate.matmat(xs, b, &mut gl, pool);
6080 (0..b)
6081 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
6082 .collect()
6083 } else {
6084 (0..b).map(|bi| (bi, 1.0)).collect()
6085 };
6086 run_expert(se, &all);
6087 }
6088 out
6089}
6090
6091thread_local! {
6092 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
6096 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
6097}
6098
6099fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
6101 if crate::gpu::enabled_here()
6112 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
6113 {
6114 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
6115 crate::gpu::ProbeArm::Gpu
6116 } else {
6117 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
6118 };
6119 match arm {
6120 crate::gpu::ProbeArm::Gpu => {
6121 let t0 = std::time::Instant::now();
6122 if let Some(out) = dense_ffn_gpu(d, x, pool) {
6123 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
6124 return out;
6125 }
6126 }
6127 crate::gpu::ProbeArm::CpuTimed => {
6128 let t0 = std::time::Instant::now();
6129 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
6130 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
6131 return out;
6132 }
6133 crate::gpu::ProbeArm::Cpu => {
6134 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
6135 }
6136 }
6137 }
6138 dense_ffn_cpu(d, x, pool)
6139}
6140
6141fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
6143 let inter = d.gate_proj.rows();
6144 FFN_SCRATCH.with(|s| {
6145 let mut s = s.borrow_mut();
6146 let [g, u, ..] = &mut *s;
6147 g.resize(inter, 0.0);
6148 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
6151 } else {
6153 u.resize(inter, 0.0);
6154 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
6156 for i in 0..inter {
6157 g[i] = d.act.combine(g[i], u[i]);
6158 }
6159 }
6160 FFN_PROBE.with(|pr| {
6163 if let Some(acc) = pr.borrow_mut().as_mut() {
6164 let li = crate::gpu::cur_layer();
6165 if li >= 0 {
6166 if let Some(row) = acc.get_mut(li as usize) {
6167 for (a, &v) in row.iter_mut().zip(g.iter()) {
6168 *a += (v as f64).abs();
6169 }
6170 }
6171 }
6172 }
6173 });
6174 let mut out = attention::take_buf(d.down_proj.rows());
6175 d.down_proj.matvec(g, &mut out, pool);
6176 out
6177 })
6178}
6179
6180thread_local! {
6181 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
6184 const { std::cell::RefCell::new(None) };
6185}
6186
6187fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
6193 if d.act != Act::Silu {
6195 return None;
6196 }
6197 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
6200 return None;
6201 }
6202 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
6203 let mut model_ref = None;
6204 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
6205 let model = model_ref?;
6206 let hidden = jobs[0].down.1;
6207 let mut out = attention::take_buf(hidden);
6208 if crate::gpu::moe_block(&model, &jobs, &mut out) {
6209 Some(out)
6210 } else {
6211 let mut out = out;
6212 attention::recycle_buf(&mut out);
6213 None
6214 }
6215}
6216
6217#[allow(clippy::type_complexity)]
6222#[allow(clippy::type_complexity)]
6223pub(crate) fn moe_parts(
6224 t: &QTensor,
6225) -> Option<(
6226 &std::sync::Arc<cortiq_core::CmfModel>,
6227 usize,
6228 usize,
6229 usize,
6230 &[f32],
6231 &[f32],
6232 bool,
6233 bool,
6234)> {
6235 match t {
6236 QTensor::Mapped {
6237 model,
6238 idx,
6239 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
6240 rows,
6241 cols,
6242 row_scale,
6243 col_field,
6244 ..
6245 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
6246 model, *idx, *rows, *cols, row_scale, col_field, false, false,
6247 )),
6248 QTensor::Mapped {
6250 model,
6251 idx,
6252 dtype: cortiq_core::TensorDtype::Q1,
6253 rows,
6254 cols,
6255 ..
6256 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
6257 QTensor::Mapped {
6259 model,
6260 idx,
6261 dtype: cortiq_core::TensorDtype::Q4Tiled,
6262 rows,
6263 cols,
6264 ..
6265 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
6266 QTensor::Mapped {
6268 model,
6269 idx,
6270 dtype: cortiq_core::TensorDtype::Q4TiledP,
6271 rows,
6272 cols,
6273 ..
6274 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
6275 _ => None,
6276 }
6277}
6278
6279pub(crate) fn moe_push_job_parts<'a>(
6283 gate: &'a QTensor,
6284 up: &'a QTensor,
6285 down: &'a QTensor,
6286 x: &[f32],
6287 w: f32,
6288 swiglu_limit: f32,
6289 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
6290 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
6291) -> Option<()> {
6292 use crate::qtensor::prescale;
6293 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(gate)?;
6294 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(up)?;
6295 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(down)?;
6296 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
6297 return None; }
6299 model_ref.get_or_insert_with(|| gm.clone());
6300 let dt = |cf: &[f32]| {
6301 if cf.is_empty() {
6302 cortiq_core::TensorDtype::Q8Row
6303 } else {
6304 cortiq_core::TensorDtype::Q8_2f
6305 }
6306 };
6307 jobs.push(crate::gpu::MoeJob {
6308 gate: (gi, gr, gc, grs),
6309 up: (ui, ur, uc, urs),
6310 down: (di, dr, dc, drs),
6311 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
6312 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
6313 down_col: dcf,
6314 w,
6315 q1: gq1,
6316 q4t: gq4 && gate.mapped_q4tp().is_none(),
6317 q4tp: gq4 && gate.mapped_q4tp().is_some(),
6318 swiglu_limit,
6319 });
6320 Some(())
6321}
6322
6323fn moe_push_job<'a>(
6325 d: &'a DenseFfn,
6326 x: &[f32],
6327 w: f32,
6328 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
6329 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
6330) -> Option<()> {
6331 use crate::qtensor::prescale;
6332 if d.act != Act::Silu {
6333 return None; }
6335 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
6336 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
6337 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
6338 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
6339 return None; }
6341 model_ref.get_or_insert_with(|| gm.clone());
6342 let gdt = if gcf.is_empty() {
6343 cortiq_core::TensorDtype::Q8Row
6344 } else {
6345 cortiq_core::TensorDtype::Q8_2f
6346 };
6347 let udt = if ucf.is_empty() {
6348 cortiq_core::TensorDtype::Q8Row
6349 } else {
6350 cortiq_core::TensorDtype::Q8_2f
6351 };
6352 jobs.push(crate::gpu::MoeJob {
6353 gate: (gi, gr, gc, grs),
6354 up: (ui, ur, uc, urs),
6355 down: (di, dr, dc, drs),
6356 xs_gate: prescale(x, gcf, gdt).into_owned(),
6357 xs_up: prescale(x, ucf, udt).into_owned(),
6358 down_col: dcf,
6359 w,
6360 q1: gq1,
6361 q4t: gq4 && d.gate_proj.mapped_q4tp().is_none(),
6362 q4tp: gq4 && d.gate_proj.mapped_q4tp().is_some(),
6363 swiglu_limit: 0.0,
6364 });
6365 Some(())
6366}
6367
6368fn sparse_ffn_quant(
6375 d: &DenseFfn,
6376 x: &[f32],
6377 active: &[u16],
6378 hidden: usize,
6379 pool: Option<&Pool>,
6380) -> Vec<f32> {
6381 let n = active.len();
6382 let inter = d.gate_proj.rows();
6383 let mut act = vec![0.0f32; n];
6384 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
6387 let compute = |ai: usize| -> f32 {
6388 let idx = active[ai] as usize;
6389 if idx >= inter {
6390 return 0.0; }
6392 let mut s = if need_scratch {
6393 vec![0.0f32; hidden]
6394 } else {
6395 Vec::new()
6396 };
6397 let gate = d.gate_proj.row_dot(idx, x, &mut s);
6398 let up = d.up_proj.row_dot(idx, x, &mut s);
6399 d.act.combine(gate, up)
6400 };
6401 match pool {
6402 Some(p) if n >= 256 => {
6403 let ptr = SendMut(act.as_mut_ptr());
6404 p.run(&|widx, nw| {
6405 let chunk = n.div_ceil(nw);
6406 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
6407 for ai in s..e {
6408 unsafe { *ptr.at(ai) = compute(ai) };
6409 }
6410 });
6411 }
6412 _ => {
6413 for (ai, a) in act.iter_mut().enumerate() {
6414 *a = compute(ai);
6415 }
6416 }
6417 }
6418 let mut out = vec![0.0f32; hidden];
6420 for (ai, &idx) in active.iter().enumerate() {
6421 let w = act[ai];
6422 if w.abs() >= 1e-12 && (idx as usize) < inter {
6423 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
6424 }
6425 }
6426 out
6427}
6428
6429#[doc(hidden)]
6431pub fn sparse_ffn_quant_for_test(
6432 d: &DenseFfn,
6433 x: &[f32],
6434 active: &[u16],
6435 hidden: usize,
6436) -> Vec<f32> {
6437 sparse_ffn_quant(d, x, active, hidden, None)
6438}
6439
6440fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
6444 let deq = |t: &QTensor| -> Vec<f32> {
6445 let (rows, cols) = (t.rows(), t.cols());
6446 let mut out = vec![0.0f32; rows * cols];
6447 for r in 0..rows {
6448 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
6449 }
6450 out
6451 };
6452 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
6453}
6454
6455struct SendMut(*mut f32);
6457unsafe impl Send for SendMut {}
6458unsafe impl Sync for SendMut {}
6459impl SendMut {
6460 #[inline]
6461 #[allow(clippy::mut_from_ref)]
6464 unsafe fn at(&self, i: usize) -> &mut f32 {
6465 unsafe { &mut *self.0.add(i) }
6466 }
6467}
6468
6469fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
6479 let ne = logits.len();
6480 let p: Vec<f32> = if m.router_sigmoid {
6481 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
6482 } else {
6483 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6484 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
6485 let s: f32 = e.iter().sum();
6486 for v in &mut e {
6487 *v /= s;
6488 }
6489 e
6490 };
6491 let admit = |e: usize| {
6497 m.mask.as_ref().is_none_or(|mk| mk[e])
6498 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
6499 };
6500 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
6501 match &m.expert_bias {
6503 Some(b) => idx.sort_unstable_by(|&x, &y| {
6504 (p[y] + b[y])
6505 .partial_cmp(&(p[x] + b[x]))
6506 .unwrap()
6507 .then(x.cmp(&y))
6508 }),
6509 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
6510 }
6511 idx.truncate(m.top_k);
6512 if let Some(tau) = m.route_tau {
6516 let total: f32 = idx.iter().map(|&e| p[e]).sum();
6517 if total > 0.0 {
6518 let mut acc = 0.0f32;
6519 let mut keep = idx.len();
6520 for (i, &e) in idx.iter().enumerate() {
6521 acc += p[e];
6522 if acc >= tau * total {
6523 keep = i + 1;
6524 break;
6525 }
6526 }
6527 idx.truncate(keep);
6528 }
6529 }
6530 let wsum: f32 = if m.norm_topk_prob {
6531 let s: f32 = idx.iter().map(|&e| p[e]).sum();
6532 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
6535 } else {
6536 1.0 / m.routed_scaling
6537 };
6538 (idx, p, wsum)
6539}
6540
6541fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
6544 accumulate_act(m, x, 1);
6545 let ne = m.experts.len();
6546 let mut logits = vec![0.0f32; ne];
6547 m.router.matvec(x, &mut logits, pool);
6548 let (idx, p, wsum) = moe_route(&logits, m, allowed);
6549 {
6550 let mut st = m.stats.borrow_mut();
6551 if st.len() < ne {
6552 st.resize(ne, 0);
6553 }
6554 for &e in &idx {
6555 st[e] += 1;
6556 }
6557 }
6558 if crate::gpu::enabled_here() {
6563 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
6564 crate::gpu::ProbeArm::Gpu => {
6565 let t0 = std::time::Instant::now();
6566 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
6567 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
6568 return out;
6569 }
6570 }
6571 crate::gpu::ProbeArm::CpuTimed => {
6572 let t0 = std::time::Instant::now();
6573 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
6574 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
6575 return out;
6576 }
6577 crate::gpu::ProbeArm::Cpu => {
6578 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
6579 }
6580 }
6581 }
6582 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
6583}
6584
6585fn graph_note(built: bool) {
6589 use std::sync::atomic::{AtomicBool, Ordering};
6590 static SAID: AtomicBool = AtomicBool::new(false);
6591 if !SAID.swap(true, Ordering::Relaxed) {
6592 if built {
6593 tracing::info!("wgpu whole-token graph: ACTIVE");
6594 } else {
6595 tracing::warn!("wgpu whole-token graph refused — per-op path");
6596 }
6597 }
6598}
6599
6600fn moe_batch_enabled() -> bool {
6603 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6604 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
6605}
6606
6607fn moe_ffn_cpu_batched(
6613 m: &MoeFfn,
6614 x: &[f32],
6615 idx: &[usize],
6616 p: &[f32],
6617 wsum: f32,
6618 pool: Option<&Pool>,
6619) -> Option<Vec<f32>> {
6620 if idx.is_empty() || !moe_batch_enabled() {
6621 return None;
6622 }
6623 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
6627 return None;
6628 }
6629 let n = idx.len() + usize::from(m.shared.is_some());
6630 let mut pairs = Vec::with_capacity(n);
6631 let mut downs = Vec::with_capacity(n);
6632 let mut ws = Vec::with_capacity(n);
6633 for &e in idx {
6634 let d = &m.experts[e];
6635 if d.act != Act::Silu {
6636 return None;
6637 }
6638 pairs.push((&d.gate_proj, &d.up_proj));
6639 downs.push(&d.down_proj);
6640 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
6641 }
6642 if let Some((se, gate)) = &m.shared {
6645 if se.act != Act::Silu {
6646 return None;
6647 }
6648 let g = gate.as_ref().map_or(1.0, |gate| {
6649 let mut gl = [0.0f32; 1];
6650 gate.matvec(x, &mut gl, pool);
6651 1.0 / (1.0 + (-gl[0]).exp())
6652 });
6653 pairs.push((&se.gate_proj, &se.up_proj));
6654 downs.push(&se.down_proj);
6655 ws.push(g);
6656 }
6657 let inter = pairs[0].0.rows();
6658 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
6659 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
6660 return None;
6661 }
6662 let mut out = attention::take_buf(x.len());
6663 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
6664 attention::recycle_buf(&mut out);
6665 return None;
6666 }
6667 Some(out)
6668}
6669
6670fn moe_ffn_cpu(
6672 m: &MoeFfn,
6673 x: &[f32],
6674 idx: &[usize],
6675 p: &[f32],
6676 wsum: f32,
6677 pool: Option<&Pool>,
6678) -> Vec<f32> {
6679 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
6680 return out;
6681 }
6682 let mut out = attention::take_buf(x.len());
6683 for &e in idx {
6684 let mut eo = dense_ffn(&m.experts[e], x, pool);
6685 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
6686 for i in 0..out.len() {
6687 out[i] += w * eo[i];
6688 }
6689 attention::recycle_buf(&mut eo);
6690 }
6691 if let Some((se, gate)) = &m.shared {
6692 let mut so = dense_ffn(se, x, pool);
6693 let g = gate.as_ref().map_or(1.0, |gate| {
6694 let mut gl = [0.0f32; 1];
6695 gate.matvec(x, &mut gl, pool);
6696 1.0 / (1.0 + (-gl[0]).exp())
6697 });
6698 for i in 0..out.len() {
6699 out[i] += g * so[i];
6700 }
6701 attention::recycle_buf(&mut so);
6702 }
6703 out
6704}
6705
6706#[allow(clippy::too_many_arguments)]
6714fn mla_attention(
6715 w: &MlaWeights,
6716 normed: &[f32],
6717 cache: &mut crate::kv_cache::LayerKvCache,
6718 position: usize,
6719 inv_freq: &[f32],
6720 rope_scale: f32,
6721 eps: f64,
6722 pool: Option<&Pool>,
6723) -> Vec<f32> {
6724 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
6725 let hd = dr + dn;
6726 let mut q = vec![0.0f32; nh * hd];
6727 match (&w.q_a, &w.q_a_norm) {
6728 (Some(qa), Some(qn)) => {
6729 let mut t = vec![0.0f32; qa.rows()];
6730 qa.matvec(normed, &mut t, pool);
6731 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
6732 w.q_proj.matvec(&tn, &mut q, pool);
6733 }
6734 _ => w.q_proj.matvec(normed, &mut q, pool),
6735 }
6736 let mut ca = vec![0.0f32; lora + dr];
6737 w.kv_a.matvec(normed, &mut ca, pool);
6738 let (c_lat, k_rope) = ca.split_at_mut(lora);
6739 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
6740 let mut kvb = vec![0.0f32; nh * (dn + dv)];
6741 w.kv_b.matvec(&latn, &mut kvb, pool);
6742 if !w.nope {
6743 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
6744 }
6745 for h in 0..nh {
6746 if !w.nope {
6747 attention::rope_rotate_scaled(
6748 &mut q[h * hd..h * hd + dr],
6749 position,
6750 inv_freq,
6751 rope_scale,
6752 );
6753 }
6754 }
6755 let mut k = vec![0.0f32; nh * hd];
6756 let mut v = vec![0.0f32; nh * hd];
6757 for h in 0..nh {
6758 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
6759 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
6760 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
6761 }
6762 cache.append(&k, &v, &vec![true; nh]);
6763 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
6764 attention::recycle_buf(&mut imp);
6765 let mut ov = vec![0.0f32; nh * dv];
6766 for h in 0..nh {
6767 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
6768 }
6769 let mut out = vec![0.0f32; w.o_proj.rows()];
6770 w.o_proj.matvec(&ov, &mut out, pool);
6771 out
6772}
6773
6774fn dense_moe_ffn(
6781 dm: &DenseMoeFfn,
6782 x_normed: &[f32],
6783 h_raw: &[f32],
6784 eps: f64,
6785 norm_style: NormStyle,
6786 pool: Option<&Pool>,
6787) -> Vec<f32> {
6788 let mut d = dense_ffn(&dm.dense, x_normed, pool);
6789 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
6790 let m = &dm.moe;
6791 let ne = m.experts.len();
6792 let mut logits = vec![0.0f32; ne];
6793 if m.router_input_norm {
6794 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
6795 let inv = 1.0 / (ss + eps as f32).sqrt();
6796 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
6797 m.router.matvec(&xr, &mut logits, pool);
6798 } else {
6799 m.router.matvec(h_raw, &mut logits, pool);
6800 }
6801 let (idx, p, wsum) = moe_route(&logits, m, None);
6802 {
6803 let mut st = m.stats.borrow_mut();
6804 if st.len() < ne {
6805 st.resize(ne, 0);
6806 }
6807 for &e in &idx {
6808 st[e] += 1;
6809 }
6810 }
6811 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
6812 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
6813 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
6814 for (di, mi) in d.iter_mut().zip(&mo) {
6815 *di += mi;
6816 }
6817 d
6818}
6819
6820fn moe_gpu_refused(why: &'static str) {
6827 use std::sync::atomic::{AtomicBool, Ordering};
6828 static SAID: AtomicBool = AtomicBool::new(false);
6829 if !SAID.swap(true, Ordering::Relaxed) {
6830 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
6831 }
6832}
6833
6834fn moe_ffn_gpu(
6835 m: &MoeFfn,
6836 x: &[f32],
6837 idx: &[usize],
6838 p: &[f32],
6839 wsum: f32,
6840 pool: Option<&Pool>,
6841) -> Option<Vec<f32>> {
6842 use crate::gpu::MoeJob;
6843
6844 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
6845 let mut model_ref = None;
6846 for &e in idx {
6847 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
6848 moe_gpu_refused("push_job(expert)");
6849 return None;
6850 }
6851 }
6852 if let Some((se, gate)) = &m.shared {
6853 let g = gate.as_ref().map_or(1.0, |gate| {
6854 let mut gl = [0.0f32; 1];
6855 gate.matvec(x, &mut gl, pool);
6856 1.0 / (1.0 + (-gl[0]).exp())
6857 });
6858 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
6859 moe_gpu_refused("push_job(shared)");
6860 return None;
6861 }
6862 }
6863 let Some(model) = model_ref else {
6864 moe_gpu_refused("no model_ref");
6865 return None;
6866 };
6867 let hidden = jobs[0].down.1;
6868 let mut out = vec![0.0f32; hidden];
6869 if crate::gpu::moe_block(&model, &jobs, &mut out) {
6870 Some(out)
6871 } else {
6872 moe_gpu_refused("gpu::moe_block");
6873 None
6874 }
6875}
6876
6877fn ffn_forward(
6879 ffn: &FfnKind,
6880 x: &[f32],
6881 pool: Option<&Pool>,
6882 experts_allowed: Option<&[bool]>,
6883) -> Vec<f32> {
6884 match ffn {
6885 FfnKind::Dense(d) => dense_ffn(d, x, pool),
6886 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
6887 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
6891 }
6892}
6893
6894fn ffn_forward_pair(
6898 ffn: &FfnKind,
6899 x1: &[f32],
6900 x2: &[f32],
6901 pool: Option<&Pool>,
6902 experts_allowed: Option<&[bool]>,
6903) -> (Vec<f32>, Vec<f32>) {
6904 let d = match ffn {
6905 FfnKind::Dense(d) => d,
6906 FfnKind::Moe(m) => {
6907 return (
6908 moe_ffn(m, x1, pool, experts_allowed),
6909 moe_ffn(m, x2, pool, experts_allowed),
6910 );
6911 }
6912 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
6913 };
6914 let inter = d.gate_proj.rows();
6915 FFN_SCRATCH.with(|s| {
6916 let mut s = s.borrow_mut();
6917 let [g1, g2, u1, u2] = &mut *s;
6918 g1.resize(inter, 0.0);
6919 g2.resize(inter, 0.0);
6920 u1.resize(inter, 0.0);
6921 u2.resize(inter, 0.0);
6922 QTensor::matvec2_many(
6925 [&d.gate_proj, &d.up_proj],
6926 x1,
6927 x2,
6928 [g1.as_mut_slice(), u1.as_mut_slice()],
6929 [g2.as_mut_slice(), u2.as_mut_slice()],
6930 pool,
6931 );
6932 for i in 0..inter {
6933 g1[i] = d.act.combine(g1[i], u1[i]);
6934 g2[i] = d.act.combine(g2[i], u2[i]);
6935 }
6936 let mut o1 = attention::take_buf(d.down_proj.rows());
6937 let mut o2 = attention::take_buf(d.down_proj.rows());
6938 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
6939 (o1, o2)
6940 })
6941}
6942
6943#[cfg(test)]
6944mod tests {
6945
6946 #[test]
6947 fn cancel_flag_stops_generation() {
6948 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
6949 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
6952 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
6953 assert_eq!(r.finish_reason, "cancelled");
6954 assert!(
6955 r.token_ids.is_empty(),
6956 "no tokens after cancel: {:?}",
6957 r.token_ids
6958 );
6959 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
6961 assert_ne!(r2.finish_reason, "cancelled");
6962 }
6963 use super::*;
6964
6965 #[test]
6971 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
6972 let (hidden, inter) = (16usize, 40usize);
6973 let synth = |n: usize, salt: usize| -> Vec<f32> {
6974 (0..n)
6975 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
6976 .collect()
6977 };
6978 let d = DenseFfn {
6979 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
6980 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
6981 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
6982 act: Act::Silu,
6983 };
6984 let x = synth(hidden, 9);
6985 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
6987
6988 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
6989
6990 let mut g = vec![0.0f32; inter];
6992 d.gate_proj.matvec(&x, &mut g, None);
6993 let mut u = vec![0.0f32; inter];
6994 d.up_proj.matvec(&x, &mut u, None);
6995 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
6996 for i in 0..inter {
6997 g[i] = if act_set.contains(&(i as u16)) {
6998 inference::silu(g[i]) * u[i]
6999 } else {
7000 0.0
7001 };
7002 }
7003 let mut reference = vec![0.0f32; hidden];
7004 d.down_proj.matvec(&g, &mut reference, None);
7005
7006 let max_d = sparse
7007 .iter()
7008 .zip(&reference)
7009 .map(|(a, b)| (a - b).abs())
7010 .fold(0.0f32, f32::max);
7011 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
7012 }
7013
7014 fn attach_test_mtp(p: &mut Pipeline) {
7016 let (h, inter, heads, kv, hd) = (
7017 p.hidden_size,
7018 p.intermediate_size,
7019 p.num_heads,
7020 p.num_kv_heads,
7021 p.head_dim,
7022 );
7023 let synth = |n: usize, salt: usize| -> Vec<f32> {
7024 (0..n)
7025 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
7026 .collect()
7027 };
7028 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
7029 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
7030 };
7031 p.mtp = Some(MtpModule {
7032 enorm: vec![1.0; h],
7033 hnorm: vec![1.0; h],
7034 eh_proj: qt(h, 2 * h, 301),
7035 layer: LayerWeights {
7036 input_norm: vec![1.0; h],
7037 post_norm: vec![1.0; h],
7038 attn_out_norm: None,
7039 ffn_out_norm: None,
7040 layer_scale: None,
7041 ffn: FfnKind::Dense(DenseFfn {
7042 gate_proj: qt(inter, h, 315),
7043 up_proj: qt(inter, h, 316),
7044 down_proj: qt(h, inter, 317),
7045 act: Act::Silu,
7046 }),
7047 attn: AttnKind::Full {
7048 bias: None,
7049 wq: qt(heads * hd, h, 311),
7050 wk: qt(kv * hd, h, 312),
7051 wv: qt(kv * hd, h, 313),
7052 wo: qt(h, heads * hd, 314),
7053 q_norm: None,
7054 k_norm: None,
7055 output_gate: false,
7056 softplus_gate: None,
7057 },
7058 },
7059 final_norm: vec![1.0; h],
7060 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
7061 });
7062 }
7063
7064 #[test]
7065 fn speculative_equals_vanilla_greedy() {
7066 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7070 let run = |spec: bool| {
7071 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7072 p.sampler_config.temperature = 0.0;
7073 attach_test_mtp(&mut p);
7074 p.speculative = spec;
7075 let r = p.generate("abcdef", 12, None, None).unwrap();
7076 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
7077 };
7078 let (vanilla, d0, _) = run(false);
7079 let (spec, d1, a1) = run(true);
7080 assert_eq!(d0, 0, "vanilla path must not draft");
7081 assert!(d1 > 0, "speculative path must draft");
7082 assert_eq!(
7083 vanilla, spec,
7084 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
7085 );
7086 }
7087
7088 #[test]
7089 fn speculative_accepts_constant_oracle() {
7090 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7092 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7093 p.sampler_config.temperature = 0.0;
7094 p.sampler_config.repetition_penalty = 1.0;
7095 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
7098 attach_test_mtp(&mut p);
7099 p.speculative = true;
7100 let r = p.generate("abcd", 10, None, None).unwrap();
7101 assert!(r.mtp_drafted > 0);
7102 assert_eq!(
7103 r.mtp_accepted, r.mtp_drafted,
7104 "constant logits → every draft accepted"
7105 );
7106 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
7109 }
7110
7111 #[test]
7112 fn empty_prompt_is_an_error_not_a_panic() {
7113 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
7114 let r = p.generate("", 4, None, None);
7115 assert!(r.is_err(), "empty prompt must be a clean error");
7116 }
7117
7118 #[test]
7119 fn every_token_enters_kv_exactly_once() {
7120 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7121 p.sampler_config.temperature = 0.0;
7123 let r = p.generate("abc", 2, None, None).unwrap();
7124 assert_eq!(r.prompt_tokens, 3);
7125 assert_eq!(
7129 p.kv_cache.seq_len(),
7130 3 + r.tokens_generated - 1,
7131 "each token must be cached exactly once (v1 cached the last prompt token twice)"
7132 );
7133 }
7134
7135 #[test]
7136 fn generation_is_reproducible_with_seed() {
7137 let run = || {
7138 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7139 p.generate("hello", 8, None, None).unwrap().token_ids
7140 };
7141 assert_eq!(run(), run());
7142 }
7143
7144 #[test]
7145 fn resetting_sampler_restarts_the_seeded_stream() {
7146 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7147 let config = SamplerConfig {
7148 seed: Some(1234),
7149 ..SamplerConfig::default()
7150 };
7151 p.set_sampler_config(config.clone());
7152 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
7153 p.set_sampler_config(config);
7154 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
7155 assert_eq!(first, second);
7156 }
7157
7158 #[test]
7159 fn eviction_bounds_the_cache() {
7160 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
7161 p.kv_cache.max_seq_len = 6;
7162 p.sampler_config.temperature = 0.0;
7163 let _ = p.generate("abcd", 12, None, None).unwrap();
7164 assert!(
7165 p.kv_cache.seq_len() <= 6 + 1,
7166 "cache must stay bounded by max_seq_len (got {})",
7167 p.kv_cache.seq_len()
7168 );
7169 }
7170
7171 #[test]
7172 fn confidence_matches_tokens_and_is_a_probability() {
7173 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7174 p.sampler_config.temperature = 0.0;
7175 p.sampler_config.repetition_penalty = 1.0;
7176 let r = p.generate("abcd", 10, None, None).unwrap();
7177 assert_eq!(
7178 r.token_confidence.len(),
7179 r.token_ids.len(),
7180 "one confidence per emitted token"
7181 );
7182 for &c in &r.token_confidence {
7183 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
7184 }
7185 let logits = [1.0f32, 3.0, 0.5, 3.0];
7187 let p0 = top1_prob_t(&logits, 1, 1.0);
7188 let p1 = top1_prob_t(&logits, 3, 1.0);
7189 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
7190 assert!(p0 > 0.0 && p0 < 1.0);
7191 let sharp = top1_prob_t(&logits, 1, 1.0);
7193 let soft = top1_prob_t(&logits, 1, 2.0);
7194 assert!(soft < sharp, "higher temperature lowers peak confidence");
7195 }
7196
7197 #[test]
7198 fn trace_is_opt_in_and_parallels_the_output() {
7199 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7201 p.sampler_config.temperature = 0.0;
7202 p.sampler_config.repetition_penalty = 1.0;
7203 let r = p.generate("abcd", 10, None, None).unwrap();
7204 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
7205
7206 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7208 p.sampler_config.temperature = 0.0;
7209 p.sampler_config.repetition_penalty = 1.0;
7210 p.set_trace(true);
7211 let r = p.generate("abcd", 10, None, None).unwrap();
7212 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
7213 for (i, tr) in r.traces.iter().enumerate() {
7214 assert_eq!(tr.t, i, "trace index is sequential");
7215 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
7216 assert_eq!(
7217 tr.confidence, r.token_confidence[i],
7218 "trace confidence matches the confidence channel"
7219 );
7220 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
7222 }
7223 }
7224
7225 #[test]
7226 fn explain_prefill_logits_match_greedy_first_token() {
7227 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7231 p.sampler_config.temperature = 0.0;
7232 p.sampler_config.repetition_penalty = 1.0;
7233 let ids = p.tokenizer.encode("abcd");
7234 let logits = p.prefill_next_logits(&ids, None);
7235 let argmax = logits
7236 .iter()
7237 .enumerate()
7238 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7239 .unwrap()
7240 .0 as u32;
7241 let r = p.generate("abcd", 1, None, None).unwrap();
7242 assert_eq!(
7243 argmax, r.token_ids[0],
7244 "explain preview must match greedy emit"
7245 );
7246 }
7247
7248 #[test]
7249 fn laguna_shared_expert_is_unconditionally_added() {
7250 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
7251 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
7252 let zero_dense = || DenseFfn {
7253 gate_proj: matrix(vec![0.0; 4]),
7254 up_proj: matrix(vec![0.0; 4]),
7255 down_proj: matrix(vec![0.0; 4]),
7256 act: Act::Silu,
7257 };
7258 let shared = DenseFfn {
7259 gate_proj: identity(),
7260 up_proj: identity(),
7261 down_proj: identity(),
7262 act: Act::Silu,
7263 };
7264 let x = [1.0, 2.0];
7265 let expected = dense_ffn(&shared, &x, None);
7266 let moe = MoeFfn {
7267 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
7268 experts: vec![zero_dense()],
7269 top_k: 1,
7270 norm_topk_prob: true,
7271 router_sigmoid: true,
7272 expert_bias: None,
7273 routed_scaling: 1.0,
7274 route_tau: None,
7275 shared: Some((shared, None)),
7276 stats: std::cell::RefCell::new(Vec::new()),
7277 act_sq: std::cell::RefCell::new(Vec::new()),
7278 act_rows: std::cell::RefCell::new(Vec::new()),
7279 mask: None,
7280 per_expert_scale: None,
7281 router_input_norm: false,
7282 };
7283 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
7284 for (actual, expected) in actual.iter().zip(expected) {
7285 assert!((actual - expected).abs() < 1e-6);
7286 }
7287 }
7288}