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 logit_multiplier: Option<f32>,
81 pub kv_history: Vec<u32>,
86 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
88 pub short_conv_cfg: Option<ShortConvCfg>,
91 pub mtp: Option<MtpModule>,
93 pub speculative: bool,
95 rng: SplitMix64,
96 sampler_scratch: SamplerScratch,
97 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
101 ws: ForwardScratch,
105 pool: Option<std::sync::Arc<Pool>>,
107 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
111 pub(crate) dyn_force_f32: bool,
113 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
118 pub(crate) dyn_active: Option<usize>,
124 pub(crate) dyn_blend_loaded: bool,
128 pub(crate) dyn_phi_layer: Option<usize>,
131 dyn_phi_ema: Vec<f32>,
133 dyn_phi_seen: usize,
134 pub dyn_router: Option<crate::swarm::DynRouter>,
137 o1_cfg: Option<crate::nystrom::O1Cfg>,
140 o1_flags: Vec<bool>,
142 trace: bool,
145 calib_temp: f32,
148 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
150 graph_kv_id: u64,
151 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
154 graph_want_logits: bool,
155 graph_logits: Option<Vec<f32>>,
158 pub embed_multiplier: f32,
160 pub attn_scale: f32,
163 pub swa: Option<(usize, usize)>,
166 pub sliding_layers: Option<Vec<bool>>,
169 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
172 pub rotary_dim_local: Option<usize>,
173 pub rope_scale: f32,
174 pub rope_scale_local: f32,
175 pub global_attn: Option<(usize, usize)>,
178 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
181 pub attn_v_norm: bool,
183 pub final_softcap: Option<f32>,
185 pub attn_softcap: f32,
187 confidence_on: bool,
191}
192
193#[cfg(target_os = "macos")]
194impl Drop for Pipeline {
195 fn drop(&mut self) {
196 crate::gpu::kv_mirror_drop(self.graph_kv_id);
197 }
198}
199
200pub struct PipelineWeights {
205 pub embed_tokens: QTensor,
207 pub layers: Vec<LayerWeights>,
209 pub lm_head: QTensor,
211 pub final_norm: Vec<f32>,
213}
214
215pub struct LayerWeights {
217 pub input_norm: Vec<f32>,
218 pub post_norm: Vec<f32>,
221 pub attn_out_norm: Option<Vec<f32>>,
224 pub layer_scale: Option<f32>,
226 pub ffn_out_norm: Option<Vec<f32>>,
229 pub ffn: FfnKind,
230 pub attn: AttnKind,
231}
232
233#[derive(Clone, Copy, PartialEq, Debug, Default)]
236pub enum Act {
237 #[default]
238 Silu,
239 GeluTanh,
240 Situ { beta: f32, linear_beta: f32 },
243}
244
245impl Act {
246 pub fn from_arch(name: &str) -> Self {
247 if name == "gelu_tanh" {
248 Self::GeluTanh
249 } else {
250 Self::Silu
251 }
252 }
253
254 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
256 match arch.hidden_act.as_str() {
257 "situ" => Self::Situ {
258 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
259 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
260 },
261 other => Self::from_arch(other),
262 }
263 }
264
265 #[inline]
266 pub fn apply(self, x: f32) -> f32 {
267 match self {
268 Self::Silu => inference::silu(x),
269 Self::GeluTanh => inference::gelu_tanh(x),
270 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
271 }
272 }
273
274 #[inline]
277 pub fn combine(self, g: f32, u: f32) -> f32 {
278 match self {
279 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
280 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
281 }
282 _ => self.apply(g) * u,
283 }
284 }
285}
286
287pub struct DenseFfn {
289 pub gate_proj: QTensor,
290 pub up_proj: QTensor,
291 pub down_proj: QTensor,
292 pub act: Act,
294}
295
296pub enum FfnKind {
299 Dense(DenseFfn),
300 Moe(MoeFfn),
304 DenseMoe(Box<DenseMoeFfn>),
311}
312
313pub struct DenseMoeFfn {
315 pub dense: DenseFfn,
316 pub moe: MoeFfn,
317 pub post_norm_1: Vec<f32>,
319 pub pre_norm_2: Vec<f32>,
322 pub post_norm_2: Vec<f32>,
324}
325
326pub struct MoeFfn {
327 pub router: QTensor,
329 pub experts: Vec<DenseFfn>,
330 pub top_k: usize,
331 pub norm_topk_prob: bool,
332 pub router_sigmoid: bool,
335 pub expert_bias: Option<Vec<f32>>,
339 pub routed_scaling: f32,
342 pub route_tau: Option<f32>,
348 pub shared: Option<(DenseFfn, Option<QTensor>)>,
351 pub stats: std::cell::RefCell<Vec<u64>>,
355 pub mask: Option<Vec<bool>>,
360 pub per_expert_scale: Option<Vec<f32>>,
363 pub router_input_norm: bool,
367}
368
369pub enum AttnKind {
372 Full {
374 wq: QTensor,
375 wk: QTensor,
376 wv: QTensor,
377 wo: QTensor,
378 q_norm: Option<Vec<f32>>,
379 k_norm: Option<Vec<f32>>,
380 output_gate: bool,
381 softplus_gate: Option<(QTensor, bool)>,
385 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
387 },
388 Linear(VmfPhaseWeights),
390 LinearGdn(GdnWeights),
392 ShortConv(ShortConvWeights),
395 Mla(Box<MlaWeights>),
403 Kda(Box<crate::linear_core::KdaWeights>),
407}
408
409pub struct MlaWeights {
411 pub q_proj: QTensor,
415 pub q_a: Option<QTensor>,
418 pub q_a_norm: Option<Vec<f32>>,
419 pub kv_a: QTensor,
421 pub kv_a_norm: Vec<f32>,
423 pub kv_b: QTensor,
425 pub o_proj: QTensor,
427 pub nh: usize,
428 pub qk_rope: usize,
429 pub qk_nope: usize,
430 pub v_dim: usize,
431 pub lora: usize,
432 pub scale: f32,
434 pub nope: bool,
436}
437
438pub struct MtpModule {
443 pub enorm: Vec<f32>,
444 pub hnorm: Vec<f32>,
445 pub eh_proj: QTensor,
447 pub layer: LayerWeights,
448 pub final_norm: Vec<f32>,
449 pub kv: crate::kv_cache::LayerKvCache,
450}
451
452pub struct GenerateResult {
454 pub text: String,
455 pub token_ids: Vec<u32>,
456 pub prompt_tokens: usize,
457 pub tokens_generated: usize,
458 pub finish_reason: String,
459 pub mtp_drafted: usize,
461 pub mtp_accepted: usize,
462 pub token_confidence: Vec<f32>,
467 pub traces: Vec<TokenTrace>,
470}
471
472#[derive(Clone, Debug)]
477pub struct TokenTrace {
478 pub t: usize,
480 pub token_id: u32,
482 pub confidence: f32,
484 pub active_skill: Option<String>,
486 pub recon: Option<f32>,
490 pub switched: bool,
493}
494
495fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
500 let t = if temp > 1e-3 { temp } else { 1.0 };
501 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
502 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
503 if sum > 0.0 {
504 (((logits[id as usize] - max) / t).exp()) / sum
505 } else {
506 0.0
507 }
508}
509
510fn prefill_batched() -> bool {
513 std::env::var("CMF_PREFILL")
514 .map(|v| v != "seq")
515 .unwrap_or(true)
516}
517
518fn prefill_chunk() -> usize {
523 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
524 .ok()
525 .and_then(|v| v.parse::<usize>().ok())
526 {
527 return n.max(1);
528 }
529 if cfg!(target_os = "macos") {
530 512
531 } else if cfg!(target_arch = "aarch64") {
532 256
535 } else {
536 48
537 }
538}
539
540pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
542
543impl Pipeline {
544 #[inline]
548 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
549 virtual_idx % self.physical_layers
550 }
551
552 #[inline]
555 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
556 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
557 }
558
559 #[allow(clippy::too_many_arguments)]
561
562 #[cfg(target_os = "macos")]
581 fn graph_prefill_preferred(&self) -> bool {
582 if !crate::gpu::enabled_here()
583 || !crate::gpu::q1_force()
584 || std::env::var("CMF_GPU_BLOCK")
585 .map(|v| v == "0")
586 .unwrap_or(false)
587 {
588 return false;
589 }
590 if self.loop_final_norm {
591 return true;
592 }
593 self.weights
594 .layers
595 .iter()
596 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
597 }
598
599 #[cfg(not(target_os = "macos"))]
600 fn graph_prefill_preferred(&self) -> bool {
601 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
609 .map(|v| v != "0")
610 .unwrap_or_else(|_| {
611 crate::gpu::wgpu_graph_default()
615 });
616 if !graph_on || !crate::gpu::enabled_here() {
617 return false;
618 }
619 self.weights
620 .layers
621 .iter()
622 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
623 }
624
625 #[cfg(target_os = "macos")]
626 fn q1_graph_gpu(
627 &mut self,
628 start: usize,
629 upto: Option<usize>,
630 position: usize,
631 h: &mut [f32],
632 ) -> usize {
633 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
634 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
636 || !crate::gpu::q1_force()
637 || std::env::var("CMF_GPU_BLOCK")
638 .map(|v| v == "0")
639 .unwrap_or(false)
640 {
641 return start;
642 }
643 if self.swa.is_some()
648 || self.global_attn.is_some()
649 || self.attention_heads_per_layer.is_some()
650 || self.attn_v_norm
651 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
652 || self.weights.layers.iter().any(|lw| {
653 lw.attn_out_norm.is_some()
654 || lw.ffn_out_norm.is_some()
655 || lw.layer_scale.is_some()
656 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
657 })
658 {
659 return start;
660 }
661 let limit = upto
664 .map(|u| u + 1)
665 .unwrap_or(self.num_layers)
666 .min(self.num_layers);
667
668 enum Item<'a> {
669 Gdn {
670 run: Vec<GdnGpuLayer<'a>>,
671 first: usize,
672 },
673 Attn {
674 l: AttnGpuLayer<'a>,
675 li: usize,
676 q_norm: Option<&'a [f32]>,
677 k_norm: Option<&'a [f32]>,
678 output_gate: bool,
679 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
680 full_gpu: bool,
683 },
684 }
685
686 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
688 let dev_attend = attend_mode != "0"
689 && attend_mode != "off"
690 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
694 && self.head_dim % 4 == 0
695 && self.head_dim <= 256
696 && self.rotary_dim >= 2
697 && self.rotary_dim <= self.head_dim
698 && (self.rotary_dim / 2) % 32 == 0
699 && self.num_kv_heads > 0
700 && self.num_heads % self.num_kv_heads == 0;
701
702 let mut plan: Vec<Item> = Vec::new();
703 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
704 let mut scan = start;
705 while scan < limit {
706 let lw = &self.weights.layers[self.phys_layer(scan)];
707 let FfnKind::Dense(d) = &lw.ffn else { break };
708 let (Some(g), Some(u), Some(dn)) = (
709 d.gate_proj.q1_parts(),
710 d.up_proj.q1_parts(),
711 d.down_proj.q1_parts(),
712 ) else {
713 break;
714 };
715 match &lw.attn {
716 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
717 let parts = (
718 w.in_proj_qkv.q1_parts(),
719 w.in_proj_z.q1_parts(),
720 w.in_proj_a.f32_parts(),
721 w.in_proj_b.f32_parts(),
722 w.out_proj.q1_parts(),
723 );
724 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
725 break;
726 };
727 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
728 model_ref.get_or_insert_with(|| model.clone());
729 }
730 let gl = GdnGpuLayer {
731 attn_norm: &lw.input_norm,
732 post_norm: &lw.post_norm,
733 qkv,
734 z,
735 a,
736 b,
737 out,
738 gate: g,
739 up: u,
740 down: dn,
741 conv1d: &w.conv1d,
742 a_log: &w.a_log,
743 dt_bias: &w.dt_bias,
744 gnorm: &w.norm,
745 };
746 match plan.last_mut() {
747 Some(Item::Gdn { run, .. }) => run.push(gl),
748 _ => plan.push(Item::Gdn {
749 run: vec![gl],
750 first: scan,
751 }),
752 }
753 }
754 AttnKind::Full {
755 wq,
756 wk,
757 wv,
758 wo,
759 q_norm,
760 k_norm,
761 output_gate,
762 softplus_gate: None,
763 bias,
764 } if !self.kv_cache.layers[scan].o1_sealed() => {
765 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
766 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
767 break;
768 };
769 if let QTensor::Mapped { model, .. } = wq {
770 model_ref.get_or_insert_with(|| model.clone());
771 }
772 let cache = &self.kv_cache.layers[scan];
773 let full_gpu = dev_attend
774 && cache.mode == crate::kv_cache::KvMode::F32
775 && cache.o1.is_none()
776 && bias.is_none()
777 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
778 && pk.1 == self.num_kv_heads * self.head_dim
779 && pv.1 == self.num_kv_heads * self.head_dim
780 && po.2 == self.num_heads * self.head_dim;
781 plan.push(Item::Attn {
782 l: AttnGpuLayer {
783 attn_norm: &lw.input_norm,
784 post_norm: &lw.post_norm,
785 wq: pq,
786 wk: pk,
787 wv: pv,
788 wo: po,
789 gate: g,
790 up: u,
791 down: dn,
792 },
793 li: scan,
794 q_norm: q_norm.as_deref(),
795 k_norm: k_norm.as_deref(),
796 output_gate: *output_gate,
797 bias: bias
798 .as_ref()
799 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
800 full_gpu,
801 });
802 }
803 _ => break,
804 }
805 scan += 1;
806 }
807 let Some(model) = model_ref else { return start };
808 if plan.is_empty() {
809 return start;
810 }
811 let dims = GraphDims {
812 hidden: self.hidden_size,
813 eps: self.rms_eps as f32,
814 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
815 };
816 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
817 return start;
818 };
819 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
820 nv: cfg.num_v_heads,
821 nk: cfg.num_k_heads,
822 dk: cfg.key_head_dim,
823 dv: cfg.value_head_dim,
824 kk: cfg.conv_kernel,
825 hidden: self.hidden_size,
826 inter: self.intermediate_size,
827 c_dim: cfg.conv_dim(),
828 eps: cfg.rms_eps as f32,
829 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
830 });
831 let mut valid = 0usize;
835 let mut end = start;
836 for item in &plan {
837 let ok = match item {
838 Item::Gdn { run, .. } => gcfg
839 .as_ref()
840 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
841 .unwrap_or(false),
842 Item::Attn { l, .. } => graph.attn_ok(l),
843 };
844 if !ok {
845 break;
846 }
847 valid += 1;
848 end += match item {
849 Item::Gdn { run, .. } => run.len(),
850 Item::Attn { .. } => 1,
851 };
852 }
853 plan.truncate(valid);
854 if plan.is_empty() {
855 return start;
856 }
857
858 let inv_freq = self.inv_freq.clone();
859 let pool = self.pool.clone();
860 let (nh, nkv, hd, hs, rd, eps) = (
861 self.num_heads,
862 self.num_kv_heads,
863 self.head_dim,
864 self.hidden_size,
865 self.rotary_dim,
866 self.rms_eps,
867 );
868 let norm_style = self.norm_style;
869 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
870 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
871 let kv_id = self.graph_kv_id;
872 let mut pending: Vec<(usize, usize)> = Vec::new();
875 let mut dev_attn: Vec<usize> = Vec::new();
878 for item in &plan {
879 if self.loop_final_norm {
881 let item_start = match item {
882 Item::Gdn { first, .. } => *first,
883 Item::Attn { li, .. } => *li,
884 };
885 if item_start > start && self.is_loop_end(item_start - 1) {
886 graph.encode_loop_norm(&self.weights.final_norm);
887 }
888 }
889 match item {
890 Item::Gdn { run, first } => {
891 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
892 if l.linear_state.len() != want {
893 l.linear_state = vec![0f32; want];
894 }
895 }
896 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
897 .iter()
898 .map(|l| l.linear_state.as_slice())
899 .collect();
900 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
901 tracing::error!("q1 graph: GDN run refused after validation");
903 return start;
904 }
905 graph.commit();
908 pending.push((*first, run.len()));
909 }
910 Item::Attn {
911 l,
912 li,
913 q_norm,
914 k_norm,
915 output_gate,
916 bias,
917 full_gpu,
918 } => {
919 if *full_gpu {
921 let cache = &self.kv_cache.layers[*li];
922 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
923 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
924 let cpu_stored = cpu_k[0].len() / hd;
925 let p = crate::gpu::AttnDeviceParams {
926 kv_id,
927 layer: *li,
928 nh,
929 nkv,
930 hd,
931 rd,
932 position,
933 eps: eps as f32,
934 gemma,
935 output_gate: *output_gate,
936 q_norm: *q_norm,
937 k_norm: *k_norm,
938 inv_freq: &inv_freq,
939 cpu_k,
940 cpu_v,
941 cpu_stored,
942 };
943 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
944 graph.commit();
945 dev_attn.push(*li);
946 continue;
947 }
948 }
950 graph.encode_attn_prefix(l);
951 graph.sync();
952 if !pending.is_empty() {
953 let idxs: Vec<usize> =
954 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
955 let mut outs: Vec<&mut [f32]> = self
956 .kv_cache
957 .layers
958 .iter_mut()
959 .enumerate()
960 .filter(|(i, _)| idxs.binary_search(i).is_ok())
961 .map(|(_, s)| s.linear_state.as_mut_slice())
962 .collect();
963 graph.read_states(&mut outs);
964 }
965 let mut q_raw = attention::take_buf(l.wq.1);
966 let mut k = attention::take_buf(l.wk.1);
967 let mut v = attention::take_buf(l.wv.1);
968 graph.read_qkv(&mut q_raw, &mut k, &mut v);
969 let cfg = QwenAttnCfg {
970 num_heads: nh,
971 num_kv_heads: nkv,
972 head_dim: hd,
973 hidden_size: hs,
974 position,
975 inv_freq: &inv_freq,
976 rotary_dim: rd,
977 scale: self.attn_scale,
978 softcap: self.attn_softcap,
979 window: None,
980 v_norm: false,
981 q_norm: *q_norm,
982 k_norm: *k_norm,
983 output_gate: *output_gate,
984 softplus_gate: None,
985 rope_scale: 1.0,
986 bias: *bias,
987 rms_eps: eps,
988 norm_style,
989 pool: pool.as_deref(),
990 };
991 let mut ao = attention::qwen_attention_core(
992 q_raw,
993 k,
994 v,
995 &mut self.kv_cache.layers[*li],
996 &cfg,
997 );
998 graph.encode_attn_suffix(l, &ao);
999 graph.commit();
1002 attention::recycle_buf(&mut ao);
1003 }
1004 }
1005 }
1006 let mut lm_rows = None;
1011 if self.graph_want_logits
1012 && upto.is_none()
1013 && end == self.num_layers
1014 && std::env::var("CMF_GPU_LMHEAD")
1015 .map(|v| v != "0")
1016 .unwrap_or(true)
1017 {
1018 if let Some(lm) = self.weights.lm_head.q1_parts() {
1019 if graph.lm_head_ok(lm) {
1020 graph.encode_lm_head(&self.weights.final_norm, lm);
1021 lm_rows = Some(lm.1);
1022 }
1023 }
1024 }
1025 graph.sync();
1026 if !pending.is_empty() {
1027 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1028 let mut outs: Vec<&mut [f32]> = self
1029 .kv_cache
1030 .layers
1031 .iter_mut()
1032 .enumerate()
1033 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1034 .map(|(_, s)| s.linear_state.as_mut_slice())
1035 .collect();
1036 graph.read_states(&mut outs);
1037 }
1038 if let Some(rows) = lm_rows {
1039 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1040 graph.read_logits(&mut lg);
1041 lg.resize(self.vocab_size, 0.0);
1042 if let Some(c) = self.final_softcap {
1043 for l in lg.iter_mut() {
1044 *l = c * (*l / c).tanh();
1045 }
1046 }
1047 self.graph_logits = Some(lg);
1048 }
1049 graph.finish(h);
1050 for li in dev_attn {
1054 let mut krow = attention::take_buf(nkv * hd);
1055 let mut vrow = attention::take_buf(nkv * hd);
1056 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1057 let cache = &mut self.kv_cache.layers[li];
1058 cache.append(&krow, &vrow, &[]);
1059 let n = cache.seq_len;
1060 let mut imp = attention::take_buf(n);
1061 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1062 cache.accumulate_imp(&imp);
1063 attention::recycle_buf(&mut imp);
1064 }
1065 attention::recycle_buf(&mut krow);
1066 attention::recycle_buf(&mut vrow);
1067 }
1068 end
1069 }
1070
1071 pub fn new(
1072 tokenizer: Tokenizer,
1073 weights: PipelineWeights,
1074 hidden_size: usize,
1075 intermediate_size: usize,
1076 num_heads: usize,
1077 num_kv_heads: usize,
1078 head_dim: usize,
1079 num_layers: usize,
1080 physical_layers: usize,
1081 loop_final_norm: bool,
1082 vocab_size: usize,
1083 rms_eps: f64,
1084 rope_base: f32,
1085 norm_style: NormStyle,
1086 max_seq_len: usize,
1087 sampler_config: SamplerConfig,
1088 ) -> Self {
1089 let rng = match sampler_config.seed {
1090 Some(s) => SplitMix64::new(s),
1091 None => SplitMix64::from_entropy(),
1092 };
1093 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1094 let pool = Pool::from_env();
1095 if let Some(p) = &pool {
1096 tracing::info!("worker pool: {} threads", p.n_workers());
1097 }
1098 Self {
1099 tokenizer: std::sync::Arc::new(tokenizer),
1100 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1101 sampler_config,
1102 weights,
1103 hidden_size,
1104 intermediate_size,
1105 num_heads,
1106 num_kv_heads,
1107 head_dim,
1108 num_layers,
1109 physical_layers,
1110 loop_final_norm,
1111 vocab_size,
1112 rms_eps,
1113 rope_base,
1114 norm_style,
1115 rotary_dim: head_dim,
1116 attention_heads_per_layer: None,
1117 vmf_cfg: None,
1118 gdn_cfg: None,
1119 kda_cfg: None,
1120 logit_multiplier: None,
1121 kv_history: Vec::new(),
1122 short_conv_cfg: None,
1123 mtp: None,
1124 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1125 rng,
1126 sampler_scratch: SamplerScratch::default(),
1127 inv_freq,
1128 ws: ForwardScratch::new(hidden_size),
1129 pool,
1130 model: None,
1131 dyn_force_f32: false,
1132 dyn_skill_layers: Vec::new(),
1133 dyn_active: None,
1134 dyn_blend_loaded: false,
1135 dyn_phi_layer: None,
1136 dyn_phi_ema: Vec::new(),
1137 dyn_phi_seen: 0,
1138 dyn_router: None,
1139 o1_cfg: None,
1140 o1_flags: Vec::new(),
1141 trace: false,
1142 calib_temp: 1.0,
1143 confidence_on: true,
1144 embed_multiplier: 1.0,
1145 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1146 swa: None,
1147 sliding_layers: None,
1148 inv_freq_local: None,
1149 rotary_dim_local: None,
1150 rope_scale: 1.0,
1151 rope_scale_local: 1.0,
1152 global_attn: None,
1153 inv_freq_global: None,
1154 attn_v_norm: false,
1155 final_softcap: None,
1156 attn_softcap: 0.0,
1157 graph_want_logits: false,
1158 graph_logits: None,
1159 graph_kv_id: {
1160 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1161 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1162 },
1163 }
1164 }
1165
1166 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1173 self.o1_flags = match &cfg {
1174 Some(c) => {
1175 let mut flags = c.layer_flags(self.num_layers);
1176 for (li, f) in flags.iter_mut().enumerate() {
1177 if *f
1178 && !matches!(
1179 self.weights.layers[self.phys_layer(li)].attn,
1180 AttnKind::Full { .. }
1181 )
1182 {
1183 *f = false;
1184 }
1185 }
1186 flags
1187 }
1188 None => Vec::new(),
1189 };
1190 if let Some(c) = &cfg {
1191 let n = self.o1_flags.iter().filter(|&&f| f).count();
1192 tracing::info!(
1193 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1194 self.num_layers,
1195 c.m,
1196 c.w,
1197 c.sink,
1198 c.rect
1199 );
1200 }
1201 self.o1_cfg = cfg;
1202 }
1203
1204 pub fn o1_active(&self) -> bool {
1206 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1207 }
1208
1209 fn o1_begin(&mut self) {
1211 if let Some(c) = &self.o1_cfg {
1212 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1213 for (li, &f) in self.o1_flags.iter().enumerate() {
1214 if f {
1215 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1216 }
1217 }
1218 }
1219 }
1220
1221 fn o1_seal(&mut self) {
1224 if self.o1_cfg.is_none() {
1225 return;
1226 }
1227 for li in 0..self.num_layers {
1228 if self.o1_flags.get(li).copied().unwrap_or(false) {
1229 self.kv_cache.layers[li].o1_seal(self.num_heads);
1230 }
1231 }
1232 }
1233
1234 pub fn set_trace(&mut self, on: bool) {
1236 self.trace = on;
1237 }
1238
1239 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1242 self.rng = match config.seed {
1243 Some(seed) => SplitMix64::new(seed),
1244 None => SplitMix64::from_entropy(),
1245 };
1246 self.sampler_config = config;
1247 }
1248
1249 pub fn set_confidence(&mut self, on: bool) {
1254 self.confidence_on = on;
1255 }
1256
1257 pub fn set_calib_temp(&mut self, t: f32) {
1260 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1261 }
1262
1263 pub fn calib_temp(&self) -> f32 {
1265 self.calib_temp
1266 }
1267
1268 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1271 self.rotary_dim = rotary_dim.min(self.head_dim);
1272 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1273 }
1274
1275 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1276 QwenAttnCfg {
1277 num_heads: self.num_heads,
1278 num_kv_heads: self.num_kv_heads,
1279 head_dim: self.head_dim,
1280 hidden_size: self.hidden_size,
1281 position,
1282 inv_freq: &self.inv_freq,
1283 rotary_dim: self.rotary_dim,
1284 scale: self.attn_scale,
1285 softcap: self.attn_softcap,
1286 window: None,
1287 v_norm: false,
1288 q_norm: None,
1289 k_norm: None,
1290 output_gate: false,
1291 softplus_gate: None,
1292 rope_scale: self.rope_scale,
1293 bias: None,
1294 rms_eps: self.rms_eps,
1295 norm_style: self.norm_style,
1296 pool: self.pool.as_deref(),
1297 }
1298 }
1299
1300 pub fn generate(
1302 &mut self,
1303 prompt: &str,
1304 max_tokens: usize,
1305 task_mask: Option<&TaskMask>,
1306 on_token: Option<TokenCallback>,
1307 ) -> Result<GenerateResult, String> {
1308 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1309 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1310 }
1311
1312 pub fn generate_from_ids(
1320 &mut self,
1321 input_ids: &[u32],
1322 max_tokens: usize,
1323 task_mask: Option<&TaskMask>,
1324 mut on_token: Option<TokenCallback>,
1325 ) -> Result<GenerateResult, String> {
1326 if std::env::var("CMF_TRACE_H").is_ok() {
1327 eprintln!("input_ids: {input_ids:?}");
1328 }
1329 if input_ids.is_empty() {
1330 return Err("empty prompt: nothing to generate from".to_string());
1331 }
1332
1333 let reuse_from = {
1341 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1342 let h = &self.kv_history;
1343 if on
1344 && task_mask.is_none()
1345 && self.mtp.is_none()
1346 && self.o1_cfg.is_none()
1347 && !h.is_empty()
1348 && h.len() < input_ids.len()
1349 && input_ids[..h.len()] == h[..]
1350 {
1351 h.len()
1352 } else {
1353 0
1354 }
1355 };
1356 if reuse_from == 0 {
1357 self.kv_cache.clear();
1359 self.kv_history.clear();
1360 crate::gpu::graph_kv_reset(self.graph_kv_id);
1361 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1362 eprintln!(
1363 "kv-reuse: {} of {} prompt positions already cached",
1364 reuse_from,
1365 input_ids.len()
1366 );
1367 }
1368 crate::gpu::graph_race_begin_generation();
1369 self.o1_begin();
1370
1371 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1377 .map(|v| v != "0")
1378 .unwrap_or_else(|_| {
1379 crate::gpu::wgpu_graph_default()
1383 });
1384 let spec_active = self.speculative
1385 && self.mtp.is_some()
1386 && task_mask.is_none()
1387 && !self.o1_active()
1388 && !graph_on
1389 && self.sampler_config.temperature < 1e-6;
1390 let mut mtp = if spec_active { self.mtp.take() } else { None };
1393 if let Some(m) = &mut mtp {
1394 m.kv.clear();
1395 }
1396 let mut router = if mtp.is_none() {
1400 self.dyn_router.take()
1401 } else {
1402 None
1403 };
1404 if let Some(r) = &mut router {
1405 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1408 }
1409
1410 let mut all_ids = input_ids.to_vec();
1411 let mut generated = 0usize;
1412 let mut finish_reason = "max_tokens".to_string();
1413 let mut drafted = 0usize;
1414 let mut accepted = 0usize;
1415 let mut confidence: Vec<f32> = Vec::new();
1416 let trace_on = self.trace;
1417 let calib_temp = self.calib_temp;
1418 let mut traces: Vec<TokenTrace> = Vec::new();
1419
1420 let mut hidden = vec![0.0f32; self.hidden_size];
1426 let mut pos = reuse_from;
1427 let fuse_lm = mtp.is_none() && router.is_none();
1431 self.graph_logits = None;
1432 self.graph_want_logits = false;
1433 let dyn_prefill = router.is_some();
1438 let graph_prefill = self.graph_prefill_preferred();
1444 if task_mask.is_none()
1445 && !dyn_prefill
1446 && !graph_prefill
1447 && prefill_batched()
1448 && input_ids.len() > 2
1449 {
1450 let chunk = prefill_chunk();
1456 let hs = self.hidden_size;
1457 while pos < input_ids.len() {
1458 let end = (pos + chunk).min(input_ids.len());
1459 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1460 if let Some(m) = &mut mtp {
1461 for p in pos..end {
1462 if p + 1 < input_ids.len() {
1463 let _ = self.mtp_step(
1464 m,
1465 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1466 input_ids[p + 1],
1467 p,
1468 );
1469 }
1470 }
1471 }
1472 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1473 pos = end;
1474 }
1475 }
1476 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
1477 if task_mask.is_none() && !dyn_prefill && !graph_prefill && !pair_off && self.pair_supported()
1478 {
1479 while pos + 1 < input_ids.len() {
1480 let e1 = self.embed_single(input_ids[pos]);
1481 let e2 = self.embed_single(input_ids[pos + 1]);
1482 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1483 self.commit_linear_scratch();
1485 if let Some(m) = &mut mtp {
1486 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1487 if pos + 2 < input_ids.len() {
1488 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1489 }
1490 }
1491 hidden = h2;
1492 pos += 2;
1493 }
1494 }
1495 let _tpf = std::time::Instant::now();
1504 let batch_k = std::env::var("CMF_BATCH_K")
1505 .ok()
1506 .and_then(|v| v.parse::<usize>().ok())
1507 .unwrap_or(0);
1508 if batch_k > 0
1509 && graph_prefill
1510 && task_mask.is_none()
1511 && mtp.is_none()
1512 && !dyn_prefill
1513 && pos + 1 < input_ids.len()
1514 {
1515 let hs = self.hidden_size;
1516 let chunk = batch_k;
1517 while pos < input_ids.len() {
1518 let end = (pos + chunk).min(input_ids.len());
1519 let bk = end - pos;
1520 let mut hiddens = vec![0f32; bk * hs];
1521 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1522 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1523 }
1524 let positions: Vec<usize> = (pos..end).collect();
1525 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1526 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1527 pos = end;
1528 } else {
1529 break; }
1531 }
1532 }
1533 while pos < input_ids.len() {
1534 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1535 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1536 if let Some(m) = &mut mtp {
1537 if pos + 1 < input_ids.len() {
1538 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1539 }
1540 }
1541 pos += 1;
1542 }
1543 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1544 eprintln!(
1545 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1546 input_ids.len(),
1547 _tpf.elapsed().as_secs_f64() * 1000.0
1548 );
1549 }
1550 self.o1_seal();
1553
1554 macro_rules! commit {
1556 ($id:expr) => {{
1557 all_ids.push($id);
1558 generated += 1;
1559 if self.tokenizer.is_eos($id) {
1560 finish_reason = "stop".to_string();
1561 false
1562 } else {
1563 let token_text = self.tokenizer.decode_token($id);
1564 let mut go = true;
1565 if let Some(ref mut cb) = on_token {
1566 if !cb(&token_text) {
1567 finish_reason = "cancelled".to_string();
1568 go = false;
1569 }
1570 }
1571 go
1572 }
1573 }};
1574 }
1575
1576 let mut next_pos = input_ids.len();
1578 'decode: while generated < max_tokens {
1579 let mut logits = match self.graph_logits.take() {
1580 Some(lg) => lg,
1581 None => {
1582 inference::rms_norm_into(
1583 &hidden,
1584 &self.weights.final_norm,
1585 self.rms_eps,
1586 self.norm_style,
1587 &mut self.ws.n1,
1588 );
1589 self.lm_head_forward(&self.ws.n1)
1590 }
1591 };
1592 let t_next = sampler::sample_with_scratch(
1593 &logits,
1594 &self.sampler_config,
1595 &all_ids,
1596 &mut self.rng,
1597 &mut self.sampler_scratch,
1598 );
1599 if self.confidence_on {
1600 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1601 }
1602 attention::recycle_buf(&mut logits);
1603 if trace_on {
1604 let skill = router.as_ref().and_then(|r| r.active_id());
1608 traces.push(TokenTrace {
1609 t: generated,
1610 token_id: t_next,
1611 confidence: confidence.last().copied().unwrap_or(0.0),
1612 active_skill: skill,
1613 recon: None,
1614 switched: false,
1615 });
1616 }
1617 if !commit!(t_next) {
1618 break 'decode;
1619 }
1620 if generated >= max_tokens {
1621 break 'decode;
1622 }
1623
1624 if self.kv_cache.needs_eviction() {
1625 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1626 self.kv_cache.evict(keep);
1627 }
1628
1629 match &mut mtp {
1630 Some(m) if generated + 1 < max_tokens => {
1632 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1633 drafted += 1;
1634 let emb1 = self.embed_single(t_next);
1635 let emb2 = self.embed_single(draft);
1636 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1637
1638 inference::rms_norm_into(
1639 &h1,
1640 &self.weights.final_norm,
1641 self.rms_eps,
1642 self.norm_style,
1643 &mut self.ws.n1,
1644 );
1645 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1646 let t_after = sampler::sample_with_scratch(
1647 &logits1,
1648 &self.sampler_config,
1649 &all_ids,
1650 &mut self.rng,
1651 &mut self.sampler_scratch,
1652 );
1653 if self.confidence_on {
1654 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1655 }
1656 attention::recycle_buf(&mut logits1);
1657 if trace_on {
1658 traces.push(TokenTrace {
1661 t: generated,
1662 token_id: t_after,
1663 confidence: confidence.last().copied().unwrap_or(0.0),
1664 active_skill: None,
1665 recon: None,
1666 switched: false,
1667 });
1668 }
1669 let stop = !commit!(t_after);
1670
1671 if t_after == draft {
1672 accepted += 1;
1673 self.commit_linear_scratch();
1674 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1675 hidden = h2;
1676 next_pos += 2;
1677 } else {
1678 for layer in &mut self.kv_cache.layers {
1680 layer.truncate_last(1);
1681 }
1682 if !stop {
1683 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1684 hidden = self.forward_layers(
1685 &self.embed_single(t_after),
1686 next_pos + 1,
1687 None,
1688 );
1689 }
1690 next_pos += 2;
1691 }
1692 if stop {
1693 break 'decode;
1694 }
1695 }
1696 _ => {
1698 self.graph_want_logits = fuse_lm;
1699 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1700 next_pos += 1;
1701 if let Some(r) = &mut router {
1704 let phi = self.dyn_phi_ema.clone();
1705 let decision = r.step(&phi, generated);
1706 if let Some(new_active) = decision {
1707 let _ = self.set_active_skill(new_active);
1708 }
1709 if trace_on {
1712 if let Some(last) = traces.last_mut() {
1713 let e = r.last_best_e();
1714 last.recon = e.is_finite().then_some(e);
1715 last.switched = decision.is_some();
1716 }
1717 }
1718 }
1719 }
1720 }
1721 }
1722
1723 self.graph_want_logits = false;
1724 self.graph_logits = None;
1725 if router.is_some() {
1727 let _ = self.set_active_skill(None);
1728 }
1729 self.dyn_router = router.or(self.dyn_router.take());
1730 self.mtp = mtp.or(self.mtp.take());
1731
1732 let output_ids = &all_ids[input_ids.len()..];
1733 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
1737 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
1738 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1740 Ok(GenerateResult {
1741 text: self.tokenizer.decode(output_ids),
1742 token_ids: output_ids.to_vec(),
1743 prompt_tokens: input_ids.len(),
1744 tokens_generated: generated,
1745 finish_reason,
1746 mtp_drafted: drafted,
1747 mtp_accepted: accepted,
1748 token_confidence: confidence,
1749 traces,
1750 })
1751 }
1752
1753 fn mtp_step(
1757 &mut self,
1758 m: &mut MtpModule,
1759 hidden: &[f32],
1760 next_token: u32,
1761 position: usize,
1762 ) -> u32 {
1763 let e = self.embed_single(next_token);
1767 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1768 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1769 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1770 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1771 let mut x = vec![0.0f32; self.hidden_size];
1772 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1773
1774 let lw = &m.layer;
1776 inference::rms_norm_into(
1777 &x,
1778 &lw.input_norm,
1779 self.rms_eps,
1780 self.norm_style,
1781 &mut self.ws.n1,
1782 );
1783 let attn = match &lw.attn {
1784 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
1786 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1787 AttnKind::Full {
1788 wq,
1789 wk,
1790 wv,
1791 wo,
1792 q_norm,
1793 k_norm,
1794 output_gate,
1795 softplus_gate,
1796 bias,
1797 } => {
1798 let mut cfg = self.attn_cfg(position);
1799 cfg.q_norm = q_norm.as_deref();
1800 cfg.k_norm = k_norm.as_deref();
1801 cfg.output_gate = *output_gate;
1802 cfg.softplus_gate = softplus_gate
1803 .as_ref()
1804 .map(|(gate, per_head)| (gate, *per_head));
1805 cfg.bias = bias
1806 .as_ref()
1807 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1808 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1809 }
1810 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1811 unreachable!("MTP block is full attention")
1812 }
1813 };
1814 for (i, &a) in attn.iter().enumerate() {
1815 x[i] += a;
1816 }
1817 inference::rms_norm_into(
1818 &x,
1819 &lw.post_norm,
1820 self.rms_eps,
1821 self.norm_style,
1822 &mut self.ws.p1,
1823 );
1824 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
1825 for (i, &f) in ffn.iter().enumerate() {
1826 x[i] += f;
1827 }
1828
1829 inference::rms_norm_into(
1830 &x,
1831 &m.final_norm,
1832 self.rms_eps,
1833 self.norm_style,
1834 &mut self.ws.n1,
1835 );
1836 let mut lg = self.lm_head_forward(&self.ws.n1);
1837 let draft = sampler::argmax(&lg);
1838 attention::recycle_buf(&mut lg);
1839 draft
1840 }
1841
1842 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1846 let emb1 = self.embed_single(1);
1847 let emb2 = self.embed_single(2);
1848 let pos = self.kv_cache.seq_len();
1849
1850 let t0 = std::time::Instant::now();
1851 for _ in 0..iters {
1852 let _ = self.forward_layers(&emb1, pos, None);
1853 let _ = self.forward_layers(&emb2, pos + 1, None);
1854 for l in &mut self.kv_cache.layers {
1855 l.truncate_last(2);
1856 }
1857 }
1858 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1859
1860 let t1 = std::time::Instant::now();
1861 for _ in 0..iters {
1862 let _ = self.forward_pair(&emb1, &emb2, pos);
1863 for l in &mut self.kv_cache.layers {
1864 l.truncate_last(2);
1865 }
1866 }
1867 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1868 (singles_ms, pair_ms)
1869 }
1870
1871 fn pair_supported(&self) -> bool {
1879 !self
1880 .weights
1881 .layers
1882 .iter()
1883 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
1884 }
1885
1886 fn forward_pair(
1887 &mut self,
1888 emb1: &[f32],
1889 emb2: &[f32],
1890 position: usize,
1891 ) -> (Vec<f32>, Vec<f32>) {
1892 let mut h1 = emb1.to_vec();
1893 let mut h2 = emb2.to_vec();
1894 let (_nkv, _hd, hs, _rd, eps) = (
1895 self.num_kv_heads,
1896 self.head_dim,
1897 self.hidden_size,
1898 self.rotary_dim,
1899 self.rms_eps,
1900 );
1901 let pool = self.pool.clone();
1902
1903 for li in 0..self.num_layers {
1904 let lw = &self.weights.layers[self.phys_layer(li)];
1905 inference::rms_norm_into(
1908 &h1,
1909 &lw.input_norm,
1910 self.rms_eps,
1911 self.norm_style,
1912 &mut self.ws.n1,
1913 );
1914 inference::rms_norm_into(
1915 &h2,
1916 &lw.input_norm,
1917 self.rms_eps,
1918 self.norm_style,
1919 &mut self.ws.n2,
1920 );
1921
1922 let (a1, a2) = match &lw.attn {
1923 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
1924 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1925 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1926 AttnKind::Linear(w) => {
1927 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1928 let layer = &mut self.kv_cache.layers[li];
1929 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1930 vmf_phase_pair(
1931 &self.ws.n1,
1932 &self.ws.n2,
1933 w,
1934 &cfg,
1935 state,
1936 scratch,
1937 self.pool.as_deref(),
1938 )
1939 }
1940 AttnKind::LinearGdn(w) => {
1941 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1942 let layer = &mut self.kv_cache.layers[li];
1943 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1944 gdn_pair(
1945 &self.ws.n1,
1946 &self.ws.n2,
1947 w,
1948 &cfg,
1949 state,
1950 scratch,
1951 self.pool.as_deref(),
1952 )
1953 }
1954 AttnKind::ShortConv(w) => {
1955 let cfg = self
1956 .short_conv_cfg
1957 .expect("short-conv layer without short_conv_cfg");
1958 let layer = &mut self.kv_cache.layers[li];
1959 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1960 short_conv_pair(
1961 &self.ws.n1,
1962 &self.ws.n2,
1963 w,
1964 &cfg,
1965 state,
1966 scratch,
1967 self.pool.as_deref(),
1968 )
1969 }
1970 AttnKind::Full {
1971 wq,
1972 wk,
1973 wv,
1974 wo,
1975 q_norm,
1976 k_norm,
1977 output_gate,
1978 softplus_gate,
1979 bias,
1980 } => {
1981 let inv_freq_l = self.layer_inv_freq(li);
1982 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1983 let cfg = QwenAttnCfg {
1984 num_heads: self.layer_num_heads(li),
1985 num_kv_heads: nkv_l,
1986 head_dim: hd_l,
1987 hidden_size: hs,
1988 position,
1989 inv_freq: &inv_freq_l,
1990 rotary_dim: rd_l,
1991 scale: self.attn_scale,
1992 softcap: self.attn_softcap,
1993 window: self.layer_window(li),
1994 v_norm: self.attn_v_norm,
1995 q_norm: q_norm.as_deref(),
1996 k_norm: k_norm.as_deref(),
1997 output_gate: *output_gate,
1998 softplus_gate: softplus_gate
1999 .as_ref()
2000 .map(|(gate, per_head)| (gate, *per_head)),
2001 rope_scale: self.layer_rope_scale(li),
2002 bias: bias
2003 .as_ref()
2004 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2005 rms_eps: eps,
2006 norm_style: self.norm_style,
2007 pool: pool.as_deref(),
2008 };
2009 attention::qwen_attention_pair(
2010 &self.ws.n1,
2011 &self.ws.n2,
2012 wq,
2013 wk,
2014 wv,
2015 wo,
2016 &mut self.kv_cache.layers[li],
2017 &cfg,
2018 )
2019 }
2020 };
2021 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
2022 Some(w) => (
2023 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
2024 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
2025 ),
2026 None => (a1, a2),
2027 };
2028 for i in 0..self.hidden_size {
2029 h1[i] += a1[i];
2030 h2[i] += a2[i];
2031 }
2032 let (mut a1, mut a2) = (a1, a2);
2033 attention::recycle_buf(&mut a1);
2034 attention::recycle_buf(&mut a2);
2035
2036 let lw = &self.weights.layers[self.phys_layer(li)];
2037 inference::rms_norm_into(
2038 &h1,
2039 &lw.post_norm,
2040 self.rms_eps,
2041 self.norm_style,
2042 &mut self.ws.p1,
2043 );
2044 inference::rms_norm_into(
2045 &h2,
2046 &lw.post_norm,
2047 self.rms_eps,
2048 self.norm_style,
2049 &mut self.ws.p2,
2050 );
2051 let (f1, f2) = match &lw.ffn {
2052 FfnKind::DenseMoe(dm) => (
2055 dense_moe_ffn(
2056 dm,
2057 &self.ws.p1,
2058 &h1,
2059 self.rms_eps,
2060 self.norm_style,
2061 self.pool.as_deref(),
2062 ),
2063 dense_moe_ffn(
2064 dm,
2065 &self.ws.p2,
2066 &h2,
2067 self.rms_eps,
2068 self.norm_style,
2069 self.pool.as_deref(),
2070 ),
2071 ),
2072 _ => ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref(), None),
2073 };
2074 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
2075 Some(w) => (
2076 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
2077 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
2078 ),
2079 None => (f1, f2),
2080 };
2081 for i in 0..self.hidden_size {
2082 h1[i] += f1[i];
2083 h2[i] += f2[i];
2084 }
2085 let (mut f1, mut f2) = (f1, f2);
2086 attention::recycle_buf(&mut f1);
2087 attention::recycle_buf(&mut f2);
2088 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
2089 for i in 0..self.hidden_size {
2090 h1[i] *= sc;
2091 h2[i] *= sc;
2092 }
2093 }
2094 if self.is_loop_end(li) && li + 1 < self.num_layers {
2096 h1 = inference::rms_norm(
2097 &h1,
2098 &self.weights.final_norm,
2099 self.rms_eps,
2100 self.norm_style,
2101 );
2102 h2 = inference::rms_norm(
2103 &h2,
2104 &self.weights.final_norm,
2105 self.rms_eps,
2106 self.norm_style,
2107 );
2108 }
2109 }
2110 (h1, h2)
2111 }
2112
2113 fn commit_linear_scratch(&mut self) {
2115 for layer in &mut self.kv_cache.layers {
2116 if !layer.linear_scratch.is_empty() {
2117 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
2118 layer.linear_scratch.clear();
2119 }
2120 }
2121 }
2122
2123 pub fn forward_ids(
2126 &mut self,
2127 ids: &[u32],
2128 task_mask: Option<&TaskMask>,
2129 ) -> Result<Vec<f32>, String> {
2130 if ids.is_empty() {
2131 return Err("empty id sequence".to_string());
2132 }
2133 self.kv_cache.clear();
2134 self.kv_history.clear();
2135 self.o1_begin();
2136 let mut hidden = vec![0.0f32; self.hidden_size];
2137 let mut pos = 0usize;
2138 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
2139 let chunk = prefill_chunk();
2143 let hs = self.hidden_size;
2144 while pos < ids.len() {
2145 let end = (pos + chunk).min(ids.len());
2146 let hb = self.prefill_batch(&ids[pos..end], pos);
2147 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2148 pos = end;
2149 }
2150 }
2151 if task_mask.is_none() {
2152 while pos + 1 < ids.len() {
2153 let e1 = self.embed_single(ids[pos]);
2154 let e2 = self.embed_single(ids[pos + 1]);
2155 let (_, h2) = self.forward_pair(&e1, &e2, pos);
2156 self.commit_linear_scratch();
2157 hidden = h2;
2158 pos += 2;
2159 }
2160 }
2161 while pos < ids.len() {
2162 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2163 pos += 1;
2164 }
2165 self.o1_seal();
2169 let normed = inference::rms_norm(
2170 &hidden,
2171 &self.weights.final_norm,
2172 self.rms_eps,
2173 self.norm_style,
2174 );
2175 Ok(self.lm_head_forward(&normed))
2176 }
2177
2178 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
2185 let (nll, cnt) = self.nll_ids_from(ids, 0);
2186 (nll / cnt.max(1) as f64).exp()
2187 }
2188
2189 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2194 self.kv_cache.clear();
2195 self.kv_history.clear();
2196 FFN_PROBE.with(|p| {
2197 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2198 });
2199 crate::gpu::cpu_scope(|| {
2200 for (pos, &id) in ids.iter().enumerate() {
2201 let emb = self.embed_single(id);
2202 let _ = self.forward_layers(&emb, pos, None);
2203 }
2204 });
2205 self.kv_cache.clear();
2206 self.kv_history.clear();
2207 FFN_PROBE
2208 .with(|p| p.borrow_mut().take())
2209 .unwrap_or_default()
2210 }
2211
2212 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2216 self.kv_cache.clear();
2217 self.kv_history.clear();
2218 let mut nll = 0f64;
2219 let mut cnt = 0usize;
2220 let mut hidden = vec![0f32; self.hidden_size];
2221 for (pos, &id) in ids.iter().enumerate() {
2222 if pos > 0 {
2223 inference::rms_norm_into(
2224 &hidden,
2225 &self.weights.final_norm,
2226 self.rms_eps,
2227 self.norm_style,
2228 &mut self.ws.n1,
2229 );
2230 let mut logits = self.lm_head_forward(&self.ws.n1);
2231 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2232 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2233 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2234 nll -= p.max(1e-300).ln();
2235 cnt += 1;
2236 attention::recycle_buf(&mut logits);
2237 }
2238 let emb = self.embed_single(id);
2239 hidden = self.forward_layers(&emb, pos, Some(mask));
2240 }
2241 self.kv_cache.clear();
2242 self.kv_history.clear();
2243 (nll / cnt.max(1) as f64).exp()
2244 }
2245
2246 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2255 self.kv_cache.clear();
2256 self.kv_history.clear();
2257 let mut nll = 0f64;
2258 let mut cnt = 0usize;
2259 if prefill_batched() {
2260 const CHUNK: usize = 128;
2266 const LM_SUB: usize = 32;
2267 let n = ids.len().saturating_sub(1);
2268 let hs = self.hidden_size;
2269 let rows = self.weights.lm_head.rows();
2270 let mut pos = 0usize;
2271 while pos < n {
2272 let end = (pos + CHUNK).min(n);
2273 let bsz = end - pos;
2274 let hb = self.prefill_batch(&ids[pos..end], pos);
2275 let mut k0 = 0usize;
2276 while k0 < bsz {
2277 let k1 = (k0 + LM_SUB).min(bsz);
2278 let sb = k1 - k0;
2279 if pos + k1 <= start {
2282 k0 = k1;
2283 continue;
2284 }
2285 let mut normed = vec![0.0f32; sb * hs];
2286 for k in 0..sb {
2287 let r = inference::rms_norm(
2288 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2289 &self.weights.final_norm,
2290 self.rms_eps,
2291 self.norm_style,
2292 );
2293 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2294 }
2295 let mut logits = vec![0.0f32; sb * rows];
2296 self.weights
2297 .lm_head
2298 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2299 for k in 0..sb {
2300 if pos + k0 + k < start {
2301 continue;
2302 }
2303 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
2304 if let Some(mu) = self.logit_multiplier {
2305 for v in lg.iter_mut() {
2306 *v *= mu;
2307 }
2308 }
2309 if let Some(c) = self.final_softcap {
2313 for v in lg.iter_mut() {
2314 *v = c * (*v / c).tanh();
2315 }
2316 }
2317 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2318 let target = ids[pos + k0 + k + 1] as usize;
2319 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2320 let lse: f64 = lg
2321 .iter()
2322 .map(|&v| ((v - max) as f64).exp())
2323 .sum::<f64>()
2324 .ln()
2325 + max as f64;
2326 nll += lse - lg[target] as f64;
2327 cnt += 1;
2328 if std::env::var("CMF_PPL_TRACE").is_ok() {
2329 let top = lg
2330 .iter()
2331 .enumerate()
2332 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2333 .map(|(i, _)| i)
2334 .unwrap_or(0);
2335 eprintln!(
2336 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
2337 pos + k0 + k, target, lse - lg[target] as f64, top, lg[target], lg[top]
2338 );
2339 }
2340 }
2341 k0 = k1;
2342 }
2343 pos = end;
2344 }
2345 self.kv_cache.clear();
2346 self.kv_history.clear();
2347 return (nll, cnt);
2348 }
2349 for pos in 0..ids.len().saturating_sub(1) {
2350 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2351 if pos < start {
2352 continue;
2353 }
2354 let normed = inference::rms_norm(
2355 &hidden,
2356 &self.weights.final_norm,
2357 self.rms_eps,
2358 self.norm_style,
2359 );
2360 let logits = self.lm_head_forward(&normed);
2364 let target = ids[pos + 1] as usize;
2365 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2366 let lse: f64 = logits
2367 .iter()
2368 .map(|&v| ((v - max) as f64).exp())
2369 .sum::<f64>()
2370 .ln()
2371 + max as f64;
2372 let tok_nll = lse - logits[target] as f64;
2373 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2374 let top = logits
2375 .iter()
2376 .enumerate()
2377 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2378 .map(|(i, _)| i)
2379 .unwrap_or(0);
2380 eprintln!(
2381 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2382 logits[target], logits[top]
2383 );
2384 }
2385 nll += tok_nll;
2386 cnt += 1;
2387 }
2388 self.kv_cache.clear();
2389 self.kv_history.clear();
2390 (nll, cnt)
2391 }
2392
2393 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2409 self.kv_cache.clear();
2410 self.kv_history.clear();
2411 self.o1_begin();
2412 let n = ids.len().saturating_sub(1);
2413 let p = prefill.min(n);
2414 let mut pos = 0usize;
2416 if prefill_batched() {
2417 const CHUNK: usize = 128;
2418 while pos < p {
2419 let end = (pos + CHUNK).min(p);
2420 let _ = self.prefill_batch(&ids[pos..end], pos);
2421 pos = end;
2422 }
2423 } else {
2424 while pos < p {
2425 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2426 pos += 1;
2427 }
2428 }
2429 self.o1_seal();
2430
2431 let mut nll = 0f64;
2432 let mut cnt = 0usize;
2433 for pos in p..n {
2434 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2435 let normed = inference::rms_norm(
2436 &hidden,
2437 &self.weights.final_norm,
2438 self.rms_eps,
2439 self.norm_style,
2440 );
2441 let logits = self.lm_head_forward(&normed);
2445 let target = ids[pos + 1] as usize;
2446 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2447 let lse: f64 = logits
2448 .iter()
2449 .map(|&v| ((v - max) as f64).exp())
2450 .sum::<f64>()
2451 .ln()
2452 + max as f64;
2453 let tok_nll = lse - logits[target] as f64;
2454 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2455 let top = logits
2456 .iter()
2457 .enumerate()
2458 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2459 .map(|(i, _)| i)
2460 .unwrap_or(0);
2461 eprintln!(
2462 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2463 logits[target], logits[top]
2464 );
2465 }
2466 nll += tok_nll;
2467 cnt += 1;
2468 }
2469 self.kv_cache.clear();
2470 self.kv_history.clear();
2471 (nll, cnt)
2472 }
2473
2474 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2482 self.kv_cache.clear();
2483 self.kv_history.clear();
2484 let n = ids.len().saturating_sub(1);
2485 let mut correct = Vec::with_capacity(n);
2486 let mut pmax = Vec::with_capacity(n);
2487 for pos in 0..n {
2488 let emb = self.embed_single(ids[pos]);
2489 let hidden = self.forward_layers(&emb, pos, None);
2490 let normed = inference::rms_norm(
2491 &hidden,
2492 &self.weights.final_norm,
2493 self.rms_eps,
2494 self.norm_style,
2495 );
2496 let logits = self.lm_head_forward(&normed);
2500 let target = ids[pos + 1] as usize;
2501 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2502 for (i, &v) in logits.iter().enumerate() {
2503 if v > mval {
2504 mval = v;
2505 amax = i;
2506 }
2507 }
2508 correct.push(amax == target);
2509 let row: Vec<f32> = temps
2510 .iter()
2511 .map(|&t| {
2512 let tt = t.max(1e-3);
2513 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2514 1.0 / s.max(1e-12) })
2516 .collect();
2517 pmax.push(row);
2518 }
2519 self.kv_cache.clear();
2520 self.kv_history.clear();
2521 (correct, pmax)
2522 }
2523
2524 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2531 let mut router = match self.dyn_router.take() {
2532 Some(r) => r,
2533 None => return (self.ppl_ids(ids), 0),
2534 };
2535 router.reset();
2536 self.dyn_phi_seen = 0;
2537 let _ = self.set_active_skill(None);
2538
2539 self.kv_cache.clear();
2540
2541 self.kv_history.clear();
2542 let mut nll = 0f64;
2543 let mut cnt = 0usize;
2544 for pos in 0..ids.len().saturating_sub(1) {
2545 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2546 let normed = inference::rms_norm(
2547 &hidden,
2548 &self.weights.final_norm,
2549 self.rms_eps,
2550 self.norm_style,
2551 );
2552 let logits = self.lm_head_forward(&normed);
2556 let target = ids[pos + 1] as usize;
2557 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2558 let lse: f64 = logits
2559 .iter()
2560 .map(|&v| ((v - max) as f64).exp())
2561 .sum::<f64>()
2562 .ln()
2563 + max as f64;
2564 let tok_nll = lse - logits[target] as f64;
2565 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2566 let top = logits
2567 .iter()
2568 .enumerate()
2569 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2570 .map(|(i, _)| i)
2571 .unwrap_or(0);
2572 eprintln!(
2573 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2574 logits[target], logits[top]
2575 );
2576 }
2577 nll += tok_nll;
2578 cnt += 1;
2579 let phi = self.dyn_phi_ema.clone();
2581 if let Some(new_active) = router.step(&phi, pos) {
2582 let _ = self.set_active_skill(new_active);
2583 }
2584 }
2585 let switches = router.switches.len();
2586 let _ = self.set_active_skill(None);
2587 self.dyn_router = Some(router);
2588 self.kv_cache.clear();
2589 self.kv_history.clear();
2590 ((nll / cnt.max(1) as f64).exp(), switches)
2591 }
2592
2593 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2595 self.kv_cache.clear();
2596 self.kv_history.clear();
2597 let mut acc = vec![0f32; self.hidden_size];
2598 for (pos, &id) in ids.iter().enumerate() {
2599 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2600 for (a, v) in acc.iter_mut().zip(&h) {
2601 *a += v;
2602 }
2603 }
2604 let n = ids.len().max(1) as f32;
2605 for a in acc.iter_mut() {
2606 *a /= n;
2607 }
2608 self.kv_cache.clear();
2609 self.kv_history.clear();
2610 acc
2611 }
2612
2613 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2619 let b = ids.len();
2620 let hs = self.hidden_size;
2621 let mut h: Vec<f32> = vec![0.0; b * hs];
2624 let mut h_ready = false;
2625 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2626 for (bi, &id) in ids.iter().enumerate() {
2627 let e = me.embed_single(id);
2628 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2629 }
2630 };
2631 let (_nkv, _hd, _rd, eps) = (
2632 self.num_kv_heads,
2633 self.head_dim,
2634 self.rotary_dim,
2635 self.rms_eps,
2636 );
2637 let pool = self.pool.clone();
2638 let norm_style = self.norm_style;
2639
2640 #[cfg(target_os = "macos")]
2641 let mut chunk_skip_until = 0usize;
2642 for li in 0..self.num_layers {
2643 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2650 {
2651 if li < chunk_skip_until {
2652 continue;
2653 }
2654 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2655 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2656 if end > li {
2657 h_ready = true;
2658 chunk_skip_until = end;
2659 if self.is_loop_end(end - 1) && end < self.num_layers {
2662 for bi in 0..b {
2663 let normed = inference::rms_norm(
2664 &h[bi * hs..(bi + 1) * hs],
2665 &self.weights.final_norm,
2666 eps,
2667 norm_style,
2668 );
2669 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2670 }
2671 }
2672 continue;
2673 }
2674 }
2675 if !h_ready {
2676 fill_h(&mut h, self);
2677 h_ready = true;
2678 }
2679 let lw = &self.weights.layers[self.phys_layer(li)];
2680 match &lw.attn {
2682 AttnKind::Kda(w) => {
2683 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
2685 let mut normed = vec![0.0f32; b * hs];
2686 for bi in 0..b {
2687 inference::rms_norm_into(
2688 &h[bi * hs..(bi + 1) * hs],
2689 &lw.input_norm,
2690 eps,
2691 norm_style,
2692 &mut normed[bi * hs..(bi + 1) * hs],
2693 );
2694 }
2695 let attn = crate::linear_core::kda_forward_batch(
2696 &normed,
2697 b,
2698 w,
2699 &cfg,
2700 &mut self.kv_cache.layers[li].linear_state,
2701 pool.as_deref(),
2702 );
2703 for (dst, &a) in h.iter_mut().zip(&attn) {
2704 *dst += a;
2705 }
2706 }
2707 AttnKind::LinearGdn(w) => {
2708 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2710 let mut normed = vec![0.0f32; b * hs];
2711 for bi in 0..b {
2712 let r = inference::rms_norm(
2713 &h[bi * hs..(bi + 1) * hs],
2714 &lw.input_norm,
2715 eps,
2716 norm_style,
2717 );
2718 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2719 }
2720 let attn = crate::linear_core::gdn_forward_batch(
2721 &normed,
2722 b,
2723 w,
2724 &cfg,
2725 &mut self.kv_cache.layers[li].linear_state,
2726 pool.as_deref(),
2727 );
2728 for (dst, &a) in h.iter_mut().zip(&attn) {
2729 *dst += a;
2730 }
2731 }
2732 AttnKind::ShortConv(w) => {
2733 let cfg = self
2736 .short_conv_cfg
2737 .expect("short-conv layer without short_conv_cfg");
2738 let mut normed = vec![0.0f32; b * hs];
2739 for bi in 0..b {
2740 inference::rms_norm_into(
2741 &h[bi * hs..(bi + 1) * hs],
2742 &lw.input_norm,
2743 eps,
2744 norm_style,
2745 &mut normed[bi * hs..(bi + 1) * hs],
2746 );
2747 }
2748 let attn = short_conv_forward_batch(
2749 &normed,
2750 b,
2751 w,
2752 &cfg,
2753 &mut self.kv_cache.layers[li].linear_state,
2754 pool.as_deref(),
2755 );
2756 for (dst, &a) in h.iter_mut().zip(&attn) {
2757 *dst += a;
2758 }
2759 }
2760 AttnKind::Mla(w) => {
2761 let inv_freq_l = self.layer_inv_freq(li);
2764 let rs = self.layer_rope_scale(li);
2765 let mut normed = vec![0.0f32; hs];
2766 for bi in 0..b {
2767 inference::rms_norm_into(
2768 &h[bi * hs..(bi + 1) * hs],
2769 &lw.input_norm,
2770 eps,
2771 norm_style,
2772 &mut normed,
2773 );
2774 let ao = mla_attention(
2775 w,
2776 &normed,
2777 &mut self.kv_cache.layers[li],
2778 start_pos + bi,
2779 &inv_freq_l,
2780 rs,
2781 eps,
2782 pool.as_deref(),
2783 );
2784 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
2785 *dst += a;
2786 }
2787 }
2788 }
2789 AttnKind::Full {
2790 wq,
2791 wk,
2792 wv,
2793 wo,
2794 q_norm,
2795 k_norm,
2796 output_gate,
2797 softplus_gate,
2798 bias,
2799 } => {
2800 let mut normed = vec![0.0f32; b * hs];
2804 for bi in 0..b {
2805 inference::rms_norm_into(
2806 &h[bi * hs..(bi + 1) * hs],
2807 &lw.input_norm,
2808 eps,
2809 norm_style,
2810 &mut normed[bi * hs..(bi + 1) * hs],
2811 );
2812 }
2813 let inv_freq_l = self.layer_inv_freq(li);
2814 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2815 let cfg = QwenAttnCfg {
2816 num_heads: self.layer_num_heads(li),
2817 num_kv_heads: nkv_l,
2818 head_dim: hd_l,
2819 hidden_size: hs,
2820 position: start_pos,
2821 inv_freq: &inv_freq_l,
2822 rotary_dim: rd_l,
2823 scale: self.attn_scale,
2824 softcap: self.attn_softcap,
2825 window: self.layer_window(li),
2826 v_norm: self.attn_v_norm,
2827 q_norm: q_norm.as_deref(),
2828 k_norm: k_norm.as_deref(),
2829 output_gate: *output_gate,
2830 softplus_gate: softplus_gate
2831 .as_ref()
2832 .map(|(gate, per_head)| (gate, *per_head)),
2833 rope_scale: self.layer_rope_scale(li),
2834 bias: bias
2835 .as_ref()
2836 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2837 rms_eps: eps,
2838 norm_style,
2839 pool: pool.as_deref(),
2840 };
2841 let mut attn = attention::qwen_attention_batch(
2842 &normed,
2843 b,
2844 wq,
2845 wk,
2846 wv,
2847 wo,
2848 &mut self.kv_cache.layers[li],
2849 &cfg,
2850 );
2851 if let Some(w) = &lw.attn_out_norm {
2852 for bi in 0..b {
2853 inference::rms_norm_into(
2854 &attn[bi * hs..(bi + 1) * hs],
2855 w,
2856 eps,
2857 norm_style,
2858 &mut normed[bi * hs..(bi + 1) * hs],
2859 );
2860 }
2861 attn.copy_from_slice(&normed);
2862 }
2863 for (dst, &a) in h.iter_mut().zip(&attn) {
2864 *dst += a;
2865 }
2866 }
2867 AttnKind::Linear(w) => {
2868 for bi in 0..b {
2869 let normed = inference::rms_norm(
2870 &h[bi * hs..(bi + 1) * hs],
2871 &lw.input_norm,
2872 eps,
2873 norm_style,
2874 );
2875 vmf_phase_forward(
2876 &normed,
2877 w,
2878 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2879 &mut self.kv_cache.layers[li].linear_state,
2880 pool.as_deref(),
2881 )
2882 .iter()
2883 .enumerate()
2884 .for_each(|(i, &a)| h[bi * hs + i] += a);
2885 }
2886 }
2887 }
2888
2889 let lw = &self.weights.layers[self.phys_layer(li)];
2891 let mut post = vec![0.0f32; b * hs];
2892 for bi in 0..b {
2893 let r =
2894 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2895 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2896 }
2897 let mut ffn = match &lw.ffn {
2898 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2899 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
2900 FfnKind::DenseMoe(dm) => {
2903 let mut out = vec![0.0f32; b * hs];
2904 for bi in 0..b {
2905 let r = dense_moe_ffn(
2906 dm,
2907 &post[bi * hs..(bi + 1) * hs],
2908 &h[bi * hs..(bi + 1) * hs],
2909 eps,
2910 norm_style,
2911 pool.as_deref(),
2912 );
2913 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2914 }
2915 out
2916 }
2917 };
2918 if let Some(w) = &lw.ffn_out_norm {
2919 for bi in 0..b {
2920 inference::rms_norm_into(
2921 &ffn[bi * hs..(bi + 1) * hs],
2922 w,
2923 eps,
2924 norm_style,
2925 &mut post[bi * hs..(bi + 1) * hs],
2926 );
2927 }
2928 ffn.copy_from_slice(&post);
2929 }
2930 for (dst, &f) in h.iter_mut().zip(&ffn) {
2931 *dst += f;
2932 }
2933 if let Some(sc) = lw.layer_scale {
2934 for v in h.iter_mut() {
2935 *v *= sc;
2936 }
2937 }
2938 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
2939 if let Some(t) = tp.parse::<usize>().ok() {
2940 if t >= start_pos && t < start_pos + b {
2941 let bi = t - start_pos;
2942 let row = &h[bi * hs..(bi + 1) * hs];
2943 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
2944 eprintln!(
2945 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
2946 row[0], row[1]
2947 );
2948 }
2949 }
2950 }
2951 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
2955 let row = &h[(b - 1) * hs..b * hs];
2956 let rms =
2957 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
2958 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
2959 eprintln!(
2960 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
2961 match &self.weights.layers[self.phys_layer(li)].attn {
2962 AttnKind::LinearGdn(_) => "gdn",
2963 AttnKind::Linear(_) => "vmf",
2964 AttnKind::ShortConv(_) => "conv",
2965 _ => "attn",
2966 },
2967 match &lw.ffn {
2968 FfnKind::Moe(_) => "moe",
2969 FfnKind::Dense(_) => "dense",
2970 FfnKind::DenseMoe(_) => "dense+moe",
2971 },
2972 );
2973 }
2974 if self.is_loop_end(li) && li + 1 < self.num_layers {
2976 for bi in 0..b {
2977 let normed = inference::rms_norm(
2978 &h[bi * hs..(bi + 1) * hs],
2979 &self.weights.final_norm,
2980 eps,
2981 norm_style,
2982 );
2983 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2984 }
2985 }
2986 if std::env::var("CMF_TRACE_H").is_ok() {
2987 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2988 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2989 eprintln!(
2990 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
2991 lw.layer_scale
2992 );
2993 }
2994 }
2995 crate::gpu::set_layer(-1); h
2997 }
2998
2999 fn embed_single(&self, id: u32) -> Vec<f32> {
3001 let mut out = vec![0.0f32; self.hidden_size];
3002 if (id as usize) < self.weights.embed_tokens.rows() {
3003 self.weights.embed_tokens.row_f32(id as usize, &mut out);
3004 }
3005 if self.embed_multiplier != 1.0 {
3006 for v in out.iter_mut() {
3007 *v *= self.embed_multiplier;
3008 }
3009 }
3010 out
3011 }
3012
3013 #[cfg(target_os = "macos")]
3019 fn chunk_run_gpu(
3020 &mut self,
3021 li0: usize,
3022 h: &mut [f32],
3023 b: usize,
3024 pos0: usize,
3025 embed_ids: Option<&[u32]>,
3026 ) -> usize {
3027 if !crate::gpu::enabled_here()
3031 || std::env::var("CMF_GPU_CHUNK")
3032 .map(|v| v == "0")
3033 .unwrap_or(false)
3034 || b < 32
3035 || self.swa.is_some()
3036 || self.global_attn.is_some()
3037 || self.attn_v_norm
3038 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
3039 {
3040 return li0;
3041 }
3042 let Some(model) = self.model.clone() else {
3043 return li0;
3044 };
3045 let inv_freq = self.inv_freq.clone();
3046 let (nh, nkv, hd, hs) = (
3047 self.num_heads,
3048 self.num_kv_heads,
3049 self.head_dim,
3050 self.hidden_size,
3051 );
3052 let loop_end = if self.loop_final_norm {
3056 ((li0 / self.physical_layers) + 1) * self.physical_layers
3057 } else {
3058 self.num_layers
3059 };
3060 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
3061 let mut stored_at: Vec<usize> = Vec::new();
3062 for li in li0..self.num_layers.min(loop_end) {
3063 let lw = &self.weights.layers[self.phys_layer(li)];
3064 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
3065 break;
3066 }
3067 let AttnKind::Full {
3068 wq,
3069 wk,
3070 wv,
3071 wo,
3072 q_norm,
3073 k_norm,
3074 output_gate: false,
3075 softplus_gate: None,
3076 bias,
3077 } = &lw.attn
3078 else {
3079 break;
3080 };
3081 let FfnKind::Dense(d) = &lw.ffn else { break };
3082 if d.act != Act::Silu {
3083 break;
3084 }
3085 let parts = (
3086 wq.q8_row_parts(),
3087 wk.q8_row_parts(),
3088 wv.q8_row_parts(),
3089 wo.q8_row_parts(),
3090 d.gate_proj.q8_row_parts(),
3091 d.up_proj.q8_row_parts(),
3092 d.down_proj.q8_row_parts(),
3093 );
3094 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
3095 else {
3096 break;
3097 };
3098 let layer = &self.kv_cache.layers[li];
3099 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
3100 break;
3101 }
3102 stored_at.push(layer.head_len(0));
3103 layers.push(crate::gpu_metal::ChunkLayer {
3104 model: &model,
3105 kv_id: self.graph_kv_id,
3106 layer: li,
3107 wq: pq,
3108 wk: pk,
3109 wv: pv,
3110 wo: po,
3111 gate: pg,
3112 up: pu,
3113 down: pd,
3114 input_norm: &lw.input_norm,
3115 post_norm: &lw.post_norm,
3116 bias: bias
3117 .as_ref()
3118 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
3119 q_norm: q_norm.as_deref(),
3120 k_norm: k_norm.as_deref(),
3121 inv_freq: &inv_freq,
3122 rd: self.rotary_dim,
3123 nh,
3124 nkv,
3125 hd,
3126 hs,
3127 inter: d.gate_proj.rows(),
3128 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
3129 eps: self.rms_eps as f32,
3130 });
3131 }
3132 if layers.is_empty() {
3133 return li0;
3134 }
3135 let row = nkv * hd;
3136 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
3137 .iter()
3138 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
3139 .collect();
3140 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
3141 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
3142 let li = layers[i].layer;
3143 let layer = &self.kv_cache.layers[li];
3144 io.push(crate::gpu_metal::ChunkIo {
3145 cpu_stored: stored_at[i],
3146 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
3147 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
3148 out_k: ok,
3149 out_v: ov,
3150 imp: oi,
3151 });
3152 }
3153 let n_run = layers.len();
3154 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
3155 let ep = embed_ids.and_then(|ids| {
3158 self.weights
3159 .embed_tokens
3160 .q8_row_parts()
3161 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
3162 idx,
3163 rows,
3164 row_scale: rs,
3165 ids,
3166 mult: self.embed_multiplier,
3167 })
3168 });
3169 if embed_ids.is_some() && ep.is_none() {
3170 return li0;
3171 }
3172 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
3173 return li0;
3174 }
3175 drop(io);
3176 drop(layers);
3177 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
3180 let li = li0 + i;
3181 let layer = &mut self.kv_cache.layers[li];
3182 for bi in 0..b {
3183 layer.append(
3184 &ok[bi * row..(bi + 1) * row],
3185 &ov[bi * row..(bi + 1) * row],
3186 &[],
3187 );
3188 }
3189 layer.accumulate_imp(oi);
3190 }
3191 last
3192 }
3193
3194 fn layer_is_local(&self, li: usize) -> bool {
3197 if let Some(layers) = &self.sliding_layers {
3198 return layers.get(li).copied().unwrap_or(false);
3199 }
3200 match self.swa {
3201 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
3202 None => false,
3203 }
3204 }
3205
3206 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
3209 if self.layer_is_local(li) {
3210 if let Some(f) = &self.inv_freq_local {
3211 return f.clone();
3212 }
3213 } else if let Some(f) = &self.inv_freq_global {
3214 return f.clone();
3215 }
3216 self.inv_freq.clone()
3217 }
3218
3219 fn layer_window(&self, li: usize) -> Option<usize> {
3221 self.swa
3222 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
3223 }
3224
3225 fn layer_num_heads(&self, li: usize) -> usize {
3226 self.attention_heads_per_layer
3227 .as_ref()
3228 .and_then(|v| v.get(li).copied())
3229 .unwrap_or(self.num_heads)
3230 }
3231
3232 fn layer_rope_scale(&self, li: usize) -> f32 {
3233 if self.layer_is_local(li) {
3234 self.rope_scale_local
3235 } else {
3236 self.rope_scale
3237 }
3238 }
3239
3240 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
3243 if !self.layer_is_local(li) {
3244 if let Some((ghd, gkv)) = self.global_attn {
3245 return (gkv, ghd, ghd);
3246 }
3247 }
3248 (
3249 self.num_kv_heads,
3250 self.head_dim,
3251 if self.layer_is_local(li) {
3252 self.rotary_dim_local.unwrap_or(self.rotary_dim)
3253 } else {
3254 self.rotary_dim
3255 },
3256 )
3257 }
3258
3259 fn forward_layers(
3261 &mut self,
3262 hidden: &[f32],
3263 position: usize,
3264 task_mask: Option<&TaskMask>,
3265 ) -> Vec<f32> {
3266 self.forward_layers_upto(hidden, position, task_mask, None)
3267 }
3268
3269 fn try_token_graph_wgpu(
3273 &self,
3274 hidden: &[f32],
3275 position: usize,
3276 logits_out: &mut Vec<f32>,
3277 ) -> Option<Vec<f32>> {
3278 if self.o1_active() || self.attn_softcap > 0.0 {
3281 return None;
3283 }
3284 let nh = self.num_heads;
3285 let (nkv, hd, rd) = self.layer_geom(0);
3286 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3287 let mut layers = Vec::with_capacity(self.num_layers);
3288 let mut model = None;
3289 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
3290 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3291 if let Some((_, i, kind, rs)) = t.graph_weight() {
3292 return Some(crate::gpu::GraphW {
3293 idx: i,
3294 kind,
3295 row_scale: rs,
3296 data: &[],
3297 });
3298 }
3299 t.as_f32().map(|d| crate::gpu::GraphW {
3301 idx: 0,
3302 kind: 4,
3303 row_scale: &[],
3304 data: d,
3305 })
3306 }
3307 for li in 0..self.num_layers {
3308 let lw = &self.weights.layers[self.phys_layer(li)];
3309 if dbg {
3310 let ak = match &lw.attn {
3311 AttnKind::Mla(_) => "Mla".into(),
3312 AttnKind::Full {
3313 output_gate, bias, ..
3314 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
3315 AttnKind::LinearGdn(_) => "LinearGdn".into(),
3316 AttnKind::Kda(_) => "Kda".into(),
3317 AttnKind::Linear(_) => "Linear".into(),
3318 AttnKind::ShortConv(_) => "ShortConv".into(),
3319 };
3320 let fk = match &lw.ffn {
3321 FfnKind::Dense(_) => "Dense",
3322 FfnKind::Moe(_) => "Moe",
3323 FfnKind::DenseMoe(_) => "DenseMoe",
3324 };
3325 eprintln!("graph L{li}: attn={ak} ffn={fk}");
3326 }
3327 let gffn = match &lw.ffn {
3328 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3330 gate: gw(&d.gate_proj)?,
3331 up: gw(&d.up_proj)?,
3332 down: gw(&d.down_proj)?,
3333 },
3334 FfnKind::Moe(m) => {
3335 if m.router_sigmoid
3340 || m.expert_bias.is_some()
3341 || m.route_tau.is_some()
3342 || m.mask.is_some()
3343 {
3344 return None;
3345 }
3346 let (se, sg) = m.shared.as_ref()?;
3347 let sgate = gw(sg.as_ref()?)?;
3348 let router = gw(&m.router)?;
3349 let inter = m.experts.first()?.gate_proj.rows();
3350 let mut experts = Vec::with_capacity(m.experts.len() + 1);
3351 for e in m.experts.iter().chain(std::iter::once(se)) {
3352 if !matches!(e.act, Act::Silu)
3353 || e.gate_proj.rows() != inter
3354 || e.up_proj.rows() != inter
3355 {
3356 return None;
3357 }
3358 let (mm, gi) = e.gate_proj.mapped_q4t()?;
3359 let (_, ui) = e.up_proj.mapped_q4t()?;
3360 let (_, di) = e.down_proj.mapped_q4t()?;
3361 model.get_or_insert_with(|| mm.clone());
3362 experts.push((gi, ui, di));
3363 }
3364 crate::gpu::GraphFfn::Moe {
3365 router,
3366 shared_gate: sgate,
3367 experts,
3368 n_exp: m.experts.len(),
3369 top_k: m.top_k,
3370 inter,
3371 norm_topk: m.norm_topk_prob,
3372 }
3373 }
3374 };
3375 let attn = match &lw.attn {
3376 AttnKind::Full {
3377 wq,
3378 wk,
3379 wv,
3380 wo,
3381 q_norm,
3382 k_norm,
3383 output_gate,
3384 softplus_gate,
3385 bias,
3386 } => {
3387 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3388 return None;
3389 }
3390 let (m, _, _, _) = wq.graph_weight()?;
3391 model = Some(m.clone());
3392 crate::gpu::GraphAttn::Full {
3393 wq: gw(wq)?,
3394 wk: gw(wk)?,
3395 wv: gw(wv)?,
3396 wo: gw(wo)?,
3397 q_norm: q_norm.as_deref(),
3398 k_norm: k_norm.as_deref(),
3399 bias: bias
3400 .as_ref()
3401 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3402 output_gate: *output_gate,
3403 cpu_k: self.kv_cache.layers[li].k_heads(),
3404 cpu_v: self.kv_cache.layers[li].v_heads(),
3405 }
3406 }
3407 AttnKind::LinearGdn(w) => {
3408 let cfg = self.gdn_cfg?;
3409 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3410 model = Some(m.clone());
3411 crate::gpu::GraphAttn::Gdn {
3412 qkv: gw(&w.in_proj_qkv)?,
3413 z: gw(&w.in_proj_z)?,
3414 a: gw(&w.in_proj_a)?,
3415 b: gw(&w.in_proj_b)?,
3416 out: gw(&w.out_proj)?,
3417 conv1d: &w.conv1d,
3418 a_log: &w.a_log,
3419 dt_bias: &w.dt_bias,
3420 norm: &w.norm,
3421 nv: cfg.num_v_heads,
3422 nk: cfg.num_k_heads,
3423 dk: cfg.key_head_dim,
3424 dv: cfg.value_head_dim,
3425 kk: cfg.conv_kernel,
3426 }
3427 }
3428 _ => return None,
3429 };
3430 layers.push(crate::gpu::GraphLayer {
3431 input_norm: &lw.input_norm,
3432 attn,
3433 post_norm: &lw.post_norm,
3434 ffn: gffn,
3435 });
3436 }
3437 let model = model?;
3438 let lm_gw = if self.graph_want_logits
3444 && std::env::var("CMF_GPU_LMHEAD")
3445 .map(|v| v != "0")
3446 .unwrap_or(true)
3447 {
3448 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
3449 (
3450 crate::gpu::GraphW {
3451 idx: i,
3452 kind,
3453 row_scale: rs,
3454 data: &[],
3455 },
3456 self.weights.lm_head.rows(),
3457 )
3458 })
3459 } else {
3460 None
3461 };
3462 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
3463 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
3466 (0..self.num_layers - 1)
3467 .filter(|&li| (li + 1) % self.physical_layers == 0)
3468 .collect()
3469 } else {
3470 Vec::new()
3471 };
3472 let mut h = hidden.to_vec();
3473 crate::gpu::forward_token_graph(
3474 &model,
3475 self.graph_kv_id,
3476 &layers,
3477 &self.inv_freq,
3478 &mut h,
3479 nh,
3480 nkv,
3481 hd,
3482 rd,
3483 self.hidden_size,
3484 self.intermediate_size,
3485 position,
3486 self.kv_cache.max_seq_len,
3487 gemma,
3488 self.rms_eps as f32,
3489 lm,
3490 &self.weights.final_norm,
3491 logits_out,
3492 &loop_norm_at,
3493 )
3494 .then_some(h)
3495 }
3496
3497 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
3502 if self.attn_softcap > 0.0 {
3503 return false; }
3505 if self.o1_active() {
3506 return false;
3507 }
3508 let nh = self.num_heads;
3509 let (nkv, hd, rd) = self.layer_geom(0);
3510 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3511 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3512 if let Some((_, i, kind, rs)) = t.graph_weight() {
3513 return Some(crate::gpu::GraphW {
3514 idx: i,
3515 kind,
3516 row_scale: rs,
3517 data: &[],
3518 });
3519 }
3520 t.as_f32().map(|d| crate::gpu::GraphW {
3521 idx: 0,
3522 kind: 4,
3523 row_scale: &[],
3524 data: d,
3525 })
3526 }
3527 let built: Option<(
3528 Vec<crate::gpu::GraphLayer<'_>>,
3529 std::sync::Arc<cortiq_core::CmfModel>,
3530 )> = (|| {
3531 let mut layers = Vec::with_capacity(self.num_layers);
3532 let mut model = None;
3533 for li in 0..self.num_layers {
3534 let lw = &self.weights.layers[self.phys_layer(li)];
3535 let gffn = match &lw.ffn {
3538 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3539 gate: gw(&d.gate_proj)?,
3540 up: gw(&d.up_proj)?,
3541 down: gw(&d.down_proj)?,
3542 },
3543 _ => return None,
3544 };
3545 let attn = match &lw.attn {
3546 AttnKind::Full {
3547 wq,
3548 wk,
3549 wv,
3550 wo,
3551 q_norm,
3552 k_norm,
3553 output_gate,
3554 softplus_gate,
3555 bias,
3556 } => {
3557 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3558 return None;
3559 }
3560 let (m, _, _, _) = wq.graph_weight()?;
3561 model = Some(m.clone());
3562 crate::gpu::GraphAttn::Full {
3563 wq: gw(wq)?,
3564 wk: gw(wk)?,
3565 wv: gw(wv)?,
3566 wo: gw(wo)?,
3567 q_norm: q_norm.as_deref(),
3568 k_norm: k_norm.as_deref(),
3569 bias: bias
3570 .as_ref()
3571 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3572 output_gate: *output_gate,
3573 cpu_k: self.kv_cache.layers[li].k_heads(),
3574 cpu_v: self.kv_cache.layers[li].v_heads(),
3575 }
3576 }
3577 AttnKind::LinearGdn(w) => {
3578 let cfg = self.gdn_cfg?;
3579 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3580 model = Some(m.clone());
3581 crate::gpu::GraphAttn::Gdn {
3582 qkv: gw(&w.in_proj_qkv)?,
3583 z: gw(&w.in_proj_z)?,
3584 a: gw(&w.in_proj_a)?,
3585 b: gw(&w.in_proj_b)?,
3586 out: gw(&w.out_proj)?,
3587 conv1d: &w.conv1d,
3588 a_log: &w.a_log,
3589 dt_bias: &w.dt_bias,
3590 norm: &w.norm,
3591 nv: cfg.num_v_heads,
3592 nk: cfg.num_k_heads,
3593 dk: cfg.key_head_dim,
3594 dv: cfg.value_head_dim,
3595 kk: cfg.conv_kernel,
3596 }
3597 }
3598 _ => return None,
3599 };
3600 layers.push(crate::gpu::GraphLayer {
3601 input_norm: &lw.input_norm,
3602 attn,
3603 post_norm: &lw.post_norm,
3604 ffn: gffn,
3605 });
3606 }
3607 Some((layers, model?))
3608 })();
3609 let Some((layers, model)) = built else {
3610 return false;
3611 };
3612 crate::gpu::forward_batch_graph(
3613 &model,
3614 self.graph_kv_id,
3615 &layers,
3616 &self.inv_freq,
3617 hiddens,
3618 nh,
3619 nkv,
3620 hd,
3621 rd,
3622 self.hidden_size,
3623 self.intermediate_size,
3624 positions,
3625 self.kv_cache.max_seq_len,
3626 gemma,
3627 self.rms_eps as f32,
3628 k,
3629 )
3630 }
3631
3632 fn forward_layers_upto(
3634 &mut self,
3635 hidden: &[f32],
3636 position: usize,
3637 task_mask: Option<&TaskMask>,
3638 upto: Option<usize>,
3639 ) -> Vec<f32> {
3640 let mut h = hidden.to_vec();
3641 let (nh, _nkv, _hd, hs, _rd, eps) = (
3644 self.num_heads,
3645 self.num_kv_heads,
3646 self.head_dim,
3647 self.hidden_size,
3648 self.rotary_dim,
3649 self.rms_eps,
3650 );
3651 let pool = self.pool.clone();
3652 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
3664 let graph_on = match graph_env.as_deref() {
3665 Some("0") => false,
3666 Some(_) => true,
3667 None => {
3668 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
3669 || crate::gpu::wgpu_active()
3670 }
3671 };
3672 let graph_trusted =
3673 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
3674 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
3675 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
3676 let t_graph = std::time::Instant::now();
3677 let mut lg = Vec::new();
3678 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
3679 let dur = t_graph.elapsed();
3680 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3681 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
3682 }
3683 if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
3684 if !graph_trusted {
3685 crate::gpu::graph_race_record(true, dur);
3686 }
3687 if !lg.is_empty() {
3688 lg.resize(self.vocab_size, 0.0);
3691 if let Some(c) = self.final_softcap {
3692 for l in lg.iter_mut() {
3693 *l = c * (*l / c).tanh();
3694 }
3695 }
3696 self.graph_logits = Some(lg);
3697 }
3698 return hh;
3699 }
3700 }
3706 }
3707 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
3708
3709 #[cfg(target_os = "macos")]
3710 let mut gpu_skip_until = 0usize;
3711 for li in 0..self.num_layers {
3712 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
3714 if li > u {
3715 break;
3716 }
3717 }
3718 if let Some(mask) = task_mask {
3719 if !mask.layer_alive(li) {
3720 continue; }
3722 }
3723 #[cfg(target_os = "macos")]
3727 {
3728 if li < gpu_skip_until {
3729 continue;
3730 }
3731 if task_mask.is_none() {
3732 let end = self.q1_graph_gpu(li, upto, position, &mut h);
3733 if end > li {
3734 gpu_skip_until = end;
3735 if self.is_loop_end(end - 1) && end < self.num_layers {
3738 h = inference::rms_norm(
3739 &h,
3740 &self.weights.final_norm,
3741 self.rms_eps,
3742 self.norm_style,
3743 );
3744 }
3745 continue;
3746 }
3747 }
3748 }
3749
3750 let lw = &self.weights.layers[self.phys_layer(li)];
3751 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
3752 if tp.parse::<usize>().ok() == Some(position) {
3753 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
3754 eprintln!(
3755 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
3756 h[0], h[1]
3757 );
3758 }
3759 }
3760 inference::rms_norm_into(
3763 &h,
3764 &lw.input_norm,
3765 self.rms_eps,
3766 self.norm_style,
3767 &mut self.ws.n1,
3768 );
3769
3770 let attn_out = match &lw.attn {
3771 AttnKind::Mla(w) => {
3772 let inv_freq_l = self.layer_inv_freq(li);
3773 let rs = self.layer_rope_scale(li);
3774 let eps = self.rms_eps;
3775 let pool = self.pool.clone();
3776 mla_attention(
3777 w,
3778 &self.ws.n1,
3779 &mut self.kv_cache.layers[li],
3780 position,
3781 &inv_freq_l,
3782 rs,
3783 eps,
3784 pool.as_deref(),
3785 )
3786 }
3787 AttnKind::Linear(w) => {
3788 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3789 vmf_phase_forward(
3790 &self.ws.n1,
3791 w,
3792 &cfg,
3793 &mut self.kv_cache.layers[li].linear_state,
3794 self.pool.as_deref(),
3795 )
3796 }
3797 AttnKind::Kda(w) => {
3798 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
3799 crate::linear_core::kda_forward(
3800 &self.ws.n1,
3801 w,
3802 &cfg,
3803 &mut self.kv_cache.layers[li].linear_state,
3804 self.pool.as_deref(),
3805 )
3806 }
3807 AttnKind::LinearGdn(w) => {
3808 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3809 gdn_forward(
3810 &self.ws.n1,
3811 w,
3812 &cfg,
3813 &mut self.kv_cache.layers[li].linear_state,
3814 self.pool.as_deref(),
3815 )
3816 }
3817 AttnKind::ShortConv(w) => {
3818 let cfg = self
3819 .short_conv_cfg
3820 .expect("short-conv layer without short_conv_cfg");
3821 short_conv_forward(
3822 &self.ws.n1,
3823 w,
3824 &cfg,
3825 &mut self.kv_cache.layers[li].linear_state,
3826 self.pool.as_deref(),
3827 )
3828 }
3829 AttnKind::Full {
3830 wq,
3831 wk,
3832 wv,
3833 wo,
3834 q_norm,
3835 k_norm,
3836 output_gate,
3837 softplus_gate,
3838 bias,
3839 } if self.kv_cache.layers[li].o1_sealed() => {
3840 let inv_freq_l = self.layer_inv_freq(li);
3843 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3844 let cfg = QwenAttnCfg {
3845 num_heads: self.layer_num_heads(li),
3846 num_kv_heads: nkv_l,
3847 head_dim: hd_l,
3848 hidden_size: hs,
3849 position,
3850 inv_freq: &inv_freq_l,
3851 rotary_dim: rd_l,
3852 scale: self.attn_scale,
3853 softcap: self.attn_softcap,
3854 window: None,
3855 v_norm: self.attn_v_norm,
3856 q_norm: q_norm.as_deref(),
3857 k_norm: k_norm.as_deref(),
3858 output_gate: *output_gate,
3859 softplus_gate: softplus_gate
3860 .as_ref()
3861 .map(|(gate, per_head)| (gate, *per_head)),
3862 rope_scale: self.layer_rope_scale(li),
3863 bias: bias
3864 .as_ref()
3865 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3866 rms_eps: eps,
3867 norm_style: self.norm_style,
3868 pool: pool.as_deref(),
3869 };
3870 attention::qwen_attention_nystrom(
3871 &self.ws.n1,
3872 wq,
3873 wk,
3874 wv,
3875 wo,
3876 &mut self.kv_cache.layers[li],
3877 &cfg,
3878 )
3879 }
3880 AttnKind::Full {
3881 wq,
3882 wk,
3883 wv,
3884 wo,
3885 q_norm,
3886 k_norm,
3887 output_gate,
3888 softplus_gate,
3889 bias,
3890 } => 'attn: {
3891 if graph_on
3894 && !*output_gate
3895 && softplus_gate.is_none()
3896 && self.attention_heads_per_layer.is_none()
3897 && bias.is_none()
3898 && task_mask.is_none()
3899 {
3900 let inv_freq_l = self.layer_inv_freq(li);
3901 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3902 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3903 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
3904 wq.mapped_q1(),
3905 wk.mapped_q1(),
3906 wv.mapped_q1(),
3907 wo.mapped_q1(),
3908 ) {
3909 let gm = gm.clone();
3910 let mut out = vec![0f32; hs];
3911 let cache = &self.kv_cache.layers[li];
3912 if crate::gpu::attn_dropin(
3913 &gm,
3914 self.graph_kv_id,
3915 li,
3916 &self.ws.n1,
3917 qi,
3918 ki,
3919 vi,
3920 oi,
3921 q_norm.as_deref(),
3922 k_norm.as_deref(),
3923 &inv_freq_l,
3924 nh,
3925 nkv_l,
3926 hd_l,
3927 rd_l,
3928 hs,
3929 position,
3930 self.kv_cache.max_seq_len,
3931 gemma,
3932 eps as f32,
3933 cache.k_heads(),
3934 cache.v_heads(),
3935 &mut out,
3936 ) {
3937 break 'attn out;
3938 }
3939 }
3940 }
3941 let masked = task_mask
3942 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
3943 .unwrap_or(false);
3944 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
3945 match (masked, f32_view) {
3946 (true, (Some(q), Some(k), Some(v), Some(o))) => {
3949 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
3950 attention::multi_head_attention(
3951 &self.ws.n1,
3952 q,
3953 k,
3954 v,
3955 o,
3956 &mut self.kv_cache.layers[li],
3957 self.num_heads,
3958 self.num_kv_heads,
3959 self.head_dim,
3960 self.hidden_size,
3961 position,
3962 &active_heads,
3963 &self.inv_freq,
3964 )
3965 }
3966 (masked, _) => {
3967 if masked {
3968 tracing::warn!(
3969 "layer {li}: head mask on quantized weights not \
3970 supported yet — executing dense"
3971 );
3972 }
3973 let inv_freq_l = self.layer_inv_freq(li);
3974 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3975 let cfg = QwenAttnCfg {
3976 num_heads: self.layer_num_heads(li),
3977 num_kv_heads: nkv_l,
3978 head_dim: hd_l,
3979 hidden_size: hs,
3980 position,
3981 inv_freq: &inv_freq_l,
3982 rotary_dim: rd_l,
3983 scale: self.attn_scale,
3984 softcap: self.attn_softcap,
3985 window: self.layer_window(li),
3986 v_norm: self.attn_v_norm,
3987 q_norm: q_norm.as_deref(),
3988 k_norm: k_norm.as_deref(),
3989 output_gate: *output_gate,
3990 softplus_gate: softplus_gate
3991 .as_ref()
3992 .map(|(gate, per_head)| (gate, *per_head)),
3993 rope_scale: self.layer_rope_scale(li),
3994 bias: bias
3995 .as_ref()
3996 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3997 rms_eps: eps,
3998 norm_style: self.norm_style,
3999 pool: pool.as_deref(),
4000 };
4001 attention::qwen_attention(
4002 &self.ws.n1,
4003 wq,
4004 wk,
4005 wv,
4006 wo,
4007 &mut self.kv_cache.layers[li],
4008 &cfg,
4009 )
4010 }
4011 }
4012 }
4013 };
4014 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
4017 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
4018 None => attn_out,
4019 };
4020 let lw = &self.weights.layers[self.phys_layer(li)];
4021 inference::add_rmsnorm_fused_into(
4022 &mut h,
4023 &attn_out,
4024 &lw.post_norm,
4025 self.rms_eps,
4026 self.norm_style,
4027 &mut self.ws.p1,
4028 );
4029 let mut attn_out = attn_out;
4030 attention::recycle_buf(&mut attn_out);
4031 let post_normed = &self.ws.p1;
4032
4033 let ffn_masked = task_mask
4034 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
4035 .unwrap_or(false);
4036 let f32_ffn = match &lw.ffn {
4039 FfnKind::Dense(d) => (
4040 d.gate_proj.as_f32(),
4041 d.up_proj.as_f32(),
4042 d.down_proj.as_f32(),
4043 ),
4044 FfnKind::Moe(_) | FfnKind::DenseMoe(_) => (None, None, None),
4045 };
4046 let ffn_out = match (ffn_masked, f32_ffn) {
4047 (true, (Some(g), Some(u), Some(d))) => {
4048 let active = task_mask.unwrap().ffn_active_indices(li);
4049 inference::sparse_ffn_forward(
4050 post_normed,
4051 g,
4052 u,
4053 d,
4054 self.hidden_size,
4055 self.intermediate_size,
4056 &active,
4057 self.pool.as_deref(),
4058 )
4059 }
4060 (true, _) => match &lw.ffn {
4064 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
4065 let active = task_mask.unwrap().ffn_active_indices(li);
4066 sparse_ffn_quant(
4067 d,
4068 post_normed,
4069 &active,
4070 self.hidden_size,
4071 self.pool.as_deref(),
4072 )
4073 }
4074 FfnKind::Dense(d) => {
4079 let active = task_mask.unwrap().ffn_active_indices(li);
4080 let (gf, uf, df) = dequant_dense_f32(d);
4081 inference::sparse_ffn_forward(
4082 post_normed,
4083 &gf,
4084 &uf,
4085 &df,
4086 self.hidden_size,
4087 self.intermediate_size,
4088 &active,
4089 self.pool.as_deref(),
4090 )
4091 }
4092 FfnKind::Moe(m) => {
4093 let allowed = task_mask
4097 .and_then(|tm| tm.expert_flags(li, m.experts.len()));
4098 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
4099 }
4100 FfnKind::DenseMoe(dm) => dense_moe_ffn(
4101 dm,
4102 post_normed,
4103 &h,
4104 self.rms_eps,
4105 self.norm_style,
4106 self.pool.as_deref(),
4107 ),
4108 },
4109 (false, _) => match &lw.ffn {
4110 FfnKind::DenseMoe(dm) => dense_moe_ffn(
4111 dm,
4112 post_normed,
4113 &h,
4114 self.rms_eps,
4115 self.norm_style,
4116 self.pool.as_deref(),
4117 ),
4118 _ => {
4119 let allowed = match (&lw.ffn, task_mask) {
4120 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
4121 _ => None,
4122 };
4123 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
4124 }
4125 },
4126 };
4127 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4128 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
4129 None => ffn_out,
4130 };
4131 for (i, &f) in ffn_out.iter().enumerate() {
4132 h[i] += f;
4133 }
4134 let mut ffn_out = ffn_out;
4135 attention::recycle_buf(&mut ffn_out);
4136
4137 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4139 for v in h.iter_mut() {
4140 *v *= sc;
4141 }
4142 }
4143
4144 if self.is_loop_end(li) && li + 1 < self.num_layers {
4147 h = inference::rms_norm(
4148 &h,
4149 &self.weights.final_norm,
4150 self.rms_eps,
4151 self.norm_style,
4152 );
4153 }
4154
4155 if self.dyn_phi_layer == Some(li) {
4159 self.update_dyn_phi(&h);
4160 }
4161 }
4162 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
4164 crate::gpu::graph_race_record(false, t.elapsed());
4165 }
4166
4167 h
4168 }
4169
4170 fn update_dyn_phi(&mut self, h: &[f32]) {
4173 const A: f32 = 0.2;
4174 if self.dyn_phi_ema.len() != h.len() {
4175 self.dyn_phi_ema = vec![0.0; h.len()];
4176 self.dyn_phi_seen = 0;
4177 }
4178 if self.dyn_phi_seen == 0 {
4179 self.dyn_phi_ema.copy_from_slice(h);
4180 } else {
4181 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
4182 *e = (1.0 - A) * *e + A * v;
4183 }
4184 }
4185 self.dyn_phi_seen += 1;
4186 }
4187
4188 pub fn dyn_phi(&self) -> &[f32] {
4190 &self.dyn_phi_ema
4191 }
4192
4193 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
4195 self.dyn_phi_layer = layer;
4196 self.dyn_phi_ema.clear();
4197 self.dyn_phi_seen = 0;
4198 }
4199
4200 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
4202 let Some(model) = &self.model else {
4203 return Vec::new();
4204 };
4205 model
4206 .header
4207 .skills
4208 .iter()
4209 .enumerate()
4210 .filter_map(|(i, sk)| {
4211 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
4212 let sel = sk.selection.as_ref()?;
4213 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
4214 })
4215 .collect()
4216 }
4217
4218 pub fn active_skill(&self) -> Option<usize> {
4220 self.dyn_active
4221 }
4222
4223 pub fn enable_dynamic_routing(&mut self) -> usize {
4228 use crate::swarm::{DynRouter, RoutableSkill};
4229 let Some(model) = self.model.clone() else {
4230 return 0;
4231 };
4232 if self.dyn_blend_loaded {
4235 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
4236 return 0;
4237 }
4238 if let Some(a) = self.dyn_active {
4242 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
4243 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
4244 return 0;
4245 }
4246 }
4247 let hidden = self.hidden_size;
4248 let mut skills = Vec::new();
4249 for (idx, id, _phi) in self.dynamic_skills() {
4250 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
4251 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
4252 skills.push(rs);
4253 }
4254 }
4255 }
4256 if skills.is_empty() {
4257 return 0;
4258 }
4259 let phi = skills[0].phi_layer;
4261 if skills.iter().any(|s| s.phi_layer != phi) {
4262 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
4263 }
4264 let n = skills.len();
4265 self.set_dyn_phi_layer(Some(phi));
4266 self.dyn_router = Some(DynRouter::new(skills));
4267 n
4268 }
4269
4270 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
4272 self.dyn_router
4273 .as_ref()
4274 .map(|r| r.switches.clone())
4275 .unwrap_or_default()
4276 }
4277
4278 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
4281 let rows = self.weights.lm_head.rows();
4282 let mut logits = attention::take_buf(rows.min(self.vocab_size));
4283 self.weights
4284 .lm_head
4285 .matvec(hidden, &mut logits, self.pool.as_deref());
4286 logits.resize(self.vocab_size, 0.0);
4287 if let Some(m) = self.logit_multiplier {
4288 for l in logits.iter_mut() {
4289 *l *= m;
4290 }
4291 }
4292 if let Some(c) = self.final_softcap {
4293 for l in logits.iter_mut() {
4294 *l = c * (*l / c).tanh();
4295 }
4296 }
4297 logits
4298 }
4299
4300 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
4305 self.kv_cache.clear();
4306 self.kv_history.clear();
4307 let mut hidden = vec![0.0f32; self.hidden_size];
4308 for (pos, &id) in ids.iter().enumerate() {
4309 let emb = self.embed_single(id);
4310 hidden = self.forward_layers(&emb, pos, task_mask);
4311 }
4312 inference::rms_norm_into(
4313 &hidden,
4314 &self.weights.final_norm,
4315 self.rms_eps,
4316 self.norm_style,
4317 &mut self.ws.n1,
4318 );
4319 self.lm_head_forward(&self.ws.n1)
4320 }
4321}
4322
4323pub fn create_test_pipeline(
4325 hidden_size: usize,
4326 intermediate_size: usize,
4327 num_heads: usize,
4328 num_kv_heads: usize,
4329 head_dim: usize,
4330 num_layers: usize,
4331 vocab_size: usize,
4332) -> Pipeline {
4333 let synth = |n: usize, salt: usize| -> Vec<f32> {
4336 (0..n)
4337 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
4338 .collect()
4339 };
4340 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4341 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4342 };
4343 let layer_weights: Vec<LayerWeights> = (0..num_layers)
4344 .map(|li| LayerWeights {
4345 input_norm: vec![1.0; hidden_size],
4346 post_norm: vec![1.0; hidden_size],
4347 attn_out_norm: None,
4348 ffn_out_norm: None,
4349 layer_scale: None,
4350 ffn: FfnKind::Dense(DenseFfn {
4351 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
4352 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
4353 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
4354 act: Act::Silu,
4355 }),
4356 attn: AttnKind::Full {
4357 bias: None,
4358 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
4359 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
4360 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
4361 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
4362 q_norm: None,
4363 k_norm: None,
4364 output_gate: false,
4365 softplus_gate: None,
4366 },
4367 })
4368 .collect();
4369
4370 Pipeline::new(
4371 Tokenizer::byte_level(),
4372 PipelineWeights {
4373 embed_tokens: qt(vocab_size, hidden_size, 100),
4374 layers: layer_weights,
4375 lm_head: qt(vocab_size, hidden_size, 200),
4376 final_norm: vec![1.0; hidden_size],
4377 },
4378 hidden_size,
4379 intermediate_size,
4380 num_heads,
4381 num_kv_heads,
4382 head_dim,
4383 num_layers,
4384 num_layers, false, vocab_size,
4387 1e-6,
4388 10_000.0,
4389 NormStyle::Qwen,
4390 4096,
4391 SamplerConfig {
4392 seed: Some(42),
4393 ..Default::default()
4394 },
4395 )
4396}
4397
4398fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
4401 let inter = d.gate_proj.rows();
4402 let hidden = d.down_proj.rows();
4403 let mut g = vec![0.0f32; b * inter];
4404 d.gate_proj.matmat(xs, b, &mut g, pool);
4405 let mut u = vec![0.0f32; b * inter];
4406 d.up_proj.matmat(xs, b, &mut u, pool);
4407 for i in 0..b * inter {
4408 g[i] = d.act.combine(g[i], u[i]);
4409 }
4410 let mut out = vec![0.0f32; b * hidden];
4411 d.down_proj.matmat(&g, b, &mut out, pool);
4412 out
4413}
4414
4415fn moe_ffn_batch(
4419 m: &MoeFfn,
4420 xs: &[f32],
4421 b: usize,
4422 hidden: usize,
4423 pool: Option<&Pool>,
4424 allowed: Option<&[bool]>,
4425) -> Vec<f32> {
4426 let ne = m.experts.len();
4427 let mut logits = vec![0.0f32; b * ne];
4428 m.router.matmat(xs, b, &mut logits, pool);
4429
4430 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
4433 {
4434 let mut st = m.stats.borrow_mut();
4435 if st.len() < ne {
4436 st.resize(ne, 0);
4437 }
4438 for bi in 0..b {
4439 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
4440 for &e in &idx {
4441 st[e] += 1;
4442 assign[e].push((bi, p[e] / wsum));
4443 }
4444 }
4445 }
4446
4447 let mut out = vec![0.0f32; b * hidden];
4448 let cols = m.experts[0].gate_proj.cols();
4449 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
4450 let sb = list.len();
4451 let mut sub = vec![0.0f32; sb * cols];
4452 for (k, &(bi, _)) in list.iter().enumerate() {
4453 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
4454 }
4455 let eo = dense_ffn_batch(d, &sub, sb, pool);
4456 for (k, &(bi, w)) in list.iter().enumerate() {
4457 for i in 0..hidden {
4458 out[bi * hidden + i] += w * eo[k * hidden + i];
4459 }
4460 }
4461 };
4462 for (e, a) in assign.iter().enumerate().take(ne) {
4463 if !a.is_empty() {
4464 run_expert(&m.experts[e], a);
4465 }
4466 }
4467 if let Some((se, gate)) = &m.shared {
4468 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
4469 let mut gl = vec![0.0f32; b];
4470 gate.matmat(xs, b, &mut gl, pool);
4471 (0..b)
4472 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
4473 .collect()
4474 } else {
4475 (0..b).map(|bi| (bi, 1.0)).collect()
4476 };
4477 run_expert(se, &all);
4478 }
4479 out
4480}
4481
4482thread_local! {
4483 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
4487 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
4488}
4489
4490fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4492 if crate::gpu::enabled_here()
4503 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
4504 {
4505 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
4506 crate::gpu::ProbeArm::Gpu
4507 } else {
4508 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
4509 };
4510 match arm {
4511 crate::gpu::ProbeArm::Gpu => {
4512 let t0 = std::time::Instant::now();
4513 if let Some(out) = dense_ffn_gpu(d, x, pool) {
4514 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4515 return out;
4516 }
4517 }
4518 crate::gpu::ProbeArm::CpuTimed => {
4519 let t0 = std::time::Instant::now();
4520 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4521 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4522 return out;
4523 }
4524 crate::gpu::ProbeArm::Cpu => {
4525 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4526 }
4527 }
4528 }
4529 dense_ffn_cpu(d, x, pool)
4530}
4531
4532fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4534 let inter = d.gate_proj.rows();
4535 FFN_SCRATCH.with(|s| {
4536 let mut s = s.borrow_mut();
4537 let [g, u, ..] = &mut *s;
4538 g.resize(inter, 0.0);
4539 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
4542 } else {
4544 u.resize(inter, 0.0);
4545 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
4547 for i in 0..inter {
4548 g[i] = d.act.combine(g[i], u[i]);
4549 }
4550 }
4551 FFN_PROBE.with(|pr| {
4554 if let Some(acc) = pr.borrow_mut().as_mut() {
4555 let li = crate::gpu::cur_layer();
4556 if li >= 0 {
4557 if let Some(row) = acc.get_mut(li as usize) {
4558 for (a, &v) in row.iter_mut().zip(g.iter()) {
4559 *a += (v as f64).abs();
4560 }
4561 }
4562 }
4563 }
4564 });
4565 let mut out = attention::take_buf(d.down_proj.rows());
4566 d.down_proj.matvec(g, &mut out, pool);
4567 out
4568 })
4569}
4570
4571thread_local! {
4572 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
4575 const { std::cell::RefCell::new(None) };
4576}
4577
4578fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
4584 if d.act != Act::Silu {
4586 return None;
4587 }
4588 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
4591 return None;
4592 }
4593 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
4594 let mut model_ref = None;
4595 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
4596 let model = model_ref?;
4597 let hidden = jobs[0].down.1;
4598 let mut out = attention::take_buf(hidden);
4599 if crate::gpu::moe_block(&model, &jobs, &mut out) {
4600 Some(out)
4601 } else {
4602 let mut out = out;
4603 attention::recycle_buf(&mut out);
4604 None
4605 }
4606}
4607
4608#[allow(clippy::type_complexity)]
4613#[allow(clippy::type_complexity)]
4614fn moe_parts(
4615 t: &QTensor,
4616) -> Option<(
4617 &std::sync::Arc<cortiq_core::CmfModel>,
4618 usize,
4619 usize,
4620 usize,
4621 &[f32],
4622 &[f32],
4623 bool,
4624 bool,
4625)> {
4626 match t {
4627 QTensor::Mapped {
4628 model,
4629 idx,
4630 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
4631 rows,
4632 cols,
4633 row_scale,
4634 col_field,
4635 ..
4636 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
4637 Some((model, *idx, *rows, *cols, row_scale, col_field, false, false))
4638 }
4639 QTensor::Mapped {
4641 model,
4642 idx,
4643 dtype: cortiq_core::TensorDtype::Q1,
4644 rows,
4645 cols,
4646 ..
4647 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
4648 QTensor::Mapped {
4650 model,
4651 idx,
4652 dtype: cortiq_core::TensorDtype::Q4Tiled,
4653 rows,
4654 cols,
4655 ..
4656 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
4657 _ => None,
4658 }
4659}
4660
4661fn moe_push_job<'a>(
4663 d: &'a DenseFfn,
4664 x: &[f32],
4665 w: f32,
4666 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
4667 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
4668) -> Option<()> {
4669 use crate::qtensor::prescale;
4670 if d.act != Act::Silu {
4671 return None; }
4673 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
4674 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
4675 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
4676 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
4677 return None; }
4679 model_ref.get_or_insert_with(|| gm.clone());
4680 let gdt = if gcf.is_empty() {
4681 cortiq_core::TensorDtype::Q8Row
4682 } else {
4683 cortiq_core::TensorDtype::Q8_2f
4684 };
4685 let udt = if ucf.is_empty() {
4686 cortiq_core::TensorDtype::Q8Row
4687 } else {
4688 cortiq_core::TensorDtype::Q8_2f
4689 };
4690 jobs.push(crate::gpu::MoeJob {
4691 gate: (gi, gr, gc, grs),
4692 up: (ui, ur, uc, urs),
4693 down: (di, dr, dc, drs),
4694 xs_gate: prescale(x, gcf, gdt).into_owned(),
4695 xs_up: prescale(x, ucf, udt).into_owned(),
4696 down_col: dcf,
4697 w,
4698 q1: gq1,
4699 q4t: gq4,
4700 });
4701 Some(())
4702}
4703
4704fn sparse_ffn_quant(
4711 d: &DenseFfn,
4712 x: &[f32],
4713 active: &[u16],
4714 hidden: usize,
4715 pool: Option<&Pool>,
4716) -> Vec<f32> {
4717 let n = active.len();
4718 let inter = d.gate_proj.rows();
4719 let mut act = vec![0.0f32; n];
4720 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
4723 let compute = |ai: usize| -> f32 {
4724 let idx = active[ai] as usize;
4725 if idx >= inter {
4726 return 0.0; }
4728 let mut s = if need_scratch {
4729 vec![0.0f32; hidden]
4730 } else {
4731 Vec::new()
4732 };
4733 let gate = d.gate_proj.row_dot(idx, x, &mut s);
4734 let up = d.up_proj.row_dot(idx, x, &mut s);
4735 d.act.combine(gate, up)
4736 };
4737 match pool {
4738 Some(p) if n >= 256 => {
4739 let ptr = SendMut(act.as_mut_ptr());
4740 p.run(&|widx, nw| {
4741 let chunk = n.div_ceil(nw);
4742 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
4743 for ai in s..e {
4744 unsafe { *ptr.at(ai) = compute(ai) };
4745 }
4746 });
4747 }
4748 _ => {
4749 for (ai, a) in act.iter_mut().enumerate() {
4750 *a = compute(ai);
4751 }
4752 }
4753 }
4754 let mut out = vec![0.0f32; hidden];
4756 for (ai, &idx) in active.iter().enumerate() {
4757 let w = act[ai];
4758 if w.abs() >= 1e-12 && (idx as usize) < inter {
4759 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
4760 }
4761 }
4762 out
4763}
4764
4765#[doc(hidden)]
4767pub fn sparse_ffn_quant_for_test(
4768 d: &DenseFfn,
4769 x: &[f32],
4770 active: &[u16],
4771 hidden: usize,
4772) -> Vec<f32> {
4773 sparse_ffn_quant(d, x, active, hidden, None)
4774}
4775
4776fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
4780 let deq = |t: &QTensor| -> Vec<f32> {
4781 let (rows, cols) = (t.rows(), t.cols());
4782 let mut out = vec![0.0f32; rows * cols];
4783 for r in 0..rows {
4784 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
4785 }
4786 out
4787 };
4788 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
4789}
4790
4791struct SendMut(*mut f32);
4793unsafe impl Send for SendMut {}
4794unsafe impl Sync for SendMut {}
4795impl SendMut {
4796 #[inline]
4797 #[allow(clippy::mut_from_ref)]
4800 unsafe fn at(&self, i: usize) -> &mut f32 {
4801 unsafe { &mut *self.0.add(i) }
4802 }
4803}
4804
4805fn moe_route(
4815 logits: &[f32],
4816 m: &MoeFfn,
4817 allowed: Option<&[bool]>,
4818) -> (Vec<usize>, Vec<f32>, f32) {
4819 let ne = logits.len();
4820 let p: Vec<f32> = if m.router_sigmoid {
4821 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
4822 } else {
4823 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4824 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
4825 let s: f32 = e.iter().sum();
4826 for v in &mut e {
4827 *v /= s;
4828 }
4829 e
4830 };
4831 let admit = |e: usize| {
4837 m.mask.as_ref().is_none_or(|mk| mk[e]) && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
4838 };
4839 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
4840 match &m.expert_bias {
4842 Some(b) => idx.sort_unstable_by(|&x, &y| {
4843 (p[y] + b[y])
4844 .partial_cmp(&(p[x] + b[x]))
4845 .unwrap()
4846 .then(x.cmp(&y))
4847 }),
4848 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
4849 }
4850 idx.truncate(m.top_k);
4851 if let Some(tau) = m.route_tau {
4855 let total: f32 = idx.iter().map(|&e| p[e]).sum();
4856 if total > 0.0 {
4857 let mut acc = 0.0f32;
4858 let mut keep = idx.len();
4859 for (i, &e) in idx.iter().enumerate() {
4860 acc += p[e];
4861 if acc >= tau * total {
4862 keep = i + 1;
4863 break;
4864 }
4865 }
4866 idx.truncate(keep);
4867 }
4868 }
4869 let wsum: f32 = if m.norm_topk_prob {
4870 let s: f32 = idx.iter().map(|&e| p[e]).sum();
4871 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
4874 } else {
4875 1.0 / m.routed_scaling
4876 };
4877 (idx, p, wsum)
4878}
4879
4880fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
4883 let ne = m.experts.len();
4884 let mut logits = vec![0.0f32; ne];
4885 m.router.matvec(x, &mut logits, pool);
4886 let (idx, p, wsum) = moe_route(&logits, m, allowed);
4887 {
4888 let mut st = m.stats.borrow_mut();
4889 if st.len() < ne {
4890 st.resize(ne, 0);
4891 }
4892 for &e in &idx {
4893 st[e] += 1;
4894 }
4895 }
4896 if crate::gpu::enabled_here() {
4901 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
4902 crate::gpu::ProbeArm::Gpu => {
4903 let t0 = std::time::Instant::now();
4904 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
4905 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4906 return out;
4907 }
4908 }
4909 crate::gpu::ProbeArm::CpuTimed => {
4910 let t0 = std::time::Instant::now();
4911 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4912 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4913 return out;
4914 }
4915 crate::gpu::ProbeArm::Cpu => {
4916 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4917 }
4918 }
4919 }
4920 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
4921}
4922
4923fn moe_ffn_cpu(
4925 m: &MoeFfn,
4926 x: &[f32],
4927 idx: &[usize],
4928 p: &[f32],
4929 wsum: f32,
4930 pool: Option<&Pool>,
4931) -> Vec<f32> {
4932 let mut out = attention::take_buf(x.len());
4933 for &e in idx {
4934 let mut eo = dense_ffn(&m.experts[e], x, pool);
4935 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
4936 for i in 0..out.len() {
4937 out[i] += w * eo[i];
4938 }
4939 attention::recycle_buf(&mut eo);
4940 }
4941 if let Some((se, gate)) = &m.shared {
4942 let mut so = dense_ffn(se, x, pool);
4943 let g = gate.as_ref().map_or(1.0, |gate| {
4944 let mut gl = [0.0f32; 1];
4945 gate.matvec(x, &mut gl, pool);
4946 1.0 / (1.0 + (-gl[0]).exp())
4947 });
4948 for i in 0..out.len() {
4949 out[i] += g * so[i];
4950 }
4951 attention::recycle_buf(&mut so);
4952 }
4953 out
4954}
4955
4956#[allow(clippy::too_many_arguments)]
4964fn mla_attention(
4965 w: &MlaWeights,
4966 normed: &[f32],
4967 cache: &mut crate::kv_cache::LayerKvCache,
4968 position: usize,
4969 inv_freq: &[f32],
4970 rope_scale: f32,
4971 eps: f64,
4972 pool: Option<&Pool>,
4973) -> Vec<f32> {
4974 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
4975 let hd = dr + dn;
4976 let mut q = vec![0.0f32; nh * hd];
4977 match (&w.q_a, &w.q_a_norm) {
4978 (Some(qa), Some(qn)) => {
4979 let mut t = vec![0.0f32; qa.rows()];
4980 qa.matvec(normed, &mut t, pool);
4981 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
4982 w.q_proj.matvec(&tn, &mut q, pool);
4983 }
4984 _ => w.q_proj.matvec(normed, &mut q, pool),
4985 }
4986 let mut ca = vec![0.0f32; lora + dr];
4987 w.kv_a.matvec(normed, &mut ca, pool);
4988 let (c_lat, k_rope) = ca.split_at_mut(lora);
4989 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
4990 let mut kvb = vec![0.0f32; nh * (dn + dv)];
4991 w.kv_b.matvec(&latn, &mut kvb, pool);
4992 if !w.nope {
4993 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
4994 }
4995 for h in 0..nh {
4996 if !w.nope {
4997 attention::rope_rotate_scaled(
4998 &mut q[h * hd..h * hd + dr],
4999 position,
5000 inv_freq,
5001 rope_scale,
5002 );
5003 }
5004 }
5005 let mut k = vec![0.0f32; nh * hd];
5006 let mut v = vec![0.0f32; nh * hd];
5007 for h in 0..nh {
5008 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
5009 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
5010 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
5011 }
5012 cache.append(&k, &v, &vec![true; nh]);
5013 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
5014 attention::recycle_buf(&mut imp);
5015 let mut ov = vec![0.0f32; nh * dv];
5016 for h in 0..nh {
5017 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
5018 }
5019 let mut out = vec![0.0f32; w.o_proj.rows()];
5020 w.o_proj.matvec(&ov, &mut out, pool);
5021 out
5022}
5023
5024fn dense_moe_ffn(
5031 dm: &DenseMoeFfn,
5032 x_normed: &[f32],
5033 h_raw: &[f32],
5034 eps: f64,
5035 norm_style: NormStyle,
5036 pool: Option<&Pool>,
5037) -> Vec<f32> {
5038 let mut d = dense_ffn(&dm.dense, x_normed, pool);
5039 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
5040 let m = &dm.moe;
5041 let ne = m.experts.len();
5042 let mut logits = vec![0.0f32; ne];
5043 if m.router_input_norm {
5044 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
5045 let inv = 1.0 / (ss + eps as f32).sqrt();
5046 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
5047 m.router.matvec(&xr, &mut logits, pool);
5048 } else {
5049 m.router.matvec(h_raw, &mut logits, pool);
5050 }
5051 let (idx, p, wsum) = moe_route(&logits, m, None);
5052 {
5053 let mut st = m.stats.borrow_mut();
5054 if st.len() < ne {
5055 st.resize(ne, 0);
5056 }
5057 for &e in &idx {
5058 st[e] += 1;
5059 }
5060 }
5061 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
5062 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
5063 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
5064 for (di, mi) in d.iter_mut().zip(&mo) {
5065 *di += mi;
5066 }
5067 d
5068}
5069
5070fn moe_ffn_gpu(
5073 m: &MoeFfn,
5074 x: &[f32],
5075 idx: &[usize],
5076 p: &[f32],
5077 wsum: f32,
5078 pool: Option<&Pool>,
5079) -> Option<Vec<f32>> {
5080 use crate::gpu::MoeJob;
5081
5082 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
5083 let mut model_ref = None;
5084 for &e in idx {
5085 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
5086 }
5087 if let Some((se, gate)) = &m.shared {
5088 let g = gate.as_ref().map_or(1.0, |gate| {
5089 let mut gl = [0.0f32; 1];
5090 gate.matvec(x, &mut gl, pool);
5091 1.0 / (1.0 + (-gl[0]).exp())
5092 });
5093 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
5094 }
5095 let model = model_ref?;
5096 let hidden = jobs[0].down.1;
5097 let mut out = vec![0.0f32; hidden];
5098 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
5099}
5100
5101fn ffn_forward(
5103 ffn: &FfnKind,
5104 x: &[f32],
5105 pool: Option<&Pool>,
5106 experts_allowed: Option<&[bool]>,
5107) -> Vec<f32> {
5108 match ffn {
5109 FfnKind::Dense(d) => dense_ffn(d, x, pool),
5110 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
5111 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
5115 }
5116}
5117
5118fn ffn_forward_pair(
5122 ffn: &FfnKind,
5123 x1: &[f32],
5124 x2: &[f32],
5125 pool: Option<&Pool>,
5126 experts_allowed: Option<&[bool]>,
5127) -> (Vec<f32>, Vec<f32>) {
5128 let d = match ffn {
5129 FfnKind::Dense(d) => d,
5130 FfnKind::Moe(m) => {
5131 return (
5132 moe_ffn(m, x1, pool, experts_allowed),
5133 moe_ffn(m, x2, pool, experts_allowed),
5134 );
5135 }
5136 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
5137 };
5138 let inter = d.gate_proj.rows();
5139 FFN_SCRATCH.with(|s| {
5140 let mut s = s.borrow_mut();
5141 let [g1, g2, u1, u2] = &mut *s;
5142 g1.resize(inter, 0.0);
5143 g2.resize(inter, 0.0);
5144 u1.resize(inter, 0.0);
5145 u2.resize(inter, 0.0);
5146 QTensor::matvec2_many(
5149 [&d.gate_proj, &d.up_proj],
5150 x1,
5151 x2,
5152 [g1.as_mut_slice(), u1.as_mut_slice()],
5153 [g2.as_mut_slice(), u2.as_mut_slice()],
5154 pool,
5155 );
5156 for i in 0..inter {
5157 g1[i] = d.act.combine(g1[i], u1[i]);
5158 g2[i] = d.act.combine(g2[i], u2[i]);
5159 }
5160 let mut o1 = attention::take_buf(d.down_proj.rows());
5161 let mut o2 = attention::take_buf(d.down_proj.rows());
5162 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
5163 (o1, o2)
5164 })
5165}
5166
5167#[cfg(test)]
5168mod tests {
5169 use super::*;
5170
5171 #[test]
5177 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
5178 let (hidden, inter) = (16usize, 40usize);
5179 let synth = |n: usize, salt: usize| -> Vec<f32> {
5180 (0..n)
5181 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
5182 .collect()
5183 };
5184 let d = DenseFfn {
5185 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
5186 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
5187 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
5188 act: Act::Silu,
5189 };
5190 let x = synth(hidden, 9);
5191 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
5193
5194 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
5195
5196 let mut g = vec![0.0f32; inter];
5198 d.gate_proj.matvec(&x, &mut g, None);
5199 let mut u = vec![0.0f32; inter];
5200 d.up_proj.matvec(&x, &mut u, None);
5201 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
5202 for i in 0..inter {
5203 g[i] = if act_set.contains(&(i as u16)) {
5204 inference::silu(g[i]) * u[i]
5205 } else {
5206 0.0
5207 };
5208 }
5209 let mut reference = vec![0.0f32; hidden];
5210 d.down_proj.matvec(&g, &mut reference, None);
5211
5212 let max_d = sparse
5213 .iter()
5214 .zip(&reference)
5215 .map(|(a, b)| (a - b).abs())
5216 .fold(0.0f32, f32::max);
5217 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
5218 }
5219
5220 fn attach_test_mtp(p: &mut Pipeline) {
5222 let (h, inter, heads, kv, hd) = (
5223 p.hidden_size,
5224 p.intermediate_size,
5225 p.num_heads,
5226 p.num_kv_heads,
5227 p.head_dim,
5228 );
5229 let synth = |n: usize, salt: usize| -> Vec<f32> {
5230 (0..n)
5231 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
5232 .collect()
5233 };
5234 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
5235 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
5236 };
5237 p.mtp = Some(MtpModule {
5238 enorm: vec![1.0; h],
5239 hnorm: vec![1.0; h],
5240 eh_proj: qt(h, 2 * h, 301),
5241 layer: LayerWeights {
5242 input_norm: vec![1.0; h],
5243 post_norm: vec![1.0; h],
5244 attn_out_norm: None,
5245 ffn_out_norm: None,
5246 layer_scale: None,
5247 ffn: FfnKind::Dense(DenseFfn {
5248 gate_proj: qt(inter, h, 315),
5249 up_proj: qt(inter, h, 316),
5250 down_proj: qt(h, inter, 317),
5251 act: Act::Silu,
5252 }),
5253 attn: AttnKind::Full {
5254 bias: None,
5255 wq: qt(heads * hd, h, 311),
5256 wk: qt(kv * hd, h, 312),
5257 wv: qt(kv * hd, h, 313),
5258 wo: qt(h, heads * hd, 314),
5259 q_norm: None,
5260 k_norm: None,
5261 output_gate: false,
5262 softplus_gate: None,
5263 },
5264 },
5265 final_norm: vec![1.0; h],
5266 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
5267 });
5268 }
5269
5270 #[test]
5271 fn speculative_equals_vanilla_greedy() {
5272 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5276 let run = |spec: bool| {
5277 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5278 p.sampler_config.temperature = 0.0;
5279 attach_test_mtp(&mut p);
5280 p.speculative = spec;
5281 let r = p.generate("abcdef", 12, None, None).unwrap();
5282 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
5283 };
5284 let (vanilla, d0, _) = run(false);
5285 let (spec, d1, a1) = run(true);
5286 assert_eq!(d0, 0, "vanilla path must not draft");
5287 assert!(d1 > 0, "speculative path must draft");
5288 assert_eq!(
5289 vanilla, spec,
5290 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
5291 );
5292 }
5293
5294 #[test]
5295 fn speculative_accepts_constant_oracle() {
5296 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5298 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5299 p.sampler_config.temperature = 0.0;
5300 p.sampler_config.repetition_penalty = 1.0;
5301 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
5304 attach_test_mtp(&mut p);
5305 p.speculative = true;
5306 let r = p.generate("abcd", 10, None, None).unwrap();
5307 assert!(r.mtp_drafted > 0);
5308 assert_eq!(
5309 r.mtp_accepted, r.mtp_drafted,
5310 "constant logits → every draft accepted"
5311 );
5312 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
5315 }
5316
5317 #[test]
5318 fn empty_prompt_is_an_error_not_a_panic() {
5319 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
5320 let r = p.generate("", 4, None, None);
5321 assert!(r.is_err(), "empty prompt must be a clean error");
5322 }
5323
5324 #[test]
5325 fn every_token_enters_kv_exactly_once() {
5326 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5327 p.sampler_config.temperature = 0.0;
5329 let r = p.generate("abc", 2, None, None).unwrap();
5330 assert_eq!(r.prompt_tokens, 3);
5331 assert_eq!(
5335 p.kv_cache.seq_len(),
5336 3 + r.tokens_generated - 1,
5337 "each token must be cached exactly once (v1 cached the last prompt token twice)"
5338 );
5339 }
5340
5341 #[test]
5342 fn generation_is_reproducible_with_seed() {
5343 let run = || {
5344 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5345 p.generate("hello", 8, None, None).unwrap().token_ids
5346 };
5347 assert_eq!(run(), run());
5348 }
5349
5350 #[test]
5351 fn resetting_sampler_restarts_the_seeded_stream() {
5352 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5353 let config = SamplerConfig {
5354 seed: Some(1234),
5355 ..SamplerConfig::default()
5356 };
5357 p.set_sampler_config(config.clone());
5358 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
5359 p.set_sampler_config(config);
5360 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
5361 assert_eq!(first, second);
5362 }
5363
5364 #[test]
5365 fn eviction_bounds_the_cache() {
5366 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
5367 p.kv_cache.max_seq_len = 6;
5368 p.sampler_config.temperature = 0.0;
5369 let _ = p.generate("abcd", 12, None, None).unwrap();
5370 assert!(
5371 p.kv_cache.seq_len() <= 6 + 1,
5372 "cache must stay bounded by max_seq_len (got {})",
5373 p.kv_cache.seq_len()
5374 );
5375 }
5376
5377 #[test]
5378 fn confidence_matches_tokens_and_is_a_probability() {
5379 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5380 p.sampler_config.temperature = 0.0;
5381 p.sampler_config.repetition_penalty = 1.0;
5382 let r = p.generate("abcd", 10, None, None).unwrap();
5383 assert_eq!(
5384 r.token_confidence.len(),
5385 r.token_ids.len(),
5386 "one confidence per emitted token"
5387 );
5388 for &c in &r.token_confidence {
5389 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
5390 }
5391 let logits = [1.0f32, 3.0, 0.5, 3.0];
5393 let p0 = top1_prob_t(&logits, 1, 1.0);
5394 let p1 = top1_prob_t(&logits, 3, 1.0);
5395 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
5396 assert!(p0 > 0.0 && p0 < 1.0);
5397 let sharp = top1_prob_t(&logits, 1, 1.0);
5399 let soft = top1_prob_t(&logits, 1, 2.0);
5400 assert!(soft < sharp, "higher temperature lowers peak confidence");
5401 }
5402
5403 #[test]
5404 fn trace_is_opt_in_and_parallels_the_output() {
5405 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5407 p.sampler_config.temperature = 0.0;
5408 p.sampler_config.repetition_penalty = 1.0;
5409 let r = p.generate("abcd", 10, None, None).unwrap();
5410 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
5411
5412 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5414 p.sampler_config.temperature = 0.0;
5415 p.sampler_config.repetition_penalty = 1.0;
5416 p.set_trace(true);
5417 let r = p.generate("abcd", 10, None, None).unwrap();
5418 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
5419 for (i, tr) in r.traces.iter().enumerate() {
5420 assert_eq!(tr.t, i, "trace index is sequential");
5421 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
5422 assert_eq!(
5423 tr.confidence, r.token_confidence[i],
5424 "trace confidence matches the confidence channel"
5425 );
5426 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
5428 }
5429 }
5430
5431 #[test]
5432 fn explain_prefill_logits_match_greedy_first_token() {
5433 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5437 p.sampler_config.temperature = 0.0;
5438 p.sampler_config.repetition_penalty = 1.0;
5439 let ids = p.tokenizer.encode("abcd");
5440 let logits = p.prefill_next_logits(&ids, None);
5441 let argmax = logits
5442 .iter()
5443 .enumerate()
5444 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
5445 .unwrap()
5446 .0 as u32;
5447 let r = p.generate("abcd", 1, None, None).unwrap();
5448 assert_eq!(
5449 argmax, r.token_ids[0],
5450 "explain preview must match greedy emit"
5451 );
5452 }
5453
5454 #[test]
5455 fn laguna_shared_expert_is_unconditionally_added() {
5456 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
5457 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
5458 let zero_dense = || DenseFfn {
5459 gate_proj: matrix(vec![0.0; 4]),
5460 up_proj: matrix(vec![0.0; 4]),
5461 down_proj: matrix(vec![0.0; 4]),
5462 act: Act::Silu,
5463 };
5464 let shared = DenseFfn {
5465 gate_proj: identity(),
5466 up_proj: identity(),
5467 down_proj: identity(),
5468 act: Act::Silu,
5469 };
5470 let x = [1.0, 2.0];
5471 let expected = dense_ffn(&shared, &x, None);
5472 let moe = MoeFfn {
5473 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
5474 experts: vec![zero_dense()],
5475 top_k: 1,
5476 norm_topk_prob: true,
5477 router_sigmoid: true,
5478 expert_bias: None,
5479 routed_scaling: 1.0,
5480 route_tau: None,
5481 shared: Some((shared, None)),
5482 stats: std::cell::RefCell::new(Vec::new()),
5483 mask: None,
5484 per_expert_scale: None,
5485 router_input_norm: false,
5486 };
5487 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
5488 for (actual, expected) in actual.iter().zip(expected) {
5489 assert!((actual - expected).abs() < 1e-6);
5490 }
5491 }
5492}