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