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