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