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
1052 && !matches!(
1053 self.weights.layers[self.phys_layer(li)].attn,
1054 AttnKind::Full { .. }
1055 )
1056 {
1057 *f = false;
1058 }
1059 }
1060 flags
1061 }
1062 None => Vec::new(),
1063 };
1064 if let Some(c) = &cfg {
1065 let n = self.o1_flags.iter().filter(|&&f| f).count();
1066 tracing::info!(
1067 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1068 self.num_layers,
1069 c.m,
1070 c.w,
1071 c.sink,
1072 c.rect
1073 );
1074 }
1075 self.o1_cfg = cfg;
1076 }
1077
1078 pub fn o1_active(&self) -> bool {
1080 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1081 }
1082
1083 fn o1_begin(&mut self) {
1085 if let Some(c) = &self.o1_cfg {
1086 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1087 for (li, &f) in self.o1_flags.iter().enumerate() {
1088 if f {
1089 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1090 }
1091 }
1092 }
1093 }
1094
1095 fn o1_seal(&mut self) {
1098 if self.o1_cfg.is_none() {
1099 return;
1100 }
1101 for li in 0..self.num_layers {
1102 if self.o1_flags.get(li).copied().unwrap_or(false) {
1103 self.kv_cache.layers[li].o1_seal(self.num_heads);
1104 }
1105 }
1106 }
1107
1108 pub fn set_trace(&mut self, on: bool) {
1110 self.trace = on;
1111 }
1112
1113 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1116 self.rng = match config.seed {
1117 Some(seed) => SplitMix64::new(seed),
1118 None => SplitMix64::from_entropy(),
1119 };
1120 self.sampler_config = config;
1121 }
1122
1123 pub fn set_confidence(&mut self, on: bool) {
1128 self.confidence_on = on;
1129 }
1130
1131 pub fn set_calib_temp(&mut self, t: f32) {
1134 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1135 }
1136
1137 pub fn calib_temp(&self) -> f32 {
1139 self.calib_temp
1140 }
1141
1142 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1145 self.rotary_dim = rotary_dim.min(self.head_dim);
1146 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1147 }
1148
1149 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1150 QwenAttnCfg {
1151 num_heads: self.num_heads,
1152 num_kv_heads: self.num_kv_heads,
1153 head_dim: self.head_dim,
1154 hidden_size: self.hidden_size,
1155 position,
1156 inv_freq: &self.inv_freq,
1157 rotary_dim: self.rotary_dim,
1158 scale: self.attn_scale,
1159 window: None,
1160 v_norm: false,
1161 q_norm: None,
1162 k_norm: None,
1163 output_gate: false,
1164 softplus_gate: None,
1165 rope_scale: self.rope_scale,
1166 bias: None,
1167 rms_eps: self.rms_eps,
1168 norm_style: self.norm_style,
1169 pool: self.pool.as_deref(),
1170 }
1171 }
1172
1173 pub fn generate(
1175 &mut self,
1176 prompt: &str,
1177 max_tokens: usize,
1178 task_mask: Option<&TaskMask>,
1179 on_token: Option<TokenCallback>,
1180 ) -> Result<GenerateResult, String> {
1181 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1182 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1183 }
1184
1185 pub fn generate_from_ids(
1193 &mut self,
1194 input_ids: &[u32],
1195 max_tokens: usize,
1196 task_mask: Option<&TaskMask>,
1197 mut on_token: Option<TokenCallback>,
1198 ) -> Result<GenerateResult, String> {
1199 if std::env::var("CMF_TRACE_H").is_ok() {
1200 eprintln!("input_ids: {input_ids:?}");
1201 }
1202 if input_ids.is_empty() {
1203 return Err("empty prompt: nothing to generate from".to_string());
1204 }
1205
1206 self.kv_cache.clear();
1208 crate::gpu::graph_kv_reset(self.graph_kv_id);
1209 self.o1_begin();
1210
1211 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1217 .map(|v| v != "0")
1218 .unwrap_or_else(|_| {
1219 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
1220 });
1221 let spec_active = self.speculative
1222 && self.mtp.is_some()
1223 && task_mask.is_none()
1224 && !self.o1_active()
1225 && !graph_on
1226 && self.sampler_config.temperature < 1e-6;
1227 let mut mtp = if spec_active { self.mtp.take() } else { None };
1230 if let Some(m) = &mut mtp {
1231 m.kv.clear();
1232 }
1233 let mut router = if mtp.is_none() {
1237 self.dyn_router.take()
1238 } else {
1239 None
1240 };
1241 if let Some(r) = &mut router {
1242 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1245 }
1246
1247 let mut all_ids = input_ids.to_vec();
1248 let mut generated = 0usize;
1249 let mut finish_reason = "max_tokens".to_string();
1250 let mut drafted = 0usize;
1251 let mut accepted = 0usize;
1252 let mut confidence: Vec<f32> = Vec::new();
1253 let trace_on = self.trace;
1254 let calib_temp = self.calib_temp;
1255 let mut traces: Vec<TokenTrace> = Vec::new();
1256
1257 let mut hidden = vec![0.0f32; self.hidden_size];
1263 let mut pos = 0usize;
1264 let fuse_lm = mtp.is_none() && router.is_none();
1268 self.graph_logits = None;
1269 self.graph_want_logits = false;
1270 let dyn_prefill = router.is_some();
1275 let graph_prefill = self.graph_prefill_preferred();
1281 if task_mask.is_none()
1282 && !dyn_prefill
1283 && !graph_prefill
1284 && prefill_batched()
1285 && input_ids.len() > 2
1286 {
1287 let chunk = prefill_chunk();
1293 let hs = self.hidden_size;
1294 while pos < input_ids.len() {
1295 let end = (pos + chunk).min(input_ids.len());
1296 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1297 if let Some(m) = &mut mtp {
1298 for p in pos..end {
1299 if p + 1 < input_ids.len() {
1300 let _ = self.mtp_step(
1301 m,
1302 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1303 input_ids[p + 1],
1304 p,
1305 );
1306 }
1307 }
1308 }
1309 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1310 pos = end;
1311 }
1312 }
1313 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1314 while pos + 1 < input_ids.len() {
1315 let e1 = self.embed_single(input_ids[pos]);
1316 let e2 = self.embed_single(input_ids[pos + 1]);
1317 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1318 self.commit_linear_scratch();
1320 if let Some(m) = &mut mtp {
1321 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1322 if pos + 2 < input_ids.len() {
1323 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1324 }
1325 }
1326 hidden = h2;
1327 pos += 2;
1328 }
1329 }
1330 let _tpf = std::time::Instant::now();
1339 let batch_k = std::env::var("CMF_BATCH_K")
1340 .ok()
1341 .and_then(|v| v.parse::<usize>().ok())
1342 .unwrap_or(0);
1343 if batch_k > 0
1344 && graph_prefill
1345 && task_mask.is_none()
1346 && mtp.is_none()
1347 && !dyn_prefill
1348 && pos + 1 < input_ids.len()
1349 {
1350 let hs = self.hidden_size;
1351 let chunk = batch_k;
1352 while pos < input_ids.len() {
1353 let end = (pos + chunk).min(input_ids.len());
1354 let bk = end - pos;
1355 let mut hiddens = vec![0f32; bk * hs];
1356 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1357 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1358 }
1359 let positions: Vec<usize> = (pos..end).collect();
1360 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1361 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1362 pos = end;
1363 } else {
1364 break; }
1366 }
1367 }
1368 while pos < input_ids.len() {
1369 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1370 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1371 if let Some(m) = &mut mtp {
1372 if pos + 1 < input_ids.len() {
1373 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1374 }
1375 }
1376 pos += 1;
1377 }
1378 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1379 eprintln!(
1380 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1381 input_ids.len(),
1382 _tpf.elapsed().as_secs_f64() * 1000.0
1383 );
1384 }
1385 self.o1_seal();
1388
1389 macro_rules! commit {
1391 ($id:expr) => {{
1392 all_ids.push($id);
1393 generated += 1;
1394 if self.tokenizer.is_eos($id) {
1395 finish_reason = "stop".to_string();
1396 false
1397 } else {
1398 let token_text = self.tokenizer.decode_token($id);
1399 let mut go = true;
1400 if let Some(ref mut cb) = on_token {
1401 if !cb(&token_text) {
1402 finish_reason = "cancelled".to_string();
1403 go = false;
1404 }
1405 }
1406 go
1407 }
1408 }};
1409 }
1410
1411 let mut next_pos = input_ids.len();
1413 'decode: while generated < max_tokens {
1414 let mut logits = match self.graph_logits.take() {
1415 Some(lg) => lg,
1416 None => {
1417 inference::rms_norm_into(
1418 &hidden,
1419 &self.weights.final_norm,
1420 self.rms_eps,
1421 self.norm_style,
1422 &mut self.ws.n1,
1423 );
1424 self.lm_head_forward(&self.ws.n1)
1425 }
1426 };
1427 let t_next = sampler::sample_with_scratch(
1428 &logits,
1429 &self.sampler_config,
1430 &all_ids,
1431 &mut self.rng,
1432 &mut self.sampler_scratch,
1433 );
1434 if self.confidence_on {
1435 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1436 }
1437 attention::recycle_buf(&mut logits);
1438 if trace_on {
1439 let skill = router.as_ref().and_then(|r| r.active_id());
1443 traces.push(TokenTrace {
1444 t: generated,
1445 token_id: t_next,
1446 confidence: confidence.last().copied().unwrap_or(0.0),
1447 active_skill: skill,
1448 recon: None,
1449 switched: false,
1450 });
1451 }
1452 if !commit!(t_next) {
1453 break 'decode;
1454 }
1455 if generated >= max_tokens {
1456 break 'decode;
1457 }
1458
1459 if self.kv_cache.needs_eviction() {
1460 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1461 self.kv_cache.evict(keep);
1462 }
1463
1464 match &mut mtp {
1465 Some(m) if generated + 1 < max_tokens => {
1467 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1468 drafted += 1;
1469 let emb1 = self.embed_single(t_next);
1470 let emb2 = self.embed_single(draft);
1471 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1472
1473 inference::rms_norm_into(
1474 &h1,
1475 &self.weights.final_norm,
1476 self.rms_eps,
1477 self.norm_style,
1478 &mut self.ws.n1,
1479 );
1480 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1481 let t_after = sampler::sample_with_scratch(
1482 &logits1,
1483 &self.sampler_config,
1484 &all_ids,
1485 &mut self.rng,
1486 &mut self.sampler_scratch,
1487 );
1488 if self.confidence_on {
1489 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1490 }
1491 attention::recycle_buf(&mut logits1);
1492 if trace_on {
1493 traces.push(TokenTrace {
1496 t: generated,
1497 token_id: t_after,
1498 confidence: confidence.last().copied().unwrap_or(0.0),
1499 active_skill: None,
1500 recon: None,
1501 switched: false,
1502 });
1503 }
1504 let stop = !commit!(t_after);
1505
1506 if t_after == draft {
1507 accepted += 1;
1508 self.commit_linear_scratch();
1509 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1510 hidden = h2;
1511 next_pos += 2;
1512 } else {
1513 for layer in &mut self.kv_cache.layers {
1515 layer.truncate_last(1);
1516 }
1517 if !stop {
1518 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1519 hidden = self.forward_layers(
1520 &self.embed_single(t_after),
1521 next_pos + 1,
1522 None,
1523 );
1524 }
1525 next_pos += 2;
1526 }
1527 if stop {
1528 break 'decode;
1529 }
1530 }
1531 _ => {
1533 self.graph_want_logits = fuse_lm;
1534 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1535 next_pos += 1;
1536 if let Some(r) = &mut router {
1539 let phi = self.dyn_phi_ema.clone();
1540 let decision = r.step(&phi, generated);
1541 if let Some(new_active) = decision {
1542 let _ = self.set_active_skill(new_active);
1543 }
1544 if trace_on {
1547 if let Some(last) = traces.last_mut() {
1548 let e = r.last_best_e();
1549 last.recon = e.is_finite().then_some(e);
1550 last.switched = decision.is_some();
1551 }
1552 }
1553 }
1554 }
1555 }
1556 }
1557
1558 self.graph_want_logits = false;
1559 self.graph_logits = None;
1560 if router.is_some() {
1562 let _ = self.set_active_skill(None);
1563 }
1564 self.dyn_router = router.or(self.dyn_router.take());
1565 self.mtp = mtp.or(self.mtp.take());
1566
1567 let output_ids = &all_ids[input_ids.len()..];
1568 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1570 Ok(GenerateResult {
1571 text: self.tokenizer.decode(output_ids),
1572 token_ids: output_ids.to_vec(),
1573 prompt_tokens: input_ids.len(),
1574 tokens_generated: generated,
1575 finish_reason,
1576 mtp_drafted: drafted,
1577 mtp_accepted: accepted,
1578 token_confidence: confidence,
1579 traces,
1580 })
1581 }
1582
1583 fn mtp_step(
1587 &mut self,
1588 m: &mut MtpModule,
1589 hidden: &[f32],
1590 next_token: u32,
1591 position: usize,
1592 ) -> u32 {
1593 let e = self.embed_single(next_token);
1597 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1598 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1599 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1600 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1601 let mut x = vec![0.0f32; self.hidden_size];
1602 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1603
1604 let lw = &m.layer;
1606 inference::rms_norm_into(
1607 &x,
1608 &lw.input_norm,
1609 self.rms_eps,
1610 self.norm_style,
1611 &mut self.ws.n1,
1612 );
1613 let attn = match &lw.attn {
1614 AttnKind::Full {
1615 wq,
1616 wk,
1617 wv,
1618 wo,
1619 q_norm,
1620 k_norm,
1621 output_gate,
1622 softplus_gate,
1623 bias,
1624 } => {
1625 let mut cfg = self.attn_cfg(position);
1626 cfg.q_norm = q_norm.as_deref();
1627 cfg.k_norm = k_norm.as_deref();
1628 cfg.output_gate = *output_gate;
1629 cfg.softplus_gate = softplus_gate
1630 .as_ref()
1631 .map(|(gate, per_head)| (gate, *per_head));
1632 cfg.bias = bias
1633 .as_ref()
1634 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1635 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1636 }
1637 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1638 unreachable!("MTP block is full attention")
1639 }
1640 };
1641 for (i, &a) in attn.iter().enumerate() {
1642 x[i] += a;
1643 }
1644 inference::rms_norm_into(
1645 &x,
1646 &lw.post_norm,
1647 self.rms_eps,
1648 self.norm_style,
1649 &mut self.ws.p1,
1650 );
1651 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1652 for (i, &f) in ffn.iter().enumerate() {
1653 x[i] += f;
1654 }
1655
1656 inference::rms_norm_into(
1657 &x,
1658 &m.final_norm,
1659 self.rms_eps,
1660 self.norm_style,
1661 &mut self.ws.n1,
1662 );
1663 let mut lg = self.lm_head_forward(&self.ws.n1);
1664 let draft = sampler::argmax(&lg);
1665 attention::recycle_buf(&mut lg);
1666 draft
1667 }
1668
1669 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1673 let emb1 = self.embed_single(1);
1674 let emb2 = self.embed_single(2);
1675 let pos = self.kv_cache.seq_len();
1676
1677 let t0 = std::time::Instant::now();
1678 for _ in 0..iters {
1679 let _ = self.forward_layers(&emb1, pos, None);
1680 let _ = self.forward_layers(&emb2, pos + 1, None);
1681 for l in &mut self.kv_cache.layers {
1682 l.truncate_last(2);
1683 }
1684 }
1685 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1686
1687 let t1 = std::time::Instant::now();
1688 for _ in 0..iters {
1689 let _ = self.forward_pair(&emb1, &emb2, pos);
1690 for l in &mut self.kv_cache.layers {
1691 l.truncate_last(2);
1692 }
1693 }
1694 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1695 (singles_ms, pair_ms)
1696 }
1697
1698 fn forward_pair(
1703 &mut self,
1704 emb1: &[f32],
1705 emb2: &[f32],
1706 position: usize,
1707 ) -> (Vec<f32>, Vec<f32>) {
1708 let mut h1 = emb1.to_vec();
1709 let mut h2 = emb2.to_vec();
1710 let (_nkv, _hd, hs, _rd, eps) = (
1711 self.num_kv_heads,
1712 self.head_dim,
1713 self.hidden_size,
1714 self.rotary_dim,
1715 self.rms_eps,
1716 );
1717 let pool = self.pool.clone();
1718
1719 for li in 0..self.num_layers {
1720 let lw = &self.weights.layers[self.phys_layer(li)];
1721 inference::rms_norm_into(
1724 &h1,
1725 &lw.input_norm,
1726 self.rms_eps,
1727 self.norm_style,
1728 &mut self.ws.n1,
1729 );
1730 inference::rms_norm_into(
1731 &h2,
1732 &lw.input_norm,
1733 self.rms_eps,
1734 self.norm_style,
1735 &mut self.ws.n2,
1736 );
1737
1738 let (a1, a2) = match &lw.attn {
1739 AttnKind::Linear(w) => {
1740 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1741 let layer = &mut self.kv_cache.layers[li];
1742 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1743 vmf_phase_pair(
1744 &self.ws.n1,
1745 &self.ws.n2,
1746 w,
1747 &cfg,
1748 state,
1749 scratch,
1750 self.pool.as_deref(),
1751 )
1752 }
1753 AttnKind::LinearGdn(w) => {
1754 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1755 let layer = &mut self.kv_cache.layers[li];
1756 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1757 gdn_pair(
1758 &self.ws.n1,
1759 &self.ws.n2,
1760 w,
1761 &cfg,
1762 state,
1763 scratch,
1764 self.pool.as_deref(),
1765 )
1766 }
1767 AttnKind::ShortConv(w) => {
1768 let cfg = self
1769 .short_conv_cfg
1770 .expect("short-conv layer without short_conv_cfg");
1771 let layer = &mut self.kv_cache.layers[li];
1772 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1773 short_conv_pair(
1774 &self.ws.n1,
1775 &self.ws.n2,
1776 w,
1777 &cfg,
1778 state,
1779 scratch,
1780 self.pool.as_deref(),
1781 )
1782 }
1783 AttnKind::Full {
1784 wq,
1785 wk,
1786 wv,
1787 wo,
1788 q_norm,
1789 k_norm,
1790 output_gate,
1791 softplus_gate,
1792 bias,
1793 } => {
1794 let inv_freq_l = self.layer_inv_freq(li);
1795 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1796 let cfg = QwenAttnCfg {
1797 num_heads: self.layer_num_heads(li),
1798 num_kv_heads: nkv_l,
1799 head_dim: hd_l,
1800 hidden_size: hs,
1801 position,
1802 inv_freq: &inv_freq_l,
1803 rotary_dim: rd_l,
1804 scale: self.attn_scale,
1805 window: self.layer_window(li),
1806 v_norm: self.attn_v_norm,
1807 q_norm: q_norm.as_deref(),
1808 k_norm: k_norm.as_deref(),
1809 output_gate: *output_gate,
1810 softplus_gate: softplus_gate
1811 .as_ref()
1812 .map(|(gate, per_head)| (gate, *per_head)),
1813 rope_scale: self.layer_rope_scale(li),
1814 bias: bias
1815 .as_ref()
1816 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1817 rms_eps: eps,
1818 norm_style: self.norm_style,
1819 pool: pool.as_deref(),
1820 };
1821 attention::qwen_attention_pair(
1822 &self.ws.n1,
1823 &self.ws.n2,
1824 wq,
1825 wk,
1826 wv,
1827 wo,
1828 &mut self.kv_cache.layers[li],
1829 &cfg,
1830 )
1831 }
1832 };
1833 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
1834 Some(w) => (
1835 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1836 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1837 ),
1838 None => (a1, a2),
1839 };
1840 for i in 0..self.hidden_size {
1841 h1[i] += a1[i];
1842 h2[i] += a2[i];
1843 }
1844 let (mut a1, mut a2) = (a1, a2);
1845 attention::recycle_buf(&mut a1);
1846 attention::recycle_buf(&mut a2);
1847
1848 let lw = &self.weights.layers[self.phys_layer(li)];
1849 inference::rms_norm_into(
1850 &h1,
1851 &lw.post_norm,
1852 self.rms_eps,
1853 self.norm_style,
1854 &mut self.ws.p1,
1855 );
1856 inference::rms_norm_into(
1857 &h2,
1858 &lw.post_norm,
1859 self.rms_eps,
1860 self.norm_style,
1861 &mut self.ws.p2,
1862 );
1863 let (f1, f2) =
1864 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1865 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
1866 Some(w) => (
1867 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1868 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1869 ),
1870 None => (f1, f2),
1871 };
1872 for i in 0..self.hidden_size {
1873 h1[i] += f1[i];
1874 h2[i] += f2[i];
1875 }
1876 let (mut f1, mut f2) = (f1, f2);
1877 attention::recycle_buf(&mut f1);
1878 attention::recycle_buf(&mut f2);
1879 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
1880 for i in 0..self.hidden_size {
1881 h1[i] *= sc;
1882 h2[i] *= sc;
1883 }
1884 }
1885 if self.is_loop_end(li) && li + 1 < self.num_layers {
1887 h1 = inference::rms_norm(
1888 &h1,
1889 &self.weights.final_norm,
1890 self.rms_eps,
1891 self.norm_style,
1892 );
1893 h2 = inference::rms_norm(
1894 &h2,
1895 &self.weights.final_norm,
1896 self.rms_eps,
1897 self.norm_style,
1898 );
1899 }
1900 }
1901 (h1, h2)
1902 }
1903
1904 fn commit_linear_scratch(&mut self) {
1906 for layer in &mut self.kv_cache.layers {
1907 if !layer.linear_scratch.is_empty() {
1908 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1909 layer.linear_scratch.clear();
1910 }
1911 }
1912 }
1913
1914 pub fn forward_ids(
1917 &mut self,
1918 ids: &[u32],
1919 task_mask: Option<&TaskMask>,
1920 ) -> Result<Vec<f32>, String> {
1921 if ids.is_empty() {
1922 return Err("empty id sequence".to_string());
1923 }
1924 self.kv_cache.clear();
1925 self.o1_begin();
1926 let mut hidden = vec![0.0f32; self.hidden_size];
1927 let mut pos = 0usize;
1928 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1929 let chunk = prefill_chunk();
1933 let hs = self.hidden_size;
1934 while pos < ids.len() {
1935 let end = (pos + chunk).min(ids.len());
1936 let hb = self.prefill_batch(&ids[pos..end], pos);
1937 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1938 pos = end;
1939 }
1940 }
1941 if task_mask.is_none() {
1942 while pos + 1 < ids.len() {
1943 let e1 = self.embed_single(ids[pos]);
1944 let e2 = self.embed_single(ids[pos + 1]);
1945 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1946 self.commit_linear_scratch();
1947 hidden = h2;
1948 pos += 2;
1949 }
1950 }
1951 while pos < ids.len() {
1952 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1953 pos += 1;
1954 }
1955 self.o1_seal();
1959 let normed = inference::rms_norm(
1960 &hidden,
1961 &self.weights.final_norm,
1962 self.rms_eps,
1963 self.norm_style,
1964 );
1965 Ok(self.lm_head_forward(&normed))
1966 }
1967
1968 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1975 let (nll, cnt) = self.nll_ids_from(ids, 0);
1976 (nll / cnt.max(1) as f64).exp()
1977 }
1978
1979 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
1984 self.kv_cache.clear();
1985 FFN_PROBE.with(|p| {
1986 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
1987 });
1988 crate::gpu::cpu_scope(|| {
1989 for (pos, &id) in ids.iter().enumerate() {
1990 let emb = self.embed_single(id);
1991 let _ = self.forward_layers(&emb, pos, None);
1992 }
1993 });
1994 self.kv_cache.clear();
1995 FFN_PROBE
1996 .with(|p| p.borrow_mut().take())
1997 .unwrap_or_default()
1998 }
1999
2000 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2004 self.kv_cache.clear();
2005 let mut nll = 0f64;
2006 let mut cnt = 0usize;
2007 let mut hidden = vec![0f32; self.hidden_size];
2008 for (pos, &id) in ids.iter().enumerate() {
2009 if pos > 0 {
2010 inference::rms_norm_into(
2011 &hidden,
2012 &self.weights.final_norm,
2013 self.rms_eps,
2014 self.norm_style,
2015 &mut self.ws.n1,
2016 );
2017 let mut logits = self.lm_head_forward(&self.ws.n1);
2018 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2019 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2020 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2021 nll -= p.max(1e-300).ln();
2022 cnt += 1;
2023 attention::recycle_buf(&mut logits);
2024 }
2025 let emb = self.embed_single(id);
2026 hidden = self.forward_layers(&emb, pos, Some(mask));
2027 }
2028 self.kv_cache.clear();
2029 (nll / cnt.max(1) as f64).exp()
2030 }
2031
2032 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2041 self.kv_cache.clear();
2042 let mut nll = 0f64;
2043 let mut cnt = 0usize;
2044 if prefill_batched() {
2045 const CHUNK: usize = 128;
2051 const LM_SUB: usize = 32;
2052 let n = ids.len().saturating_sub(1);
2053 let hs = self.hidden_size;
2054 let rows = self.weights.lm_head.rows();
2055 let mut pos = 0usize;
2056 while pos < n {
2057 let end = (pos + CHUNK).min(n);
2058 let bsz = end - pos;
2059 let hb = self.prefill_batch(&ids[pos..end], pos);
2060 let mut k0 = 0usize;
2061 while k0 < bsz {
2062 let k1 = (k0 + LM_SUB).min(bsz);
2063 let sb = k1 - k0;
2064 if pos + k1 <= start {
2067 k0 = k1;
2068 continue;
2069 }
2070 let mut normed = vec![0.0f32; sb * hs];
2071 for k in 0..sb {
2072 let r = inference::rms_norm(
2073 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2074 &self.weights.final_norm,
2075 self.rms_eps,
2076 self.norm_style,
2077 );
2078 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2079 }
2080 let mut logits = vec![0.0f32; sb * rows];
2081 self.weights
2082 .lm_head
2083 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2084 for k in 0..sb {
2085 if pos + k0 + k < start {
2086 continue;
2087 }
2088 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2089 let target = ids[pos + k0 + k + 1] as usize;
2090 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2091 let lse: f64 = lg
2092 .iter()
2093 .map(|&v| ((v - max) as f64).exp())
2094 .sum::<f64>()
2095 .ln()
2096 + max as f64;
2097 nll += lse - lg[target] as f64;
2098 cnt += 1;
2099 }
2100 k0 = k1;
2101 }
2102 pos = end;
2103 }
2104 self.kv_cache.clear();
2105 return (nll, cnt);
2106 }
2107 for pos in 0..ids.len().saturating_sub(1) {
2108 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2109 if pos < start {
2110 continue;
2111 }
2112 let normed = inference::rms_norm(
2113 &hidden,
2114 &self.weights.final_norm,
2115 self.rms_eps,
2116 self.norm_style,
2117 );
2118 let logits = self.lm_head_forward(&normed);
2119 let target = ids[pos + 1] as usize;
2120 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2121 let lse: f64 = logits
2122 .iter()
2123 .map(|&v| ((v - max) as f64).exp())
2124 .sum::<f64>()
2125 .ln()
2126 + max as f64;
2127 nll += lse - logits[target] as f64;
2128 cnt += 1;
2129 }
2130 self.kv_cache.clear();
2131 (nll, cnt)
2132 }
2133
2134 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2150 self.kv_cache.clear();
2151 self.o1_begin();
2152 let n = ids.len().saturating_sub(1);
2153 let p = prefill.min(n);
2154 let mut pos = 0usize;
2156 if prefill_batched() {
2157 const CHUNK: usize = 128;
2158 while pos < p {
2159 let end = (pos + CHUNK).min(p);
2160 let _ = self.prefill_batch(&ids[pos..end], pos);
2161 pos = end;
2162 }
2163 } else {
2164 while pos < p {
2165 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2166 pos += 1;
2167 }
2168 }
2169 self.o1_seal();
2170
2171 let mut nll = 0f64;
2172 let mut cnt = 0usize;
2173 for pos in p..n {
2174 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2175 let normed = inference::rms_norm(
2176 &hidden,
2177 &self.weights.final_norm,
2178 self.rms_eps,
2179 self.norm_style,
2180 );
2181 let logits = self.lm_head_forward(&normed);
2182 let target = ids[pos + 1] as usize;
2183 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2184 let lse: f64 = logits
2185 .iter()
2186 .map(|&v| ((v - max) as f64).exp())
2187 .sum::<f64>()
2188 .ln()
2189 + max as f64;
2190 nll += lse - logits[target] as f64;
2191 cnt += 1;
2192 }
2193 self.kv_cache.clear();
2194 (nll, cnt)
2195 }
2196
2197 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2205 self.kv_cache.clear();
2206 let n = ids.len().saturating_sub(1);
2207 let mut correct = Vec::with_capacity(n);
2208 let mut pmax = Vec::with_capacity(n);
2209 for pos in 0..n {
2210 let emb = self.embed_single(ids[pos]);
2211 let hidden = self.forward_layers(&emb, pos, None);
2212 let normed = inference::rms_norm(
2213 &hidden,
2214 &self.weights.final_norm,
2215 self.rms_eps,
2216 self.norm_style,
2217 );
2218 let logits = self.lm_head_forward(&normed);
2219 let target = ids[pos + 1] as usize;
2220 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2221 for (i, &v) in logits.iter().enumerate() {
2222 if v > mval {
2223 mval = v;
2224 amax = i;
2225 }
2226 }
2227 correct.push(amax == target);
2228 let row: Vec<f32> = temps
2229 .iter()
2230 .map(|&t| {
2231 let tt = t.max(1e-3);
2232 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2233 1.0 / s.max(1e-12) })
2235 .collect();
2236 pmax.push(row);
2237 }
2238 self.kv_cache.clear();
2239 (correct, pmax)
2240 }
2241
2242 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2249 let mut router = match self.dyn_router.take() {
2250 Some(r) => r,
2251 None => return (self.ppl_ids(ids), 0),
2252 };
2253 router.reset();
2254 self.dyn_phi_seen = 0;
2255 let _ = self.set_active_skill(None);
2256
2257 self.kv_cache.clear();
2258 let mut nll = 0f64;
2259 let mut cnt = 0usize;
2260 for pos in 0..ids.len().saturating_sub(1) {
2261 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2262 let normed = inference::rms_norm(
2263 &hidden,
2264 &self.weights.final_norm,
2265 self.rms_eps,
2266 self.norm_style,
2267 );
2268 let logits = self.lm_head_forward(&normed);
2269 let target = ids[pos + 1] as usize;
2270 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2271 let lse: f64 = logits
2272 .iter()
2273 .map(|&v| ((v - max) as f64).exp())
2274 .sum::<f64>()
2275 .ln()
2276 + max as f64;
2277 nll += lse - logits[target] as f64;
2278 cnt += 1;
2279 let phi = self.dyn_phi_ema.clone();
2281 if let Some(new_active) = router.step(&phi, pos) {
2282 let _ = self.set_active_skill(new_active);
2283 }
2284 }
2285 let switches = router.switches.len();
2286 let _ = self.set_active_skill(None);
2287 self.dyn_router = Some(router);
2288 self.kv_cache.clear();
2289 ((nll / cnt.max(1) as f64).exp(), switches)
2290 }
2291
2292 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2294 self.kv_cache.clear();
2295 let mut acc = vec![0f32; self.hidden_size];
2296 for (pos, &id) in ids.iter().enumerate() {
2297 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2298 for (a, v) in acc.iter_mut().zip(&h) {
2299 *a += v;
2300 }
2301 }
2302 let n = ids.len().max(1) as f32;
2303 for a in acc.iter_mut() {
2304 *a /= n;
2305 }
2306 self.kv_cache.clear();
2307 acc
2308 }
2309
2310 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2316 let b = ids.len();
2317 let hs = self.hidden_size;
2318 let mut h: Vec<f32> = vec![0.0; b * hs];
2321 let mut h_ready = false;
2322 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2323 for (bi, &id) in ids.iter().enumerate() {
2324 let e = me.embed_single(id);
2325 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2326 }
2327 };
2328 let (_nkv, _hd, _rd, eps) = (
2329 self.num_kv_heads,
2330 self.head_dim,
2331 self.rotary_dim,
2332 self.rms_eps,
2333 );
2334 let pool = self.pool.clone();
2335 let norm_style = self.norm_style;
2336
2337 #[cfg(target_os = "macos")]
2338 let mut chunk_skip_until = 0usize;
2339 for li in 0..self.num_layers {
2340 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2347 {
2348 if li < chunk_skip_until {
2349 continue;
2350 }
2351 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2352 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2353 if end > li {
2354 h_ready = true;
2355 chunk_skip_until = end;
2356 if self.is_loop_end(end - 1) && end < self.num_layers {
2359 for bi in 0..b {
2360 let normed = inference::rms_norm(
2361 &h[bi * hs..(bi + 1) * hs],
2362 &self.weights.final_norm,
2363 eps,
2364 norm_style,
2365 );
2366 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2367 }
2368 }
2369 continue;
2370 }
2371 }
2372 if !h_ready {
2373 fill_h(&mut h, self);
2374 h_ready = true;
2375 }
2376 let lw = &self.weights.layers[self.phys_layer(li)];
2377 match &lw.attn {
2379 AttnKind::LinearGdn(w) => {
2380 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2382 let mut normed = vec![0.0f32; b * hs];
2383 for bi in 0..b {
2384 let r = inference::rms_norm(
2385 &h[bi * hs..(bi + 1) * hs],
2386 &lw.input_norm,
2387 eps,
2388 norm_style,
2389 );
2390 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2391 }
2392 let attn = crate::linear_core::gdn_forward_batch(
2393 &normed,
2394 b,
2395 w,
2396 &cfg,
2397 &mut self.kv_cache.layers[li].linear_state,
2398 pool.as_deref(),
2399 );
2400 for (dst, &a) in h.iter_mut().zip(&attn) {
2401 *dst += a;
2402 }
2403 }
2404 AttnKind::ShortConv(w) => {
2405 let cfg = self
2408 .short_conv_cfg
2409 .expect("short-conv layer without short_conv_cfg");
2410 let mut normed = vec![0.0f32; b * hs];
2411 for bi in 0..b {
2412 inference::rms_norm_into(
2413 &h[bi * hs..(bi + 1) * hs],
2414 &lw.input_norm,
2415 eps,
2416 norm_style,
2417 &mut normed[bi * hs..(bi + 1) * hs],
2418 );
2419 }
2420 let attn = short_conv_forward_batch(
2421 &normed,
2422 b,
2423 w,
2424 &cfg,
2425 &mut self.kv_cache.layers[li].linear_state,
2426 pool.as_deref(),
2427 );
2428 for (dst, &a) in h.iter_mut().zip(&attn) {
2429 *dst += a;
2430 }
2431 }
2432 AttnKind::Full {
2433 wq,
2434 wk,
2435 wv,
2436 wo,
2437 q_norm,
2438 k_norm,
2439 output_gate,
2440 softplus_gate,
2441 bias,
2442 } => {
2443 let mut normed = vec![0.0f32; b * hs];
2447 for bi in 0..b {
2448 inference::rms_norm_into(
2449 &h[bi * hs..(bi + 1) * hs],
2450 &lw.input_norm,
2451 eps,
2452 norm_style,
2453 &mut normed[bi * hs..(bi + 1) * hs],
2454 );
2455 }
2456 let inv_freq_l = self.layer_inv_freq(li);
2457 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2458 let cfg = QwenAttnCfg {
2459 num_heads: self.layer_num_heads(li),
2460 num_kv_heads: nkv_l,
2461 head_dim: hd_l,
2462 hidden_size: hs,
2463 position: start_pos,
2464 inv_freq: &inv_freq_l,
2465 rotary_dim: rd_l,
2466 scale: self.attn_scale,
2467 window: self.layer_window(li),
2468 v_norm: self.attn_v_norm,
2469 q_norm: q_norm.as_deref(),
2470 k_norm: k_norm.as_deref(),
2471 output_gate: *output_gate,
2472 softplus_gate: softplus_gate
2473 .as_ref()
2474 .map(|(gate, per_head)| (gate, *per_head)),
2475 rope_scale: self.layer_rope_scale(li),
2476 bias: bias
2477 .as_ref()
2478 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2479 rms_eps: eps,
2480 norm_style,
2481 pool: pool.as_deref(),
2482 };
2483 let mut attn = attention::qwen_attention_batch(
2484 &normed,
2485 b,
2486 wq,
2487 wk,
2488 wv,
2489 wo,
2490 &mut self.kv_cache.layers[li],
2491 &cfg,
2492 );
2493 if let Some(w) = &lw.attn_out_norm {
2494 for bi in 0..b {
2495 inference::rms_norm_into(
2496 &attn[bi * hs..(bi + 1) * hs],
2497 w,
2498 eps,
2499 norm_style,
2500 &mut normed[bi * hs..(bi + 1) * hs],
2501 );
2502 }
2503 attn.copy_from_slice(&normed);
2504 }
2505 for (dst, &a) in h.iter_mut().zip(&attn) {
2506 *dst += a;
2507 }
2508 }
2509 AttnKind::Linear(w) => {
2510 for bi in 0..b {
2511 let normed = inference::rms_norm(
2512 &h[bi * hs..(bi + 1) * hs],
2513 &lw.input_norm,
2514 eps,
2515 norm_style,
2516 );
2517 vmf_phase_forward(
2518 &normed,
2519 w,
2520 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2521 &mut self.kv_cache.layers[li].linear_state,
2522 pool.as_deref(),
2523 )
2524 .iter()
2525 .enumerate()
2526 .for_each(|(i, &a)| h[bi * hs + i] += a);
2527 }
2528 }
2529 }
2530
2531 let lw = &self.weights.layers[self.phys_layer(li)];
2533 let mut post = vec![0.0f32; b * hs];
2534 for bi in 0..b {
2535 let r =
2536 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2537 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2538 }
2539 let mut ffn = match &lw.ffn {
2540 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2541 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
2542 };
2543 if let Some(w) = &lw.ffn_out_norm {
2544 for bi in 0..b {
2545 inference::rms_norm_into(
2546 &ffn[bi * hs..(bi + 1) * hs],
2547 w,
2548 eps,
2549 norm_style,
2550 &mut post[bi * hs..(bi + 1) * hs],
2551 );
2552 }
2553 ffn.copy_from_slice(&post);
2554 }
2555 for (dst, &f) in h.iter_mut().zip(&ffn) {
2556 *dst += f;
2557 }
2558 if let Some(sc) = lw.layer_scale {
2559 for v in h.iter_mut() {
2560 *v *= sc;
2561 }
2562 }
2563 if self.is_loop_end(li) && li + 1 < self.num_layers {
2565 for bi in 0..b {
2566 let normed = inference::rms_norm(
2567 &h[bi * hs..(bi + 1) * hs],
2568 &self.weights.final_norm,
2569 eps,
2570 norm_style,
2571 );
2572 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2573 }
2574 }
2575 if std::env::var("CMF_TRACE_H").is_ok() {
2576 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2577 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2578 eprintln!(
2579 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
2580 lw.layer_scale
2581 );
2582 }
2583 }
2584 crate::gpu::set_layer(-1); h
2586 }
2587
2588 fn embed_single(&self, id: u32) -> Vec<f32> {
2590 let mut out = vec![0.0f32; self.hidden_size];
2591 if (id as usize) < self.weights.embed_tokens.rows() {
2592 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2593 }
2594 if self.embed_multiplier != 1.0 {
2595 for v in out.iter_mut() {
2596 *v *= self.embed_multiplier;
2597 }
2598 }
2599 out
2600 }
2601
2602 #[cfg(target_os = "macos")]
2608 fn chunk_run_gpu(
2609 &mut self,
2610 li0: usize,
2611 h: &mut [f32],
2612 b: usize,
2613 pos0: usize,
2614 embed_ids: Option<&[u32]>,
2615 ) -> usize {
2616 if !crate::gpu::enabled_here()
2620 || std::env::var("CMF_GPU_CHUNK")
2621 .map(|v| v == "0")
2622 .unwrap_or(false)
2623 || b < 32
2624 || self.swa.is_some()
2625 || self.global_attn.is_some()
2626 || self.attn_v_norm
2627 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2628 {
2629 return li0;
2630 }
2631 let Some(model) = self.model.clone() else {
2632 return li0;
2633 };
2634 let inv_freq = self.inv_freq.clone();
2635 let (nh, nkv, hd, hs) = (
2636 self.num_heads,
2637 self.num_kv_heads,
2638 self.head_dim,
2639 self.hidden_size,
2640 );
2641 let loop_end = if self.loop_final_norm {
2645 ((li0 / self.physical_layers) + 1) * self.physical_layers
2646 } else {
2647 self.num_layers
2648 };
2649 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2650 let mut stored_at: Vec<usize> = Vec::new();
2651 for li in li0..self.num_layers.min(loop_end) {
2652 let lw = &self.weights.layers[self.phys_layer(li)];
2653 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
2654 break;
2655 }
2656 let AttnKind::Full {
2657 wq,
2658 wk,
2659 wv,
2660 wo,
2661 q_norm,
2662 k_norm,
2663 output_gate: false,
2664 softplus_gate: None,
2665 bias,
2666 } = &lw.attn
2667 else {
2668 break;
2669 };
2670 let FfnKind::Dense(d) = &lw.ffn else { break };
2671 if d.act != Act::Silu {
2672 break;
2673 }
2674 let parts = (
2675 wq.q8_row_parts(),
2676 wk.q8_row_parts(),
2677 wv.q8_row_parts(),
2678 wo.q8_row_parts(),
2679 d.gate_proj.q8_row_parts(),
2680 d.up_proj.q8_row_parts(),
2681 d.down_proj.q8_row_parts(),
2682 );
2683 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2684 else {
2685 break;
2686 };
2687 let layer = &self.kv_cache.layers[li];
2688 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2689 break;
2690 }
2691 stored_at.push(layer.head_len(0));
2692 layers.push(crate::gpu_metal::ChunkLayer {
2693 model: &model,
2694 kv_id: self.graph_kv_id,
2695 layer: li,
2696 wq: pq,
2697 wk: pk,
2698 wv: pv,
2699 wo: po,
2700 gate: pg,
2701 up: pu,
2702 down: pd,
2703 input_norm: &lw.input_norm,
2704 post_norm: &lw.post_norm,
2705 bias: bias
2706 .as_ref()
2707 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2708 q_norm: q_norm.as_deref(),
2709 k_norm: k_norm.as_deref(),
2710 inv_freq: &inv_freq,
2711 rd: self.rotary_dim,
2712 nh,
2713 nkv,
2714 hd,
2715 hs,
2716 inter: d.gate_proj.rows(),
2717 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2718 eps: self.rms_eps as f32,
2719 });
2720 }
2721 if layers.is_empty() {
2722 return li0;
2723 }
2724 let row = nkv * hd;
2725 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2726 .iter()
2727 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2728 .collect();
2729 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2730 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2731 let li = layers[i].layer;
2732 let layer = &self.kv_cache.layers[li];
2733 io.push(crate::gpu_metal::ChunkIo {
2734 cpu_stored: stored_at[i],
2735 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2736 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2737 out_k: ok,
2738 out_v: ov,
2739 imp: oi,
2740 });
2741 }
2742 let n_run = layers.len();
2743 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2744 let ep = embed_ids.and_then(|ids| {
2747 self.weights
2748 .embed_tokens
2749 .q8_row_parts()
2750 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
2751 idx,
2752 rows,
2753 row_scale: rs,
2754 ids,
2755 mult: self.embed_multiplier,
2756 })
2757 });
2758 if embed_ids.is_some() && ep.is_none() {
2759 return li0;
2760 }
2761 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2762 return li0;
2763 }
2764 drop(io);
2765 drop(layers);
2766 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2769 let li = li0 + i;
2770 let layer = &mut self.kv_cache.layers[li];
2771 for bi in 0..b {
2772 layer.append(
2773 &ok[bi * row..(bi + 1) * row],
2774 &ov[bi * row..(bi + 1) * row],
2775 &[],
2776 );
2777 }
2778 layer.accumulate_imp(oi);
2779 }
2780 last
2781 }
2782
2783 fn layer_is_local(&self, li: usize) -> bool {
2786 if let Some(layers) = &self.sliding_layers {
2787 return layers.get(li).copied().unwrap_or(false);
2788 }
2789 match self.swa {
2790 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2791 None => false,
2792 }
2793 }
2794
2795 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2798 if self.layer_is_local(li) {
2799 if let Some(f) = &self.inv_freq_local {
2800 return f.clone();
2801 }
2802 } else if let Some(f) = &self.inv_freq_global {
2803 return f.clone();
2804 }
2805 self.inv_freq.clone()
2806 }
2807
2808 fn layer_window(&self, li: usize) -> Option<usize> {
2810 self.swa
2811 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
2812 }
2813
2814 fn layer_num_heads(&self, li: usize) -> usize {
2815 self.attention_heads_per_layer
2816 .as_ref()
2817 .and_then(|v| v.get(li).copied())
2818 .unwrap_or(self.num_heads)
2819 }
2820
2821 fn layer_rope_scale(&self, li: usize) -> f32 {
2822 if self.layer_is_local(li) {
2823 self.rope_scale_local
2824 } else {
2825 self.rope_scale
2826 }
2827 }
2828
2829 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2832 if !self.layer_is_local(li) {
2833 if let Some((ghd, gkv)) = self.global_attn {
2834 return (gkv, ghd, ghd);
2835 }
2836 }
2837 (
2838 self.num_kv_heads,
2839 self.head_dim,
2840 if self.layer_is_local(li) {
2841 self.rotary_dim_local.unwrap_or(self.rotary_dim)
2842 } else {
2843 self.rotary_dim
2844 },
2845 )
2846 }
2847
2848 fn forward_layers(
2850 &mut self,
2851 hidden: &[f32],
2852 position: usize,
2853 task_mask: Option<&TaskMask>,
2854 ) -> Vec<f32> {
2855 self.forward_layers_upto(hidden, position, task_mask, None)
2856 }
2857
2858 fn try_token_graph_wgpu(
2862 &self,
2863 hidden: &[f32],
2864 position: usize,
2865 logits_out: &mut Vec<f32>,
2866 ) -> Option<Vec<f32>> {
2867 if self.o1_active() {
2870 return None;
2871 }
2872 let nh = self.num_heads;
2873 let (nkv, hd, rd) = self.layer_geom(0);
2874 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2875 let mut layers = Vec::with_capacity(self.num_layers);
2876 let mut model = None;
2877 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
2878 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2879 if let Some((_, i, kind, rs)) = t.graph_weight() {
2880 return Some(crate::gpu::GraphW {
2881 idx: i,
2882 kind,
2883 row_scale: rs,
2884 data: &[],
2885 });
2886 }
2887 t.as_f32().map(|d| crate::gpu::GraphW {
2889 idx: 0,
2890 kind: 4,
2891 row_scale: &[],
2892 data: d,
2893 })
2894 }
2895 for li in 0..self.num_layers {
2896 let lw = &self.weights.layers[self.phys_layer(li)];
2897 if dbg {
2898 let ak = match &lw.attn {
2899 AttnKind::Full {
2900 output_gate, bias, ..
2901 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
2902 AttnKind::LinearGdn(_) => "LinearGdn".into(),
2903 AttnKind::Linear(_) => "Linear".into(),
2904 AttnKind::ShortConv(_) => "ShortConv".into(),
2905 };
2906 let fk = match &lw.ffn {
2907 FfnKind::Dense(_) => "Dense",
2908 FfnKind::Moe(_) => "Moe",
2909 };
2910 eprintln!("graph L{li}: attn={ak} ffn={fk}");
2911 }
2912 let (gate, up, down) = match &lw.ffn {
2913 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
2914 _ => return None,
2915 };
2916 let attn = match &lw.attn {
2917 AttnKind::Full {
2918 wq,
2919 wk,
2920 wv,
2921 wo,
2922 q_norm,
2923 k_norm,
2924 output_gate,
2925 softplus_gate,
2926 bias,
2927 } => {
2928 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
2929 return None;
2930 }
2931 let (m, _, _, _) = wq.graph_weight()?;
2932 model = Some(m.clone());
2933 crate::gpu::GraphAttn::Full {
2934 wq: gw(wq)?,
2935 wk: gw(wk)?,
2936 wv: gw(wv)?,
2937 wo: gw(wo)?,
2938 q_norm: q_norm.as_deref(),
2939 k_norm: k_norm.as_deref(),
2940 bias: bias
2941 .as_ref()
2942 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2943 output_gate: *output_gate,
2944 cpu_k: self.kv_cache.layers[li].k_heads(),
2945 cpu_v: self.kv_cache.layers[li].v_heads(),
2946 }
2947 }
2948 AttnKind::LinearGdn(w) => {
2949 let cfg = self.gdn_cfg?;
2950 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
2951 model = Some(m.clone());
2952 crate::gpu::GraphAttn::Gdn {
2953 qkv: gw(&w.in_proj_qkv)?,
2954 z: gw(&w.in_proj_z)?,
2955 a: gw(&w.in_proj_a)?,
2956 b: gw(&w.in_proj_b)?,
2957 out: gw(&w.out_proj)?,
2958 conv1d: &w.conv1d,
2959 a_log: &w.a_log,
2960 dt_bias: &w.dt_bias,
2961 norm: &w.norm,
2962 nv: cfg.num_v_heads,
2963 nk: cfg.num_k_heads,
2964 dk: cfg.key_head_dim,
2965 dv: cfg.value_head_dim,
2966 kk: cfg.conv_kernel,
2967 }
2968 }
2969 _ => return None,
2970 };
2971 layers.push(crate::gpu::GraphLayer {
2972 input_norm: &lw.input_norm,
2973 attn,
2974 post_norm: &lw.post_norm,
2975 gate: gw(gate)?,
2976 up: gw(up)?,
2977 down: gw(down)?,
2978 });
2979 }
2980 let model = model?;
2981 let lm_gw = if self.graph_want_logits
2987 && std::env::var("CMF_GPU_LMHEAD")
2988 .map(|v| v != "0")
2989 .unwrap_or(true)
2990 {
2991 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
2992 (
2993 crate::gpu::GraphW {
2994 idx: i,
2995 kind,
2996 row_scale: rs,
2997 data: &[],
2998 },
2999 self.weights.lm_head.rows(),
3000 )
3001 })
3002 } else {
3003 None
3004 };
3005 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
3006 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
3009 (0..self.num_layers - 1)
3010 .filter(|&li| (li + 1) % self.physical_layers == 0)
3011 .collect()
3012 } else {
3013 Vec::new()
3014 };
3015 let mut h = hidden.to_vec();
3016 crate::gpu::forward_token_graph(
3017 &model,
3018 self.graph_kv_id,
3019 &layers,
3020 &self.inv_freq,
3021 &mut h,
3022 nh,
3023 nkv,
3024 hd,
3025 rd,
3026 self.hidden_size,
3027 self.intermediate_size,
3028 position,
3029 self.kv_cache.max_seq_len,
3030 gemma,
3031 self.rms_eps as f32,
3032 lm,
3033 &self.weights.final_norm,
3034 logits_out,
3035 &loop_norm_at,
3036 )
3037 .then_some(h)
3038 }
3039
3040 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
3045 if self.o1_active() {
3046 return false;
3047 }
3048 let nh = self.num_heads;
3049 let (nkv, hd, rd) = self.layer_geom(0);
3050 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3051 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3052 if let Some((_, i, kind, rs)) = t.graph_weight() {
3053 return Some(crate::gpu::GraphW {
3054 idx: i,
3055 kind,
3056 row_scale: rs,
3057 data: &[],
3058 });
3059 }
3060 t.as_f32().map(|d| crate::gpu::GraphW {
3061 idx: 0,
3062 kind: 4,
3063 row_scale: &[],
3064 data: d,
3065 })
3066 }
3067 let built: Option<(
3068 Vec<crate::gpu::GraphLayer<'_>>,
3069 std::sync::Arc<cortiq_core::CmfModel>,
3070 )> = (|| {
3071 let mut layers = Vec::with_capacity(self.num_layers);
3072 let mut model = None;
3073 for li in 0..self.num_layers {
3074 let lw = &self.weights.layers[self.phys_layer(li)];
3075 let (gate, up, down) = match &lw.ffn {
3076 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
3077 _ => return None,
3078 };
3079 let attn = match &lw.attn {
3080 AttnKind::Full {
3081 wq,
3082 wk,
3083 wv,
3084 wo,
3085 q_norm,
3086 k_norm,
3087 output_gate,
3088 softplus_gate,
3089 bias,
3090 } => {
3091 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3092 return None;
3093 }
3094 let (m, _, _, _) = wq.graph_weight()?;
3095 model = Some(m.clone());
3096 crate::gpu::GraphAttn::Full {
3097 wq: gw(wq)?,
3098 wk: gw(wk)?,
3099 wv: gw(wv)?,
3100 wo: gw(wo)?,
3101 q_norm: q_norm.as_deref(),
3102 k_norm: k_norm.as_deref(),
3103 bias: bias
3104 .as_ref()
3105 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3106 output_gate: *output_gate,
3107 cpu_k: self.kv_cache.layers[li].k_heads(),
3108 cpu_v: self.kv_cache.layers[li].v_heads(),
3109 }
3110 }
3111 AttnKind::LinearGdn(w) => {
3112 let cfg = self.gdn_cfg?;
3113 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3114 model = Some(m.clone());
3115 crate::gpu::GraphAttn::Gdn {
3116 qkv: gw(&w.in_proj_qkv)?,
3117 z: gw(&w.in_proj_z)?,
3118 a: gw(&w.in_proj_a)?,
3119 b: gw(&w.in_proj_b)?,
3120 out: gw(&w.out_proj)?,
3121 conv1d: &w.conv1d,
3122 a_log: &w.a_log,
3123 dt_bias: &w.dt_bias,
3124 norm: &w.norm,
3125 nv: cfg.num_v_heads,
3126 nk: cfg.num_k_heads,
3127 dk: cfg.key_head_dim,
3128 dv: cfg.value_head_dim,
3129 kk: cfg.conv_kernel,
3130 }
3131 }
3132 _ => return None,
3133 };
3134 layers.push(crate::gpu::GraphLayer {
3135 input_norm: &lw.input_norm,
3136 attn,
3137 post_norm: &lw.post_norm,
3138 gate: gw(gate)?,
3139 up: gw(up)?,
3140 down: gw(down)?,
3141 });
3142 }
3143 Some((layers, model?))
3144 })();
3145 let Some((layers, model)) = built else {
3146 return false;
3147 };
3148 crate::gpu::forward_batch_graph(
3149 &model,
3150 self.graph_kv_id,
3151 &layers,
3152 &self.inv_freq,
3153 hiddens,
3154 nh,
3155 nkv,
3156 hd,
3157 rd,
3158 self.hidden_size,
3159 self.intermediate_size,
3160 positions,
3161 self.kv_cache.max_seq_len,
3162 gemma,
3163 self.rms_eps as f32,
3164 k,
3165 )
3166 }
3167
3168 fn forward_layers_upto(
3170 &mut self,
3171 hidden: &[f32],
3172 position: usize,
3173 task_mask: Option<&TaskMask>,
3174 upto: Option<usize>,
3175 ) -> Vec<f32> {
3176 let mut h = hidden.to_vec();
3177 let (nh, _nkv, _hd, hs, _rd, eps) = (
3180 self.num_heads,
3181 self.num_kv_heads,
3182 self.head_dim,
3183 self.hidden_size,
3184 self.rotary_dim,
3185 self.rms_eps,
3186 );
3187 let pool = self.pool.clone();
3188 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
3191 .map(|v| v != "0")
3192 .unwrap_or_else(|_| {
3193 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
3194 });
3195 if graph_on && upto.is_none() && task_mask.is_none() {
3199 let mut lg = Vec::new();
3200 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
3201 if !lg.is_empty() {
3202 lg.resize(self.vocab_size, 0.0);
3205 if let Some(c) = self.final_softcap {
3206 for l in lg.iter_mut() {
3207 *l = c * (*l / c).tanh();
3208 }
3209 }
3210 self.graph_logits = Some(lg);
3211 }
3212 return hh;
3213 }
3214 }
3215
3216 #[cfg(target_os = "macos")]
3217 let mut gpu_skip_until = 0usize;
3218 for li in 0..self.num_layers {
3219 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
3221 if li > u {
3222 break;
3223 }
3224 }
3225 if let Some(mask) = task_mask {
3226 if !mask.layer_alive(li) {
3227 continue; }
3229 }
3230 #[cfg(target_os = "macos")]
3234 {
3235 if li < gpu_skip_until {
3236 continue;
3237 }
3238 if task_mask.is_none() {
3239 let end = self.q1_graph_gpu(li, upto, position, &mut h);
3240 if end > li {
3241 gpu_skip_until = end;
3242 if self.is_loop_end(end - 1) && end < self.num_layers {
3245 h = inference::rms_norm(
3246 &h,
3247 &self.weights.final_norm,
3248 self.rms_eps,
3249 self.norm_style,
3250 );
3251 }
3252 continue;
3253 }
3254 }
3255 }
3256
3257 let lw = &self.weights.layers[self.phys_layer(li)];
3258 inference::rms_norm_into(
3261 &h,
3262 &lw.input_norm,
3263 self.rms_eps,
3264 self.norm_style,
3265 &mut self.ws.n1,
3266 );
3267
3268 let attn_out = match &lw.attn {
3269 AttnKind::Linear(w) => {
3270 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3271 vmf_phase_forward(
3272 &self.ws.n1,
3273 w,
3274 &cfg,
3275 &mut self.kv_cache.layers[li].linear_state,
3276 self.pool.as_deref(),
3277 )
3278 }
3279 AttnKind::LinearGdn(w) => {
3280 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3281 gdn_forward(
3282 &self.ws.n1,
3283 w,
3284 &cfg,
3285 &mut self.kv_cache.layers[li].linear_state,
3286 self.pool.as_deref(),
3287 )
3288 }
3289 AttnKind::ShortConv(w) => {
3290 let cfg = self
3291 .short_conv_cfg
3292 .expect("short-conv layer without short_conv_cfg");
3293 short_conv_forward(
3294 &self.ws.n1,
3295 w,
3296 &cfg,
3297 &mut self.kv_cache.layers[li].linear_state,
3298 self.pool.as_deref(),
3299 )
3300 }
3301 AttnKind::Full {
3302 wq,
3303 wk,
3304 wv,
3305 wo,
3306 q_norm,
3307 k_norm,
3308 output_gate,
3309 softplus_gate,
3310 bias,
3311 } if self.kv_cache.layers[li].o1_sealed() => {
3312 let inv_freq_l = self.layer_inv_freq(li);
3315 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3316 let cfg = QwenAttnCfg {
3317 num_heads: self.layer_num_heads(li),
3318 num_kv_heads: nkv_l,
3319 head_dim: hd_l,
3320 hidden_size: hs,
3321 position,
3322 inv_freq: &inv_freq_l,
3323 rotary_dim: rd_l,
3324 scale: self.attn_scale,
3325 window: None,
3326 v_norm: self.attn_v_norm,
3327 q_norm: q_norm.as_deref(),
3328 k_norm: k_norm.as_deref(),
3329 output_gate: *output_gate,
3330 softplus_gate: softplus_gate
3331 .as_ref()
3332 .map(|(gate, per_head)| (gate, *per_head)),
3333 rope_scale: self.layer_rope_scale(li),
3334 bias: bias
3335 .as_ref()
3336 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3337 rms_eps: eps,
3338 norm_style: self.norm_style,
3339 pool: pool.as_deref(),
3340 };
3341 attention::qwen_attention_nystrom(
3342 &self.ws.n1,
3343 wq,
3344 wk,
3345 wv,
3346 wo,
3347 &mut self.kv_cache.layers[li],
3348 &cfg,
3349 )
3350 }
3351 AttnKind::Full {
3352 wq,
3353 wk,
3354 wv,
3355 wo,
3356 q_norm,
3357 k_norm,
3358 output_gate,
3359 softplus_gate,
3360 bias,
3361 } => 'attn: {
3362 if graph_on
3365 && !*output_gate
3366 && softplus_gate.is_none()
3367 && self.attention_heads_per_layer.is_none()
3368 && bias.is_none()
3369 && task_mask.is_none()
3370 {
3371 let inv_freq_l = self.layer_inv_freq(li);
3372 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3373 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3374 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
3375 wq.mapped_q1(),
3376 wk.mapped_q1(),
3377 wv.mapped_q1(),
3378 wo.mapped_q1(),
3379 ) {
3380 let gm = gm.clone();
3381 let mut out = vec![0f32; hs];
3382 let cache = &self.kv_cache.layers[li];
3383 if crate::gpu::attn_dropin(
3384 &gm,
3385 self.graph_kv_id,
3386 li,
3387 &self.ws.n1,
3388 qi,
3389 ki,
3390 vi,
3391 oi,
3392 q_norm.as_deref(),
3393 k_norm.as_deref(),
3394 &inv_freq_l,
3395 nh,
3396 nkv_l,
3397 hd_l,
3398 rd_l,
3399 hs,
3400 position,
3401 self.kv_cache.max_seq_len,
3402 gemma,
3403 eps as f32,
3404 cache.k_heads(),
3405 cache.v_heads(),
3406 &mut out,
3407 ) {
3408 break 'attn out;
3409 }
3410 }
3411 }
3412 let masked = task_mask
3413 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
3414 .unwrap_or(false);
3415 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
3416 match (masked, f32_view) {
3417 (true, (Some(q), Some(k), Some(v), Some(o))) => {
3420 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
3421 attention::multi_head_attention(
3422 &self.ws.n1,
3423 q,
3424 k,
3425 v,
3426 o,
3427 &mut self.kv_cache.layers[li],
3428 self.num_heads,
3429 self.num_kv_heads,
3430 self.head_dim,
3431 self.hidden_size,
3432 position,
3433 &active_heads,
3434 &self.inv_freq,
3435 )
3436 }
3437 (masked, _) => {
3438 if masked {
3439 tracing::warn!(
3440 "layer {li}: head mask on quantized weights not \
3441 supported yet — executing dense"
3442 );
3443 }
3444 let inv_freq_l = self.layer_inv_freq(li);
3445 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3446 let cfg = QwenAttnCfg {
3447 num_heads: self.layer_num_heads(li),
3448 num_kv_heads: nkv_l,
3449 head_dim: hd_l,
3450 hidden_size: hs,
3451 position,
3452 inv_freq: &inv_freq_l,
3453 rotary_dim: rd_l,
3454 scale: self.attn_scale,
3455 window: self.layer_window(li),
3456 v_norm: self.attn_v_norm,
3457 q_norm: q_norm.as_deref(),
3458 k_norm: k_norm.as_deref(),
3459 output_gate: *output_gate,
3460 softplus_gate: softplus_gate
3461 .as_ref()
3462 .map(|(gate, per_head)| (gate, *per_head)),
3463 rope_scale: self.layer_rope_scale(li),
3464 bias: bias
3465 .as_ref()
3466 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3467 rms_eps: eps,
3468 norm_style: self.norm_style,
3469 pool: pool.as_deref(),
3470 };
3471 attention::qwen_attention(
3472 &self.ws.n1,
3473 wq,
3474 wk,
3475 wv,
3476 wo,
3477 &mut self.kv_cache.layers[li],
3478 &cfg,
3479 )
3480 }
3481 }
3482 }
3483 };
3484 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
3487 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
3488 None => attn_out,
3489 };
3490 let lw = &self.weights.layers[self.phys_layer(li)];
3491 inference::add_rmsnorm_fused_into(
3492 &mut h,
3493 &attn_out,
3494 &lw.post_norm,
3495 self.rms_eps,
3496 self.norm_style,
3497 &mut self.ws.p1,
3498 );
3499 let mut attn_out = attn_out;
3500 attention::recycle_buf(&mut attn_out);
3501 let post_normed = &self.ws.p1;
3502
3503 let ffn_masked = task_mask
3504 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
3505 .unwrap_or(false);
3506 let f32_ffn = match &lw.ffn {
3509 FfnKind::Dense(d) => (
3510 d.gate_proj.as_f32(),
3511 d.up_proj.as_f32(),
3512 d.down_proj.as_f32(),
3513 ),
3514 FfnKind::Moe(_) => (None, None, None),
3515 };
3516 let ffn_out = match (ffn_masked, f32_ffn) {
3517 (true, (Some(g), Some(u), Some(d))) => {
3518 let active = task_mask.unwrap().ffn_active_indices(li);
3519 inference::sparse_ffn_forward(
3520 post_normed,
3521 g,
3522 u,
3523 d,
3524 self.hidden_size,
3525 self.intermediate_size,
3526 &active,
3527 self.pool.as_deref(),
3528 )
3529 }
3530 (true, _) => match &lw.ffn {
3534 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
3535 let active = task_mask.unwrap().ffn_active_indices(li);
3536 sparse_ffn_quant(
3537 d,
3538 post_normed,
3539 &active,
3540 self.hidden_size,
3541 self.pool.as_deref(),
3542 )
3543 }
3544 FfnKind::Dense(d) => {
3549 let active = task_mask.unwrap().ffn_active_indices(li);
3550 let (gf, uf, df) = dequant_dense_f32(d);
3551 inference::sparse_ffn_forward(
3552 post_normed,
3553 &gf,
3554 &uf,
3555 &df,
3556 self.hidden_size,
3557 self.intermediate_size,
3558 &active,
3559 self.pool.as_deref(),
3560 )
3561 }
3562 FfnKind::Moe(_) => {
3563 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref())
3566 }
3567 },
3568 (false, _) => ffn_forward(&lw.ffn, post_normed, self.pool.as_deref()),
3569 };
3570 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
3571 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
3572 None => ffn_out,
3573 };
3574 for (i, &f) in ffn_out.iter().enumerate() {
3575 h[i] += f;
3576 }
3577 let mut ffn_out = ffn_out;
3578 attention::recycle_buf(&mut ffn_out);
3579
3580 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
3582 for v in h.iter_mut() {
3583 *v *= sc;
3584 }
3585 }
3586
3587 if self.is_loop_end(li) && li + 1 < self.num_layers {
3590 h = inference::rms_norm(
3591 &h,
3592 &self.weights.final_norm,
3593 self.rms_eps,
3594 self.norm_style,
3595 );
3596 }
3597
3598 if self.dyn_phi_layer == Some(li) {
3602 self.update_dyn_phi(&h);
3603 }
3604 }
3605 crate::gpu::set_layer(-1); h
3608 }
3609
3610 fn update_dyn_phi(&mut self, h: &[f32]) {
3613 const A: f32 = 0.2;
3614 if self.dyn_phi_ema.len() != h.len() {
3615 self.dyn_phi_ema = vec![0.0; h.len()];
3616 self.dyn_phi_seen = 0;
3617 }
3618 if self.dyn_phi_seen == 0 {
3619 self.dyn_phi_ema.copy_from_slice(h);
3620 } else {
3621 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
3622 *e = (1.0 - A) * *e + A * v;
3623 }
3624 }
3625 self.dyn_phi_seen += 1;
3626 }
3627
3628 pub fn dyn_phi(&self) -> &[f32] {
3630 &self.dyn_phi_ema
3631 }
3632
3633 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
3635 self.dyn_phi_layer = layer;
3636 self.dyn_phi_ema.clear();
3637 self.dyn_phi_seen = 0;
3638 }
3639
3640 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
3642 let Some(model) = &self.model else {
3643 return Vec::new();
3644 };
3645 model
3646 .header
3647 .skills
3648 .iter()
3649 .enumerate()
3650 .filter_map(|(i, sk)| {
3651 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
3652 let sel = sk.selection.as_ref()?;
3653 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
3654 })
3655 .collect()
3656 }
3657
3658 pub fn active_skill(&self) -> Option<usize> {
3660 self.dyn_active
3661 }
3662
3663 pub fn enable_dynamic_routing(&mut self) -> usize {
3668 use crate::swarm::{DynRouter, RoutableSkill};
3669 let Some(model) = self.model.clone() else {
3670 return 0;
3671 };
3672 if self.dyn_blend_loaded {
3675 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
3676 return 0;
3677 }
3678 if let Some(a) = self.dyn_active {
3682 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
3683 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
3684 return 0;
3685 }
3686 }
3687 let hidden = self.hidden_size;
3688 let mut skills = Vec::new();
3689 for (idx, id, _phi) in self.dynamic_skills() {
3690 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
3691 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
3692 skills.push(rs);
3693 }
3694 }
3695 }
3696 if skills.is_empty() {
3697 return 0;
3698 }
3699 let phi = skills[0].phi_layer;
3701 if skills.iter().any(|s| s.phi_layer != phi) {
3702 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
3703 }
3704 let n = skills.len();
3705 self.set_dyn_phi_layer(Some(phi));
3706 self.dyn_router = Some(DynRouter::new(skills));
3707 n
3708 }
3709
3710 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
3712 self.dyn_router
3713 .as_ref()
3714 .map(|r| r.switches.clone())
3715 .unwrap_or_default()
3716 }
3717
3718 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
3721 let rows = self.weights.lm_head.rows();
3722 let mut logits = attention::take_buf(rows.min(self.vocab_size));
3723 self.weights
3724 .lm_head
3725 .matvec(hidden, &mut logits, self.pool.as_deref());
3726 logits.resize(self.vocab_size, 0.0);
3727 if let Some(c) = self.final_softcap {
3728 for l in logits.iter_mut() {
3729 *l = c * (*l / c).tanh();
3730 }
3731 }
3732 logits
3733 }
3734
3735 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
3740 self.kv_cache.clear();
3741 let mut hidden = vec![0.0f32; self.hidden_size];
3742 for (pos, &id) in ids.iter().enumerate() {
3743 let emb = self.embed_single(id);
3744 hidden = self.forward_layers(&emb, pos, task_mask);
3745 }
3746 inference::rms_norm_into(
3747 &hidden,
3748 &self.weights.final_norm,
3749 self.rms_eps,
3750 self.norm_style,
3751 &mut self.ws.n1,
3752 );
3753 self.lm_head_forward(&self.ws.n1)
3754 }
3755}
3756
3757pub fn create_test_pipeline(
3759 hidden_size: usize,
3760 intermediate_size: usize,
3761 num_heads: usize,
3762 num_kv_heads: usize,
3763 head_dim: usize,
3764 num_layers: usize,
3765 vocab_size: usize,
3766) -> Pipeline {
3767 let synth = |n: usize, salt: usize| -> Vec<f32> {
3770 (0..n)
3771 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
3772 .collect()
3773 };
3774 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3775 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3776 };
3777 let layer_weights: Vec<LayerWeights> = (0..num_layers)
3778 .map(|li| LayerWeights {
3779 input_norm: vec![1.0; hidden_size],
3780 post_norm: vec![1.0; hidden_size],
3781 attn_out_norm: None,
3782 ffn_out_norm: None,
3783 layer_scale: None,
3784 ffn: FfnKind::Dense(DenseFfn {
3785 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
3786 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
3787 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
3788 act: Act::Silu,
3789 }),
3790 attn: AttnKind::Full {
3791 bias: None,
3792 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
3793 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
3794 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
3795 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
3796 q_norm: None,
3797 k_norm: None,
3798 output_gate: false,
3799 softplus_gate: None,
3800 },
3801 })
3802 .collect();
3803
3804 Pipeline::new(
3805 Tokenizer::byte_level(),
3806 PipelineWeights {
3807 embed_tokens: qt(vocab_size, hidden_size, 100),
3808 layers: layer_weights,
3809 lm_head: qt(vocab_size, hidden_size, 200),
3810 final_norm: vec![1.0; hidden_size],
3811 },
3812 hidden_size,
3813 intermediate_size,
3814 num_heads,
3815 num_kv_heads,
3816 head_dim,
3817 num_layers,
3818 num_layers, false, vocab_size,
3821 1e-6,
3822 10_000.0,
3823 NormStyle::Qwen,
3824 4096,
3825 SamplerConfig {
3826 seed: Some(42),
3827 ..Default::default()
3828 },
3829 )
3830}
3831
3832fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
3835 let inter = d.gate_proj.rows();
3836 let hidden = d.down_proj.rows();
3837 let mut g = vec![0.0f32; b * inter];
3838 d.gate_proj.matmat(xs, b, &mut g, pool);
3839 let mut u = vec![0.0f32; b * inter];
3840 d.up_proj.matmat(xs, b, &mut u, pool);
3841 for i in 0..b * inter {
3842 g[i] = d.act.apply(g[i]) * u[i];
3843 }
3844 let mut out = vec![0.0f32; b * hidden];
3845 d.down_proj.matmat(&g, b, &mut out, pool);
3846 out
3847}
3848
3849fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
3853 let ne = m.experts.len();
3854 let mut logits = vec![0.0f32; b * ne];
3855 m.router.matmat(xs, b, &mut logits, pool);
3856
3857 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
3860 {
3861 let mut st = m.stats.borrow_mut();
3862 if st.len() < ne {
3863 st.resize(ne, 0);
3864 }
3865 for bi in 0..b {
3866 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m);
3867 for &e in &idx {
3868 st[e] += 1;
3869 assign[e].push((bi, p[e] / wsum));
3870 }
3871 }
3872 }
3873
3874 let mut out = vec![0.0f32; b * hidden];
3875 let cols = m.experts[0].gate_proj.cols();
3876 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
3877 let sb = list.len();
3878 let mut sub = vec![0.0f32; sb * cols];
3879 for (k, &(bi, _)) in list.iter().enumerate() {
3880 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
3881 }
3882 let eo = dense_ffn_batch(d, &sub, sb, pool);
3883 for (k, &(bi, w)) in list.iter().enumerate() {
3884 for i in 0..hidden {
3885 out[bi * hidden + i] += w * eo[k * hidden + i];
3886 }
3887 }
3888 };
3889 for (e, a) in assign.iter().enumerate().take(ne) {
3890 if !a.is_empty() {
3891 run_expert(&m.experts[e], a);
3892 }
3893 }
3894 if let Some((se, gate)) = &m.shared {
3895 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
3896 let mut gl = vec![0.0f32; b];
3897 gate.matmat(xs, b, &mut gl, pool);
3898 (0..b)
3899 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
3900 .collect()
3901 } else {
3902 (0..b).map(|bi| (bi, 1.0)).collect()
3903 };
3904 run_expert(se, &all);
3905 }
3906 out
3907}
3908
3909thread_local! {
3910 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
3914 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
3915}
3916
3917fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3919 if crate::gpu::enabled_here()
3930 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
3931 {
3932 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
3933 crate::gpu::ProbeArm::Gpu
3934 } else {
3935 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
3936 };
3937 match arm {
3938 crate::gpu::ProbeArm::Gpu => {
3939 let t0 = std::time::Instant::now();
3940 if let Some(out) = dense_ffn_gpu(d, x, pool) {
3941 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3942 return out;
3943 }
3944 }
3945 crate::gpu::ProbeArm::CpuTimed => {
3946 let t0 = std::time::Instant::now();
3947 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3948 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3949 return out;
3950 }
3951 crate::gpu::ProbeArm::Cpu => {
3952 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3953 }
3954 }
3955 }
3956 dense_ffn_cpu(d, x, pool)
3957}
3958
3959fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3961 let inter = d.gate_proj.rows();
3962 FFN_SCRATCH.with(|s| {
3963 let mut s = s.borrow_mut();
3964 let [g, u, ..] = &mut *s;
3965 g.resize(inter, 0.0);
3966 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
3969 } else {
3971 u.resize(inter, 0.0);
3972 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
3974 for i in 0..inter {
3975 g[i] = d.act.apply(g[i]) * u[i];
3976 }
3977 }
3978 FFN_PROBE.with(|pr| {
3981 if let Some(acc) = pr.borrow_mut().as_mut() {
3982 let li = crate::gpu::cur_layer();
3983 if li >= 0 {
3984 if let Some(row) = acc.get_mut(li as usize) {
3985 for (a, &v) in row.iter_mut().zip(g.iter()) {
3986 *a += (v as f64).abs();
3987 }
3988 }
3989 }
3990 }
3991 });
3992 let mut out = attention::take_buf(d.down_proj.rows());
3993 d.down_proj.matvec(g, &mut out, pool);
3994 out
3995 })
3996}
3997
3998thread_local! {
3999 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
4002 const { std::cell::RefCell::new(None) };
4003}
4004
4005fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
4011 if d.act != Act::Silu {
4013 return None;
4014 }
4015 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
4018 return None;
4019 }
4020 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
4021 let mut model_ref = None;
4022 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
4023 let model = model_ref?;
4024 let hidden = jobs[0].down.1;
4025 let mut out = attention::take_buf(hidden);
4026 if crate::gpu::moe_block(&model, &jobs, &mut out) {
4027 Some(out)
4028 } else {
4029 let mut out = out;
4030 attention::recycle_buf(&mut out);
4031 None
4032 }
4033}
4034
4035#[allow(clippy::type_complexity)]
4040#[allow(clippy::type_complexity)]
4041fn moe_parts(
4042 t: &QTensor,
4043) -> Option<(
4044 &std::sync::Arc<cortiq_core::CmfModel>,
4045 usize,
4046 usize,
4047 usize,
4048 &[f32],
4049 &[f32],
4050 bool,
4051)> {
4052 match t {
4053 QTensor::Mapped {
4054 model,
4055 idx,
4056 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
4057 rows,
4058 cols,
4059 row_scale,
4060 col_field,
4061 ..
4062 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
4063 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
4064 }
4065 QTensor::Mapped {
4067 model,
4068 idx,
4069 dtype: cortiq_core::TensorDtype::Q1,
4070 rows,
4071 cols,
4072 ..
4073 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
4074 _ => None,
4075 }
4076}
4077
4078fn moe_push_job<'a>(
4080 d: &'a DenseFfn,
4081 x: &[f32],
4082 w: f32,
4083 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
4084 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
4085) -> Option<()> {
4086 use crate::qtensor::prescale;
4087 if d.act != Act::Silu {
4088 return None; }
4090 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
4091 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
4092 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
4093 if gq1 != uq1 || uq1 != dq1 {
4094 return None; }
4096 model_ref.get_or_insert_with(|| gm.clone());
4097 let gdt = if gcf.is_empty() {
4098 cortiq_core::TensorDtype::Q8Row
4099 } else {
4100 cortiq_core::TensorDtype::Q8_2f
4101 };
4102 let udt = if ucf.is_empty() {
4103 cortiq_core::TensorDtype::Q8Row
4104 } else {
4105 cortiq_core::TensorDtype::Q8_2f
4106 };
4107 jobs.push(crate::gpu::MoeJob {
4108 gate: (gi, gr, gc, grs),
4109 up: (ui, ur, uc, urs),
4110 down: (di, dr, dc, drs),
4111 xs_gate: prescale(x, gcf, gdt).into_owned(),
4112 xs_up: prescale(x, ucf, udt).into_owned(),
4113 down_col: dcf,
4114 w,
4115 q1: gq1,
4116 });
4117 Some(())
4118}
4119
4120fn sparse_ffn_quant(
4127 d: &DenseFfn,
4128 x: &[f32],
4129 active: &[u16],
4130 hidden: usize,
4131 pool: Option<&Pool>,
4132) -> Vec<f32> {
4133 let n = active.len();
4134 let inter = d.gate_proj.rows();
4135 let mut act = vec![0.0f32; n];
4136 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
4139 let compute = |ai: usize| -> f32 {
4140 let idx = active[ai] as usize;
4141 if idx >= inter {
4142 return 0.0; }
4144 let mut s = if need_scratch {
4145 vec![0.0f32; hidden]
4146 } else {
4147 Vec::new()
4148 };
4149 let gate = d.gate_proj.row_dot(idx, x, &mut s);
4150 let up = d.up_proj.row_dot(idx, x, &mut s);
4151 d.act.apply(gate) * up
4152 };
4153 match pool {
4154 Some(p) if n >= 256 => {
4155 let ptr = SendMut(act.as_mut_ptr());
4156 p.run(&|widx, nw| {
4157 let chunk = n.div_ceil(nw);
4158 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
4159 for ai in s..e {
4160 unsafe { *ptr.at(ai) = compute(ai) };
4161 }
4162 });
4163 }
4164 _ => {
4165 for (ai, a) in act.iter_mut().enumerate() {
4166 *a = compute(ai);
4167 }
4168 }
4169 }
4170 let mut out = vec![0.0f32; hidden];
4172 for (ai, &idx) in active.iter().enumerate() {
4173 let w = act[ai];
4174 if w.abs() >= 1e-12 && (idx as usize) < inter {
4175 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
4176 }
4177 }
4178 out
4179}
4180
4181#[doc(hidden)]
4183pub fn sparse_ffn_quant_for_test(
4184 d: &DenseFfn,
4185 x: &[f32],
4186 active: &[u16],
4187 hidden: usize,
4188) -> Vec<f32> {
4189 sparse_ffn_quant(d, x, active, hidden, None)
4190}
4191
4192fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
4196 let deq = |t: &QTensor| -> Vec<f32> {
4197 let (rows, cols) = (t.rows(), t.cols());
4198 let mut out = vec![0.0f32; rows * cols];
4199 for r in 0..rows {
4200 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
4201 }
4202 out
4203 };
4204 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
4205}
4206
4207struct SendMut(*mut f32);
4209unsafe impl Send for SendMut {}
4210unsafe impl Sync for SendMut {}
4211impl SendMut {
4212 #[inline]
4213 #[allow(clippy::mut_from_ref)]
4216 unsafe fn at(&self, i: usize) -> &mut f32 {
4217 unsafe { &mut *self.0.add(i) }
4218 }
4219}
4220
4221fn moe_route(logits: &[f32], m: &MoeFfn) -> (Vec<usize>, Vec<f32>, f32) {
4231 let ne = logits.len();
4232 let p: Vec<f32> = if m.router_sigmoid {
4233 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
4234 } else {
4235 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4236 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
4237 let s: f32 = e.iter().sum();
4238 for v in &mut e {
4239 *v /= s;
4240 }
4241 e
4242 };
4243 let mut idx: Vec<usize> = (0..ne).collect();
4244 match &m.expert_bias {
4246 Some(b) => idx.sort_unstable_by(|&x, &y| {
4247 (p[y] + b[y])
4248 .partial_cmp(&(p[x] + b[x]))
4249 .unwrap()
4250 .then(x.cmp(&y))
4251 }),
4252 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
4253 }
4254 idx.truncate(m.top_k);
4255 let wsum: f32 = if m.norm_topk_prob {
4256 let s: f32 = idx.iter().map(|&e| p[e]).sum();
4257 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
4260 } else {
4261 1.0 / m.routed_scaling
4262 };
4263 (idx, p, wsum)
4264}
4265
4266fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4269 let ne = m.experts.len();
4270 let mut logits = vec![0.0f32; ne];
4271 m.router.matvec(x, &mut logits, pool);
4272 let (idx, p, wsum) = moe_route(&logits, m);
4273 {
4274 let mut st = m.stats.borrow_mut();
4275 if st.len() < ne {
4276 st.resize(ne, 0);
4277 }
4278 for &e in &idx {
4279 st[e] += 1;
4280 }
4281 }
4282 if crate::gpu::enabled_here() {
4287 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
4288 crate::gpu::ProbeArm::Gpu => {
4289 let t0 = std::time::Instant::now();
4290 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
4291 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4292 return out;
4293 }
4294 }
4295 crate::gpu::ProbeArm::CpuTimed => {
4296 let t0 = std::time::Instant::now();
4297 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4298 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4299 return out;
4300 }
4301 crate::gpu::ProbeArm::Cpu => {
4302 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4303 }
4304 }
4305 }
4306 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
4307}
4308
4309fn moe_ffn_cpu(
4311 m: &MoeFfn,
4312 x: &[f32],
4313 idx: &[usize],
4314 p: &[f32],
4315 wsum: f32,
4316 pool: Option<&Pool>,
4317) -> Vec<f32> {
4318 let mut out = attention::take_buf(x.len());
4319 for &e in idx {
4320 let mut eo = dense_ffn(&m.experts[e], x, pool);
4321 let w = p[e] / wsum;
4322 for i in 0..out.len() {
4323 out[i] += w * eo[i];
4324 }
4325 attention::recycle_buf(&mut eo);
4326 }
4327 if let Some((se, gate)) = &m.shared {
4328 let mut so = dense_ffn(se, x, pool);
4329 let g = gate.as_ref().map_or(1.0, |gate| {
4330 let mut gl = [0.0f32; 1];
4331 gate.matvec(x, &mut gl, pool);
4332 1.0 / (1.0 + (-gl[0]).exp())
4333 });
4334 for i in 0..out.len() {
4335 out[i] += g * so[i];
4336 }
4337 attention::recycle_buf(&mut so);
4338 }
4339 out
4340}
4341
4342fn moe_ffn_gpu(
4345 m: &MoeFfn,
4346 x: &[f32],
4347 idx: &[usize],
4348 p: &[f32],
4349 wsum: f32,
4350 pool: Option<&Pool>,
4351) -> Option<Vec<f32>> {
4352 use crate::gpu::MoeJob;
4353
4354 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
4355 let mut model_ref = None;
4356 for &e in idx {
4357 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
4358 }
4359 if let Some((se, gate)) = &m.shared {
4360 let g = gate.as_ref().map_or(1.0, |gate| {
4361 let mut gl = [0.0f32; 1];
4362 gate.matvec(x, &mut gl, pool);
4363 1.0 / (1.0 + (-gl[0]).exp())
4364 });
4365 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
4366 }
4367 let model = model_ref?;
4368 let hidden = jobs[0].down.1;
4369 let mut out = vec![0.0f32; hidden];
4370 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
4371}
4372
4373fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4375 match ffn {
4376 FfnKind::Dense(d) => dense_ffn(d, x, pool),
4377 FfnKind::Moe(m) => moe_ffn(m, x, pool),
4378 }
4379}
4380
4381fn ffn_forward_pair(
4385 ffn: &FfnKind,
4386 x1: &[f32],
4387 x2: &[f32],
4388 pool: Option<&Pool>,
4389) -> (Vec<f32>, Vec<f32>) {
4390 let d = match ffn {
4391 FfnKind::Dense(d) => d,
4392 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
4393 };
4394 let inter = d.gate_proj.rows();
4395 FFN_SCRATCH.with(|s| {
4396 let mut s = s.borrow_mut();
4397 let [g1, g2, u1, u2] = &mut *s;
4398 g1.resize(inter, 0.0);
4399 g2.resize(inter, 0.0);
4400 u1.resize(inter, 0.0);
4401 u2.resize(inter, 0.0);
4402 QTensor::matvec2_many(
4405 [&d.gate_proj, &d.up_proj],
4406 x1,
4407 x2,
4408 [g1.as_mut_slice(), u1.as_mut_slice()],
4409 [g2.as_mut_slice(), u2.as_mut_slice()],
4410 pool,
4411 );
4412 for i in 0..inter {
4413 g1[i] = d.act.apply(g1[i]) * u1[i];
4414 g2[i] = d.act.apply(g2[i]) * u2[i];
4415 }
4416 let mut o1 = attention::take_buf(d.down_proj.rows());
4417 let mut o2 = attention::take_buf(d.down_proj.rows());
4418 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
4419 (o1, o2)
4420 })
4421}
4422
4423#[cfg(test)]
4424mod tests {
4425 use super::*;
4426
4427 #[test]
4433 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
4434 let (hidden, inter) = (16usize, 40usize);
4435 let synth = |n: usize, salt: usize| -> Vec<f32> {
4436 (0..n)
4437 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
4438 .collect()
4439 };
4440 let d = DenseFfn {
4441 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
4442 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
4443 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
4444 act: Act::Silu,
4445 };
4446 let x = synth(hidden, 9);
4447 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
4449
4450 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
4451
4452 let mut g = vec![0.0f32; inter];
4454 d.gate_proj.matvec(&x, &mut g, None);
4455 let mut u = vec![0.0f32; inter];
4456 d.up_proj.matvec(&x, &mut u, None);
4457 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
4458 for i in 0..inter {
4459 g[i] = if act_set.contains(&(i as u16)) {
4460 inference::silu(g[i]) * u[i]
4461 } else {
4462 0.0
4463 };
4464 }
4465 let mut reference = vec![0.0f32; hidden];
4466 d.down_proj.matvec(&g, &mut reference, None);
4467
4468 let max_d = sparse
4469 .iter()
4470 .zip(&reference)
4471 .map(|(a, b)| (a - b).abs())
4472 .fold(0.0f32, f32::max);
4473 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
4474 }
4475
4476 fn attach_test_mtp(p: &mut Pipeline) {
4478 let (h, inter, heads, kv, hd) = (
4479 p.hidden_size,
4480 p.intermediate_size,
4481 p.num_heads,
4482 p.num_kv_heads,
4483 p.head_dim,
4484 );
4485 let synth = |n: usize, salt: usize| -> Vec<f32> {
4486 (0..n)
4487 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
4488 .collect()
4489 };
4490 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4491 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4492 };
4493 p.mtp = Some(MtpModule {
4494 enorm: vec![1.0; h],
4495 hnorm: vec![1.0; h],
4496 eh_proj: qt(h, 2 * h, 301),
4497 layer: LayerWeights {
4498 input_norm: vec![1.0; h],
4499 post_norm: vec![1.0; h],
4500 attn_out_norm: None,
4501 ffn_out_norm: None,
4502 layer_scale: None,
4503 ffn: FfnKind::Dense(DenseFfn {
4504 gate_proj: qt(inter, h, 315),
4505 up_proj: qt(inter, h, 316),
4506 down_proj: qt(h, inter, 317),
4507 act: Act::Silu,
4508 }),
4509 attn: AttnKind::Full {
4510 bias: None,
4511 wq: qt(heads * hd, h, 311),
4512 wk: qt(kv * hd, h, 312),
4513 wv: qt(kv * hd, h, 313),
4514 wo: qt(h, heads * hd, 314),
4515 q_norm: None,
4516 k_norm: None,
4517 output_gate: false,
4518 softplus_gate: None,
4519 },
4520 },
4521 final_norm: vec![1.0; h],
4522 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
4523 });
4524 }
4525
4526 #[test]
4527 fn speculative_equals_vanilla_greedy() {
4528 let run = |spec: bool| {
4529 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4530 p.sampler_config.temperature = 0.0;
4531 attach_test_mtp(&mut p);
4532 p.speculative = spec;
4533 let r = p.generate("abcdef", 12, None, None).unwrap();
4534 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
4535 };
4536 let (vanilla, d0, _) = run(false);
4537 let (spec, d1, a1) = run(true);
4538 assert_eq!(d0, 0, "vanilla path must not draft");
4539 assert!(d1 > 0, "speculative path must draft");
4540 assert_eq!(
4541 vanilla, spec,
4542 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
4543 );
4544 }
4545
4546 #[test]
4547 fn speculative_accepts_constant_oracle() {
4548 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4549 p.sampler_config.temperature = 0.0;
4550 p.sampler_config.repetition_penalty = 1.0;
4551 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
4554 attach_test_mtp(&mut p);
4555 p.speculative = true;
4556 let r = p.generate("abcd", 10, None, None).unwrap();
4557 assert!(r.mtp_drafted > 0);
4558 assert_eq!(
4559 r.mtp_accepted, r.mtp_drafted,
4560 "constant logits → every draft accepted"
4561 );
4562 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
4565 }
4566
4567 #[test]
4568 fn empty_prompt_is_an_error_not_a_panic() {
4569 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4570 let r = p.generate("", 4, None, None);
4571 assert!(r.is_err(), "empty prompt must be a clean error");
4572 }
4573
4574 #[test]
4575 fn every_token_enters_kv_exactly_once() {
4576 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4577 p.sampler_config.temperature = 0.0;
4579 let r = p.generate("abc", 2, None, None).unwrap();
4580 assert_eq!(r.prompt_tokens, 3);
4581 assert_eq!(
4585 p.kv_cache.seq_len(),
4586 3 + r.tokens_generated - 1,
4587 "each token must be cached exactly once (v1 cached the last prompt token twice)"
4588 );
4589 }
4590
4591 #[test]
4592 fn generation_is_reproducible_with_seed() {
4593 let run = || {
4594 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4595 p.generate("hello", 8, None, None).unwrap().token_ids
4596 };
4597 assert_eq!(run(), run());
4598 }
4599
4600 #[test]
4601 fn resetting_sampler_restarts_the_seeded_stream() {
4602 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4603 let config = SamplerConfig {
4604 seed: Some(1234),
4605 ..SamplerConfig::default()
4606 };
4607 p.set_sampler_config(config.clone());
4608 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
4609 p.set_sampler_config(config);
4610 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
4611 assert_eq!(first, second);
4612 }
4613
4614 #[test]
4615 fn eviction_bounds_the_cache() {
4616 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4617 p.kv_cache.max_seq_len = 6;
4618 p.sampler_config.temperature = 0.0;
4619 let _ = p.generate("abcd", 12, None, None).unwrap();
4620 assert!(
4621 p.kv_cache.seq_len() <= 6 + 1,
4622 "cache must stay bounded by max_seq_len (got {})",
4623 p.kv_cache.seq_len()
4624 );
4625 }
4626
4627 #[test]
4628 fn confidence_matches_tokens_and_is_a_probability() {
4629 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4630 p.sampler_config.temperature = 0.0;
4631 p.sampler_config.repetition_penalty = 1.0;
4632 let r = p.generate("abcd", 10, None, None).unwrap();
4633 assert_eq!(
4634 r.token_confidence.len(),
4635 r.token_ids.len(),
4636 "one confidence per emitted token"
4637 );
4638 for &c in &r.token_confidence {
4639 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
4640 }
4641 let logits = [1.0f32, 3.0, 0.5, 3.0];
4643 let p0 = top1_prob_t(&logits, 1, 1.0);
4644 let p1 = top1_prob_t(&logits, 3, 1.0);
4645 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
4646 assert!(p0 > 0.0 && p0 < 1.0);
4647 let sharp = top1_prob_t(&logits, 1, 1.0);
4649 let soft = top1_prob_t(&logits, 1, 2.0);
4650 assert!(soft < sharp, "higher temperature lowers peak confidence");
4651 }
4652
4653 #[test]
4654 fn trace_is_opt_in_and_parallels_the_output() {
4655 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4657 p.sampler_config.temperature = 0.0;
4658 p.sampler_config.repetition_penalty = 1.0;
4659 let r = p.generate("abcd", 10, None, None).unwrap();
4660 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
4661
4662 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 p.set_trace(true);
4667 let r = p.generate("abcd", 10, None, None).unwrap();
4668 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
4669 for (i, tr) in r.traces.iter().enumerate() {
4670 assert_eq!(tr.t, i, "trace index is sequential");
4671 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
4672 assert_eq!(
4673 tr.confidence, r.token_confidence[i],
4674 "trace confidence matches the confidence channel"
4675 );
4676 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
4678 }
4679 }
4680
4681 #[test]
4682 fn explain_prefill_logits_match_greedy_first_token() {
4683 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4687 p.sampler_config.temperature = 0.0;
4688 p.sampler_config.repetition_penalty = 1.0;
4689 let ids = p.tokenizer.encode("abcd");
4690 let logits = p.prefill_next_logits(&ids, None);
4691 let argmax = logits
4692 .iter()
4693 .enumerate()
4694 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4695 .unwrap()
4696 .0 as u32;
4697 let r = p.generate("abcd", 1, None, None).unwrap();
4698 assert_eq!(
4699 argmax, r.token_ids[0],
4700 "explain preview must match greedy emit"
4701 );
4702 }
4703
4704 #[test]
4705 fn laguna_shared_expert_is_unconditionally_added() {
4706 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
4707 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
4708 let zero_dense = || DenseFfn {
4709 gate_proj: matrix(vec![0.0; 4]),
4710 up_proj: matrix(vec![0.0; 4]),
4711 down_proj: matrix(vec![0.0; 4]),
4712 act: Act::Silu,
4713 };
4714 let shared = DenseFfn {
4715 gate_proj: identity(),
4716 up_proj: identity(),
4717 down_proj: identity(),
4718 act: Act::Silu,
4719 };
4720 let x = [1.0, 2.0];
4721 let expected = dense_ffn(&shared, &x, None);
4722 let moe = MoeFfn {
4723 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
4724 experts: vec![zero_dense()],
4725 top_k: 1,
4726 norm_topk_prob: true,
4727 router_sigmoid: true,
4728 expert_bias: None,
4729 routed_scaling: 1.0,
4730 shared: Some((shared, None)),
4731 stats: std::cell::RefCell::new(Vec::new()),
4732 };
4733 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
4734 for (actual, expected) in actual.iter().zip(expected) {
4735 assert!((actual - expected).abs() < 1e-6);
4736 }
4737 }
4738}