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_epoch: u64,
151 o1_flags: Vec<bool>,
153 trace: bool,
156 calib_temp: f32,
159 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
161 graph_kv_id: u64,
162 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
165 graph_want_logits: bool,
166 graph_logits: Option<Vec<f32>>,
169 pub embed_multiplier: f32,
171 pub attn_scale: f32,
174 pub swa: Option<(usize, usize)>,
177 pub sliding_layers: Option<Vec<bool>>,
180 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
183 pub rotary_dim_local: Option<usize>,
184 pub rope_scale: f32,
185 pub rope_scale_local: f32,
186 pub global_attn: Option<(usize, usize)>,
189 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
192 pub attn_v_norm: bool,
194 pub final_softcap: Option<f32>,
196 pub attn_softcap: f32,
198 confidence_on: bool,
202}
203
204#[cfg(target_os = "macos")]
205impl Drop for Pipeline {
206 fn drop(&mut self) {
207 crate::gpu::kv_mirror_drop(self.graph_kv_id);
208 }
209}
210
211pub struct PipelineWeights {
216 pub embed_tokens: QTensor,
218 pub layers: Vec<LayerWeights>,
220 pub lm_head: QTensor,
222 pub final_norm: Vec<f32>,
224}
225
226pub struct LayerWeights {
228 pub input_norm: Vec<f32>,
229 pub post_norm: Vec<f32>,
232 pub attn_out_norm: Option<Vec<f32>>,
235 pub layer_scale: Option<f32>,
237 pub ffn_out_norm: Option<Vec<f32>>,
240 pub ffn: FfnKind,
241 pub attn: AttnKind,
242}
243
244#[derive(Clone, Copy, PartialEq, Debug, Default)]
247pub enum Act {
248 #[default]
249 Silu,
250 GeluTanh,
251 Situ { beta: f32, linear_beta: f32 },
254}
255
256impl Act {
257 pub fn from_arch(name: &str) -> Self {
258 if name == "gelu_tanh" {
259 Self::GeluTanh
260 } else {
261 Self::Silu
262 }
263 }
264
265 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
267 match arch.hidden_act.as_str() {
268 "situ" => Self::Situ {
269 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
270 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
271 },
272 other => Self::from_arch(other),
273 }
274 }
275
276 #[inline]
277 pub fn apply(self, x: f32) -> f32 {
278 match self {
279 Self::Silu => inference::silu(x),
280 Self::GeluTanh => inference::gelu_tanh(x),
281 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
282 }
283 }
284
285 #[inline]
288 pub fn combine(self, g: f32, u: f32) -> f32 {
289 match self {
290 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
291 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
292 }
293 _ => self.apply(g) * u,
294 }
295 }
296}
297
298pub struct DenseFfn {
300 pub gate_proj: QTensor,
301 pub up_proj: QTensor,
302 pub down_proj: QTensor,
303 pub act: Act,
305}
306
307pub enum FfnKind {
310 Dense(DenseFfn),
311 Moe(MoeFfn),
315 DenseMoe(Box<DenseMoeFfn>),
322}
323
324pub struct DenseMoeFfn {
326 pub dense: DenseFfn,
327 pub moe: MoeFfn,
328 pub post_norm_1: Vec<f32>,
330 pub pre_norm_2: Vec<f32>,
333 pub post_norm_2: Vec<f32>,
335}
336
337pub struct MoeFfn {
338 pub router: QTensor,
340 pub experts: Vec<DenseFfn>,
341 pub top_k: usize,
342 pub norm_topk_prob: bool,
343 pub router_sigmoid: bool,
346 pub expert_bias: Option<Vec<f32>>,
350 pub routed_scaling: f32,
353 pub route_tau: Option<f32>,
359 pub shared: Option<(DenseFfn, Option<QTensor>)>,
362 pub stats: std::cell::RefCell<Vec<u64>>,
366 pub act_sq: std::cell::RefCell<Vec<f64>>,
373 pub act_rows: std::cell::RefCell<Vec<f32>>,
379 pub mask: Option<Vec<bool>>,
384 pub per_expert_scale: Option<Vec<f32>>,
387 pub router_input_norm: bool,
391}
392
393pub enum AttnKind {
396 Full {
398 wq: QTensor,
399 wk: QTensor,
400 wv: QTensor,
401 wo: QTensor,
402 q_norm: Option<Vec<f32>>,
403 k_norm: Option<Vec<f32>>,
404 output_gate: bool,
405 softplus_gate: Option<(QTensor, bool)>,
409 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
411 },
412 Linear(VmfPhaseWeights),
414 LinearGdn(GdnWeights),
416 ShortConv(ShortConvWeights),
419 Mla(Box<MlaWeights>),
427 Kda(Box<crate::linear_core::KdaWeights>),
431}
432
433pub struct MlaWeights {
435 pub q_proj: QTensor,
439 pub q_a: Option<QTensor>,
442 pub q_a_norm: Option<Vec<f32>>,
443 pub kv_a: QTensor,
445 pub kv_a_norm: Vec<f32>,
447 pub kv_b: QTensor,
449 pub o_proj: QTensor,
451 pub nh: usize,
452 pub qk_rope: usize,
453 pub qk_nope: usize,
454 pub v_dim: usize,
455 pub lora: usize,
456 pub scale: f32,
458 pub nope: bool,
460}
461
462pub struct MtpModule {
467 pub enorm: Vec<f32>,
468 pub hnorm: Vec<f32>,
469 pub eh_proj: QTensor,
471 pub layer: LayerWeights,
472 pub final_norm: Vec<f32>,
473 pub kv: crate::kv_cache::LayerKvCache,
474}
475
476pub struct GenerateResult {
478 pub text: String,
479 pub token_ids: Vec<u32>,
480 pub prompt_tokens: usize,
481 pub tokens_generated: usize,
482 pub finish_reason: String,
483 pub mtp_drafted: usize,
485 pub mtp_accepted: usize,
486 pub token_confidence: Vec<f32>,
491 pub traces: Vec<TokenTrace>,
494}
495
496#[derive(Clone, Debug)]
501pub struct TokenTrace {
502 pub t: usize,
504 pub token_id: u32,
506 pub confidence: f32,
508 pub active_skill: Option<String>,
510 pub recon: Option<f32>,
514 pub switched: bool,
517}
518
519fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
524 let t = if temp > 1e-3 { temp } else { 1.0 };
525 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
526 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
527 if sum > 0.0 {
528 (((logits[id as usize] - max) / t).exp()) / sum
529 } else {
530 0.0
531 }
532}
533
534fn prefill_batched() -> bool {
537 std::env::var("CMF_PREFILL")
538 .map(|v| v != "seq")
539 .unwrap_or(true)
540}
541
542fn prefill_chunk() -> usize {
547 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
548 .ok()
549 .and_then(|v| v.parse::<usize>().ok())
550 {
551 return n.max(1);
552 }
553 if cfg!(target_os = "macos") {
554 512
555 } else if cfg!(target_arch = "aarch64") {
556 256
559 } else {
560 48
561 }
562}
563
564pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
566
567impl Pipeline {
568 #[inline]
572 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
573 virtual_idx % self.physical_layers
574 }
575
576 #[inline]
579 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
580 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
581 }
582
583 #[allow(clippy::too_many_arguments)]
585
586 #[cfg(target_os = "macos")]
605 fn graph_prefill_preferred(&self) -> bool {
606 if !crate::gpu::enabled_here()
607 || !crate::gpu::q1_force()
608 || std::env::var("CMF_GPU_BLOCK")
609 .map(|v| v == "0")
610 .unwrap_or(false)
611 {
612 return false;
613 }
614 self.weights
615 .layers
616 .iter()
617 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
618 }
619
620 #[cfg(not(target_os = "macos"))]
621 fn graph_prefill_preferred(&self) -> bool {
622 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
630 .map(|v| v != "0")
631 .unwrap_or_else(|_| {
632 crate::gpu::wgpu_graph_default()
636 });
637 if !graph_on || !crate::gpu::enabled_here() {
638 return false;
639 }
640 self.weights
641 .layers
642 .iter()
643 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
644 }
645
646 #[cfg(target_os = "macos")]
647 fn q1_graph_gpu(
648 &mut self,
649 start: usize,
650 upto: Option<usize>,
651 position: usize,
652 h: &mut [f32],
653 ) -> usize {
654 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
655 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
657 || !crate::gpu::q1_force()
658 || std::env::var("CMF_GPU_BLOCK")
659 .map(|v| v == "0")
660 .unwrap_or(false)
661 {
662 return start;
663 }
664 if self.swa.is_some()
669 || self.global_attn.is_some()
670 || self.attention_heads_per_layer.is_some()
671 || self.attn_v_norm
672 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
673 || self.weights.layers.iter().any(|lw| {
674 lw.attn_out_norm.is_some()
675 || lw.ffn_out_norm.is_some()
676 || lw.layer_scale.is_some()
677 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
678 })
679 {
680 return start;
681 }
682 let limit = upto
685 .map(|u| u + 1)
686 .unwrap_or(self.num_layers)
687 .min(self.num_layers);
688
689 enum Item<'a> {
690 Gdn {
691 run: Vec<GdnGpuLayer<'a>>,
692 first: usize,
693 },
694 Attn {
695 l: AttnGpuLayer<'a>,
696 li: usize,
697 q_norm: Option<&'a [f32]>,
698 k_norm: Option<&'a [f32]>,
699 output_gate: bool,
700 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
701 full_gpu: bool,
704 },
705 }
706
707 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
709 let dev_attend = attend_mode != "0"
710 && attend_mode != "off"
711 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
715 && self.head_dim % 4 == 0
716 && self.head_dim <= 256
717 && self.rotary_dim >= 2
718 && self.rotary_dim <= self.head_dim
719 && (self.rotary_dim / 2) % 32 == 0
720 && self.num_kv_heads > 0
721 && self.num_heads % self.num_kv_heads == 0;
722
723 let mut plan: Vec<Item> = Vec::new();
724 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
725 let mut scan = start;
726 while scan < limit {
727 let lw = &self.weights.layers[self.phys_layer(scan)];
728 let FfnKind::Dense(d) = &lw.ffn else { break };
729 let (Some(g), Some(u), Some(dn)) = (
730 d.gate_proj.q1_parts(),
731 d.up_proj.q1_parts(),
732 d.down_proj.q1_parts(),
733 ) else {
734 break;
735 };
736 match &lw.attn {
737 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
738 let parts = (
739 w.in_proj_qkv.q1_parts(),
740 w.in_proj_z.q1_parts(),
741 w.in_proj_a.f32_parts(),
742 w.in_proj_b.f32_parts(),
743 w.out_proj.q1_parts(),
744 );
745 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
746 break;
747 };
748 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
749 model_ref.get_or_insert_with(|| model.clone());
750 }
751 let gl = GdnGpuLayer {
752 attn_norm: &lw.input_norm,
753 post_norm: &lw.post_norm,
754 qkv,
755 z,
756 a,
757 b,
758 out,
759 gate: g,
760 up: u,
761 down: dn,
762 conv1d: &w.conv1d,
763 a_log: &w.a_log,
764 dt_bias: &w.dt_bias,
765 gnorm: &w.norm,
766 };
767 match plan.last_mut() {
768 Some(Item::Gdn { run, .. }) => run.push(gl),
769 _ => plan.push(Item::Gdn {
770 run: vec![gl],
771 first: scan,
772 }),
773 }
774 }
775 AttnKind::Full {
776 wq,
777 wk,
778 wv,
779 wo,
780 q_norm,
781 k_norm,
782 output_gate,
783 softplus_gate: None,
784 bias,
785 } if !self.kv_cache.layers[scan].o1_sealed() => {
786 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
787 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
788 break;
789 };
790 if let QTensor::Mapped { model, .. } = wq {
791 model_ref.get_or_insert_with(|| model.clone());
792 }
793 let cache = &self.kv_cache.layers[scan];
794 let full_gpu = dev_attend
795 && cache.mode == crate::kv_cache::KvMode::F32
796 && cache.o1.is_none()
797 && bias.is_none()
798 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
799 && pk.1 == self.num_kv_heads * self.head_dim
800 && pv.1 == self.num_kv_heads * self.head_dim
801 && po.2 == self.num_heads * self.head_dim;
802 plan.push(Item::Attn {
803 l: AttnGpuLayer {
804 attn_norm: &lw.input_norm,
805 post_norm: &lw.post_norm,
806 wq: pq,
807 wk: pk,
808 wv: pv,
809 wo: po,
810 gate: g,
811 up: u,
812 down: dn,
813 },
814 li: scan,
815 q_norm: q_norm.as_deref(),
816 k_norm: k_norm.as_deref(),
817 output_gate: *output_gate,
818 bias: bias
819 .as_ref()
820 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
821 full_gpu,
822 });
823 }
824 _ => break,
825 }
826 scan += 1;
827 }
828 let Some(model) = model_ref else { return start };
829 if plan.is_empty() {
830 return start;
831 }
832 let dims = GraphDims {
833 hidden: self.hidden_size,
834 eps: self.rms_eps as f32,
835 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
836 };
837 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
838 return start;
839 };
840 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
841 nv: cfg.num_v_heads,
842 nk: cfg.num_k_heads,
843 dk: cfg.key_head_dim,
844 dv: cfg.value_head_dim,
845 kk: cfg.conv_kernel,
846 hidden: self.hidden_size,
847 inter: self.intermediate_size,
848 c_dim: cfg.conv_dim(),
849 eps: cfg.rms_eps as f32,
850 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
851 });
852 let mut valid = 0usize;
856 let mut end = start;
857 for item in &plan {
858 let ok = match item {
859 Item::Gdn { run, .. } => gcfg
860 .as_ref()
861 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
862 .unwrap_or(false),
863 Item::Attn { l, .. } => graph.attn_ok(l),
864 };
865 if !ok {
866 break;
867 }
868 valid += 1;
869 end += match item {
870 Item::Gdn { run, .. } => run.len(),
871 Item::Attn { .. } => 1,
872 };
873 }
874 plan.truncate(valid);
875 if plan.is_empty() {
876 return start;
877 }
878
879 let inv_freq = self.inv_freq.clone();
880 let pool = self.pool.clone();
881 let (nh, nkv, hd, hs, rd, eps) = (
882 self.num_heads,
883 self.num_kv_heads,
884 self.head_dim,
885 self.hidden_size,
886 self.rotary_dim,
887 self.rms_eps,
888 );
889 let norm_style = self.norm_style;
890 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
891 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
892 let kv_id = self.graph_kv_id;
893 let mut pending: Vec<(usize, usize)> = Vec::new();
896 let mut dev_attn: Vec<usize> = Vec::new();
899 for item in &plan {
900 if self.loop_final_norm {
902 let item_start = match item {
903 Item::Gdn { first, .. } => *first,
904 Item::Attn { li, .. } => *li,
905 };
906 if item_start > start && self.is_loop_end(item_start - 1) {
907 graph.encode_loop_norm(&self.weights.final_norm);
908 }
909 }
910 match item {
911 Item::Gdn { run, first } => {
912 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
913 if l.linear_state.len() != want {
914 l.linear_state = vec![0f32; want];
915 }
916 }
917 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
918 .iter()
919 .map(|l| l.linear_state.as_slice())
920 .collect();
921 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
922 tracing::error!("q1 graph: GDN run refused after validation");
924 return start;
925 }
926 graph.commit();
929 pending.push((*first, run.len()));
930 }
931 Item::Attn {
932 l,
933 li,
934 q_norm,
935 k_norm,
936 output_gate,
937 bias,
938 full_gpu,
939 } => {
940 if *full_gpu {
942 let cache = &self.kv_cache.layers[*li];
943 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
944 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
945 let cpu_stored = cpu_k[0].len() / hd;
946 let p = crate::gpu::AttnDeviceParams {
947 kv_id,
948 layer: *li,
949 nh,
950 nkv,
951 hd,
952 rd,
953 position,
954 eps: eps as f32,
955 gemma,
956 output_gate: *output_gate,
957 q_norm: *q_norm,
958 k_norm: *k_norm,
959 inv_freq: &inv_freq,
960 cpu_k,
961 cpu_v,
962 cpu_stored,
963 };
964 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
965 graph.commit();
966 dev_attn.push(*li);
967 continue;
968 }
969 }
971 graph.encode_attn_prefix(l);
972 graph.sync();
973 if !pending.is_empty() {
974 let idxs: Vec<usize> =
975 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
976 let mut outs: Vec<&mut [f32]> = self
977 .kv_cache
978 .layers
979 .iter_mut()
980 .enumerate()
981 .filter(|(i, _)| idxs.binary_search(i).is_ok())
982 .map(|(_, s)| s.linear_state.as_mut_slice())
983 .collect();
984 graph.read_states(&mut outs);
985 }
986 let mut q_raw = attention::take_buf(l.wq.1);
987 let mut k = attention::take_buf(l.wk.1);
988 let mut v = attention::take_buf(l.wv.1);
989 graph.read_qkv(&mut q_raw, &mut k, &mut v);
990 let cfg = QwenAttnCfg {
991 num_heads: nh,
992 num_kv_heads: nkv,
993 head_dim: hd,
994 hidden_size: hs,
995 position,
996 inv_freq: &inv_freq,
997 rotary_dim: rd,
998 scale: self.attn_scale,
999 softcap: self.attn_softcap,
1000 window: None,
1001 v_norm: false,
1002 q_norm: *q_norm,
1003 k_norm: *k_norm,
1004 output_gate: *output_gate,
1005 softplus_gate: None,
1006 rope_scale: 1.0,
1007 bias: *bias,
1008 rms_eps: eps,
1009 norm_style,
1010 pool: pool.as_deref(),
1011 };
1012 let mut ao = attention::qwen_attention_core(
1013 q_raw,
1014 k,
1015 v,
1016 &mut self.kv_cache.layers[*li],
1017 &cfg,
1018 );
1019 graph.encode_attn_suffix(l, &ao);
1020 graph.commit();
1023 attention::recycle_buf(&mut ao);
1024 }
1025 }
1026 }
1027 let mut lm_rows = None;
1032 if self.graph_want_logits
1033 && upto.is_none()
1034 && end == self.num_layers
1035 && std::env::var("CMF_GPU_LMHEAD")
1036 .map(|v| v != "0")
1037 .unwrap_or(true)
1038 {
1039 if let Some(lm) = self.weights.lm_head.q1_parts() {
1040 if graph.lm_head_ok(lm) {
1041 graph.encode_lm_head(&self.weights.final_norm, lm);
1042 lm_rows = Some(lm.1);
1043 }
1044 }
1045 }
1046 graph.sync();
1047 if !pending.is_empty() {
1048 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1049 let mut outs: Vec<&mut [f32]> = self
1050 .kv_cache
1051 .layers
1052 .iter_mut()
1053 .enumerate()
1054 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1055 .map(|(_, s)| s.linear_state.as_mut_slice())
1056 .collect();
1057 graph.read_states(&mut outs);
1058 }
1059 if let Some(rows) = lm_rows {
1060 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1061 graph.read_logits(&mut lg);
1062 lg.resize(self.vocab_size, 0.0);
1063 if let Some(c) = self.final_softcap {
1064 for l in lg.iter_mut() {
1065 *l = c * (*l / c).tanh();
1066 }
1067 }
1068 self.graph_logits = Some(lg);
1069 }
1070 graph.finish(h);
1071 for li in dev_attn {
1075 let mut krow = attention::take_buf(nkv * hd);
1076 let mut vrow = attention::take_buf(nkv * hd);
1077 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1078 let cache = &mut self.kv_cache.layers[li];
1079 cache.append(&krow, &vrow, &[]);
1080 let n = cache.seq_len;
1081 let mut imp = attention::take_buf(n);
1082 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1083 cache.accumulate_imp(&imp);
1084 attention::recycle_buf(&mut imp);
1085 }
1086 attention::recycle_buf(&mut krow);
1087 attention::recycle_buf(&mut vrow);
1088 }
1089 end
1090 }
1091
1092 pub fn new(
1093 tokenizer: Tokenizer,
1094 weights: PipelineWeights,
1095 hidden_size: usize,
1096 intermediate_size: usize,
1097 num_heads: usize,
1098 num_kv_heads: usize,
1099 head_dim: usize,
1100 num_layers: usize,
1101 physical_layers: usize,
1102 loop_final_norm: bool,
1103 vocab_size: usize,
1104 rms_eps: f64,
1105 rope_base: f32,
1106 norm_style: NormStyle,
1107 max_seq_len: usize,
1108 sampler_config: SamplerConfig,
1109 ) -> Self {
1110 let rng = match sampler_config.seed {
1111 Some(s) => SplitMix64::new(s),
1112 None => SplitMix64::from_entropy(),
1113 };
1114 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1115 let pool = Pool::from_env();
1116 if let Some(p) = &pool {
1117 tracing::info!("worker pool: {} threads", p.n_workers());
1118 }
1119 Self {
1120 tokenizer: std::sync::Arc::new(tokenizer),
1121 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1122 sampler_config,
1123 weights,
1124 hidden_size,
1125 intermediate_size,
1126 num_heads,
1127 num_kv_heads,
1128 head_dim,
1129 num_layers,
1130 physical_layers,
1131 loop_final_norm,
1132 vocab_size,
1133 rms_eps,
1134 rope_base,
1135 norm_style,
1136 rotary_dim: head_dim,
1137 attention_heads_per_layer: None,
1138 vmf_cfg: None,
1139 gdn_cfg: None,
1140 kda_cfg: None,
1141 g3n: None,
1142 logit_multiplier: None,
1143 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1144 kv_history: Vec::new(),
1145 short_conv_cfg: None,
1146 mtp: None,
1147 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1148 rng,
1149 sampler_scratch: SamplerScratch::default(),
1150 inv_freq,
1151 ws: ForwardScratch::new(hidden_size),
1152 pool,
1153 model: None,
1154 dyn_force_f32: false,
1155 dyn_skill_layers: Vec::new(),
1156 dyn_active: None,
1157 dyn_blend_loaded: false,
1158 dyn_phi_layer: None,
1159 dyn_phi_ema: Vec::new(),
1160 dyn_phi_seen: 0,
1161 dyn_router: None,
1162 o1_cfg: None,
1163 o1_epoch: 0,
1164 o1_flags: Vec::new(),
1165 trace: false,
1166 calib_temp: 1.0,
1167 confidence_on: true,
1168 embed_multiplier: 1.0,
1169 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1170 swa: None,
1171 sliding_layers: None,
1172 inv_freq_local: None,
1173 rotary_dim_local: None,
1174 rope_scale: 1.0,
1175 rope_scale_local: 1.0,
1176 global_attn: None,
1177 inv_freq_global: None,
1178 attn_v_norm: false,
1179 final_softcap: None,
1180 attn_softcap: 0.0,
1181 graph_want_logits: false,
1182 graph_logits: None,
1183 graph_kv_id: {
1184 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1185 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1186 },
1187 }
1188 }
1189
1190 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1197 self.o1_flags = match &cfg {
1198 Some(c) => {
1199 let mut flags = c.layer_flags(self.num_layers);
1200 for (li, f) in flags.iter_mut().enumerate() {
1201 if *f
1202 && !matches!(
1203 self.weights.layers[self.phys_layer(li)].attn,
1204 AttnKind::Full { .. }
1205 )
1206 {
1207 *f = false;
1208 }
1209 }
1210 flags
1211 }
1212 None => Vec::new(),
1213 };
1214 if let Some(c) = &cfg {
1215 let n = self.o1_flags.iter().filter(|&&f| f).count();
1216 tracing::info!(
1217 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1218 self.num_layers,
1219 c.m,
1220 c.w,
1221 c.sink,
1222 c.rect
1223 );
1224 }
1225 self.o1_cfg = cfg;
1226 }
1227
1228 pub fn o1_active(&self) -> bool {
1230 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1231 }
1232
1233 fn o1_begin(&mut self) {
1235 if let Some(c) = &self.o1_cfg {
1236 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1237 for (li, &f) in self.o1_flags.iter().enumerate() {
1238 if f {
1239 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1240 }
1241 }
1242 }
1243 }
1244
1245 fn o1_seal(&mut self) {
1248 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1249 if self.o1_cfg.is_none() {
1250 return;
1251 }
1252 for li in 0..self.num_layers {
1253 if self.o1_flags.get(li).copied().unwrap_or(false) {
1254 self.kv_cache.layers[li].o1_seal(self.num_heads);
1255 }
1256 }
1257 }
1258
1259 pub fn set_trace(&mut self, on: bool) {
1261 self.trace = on;
1262 }
1263
1264 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1267 self.rng = match config.seed {
1268 Some(seed) => SplitMix64::new(seed),
1269 None => SplitMix64::from_entropy(),
1270 };
1271 self.sampler_config = config;
1272 }
1273
1274 pub fn set_confidence(&mut self, on: bool) {
1279 self.confidence_on = on;
1280 }
1281
1282 pub fn set_calib_temp(&mut self, t: f32) {
1285 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1286 }
1287
1288 pub fn calib_temp(&self) -> f32 {
1290 self.calib_temp
1291 }
1292
1293 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1296 self.rotary_dim = rotary_dim.min(self.head_dim);
1297 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1298 }
1299
1300 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1301 QwenAttnCfg {
1302 num_heads: self.num_heads,
1303 num_kv_heads: self.num_kv_heads,
1304 head_dim: self.head_dim,
1305 hidden_size: self.hidden_size,
1306 position,
1307 inv_freq: &self.inv_freq,
1308 rotary_dim: self.rotary_dim,
1309 scale: self.attn_scale,
1310 softcap: self.attn_softcap,
1311 window: None,
1312 v_norm: false,
1313 q_norm: None,
1314 k_norm: None,
1315 output_gate: false,
1316 softplus_gate: None,
1317 rope_scale: self.rope_scale,
1318 bias: None,
1319 rms_eps: self.rms_eps,
1320 norm_style: self.norm_style,
1321 pool: self.pool.as_deref(),
1322 }
1323 }
1324
1325 pub fn generate(
1327 &mut self,
1328 prompt: &str,
1329 max_tokens: usize,
1330 task_mask: Option<&TaskMask>,
1331 on_token: Option<TokenCallback>,
1332 ) -> Result<GenerateResult, String> {
1333 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1334 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1335 }
1336
1337 pub fn generate_from_ids(
1345 &mut self,
1346 input_ids: &[u32],
1347 max_tokens: usize,
1348 task_mask: Option<&TaskMask>,
1349 mut on_token: Option<TokenCallback>,
1350 ) -> Result<GenerateResult, String> {
1351 if std::env::var("CMF_TRACE_H").is_ok() {
1352 eprintln!("input_ids: {input_ids:?}");
1353 }
1354 if input_ids.is_empty() {
1355 return Err("empty prompt: nothing to generate from".to_string());
1356 }
1357
1358 let reuse_from = {
1366 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1367 let h = &self.kv_history;
1368 if on
1369 && task_mask.is_none()
1370 && self.mtp.is_none()
1371 && self.o1_cfg.is_none()
1372 && !h.is_empty()
1373 && h.len() < input_ids.len()
1374 && input_ids[..h.len()] == h[..]
1375 {
1376 h.len()
1377 } else {
1378 0
1379 }
1380 };
1381 if reuse_from == 0 {
1382 self.kv_cache.clear();
1384 self.kv_history.clear();
1385 crate::gpu::graph_kv_reset(self.graph_kv_id);
1386 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1387 eprintln!(
1388 "kv-reuse: {} of {} prompt positions already cached",
1389 reuse_from,
1390 input_ids.len()
1391 );
1392 }
1393 crate::gpu::graph_race_begin_generation();
1394 self.o1_begin();
1395
1396 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1402 .map(|v| v != "0")
1403 .unwrap_or_else(|_| {
1404 crate::gpu::wgpu_graph_default()
1408 });
1409 let spec_active = self.speculative
1410 && self.mtp.is_some()
1411 && task_mask.is_none()
1412 && !self.o1_active()
1413 && !graph_on
1414 && self.sampler_config.temperature < 1e-6;
1415 let mut mtp = if spec_active { self.mtp.take() } else { None };
1418 if let Some(m) = &mut mtp {
1419 m.kv.clear();
1420 }
1421 let mut router = if mtp.is_none() {
1425 self.dyn_router.take()
1426 } else {
1427 None
1428 };
1429 if let Some(r) = &mut router {
1430 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1433 }
1434
1435 let mut all_ids = input_ids.to_vec();
1436 let mut generated = 0usize;
1437 let mut finish_reason = "max_tokens".to_string();
1438 let mut drafted = 0usize;
1439 let mut accepted = 0usize;
1440 let mut confidence: Vec<f32> = Vec::new();
1441 let trace_on = self.trace;
1442 let calib_temp = self.calib_temp;
1443 let mut traces: Vec<TokenTrace> = Vec::new();
1444
1445 let mut hidden = vec![0.0f32; self.hidden_size];
1451 let mut pos = reuse_from;
1452 let fuse_lm = mtp.is_none()
1461 && router.is_none()
1462 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
1463 self.graph_logits = None;
1464 self.graph_want_logits = false;
1465 let dyn_prefill = router.is_some();
1470 let graph_prefill = self.graph_prefill_preferred();
1476 if task_mask.is_none()
1477 && !dyn_prefill
1478 && !graph_prefill
1479 && prefill_batched()
1480 && self.g3n.is_none()
1481 && input_ids.len() > 2
1482 {
1483 let chunk = prefill_chunk();
1489 let hs = self.hidden_size;
1490 while pos < input_ids.len()
1491 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1492 {
1493 let end = (pos + chunk).min(input_ids.len());
1494 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1495 if let Some(m) = &mut mtp {
1496 for p in pos..end {
1497 if p + 1 < input_ids.len() {
1498 let _ = self.mtp_step(
1499 m,
1500 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1501 input_ids[p + 1],
1502 p,
1503 );
1504 }
1505 }
1506 }
1507 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1508 pos = end;
1509 }
1510 }
1511 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
1512 if task_mask.is_none() && !dyn_prefill && !graph_prefill && !pair_off && self.pair_supported()
1513 {
1514 while pos + 1 < input_ids.len()
1515 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1516 {
1517 let e1 = self.embed_single(input_ids[pos]);
1518 let e2 = self.embed_single(input_ids[pos + 1]);
1519 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1520 self.commit_linear_scratch();
1522 if let Some(m) = &mut mtp {
1523 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1524 if pos + 2 < input_ids.len() {
1525 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1526 }
1527 }
1528 hidden = h2;
1529 pos += 2;
1530 }
1531 }
1532 let _tpf = std::time::Instant::now();
1541 let batch_k = std::env::var("CMF_BATCH_K")
1542 .ok()
1543 .and_then(|v| v.parse::<usize>().ok())
1544 .unwrap_or(0);
1545 if batch_k > 0
1546 && graph_prefill
1547 && task_mask.is_none()
1548 && !self.o1_active()
1549 && mtp.is_none()
1550 && !dyn_prefill
1551 && pos + 1 < input_ids.len()
1552 {
1553 let hs = self.hidden_size;
1554 let chunk = batch_k;
1555 while pos < input_ids.len() {
1556 let end = (pos + chunk).min(input_ids.len());
1557 let bk = end - pos;
1558 let mut hiddens = vec![0f32; bk * hs];
1559 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1560 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1561 }
1562 let positions: Vec<usize> = (pos..end).collect();
1563 let t_chunk = std::time::Instant::now();
1564 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk);
1565 if std::env::var("CMF_GRAPH_PROF").is_ok() {
1566 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
1567 eprintln!(
1568 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
1569 bk as f64 / (ms / 1000.0)
1570 );
1571 }
1572 {
1573 use std::sync::atomic::{AtomicBool, Ordering};
1574 static SAID: AtomicBool = AtomicBool::new(false);
1575 if !SAID.swap(true, Ordering::Relaxed) {
1576 if ok_b {
1577 tracing::info!("batched prefill: ACTIVE (k={bk})");
1578 } else {
1579 tracing::warn!("batched prefill declined — per-position graph");
1580 }
1581 }
1582 }
1583 if ok_b {
1584 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1585 pos = end;
1586 } else {
1587 break; }
1589 }
1590 }
1591 while pos < input_ids.len()
1592 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1593 {
1594 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1595 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1596 if let Some(m) = &mut mtp {
1597 if pos + 1 < input_ids.len() {
1598 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1599 }
1600 }
1601 pos += 1;
1602 }
1603 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1604 eprintln!(
1605 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1606 input_ids.len(),
1607 _tpf.elapsed().as_secs_f64() * 1000.0
1608 );
1609 }
1610 if self.cancel.swap(false, std::sync::atomic::Ordering::Relaxed) {
1613 self.kv_history.clear();
1614 if let Some(m) = mtp {
1615 self.mtp = Some(m);
1616 }
1617 return Ok(GenerateResult {
1618 text: String::new(),
1619 token_ids: Vec::new(),
1620 prompt_tokens: input_ids.len(),
1621 tokens_generated: 0,
1622 finish_reason: "cancelled".to_string(),
1623 mtp_drafted: 0,
1624 mtp_accepted: 0,
1625 token_confidence: Vec::new(),
1626 traces: Vec::new(),
1627 });
1628 }
1629
1630 self.o1_seal();
1633
1634 macro_rules! commit {
1636 ($id:expr) => {{
1637 all_ids.push($id);
1638 generated += 1;
1639 if self.tokenizer.is_eos($id) {
1640 finish_reason = "stop".to_string();
1641 false
1642 } else {
1643 let token_text = self.tokenizer.decode_token($id);
1644 let mut go = true;
1645 if let Some(ref mut cb) = on_token {
1646 if !cb(&token_text) {
1647 finish_reason = "cancelled".to_string();
1648 go = false;
1649 }
1650 }
1651 go
1652 }
1653 }};
1654 }
1655
1656 let mut next_pos = input_ids.len();
1658 'decode: while generated < max_tokens {
1659 if self.cancel.swap(false, std::sync::atomic::Ordering::Relaxed) {
1660 finish_reason = "cancelled".to_string();
1661 break 'decode;
1662 }
1663 let mut logits = match self.graph_logits.take() {
1664 Some(lg) => lg,
1665 None => {
1666 inference::rms_norm_into(
1667 &hidden,
1668 &self.weights.final_norm,
1669 self.rms_eps,
1670 self.norm_style,
1671 &mut self.ws.n1,
1672 );
1673 self.lm_head_forward(&self.ws.n1)
1674 }
1675 };
1676 let t_next = sampler::sample_with_scratch(
1677 &logits,
1678 &self.sampler_config,
1679 &all_ids,
1680 &mut self.rng,
1681 &mut self.sampler_scratch,
1682 );
1683 if self.confidence_on {
1684 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1685 }
1686 attention::recycle_buf(&mut logits);
1687 if trace_on {
1688 let skill = router.as_ref().and_then(|r| r.active_id());
1692 traces.push(TokenTrace {
1693 t: generated,
1694 token_id: t_next,
1695 confidence: confidence.last().copied().unwrap_or(0.0),
1696 active_skill: skill,
1697 recon: None,
1698 switched: false,
1699 });
1700 }
1701 if !commit!(t_next) {
1702 break 'decode;
1703 }
1704 if generated >= max_tokens {
1705 break 'decode;
1706 }
1707
1708 if self.kv_cache.needs_eviction() {
1709 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1710 self.kv_cache.evict(keep);
1711 }
1712
1713 match &mut mtp {
1714 Some(m) if generated + 1 < max_tokens => {
1716 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1717 drafted += 1;
1718 let emb1 = self.embed_single(t_next);
1719 let emb2 = self.embed_single(draft);
1720 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1721
1722 inference::rms_norm_into(
1723 &h1,
1724 &self.weights.final_norm,
1725 self.rms_eps,
1726 self.norm_style,
1727 &mut self.ws.n1,
1728 );
1729 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1730 let t_after = sampler::sample_with_scratch(
1731 &logits1,
1732 &self.sampler_config,
1733 &all_ids,
1734 &mut self.rng,
1735 &mut self.sampler_scratch,
1736 );
1737 if self.confidence_on {
1738 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1739 }
1740 attention::recycle_buf(&mut logits1);
1741 if trace_on {
1742 traces.push(TokenTrace {
1745 t: generated,
1746 token_id: t_after,
1747 confidence: confidence.last().copied().unwrap_or(0.0),
1748 active_skill: None,
1749 recon: None,
1750 switched: false,
1751 });
1752 }
1753 let stop = !commit!(t_after);
1754
1755 if t_after == draft {
1756 accepted += 1;
1757 self.commit_linear_scratch();
1758 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1759 hidden = h2;
1760 next_pos += 2;
1761 } else {
1762 for layer in &mut self.kv_cache.layers {
1764 layer.truncate_last(1);
1765 }
1766 if !stop {
1767 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1768 hidden = self.forward_layers(
1769 &self.embed_single(t_after),
1770 next_pos + 1,
1771 None,
1772 );
1773 }
1774 next_pos += 2;
1775 }
1776 if stop {
1777 break 'decode;
1778 }
1779 }
1780 _ => {
1782 self.graph_want_logits = fuse_lm;
1783 let mut t_fwd = t_next;
1789 let pure_greedy = self.sampler_config.temperature < 1e-6
1790 && self.sampler_config.repetition_penalty == 1.0
1791 && self.sampler_config.suppress_tokens.is_empty();
1792 let burst_k = std::env::var("CMF_MULTISTEP")
1797 .ok()
1798 .and_then(|v| v.parse::<usize>().ok())
1799 .unwrap_or(0);
1800 if pure_greedy
1801 && burst_k >= 1
1802 && fuse_lm
1803 && task_mask.is_none()
1804 && router.is_none()
1805 && !trace_on
1806 && !self.confidence_on
1807 {
1808 let mut stopped = false;
1809 loop {
1810 let room = max_tokens.saturating_sub(generated);
1811 if room <= 2 {
1812 break;
1813 }
1814 let k = burst_k.min(room - 1);
1815 if k < 1 {
1816 break;
1817 }
1818 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
1819 break;
1820 };
1821 next_pos += k;
1822 for &id in &ids {
1823 if !commit!(id) {
1824 stopped = true;
1825 break;
1826 }
1827 }
1828 if stopped {
1829 break;
1830 }
1831 t_fwd = *ids.last().unwrap();
1832 }
1833 if stopped {
1834 break 'decode;
1835 }
1836 }
1837 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
1838 next_pos += 1;
1839 if let Some(r) = &mut router {
1842 let phi = self.dyn_phi_ema.clone();
1843 let decision = r.step(&phi, generated);
1844 if let Some(new_active) = decision {
1845 let _ = self.set_active_skill(new_active);
1846 }
1847 if trace_on {
1850 if let Some(last) = traces.last_mut() {
1851 let e = r.last_best_e();
1852 last.recon = e.is_finite().then_some(e);
1853 last.switched = decision.is_some();
1854 }
1855 }
1856 }
1857 }
1858 }
1859 }
1860
1861 self.graph_want_logits = false;
1862 self.graph_logits = None;
1863 if router.is_some() {
1865 let _ = self.set_active_skill(None);
1866 }
1867 self.dyn_router = router.or(self.dyn_router.take());
1868 self.mtp = mtp.or(self.mtp.take());
1869
1870 let output_ids = &all_ids[input_ids.len()..];
1871 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
1875 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
1876 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1878 Ok(GenerateResult {
1879 text: self.tokenizer.decode(output_ids),
1880 token_ids: output_ids.to_vec(),
1881 prompt_tokens: input_ids.len(),
1882 tokens_generated: generated,
1883 finish_reason,
1884 mtp_drafted: drafted,
1885 mtp_accepted: accepted,
1886 token_confidence: confidence,
1887 traces,
1888 })
1889 }
1890
1891 fn mtp_step(
1895 &mut self,
1896 m: &mut MtpModule,
1897 hidden: &[f32],
1898 next_token: u32,
1899 position: usize,
1900 ) -> u32 {
1901 let e = self.embed_single(next_token);
1905 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1906 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1907 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1908 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1909 let mut x = vec![0.0f32; self.hidden_size];
1910 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1911
1912 let lw = &m.layer;
1914 inference::rms_norm_into(
1915 &x,
1916 &lw.input_norm,
1917 self.rms_eps,
1918 self.norm_style,
1919 &mut self.ws.n1,
1920 );
1921 let attn = match &lw.attn {
1922 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
1924 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
1925 AttnKind::Full {
1926 wq,
1927 wk,
1928 wv,
1929 wo,
1930 q_norm,
1931 k_norm,
1932 output_gate,
1933 softplus_gate,
1934 bias,
1935 } => {
1936 let mut cfg = self.attn_cfg(position);
1937 cfg.q_norm = q_norm.as_deref();
1938 cfg.k_norm = k_norm.as_deref();
1939 cfg.output_gate = *output_gate;
1940 cfg.softplus_gate = softplus_gate
1941 .as_ref()
1942 .map(|(gate, per_head)| (gate, *per_head));
1943 cfg.bias = bias
1944 .as_ref()
1945 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1946 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1947 }
1948 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1949 unreachable!("MTP block is full attention")
1950 }
1951 };
1952 for (i, &a) in attn.iter().enumerate() {
1953 x[i] += a;
1954 }
1955 inference::rms_norm_into(
1956 &x,
1957 &lw.post_norm,
1958 self.rms_eps,
1959 self.norm_style,
1960 &mut self.ws.p1,
1961 );
1962 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
1963 for (i, &f) in ffn.iter().enumerate() {
1964 x[i] += f;
1965 }
1966
1967 inference::rms_norm_into(
1968 &x,
1969 &m.final_norm,
1970 self.rms_eps,
1971 self.norm_style,
1972 &mut self.ws.n1,
1973 );
1974 let mut lg = self.lm_head_forward(&self.ws.n1);
1975 let draft = sampler::argmax(&lg);
1976 attention::recycle_buf(&mut lg);
1977 draft
1978 }
1979
1980 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1984 let emb1 = self.embed_single(1);
1985 let emb2 = self.embed_single(2);
1986 let pos = self.kv_cache.seq_len();
1987
1988 let t0 = std::time::Instant::now();
1989 for _ in 0..iters {
1990 let _ = self.forward_layers(&emb1, pos, None);
1991 let _ = self.forward_layers(&emb2, pos + 1, None);
1992 for l in &mut self.kv_cache.layers {
1993 l.truncate_last(2);
1994 }
1995 }
1996 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1997
1998 let t1 = std::time::Instant::now();
1999 for _ in 0..iters {
2000 let _ = self.forward_pair(&emb1, &emb2, pos);
2001 for l in &mut self.kv_cache.layers {
2002 l.truncate_last(2);
2003 }
2004 }
2005 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2006 (singles_ms, pair_ms)
2007 }
2008
2009 fn pair_supported(&self) -> bool {
2017 self.g3n.is_none()
2018 && !self
2019 .weights
2020 .layers
2021 .iter()
2022 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
2023 }
2024
2025 fn forward_pair(
2026 &mut self,
2027 emb1: &[f32],
2028 emb2: &[f32],
2029 position: usize,
2030 ) -> (Vec<f32>, Vec<f32>) {
2031 let mut h1 = emb1.to_vec();
2032 let mut h2 = emb2.to_vec();
2033 let (_nkv, _hd, hs, _rd, eps) = (
2034 self.num_kv_heads,
2035 self.head_dim,
2036 self.hidden_size,
2037 self.rotary_dim,
2038 self.rms_eps,
2039 );
2040 let pool = self.pool.clone();
2041
2042 for li in 0..self.num_layers {
2043 let lw = &self.weights.layers[self.phys_layer(li)];
2044 inference::rms_norm_into(
2047 &h1,
2048 &lw.input_norm,
2049 self.rms_eps,
2050 self.norm_style,
2051 &mut self.ws.n1,
2052 );
2053 inference::rms_norm_into(
2054 &h2,
2055 &lw.input_norm,
2056 self.rms_eps,
2057 self.norm_style,
2058 &mut self.ws.n2,
2059 );
2060
2061 let (a1, a2) = match &lw.attn {
2062 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2063 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2064 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2065 AttnKind::Linear(w) => {
2066 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2067 let layer = &mut self.kv_cache.layers[li];
2068 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2069 vmf_phase_pair(
2070 &self.ws.n1,
2071 &self.ws.n2,
2072 w,
2073 &cfg,
2074 state,
2075 scratch,
2076 self.pool.as_deref(),
2077 )
2078 }
2079 AttnKind::LinearGdn(w) => {
2080 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2081 let layer = &mut self.kv_cache.layers[li];
2082 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2083 gdn_pair(
2084 &self.ws.n1,
2085 &self.ws.n2,
2086 w,
2087 &cfg,
2088 state,
2089 scratch,
2090 self.pool.as_deref(),
2091 )
2092 }
2093 AttnKind::ShortConv(w) => {
2094 let cfg = self
2095 .short_conv_cfg
2096 .expect("short-conv layer without short_conv_cfg");
2097 let layer = &mut self.kv_cache.layers[li];
2098 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2099 short_conv_pair(
2100 &self.ws.n1,
2101 &self.ws.n2,
2102 w,
2103 &cfg,
2104 state,
2105 scratch,
2106 self.pool.as_deref(),
2107 )
2108 }
2109 AttnKind::Full {
2110 wq,
2111 wk,
2112 wv,
2113 wo,
2114 q_norm,
2115 k_norm,
2116 output_gate,
2117 softplus_gate,
2118 bias,
2119 } => {
2120 let inv_freq_l = self.layer_inv_freq(li);
2121 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2122 let cfg = QwenAttnCfg {
2123 num_heads: self.layer_num_heads(li),
2124 num_kv_heads: nkv_l,
2125 head_dim: hd_l,
2126 hidden_size: hs,
2127 position,
2128 inv_freq: &inv_freq_l,
2129 rotary_dim: rd_l,
2130 scale: self.attn_scale,
2131 softcap: self.attn_softcap,
2132 window: self.layer_window(li),
2133 v_norm: self.attn_v_norm,
2134 q_norm: q_norm.as_deref(),
2135 k_norm: k_norm.as_deref(),
2136 output_gate: *output_gate,
2137 softplus_gate: softplus_gate
2138 .as_ref()
2139 .map(|(gate, per_head)| (gate, *per_head)),
2140 rope_scale: self.layer_rope_scale(li),
2141 bias: bias
2142 .as_ref()
2143 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2144 rms_eps: eps,
2145 norm_style: self.norm_style,
2146 pool: pool.as_deref(),
2147 };
2148 attention::qwen_attention_pair(
2149 &self.ws.n1,
2150 &self.ws.n2,
2151 wq,
2152 wk,
2153 wv,
2154 wo,
2155 &mut self.kv_cache.layers[li],
2156 &cfg,
2157 )
2158 }
2159 };
2160 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
2161 Some(w) => (
2162 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
2163 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
2164 ),
2165 None => (a1, a2),
2166 };
2167 for i in 0..self.hidden_size {
2168 h1[i] += a1[i];
2169 h2[i] += a2[i];
2170 }
2171 let (mut a1, mut a2) = (a1, a2);
2172 attention::recycle_buf(&mut a1);
2173 attention::recycle_buf(&mut a2);
2174
2175 let lw = &self.weights.layers[self.phys_layer(li)];
2176 inference::rms_norm_into(
2177 &h1,
2178 &lw.post_norm,
2179 self.rms_eps,
2180 self.norm_style,
2181 &mut self.ws.p1,
2182 );
2183 inference::rms_norm_into(
2184 &h2,
2185 &lw.post_norm,
2186 self.rms_eps,
2187 self.norm_style,
2188 &mut self.ws.p2,
2189 );
2190 let (f1, f2) = match &lw.ffn {
2191 FfnKind::DenseMoe(dm) => (
2194 dense_moe_ffn(
2195 dm,
2196 &self.ws.p1,
2197 &h1,
2198 self.rms_eps,
2199 self.norm_style,
2200 self.pool.as_deref(),
2201 ),
2202 dense_moe_ffn(
2203 dm,
2204 &self.ws.p2,
2205 &h2,
2206 self.rms_eps,
2207 self.norm_style,
2208 self.pool.as_deref(),
2209 ),
2210 ),
2211 _ => ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref(), None),
2212 };
2213 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
2214 Some(w) => (
2215 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
2216 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
2217 ),
2218 None => (f1, f2),
2219 };
2220 for i in 0..self.hidden_size {
2221 h1[i] += f1[i];
2222 h2[i] += f2[i];
2223 }
2224 let (mut f1, mut f2) = (f1, f2);
2225 attention::recycle_buf(&mut f1);
2226 attention::recycle_buf(&mut f2);
2227 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
2228 for i in 0..self.hidden_size {
2229 h1[i] *= sc;
2230 h2[i] *= sc;
2231 }
2232 }
2233 if self.is_loop_end(li) && li + 1 < self.num_layers {
2235 h1 = inference::rms_norm(
2236 &h1,
2237 &self.weights.final_norm,
2238 self.rms_eps,
2239 self.norm_style,
2240 );
2241 h2 = inference::rms_norm(
2242 &h2,
2243 &self.weights.final_norm,
2244 self.rms_eps,
2245 self.norm_style,
2246 );
2247 }
2248 }
2249 (h1, h2)
2250 }
2251
2252 fn commit_linear_scratch(&mut self) {
2254 for layer in &mut self.kv_cache.layers {
2255 if !layer.linear_scratch.is_empty() {
2256 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
2257 layer.linear_scratch.clear();
2258 }
2259 }
2260 }
2261
2262 pub fn forward_ids(
2265 &mut self,
2266 ids: &[u32],
2267 task_mask: Option<&TaskMask>,
2268 ) -> Result<Vec<f32>, String> {
2269 if ids.is_empty() {
2270 return Err("empty id sequence".to_string());
2271 }
2272 self.kv_cache.clear();
2273 self.kv_history.clear();
2274 self.o1_begin();
2275 let mut hidden = vec![0.0f32; self.hidden_size];
2276 let mut pos = 0usize;
2277 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
2278 let chunk = prefill_chunk();
2282 let hs = self.hidden_size;
2283 while pos < ids.len() {
2284 let end = (pos + chunk).min(ids.len());
2285 let hb = self.prefill_batch(&ids[pos..end], pos);
2286 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2287 pos = end;
2288 }
2289 }
2290 if task_mask.is_none() {
2291 while pos + 1 < ids.len() {
2292 let e1 = self.embed_single(ids[pos]);
2293 let e2 = self.embed_single(ids[pos + 1]);
2294 let (_, h2) = self.forward_pair(&e1, &e2, pos);
2295 self.commit_linear_scratch();
2296 hidden = h2;
2297 pos += 2;
2298 }
2299 }
2300 while pos < ids.len() {
2301 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2302 pos += 1;
2303 }
2304 self.o1_seal();
2308 let normed = inference::rms_norm(
2309 &hidden,
2310 &self.weights.final_norm,
2311 self.rms_eps,
2312 self.norm_style,
2313 );
2314 Ok(self.lm_head_forward(&normed))
2315 }
2316
2317 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
2324 let (nll, cnt) = self.nll_ids_from(ids, 0);
2325 (nll / cnt.max(1) as f64).exp()
2326 }
2327
2328 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2333 self.kv_cache.clear();
2334 self.kv_history.clear();
2335 FFN_PROBE.with(|p| {
2336 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2337 });
2338 crate::gpu::cpu_scope(|| {
2339 for (pos, &id) in ids.iter().enumerate() {
2340 let emb = self.embed_single(id);
2341 let _ = self.forward_layers(&emb, pos, None);
2342 }
2343 });
2344 self.kv_cache.clear();
2345 self.kv_history.clear();
2346 FFN_PROBE
2347 .with(|p| p.borrow_mut().take())
2348 .unwrap_or_default()
2349 }
2350
2351 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2355 self.kv_cache.clear();
2356 self.kv_history.clear();
2357 let mut nll = 0f64;
2358 let mut cnt = 0usize;
2359 let mut hidden = vec![0f32; self.hidden_size];
2360 for (pos, &id) in ids.iter().enumerate() {
2361 if pos > 0 {
2362 inference::rms_norm_into(
2363 &hidden,
2364 &self.weights.final_norm,
2365 self.rms_eps,
2366 self.norm_style,
2367 &mut self.ws.n1,
2368 );
2369 let mut logits = self.lm_head_forward(&self.ws.n1);
2370 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2371 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2372 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2373 nll -= p.max(1e-300).ln();
2374 cnt += 1;
2375 attention::recycle_buf(&mut logits);
2376 }
2377 let emb = self.embed_single(id);
2378 hidden = self.forward_layers(&emb, pos, Some(mask));
2379 }
2380 self.kv_cache.clear();
2381 self.kv_history.clear();
2382 (nll / cnt.max(1) as f64).exp()
2383 }
2384
2385 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2394 self.kv_cache.clear();
2395 self.kv_history.clear();
2396 let mut nll = 0f64;
2397 let mut cnt = 0usize;
2398 if prefill_batched() && self.g3n.is_none() {
2399 const CHUNK: usize = 128;
2405 const LM_SUB: usize = 32;
2406 let n = ids.len().saturating_sub(1);
2407 let hs = self.hidden_size;
2408 let rows = self.weights.lm_head.rows();
2409 let mut pos = 0usize;
2410 while pos < n {
2411 let end = (pos + CHUNK).min(n);
2412 let bsz = end - pos;
2413 let hb = self.prefill_batch(&ids[pos..end], pos);
2414 let mut k0 = 0usize;
2415 while k0 < bsz {
2416 let k1 = (k0 + LM_SUB).min(bsz);
2417 let sb = k1 - k0;
2418 if pos + k1 <= start {
2421 k0 = k1;
2422 continue;
2423 }
2424 let mut normed = vec![0.0f32; sb * hs];
2425 for k in 0..sb {
2426 let r = inference::rms_norm(
2427 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2428 &self.weights.final_norm,
2429 self.rms_eps,
2430 self.norm_style,
2431 );
2432 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2433 }
2434 let mut logits = vec![0.0f32; sb * rows];
2435 self.weights
2436 .lm_head
2437 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2438 for k in 0..sb {
2439 if pos + k0 + k < start {
2440 continue;
2441 }
2442 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
2443 if let Some(mu) = self.logit_multiplier {
2444 for v in lg.iter_mut() {
2445 *v *= mu;
2446 }
2447 }
2448 if let Some(c) = self.final_softcap {
2452 for v in lg.iter_mut() {
2453 *v = c * (*v / c).tanh();
2454 }
2455 }
2456 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2457 let target = ids[pos + k0 + k + 1] as usize;
2458 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2459 let lse: f64 = lg
2460 .iter()
2461 .map(|&v| ((v - max) as f64).exp())
2462 .sum::<f64>()
2463 .ln()
2464 + max as f64;
2465 nll += lse - lg[target] as f64;
2466 cnt += 1;
2467 if std::env::var("CMF_PPL_TRACE").is_ok() {
2468 let top = lg
2469 .iter()
2470 .enumerate()
2471 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2472 .map(|(i, _)| i)
2473 .unwrap_or(0);
2474 eprintln!(
2475 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
2476 pos + k0 + k, target, lse - lg[target] as f64, top, lg[target], lg[top]
2477 );
2478 }
2479 }
2480 k0 = k1;
2481 }
2482 pos = end;
2483 }
2484 self.kv_cache.clear();
2485 self.kv_history.clear();
2486 return (nll, cnt);
2487 }
2488 for pos in 0..ids.len().saturating_sub(1) {
2489 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2490 if pos < start {
2491 continue;
2492 }
2493 let normed = inference::rms_norm(
2494 &hidden,
2495 &self.weights.final_norm,
2496 self.rms_eps,
2497 self.norm_style,
2498 );
2499 let logits = self.lm_head_forward(&normed);
2503 let target = ids[pos + 1] as usize;
2504 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2505 let lse: f64 = logits
2506 .iter()
2507 .map(|&v| ((v - max) as f64).exp())
2508 .sum::<f64>()
2509 .ln()
2510 + max as f64;
2511 let tok_nll = lse - logits[target] as f64;
2512 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2513 let top = logits
2514 .iter()
2515 .enumerate()
2516 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2517 .map(|(i, _)| i)
2518 .unwrap_or(0);
2519 eprintln!(
2520 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2521 logits[target], logits[top]
2522 );
2523 }
2524 nll += tok_nll;
2525 cnt += 1;
2526 }
2527 self.kv_cache.clear();
2528 self.kv_history.clear();
2529 (nll, cnt)
2530 }
2531
2532 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2548 self.kv_cache.clear();
2549 self.kv_history.clear();
2550 self.o1_begin();
2551 let n = ids.len().saturating_sub(1);
2552 let p = prefill.min(n);
2553 let mut pos = 0usize;
2555 if prefill_batched() {
2556 const CHUNK: usize = 128;
2557 while pos < p {
2558 let end = (pos + CHUNK).min(p);
2559 let _ = self.prefill_batch(&ids[pos..end], pos);
2560 pos = end;
2561 }
2562 } else {
2563 while pos < p {
2564 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2565 pos += 1;
2566 }
2567 }
2568 self.o1_seal();
2569
2570 let mut nll = 0f64;
2571 let mut cnt = 0usize;
2572 for pos in p..n {
2573 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2574 let normed = inference::rms_norm(
2575 &hidden,
2576 &self.weights.final_norm,
2577 self.rms_eps,
2578 self.norm_style,
2579 );
2580 let logits = self.lm_head_forward(&normed);
2584 let target = ids[pos + 1] as usize;
2585 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2586 let lse: f64 = logits
2587 .iter()
2588 .map(|&v| ((v - max) as f64).exp())
2589 .sum::<f64>()
2590 .ln()
2591 + max as f64;
2592 let tok_nll = lse - logits[target] as f64;
2593 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2594 let top = logits
2595 .iter()
2596 .enumerate()
2597 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2598 .map(|(i, _)| i)
2599 .unwrap_or(0);
2600 eprintln!(
2601 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2602 logits[target], logits[top]
2603 );
2604 }
2605 nll += tok_nll;
2606 cnt += 1;
2607 }
2608 self.kv_cache.clear();
2609 self.kv_history.clear();
2610 (nll, cnt)
2611 }
2612
2613 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2621 self.kv_cache.clear();
2622 self.kv_history.clear();
2623 let n = ids.len().saturating_sub(1);
2624 let mut correct = Vec::with_capacity(n);
2625 let mut pmax = Vec::with_capacity(n);
2626 for pos in 0..n {
2627 let emb = self.embed_single(ids[pos]);
2628 let hidden = self.forward_layers(&emb, pos, None);
2629 let normed = inference::rms_norm(
2630 &hidden,
2631 &self.weights.final_norm,
2632 self.rms_eps,
2633 self.norm_style,
2634 );
2635 let logits = self.lm_head_forward(&normed);
2639 let target = ids[pos + 1] as usize;
2640 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2641 for (i, &v) in logits.iter().enumerate() {
2642 if v > mval {
2643 mval = v;
2644 amax = i;
2645 }
2646 }
2647 correct.push(amax == target);
2648 let row: Vec<f32> = temps
2649 .iter()
2650 .map(|&t| {
2651 let tt = t.max(1e-3);
2652 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2653 1.0 / s.max(1e-12) })
2655 .collect();
2656 pmax.push(row);
2657 }
2658 self.kv_cache.clear();
2659 self.kv_history.clear();
2660 (correct, pmax)
2661 }
2662
2663 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2670 let mut router = match self.dyn_router.take() {
2671 Some(r) => r,
2672 None => return (self.ppl_ids(ids), 0),
2673 };
2674 router.reset();
2675 self.dyn_phi_seen = 0;
2676 let _ = self.set_active_skill(None);
2677
2678 self.kv_cache.clear();
2679
2680 self.kv_history.clear();
2681 let mut nll = 0f64;
2682 let mut cnt = 0usize;
2683 for pos in 0..ids.len().saturating_sub(1) {
2684 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2685 let normed = inference::rms_norm(
2686 &hidden,
2687 &self.weights.final_norm,
2688 self.rms_eps,
2689 self.norm_style,
2690 );
2691 let logits = self.lm_head_forward(&normed);
2695 let target = ids[pos + 1] as usize;
2696 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2697 let lse: f64 = logits
2698 .iter()
2699 .map(|&v| ((v - max) as f64).exp())
2700 .sum::<f64>()
2701 .ln()
2702 + max as f64;
2703 let tok_nll = lse - logits[target] as f64;
2704 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
2705 let top = logits
2706 .iter()
2707 .enumerate()
2708 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2709 .map(|(i, _)| i)
2710 .unwrap_or(0);
2711 eprintln!(
2712 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
2713 logits[target], logits[top]
2714 );
2715 }
2716 nll += tok_nll;
2717 cnt += 1;
2718 let phi = self.dyn_phi_ema.clone();
2720 if let Some(new_active) = router.step(&phi, pos) {
2721 let _ = self.set_active_skill(new_active);
2722 }
2723 }
2724 let switches = router.switches.len();
2725 let _ = self.set_active_skill(None);
2726 self.dyn_router = Some(router);
2727 self.kv_cache.clear();
2728 self.kv_history.clear();
2729 ((nll / cnt.max(1) as f64).exp(), switches)
2730 }
2731
2732 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2734 self.kv_cache.clear();
2735 self.kv_history.clear();
2736 let mut acc = vec![0f32; self.hidden_size];
2737 for (pos, &id) in ids.iter().enumerate() {
2738 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2739 for (a, v) in acc.iter_mut().zip(&h) {
2740 *a += v;
2741 }
2742 }
2743 let n = ids.len().max(1) as f32;
2744 for a in acc.iter_mut() {
2745 *a /= n;
2746 }
2747 self.kv_cache.clear();
2748 self.kv_history.clear();
2749 acc
2750 }
2751
2752 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2758 let b = ids.len();
2759 let hs = self.hidden_size;
2760 let mut h: Vec<f32> = vec![0.0; b * hs];
2763 let mut h_ready = false;
2764 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2765 for (bi, &id) in ids.iter().enumerate() {
2766 let e = me.embed_single(id);
2767 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2768 }
2769 };
2770 let (_nkv, _hd, _rd, eps) = (
2771 self.num_kv_heads,
2772 self.head_dim,
2773 self.rotary_dim,
2774 self.rms_eps,
2775 );
2776 let pool = self.pool.clone();
2777 let norm_style = self.norm_style;
2778
2779 #[cfg(target_os = "macos")]
2780 let mut chunk_skip_until = 0usize;
2781 for li in 0..self.num_layers {
2782 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2789 {
2790 if li < chunk_skip_until {
2791 continue;
2792 }
2793 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
2799 fill_h(&mut h, self);
2800 h_ready = true;
2801 }
2802 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2803 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2804 if end > li {
2805 h_ready = true;
2806 chunk_skip_until = end;
2807 if self.is_loop_end(end - 1) && end < self.num_layers {
2810 for bi in 0..b {
2811 let normed = inference::rms_norm(
2812 &h[bi * hs..(bi + 1) * hs],
2813 &self.weights.final_norm,
2814 eps,
2815 norm_style,
2816 );
2817 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2818 }
2819 }
2820 continue;
2821 }
2822 }
2823 if !h_ready {
2824 fill_h(&mut h, self);
2825 h_ready = true;
2826 }
2827 let lw = &self.weights.layers[self.phys_layer(li)];
2828 match &lw.attn {
2830 AttnKind::Kda(w) => {
2831 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
2833 let mut normed = vec![0.0f32; b * hs];
2834 for bi in 0..b {
2835 inference::rms_norm_into(
2836 &h[bi * hs..(bi + 1) * hs],
2837 &lw.input_norm,
2838 eps,
2839 norm_style,
2840 &mut normed[bi * hs..(bi + 1) * hs],
2841 );
2842 }
2843 let attn = crate::linear_core::kda_forward_batch(
2844 &normed,
2845 b,
2846 w,
2847 &cfg,
2848 &mut self.kv_cache.layers[li].linear_state,
2849 pool.as_deref(),
2850 );
2851 for (dst, &a) in h.iter_mut().zip(&attn) {
2852 *dst += a;
2853 }
2854 }
2855 AttnKind::LinearGdn(w) => {
2856 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2858 let mut normed = vec![0.0f32; b * hs];
2859 for bi in 0..b {
2860 let r = inference::rms_norm(
2861 &h[bi * hs..(bi + 1) * hs],
2862 &lw.input_norm,
2863 eps,
2864 norm_style,
2865 );
2866 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2867 }
2868 let attn = crate::linear_core::gdn_forward_batch(
2869 &normed,
2870 b,
2871 w,
2872 &cfg,
2873 &mut self.kv_cache.layers[li].linear_state,
2874 pool.as_deref(),
2875 );
2876 for (dst, &a) in h.iter_mut().zip(&attn) {
2877 *dst += a;
2878 }
2879 }
2880 AttnKind::ShortConv(w) => {
2881 let cfg = self
2884 .short_conv_cfg
2885 .expect("short-conv layer without short_conv_cfg");
2886 let mut normed = vec![0.0f32; b * hs];
2887 for bi in 0..b {
2888 inference::rms_norm_into(
2889 &h[bi * hs..(bi + 1) * hs],
2890 &lw.input_norm,
2891 eps,
2892 norm_style,
2893 &mut normed[bi * hs..(bi + 1) * hs],
2894 );
2895 }
2896 let attn = short_conv_forward_batch(
2897 &normed,
2898 b,
2899 w,
2900 &cfg,
2901 &mut self.kv_cache.layers[li].linear_state,
2902 pool.as_deref(),
2903 );
2904 for (dst, &a) in h.iter_mut().zip(&attn) {
2905 *dst += a;
2906 }
2907 }
2908 AttnKind::Mla(w) => {
2909 let inv_freq_l = self.layer_inv_freq(li);
2912 let rs = self.layer_rope_scale(li);
2913 let mut normed = vec![0.0f32; hs];
2914 for bi in 0..b {
2915 inference::rms_norm_into(
2916 &h[bi * hs..(bi + 1) * hs],
2917 &lw.input_norm,
2918 eps,
2919 norm_style,
2920 &mut normed,
2921 );
2922 let ao = mla_attention(
2923 w,
2924 &normed,
2925 &mut self.kv_cache.layers[li],
2926 start_pos + bi,
2927 &inv_freq_l,
2928 rs,
2929 eps,
2930 pool.as_deref(),
2931 );
2932 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
2933 *dst += a;
2934 }
2935 }
2936 }
2937 AttnKind::Full {
2938 wq,
2939 wk,
2940 wv,
2941 wo,
2942 q_norm,
2943 k_norm,
2944 output_gate,
2945 softplus_gate,
2946 bias,
2947 } => {
2948 let mut normed = vec![0.0f32; b * hs];
2952 for bi in 0..b {
2953 inference::rms_norm_into(
2954 &h[bi * hs..(bi + 1) * hs],
2955 &lw.input_norm,
2956 eps,
2957 norm_style,
2958 &mut normed[bi * hs..(bi + 1) * hs],
2959 );
2960 }
2961 let inv_freq_l = self.layer_inv_freq(li);
2962 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2963 let cfg = QwenAttnCfg {
2964 num_heads: self.layer_num_heads(li),
2965 num_kv_heads: nkv_l,
2966 head_dim: hd_l,
2967 hidden_size: hs,
2968 position: start_pos,
2969 inv_freq: &inv_freq_l,
2970 rotary_dim: rd_l,
2971 scale: self.attn_scale,
2972 softcap: self.attn_softcap,
2973 window: self.layer_window(li),
2974 v_norm: self.attn_v_norm,
2975 q_norm: q_norm.as_deref(),
2976 k_norm: k_norm.as_deref(),
2977 output_gate: *output_gate,
2978 softplus_gate: softplus_gate
2979 .as_ref()
2980 .map(|(gate, per_head)| (gate, *per_head)),
2981 rope_scale: self.layer_rope_scale(li),
2982 bias: bias
2983 .as_ref()
2984 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2985 rms_eps: eps,
2986 norm_style,
2987 pool: pool.as_deref(),
2988 };
2989 let mut attn = attention::qwen_attention_batch(
2990 &normed,
2991 b,
2992 wq,
2993 wk,
2994 wv,
2995 wo,
2996 &mut self.kv_cache.layers[li],
2997 &cfg,
2998 );
2999 if let Some(w) = &lw.attn_out_norm {
3000 for bi in 0..b {
3001 inference::rms_norm_into(
3002 &attn[bi * hs..(bi + 1) * hs],
3003 w,
3004 eps,
3005 norm_style,
3006 &mut normed[bi * hs..(bi + 1) * hs],
3007 );
3008 }
3009 attn.copy_from_slice(&normed);
3010 }
3011 for (dst, &a) in h.iter_mut().zip(&attn) {
3012 *dst += a;
3013 }
3014 }
3015 AttnKind::Linear(w) => {
3016 for bi in 0..b {
3017 let normed = inference::rms_norm(
3018 &h[bi * hs..(bi + 1) * hs],
3019 &lw.input_norm,
3020 eps,
3021 norm_style,
3022 );
3023 vmf_phase_forward(
3024 &normed,
3025 w,
3026 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
3027 &mut self.kv_cache.layers[li].linear_state,
3028 pool.as_deref(),
3029 )
3030 .iter()
3031 .enumerate()
3032 .for_each(|(i, &a)| h[bi * hs + i] += a);
3033 }
3034 }
3035 }
3036
3037 let lw = &self.weights.layers[self.phys_layer(li)];
3039 let mut post = vec![0.0f32; b * hs];
3040 for bi in 0..b {
3041 let r =
3042 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
3043 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3044 }
3045 let mut ffn = match &lw.ffn {
3046 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
3047 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
3048 FfnKind::DenseMoe(dm) => {
3051 let mut out = vec![0.0f32; b * hs];
3052 for bi in 0..b {
3053 let r = dense_moe_ffn(
3054 dm,
3055 &post[bi * hs..(bi + 1) * hs],
3056 &h[bi * hs..(bi + 1) * hs],
3057 eps,
3058 norm_style,
3059 pool.as_deref(),
3060 );
3061 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3062 }
3063 out
3064 }
3065 };
3066 if let Some(w) = &lw.ffn_out_norm {
3067 for bi in 0..b {
3068 inference::rms_norm_into(
3069 &ffn[bi * hs..(bi + 1) * hs],
3070 w,
3071 eps,
3072 norm_style,
3073 &mut post[bi * hs..(bi + 1) * hs],
3074 );
3075 }
3076 ffn.copy_from_slice(&post);
3077 }
3078 for (dst, &f) in h.iter_mut().zip(&ffn) {
3079 *dst += f;
3080 }
3081 if let Some(sc) = lw.layer_scale {
3082 for v in h.iter_mut() {
3083 *v *= sc;
3084 }
3085 }
3086 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
3087 if let Some(t) = tp.parse::<usize>().ok() {
3088 if t >= start_pos && t < start_pos + b {
3089 let bi = t - start_pos;
3090 let row = &h[bi * hs..(bi + 1) * hs];
3091 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
3092 eprintln!(
3093 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
3094 row[0], row[1]
3095 );
3096 }
3097 }
3098 }
3099 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
3103 let row = &h[(b - 1) * hs..b * hs];
3104 let rms =
3105 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
3106 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
3107 eprintln!(
3108 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
3109 match &self.weights.layers[self.phys_layer(li)].attn {
3110 AttnKind::LinearGdn(_) => "gdn",
3111 AttnKind::Linear(_) => "vmf",
3112 AttnKind::ShortConv(_) => "conv",
3113 _ => "attn",
3114 },
3115 match &lw.ffn {
3116 FfnKind::Moe(_) => "moe",
3117 FfnKind::Dense(_) => "dense",
3118 FfnKind::DenseMoe(_) => "dense+moe",
3119 },
3120 );
3121 }
3122 if self.is_loop_end(li) && li + 1 < self.num_layers {
3124 for bi in 0..b {
3125 let normed = inference::rms_norm(
3126 &h[bi * hs..(bi + 1) * hs],
3127 &self.weights.final_norm,
3128 eps,
3129 norm_style,
3130 );
3131 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3132 }
3133 }
3134 if std::env::var("CMF_TRACE_H").is_ok() {
3135 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
3136 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
3137 eprintln!(
3138 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
3139 lw.layer_scale
3140 );
3141 }
3142 }
3143 crate::gpu::set_layer(-1); h
3145 }
3146
3147 fn embed_single(&self, id: u32) -> Vec<f32> {
3149 let mut out = vec![0.0f32; self.hidden_size];
3150 if (id as usize) < self.weights.embed_tokens.rows() {
3151 self.weights.embed_tokens.row_f32(id as usize, &mut out);
3152 }
3153 if self.embed_multiplier != 1.0 {
3154 for v in out.iter_mut() {
3155 *v *= self.embed_multiplier;
3156 }
3157 }
3158 if let Some(b) = &self.g3n {
3161 return b.0.extend_embedding(id, &out, self.pool.as_deref());
3162 }
3163 out
3164 }
3165
3166 #[cfg(target_os = "macos")]
3172 fn chunk_run_gpu(
3173 &mut self,
3174 li0: usize,
3175 h: &mut [f32],
3176 b: usize,
3177 pos0: usize,
3178 embed_ids: Option<&[u32]>,
3179 ) -> usize {
3180 if !crate::gpu::enabled_here()
3184 || std::env::var("CMF_GPU_CHUNK")
3185 .map(|v| v == "0")
3186 .unwrap_or(false)
3187 || b < 32
3188 || self.swa.is_some()
3189 || self.global_attn.is_some()
3190 || self.attn_v_norm
3191 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
3192 {
3193 return li0;
3194 }
3195 let Some(model) = self.model.clone() else {
3196 return li0;
3197 };
3198 let inv_freq = self.inv_freq.clone();
3199 let (nh, nkv, hd, hs) = (
3200 self.num_heads,
3201 self.num_kv_heads,
3202 self.head_dim,
3203 self.hidden_size,
3204 );
3205 let loop_end = if self.loop_final_norm {
3209 ((li0 / self.physical_layers) + 1) * self.physical_layers
3210 } else {
3211 self.num_layers
3212 };
3213 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
3214 let mut stored_at: Vec<usize> = Vec::new();
3215 for li in li0..self.num_layers.min(loop_end) {
3216 let lw = &self.weights.layers[self.phys_layer(li)];
3217 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
3218 break;
3219 }
3220 let AttnKind::Full {
3221 wq,
3222 wk,
3223 wv,
3224 wo,
3225 q_norm,
3226 k_norm,
3227 output_gate: false,
3228 softplus_gate: None,
3229 bias,
3230 } = &lw.attn
3231 else {
3232 break;
3233 };
3234 let FfnKind::Dense(d) = &lw.ffn else { break };
3235 if d.act != Act::Silu {
3236 break;
3237 }
3238 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
3243 t.q8_row_parts()
3244 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3245 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3246 }
3247 let parts = (
3248 cw(wq),
3249 cw(wk),
3250 cw(wv),
3251 cw(wo),
3252 cw(&d.gate_proj),
3253 cw(&d.up_proj),
3254 cw(&d.down_proj),
3255 );
3256 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
3257 else {
3258 break;
3259 };
3260 let layer = &self.kv_cache.layers[li];
3261 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
3262 break;
3263 }
3264 stored_at.push(layer.head_len(0));
3265 layers.push(crate::gpu_metal::ChunkLayer {
3266 model: &model,
3267 kv_id: self.graph_kv_id,
3268 layer: li,
3269 wq: pq,
3270 wk: pk,
3271 wv: pv,
3272 wo: po,
3273 gate: pg,
3274 up: pu,
3275 down: pd,
3276 input_norm: &lw.input_norm,
3277 post_norm: &lw.post_norm,
3278 bias: bias
3279 .as_ref()
3280 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
3281 q_norm: q_norm.as_deref(),
3282 k_norm: k_norm.as_deref(),
3283 inv_freq: &inv_freq,
3284 rd: self.rotary_dim,
3285 nh,
3286 nkv,
3287 hd,
3288 hs,
3289 inter: d.gate_proj.rows(),
3290 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
3291 eps: self.rms_eps as f32,
3292 });
3293 }
3294 if layers.is_empty() {
3295 return li0;
3296 }
3297 let row = nkv * hd;
3298 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
3299 .iter()
3300 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
3301 .collect();
3302 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
3303 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
3304 let li = layers[i].layer;
3305 let layer = &self.kv_cache.layers[li];
3306 io.push(crate::gpu_metal::ChunkIo {
3307 cpu_stored: stored_at[i],
3308 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
3309 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
3310 out_k: ok,
3311 out_v: ov,
3312 imp: oi,
3313 });
3314 }
3315 let n_run = layers.len();
3316 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
3317 let ep = embed_ids.and_then(|ids| {
3320 self.weights
3321 .embed_tokens
3322 .q8_row_parts()
3323 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
3324 idx,
3325 rows,
3326 row_scale: rs,
3327 ids,
3328 mult: self.embed_multiplier,
3329 })
3330 });
3331 if embed_ids.is_some() && ep.is_none() {
3332 return li0;
3333 }
3334 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
3335 return li0;
3336 }
3337 drop(io);
3338 drop(layers);
3339 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
3342 let li = li0 + i;
3343 let layer = &mut self.kv_cache.layers[li];
3344 for bi in 0..b {
3345 layer.append(
3346 &ok[bi * row..(bi + 1) * row],
3347 &ov[bi * row..(bi + 1) * row],
3348 &[],
3349 );
3350 }
3351 layer.accumulate_imp(oi);
3352 }
3353 last
3354 }
3355
3356 fn layer_is_local(&self, li: usize) -> bool {
3359 if let Some(layers) = &self.sliding_layers {
3360 return layers.get(li).copied().unwrap_or(false);
3361 }
3362 match self.swa {
3363 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
3364 None => false,
3365 }
3366 }
3367
3368 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
3371 if self.layer_is_local(li) {
3372 if let Some(f) = &self.inv_freq_local {
3373 return f.clone();
3374 }
3375 } else if let Some(f) = &self.inv_freq_global {
3376 return f.clone();
3377 }
3378 self.inv_freq.clone()
3379 }
3380
3381 fn layer_window(&self, li: usize) -> Option<usize> {
3383 self.swa
3384 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
3385 }
3386
3387 fn layer_num_heads(&self, li: usize) -> usize {
3388 self.attention_heads_per_layer
3389 .as_ref()
3390 .and_then(|v| v.get(li).copied())
3391 .unwrap_or(self.num_heads)
3392 }
3393
3394 fn layer_rope_scale(&self, li: usize) -> f32 {
3395 if self.layer_is_local(li) {
3396 self.rope_scale_local
3397 } else {
3398 self.rope_scale
3399 }
3400 }
3401
3402 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
3405 if !self.layer_is_local(li) {
3406 if let Some((ghd, gkv)) = self.global_attn {
3407 return (gkv, ghd, ghd);
3408 }
3409 }
3410 (
3411 self.num_kv_heads,
3412 self.head_dim,
3413 if self.layer_is_local(li) {
3414 self.rotary_dim_local.unwrap_or(self.rotary_dim)
3415 } else {
3416 self.rotary_dim
3417 },
3418 )
3419 }
3420
3421 fn forward_layers(
3423 &mut self,
3424 hidden: &[f32],
3425 position: usize,
3426 task_mask: Option<&TaskMask>,
3427 ) -> Vec<f32> {
3428 self.forward_layers_upto(hidden, position, task_mask, None)
3429 }
3430
3431 fn try_token_graph_wgpu(
3435 &self,
3436 hidden: &[f32],
3437 position: usize,
3438 logits_out: &mut Vec<f32>,
3439 ) -> Option<Vec<f32>> {
3440 self.try_token_graph_wgpu_steps(hidden, position, logits_out, 1, None)
3441 }
3442
3443 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
3447 if self.o1_active() || self.attn_softcap > 0.0 {
3448 return None;
3449 }
3450 let graph_on = match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
3451 Some("0") => return None,
3452 Some(_) => true,
3453 None => crate::gpu::wgpu_graph_default(),
3454 };
3455 if !graph_on {
3456 return None;
3457 }
3458 let emb = self.embed_single(t_next);
3459 let mut lg = Vec::new();
3460 let mut ids = Vec::new();
3461 self.try_token_graph_wgpu_steps(&emb, position, &mut lg, k, Some(&mut ids))?;
3462 (ids.len() == k).then_some(ids)
3463 }
3464
3465 fn try_token_graph_wgpu_steps(
3469 &self,
3470 hidden: &[f32],
3471 position: usize,
3472 logits_out: &mut Vec<f32>,
3473 steps: usize,
3474 ids_out: Option<&mut Vec<u32>>,
3475 ) -> Option<Vec<f32>> {
3476 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
3479 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
3480 return None;
3484 }
3485 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (0..self.num_layers)
3490 .map(|li| {
3491 if !o1_gpu {
3492 return None;
3493 }
3494 self.kv_cache.layers[self.phys_layer(li)].o1_views()
3495 })
3496 .collect();
3497 if self.o1_active() && o1_gpu {
3498 let want: usize = (0..self.num_layers)
3501 .filter(|li| {
3502 !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None)
3503 })
3504 .count();
3505 let have = o1_views.iter().filter(|v| v.is_some()).count();
3506 if want == 0 || have != want {
3507 return None;
3508 }
3509 }
3510 let nh = self.num_heads;
3511 let (nkv, hd, rd) = self.layer_geom(0);
3512 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3513 let mut layers = Vec::with_capacity(self.num_layers);
3514 let mut model = None;
3515 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
3516 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3517 if let Some((_, i, kind, rs)) = t.graph_weight() {
3518 return Some(crate::gpu::GraphW {
3519 idx: i,
3520 kind,
3521 row_scale: rs,
3522 data: &[],
3523 });
3524 }
3525 t.as_f32().map(|d| crate::gpu::GraphW {
3527 idx: 0,
3528 kind: 4,
3529 row_scale: &[],
3530 data: d,
3531 })
3532 }
3533 for li in 0..self.num_layers {
3534 let lw = &self.weights.layers[self.phys_layer(li)];
3535 if dbg {
3536 let ak = match &lw.attn {
3537 AttnKind::Mla(_) => "Mla".into(),
3538 AttnKind::Full {
3539 output_gate, bias, ..
3540 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
3541 AttnKind::LinearGdn(_) => "LinearGdn".into(),
3542 AttnKind::Kda(_) => "Kda".into(),
3543 AttnKind::Linear(_) => "Linear".into(),
3544 AttnKind::ShortConv(_) => "ShortConv".into(),
3545 };
3546 let fk = match &lw.ffn {
3547 FfnKind::Dense(_) => "Dense",
3548 FfnKind::Moe(_) => "Moe",
3549 FfnKind::DenseMoe(_) => "DenseMoe",
3550 };
3551 eprintln!("graph L{li}: attn={ak} ffn={fk}");
3552 }
3553 let gffn = match &lw.ffn {
3554 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3556 gate: gw(&d.gate_proj)?,
3557 up: gw(&d.up_proj)?,
3558 down: gw(&d.down_proj)?,
3559 },
3560 FfnKind::Moe(m) => {
3561 if m.router_sigmoid
3566 || m.expert_bias.is_some()
3567 || m.route_tau.is_some()
3568 || m.mask.is_some()
3569 {
3570 return None;
3571 }
3572 let (se, sg) = m.shared.as_ref()?;
3573 let sgate = gw(sg.as_ref()?)?;
3574 let router = gw(&m.router)?;
3575 let inter = m.experts.first()?.gate_proj.rows();
3576 let mut experts = Vec::with_capacity(m.experts.len() + 1);
3577 let mut q4tp: Option<bool> = None;
3580 let mut gu_q2: Option<bool> = None;
3583 for e in m.experts.iter().chain(std::iter::once(se)) {
3584 if !matches!(e.act, Act::Silu)
3585 || e.gate_proj.rows() != inter
3586 || e.up_proj.rows() != inter
3587 {
3588 return None;
3589 }
3590 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
3591 Some((mm, gi)) => (
3592 mm,
3593 gi,
3594 e.up_proj.mapped_q4t()?.1,
3595 e.down_proj.mapped_q4t()?.1,
3596 false,
3597 false,
3598 ),
3599 None => match e.gate_proj.mapped_q2tp() {
3600 Some((mm, gi)) => (
3601 mm,
3602 gi,
3603 e.up_proj.mapped_q2tp()?.1,
3604 e.down_proj.mapped_q4tp()?.1,
3605 true,
3606 true,
3607 ),
3608 None => {
3609 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
3610 (
3611 mm,
3612 gi,
3613 e.up_proj.mapped_q4tp()?.1,
3614 e.down_proj.mapped_q4tp()?.1,
3615 true,
3616 false,
3617 )
3618 }
3619 },
3620 };
3621 if *q4tp.get_or_insert(is_p) != is_p
3622 || *gu_q2.get_or_insert(is_q2) != is_q2
3623 {
3624 tracing::warn!(
3630 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
3631 );
3632 return None;
3633 }
3634 model.get_or_insert_with(|| mm.clone());
3635 experts.push((gi, ui, di));
3636 }
3637 crate::gpu::GraphFfn::Moe {
3638 router,
3639 shared_gate: sgate,
3640 experts,
3641 n_exp: m.experts.len(),
3642 top_k: std::env::var("CMF_TOPK_PROBE")
3648 .ok()
3649 .and_then(|v| v.parse::<usize>().ok())
3650 .filter(|k| *k > 0 && *k <= m.top_k)
3651 .unwrap_or(m.top_k),
3652 inter,
3653 norm_topk: m.norm_topk_prob,
3654 q4tp: q4tp?,
3655 gu_q2: gu_q2.unwrap_or(false),
3656 }
3657 }
3658 };
3659 let attn = match &lw.attn {
3660 AttnKind::Full {
3661 wq,
3662 wk,
3663 wv,
3664 wo,
3665 q_norm,
3666 k_norm,
3667 output_gate,
3668 softplus_gate,
3669 bias,
3670 } => {
3671 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3672 return None;
3673 }
3674 let (m, _, _, _) = wq.graph_weight()?;
3675 model = Some(m.clone());
3676 crate::gpu::GraphAttn::Full {
3677 wq: gw(wq)?,
3678 wk: gw(wk)?,
3679 wv: gw(wv)?,
3680 wo: gw(wo)?,
3681 q_norm: q_norm.as_deref(),
3682 k_norm: k_norm.as_deref(),
3683 bias: bias
3684 .as_ref()
3685 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3686 output_gate: *output_gate,
3687 cpu_k: self.kv_cache.layers[li].k_heads(),
3688 cpu_v: self.kv_cache.layers[li].v_heads(),
3689 }
3690 }
3691 AttnKind::LinearGdn(w) => {
3692 let cfg = self.gdn_cfg?;
3693 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3694 model = Some(m.clone());
3695 crate::gpu::GraphAttn::Gdn {
3696 qkv: gw(&w.in_proj_qkv)?,
3697 z: gw(&w.in_proj_z)?,
3698 a: gw(&w.in_proj_a)?,
3699 b: gw(&w.in_proj_b)?,
3700 out: gw(&w.out_proj)?,
3701 conv1d: &w.conv1d,
3702 a_log: &w.a_log,
3703 dt_bias: &w.dt_bias,
3704 norm: &w.norm,
3705 nv: cfg.num_v_heads,
3706 nk: cfg.num_k_heads,
3707 dk: cfg.key_head_dim,
3708 dv: cfg.value_head_dim,
3709 kk: cfg.conv_kernel,
3710 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
3711 }
3712 }
3713 _ => return None,
3714 };
3715 layers.push(crate::gpu::GraphLayer {
3716 input_norm: &lw.input_norm,
3717 attn,
3718 post_norm: &lw.post_norm,
3719 ffn: gffn,
3720 });
3721 }
3722 let model = model?;
3723 let lm_gw = if self.graph_want_logits
3729 && std::env::var("CMF_GPU_LMHEAD")
3730 .map(|v| v != "0")
3731 .unwrap_or(true)
3732 {
3733 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
3734 (
3735 crate::gpu::GraphW {
3736 idx: i,
3737 kind,
3738 row_scale: rs,
3739 data: &[],
3740 },
3741 self.weights.lm_head.rows(),
3742 )
3743 })
3744 } else {
3745 None
3746 };
3747 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
3748 let emb_gw = if steps > 1 {
3750 self.weights.embed_tokens.graph_weight().map(|(_, i, kind, rs)| {
3751 (
3752 crate::gpu::GraphW {
3753 idx: i,
3754 kind,
3755 row_scale: rs,
3756 data: &[],
3757 },
3758 self.weights.embed_tokens.rows(),
3759 self.embed_multiplier as f32,
3760 )
3761 })
3762 } else {
3763 None
3764 };
3765
3766 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
3769 (0..self.num_layers - 1)
3770 .filter(|&li| (li + 1) % self.physical_layers == 0)
3771 .collect()
3772 } else {
3773 Vec::new()
3774 };
3775 let mut h = hidden.to_vec();
3776 crate::gpu::forward_token_graph(
3777 &model,
3778 self.graph_kv_id,
3779 &layers,
3780 &o1_views,
3781 self.o1_epoch,
3782 &self.inv_freq,
3783 &mut h,
3784 nh,
3785 nkv,
3786 hd,
3787 rd,
3788 self.hidden_size,
3789 self.intermediate_size,
3790 position,
3791 self.kv_cache.max_seq_len,
3792 gemma,
3793 self.rms_eps as f32,
3794 lm,
3795 &self.weights.final_norm,
3796 logits_out,
3797 &loop_norm_at,
3798 steps,
3799 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
3800 ids_out,
3801 )
3802 .then_some(h)
3803 }
3804
3805 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
3810 if self.attn_softcap > 0.0 {
3811 return false; }
3813 if self.o1_active() {
3814 return false;
3815 }
3816 let nh = self.num_heads;
3817 let (nkv, hd, rd) = self.layer_geom(0);
3818 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3819 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3820 if let Some((_, i, kind, rs)) = t.graph_weight() {
3821 return Some(crate::gpu::GraphW {
3822 idx: i,
3823 kind,
3824 row_scale: rs,
3825 data: &[],
3826 });
3827 }
3828 t.as_f32().map(|d| crate::gpu::GraphW {
3829 idx: 0,
3830 kind: 4,
3831 row_scale: &[],
3832 data: d,
3833 })
3834 }
3835 let built: Option<(
3836 Vec<crate::gpu::GraphLayer<'_>>,
3837 std::sync::Arc<cortiq_core::CmfModel>,
3838 )> = (|| {
3839 let mut layers = Vec::with_capacity(self.num_layers);
3840 let mut model = None;
3841 for li in 0..self.num_layers {
3842 let lw = &self.weights.layers[self.phys_layer(li)];
3843 let gffn = match &lw.ffn {
3850 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3851 gate: gw(&d.gate_proj)?,
3852 up: gw(&d.up_proj)?,
3853 down: gw(&d.down_proj)?,
3854 },
3855 FfnKind::Moe(m) => {
3856 if m.router_sigmoid
3857 || m.expert_bias.is_some()
3858 || m.route_tau.is_some()
3859 || m.mask.is_some()
3860 {
3861 return None;
3862 }
3863 let (se, sg) = m.shared.as_ref()?;
3864 let sgate = gw(sg.as_ref()?)?;
3865 let router = gw(&m.router)?;
3866 let inter = m.experts.first()?.gate_proj.rows();
3867 let mut experts = Vec::with_capacity(m.experts.len() + 1);
3868 let mut q4tp: Option<bool> = None;
3869 for e in m.experts.iter().chain(std::iter::once(se)) {
3870 if !matches!(e.act, Act::Silu)
3871 || e.gate_proj.rows() != inter
3872 || e.up_proj.rows() != inter
3873 {
3874 return None;
3875 }
3876 let (mm, gi, ui, di, is_p) = match e.gate_proj.mapped_q4t() {
3877 Some((mm, gi)) => (
3878 mm,
3879 gi,
3880 e.up_proj.mapped_q4t()?.1,
3881 e.down_proj.mapped_q4t()?.1,
3882 false,
3883 ),
3884 None => {
3885 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
3886 (
3887 mm,
3888 gi,
3889 e.up_proj.mapped_q4tp()?.1,
3890 e.down_proj.mapped_q4tp()?.1,
3891 true,
3892 )
3893 }
3894 };
3895 if *q4tp.get_or_insert(is_p) != is_p {
3896 return None;
3897 }
3898 model.get_or_insert_with(|| mm.clone());
3899 experts.push((gi, ui, di));
3900 }
3901 crate::gpu::GraphFfn::Moe {
3902 router,
3903 shared_gate: sgate,
3904 experts,
3905 n_exp: m.experts.len(),
3906 top_k: m.top_k,
3907 inter,
3908 norm_topk: m.norm_topk_prob,
3909 q4tp: q4tp?,
3910 gu_q2: false,
3913 }
3914 }
3915 _ => return None,
3916 };
3917 let attn = match &lw.attn {
3918 AttnKind::Full {
3919 wq,
3920 wk,
3921 wv,
3922 wo,
3923 q_norm,
3924 k_norm,
3925 output_gate,
3926 softplus_gate,
3927 bias,
3928 } => {
3929 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3930 return None;
3931 }
3932 let (m, _, _, _) = wq.graph_weight()?;
3933 model = Some(m.clone());
3934 crate::gpu::GraphAttn::Full {
3935 wq: gw(wq)?,
3936 wk: gw(wk)?,
3937 wv: gw(wv)?,
3938 wo: gw(wo)?,
3939 q_norm: q_norm.as_deref(),
3940 k_norm: k_norm.as_deref(),
3941 bias: bias
3942 .as_ref()
3943 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3944 output_gate: *output_gate,
3945 cpu_k: self.kv_cache.layers[li].k_heads(),
3946 cpu_v: self.kv_cache.layers[li].v_heads(),
3947 }
3948 }
3949 AttnKind::LinearGdn(w) => {
3950 let cfg = self.gdn_cfg?;
3951 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3952 model = Some(m.clone());
3953 crate::gpu::GraphAttn::Gdn {
3954 qkv: gw(&w.in_proj_qkv)?,
3955 z: gw(&w.in_proj_z)?,
3956 a: gw(&w.in_proj_a)?,
3957 b: gw(&w.in_proj_b)?,
3958 out: gw(&w.out_proj)?,
3959 conv1d: &w.conv1d,
3960 a_log: &w.a_log,
3961 dt_bias: &w.dt_bias,
3962 norm: &w.norm,
3963 nv: cfg.num_v_heads,
3964 nk: cfg.num_k_heads,
3965 dk: cfg.key_head_dim,
3966 dv: cfg.value_head_dim,
3967 kk: cfg.conv_kernel,
3968 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
3969 }
3970 }
3971 _ => return None,
3972 };
3973 layers.push(crate::gpu::GraphLayer {
3974 input_norm: &lw.input_norm,
3975 attn,
3976 post_norm: &lw.post_norm,
3977 ffn: gffn,
3978 });
3979 }
3980 Some((layers, model?))
3981 })();
3982 let Some((layers, model)) = built else {
3983 {
3984 use std::sync::atomic::{AtomicBool, Ordering};
3985 static SAID: AtomicBool = AtomicBool::new(false);
3986 if !SAID.swap(true, Ordering::Relaxed) {
3987 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
3988 }
3989 }
3990 return false;
3991 };
3992 crate::gpu::forward_batch_graph(
3993 &model,
3994 self.graph_kv_id,
3995 &layers,
3996 &self.inv_freq,
3997 hiddens,
3998 nh,
3999 nkv,
4000 hd,
4001 rd,
4002 self.hidden_size,
4003 self.intermediate_size,
4004 positions,
4005 self.kv_cache.max_seq_len,
4006 gemma,
4007 self.rms_eps as f32,
4008 k,
4009 )
4010 }
4011
4012 fn forward_layers_upto(
4014 &mut self,
4015 hidden: &[f32],
4016 position: usize,
4017 task_mask: Option<&TaskMask>,
4018 upto: Option<usize>,
4019 ) -> Vec<f32> {
4020 if let Some(b) = &self.g3n {
4023 let _ = (task_mask, upto);
4024 return crate::g3n::g3n_forward(
4025 &b.0,
4026 &b.1,
4027 hidden,
4028 position,
4029 &mut self.kv_cache.layers,
4030 self.num_heads,
4031 self.num_kv_heads,
4032 self.head_dim,
4033 self.pool.as_deref(),
4034 );
4035 }
4036 let mut h = hidden.to_vec();
4037 let (nh, _nkv, _hd, hs, _rd, eps) = (
4040 self.num_heads,
4041 self.num_kv_heads,
4042 self.head_dim,
4043 self.hidden_size,
4044 self.rotary_dim,
4045 self.rms_eps,
4046 );
4047 let pool = self.pool.clone();
4048 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
4060 let graph_on = match graph_env.as_deref() {
4061 Some("0") => false,
4062 Some(_) => true,
4063 None => crate::gpu::wgpu_graph_default(),
4069 };
4070 let graph_trusted =
4071 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
4072 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
4073 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
4074 let t_graph = std::time::Instant::now();
4075 let mut lg = Vec::new();
4076 let built = self.try_token_graph_wgpu(hidden, position, &mut lg);
4077 graph_note(built.is_some());
4078 if let Some(hh) = built {
4079 let dur = t_graph.elapsed();
4080 if std::env::var("CMF_GRAPH_PROF").is_ok() {
4081 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
4082 }
4083 if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
4084 if !graph_trusted {
4085 crate::gpu::graph_race_record(true, dur);
4086 }
4087 if !lg.is_empty() {
4088 lg.resize(self.vocab_size, 0.0);
4091 if let Some(c) = self.final_softcap {
4092 for l in lg.iter_mut() {
4093 *l = c * (*l / c).tanh();
4094 }
4095 }
4096 self.graph_logits = Some(lg);
4097 }
4098 return hh;
4099 }
4100 }
4106 }
4107 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
4108
4109 #[cfg(target_os = "macos")]
4110 let mut gpu_skip_until = 0usize;
4111 for li in 0..self.num_layers {
4112 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
4114 if li > u {
4115 break;
4116 }
4117 }
4118 if let Some(mask) = task_mask {
4119 if !mask.layer_alive(li) {
4120 continue; }
4122 }
4123 #[cfg(target_os = "macos")]
4127 {
4128 if li < gpu_skip_until {
4129 continue;
4130 }
4131 if task_mask.is_none() {
4132 let end = self.q1_graph_gpu(li, upto, position, &mut h);
4133 if end > li {
4134 gpu_skip_until = end;
4135 if self.is_loop_end(end - 1) && end < self.num_layers {
4138 h = inference::rms_norm(
4139 &h,
4140 &self.weights.final_norm,
4141 self.rms_eps,
4142 self.norm_style,
4143 );
4144 }
4145 continue;
4146 }
4147 }
4148 }
4149
4150 let lw = &self.weights.layers[self.phys_layer(li)];
4151 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
4152 if tp.parse::<usize>().ok() == Some(position) {
4153 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
4154 eprintln!(
4155 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
4156 h[0], h[1]
4157 );
4158 }
4159 }
4160 inference::rms_norm_into(
4163 &h,
4164 &lw.input_norm,
4165 self.rms_eps,
4166 self.norm_style,
4167 &mut self.ws.n1,
4168 );
4169
4170 let attn_out = match &lw.attn {
4171 AttnKind::Mla(w) => {
4172 let inv_freq_l = self.layer_inv_freq(li);
4173 let rs = self.layer_rope_scale(li);
4174 let eps = self.rms_eps;
4175 let pool = self.pool.clone();
4176 mla_attention(
4177 w,
4178 &self.ws.n1,
4179 &mut self.kv_cache.layers[li],
4180 position,
4181 &inv_freq_l,
4182 rs,
4183 eps,
4184 pool.as_deref(),
4185 )
4186 }
4187 AttnKind::Linear(w) => {
4188 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
4189 vmf_phase_forward(
4190 &self.ws.n1,
4191 w,
4192 &cfg,
4193 &mut self.kv_cache.layers[li].linear_state,
4194 self.pool.as_deref(),
4195 )
4196 }
4197 AttnKind::Kda(w) => {
4198 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
4199 crate::linear_core::kda_forward(
4200 &self.ws.n1,
4201 w,
4202 &cfg,
4203 &mut self.kv_cache.layers[li].linear_state,
4204 self.pool.as_deref(),
4205 )
4206 }
4207 AttnKind::LinearGdn(w) => {
4208 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
4209 gdn_forward(
4210 &self.ws.n1,
4211 w,
4212 &cfg,
4213 &mut self.kv_cache.layers[li].linear_state,
4214 self.pool.as_deref(),
4215 )
4216 }
4217 AttnKind::ShortConv(w) => {
4218 let cfg = self
4219 .short_conv_cfg
4220 .expect("short-conv layer without short_conv_cfg");
4221 short_conv_forward(
4222 &self.ws.n1,
4223 w,
4224 &cfg,
4225 &mut self.kv_cache.layers[li].linear_state,
4226 self.pool.as_deref(),
4227 )
4228 }
4229 AttnKind::Full {
4230 wq,
4231 wk,
4232 wv,
4233 wo,
4234 q_norm,
4235 k_norm,
4236 output_gate,
4237 softplus_gate,
4238 bias,
4239 } if self.kv_cache.layers[li].o1_sealed() => {
4240 let inv_freq_l = self.layer_inv_freq(li);
4243 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4244 let cfg = QwenAttnCfg {
4245 num_heads: self.layer_num_heads(li),
4246 num_kv_heads: nkv_l,
4247 head_dim: hd_l,
4248 hidden_size: hs,
4249 position,
4250 inv_freq: &inv_freq_l,
4251 rotary_dim: rd_l,
4252 scale: self.attn_scale,
4253 softcap: self.attn_softcap,
4254 window: None,
4255 v_norm: self.attn_v_norm,
4256 q_norm: q_norm.as_deref(),
4257 k_norm: k_norm.as_deref(),
4258 output_gate: *output_gate,
4259 softplus_gate: softplus_gate
4260 .as_ref()
4261 .map(|(gate, per_head)| (gate, *per_head)),
4262 rope_scale: self.layer_rope_scale(li),
4263 bias: bias
4264 .as_ref()
4265 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4266 rms_eps: eps,
4267 norm_style: self.norm_style,
4268 pool: pool.as_deref(),
4269 };
4270 attention::qwen_attention_nystrom(
4271 &self.ws.n1,
4272 wq,
4273 wk,
4274 wv,
4275 wo,
4276 &mut self.kv_cache.layers[li],
4277 &cfg,
4278 )
4279 }
4280 AttnKind::Full {
4281 wq,
4282 wk,
4283 wv,
4284 wo,
4285 q_norm,
4286 k_norm,
4287 output_gate,
4288 softplus_gate,
4289 bias,
4290 } => 'attn: {
4291 if graph_on
4294 && !*output_gate
4295 && softplus_gate.is_none()
4296 && self.attention_heads_per_layer.is_none()
4297 && bias.is_none()
4298 && task_mask.is_none()
4299 {
4300 let inv_freq_l = self.layer_inv_freq(li);
4301 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4302 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4303 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
4304 wq.mapped_q1(),
4305 wk.mapped_q1(),
4306 wv.mapped_q1(),
4307 wo.mapped_q1(),
4308 ) {
4309 let gm = gm.clone();
4310 let mut out = vec![0f32; hs];
4311 let cache = &self.kv_cache.layers[li];
4312 if crate::gpu::attn_dropin(
4313 &gm,
4314 self.graph_kv_id,
4315 li,
4316 &self.ws.n1,
4317 qi,
4318 ki,
4319 vi,
4320 oi,
4321 q_norm.as_deref(),
4322 k_norm.as_deref(),
4323 &inv_freq_l,
4324 nh,
4325 nkv_l,
4326 hd_l,
4327 rd_l,
4328 hs,
4329 position,
4330 self.kv_cache.max_seq_len,
4331 gemma,
4332 eps as f32,
4333 cache.k_heads(),
4334 cache.v_heads(),
4335 &mut out,
4336 ) {
4337 break 'attn out;
4338 }
4339 }
4340 }
4341 let masked = task_mask
4342 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
4343 .unwrap_or(false);
4344 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
4345 match (masked, f32_view) {
4346 (true, (Some(q), Some(k), Some(v), Some(o))) => {
4349 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
4350 attention::multi_head_attention(
4351 &self.ws.n1,
4352 q,
4353 k,
4354 v,
4355 o,
4356 &mut self.kv_cache.layers[li],
4357 self.num_heads,
4358 self.num_kv_heads,
4359 self.head_dim,
4360 self.hidden_size,
4361 position,
4362 &active_heads,
4363 &self.inv_freq,
4364 )
4365 }
4366 (masked, _) => {
4367 if masked {
4368 tracing::warn!(
4369 "layer {li}: head mask on quantized weights not \
4370 supported yet — executing dense"
4371 );
4372 }
4373 let inv_freq_l = self.layer_inv_freq(li);
4374 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
4375 let cfg = QwenAttnCfg {
4376 num_heads: self.layer_num_heads(li),
4377 num_kv_heads: nkv_l,
4378 head_dim: hd_l,
4379 hidden_size: hs,
4380 position,
4381 inv_freq: &inv_freq_l,
4382 rotary_dim: rd_l,
4383 scale: self.attn_scale,
4384 softcap: self.attn_softcap,
4385 window: self.layer_window(li),
4386 v_norm: self.attn_v_norm,
4387 q_norm: q_norm.as_deref(),
4388 k_norm: k_norm.as_deref(),
4389 output_gate: *output_gate,
4390 softplus_gate: softplus_gate
4391 .as_ref()
4392 .map(|(gate, per_head)| (gate, *per_head)),
4393 rope_scale: self.layer_rope_scale(li),
4394 bias: bias
4395 .as_ref()
4396 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4397 rms_eps: eps,
4398 norm_style: self.norm_style,
4399 pool: pool.as_deref(),
4400 };
4401 attention::qwen_attention(
4402 &self.ws.n1,
4403 wq,
4404 wk,
4405 wv,
4406 wo,
4407 &mut self.kv_cache.layers[li],
4408 &cfg,
4409 )
4410 }
4411 }
4412 }
4413 };
4414 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
4417 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
4418 None => attn_out,
4419 };
4420 let lw = &self.weights.layers[self.phys_layer(li)];
4421 inference::add_rmsnorm_fused_into(
4422 &mut h,
4423 &attn_out,
4424 &lw.post_norm,
4425 self.rms_eps,
4426 self.norm_style,
4427 &mut self.ws.p1,
4428 );
4429 let mut attn_out = attn_out;
4430 attention::recycle_buf(&mut attn_out);
4431 let post_normed = &self.ws.p1;
4432
4433 let ffn_masked = task_mask
4434 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
4435 .unwrap_or(false);
4436 let f32_ffn = match &lw.ffn {
4439 FfnKind::Dense(d) => (
4440 d.gate_proj.as_f32(),
4441 d.up_proj.as_f32(),
4442 d.down_proj.as_f32(),
4443 ),
4444 FfnKind::Moe(_) | FfnKind::DenseMoe(_) => (None, None, None),
4445 };
4446 let ffn_out = match (ffn_masked, f32_ffn) {
4447 (true, (Some(g), Some(u), Some(d))) => {
4448 let active = task_mask.unwrap().ffn_active_indices(li);
4449 inference::sparse_ffn_forward(
4450 post_normed,
4451 g,
4452 u,
4453 d,
4454 self.hidden_size,
4455 self.intermediate_size,
4456 &active,
4457 self.pool.as_deref(),
4458 )
4459 }
4460 (true, _) => match &lw.ffn {
4464 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
4465 let active = task_mask.unwrap().ffn_active_indices(li);
4466 sparse_ffn_quant(
4467 d,
4468 post_normed,
4469 &active,
4470 self.hidden_size,
4471 self.pool.as_deref(),
4472 )
4473 }
4474 FfnKind::Dense(d) => {
4479 let active = task_mask.unwrap().ffn_active_indices(li);
4480 let (gf, uf, df) = dequant_dense_f32(d);
4481 inference::sparse_ffn_forward(
4482 post_normed,
4483 &gf,
4484 &uf,
4485 &df,
4486 self.hidden_size,
4487 self.intermediate_size,
4488 &active,
4489 self.pool.as_deref(),
4490 )
4491 }
4492 FfnKind::Moe(m) => {
4493 let allowed = task_mask
4497 .and_then(|tm| tm.expert_flags(li, m.experts.len()));
4498 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
4499 }
4500 FfnKind::DenseMoe(dm) => dense_moe_ffn(
4501 dm,
4502 post_normed,
4503 &h,
4504 self.rms_eps,
4505 self.norm_style,
4506 self.pool.as_deref(),
4507 ),
4508 },
4509 (false, _) => match &lw.ffn {
4510 FfnKind::DenseMoe(dm) => dense_moe_ffn(
4511 dm,
4512 post_normed,
4513 &h,
4514 self.rms_eps,
4515 self.norm_style,
4516 self.pool.as_deref(),
4517 ),
4518 _ => {
4519 let allowed = match (&lw.ffn, task_mask) {
4520 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
4521 _ => None,
4522 };
4523 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
4524 }
4525 },
4526 };
4527 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
4528 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
4529 None => ffn_out,
4530 };
4531 for (i, &f) in ffn_out.iter().enumerate() {
4532 h[i] += f;
4533 }
4534 let mut ffn_out = ffn_out;
4535 attention::recycle_buf(&mut ffn_out);
4536
4537 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
4539 for v in h.iter_mut() {
4540 *v *= sc;
4541 }
4542 }
4543
4544 if self.is_loop_end(li) && li + 1 < self.num_layers {
4547 h = inference::rms_norm(
4548 &h,
4549 &self.weights.final_norm,
4550 self.rms_eps,
4551 self.norm_style,
4552 );
4553 }
4554
4555 if self.dyn_phi_layer == Some(li) {
4559 self.update_dyn_phi(&h);
4560 }
4561 }
4562 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
4564 crate::gpu::graph_race_record(false, t.elapsed());
4565 }
4566
4567 h
4568 }
4569
4570 fn update_dyn_phi(&mut self, h: &[f32]) {
4573 const A: f32 = 0.2;
4574 if self.dyn_phi_ema.len() != h.len() {
4575 self.dyn_phi_ema = vec![0.0; h.len()];
4576 self.dyn_phi_seen = 0;
4577 }
4578 if self.dyn_phi_seen == 0 {
4579 self.dyn_phi_ema.copy_from_slice(h);
4580 } else {
4581 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
4582 *e = (1.0 - A) * *e + A * v;
4583 }
4584 }
4585 self.dyn_phi_seen += 1;
4586 }
4587
4588 pub fn dyn_phi(&self) -> &[f32] {
4590 &self.dyn_phi_ema
4591 }
4592
4593 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
4595 self.dyn_phi_layer = layer;
4596 self.dyn_phi_ema.clear();
4597 self.dyn_phi_seen = 0;
4598 }
4599
4600 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
4602 let Some(model) = &self.model else {
4603 return Vec::new();
4604 };
4605 model
4606 .header
4607 .skills
4608 .iter()
4609 .enumerate()
4610 .filter_map(|(i, sk)| {
4611 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
4612 let sel = sk.selection.as_ref()?;
4613 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
4614 })
4615 .collect()
4616 }
4617
4618 pub fn active_skill(&self) -> Option<usize> {
4620 self.dyn_active
4621 }
4622
4623 pub fn enable_dynamic_routing(&mut self) -> usize {
4628 use crate::swarm::{DynRouter, RoutableSkill};
4629 let Some(model) = self.model.clone() else {
4630 return 0;
4631 };
4632 if self.dyn_blend_loaded {
4635 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
4636 return 0;
4637 }
4638 if let Some(a) = self.dyn_active {
4642 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
4643 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
4644 return 0;
4645 }
4646 }
4647 let hidden = self.hidden_size;
4648 let mut skills = Vec::new();
4649 for (idx, id, _phi) in self.dynamic_skills() {
4650 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
4651 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
4652 skills.push(rs);
4653 }
4654 }
4655 }
4656 if skills.is_empty() {
4657 return 0;
4658 }
4659 let phi = skills[0].phi_layer;
4661 if skills.iter().any(|s| s.phi_layer != phi) {
4662 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
4663 }
4664 let n = skills.len();
4665 self.set_dyn_phi_layer(Some(phi));
4666 self.dyn_router = Some(DynRouter::new(skills));
4667 n
4668 }
4669
4670 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
4672 self.dyn_router
4673 .as_ref()
4674 .map(|r| r.switches.clone())
4675 .unwrap_or_default()
4676 }
4677
4678 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
4681 let rows = self.weights.lm_head.rows();
4682 let mut logits = attention::take_buf(rows.min(self.vocab_size));
4683 self.weights
4684 .lm_head
4685 .matvec(hidden, &mut logits, self.pool.as_deref());
4686 logits.resize(self.vocab_size, 0.0);
4687 if let Some(m) = self.logit_multiplier {
4688 for l in logits.iter_mut() {
4689 *l *= m;
4690 }
4691 }
4692 if let Some(c) = self.final_softcap {
4693 for l in logits.iter_mut() {
4694 *l = c * (*l / c).tanh();
4695 }
4696 }
4697 logits
4698 }
4699
4700 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
4705 self.kv_cache.clear();
4706 self.kv_history.clear();
4707 let mut hidden = vec![0.0f32; self.hidden_size];
4708 for (pos, &id) in ids.iter().enumerate() {
4709 let emb = self.embed_single(id);
4710 hidden = self.forward_layers(&emb, pos, task_mask);
4711 }
4712 inference::rms_norm_into(
4713 &hidden,
4714 &self.weights.final_norm,
4715 self.rms_eps,
4716 self.norm_style,
4717 &mut self.ws.n1,
4718 );
4719 self.lm_head_forward(&self.ws.n1)
4720 }
4721}
4722
4723pub fn create_test_pipeline(
4725 hidden_size: usize,
4726 intermediate_size: usize,
4727 num_heads: usize,
4728 num_kv_heads: usize,
4729 head_dim: usize,
4730 num_layers: usize,
4731 vocab_size: usize,
4732) -> Pipeline {
4733 let synth = |n: usize, salt: usize| -> Vec<f32> {
4736 (0..n)
4737 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
4738 .collect()
4739 };
4740 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4741 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4742 };
4743 let layer_weights: Vec<LayerWeights> = (0..num_layers)
4744 .map(|li| LayerWeights {
4745 input_norm: vec![1.0; hidden_size],
4746 post_norm: vec![1.0; hidden_size],
4747 attn_out_norm: None,
4748 ffn_out_norm: None,
4749 layer_scale: None,
4750 ffn: FfnKind::Dense(DenseFfn {
4751 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
4752 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
4753 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
4754 act: Act::Silu,
4755 }),
4756 attn: AttnKind::Full {
4757 bias: None,
4758 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
4759 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
4760 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
4761 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
4762 q_norm: None,
4763 k_norm: None,
4764 output_gate: false,
4765 softplus_gate: None,
4766 },
4767 })
4768 .collect();
4769
4770 Pipeline::new(
4771 Tokenizer::byte_level(),
4772 PipelineWeights {
4773 embed_tokens: qt(vocab_size, hidden_size, 100),
4774 layers: layer_weights,
4775 lm_head: qt(vocab_size, hidden_size, 200),
4776 final_norm: vec![1.0; hidden_size],
4777 },
4778 hidden_size,
4779 intermediate_size,
4780 num_heads,
4781 num_kv_heads,
4782 head_dim,
4783 num_layers,
4784 num_layers, false, vocab_size,
4787 1e-6,
4788 10_000.0,
4789 NormStyle::Qwen,
4790 4096,
4791 SamplerConfig {
4792 seed: Some(42),
4793 ..Default::default()
4794 },
4795 )
4796}
4797
4798fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
4801 let inter = d.gate_proj.rows();
4802 let hidden = d.down_proj.rows();
4803 if d.act == Act::Silu && b >= 32 && crate::gpu::enabled_here() && !crate::gpu::mm_killed() {
4809 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
4810 d.gate_proj.mapped_q4t(),
4811 d.up_proj.mapped_q4t(),
4812 d.down_proj.mapped_q4t(),
4813 ) {
4814 let mut out = vec![0.0f32; b * hidden];
4815 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
4816 return out;
4817 }
4818 }
4819 }
4820 let mut g = vec![0.0f32; b * inter];
4821 d.gate_proj.matmat(xs, b, &mut g, pool);
4822 let mut u = vec![0.0f32; b * inter];
4823 d.up_proj.matmat(xs, b, &mut u, pool);
4824 for i in 0..b * inter {
4825 g[i] = d.act.combine(g[i], u[i]);
4826 }
4827 let mut out = vec![0.0f32; b * hidden];
4828 d.down_proj.matmat(&g, b, &mut out, pool);
4829 out
4830}
4831
4832fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
4837 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4838 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4839 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
4840 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
4841 if (!on && !dump) || b == 0 {
4842 return;
4843 }
4844 let hidden = xs.len() / b;
4845 if on {
4846 let mut acc = m.act_sq.borrow_mut();
4847 if acc.len() < hidden {
4848 acc.resize(hidden, 0.0);
4849 }
4850 for t in 0..b {
4851 let row = &xs[t * hidden..(t + 1) * hidden];
4852 for (a, &v) in acc.iter_mut().zip(row) {
4853 *a += (v as f64) * (v as f64);
4854 }
4855 }
4856 }
4857 if dump {
4858 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
4861 .ok()
4862 .and_then(|v| v.parse().ok())
4863 .unwrap_or(4096);
4864 let mut rows = m.act_rows.borrow_mut();
4865 if rows.len() < cap * hidden {
4866 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
4867 rows.extend_from_slice(&xs[..take * hidden]);
4868 }
4869 }
4870}
4871
4872fn moe_ffn_batch(
4873 m: &MoeFfn,
4874 xs: &[f32],
4875 b: usize,
4876 hidden: usize,
4877 pool: Option<&Pool>,
4878 allowed: Option<&[bool]>,
4879) -> Vec<f32> {
4880 accumulate_act(m, xs, b);
4881 let ne = m.experts.len();
4882 let mut logits = vec![0.0f32; b * ne];
4883 m.router.matmat(xs, b, &mut logits, pool);
4884
4885 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
4888 {
4889 let mut st = m.stats.borrow_mut();
4890 if st.len() < ne {
4891 st.resize(ne, 0);
4892 }
4893 for bi in 0..b {
4894 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
4895 for &e in &idx {
4896 st[e] += 1;
4897 assign[e].push((bi, p[e] / wsum));
4898 }
4899 }
4900 }
4901
4902 let mut out = vec![0.0f32; b * hidden];
4903 let cols = m.experts[0].gate_proj.cols();
4904 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
4905 let sb = list.len();
4906 let mut sub = vec![0.0f32; sb * cols];
4907 for (k, &(bi, _)) in list.iter().enumerate() {
4908 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
4909 }
4910 let eo = dense_ffn_batch(d, &sub, sb, pool);
4911 for (k, &(bi, w)) in list.iter().enumerate() {
4912 for i in 0..hidden {
4913 out[bi * hidden + i] += w * eo[k * hidden + i];
4914 }
4915 }
4916 };
4917 for (e, a) in assign.iter().enumerate().take(ne) {
4918 if !a.is_empty() {
4919 run_expert(&m.experts[e], a);
4920 }
4921 }
4922 if let Some((se, gate)) = &m.shared {
4923 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
4924 let mut gl = vec![0.0f32; b];
4925 gate.matmat(xs, b, &mut gl, pool);
4926 (0..b)
4927 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
4928 .collect()
4929 } else {
4930 (0..b).map(|bi| (bi, 1.0)).collect()
4931 };
4932 run_expert(se, &all);
4933 }
4934 out
4935}
4936
4937thread_local! {
4938 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
4942 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
4943}
4944
4945fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4947 if crate::gpu::enabled_here()
4958 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
4959 {
4960 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
4961 crate::gpu::ProbeArm::Gpu
4962 } else {
4963 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
4964 };
4965 match arm {
4966 crate::gpu::ProbeArm::Gpu => {
4967 let t0 = std::time::Instant::now();
4968 if let Some(out) = dense_ffn_gpu(d, x, pool) {
4969 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4970 return out;
4971 }
4972 }
4973 crate::gpu::ProbeArm::CpuTimed => {
4974 let t0 = std::time::Instant::now();
4975 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4976 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4977 return out;
4978 }
4979 crate::gpu::ProbeArm::Cpu => {
4980 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4981 }
4982 }
4983 }
4984 dense_ffn_cpu(d, x, pool)
4985}
4986
4987fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4989 let inter = d.gate_proj.rows();
4990 FFN_SCRATCH.with(|s| {
4991 let mut s = s.borrow_mut();
4992 let [g, u, ..] = &mut *s;
4993 g.resize(inter, 0.0);
4994 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
4997 } else {
4999 u.resize(inter, 0.0);
5000 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
5002 for i in 0..inter {
5003 g[i] = d.act.combine(g[i], u[i]);
5004 }
5005 }
5006 FFN_PROBE.with(|pr| {
5009 if let Some(acc) = pr.borrow_mut().as_mut() {
5010 let li = crate::gpu::cur_layer();
5011 if li >= 0 {
5012 if let Some(row) = acc.get_mut(li as usize) {
5013 for (a, &v) in row.iter_mut().zip(g.iter()) {
5014 *a += (v as f64).abs();
5015 }
5016 }
5017 }
5018 }
5019 });
5020 let mut out = attention::take_buf(d.down_proj.rows());
5021 d.down_proj.matvec(g, &mut out, pool);
5022 out
5023 })
5024}
5025
5026thread_local! {
5027 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
5030 const { std::cell::RefCell::new(None) };
5031}
5032
5033fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
5039 if d.act != Act::Silu {
5041 return None;
5042 }
5043 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
5046 return None;
5047 }
5048 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
5049 let mut model_ref = None;
5050 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
5051 let model = model_ref?;
5052 let hidden = jobs[0].down.1;
5053 let mut out = attention::take_buf(hidden);
5054 if crate::gpu::moe_block(&model, &jobs, &mut out) {
5055 Some(out)
5056 } else {
5057 let mut out = out;
5058 attention::recycle_buf(&mut out);
5059 None
5060 }
5061}
5062
5063#[allow(clippy::type_complexity)]
5068#[allow(clippy::type_complexity)]
5069fn moe_parts(
5070 t: &QTensor,
5071) -> Option<(
5072 &std::sync::Arc<cortiq_core::CmfModel>,
5073 usize,
5074 usize,
5075 usize,
5076 &[f32],
5077 &[f32],
5078 bool,
5079 bool,
5080)> {
5081 match t {
5082 QTensor::Mapped {
5083 model,
5084 idx,
5085 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
5086 rows,
5087 cols,
5088 row_scale,
5089 col_field,
5090 ..
5091 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
5092 Some((model, *idx, *rows, *cols, row_scale, col_field, false, false))
5093 }
5094 QTensor::Mapped {
5096 model,
5097 idx,
5098 dtype: cortiq_core::TensorDtype::Q1,
5099 rows,
5100 cols,
5101 ..
5102 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
5103 QTensor::Mapped {
5105 model,
5106 idx,
5107 dtype: cortiq_core::TensorDtype::Q4Tiled,
5108 rows,
5109 cols,
5110 ..
5111 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
5112 QTensor::Mapped {
5114 model,
5115 idx,
5116 dtype: cortiq_core::TensorDtype::Q4TiledP,
5117 rows,
5118 cols,
5119 ..
5120 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
5121 _ => None,
5122 }
5123}
5124
5125fn moe_push_job<'a>(
5127 d: &'a DenseFfn,
5128 x: &[f32],
5129 w: f32,
5130 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
5131 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
5132) -> Option<()> {
5133 use crate::qtensor::prescale;
5134 if d.act != Act::Silu {
5135 return None; }
5137 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
5138 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
5139 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
5140 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
5141 return None; }
5143 model_ref.get_or_insert_with(|| gm.clone());
5144 let gdt = if gcf.is_empty() {
5145 cortiq_core::TensorDtype::Q8Row
5146 } else {
5147 cortiq_core::TensorDtype::Q8_2f
5148 };
5149 let udt = if ucf.is_empty() {
5150 cortiq_core::TensorDtype::Q8Row
5151 } else {
5152 cortiq_core::TensorDtype::Q8_2f
5153 };
5154 jobs.push(crate::gpu::MoeJob {
5155 gate: (gi, gr, gc, grs),
5156 up: (ui, ur, uc, urs),
5157 down: (di, dr, dc, drs),
5158 xs_gate: prescale(x, gcf, gdt).into_owned(),
5159 xs_up: prescale(x, ucf, udt).into_owned(),
5160 down_col: dcf,
5161 w,
5162 q1: gq1,
5163 q4t: gq4 && d.gate_proj.mapped_q4tp().is_none(),
5164 q4tp: gq4 && d.gate_proj.mapped_q4tp().is_some(),
5165 });
5166 Some(())
5167}
5168
5169fn sparse_ffn_quant(
5176 d: &DenseFfn,
5177 x: &[f32],
5178 active: &[u16],
5179 hidden: usize,
5180 pool: Option<&Pool>,
5181) -> Vec<f32> {
5182 let n = active.len();
5183 let inter = d.gate_proj.rows();
5184 let mut act = vec![0.0f32; n];
5185 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
5188 let compute = |ai: usize| -> f32 {
5189 let idx = active[ai] as usize;
5190 if idx >= inter {
5191 return 0.0; }
5193 let mut s = if need_scratch {
5194 vec![0.0f32; hidden]
5195 } else {
5196 Vec::new()
5197 };
5198 let gate = d.gate_proj.row_dot(idx, x, &mut s);
5199 let up = d.up_proj.row_dot(idx, x, &mut s);
5200 d.act.combine(gate, up)
5201 };
5202 match pool {
5203 Some(p) if n >= 256 => {
5204 let ptr = SendMut(act.as_mut_ptr());
5205 p.run(&|widx, nw| {
5206 let chunk = n.div_ceil(nw);
5207 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
5208 for ai in s..e {
5209 unsafe { *ptr.at(ai) = compute(ai) };
5210 }
5211 });
5212 }
5213 _ => {
5214 for (ai, a) in act.iter_mut().enumerate() {
5215 *a = compute(ai);
5216 }
5217 }
5218 }
5219 let mut out = vec![0.0f32; hidden];
5221 for (ai, &idx) in active.iter().enumerate() {
5222 let w = act[ai];
5223 if w.abs() >= 1e-12 && (idx as usize) < inter {
5224 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
5225 }
5226 }
5227 out
5228}
5229
5230#[doc(hidden)]
5232pub fn sparse_ffn_quant_for_test(
5233 d: &DenseFfn,
5234 x: &[f32],
5235 active: &[u16],
5236 hidden: usize,
5237) -> Vec<f32> {
5238 sparse_ffn_quant(d, x, active, hidden, None)
5239}
5240
5241fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
5245 let deq = |t: &QTensor| -> Vec<f32> {
5246 let (rows, cols) = (t.rows(), t.cols());
5247 let mut out = vec![0.0f32; rows * cols];
5248 for r in 0..rows {
5249 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
5250 }
5251 out
5252 };
5253 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
5254}
5255
5256struct SendMut(*mut f32);
5258unsafe impl Send for SendMut {}
5259unsafe impl Sync for SendMut {}
5260impl SendMut {
5261 #[inline]
5262 #[allow(clippy::mut_from_ref)]
5265 unsafe fn at(&self, i: usize) -> &mut f32 {
5266 unsafe { &mut *self.0.add(i) }
5267 }
5268}
5269
5270fn moe_route(
5280 logits: &[f32],
5281 m: &MoeFfn,
5282 allowed: Option<&[bool]>,
5283) -> (Vec<usize>, Vec<f32>, f32) {
5284 let ne = logits.len();
5285 let p: Vec<f32> = if m.router_sigmoid {
5286 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
5287 } else {
5288 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
5289 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
5290 let s: f32 = e.iter().sum();
5291 for v in &mut e {
5292 *v /= s;
5293 }
5294 e
5295 };
5296 let admit = |e: usize| {
5302 m.mask.as_ref().is_none_or(|mk| mk[e]) && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
5303 };
5304 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
5305 match &m.expert_bias {
5307 Some(b) => idx.sort_unstable_by(|&x, &y| {
5308 (p[y] + b[y])
5309 .partial_cmp(&(p[x] + b[x]))
5310 .unwrap()
5311 .then(x.cmp(&y))
5312 }),
5313 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
5314 }
5315 idx.truncate(m.top_k);
5316 if let Some(tau) = m.route_tau {
5320 let total: f32 = idx.iter().map(|&e| p[e]).sum();
5321 if total > 0.0 {
5322 let mut acc = 0.0f32;
5323 let mut keep = idx.len();
5324 for (i, &e) in idx.iter().enumerate() {
5325 acc += p[e];
5326 if acc >= tau * total {
5327 keep = i + 1;
5328 break;
5329 }
5330 }
5331 idx.truncate(keep);
5332 }
5333 }
5334 let wsum: f32 = if m.norm_topk_prob {
5335 let s: f32 = idx.iter().map(|&e| p[e]).sum();
5336 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
5339 } else {
5340 1.0 / m.routed_scaling
5341 };
5342 (idx, p, wsum)
5343}
5344
5345fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
5348 accumulate_act(m, x, 1);
5349 let ne = m.experts.len();
5350 let mut logits = vec![0.0f32; ne];
5351 m.router.matvec(x, &mut logits, pool);
5352 let (idx, p, wsum) = moe_route(&logits, m, allowed);
5353 {
5354 let mut st = m.stats.borrow_mut();
5355 if st.len() < ne {
5356 st.resize(ne, 0);
5357 }
5358 for &e in &idx {
5359 st[e] += 1;
5360 }
5361 }
5362 if crate::gpu::enabled_here() {
5367 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
5368 crate::gpu::ProbeArm::Gpu => {
5369 let t0 = std::time::Instant::now();
5370 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
5371 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
5372 return out;
5373 }
5374 }
5375 crate::gpu::ProbeArm::CpuTimed => {
5376 let t0 = std::time::Instant::now();
5377 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
5378 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
5379 return out;
5380 }
5381 crate::gpu::ProbeArm::Cpu => {
5382 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
5383 }
5384 }
5385 }
5386 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
5387}
5388
5389fn graph_note(built: bool) {
5393 use std::sync::atomic::{AtomicBool, Ordering};
5394 static SAID: AtomicBool = AtomicBool::new(false);
5395 if !SAID.swap(true, Ordering::Relaxed) {
5396 if built {
5397 tracing::info!("wgpu whole-token graph: ACTIVE");
5398 } else {
5399 tracing::warn!("wgpu whole-token graph refused — per-op path");
5400 }
5401 }
5402}
5403
5404fn moe_batch_enabled() -> bool {
5407 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5408 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
5409}
5410
5411fn moe_ffn_cpu_batched(
5417 m: &MoeFfn,
5418 x: &[f32],
5419 idx: &[usize],
5420 p: &[f32],
5421 wsum: f32,
5422 pool: Option<&Pool>,
5423) -> Option<Vec<f32>> {
5424 if idx.is_empty() || !moe_batch_enabled() {
5425 return None;
5426 }
5427 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
5431 return None;
5432 }
5433 let n = idx.len() + usize::from(m.shared.is_some());
5434 let mut pairs = Vec::with_capacity(n);
5435 let mut downs = Vec::with_capacity(n);
5436 let mut ws = Vec::with_capacity(n);
5437 for &e in idx {
5438 let d = &m.experts[e];
5439 if d.act != Act::Silu {
5440 return None;
5441 }
5442 pairs.push((&d.gate_proj, &d.up_proj));
5443 downs.push(&d.down_proj);
5444 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
5445 }
5446 if let Some((se, gate)) = &m.shared {
5449 if se.act != Act::Silu {
5450 return None;
5451 }
5452 let g = gate.as_ref().map_or(1.0, |gate| {
5453 let mut gl = [0.0f32; 1];
5454 gate.matvec(x, &mut gl, pool);
5455 1.0 / (1.0 + (-gl[0]).exp())
5456 });
5457 pairs.push((&se.gate_proj, &se.up_proj));
5458 downs.push(&se.down_proj);
5459 ws.push(g);
5460 }
5461 let inter = pairs[0].0.rows();
5462 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
5463 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
5464 return None;
5465 }
5466 let mut out = attention::take_buf(x.len());
5467 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
5468 attention::recycle_buf(&mut out);
5469 return None;
5470 }
5471 Some(out)
5472}
5473
5474fn moe_ffn_cpu(
5476 m: &MoeFfn,
5477 x: &[f32],
5478 idx: &[usize],
5479 p: &[f32],
5480 wsum: f32,
5481 pool: Option<&Pool>,
5482) -> Vec<f32> {
5483 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
5484 return out;
5485 }
5486 let mut out = attention::take_buf(x.len());
5487 for &e in idx {
5488 let mut eo = dense_ffn(&m.experts[e], x, pool);
5489 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
5490 for i in 0..out.len() {
5491 out[i] += w * eo[i];
5492 }
5493 attention::recycle_buf(&mut eo);
5494 }
5495 if let Some((se, gate)) = &m.shared {
5496 let mut so = dense_ffn(se, x, pool);
5497 let g = gate.as_ref().map_or(1.0, |gate| {
5498 let mut gl = [0.0f32; 1];
5499 gate.matvec(x, &mut gl, pool);
5500 1.0 / (1.0 + (-gl[0]).exp())
5501 });
5502 for i in 0..out.len() {
5503 out[i] += g * so[i];
5504 }
5505 attention::recycle_buf(&mut so);
5506 }
5507 out
5508}
5509
5510#[allow(clippy::too_many_arguments)]
5518fn mla_attention(
5519 w: &MlaWeights,
5520 normed: &[f32],
5521 cache: &mut crate::kv_cache::LayerKvCache,
5522 position: usize,
5523 inv_freq: &[f32],
5524 rope_scale: f32,
5525 eps: f64,
5526 pool: Option<&Pool>,
5527) -> Vec<f32> {
5528 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
5529 let hd = dr + dn;
5530 let mut q = vec![0.0f32; nh * hd];
5531 match (&w.q_a, &w.q_a_norm) {
5532 (Some(qa), Some(qn)) => {
5533 let mut t = vec![0.0f32; qa.rows()];
5534 qa.matvec(normed, &mut t, pool);
5535 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
5536 w.q_proj.matvec(&tn, &mut q, pool);
5537 }
5538 _ => w.q_proj.matvec(normed, &mut q, pool),
5539 }
5540 let mut ca = vec![0.0f32; lora + dr];
5541 w.kv_a.matvec(normed, &mut ca, pool);
5542 let (c_lat, k_rope) = ca.split_at_mut(lora);
5543 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
5544 let mut kvb = vec![0.0f32; nh * (dn + dv)];
5545 w.kv_b.matvec(&latn, &mut kvb, pool);
5546 if !w.nope {
5547 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
5548 }
5549 for h in 0..nh {
5550 if !w.nope {
5551 attention::rope_rotate_scaled(
5552 &mut q[h * hd..h * hd + dr],
5553 position,
5554 inv_freq,
5555 rope_scale,
5556 );
5557 }
5558 }
5559 let mut k = vec![0.0f32; nh * hd];
5560 let mut v = vec![0.0f32; nh * hd];
5561 for h in 0..nh {
5562 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
5563 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
5564 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
5565 }
5566 cache.append(&k, &v, &vec![true; nh]);
5567 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
5568 attention::recycle_buf(&mut imp);
5569 let mut ov = vec![0.0f32; nh * dv];
5570 for h in 0..nh {
5571 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
5572 }
5573 let mut out = vec![0.0f32; w.o_proj.rows()];
5574 w.o_proj.matvec(&ov, &mut out, pool);
5575 out
5576}
5577
5578fn dense_moe_ffn(
5585 dm: &DenseMoeFfn,
5586 x_normed: &[f32],
5587 h_raw: &[f32],
5588 eps: f64,
5589 norm_style: NormStyle,
5590 pool: Option<&Pool>,
5591) -> Vec<f32> {
5592 let mut d = dense_ffn(&dm.dense, x_normed, pool);
5593 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
5594 let m = &dm.moe;
5595 let ne = m.experts.len();
5596 let mut logits = vec![0.0f32; ne];
5597 if m.router_input_norm {
5598 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
5599 let inv = 1.0 / (ss + eps as f32).sqrt();
5600 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
5601 m.router.matvec(&xr, &mut logits, pool);
5602 } else {
5603 m.router.matvec(h_raw, &mut logits, pool);
5604 }
5605 let (idx, p, wsum) = moe_route(&logits, m, None);
5606 {
5607 let mut st = m.stats.borrow_mut();
5608 if st.len() < ne {
5609 st.resize(ne, 0);
5610 }
5611 for &e in &idx {
5612 st[e] += 1;
5613 }
5614 }
5615 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
5616 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
5617 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
5618 for (di, mi) in d.iter_mut().zip(&mo) {
5619 *di += mi;
5620 }
5621 d
5622}
5623
5624fn moe_gpu_refused(why: &'static str) {
5631 use std::sync::atomic::{AtomicBool, Ordering};
5632 static SAID: AtomicBool = AtomicBool::new(false);
5633 if !SAID.swap(true, Ordering::Relaxed) {
5634 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
5635 }
5636}
5637
5638fn moe_ffn_gpu(
5639 m: &MoeFfn,
5640 x: &[f32],
5641 idx: &[usize],
5642 p: &[f32],
5643 wsum: f32,
5644 pool: Option<&Pool>,
5645) -> Option<Vec<f32>> {
5646 use crate::gpu::MoeJob;
5647
5648 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
5649 let mut model_ref = None;
5650 for &e in idx {
5651 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
5652 moe_gpu_refused("push_job(expert)");
5653 return None;
5654 }
5655 }
5656 if let Some((se, gate)) = &m.shared {
5657 let g = gate.as_ref().map_or(1.0, |gate| {
5658 let mut gl = [0.0f32; 1];
5659 gate.matvec(x, &mut gl, pool);
5660 1.0 / (1.0 + (-gl[0]).exp())
5661 });
5662 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
5663 moe_gpu_refused("push_job(shared)");
5664 return None;
5665 }
5666 }
5667 let Some(model) = model_ref else {
5668 moe_gpu_refused("no model_ref");
5669 return None;
5670 };
5671 let hidden = jobs[0].down.1;
5672 let mut out = vec![0.0f32; hidden];
5673 if crate::gpu::moe_block(&model, &jobs, &mut out) {
5674 Some(out)
5675 } else {
5676 moe_gpu_refused("gpu::moe_block");
5677 None
5678 }
5679}
5680
5681fn ffn_forward(
5683 ffn: &FfnKind,
5684 x: &[f32],
5685 pool: Option<&Pool>,
5686 experts_allowed: Option<&[bool]>,
5687) -> Vec<f32> {
5688 match ffn {
5689 FfnKind::Dense(d) => dense_ffn(d, x, pool),
5690 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
5691 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
5695 }
5696}
5697
5698fn ffn_forward_pair(
5702 ffn: &FfnKind,
5703 x1: &[f32],
5704 x2: &[f32],
5705 pool: Option<&Pool>,
5706 experts_allowed: Option<&[bool]>,
5707) -> (Vec<f32>, Vec<f32>) {
5708 let d = match ffn {
5709 FfnKind::Dense(d) => d,
5710 FfnKind::Moe(m) => {
5711 return (
5712 moe_ffn(m, x1, pool, experts_allowed),
5713 moe_ffn(m, x2, pool, experts_allowed),
5714 );
5715 }
5716 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
5717 };
5718 let inter = d.gate_proj.rows();
5719 FFN_SCRATCH.with(|s| {
5720 let mut s = s.borrow_mut();
5721 let [g1, g2, u1, u2] = &mut *s;
5722 g1.resize(inter, 0.0);
5723 g2.resize(inter, 0.0);
5724 u1.resize(inter, 0.0);
5725 u2.resize(inter, 0.0);
5726 QTensor::matvec2_many(
5729 [&d.gate_proj, &d.up_proj],
5730 x1,
5731 x2,
5732 [g1.as_mut_slice(), u1.as_mut_slice()],
5733 [g2.as_mut_slice(), u2.as_mut_slice()],
5734 pool,
5735 );
5736 for i in 0..inter {
5737 g1[i] = d.act.combine(g1[i], u1[i]);
5738 g2[i] = d.act.combine(g2[i], u2[i]);
5739 }
5740 let mut o1 = attention::take_buf(d.down_proj.rows());
5741 let mut o2 = attention::take_buf(d.down_proj.rows());
5742 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
5743 (o1, o2)
5744 })
5745}
5746
5747#[cfg(test)]
5748mod tests {
5749
5750 #[test]
5751 fn cancel_flag_stops_generation() {
5752 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
5753 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
5756 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
5757 assert_eq!(r.finish_reason, "cancelled");
5758 assert!(r.token_ids.is_empty(), "no tokens after cancel: {:?}", r.token_ids);
5759 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
5761 assert_ne!(r2.finish_reason, "cancelled");
5762 }
5763 use super::*;
5764
5765 #[test]
5771 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
5772 let (hidden, inter) = (16usize, 40usize);
5773 let synth = |n: usize, salt: usize| -> Vec<f32> {
5774 (0..n)
5775 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
5776 .collect()
5777 };
5778 let d = DenseFfn {
5779 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
5780 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
5781 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
5782 act: Act::Silu,
5783 };
5784 let x = synth(hidden, 9);
5785 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
5787
5788 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
5789
5790 let mut g = vec![0.0f32; inter];
5792 d.gate_proj.matvec(&x, &mut g, None);
5793 let mut u = vec![0.0f32; inter];
5794 d.up_proj.matvec(&x, &mut u, None);
5795 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
5796 for i in 0..inter {
5797 g[i] = if act_set.contains(&(i as u16)) {
5798 inference::silu(g[i]) * u[i]
5799 } else {
5800 0.0
5801 };
5802 }
5803 let mut reference = vec![0.0f32; hidden];
5804 d.down_proj.matvec(&g, &mut reference, None);
5805
5806 let max_d = sparse
5807 .iter()
5808 .zip(&reference)
5809 .map(|(a, b)| (a - b).abs())
5810 .fold(0.0f32, f32::max);
5811 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
5812 }
5813
5814 fn attach_test_mtp(p: &mut Pipeline) {
5816 let (h, inter, heads, kv, hd) = (
5817 p.hidden_size,
5818 p.intermediate_size,
5819 p.num_heads,
5820 p.num_kv_heads,
5821 p.head_dim,
5822 );
5823 let synth = |n: usize, salt: usize| -> Vec<f32> {
5824 (0..n)
5825 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
5826 .collect()
5827 };
5828 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
5829 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
5830 };
5831 p.mtp = Some(MtpModule {
5832 enorm: vec![1.0; h],
5833 hnorm: vec![1.0; h],
5834 eh_proj: qt(h, 2 * h, 301),
5835 layer: LayerWeights {
5836 input_norm: vec![1.0; h],
5837 post_norm: vec![1.0; h],
5838 attn_out_norm: None,
5839 ffn_out_norm: None,
5840 layer_scale: None,
5841 ffn: FfnKind::Dense(DenseFfn {
5842 gate_proj: qt(inter, h, 315),
5843 up_proj: qt(inter, h, 316),
5844 down_proj: qt(h, inter, 317),
5845 act: Act::Silu,
5846 }),
5847 attn: AttnKind::Full {
5848 bias: None,
5849 wq: qt(heads * hd, h, 311),
5850 wk: qt(kv * hd, h, 312),
5851 wv: qt(kv * hd, h, 313),
5852 wo: qt(h, heads * hd, 314),
5853 q_norm: None,
5854 k_norm: None,
5855 output_gate: false,
5856 softplus_gate: None,
5857 },
5858 },
5859 final_norm: vec![1.0; h],
5860 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
5861 });
5862 }
5863
5864 #[test]
5865 fn speculative_equals_vanilla_greedy() {
5866 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5870 let run = |spec: bool| {
5871 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5872 p.sampler_config.temperature = 0.0;
5873 attach_test_mtp(&mut p);
5874 p.speculative = spec;
5875 let r = p.generate("abcdef", 12, None, None).unwrap();
5876 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
5877 };
5878 let (vanilla, d0, _) = run(false);
5879 let (spec, d1, a1) = run(true);
5880 assert_eq!(d0, 0, "vanilla path must not draft");
5881 assert!(d1 > 0, "speculative path must draft");
5882 assert_eq!(
5883 vanilla, spec,
5884 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
5885 );
5886 }
5887
5888 #[test]
5889 fn speculative_accepts_constant_oracle() {
5890 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
5892 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5893 p.sampler_config.temperature = 0.0;
5894 p.sampler_config.repetition_penalty = 1.0;
5895 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
5898 attach_test_mtp(&mut p);
5899 p.speculative = true;
5900 let r = p.generate("abcd", 10, None, None).unwrap();
5901 assert!(r.mtp_drafted > 0);
5902 assert_eq!(
5903 r.mtp_accepted, r.mtp_drafted,
5904 "constant logits → every draft accepted"
5905 );
5906 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
5909 }
5910
5911 #[test]
5912 fn empty_prompt_is_an_error_not_a_panic() {
5913 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
5914 let r = p.generate("", 4, None, None);
5915 assert!(r.is_err(), "empty prompt must be a clean error");
5916 }
5917
5918 #[test]
5919 fn every_token_enters_kv_exactly_once() {
5920 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5921 p.sampler_config.temperature = 0.0;
5923 let r = p.generate("abc", 2, None, None).unwrap();
5924 assert_eq!(r.prompt_tokens, 3);
5925 assert_eq!(
5929 p.kv_cache.seq_len(),
5930 3 + r.tokens_generated - 1,
5931 "each token must be cached exactly once (v1 cached the last prompt token twice)"
5932 );
5933 }
5934
5935 #[test]
5936 fn generation_is_reproducible_with_seed() {
5937 let run = || {
5938 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5939 p.generate("hello", 8, None, None).unwrap().token_ids
5940 };
5941 assert_eq!(run(), run());
5942 }
5943
5944 #[test]
5945 fn resetting_sampler_restarts_the_seeded_stream() {
5946 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
5947 let config = SamplerConfig {
5948 seed: Some(1234),
5949 ..SamplerConfig::default()
5950 };
5951 p.set_sampler_config(config.clone());
5952 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
5953 p.set_sampler_config(config);
5954 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
5955 assert_eq!(first, second);
5956 }
5957
5958 #[test]
5959 fn eviction_bounds_the_cache() {
5960 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
5961 p.kv_cache.max_seq_len = 6;
5962 p.sampler_config.temperature = 0.0;
5963 let _ = p.generate("abcd", 12, None, None).unwrap();
5964 assert!(
5965 p.kv_cache.seq_len() <= 6 + 1,
5966 "cache must stay bounded by max_seq_len (got {})",
5967 p.kv_cache.seq_len()
5968 );
5969 }
5970
5971 #[test]
5972 fn confidence_matches_tokens_and_is_a_probability() {
5973 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
5974 p.sampler_config.temperature = 0.0;
5975 p.sampler_config.repetition_penalty = 1.0;
5976 let r = p.generate("abcd", 10, None, None).unwrap();
5977 assert_eq!(
5978 r.token_confidence.len(),
5979 r.token_ids.len(),
5980 "one confidence per emitted token"
5981 );
5982 for &c in &r.token_confidence {
5983 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
5984 }
5985 let logits = [1.0f32, 3.0, 0.5, 3.0];
5987 let p0 = top1_prob_t(&logits, 1, 1.0);
5988 let p1 = top1_prob_t(&logits, 3, 1.0);
5989 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
5990 assert!(p0 > 0.0 && p0 < 1.0);
5991 let sharp = top1_prob_t(&logits, 1, 1.0);
5993 let soft = top1_prob_t(&logits, 1, 2.0);
5994 assert!(soft < sharp, "higher temperature lowers peak confidence");
5995 }
5996
5997 #[test]
5998 fn trace_is_opt_in_and_parallels_the_output() {
5999 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
6001 p.sampler_config.temperature = 0.0;
6002 p.sampler_config.repetition_penalty = 1.0;
6003 let r = p.generate("abcd", 10, None, None).unwrap();
6004 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
6005
6006 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
6008 p.sampler_config.temperature = 0.0;
6009 p.sampler_config.repetition_penalty = 1.0;
6010 p.set_trace(true);
6011 let r = p.generate("abcd", 10, None, None).unwrap();
6012 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
6013 for (i, tr) in r.traces.iter().enumerate() {
6014 assert_eq!(tr.t, i, "trace index is sequential");
6015 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
6016 assert_eq!(
6017 tr.confidence, r.token_confidence[i],
6018 "trace confidence matches the confidence channel"
6019 );
6020 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
6022 }
6023 }
6024
6025 #[test]
6026 fn explain_prefill_logits_match_greedy_first_token() {
6027 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
6031 p.sampler_config.temperature = 0.0;
6032 p.sampler_config.repetition_penalty = 1.0;
6033 let ids = p.tokenizer.encode("abcd");
6034 let logits = p.prefill_next_logits(&ids, None);
6035 let argmax = logits
6036 .iter()
6037 .enumerate()
6038 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
6039 .unwrap()
6040 .0 as u32;
6041 let r = p.generate("abcd", 1, None, None).unwrap();
6042 assert_eq!(
6043 argmax, r.token_ids[0],
6044 "explain preview must match greedy emit"
6045 );
6046 }
6047
6048 #[test]
6049 fn laguna_shared_expert_is_unconditionally_added() {
6050 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
6051 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
6052 let zero_dense = || DenseFfn {
6053 gate_proj: matrix(vec![0.0; 4]),
6054 up_proj: matrix(vec![0.0; 4]),
6055 down_proj: matrix(vec![0.0; 4]),
6056 act: Act::Silu,
6057 };
6058 let shared = DenseFfn {
6059 gate_proj: identity(),
6060 up_proj: identity(),
6061 down_proj: identity(),
6062 act: Act::Silu,
6063 };
6064 let x = [1.0, 2.0];
6065 let expected = dense_ffn(&shared, &x, None);
6066 let moe = MoeFfn {
6067 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
6068 experts: vec![zero_dense()],
6069 top_k: 1,
6070 norm_topk_prob: true,
6071 router_sigmoid: true,
6072 expert_bias: None,
6073 routed_scaling: 1.0,
6074 route_tau: None,
6075 shared: Some((shared, None)),
6076 stats: std::cell::RefCell::new(Vec::new()),
6077 act_sq: std::cell::RefCell::new(Vec::new()),
6078 act_rows: std::cell::RefCell::new(Vec::new()),
6079 mask: None,
6080 per_expert_scale: None,
6081 router_input_norm: false,
6082 };
6083 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
6084 for (actual, expected) in actual.iter().zip(expected) {
6085 assert!((actual - expected).abs() < 1e-6);
6086 }
6087 }
6088}