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