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