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 kda_cfg: Option<crate::linear_core::KdaCfg>,
83 pub short_conv_cfg: Option<ShortConvCfg>,
86 pub mtp: Option<MtpModule>,
88 pub speculative: bool,
90 rng: SplitMix64,
91 sampler_scratch: SamplerScratch,
92 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
96 ws: ForwardScratch,
100 pool: Option<std::sync::Arc<Pool>>,
102 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
106 pub(crate) dyn_force_f32: bool,
108 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
113 pub(crate) dyn_active: Option<usize>,
119 pub(crate) dyn_blend_loaded: bool,
123 pub(crate) dyn_phi_layer: Option<usize>,
126 dyn_phi_ema: Vec<f32>,
128 dyn_phi_seen: usize,
129 pub dyn_router: Option<crate::swarm::DynRouter>,
132 o1_cfg: Option<crate::nystrom::O1Cfg>,
135 o1_flags: Vec<bool>,
137 trace: bool,
140 calib_temp: f32,
143 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
145 graph_kv_id: u64,
146 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
149 graph_want_logits: bool,
150 graph_logits: Option<Vec<f32>>,
153 pub embed_multiplier: f32,
155 pub attn_scale: f32,
158 pub swa: Option<(usize, usize)>,
161 pub sliding_layers: Option<Vec<bool>>,
164 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
167 pub rotary_dim_local: Option<usize>,
168 pub rope_scale: f32,
169 pub rope_scale_local: f32,
170 pub global_attn: Option<(usize, usize)>,
173 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
176 pub attn_v_norm: bool,
178 pub final_softcap: Option<f32>,
180 pub attn_softcap: f32,
182 confidence_on: bool,
186}
187
188#[cfg(target_os = "macos")]
189impl Drop for Pipeline {
190 fn drop(&mut self) {
191 crate::gpu::kv_mirror_drop(self.graph_kv_id);
192 }
193}
194
195pub struct PipelineWeights {
200 pub embed_tokens: QTensor,
202 pub layers: Vec<LayerWeights>,
204 pub lm_head: QTensor,
206 pub final_norm: Vec<f32>,
208}
209
210pub struct LayerWeights {
212 pub input_norm: Vec<f32>,
213 pub post_norm: Vec<f32>,
216 pub attn_out_norm: Option<Vec<f32>>,
219 pub layer_scale: Option<f32>,
221 pub ffn_out_norm: Option<Vec<f32>>,
224 pub ffn: FfnKind,
225 pub attn: AttnKind,
226}
227
228#[derive(Clone, Copy, PartialEq, Debug, Default)]
231pub enum Act {
232 #[default]
233 Silu,
234 GeluTanh,
235 Situ { beta: f32, linear_beta: f32 },
238}
239
240impl Act {
241 pub fn from_arch(name: &str) -> Self {
242 if name == "gelu_tanh" {
243 Self::GeluTanh
244 } else {
245 Self::Silu
246 }
247 }
248
249 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
251 match arch.hidden_act.as_str() {
252 "situ" => Self::Situ {
253 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
254 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
255 },
256 other => Self::from_arch(other),
257 }
258 }
259
260 #[inline]
261 pub fn apply(self, x: f32) -> f32 {
262 match self {
263 Self::Silu => inference::silu(x),
264 Self::GeluTanh => inference::gelu_tanh(x),
265 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
266 }
267 }
268
269 #[inline]
272 pub fn combine(self, g: f32, u: f32) -> f32 {
273 match self {
274 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
275 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
276 }
277 _ => self.apply(g) * u,
278 }
279 }
280}
281
282pub struct DenseFfn {
284 pub gate_proj: QTensor,
285 pub up_proj: QTensor,
286 pub down_proj: QTensor,
287 pub act: Act,
289}
290
291pub enum FfnKind {
294 Dense(DenseFfn),
295 Moe(MoeFfn),
299 DenseMoe(Box<DenseMoeFfn>),
306}
307
308pub struct DenseMoeFfn {
310 pub dense: DenseFfn,
311 pub moe: MoeFfn,
312 pub post_norm_1: Vec<f32>,
314 pub pre_norm_2: Vec<f32>,
317 pub post_norm_2: Vec<f32>,
319}
320
321pub struct MoeFfn {
322 pub router: QTensor,
324 pub experts: Vec<DenseFfn>,
325 pub top_k: usize,
326 pub norm_topk_prob: bool,
327 pub router_sigmoid: bool,
330 pub expert_bias: Option<Vec<f32>>,
334 pub routed_scaling: f32,
337 pub route_tau: Option<f32>,
343 pub shared: Option<(DenseFfn, Option<QTensor>)>,
346 pub stats: std::cell::RefCell<Vec<u64>>,
350 pub mask: Option<Vec<bool>>,
355 pub per_expert_scale: Option<Vec<f32>>,
358 pub router_input_norm: bool,
362}
363
364pub enum AttnKind {
367 Full {
369 wq: QTensor,
370 wk: QTensor,
371 wv: QTensor,
372 wo: QTensor,
373 q_norm: Option<Vec<f32>>,
374 k_norm: Option<Vec<f32>>,
375 output_gate: bool,
376 softplus_gate: Option<(QTensor, bool)>,
380 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
382 },
383 Linear(VmfPhaseWeights),
385 LinearGdn(GdnWeights),
387 ShortConv(ShortConvWeights),
390 Mla(Box<MlaWeights>),
398 Kda(Box<crate::linear_core::KdaWeights>),
402}
403
404pub struct MlaWeights {
406 pub q_proj: QTensor,
410 pub q_a: Option<QTensor>,
413 pub q_a_norm: Option<Vec<f32>>,
414 pub kv_a: QTensor,
416 pub kv_a_norm: Vec<f32>,
418 pub kv_b: QTensor,
420 pub o_proj: QTensor,
422 pub nh: usize,
423 pub qk_rope: usize,
424 pub qk_nope: usize,
425 pub v_dim: usize,
426 pub lora: usize,
427 pub scale: f32,
429 pub nope: bool,
431}
432
433pub struct MtpModule {
438 pub enorm: Vec<f32>,
439 pub hnorm: Vec<f32>,
440 pub eh_proj: QTensor,
442 pub layer: LayerWeights,
443 pub final_norm: Vec<f32>,
444 pub kv: crate::kv_cache::LayerKvCache,
445}
446
447pub struct GenerateResult {
449 pub text: String,
450 pub token_ids: Vec<u32>,
451 pub prompt_tokens: usize,
452 pub tokens_generated: usize,
453 pub finish_reason: String,
454 pub mtp_drafted: usize,
456 pub mtp_accepted: usize,
457 pub token_confidence: Vec<f32>,
462 pub traces: Vec<TokenTrace>,
465}
466
467#[derive(Clone, Debug)]
472pub struct TokenTrace {
473 pub t: usize,
475 pub token_id: u32,
477 pub confidence: f32,
479 pub active_skill: Option<String>,
481 pub recon: Option<f32>,
485 pub switched: bool,
488}
489
490fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
495 let t = if temp > 1e-3 { temp } else { 1.0 };
496 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
497 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
498 if sum > 0.0 {
499 (((logits[id as usize] - max) / t).exp()) / sum
500 } else {
501 0.0
502 }
503}
504
505fn prefill_batched() -> bool {
508 std::env::var("CMF_PREFILL")
509 .map(|v| v != "seq")
510 .unwrap_or(true)
511}
512
513fn prefill_chunk() -> usize {
518 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
519 .ok()
520 .and_then(|v| v.parse::<usize>().ok())
521 {
522 return n.max(1);
523 }
524 if cfg!(target_os = "macos") {
525 512
526 } else if cfg!(target_arch = "aarch64") {
527 256
530 } else {
531 48
532 }
533}
534
535pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
537
538impl Pipeline {
539 #[inline]
543 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
544 virtual_idx % self.physical_layers
545 }
546
547 #[inline]
550 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
551 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
552 }
553
554 #[allow(clippy::too_many_arguments)]
556
557 #[cfg(target_os = "macos")]
576 fn graph_prefill_preferred(&self) -> bool {
577 if !crate::gpu::enabled_here()
578 || !crate::gpu::q1_force()
579 || std::env::var("CMF_GPU_BLOCK")
580 .map(|v| v == "0")
581 .unwrap_or(false)
582 {
583 return false;
584 }
585 if self.loop_final_norm {
586 return true;
587 }
588 self.weights
589 .layers
590 .iter()
591 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
592 }
593
594 #[cfg(not(target_os = "macos"))]
595 fn graph_prefill_preferred(&self) -> bool {
596 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
604 .map(|v| v != "0")
605 .unwrap_or_else(|_| {
606 crate::gpu::wgpu_graph_default()
610 });
611 if !graph_on || !crate::gpu::enabled_here() {
612 return false;
613 }
614 self.weights
615 .layers
616 .iter()
617 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
618 }
619
620 #[cfg(target_os = "macos")]
621 fn q1_graph_gpu(
622 &mut self,
623 start: usize,
624 upto: Option<usize>,
625 position: usize,
626 h: &mut [f32],
627 ) -> usize {
628 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
629 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
631 || !crate::gpu::q1_force()
632 || std::env::var("CMF_GPU_BLOCK")
633 .map(|v| v == "0")
634 .unwrap_or(false)
635 {
636 return start;
637 }
638 if self.swa.is_some()
643 || self.global_attn.is_some()
644 || self.attention_heads_per_layer.is_some()
645 || self.attn_v_norm
646 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
647 || self.weights.layers.iter().any(|lw| {
648 lw.attn_out_norm.is_some()
649 || lw.ffn_out_norm.is_some()
650 || lw.layer_scale.is_some()
651 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
652 })
653 {
654 return start;
655 }
656 let limit = upto
659 .map(|u| u + 1)
660 .unwrap_or(self.num_layers)
661 .min(self.num_layers);
662
663 enum Item<'a> {
664 Gdn {
665 run: Vec<GdnGpuLayer<'a>>,
666 first: usize,
667 },
668 Attn {
669 l: AttnGpuLayer<'a>,
670 li: usize,
671 q_norm: Option<&'a [f32]>,
672 k_norm: Option<&'a [f32]>,
673 output_gate: bool,
674 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
675 full_gpu: bool,
678 },
679 }
680
681 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
683 let dev_attend = attend_mode != "0"
684 && attend_mode != "off"
685 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
689 && self.head_dim % 4 == 0
690 && self.head_dim <= 256
691 && self.rotary_dim >= 2
692 && self.rotary_dim <= self.head_dim
693 && (self.rotary_dim / 2) % 32 == 0
694 && self.num_kv_heads > 0
695 && self.num_heads % self.num_kv_heads == 0;
696
697 let mut plan: Vec<Item> = Vec::new();
698 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
699 let mut scan = start;
700 while scan < limit {
701 let lw = &self.weights.layers[self.phys_layer(scan)];
702 let FfnKind::Dense(d) = &lw.ffn else { break };
703 let (Some(g), Some(u), Some(dn)) = (
704 d.gate_proj.q1_parts(),
705 d.up_proj.q1_parts(),
706 d.down_proj.q1_parts(),
707 ) else {
708 break;
709 };
710 match &lw.attn {
711 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
712 let parts = (
713 w.in_proj_qkv.q1_parts(),
714 w.in_proj_z.q1_parts(),
715 w.in_proj_a.f32_parts(),
716 w.in_proj_b.f32_parts(),
717 w.out_proj.q1_parts(),
718 );
719 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
720 break;
721 };
722 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
723 model_ref.get_or_insert_with(|| model.clone());
724 }
725 let gl = GdnGpuLayer {
726 attn_norm: &lw.input_norm,
727 post_norm: &lw.post_norm,
728 qkv,
729 z,
730 a,
731 b,
732 out,
733 gate: g,
734 up: u,
735 down: dn,
736 conv1d: &w.conv1d,
737 a_log: &w.a_log,
738 dt_bias: &w.dt_bias,
739 gnorm: &w.norm,
740 };
741 match plan.last_mut() {
742 Some(Item::Gdn { run, .. }) => run.push(gl),
743 _ => plan.push(Item::Gdn {
744 run: vec![gl],
745 first: scan,
746 }),
747 }
748 }
749 AttnKind::Full {
750 wq,
751 wk,
752 wv,
753 wo,
754 q_norm,
755 k_norm,
756 output_gate,
757 softplus_gate: None,
758 bias,
759 } if !self.kv_cache.layers[scan].o1_sealed() => {
760 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
761 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
762 break;
763 };
764 if let QTensor::Mapped { model, .. } = wq {
765 model_ref.get_or_insert_with(|| model.clone());
766 }
767 let cache = &self.kv_cache.layers[scan];
768 let full_gpu = dev_attend
769 && cache.mode == crate::kv_cache::KvMode::F32
770 && cache.o1.is_none()
771 && bias.is_none()
772 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
773 && pk.1 == self.num_kv_heads * self.head_dim
774 && pv.1 == self.num_kv_heads * self.head_dim
775 && po.2 == self.num_heads * self.head_dim;
776 plan.push(Item::Attn {
777 l: AttnGpuLayer {
778 attn_norm: &lw.input_norm,
779 post_norm: &lw.post_norm,
780 wq: pq,
781 wk: pk,
782 wv: pv,
783 wo: po,
784 gate: g,
785 up: u,
786 down: dn,
787 },
788 li: scan,
789 q_norm: q_norm.as_deref(),
790 k_norm: k_norm.as_deref(),
791 output_gate: *output_gate,
792 bias: bias
793 .as_ref()
794 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
795 full_gpu,
796 });
797 }
798 _ => break,
799 }
800 scan += 1;
801 }
802 let Some(model) = model_ref else { return start };
803 if plan.is_empty() {
804 return start;
805 }
806 let dims = GraphDims {
807 hidden: self.hidden_size,
808 eps: self.rms_eps as f32,
809 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
810 };
811 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
812 return start;
813 };
814 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
815 nv: cfg.num_v_heads,
816 nk: cfg.num_k_heads,
817 dk: cfg.key_head_dim,
818 dv: cfg.value_head_dim,
819 kk: cfg.conv_kernel,
820 hidden: self.hidden_size,
821 inter: self.intermediate_size,
822 c_dim: cfg.conv_dim(),
823 eps: cfg.rms_eps as f32,
824 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
825 });
826 let mut valid = 0usize;
830 let mut end = start;
831 for item in &plan {
832 let ok = match item {
833 Item::Gdn { run, .. } => gcfg
834 .as_ref()
835 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
836 .unwrap_or(false),
837 Item::Attn { l, .. } => graph.attn_ok(l),
838 };
839 if !ok {
840 break;
841 }
842 valid += 1;
843 end += match item {
844 Item::Gdn { run, .. } => run.len(),
845 Item::Attn { .. } => 1,
846 };
847 }
848 plan.truncate(valid);
849 if plan.is_empty() {
850 return start;
851 }
852
853 let inv_freq = self.inv_freq.clone();
854 let pool = self.pool.clone();
855 let (nh, nkv, hd, hs, rd, eps) = (
856 self.num_heads,
857 self.num_kv_heads,
858 self.head_dim,
859 self.hidden_size,
860 self.rotary_dim,
861 self.rms_eps,
862 );
863 let norm_style = self.norm_style;
864 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
865 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
866 let kv_id = self.graph_kv_id;
867 let mut pending: Vec<(usize, usize)> = Vec::new();
870 let mut dev_attn: Vec<usize> = Vec::new();
873 for item in &plan {
874 if self.loop_final_norm {
876 let item_start = match item {
877 Item::Gdn { first, .. } => *first,
878 Item::Attn { li, .. } => *li,
879 };
880 if item_start > start && self.is_loop_end(item_start - 1) {
881 graph.encode_loop_norm(&self.weights.final_norm);
882 }
883 }
884 match item {
885 Item::Gdn { run, first } => {
886 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
887 if l.linear_state.len() != want {
888 l.linear_state = vec![0f32; want];
889 }
890 }
891 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
892 .iter()
893 .map(|l| l.linear_state.as_slice())
894 .collect();
895 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
896 tracing::error!("q1 graph: GDN run refused after validation");
898 return start;
899 }
900 graph.commit();
903 pending.push((*first, run.len()));
904 }
905 Item::Attn {
906 l,
907 li,
908 q_norm,
909 k_norm,
910 output_gate,
911 bias,
912 full_gpu,
913 } => {
914 if *full_gpu {
916 let cache = &self.kv_cache.layers[*li];
917 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
918 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
919 let cpu_stored = cpu_k[0].len() / hd;
920 let p = crate::gpu::AttnDeviceParams {
921 kv_id,
922 layer: *li,
923 nh,
924 nkv,
925 hd,
926 rd,
927 position,
928 eps: eps as f32,
929 gemma,
930 output_gate: *output_gate,
931 q_norm: *q_norm,
932 k_norm: *k_norm,
933 inv_freq: &inv_freq,
934 cpu_k,
935 cpu_v,
936 cpu_stored,
937 };
938 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
939 graph.commit();
940 dev_attn.push(*li);
941 continue;
942 }
943 }
945 graph.encode_attn_prefix(l);
946 graph.sync();
947 if !pending.is_empty() {
948 let idxs: Vec<usize> =
949 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
950 let mut outs: Vec<&mut [f32]> = self
951 .kv_cache
952 .layers
953 .iter_mut()
954 .enumerate()
955 .filter(|(i, _)| idxs.binary_search(i).is_ok())
956 .map(|(_, s)| s.linear_state.as_mut_slice())
957 .collect();
958 graph.read_states(&mut outs);
959 }
960 let mut q_raw = attention::take_buf(l.wq.1);
961 let mut k = attention::take_buf(l.wk.1);
962 let mut v = attention::take_buf(l.wv.1);
963 graph.read_qkv(&mut q_raw, &mut k, &mut v);
964 let cfg = QwenAttnCfg {
965 num_heads: nh,
966 num_kv_heads: nkv,
967 head_dim: hd,
968 hidden_size: hs,
969 position,
970 inv_freq: &inv_freq,
971 rotary_dim: rd,
972 scale: self.attn_scale,
973 softcap: self.attn_softcap,
974 window: None,
975 v_norm: false,
976 q_norm: *q_norm,
977 k_norm: *k_norm,
978 output_gate: *output_gate,
979 softplus_gate: None,
980 rope_scale: 1.0,
981 bias: *bias,
982 rms_eps: eps,
983 norm_style,
984 pool: pool.as_deref(),
985 };
986 let mut ao = attention::qwen_attention_core(
987 q_raw,
988 k,
989 v,
990 &mut self.kv_cache.layers[*li],
991 &cfg,
992 );
993 graph.encode_attn_suffix(l, &ao);
994 graph.commit();
997 attention::recycle_buf(&mut ao);
998 }
999 }
1000 }
1001 let mut lm_rows = None;
1006 if self.graph_want_logits
1007 && upto.is_none()
1008 && end == self.num_layers
1009 && std::env::var("CMF_GPU_LMHEAD")
1010 .map(|v| v != "0")
1011 .unwrap_or(true)
1012 {
1013 if let Some(lm) = self.weights.lm_head.q1_parts() {
1014 if graph.lm_head_ok(lm) {
1015 graph.encode_lm_head(&self.weights.final_norm, lm);
1016 lm_rows = Some(lm.1);
1017 }
1018 }
1019 }
1020 graph.sync();
1021 if !pending.is_empty() {
1022 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1023 let mut outs: Vec<&mut [f32]> = self
1024 .kv_cache
1025 .layers
1026 .iter_mut()
1027 .enumerate()
1028 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1029 .map(|(_, s)| s.linear_state.as_mut_slice())
1030 .collect();
1031 graph.read_states(&mut outs);
1032 }
1033 if let Some(rows) = lm_rows {
1034 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1035 graph.read_logits(&mut lg);
1036 lg.resize(self.vocab_size, 0.0);
1037 if let Some(c) = self.final_softcap {
1038 for l in lg.iter_mut() {
1039 *l = c * (*l / c).tanh();
1040 }
1041 }
1042 self.graph_logits = Some(lg);
1043 }
1044 graph.finish(h);
1045 for li in dev_attn {
1049 let mut krow = attention::take_buf(nkv * hd);
1050 let mut vrow = attention::take_buf(nkv * hd);
1051 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1052 let cache = &mut self.kv_cache.layers[li];
1053 cache.append(&krow, &vrow, &[]);
1054 let n = cache.seq_len;
1055 let mut imp = attention::take_buf(n);
1056 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1057 cache.accumulate_imp(&imp);
1058 attention::recycle_buf(&mut imp);
1059 }
1060 attention::recycle_buf(&mut krow);
1061 attention::recycle_buf(&mut vrow);
1062 }
1063 end
1064 }
1065
1066 pub fn new(
1067 tokenizer: Tokenizer,
1068 weights: PipelineWeights,
1069 hidden_size: usize,
1070 intermediate_size: usize,
1071 num_heads: usize,
1072 num_kv_heads: usize,
1073 head_dim: usize,
1074 num_layers: usize,
1075 physical_layers: usize,
1076 loop_final_norm: bool,
1077 vocab_size: usize,
1078 rms_eps: f64,
1079 rope_base: f32,
1080 norm_style: NormStyle,
1081 max_seq_len: usize,
1082 sampler_config: SamplerConfig,
1083 ) -> Self {
1084 let rng = match sampler_config.seed {
1085 Some(s) => SplitMix64::new(s),
1086 None => SplitMix64::from_entropy(),
1087 };
1088 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1089 let pool = Pool::from_env();
1090 if let Some(p) = &pool {
1091 tracing::info!("worker pool: {} threads", p.n_workers());
1092 }
1093 Self {
1094 tokenizer: std::sync::Arc::new(tokenizer),
1095 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1096 sampler_config,
1097 weights,
1098 hidden_size,
1099 intermediate_size,
1100 num_heads,
1101 num_kv_heads,
1102 head_dim,
1103 num_layers,
1104 physical_layers,
1105 loop_final_norm,
1106 vocab_size,
1107 rms_eps,
1108 rope_base,
1109 norm_style,
1110 rotary_dim: head_dim,
1111 attention_heads_per_layer: None,
1112 vmf_cfg: None,
1113 gdn_cfg: None,
1114 kda_cfg: None,
1115 logit_multiplier: None,
1116 short_conv_cfg: None,
1117 mtp: None,
1118 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1119 rng,
1120 sampler_scratch: SamplerScratch::default(),
1121 inv_freq,
1122 ws: ForwardScratch::new(hidden_size),
1123 pool,
1124 model: None,
1125 dyn_force_f32: false,
1126 dyn_skill_layers: Vec::new(),
1127 dyn_active: None,
1128 dyn_blend_loaded: false,
1129 dyn_phi_layer: None,
1130 dyn_phi_ema: Vec::new(),
1131 dyn_phi_seen: 0,
1132 dyn_router: None,
1133 o1_cfg: None,
1134 o1_flags: Vec::new(),
1135 trace: false,
1136 calib_temp: 1.0,
1137 confidence_on: true,
1138 embed_multiplier: 1.0,
1139 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1140 swa: None,
1141 sliding_layers: None,
1142 inv_freq_local: None,
1143 rotary_dim_local: None,
1144 rope_scale: 1.0,
1145 rope_scale_local: 1.0,
1146 global_attn: None,
1147 inv_freq_global: None,
1148 attn_v_norm: false,
1149 final_softcap: None,
1150 attn_softcap: 0.0,
1151 graph_want_logits: false,
1152 graph_logits: None,
1153 graph_kv_id: {
1154 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1155 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1156 },
1157 }
1158 }
1159
1160 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1167 self.o1_flags = match &cfg {
1168 Some(c) => {
1169 let mut flags = c.layer_flags(self.num_layers);
1170 for (li, f) in flags.iter_mut().enumerate() {
1171 if *f
1172 && !matches!(
1173 self.weights.layers[self.phys_layer(li)].attn,
1174 AttnKind::Full { .. }
1175 )
1176 {
1177 *f = false;
1178 }
1179 }
1180 flags
1181 }
1182 None => Vec::new(),
1183 };
1184 if let Some(c) = &cfg {
1185 let n = self.o1_flags.iter().filter(|&&f| f).count();
1186 tracing::info!(
1187 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1188 self.num_layers,
1189 c.m,
1190 c.w,
1191 c.sink,
1192 c.rect
1193 );
1194 }
1195 self.o1_cfg = cfg;
1196 }
1197
1198 pub fn o1_active(&self) -> bool {
1200 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1201 }
1202
1203 fn o1_begin(&mut self) {
1205 if let Some(c) = &self.o1_cfg {
1206 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1207 for (li, &f) in self.o1_flags.iter().enumerate() {
1208 if f {
1209 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1210 }
1211 }
1212 }
1213 }
1214
1215 fn o1_seal(&mut self) {
1218 if self.o1_cfg.is_none() {
1219 return;
1220 }
1221 for li in 0..self.num_layers {
1222 if self.o1_flags.get(li).copied().unwrap_or(false) {
1223 self.kv_cache.layers[li].o1_seal(self.num_heads);
1224 }
1225 }
1226 }
1227
1228 pub fn set_trace(&mut self, on: bool) {
1230 self.trace = on;
1231 }
1232
1233 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1236 self.rng = match config.seed {
1237 Some(seed) => SplitMix64::new(seed),
1238 None => SplitMix64::from_entropy(),
1239 };
1240 self.sampler_config = config;
1241 }
1242
1243 pub fn set_confidence(&mut self, on: bool) {
1248 self.confidence_on = on;
1249 }
1250
1251 pub fn set_calib_temp(&mut self, t: f32) {
1254 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1255 }
1256
1257 pub fn calib_temp(&self) -> f32 {
1259 self.calib_temp
1260 }
1261
1262 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1265 self.rotary_dim = rotary_dim.min(self.head_dim);
1266 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1267 }
1268
1269 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1270 QwenAttnCfg {
1271 num_heads: self.num_heads,
1272 num_kv_heads: self.num_kv_heads,
1273 head_dim: self.head_dim,
1274 hidden_size: self.hidden_size,
1275 position,
1276 inv_freq: &self.inv_freq,
1277 rotary_dim: self.rotary_dim,
1278 scale: self.attn_scale,
1279 softcap: self.attn_softcap,
1280 window: None,
1281 v_norm: false,
1282 q_norm: None,
1283 k_norm: None,
1284 output_gate: false,
1285 softplus_gate: None,
1286 rope_scale: self.rope_scale,
1287 bias: None,
1288 rms_eps: self.rms_eps,
1289 norm_style: self.norm_style,
1290 pool: self.pool.as_deref(),
1291 }
1292 }
1293
1294 pub fn generate(
1296 &mut self,
1297 prompt: &str,
1298 max_tokens: usize,
1299 task_mask: Option<&TaskMask>,
1300 on_token: Option<TokenCallback>,
1301 ) -> Result<GenerateResult, String> {
1302 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1303 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1304 }
1305
1306 pub fn generate_from_ids(
1314 &mut self,
1315 input_ids: &[u32],
1316 max_tokens: usize,
1317 task_mask: Option<&TaskMask>,
1318 mut on_token: Option<TokenCallback>,
1319 ) -> Result<GenerateResult, String> {
1320 if std::env::var("CMF_TRACE_H").is_ok() {
1321 eprintln!("input_ids: {input_ids:?}");
1322 }
1323 if input_ids.is_empty() {
1324 return Err("empty prompt: nothing to generate from".to_string());
1325 }
1326
1327 self.kv_cache.clear();
1329 crate::gpu::graph_kv_reset(self.graph_kv_id);
1330 crate::gpu::graph_race_begin_generation();
1331 self.o1_begin();
1332
1333 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1339 .map(|v| v != "0")
1340 .unwrap_or_else(|_| {
1341 crate::gpu::wgpu_graph_default()
1345 });
1346 let spec_active = self.speculative
1347 && self.mtp.is_some()
1348 && task_mask.is_none()
1349 && !self.o1_active()
1350 && !graph_on
1351 && self.sampler_config.temperature < 1e-6;
1352 let mut mtp = if spec_active { self.mtp.take() } else { None };
1355 if let Some(m) = &mut mtp {
1356 m.kv.clear();
1357 }
1358 let mut router = if mtp.is_none() {
1362 self.dyn_router.take()
1363 } else {
1364 None
1365 };
1366 if let Some(r) = &mut router {
1367 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1370 }
1371
1372 let mut all_ids = input_ids.to_vec();
1373 let mut generated = 0usize;
1374 let mut finish_reason = "max_tokens".to_string();
1375 let mut drafted = 0usize;
1376 let mut accepted = 0usize;
1377 let mut confidence: Vec<f32> = Vec::new();
1378 let trace_on = self.trace;
1379 let calib_temp = self.calib_temp;
1380 let mut traces: Vec<TokenTrace> = Vec::new();
1381
1382 let mut hidden = vec![0.0f32; self.hidden_size];
1388 let mut pos = 0usize;
1389 let fuse_lm = mtp.is_none() && router.is_none();
1393 self.graph_logits = None;
1394 self.graph_want_logits = false;
1395 let dyn_prefill = router.is_some();
1400 let graph_prefill = self.graph_prefill_preferred();
1406 if task_mask.is_none()
1407 && !dyn_prefill
1408 && !graph_prefill
1409 && prefill_batched()
1410 && input_ids.len() > 2
1411 {
1412 let chunk = prefill_chunk();
1418 let hs = self.hidden_size;
1419 while pos < input_ids.len() {
1420 let end = (pos + chunk).min(input_ids.len());
1421 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1422 if let Some(m) = &mut mtp {
1423 for p in pos..end {
1424 if p + 1 < input_ids.len() {
1425 let _ = self.mtp_step(
1426 m,
1427 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1428 input_ids[p + 1],
1429 p,
1430 );
1431 }
1432 }
1433 }
1434 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1435 pos = end;
1436 }
1437 }
1438 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
1439 if task_mask.is_none() && !dyn_prefill && !graph_prefill && !pair_off && self.pair_supported()
1440 {
1441 while pos + 1 < input_ids.len() {
1442 let e1 = self.embed_single(input_ids[pos]);
1443 let e2 = self.embed_single(input_ids[pos + 1]);
1444 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1445 self.commit_linear_scratch();
1447 if let Some(m) = &mut mtp {
1448 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1449 if pos + 2 < input_ids.len() {
1450 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1451 }
1452 }
1453 hidden = h2;
1454 pos += 2;
1455 }
1456 }
1457 let _tpf = std::time::Instant::now();
1466 let batch_k = std::env::var("CMF_BATCH_K")
1467 .ok()
1468 .and_then(|v| v.parse::<usize>().ok())
1469 .unwrap_or(0);
1470 if batch_k > 0
1471 && graph_prefill
1472 && task_mask.is_none()
1473 && mtp.is_none()
1474 && !dyn_prefill
1475 && pos + 1 < input_ids.len()
1476 {
1477 let hs = self.hidden_size;
1478 let chunk = batch_k;
1479 while pos < input_ids.len() {
1480 let end = (pos + chunk).min(input_ids.len());
1481 let bk = end - pos;
1482 let mut hiddens = vec![0f32; bk * hs];
1483 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1484 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1485 }
1486 let positions: Vec<usize> = (pos..end).collect();
1487 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1488 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1489 pos = end;
1490 } else {
1491 break; }
1493 }
1494 }
1495 while pos < input_ids.len() {
1496 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1497 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1498 if let Some(m) = &mut mtp {
1499 if pos + 1 < input_ids.len() {
1500 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1501 }
1502 }
1503 pos += 1;
1504 }
1505 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1506 eprintln!(
1507 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1508 input_ids.len(),
1509 _tpf.elapsed().as_secs_f64() * 1000.0
1510 );
1511 }
1512 self.o1_seal();
1515
1516 macro_rules! commit {
1518 ($id:expr) => {{
1519 all_ids.push($id);
1520 generated += 1;
1521 if self.tokenizer.is_eos($id) {
1522 finish_reason = "stop".to_string();
1523 false
1524 } else {
1525 let token_text = self.tokenizer.decode_token($id);
1526 let mut go = true;
1527 if let Some(ref mut cb) = on_token {
1528 if !cb(&token_text) {
1529 finish_reason = "cancelled".to_string();
1530 go = false;
1531 }
1532 }
1533 go
1534 }
1535 }};
1536 }
1537
1538 let mut next_pos = input_ids.len();
1540 'decode: while generated < max_tokens {
1541 let mut logits = match self.graph_logits.take() {
1542 Some(lg) => lg,
1543 None => {
1544 inference::rms_norm_into(
1545 &hidden,
1546 &self.weights.final_norm,
1547 self.rms_eps,
1548 self.norm_style,
1549 &mut self.ws.n1,
1550 );
1551 self.lm_head_forward(&self.ws.n1)
1552 }
1553 };
1554 let t_next = sampler::sample_with_scratch(
1555 &logits,
1556 &self.sampler_config,
1557 &all_ids,
1558 &mut self.rng,
1559 &mut self.sampler_scratch,
1560 );
1561 if self.confidence_on {
1562 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1563 }
1564 attention::recycle_buf(&mut logits);
1565 if trace_on {
1566 let skill = router.as_ref().and_then(|r| r.active_id());
1570 traces.push(TokenTrace {
1571 t: generated,
1572 token_id: t_next,
1573 confidence: confidence.last().copied().unwrap_or(0.0),
1574 active_skill: skill,
1575 recon: None,
1576 switched: false,
1577 });
1578 }
1579 if !commit!(t_next) {
1580 break 'decode;
1581 }
1582 if generated >= max_tokens {
1583 break 'decode;
1584 }
1585
1586 if self.kv_cache.needs_eviction() {
1587 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1588 self.kv_cache.evict(keep);
1589 }
1590
1591 match &mut mtp {
1592 Some(m) if generated + 1 < max_tokens => {
1594 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1595 drafted += 1;
1596 let emb1 = self.embed_single(t_next);
1597 let emb2 = self.embed_single(draft);
1598 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1599
1600 inference::rms_norm_into(
1601 &h1,
1602 &self.weights.final_norm,
1603 self.rms_eps,
1604 self.norm_style,
1605 &mut self.ws.n1,
1606 );
1607 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1608 let t_after = sampler::sample_with_scratch(
1609 &logits1,
1610 &self.sampler_config,
1611 &all_ids,
1612 &mut self.rng,
1613 &mut self.sampler_scratch,
1614 );
1615 if self.confidence_on {
1616 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1617 }
1618 attention::recycle_buf(&mut logits1);
1619 if trace_on {
1620 traces.push(TokenTrace {
1623 t: generated,
1624 token_id: t_after,
1625 confidence: confidence.last().copied().unwrap_or(0.0),
1626 active_skill: None,
1627 recon: None,
1628 switched: false,
1629 });
1630 }
1631 let stop = !commit!(t_after);
1632
1633 if t_after == draft {
1634 accepted += 1;
1635 self.commit_linear_scratch();
1636 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1637 hidden = h2;
1638 next_pos += 2;
1639 } else {
1640 for layer in &mut self.kv_cache.layers {
1642 layer.truncate_last(1);
1643 }
1644 if !stop {
1645 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1646 hidden = self.forward_layers(
1647 &self.embed_single(t_after),
1648 next_pos + 1,
1649 None,
1650 );
1651 }
1652 next_pos += 2;
1653 }
1654 if stop {
1655 break 'decode;
1656 }
1657 }
1658 _ => {
1660 self.graph_want_logits = fuse_lm;
1661 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1662 next_pos += 1;
1663 if let Some(r) = &mut router {
1666 let phi = self.dyn_phi_ema.clone();
1667 let decision = r.step(&phi, generated);
1668 if let Some(new_active) = decision {
1669 let _ = self.set_active_skill(new_active);
1670 }
1671 if trace_on {
1674 if let Some(last) = traces.last_mut() {
1675 let e = r.last_best_e();
1676 last.recon = e.is_finite().then_some(e);
1677 last.switched = decision.is_some();
1678 }
1679 }
1680 }
1681 }
1682 }
1683 }
1684
1685 self.graph_want_logits = false;
1686 self.graph_logits = None;
1687 if router.is_some() {
1689 let _ = self.set_active_skill(None);
1690 }
1691 self.dyn_router = router.or(self.dyn_router.take());
1692 self.mtp = mtp.or(self.mtp.take());
1693
1694 let output_ids = &all_ids[input_ids.len()..];
1695 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1697 Ok(GenerateResult {
1698 text: self.tokenizer.decode(output_ids),
1699 token_ids: output_ids.to_vec(),
1700 prompt_tokens: input_ids.len(),
1701 tokens_generated: generated,
1702 finish_reason,
1703 mtp_drafted: drafted,
1704 mtp_accepted: accepted,
1705 token_confidence: confidence,
1706 traces,
1707 })
1708 }
1709
1710 fn mtp_step(
1714 &mut self,
1715 m: &mut MtpModule,
1716 hidden: &[f32],
1717 next_token: u32,
1718 position: usize,
1719 ) -> u32 {
1720 let e = self.embed_single(next_token);
1724 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1725 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1726 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1727 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1728 let mut x = vec![0.0f32; self.hidden_size];
1729 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1730
1731 let lw = &m.layer;
1733 inference::rms_norm_into(
1734 &x,
1735 &lw.input_norm,
1736 self.rms_eps,
1737 self.norm_style,
1738 &mut self.ws.n1,
1739 );
1740 let attn = match &lw.attn {
1741 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
1743 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1744 AttnKind::Full {
1745 wq,
1746 wk,
1747 wv,
1748 wo,
1749 q_norm,
1750 k_norm,
1751 output_gate,
1752 softplus_gate,
1753 bias,
1754 } => {
1755 let mut cfg = self.attn_cfg(position);
1756 cfg.q_norm = q_norm.as_deref();
1757 cfg.k_norm = k_norm.as_deref();
1758 cfg.output_gate = *output_gate;
1759 cfg.softplus_gate = softplus_gate
1760 .as_ref()
1761 .map(|(gate, per_head)| (gate, *per_head));
1762 cfg.bias = bias
1763 .as_ref()
1764 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1765 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1766 }
1767 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1768 unreachable!("MTP block is full attention")
1769 }
1770 };
1771 for (i, &a) in attn.iter().enumerate() {
1772 x[i] += a;
1773 }
1774 inference::rms_norm_into(
1775 &x,
1776 &lw.post_norm,
1777 self.rms_eps,
1778 self.norm_style,
1779 &mut self.ws.p1,
1780 );
1781 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
1782 for (i, &f) in ffn.iter().enumerate() {
1783 x[i] += f;
1784 }
1785
1786 inference::rms_norm_into(
1787 &x,
1788 &m.final_norm,
1789 self.rms_eps,
1790 self.norm_style,
1791 &mut self.ws.n1,
1792 );
1793 let mut lg = self.lm_head_forward(&self.ws.n1);
1794 let draft = sampler::argmax(&lg);
1795 attention::recycle_buf(&mut lg);
1796 draft
1797 }
1798
1799 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1803 let emb1 = self.embed_single(1);
1804 let emb2 = self.embed_single(2);
1805 let pos = self.kv_cache.seq_len();
1806
1807 let t0 = std::time::Instant::now();
1808 for _ in 0..iters {
1809 let _ = self.forward_layers(&emb1, pos, None);
1810 let _ = self.forward_layers(&emb2, pos + 1, None);
1811 for l in &mut self.kv_cache.layers {
1812 l.truncate_last(2);
1813 }
1814 }
1815 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1816
1817 let t1 = std::time::Instant::now();
1818 for _ in 0..iters {
1819 let _ = self.forward_pair(&emb1, &emb2, pos);
1820 for l in &mut self.kv_cache.layers {
1821 l.truncate_last(2);
1822 }
1823 }
1824 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1825 (singles_ms, pair_ms)
1826 }
1827
1828 fn pair_supported(&self) -> bool {
1836 !self
1837 .weights
1838 .layers
1839 .iter()
1840 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
1841 }
1842
1843 fn forward_pair(
1844 &mut self,
1845 emb1: &[f32],
1846 emb2: &[f32],
1847 position: usize,
1848 ) -> (Vec<f32>, Vec<f32>) {
1849 let mut h1 = emb1.to_vec();
1850 let mut h2 = emb2.to_vec();
1851 let (_nkv, _hd, hs, _rd, eps) = (
1852 self.num_kv_heads,
1853 self.head_dim,
1854 self.hidden_size,
1855 self.rotary_dim,
1856 self.rms_eps,
1857 );
1858 let pool = self.pool.clone();
1859
1860 for li in 0..self.num_layers {
1861 let lw = &self.weights.layers[self.phys_layer(li)];
1862 inference::rms_norm_into(
1865 &h1,
1866 &lw.input_norm,
1867 self.rms_eps,
1868 self.norm_style,
1869 &mut self.ws.n1,
1870 );
1871 inference::rms_norm_into(
1872 &h2,
1873 &lw.input_norm,
1874 self.rms_eps,
1875 self.norm_style,
1876 &mut self.ws.n2,
1877 );
1878
1879 let (a1, a2) = match &lw.attn {
1880 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
1881 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1882 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1883 AttnKind::Linear(w) => {
1884 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1885 let layer = &mut self.kv_cache.layers[li];
1886 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1887 vmf_phase_pair(
1888 &self.ws.n1,
1889 &self.ws.n2,
1890 w,
1891 &cfg,
1892 state,
1893 scratch,
1894 self.pool.as_deref(),
1895 )
1896 }
1897 AttnKind::LinearGdn(w) => {
1898 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1899 let layer = &mut self.kv_cache.layers[li];
1900 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1901 gdn_pair(
1902 &self.ws.n1,
1903 &self.ws.n2,
1904 w,
1905 &cfg,
1906 state,
1907 scratch,
1908 self.pool.as_deref(),
1909 )
1910 }
1911 AttnKind::ShortConv(w) => {
1912 let cfg = self
1913 .short_conv_cfg
1914 .expect("short-conv layer without short_conv_cfg");
1915 let layer = &mut self.kv_cache.layers[li];
1916 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1917 short_conv_pair(
1918 &self.ws.n1,
1919 &self.ws.n2,
1920 w,
1921 &cfg,
1922 state,
1923 scratch,
1924 self.pool.as_deref(),
1925 )
1926 }
1927 AttnKind::Full {
1928 wq,
1929 wk,
1930 wv,
1931 wo,
1932 q_norm,
1933 k_norm,
1934 output_gate,
1935 softplus_gate,
1936 bias,
1937 } => {
1938 let inv_freq_l = self.layer_inv_freq(li);
1939 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1940 let cfg = QwenAttnCfg {
1941 num_heads: self.layer_num_heads(li),
1942 num_kv_heads: nkv_l,
1943 head_dim: hd_l,
1944 hidden_size: hs,
1945 position,
1946 inv_freq: &inv_freq_l,
1947 rotary_dim: rd_l,
1948 scale: self.attn_scale,
1949 softcap: self.attn_softcap,
1950 window: self.layer_window(li),
1951 v_norm: self.attn_v_norm,
1952 q_norm: q_norm.as_deref(),
1953 k_norm: k_norm.as_deref(),
1954 output_gate: *output_gate,
1955 softplus_gate: softplus_gate
1956 .as_ref()
1957 .map(|(gate, per_head)| (gate, *per_head)),
1958 rope_scale: self.layer_rope_scale(li),
1959 bias: bias
1960 .as_ref()
1961 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1962 rms_eps: eps,
1963 norm_style: self.norm_style,
1964 pool: pool.as_deref(),
1965 };
1966 attention::qwen_attention_pair(
1967 &self.ws.n1,
1968 &self.ws.n2,
1969 wq,
1970 wk,
1971 wv,
1972 wo,
1973 &mut self.kv_cache.layers[li],
1974 &cfg,
1975 )
1976 }
1977 };
1978 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
1979 Some(w) => (
1980 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1981 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1982 ),
1983 None => (a1, a2),
1984 };
1985 for i in 0..self.hidden_size {
1986 h1[i] += a1[i];
1987 h2[i] += a2[i];
1988 }
1989 let (mut a1, mut a2) = (a1, a2);
1990 attention::recycle_buf(&mut a1);
1991 attention::recycle_buf(&mut a2);
1992
1993 let lw = &self.weights.layers[self.phys_layer(li)];
1994 inference::rms_norm_into(
1995 &h1,
1996 &lw.post_norm,
1997 self.rms_eps,
1998 self.norm_style,
1999 &mut self.ws.p1,
2000 );
2001 inference::rms_norm_into(
2002 &h2,
2003 &lw.post_norm,
2004 self.rms_eps,
2005 self.norm_style,
2006 &mut self.ws.p2,
2007 );
2008 let (f1, f2) = match &lw.ffn {
2009 FfnKind::DenseMoe(dm) => (
2012 dense_moe_ffn(
2013 dm,
2014 &self.ws.p1,
2015 &h1,
2016 self.rms_eps,
2017 self.norm_style,
2018 self.pool.as_deref(),
2019 ),
2020 dense_moe_ffn(
2021 dm,
2022 &self.ws.p2,
2023 &h2,
2024 self.rms_eps,
2025 self.norm_style,
2026 self.pool.as_deref(),
2027 ),
2028 ),
2029 _ => ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref(), None),
2030 };
2031 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
2032 Some(w) => (
2033 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
2034 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
2035 ),
2036 None => (f1, f2),
2037 };
2038 for i in 0..self.hidden_size {
2039 h1[i] += f1[i];
2040 h2[i] += f2[i];
2041 }
2042 let (mut f1, mut f2) = (f1, f2);
2043 attention::recycle_buf(&mut f1);
2044 attention::recycle_buf(&mut f2);
2045 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
2046 for i in 0..self.hidden_size {
2047 h1[i] *= sc;
2048 h2[i] *= sc;
2049 }
2050 }
2051 if self.is_loop_end(li) && li + 1 < self.num_layers {
2053 h1 = inference::rms_norm(
2054 &h1,
2055 &self.weights.final_norm,
2056 self.rms_eps,
2057 self.norm_style,
2058 );
2059 h2 = inference::rms_norm(
2060 &h2,
2061 &self.weights.final_norm,
2062 self.rms_eps,
2063 self.norm_style,
2064 );
2065 }
2066 }
2067 (h1, h2)
2068 }
2069
2070 fn commit_linear_scratch(&mut self) {
2072 for layer in &mut self.kv_cache.layers {
2073 if !layer.linear_scratch.is_empty() {
2074 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
2075 layer.linear_scratch.clear();
2076 }
2077 }
2078 }
2079
2080 pub fn forward_ids(
2083 &mut self,
2084 ids: &[u32],
2085 task_mask: Option<&TaskMask>,
2086 ) -> Result<Vec<f32>, String> {
2087 if ids.is_empty() {
2088 return Err("empty id sequence".to_string());
2089 }
2090 self.kv_cache.clear();
2091 self.o1_begin();
2092 let mut hidden = vec![0.0f32; self.hidden_size];
2093 let mut pos = 0usize;
2094 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
2095 let chunk = prefill_chunk();
2099 let hs = self.hidden_size;
2100 while pos < ids.len() {
2101 let end = (pos + chunk).min(ids.len());
2102 let hb = self.prefill_batch(&ids[pos..end], pos);
2103 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2104 pos = end;
2105 }
2106 }
2107 if task_mask.is_none() {
2108 while pos + 1 < ids.len() {
2109 let e1 = self.embed_single(ids[pos]);
2110 let e2 = self.embed_single(ids[pos + 1]);
2111 let (_, h2) = self.forward_pair(&e1, &e2, pos);
2112 self.commit_linear_scratch();
2113 hidden = h2;
2114 pos += 2;
2115 }
2116 }
2117 while pos < ids.len() {
2118 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2119 pos += 1;
2120 }
2121 self.o1_seal();
2125 let normed = inference::rms_norm(
2126 &hidden,
2127 &self.weights.final_norm,
2128 self.rms_eps,
2129 self.norm_style,
2130 );
2131 Ok(self.lm_head_forward(&normed))
2132 }
2133
2134 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
2141 let (nll, cnt) = self.nll_ids_from(ids, 0);
2142 (nll / cnt.max(1) as f64).exp()
2143 }
2144
2145 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2150 self.kv_cache.clear();
2151 FFN_PROBE.with(|p| {
2152 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2153 });
2154 crate::gpu::cpu_scope(|| {
2155 for (pos, &id) in ids.iter().enumerate() {
2156 let emb = self.embed_single(id);
2157 let _ = self.forward_layers(&emb, pos, None);
2158 }
2159 });
2160 self.kv_cache.clear();
2161 FFN_PROBE
2162 .with(|p| p.borrow_mut().take())
2163 .unwrap_or_default()
2164 }
2165
2166 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2170 self.kv_cache.clear();
2171 let mut nll = 0f64;
2172 let mut cnt = 0usize;
2173 let mut hidden = vec![0f32; self.hidden_size];
2174 for (pos, &id) in ids.iter().enumerate() {
2175 if pos > 0 {
2176 inference::rms_norm_into(
2177 &hidden,
2178 &self.weights.final_norm,
2179 self.rms_eps,
2180 self.norm_style,
2181 &mut self.ws.n1,
2182 );
2183 let mut logits = self.lm_head_forward(&self.ws.n1);
2184 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2185 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2186 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2187 nll -= p.max(1e-300).ln();
2188 cnt += 1;
2189 attention::recycle_buf(&mut logits);
2190 }
2191 let emb = self.embed_single(id);
2192 hidden = self.forward_layers(&emb, pos, Some(mask));
2193 }
2194 self.kv_cache.clear();
2195 (nll / cnt.max(1) as f64).exp()
2196 }
2197
2198 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2207 self.kv_cache.clear();
2208 let mut nll = 0f64;
2209 let mut cnt = 0usize;
2210 if prefill_batched() {
2211 const CHUNK: usize = 128;
2217 const LM_SUB: usize = 32;
2218 let n = ids.len().saturating_sub(1);
2219 let hs = self.hidden_size;
2220 let rows = self.weights.lm_head.rows();
2221 let mut pos = 0usize;
2222 while pos < n {
2223 let end = (pos + CHUNK).min(n);
2224 let bsz = end - pos;
2225 let hb = self.prefill_batch(&ids[pos..end], pos);
2226 let mut k0 = 0usize;
2227 while k0 < bsz {
2228 let k1 = (k0 + LM_SUB).min(bsz);
2229 let sb = k1 - k0;
2230 if pos + k1 <= start {
2233 k0 = k1;
2234 continue;
2235 }
2236 let mut normed = vec![0.0f32; sb * hs];
2237 for k in 0..sb {
2238 let r = inference::rms_norm(
2239 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2240 &self.weights.final_norm,
2241 self.rms_eps,
2242 self.norm_style,
2243 );
2244 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2245 }
2246 let mut logits = vec![0.0f32; sb * rows];
2247 self.weights
2248 .lm_head
2249 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2250 for k in 0..sb {
2251 if pos + k0 + k < start {
2252 continue;
2253 }
2254 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
2255 if let Some(mu) = self.logit_multiplier {
2256 for v in lg.iter_mut() {
2257 *v *= mu;
2258 }
2259 }
2260 if let Some(c) = self.final_softcap {
2264 for v in lg.iter_mut() {
2265 *v = c * (*v / c).tanh();
2266 }
2267 }
2268 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2269 let target = ids[pos + k0 + k + 1] as usize;
2270 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2271 let lse: f64 = lg
2272 .iter()
2273 .map(|&v| ((v - max) as f64).exp())
2274 .sum::<f64>()
2275 .ln()
2276 + max as f64;
2277 nll += lse - lg[target] as f64;
2278 cnt += 1;
2279 if std::env::var("CMF_PPL_TRACE").is_ok() {
2280 let top = lg
2281 .iter()
2282 .enumerate()
2283 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2284 .map(|(i, _)| i)
2285 .unwrap_or(0);
2286 eprintln!(
2287 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
2288 pos + k0 + k, target, lse - lg[target] as f64, top, lg[target], lg[top]
2289 );
2290 }
2291 }
2292 k0 = k1;
2293 }
2294 pos = end;
2295 }
2296 self.kv_cache.clear();
2297 return (nll, cnt);
2298 }
2299 for pos in 0..ids.len().saturating_sub(1) {
2300 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2301 if pos < start {
2302 continue;
2303 }
2304 let normed = inference::rms_norm(
2305 &hidden,
2306 &self.weights.final_norm,
2307 self.rms_eps,
2308 self.norm_style,
2309 );
2310 let logits = self.lm_head_forward(&normed);
2314 let target = ids[pos + 1] as usize;
2315 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2316 let lse: f64 = logits
2317 .iter()
2318 .map(|&v| ((v - max) as f64).exp())
2319 .sum::<f64>()
2320 .ln()
2321 + max as f64;
2322 let tok_nll = lse - logits[target] as f64;
2323 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2324 let top = logits
2325 .iter()
2326 .enumerate()
2327 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2328 .map(|(i, _)| i)
2329 .unwrap_or(0);
2330 eprintln!(
2331 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2332 logits[target], logits[top]
2333 );
2334 }
2335 nll += tok_nll;
2336 cnt += 1;
2337 }
2338 self.kv_cache.clear();
2339 (nll, cnt)
2340 }
2341
2342 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2358 self.kv_cache.clear();
2359 self.o1_begin();
2360 let n = ids.len().saturating_sub(1);
2361 let p = prefill.min(n);
2362 let mut pos = 0usize;
2364 if prefill_batched() {
2365 const CHUNK: usize = 128;
2366 while pos < p {
2367 let end = (pos + CHUNK).min(p);
2368 let _ = self.prefill_batch(&ids[pos..end], pos);
2369 pos = end;
2370 }
2371 } else {
2372 while pos < p {
2373 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2374 pos += 1;
2375 }
2376 }
2377 self.o1_seal();
2378
2379 let mut nll = 0f64;
2380 let mut cnt = 0usize;
2381 for pos in p..n {
2382 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2383 let normed = inference::rms_norm(
2384 &hidden,
2385 &self.weights.final_norm,
2386 self.rms_eps,
2387 self.norm_style,
2388 );
2389 let logits = self.lm_head_forward(&normed);
2393 let target = ids[pos + 1] as usize;
2394 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2395 let lse: f64 = logits
2396 .iter()
2397 .map(|&v| ((v - max) as f64).exp())
2398 .sum::<f64>()
2399 .ln()
2400 + max as f64;
2401 let tok_nll = lse - logits[target] as f64;
2402 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2403 let top = logits
2404 .iter()
2405 .enumerate()
2406 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2407 .map(|(i, _)| i)
2408 .unwrap_or(0);
2409 eprintln!(
2410 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2411 logits[target], logits[top]
2412 );
2413 }
2414 nll += tok_nll;
2415 cnt += 1;
2416 }
2417 self.kv_cache.clear();
2418 (nll, cnt)
2419 }
2420
2421 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2429 self.kv_cache.clear();
2430 let n = ids.len().saturating_sub(1);
2431 let mut correct = Vec::with_capacity(n);
2432 let mut pmax = Vec::with_capacity(n);
2433 for pos in 0..n {
2434 let emb = self.embed_single(ids[pos]);
2435 let hidden = self.forward_layers(&emb, pos, None);
2436 let normed = inference::rms_norm(
2437 &hidden,
2438 &self.weights.final_norm,
2439 self.rms_eps,
2440 self.norm_style,
2441 );
2442 let logits = self.lm_head_forward(&normed);
2446 let target = ids[pos + 1] as usize;
2447 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2448 for (i, &v) in logits.iter().enumerate() {
2449 if v > mval {
2450 mval = v;
2451 amax = i;
2452 }
2453 }
2454 correct.push(amax == target);
2455 let row: Vec<f32> = temps
2456 .iter()
2457 .map(|&t| {
2458 let tt = t.max(1e-3);
2459 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2460 1.0 / s.max(1e-12) })
2462 .collect();
2463 pmax.push(row);
2464 }
2465 self.kv_cache.clear();
2466 (correct, pmax)
2467 }
2468
2469 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2476 let mut router = match self.dyn_router.take() {
2477 Some(r) => r,
2478 None => return (self.ppl_ids(ids), 0),
2479 };
2480 router.reset();
2481 self.dyn_phi_seen = 0;
2482 let _ = self.set_active_skill(None);
2483
2484 self.kv_cache.clear();
2485 let mut nll = 0f64;
2486 let mut cnt = 0usize;
2487 for pos in 0..ids.len().saturating_sub(1) {
2488 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2489 let normed = inference::rms_norm(
2490 &hidden,
2491 &self.weights.final_norm,
2492 self.rms_eps,
2493 self.norm_style,
2494 );
2495 let logits = self.lm_head_forward(&normed);
2499 let target = ids[pos + 1] as usize;
2500 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2501 let lse: f64 = logits
2502 .iter()
2503 .map(|&v| ((v - max) as f64).exp())
2504 .sum::<f64>()
2505 .ln()
2506 + max as f64;
2507 let tok_nll = lse - logits[target] as f64;
2508 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2509 let top = logits
2510 .iter()
2511 .enumerate()
2512 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2513 .map(|(i, _)| i)
2514 .unwrap_or(0);
2515 eprintln!(
2516 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2517 logits[target], logits[top]
2518 );
2519 }
2520 nll += tok_nll;
2521 cnt += 1;
2522 let phi = self.dyn_phi_ema.clone();
2524 if let Some(new_active) = router.step(&phi, pos) {
2525 let _ = self.set_active_skill(new_active);
2526 }
2527 }
2528 let switches = router.switches.len();
2529 let _ = self.set_active_skill(None);
2530 self.dyn_router = Some(router);
2531 self.kv_cache.clear();
2532 ((nll / cnt.max(1) as f64).exp(), switches)
2533 }
2534
2535 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2537 self.kv_cache.clear();
2538 let mut acc = vec![0f32; self.hidden_size];
2539 for (pos, &id) in ids.iter().enumerate() {
2540 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2541 for (a, v) in acc.iter_mut().zip(&h) {
2542 *a += v;
2543 }
2544 }
2545 let n = ids.len().max(1) as f32;
2546 for a in acc.iter_mut() {
2547 *a /= n;
2548 }
2549 self.kv_cache.clear();
2550 acc
2551 }
2552
2553 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2559 let b = ids.len();
2560 let hs = self.hidden_size;
2561 let mut h: Vec<f32> = vec![0.0; b * hs];
2564 let mut h_ready = false;
2565 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2566 for (bi, &id) in ids.iter().enumerate() {
2567 let e = me.embed_single(id);
2568 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2569 }
2570 };
2571 let (_nkv, _hd, _rd, eps) = (
2572 self.num_kv_heads,
2573 self.head_dim,
2574 self.rotary_dim,
2575 self.rms_eps,
2576 );
2577 let pool = self.pool.clone();
2578 let norm_style = self.norm_style;
2579
2580 #[cfg(target_os = "macos")]
2581 let mut chunk_skip_until = 0usize;
2582 for li in 0..self.num_layers {
2583 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2590 {
2591 if li < chunk_skip_until {
2592 continue;
2593 }
2594 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2595 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2596 if end > li {
2597 h_ready = true;
2598 chunk_skip_until = end;
2599 if self.is_loop_end(end - 1) && end < self.num_layers {
2602 for bi in 0..b {
2603 let normed = inference::rms_norm(
2604 &h[bi * hs..(bi + 1) * hs],
2605 &self.weights.final_norm,
2606 eps,
2607 norm_style,
2608 );
2609 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2610 }
2611 }
2612 continue;
2613 }
2614 }
2615 if !h_ready {
2616 fill_h(&mut h, self);
2617 h_ready = true;
2618 }
2619 let lw = &self.weights.layers[self.phys_layer(li)];
2620 match &lw.attn {
2622 AttnKind::Kda(w) => {
2623 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
2625 let mut normed = vec![0.0f32; b * hs];
2626 for bi in 0..b {
2627 inference::rms_norm_into(
2628 &h[bi * hs..(bi + 1) * hs],
2629 &lw.input_norm,
2630 eps,
2631 norm_style,
2632 &mut normed[bi * hs..(bi + 1) * hs],
2633 );
2634 }
2635 let attn = crate::linear_core::kda_forward_batch(
2636 &normed,
2637 b,
2638 w,
2639 &cfg,
2640 &mut self.kv_cache.layers[li].linear_state,
2641 pool.as_deref(),
2642 );
2643 for (dst, &a) in h.iter_mut().zip(&attn) {
2644 *dst += a;
2645 }
2646 }
2647 AttnKind::LinearGdn(w) => {
2648 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2650 let mut normed = vec![0.0f32; b * hs];
2651 for bi in 0..b {
2652 let r = inference::rms_norm(
2653 &h[bi * hs..(bi + 1) * hs],
2654 &lw.input_norm,
2655 eps,
2656 norm_style,
2657 );
2658 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2659 }
2660 let attn = crate::linear_core::gdn_forward_batch(
2661 &normed,
2662 b,
2663 w,
2664 &cfg,
2665 &mut self.kv_cache.layers[li].linear_state,
2666 pool.as_deref(),
2667 );
2668 for (dst, &a) in h.iter_mut().zip(&attn) {
2669 *dst += a;
2670 }
2671 }
2672 AttnKind::ShortConv(w) => {
2673 let cfg = self
2676 .short_conv_cfg
2677 .expect("short-conv layer without short_conv_cfg");
2678 let mut normed = vec![0.0f32; b * hs];
2679 for bi in 0..b {
2680 inference::rms_norm_into(
2681 &h[bi * hs..(bi + 1) * hs],
2682 &lw.input_norm,
2683 eps,
2684 norm_style,
2685 &mut normed[bi * hs..(bi + 1) * hs],
2686 );
2687 }
2688 let attn = short_conv_forward_batch(
2689 &normed,
2690 b,
2691 w,
2692 &cfg,
2693 &mut self.kv_cache.layers[li].linear_state,
2694 pool.as_deref(),
2695 );
2696 for (dst, &a) in h.iter_mut().zip(&attn) {
2697 *dst += a;
2698 }
2699 }
2700 AttnKind::Mla(w) => {
2701 let inv_freq_l = self.layer_inv_freq(li);
2704 let rs = self.layer_rope_scale(li);
2705 let mut normed = vec![0.0f32; hs];
2706 for bi in 0..b {
2707 inference::rms_norm_into(
2708 &h[bi * hs..(bi + 1) * hs],
2709 &lw.input_norm,
2710 eps,
2711 norm_style,
2712 &mut normed,
2713 );
2714 let ao = mla_attention(
2715 w,
2716 &normed,
2717 &mut self.kv_cache.layers[li],
2718 start_pos + bi,
2719 &inv_freq_l,
2720 rs,
2721 eps,
2722 pool.as_deref(),
2723 );
2724 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
2725 *dst += a;
2726 }
2727 }
2728 }
2729 AttnKind::Full {
2730 wq,
2731 wk,
2732 wv,
2733 wo,
2734 q_norm,
2735 k_norm,
2736 output_gate,
2737 softplus_gate,
2738 bias,
2739 } => {
2740 let mut normed = vec![0.0f32; b * hs];
2744 for bi in 0..b {
2745 inference::rms_norm_into(
2746 &h[bi * hs..(bi + 1) * hs],
2747 &lw.input_norm,
2748 eps,
2749 norm_style,
2750 &mut normed[bi * hs..(bi + 1) * hs],
2751 );
2752 }
2753 let inv_freq_l = self.layer_inv_freq(li);
2754 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2755 let cfg = QwenAttnCfg {
2756 num_heads: self.layer_num_heads(li),
2757 num_kv_heads: nkv_l,
2758 head_dim: hd_l,
2759 hidden_size: hs,
2760 position: start_pos,
2761 inv_freq: &inv_freq_l,
2762 rotary_dim: rd_l,
2763 scale: self.attn_scale,
2764 softcap: self.attn_softcap,
2765 window: self.layer_window(li),
2766 v_norm: self.attn_v_norm,
2767 q_norm: q_norm.as_deref(),
2768 k_norm: k_norm.as_deref(),
2769 output_gate: *output_gate,
2770 softplus_gate: softplus_gate
2771 .as_ref()
2772 .map(|(gate, per_head)| (gate, *per_head)),
2773 rope_scale: self.layer_rope_scale(li),
2774 bias: bias
2775 .as_ref()
2776 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2777 rms_eps: eps,
2778 norm_style,
2779 pool: pool.as_deref(),
2780 };
2781 let mut attn = attention::qwen_attention_batch(
2782 &normed,
2783 b,
2784 wq,
2785 wk,
2786 wv,
2787 wo,
2788 &mut self.kv_cache.layers[li],
2789 &cfg,
2790 );
2791 if let Some(w) = &lw.attn_out_norm {
2792 for bi in 0..b {
2793 inference::rms_norm_into(
2794 &attn[bi * hs..(bi + 1) * hs],
2795 w,
2796 eps,
2797 norm_style,
2798 &mut normed[bi * hs..(bi + 1) * hs],
2799 );
2800 }
2801 attn.copy_from_slice(&normed);
2802 }
2803 for (dst, &a) in h.iter_mut().zip(&attn) {
2804 *dst += a;
2805 }
2806 }
2807 AttnKind::Linear(w) => {
2808 for bi in 0..b {
2809 let normed = inference::rms_norm(
2810 &h[bi * hs..(bi + 1) * hs],
2811 &lw.input_norm,
2812 eps,
2813 norm_style,
2814 );
2815 vmf_phase_forward(
2816 &normed,
2817 w,
2818 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2819 &mut self.kv_cache.layers[li].linear_state,
2820 pool.as_deref(),
2821 )
2822 .iter()
2823 .enumerate()
2824 .for_each(|(i, &a)| h[bi * hs + i] += a);
2825 }
2826 }
2827 }
2828
2829 let lw = &self.weights.layers[self.phys_layer(li)];
2831 let mut post = vec![0.0f32; b * hs];
2832 for bi in 0..b {
2833 let r =
2834 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2835 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2836 }
2837 let mut ffn = match &lw.ffn {
2838 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2839 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
2840 FfnKind::DenseMoe(dm) => {
2843 let mut out = vec![0.0f32; b * hs];
2844 for bi in 0..b {
2845 let r = dense_moe_ffn(
2846 dm,
2847 &post[bi * hs..(bi + 1) * hs],
2848 &h[bi * hs..(bi + 1) * hs],
2849 eps,
2850 norm_style,
2851 pool.as_deref(),
2852 );
2853 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2854 }
2855 out
2856 }
2857 };
2858 if let Some(w) = &lw.ffn_out_norm {
2859 for bi in 0..b {
2860 inference::rms_norm_into(
2861 &ffn[bi * hs..(bi + 1) * hs],
2862 w,
2863 eps,
2864 norm_style,
2865 &mut post[bi * hs..(bi + 1) * hs],
2866 );
2867 }
2868 ffn.copy_from_slice(&post);
2869 }
2870 for (dst, &f) in h.iter_mut().zip(&ffn) {
2871 *dst += f;
2872 }
2873 if let Some(sc) = lw.layer_scale {
2874 for v in h.iter_mut() {
2875 *v *= sc;
2876 }
2877 }
2878 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
2879 if let Some(t) = tp.parse::<usize>().ok() {
2880 if t >= start_pos && t < start_pos + b {
2881 let bi = t - start_pos;
2882 let row = &h[bi * hs..(bi + 1) * hs];
2883 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
2884 eprintln!(
2885 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
2886 row[0], row[1]
2887 );
2888 }
2889 }
2890 }
2891 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
2895 let row = &h[(b - 1) * hs..b * hs];
2896 let rms =
2897 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
2898 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
2899 eprintln!(
2900 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
2901 match &self.weights.layers[self.phys_layer(li)].attn {
2902 AttnKind::LinearGdn(_) => "gdn",
2903 AttnKind::Linear(_) => "vmf",
2904 AttnKind::ShortConv(_) => "conv",
2905 _ => "attn",
2906 },
2907 match &lw.ffn {
2908 FfnKind::Moe(_) => "moe",
2909 FfnKind::Dense(_) => "dense",
2910 FfnKind::DenseMoe(_) => "dense+moe",
2911 },
2912 );
2913 }
2914 if self.is_loop_end(li) && li + 1 < self.num_layers {
2916 for bi in 0..b {
2917 let normed = inference::rms_norm(
2918 &h[bi * hs..(bi + 1) * hs],
2919 &self.weights.final_norm,
2920 eps,
2921 norm_style,
2922 );
2923 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2924 }
2925 }
2926 if std::env::var("CMF_TRACE_H").is_ok() {
2927 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2928 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2929 eprintln!(
2930 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
2931 lw.layer_scale
2932 );
2933 }
2934 }
2935 crate::gpu::set_layer(-1); h
2937 }
2938
2939 fn embed_single(&self, id: u32) -> Vec<f32> {
2941 let mut out = vec![0.0f32; self.hidden_size];
2942 if (id as usize) < self.weights.embed_tokens.rows() {
2943 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2944 }
2945 if self.embed_multiplier != 1.0 {
2946 for v in out.iter_mut() {
2947 *v *= self.embed_multiplier;
2948 }
2949 }
2950 out
2951 }
2952
2953 #[cfg(target_os = "macos")]
2959 fn chunk_run_gpu(
2960 &mut self,
2961 li0: usize,
2962 h: &mut [f32],
2963 b: usize,
2964 pos0: usize,
2965 embed_ids: Option<&[u32]>,
2966 ) -> usize {
2967 if !crate::gpu::enabled_here()
2971 || std::env::var("CMF_GPU_CHUNK")
2972 .map(|v| v == "0")
2973 .unwrap_or(false)
2974 || b < 32
2975 || self.swa.is_some()
2976 || self.global_attn.is_some()
2977 || self.attn_v_norm
2978 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2979 {
2980 return li0;
2981 }
2982 let Some(model) = self.model.clone() else {
2983 return li0;
2984 };
2985 let inv_freq = self.inv_freq.clone();
2986 let (nh, nkv, hd, hs) = (
2987 self.num_heads,
2988 self.num_kv_heads,
2989 self.head_dim,
2990 self.hidden_size,
2991 );
2992 let loop_end = if self.loop_final_norm {
2996 ((li0 / self.physical_layers) + 1) * self.physical_layers
2997 } else {
2998 self.num_layers
2999 };
3000 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
3001 let mut stored_at: Vec<usize> = Vec::new();
3002 for li in li0..self.num_layers.min(loop_end) {
3003 let lw = &self.weights.layers[self.phys_layer(li)];
3004 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
3005 break;
3006 }
3007 let AttnKind::Full {
3008 wq,
3009 wk,
3010 wv,
3011 wo,
3012 q_norm,
3013 k_norm,
3014 output_gate: false,
3015 softplus_gate: None,
3016 bias,
3017 } = &lw.attn
3018 else {
3019 break;
3020 };
3021 let FfnKind::Dense(d) = &lw.ffn else { break };
3022 if d.act != Act::Silu {
3023 break;
3024 }
3025 let parts = (
3026 wq.q8_row_parts(),
3027 wk.q8_row_parts(),
3028 wv.q8_row_parts(),
3029 wo.q8_row_parts(),
3030 d.gate_proj.q8_row_parts(),
3031 d.up_proj.q8_row_parts(),
3032 d.down_proj.q8_row_parts(),
3033 );
3034 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
3035 else {
3036 break;
3037 };
3038 let layer = &self.kv_cache.layers[li];
3039 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
3040 break;
3041 }
3042 stored_at.push(layer.head_len(0));
3043 layers.push(crate::gpu_metal::ChunkLayer {
3044 model: &model,
3045 kv_id: self.graph_kv_id,
3046 layer: li,
3047 wq: pq,
3048 wk: pk,
3049 wv: pv,
3050 wo: po,
3051 gate: pg,
3052 up: pu,
3053 down: pd,
3054 input_norm: &lw.input_norm,
3055 post_norm: &lw.post_norm,
3056 bias: bias
3057 .as_ref()
3058 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
3059 q_norm: q_norm.as_deref(),
3060 k_norm: k_norm.as_deref(),
3061 inv_freq: &inv_freq,
3062 rd: self.rotary_dim,
3063 nh,
3064 nkv,
3065 hd,
3066 hs,
3067 inter: d.gate_proj.rows(),
3068 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
3069 eps: self.rms_eps as f32,
3070 });
3071 }
3072 if layers.is_empty() {
3073 return li0;
3074 }
3075 let row = nkv * hd;
3076 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
3077 .iter()
3078 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
3079 .collect();
3080 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
3081 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
3082 let li = layers[i].layer;
3083 let layer = &self.kv_cache.layers[li];
3084 io.push(crate::gpu_metal::ChunkIo {
3085 cpu_stored: stored_at[i],
3086 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
3087 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
3088 out_k: ok,
3089 out_v: ov,
3090 imp: oi,
3091 });
3092 }
3093 let n_run = layers.len();
3094 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
3095 let ep = embed_ids.and_then(|ids| {
3098 self.weights
3099 .embed_tokens
3100 .q8_row_parts()
3101 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
3102 idx,
3103 rows,
3104 row_scale: rs,
3105 ids,
3106 mult: self.embed_multiplier,
3107 })
3108 });
3109 if embed_ids.is_some() && ep.is_none() {
3110 return li0;
3111 }
3112 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
3113 return li0;
3114 }
3115 drop(io);
3116 drop(layers);
3117 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
3120 let li = li0 + i;
3121 let layer = &mut self.kv_cache.layers[li];
3122 for bi in 0..b {
3123 layer.append(
3124 &ok[bi * row..(bi + 1) * row],
3125 &ov[bi * row..(bi + 1) * row],
3126 &[],
3127 );
3128 }
3129 layer.accumulate_imp(oi);
3130 }
3131 last
3132 }
3133
3134 fn layer_is_local(&self, li: usize) -> bool {
3137 if let Some(layers) = &self.sliding_layers {
3138 return layers.get(li).copied().unwrap_or(false);
3139 }
3140 match self.swa {
3141 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
3142 None => false,
3143 }
3144 }
3145
3146 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
3149 if self.layer_is_local(li) {
3150 if let Some(f) = &self.inv_freq_local {
3151 return f.clone();
3152 }
3153 } else if let Some(f) = &self.inv_freq_global {
3154 return f.clone();
3155 }
3156 self.inv_freq.clone()
3157 }
3158
3159 fn layer_window(&self, li: usize) -> Option<usize> {
3161 self.swa
3162 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
3163 }
3164
3165 fn layer_num_heads(&self, li: usize) -> usize {
3166 self.attention_heads_per_layer
3167 .as_ref()
3168 .and_then(|v| v.get(li).copied())
3169 .unwrap_or(self.num_heads)
3170 }
3171
3172 fn layer_rope_scale(&self, li: usize) -> f32 {
3173 if self.layer_is_local(li) {
3174 self.rope_scale_local
3175 } else {
3176 self.rope_scale
3177 }
3178 }
3179
3180 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
3183 if !self.layer_is_local(li) {
3184 if let Some((ghd, gkv)) = self.global_attn {
3185 return (gkv, ghd, ghd);
3186 }
3187 }
3188 (
3189 self.num_kv_heads,
3190 self.head_dim,
3191 if self.layer_is_local(li) {
3192 self.rotary_dim_local.unwrap_or(self.rotary_dim)
3193 } else {
3194 self.rotary_dim
3195 },
3196 )
3197 }
3198
3199 fn forward_layers(
3201 &mut self,
3202 hidden: &[f32],
3203 position: usize,
3204 task_mask: Option<&TaskMask>,
3205 ) -> Vec<f32> {
3206 self.forward_layers_upto(hidden, position, task_mask, None)
3207 }
3208
3209 fn try_token_graph_wgpu(
3213 &self,
3214 hidden: &[f32],
3215 position: usize,
3216 logits_out: &mut Vec<f32>,
3217 ) -> Option<Vec<f32>> {
3218 if self.o1_active() || self.attn_softcap > 0.0 {
3221 return None;
3223 }
3224 let nh = self.num_heads;
3225 let (nkv, hd, rd) = self.layer_geom(0);
3226 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3227 let mut layers = Vec::with_capacity(self.num_layers);
3228 let mut model = None;
3229 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
3230 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3231 if let Some((_, i, kind, rs)) = t.graph_weight() {
3232 return Some(crate::gpu::GraphW {
3233 idx: i,
3234 kind,
3235 row_scale: rs,
3236 data: &[],
3237 });
3238 }
3239 t.as_f32().map(|d| crate::gpu::GraphW {
3241 idx: 0,
3242 kind: 4,
3243 row_scale: &[],
3244 data: d,
3245 })
3246 }
3247 for li in 0..self.num_layers {
3248 let lw = &self.weights.layers[self.phys_layer(li)];
3249 if dbg {
3250 let ak = match &lw.attn {
3251 AttnKind::Mla(_) => "Mla".into(),
3252 AttnKind::Full {
3253 output_gate, bias, ..
3254 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
3255 AttnKind::LinearGdn(_) => "LinearGdn".into(),
3256 AttnKind::Kda(_) => "Kda".into(),
3257 AttnKind::Linear(_) => "Linear".into(),
3258 AttnKind::ShortConv(_) => "ShortConv".into(),
3259 };
3260 let fk = match &lw.ffn {
3261 FfnKind::Dense(_) => "Dense",
3262 FfnKind::Moe(_) => "Moe",
3263 FfnKind::DenseMoe(_) => "DenseMoe",
3264 };
3265 eprintln!("graph L{li}: attn={ak} ffn={fk}");
3266 }
3267 let gffn = match &lw.ffn {
3268 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3270 gate: gw(&d.gate_proj)?,
3271 up: gw(&d.up_proj)?,
3272 down: gw(&d.down_proj)?,
3273 },
3274 FfnKind::Moe(m) => {
3275 if m.router_sigmoid
3280 || m.expert_bias.is_some()
3281 || m.route_tau.is_some()
3282 || m.mask.is_some()
3283 {
3284 return None;
3285 }
3286 let (se, sg) = m.shared.as_ref()?;
3287 let sgate = gw(sg.as_ref()?)?;
3288 let router = gw(&m.router)?;
3289 let inter = m.experts.first()?.gate_proj.rows();
3290 let mut experts = Vec::with_capacity(m.experts.len() + 1);
3291 for e in m.experts.iter().chain(std::iter::once(se)) {
3292 if !matches!(e.act, Act::Silu)
3293 || e.gate_proj.rows() != inter
3294 || e.up_proj.rows() != inter
3295 {
3296 return None;
3297 }
3298 let (mm, gi) = e.gate_proj.mapped_q4t()?;
3299 let (_, ui) = e.up_proj.mapped_q4t()?;
3300 let (_, di) = e.down_proj.mapped_q4t()?;
3301 model.get_or_insert_with(|| mm.clone());
3302 experts.push((gi, ui, di));
3303 }
3304 crate::gpu::GraphFfn::Moe {
3305 router,
3306 shared_gate: sgate,
3307 experts,
3308 n_exp: m.experts.len(),
3309 top_k: m.top_k,
3310 inter,
3311 norm_topk: m.norm_topk_prob,
3312 }
3313 }
3314 };
3315 let attn = match &lw.attn {
3316 AttnKind::Full {
3317 wq,
3318 wk,
3319 wv,
3320 wo,
3321 q_norm,
3322 k_norm,
3323 output_gate,
3324 softplus_gate,
3325 bias,
3326 } => {
3327 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3328 return None;
3329 }
3330 let (m, _, _, _) = wq.graph_weight()?;
3331 model = Some(m.clone());
3332 crate::gpu::GraphAttn::Full {
3333 wq: gw(wq)?,
3334 wk: gw(wk)?,
3335 wv: gw(wv)?,
3336 wo: gw(wo)?,
3337 q_norm: q_norm.as_deref(),
3338 k_norm: k_norm.as_deref(),
3339 bias: bias
3340 .as_ref()
3341 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3342 output_gate: *output_gate,
3343 cpu_k: self.kv_cache.layers[li].k_heads(),
3344 cpu_v: self.kv_cache.layers[li].v_heads(),
3345 }
3346 }
3347 AttnKind::LinearGdn(w) => {
3348 let cfg = self.gdn_cfg?;
3349 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3350 model = Some(m.clone());
3351 crate::gpu::GraphAttn::Gdn {
3352 qkv: gw(&w.in_proj_qkv)?,
3353 z: gw(&w.in_proj_z)?,
3354 a: gw(&w.in_proj_a)?,
3355 b: gw(&w.in_proj_b)?,
3356 out: gw(&w.out_proj)?,
3357 conv1d: &w.conv1d,
3358 a_log: &w.a_log,
3359 dt_bias: &w.dt_bias,
3360 norm: &w.norm,
3361 nv: cfg.num_v_heads,
3362 nk: cfg.num_k_heads,
3363 dk: cfg.key_head_dim,
3364 dv: cfg.value_head_dim,
3365 kk: cfg.conv_kernel,
3366 }
3367 }
3368 _ => return None,
3369 };
3370 layers.push(crate::gpu::GraphLayer {
3371 input_norm: &lw.input_norm,
3372 attn,
3373 post_norm: &lw.post_norm,
3374 ffn: gffn,
3375 });
3376 }
3377 let model = model?;
3378 let lm_gw = if self.graph_want_logits
3384 && std::env::var("CMF_GPU_LMHEAD")
3385 .map(|v| v != "0")
3386 .unwrap_or(true)
3387 {
3388 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
3389 (
3390 crate::gpu::GraphW {
3391 idx: i,
3392 kind,
3393 row_scale: rs,
3394 data: &[],
3395 },
3396 self.weights.lm_head.rows(),
3397 )
3398 })
3399 } else {
3400 None
3401 };
3402 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
3403 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
3406 (0..self.num_layers - 1)
3407 .filter(|&li| (li + 1) % self.physical_layers == 0)
3408 .collect()
3409 } else {
3410 Vec::new()
3411 };
3412 let mut h = hidden.to_vec();
3413 crate::gpu::forward_token_graph(
3414 &model,
3415 self.graph_kv_id,
3416 &layers,
3417 &self.inv_freq,
3418 &mut h,
3419 nh,
3420 nkv,
3421 hd,
3422 rd,
3423 self.hidden_size,
3424 self.intermediate_size,
3425 position,
3426 self.kv_cache.max_seq_len,
3427 gemma,
3428 self.rms_eps as f32,
3429 lm,
3430 &self.weights.final_norm,
3431 logits_out,
3432 &loop_norm_at,
3433 )
3434 .then_some(h)
3435 }
3436
3437 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
3442 if self.attn_softcap > 0.0 {
3443 return false; }
3445 if self.o1_active() {
3446 return false;
3447 }
3448 let nh = self.num_heads;
3449 let (nkv, hd, rd) = self.layer_geom(0);
3450 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3451 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3452 if let Some((_, i, kind, rs)) = t.graph_weight() {
3453 return Some(crate::gpu::GraphW {
3454 idx: i,
3455 kind,
3456 row_scale: rs,
3457 data: &[],
3458 });
3459 }
3460 t.as_f32().map(|d| crate::gpu::GraphW {
3461 idx: 0,
3462 kind: 4,
3463 row_scale: &[],
3464 data: d,
3465 })
3466 }
3467 let built: Option<(
3468 Vec<crate::gpu::GraphLayer<'_>>,
3469 std::sync::Arc<cortiq_core::CmfModel>,
3470 )> = (|| {
3471 let mut layers = Vec::with_capacity(self.num_layers);
3472 let mut model = None;
3473 for li in 0..self.num_layers {
3474 let lw = &self.weights.layers[self.phys_layer(li)];
3475 let gffn = match &lw.ffn {
3478 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3479 gate: gw(&d.gate_proj)?,
3480 up: gw(&d.up_proj)?,
3481 down: gw(&d.down_proj)?,
3482 },
3483 _ => return None,
3484 };
3485 let attn = match &lw.attn {
3486 AttnKind::Full {
3487 wq,
3488 wk,
3489 wv,
3490 wo,
3491 q_norm,
3492 k_norm,
3493 output_gate,
3494 softplus_gate,
3495 bias,
3496 } => {
3497 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3498 return None;
3499 }
3500 let (m, _, _, _) = wq.graph_weight()?;
3501 model = Some(m.clone());
3502 crate::gpu::GraphAttn::Full {
3503 wq: gw(wq)?,
3504 wk: gw(wk)?,
3505 wv: gw(wv)?,
3506 wo: gw(wo)?,
3507 q_norm: q_norm.as_deref(),
3508 k_norm: k_norm.as_deref(),
3509 bias: bias
3510 .as_ref()
3511 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3512 output_gate: *output_gate,
3513 cpu_k: self.kv_cache.layers[li].k_heads(),
3514 cpu_v: self.kv_cache.layers[li].v_heads(),
3515 }
3516 }
3517 AttnKind::LinearGdn(w) => {
3518 let cfg = self.gdn_cfg?;
3519 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3520 model = Some(m.clone());
3521 crate::gpu::GraphAttn::Gdn {
3522 qkv: gw(&w.in_proj_qkv)?,
3523 z: gw(&w.in_proj_z)?,
3524 a: gw(&w.in_proj_a)?,
3525 b: gw(&w.in_proj_b)?,
3526 out: gw(&w.out_proj)?,
3527 conv1d: &w.conv1d,
3528 a_log: &w.a_log,
3529 dt_bias: &w.dt_bias,
3530 norm: &w.norm,
3531 nv: cfg.num_v_heads,
3532 nk: cfg.num_k_heads,
3533 dk: cfg.key_head_dim,
3534 dv: cfg.value_head_dim,
3535 kk: cfg.conv_kernel,
3536 }
3537 }
3538 _ => return None,
3539 };
3540 layers.push(crate::gpu::GraphLayer {
3541 input_norm: &lw.input_norm,
3542 attn,
3543 post_norm: &lw.post_norm,
3544 ffn: gffn,
3545 });
3546 }
3547 Some((layers, model?))
3548 })();
3549 let Some((layers, model)) = built else {
3550 return false;
3551 };
3552 crate::gpu::forward_batch_graph(
3553 &model,
3554 self.graph_kv_id,
3555 &layers,
3556 &self.inv_freq,
3557 hiddens,
3558 nh,
3559 nkv,
3560 hd,
3561 rd,
3562 self.hidden_size,
3563 self.intermediate_size,
3564 positions,
3565 self.kv_cache.max_seq_len,
3566 gemma,
3567 self.rms_eps as f32,
3568 k,
3569 )
3570 }
3571
3572 fn forward_layers_upto(
3574 &mut self,
3575 hidden: &[f32],
3576 position: usize,
3577 task_mask: Option<&TaskMask>,
3578 upto: Option<usize>,
3579 ) -> Vec<f32> {
3580 let mut h = hidden.to_vec();
3581 let (nh, _nkv, _hd, hs, _rd, eps) = (
3584 self.num_heads,
3585 self.num_kv_heads,
3586 self.head_dim,
3587 self.hidden_size,
3588 self.rotary_dim,
3589 self.rms_eps,
3590 );
3591 let pool = self.pool.clone();
3592 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
3604 let graph_on = match graph_env.as_deref() {
3605 Some("0") => false,
3606 Some(_) => true,
3607 None => {
3608 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
3609 || crate::gpu::wgpu_active()
3610 }
3611 };
3612 let graph_trusted =
3613 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
3614 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
3615 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
3616 let t_graph = std::time::Instant::now();
3617 let mut lg = Vec::new();
3618 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
3619 let dur = t_graph.elapsed();
3620 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3621 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
3622 }
3623 if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
3624 if !graph_trusted {
3625 crate::gpu::graph_race_record(true, dur);
3626 }
3627 if !lg.is_empty() {
3628 lg.resize(self.vocab_size, 0.0);
3631 if let Some(c) = self.final_softcap {
3632 for l in lg.iter_mut() {
3633 *l = c * (*l / c).tanh();
3634 }
3635 }
3636 self.graph_logits = Some(lg);
3637 }
3638 return hh;
3639 }
3640 }
3646 }
3647 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
3648
3649 #[cfg(target_os = "macos")]
3650 let mut gpu_skip_until = 0usize;
3651 for li in 0..self.num_layers {
3652 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
3654 if li > u {
3655 break;
3656 }
3657 }
3658 if let Some(mask) = task_mask {
3659 if !mask.layer_alive(li) {
3660 continue; }
3662 }
3663 #[cfg(target_os = "macos")]
3667 {
3668 if li < gpu_skip_until {
3669 continue;
3670 }
3671 if task_mask.is_none() {
3672 let end = self.q1_graph_gpu(li, upto, position, &mut h);
3673 if end > li {
3674 gpu_skip_until = end;
3675 if self.is_loop_end(end - 1) && end < self.num_layers {
3678 h = inference::rms_norm(
3679 &h,
3680 &self.weights.final_norm,
3681 self.rms_eps,
3682 self.norm_style,
3683 );
3684 }
3685 continue;
3686 }
3687 }
3688 }
3689
3690 let lw = &self.weights.layers[self.phys_layer(li)];
3691 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
3692 if tp.parse::<usize>().ok() == Some(position) {
3693 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
3694 eprintln!(
3695 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
3696 h[0], h[1]
3697 );
3698 }
3699 }
3700 inference::rms_norm_into(
3703 &h,
3704 &lw.input_norm,
3705 self.rms_eps,
3706 self.norm_style,
3707 &mut self.ws.n1,
3708 );
3709
3710 let attn_out = match &lw.attn {
3711 AttnKind::Mla(w) => {
3712 let inv_freq_l = self.layer_inv_freq(li);
3713 let rs = self.layer_rope_scale(li);
3714 let eps = self.rms_eps;
3715 let pool = self.pool.clone();
3716 mla_attention(
3717 w,
3718 &self.ws.n1,
3719 &mut self.kv_cache.layers[li],
3720 position,
3721 &inv_freq_l,
3722 rs,
3723 eps,
3724 pool.as_deref(),
3725 )
3726 }
3727 AttnKind::Linear(w) => {
3728 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3729 vmf_phase_forward(
3730 &self.ws.n1,
3731 w,
3732 &cfg,
3733 &mut self.kv_cache.layers[li].linear_state,
3734 self.pool.as_deref(),
3735 )
3736 }
3737 AttnKind::Kda(w) => {
3738 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
3739 crate::linear_core::kda_forward(
3740 &self.ws.n1,
3741 w,
3742 &cfg,
3743 &mut self.kv_cache.layers[li].linear_state,
3744 self.pool.as_deref(),
3745 )
3746 }
3747 AttnKind::LinearGdn(w) => {
3748 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3749 gdn_forward(
3750 &self.ws.n1,
3751 w,
3752 &cfg,
3753 &mut self.kv_cache.layers[li].linear_state,
3754 self.pool.as_deref(),
3755 )
3756 }
3757 AttnKind::ShortConv(w) => {
3758 let cfg = self
3759 .short_conv_cfg
3760 .expect("short-conv layer without short_conv_cfg");
3761 short_conv_forward(
3762 &self.ws.n1,
3763 w,
3764 &cfg,
3765 &mut self.kv_cache.layers[li].linear_state,
3766 self.pool.as_deref(),
3767 )
3768 }
3769 AttnKind::Full {
3770 wq,
3771 wk,
3772 wv,
3773 wo,
3774 q_norm,
3775 k_norm,
3776 output_gate,
3777 softplus_gate,
3778 bias,
3779 } if self.kv_cache.layers[li].o1_sealed() => {
3780 let inv_freq_l = self.layer_inv_freq(li);
3783 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3784 let cfg = QwenAttnCfg {
3785 num_heads: self.layer_num_heads(li),
3786 num_kv_heads: nkv_l,
3787 head_dim: hd_l,
3788 hidden_size: hs,
3789 position,
3790 inv_freq: &inv_freq_l,
3791 rotary_dim: rd_l,
3792 scale: self.attn_scale,
3793 softcap: self.attn_softcap,
3794 window: None,
3795 v_norm: self.attn_v_norm,
3796 q_norm: q_norm.as_deref(),
3797 k_norm: k_norm.as_deref(),
3798 output_gate: *output_gate,
3799 softplus_gate: softplus_gate
3800 .as_ref()
3801 .map(|(gate, per_head)| (gate, *per_head)),
3802 rope_scale: self.layer_rope_scale(li),
3803 bias: bias
3804 .as_ref()
3805 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3806 rms_eps: eps,
3807 norm_style: self.norm_style,
3808 pool: pool.as_deref(),
3809 };
3810 attention::qwen_attention_nystrom(
3811 &self.ws.n1,
3812 wq,
3813 wk,
3814 wv,
3815 wo,
3816 &mut self.kv_cache.layers[li],
3817 &cfg,
3818 )
3819 }
3820 AttnKind::Full {
3821 wq,
3822 wk,
3823 wv,
3824 wo,
3825 q_norm,
3826 k_norm,
3827 output_gate,
3828 softplus_gate,
3829 bias,
3830 } => 'attn: {
3831 if graph_on
3834 && !*output_gate
3835 && softplus_gate.is_none()
3836 && self.attention_heads_per_layer.is_none()
3837 && bias.is_none()
3838 && task_mask.is_none()
3839 {
3840 let inv_freq_l = self.layer_inv_freq(li);
3841 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3842 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3843 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
3844 wq.mapped_q1(),
3845 wk.mapped_q1(),
3846 wv.mapped_q1(),
3847 wo.mapped_q1(),
3848 ) {
3849 let gm = gm.clone();
3850 let mut out = vec![0f32; hs];
3851 let cache = &self.kv_cache.layers[li];
3852 if crate::gpu::attn_dropin(
3853 &gm,
3854 self.graph_kv_id,
3855 li,
3856 &self.ws.n1,
3857 qi,
3858 ki,
3859 vi,
3860 oi,
3861 q_norm.as_deref(),
3862 k_norm.as_deref(),
3863 &inv_freq_l,
3864 nh,
3865 nkv_l,
3866 hd_l,
3867 rd_l,
3868 hs,
3869 position,
3870 self.kv_cache.max_seq_len,
3871 gemma,
3872 eps as f32,
3873 cache.k_heads(),
3874 cache.v_heads(),
3875 &mut out,
3876 ) {
3877 break 'attn out;
3878 }
3879 }
3880 }
3881 let masked = task_mask
3882 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
3883 .unwrap_or(false);
3884 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
3885 match (masked, f32_view) {
3886 (true, (Some(q), Some(k), Some(v), Some(o))) => {
3889 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
3890 attention::multi_head_attention(
3891 &self.ws.n1,
3892 q,
3893 k,
3894 v,
3895 o,
3896 &mut self.kv_cache.layers[li],
3897 self.num_heads,
3898 self.num_kv_heads,
3899 self.head_dim,
3900 self.hidden_size,
3901 position,
3902 &active_heads,
3903 &self.inv_freq,
3904 )
3905 }
3906 (masked, _) => {
3907 if masked {
3908 tracing::warn!(
3909 "layer {li}: head mask on quantized weights not \
3910 supported yet — executing dense"
3911 );
3912 }
3913 let inv_freq_l = self.layer_inv_freq(li);
3914 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3915 let cfg = QwenAttnCfg {
3916 num_heads: self.layer_num_heads(li),
3917 num_kv_heads: nkv_l,
3918 head_dim: hd_l,
3919 hidden_size: hs,
3920 position,
3921 inv_freq: &inv_freq_l,
3922 rotary_dim: rd_l,
3923 scale: self.attn_scale,
3924 softcap: self.attn_softcap,
3925 window: self.layer_window(li),
3926 v_norm: self.attn_v_norm,
3927 q_norm: q_norm.as_deref(),
3928 k_norm: k_norm.as_deref(),
3929 output_gate: *output_gate,
3930 softplus_gate: softplus_gate
3931 .as_ref()
3932 .map(|(gate, per_head)| (gate, *per_head)),
3933 rope_scale: self.layer_rope_scale(li),
3934 bias: bias
3935 .as_ref()
3936 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3937 rms_eps: eps,
3938 norm_style: self.norm_style,
3939 pool: pool.as_deref(),
3940 };
3941 attention::qwen_attention(
3942 &self.ws.n1,
3943 wq,
3944 wk,
3945 wv,
3946 wo,
3947 &mut self.kv_cache.layers[li],
3948 &cfg,
3949 )
3950 }
3951 }
3952 }
3953 };
3954 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
3957 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
3958 None => attn_out,
3959 };
3960 let lw = &self.weights.layers[self.phys_layer(li)];
3961 inference::add_rmsnorm_fused_into(
3962 &mut h,
3963 &attn_out,
3964 &lw.post_norm,
3965 self.rms_eps,
3966 self.norm_style,
3967 &mut self.ws.p1,
3968 );
3969 let mut attn_out = attn_out;
3970 attention::recycle_buf(&mut attn_out);
3971 let post_normed = &self.ws.p1;
3972
3973 let ffn_masked = task_mask
3974 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
3975 .unwrap_or(false);
3976 let f32_ffn = match &lw.ffn {
3979 FfnKind::Dense(d) => (
3980 d.gate_proj.as_f32(),
3981 d.up_proj.as_f32(),
3982 d.down_proj.as_f32(),
3983 ),
3984 FfnKind::Moe(_) | FfnKind::DenseMoe(_) => (None, None, None),
3985 };
3986 let ffn_out = match (ffn_masked, f32_ffn) {
3987 (true, (Some(g), Some(u), Some(d))) => {
3988 let active = task_mask.unwrap().ffn_active_indices(li);
3989 inference::sparse_ffn_forward(
3990 post_normed,
3991 g,
3992 u,
3993 d,
3994 self.hidden_size,
3995 self.intermediate_size,
3996 &active,
3997 self.pool.as_deref(),
3998 )
3999 }
4000 (true, _) => match &lw.ffn {
4004 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
4005 let active = task_mask.unwrap().ffn_active_indices(li);
4006 sparse_ffn_quant(
4007 d,
4008 post_normed,
4009 &active,
4010 self.hidden_size,
4011 self.pool.as_deref(),
4012 )
4013 }
4014 FfnKind::Dense(d) => {
4019 let active = task_mask.unwrap().ffn_active_indices(li);
4020 let (gf, uf, df) = dequant_dense_f32(d);
4021 inference::sparse_ffn_forward(
4022 post_normed,
4023 &gf,
4024 &uf,
4025 &df,
4026 self.hidden_size,
4027 self.intermediate_size,
4028 &active,
4029 self.pool.as_deref(),
4030 )
4031 }
4032 FfnKind::Moe(m) => {
4033 let allowed = task_mask
4037 .and_then(|tm| tm.expert_flags(li, m.experts.len()));
4038 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
4039 }
4040 FfnKind::DenseMoe(dm) => dense_moe_ffn(
4041 dm,
4042 post_normed,
4043 &h,
4044 self.rms_eps,
4045 self.norm_style,
4046 self.pool.as_deref(),
4047 ),
4048 },
4049 (false, _) => match &lw.ffn {
4050 FfnKind::DenseMoe(dm) => dense_moe_ffn(
4051 dm,
4052 post_normed,
4053 &h,
4054 self.rms_eps,
4055 self.norm_style,
4056 self.pool.as_deref(),
4057 ),
4058 _ => {
4059 let allowed = match (&lw.ffn, task_mask) {
4060 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
4061 _ => None,
4062 };
4063 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
4064 }
4065 },
4066 };
4067 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4068 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
4069 None => ffn_out,
4070 };
4071 for (i, &f) in ffn_out.iter().enumerate() {
4072 h[i] += f;
4073 }
4074 let mut ffn_out = ffn_out;
4075 attention::recycle_buf(&mut ffn_out);
4076
4077 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4079 for v in h.iter_mut() {
4080 *v *= sc;
4081 }
4082 }
4083
4084 if self.is_loop_end(li) && li + 1 < self.num_layers {
4087 h = inference::rms_norm(
4088 &h,
4089 &self.weights.final_norm,
4090 self.rms_eps,
4091 self.norm_style,
4092 );
4093 }
4094
4095 if self.dyn_phi_layer == Some(li) {
4099 self.update_dyn_phi(&h);
4100 }
4101 }
4102 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
4104 crate::gpu::graph_race_record(false, t.elapsed());
4105 }
4106
4107 h
4108 }
4109
4110 fn update_dyn_phi(&mut self, h: &[f32]) {
4113 const A: f32 = 0.2;
4114 if self.dyn_phi_ema.len() != h.len() {
4115 self.dyn_phi_ema = vec![0.0; h.len()];
4116 self.dyn_phi_seen = 0;
4117 }
4118 if self.dyn_phi_seen == 0 {
4119 self.dyn_phi_ema.copy_from_slice(h);
4120 } else {
4121 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
4122 *e = (1.0 - A) * *e + A * v;
4123 }
4124 }
4125 self.dyn_phi_seen += 1;
4126 }
4127
4128 pub fn dyn_phi(&self) -> &[f32] {
4130 &self.dyn_phi_ema
4131 }
4132
4133 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
4135 self.dyn_phi_layer = layer;
4136 self.dyn_phi_ema.clear();
4137 self.dyn_phi_seen = 0;
4138 }
4139
4140 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
4142 let Some(model) = &self.model else {
4143 return Vec::new();
4144 };
4145 model
4146 .header
4147 .skills
4148 .iter()
4149 .enumerate()
4150 .filter_map(|(i, sk)| {
4151 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
4152 let sel = sk.selection.as_ref()?;
4153 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
4154 })
4155 .collect()
4156 }
4157
4158 pub fn active_skill(&self) -> Option<usize> {
4160 self.dyn_active
4161 }
4162
4163 pub fn enable_dynamic_routing(&mut self) -> usize {
4168 use crate::swarm::{DynRouter, RoutableSkill};
4169 let Some(model) = self.model.clone() else {
4170 return 0;
4171 };
4172 if self.dyn_blend_loaded {
4175 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
4176 return 0;
4177 }
4178 if let Some(a) = self.dyn_active {
4182 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
4183 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
4184 return 0;
4185 }
4186 }
4187 let hidden = self.hidden_size;
4188 let mut skills = Vec::new();
4189 for (idx, id, _phi) in self.dynamic_skills() {
4190 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
4191 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
4192 skills.push(rs);
4193 }
4194 }
4195 }
4196 if skills.is_empty() {
4197 return 0;
4198 }
4199 let phi = skills[0].phi_layer;
4201 if skills.iter().any(|s| s.phi_layer != phi) {
4202 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
4203 }
4204 let n = skills.len();
4205 self.set_dyn_phi_layer(Some(phi));
4206 self.dyn_router = Some(DynRouter::new(skills));
4207 n
4208 }
4209
4210 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
4212 self.dyn_router
4213 .as_ref()
4214 .map(|r| r.switches.clone())
4215 .unwrap_or_default()
4216 }
4217
4218 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
4221 let rows = self.weights.lm_head.rows();
4222 let mut logits = attention::take_buf(rows.min(self.vocab_size));
4223 self.weights
4224 .lm_head
4225 .matvec(hidden, &mut logits, self.pool.as_deref());
4226 logits.resize(self.vocab_size, 0.0);
4227 if let Some(m) = self.logit_multiplier {
4228 for l in logits.iter_mut() {
4229 *l *= m;
4230 }
4231 }
4232 if let Some(c) = self.final_softcap {
4233 for l in logits.iter_mut() {
4234 *l = c * (*l / c).tanh();
4235 }
4236 }
4237 logits
4238 }
4239
4240 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
4245 self.kv_cache.clear();
4246 let mut hidden = vec![0.0f32; self.hidden_size];
4247 for (pos, &id) in ids.iter().enumerate() {
4248 let emb = self.embed_single(id);
4249 hidden = self.forward_layers(&emb, pos, task_mask);
4250 }
4251 inference::rms_norm_into(
4252 &hidden,
4253 &self.weights.final_norm,
4254 self.rms_eps,
4255 self.norm_style,
4256 &mut self.ws.n1,
4257 );
4258 self.lm_head_forward(&self.ws.n1)
4259 }
4260}
4261
4262pub fn create_test_pipeline(
4264 hidden_size: usize,
4265 intermediate_size: usize,
4266 num_heads: usize,
4267 num_kv_heads: usize,
4268 head_dim: usize,
4269 num_layers: usize,
4270 vocab_size: usize,
4271) -> Pipeline {
4272 let synth = |n: usize, salt: usize| -> Vec<f32> {
4275 (0..n)
4276 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
4277 .collect()
4278 };
4279 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4280 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4281 };
4282 let layer_weights: Vec<LayerWeights> = (0..num_layers)
4283 .map(|li| LayerWeights {
4284 input_norm: vec![1.0; hidden_size],
4285 post_norm: vec![1.0; hidden_size],
4286 attn_out_norm: None,
4287 ffn_out_norm: None,
4288 layer_scale: None,
4289 ffn: FfnKind::Dense(DenseFfn {
4290 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
4291 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
4292 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
4293 act: Act::Silu,
4294 }),
4295 attn: AttnKind::Full {
4296 bias: None,
4297 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
4298 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
4299 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
4300 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
4301 q_norm: None,
4302 k_norm: None,
4303 output_gate: false,
4304 softplus_gate: None,
4305 },
4306 })
4307 .collect();
4308
4309 Pipeline::new(
4310 Tokenizer::byte_level(),
4311 PipelineWeights {
4312 embed_tokens: qt(vocab_size, hidden_size, 100),
4313 layers: layer_weights,
4314 lm_head: qt(vocab_size, hidden_size, 200),
4315 final_norm: vec![1.0; hidden_size],
4316 },
4317 hidden_size,
4318 intermediate_size,
4319 num_heads,
4320 num_kv_heads,
4321 head_dim,
4322 num_layers,
4323 num_layers, false, vocab_size,
4326 1e-6,
4327 10_000.0,
4328 NormStyle::Qwen,
4329 4096,
4330 SamplerConfig {
4331 seed: Some(42),
4332 ..Default::default()
4333 },
4334 )
4335}
4336
4337fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
4340 let inter = d.gate_proj.rows();
4341 let hidden = d.down_proj.rows();
4342 let mut g = vec![0.0f32; b * inter];
4343 d.gate_proj.matmat(xs, b, &mut g, pool);
4344 let mut u = vec![0.0f32; b * inter];
4345 d.up_proj.matmat(xs, b, &mut u, pool);
4346 for i in 0..b * inter {
4347 g[i] = d.act.combine(g[i], u[i]);
4348 }
4349 let mut out = vec![0.0f32; b * hidden];
4350 d.down_proj.matmat(&g, b, &mut out, pool);
4351 out
4352}
4353
4354fn moe_ffn_batch(
4358 m: &MoeFfn,
4359 xs: &[f32],
4360 b: usize,
4361 hidden: usize,
4362 pool: Option<&Pool>,
4363 allowed: Option<&[bool]>,
4364) -> Vec<f32> {
4365 let ne = m.experts.len();
4366 let mut logits = vec![0.0f32; b * ne];
4367 m.router.matmat(xs, b, &mut logits, pool);
4368
4369 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
4372 {
4373 let mut st = m.stats.borrow_mut();
4374 if st.len() < ne {
4375 st.resize(ne, 0);
4376 }
4377 for bi in 0..b {
4378 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
4379 for &e in &idx {
4380 st[e] += 1;
4381 assign[e].push((bi, p[e] / wsum));
4382 }
4383 }
4384 }
4385
4386 let mut out = vec![0.0f32; b * hidden];
4387 let cols = m.experts[0].gate_proj.cols();
4388 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
4389 let sb = list.len();
4390 let mut sub = vec![0.0f32; sb * cols];
4391 for (k, &(bi, _)) in list.iter().enumerate() {
4392 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
4393 }
4394 let eo = dense_ffn_batch(d, &sub, sb, pool);
4395 for (k, &(bi, w)) in list.iter().enumerate() {
4396 for i in 0..hidden {
4397 out[bi * hidden + i] += w * eo[k * hidden + i];
4398 }
4399 }
4400 };
4401 for (e, a) in assign.iter().enumerate().take(ne) {
4402 if !a.is_empty() {
4403 run_expert(&m.experts[e], a);
4404 }
4405 }
4406 if let Some((se, gate)) = &m.shared {
4407 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
4408 let mut gl = vec![0.0f32; b];
4409 gate.matmat(xs, b, &mut gl, pool);
4410 (0..b)
4411 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
4412 .collect()
4413 } else {
4414 (0..b).map(|bi| (bi, 1.0)).collect()
4415 };
4416 run_expert(se, &all);
4417 }
4418 out
4419}
4420
4421thread_local! {
4422 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
4426 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
4427}
4428
4429fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4431 if crate::gpu::enabled_here()
4442 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
4443 {
4444 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
4445 crate::gpu::ProbeArm::Gpu
4446 } else {
4447 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
4448 };
4449 match arm {
4450 crate::gpu::ProbeArm::Gpu => {
4451 let t0 = std::time::Instant::now();
4452 if let Some(out) = dense_ffn_gpu(d, x, pool) {
4453 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4454 return out;
4455 }
4456 }
4457 crate::gpu::ProbeArm::CpuTimed => {
4458 let t0 = std::time::Instant::now();
4459 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4460 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4461 return out;
4462 }
4463 crate::gpu::ProbeArm::Cpu => {
4464 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4465 }
4466 }
4467 }
4468 dense_ffn_cpu(d, x, pool)
4469}
4470
4471fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4473 let inter = d.gate_proj.rows();
4474 FFN_SCRATCH.with(|s| {
4475 let mut s = s.borrow_mut();
4476 let [g, u, ..] = &mut *s;
4477 g.resize(inter, 0.0);
4478 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
4481 } else {
4483 u.resize(inter, 0.0);
4484 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
4486 for i in 0..inter {
4487 g[i] = d.act.combine(g[i], u[i]);
4488 }
4489 }
4490 FFN_PROBE.with(|pr| {
4493 if let Some(acc) = pr.borrow_mut().as_mut() {
4494 let li = crate::gpu::cur_layer();
4495 if li >= 0 {
4496 if let Some(row) = acc.get_mut(li as usize) {
4497 for (a, &v) in row.iter_mut().zip(g.iter()) {
4498 *a += (v as f64).abs();
4499 }
4500 }
4501 }
4502 }
4503 });
4504 let mut out = attention::take_buf(d.down_proj.rows());
4505 d.down_proj.matvec(g, &mut out, pool);
4506 out
4507 })
4508}
4509
4510thread_local! {
4511 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
4514 const { std::cell::RefCell::new(None) };
4515}
4516
4517fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
4523 if d.act != Act::Silu {
4525 return None;
4526 }
4527 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
4530 return None;
4531 }
4532 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
4533 let mut model_ref = None;
4534 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
4535 let model = model_ref?;
4536 let hidden = jobs[0].down.1;
4537 let mut out = attention::take_buf(hidden);
4538 if crate::gpu::moe_block(&model, &jobs, &mut out) {
4539 Some(out)
4540 } else {
4541 let mut out = out;
4542 attention::recycle_buf(&mut out);
4543 None
4544 }
4545}
4546
4547#[allow(clippy::type_complexity)]
4552#[allow(clippy::type_complexity)]
4553fn moe_parts(
4554 t: &QTensor,
4555) -> Option<(
4556 &std::sync::Arc<cortiq_core::CmfModel>,
4557 usize,
4558 usize,
4559 usize,
4560 &[f32],
4561 &[f32],
4562 bool,
4563 bool,
4564)> {
4565 match t {
4566 QTensor::Mapped {
4567 model,
4568 idx,
4569 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
4570 rows,
4571 cols,
4572 row_scale,
4573 col_field,
4574 ..
4575 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
4576 Some((model, *idx, *rows, *cols, row_scale, col_field, false, false))
4577 }
4578 QTensor::Mapped {
4580 model,
4581 idx,
4582 dtype: cortiq_core::TensorDtype::Q1,
4583 rows,
4584 cols,
4585 ..
4586 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
4587 QTensor::Mapped {
4589 model,
4590 idx,
4591 dtype: cortiq_core::TensorDtype::Q4Tiled,
4592 rows,
4593 cols,
4594 ..
4595 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
4596 _ => None,
4597 }
4598}
4599
4600fn moe_push_job<'a>(
4602 d: &'a DenseFfn,
4603 x: &[f32],
4604 w: f32,
4605 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
4606 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
4607) -> Option<()> {
4608 use crate::qtensor::prescale;
4609 if d.act != Act::Silu {
4610 return None; }
4612 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
4613 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
4614 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
4615 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
4616 return None; }
4618 model_ref.get_or_insert_with(|| gm.clone());
4619 let gdt = if gcf.is_empty() {
4620 cortiq_core::TensorDtype::Q8Row
4621 } else {
4622 cortiq_core::TensorDtype::Q8_2f
4623 };
4624 let udt = if ucf.is_empty() {
4625 cortiq_core::TensorDtype::Q8Row
4626 } else {
4627 cortiq_core::TensorDtype::Q8_2f
4628 };
4629 jobs.push(crate::gpu::MoeJob {
4630 gate: (gi, gr, gc, grs),
4631 up: (ui, ur, uc, urs),
4632 down: (di, dr, dc, drs),
4633 xs_gate: prescale(x, gcf, gdt).into_owned(),
4634 xs_up: prescale(x, ucf, udt).into_owned(),
4635 down_col: dcf,
4636 w,
4637 q1: gq1,
4638 q4t: gq4,
4639 });
4640 Some(())
4641}
4642
4643fn sparse_ffn_quant(
4650 d: &DenseFfn,
4651 x: &[f32],
4652 active: &[u16],
4653 hidden: usize,
4654 pool: Option<&Pool>,
4655) -> Vec<f32> {
4656 let n = active.len();
4657 let inter = d.gate_proj.rows();
4658 let mut act = vec![0.0f32; n];
4659 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
4662 let compute = |ai: usize| -> f32 {
4663 let idx = active[ai] as usize;
4664 if idx >= inter {
4665 return 0.0; }
4667 let mut s = if need_scratch {
4668 vec![0.0f32; hidden]
4669 } else {
4670 Vec::new()
4671 };
4672 let gate = d.gate_proj.row_dot(idx, x, &mut s);
4673 let up = d.up_proj.row_dot(idx, x, &mut s);
4674 d.act.combine(gate, up)
4675 };
4676 match pool {
4677 Some(p) if n >= 256 => {
4678 let ptr = SendMut(act.as_mut_ptr());
4679 p.run(&|widx, nw| {
4680 let chunk = n.div_ceil(nw);
4681 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
4682 for ai in s..e {
4683 unsafe { *ptr.at(ai) = compute(ai) };
4684 }
4685 });
4686 }
4687 _ => {
4688 for (ai, a) in act.iter_mut().enumerate() {
4689 *a = compute(ai);
4690 }
4691 }
4692 }
4693 let mut out = vec![0.0f32; hidden];
4695 for (ai, &idx) in active.iter().enumerate() {
4696 let w = act[ai];
4697 if w.abs() >= 1e-12 && (idx as usize) < inter {
4698 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
4699 }
4700 }
4701 out
4702}
4703
4704#[doc(hidden)]
4706pub fn sparse_ffn_quant_for_test(
4707 d: &DenseFfn,
4708 x: &[f32],
4709 active: &[u16],
4710 hidden: usize,
4711) -> Vec<f32> {
4712 sparse_ffn_quant(d, x, active, hidden, None)
4713}
4714
4715fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
4719 let deq = |t: &QTensor| -> Vec<f32> {
4720 let (rows, cols) = (t.rows(), t.cols());
4721 let mut out = vec![0.0f32; rows * cols];
4722 for r in 0..rows {
4723 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
4724 }
4725 out
4726 };
4727 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
4728}
4729
4730struct SendMut(*mut f32);
4732unsafe impl Send for SendMut {}
4733unsafe impl Sync for SendMut {}
4734impl SendMut {
4735 #[inline]
4736 #[allow(clippy::mut_from_ref)]
4739 unsafe fn at(&self, i: usize) -> &mut f32 {
4740 unsafe { &mut *self.0.add(i) }
4741 }
4742}
4743
4744fn moe_route(
4754 logits: &[f32],
4755 m: &MoeFfn,
4756 allowed: Option<&[bool]>,
4757) -> (Vec<usize>, Vec<f32>, f32) {
4758 let ne = logits.len();
4759 let p: Vec<f32> = if m.router_sigmoid {
4760 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
4761 } else {
4762 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4763 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
4764 let s: f32 = e.iter().sum();
4765 for v in &mut e {
4766 *v /= s;
4767 }
4768 e
4769 };
4770 let admit = |e: usize| {
4776 m.mask.as_ref().is_none_or(|mk| mk[e]) && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
4777 };
4778 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
4779 match &m.expert_bias {
4781 Some(b) => idx.sort_unstable_by(|&x, &y| {
4782 (p[y] + b[y])
4783 .partial_cmp(&(p[x] + b[x]))
4784 .unwrap()
4785 .then(x.cmp(&y))
4786 }),
4787 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
4788 }
4789 idx.truncate(m.top_k);
4790 if let Some(tau) = m.route_tau {
4794 let total: f32 = idx.iter().map(|&e| p[e]).sum();
4795 if total > 0.0 {
4796 let mut acc = 0.0f32;
4797 let mut keep = idx.len();
4798 for (i, &e) in idx.iter().enumerate() {
4799 acc += p[e];
4800 if acc >= tau * total {
4801 keep = i + 1;
4802 break;
4803 }
4804 }
4805 idx.truncate(keep);
4806 }
4807 }
4808 let wsum: f32 = if m.norm_topk_prob {
4809 let s: f32 = idx.iter().map(|&e| p[e]).sum();
4810 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
4813 } else {
4814 1.0 / m.routed_scaling
4815 };
4816 (idx, p, wsum)
4817}
4818
4819fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
4822 let ne = m.experts.len();
4823 let mut logits = vec![0.0f32; ne];
4824 m.router.matvec(x, &mut logits, pool);
4825 let (idx, p, wsum) = moe_route(&logits, m, allowed);
4826 {
4827 let mut st = m.stats.borrow_mut();
4828 if st.len() < ne {
4829 st.resize(ne, 0);
4830 }
4831 for &e in &idx {
4832 st[e] += 1;
4833 }
4834 }
4835 if crate::gpu::enabled_here() {
4840 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
4841 crate::gpu::ProbeArm::Gpu => {
4842 let t0 = std::time::Instant::now();
4843 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
4844 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4845 return out;
4846 }
4847 }
4848 crate::gpu::ProbeArm::CpuTimed => {
4849 let t0 = std::time::Instant::now();
4850 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4851 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4852 return out;
4853 }
4854 crate::gpu::ProbeArm::Cpu => {
4855 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4856 }
4857 }
4858 }
4859 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
4860}
4861
4862fn moe_ffn_cpu(
4864 m: &MoeFfn,
4865 x: &[f32],
4866 idx: &[usize],
4867 p: &[f32],
4868 wsum: f32,
4869 pool: Option<&Pool>,
4870) -> Vec<f32> {
4871 let mut out = attention::take_buf(x.len());
4872 for &e in idx {
4873 let mut eo = dense_ffn(&m.experts[e], x, pool);
4874 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
4875 for i in 0..out.len() {
4876 out[i] += w * eo[i];
4877 }
4878 attention::recycle_buf(&mut eo);
4879 }
4880 if let Some((se, gate)) = &m.shared {
4881 let mut so = dense_ffn(se, x, pool);
4882 let g = gate.as_ref().map_or(1.0, |gate| {
4883 let mut gl = [0.0f32; 1];
4884 gate.matvec(x, &mut gl, pool);
4885 1.0 / (1.0 + (-gl[0]).exp())
4886 });
4887 for i in 0..out.len() {
4888 out[i] += g * so[i];
4889 }
4890 attention::recycle_buf(&mut so);
4891 }
4892 out
4893}
4894
4895#[allow(clippy::too_many_arguments)]
4903fn mla_attention(
4904 w: &MlaWeights,
4905 normed: &[f32],
4906 cache: &mut crate::kv_cache::LayerKvCache,
4907 position: usize,
4908 inv_freq: &[f32],
4909 rope_scale: f32,
4910 eps: f64,
4911 pool: Option<&Pool>,
4912) -> Vec<f32> {
4913 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
4914 let hd = dr + dn;
4915 let mut q = vec![0.0f32; nh * hd];
4916 match (&w.q_a, &w.q_a_norm) {
4917 (Some(qa), Some(qn)) => {
4918 let mut t = vec![0.0f32; qa.rows()];
4919 qa.matvec(normed, &mut t, pool);
4920 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
4921 w.q_proj.matvec(&tn, &mut q, pool);
4922 }
4923 _ => w.q_proj.matvec(normed, &mut q, pool),
4924 }
4925 let mut ca = vec![0.0f32; lora + dr];
4926 w.kv_a.matvec(normed, &mut ca, pool);
4927 let (c_lat, k_rope) = ca.split_at_mut(lora);
4928 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
4929 let mut kvb = vec![0.0f32; nh * (dn + dv)];
4930 w.kv_b.matvec(&latn, &mut kvb, pool);
4931 if !w.nope {
4932 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
4933 }
4934 for h in 0..nh {
4935 if !w.nope {
4936 attention::rope_rotate_scaled(
4937 &mut q[h * hd..h * hd + dr],
4938 position,
4939 inv_freq,
4940 rope_scale,
4941 );
4942 }
4943 }
4944 let mut k = vec![0.0f32; nh * hd];
4945 let mut v = vec![0.0f32; nh * hd];
4946 for h in 0..nh {
4947 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
4948 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
4949 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
4950 }
4951 cache.append(&k, &v, &vec![true; nh]);
4952 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
4953 attention::recycle_buf(&mut imp);
4954 let mut ov = vec![0.0f32; nh * dv];
4955 for h in 0..nh {
4956 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
4957 }
4958 let mut out = vec![0.0f32; w.o_proj.rows()];
4959 w.o_proj.matvec(&ov, &mut out, pool);
4960 out
4961}
4962
4963fn dense_moe_ffn(
4970 dm: &DenseMoeFfn,
4971 x_normed: &[f32],
4972 h_raw: &[f32],
4973 eps: f64,
4974 norm_style: NormStyle,
4975 pool: Option<&Pool>,
4976) -> Vec<f32> {
4977 let mut d = dense_ffn(&dm.dense, x_normed, pool);
4978 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
4979 let m = &dm.moe;
4980 let ne = m.experts.len();
4981 let mut logits = vec![0.0f32; ne];
4982 if m.router_input_norm {
4983 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
4984 let inv = 1.0 / (ss + eps as f32).sqrt();
4985 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
4986 m.router.matvec(&xr, &mut logits, pool);
4987 } else {
4988 m.router.matvec(h_raw, &mut logits, pool);
4989 }
4990 let (idx, p, wsum) = moe_route(&logits, m, None);
4991 {
4992 let mut st = m.stats.borrow_mut();
4993 if st.len() < ne {
4994 st.resize(ne, 0);
4995 }
4996 for &e in &idx {
4997 st[e] += 1;
4998 }
4999 }
5000 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
5001 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
5002 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
5003 for (di, mi) in d.iter_mut().zip(&mo) {
5004 *di += mi;
5005 }
5006 d
5007}
5008
5009fn moe_ffn_gpu(
5012 m: &MoeFfn,
5013 x: &[f32],
5014 idx: &[usize],
5015 p: &[f32],
5016 wsum: f32,
5017 pool: Option<&Pool>,
5018) -> Option<Vec<f32>> {
5019 use crate::gpu::MoeJob;
5020
5021 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
5022 let mut model_ref = None;
5023 for &e in idx {
5024 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
5025 }
5026 if let Some((se, gate)) = &m.shared {
5027 let g = gate.as_ref().map_or(1.0, |gate| {
5028 let mut gl = [0.0f32; 1];
5029 gate.matvec(x, &mut gl, pool);
5030 1.0 / (1.0 + (-gl[0]).exp())
5031 });
5032 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
5033 }
5034 let model = model_ref?;
5035 let hidden = jobs[0].down.1;
5036 let mut out = vec![0.0f32; hidden];
5037 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
5038}
5039
5040fn ffn_forward(
5042 ffn: &FfnKind,
5043 x: &[f32],
5044 pool: Option<&Pool>,
5045 experts_allowed: Option<&[bool]>,
5046) -> Vec<f32> {
5047 match ffn {
5048 FfnKind::Dense(d) => dense_ffn(d, x, pool),
5049 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
5050 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
5054 }
5055}
5056
5057fn ffn_forward_pair(
5061 ffn: &FfnKind,
5062 x1: &[f32],
5063 x2: &[f32],
5064 pool: Option<&Pool>,
5065 experts_allowed: Option<&[bool]>,
5066) -> (Vec<f32>, Vec<f32>) {
5067 let d = match ffn {
5068 FfnKind::Dense(d) => d,
5069 FfnKind::Moe(m) => {
5070 return (
5071 moe_ffn(m, x1, pool, experts_allowed),
5072 moe_ffn(m, x2, pool, experts_allowed),
5073 );
5074 }
5075 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
5076 };
5077 let inter = d.gate_proj.rows();
5078 FFN_SCRATCH.with(|s| {
5079 let mut s = s.borrow_mut();
5080 let [g1, g2, u1, u2] = &mut *s;
5081 g1.resize(inter, 0.0);
5082 g2.resize(inter, 0.0);
5083 u1.resize(inter, 0.0);
5084 u2.resize(inter, 0.0);
5085 QTensor::matvec2_many(
5088 [&d.gate_proj, &d.up_proj],
5089 x1,
5090 x2,
5091 [g1.as_mut_slice(), u1.as_mut_slice()],
5092 [g2.as_mut_slice(), u2.as_mut_slice()],
5093 pool,
5094 );
5095 for i in 0..inter {
5096 g1[i] = d.act.combine(g1[i], u1[i]);
5097 g2[i] = d.act.combine(g2[i], u2[i]);
5098 }
5099 let mut o1 = attention::take_buf(d.down_proj.rows());
5100 let mut o2 = attention::take_buf(d.down_proj.rows());
5101 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
5102 (o1, o2)
5103 })
5104}
5105
5106#[cfg(test)]
5107mod tests {
5108 use super::*;
5109
5110 #[test]
5116 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
5117 let (hidden, inter) = (16usize, 40usize);
5118 let synth = |n: usize, salt: usize| -> Vec<f32> {
5119 (0..n)
5120 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
5121 .collect()
5122 };
5123 let d = DenseFfn {
5124 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
5125 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
5126 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
5127 act: Act::Silu,
5128 };
5129 let x = synth(hidden, 9);
5130 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
5132
5133 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
5134
5135 let mut g = vec![0.0f32; inter];
5137 d.gate_proj.matvec(&x, &mut g, None);
5138 let mut u = vec![0.0f32; inter];
5139 d.up_proj.matvec(&x, &mut u, None);
5140 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
5141 for i in 0..inter {
5142 g[i] = if act_set.contains(&(i as u16)) {
5143 inference::silu(g[i]) * u[i]
5144 } else {
5145 0.0
5146 };
5147 }
5148 let mut reference = vec![0.0f32; hidden];
5149 d.down_proj.matvec(&g, &mut reference, None);
5150
5151 let max_d = sparse
5152 .iter()
5153 .zip(&reference)
5154 .map(|(a, b)| (a - b).abs())
5155 .fold(0.0f32, f32::max);
5156 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
5157 }
5158
5159 fn attach_test_mtp(p: &mut Pipeline) {
5161 let (h, inter, heads, kv, hd) = (
5162 p.hidden_size,
5163 p.intermediate_size,
5164 p.num_heads,
5165 p.num_kv_heads,
5166 p.head_dim,
5167 );
5168 let synth = |n: usize, salt: usize| -> Vec<f32> {
5169 (0..n)
5170 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
5171 .collect()
5172 };
5173 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
5174 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
5175 };
5176 p.mtp = Some(MtpModule {
5177 enorm: vec![1.0; h],
5178 hnorm: vec![1.0; h],
5179 eh_proj: qt(h, 2 * h, 301),
5180 layer: LayerWeights {
5181 input_norm: vec![1.0; h],
5182 post_norm: vec![1.0; h],
5183 attn_out_norm: None,
5184 ffn_out_norm: None,
5185 layer_scale: None,
5186 ffn: FfnKind::Dense(DenseFfn {
5187 gate_proj: qt(inter, h, 315),
5188 up_proj: qt(inter, h, 316),
5189 down_proj: qt(h, inter, 317),
5190 act: Act::Silu,
5191 }),
5192 attn: AttnKind::Full {
5193 bias: None,
5194 wq: qt(heads * hd, h, 311),
5195 wk: qt(kv * hd, h, 312),
5196 wv: qt(kv * hd, h, 313),
5197 wo: qt(h, heads * hd, 314),
5198 q_norm: None,
5199 k_norm: None,
5200 output_gate: false,
5201 softplus_gate: None,
5202 },
5203 },
5204 final_norm: vec![1.0; h],
5205 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
5206 });
5207 }
5208
5209 #[test]
5210 fn speculative_equals_vanilla_greedy() {
5211 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5215 let run = |spec: bool| {
5216 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5217 p.sampler_config.temperature = 0.0;
5218 attach_test_mtp(&mut p);
5219 p.speculative = spec;
5220 let r = p.generate("abcdef", 12, None, None).unwrap();
5221 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
5222 };
5223 let (vanilla, d0, _) = run(false);
5224 let (spec, d1, a1) = run(true);
5225 assert_eq!(d0, 0, "vanilla path must not draft");
5226 assert!(d1 > 0, "speculative path must draft");
5227 assert_eq!(
5228 vanilla, spec,
5229 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
5230 );
5231 }
5232
5233 #[test]
5234 fn speculative_accepts_constant_oracle() {
5235 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5237 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5238 p.sampler_config.temperature = 0.0;
5239 p.sampler_config.repetition_penalty = 1.0;
5240 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
5243 attach_test_mtp(&mut p);
5244 p.speculative = true;
5245 let r = p.generate("abcd", 10, None, None).unwrap();
5246 assert!(r.mtp_drafted > 0);
5247 assert_eq!(
5248 r.mtp_accepted, r.mtp_drafted,
5249 "constant logits → every draft accepted"
5250 );
5251 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
5254 }
5255
5256 #[test]
5257 fn empty_prompt_is_an_error_not_a_panic() {
5258 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
5259 let r = p.generate("", 4, None, None);
5260 assert!(r.is_err(), "empty prompt must be a clean error");
5261 }
5262
5263 #[test]
5264 fn every_token_enters_kv_exactly_once() {
5265 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5266 p.sampler_config.temperature = 0.0;
5268 let r = p.generate("abc", 2, None, None).unwrap();
5269 assert_eq!(r.prompt_tokens, 3);
5270 assert_eq!(
5274 p.kv_cache.seq_len(),
5275 3 + r.tokens_generated - 1,
5276 "each token must be cached exactly once (v1 cached the last prompt token twice)"
5277 );
5278 }
5279
5280 #[test]
5281 fn generation_is_reproducible_with_seed() {
5282 let run = || {
5283 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5284 p.generate("hello", 8, None, None).unwrap().token_ids
5285 };
5286 assert_eq!(run(), run());
5287 }
5288
5289 #[test]
5290 fn resetting_sampler_restarts_the_seeded_stream() {
5291 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5292 let config = SamplerConfig {
5293 seed: Some(1234),
5294 ..SamplerConfig::default()
5295 };
5296 p.set_sampler_config(config.clone());
5297 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
5298 p.set_sampler_config(config);
5299 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
5300 assert_eq!(first, second);
5301 }
5302
5303 #[test]
5304 fn eviction_bounds_the_cache() {
5305 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
5306 p.kv_cache.max_seq_len = 6;
5307 p.sampler_config.temperature = 0.0;
5308 let _ = p.generate("abcd", 12, None, None).unwrap();
5309 assert!(
5310 p.kv_cache.seq_len() <= 6 + 1,
5311 "cache must stay bounded by max_seq_len (got {})",
5312 p.kv_cache.seq_len()
5313 );
5314 }
5315
5316 #[test]
5317 fn confidence_matches_tokens_and_is_a_probability() {
5318 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5319 p.sampler_config.temperature = 0.0;
5320 p.sampler_config.repetition_penalty = 1.0;
5321 let r = p.generate("abcd", 10, None, None).unwrap();
5322 assert_eq!(
5323 r.token_confidence.len(),
5324 r.token_ids.len(),
5325 "one confidence per emitted token"
5326 );
5327 for &c in &r.token_confidence {
5328 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
5329 }
5330 let logits = [1.0f32, 3.0, 0.5, 3.0];
5332 let p0 = top1_prob_t(&logits, 1, 1.0);
5333 let p1 = top1_prob_t(&logits, 3, 1.0);
5334 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
5335 assert!(p0 > 0.0 && p0 < 1.0);
5336 let sharp = top1_prob_t(&logits, 1, 1.0);
5338 let soft = top1_prob_t(&logits, 1, 2.0);
5339 assert!(soft < sharp, "higher temperature lowers peak confidence");
5340 }
5341
5342 #[test]
5343 fn trace_is_opt_in_and_parallels_the_output() {
5344 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5346 p.sampler_config.temperature = 0.0;
5347 p.sampler_config.repetition_penalty = 1.0;
5348 let r = p.generate("abcd", 10, None, None).unwrap();
5349 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
5350
5351 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5353 p.sampler_config.temperature = 0.0;
5354 p.sampler_config.repetition_penalty = 1.0;
5355 p.set_trace(true);
5356 let r = p.generate("abcd", 10, None, None).unwrap();
5357 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
5358 for (i, tr) in r.traces.iter().enumerate() {
5359 assert_eq!(tr.t, i, "trace index is sequential");
5360 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
5361 assert_eq!(
5362 tr.confidence, r.token_confidence[i],
5363 "trace confidence matches the confidence channel"
5364 );
5365 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
5367 }
5368 }
5369
5370 #[test]
5371 fn explain_prefill_logits_match_greedy_first_token() {
5372 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5376 p.sampler_config.temperature = 0.0;
5377 p.sampler_config.repetition_penalty = 1.0;
5378 let ids = p.tokenizer.encode("abcd");
5379 let logits = p.prefill_next_logits(&ids, None);
5380 let argmax = logits
5381 .iter()
5382 .enumerate()
5383 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5384 .unwrap()
5385 .0 as u32;
5386 let r = p.generate("abcd", 1, None, None).unwrap();
5387 assert_eq!(
5388 argmax, r.token_ids[0],
5389 "explain preview must match greedy emit"
5390 );
5391 }
5392
5393 #[test]
5394 fn laguna_shared_expert_is_unconditionally_added() {
5395 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
5396 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
5397 let zero_dense = || DenseFfn {
5398 gate_proj: matrix(vec![0.0; 4]),
5399 up_proj: matrix(vec![0.0; 4]),
5400 down_proj: matrix(vec![0.0; 4]),
5401 act: Act::Silu,
5402 };
5403 let shared = DenseFfn {
5404 gate_proj: identity(),
5405 up_proj: identity(),
5406 down_proj: identity(),
5407 act: Act::Silu,
5408 };
5409 let x = [1.0, 2.0];
5410 let expected = dense_ffn(&shared, &x, None);
5411 let moe = MoeFfn {
5412 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
5413 experts: vec![zero_dense()],
5414 top_k: 1,
5415 norm_topk_prob: true,
5416 router_sigmoid: true,
5417 expert_bias: None,
5418 routed_scaling: 1.0,
5419 route_tau: None,
5420 shared: Some((shared, None)),
5421 stats: std::cell::RefCell::new(Vec::new()),
5422 mask: None,
5423 per_expert_scale: None,
5424 router_input_norm: false,
5425 };
5426 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
5427 for (actual, expected) in actual.iter().zip(expected) {
5428 assert!((actual - expected).abs() < 1e-6);
5429 }
5430 }
5431}