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