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 short_conv_cfg: Option<ShortConvCfg>,
82 pub mtp: Option<MtpModule>,
84 pub speculative: bool,
86 rng: SplitMix64,
87 sampler_scratch: SamplerScratch,
88 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
92 ws: ForwardScratch,
96 pool: Option<std::sync::Arc<Pool>>,
98 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
102 pub(crate) dyn_force_f32: bool,
104 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
109 pub(crate) dyn_active: Option<usize>,
115 pub(crate) dyn_blend_loaded: bool,
119 pub(crate) dyn_phi_layer: Option<usize>,
122 dyn_phi_ema: Vec<f32>,
124 dyn_phi_seen: usize,
125 pub dyn_router: Option<crate::swarm::DynRouter>,
128 o1_cfg: Option<crate::nystrom::O1Cfg>,
131 o1_flags: Vec<bool>,
133 trace: bool,
136 calib_temp: f32,
139 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
141 graph_kv_id: u64,
142 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
145 graph_want_logits: bool,
146 graph_logits: Option<Vec<f32>>,
149 pub embed_multiplier: f32,
151 pub attn_scale: f32,
154 pub swa: Option<(usize, usize)>,
157 pub sliding_layers: Option<Vec<bool>>,
160 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
163 pub rotary_dim_local: Option<usize>,
164 pub rope_scale: f32,
165 pub rope_scale_local: f32,
166 pub global_attn: Option<(usize, usize)>,
169 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
172 pub attn_v_norm: bool,
174 pub final_softcap: Option<f32>,
176 confidence_on: bool,
180}
181
182#[cfg(target_os = "macos")]
183impl Drop for Pipeline {
184 fn drop(&mut self) {
185 crate::gpu::kv_mirror_drop(self.graph_kv_id);
186 }
187}
188
189pub struct PipelineWeights {
194 pub embed_tokens: QTensor,
196 pub layers: Vec<LayerWeights>,
198 pub lm_head: QTensor,
200 pub final_norm: Vec<f32>,
202}
203
204pub struct LayerWeights {
206 pub input_norm: Vec<f32>,
207 pub post_norm: Vec<f32>,
210 pub attn_out_norm: Option<Vec<f32>>,
213 pub layer_scale: Option<f32>,
215 pub ffn_out_norm: Option<Vec<f32>>,
218 pub ffn: FfnKind,
219 pub attn: AttnKind,
220}
221
222#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
225pub enum Act {
226 #[default]
227 Silu,
228 GeluTanh,
229}
230
231impl Act {
232 pub fn from_arch(name: &str) -> Self {
233 if name == "gelu_tanh" {
234 Self::GeluTanh
235 } else {
236 Self::Silu
237 }
238 }
239
240 #[inline]
241 pub fn apply(self, x: f32) -> f32 {
242 match self {
243 Self::Silu => inference::silu(x),
244 Self::GeluTanh => inference::gelu_tanh(x),
245 }
246 }
247}
248
249pub struct DenseFfn {
251 pub gate_proj: QTensor,
252 pub up_proj: QTensor,
253 pub down_proj: QTensor,
254 pub act: Act,
256}
257
258pub enum FfnKind {
261 Dense(DenseFfn),
262 Moe(MoeFfn),
266}
267
268pub struct MoeFfn {
269 pub router: QTensor,
271 pub experts: Vec<DenseFfn>,
272 pub top_k: usize,
273 pub norm_topk_prob: bool,
274 pub router_sigmoid: bool,
277 pub expert_bias: Option<Vec<f32>>,
281 pub routed_scaling: f32,
284 pub route_tau: Option<f32>,
290 pub shared: Option<(DenseFfn, Option<QTensor>)>,
293 pub stats: std::cell::RefCell<Vec<u64>>,
297 pub mask: Option<Vec<bool>>,
302}
303
304pub enum AttnKind {
307 Full {
309 wq: QTensor,
310 wk: QTensor,
311 wv: QTensor,
312 wo: QTensor,
313 q_norm: Option<Vec<f32>>,
314 k_norm: Option<Vec<f32>>,
315 output_gate: bool,
316 softplus_gate: Option<(QTensor, bool)>,
320 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
322 },
323 Linear(VmfPhaseWeights),
325 LinearGdn(GdnWeights),
327 ShortConv(ShortConvWeights),
330}
331
332pub struct MtpModule {
337 pub enorm: Vec<f32>,
338 pub hnorm: Vec<f32>,
339 pub eh_proj: QTensor,
341 pub layer: LayerWeights,
342 pub final_norm: Vec<f32>,
343 pub kv: crate::kv_cache::LayerKvCache,
344}
345
346pub struct GenerateResult {
348 pub text: String,
349 pub token_ids: Vec<u32>,
350 pub prompt_tokens: usize,
351 pub tokens_generated: usize,
352 pub finish_reason: String,
353 pub mtp_drafted: usize,
355 pub mtp_accepted: usize,
356 pub token_confidence: Vec<f32>,
361 pub traces: Vec<TokenTrace>,
364}
365
366#[derive(Clone, Debug)]
371pub struct TokenTrace {
372 pub t: usize,
374 pub token_id: u32,
376 pub confidence: f32,
378 pub active_skill: Option<String>,
380 pub recon: Option<f32>,
384 pub switched: bool,
387}
388
389fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
394 let t = if temp > 1e-3 { temp } else { 1.0 };
395 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
396 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
397 if sum > 0.0 {
398 (((logits[id as usize] - max) / t).exp()) / sum
399 } else {
400 0.0
401 }
402}
403
404fn prefill_batched() -> bool {
407 std::env::var("CMF_PREFILL")
408 .map(|v| v != "seq")
409 .unwrap_or(true)
410}
411
412fn prefill_chunk() -> usize {
417 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
418 .ok()
419 .and_then(|v| v.parse::<usize>().ok())
420 {
421 return n.max(1);
422 }
423 if cfg!(target_os = "macos") {
424 512
425 } else if cfg!(target_arch = "aarch64") {
426 256
429 } else {
430 48
431 }
432}
433
434pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
436
437impl Pipeline {
438 #[inline]
442 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
443 virtual_idx % self.physical_layers
444 }
445
446 #[inline]
449 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
450 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
451 }
452
453 #[allow(clippy::too_many_arguments)]
455
456 #[cfg(target_os = "macos")]
475 fn graph_prefill_preferred(&self) -> bool {
476 if !crate::gpu::enabled_here()
477 || !crate::gpu::q1_force()
478 || std::env::var("CMF_GPU_BLOCK")
479 .map(|v| v == "0")
480 .unwrap_or(false)
481 {
482 return false;
483 }
484 if self.loop_final_norm {
485 return true;
486 }
487 self.weights
488 .layers
489 .iter()
490 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
491 }
492
493 #[cfg(not(target_os = "macos"))]
494 fn graph_prefill_preferred(&self) -> bool {
495 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
503 .map(|v| v != "0")
504 .unwrap_or_else(|_| {
505 crate::gpu::wgpu_graph_default()
509 });
510 if !graph_on || !crate::gpu::enabled_here() {
511 return false;
512 }
513 self.weights
514 .layers
515 .iter()
516 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
517 }
518
519 #[cfg(target_os = "macos")]
520 fn q1_graph_gpu(
521 &mut self,
522 start: usize,
523 upto: Option<usize>,
524 position: usize,
525 h: &mut [f32],
526 ) -> usize {
527 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
528 if !crate::gpu::enabled_here()
529 || !crate::gpu::q1_force()
530 || std::env::var("CMF_GPU_BLOCK")
531 .map(|v| v == "0")
532 .unwrap_or(false)
533 {
534 return start;
535 }
536 if self.swa.is_some()
541 || self.global_attn.is_some()
542 || self.attention_heads_per_layer.is_some()
543 || self.attn_v_norm
544 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
545 || self.weights.layers.iter().any(|lw| {
546 lw.attn_out_norm.is_some()
547 || lw.ffn_out_norm.is_some()
548 || lw.layer_scale.is_some()
549 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
550 })
551 {
552 return start;
553 }
554 let limit = upto
557 .map(|u| u + 1)
558 .unwrap_or(self.num_layers)
559 .min(self.num_layers);
560
561 enum Item<'a> {
562 Gdn {
563 run: Vec<GdnGpuLayer<'a>>,
564 first: usize,
565 },
566 Attn {
567 l: AttnGpuLayer<'a>,
568 li: usize,
569 q_norm: Option<&'a [f32]>,
570 k_norm: Option<&'a [f32]>,
571 output_gate: bool,
572 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
573 full_gpu: bool,
576 },
577 }
578
579 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
581 let dev_attend = attend_mode != "0"
582 && attend_mode != "off"
583 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
587 && self.head_dim % 4 == 0
588 && self.head_dim <= 256
589 && self.rotary_dim >= 2
590 && self.rotary_dim <= self.head_dim
591 && (self.rotary_dim / 2) % 32 == 0
592 && self.num_kv_heads > 0
593 && self.num_heads % self.num_kv_heads == 0;
594
595 let mut plan: Vec<Item> = Vec::new();
596 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
597 let mut scan = start;
598 while scan < limit {
599 let lw = &self.weights.layers[self.phys_layer(scan)];
600 let FfnKind::Dense(d) = &lw.ffn else { break };
601 let (Some(g), Some(u), Some(dn)) = (
602 d.gate_proj.q1_parts(),
603 d.up_proj.q1_parts(),
604 d.down_proj.q1_parts(),
605 ) else {
606 break;
607 };
608 match &lw.attn {
609 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
610 let parts = (
611 w.in_proj_qkv.q1_parts(),
612 w.in_proj_z.q1_parts(),
613 w.in_proj_a.f32_parts(),
614 w.in_proj_b.f32_parts(),
615 w.out_proj.q1_parts(),
616 );
617 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
618 break;
619 };
620 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
621 model_ref.get_or_insert_with(|| model.clone());
622 }
623 let gl = GdnGpuLayer {
624 attn_norm: &lw.input_norm,
625 post_norm: &lw.post_norm,
626 qkv,
627 z,
628 a,
629 b,
630 out,
631 gate: g,
632 up: u,
633 down: dn,
634 conv1d: &w.conv1d,
635 a_log: &w.a_log,
636 dt_bias: &w.dt_bias,
637 gnorm: &w.norm,
638 };
639 match plan.last_mut() {
640 Some(Item::Gdn { run, .. }) => run.push(gl),
641 _ => plan.push(Item::Gdn {
642 run: vec![gl],
643 first: scan,
644 }),
645 }
646 }
647 AttnKind::Full {
648 wq,
649 wk,
650 wv,
651 wo,
652 q_norm,
653 k_norm,
654 output_gate,
655 softplus_gate: None,
656 bias,
657 } if !self.kv_cache.layers[scan].o1_sealed() => {
658 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
659 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
660 break;
661 };
662 if let QTensor::Mapped { model, .. } = wq {
663 model_ref.get_or_insert_with(|| model.clone());
664 }
665 let cache = &self.kv_cache.layers[scan];
666 let full_gpu = dev_attend
667 && cache.mode == crate::kv_cache::KvMode::F32
668 && cache.o1.is_none()
669 && bias.is_none()
670 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
671 && pk.1 == self.num_kv_heads * self.head_dim
672 && pv.1 == self.num_kv_heads * self.head_dim
673 && po.2 == self.num_heads * self.head_dim;
674 plan.push(Item::Attn {
675 l: AttnGpuLayer {
676 attn_norm: &lw.input_norm,
677 post_norm: &lw.post_norm,
678 wq: pq,
679 wk: pk,
680 wv: pv,
681 wo: po,
682 gate: g,
683 up: u,
684 down: dn,
685 },
686 li: scan,
687 q_norm: q_norm.as_deref(),
688 k_norm: k_norm.as_deref(),
689 output_gate: *output_gate,
690 bias: bias
691 .as_ref()
692 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
693 full_gpu,
694 });
695 }
696 _ => break,
697 }
698 scan += 1;
699 }
700 let Some(model) = model_ref else { return start };
701 if plan.is_empty() {
702 return start;
703 }
704 let dims = GraphDims {
705 hidden: self.hidden_size,
706 eps: self.rms_eps as f32,
707 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
708 };
709 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
710 return start;
711 };
712 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
713 nv: cfg.num_v_heads,
714 nk: cfg.num_k_heads,
715 dk: cfg.key_head_dim,
716 dv: cfg.value_head_dim,
717 kk: cfg.conv_kernel,
718 hidden: self.hidden_size,
719 inter: self.intermediate_size,
720 c_dim: cfg.conv_dim(),
721 eps: cfg.rms_eps as f32,
722 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
723 });
724 let mut valid = 0usize;
728 let mut end = start;
729 for item in &plan {
730 let ok = match item {
731 Item::Gdn { run, .. } => gcfg
732 .as_ref()
733 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
734 .unwrap_or(false),
735 Item::Attn { l, .. } => graph.attn_ok(l),
736 };
737 if !ok {
738 break;
739 }
740 valid += 1;
741 end += match item {
742 Item::Gdn { run, .. } => run.len(),
743 Item::Attn { .. } => 1,
744 };
745 }
746 plan.truncate(valid);
747 if plan.is_empty() {
748 return start;
749 }
750
751 let inv_freq = self.inv_freq.clone();
752 let pool = self.pool.clone();
753 let (nh, nkv, hd, hs, rd, eps) = (
754 self.num_heads,
755 self.num_kv_heads,
756 self.head_dim,
757 self.hidden_size,
758 self.rotary_dim,
759 self.rms_eps,
760 );
761 let norm_style = self.norm_style;
762 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
763 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
764 let kv_id = self.graph_kv_id;
765 let mut pending: Vec<(usize, usize)> = Vec::new();
768 let mut dev_attn: Vec<usize> = Vec::new();
771 for item in &plan {
772 if self.loop_final_norm {
774 let item_start = match item {
775 Item::Gdn { first, .. } => *first,
776 Item::Attn { li, .. } => *li,
777 };
778 if item_start > start && self.is_loop_end(item_start - 1) {
779 graph.encode_loop_norm(&self.weights.final_norm);
780 }
781 }
782 match item {
783 Item::Gdn { run, first } => {
784 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
785 if l.linear_state.len() != want {
786 l.linear_state = vec![0f32; want];
787 }
788 }
789 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
790 .iter()
791 .map(|l| l.linear_state.as_slice())
792 .collect();
793 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
794 tracing::error!("q1 graph: GDN run refused after validation");
796 return start;
797 }
798 graph.commit();
801 pending.push((*first, run.len()));
802 }
803 Item::Attn {
804 l,
805 li,
806 q_norm,
807 k_norm,
808 output_gate,
809 bias,
810 full_gpu,
811 } => {
812 if *full_gpu {
814 let cache = &self.kv_cache.layers[*li];
815 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
816 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
817 let cpu_stored = cpu_k[0].len() / hd;
818 let p = crate::gpu::AttnDeviceParams {
819 kv_id,
820 layer: *li,
821 nh,
822 nkv,
823 hd,
824 rd,
825 position,
826 eps: eps as f32,
827 gemma,
828 output_gate: *output_gate,
829 q_norm: *q_norm,
830 k_norm: *k_norm,
831 inv_freq: &inv_freq,
832 cpu_k,
833 cpu_v,
834 cpu_stored,
835 };
836 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
837 graph.commit();
838 dev_attn.push(*li);
839 continue;
840 }
841 }
843 graph.encode_attn_prefix(l);
844 graph.sync();
845 if !pending.is_empty() {
846 let idxs: Vec<usize> =
847 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
848 let mut outs: Vec<&mut [f32]> = self
849 .kv_cache
850 .layers
851 .iter_mut()
852 .enumerate()
853 .filter(|(i, _)| idxs.binary_search(i).is_ok())
854 .map(|(_, s)| s.linear_state.as_mut_slice())
855 .collect();
856 graph.read_states(&mut outs);
857 }
858 let mut q_raw = attention::take_buf(l.wq.1);
859 let mut k = attention::take_buf(l.wk.1);
860 let mut v = attention::take_buf(l.wv.1);
861 graph.read_qkv(&mut q_raw, &mut k, &mut v);
862 let cfg = QwenAttnCfg {
863 num_heads: nh,
864 num_kv_heads: nkv,
865 head_dim: hd,
866 hidden_size: hs,
867 position,
868 inv_freq: &inv_freq,
869 rotary_dim: rd,
870 scale: self.attn_scale,
871 window: None,
872 v_norm: false,
873 q_norm: *q_norm,
874 k_norm: *k_norm,
875 output_gate: *output_gate,
876 softplus_gate: None,
877 rope_scale: 1.0,
878 bias: *bias,
879 rms_eps: eps,
880 norm_style,
881 pool: pool.as_deref(),
882 };
883 let mut ao = attention::qwen_attention_core(
884 q_raw,
885 k,
886 v,
887 &mut self.kv_cache.layers[*li],
888 &cfg,
889 );
890 graph.encode_attn_suffix(l, &ao);
891 graph.commit();
894 attention::recycle_buf(&mut ao);
895 }
896 }
897 }
898 let mut lm_rows = None;
903 if self.graph_want_logits
904 && upto.is_none()
905 && end == self.num_layers
906 && std::env::var("CMF_GPU_LMHEAD")
907 .map(|v| v != "0")
908 .unwrap_or(true)
909 {
910 if let Some(lm) = self.weights.lm_head.q1_parts() {
911 if graph.lm_head_ok(lm) {
912 graph.encode_lm_head(&self.weights.final_norm, lm);
913 lm_rows = Some(lm.1);
914 }
915 }
916 }
917 graph.sync();
918 if !pending.is_empty() {
919 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
920 let mut outs: Vec<&mut [f32]> = self
921 .kv_cache
922 .layers
923 .iter_mut()
924 .enumerate()
925 .filter(|(i, _)| idxs.binary_search(i).is_ok())
926 .map(|(_, s)| s.linear_state.as_mut_slice())
927 .collect();
928 graph.read_states(&mut outs);
929 }
930 if let Some(rows) = lm_rows {
931 let mut lg = attention::take_buf(rows.min(self.vocab_size));
932 graph.read_logits(&mut lg);
933 lg.resize(self.vocab_size, 0.0);
934 if let Some(c) = self.final_softcap {
935 for l in lg.iter_mut() {
936 *l = c * (*l / c).tanh();
937 }
938 }
939 self.graph_logits = Some(lg);
940 }
941 graph.finish(h);
942 for li in dev_attn {
946 let mut krow = attention::take_buf(nkv * hd);
947 let mut vrow = attention::take_buf(nkv * hd);
948 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
949 let cache = &mut self.kv_cache.layers[li];
950 cache.append(&krow, &vrow, &[]);
951 let n = cache.seq_len;
952 let mut imp = attention::take_buf(n);
953 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
954 cache.accumulate_imp(&imp);
955 attention::recycle_buf(&mut imp);
956 }
957 attention::recycle_buf(&mut krow);
958 attention::recycle_buf(&mut vrow);
959 }
960 end
961 }
962
963 pub fn new(
964 tokenizer: Tokenizer,
965 weights: PipelineWeights,
966 hidden_size: usize,
967 intermediate_size: usize,
968 num_heads: usize,
969 num_kv_heads: usize,
970 head_dim: usize,
971 num_layers: usize,
972 physical_layers: usize,
973 loop_final_norm: bool,
974 vocab_size: usize,
975 rms_eps: f64,
976 rope_base: f32,
977 norm_style: NormStyle,
978 max_seq_len: usize,
979 sampler_config: SamplerConfig,
980 ) -> Self {
981 let rng = match sampler_config.seed {
982 Some(s) => SplitMix64::new(s),
983 None => SplitMix64::from_entropy(),
984 };
985 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
986 let pool = Pool::from_env();
987 if let Some(p) = &pool {
988 tracing::info!("worker pool: {} threads", p.n_workers());
989 }
990 Self {
991 tokenizer: std::sync::Arc::new(tokenizer),
992 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
993 sampler_config,
994 weights,
995 hidden_size,
996 intermediate_size,
997 num_heads,
998 num_kv_heads,
999 head_dim,
1000 num_layers,
1001 physical_layers,
1002 loop_final_norm,
1003 vocab_size,
1004 rms_eps,
1005 rope_base,
1006 norm_style,
1007 rotary_dim: head_dim,
1008 attention_heads_per_layer: None,
1009 vmf_cfg: None,
1010 gdn_cfg: None,
1011 short_conv_cfg: None,
1012 mtp: None,
1013 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1014 rng,
1015 sampler_scratch: SamplerScratch::default(),
1016 inv_freq,
1017 ws: ForwardScratch::new(hidden_size),
1018 pool,
1019 model: None,
1020 dyn_force_f32: false,
1021 dyn_skill_layers: Vec::new(),
1022 dyn_active: None,
1023 dyn_blend_loaded: false,
1024 dyn_phi_layer: None,
1025 dyn_phi_ema: Vec::new(),
1026 dyn_phi_seen: 0,
1027 dyn_router: None,
1028 o1_cfg: None,
1029 o1_flags: Vec::new(),
1030 trace: false,
1031 calib_temp: 1.0,
1032 confidence_on: true,
1033 embed_multiplier: 1.0,
1034 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1035 swa: None,
1036 sliding_layers: None,
1037 inv_freq_local: None,
1038 rotary_dim_local: None,
1039 rope_scale: 1.0,
1040 rope_scale_local: 1.0,
1041 global_attn: None,
1042 inv_freq_global: None,
1043 attn_v_norm: false,
1044 final_softcap: None,
1045 graph_want_logits: false,
1046 graph_logits: None,
1047 graph_kv_id: {
1048 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1049 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1050 },
1051 }
1052 }
1053
1054 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1061 self.o1_flags = match &cfg {
1062 Some(c) => {
1063 let mut flags = c.layer_flags(self.num_layers);
1064 for (li, f) in flags.iter_mut().enumerate() {
1065 if *f
1066 && !matches!(
1067 self.weights.layers[self.phys_layer(li)].attn,
1068 AttnKind::Full { .. }
1069 )
1070 {
1071 *f = false;
1072 }
1073 }
1074 flags
1075 }
1076 None => Vec::new(),
1077 };
1078 if let Some(c) = &cfg {
1079 let n = self.o1_flags.iter().filter(|&&f| f).count();
1080 tracing::info!(
1081 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1082 self.num_layers,
1083 c.m,
1084 c.w,
1085 c.sink,
1086 c.rect
1087 );
1088 }
1089 self.o1_cfg = cfg;
1090 }
1091
1092 pub fn o1_active(&self) -> bool {
1094 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1095 }
1096
1097 fn o1_begin(&mut self) {
1099 if let Some(c) = &self.o1_cfg {
1100 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1101 for (li, &f) in self.o1_flags.iter().enumerate() {
1102 if f {
1103 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1104 }
1105 }
1106 }
1107 }
1108
1109 fn o1_seal(&mut self) {
1112 if self.o1_cfg.is_none() {
1113 return;
1114 }
1115 for li in 0..self.num_layers {
1116 if self.o1_flags.get(li).copied().unwrap_or(false) {
1117 self.kv_cache.layers[li].o1_seal(self.num_heads);
1118 }
1119 }
1120 }
1121
1122 pub fn set_trace(&mut self, on: bool) {
1124 self.trace = on;
1125 }
1126
1127 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1130 self.rng = match config.seed {
1131 Some(seed) => SplitMix64::new(seed),
1132 None => SplitMix64::from_entropy(),
1133 };
1134 self.sampler_config = config;
1135 }
1136
1137 pub fn set_confidence(&mut self, on: bool) {
1142 self.confidence_on = on;
1143 }
1144
1145 pub fn set_calib_temp(&mut self, t: f32) {
1148 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1149 }
1150
1151 pub fn calib_temp(&self) -> f32 {
1153 self.calib_temp
1154 }
1155
1156 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1159 self.rotary_dim = rotary_dim.min(self.head_dim);
1160 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1161 }
1162
1163 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1164 QwenAttnCfg {
1165 num_heads: self.num_heads,
1166 num_kv_heads: self.num_kv_heads,
1167 head_dim: self.head_dim,
1168 hidden_size: self.hidden_size,
1169 position,
1170 inv_freq: &self.inv_freq,
1171 rotary_dim: self.rotary_dim,
1172 scale: self.attn_scale,
1173 window: None,
1174 v_norm: false,
1175 q_norm: None,
1176 k_norm: None,
1177 output_gate: false,
1178 softplus_gate: None,
1179 rope_scale: self.rope_scale,
1180 bias: None,
1181 rms_eps: self.rms_eps,
1182 norm_style: self.norm_style,
1183 pool: self.pool.as_deref(),
1184 }
1185 }
1186
1187 pub fn generate(
1189 &mut self,
1190 prompt: &str,
1191 max_tokens: usize,
1192 task_mask: Option<&TaskMask>,
1193 on_token: Option<TokenCallback>,
1194 ) -> Result<GenerateResult, String> {
1195 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1196 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1197 }
1198
1199 pub fn generate_from_ids(
1207 &mut self,
1208 input_ids: &[u32],
1209 max_tokens: usize,
1210 task_mask: Option<&TaskMask>,
1211 mut on_token: Option<TokenCallback>,
1212 ) -> Result<GenerateResult, String> {
1213 if std::env::var("CMF_TRACE_H").is_ok() {
1214 eprintln!("input_ids: {input_ids:?}");
1215 }
1216 if input_ids.is_empty() {
1217 return Err("empty prompt: nothing to generate from".to_string());
1218 }
1219
1220 self.kv_cache.clear();
1222 crate::gpu::graph_kv_reset(self.graph_kv_id);
1223 crate::gpu::graph_race_begin_generation();
1224 self.o1_begin();
1225
1226 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1232 .map(|v| v != "0")
1233 .unwrap_or_else(|_| {
1234 crate::gpu::wgpu_graph_default()
1238 });
1239 let spec_active = self.speculative
1240 && self.mtp.is_some()
1241 && task_mask.is_none()
1242 && !self.o1_active()
1243 && !graph_on
1244 && self.sampler_config.temperature < 1e-6;
1245 let mut mtp = if spec_active { self.mtp.take() } else { None };
1248 if let Some(m) = &mut mtp {
1249 m.kv.clear();
1250 }
1251 let mut router = if mtp.is_none() {
1255 self.dyn_router.take()
1256 } else {
1257 None
1258 };
1259 if let Some(r) = &mut router {
1260 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1263 }
1264
1265 let mut all_ids = input_ids.to_vec();
1266 let mut generated = 0usize;
1267 let mut finish_reason = "max_tokens".to_string();
1268 let mut drafted = 0usize;
1269 let mut accepted = 0usize;
1270 let mut confidence: Vec<f32> = Vec::new();
1271 let trace_on = self.trace;
1272 let calib_temp = self.calib_temp;
1273 let mut traces: Vec<TokenTrace> = Vec::new();
1274
1275 let mut hidden = vec![0.0f32; self.hidden_size];
1281 let mut pos = 0usize;
1282 let fuse_lm = mtp.is_none() && router.is_none();
1286 self.graph_logits = None;
1287 self.graph_want_logits = false;
1288 let dyn_prefill = router.is_some();
1293 let graph_prefill = self.graph_prefill_preferred();
1299 if task_mask.is_none()
1300 && !dyn_prefill
1301 && !graph_prefill
1302 && prefill_batched()
1303 && input_ids.len() > 2
1304 {
1305 let chunk = prefill_chunk();
1311 let hs = self.hidden_size;
1312 while pos < input_ids.len() {
1313 let end = (pos + chunk).min(input_ids.len());
1314 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1315 if let Some(m) = &mut mtp {
1316 for p in pos..end {
1317 if p + 1 < input_ids.len() {
1318 let _ = self.mtp_step(
1319 m,
1320 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1321 input_ids[p + 1],
1322 p,
1323 );
1324 }
1325 }
1326 }
1327 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1328 pos = end;
1329 }
1330 }
1331 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1332 while pos + 1 < input_ids.len() {
1333 let e1 = self.embed_single(input_ids[pos]);
1334 let e2 = self.embed_single(input_ids[pos + 1]);
1335 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1336 self.commit_linear_scratch();
1338 if let Some(m) = &mut mtp {
1339 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1340 if pos + 2 < input_ids.len() {
1341 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1342 }
1343 }
1344 hidden = h2;
1345 pos += 2;
1346 }
1347 }
1348 let _tpf = std::time::Instant::now();
1357 let batch_k = std::env::var("CMF_BATCH_K")
1358 .ok()
1359 .and_then(|v| v.parse::<usize>().ok())
1360 .unwrap_or(0);
1361 if batch_k > 0
1362 && graph_prefill
1363 && task_mask.is_none()
1364 && mtp.is_none()
1365 && !dyn_prefill
1366 && pos + 1 < input_ids.len()
1367 {
1368 let hs = self.hidden_size;
1369 let chunk = batch_k;
1370 while pos < input_ids.len() {
1371 let end = (pos + chunk).min(input_ids.len());
1372 let bk = end - pos;
1373 let mut hiddens = vec![0f32; bk * hs];
1374 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1375 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1376 }
1377 let positions: Vec<usize> = (pos..end).collect();
1378 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1379 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1380 pos = end;
1381 } else {
1382 break; }
1384 }
1385 }
1386 while pos < input_ids.len() {
1387 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1388 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1389 if let Some(m) = &mut mtp {
1390 if pos + 1 < input_ids.len() {
1391 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1392 }
1393 }
1394 pos += 1;
1395 }
1396 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1397 eprintln!(
1398 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1399 input_ids.len(),
1400 _tpf.elapsed().as_secs_f64() * 1000.0
1401 );
1402 }
1403 self.o1_seal();
1406
1407 macro_rules! commit {
1409 ($id:expr) => {{
1410 all_ids.push($id);
1411 generated += 1;
1412 if self.tokenizer.is_eos($id) {
1413 finish_reason = "stop".to_string();
1414 false
1415 } else {
1416 let token_text = self.tokenizer.decode_token($id);
1417 let mut go = true;
1418 if let Some(ref mut cb) = on_token {
1419 if !cb(&token_text) {
1420 finish_reason = "cancelled".to_string();
1421 go = false;
1422 }
1423 }
1424 go
1425 }
1426 }};
1427 }
1428
1429 let mut next_pos = input_ids.len();
1431 'decode: while generated < max_tokens {
1432 let mut logits = match self.graph_logits.take() {
1433 Some(lg) => lg,
1434 None => {
1435 inference::rms_norm_into(
1436 &hidden,
1437 &self.weights.final_norm,
1438 self.rms_eps,
1439 self.norm_style,
1440 &mut self.ws.n1,
1441 );
1442 self.lm_head_forward(&self.ws.n1)
1443 }
1444 };
1445 let t_next = sampler::sample_with_scratch(
1446 &logits,
1447 &self.sampler_config,
1448 &all_ids,
1449 &mut self.rng,
1450 &mut self.sampler_scratch,
1451 );
1452 if self.confidence_on {
1453 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1454 }
1455 attention::recycle_buf(&mut logits);
1456 if trace_on {
1457 let skill = router.as_ref().and_then(|r| r.active_id());
1461 traces.push(TokenTrace {
1462 t: generated,
1463 token_id: t_next,
1464 confidence: confidence.last().copied().unwrap_or(0.0),
1465 active_skill: skill,
1466 recon: None,
1467 switched: false,
1468 });
1469 }
1470 if !commit!(t_next) {
1471 break 'decode;
1472 }
1473 if generated >= max_tokens {
1474 break 'decode;
1475 }
1476
1477 if self.kv_cache.needs_eviction() {
1478 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1479 self.kv_cache.evict(keep);
1480 }
1481
1482 match &mut mtp {
1483 Some(m) if generated + 1 < max_tokens => {
1485 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1486 drafted += 1;
1487 let emb1 = self.embed_single(t_next);
1488 let emb2 = self.embed_single(draft);
1489 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1490
1491 inference::rms_norm_into(
1492 &h1,
1493 &self.weights.final_norm,
1494 self.rms_eps,
1495 self.norm_style,
1496 &mut self.ws.n1,
1497 );
1498 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1499 let t_after = sampler::sample_with_scratch(
1500 &logits1,
1501 &self.sampler_config,
1502 &all_ids,
1503 &mut self.rng,
1504 &mut self.sampler_scratch,
1505 );
1506 if self.confidence_on {
1507 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1508 }
1509 attention::recycle_buf(&mut logits1);
1510 if trace_on {
1511 traces.push(TokenTrace {
1514 t: generated,
1515 token_id: t_after,
1516 confidence: confidence.last().copied().unwrap_or(0.0),
1517 active_skill: None,
1518 recon: None,
1519 switched: false,
1520 });
1521 }
1522 let stop = !commit!(t_after);
1523
1524 if t_after == draft {
1525 accepted += 1;
1526 self.commit_linear_scratch();
1527 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1528 hidden = h2;
1529 next_pos += 2;
1530 } else {
1531 for layer in &mut self.kv_cache.layers {
1533 layer.truncate_last(1);
1534 }
1535 if !stop {
1536 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1537 hidden = self.forward_layers(
1538 &self.embed_single(t_after),
1539 next_pos + 1,
1540 None,
1541 );
1542 }
1543 next_pos += 2;
1544 }
1545 if stop {
1546 break 'decode;
1547 }
1548 }
1549 _ => {
1551 self.graph_want_logits = fuse_lm;
1552 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1553 next_pos += 1;
1554 if let Some(r) = &mut router {
1557 let phi = self.dyn_phi_ema.clone();
1558 let decision = r.step(&phi, generated);
1559 if let Some(new_active) = decision {
1560 let _ = self.set_active_skill(new_active);
1561 }
1562 if trace_on {
1565 if let Some(last) = traces.last_mut() {
1566 let e = r.last_best_e();
1567 last.recon = e.is_finite().then_some(e);
1568 last.switched = decision.is_some();
1569 }
1570 }
1571 }
1572 }
1573 }
1574 }
1575
1576 self.graph_want_logits = false;
1577 self.graph_logits = None;
1578 if router.is_some() {
1580 let _ = self.set_active_skill(None);
1581 }
1582 self.dyn_router = router.or(self.dyn_router.take());
1583 self.mtp = mtp.or(self.mtp.take());
1584
1585 let output_ids = &all_ids[input_ids.len()..];
1586 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1588 Ok(GenerateResult {
1589 text: self.tokenizer.decode(output_ids),
1590 token_ids: output_ids.to_vec(),
1591 prompt_tokens: input_ids.len(),
1592 tokens_generated: generated,
1593 finish_reason,
1594 mtp_drafted: drafted,
1595 mtp_accepted: accepted,
1596 token_confidence: confidence,
1597 traces,
1598 })
1599 }
1600
1601 fn mtp_step(
1605 &mut self,
1606 m: &mut MtpModule,
1607 hidden: &[f32],
1608 next_token: u32,
1609 position: usize,
1610 ) -> u32 {
1611 let e = self.embed_single(next_token);
1615 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1616 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1617 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1618 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1619 let mut x = vec![0.0f32; self.hidden_size];
1620 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1621
1622 let lw = &m.layer;
1624 inference::rms_norm_into(
1625 &x,
1626 &lw.input_norm,
1627 self.rms_eps,
1628 self.norm_style,
1629 &mut self.ws.n1,
1630 );
1631 let attn = match &lw.attn {
1632 AttnKind::Full {
1633 wq,
1634 wk,
1635 wv,
1636 wo,
1637 q_norm,
1638 k_norm,
1639 output_gate,
1640 softplus_gate,
1641 bias,
1642 } => {
1643 let mut cfg = self.attn_cfg(position);
1644 cfg.q_norm = q_norm.as_deref();
1645 cfg.k_norm = k_norm.as_deref();
1646 cfg.output_gate = *output_gate;
1647 cfg.softplus_gate = softplus_gate
1648 .as_ref()
1649 .map(|(gate, per_head)| (gate, *per_head));
1650 cfg.bias = bias
1651 .as_ref()
1652 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1653 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1654 }
1655 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1656 unreachable!("MTP block is full attention")
1657 }
1658 };
1659 for (i, &a) in attn.iter().enumerate() {
1660 x[i] += a;
1661 }
1662 inference::rms_norm_into(
1663 &x,
1664 &lw.post_norm,
1665 self.rms_eps,
1666 self.norm_style,
1667 &mut self.ws.p1,
1668 );
1669 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
1670 for (i, &f) in ffn.iter().enumerate() {
1671 x[i] += f;
1672 }
1673
1674 inference::rms_norm_into(
1675 &x,
1676 &m.final_norm,
1677 self.rms_eps,
1678 self.norm_style,
1679 &mut self.ws.n1,
1680 );
1681 let mut lg = self.lm_head_forward(&self.ws.n1);
1682 let draft = sampler::argmax(&lg);
1683 attention::recycle_buf(&mut lg);
1684 draft
1685 }
1686
1687 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1691 let emb1 = self.embed_single(1);
1692 let emb2 = self.embed_single(2);
1693 let pos = self.kv_cache.seq_len();
1694
1695 let t0 = std::time::Instant::now();
1696 for _ in 0..iters {
1697 let _ = self.forward_layers(&emb1, pos, None);
1698 let _ = self.forward_layers(&emb2, pos + 1, None);
1699 for l in &mut self.kv_cache.layers {
1700 l.truncate_last(2);
1701 }
1702 }
1703 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1704
1705 let t1 = std::time::Instant::now();
1706 for _ in 0..iters {
1707 let _ = self.forward_pair(&emb1, &emb2, pos);
1708 for l in &mut self.kv_cache.layers {
1709 l.truncate_last(2);
1710 }
1711 }
1712 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1713 (singles_ms, pair_ms)
1714 }
1715
1716 fn forward_pair(
1721 &mut self,
1722 emb1: &[f32],
1723 emb2: &[f32],
1724 position: usize,
1725 ) -> (Vec<f32>, Vec<f32>) {
1726 let mut h1 = emb1.to_vec();
1727 let mut h2 = emb2.to_vec();
1728 let (_nkv, _hd, hs, _rd, eps) = (
1729 self.num_kv_heads,
1730 self.head_dim,
1731 self.hidden_size,
1732 self.rotary_dim,
1733 self.rms_eps,
1734 );
1735 let pool = self.pool.clone();
1736
1737 for li in 0..self.num_layers {
1738 let lw = &self.weights.layers[self.phys_layer(li)];
1739 inference::rms_norm_into(
1742 &h1,
1743 &lw.input_norm,
1744 self.rms_eps,
1745 self.norm_style,
1746 &mut self.ws.n1,
1747 );
1748 inference::rms_norm_into(
1749 &h2,
1750 &lw.input_norm,
1751 self.rms_eps,
1752 self.norm_style,
1753 &mut self.ws.n2,
1754 );
1755
1756 let (a1, a2) = match &lw.attn {
1757 AttnKind::Linear(w) => {
1758 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1759 let layer = &mut self.kv_cache.layers[li];
1760 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1761 vmf_phase_pair(
1762 &self.ws.n1,
1763 &self.ws.n2,
1764 w,
1765 &cfg,
1766 state,
1767 scratch,
1768 self.pool.as_deref(),
1769 )
1770 }
1771 AttnKind::LinearGdn(w) => {
1772 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1773 let layer = &mut self.kv_cache.layers[li];
1774 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1775 gdn_pair(
1776 &self.ws.n1,
1777 &self.ws.n2,
1778 w,
1779 &cfg,
1780 state,
1781 scratch,
1782 self.pool.as_deref(),
1783 )
1784 }
1785 AttnKind::ShortConv(w) => {
1786 let cfg = self
1787 .short_conv_cfg
1788 .expect("short-conv layer without short_conv_cfg");
1789 let layer = &mut self.kv_cache.layers[li];
1790 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1791 short_conv_pair(
1792 &self.ws.n1,
1793 &self.ws.n2,
1794 w,
1795 &cfg,
1796 state,
1797 scratch,
1798 self.pool.as_deref(),
1799 )
1800 }
1801 AttnKind::Full {
1802 wq,
1803 wk,
1804 wv,
1805 wo,
1806 q_norm,
1807 k_norm,
1808 output_gate,
1809 softplus_gate,
1810 bias,
1811 } => {
1812 let inv_freq_l = self.layer_inv_freq(li);
1813 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1814 let cfg = QwenAttnCfg {
1815 num_heads: self.layer_num_heads(li),
1816 num_kv_heads: nkv_l,
1817 head_dim: hd_l,
1818 hidden_size: hs,
1819 position,
1820 inv_freq: &inv_freq_l,
1821 rotary_dim: rd_l,
1822 scale: self.attn_scale,
1823 window: self.layer_window(li),
1824 v_norm: self.attn_v_norm,
1825 q_norm: q_norm.as_deref(),
1826 k_norm: k_norm.as_deref(),
1827 output_gate: *output_gate,
1828 softplus_gate: softplus_gate
1829 .as_ref()
1830 .map(|(gate, per_head)| (gate, *per_head)),
1831 rope_scale: self.layer_rope_scale(li),
1832 bias: bias
1833 .as_ref()
1834 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1835 rms_eps: eps,
1836 norm_style: self.norm_style,
1837 pool: pool.as_deref(),
1838 };
1839 attention::qwen_attention_pair(
1840 &self.ws.n1,
1841 &self.ws.n2,
1842 wq,
1843 wk,
1844 wv,
1845 wo,
1846 &mut self.kv_cache.layers[li],
1847 &cfg,
1848 )
1849 }
1850 };
1851 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
1852 Some(w) => (
1853 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1854 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1855 ),
1856 None => (a1, a2),
1857 };
1858 for i in 0..self.hidden_size {
1859 h1[i] += a1[i];
1860 h2[i] += a2[i];
1861 }
1862 let (mut a1, mut a2) = (a1, a2);
1863 attention::recycle_buf(&mut a1);
1864 attention::recycle_buf(&mut a2);
1865
1866 let lw = &self.weights.layers[self.phys_layer(li)];
1867 inference::rms_norm_into(
1868 &h1,
1869 &lw.post_norm,
1870 self.rms_eps,
1871 self.norm_style,
1872 &mut self.ws.p1,
1873 );
1874 inference::rms_norm_into(
1875 &h2,
1876 &lw.post_norm,
1877 self.rms_eps,
1878 self.norm_style,
1879 &mut self.ws.p2,
1880 );
1881 let (f1, f2) =
1882 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref(), None);
1883 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
1884 Some(w) => (
1885 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1886 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1887 ),
1888 None => (f1, f2),
1889 };
1890 for i in 0..self.hidden_size {
1891 h1[i] += f1[i];
1892 h2[i] += f2[i];
1893 }
1894 let (mut f1, mut f2) = (f1, f2);
1895 attention::recycle_buf(&mut f1);
1896 attention::recycle_buf(&mut f2);
1897 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
1898 for i in 0..self.hidden_size {
1899 h1[i] *= sc;
1900 h2[i] *= sc;
1901 }
1902 }
1903 if self.is_loop_end(li) && li + 1 < self.num_layers {
1905 h1 = inference::rms_norm(
1906 &h1,
1907 &self.weights.final_norm,
1908 self.rms_eps,
1909 self.norm_style,
1910 );
1911 h2 = inference::rms_norm(
1912 &h2,
1913 &self.weights.final_norm,
1914 self.rms_eps,
1915 self.norm_style,
1916 );
1917 }
1918 }
1919 (h1, h2)
1920 }
1921
1922 fn commit_linear_scratch(&mut self) {
1924 for layer in &mut self.kv_cache.layers {
1925 if !layer.linear_scratch.is_empty() {
1926 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1927 layer.linear_scratch.clear();
1928 }
1929 }
1930 }
1931
1932 pub fn forward_ids(
1935 &mut self,
1936 ids: &[u32],
1937 task_mask: Option<&TaskMask>,
1938 ) -> Result<Vec<f32>, String> {
1939 if ids.is_empty() {
1940 return Err("empty id sequence".to_string());
1941 }
1942 self.kv_cache.clear();
1943 self.o1_begin();
1944 let mut hidden = vec![0.0f32; self.hidden_size];
1945 let mut pos = 0usize;
1946 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1947 let chunk = prefill_chunk();
1951 let hs = self.hidden_size;
1952 while pos < ids.len() {
1953 let end = (pos + chunk).min(ids.len());
1954 let hb = self.prefill_batch(&ids[pos..end], pos);
1955 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1956 pos = end;
1957 }
1958 }
1959 if task_mask.is_none() {
1960 while pos + 1 < ids.len() {
1961 let e1 = self.embed_single(ids[pos]);
1962 let e2 = self.embed_single(ids[pos + 1]);
1963 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1964 self.commit_linear_scratch();
1965 hidden = h2;
1966 pos += 2;
1967 }
1968 }
1969 while pos < ids.len() {
1970 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1971 pos += 1;
1972 }
1973 self.o1_seal();
1977 let normed = inference::rms_norm(
1978 &hidden,
1979 &self.weights.final_norm,
1980 self.rms_eps,
1981 self.norm_style,
1982 );
1983 Ok(self.lm_head_forward(&normed))
1984 }
1985
1986 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1993 let (nll, cnt) = self.nll_ids_from(ids, 0);
1994 (nll / cnt.max(1) as f64).exp()
1995 }
1996
1997 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2002 self.kv_cache.clear();
2003 FFN_PROBE.with(|p| {
2004 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2005 });
2006 crate::gpu::cpu_scope(|| {
2007 for (pos, &id) in ids.iter().enumerate() {
2008 let emb = self.embed_single(id);
2009 let _ = self.forward_layers(&emb, pos, None);
2010 }
2011 });
2012 self.kv_cache.clear();
2013 FFN_PROBE
2014 .with(|p| p.borrow_mut().take())
2015 .unwrap_or_default()
2016 }
2017
2018 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2022 self.kv_cache.clear();
2023 let mut nll = 0f64;
2024 let mut cnt = 0usize;
2025 let mut hidden = vec![0f32; self.hidden_size];
2026 for (pos, &id) in ids.iter().enumerate() {
2027 if pos > 0 {
2028 inference::rms_norm_into(
2029 &hidden,
2030 &self.weights.final_norm,
2031 self.rms_eps,
2032 self.norm_style,
2033 &mut self.ws.n1,
2034 );
2035 let mut logits = self.lm_head_forward(&self.ws.n1);
2036 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2037 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2038 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2039 nll -= p.max(1e-300).ln();
2040 cnt += 1;
2041 attention::recycle_buf(&mut logits);
2042 }
2043 let emb = self.embed_single(id);
2044 hidden = self.forward_layers(&emb, pos, Some(mask));
2045 }
2046 self.kv_cache.clear();
2047 (nll / cnt.max(1) as f64).exp()
2048 }
2049
2050 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2059 self.kv_cache.clear();
2060 let mut nll = 0f64;
2061 let mut cnt = 0usize;
2062 if prefill_batched() {
2063 const CHUNK: usize = 128;
2069 const LM_SUB: usize = 32;
2070 let n = ids.len().saturating_sub(1);
2071 let hs = self.hidden_size;
2072 let rows = self.weights.lm_head.rows();
2073 let mut pos = 0usize;
2074 while pos < n {
2075 let end = (pos + CHUNK).min(n);
2076 let bsz = end - pos;
2077 let hb = self.prefill_batch(&ids[pos..end], pos);
2078 let mut k0 = 0usize;
2079 while k0 < bsz {
2080 let k1 = (k0 + LM_SUB).min(bsz);
2081 let sb = k1 - k0;
2082 if pos + k1 <= start {
2085 k0 = k1;
2086 continue;
2087 }
2088 let mut normed = vec![0.0f32; sb * hs];
2089 for k in 0..sb {
2090 let r = inference::rms_norm(
2091 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2092 &self.weights.final_norm,
2093 self.rms_eps,
2094 self.norm_style,
2095 );
2096 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2097 }
2098 let mut logits = vec![0.0f32; sb * rows];
2099 self.weights
2100 .lm_head
2101 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2102 for k in 0..sb {
2103 if pos + k0 + k < start {
2104 continue;
2105 }
2106 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2107 let target = ids[pos + k0 + k + 1] as usize;
2108 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2109 let lse: f64 = lg
2110 .iter()
2111 .map(|&v| ((v - max) as f64).exp())
2112 .sum::<f64>()
2113 .ln()
2114 + max as f64;
2115 nll += lse - lg[target] as f64;
2116 cnt += 1;
2117 }
2118 k0 = k1;
2119 }
2120 pos = end;
2121 }
2122 self.kv_cache.clear();
2123 return (nll, cnt);
2124 }
2125 for pos in 0..ids.len().saturating_sub(1) {
2126 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2127 if pos < start {
2128 continue;
2129 }
2130 let normed = inference::rms_norm(
2131 &hidden,
2132 &self.weights.final_norm,
2133 self.rms_eps,
2134 self.norm_style,
2135 );
2136 let logits = self.lm_head_forward(&normed);
2137 let target = ids[pos + 1] as usize;
2138 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2139 let lse: f64 = logits
2140 .iter()
2141 .map(|&v| ((v - max) as f64).exp())
2142 .sum::<f64>()
2143 .ln()
2144 + max as f64;
2145 nll += lse - logits[target] as f64;
2146 cnt += 1;
2147 }
2148 self.kv_cache.clear();
2149 (nll, cnt)
2150 }
2151
2152 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2168 self.kv_cache.clear();
2169 self.o1_begin();
2170 let n = ids.len().saturating_sub(1);
2171 let p = prefill.min(n);
2172 let mut pos = 0usize;
2174 if prefill_batched() {
2175 const CHUNK: usize = 128;
2176 while pos < p {
2177 let end = (pos + CHUNK).min(p);
2178 let _ = self.prefill_batch(&ids[pos..end], pos);
2179 pos = end;
2180 }
2181 } else {
2182 while pos < p {
2183 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2184 pos += 1;
2185 }
2186 }
2187 self.o1_seal();
2188
2189 let mut nll = 0f64;
2190 let mut cnt = 0usize;
2191 for pos in p..n {
2192 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2193 let normed = inference::rms_norm(
2194 &hidden,
2195 &self.weights.final_norm,
2196 self.rms_eps,
2197 self.norm_style,
2198 );
2199 let logits = self.lm_head_forward(&normed);
2200 let target = ids[pos + 1] as usize;
2201 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2202 let lse: f64 = logits
2203 .iter()
2204 .map(|&v| ((v - max) as f64).exp())
2205 .sum::<f64>()
2206 .ln()
2207 + max as f64;
2208 nll += lse - logits[target] as f64;
2209 cnt += 1;
2210 }
2211 self.kv_cache.clear();
2212 (nll, cnt)
2213 }
2214
2215 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2223 self.kv_cache.clear();
2224 let n = ids.len().saturating_sub(1);
2225 let mut correct = Vec::with_capacity(n);
2226 let mut pmax = Vec::with_capacity(n);
2227 for pos in 0..n {
2228 let emb = self.embed_single(ids[pos]);
2229 let hidden = self.forward_layers(&emb, pos, None);
2230 let normed = inference::rms_norm(
2231 &hidden,
2232 &self.weights.final_norm,
2233 self.rms_eps,
2234 self.norm_style,
2235 );
2236 let logits = self.lm_head_forward(&normed);
2237 let target = ids[pos + 1] as usize;
2238 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2239 for (i, &v) in logits.iter().enumerate() {
2240 if v > mval {
2241 mval = v;
2242 amax = i;
2243 }
2244 }
2245 correct.push(amax == target);
2246 let row: Vec<f32> = temps
2247 .iter()
2248 .map(|&t| {
2249 let tt = t.max(1e-3);
2250 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2251 1.0 / s.max(1e-12) })
2253 .collect();
2254 pmax.push(row);
2255 }
2256 self.kv_cache.clear();
2257 (correct, pmax)
2258 }
2259
2260 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2267 let mut router = match self.dyn_router.take() {
2268 Some(r) => r,
2269 None => return (self.ppl_ids(ids), 0),
2270 };
2271 router.reset();
2272 self.dyn_phi_seen = 0;
2273 let _ = self.set_active_skill(None);
2274
2275 self.kv_cache.clear();
2276 let mut nll = 0f64;
2277 let mut cnt = 0usize;
2278 for pos in 0..ids.len().saturating_sub(1) {
2279 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2280 let normed = inference::rms_norm(
2281 &hidden,
2282 &self.weights.final_norm,
2283 self.rms_eps,
2284 self.norm_style,
2285 );
2286 let logits = self.lm_head_forward(&normed);
2287 let target = ids[pos + 1] as usize;
2288 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2289 let lse: f64 = logits
2290 .iter()
2291 .map(|&v| ((v - max) as f64).exp())
2292 .sum::<f64>()
2293 .ln()
2294 + max as f64;
2295 nll += lse - logits[target] as f64;
2296 cnt += 1;
2297 let phi = self.dyn_phi_ema.clone();
2299 if let Some(new_active) = router.step(&phi, pos) {
2300 let _ = self.set_active_skill(new_active);
2301 }
2302 }
2303 let switches = router.switches.len();
2304 let _ = self.set_active_skill(None);
2305 self.dyn_router = Some(router);
2306 self.kv_cache.clear();
2307 ((nll / cnt.max(1) as f64).exp(), switches)
2308 }
2309
2310 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2312 self.kv_cache.clear();
2313 let mut acc = vec![0f32; self.hidden_size];
2314 for (pos, &id) in ids.iter().enumerate() {
2315 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2316 for (a, v) in acc.iter_mut().zip(&h) {
2317 *a += v;
2318 }
2319 }
2320 let n = ids.len().max(1) as f32;
2321 for a in acc.iter_mut() {
2322 *a /= n;
2323 }
2324 self.kv_cache.clear();
2325 acc
2326 }
2327
2328 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2334 let b = ids.len();
2335 let hs = self.hidden_size;
2336 let mut h: Vec<f32> = vec![0.0; b * hs];
2339 let mut h_ready = false;
2340 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2341 for (bi, &id) in ids.iter().enumerate() {
2342 let e = me.embed_single(id);
2343 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2344 }
2345 };
2346 let (_nkv, _hd, _rd, eps) = (
2347 self.num_kv_heads,
2348 self.head_dim,
2349 self.rotary_dim,
2350 self.rms_eps,
2351 );
2352 let pool = self.pool.clone();
2353 let norm_style = self.norm_style;
2354
2355 #[cfg(target_os = "macos")]
2356 let mut chunk_skip_until = 0usize;
2357 for li in 0..self.num_layers {
2358 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2365 {
2366 if li < chunk_skip_until {
2367 continue;
2368 }
2369 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2370 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2371 if end > li {
2372 h_ready = true;
2373 chunk_skip_until = end;
2374 if self.is_loop_end(end - 1) && end < self.num_layers {
2377 for bi in 0..b {
2378 let normed = inference::rms_norm(
2379 &h[bi * hs..(bi + 1) * hs],
2380 &self.weights.final_norm,
2381 eps,
2382 norm_style,
2383 );
2384 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2385 }
2386 }
2387 continue;
2388 }
2389 }
2390 if !h_ready {
2391 fill_h(&mut h, self);
2392 h_ready = true;
2393 }
2394 let lw = &self.weights.layers[self.phys_layer(li)];
2395 match &lw.attn {
2397 AttnKind::LinearGdn(w) => {
2398 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2400 let mut normed = vec![0.0f32; b * hs];
2401 for bi in 0..b {
2402 let r = inference::rms_norm(
2403 &h[bi * hs..(bi + 1) * hs],
2404 &lw.input_norm,
2405 eps,
2406 norm_style,
2407 );
2408 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2409 }
2410 let attn = crate::linear_core::gdn_forward_batch(
2411 &normed,
2412 b,
2413 w,
2414 &cfg,
2415 &mut self.kv_cache.layers[li].linear_state,
2416 pool.as_deref(),
2417 );
2418 for (dst, &a) in h.iter_mut().zip(&attn) {
2419 *dst += a;
2420 }
2421 }
2422 AttnKind::ShortConv(w) => {
2423 let cfg = self
2426 .short_conv_cfg
2427 .expect("short-conv layer without short_conv_cfg");
2428 let mut normed = vec![0.0f32; b * hs];
2429 for bi in 0..b {
2430 inference::rms_norm_into(
2431 &h[bi * hs..(bi + 1) * hs],
2432 &lw.input_norm,
2433 eps,
2434 norm_style,
2435 &mut normed[bi * hs..(bi + 1) * hs],
2436 );
2437 }
2438 let attn = short_conv_forward_batch(
2439 &normed,
2440 b,
2441 w,
2442 &cfg,
2443 &mut self.kv_cache.layers[li].linear_state,
2444 pool.as_deref(),
2445 );
2446 for (dst, &a) in h.iter_mut().zip(&attn) {
2447 *dst += a;
2448 }
2449 }
2450 AttnKind::Full {
2451 wq,
2452 wk,
2453 wv,
2454 wo,
2455 q_norm,
2456 k_norm,
2457 output_gate,
2458 softplus_gate,
2459 bias,
2460 } => {
2461 let mut normed = vec![0.0f32; b * hs];
2465 for bi in 0..b {
2466 inference::rms_norm_into(
2467 &h[bi * hs..(bi + 1) * hs],
2468 &lw.input_norm,
2469 eps,
2470 norm_style,
2471 &mut normed[bi * hs..(bi + 1) * hs],
2472 );
2473 }
2474 let inv_freq_l = self.layer_inv_freq(li);
2475 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2476 let cfg = QwenAttnCfg {
2477 num_heads: self.layer_num_heads(li),
2478 num_kv_heads: nkv_l,
2479 head_dim: hd_l,
2480 hidden_size: hs,
2481 position: start_pos,
2482 inv_freq: &inv_freq_l,
2483 rotary_dim: rd_l,
2484 scale: self.attn_scale,
2485 window: self.layer_window(li),
2486 v_norm: self.attn_v_norm,
2487 q_norm: q_norm.as_deref(),
2488 k_norm: k_norm.as_deref(),
2489 output_gate: *output_gate,
2490 softplus_gate: softplus_gate
2491 .as_ref()
2492 .map(|(gate, per_head)| (gate, *per_head)),
2493 rope_scale: self.layer_rope_scale(li),
2494 bias: bias
2495 .as_ref()
2496 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2497 rms_eps: eps,
2498 norm_style,
2499 pool: pool.as_deref(),
2500 };
2501 let mut attn = attention::qwen_attention_batch(
2502 &normed,
2503 b,
2504 wq,
2505 wk,
2506 wv,
2507 wo,
2508 &mut self.kv_cache.layers[li],
2509 &cfg,
2510 );
2511 if let Some(w) = &lw.attn_out_norm {
2512 for bi in 0..b {
2513 inference::rms_norm_into(
2514 &attn[bi * hs..(bi + 1) * hs],
2515 w,
2516 eps,
2517 norm_style,
2518 &mut normed[bi * hs..(bi + 1) * hs],
2519 );
2520 }
2521 attn.copy_from_slice(&normed);
2522 }
2523 for (dst, &a) in h.iter_mut().zip(&attn) {
2524 *dst += a;
2525 }
2526 }
2527 AttnKind::Linear(w) => {
2528 for bi in 0..b {
2529 let normed = inference::rms_norm(
2530 &h[bi * hs..(bi + 1) * hs],
2531 &lw.input_norm,
2532 eps,
2533 norm_style,
2534 );
2535 vmf_phase_forward(
2536 &normed,
2537 w,
2538 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2539 &mut self.kv_cache.layers[li].linear_state,
2540 pool.as_deref(),
2541 )
2542 .iter()
2543 .enumerate()
2544 .for_each(|(i, &a)| h[bi * hs + i] += a);
2545 }
2546 }
2547 }
2548
2549 let lw = &self.weights.layers[self.phys_layer(li)];
2551 let mut post = vec![0.0f32; b * hs];
2552 for bi in 0..b {
2553 let r =
2554 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2555 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2556 }
2557 let mut ffn = match &lw.ffn {
2558 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2559 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
2560 };
2561 if let Some(w) = &lw.ffn_out_norm {
2562 for bi in 0..b {
2563 inference::rms_norm_into(
2564 &ffn[bi * hs..(bi + 1) * hs],
2565 w,
2566 eps,
2567 norm_style,
2568 &mut post[bi * hs..(bi + 1) * hs],
2569 );
2570 }
2571 ffn.copy_from_slice(&post);
2572 }
2573 for (dst, &f) in h.iter_mut().zip(&ffn) {
2574 *dst += f;
2575 }
2576 if let Some(sc) = lw.layer_scale {
2577 for v in h.iter_mut() {
2578 *v *= sc;
2579 }
2580 }
2581 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
2585 let row = &h[(b - 1) * hs..b * hs];
2586 let rms =
2587 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
2588 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
2589 eprintln!(
2590 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
2591 match &self.weights.layers[self.phys_layer(li)].attn {
2592 AttnKind::LinearGdn(_) => "gdn",
2593 AttnKind::Linear(_) => "vmf",
2594 AttnKind::ShortConv(_) => "conv",
2595 _ => "attn",
2596 },
2597 match &lw.ffn {
2598 FfnKind::Moe(_) => "moe",
2599 FfnKind::Dense(_) => "dense",
2600 },
2601 );
2602 }
2603 if self.is_loop_end(li) && li + 1 < self.num_layers {
2605 for bi in 0..b {
2606 let normed = inference::rms_norm(
2607 &h[bi * hs..(bi + 1) * hs],
2608 &self.weights.final_norm,
2609 eps,
2610 norm_style,
2611 );
2612 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
2613 }
2614 }
2615 if std::env::var("CMF_TRACE_H").is_ok() {
2616 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2617 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2618 eprintln!(
2619 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
2620 lw.layer_scale
2621 );
2622 }
2623 }
2624 crate::gpu::set_layer(-1); h
2626 }
2627
2628 fn embed_single(&self, id: u32) -> Vec<f32> {
2630 let mut out = vec![0.0f32; self.hidden_size];
2631 if (id as usize) < self.weights.embed_tokens.rows() {
2632 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2633 }
2634 if self.embed_multiplier != 1.0 {
2635 for v in out.iter_mut() {
2636 *v *= self.embed_multiplier;
2637 }
2638 }
2639 out
2640 }
2641
2642 #[cfg(target_os = "macos")]
2648 fn chunk_run_gpu(
2649 &mut self,
2650 li0: usize,
2651 h: &mut [f32],
2652 b: usize,
2653 pos0: usize,
2654 embed_ids: Option<&[u32]>,
2655 ) -> usize {
2656 if !crate::gpu::enabled_here()
2660 || std::env::var("CMF_GPU_CHUNK")
2661 .map(|v| v == "0")
2662 .unwrap_or(false)
2663 || b < 32
2664 || self.swa.is_some()
2665 || self.global_attn.is_some()
2666 || self.attn_v_norm
2667 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2668 {
2669 return li0;
2670 }
2671 let Some(model) = self.model.clone() else {
2672 return li0;
2673 };
2674 let inv_freq = self.inv_freq.clone();
2675 let (nh, nkv, hd, hs) = (
2676 self.num_heads,
2677 self.num_kv_heads,
2678 self.head_dim,
2679 self.hidden_size,
2680 );
2681 let loop_end = if self.loop_final_norm {
2685 ((li0 / self.physical_layers) + 1) * self.physical_layers
2686 } else {
2687 self.num_layers
2688 };
2689 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2690 let mut stored_at: Vec<usize> = Vec::new();
2691 for li in li0..self.num_layers.min(loop_end) {
2692 let lw = &self.weights.layers[self.phys_layer(li)];
2693 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
2694 break;
2695 }
2696 let AttnKind::Full {
2697 wq,
2698 wk,
2699 wv,
2700 wo,
2701 q_norm,
2702 k_norm,
2703 output_gate: false,
2704 softplus_gate: None,
2705 bias,
2706 } = &lw.attn
2707 else {
2708 break;
2709 };
2710 let FfnKind::Dense(d) = &lw.ffn else { break };
2711 if d.act != Act::Silu {
2712 break;
2713 }
2714 let parts = (
2715 wq.q8_row_parts(),
2716 wk.q8_row_parts(),
2717 wv.q8_row_parts(),
2718 wo.q8_row_parts(),
2719 d.gate_proj.q8_row_parts(),
2720 d.up_proj.q8_row_parts(),
2721 d.down_proj.q8_row_parts(),
2722 );
2723 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2724 else {
2725 break;
2726 };
2727 let layer = &self.kv_cache.layers[li];
2728 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2729 break;
2730 }
2731 stored_at.push(layer.head_len(0));
2732 layers.push(crate::gpu_metal::ChunkLayer {
2733 model: &model,
2734 kv_id: self.graph_kv_id,
2735 layer: li,
2736 wq: pq,
2737 wk: pk,
2738 wv: pv,
2739 wo: po,
2740 gate: pg,
2741 up: pu,
2742 down: pd,
2743 input_norm: &lw.input_norm,
2744 post_norm: &lw.post_norm,
2745 bias: bias
2746 .as_ref()
2747 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2748 q_norm: q_norm.as_deref(),
2749 k_norm: k_norm.as_deref(),
2750 inv_freq: &inv_freq,
2751 rd: self.rotary_dim,
2752 nh,
2753 nkv,
2754 hd,
2755 hs,
2756 inter: d.gate_proj.rows(),
2757 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2758 eps: self.rms_eps as f32,
2759 });
2760 }
2761 if layers.is_empty() {
2762 return li0;
2763 }
2764 let row = nkv * hd;
2765 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2766 .iter()
2767 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2768 .collect();
2769 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2770 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2771 let li = layers[i].layer;
2772 let layer = &self.kv_cache.layers[li];
2773 io.push(crate::gpu_metal::ChunkIo {
2774 cpu_stored: stored_at[i],
2775 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2776 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2777 out_k: ok,
2778 out_v: ov,
2779 imp: oi,
2780 });
2781 }
2782 let n_run = layers.len();
2783 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2784 let ep = embed_ids.and_then(|ids| {
2787 self.weights
2788 .embed_tokens
2789 .q8_row_parts()
2790 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
2791 idx,
2792 rows,
2793 row_scale: rs,
2794 ids,
2795 mult: self.embed_multiplier,
2796 })
2797 });
2798 if embed_ids.is_some() && ep.is_none() {
2799 return li0;
2800 }
2801 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2802 return li0;
2803 }
2804 drop(io);
2805 drop(layers);
2806 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2809 let li = li0 + i;
2810 let layer = &mut self.kv_cache.layers[li];
2811 for bi in 0..b {
2812 layer.append(
2813 &ok[bi * row..(bi + 1) * row],
2814 &ov[bi * row..(bi + 1) * row],
2815 &[],
2816 );
2817 }
2818 layer.accumulate_imp(oi);
2819 }
2820 last
2821 }
2822
2823 fn layer_is_local(&self, li: usize) -> bool {
2826 if let Some(layers) = &self.sliding_layers {
2827 return layers.get(li).copied().unwrap_or(false);
2828 }
2829 match self.swa {
2830 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2831 None => false,
2832 }
2833 }
2834
2835 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2838 if self.layer_is_local(li) {
2839 if let Some(f) = &self.inv_freq_local {
2840 return f.clone();
2841 }
2842 } else if let Some(f) = &self.inv_freq_global {
2843 return f.clone();
2844 }
2845 self.inv_freq.clone()
2846 }
2847
2848 fn layer_window(&self, li: usize) -> Option<usize> {
2850 self.swa
2851 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
2852 }
2853
2854 fn layer_num_heads(&self, li: usize) -> usize {
2855 self.attention_heads_per_layer
2856 .as_ref()
2857 .and_then(|v| v.get(li).copied())
2858 .unwrap_or(self.num_heads)
2859 }
2860
2861 fn layer_rope_scale(&self, li: usize) -> f32 {
2862 if self.layer_is_local(li) {
2863 self.rope_scale_local
2864 } else {
2865 self.rope_scale
2866 }
2867 }
2868
2869 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2872 if !self.layer_is_local(li) {
2873 if let Some((ghd, gkv)) = self.global_attn {
2874 return (gkv, ghd, ghd);
2875 }
2876 }
2877 (
2878 self.num_kv_heads,
2879 self.head_dim,
2880 if self.layer_is_local(li) {
2881 self.rotary_dim_local.unwrap_or(self.rotary_dim)
2882 } else {
2883 self.rotary_dim
2884 },
2885 )
2886 }
2887
2888 fn forward_layers(
2890 &mut self,
2891 hidden: &[f32],
2892 position: usize,
2893 task_mask: Option<&TaskMask>,
2894 ) -> Vec<f32> {
2895 self.forward_layers_upto(hidden, position, task_mask, None)
2896 }
2897
2898 fn try_token_graph_wgpu(
2902 &self,
2903 hidden: &[f32],
2904 position: usize,
2905 logits_out: &mut Vec<f32>,
2906 ) -> Option<Vec<f32>> {
2907 if self.o1_active() {
2910 return None;
2911 }
2912 let nh = self.num_heads;
2913 let (nkv, hd, rd) = self.layer_geom(0);
2914 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2915 let mut layers = Vec::with_capacity(self.num_layers);
2916 let mut model = None;
2917 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
2918 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2919 if let Some((_, i, kind, rs)) = t.graph_weight() {
2920 return Some(crate::gpu::GraphW {
2921 idx: i,
2922 kind,
2923 row_scale: rs,
2924 data: &[],
2925 });
2926 }
2927 t.as_f32().map(|d| crate::gpu::GraphW {
2929 idx: 0,
2930 kind: 4,
2931 row_scale: &[],
2932 data: d,
2933 })
2934 }
2935 for li in 0..self.num_layers {
2936 let lw = &self.weights.layers[self.phys_layer(li)];
2937 if dbg {
2938 let ak = match &lw.attn {
2939 AttnKind::Full {
2940 output_gate, bias, ..
2941 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
2942 AttnKind::LinearGdn(_) => "LinearGdn".into(),
2943 AttnKind::Linear(_) => "Linear".into(),
2944 AttnKind::ShortConv(_) => "ShortConv".into(),
2945 };
2946 let fk = match &lw.ffn {
2947 FfnKind::Dense(_) => "Dense",
2948 FfnKind::Moe(_) => "Moe",
2949 };
2950 eprintln!("graph L{li}: attn={ak} ffn={fk}");
2951 }
2952 let gffn = match &lw.ffn {
2953 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
2954 gate: gw(&d.gate_proj)?,
2955 up: gw(&d.up_proj)?,
2956 down: gw(&d.down_proj)?,
2957 },
2958 FfnKind::Moe(m) => {
2959 if m.router_sigmoid
2964 || m.expert_bias.is_some()
2965 || m.route_tau.is_some()
2966 || m.mask.is_some()
2967 {
2968 return None;
2969 }
2970 let (se, sg) = m.shared.as_ref()?;
2971 let sgate = gw(sg.as_ref()?)?;
2972 let router = gw(&m.router)?;
2973 let inter = m.experts.first()?.gate_proj.rows();
2974 let mut experts = Vec::with_capacity(m.experts.len() + 1);
2975 for e in m.experts.iter().chain(std::iter::once(se)) {
2976 if !matches!(e.act, Act::Silu)
2977 || e.gate_proj.rows() != inter
2978 || e.up_proj.rows() != inter
2979 {
2980 return None;
2981 }
2982 let (mm, gi) = e.gate_proj.mapped_q4t()?;
2983 let (_, ui) = e.up_proj.mapped_q4t()?;
2984 let (_, di) = e.down_proj.mapped_q4t()?;
2985 model.get_or_insert_with(|| mm.clone());
2986 experts.push((gi, ui, di));
2987 }
2988 crate::gpu::GraphFfn::Moe {
2989 router,
2990 shared_gate: sgate,
2991 experts,
2992 n_exp: m.experts.len(),
2993 top_k: m.top_k,
2994 inter,
2995 norm_topk: m.norm_topk_prob,
2996 }
2997 }
2998 };
2999 let attn = match &lw.attn {
3000 AttnKind::Full {
3001 wq,
3002 wk,
3003 wv,
3004 wo,
3005 q_norm,
3006 k_norm,
3007 output_gate,
3008 softplus_gate,
3009 bias,
3010 } => {
3011 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3012 return None;
3013 }
3014 let (m, _, _, _) = wq.graph_weight()?;
3015 model = Some(m.clone());
3016 crate::gpu::GraphAttn::Full {
3017 wq: gw(wq)?,
3018 wk: gw(wk)?,
3019 wv: gw(wv)?,
3020 wo: gw(wo)?,
3021 q_norm: q_norm.as_deref(),
3022 k_norm: k_norm.as_deref(),
3023 bias: bias
3024 .as_ref()
3025 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3026 output_gate: *output_gate,
3027 cpu_k: self.kv_cache.layers[li].k_heads(),
3028 cpu_v: self.kv_cache.layers[li].v_heads(),
3029 }
3030 }
3031 AttnKind::LinearGdn(w) => {
3032 let cfg = self.gdn_cfg?;
3033 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3034 model = Some(m.clone());
3035 crate::gpu::GraphAttn::Gdn {
3036 qkv: gw(&w.in_proj_qkv)?,
3037 z: gw(&w.in_proj_z)?,
3038 a: gw(&w.in_proj_a)?,
3039 b: gw(&w.in_proj_b)?,
3040 out: gw(&w.out_proj)?,
3041 conv1d: &w.conv1d,
3042 a_log: &w.a_log,
3043 dt_bias: &w.dt_bias,
3044 norm: &w.norm,
3045 nv: cfg.num_v_heads,
3046 nk: cfg.num_k_heads,
3047 dk: cfg.key_head_dim,
3048 dv: cfg.value_head_dim,
3049 kk: cfg.conv_kernel,
3050 }
3051 }
3052 _ => return None,
3053 };
3054 layers.push(crate::gpu::GraphLayer {
3055 input_norm: &lw.input_norm,
3056 attn,
3057 post_norm: &lw.post_norm,
3058 ffn: gffn,
3059 });
3060 }
3061 let model = model?;
3062 let lm_gw = if self.graph_want_logits
3068 && std::env::var("CMF_GPU_LMHEAD")
3069 .map(|v| v != "0")
3070 .unwrap_or(true)
3071 {
3072 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
3073 (
3074 crate::gpu::GraphW {
3075 idx: i,
3076 kind,
3077 row_scale: rs,
3078 data: &[],
3079 },
3080 self.weights.lm_head.rows(),
3081 )
3082 })
3083 } else {
3084 None
3085 };
3086 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
3087 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
3090 (0..self.num_layers - 1)
3091 .filter(|&li| (li + 1) % self.physical_layers == 0)
3092 .collect()
3093 } else {
3094 Vec::new()
3095 };
3096 let mut h = hidden.to_vec();
3097 crate::gpu::forward_token_graph(
3098 &model,
3099 self.graph_kv_id,
3100 &layers,
3101 &self.inv_freq,
3102 &mut h,
3103 nh,
3104 nkv,
3105 hd,
3106 rd,
3107 self.hidden_size,
3108 self.intermediate_size,
3109 position,
3110 self.kv_cache.max_seq_len,
3111 gemma,
3112 self.rms_eps as f32,
3113 lm,
3114 &self.weights.final_norm,
3115 logits_out,
3116 &loop_norm_at,
3117 )
3118 .then_some(h)
3119 }
3120
3121 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
3126 if self.o1_active() {
3127 return false;
3128 }
3129 let nh = self.num_heads;
3130 let (nkv, hd, rd) = self.layer_geom(0);
3131 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3132 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
3133 if let Some((_, i, kind, rs)) = t.graph_weight() {
3134 return Some(crate::gpu::GraphW {
3135 idx: i,
3136 kind,
3137 row_scale: rs,
3138 data: &[],
3139 });
3140 }
3141 t.as_f32().map(|d| crate::gpu::GraphW {
3142 idx: 0,
3143 kind: 4,
3144 row_scale: &[],
3145 data: d,
3146 })
3147 }
3148 let built: Option<(
3149 Vec<crate::gpu::GraphLayer<'_>>,
3150 std::sync::Arc<cortiq_core::CmfModel>,
3151 )> = (|| {
3152 let mut layers = Vec::with_capacity(self.num_layers);
3153 let mut model = None;
3154 for li in 0..self.num_layers {
3155 let lw = &self.weights.layers[self.phys_layer(li)];
3156 let gffn = match &lw.ffn {
3159 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
3160 gate: gw(&d.gate_proj)?,
3161 up: gw(&d.up_proj)?,
3162 down: gw(&d.down_proj)?,
3163 },
3164 _ => return None,
3165 };
3166 let attn = match &lw.attn {
3167 AttnKind::Full {
3168 wq,
3169 wk,
3170 wv,
3171 wo,
3172 q_norm,
3173 k_norm,
3174 output_gate,
3175 softplus_gate,
3176 bias,
3177 } => {
3178 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
3179 return None;
3180 }
3181 let (m, _, _, _) = wq.graph_weight()?;
3182 model = Some(m.clone());
3183 crate::gpu::GraphAttn::Full {
3184 wq: gw(wq)?,
3185 wk: gw(wk)?,
3186 wv: gw(wv)?,
3187 wo: gw(wo)?,
3188 q_norm: q_norm.as_deref(),
3189 k_norm: k_norm.as_deref(),
3190 bias: bias
3191 .as_ref()
3192 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3193 output_gate: *output_gate,
3194 cpu_k: self.kv_cache.layers[li].k_heads(),
3195 cpu_v: self.kv_cache.layers[li].v_heads(),
3196 }
3197 }
3198 AttnKind::LinearGdn(w) => {
3199 let cfg = self.gdn_cfg?;
3200 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3201 model = Some(m.clone());
3202 crate::gpu::GraphAttn::Gdn {
3203 qkv: gw(&w.in_proj_qkv)?,
3204 z: gw(&w.in_proj_z)?,
3205 a: gw(&w.in_proj_a)?,
3206 b: gw(&w.in_proj_b)?,
3207 out: gw(&w.out_proj)?,
3208 conv1d: &w.conv1d,
3209 a_log: &w.a_log,
3210 dt_bias: &w.dt_bias,
3211 norm: &w.norm,
3212 nv: cfg.num_v_heads,
3213 nk: cfg.num_k_heads,
3214 dk: cfg.key_head_dim,
3215 dv: cfg.value_head_dim,
3216 kk: cfg.conv_kernel,
3217 }
3218 }
3219 _ => return None,
3220 };
3221 layers.push(crate::gpu::GraphLayer {
3222 input_norm: &lw.input_norm,
3223 attn,
3224 post_norm: &lw.post_norm,
3225 ffn: gffn,
3226 });
3227 }
3228 Some((layers, model?))
3229 })();
3230 let Some((layers, model)) = built else {
3231 return false;
3232 };
3233 crate::gpu::forward_batch_graph(
3234 &model,
3235 self.graph_kv_id,
3236 &layers,
3237 &self.inv_freq,
3238 hiddens,
3239 nh,
3240 nkv,
3241 hd,
3242 rd,
3243 self.hidden_size,
3244 self.intermediate_size,
3245 positions,
3246 self.kv_cache.max_seq_len,
3247 gemma,
3248 self.rms_eps as f32,
3249 k,
3250 )
3251 }
3252
3253 fn forward_layers_upto(
3255 &mut self,
3256 hidden: &[f32],
3257 position: usize,
3258 task_mask: Option<&TaskMask>,
3259 upto: Option<usize>,
3260 ) -> Vec<f32> {
3261 let mut h = hidden.to_vec();
3262 let (nh, _nkv, _hd, hs, _rd, eps) = (
3265 self.num_heads,
3266 self.num_kv_heads,
3267 self.head_dim,
3268 self.hidden_size,
3269 self.rotary_dim,
3270 self.rms_eps,
3271 );
3272 let pool = self.pool.clone();
3273 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
3285 let graph_on = match graph_env.as_deref() {
3286 Some("0") => false,
3287 Some(_) => true,
3288 None => {
3289 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
3290 || crate::gpu::wgpu_active()
3291 }
3292 };
3293 let graph_trusted =
3294 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
3295 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
3296 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
3297 let t_graph = std::time::Instant::now();
3298 let mut lg = Vec::new();
3299 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
3300 let dur = t_graph.elapsed();
3301 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3302 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
3303 }
3304 if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
3305 if !graph_trusted {
3306 crate::gpu::graph_race_record(true, dur);
3307 }
3308 if !lg.is_empty() {
3309 lg.resize(self.vocab_size, 0.0);
3312 if let Some(c) = self.final_softcap {
3313 for l in lg.iter_mut() {
3314 *l = c * (*l / c).tanh();
3315 }
3316 }
3317 self.graph_logits = Some(lg);
3318 }
3319 return hh;
3320 }
3321 }
3327 }
3328 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
3329
3330 #[cfg(target_os = "macos")]
3331 let mut gpu_skip_until = 0usize;
3332 for li in 0..self.num_layers {
3333 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
3335 if li > u {
3336 break;
3337 }
3338 }
3339 if let Some(mask) = task_mask {
3340 if !mask.layer_alive(li) {
3341 continue; }
3343 }
3344 #[cfg(target_os = "macos")]
3348 {
3349 if li < gpu_skip_until {
3350 continue;
3351 }
3352 if task_mask.is_none() {
3353 let end = self.q1_graph_gpu(li, upto, position, &mut h);
3354 if end > li {
3355 gpu_skip_until = end;
3356 if self.is_loop_end(end - 1) && end < self.num_layers {
3359 h = inference::rms_norm(
3360 &h,
3361 &self.weights.final_norm,
3362 self.rms_eps,
3363 self.norm_style,
3364 );
3365 }
3366 continue;
3367 }
3368 }
3369 }
3370
3371 let lw = &self.weights.layers[self.phys_layer(li)];
3372 inference::rms_norm_into(
3375 &h,
3376 &lw.input_norm,
3377 self.rms_eps,
3378 self.norm_style,
3379 &mut self.ws.n1,
3380 );
3381
3382 let attn_out = match &lw.attn {
3383 AttnKind::Linear(w) => {
3384 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3385 vmf_phase_forward(
3386 &self.ws.n1,
3387 w,
3388 &cfg,
3389 &mut self.kv_cache.layers[li].linear_state,
3390 self.pool.as_deref(),
3391 )
3392 }
3393 AttnKind::LinearGdn(w) => {
3394 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3395 gdn_forward(
3396 &self.ws.n1,
3397 w,
3398 &cfg,
3399 &mut self.kv_cache.layers[li].linear_state,
3400 self.pool.as_deref(),
3401 )
3402 }
3403 AttnKind::ShortConv(w) => {
3404 let cfg = self
3405 .short_conv_cfg
3406 .expect("short-conv layer without short_conv_cfg");
3407 short_conv_forward(
3408 &self.ws.n1,
3409 w,
3410 &cfg,
3411 &mut self.kv_cache.layers[li].linear_state,
3412 self.pool.as_deref(),
3413 )
3414 }
3415 AttnKind::Full {
3416 wq,
3417 wk,
3418 wv,
3419 wo,
3420 q_norm,
3421 k_norm,
3422 output_gate,
3423 softplus_gate,
3424 bias,
3425 } if self.kv_cache.layers[li].o1_sealed() => {
3426 let inv_freq_l = self.layer_inv_freq(li);
3429 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3430 let cfg = QwenAttnCfg {
3431 num_heads: self.layer_num_heads(li),
3432 num_kv_heads: nkv_l,
3433 head_dim: hd_l,
3434 hidden_size: hs,
3435 position,
3436 inv_freq: &inv_freq_l,
3437 rotary_dim: rd_l,
3438 scale: self.attn_scale,
3439 window: None,
3440 v_norm: self.attn_v_norm,
3441 q_norm: q_norm.as_deref(),
3442 k_norm: k_norm.as_deref(),
3443 output_gate: *output_gate,
3444 softplus_gate: softplus_gate
3445 .as_ref()
3446 .map(|(gate, per_head)| (gate, *per_head)),
3447 rope_scale: self.layer_rope_scale(li),
3448 bias: bias
3449 .as_ref()
3450 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3451 rms_eps: eps,
3452 norm_style: self.norm_style,
3453 pool: pool.as_deref(),
3454 };
3455 attention::qwen_attention_nystrom(
3456 &self.ws.n1,
3457 wq,
3458 wk,
3459 wv,
3460 wo,
3461 &mut self.kv_cache.layers[li],
3462 &cfg,
3463 )
3464 }
3465 AttnKind::Full {
3466 wq,
3467 wk,
3468 wv,
3469 wo,
3470 q_norm,
3471 k_norm,
3472 output_gate,
3473 softplus_gate,
3474 bias,
3475 } => 'attn: {
3476 if graph_on
3479 && !*output_gate
3480 && softplus_gate.is_none()
3481 && self.attention_heads_per_layer.is_none()
3482 && bias.is_none()
3483 && task_mask.is_none()
3484 {
3485 let inv_freq_l = self.layer_inv_freq(li);
3486 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3487 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3488 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
3489 wq.mapped_q1(),
3490 wk.mapped_q1(),
3491 wv.mapped_q1(),
3492 wo.mapped_q1(),
3493 ) {
3494 let gm = gm.clone();
3495 let mut out = vec![0f32; hs];
3496 let cache = &self.kv_cache.layers[li];
3497 if crate::gpu::attn_dropin(
3498 &gm,
3499 self.graph_kv_id,
3500 li,
3501 &self.ws.n1,
3502 qi,
3503 ki,
3504 vi,
3505 oi,
3506 q_norm.as_deref(),
3507 k_norm.as_deref(),
3508 &inv_freq_l,
3509 nh,
3510 nkv_l,
3511 hd_l,
3512 rd_l,
3513 hs,
3514 position,
3515 self.kv_cache.max_seq_len,
3516 gemma,
3517 eps as f32,
3518 cache.k_heads(),
3519 cache.v_heads(),
3520 &mut out,
3521 ) {
3522 break 'attn out;
3523 }
3524 }
3525 }
3526 let masked = task_mask
3527 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
3528 .unwrap_or(false);
3529 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
3530 match (masked, f32_view) {
3531 (true, (Some(q), Some(k), Some(v), Some(o))) => {
3534 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
3535 attention::multi_head_attention(
3536 &self.ws.n1,
3537 q,
3538 k,
3539 v,
3540 o,
3541 &mut self.kv_cache.layers[li],
3542 self.num_heads,
3543 self.num_kv_heads,
3544 self.head_dim,
3545 self.hidden_size,
3546 position,
3547 &active_heads,
3548 &self.inv_freq,
3549 )
3550 }
3551 (masked, _) => {
3552 if masked {
3553 tracing::warn!(
3554 "layer {li}: head mask on quantized weights not \
3555 supported yet — executing dense"
3556 );
3557 }
3558 let inv_freq_l = self.layer_inv_freq(li);
3559 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3560 let cfg = QwenAttnCfg {
3561 num_heads: self.layer_num_heads(li),
3562 num_kv_heads: nkv_l,
3563 head_dim: hd_l,
3564 hidden_size: hs,
3565 position,
3566 inv_freq: &inv_freq_l,
3567 rotary_dim: rd_l,
3568 scale: self.attn_scale,
3569 window: self.layer_window(li),
3570 v_norm: self.attn_v_norm,
3571 q_norm: q_norm.as_deref(),
3572 k_norm: k_norm.as_deref(),
3573 output_gate: *output_gate,
3574 softplus_gate: softplus_gate
3575 .as_ref()
3576 .map(|(gate, per_head)| (gate, *per_head)),
3577 rope_scale: self.layer_rope_scale(li),
3578 bias: bias
3579 .as_ref()
3580 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3581 rms_eps: eps,
3582 norm_style: self.norm_style,
3583 pool: pool.as_deref(),
3584 };
3585 attention::qwen_attention(
3586 &self.ws.n1,
3587 wq,
3588 wk,
3589 wv,
3590 wo,
3591 &mut self.kv_cache.layers[li],
3592 &cfg,
3593 )
3594 }
3595 }
3596 }
3597 };
3598 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
3601 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
3602 None => attn_out,
3603 };
3604 let lw = &self.weights.layers[self.phys_layer(li)];
3605 inference::add_rmsnorm_fused_into(
3606 &mut h,
3607 &attn_out,
3608 &lw.post_norm,
3609 self.rms_eps,
3610 self.norm_style,
3611 &mut self.ws.p1,
3612 );
3613 let mut attn_out = attn_out;
3614 attention::recycle_buf(&mut attn_out);
3615 let post_normed = &self.ws.p1;
3616
3617 let ffn_masked = task_mask
3618 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
3619 .unwrap_or(false);
3620 let f32_ffn = match &lw.ffn {
3623 FfnKind::Dense(d) => (
3624 d.gate_proj.as_f32(),
3625 d.up_proj.as_f32(),
3626 d.down_proj.as_f32(),
3627 ),
3628 FfnKind::Moe(_) => (None, None, None),
3629 };
3630 let ffn_out = match (ffn_masked, f32_ffn) {
3631 (true, (Some(g), Some(u), Some(d))) => {
3632 let active = task_mask.unwrap().ffn_active_indices(li);
3633 inference::sparse_ffn_forward(
3634 post_normed,
3635 g,
3636 u,
3637 d,
3638 self.hidden_size,
3639 self.intermediate_size,
3640 &active,
3641 self.pool.as_deref(),
3642 )
3643 }
3644 (true, _) => match &lw.ffn {
3648 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
3649 let active = task_mask.unwrap().ffn_active_indices(li);
3650 sparse_ffn_quant(
3651 d,
3652 post_normed,
3653 &active,
3654 self.hidden_size,
3655 self.pool.as_deref(),
3656 )
3657 }
3658 FfnKind::Dense(d) => {
3663 let active = task_mask.unwrap().ffn_active_indices(li);
3664 let (gf, uf, df) = dequant_dense_f32(d);
3665 inference::sparse_ffn_forward(
3666 post_normed,
3667 &gf,
3668 &uf,
3669 &df,
3670 self.hidden_size,
3671 self.intermediate_size,
3672 &active,
3673 self.pool.as_deref(),
3674 )
3675 }
3676 FfnKind::Moe(m) => {
3677 let allowed = task_mask
3681 .and_then(|tm| tm.expert_flags(li, m.experts.len()));
3682 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
3683 }
3684 },
3685 (false, _) => {
3686 let allowed = match (&lw.ffn, task_mask) {
3687 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
3688 _ => None,
3689 };
3690 ffn_forward(&lw.ffn, post_normed, self.pool.as_deref(), allowed.as_deref())
3691 }
3692 };
3693 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
3694 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
3695 None => ffn_out,
3696 };
3697 for (i, &f) in ffn_out.iter().enumerate() {
3698 h[i] += f;
3699 }
3700 let mut ffn_out = ffn_out;
3701 attention::recycle_buf(&mut ffn_out);
3702
3703 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
3705 for v in h.iter_mut() {
3706 *v *= sc;
3707 }
3708 }
3709
3710 if self.is_loop_end(li) && li + 1 < self.num_layers {
3713 h = inference::rms_norm(
3714 &h,
3715 &self.weights.final_norm,
3716 self.rms_eps,
3717 self.norm_style,
3718 );
3719 }
3720
3721 if self.dyn_phi_layer == Some(li) {
3725 self.update_dyn_phi(&h);
3726 }
3727 }
3728 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
3730 crate::gpu::graph_race_record(false, t.elapsed());
3731 }
3732
3733 h
3734 }
3735
3736 fn update_dyn_phi(&mut self, h: &[f32]) {
3739 const A: f32 = 0.2;
3740 if self.dyn_phi_ema.len() != h.len() {
3741 self.dyn_phi_ema = vec![0.0; h.len()];
3742 self.dyn_phi_seen = 0;
3743 }
3744 if self.dyn_phi_seen == 0 {
3745 self.dyn_phi_ema.copy_from_slice(h);
3746 } else {
3747 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
3748 *e = (1.0 - A) * *e + A * v;
3749 }
3750 }
3751 self.dyn_phi_seen += 1;
3752 }
3753
3754 pub fn dyn_phi(&self) -> &[f32] {
3756 &self.dyn_phi_ema
3757 }
3758
3759 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
3761 self.dyn_phi_layer = layer;
3762 self.dyn_phi_ema.clear();
3763 self.dyn_phi_seen = 0;
3764 }
3765
3766 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
3768 let Some(model) = &self.model else {
3769 return Vec::new();
3770 };
3771 model
3772 .header
3773 .skills
3774 .iter()
3775 .enumerate()
3776 .filter_map(|(i, sk)| {
3777 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
3778 let sel = sk.selection.as_ref()?;
3779 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
3780 })
3781 .collect()
3782 }
3783
3784 pub fn active_skill(&self) -> Option<usize> {
3786 self.dyn_active
3787 }
3788
3789 pub fn enable_dynamic_routing(&mut self) -> usize {
3794 use crate::swarm::{DynRouter, RoutableSkill};
3795 let Some(model) = self.model.clone() else {
3796 return 0;
3797 };
3798 if self.dyn_blend_loaded {
3801 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
3802 return 0;
3803 }
3804 if let Some(a) = self.dyn_active {
3808 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
3809 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
3810 return 0;
3811 }
3812 }
3813 let hidden = self.hidden_size;
3814 let mut skills = Vec::new();
3815 for (idx, id, _phi) in self.dynamic_skills() {
3816 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
3817 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
3818 skills.push(rs);
3819 }
3820 }
3821 }
3822 if skills.is_empty() {
3823 return 0;
3824 }
3825 let phi = skills[0].phi_layer;
3827 if skills.iter().any(|s| s.phi_layer != phi) {
3828 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
3829 }
3830 let n = skills.len();
3831 self.set_dyn_phi_layer(Some(phi));
3832 self.dyn_router = Some(DynRouter::new(skills));
3833 n
3834 }
3835
3836 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
3838 self.dyn_router
3839 .as_ref()
3840 .map(|r| r.switches.clone())
3841 .unwrap_or_default()
3842 }
3843
3844 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
3847 let rows = self.weights.lm_head.rows();
3848 let mut logits = attention::take_buf(rows.min(self.vocab_size));
3849 self.weights
3850 .lm_head
3851 .matvec(hidden, &mut logits, self.pool.as_deref());
3852 logits.resize(self.vocab_size, 0.0);
3853 if let Some(c) = self.final_softcap {
3854 for l in logits.iter_mut() {
3855 *l = c * (*l / c).tanh();
3856 }
3857 }
3858 logits
3859 }
3860
3861 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
3866 self.kv_cache.clear();
3867 let mut hidden = vec![0.0f32; self.hidden_size];
3868 for (pos, &id) in ids.iter().enumerate() {
3869 let emb = self.embed_single(id);
3870 hidden = self.forward_layers(&emb, pos, task_mask);
3871 }
3872 inference::rms_norm_into(
3873 &hidden,
3874 &self.weights.final_norm,
3875 self.rms_eps,
3876 self.norm_style,
3877 &mut self.ws.n1,
3878 );
3879 self.lm_head_forward(&self.ws.n1)
3880 }
3881}
3882
3883pub fn create_test_pipeline(
3885 hidden_size: usize,
3886 intermediate_size: usize,
3887 num_heads: usize,
3888 num_kv_heads: usize,
3889 head_dim: usize,
3890 num_layers: usize,
3891 vocab_size: usize,
3892) -> Pipeline {
3893 let synth = |n: usize, salt: usize| -> Vec<f32> {
3896 (0..n)
3897 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
3898 .collect()
3899 };
3900 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3901 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3902 };
3903 let layer_weights: Vec<LayerWeights> = (0..num_layers)
3904 .map(|li| LayerWeights {
3905 input_norm: vec![1.0; hidden_size],
3906 post_norm: vec![1.0; hidden_size],
3907 attn_out_norm: None,
3908 ffn_out_norm: None,
3909 layer_scale: None,
3910 ffn: FfnKind::Dense(DenseFfn {
3911 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
3912 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
3913 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
3914 act: Act::Silu,
3915 }),
3916 attn: AttnKind::Full {
3917 bias: None,
3918 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
3919 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
3920 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
3921 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
3922 q_norm: None,
3923 k_norm: None,
3924 output_gate: false,
3925 softplus_gate: None,
3926 },
3927 })
3928 .collect();
3929
3930 Pipeline::new(
3931 Tokenizer::byte_level(),
3932 PipelineWeights {
3933 embed_tokens: qt(vocab_size, hidden_size, 100),
3934 layers: layer_weights,
3935 lm_head: qt(vocab_size, hidden_size, 200),
3936 final_norm: vec![1.0; hidden_size],
3937 },
3938 hidden_size,
3939 intermediate_size,
3940 num_heads,
3941 num_kv_heads,
3942 head_dim,
3943 num_layers,
3944 num_layers, false, vocab_size,
3947 1e-6,
3948 10_000.0,
3949 NormStyle::Qwen,
3950 4096,
3951 SamplerConfig {
3952 seed: Some(42),
3953 ..Default::default()
3954 },
3955 )
3956}
3957
3958fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
3961 let inter = d.gate_proj.rows();
3962 let hidden = d.down_proj.rows();
3963 let mut g = vec![0.0f32; b * inter];
3964 d.gate_proj.matmat(xs, b, &mut g, pool);
3965 let mut u = vec![0.0f32; b * inter];
3966 d.up_proj.matmat(xs, b, &mut u, pool);
3967 for i in 0..b * inter {
3968 g[i] = d.act.apply(g[i]) * u[i];
3969 }
3970 let mut out = vec![0.0f32; b * hidden];
3971 d.down_proj.matmat(&g, b, &mut out, pool);
3972 out
3973}
3974
3975fn moe_ffn_batch(
3979 m: &MoeFfn,
3980 xs: &[f32],
3981 b: usize,
3982 hidden: usize,
3983 pool: Option<&Pool>,
3984 allowed: Option<&[bool]>,
3985) -> Vec<f32> {
3986 let ne = m.experts.len();
3987 let mut logits = vec![0.0f32; b * ne];
3988 m.router.matmat(xs, b, &mut logits, pool);
3989
3990 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
3993 {
3994 let mut st = m.stats.borrow_mut();
3995 if st.len() < ne {
3996 st.resize(ne, 0);
3997 }
3998 for bi in 0..b {
3999 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
4000 for &e in &idx {
4001 st[e] += 1;
4002 assign[e].push((bi, p[e] / wsum));
4003 }
4004 }
4005 }
4006
4007 let mut out = vec![0.0f32; b * hidden];
4008 let cols = m.experts[0].gate_proj.cols();
4009 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
4010 let sb = list.len();
4011 let mut sub = vec![0.0f32; sb * cols];
4012 for (k, &(bi, _)) in list.iter().enumerate() {
4013 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
4014 }
4015 let eo = dense_ffn_batch(d, &sub, sb, pool);
4016 for (k, &(bi, w)) in list.iter().enumerate() {
4017 for i in 0..hidden {
4018 out[bi * hidden + i] += w * eo[k * hidden + i];
4019 }
4020 }
4021 };
4022 for (e, a) in assign.iter().enumerate().take(ne) {
4023 if !a.is_empty() {
4024 run_expert(&m.experts[e], a);
4025 }
4026 }
4027 if let Some((se, gate)) = &m.shared {
4028 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
4029 let mut gl = vec![0.0f32; b];
4030 gate.matmat(xs, b, &mut gl, pool);
4031 (0..b)
4032 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
4033 .collect()
4034 } else {
4035 (0..b).map(|bi| (bi, 1.0)).collect()
4036 };
4037 run_expert(se, &all);
4038 }
4039 out
4040}
4041
4042thread_local! {
4043 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
4047 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
4048}
4049
4050fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4052 if crate::gpu::enabled_here()
4063 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
4064 {
4065 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
4066 crate::gpu::ProbeArm::Gpu
4067 } else {
4068 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
4069 };
4070 match arm {
4071 crate::gpu::ProbeArm::Gpu => {
4072 let t0 = std::time::Instant::now();
4073 if let Some(out) = dense_ffn_gpu(d, x, pool) {
4074 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4075 return out;
4076 }
4077 }
4078 crate::gpu::ProbeArm::CpuTimed => {
4079 let t0 = std::time::Instant::now();
4080 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4081 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4082 return out;
4083 }
4084 crate::gpu::ProbeArm::Cpu => {
4085 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
4086 }
4087 }
4088 }
4089 dense_ffn_cpu(d, x, pool)
4090}
4091
4092fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4094 let inter = d.gate_proj.rows();
4095 FFN_SCRATCH.with(|s| {
4096 let mut s = s.borrow_mut();
4097 let [g, u, ..] = &mut *s;
4098 g.resize(inter, 0.0);
4099 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
4102 } else {
4104 u.resize(inter, 0.0);
4105 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
4107 for i in 0..inter {
4108 g[i] = d.act.apply(g[i]) * u[i];
4109 }
4110 }
4111 FFN_PROBE.with(|pr| {
4114 if let Some(acc) = pr.borrow_mut().as_mut() {
4115 let li = crate::gpu::cur_layer();
4116 if li >= 0 {
4117 if let Some(row) = acc.get_mut(li as usize) {
4118 for (a, &v) in row.iter_mut().zip(g.iter()) {
4119 *a += (v as f64).abs();
4120 }
4121 }
4122 }
4123 }
4124 });
4125 let mut out = attention::take_buf(d.down_proj.rows());
4126 d.down_proj.matvec(g, &mut out, pool);
4127 out
4128 })
4129}
4130
4131thread_local! {
4132 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
4135 const { std::cell::RefCell::new(None) };
4136}
4137
4138fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
4144 if d.act != Act::Silu {
4146 return None;
4147 }
4148 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
4151 return None;
4152 }
4153 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
4154 let mut model_ref = None;
4155 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
4156 let model = model_ref?;
4157 let hidden = jobs[0].down.1;
4158 let mut out = attention::take_buf(hidden);
4159 if crate::gpu::moe_block(&model, &jobs, &mut out) {
4160 Some(out)
4161 } else {
4162 let mut out = out;
4163 attention::recycle_buf(&mut out);
4164 None
4165 }
4166}
4167
4168#[allow(clippy::type_complexity)]
4173#[allow(clippy::type_complexity)]
4174fn moe_parts(
4175 t: &QTensor,
4176) -> Option<(
4177 &std::sync::Arc<cortiq_core::CmfModel>,
4178 usize,
4179 usize,
4180 usize,
4181 &[f32],
4182 &[f32],
4183 bool,
4184 bool,
4185)> {
4186 match t {
4187 QTensor::Mapped {
4188 model,
4189 idx,
4190 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
4191 rows,
4192 cols,
4193 row_scale,
4194 col_field,
4195 ..
4196 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
4197 Some((model, *idx, *rows, *cols, row_scale, col_field, false, false))
4198 }
4199 QTensor::Mapped {
4201 model,
4202 idx,
4203 dtype: cortiq_core::TensorDtype::Q1,
4204 rows,
4205 cols,
4206 ..
4207 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
4208 QTensor::Mapped {
4210 model,
4211 idx,
4212 dtype: cortiq_core::TensorDtype::Q4Tiled,
4213 rows,
4214 cols,
4215 ..
4216 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
4217 _ => None,
4218 }
4219}
4220
4221fn moe_push_job<'a>(
4223 d: &'a DenseFfn,
4224 x: &[f32],
4225 w: f32,
4226 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
4227 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
4228) -> Option<()> {
4229 use crate::qtensor::prescale;
4230 if d.act != Act::Silu {
4231 return None; }
4233 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
4234 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
4235 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
4236 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
4237 return None; }
4239 model_ref.get_or_insert_with(|| gm.clone());
4240 let gdt = if gcf.is_empty() {
4241 cortiq_core::TensorDtype::Q8Row
4242 } else {
4243 cortiq_core::TensorDtype::Q8_2f
4244 };
4245 let udt = if ucf.is_empty() {
4246 cortiq_core::TensorDtype::Q8Row
4247 } else {
4248 cortiq_core::TensorDtype::Q8_2f
4249 };
4250 jobs.push(crate::gpu::MoeJob {
4251 gate: (gi, gr, gc, grs),
4252 up: (ui, ur, uc, urs),
4253 down: (di, dr, dc, drs),
4254 xs_gate: prescale(x, gcf, gdt).into_owned(),
4255 xs_up: prescale(x, ucf, udt).into_owned(),
4256 down_col: dcf,
4257 w,
4258 q1: gq1,
4259 q4t: gq4,
4260 });
4261 Some(())
4262}
4263
4264fn sparse_ffn_quant(
4271 d: &DenseFfn,
4272 x: &[f32],
4273 active: &[u16],
4274 hidden: usize,
4275 pool: Option<&Pool>,
4276) -> Vec<f32> {
4277 let n = active.len();
4278 let inter = d.gate_proj.rows();
4279 let mut act = vec![0.0f32; n];
4280 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
4283 let compute = |ai: usize| -> f32 {
4284 let idx = active[ai] as usize;
4285 if idx >= inter {
4286 return 0.0; }
4288 let mut s = if need_scratch {
4289 vec![0.0f32; hidden]
4290 } else {
4291 Vec::new()
4292 };
4293 let gate = d.gate_proj.row_dot(idx, x, &mut s);
4294 let up = d.up_proj.row_dot(idx, x, &mut s);
4295 d.act.apply(gate) * up
4296 };
4297 match pool {
4298 Some(p) if n >= 256 => {
4299 let ptr = SendMut(act.as_mut_ptr());
4300 p.run(&|widx, nw| {
4301 let chunk = n.div_ceil(nw);
4302 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
4303 for ai in s..e {
4304 unsafe { *ptr.at(ai) = compute(ai) };
4305 }
4306 });
4307 }
4308 _ => {
4309 for (ai, a) in act.iter_mut().enumerate() {
4310 *a = compute(ai);
4311 }
4312 }
4313 }
4314 let mut out = vec![0.0f32; hidden];
4316 for (ai, &idx) in active.iter().enumerate() {
4317 let w = act[ai];
4318 if w.abs() >= 1e-12 && (idx as usize) < inter {
4319 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
4320 }
4321 }
4322 out
4323}
4324
4325#[doc(hidden)]
4327pub fn sparse_ffn_quant_for_test(
4328 d: &DenseFfn,
4329 x: &[f32],
4330 active: &[u16],
4331 hidden: usize,
4332) -> Vec<f32> {
4333 sparse_ffn_quant(d, x, active, hidden, None)
4334}
4335
4336fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
4340 let deq = |t: &QTensor| -> Vec<f32> {
4341 let (rows, cols) = (t.rows(), t.cols());
4342 let mut out = vec![0.0f32; rows * cols];
4343 for r in 0..rows {
4344 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
4345 }
4346 out
4347 };
4348 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
4349}
4350
4351struct SendMut(*mut f32);
4353unsafe impl Send for SendMut {}
4354unsafe impl Sync for SendMut {}
4355impl SendMut {
4356 #[inline]
4357 #[allow(clippy::mut_from_ref)]
4360 unsafe fn at(&self, i: usize) -> &mut f32 {
4361 unsafe { &mut *self.0.add(i) }
4362 }
4363}
4364
4365fn moe_route(
4375 logits: &[f32],
4376 m: &MoeFfn,
4377 allowed: Option<&[bool]>,
4378) -> (Vec<usize>, Vec<f32>, f32) {
4379 let ne = logits.len();
4380 let p: Vec<f32> = if m.router_sigmoid {
4381 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
4382 } else {
4383 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4384 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
4385 let s: f32 = e.iter().sum();
4386 for v in &mut e {
4387 *v /= s;
4388 }
4389 e
4390 };
4391 let admit = |e: usize| {
4397 m.mask.as_ref().is_none_or(|mk| mk[e]) && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
4398 };
4399 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
4400 match &m.expert_bias {
4402 Some(b) => idx.sort_unstable_by(|&x, &y| {
4403 (p[y] + b[y])
4404 .partial_cmp(&(p[x] + b[x]))
4405 .unwrap()
4406 .then(x.cmp(&y))
4407 }),
4408 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
4409 }
4410 idx.truncate(m.top_k);
4411 if let Some(tau) = m.route_tau {
4415 let total: f32 = idx.iter().map(|&e| p[e]).sum();
4416 if total > 0.0 {
4417 let mut acc = 0.0f32;
4418 let mut keep = idx.len();
4419 for (i, &e) in idx.iter().enumerate() {
4420 acc += p[e];
4421 if acc >= tau * total {
4422 keep = i + 1;
4423 break;
4424 }
4425 }
4426 idx.truncate(keep);
4427 }
4428 }
4429 let wsum: f32 = if m.norm_topk_prob {
4430 let s: f32 = idx.iter().map(|&e| p[e]).sum();
4431 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
4434 } else {
4435 1.0 / m.routed_scaling
4436 };
4437 (idx, p, wsum)
4438}
4439
4440fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
4443 let ne = m.experts.len();
4444 let mut logits = vec![0.0f32; ne];
4445 m.router.matvec(x, &mut logits, pool);
4446 let (idx, p, wsum) = moe_route(&logits, m, allowed);
4447 {
4448 let mut st = m.stats.borrow_mut();
4449 if st.len() < ne {
4450 st.resize(ne, 0);
4451 }
4452 for &e in &idx {
4453 st[e] += 1;
4454 }
4455 }
4456 if crate::gpu::enabled_here() {
4461 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
4462 crate::gpu::ProbeArm::Gpu => {
4463 let t0 = std::time::Instant::now();
4464 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
4465 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4466 return out;
4467 }
4468 }
4469 crate::gpu::ProbeArm::CpuTimed => {
4470 let t0 = std::time::Instant::now();
4471 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4472 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4473 return out;
4474 }
4475 crate::gpu::ProbeArm::Cpu => {
4476 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4477 }
4478 }
4479 }
4480 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
4481}
4482
4483fn moe_ffn_cpu(
4485 m: &MoeFfn,
4486 x: &[f32],
4487 idx: &[usize],
4488 p: &[f32],
4489 wsum: f32,
4490 pool: Option<&Pool>,
4491) -> Vec<f32> {
4492 let mut out = attention::take_buf(x.len());
4493 for &e in idx {
4494 let mut eo = dense_ffn(&m.experts[e], x, pool);
4495 let w = p[e] / wsum;
4496 for i in 0..out.len() {
4497 out[i] += w * eo[i];
4498 }
4499 attention::recycle_buf(&mut eo);
4500 }
4501 if let Some((se, gate)) = &m.shared {
4502 let mut so = dense_ffn(se, x, pool);
4503 let g = gate.as_ref().map_or(1.0, |gate| {
4504 let mut gl = [0.0f32; 1];
4505 gate.matvec(x, &mut gl, pool);
4506 1.0 / (1.0 + (-gl[0]).exp())
4507 });
4508 for i in 0..out.len() {
4509 out[i] += g * so[i];
4510 }
4511 attention::recycle_buf(&mut so);
4512 }
4513 out
4514}
4515
4516fn moe_ffn_gpu(
4519 m: &MoeFfn,
4520 x: &[f32],
4521 idx: &[usize],
4522 p: &[f32],
4523 wsum: f32,
4524 pool: Option<&Pool>,
4525) -> Option<Vec<f32>> {
4526 use crate::gpu::MoeJob;
4527
4528 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
4529 let mut model_ref = None;
4530 for &e in idx {
4531 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
4532 }
4533 if let Some((se, gate)) = &m.shared {
4534 let g = gate.as_ref().map_or(1.0, |gate| {
4535 let mut gl = [0.0f32; 1];
4536 gate.matvec(x, &mut gl, pool);
4537 1.0 / (1.0 + (-gl[0]).exp())
4538 });
4539 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
4540 }
4541 let model = model_ref?;
4542 let hidden = jobs[0].down.1;
4543 let mut out = vec![0.0f32; hidden];
4544 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
4545}
4546
4547fn ffn_forward(
4549 ffn: &FfnKind,
4550 x: &[f32],
4551 pool: Option<&Pool>,
4552 experts_allowed: Option<&[bool]>,
4553) -> Vec<f32> {
4554 match ffn {
4555 FfnKind::Dense(d) => dense_ffn(d, x, pool),
4556 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
4557 }
4558}
4559
4560fn ffn_forward_pair(
4564 ffn: &FfnKind,
4565 x1: &[f32],
4566 x2: &[f32],
4567 pool: Option<&Pool>,
4568 experts_allowed: Option<&[bool]>,
4569) -> (Vec<f32>, Vec<f32>) {
4570 let d = match ffn {
4571 FfnKind::Dense(d) => d,
4572 FfnKind::Moe(m) => {
4573 return (
4574 moe_ffn(m, x1, pool, experts_allowed),
4575 moe_ffn(m, x2, pool, experts_allowed),
4576 );
4577 }
4578 };
4579 let inter = d.gate_proj.rows();
4580 FFN_SCRATCH.with(|s| {
4581 let mut s = s.borrow_mut();
4582 let [g1, g2, u1, u2] = &mut *s;
4583 g1.resize(inter, 0.0);
4584 g2.resize(inter, 0.0);
4585 u1.resize(inter, 0.0);
4586 u2.resize(inter, 0.0);
4587 QTensor::matvec2_many(
4590 [&d.gate_proj, &d.up_proj],
4591 x1,
4592 x2,
4593 [g1.as_mut_slice(), u1.as_mut_slice()],
4594 [g2.as_mut_slice(), u2.as_mut_slice()],
4595 pool,
4596 );
4597 for i in 0..inter {
4598 g1[i] = d.act.apply(g1[i]) * u1[i];
4599 g2[i] = d.act.apply(g2[i]) * u2[i];
4600 }
4601 let mut o1 = attention::take_buf(d.down_proj.rows());
4602 let mut o2 = attention::take_buf(d.down_proj.rows());
4603 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
4604 (o1, o2)
4605 })
4606}
4607
4608#[cfg(test)]
4609mod tests {
4610 use super::*;
4611
4612 #[test]
4618 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
4619 let (hidden, inter) = (16usize, 40usize);
4620 let synth = |n: usize, salt: usize| -> Vec<f32> {
4621 (0..n)
4622 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
4623 .collect()
4624 };
4625 let d = DenseFfn {
4626 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
4627 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
4628 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
4629 act: Act::Silu,
4630 };
4631 let x = synth(hidden, 9);
4632 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
4634
4635 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
4636
4637 let mut g = vec![0.0f32; inter];
4639 d.gate_proj.matvec(&x, &mut g, None);
4640 let mut u = vec![0.0f32; inter];
4641 d.up_proj.matvec(&x, &mut u, None);
4642 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
4643 for i in 0..inter {
4644 g[i] = if act_set.contains(&(i as u16)) {
4645 inference::silu(g[i]) * u[i]
4646 } else {
4647 0.0
4648 };
4649 }
4650 let mut reference = vec![0.0f32; hidden];
4651 d.down_proj.matvec(&g, &mut reference, None);
4652
4653 let max_d = sparse
4654 .iter()
4655 .zip(&reference)
4656 .map(|(a, b)| (a - b).abs())
4657 .fold(0.0f32, f32::max);
4658 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
4659 }
4660
4661 fn attach_test_mtp(p: &mut Pipeline) {
4663 let (h, inter, heads, kv, hd) = (
4664 p.hidden_size,
4665 p.intermediate_size,
4666 p.num_heads,
4667 p.num_kv_heads,
4668 p.head_dim,
4669 );
4670 let synth = |n: usize, salt: usize| -> Vec<f32> {
4671 (0..n)
4672 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
4673 .collect()
4674 };
4675 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4676 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4677 };
4678 p.mtp = Some(MtpModule {
4679 enorm: vec![1.0; h],
4680 hnorm: vec![1.0; h],
4681 eh_proj: qt(h, 2 * h, 301),
4682 layer: LayerWeights {
4683 input_norm: vec![1.0; h],
4684 post_norm: vec![1.0; h],
4685 attn_out_norm: None,
4686 ffn_out_norm: None,
4687 layer_scale: None,
4688 ffn: FfnKind::Dense(DenseFfn {
4689 gate_proj: qt(inter, h, 315),
4690 up_proj: qt(inter, h, 316),
4691 down_proj: qt(h, inter, 317),
4692 act: Act::Silu,
4693 }),
4694 attn: AttnKind::Full {
4695 bias: None,
4696 wq: qt(heads * hd, h, 311),
4697 wk: qt(kv * hd, h, 312),
4698 wv: qt(kv * hd, h, 313),
4699 wo: qt(h, heads * hd, 314),
4700 q_norm: None,
4701 k_norm: None,
4702 output_gate: false,
4703 softplus_gate: None,
4704 },
4705 },
4706 final_norm: vec![1.0; h],
4707 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
4708 });
4709 }
4710
4711 #[test]
4712 fn speculative_equals_vanilla_greedy() {
4713 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
4717 let run = |spec: bool| {
4718 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4719 p.sampler_config.temperature = 0.0;
4720 attach_test_mtp(&mut p);
4721 p.speculative = spec;
4722 let r = p.generate("abcdef", 12, None, None).unwrap();
4723 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
4724 };
4725 let (vanilla, d0, _) = run(false);
4726 let (spec, d1, a1) = run(true);
4727 assert_eq!(d0, 0, "vanilla path must not draft");
4728 assert!(d1 > 0, "speculative path must draft");
4729 assert_eq!(
4730 vanilla, spec,
4731 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
4732 );
4733 }
4734
4735 #[test]
4736 fn speculative_accepts_constant_oracle() {
4737 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
4739 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4740 p.sampler_config.temperature = 0.0;
4741 p.sampler_config.repetition_penalty = 1.0;
4742 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
4745 attach_test_mtp(&mut p);
4746 p.speculative = true;
4747 let r = p.generate("abcd", 10, None, None).unwrap();
4748 assert!(r.mtp_drafted > 0);
4749 assert_eq!(
4750 r.mtp_accepted, r.mtp_drafted,
4751 "constant logits → every draft accepted"
4752 );
4753 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
4756 }
4757
4758 #[test]
4759 fn empty_prompt_is_an_error_not_a_panic() {
4760 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4761 let r = p.generate("", 4, None, None);
4762 assert!(r.is_err(), "empty prompt must be a clean error");
4763 }
4764
4765 #[test]
4766 fn every_token_enters_kv_exactly_once() {
4767 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4768 p.sampler_config.temperature = 0.0;
4770 let r = p.generate("abc", 2, None, None).unwrap();
4771 assert_eq!(r.prompt_tokens, 3);
4772 assert_eq!(
4776 p.kv_cache.seq_len(),
4777 3 + r.tokens_generated - 1,
4778 "each token must be cached exactly once (v1 cached the last prompt token twice)"
4779 );
4780 }
4781
4782 #[test]
4783 fn generation_is_reproducible_with_seed() {
4784 let run = || {
4785 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4786 p.generate("hello", 8, None, None).unwrap().token_ids
4787 };
4788 assert_eq!(run(), run());
4789 }
4790
4791 #[test]
4792 fn resetting_sampler_restarts_the_seeded_stream() {
4793 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4794 let config = SamplerConfig {
4795 seed: Some(1234),
4796 ..SamplerConfig::default()
4797 };
4798 p.set_sampler_config(config.clone());
4799 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
4800 p.set_sampler_config(config);
4801 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
4802 assert_eq!(first, second);
4803 }
4804
4805 #[test]
4806 fn eviction_bounds_the_cache() {
4807 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4808 p.kv_cache.max_seq_len = 6;
4809 p.sampler_config.temperature = 0.0;
4810 let _ = p.generate("abcd", 12, None, None).unwrap();
4811 assert!(
4812 p.kv_cache.seq_len() <= 6 + 1,
4813 "cache must stay bounded by max_seq_len (got {})",
4814 p.kv_cache.seq_len()
4815 );
4816 }
4817
4818 #[test]
4819 fn confidence_matches_tokens_and_is_a_probability() {
4820 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4821 p.sampler_config.temperature = 0.0;
4822 p.sampler_config.repetition_penalty = 1.0;
4823 let r = p.generate("abcd", 10, None, None).unwrap();
4824 assert_eq!(
4825 r.token_confidence.len(),
4826 r.token_ids.len(),
4827 "one confidence per emitted token"
4828 );
4829 for &c in &r.token_confidence {
4830 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
4831 }
4832 let logits = [1.0f32, 3.0, 0.5, 3.0];
4834 let p0 = top1_prob_t(&logits, 1, 1.0);
4835 let p1 = top1_prob_t(&logits, 3, 1.0);
4836 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
4837 assert!(p0 > 0.0 && p0 < 1.0);
4838 let sharp = top1_prob_t(&logits, 1, 1.0);
4840 let soft = top1_prob_t(&logits, 1, 2.0);
4841 assert!(soft < sharp, "higher temperature lowers peak confidence");
4842 }
4843
4844 #[test]
4845 fn trace_is_opt_in_and_parallels_the_output() {
4846 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4848 p.sampler_config.temperature = 0.0;
4849 p.sampler_config.repetition_penalty = 1.0;
4850 let r = p.generate("abcd", 10, None, None).unwrap();
4851 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
4852
4853 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4855 p.sampler_config.temperature = 0.0;
4856 p.sampler_config.repetition_penalty = 1.0;
4857 p.set_trace(true);
4858 let r = p.generate("abcd", 10, None, None).unwrap();
4859 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
4860 for (i, tr) in r.traces.iter().enumerate() {
4861 assert_eq!(tr.t, i, "trace index is sequential");
4862 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
4863 assert_eq!(
4864 tr.confidence, r.token_confidence[i],
4865 "trace confidence matches the confidence channel"
4866 );
4867 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
4869 }
4870 }
4871
4872 #[test]
4873 fn explain_prefill_logits_match_greedy_first_token() {
4874 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4878 p.sampler_config.temperature = 0.0;
4879 p.sampler_config.repetition_penalty = 1.0;
4880 let ids = p.tokenizer.encode("abcd");
4881 let logits = p.prefill_next_logits(&ids, None);
4882 let argmax = logits
4883 .iter()
4884 .enumerate()
4885 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4886 .unwrap()
4887 .0 as u32;
4888 let r = p.generate("abcd", 1, None, None).unwrap();
4889 assert_eq!(
4890 argmax, r.token_ids[0],
4891 "explain preview must match greedy emit"
4892 );
4893 }
4894
4895 #[test]
4896 fn laguna_shared_expert_is_unconditionally_added() {
4897 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
4898 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
4899 let zero_dense = || DenseFfn {
4900 gate_proj: matrix(vec![0.0; 4]),
4901 up_proj: matrix(vec![0.0; 4]),
4902 down_proj: matrix(vec![0.0; 4]),
4903 act: Act::Silu,
4904 };
4905 let shared = DenseFfn {
4906 gate_proj: identity(),
4907 up_proj: identity(),
4908 down_proj: identity(),
4909 act: Act::Silu,
4910 };
4911 let x = [1.0, 2.0];
4912 let expected = dense_ffn(&shared, &x, None);
4913 let moe = MoeFfn {
4914 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
4915 experts: vec![zero_dense()],
4916 top_k: 1,
4917 norm_topk_prob: true,
4918 router_sigmoid: true,
4919 expert_bias: None,
4920 routed_scaling: 1.0,
4921 route_tau: None,
4922 shared: Some((shared, None)),
4923 stats: std::cell::RefCell::new(Vec::new()),
4924 mask: None,
4925 };
4926 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
4927 for (actual, expected) in actual.iter().zip(expected) {
4928 assert!((actual - expected).abs() < 1e-6);
4929 }
4930 }
4931}