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