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