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