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 short_conv_cfg: Option<ShortConvCfg>,
82 pub mtp: Option<MtpModule>,
84 pub speculative: bool,
86 rng: SplitMix64,
87 sampler_scratch: SamplerScratch,
88 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
92 ws: ForwardScratch,
96 pool: Option<std::sync::Arc<Pool>>,
98 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
102 pub(crate) dyn_force_f32: bool,
104 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
109 pub(crate) dyn_active: Option<usize>,
115 pub(crate) dyn_blend_loaded: bool,
119 pub(crate) dyn_phi_layer: Option<usize>,
122 dyn_phi_ema: Vec<f32>,
124 dyn_phi_seen: usize,
125 pub dyn_router: Option<crate::swarm::DynRouter>,
128 o1_cfg: Option<crate::nystrom::O1Cfg>,
131 o1_flags: Vec<bool>,
133 trace: bool,
136 calib_temp: f32,
139 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
141 graph_kv_id: u64,
142 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
145 graph_want_logits: bool,
146 graph_logits: Option<Vec<f32>>,
149 pub embed_multiplier: f32,
151 pub attn_scale: f32,
154 pub swa: Option<(usize, usize)>,
157 pub sliding_layers: Option<Vec<bool>>,
160 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
163 pub rotary_dim_local: Option<usize>,
164 pub rope_scale: f32,
165 pub rope_scale_local: f32,
166 pub global_attn: Option<(usize, usize)>,
169 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
172 pub attn_v_norm: bool,
174 pub final_softcap: Option<f32>,
176 confidence_on: bool,
180}
181
182#[cfg(target_os = "macos")]
183impl Drop for Pipeline {
184 fn drop(&mut self) {
185 crate::gpu::kv_mirror_drop(self.graph_kv_id);
186 }
187}
188
189pub struct PipelineWeights {
194 pub embed_tokens: QTensor,
196 pub layers: Vec<LayerWeights>,
198 pub lm_head: QTensor,
200 pub final_norm: Vec<f32>,
202}
203
204pub struct LayerWeights {
206 pub input_norm: Vec<f32>,
207 pub post_norm: Vec<f32>,
210 pub attn_out_norm: Option<Vec<f32>>,
213 pub layer_scale: Option<f32>,
215 pub ffn_out_norm: Option<Vec<f32>>,
218 pub ffn: FfnKind,
219 pub attn: AttnKind,
220}
221
222#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
225pub enum Act {
226 #[default]
227 Silu,
228 GeluTanh,
229}
230
231impl Act {
232 pub fn from_arch(name: &str) -> Self {
233 if name == "gelu_tanh" {
234 Self::GeluTanh
235 } else {
236 Self::Silu
237 }
238 }
239
240 #[inline]
241 pub fn apply(self, x: f32) -> f32 {
242 match self {
243 Self::Silu => inference::silu(x),
244 Self::GeluTanh => inference::gelu_tanh(x),
245 }
246 }
247}
248
249pub struct DenseFfn {
251 pub gate_proj: QTensor,
252 pub up_proj: QTensor,
253 pub down_proj: QTensor,
254 pub act: Act,
256}
257
258pub enum FfnKind {
261 Dense(DenseFfn),
262 Moe(MoeFfn),
266}
267
268pub struct MoeFfn {
269 pub router: QTensor,
271 pub experts: Vec<DenseFfn>,
272 pub top_k: usize,
273 pub norm_topk_prob: bool,
274 pub router_sigmoid: bool,
277 pub expert_bias: Option<Vec<f32>>,
281 pub routed_scaling: f32,
284 pub shared: Option<(DenseFfn, Option<QTensor>)>,
287 pub stats: std::cell::RefCell<Vec<u64>>,
291}
292
293pub enum AttnKind {
296 Full {
298 wq: QTensor,
299 wk: QTensor,
300 wv: QTensor,
301 wo: QTensor,
302 q_norm: Option<Vec<f32>>,
303 k_norm: Option<Vec<f32>>,
304 output_gate: bool,
305 softplus_gate: Option<(QTensor, bool)>,
309 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
311 },
312 Linear(VmfPhaseWeights),
314 LinearGdn(GdnWeights),
316 ShortConv(ShortConvWeights),
319}
320
321pub struct MtpModule {
326 pub enorm: Vec<f32>,
327 pub hnorm: Vec<f32>,
328 pub eh_proj: QTensor,
330 pub layer: LayerWeights,
331 pub final_norm: Vec<f32>,
332 pub kv: crate::kv_cache::LayerKvCache,
333}
334
335pub struct GenerateResult {
337 pub text: String,
338 pub token_ids: Vec<u32>,
339 pub prompt_tokens: usize,
340 pub tokens_generated: usize,
341 pub finish_reason: String,
342 pub mtp_drafted: usize,
344 pub mtp_accepted: usize,
345 pub token_confidence: Vec<f32>,
350 pub traces: Vec<TokenTrace>,
353}
354
355#[derive(Clone, Debug)]
360pub struct TokenTrace {
361 pub t: usize,
363 pub token_id: u32,
365 pub confidence: f32,
367 pub active_skill: Option<String>,
369 pub recon: Option<f32>,
373 pub switched: bool,
376}
377
378fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
383 let t = if temp > 1e-3 { temp } else { 1.0 };
384 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
385 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
386 if sum > 0.0 {
387 (((logits[id as usize] - max) / t).exp()) / sum
388 } else {
389 0.0
390 }
391}
392
393fn prefill_batched() -> bool {
396 std::env::var("CMF_PREFILL")
397 .map(|v| v != "seq")
398 .unwrap_or(true)
399}
400
401fn prefill_chunk() -> usize {
406 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
407 .ok()
408 .and_then(|v| v.parse::<usize>().ok())
409 {
410 return n.max(1);
411 }
412 if cfg!(target_os = "macos") {
413 512
414 } else if cfg!(target_arch = "aarch64") {
415 256
418 } else {
419 48
420 }
421}
422
423pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
425
426impl Pipeline {
427 #[inline]
431 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
432 virtual_idx % self.physical_layers
433 }
434
435 #[inline]
438 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
439 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
440 }
441
442 #[allow(clippy::too_many_arguments)]
444
445 #[cfg(target_os = "macos")]
460 fn graph_prefill_preferred(&self) -> bool {
461 if !crate::gpu::enabled_here()
462 || !crate::gpu::q1_force()
463 || std::env::var("CMF_GPU_BLOCK")
464 .map(|v| v == "0")
465 .unwrap_or(false)
466 {
467 return false;
468 }
469 self.weights
470 .layers
471 .iter()
472 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
473 }
474
475 #[cfg(not(target_os = "macos"))]
476 fn graph_prefill_preferred(&self) -> bool {
477 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
485 .map(|v| v != "0")
486 .unwrap_or_else(|_| {
487 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
488 });
489 if !graph_on || !crate::gpu::enabled_here() {
490 return false;
491 }
492 self.weights
493 .layers
494 .iter()
495 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
496 }
497
498 #[cfg(target_os = "macos")]
499 fn q1_graph_gpu(
500 &mut self,
501 start: usize,
502 upto: Option<usize>,
503 position: usize,
504 h: &mut [f32],
505 ) -> usize {
506 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
507 if !crate::gpu::enabled_here()
508 || !crate::gpu::q1_force()
509 || std::env::var("CMF_GPU_BLOCK")
510 .map(|v| v == "0")
511 .unwrap_or(false)
512 {
513 return start;
514 }
515 if self.swa.is_some()
521 || self.global_attn.is_some()
522 || self.attention_heads_per_layer.is_some()
523 || self.attn_v_norm
524 || self.loop_final_norm
525 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
526 || self.weights.layers.iter().any(|lw| {
527 lw.attn_out_norm.is_some()
528 || lw.ffn_out_norm.is_some()
529 || lw.layer_scale.is_some()
530 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
531 })
532 {
533 return start;
534 }
535 let limit = upto
536 .map(|u| u + 1)
537 .unwrap_or(self.num_layers)
538 .min(self.num_layers);
539
540 enum Item<'a> {
541 Gdn {
542 run: Vec<GdnGpuLayer<'a>>,
543 first: usize,
544 },
545 Attn {
546 l: AttnGpuLayer<'a>,
547 li: usize,
548 q_norm: Option<&'a [f32]>,
549 k_norm: Option<&'a [f32]>,
550 output_gate: bool,
551 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
552 full_gpu: bool,
555 },
556 }
557
558 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
560 let dev_attend = attend_mode != "0"
561 && attend_mode != "off"
562 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
566 && self.head_dim % 4 == 0
567 && self.head_dim <= 256
568 && self.rotary_dim >= 2
569 && self.rotary_dim <= self.head_dim
570 && (self.rotary_dim / 2) % 32 == 0
571 && self.num_kv_heads > 0
572 && self.num_heads % self.num_kv_heads == 0;
573
574 let mut plan: Vec<Item> = Vec::new();
575 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
576 let mut scan = start;
577 while scan < limit {
578 let lw = &self.weights.layers[self.phys_layer(scan)];
579 let FfnKind::Dense(d) = &lw.ffn else { break };
580 let (Some(g), Some(u), Some(dn)) = (
581 d.gate_proj.q1_parts(),
582 d.up_proj.q1_parts(),
583 d.down_proj.q1_parts(),
584 ) else {
585 break;
586 };
587 match &lw.attn {
588 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
589 let parts = (
590 w.in_proj_qkv.q1_parts(),
591 w.in_proj_z.q1_parts(),
592 w.in_proj_a.f32_parts(),
593 w.in_proj_b.f32_parts(),
594 w.out_proj.q1_parts(),
595 );
596 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
597 break;
598 };
599 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
600 model_ref.get_or_insert_with(|| model.clone());
601 }
602 let gl = GdnGpuLayer {
603 attn_norm: &lw.input_norm,
604 post_norm: &lw.post_norm,
605 qkv,
606 z,
607 a,
608 b,
609 out,
610 gate: g,
611 up: u,
612 down: dn,
613 conv1d: &w.conv1d,
614 a_log: &w.a_log,
615 dt_bias: &w.dt_bias,
616 gnorm: &w.norm,
617 };
618 match plan.last_mut() {
619 Some(Item::Gdn { run, .. }) => run.push(gl),
620 _ => plan.push(Item::Gdn {
621 run: vec![gl],
622 first: scan,
623 }),
624 }
625 }
626 AttnKind::Full {
627 wq,
628 wk,
629 wv,
630 wo,
631 q_norm,
632 k_norm,
633 output_gate,
634 softplus_gate: None,
635 bias,
636 } if !self.kv_cache.layers[scan].o1_sealed() => {
637 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
638 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
639 break;
640 };
641 if let QTensor::Mapped { model, .. } = wq {
642 model_ref.get_or_insert_with(|| model.clone());
643 }
644 let cache = &self.kv_cache.layers[scan];
645 let full_gpu = dev_attend
646 && cache.mode == crate::kv_cache::KvMode::F32
647 && cache.o1.is_none()
648 && bias.is_none()
649 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
650 && pk.1 == self.num_kv_heads * self.head_dim
651 && pv.1 == self.num_kv_heads * self.head_dim
652 && po.2 == self.num_heads * self.head_dim;
653 plan.push(Item::Attn {
654 l: AttnGpuLayer {
655 attn_norm: &lw.input_norm,
656 post_norm: &lw.post_norm,
657 wq: pq,
658 wk: pk,
659 wv: pv,
660 wo: po,
661 gate: g,
662 up: u,
663 down: dn,
664 },
665 li: scan,
666 q_norm: q_norm.as_deref(),
667 k_norm: k_norm.as_deref(),
668 output_gate: *output_gate,
669 bias: bias
670 .as_ref()
671 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
672 full_gpu,
673 });
674 }
675 _ => break,
676 }
677 scan += 1;
678 }
679 let Some(model) = model_ref else { return start };
680 if plan.is_empty() {
681 return start;
682 }
683 let dims = GraphDims {
684 hidden: self.hidden_size,
685 eps: self.rms_eps as f32,
686 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
687 };
688 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
689 return start;
690 };
691 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
692 nv: cfg.num_v_heads,
693 nk: cfg.num_k_heads,
694 dk: cfg.key_head_dim,
695 dv: cfg.value_head_dim,
696 kk: cfg.conv_kernel,
697 hidden: self.hidden_size,
698 inter: self.intermediate_size,
699 c_dim: cfg.conv_dim(),
700 eps: cfg.rms_eps as f32,
701 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
702 });
703 let mut valid = 0usize;
707 let mut end = start;
708 for item in &plan {
709 let ok = match item {
710 Item::Gdn { run, .. } => gcfg
711 .as_ref()
712 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
713 .unwrap_or(false),
714 Item::Attn { l, .. } => graph.attn_ok(l),
715 };
716 if !ok {
717 break;
718 }
719 valid += 1;
720 end += match item {
721 Item::Gdn { run, .. } => run.len(),
722 Item::Attn { .. } => 1,
723 };
724 }
725 plan.truncate(valid);
726 if plan.is_empty() {
727 return start;
728 }
729
730 let inv_freq = self.inv_freq.clone();
731 let pool = self.pool.clone();
732 let (nh, nkv, hd, hs, rd, eps) = (
733 self.num_heads,
734 self.num_kv_heads,
735 self.head_dim,
736 self.hidden_size,
737 self.rotary_dim,
738 self.rms_eps,
739 );
740 let norm_style = self.norm_style;
741 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
742 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
743 let kv_id = self.graph_kv_id;
744 let mut pending: Vec<(usize, usize)> = Vec::new();
747 let mut dev_attn: Vec<usize> = Vec::new();
750 for item in &plan {
751 match item {
752 Item::Gdn { run, first } => {
753 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
754 if l.linear_state.len() != want {
755 l.linear_state = vec![0f32; want];
756 }
757 }
758 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
759 .iter()
760 .map(|l| l.linear_state.as_slice())
761 .collect();
762 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
763 tracing::error!("q1 graph: GDN run refused after validation");
765 return start;
766 }
767 graph.commit();
770 pending.push((*first, run.len()));
771 }
772 Item::Attn {
773 l,
774 li,
775 q_norm,
776 k_norm,
777 output_gate,
778 bias,
779 full_gpu,
780 } => {
781 if *full_gpu {
783 let cache = &self.kv_cache.layers[*li];
784 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
785 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
786 let cpu_stored = cpu_k[0].len() / hd;
787 let p = crate::gpu::AttnDeviceParams {
788 kv_id,
789 layer: *li,
790 nh,
791 nkv,
792 hd,
793 rd,
794 position,
795 eps: eps as f32,
796 gemma,
797 output_gate: *output_gate,
798 q_norm: *q_norm,
799 k_norm: *k_norm,
800 inv_freq: &inv_freq,
801 cpu_k,
802 cpu_v,
803 cpu_stored,
804 };
805 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
806 graph.commit();
807 dev_attn.push(*li);
808 continue;
809 }
810 }
812 graph.encode_attn_prefix(l);
813 graph.sync();
814 if !pending.is_empty() {
815 let idxs: Vec<usize> =
816 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
817 let mut outs: Vec<&mut [f32]> = self
818 .kv_cache
819 .layers
820 .iter_mut()
821 .enumerate()
822 .filter(|(i, _)| idxs.binary_search(i).is_ok())
823 .map(|(_, s)| s.linear_state.as_mut_slice())
824 .collect();
825 graph.read_states(&mut outs);
826 }
827 let mut q_raw = attention::take_buf(l.wq.1);
828 let mut k = attention::take_buf(l.wk.1);
829 let mut v = attention::take_buf(l.wv.1);
830 graph.read_qkv(&mut q_raw, &mut k, &mut v);
831 let cfg = QwenAttnCfg {
832 num_heads: nh,
833 num_kv_heads: nkv,
834 head_dim: hd,
835 hidden_size: hs,
836 position,
837 inv_freq: &inv_freq,
838 rotary_dim: rd,
839 scale: self.attn_scale,
840 window: None,
841 v_norm: false,
842 q_norm: *q_norm,
843 k_norm: *k_norm,
844 output_gate: *output_gate,
845 softplus_gate: None,
846 rope_scale: 1.0,
847 bias: *bias,
848 rms_eps: eps,
849 norm_style,
850 pool: pool.as_deref(),
851 };
852 let mut ao = attention::qwen_attention_core(
853 q_raw,
854 k,
855 v,
856 &mut self.kv_cache.layers[*li],
857 &cfg,
858 );
859 graph.encode_attn_suffix(l, &ao);
860 graph.commit();
863 attention::recycle_buf(&mut ao);
864 }
865 }
866 }
867 let mut lm_rows = None;
872 if self.graph_want_logits
873 && upto.is_none()
874 && end == self.num_layers
875 && std::env::var("CMF_GPU_LMHEAD")
876 .map(|v| v != "0")
877 .unwrap_or(true)
878 {
879 if let Some(lm) = self.weights.lm_head.q1_parts() {
880 if graph.lm_head_ok(lm) {
881 graph.encode_lm_head(&self.weights.final_norm, lm);
882 lm_rows = Some(lm.1);
883 }
884 }
885 }
886 graph.sync();
887 if !pending.is_empty() {
888 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
889 let mut outs: Vec<&mut [f32]> = self
890 .kv_cache
891 .layers
892 .iter_mut()
893 .enumerate()
894 .filter(|(i, _)| idxs.binary_search(i).is_ok())
895 .map(|(_, s)| s.linear_state.as_mut_slice())
896 .collect();
897 graph.read_states(&mut outs);
898 }
899 if let Some(rows) = lm_rows {
900 let mut lg = attention::take_buf(rows.min(self.vocab_size));
901 graph.read_logits(&mut lg);
902 lg.resize(self.vocab_size, 0.0);
903 if let Some(c) = self.final_softcap {
904 for l in lg.iter_mut() {
905 *l = c * (*l / c).tanh();
906 }
907 }
908 self.graph_logits = Some(lg);
909 }
910 graph.finish(h);
911 for li in dev_attn {
915 let mut krow = attention::take_buf(nkv * hd);
916 let mut vrow = attention::take_buf(nkv * hd);
917 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
918 let cache = &mut self.kv_cache.layers[li];
919 cache.append(&krow, &vrow, &[]);
920 let n = cache.seq_len;
921 let mut imp = attention::take_buf(n);
922 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
923 cache.accumulate_imp(&imp);
924 attention::recycle_buf(&mut imp);
925 }
926 attention::recycle_buf(&mut krow);
927 attention::recycle_buf(&mut vrow);
928 }
929 end
930 }
931
932 pub fn new(
933 tokenizer: Tokenizer,
934 weights: PipelineWeights,
935 hidden_size: usize,
936 intermediate_size: usize,
937 num_heads: usize,
938 num_kv_heads: usize,
939 head_dim: usize,
940 num_layers: usize,
941 physical_layers: usize,
942 loop_final_norm: bool,
943 vocab_size: usize,
944 rms_eps: f64,
945 rope_base: f32,
946 norm_style: NormStyle,
947 max_seq_len: usize,
948 sampler_config: SamplerConfig,
949 ) -> Self {
950 let rng = match sampler_config.seed {
951 Some(s) => SplitMix64::new(s),
952 None => SplitMix64::from_entropy(),
953 };
954 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
955 let pool = Pool::from_env();
956 if let Some(p) = &pool {
957 tracing::info!("worker pool: {} threads", p.n_workers());
958 }
959 Self {
960 tokenizer: std::sync::Arc::new(tokenizer),
961 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
962 sampler_config,
963 weights,
964 hidden_size,
965 intermediate_size,
966 num_heads,
967 num_kv_heads,
968 head_dim,
969 num_layers,
970 physical_layers,
971 loop_final_norm,
972 vocab_size,
973 rms_eps,
974 rope_base,
975 norm_style,
976 rotary_dim: head_dim,
977 attention_heads_per_layer: None,
978 vmf_cfg: None,
979 gdn_cfg: None,
980 short_conv_cfg: None,
981 mtp: None,
982 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
983 rng,
984 sampler_scratch: SamplerScratch::default(),
985 inv_freq,
986 ws: ForwardScratch::new(hidden_size),
987 pool,
988 model: None,
989 dyn_force_f32: false,
990 dyn_skill_layers: Vec::new(),
991 dyn_active: None,
992 dyn_blend_loaded: false,
993 dyn_phi_layer: None,
994 dyn_phi_ema: Vec::new(),
995 dyn_phi_seen: 0,
996 dyn_router: None,
997 o1_cfg: None,
998 o1_flags: Vec::new(),
999 trace: false,
1000 calib_temp: 1.0,
1001 confidence_on: true,
1002 embed_multiplier: 1.0,
1003 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1004 swa: None,
1005 sliding_layers: None,
1006 inv_freq_local: None,
1007 rotary_dim_local: None,
1008 rope_scale: 1.0,
1009 rope_scale_local: 1.0,
1010 global_attn: None,
1011 inv_freq_global: None,
1012 attn_v_norm: false,
1013 final_softcap: None,
1014 graph_want_logits: false,
1015 graph_logits: None,
1016 graph_kv_id: {
1017 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1018 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1019 },
1020 }
1021 }
1022
1023 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1030 self.o1_flags = match &cfg {
1031 Some(c) => {
1032 let mut flags = c.layer_flags(self.num_layers);
1033 for (li, f) in flags.iter_mut().enumerate() {
1034 if *f && !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. }) {
1035 *f = false;
1036 }
1037 }
1038 flags
1039 }
1040 None => Vec::new(),
1041 };
1042 if let Some(c) = &cfg {
1043 let n = self.o1_flags.iter().filter(|&&f| f).count();
1044 tracing::info!(
1045 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1046 self.num_layers,
1047 c.m,
1048 c.w,
1049 c.sink,
1050 c.rect
1051 );
1052 }
1053 self.o1_cfg = cfg;
1054 }
1055
1056 pub fn o1_active(&self) -> bool {
1058 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1059 }
1060
1061 fn o1_begin(&mut self) {
1063 if let Some(c) = &self.o1_cfg {
1064 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1065 for (li, &f) in self.o1_flags.iter().enumerate() {
1066 if f {
1067 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1068 }
1069 }
1070 }
1071 }
1072
1073 fn o1_seal(&mut self) {
1076 if self.o1_cfg.is_none() {
1077 return;
1078 }
1079 for li in 0..self.num_layers {
1080 if self.o1_flags.get(li).copied().unwrap_or(false) {
1081 self.kv_cache.layers[li].o1_seal(self.num_heads);
1082 }
1083 }
1084 }
1085
1086 pub fn set_trace(&mut self, on: bool) {
1088 self.trace = on;
1089 }
1090
1091 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1094 self.rng = match config.seed {
1095 Some(seed) => SplitMix64::new(seed),
1096 None => SplitMix64::from_entropy(),
1097 };
1098 self.sampler_config = config;
1099 }
1100
1101 pub fn set_confidence(&mut self, on: bool) {
1106 self.confidence_on = on;
1107 }
1108
1109 pub fn set_calib_temp(&mut self, t: f32) {
1112 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1113 }
1114
1115 pub fn calib_temp(&self) -> f32 {
1117 self.calib_temp
1118 }
1119
1120 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1123 self.rotary_dim = rotary_dim.min(self.head_dim);
1124 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1125 }
1126
1127 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1128 QwenAttnCfg {
1129 num_heads: self.num_heads,
1130 num_kv_heads: self.num_kv_heads,
1131 head_dim: self.head_dim,
1132 hidden_size: self.hidden_size,
1133 position,
1134 inv_freq: &self.inv_freq,
1135 rotary_dim: self.rotary_dim,
1136 scale: self.attn_scale,
1137 window: None,
1138 v_norm: false,
1139 q_norm: None,
1140 k_norm: None,
1141 output_gate: false,
1142 softplus_gate: None,
1143 rope_scale: self.rope_scale,
1144 bias: None,
1145 rms_eps: self.rms_eps,
1146 norm_style: self.norm_style,
1147 pool: self.pool.as_deref(),
1148 }
1149 }
1150
1151 pub fn generate(
1153 &mut self,
1154 prompt: &str,
1155 max_tokens: usize,
1156 task_mask: Option<&TaskMask>,
1157 on_token: Option<TokenCallback>,
1158 ) -> Result<GenerateResult, String> {
1159 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1160 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1161 }
1162
1163 pub fn generate_from_ids(
1171 &mut self,
1172 input_ids: &[u32],
1173 max_tokens: usize,
1174 task_mask: Option<&TaskMask>,
1175 mut on_token: Option<TokenCallback>,
1176 ) -> Result<GenerateResult, String> {
1177 if std::env::var("CMF_TRACE_H").is_ok() {
1178 eprintln!("input_ids: {input_ids:?}");
1179 }
1180 if input_ids.is_empty() {
1181 return Err("empty prompt: nothing to generate from".to_string());
1182 }
1183
1184 self.kv_cache.clear();
1186 crate::gpu::graph_kv_reset(self.graph_kv_id);
1187 self.o1_begin();
1188
1189 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1195 .map(|v| v != "0")
1196 .unwrap_or_else(|_| {
1197 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
1198 });
1199 let spec_active = self.speculative
1200 && self.mtp.is_some()
1201 && task_mask.is_none()
1202 && !self.o1_active()
1203 && !graph_on
1204 && self.sampler_config.temperature < 1e-6;
1205 let mut mtp = if spec_active { self.mtp.take() } else { None };
1208 if let Some(m) = &mut mtp {
1209 m.kv.clear();
1210 }
1211 let mut router = if mtp.is_none() {
1215 self.dyn_router.take()
1216 } else {
1217 None
1218 };
1219 if let Some(r) = &mut router {
1220 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1223 }
1224
1225 let mut all_ids = input_ids.to_vec();
1226 let mut generated = 0usize;
1227 let mut finish_reason = "max_tokens".to_string();
1228 let mut drafted = 0usize;
1229 let mut accepted = 0usize;
1230 let mut confidence: Vec<f32> = Vec::new();
1231 let trace_on = self.trace;
1232 let calib_temp = self.calib_temp;
1233 let mut traces: Vec<TokenTrace> = Vec::new();
1234
1235 let mut hidden = vec![0.0f32; self.hidden_size];
1241 let mut pos = 0usize;
1242 let fuse_lm = mtp.is_none() && router.is_none();
1246 self.graph_logits = None;
1247 self.graph_want_logits = false;
1248 let dyn_prefill = router.is_some();
1253 let graph_prefill = self.graph_prefill_preferred();
1259 if task_mask.is_none()
1260 && !dyn_prefill
1261 && !graph_prefill
1262 && prefill_batched()
1263 && input_ids.len() > 2
1264 {
1265 let chunk = prefill_chunk();
1271 let hs = self.hidden_size;
1272 while pos < input_ids.len() {
1273 let end = (pos + chunk).min(input_ids.len());
1274 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1275 if let Some(m) = &mut mtp {
1276 for p in pos..end {
1277 if p + 1 < input_ids.len() {
1278 let _ = self.mtp_step(
1279 m,
1280 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1281 input_ids[p + 1],
1282 p,
1283 );
1284 }
1285 }
1286 }
1287 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1288 pos = end;
1289 }
1290 }
1291 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1292 while pos + 1 < input_ids.len() {
1293 let e1 = self.embed_single(input_ids[pos]);
1294 let e2 = self.embed_single(input_ids[pos + 1]);
1295 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1296 self.commit_linear_scratch();
1298 if let Some(m) = &mut mtp {
1299 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1300 if pos + 2 < input_ids.len() {
1301 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1302 }
1303 }
1304 hidden = h2;
1305 pos += 2;
1306 }
1307 }
1308 let _tpf = std::time::Instant::now();
1317 let batch_k = std::env::var("CMF_BATCH_K")
1318 .ok()
1319 .and_then(|v| v.parse::<usize>().ok())
1320 .unwrap_or(0);
1321 if batch_k > 0
1322 && graph_prefill
1323 && task_mask.is_none()
1324 && mtp.is_none()
1325 && !dyn_prefill
1326 && pos + 1 < input_ids.len()
1327 {
1328 let hs = self.hidden_size;
1329 let chunk = batch_k;
1330 while pos < input_ids.len() {
1331 let end = (pos + chunk).min(input_ids.len());
1332 let bk = end - pos;
1333 let mut hiddens = vec![0f32; bk * hs];
1334 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1335 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1336 }
1337 let positions: Vec<usize> = (pos..end).collect();
1338 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1339 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1340 pos = end;
1341 } else {
1342 break; }
1344 }
1345 }
1346 while pos < input_ids.len() {
1347 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1348 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1349 if let Some(m) = &mut mtp {
1350 if pos + 1 < input_ids.len() {
1351 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1352 }
1353 }
1354 pos += 1;
1355 }
1356 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1357 eprintln!(
1358 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1359 input_ids.len(),
1360 _tpf.elapsed().as_secs_f64() * 1000.0
1361 );
1362 }
1363 self.o1_seal();
1366
1367 macro_rules! commit {
1369 ($id:expr) => {{
1370 all_ids.push($id);
1371 generated += 1;
1372 if self.tokenizer.is_eos($id) {
1373 finish_reason = "stop".to_string();
1374 false
1375 } else {
1376 let token_text = self.tokenizer.decode_token($id);
1377 let mut go = true;
1378 if let Some(ref mut cb) = on_token {
1379 if !cb(&token_text) {
1380 finish_reason = "cancelled".to_string();
1381 go = false;
1382 }
1383 }
1384 go
1385 }
1386 }};
1387 }
1388
1389 let mut next_pos = input_ids.len();
1391 'decode: while generated < max_tokens {
1392 let mut logits = match self.graph_logits.take() {
1393 Some(lg) => lg,
1394 None => {
1395 inference::rms_norm_into(
1396 &hidden,
1397 &self.weights.final_norm,
1398 self.rms_eps,
1399 self.norm_style,
1400 &mut self.ws.n1,
1401 );
1402 self.lm_head_forward(&self.ws.n1)
1403 }
1404 };
1405 let t_next = sampler::sample_with_scratch(
1406 &logits,
1407 &self.sampler_config,
1408 &all_ids,
1409 &mut self.rng,
1410 &mut self.sampler_scratch,
1411 );
1412 if self.confidence_on {
1413 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1414 }
1415 attention::recycle_buf(&mut logits);
1416 if trace_on {
1417 let skill = router.as_ref().and_then(|r| r.active_id());
1421 traces.push(TokenTrace {
1422 t: generated,
1423 token_id: t_next,
1424 confidence: confidence.last().copied().unwrap_or(0.0),
1425 active_skill: skill,
1426 recon: None,
1427 switched: false,
1428 });
1429 }
1430 if !commit!(t_next) {
1431 break 'decode;
1432 }
1433 if generated >= max_tokens {
1434 break 'decode;
1435 }
1436
1437 if self.kv_cache.needs_eviction() {
1438 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1439 self.kv_cache.evict(keep);
1440 }
1441
1442 match &mut mtp {
1443 Some(m) if generated + 1 < max_tokens => {
1445 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1446 drafted += 1;
1447 let emb1 = self.embed_single(t_next);
1448 let emb2 = self.embed_single(draft);
1449 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1450
1451 inference::rms_norm_into(
1452 &h1,
1453 &self.weights.final_norm,
1454 self.rms_eps,
1455 self.norm_style,
1456 &mut self.ws.n1,
1457 );
1458 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1459 let t_after = sampler::sample_with_scratch(
1460 &logits1,
1461 &self.sampler_config,
1462 &all_ids,
1463 &mut self.rng,
1464 &mut self.sampler_scratch,
1465 );
1466 if self.confidence_on {
1467 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1468 }
1469 attention::recycle_buf(&mut logits1);
1470 if trace_on {
1471 traces.push(TokenTrace {
1474 t: generated,
1475 token_id: t_after,
1476 confidence: confidence.last().copied().unwrap_or(0.0),
1477 active_skill: None,
1478 recon: None,
1479 switched: false,
1480 });
1481 }
1482 let stop = !commit!(t_after);
1483
1484 if t_after == draft {
1485 accepted += 1;
1486 self.commit_linear_scratch();
1487 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1488 hidden = h2;
1489 next_pos += 2;
1490 } else {
1491 for layer in &mut self.kv_cache.layers {
1493 layer.truncate_last(1);
1494 }
1495 if !stop {
1496 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1497 hidden = self.forward_layers(
1498 &self.embed_single(t_after),
1499 next_pos + 1,
1500 None,
1501 );
1502 }
1503 next_pos += 2;
1504 }
1505 if stop {
1506 break 'decode;
1507 }
1508 }
1509 _ => {
1511 self.graph_want_logits = fuse_lm;
1512 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1513 next_pos += 1;
1514 if let Some(r) = &mut router {
1517 let phi = self.dyn_phi_ema.clone();
1518 let decision = r.step(&phi, generated);
1519 if let Some(new_active) = decision {
1520 let _ = self.set_active_skill(new_active);
1521 }
1522 if trace_on {
1525 if let Some(last) = traces.last_mut() {
1526 let e = r.last_best_e();
1527 last.recon = e.is_finite().then_some(e);
1528 last.switched = decision.is_some();
1529 }
1530 }
1531 }
1532 }
1533 }
1534 }
1535
1536 self.graph_want_logits = false;
1537 self.graph_logits = None;
1538 if router.is_some() {
1540 let _ = self.set_active_skill(None);
1541 }
1542 self.dyn_router = router.or(self.dyn_router.take());
1543 self.mtp = mtp.or(self.mtp.take());
1544
1545 let output_ids = &all_ids[input_ids.len()..];
1546 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1548 Ok(GenerateResult {
1549 text: self.tokenizer.decode(output_ids),
1550 token_ids: output_ids.to_vec(),
1551 prompt_tokens: input_ids.len(),
1552 tokens_generated: generated,
1553 finish_reason,
1554 mtp_drafted: drafted,
1555 mtp_accepted: accepted,
1556 token_confidence: confidence,
1557 traces,
1558 })
1559 }
1560
1561 fn mtp_step(
1565 &mut self,
1566 m: &mut MtpModule,
1567 hidden: &[f32],
1568 next_token: u32,
1569 position: usize,
1570 ) -> u32 {
1571 let e = self.embed_single(next_token);
1575 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1576 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1577 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1578 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1579 let mut x = vec![0.0f32; self.hidden_size];
1580 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1581
1582 let lw = &m.layer;
1584 inference::rms_norm_into(
1585 &x,
1586 &lw.input_norm,
1587 self.rms_eps,
1588 self.norm_style,
1589 &mut self.ws.n1,
1590 );
1591 let attn = match &lw.attn {
1592 AttnKind::Full {
1593 wq,
1594 wk,
1595 wv,
1596 wo,
1597 q_norm,
1598 k_norm,
1599 output_gate,
1600 softplus_gate,
1601 bias,
1602 } => {
1603 let mut cfg = self.attn_cfg(position);
1604 cfg.q_norm = q_norm.as_deref();
1605 cfg.k_norm = k_norm.as_deref();
1606 cfg.output_gate = *output_gate;
1607 cfg.softplus_gate = softplus_gate
1608 .as_ref()
1609 .map(|(gate, per_head)| (gate, *per_head));
1610 cfg.bias = bias
1611 .as_ref()
1612 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1613 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1614 }
1615 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1616 unreachable!("MTP block is full attention")
1617 }
1618 };
1619 for (i, &a) in attn.iter().enumerate() {
1620 x[i] += a;
1621 }
1622 inference::rms_norm_into(
1623 &x,
1624 &lw.post_norm,
1625 self.rms_eps,
1626 self.norm_style,
1627 &mut self.ws.p1,
1628 );
1629 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1630 for (i, &f) in ffn.iter().enumerate() {
1631 x[i] += f;
1632 }
1633
1634 inference::rms_norm_into(
1635 &x,
1636 &m.final_norm,
1637 self.rms_eps,
1638 self.norm_style,
1639 &mut self.ws.n1,
1640 );
1641 let mut lg = self.lm_head_forward(&self.ws.n1);
1642 let draft = sampler::argmax(&lg);
1643 attention::recycle_buf(&mut lg);
1644 draft
1645 }
1646
1647 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1651 let emb1 = self.embed_single(1);
1652 let emb2 = self.embed_single(2);
1653 let pos = self.kv_cache.seq_len();
1654
1655 let t0 = std::time::Instant::now();
1656 for _ in 0..iters {
1657 let _ = self.forward_layers(&emb1, pos, None);
1658 let _ = self.forward_layers(&emb2, pos + 1, None);
1659 for l in &mut self.kv_cache.layers {
1660 l.truncate_last(2);
1661 }
1662 }
1663 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1664
1665 let t1 = std::time::Instant::now();
1666 for _ in 0..iters {
1667 let _ = self.forward_pair(&emb1, &emb2, pos);
1668 for l in &mut self.kv_cache.layers {
1669 l.truncate_last(2);
1670 }
1671 }
1672 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1673 (singles_ms, pair_ms)
1674 }
1675
1676 fn forward_pair(
1681 &mut self,
1682 emb1: &[f32],
1683 emb2: &[f32],
1684 position: usize,
1685 ) -> (Vec<f32>, Vec<f32>) {
1686 let mut h1 = emb1.to_vec();
1687 let mut h2 = emb2.to_vec();
1688 let (_nkv, _hd, hs, _rd, eps) = (
1689 self.num_kv_heads,
1690 self.head_dim,
1691 self.hidden_size,
1692 self.rotary_dim,
1693 self.rms_eps,
1694 );
1695 let pool = self.pool.clone();
1696
1697 for li in 0..self.num_layers {
1698 let lw = &self.weights.layers[self.phys_layer(li)];
1699 inference::rms_norm_into(
1702 &h1,
1703 &lw.input_norm,
1704 self.rms_eps,
1705 self.norm_style,
1706 &mut self.ws.n1,
1707 );
1708 inference::rms_norm_into(
1709 &h2,
1710 &lw.input_norm,
1711 self.rms_eps,
1712 self.norm_style,
1713 &mut self.ws.n2,
1714 );
1715
1716 let (a1, a2) = match &lw.attn {
1717 AttnKind::Linear(w) => {
1718 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1719 let layer = &mut self.kv_cache.layers[li];
1720 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1721 vmf_phase_pair(
1722 &self.ws.n1,
1723 &self.ws.n2,
1724 w,
1725 &cfg,
1726 state,
1727 scratch,
1728 self.pool.as_deref(),
1729 )
1730 }
1731 AttnKind::LinearGdn(w) => {
1732 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1733 let layer = &mut self.kv_cache.layers[li];
1734 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1735 gdn_pair(
1736 &self.ws.n1,
1737 &self.ws.n2,
1738 w,
1739 &cfg,
1740 state,
1741 scratch,
1742 self.pool.as_deref(),
1743 )
1744 }
1745 AttnKind::ShortConv(w) => {
1746 let cfg = self
1747 .short_conv_cfg
1748 .expect("short-conv layer without short_conv_cfg");
1749 let layer = &mut self.kv_cache.layers[li];
1750 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1751 short_conv_pair(
1752 &self.ws.n1,
1753 &self.ws.n2,
1754 w,
1755 &cfg,
1756 state,
1757 scratch,
1758 self.pool.as_deref(),
1759 )
1760 }
1761 AttnKind::Full {
1762 wq,
1763 wk,
1764 wv,
1765 wo,
1766 q_norm,
1767 k_norm,
1768 output_gate,
1769 softplus_gate,
1770 bias,
1771 } => {
1772 let inv_freq_l = self.layer_inv_freq(li);
1773 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1774 let cfg = QwenAttnCfg {
1775 num_heads: self.layer_num_heads(li),
1776 num_kv_heads: nkv_l,
1777 head_dim: hd_l,
1778 hidden_size: hs,
1779 position,
1780 inv_freq: &inv_freq_l,
1781 rotary_dim: rd_l,
1782 scale: self.attn_scale,
1783 window: self.layer_window(li),
1784 v_norm: self.attn_v_norm,
1785 q_norm: q_norm.as_deref(),
1786 k_norm: k_norm.as_deref(),
1787 output_gate: *output_gate,
1788 softplus_gate: softplus_gate
1789 .as_ref()
1790 .map(|(gate, per_head)| (gate, *per_head)),
1791 rope_scale: self.layer_rope_scale(li),
1792 bias: bias
1793 .as_ref()
1794 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1795 rms_eps: eps,
1796 norm_style: self.norm_style,
1797 pool: pool.as_deref(),
1798 };
1799 attention::qwen_attention_pair(
1800 &self.ws.n1,
1801 &self.ws.n2,
1802 wq,
1803 wk,
1804 wv,
1805 wo,
1806 &mut self.kv_cache.layers[li],
1807 &cfg,
1808 )
1809 }
1810 };
1811 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
1812 Some(w) => (
1813 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1814 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1815 ),
1816 None => (a1, a2),
1817 };
1818 for i in 0..self.hidden_size {
1819 h1[i] += a1[i];
1820 h2[i] += a2[i];
1821 }
1822 let (mut a1, mut a2) = (a1, a2);
1823 attention::recycle_buf(&mut a1);
1824 attention::recycle_buf(&mut a2);
1825
1826 let lw = &self.weights.layers[self.phys_layer(li)];
1827 inference::rms_norm_into(
1828 &h1,
1829 &lw.post_norm,
1830 self.rms_eps,
1831 self.norm_style,
1832 &mut self.ws.p1,
1833 );
1834 inference::rms_norm_into(
1835 &h2,
1836 &lw.post_norm,
1837 self.rms_eps,
1838 self.norm_style,
1839 &mut self.ws.p2,
1840 );
1841 let (f1, f2) =
1842 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1843 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
1844 Some(w) => (
1845 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1846 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1847 ),
1848 None => (f1, f2),
1849 };
1850 for i in 0..self.hidden_size {
1851 h1[i] += f1[i];
1852 h2[i] += f2[i];
1853 }
1854 let (mut f1, mut f2) = (f1, f2);
1855 attention::recycle_buf(&mut f1);
1856 attention::recycle_buf(&mut f2);
1857 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
1858 for i in 0..self.hidden_size {
1859 h1[i] *= sc;
1860 h2[i] *= sc;
1861 }
1862 }
1863 if self.is_loop_end(li) && li + 1 < self.num_layers {
1865 h1 = inference::rms_norm(&h1, &self.weights.final_norm, self.rms_eps, self.norm_style);
1866 h2 = inference::rms_norm(&h2, &self.weights.final_norm, self.rms_eps, self.norm_style);
1867 }
1868 }
1869 (h1, h2)
1870 }
1871
1872 fn commit_linear_scratch(&mut self) {
1874 for layer in &mut self.kv_cache.layers {
1875 if !layer.linear_scratch.is_empty() {
1876 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1877 layer.linear_scratch.clear();
1878 }
1879 }
1880 }
1881
1882 pub fn forward_ids(
1885 &mut self,
1886 ids: &[u32],
1887 task_mask: Option<&TaskMask>,
1888 ) -> Result<Vec<f32>, String> {
1889 if ids.is_empty() {
1890 return Err("empty id sequence".to_string());
1891 }
1892 self.kv_cache.clear();
1893 self.o1_begin();
1894 let mut hidden = vec![0.0f32; self.hidden_size];
1895 let mut pos = 0usize;
1896 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1897 let chunk = prefill_chunk();
1901 let hs = self.hidden_size;
1902 while pos < ids.len() {
1903 let end = (pos + chunk).min(ids.len());
1904 let hb = self.prefill_batch(&ids[pos..end], pos);
1905 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1906 pos = end;
1907 }
1908 }
1909 if task_mask.is_none() {
1910 while pos + 1 < ids.len() {
1911 let e1 = self.embed_single(ids[pos]);
1912 let e2 = self.embed_single(ids[pos + 1]);
1913 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1914 self.commit_linear_scratch();
1915 hidden = h2;
1916 pos += 2;
1917 }
1918 }
1919 while pos < ids.len() {
1920 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1921 pos += 1;
1922 }
1923 self.o1_seal();
1927 let normed = inference::rms_norm(
1928 &hidden,
1929 &self.weights.final_norm,
1930 self.rms_eps,
1931 self.norm_style,
1932 );
1933 Ok(self.lm_head_forward(&normed))
1934 }
1935
1936 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1943 let (nll, cnt) = self.nll_ids_from(ids, 0);
1944 (nll / cnt.max(1) as f64).exp()
1945 }
1946
1947 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
1952 self.kv_cache.clear();
1953 FFN_PROBE.with(|p| {
1954 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
1955 });
1956 crate::gpu::cpu_scope(|| {
1957 for (pos, &id) in ids.iter().enumerate() {
1958 let emb = self.embed_single(id);
1959 let _ = self.forward_layers(&emb, pos, None);
1960 }
1961 });
1962 self.kv_cache.clear();
1963 FFN_PROBE
1964 .with(|p| p.borrow_mut().take())
1965 .unwrap_or_default()
1966 }
1967
1968 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
1972 self.kv_cache.clear();
1973 let mut nll = 0f64;
1974 let mut cnt = 0usize;
1975 let mut hidden = vec![0f32; self.hidden_size];
1976 for (pos, &id) in ids.iter().enumerate() {
1977 if pos > 0 {
1978 inference::rms_norm_into(
1979 &hidden,
1980 &self.weights.final_norm,
1981 self.rms_eps,
1982 self.norm_style,
1983 &mut self.ws.n1,
1984 );
1985 let mut logits = self.lm_head_forward(&self.ws.n1);
1986 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1987 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
1988 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
1989 nll -= p.max(1e-300).ln();
1990 cnt += 1;
1991 attention::recycle_buf(&mut logits);
1992 }
1993 let emb = self.embed_single(id);
1994 hidden = self.forward_layers(&emb, pos, Some(mask));
1995 }
1996 self.kv_cache.clear();
1997 (nll / cnt.max(1) as f64).exp()
1998 }
1999
2000 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2009 self.kv_cache.clear();
2010 let mut nll = 0f64;
2011 let mut cnt = 0usize;
2012 if prefill_batched() {
2013 const CHUNK: usize = 128;
2019 const LM_SUB: usize = 32;
2020 let n = ids.len().saturating_sub(1);
2021 let hs = self.hidden_size;
2022 let rows = self.weights.lm_head.rows();
2023 let mut pos = 0usize;
2024 while pos < n {
2025 let end = (pos + CHUNK).min(n);
2026 let bsz = end - pos;
2027 let hb = self.prefill_batch(&ids[pos..end], pos);
2028 let mut k0 = 0usize;
2029 while k0 < bsz {
2030 let k1 = (k0 + LM_SUB).min(bsz);
2031 let sb = k1 - k0;
2032 if pos + k1 <= start {
2035 k0 = k1;
2036 continue;
2037 }
2038 let mut normed = vec![0.0f32; sb * hs];
2039 for k in 0..sb {
2040 let r = inference::rms_norm(
2041 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2042 &self.weights.final_norm,
2043 self.rms_eps,
2044 self.norm_style,
2045 );
2046 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2047 }
2048 let mut logits = vec![0.0f32; sb * rows];
2049 self.weights
2050 .lm_head
2051 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2052 for k in 0..sb {
2053 if pos + k0 + k < start {
2054 continue;
2055 }
2056 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2057 let target = ids[pos + k0 + k + 1] as usize;
2058 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2059 let lse: f64 = lg
2060 .iter()
2061 .map(|&v| ((v - max) as f64).exp())
2062 .sum::<f64>()
2063 .ln()
2064 + max as f64;
2065 nll += lse - lg[target] as f64;
2066 cnt += 1;
2067 }
2068 k0 = k1;
2069 }
2070 pos = end;
2071 }
2072 self.kv_cache.clear();
2073 return (nll, cnt);
2074 }
2075 for pos in 0..ids.len().saturating_sub(1) {
2076 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2077 if pos < start {
2078 continue;
2079 }
2080 let normed = inference::rms_norm(
2081 &hidden,
2082 &self.weights.final_norm,
2083 self.rms_eps,
2084 self.norm_style,
2085 );
2086 let logits = self.lm_head_forward(&normed);
2087 let target = ids[pos + 1] as usize;
2088 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2089 let lse: f64 = logits
2090 .iter()
2091 .map(|&v| ((v - max) as f64).exp())
2092 .sum::<f64>()
2093 .ln()
2094 + max as f64;
2095 nll += lse - logits[target] as f64;
2096 cnt += 1;
2097 }
2098 self.kv_cache.clear();
2099 (nll, cnt)
2100 }
2101
2102 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2118 self.kv_cache.clear();
2119 self.o1_begin();
2120 let n = ids.len().saturating_sub(1);
2121 let p = prefill.min(n);
2122 let mut pos = 0usize;
2124 if prefill_batched() {
2125 const CHUNK: usize = 128;
2126 while pos < p {
2127 let end = (pos + CHUNK).min(p);
2128 let _ = self.prefill_batch(&ids[pos..end], pos);
2129 pos = end;
2130 }
2131 } else {
2132 while pos < p {
2133 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2134 pos += 1;
2135 }
2136 }
2137 self.o1_seal();
2138
2139 let mut nll = 0f64;
2140 let mut cnt = 0usize;
2141 for pos in p..n {
2142 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2143 let normed = inference::rms_norm(
2144 &hidden,
2145 &self.weights.final_norm,
2146 self.rms_eps,
2147 self.norm_style,
2148 );
2149 let logits = self.lm_head_forward(&normed);
2150 let target = ids[pos + 1] as usize;
2151 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2152 let lse: f64 = logits
2153 .iter()
2154 .map(|&v| ((v - max) as f64).exp())
2155 .sum::<f64>()
2156 .ln()
2157 + max as f64;
2158 nll += lse - logits[target] as f64;
2159 cnt += 1;
2160 }
2161 self.kv_cache.clear();
2162 (nll, cnt)
2163 }
2164
2165 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2173 self.kv_cache.clear();
2174 let n = ids.len().saturating_sub(1);
2175 let mut correct = Vec::with_capacity(n);
2176 let mut pmax = Vec::with_capacity(n);
2177 for pos in 0..n {
2178 let emb = self.embed_single(ids[pos]);
2179 let hidden = self.forward_layers(&emb, pos, None);
2180 let normed = inference::rms_norm(
2181 &hidden,
2182 &self.weights.final_norm,
2183 self.rms_eps,
2184 self.norm_style,
2185 );
2186 let logits = self.lm_head_forward(&normed);
2187 let target = ids[pos + 1] as usize;
2188 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2189 for (i, &v) in logits.iter().enumerate() {
2190 if v > mval {
2191 mval = v;
2192 amax = i;
2193 }
2194 }
2195 correct.push(amax == target);
2196 let row: Vec<f32> = temps
2197 .iter()
2198 .map(|&t| {
2199 let tt = t.max(1e-3);
2200 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2201 1.0 / s.max(1e-12) })
2203 .collect();
2204 pmax.push(row);
2205 }
2206 self.kv_cache.clear();
2207 (correct, pmax)
2208 }
2209
2210 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2217 let mut router = match self.dyn_router.take() {
2218 Some(r) => r,
2219 None => return (self.ppl_ids(ids), 0),
2220 };
2221 router.reset();
2222 self.dyn_phi_seen = 0;
2223 let _ = self.set_active_skill(None);
2224
2225 self.kv_cache.clear();
2226 let mut nll = 0f64;
2227 let mut cnt = 0usize;
2228 for pos in 0..ids.len().saturating_sub(1) {
2229 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2230 let normed = inference::rms_norm(
2231 &hidden,
2232 &self.weights.final_norm,
2233 self.rms_eps,
2234 self.norm_style,
2235 );
2236 let logits = self.lm_head_forward(&normed);
2237 let target = ids[pos + 1] as usize;
2238 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2239 let lse: f64 = logits
2240 .iter()
2241 .map(|&v| ((v - max) as f64).exp())
2242 .sum::<f64>()
2243 .ln()
2244 + max as f64;
2245 nll += lse - logits[target] as f64;
2246 cnt += 1;
2247 let phi = self.dyn_phi_ema.clone();
2249 if let Some(new_active) = router.step(&phi, pos) {
2250 let _ = self.set_active_skill(new_active);
2251 }
2252 }
2253 let switches = router.switches.len();
2254 let _ = self.set_active_skill(None);
2255 self.dyn_router = Some(router);
2256 self.kv_cache.clear();
2257 ((nll / cnt.max(1) as f64).exp(), switches)
2258 }
2259
2260 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2262 self.kv_cache.clear();
2263 let mut acc = vec![0f32; self.hidden_size];
2264 for (pos, &id) in ids.iter().enumerate() {
2265 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2266 for (a, v) in acc.iter_mut().zip(&h) {
2267 *a += v;
2268 }
2269 }
2270 let n = ids.len().max(1) as f32;
2271 for a in acc.iter_mut() {
2272 *a /= n;
2273 }
2274 self.kv_cache.clear();
2275 acc
2276 }
2277
2278 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2284 let b = ids.len();
2285 let hs = self.hidden_size;
2286 let mut h: Vec<f32> = vec![0.0; b * hs];
2289 let mut h_ready = false;
2290 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2291 for (bi, &id) in ids.iter().enumerate() {
2292 let e = me.embed_single(id);
2293 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2294 }
2295 };
2296 let (_nkv, _hd, _rd, eps) = (
2297 self.num_kv_heads,
2298 self.head_dim,
2299 self.rotary_dim,
2300 self.rms_eps,
2301 );
2302 let pool = self.pool.clone();
2303 let norm_style = self.norm_style;
2304
2305 #[cfg(target_os = "macos")]
2306 let mut chunk_skip_until = 0usize;
2307 for li in 0..self.num_layers {
2308 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2315 {
2316 if li < chunk_skip_until {
2317 continue;
2318 }
2319 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2320 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2321 if end > li {
2322 h_ready = true;
2323 chunk_skip_until = end;
2324 continue;
2325 }
2326 }
2327 if !h_ready {
2328 fill_h(&mut h, self);
2329 h_ready = true;
2330 }
2331 let lw = &self.weights.layers[self.phys_layer(li)];
2332 match &lw.attn {
2334 AttnKind::LinearGdn(w) => {
2335 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2337 let mut normed = vec![0.0f32; b * hs];
2338 for bi in 0..b {
2339 let r = inference::rms_norm(
2340 &h[bi * hs..(bi + 1) * hs],
2341 &lw.input_norm,
2342 eps,
2343 norm_style,
2344 );
2345 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2346 }
2347 let attn = crate::linear_core::gdn_forward_batch(
2348 &normed,
2349 b,
2350 w,
2351 &cfg,
2352 &mut self.kv_cache.layers[li].linear_state,
2353 pool.as_deref(),
2354 );
2355 for (dst, &a) in h.iter_mut().zip(&attn) {
2356 *dst += a;
2357 }
2358 }
2359 AttnKind::ShortConv(w) => {
2360 let cfg = self
2363 .short_conv_cfg
2364 .expect("short-conv layer without short_conv_cfg");
2365 let mut normed = vec![0.0f32; b * hs];
2366 for bi in 0..b {
2367 inference::rms_norm_into(
2368 &h[bi * hs..(bi + 1) * hs],
2369 &lw.input_norm,
2370 eps,
2371 norm_style,
2372 &mut normed[bi * hs..(bi + 1) * hs],
2373 );
2374 }
2375 let attn = short_conv_forward_batch(
2376 &normed,
2377 b,
2378 w,
2379 &cfg,
2380 &mut self.kv_cache.layers[li].linear_state,
2381 pool.as_deref(),
2382 );
2383 for (dst, &a) in h.iter_mut().zip(&attn) {
2384 *dst += a;
2385 }
2386 }
2387 AttnKind::Full {
2388 wq,
2389 wk,
2390 wv,
2391 wo,
2392 q_norm,
2393 k_norm,
2394 output_gate,
2395 softplus_gate,
2396 bias,
2397 } => {
2398 let mut normed = vec![0.0f32; b * hs];
2402 for bi in 0..b {
2403 inference::rms_norm_into(
2404 &h[bi * hs..(bi + 1) * hs],
2405 &lw.input_norm,
2406 eps,
2407 norm_style,
2408 &mut normed[bi * hs..(bi + 1) * hs],
2409 );
2410 }
2411 let inv_freq_l = self.layer_inv_freq(li);
2412 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2413 let cfg = QwenAttnCfg {
2414 num_heads: self.layer_num_heads(li),
2415 num_kv_heads: nkv_l,
2416 head_dim: hd_l,
2417 hidden_size: hs,
2418 position: start_pos,
2419 inv_freq: &inv_freq_l,
2420 rotary_dim: rd_l,
2421 scale: self.attn_scale,
2422 window: self.layer_window(li),
2423 v_norm: self.attn_v_norm,
2424 q_norm: q_norm.as_deref(),
2425 k_norm: k_norm.as_deref(),
2426 output_gate: *output_gate,
2427 softplus_gate: softplus_gate
2428 .as_ref()
2429 .map(|(gate, per_head)| (gate, *per_head)),
2430 rope_scale: self.layer_rope_scale(li),
2431 bias: bias
2432 .as_ref()
2433 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2434 rms_eps: eps,
2435 norm_style,
2436 pool: pool.as_deref(),
2437 };
2438 let mut attn = attention::qwen_attention_batch(
2439 &normed,
2440 b,
2441 wq,
2442 wk,
2443 wv,
2444 wo,
2445 &mut self.kv_cache.layers[li],
2446 &cfg,
2447 );
2448 if let Some(w) = &lw.attn_out_norm {
2449 for bi in 0..b {
2450 inference::rms_norm_into(
2451 &attn[bi * hs..(bi + 1) * hs],
2452 w,
2453 eps,
2454 norm_style,
2455 &mut normed[bi * hs..(bi + 1) * hs],
2456 );
2457 }
2458 attn.copy_from_slice(&normed);
2459 }
2460 for (dst, &a) in h.iter_mut().zip(&attn) {
2461 *dst += a;
2462 }
2463 }
2464 AttnKind::Linear(w) => {
2465 for bi in 0..b {
2466 let normed = inference::rms_norm(
2467 &h[bi * hs..(bi + 1) * hs],
2468 &lw.input_norm,
2469 eps,
2470 norm_style,
2471 );
2472 vmf_phase_forward(
2473 &normed,
2474 w,
2475 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2476 &mut self.kv_cache.layers[li].linear_state,
2477 pool.as_deref(),
2478 )
2479 .iter()
2480 .enumerate()
2481 .for_each(|(i, &a)| h[bi * hs + i] += a);
2482 }
2483 }
2484 }
2485
2486 let lw = &self.weights.layers[self.phys_layer(li)];
2488 let mut post = vec![0.0f32; b * hs];
2489 for bi in 0..b {
2490 let r =
2491 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2492 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2493 }
2494 let mut ffn = match &lw.ffn {
2495 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2496 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
2497 };
2498 if let Some(w) = &lw.ffn_out_norm {
2499 for bi in 0..b {
2500 inference::rms_norm_into(
2501 &ffn[bi * hs..(bi + 1) * hs],
2502 w,
2503 eps,
2504 norm_style,
2505 &mut post[bi * hs..(bi + 1) * hs],
2506 );
2507 }
2508 ffn.copy_from_slice(&post);
2509 }
2510 for (dst, &f) in h.iter_mut().zip(&ffn) {
2511 *dst += f;
2512 }
2513 if let Some(sc) = lw.layer_scale {
2514 for v in h.iter_mut() {
2515 *v *= sc;
2516 }
2517 }
2518 if self.is_loop_end(li) && li + 1 < self.num_layers {
2520 for bi in 0..b {
2521 let normed = inference::rms_norm(
2522 &h[bi * hs..(bi + 1) * hs],
2523 &self.weights.final_norm,
2524 eps,
2525 norm_style,
2526 );
2527 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2528 }
2529 }
2530 if std::env::var("CMF_TRACE_H").is_ok() {
2531 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2532 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2533 eprintln!(
2534 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
2535 lw.layer_scale
2536 );
2537 }
2538 }
2539 crate::gpu::set_layer(-1); h
2541 }
2542
2543 fn embed_single(&self, id: u32) -> Vec<f32> {
2545 let mut out = vec![0.0f32; self.hidden_size];
2546 if (id as usize) < self.weights.embed_tokens.rows() {
2547 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2548 }
2549 if self.embed_multiplier != 1.0 {
2550 for v in out.iter_mut() {
2551 *v *= self.embed_multiplier;
2552 }
2553 }
2554 out
2555 }
2556
2557 #[cfg(target_os = "macos")]
2563 fn chunk_run_gpu(
2564 &mut self,
2565 li0: usize,
2566 h: &mut [f32],
2567 b: usize,
2568 pos0: usize,
2569 embed_ids: Option<&[u32]>,
2570 ) -> usize {
2571 if !crate::gpu::enabled_here()
2575 || std::env::var("CMF_GPU_CHUNK")
2576 .map(|v| v == "0")
2577 .unwrap_or(false)
2578 || b < 32
2579 || self.swa.is_some()
2580 || self.global_attn.is_some()
2581 || self.attn_v_norm
2582 || self.loop_final_norm
2583 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2584 {
2585 return li0;
2586 }
2587 let Some(model) = self.model.clone() else {
2588 return li0;
2589 };
2590 let inv_freq = self.inv_freq.clone();
2591 let (nh, nkv, hd, hs) = (
2592 self.num_heads,
2593 self.num_kv_heads,
2594 self.head_dim,
2595 self.hidden_size,
2596 );
2597 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2599 let mut stored_at: Vec<usize> = Vec::new();
2600 for li in li0..self.num_layers {
2601 let lw = &self.weights.layers[self.phys_layer(li)];
2602 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
2603 break;
2604 }
2605 let AttnKind::Full {
2606 wq,
2607 wk,
2608 wv,
2609 wo,
2610 q_norm,
2611 k_norm,
2612 output_gate: false,
2613 softplus_gate: None,
2614 bias,
2615 } = &lw.attn
2616 else {
2617 break;
2618 };
2619 let FfnKind::Dense(d) = &lw.ffn else { break };
2620 if d.act != Act::Silu {
2621 break;
2622 }
2623 let parts = (
2624 wq.q8_row_parts(),
2625 wk.q8_row_parts(),
2626 wv.q8_row_parts(),
2627 wo.q8_row_parts(),
2628 d.gate_proj.q8_row_parts(),
2629 d.up_proj.q8_row_parts(),
2630 d.down_proj.q8_row_parts(),
2631 );
2632 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2633 else {
2634 break;
2635 };
2636 let layer = &self.kv_cache.layers[li];
2637 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2638 break;
2639 }
2640 stored_at.push(layer.head_len(0));
2641 layers.push(crate::gpu_metal::ChunkLayer {
2642 model: &model,
2643 kv_id: self.graph_kv_id,
2644 layer: li,
2645 wq: pq,
2646 wk: pk,
2647 wv: pv,
2648 wo: po,
2649 gate: pg,
2650 up: pu,
2651 down: pd,
2652 input_norm: &lw.input_norm,
2653 post_norm: &lw.post_norm,
2654 bias: bias
2655 .as_ref()
2656 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2657 q_norm: q_norm.as_deref(),
2658 k_norm: k_norm.as_deref(),
2659 inv_freq: &inv_freq,
2660 rd: self.rotary_dim,
2661 nh,
2662 nkv,
2663 hd,
2664 hs,
2665 inter: d.gate_proj.rows(),
2666 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2667 eps: self.rms_eps as f32,
2668 });
2669 }
2670 if layers.is_empty() {
2671 return li0;
2672 }
2673 let row = nkv * hd;
2674 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2675 .iter()
2676 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2677 .collect();
2678 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2679 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2680 let li = layers[i].layer;
2681 let layer = &self.kv_cache.layers[li];
2682 io.push(crate::gpu_metal::ChunkIo {
2683 cpu_stored: stored_at[i],
2684 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2685 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2686 out_k: ok,
2687 out_v: ov,
2688 imp: oi,
2689 });
2690 }
2691 let n_run = layers.len();
2692 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2693 let ep = embed_ids.and_then(|ids| {
2696 self.weights
2697 .embed_tokens
2698 .q8_row_parts()
2699 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
2700 idx,
2701 rows,
2702 row_scale: rs,
2703 ids,
2704 mult: self.embed_multiplier,
2705 })
2706 });
2707 if embed_ids.is_some() && ep.is_none() {
2708 return li0;
2709 }
2710 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2711 return li0;
2712 }
2713 drop(io);
2714 drop(layers);
2715 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2718 let li = li0 + i;
2719 let layer = &mut self.kv_cache.layers[li];
2720 for bi in 0..b {
2721 layer.append(
2722 &ok[bi * row..(bi + 1) * row],
2723 &ov[bi * row..(bi + 1) * row],
2724 &[],
2725 );
2726 }
2727 layer.accumulate_imp(oi);
2728 }
2729 last
2730 }
2731
2732 fn layer_is_local(&self, li: usize) -> bool {
2735 if let Some(layers) = &self.sliding_layers {
2736 return layers.get(li).copied().unwrap_or(false);
2737 }
2738 match self.swa {
2739 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2740 None => false,
2741 }
2742 }
2743
2744 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2747 if self.layer_is_local(li) {
2748 if let Some(f) = &self.inv_freq_local {
2749 return f.clone();
2750 }
2751 } else if let Some(f) = &self.inv_freq_global {
2752 return f.clone();
2753 }
2754 self.inv_freq.clone()
2755 }
2756
2757 fn layer_window(&self, li: usize) -> Option<usize> {
2759 self.swa
2760 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
2761 }
2762
2763 fn layer_num_heads(&self, li: usize) -> usize {
2764 self.attention_heads_per_layer
2765 .as_ref()
2766 .and_then(|v| v.get(li).copied())
2767 .unwrap_or(self.num_heads)
2768 }
2769
2770 fn layer_rope_scale(&self, li: usize) -> f32 {
2771 if self.layer_is_local(li) {
2772 self.rope_scale_local
2773 } else {
2774 self.rope_scale
2775 }
2776 }
2777
2778 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2781 if !self.layer_is_local(li) {
2782 if let Some((ghd, gkv)) = self.global_attn {
2783 return (gkv, ghd, ghd);
2784 }
2785 }
2786 (
2787 self.num_kv_heads,
2788 self.head_dim,
2789 if self.layer_is_local(li) {
2790 self.rotary_dim_local.unwrap_or(self.rotary_dim)
2791 } else {
2792 self.rotary_dim
2793 },
2794 )
2795 }
2796
2797 fn forward_layers(
2799 &mut self,
2800 hidden: &[f32],
2801 position: usize,
2802 task_mask: Option<&TaskMask>,
2803 ) -> Vec<f32> {
2804 self.forward_layers_upto(hidden, position, task_mask, None)
2805 }
2806
2807 fn try_token_graph_wgpu(
2811 &self,
2812 hidden: &[f32],
2813 position: usize,
2814 logits_out: &mut Vec<f32>,
2815 ) -> Option<Vec<f32>> {
2816 if self.o1_active() {
2819 return None;
2820 }
2821 if self.loop_final_norm {
2823 return None;
2824 }
2825 let nh = self.num_heads;
2826 let (nkv, hd, rd) = self.layer_geom(0);
2827 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2828 let mut layers = Vec::with_capacity(self.num_layers);
2829 let mut model = None;
2830 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
2831 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2832 if let Some((_, i, kind, rs)) = t.graph_weight() {
2833 return Some(crate::gpu::GraphW {
2834 idx: i,
2835 kind,
2836 row_scale: rs,
2837 data: &[],
2838 });
2839 }
2840 t.as_f32().map(|d| crate::gpu::GraphW {
2842 idx: 0,
2843 kind: 4,
2844 row_scale: &[],
2845 data: d,
2846 })
2847 }
2848 for li in 0..self.num_layers {
2849 let lw = &self.weights.layers[self.phys_layer(li)];
2850 if dbg {
2851 let ak = match &lw.attn {
2852 AttnKind::Full {
2853 output_gate, bias, ..
2854 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
2855 AttnKind::LinearGdn(_) => "LinearGdn".into(),
2856 AttnKind::Linear(_) => "Linear".into(),
2857 AttnKind::ShortConv(_) => "ShortConv".into(),
2858 };
2859 let fk = match &lw.ffn {
2860 FfnKind::Dense(_) => "Dense",
2861 FfnKind::Moe(_) => "Moe",
2862 };
2863 eprintln!("graph L{li}: attn={ak} ffn={fk}");
2864 }
2865 let (gate, up, down) = match &lw.ffn {
2866 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
2867 _ => return None,
2868 };
2869 let attn = match &lw.attn {
2870 AttnKind::Full {
2871 wq,
2872 wk,
2873 wv,
2874 wo,
2875 q_norm,
2876 k_norm,
2877 output_gate,
2878 softplus_gate,
2879 bias,
2880 } => {
2881 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
2882 return None;
2883 }
2884 let (m, _, _, _) = wq.graph_weight()?;
2885 model = Some(m.clone());
2886 crate::gpu::GraphAttn::Full {
2887 wq: gw(wq)?,
2888 wk: gw(wk)?,
2889 wv: gw(wv)?,
2890 wo: gw(wo)?,
2891 q_norm: q_norm.as_deref(),
2892 k_norm: k_norm.as_deref(),
2893 bias: bias
2894 .as_ref()
2895 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2896 output_gate: *output_gate,
2897 cpu_k: self.kv_cache.layers[li].k_heads(),
2898 cpu_v: self.kv_cache.layers[li].v_heads(),
2899 }
2900 }
2901 AttnKind::LinearGdn(w) => {
2902 let cfg = self.gdn_cfg?;
2903 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
2904 model = Some(m.clone());
2905 crate::gpu::GraphAttn::Gdn {
2906 qkv: gw(&w.in_proj_qkv)?,
2907 z: gw(&w.in_proj_z)?,
2908 a: gw(&w.in_proj_a)?,
2909 b: gw(&w.in_proj_b)?,
2910 out: gw(&w.out_proj)?,
2911 conv1d: &w.conv1d,
2912 a_log: &w.a_log,
2913 dt_bias: &w.dt_bias,
2914 norm: &w.norm,
2915 nv: cfg.num_v_heads,
2916 nk: cfg.num_k_heads,
2917 dk: cfg.key_head_dim,
2918 dv: cfg.value_head_dim,
2919 kk: cfg.conv_kernel,
2920 }
2921 }
2922 _ => return None,
2923 };
2924 layers.push(crate::gpu::GraphLayer {
2925 input_norm: &lw.input_norm,
2926 attn,
2927 post_norm: &lw.post_norm,
2928 gate: gw(gate)?,
2929 up: gw(up)?,
2930 down: gw(down)?,
2931 });
2932 }
2933 let model = model?;
2934 let lm_gw = if self.graph_want_logits
2940 && std::env::var("CMF_GPU_LMHEAD")
2941 .map(|v| v != "0")
2942 .unwrap_or(true)
2943 {
2944 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
2945 (
2946 crate::gpu::GraphW {
2947 idx: i,
2948 kind,
2949 row_scale: rs,
2950 data: &[],
2951 },
2952 self.weights.lm_head.rows(),
2953 )
2954 })
2955 } else {
2956 None
2957 };
2958 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
2959 let mut h = hidden.to_vec();
2960 crate::gpu::forward_token_graph(
2961 &model,
2962 self.graph_kv_id,
2963 &layers,
2964 &self.inv_freq,
2965 &mut h,
2966 nh,
2967 nkv,
2968 hd,
2969 rd,
2970 self.hidden_size,
2971 self.intermediate_size,
2972 position,
2973 self.kv_cache.max_seq_len,
2974 gemma,
2975 self.rms_eps as f32,
2976 lm,
2977 &self.weights.final_norm,
2978 logits_out,
2979 )
2980 .then_some(h)
2981 }
2982
2983 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
2988 if self.o1_active() {
2989 return false;
2990 }
2991 let nh = self.num_heads;
2992 let (nkv, hd, rd) = self.layer_geom(0);
2993 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2994 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2995 if let Some((_, i, kind, rs)) = t.graph_weight() {
2996 return Some(crate::gpu::GraphW {
2997 idx: i,
2998 kind,
2999 row_scale: rs,
3000 data: &[],
3001 });
3002 }
3003 t.as_f32().map(|d| crate::gpu::GraphW {
3004 idx: 0,
3005 kind: 4,
3006 row_scale: &[],
3007 data: d,
3008 })
3009 }
3010 let built: Option<(
3011 Vec<crate::gpu::GraphLayer<'_>>,
3012 std::sync::Arc<cortiq_core::CmfModel>,
3013 )> = (|| {
3014 let mut layers = Vec::with_capacity(self.num_layers);
3015 let mut model = None;
3016 for li in 0..self.num_layers {
3017 let lw = &self.weights.layers[self.phys_layer(li)];
3018 let (gate, up, down) = match &lw.ffn {
3019 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
3020 _ => return None,
3021 };
3022 let attn = match &lw.attn {
3023 AttnKind::Full {
3024 wq,
3025 wk,
3026 wv,
3027 wo,
3028 q_norm,
3029 k_norm,
3030 output_gate,
3031 softplus_gate,
3032 bias,
3033 } => {
3034 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3035 return None;
3036 }
3037 let (m, _, _, _) = wq.graph_weight()?;
3038 model = Some(m.clone());
3039 crate::gpu::GraphAttn::Full {
3040 wq: gw(wq)?,
3041 wk: gw(wk)?,
3042 wv: gw(wv)?,
3043 wo: gw(wo)?,
3044 q_norm: q_norm.as_deref(),
3045 k_norm: k_norm.as_deref(),
3046 bias: bias
3047 .as_ref()
3048 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3049 output_gate: *output_gate,
3050 cpu_k: self.kv_cache.layers[li].k_heads(),
3051 cpu_v: self.kv_cache.layers[li].v_heads(),
3052 }
3053 }
3054 AttnKind::LinearGdn(w) => {
3055 let cfg = self.gdn_cfg?;
3056 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3057 model = Some(m.clone());
3058 crate::gpu::GraphAttn::Gdn {
3059 qkv: gw(&w.in_proj_qkv)?,
3060 z: gw(&w.in_proj_z)?,
3061 a: gw(&w.in_proj_a)?,
3062 b: gw(&w.in_proj_b)?,
3063 out: gw(&w.out_proj)?,
3064 conv1d: &w.conv1d,
3065 a_log: &w.a_log,
3066 dt_bias: &w.dt_bias,
3067 norm: &w.norm,
3068 nv: cfg.num_v_heads,
3069 nk: cfg.num_k_heads,
3070 dk: cfg.key_head_dim,
3071 dv: cfg.value_head_dim,
3072 kk: cfg.conv_kernel,
3073 }
3074 }
3075 _ => return None,
3076 };
3077 layers.push(crate::gpu::GraphLayer {
3078 input_norm: &lw.input_norm,
3079 attn,
3080 post_norm: &lw.post_norm,
3081 gate: gw(gate)?,
3082 up: gw(up)?,
3083 down: gw(down)?,
3084 });
3085 }
3086 Some((layers, model?))
3087 })();
3088 let Some((layers, model)) = built else {
3089 return false;
3090 };
3091 crate::gpu::forward_batch_graph(
3092 &model,
3093 self.graph_kv_id,
3094 &layers,
3095 &self.inv_freq,
3096 hiddens,
3097 nh,
3098 nkv,
3099 hd,
3100 rd,
3101 self.hidden_size,
3102 self.intermediate_size,
3103 positions,
3104 self.kv_cache.max_seq_len,
3105 gemma,
3106 self.rms_eps as f32,
3107 k,
3108 )
3109 }
3110
3111 fn forward_layers_upto(
3113 &mut self,
3114 hidden: &[f32],
3115 position: usize,
3116 task_mask: Option<&TaskMask>,
3117 upto: Option<usize>,
3118 ) -> Vec<f32> {
3119 let mut h = hidden.to_vec();
3120 let (nh, _nkv, _hd, hs, _rd, eps) = (
3123 self.num_heads,
3124 self.num_kv_heads,
3125 self.head_dim,
3126 self.hidden_size,
3127 self.rotary_dim,
3128 self.rms_eps,
3129 );
3130 let pool = self.pool.clone();
3131 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
3134 .map(|v| v != "0")
3135 .unwrap_or_else(|_| {
3136 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
3137 });
3138 if graph_on && upto.is_none() && task_mask.is_none() {
3142 let mut lg = Vec::new();
3143 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
3144 if !lg.is_empty() {
3145 lg.resize(self.vocab_size, 0.0);
3148 if let Some(c) = self.final_softcap {
3149 for l in lg.iter_mut() {
3150 *l = c * (*l / c).tanh();
3151 }
3152 }
3153 self.graph_logits = Some(lg);
3154 }
3155 return hh;
3156 }
3157 }
3158
3159 #[cfg(target_os = "macos")]
3160 let mut gpu_skip_until = 0usize;
3161 for li in 0..self.num_layers {
3162 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
3164 if li > u {
3165 break;
3166 }
3167 }
3168 if let Some(mask) = task_mask {
3169 if !mask.layer_alive(li) {
3170 continue; }
3172 }
3173 #[cfg(target_os = "macos")]
3177 {
3178 if li < gpu_skip_until {
3179 continue;
3180 }
3181 if task_mask.is_none() {
3182 let end = self.q1_graph_gpu(li, upto, position, &mut h);
3183 if end > li {
3184 gpu_skip_until = end;
3185 continue;
3186 }
3187 }
3188 }
3189
3190 let lw = &self.weights.layers[self.phys_layer(li)];
3191 inference::rms_norm_into(
3194 &h,
3195 &lw.input_norm,
3196 self.rms_eps,
3197 self.norm_style,
3198 &mut self.ws.n1,
3199 );
3200
3201 let attn_out = match &lw.attn {
3202 AttnKind::Linear(w) => {
3203 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3204 vmf_phase_forward(
3205 &self.ws.n1,
3206 w,
3207 &cfg,
3208 &mut self.kv_cache.layers[li].linear_state,
3209 self.pool.as_deref(),
3210 )
3211 }
3212 AttnKind::LinearGdn(w) => {
3213 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3214 gdn_forward(
3215 &self.ws.n1,
3216 w,
3217 &cfg,
3218 &mut self.kv_cache.layers[li].linear_state,
3219 self.pool.as_deref(),
3220 )
3221 }
3222 AttnKind::ShortConv(w) => {
3223 let cfg = self
3224 .short_conv_cfg
3225 .expect("short-conv layer without short_conv_cfg");
3226 short_conv_forward(
3227 &self.ws.n1,
3228 w,
3229 &cfg,
3230 &mut self.kv_cache.layers[li].linear_state,
3231 self.pool.as_deref(),
3232 )
3233 }
3234 AttnKind::Full {
3235 wq,
3236 wk,
3237 wv,
3238 wo,
3239 q_norm,
3240 k_norm,
3241 output_gate,
3242 softplus_gate,
3243 bias,
3244 } if self.kv_cache.layers[li].o1_sealed() => {
3245 let inv_freq_l = self.layer_inv_freq(li);
3248 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3249 let cfg = QwenAttnCfg {
3250 num_heads: self.layer_num_heads(li),
3251 num_kv_heads: nkv_l,
3252 head_dim: hd_l,
3253 hidden_size: hs,
3254 position,
3255 inv_freq: &inv_freq_l,
3256 rotary_dim: rd_l,
3257 scale: self.attn_scale,
3258 window: None,
3259 v_norm: self.attn_v_norm,
3260 q_norm: q_norm.as_deref(),
3261 k_norm: k_norm.as_deref(),
3262 output_gate: *output_gate,
3263 softplus_gate: softplus_gate
3264 .as_ref()
3265 .map(|(gate, per_head)| (gate, *per_head)),
3266 rope_scale: self.layer_rope_scale(li),
3267 bias: bias
3268 .as_ref()
3269 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3270 rms_eps: eps,
3271 norm_style: self.norm_style,
3272 pool: pool.as_deref(),
3273 };
3274 attention::qwen_attention_nystrom(
3275 &self.ws.n1,
3276 wq,
3277 wk,
3278 wv,
3279 wo,
3280 &mut self.kv_cache.layers[li],
3281 &cfg,
3282 )
3283 }
3284 AttnKind::Full {
3285 wq,
3286 wk,
3287 wv,
3288 wo,
3289 q_norm,
3290 k_norm,
3291 output_gate,
3292 softplus_gate,
3293 bias,
3294 } => 'attn: {
3295 if graph_on
3298 && !*output_gate
3299 && softplus_gate.is_none()
3300 && self.attention_heads_per_layer.is_none()
3301 && bias.is_none()
3302 && task_mask.is_none()
3303 {
3304 let inv_freq_l = self.layer_inv_freq(li);
3305 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3306 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3307 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
3308 wq.mapped_q1(),
3309 wk.mapped_q1(),
3310 wv.mapped_q1(),
3311 wo.mapped_q1(),
3312 ) {
3313 let gm = gm.clone();
3314 let mut out = vec![0f32; hs];
3315 let cache = &self.kv_cache.layers[li];
3316 if crate::gpu::attn_dropin(
3317 &gm,
3318 self.graph_kv_id,
3319 li,
3320 &self.ws.n1,
3321 qi,
3322 ki,
3323 vi,
3324 oi,
3325 q_norm.as_deref(),
3326 k_norm.as_deref(),
3327 &inv_freq_l,
3328 nh,
3329 nkv_l,
3330 hd_l,
3331 rd_l,
3332 hs,
3333 position,
3334 self.kv_cache.max_seq_len,
3335 gemma,
3336 eps as f32,
3337 cache.k_heads(),
3338 cache.v_heads(),
3339 &mut out,
3340 ) {
3341 break 'attn out;
3342 }
3343 }
3344 }
3345 let masked = task_mask
3346 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
3347 .unwrap_or(false);
3348 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
3349 match (masked, f32_view) {
3350 (true, (Some(q), Some(k), Some(v), Some(o))) => {
3353 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
3354 attention::multi_head_attention(
3355 &self.ws.n1,
3356 q,
3357 k,
3358 v,
3359 o,
3360 &mut self.kv_cache.layers[li],
3361 self.num_heads,
3362 self.num_kv_heads,
3363 self.head_dim,
3364 self.hidden_size,
3365 position,
3366 &active_heads,
3367 &self.inv_freq,
3368 )
3369 }
3370 (masked, _) => {
3371 if masked {
3372 tracing::warn!(
3373 "layer {li}: head mask on quantized weights not \
3374 supported yet — executing dense"
3375 );
3376 }
3377 let inv_freq_l = self.layer_inv_freq(li);
3378 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3379 let cfg = QwenAttnCfg {
3380 num_heads: self.layer_num_heads(li),
3381 num_kv_heads: nkv_l,
3382 head_dim: hd_l,
3383 hidden_size: hs,
3384 position,
3385 inv_freq: &inv_freq_l,
3386 rotary_dim: rd_l,
3387 scale: self.attn_scale,
3388 window: self.layer_window(li),
3389 v_norm: self.attn_v_norm,
3390 q_norm: q_norm.as_deref(),
3391 k_norm: k_norm.as_deref(),
3392 output_gate: *output_gate,
3393 softplus_gate: softplus_gate
3394 .as_ref()
3395 .map(|(gate, per_head)| (gate, *per_head)),
3396 rope_scale: self.layer_rope_scale(li),
3397 bias: bias
3398 .as_ref()
3399 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3400 rms_eps: eps,
3401 norm_style: self.norm_style,
3402 pool: pool.as_deref(),
3403 };
3404 attention::qwen_attention(
3405 &self.ws.n1,
3406 wq,
3407 wk,
3408 wv,
3409 wo,
3410 &mut self.kv_cache.layers[li],
3411 &cfg,
3412 )
3413 }
3414 }
3415 }
3416 };
3417 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
3420 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
3421 None => attn_out,
3422 };
3423 let lw = &self.weights.layers[self.phys_layer(li)];
3424 inference::add_rmsnorm_fused_into(
3425 &mut h,
3426 &attn_out,
3427 &lw.post_norm,
3428 self.rms_eps,
3429 self.norm_style,
3430 &mut self.ws.p1,
3431 );
3432 let mut attn_out = attn_out;
3433 attention::recycle_buf(&mut attn_out);
3434 let post_normed = &self.ws.p1;
3435
3436 let ffn_masked = task_mask
3437 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
3438 .unwrap_or(false);
3439 let f32_ffn = match &lw.ffn {
3442 FfnKind::Dense(d) => (
3443 d.gate_proj.as_f32(),
3444 d.up_proj.as_f32(),
3445 d.down_proj.as_f32(),
3446 ),
3447 FfnKind::Moe(_) => (None, None, None),
3448 };
3449 let ffn_out = match (ffn_masked, f32_ffn) {
3450 (true, (Some(g), Some(u), Some(d))) => {
3451 let active = task_mask.unwrap().ffn_active_indices(li);
3452 inference::sparse_ffn_forward(
3453 &post_normed,
3454 g,
3455 u,
3456 d,
3457 self.hidden_size,
3458 self.intermediate_size,
3459 &active,
3460 self.pool.as_deref(),
3461 )
3462 }
3463 (true, _) => match &lw.ffn {
3467 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
3468 let active = task_mask.unwrap().ffn_active_indices(li);
3469 sparse_ffn_quant(
3470 d,
3471 &post_normed,
3472 &active,
3473 self.hidden_size,
3474 self.pool.as_deref(),
3475 )
3476 }
3477 FfnKind::Dense(d) => {
3482 let active = task_mask.unwrap().ffn_active_indices(li);
3483 let (gf, uf, df) = dequant_dense_f32(d);
3484 inference::sparse_ffn_forward(
3485 &post_normed,
3486 &gf,
3487 &uf,
3488 &df,
3489 self.hidden_size,
3490 self.intermediate_size,
3491 &active,
3492 self.pool.as_deref(),
3493 )
3494 }
3495 FfnKind::Moe(_) => {
3496 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
3499 }
3500 },
3501 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
3502 };
3503 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
3504 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
3505 None => ffn_out,
3506 };
3507 for (i, &f) in ffn_out.iter().enumerate() {
3508 h[i] += f;
3509 }
3510 let mut ffn_out = ffn_out;
3511 attention::recycle_buf(&mut ffn_out);
3512
3513 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
3515 for v in h.iter_mut() {
3516 *v *= sc;
3517 }
3518 }
3519
3520 if self.is_loop_end(li) && li + 1 < self.num_layers {
3523 h = inference::rms_norm(&h, &self.weights.final_norm, self.rms_eps, self.norm_style);
3524 }
3525
3526 if self.dyn_phi_layer == Some(li) {
3530 self.update_dyn_phi(&h);
3531 }
3532 }
3533 crate::gpu::set_layer(-1); h
3536 }
3537
3538 fn update_dyn_phi(&mut self, h: &[f32]) {
3541 const A: f32 = 0.2;
3542 if self.dyn_phi_ema.len() != h.len() {
3543 self.dyn_phi_ema = vec![0.0; h.len()];
3544 self.dyn_phi_seen = 0;
3545 }
3546 if self.dyn_phi_seen == 0 {
3547 self.dyn_phi_ema.copy_from_slice(h);
3548 } else {
3549 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
3550 *e = (1.0 - A) * *e + A * v;
3551 }
3552 }
3553 self.dyn_phi_seen += 1;
3554 }
3555
3556 pub fn dyn_phi(&self) -> &[f32] {
3558 &self.dyn_phi_ema
3559 }
3560
3561 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
3563 self.dyn_phi_layer = layer;
3564 self.dyn_phi_ema.clear();
3565 self.dyn_phi_seen = 0;
3566 }
3567
3568 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
3570 let Some(model) = &self.model else {
3571 return Vec::new();
3572 };
3573 model
3574 .header
3575 .skills
3576 .iter()
3577 .enumerate()
3578 .filter_map(|(i, sk)| {
3579 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
3580 let sel = sk.selection.as_ref()?;
3581 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
3582 })
3583 .collect()
3584 }
3585
3586 pub fn active_skill(&self) -> Option<usize> {
3588 self.dyn_active
3589 }
3590
3591 pub fn enable_dynamic_routing(&mut self) -> usize {
3596 use crate::swarm::{DynRouter, RoutableSkill};
3597 let Some(model) = self.model.clone() else {
3598 return 0;
3599 };
3600 if self.dyn_blend_loaded {
3603 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
3604 return 0;
3605 }
3606 if let Some(a) = self.dyn_active {
3610 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
3611 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
3612 return 0;
3613 }
3614 }
3615 let hidden = self.hidden_size;
3616 let mut skills = Vec::new();
3617 for (idx, id, _phi) in self.dynamic_skills() {
3618 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
3619 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
3620 skills.push(rs);
3621 }
3622 }
3623 }
3624 if skills.is_empty() {
3625 return 0;
3626 }
3627 let phi = skills[0].phi_layer;
3629 if skills.iter().any(|s| s.phi_layer != phi) {
3630 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
3631 }
3632 let n = skills.len();
3633 self.set_dyn_phi_layer(Some(phi));
3634 self.dyn_router = Some(DynRouter::new(skills));
3635 n
3636 }
3637
3638 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
3640 self.dyn_router
3641 .as_ref()
3642 .map(|r| r.switches.clone())
3643 .unwrap_or_default()
3644 }
3645
3646 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
3649 let rows = self.weights.lm_head.rows();
3650 let mut logits = attention::take_buf(rows.min(self.vocab_size));
3651 self.weights
3652 .lm_head
3653 .matvec(hidden, &mut logits, self.pool.as_deref());
3654 logits.resize(self.vocab_size, 0.0);
3655 if let Some(c) = self.final_softcap {
3656 for l in logits.iter_mut() {
3657 *l = c * (*l / c).tanh();
3658 }
3659 }
3660 logits
3661 }
3662
3663 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
3668 self.kv_cache.clear();
3669 let mut hidden = vec![0.0f32; self.hidden_size];
3670 for (pos, &id) in ids.iter().enumerate() {
3671 let emb = self.embed_single(id);
3672 hidden = self.forward_layers(&emb, pos, task_mask);
3673 }
3674 inference::rms_norm_into(
3675 &hidden,
3676 &self.weights.final_norm,
3677 self.rms_eps,
3678 self.norm_style,
3679 &mut self.ws.n1,
3680 );
3681 self.lm_head_forward(&self.ws.n1)
3682 }
3683}
3684
3685pub fn create_test_pipeline(
3687 hidden_size: usize,
3688 intermediate_size: usize,
3689 num_heads: usize,
3690 num_kv_heads: usize,
3691 head_dim: usize,
3692 num_layers: usize,
3693 vocab_size: usize,
3694) -> Pipeline {
3695 let synth = |n: usize, salt: usize| -> Vec<f32> {
3698 (0..n)
3699 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
3700 .collect()
3701 };
3702 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3703 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3704 };
3705 let layer_weights: Vec<LayerWeights> = (0..num_layers)
3706 .map(|li| LayerWeights {
3707 input_norm: vec![1.0; hidden_size],
3708 post_norm: vec![1.0; hidden_size],
3709 attn_out_norm: None,
3710 ffn_out_norm: None,
3711 layer_scale: None,
3712 ffn: FfnKind::Dense(DenseFfn {
3713 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
3714 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
3715 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
3716 act: Act::Silu,
3717 }),
3718 attn: AttnKind::Full {
3719 bias: None,
3720 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
3721 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
3722 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
3723 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
3724 q_norm: None,
3725 k_norm: None,
3726 output_gate: false,
3727 softplus_gate: None,
3728 },
3729 })
3730 .collect();
3731
3732 Pipeline::new(
3733 Tokenizer::byte_level(),
3734 PipelineWeights {
3735 embed_tokens: qt(vocab_size, hidden_size, 100),
3736 layers: layer_weights,
3737 lm_head: qt(vocab_size, hidden_size, 200),
3738 final_norm: vec![1.0; hidden_size],
3739 },
3740 hidden_size,
3741 intermediate_size,
3742 num_heads,
3743 num_kv_heads,
3744 head_dim,
3745 num_layers,
3746 num_layers, false, vocab_size,
3749 1e-6,
3750 10_000.0,
3751 NormStyle::Qwen,
3752 4096,
3753 SamplerConfig {
3754 seed: Some(42),
3755 ..Default::default()
3756 },
3757 )
3758}
3759
3760fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
3763 let inter = d.gate_proj.rows();
3764 let hidden = d.down_proj.rows();
3765 let mut g = vec![0.0f32; b * inter];
3766 d.gate_proj.matmat(xs, b, &mut g, pool);
3767 let mut u = vec![0.0f32; b * inter];
3768 d.up_proj.matmat(xs, b, &mut u, pool);
3769 for i in 0..b * inter {
3770 g[i] = d.act.apply(g[i]) * u[i];
3771 }
3772 let mut out = vec![0.0f32; b * hidden];
3773 d.down_proj.matmat(&g, b, &mut out, pool);
3774 out
3775}
3776
3777fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
3781 let ne = m.experts.len();
3782 let mut logits = vec![0.0f32; b * ne];
3783 m.router.matmat(xs, b, &mut logits, pool);
3784
3785 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
3788 {
3789 let mut st = m.stats.borrow_mut();
3790 if st.len() < ne {
3791 st.resize(ne, 0);
3792 }
3793 for bi in 0..b {
3794 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m);
3795 for &e in &idx {
3796 st[e] += 1;
3797 assign[e].push((bi, p[e] / wsum));
3798 }
3799 }
3800 }
3801
3802 let mut out = vec![0.0f32; b * hidden];
3803 let cols = m.experts[0].gate_proj.cols();
3804 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
3805 let sb = list.len();
3806 let mut sub = vec![0.0f32; sb * cols];
3807 for (k, &(bi, _)) in list.iter().enumerate() {
3808 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
3809 }
3810 let eo = dense_ffn_batch(d, &sub, sb, pool);
3811 for (k, &(bi, w)) in list.iter().enumerate() {
3812 for i in 0..hidden {
3813 out[bi * hidden + i] += w * eo[k * hidden + i];
3814 }
3815 }
3816 };
3817 for e in 0..ne {
3818 if !assign[e].is_empty() {
3819 run_expert(&m.experts[e], &assign[e]);
3820 }
3821 }
3822 if let Some((se, gate)) = &m.shared {
3823 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
3824 let mut gl = vec![0.0f32; b];
3825 gate.matmat(xs, b, &mut gl, pool);
3826 (0..b)
3827 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
3828 .collect()
3829 } else {
3830 (0..b).map(|bi| (bi, 1.0)).collect()
3831 };
3832 run_expert(se, &all);
3833 }
3834 out
3835}
3836
3837thread_local! {
3838 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
3842 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
3843}
3844
3845fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3847 if crate::gpu::enabled_here()
3858 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
3859 {
3860 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
3861 crate::gpu::ProbeArm::Gpu
3862 } else {
3863 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
3864 };
3865 match arm {
3866 crate::gpu::ProbeArm::Gpu => {
3867 let t0 = std::time::Instant::now();
3868 if let Some(out) = dense_ffn_gpu(d, x, pool) {
3869 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3870 return out;
3871 }
3872 }
3873 crate::gpu::ProbeArm::CpuTimed => {
3874 let t0 = std::time::Instant::now();
3875 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3876 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3877 return out;
3878 }
3879 crate::gpu::ProbeArm::Cpu => {
3880 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3881 }
3882 }
3883 }
3884 dense_ffn_cpu(d, x, pool)
3885}
3886
3887fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3889 let inter = d.gate_proj.rows();
3890 FFN_SCRATCH.with(|s| {
3891 let mut s = s.borrow_mut();
3892 let [g, u, ..] = &mut *s;
3893 g.resize(inter, 0.0);
3894 u.resize(inter, 0.0);
3895 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
3897 for i in 0..inter {
3898 g[i] = d.act.apply(g[i]) * u[i];
3899 }
3900 FFN_PROBE.with(|pr| {
3903 if let Some(acc) = pr.borrow_mut().as_mut() {
3904 let li = crate::gpu::cur_layer();
3905 if li >= 0 {
3906 if let Some(row) = acc.get_mut(li as usize) {
3907 for (a, &v) in row.iter_mut().zip(g.iter()) {
3908 *a += (v as f64).abs();
3909 }
3910 }
3911 }
3912 }
3913 });
3914 let mut out = attention::take_buf(d.down_proj.rows());
3915 d.down_proj.matvec(g, &mut out, pool);
3916 out
3917 })
3918}
3919
3920thread_local! {
3921 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
3924 const { std::cell::RefCell::new(None) };
3925}
3926
3927fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
3933 if d.act != Act::Silu {
3935 return None;
3936 }
3937 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
3940 return None;
3941 }
3942 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
3943 let mut model_ref = None;
3944 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
3945 let model = model_ref?;
3946 let hidden = jobs[0].down.1;
3947 let mut out = attention::take_buf(hidden);
3948 if crate::gpu::moe_block(&model, &jobs, &mut out) {
3949 Some(out)
3950 } else {
3951 let mut out = out;
3952 attention::recycle_buf(&mut out);
3953 None
3954 }
3955}
3956
3957#[allow(clippy::type_complexity)]
3962#[allow(clippy::type_complexity)]
3963fn moe_parts(
3964 t: &QTensor,
3965) -> Option<(
3966 &std::sync::Arc<cortiq_core::CmfModel>,
3967 usize,
3968 usize,
3969 usize,
3970 &[f32],
3971 &[f32],
3972 bool,
3973)> {
3974 match t {
3975 QTensor::Mapped {
3976 model,
3977 idx,
3978 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
3979 rows,
3980 cols,
3981 row_scale,
3982 col_field,
3983 ..
3984 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
3985 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
3986 }
3987 QTensor::Mapped {
3989 model,
3990 idx,
3991 dtype: cortiq_core::TensorDtype::Q1,
3992 rows,
3993 cols,
3994 ..
3995 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
3996 _ => None,
3997 }
3998}
3999
4000fn moe_push_job<'a>(
4002 d: &'a DenseFfn,
4003 x: &[f32],
4004 w: f32,
4005 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
4006 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
4007) -> Option<()> {
4008 use crate::qtensor::prescale;
4009 if d.act != Act::Silu {
4010 return None; }
4012 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
4013 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
4014 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
4015 if gq1 != uq1 || uq1 != dq1 {
4016 return None; }
4018 model_ref.get_or_insert_with(|| gm.clone());
4019 let gdt = if gcf.is_empty() {
4020 cortiq_core::TensorDtype::Q8Row
4021 } else {
4022 cortiq_core::TensorDtype::Q8_2f
4023 };
4024 let udt = if ucf.is_empty() {
4025 cortiq_core::TensorDtype::Q8Row
4026 } else {
4027 cortiq_core::TensorDtype::Q8_2f
4028 };
4029 jobs.push(crate::gpu::MoeJob {
4030 gate: (gi, gr, gc, grs),
4031 up: (ui, ur, uc, urs),
4032 down: (di, dr, dc, drs),
4033 xs_gate: prescale(x, gcf, gdt).into_owned(),
4034 xs_up: prescale(x, ucf, udt).into_owned(),
4035 down_col: dcf,
4036 w,
4037 q1: gq1,
4038 });
4039 Some(())
4040}
4041
4042fn sparse_ffn_quant(
4049 d: &DenseFfn,
4050 x: &[f32],
4051 active: &[u16],
4052 hidden: usize,
4053 pool: Option<&Pool>,
4054) -> Vec<f32> {
4055 let n = active.len();
4056 let inter = d.gate_proj.rows();
4057 let mut act = vec![0.0f32; n];
4058 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
4061 let compute = |ai: usize| -> f32 {
4062 let idx = active[ai] as usize;
4063 if idx >= inter {
4064 return 0.0; }
4066 let mut s = if need_scratch {
4067 vec![0.0f32; hidden]
4068 } else {
4069 Vec::new()
4070 };
4071 let gate = d.gate_proj.row_dot(idx, x, &mut s);
4072 let up = d.up_proj.row_dot(idx, x, &mut s);
4073 d.act.apply(gate) * up
4074 };
4075 match pool {
4076 Some(p) if n >= 256 => {
4077 let ptr = SendMut(act.as_mut_ptr());
4078 p.run(&|widx, nw| {
4079 let chunk = n.div_ceil(nw);
4080 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
4081 for ai in s..e {
4082 unsafe { *ptr.at(ai) = compute(ai) };
4083 }
4084 });
4085 }
4086 _ => {
4087 for (ai, a) in act.iter_mut().enumerate() {
4088 *a = compute(ai);
4089 }
4090 }
4091 }
4092 let mut out = vec![0.0f32; hidden];
4094 for (ai, &idx) in active.iter().enumerate() {
4095 let w = act[ai];
4096 if w.abs() >= 1e-12 && (idx as usize) < inter {
4097 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
4098 }
4099 }
4100 out
4101}
4102
4103#[doc(hidden)]
4105pub fn sparse_ffn_quant_for_test(
4106 d: &DenseFfn,
4107 x: &[f32],
4108 active: &[u16],
4109 hidden: usize,
4110) -> Vec<f32> {
4111 sparse_ffn_quant(d, x, active, hidden, None)
4112}
4113
4114fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
4118 let deq = |t: &QTensor| -> Vec<f32> {
4119 let (rows, cols) = (t.rows(), t.cols());
4120 let mut out = vec![0.0f32; rows * cols];
4121 for r in 0..rows {
4122 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
4123 }
4124 out
4125 };
4126 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
4127}
4128
4129struct SendMut(*mut f32);
4131unsafe impl Send for SendMut {}
4132unsafe impl Sync for SendMut {}
4133impl SendMut {
4134 #[inline]
4135 #[allow(clippy::mut_from_ref)]
4138 unsafe fn at(&self, i: usize) -> &mut f32 {
4139 unsafe { &mut *self.0.add(i) }
4140 }
4141}
4142
4143fn moe_route(logits: &[f32], m: &MoeFfn) -> (Vec<usize>, Vec<f32>, f32) {
4153 let ne = logits.len();
4154 let p: Vec<f32> = if m.router_sigmoid {
4155 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
4156 } else {
4157 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4158 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
4159 let s: f32 = e.iter().sum();
4160 for v in &mut e {
4161 *v /= s;
4162 }
4163 e
4164 };
4165 let mut idx: Vec<usize> = (0..ne).collect();
4166 match &m.expert_bias {
4168 Some(b) => idx.sort_unstable_by(|&x, &y| {
4169 (p[y] + b[y])
4170 .partial_cmp(&(p[x] + b[x]))
4171 .unwrap()
4172 .then(x.cmp(&y))
4173 }),
4174 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
4175 }
4176 idx.truncate(m.top_k);
4177 let wsum: f32 = if m.norm_topk_prob {
4178 let s: f32 = idx.iter().map(|&e| p[e]).sum();
4179 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
4182 } else {
4183 1.0 / m.routed_scaling
4184 };
4185 (idx, p, wsum)
4186}
4187
4188fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4191 let ne = m.experts.len();
4192 let mut logits = vec![0.0f32; ne];
4193 m.router.matvec(x, &mut logits, pool);
4194 let (idx, p, wsum) = moe_route(&logits, m);
4195 {
4196 let mut st = m.stats.borrow_mut();
4197 if st.len() < ne {
4198 st.resize(ne, 0);
4199 }
4200 for &e in &idx {
4201 st[e] += 1;
4202 }
4203 }
4204 if crate::gpu::enabled_here() {
4209 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
4210 crate::gpu::ProbeArm::Gpu => {
4211 let t0 = std::time::Instant::now();
4212 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
4213 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4214 return out;
4215 }
4216 }
4217 crate::gpu::ProbeArm::CpuTimed => {
4218 let t0 = std::time::Instant::now();
4219 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4220 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4221 return out;
4222 }
4223 crate::gpu::ProbeArm::Cpu => {
4224 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4225 }
4226 }
4227 }
4228 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
4229}
4230
4231fn moe_ffn_cpu(
4233 m: &MoeFfn,
4234 x: &[f32],
4235 idx: &[usize],
4236 p: &[f32],
4237 wsum: f32,
4238 pool: Option<&Pool>,
4239) -> Vec<f32> {
4240 let mut out = attention::take_buf(x.len());
4241 for &e in idx {
4242 let mut eo = dense_ffn(&m.experts[e], x, pool);
4243 let w = p[e] / wsum;
4244 for i in 0..out.len() {
4245 out[i] += w * eo[i];
4246 }
4247 attention::recycle_buf(&mut eo);
4248 }
4249 if let Some((se, gate)) = &m.shared {
4250 let mut so = dense_ffn(se, x, pool);
4251 let g = gate.as_ref().map_or(1.0, |gate| {
4252 let mut gl = [0.0f32; 1];
4253 gate.matvec(x, &mut gl, pool);
4254 1.0 / (1.0 + (-gl[0]).exp())
4255 });
4256 for i in 0..out.len() {
4257 out[i] += g * so[i];
4258 }
4259 attention::recycle_buf(&mut so);
4260 }
4261 out
4262}
4263
4264fn moe_ffn_gpu(
4267 m: &MoeFfn,
4268 x: &[f32],
4269 idx: &[usize],
4270 p: &[f32],
4271 wsum: f32,
4272 pool: Option<&Pool>,
4273) -> Option<Vec<f32>> {
4274 use crate::gpu::MoeJob;
4275
4276 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
4277 let mut model_ref = None;
4278 for &e in idx {
4279 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
4280 }
4281 if let Some((se, gate)) = &m.shared {
4282 let g = gate.as_ref().map_or(1.0, |gate| {
4283 let mut gl = [0.0f32; 1];
4284 gate.matvec(x, &mut gl, pool);
4285 1.0 / (1.0 + (-gl[0]).exp())
4286 });
4287 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
4288 }
4289 let model = model_ref?;
4290 let hidden = jobs[0].down.1;
4291 let mut out = vec![0.0f32; hidden];
4292 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
4293}
4294
4295fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4297 match ffn {
4298 FfnKind::Dense(d) => dense_ffn(d, x, pool),
4299 FfnKind::Moe(m) => moe_ffn(m, x, pool),
4300 }
4301}
4302
4303fn ffn_forward_pair(
4307 ffn: &FfnKind,
4308 x1: &[f32],
4309 x2: &[f32],
4310 pool: Option<&Pool>,
4311) -> (Vec<f32>, Vec<f32>) {
4312 let d = match ffn {
4313 FfnKind::Dense(d) => d,
4314 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
4315 };
4316 let inter = d.gate_proj.rows();
4317 FFN_SCRATCH.with(|s| {
4318 let mut s = s.borrow_mut();
4319 let [g1, g2, u1, u2] = &mut *s;
4320 g1.resize(inter, 0.0);
4321 g2.resize(inter, 0.0);
4322 u1.resize(inter, 0.0);
4323 u2.resize(inter, 0.0);
4324 QTensor::matvec2_many(
4327 [&d.gate_proj, &d.up_proj],
4328 x1,
4329 x2,
4330 [g1.as_mut_slice(), u1.as_mut_slice()],
4331 [g2.as_mut_slice(), u2.as_mut_slice()],
4332 pool,
4333 );
4334 for i in 0..inter {
4335 g1[i] = d.act.apply(g1[i]) * u1[i];
4336 g2[i] = d.act.apply(g2[i]) * u2[i];
4337 }
4338 let mut o1 = attention::take_buf(d.down_proj.rows());
4339 let mut o2 = attention::take_buf(d.down_proj.rows());
4340 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
4341 (o1, o2)
4342 })
4343}
4344
4345#[cfg(test)]
4346mod tests {
4347 use super::*;
4348
4349 #[test]
4355 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
4356 let (hidden, inter) = (16usize, 40usize);
4357 let synth = |n: usize, salt: usize| -> Vec<f32> {
4358 (0..n)
4359 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
4360 .collect()
4361 };
4362 let d = DenseFfn {
4363 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
4364 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
4365 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
4366 act: Act::Silu,
4367 };
4368 let x = synth(hidden, 9);
4369 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
4371
4372 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
4373
4374 let mut g = vec![0.0f32; inter];
4376 d.gate_proj.matvec(&x, &mut g, None);
4377 let mut u = vec![0.0f32; inter];
4378 d.up_proj.matvec(&x, &mut u, None);
4379 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
4380 for i in 0..inter {
4381 g[i] = if act_set.contains(&(i as u16)) {
4382 inference::silu(g[i]) * u[i]
4383 } else {
4384 0.0
4385 };
4386 }
4387 let mut reference = vec![0.0f32; hidden];
4388 d.down_proj.matvec(&g, &mut reference, None);
4389
4390 let max_d = sparse
4391 .iter()
4392 .zip(&reference)
4393 .map(|(a, b)| (a - b).abs())
4394 .fold(0.0f32, f32::max);
4395 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
4396 }
4397
4398 fn attach_test_mtp(p: &mut Pipeline) {
4400 let (h, inter, heads, kv, hd) = (
4401 p.hidden_size,
4402 p.intermediate_size,
4403 p.num_heads,
4404 p.num_kv_heads,
4405 p.head_dim,
4406 );
4407 let synth = |n: usize, salt: usize| -> Vec<f32> {
4408 (0..n)
4409 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
4410 .collect()
4411 };
4412 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4413 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4414 };
4415 p.mtp = Some(MtpModule {
4416 enorm: vec![1.0; h],
4417 hnorm: vec![1.0; h],
4418 eh_proj: qt(h, 2 * h, 301),
4419 layer: LayerWeights {
4420 input_norm: vec![1.0; h],
4421 post_norm: vec![1.0; h],
4422 attn_out_norm: None,
4423 ffn_out_norm: None,
4424 layer_scale: None,
4425 ffn: FfnKind::Dense(DenseFfn {
4426 gate_proj: qt(inter, h, 315),
4427 up_proj: qt(inter, h, 316),
4428 down_proj: qt(h, inter, 317),
4429 act: Act::Silu,
4430 }),
4431 attn: AttnKind::Full {
4432 bias: None,
4433 wq: qt(heads * hd, h, 311),
4434 wk: qt(kv * hd, h, 312),
4435 wv: qt(kv * hd, h, 313),
4436 wo: qt(h, heads * hd, 314),
4437 q_norm: None,
4438 k_norm: None,
4439 output_gate: false,
4440 softplus_gate: None,
4441 },
4442 },
4443 final_norm: vec![1.0; h],
4444 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
4445 });
4446 }
4447
4448 #[test]
4449 fn speculative_equals_vanilla_greedy() {
4450 let run = |spec: bool| {
4451 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4452 p.sampler_config.temperature = 0.0;
4453 attach_test_mtp(&mut p);
4454 p.speculative = spec;
4455 let r = p.generate("abcdef", 12, None, None).unwrap();
4456 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
4457 };
4458 let (vanilla, d0, _) = run(false);
4459 let (spec, d1, a1) = run(true);
4460 assert_eq!(d0, 0, "vanilla path must not draft");
4461 assert!(d1 > 0, "speculative path must draft");
4462 assert_eq!(
4463 vanilla, spec,
4464 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
4465 );
4466 }
4467
4468 #[test]
4469 fn speculative_accepts_constant_oracle() {
4470 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4471 p.sampler_config.temperature = 0.0;
4472 p.sampler_config.repetition_penalty = 1.0;
4473 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
4476 attach_test_mtp(&mut p);
4477 p.speculative = true;
4478 let r = p.generate("abcd", 10, None, None).unwrap();
4479 assert!(r.mtp_drafted > 0);
4480 assert_eq!(
4481 r.mtp_accepted, r.mtp_drafted,
4482 "constant logits → every draft accepted"
4483 );
4484 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
4487 }
4488
4489 #[test]
4490 fn empty_prompt_is_an_error_not_a_panic() {
4491 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4492 let r = p.generate("", 4, None, None);
4493 assert!(r.is_err(), "empty prompt must be a clean error");
4494 }
4495
4496 #[test]
4497 fn every_token_enters_kv_exactly_once() {
4498 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4499 p.sampler_config.temperature = 0.0;
4501 let r = p.generate("abc", 2, None, None).unwrap();
4502 assert_eq!(r.prompt_tokens, 3);
4503 assert_eq!(
4507 p.kv_cache.seq_len(),
4508 3 + r.tokens_generated - 1,
4509 "each token must be cached exactly once (v1 cached the last prompt token twice)"
4510 );
4511 }
4512
4513 #[test]
4514 fn generation_is_reproducible_with_seed() {
4515 let run = || {
4516 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4517 p.generate("hello", 8, None, None).unwrap().token_ids
4518 };
4519 assert_eq!(run(), run());
4520 }
4521
4522 #[test]
4523 fn resetting_sampler_restarts_the_seeded_stream() {
4524 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4525 let config = SamplerConfig {
4526 seed: Some(1234),
4527 ..SamplerConfig::default()
4528 };
4529 p.set_sampler_config(config.clone());
4530 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
4531 p.set_sampler_config(config);
4532 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
4533 assert_eq!(first, second);
4534 }
4535
4536 #[test]
4537 fn eviction_bounds_the_cache() {
4538 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4539 p.kv_cache.max_seq_len = 6;
4540 p.sampler_config.temperature = 0.0;
4541 let _ = p.generate("abcd", 12, None, None).unwrap();
4542 assert!(
4543 p.kv_cache.seq_len() <= 6 + 1,
4544 "cache must stay bounded by max_seq_len (got {})",
4545 p.kv_cache.seq_len()
4546 );
4547 }
4548
4549 #[test]
4550 fn confidence_matches_tokens_and_is_a_probability() {
4551 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4552 p.sampler_config.temperature = 0.0;
4553 p.sampler_config.repetition_penalty = 1.0;
4554 let r = p.generate("abcd", 10, None, None).unwrap();
4555 assert_eq!(
4556 r.token_confidence.len(),
4557 r.token_ids.len(),
4558 "one confidence per emitted token"
4559 );
4560 for &c in &r.token_confidence {
4561 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
4562 }
4563 let logits = [1.0f32, 3.0, 0.5, 3.0];
4565 let p0 = top1_prob_t(&logits, 1, 1.0);
4566 let p1 = top1_prob_t(&logits, 3, 1.0);
4567 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
4568 assert!(p0 > 0.0 && p0 < 1.0);
4569 let sharp = top1_prob_t(&logits, 1, 1.0);
4571 let soft = top1_prob_t(&logits, 1, 2.0);
4572 assert!(soft < sharp, "higher temperature lowers peak confidence");
4573 }
4574
4575 #[test]
4576 fn trace_is_opt_in_and_parallels_the_output() {
4577 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4579 p.sampler_config.temperature = 0.0;
4580 p.sampler_config.repetition_penalty = 1.0;
4581 let r = p.generate("abcd", 10, None, None).unwrap();
4582 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
4583
4584 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4586 p.sampler_config.temperature = 0.0;
4587 p.sampler_config.repetition_penalty = 1.0;
4588 p.set_trace(true);
4589 let r = p.generate("abcd", 10, None, None).unwrap();
4590 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
4591 for (i, tr) in r.traces.iter().enumerate() {
4592 assert_eq!(tr.t, i, "trace index is sequential");
4593 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
4594 assert_eq!(
4595 tr.confidence, r.token_confidence[i],
4596 "trace confidence matches the confidence channel"
4597 );
4598 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
4600 }
4601 }
4602
4603 #[test]
4604 fn explain_prefill_logits_match_greedy_first_token() {
4605 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4609 p.sampler_config.temperature = 0.0;
4610 p.sampler_config.repetition_penalty = 1.0;
4611 let ids = p.tokenizer.encode("abcd");
4612 let logits = p.prefill_next_logits(&ids, None);
4613 let argmax = logits
4614 .iter()
4615 .enumerate()
4616 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4617 .unwrap()
4618 .0 as u32;
4619 let r = p.generate("abcd", 1, None, None).unwrap();
4620 assert_eq!(
4621 argmax, r.token_ids[0],
4622 "explain preview must match greedy emit"
4623 );
4624 }
4625
4626 #[test]
4627 fn laguna_shared_expert_is_unconditionally_added() {
4628 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
4629 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
4630 let zero_dense = || DenseFfn {
4631 gate_proj: matrix(vec![0.0; 4]),
4632 up_proj: matrix(vec![0.0; 4]),
4633 down_proj: matrix(vec![0.0; 4]),
4634 act: Act::Silu,
4635 };
4636 let shared = DenseFfn {
4637 gate_proj: identity(),
4638 up_proj: identity(),
4639 down_proj: identity(),
4640 act: Act::Silu,
4641 };
4642 let x = [1.0, 2.0];
4643 let expected = dense_ffn(&shared, &x, None);
4644 let moe = MoeFfn {
4645 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
4646 experts: vec![zero_dense()],
4647 top_k: 1,
4648 norm_topk_prob: true,
4649 router_sigmoid: true,
4650 expert_bias: None,
4651 routed_scaling: 1.0,
4652 shared: Some((shared, None)),
4653 stats: std::cell::RefCell::new(Vec::new()),
4654 };
4655 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
4656 for (actual, expected) in actual.iter().zip(expected) {
4657 assert!((actual - expected).abs() < 1e-6);
4658 }
4659 }
4660}