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,
62 pub vocab_size: usize,
63 pub rms_eps: f64,
64 pub rope_base: f32,
65 pub norm_style: NormStyle,
66 pub rotary_dim: usize,
68 pub attention_heads_per_layer: Option<Vec<usize>>,
70 pub vmf_cfg: Option<VmfPhaseCfg>,
72 pub gdn_cfg: Option<GdnCfg>,
74 pub short_conv_cfg: Option<ShortConvCfg>,
77 pub mtp: Option<MtpModule>,
79 pub speculative: bool,
81 rng: SplitMix64,
82 sampler_scratch: SamplerScratch,
83 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
87 ws: ForwardScratch,
91 pool: Option<std::sync::Arc<Pool>>,
93 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
97 pub(crate) dyn_force_f32: bool,
99 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
104 pub(crate) dyn_active: Option<usize>,
110 pub(crate) dyn_blend_loaded: bool,
114 pub(crate) dyn_phi_layer: Option<usize>,
117 dyn_phi_ema: Vec<f32>,
119 dyn_phi_seen: usize,
120 pub dyn_router: Option<crate::swarm::DynRouter>,
123 o1_cfg: Option<crate::nystrom::O1Cfg>,
126 o1_flags: Vec<bool>,
128 trace: bool,
131 calib_temp: f32,
134 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
136 graph_kv_id: u64,
137 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
140 graph_want_logits: bool,
141 graph_logits: Option<Vec<f32>>,
144 pub embed_multiplier: f32,
146 pub attn_scale: f32,
149 pub swa: Option<(usize, usize)>,
152 pub sliding_layers: Option<Vec<bool>>,
155 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
158 pub rotary_dim_local: Option<usize>,
159 pub rope_scale: f32,
160 pub rope_scale_local: f32,
161 pub global_attn: Option<(usize, usize)>,
164 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
167 pub attn_v_norm: bool,
169 pub final_softcap: Option<f32>,
171 confidence_on: bool,
175}
176
177#[cfg(target_os = "macos")]
178impl Drop for Pipeline {
179 fn drop(&mut self) {
180 crate::gpu::kv_mirror_drop(self.graph_kv_id);
181 }
182}
183
184pub struct PipelineWeights {
189 pub embed_tokens: QTensor,
191 pub layers: Vec<LayerWeights>,
193 pub lm_head: QTensor,
195 pub final_norm: Vec<f32>,
197}
198
199pub struct LayerWeights {
201 pub input_norm: Vec<f32>,
202 pub post_norm: Vec<f32>,
205 pub attn_out_norm: Option<Vec<f32>>,
208 pub layer_scale: Option<f32>,
210 pub ffn_out_norm: Option<Vec<f32>>,
213 pub ffn: FfnKind,
214 pub attn: AttnKind,
215}
216
217#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
220pub enum Act {
221 #[default]
222 Silu,
223 GeluTanh,
224}
225
226impl Act {
227 pub fn from_arch(name: &str) -> Self {
228 if name == "gelu_tanh" {
229 Self::GeluTanh
230 } else {
231 Self::Silu
232 }
233 }
234
235 #[inline]
236 pub fn apply(self, x: f32) -> f32 {
237 match self {
238 Self::Silu => inference::silu(x),
239 Self::GeluTanh => inference::gelu_tanh(x),
240 }
241 }
242}
243
244pub struct DenseFfn {
246 pub gate_proj: QTensor,
247 pub up_proj: QTensor,
248 pub down_proj: QTensor,
249 pub act: Act,
251}
252
253pub enum FfnKind {
256 Dense(DenseFfn),
257 Moe(MoeFfn),
261}
262
263pub struct MoeFfn {
264 pub router: QTensor,
266 pub experts: Vec<DenseFfn>,
267 pub top_k: usize,
268 pub norm_topk_prob: bool,
269 pub router_sigmoid: bool,
272 pub expert_bias: Option<Vec<f32>>,
276 pub routed_scaling: f32,
279 pub shared: Option<(DenseFfn, Option<QTensor>)>,
282 pub stats: std::cell::RefCell<Vec<u64>>,
286}
287
288pub enum AttnKind {
291 Full {
293 wq: QTensor,
294 wk: QTensor,
295 wv: QTensor,
296 wo: QTensor,
297 q_norm: Option<Vec<f32>>,
298 k_norm: Option<Vec<f32>>,
299 output_gate: bool,
300 softplus_gate: Option<(QTensor, bool)>,
304 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
306 },
307 Linear(VmfPhaseWeights),
309 LinearGdn(GdnWeights),
311 ShortConv(ShortConvWeights),
314}
315
316pub struct MtpModule {
321 pub enorm: Vec<f32>,
322 pub hnorm: Vec<f32>,
323 pub eh_proj: QTensor,
325 pub layer: LayerWeights,
326 pub final_norm: Vec<f32>,
327 pub kv: crate::kv_cache::LayerKvCache,
328}
329
330pub struct GenerateResult {
332 pub text: String,
333 pub token_ids: Vec<u32>,
334 pub prompt_tokens: usize,
335 pub tokens_generated: usize,
336 pub finish_reason: String,
337 pub mtp_drafted: usize,
339 pub mtp_accepted: usize,
340 pub token_confidence: Vec<f32>,
345 pub traces: Vec<TokenTrace>,
348}
349
350#[derive(Clone, Debug)]
355pub struct TokenTrace {
356 pub t: usize,
358 pub token_id: u32,
360 pub confidence: f32,
362 pub active_skill: Option<String>,
364 pub recon: Option<f32>,
368 pub switched: bool,
371}
372
373fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
378 let t = if temp > 1e-3 { temp } else { 1.0 };
379 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
380 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
381 if sum > 0.0 {
382 (((logits[id as usize] - max) / t).exp()) / sum
383 } else {
384 0.0
385 }
386}
387
388fn prefill_batched() -> bool {
391 std::env::var("CMF_PREFILL")
392 .map(|v| v != "seq")
393 .unwrap_or(true)
394}
395
396fn prefill_chunk() -> usize {
401 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
402 .ok()
403 .and_then(|v| v.parse::<usize>().ok())
404 {
405 return n.max(1);
406 }
407 if cfg!(target_os = "macos") {
408 512
409 } else if cfg!(target_arch = "aarch64") {
410 256
413 } else {
414 48
415 }
416}
417
418pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
420
421impl Pipeline {
422 #[allow(clippy::too_many_arguments)]
424
425 #[cfg(target_os = "macos")]
440 fn graph_prefill_preferred(&self) -> bool {
441 if !crate::gpu::enabled_here()
442 || !crate::gpu::q1_force()
443 || std::env::var("CMF_GPU_BLOCK")
444 .map(|v| v == "0")
445 .unwrap_or(false)
446 {
447 return false;
448 }
449 self.weights
450 .layers
451 .iter()
452 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
453 }
454
455 #[cfg(not(target_os = "macos"))]
456 fn graph_prefill_preferred(&self) -> bool {
457 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
465 .map(|v| v != "0")
466 .unwrap_or_else(|_| {
467 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
468 });
469 if !graph_on || !crate::gpu::enabled_here() {
470 return false;
471 }
472 self.weights
473 .layers
474 .iter()
475 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
476 }
477
478 #[cfg(target_os = "macos")]
479 fn q1_graph_gpu(
480 &mut self,
481 start: usize,
482 upto: Option<usize>,
483 position: usize,
484 h: &mut [f32],
485 ) -> usize {
486 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
487 if !crate::gpu::enabled_here()
488 || !crate::gpu::q1_force()
489 || std::env::var("CMF_GPU_BLOCK")
490 .map(|v| v == "0")
491 .unwrap_or(false)
492 {
493 return start;
494 }
495 if self.swa.is_some()
500 || self.global_attn.is_some()
501 || self.attention_heads_per_layer.is_some()
502 || self.attn_v_norm
503 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
504 || self.weights.layers.iter().any(|lw| {
505 lw.attn_out_norm.is_some()
506 || lw.ffn_out_norm.is_some()
507 || lw.layer_scale.is_some()
508 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
509 })
510 {
511 return start;
512 }
513 let limit = upto
514 .map(|u| u + 1)
515 .unwrap_or(self.num_layers)
516 .min(self.num_layers);
517
518 enum Item<'a> {
519 Gdn {
520 run: Vec<GdnGpuLayer<'a>>,
521 first: usize,
522 },
523 Attn {
524 l: AttnGpuLayer<'a>,
525 li: usize,
526 q_norm: Option<&'a [f32]>,
527 k_norm: Option<&'a [f32]>,
528 output_gate: bool,
529 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
530 full_gpu: bool,
533 },
534 }
535
536 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
538 let dev_attend = attend_mode != "0"
539 && attend_mode != "off"
540 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
544 && self.head_dim % 4 == 0
545 && self.head_dim <= 256
546 && self.rotary_dim >= 2
547 && self.rotary_dim <= self.head_dim
548 && (self.rotary_dim / 2) % 32 == 0
549 && self.num_kv_heads > 0
550 && self.num_heads % self.num_kv_heads == 0;
551
552 let mut plan: Vec<Item> = Vec::new();
553 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
554 let mut scan = start;
555 while scan < limit {
556 let lw = &self.weights.layers[scan];
557 let FfnKind::Dense(d) = &lw.ffn else { break };
558 let (Some(g), Some(u), Some(dn)) = (
559 d.gate_proj.q1_parts(),
560 d.up_proj.q1_parts(),
561 d.down_proj.q1_parts(),
562 ) else {
563 break;
564 };
565 match &lw.attn {
566 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
567 let parts = (
568 w.in_proj_qkv.q1_parts(),
569 w.in_proj_z.q1_parts(),
570 w.in_proj_a.f32_parts(),
571 w.in_proj_b.f32_parts(),
572 w.out_proj.q1_parts(),
573 );
574 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
575 break;
576 };
577 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
578 model_ref.get_or_insert_with(|| model.clone());
579 }
580 let gl = GdnGpuLayer {
581 attn_norm: &lw.input_norm,
582 post_norm: &lw.post_norm,
583 qkv,
584 z,
585 a,
586 b,
587 out,
588 gate: g,
589 up: u,
590 down: dn,
591 conv1d: &w.conv1d,
592 a_log: &w.a_log,
593 dt_bias: &w.dt_bias,
594 gnorm: &w.norm,
595 };
596 match plan.last_mut() {
597 Some(Item::Gdn { run, .. }) => run.push(gl),
598 _ => plan.push(Item::Gdn {
599 run: vec![gl],
600 first: scan,
601 }),
602 }
603 }
604 AttnKind::Full {
605 wq,
606 wk,
607 wv,
608 wo,
609 q_norm,
610 k_norm,
611 output_gate,
612 softplus_gate: None,
613 bias,
614 } if !self.kv_cache.layers[scan].o1_sealed() => {
615 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
616 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
617 break;
618 };
619 if let QTensor::Mapped { model, .. } = wq {
620 model_ref.get_or_insert_with(|| model.clone());
621 }
622 let cache = &self.kv_cache.layers[scan];
623 let full_gpu = dev_attend
624 && cache.mode == crate::kv_cache::KvMode::F32
625 && cache.o1.is_none()
626 && bias.is_none()
627 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
628 && pk.1 == self.num_kv_heads * self.head_dim
629 && pv.1 == self.num_kv_heads * self.head_dim
630 && po.2 == self.num_heads * self.head_dim;
631 plan.push(Item::Attn {
632 l: AttnGpuLayer {
633 attn_norm: &lw.input_norm,
634 post_norm: &lw.post_norm,
635 wq: pq,
636 wk: pk,
637 wv: pv,
638 wo: po,
639 gate: g,
640 up: u,
641 down: dn,
642 },
643 li: scan,
644 q_norm: q_norm.as_deref(),
645 k_norm: k_norm.as_deref(),
646 output_gate: *output_gate,
647 bias: bias
648 .as_ref()
649 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
650 full_gpu,
651 });
652 }
653 _ => break,
654 }
655 scan += 1;
656 }
657 let Some(model) = model_ref else { return start };
658 if plan.is_empty() {
659 return start;
660 }
661 let dims = GraphDims {
662 hidden: self.hidden_size,
663 eps: self.rms_eps as f32,
664 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
665 };
666 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
667 return start;
668 };
669 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
670 nv: cfg.num_v_heads,
671 nk: cfg.num_k_heads,
672 dk: cfg.key_head_dim,
673 dv: cfg.value_head_dim,
674 kk: cfg.conv_kernel,
675 hidden: self.hidden_size,
676 inter: self.intermediate_size,
677 c_dim: cfg.conv_dim(),
678 eps: cfg.rms_eps as f32,
679 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
680 });
681 let mut valid = 0usize;
685 let mut end = start;
686 for item in &plan {
687 let ok = match item {
688 Item::Gdn { run, .. } => gcfg
689 .as_ref()
690 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
691 .unwrap_or(false),
692 Item::Attn { l, .. } => graph.attn_ok(l),
693 };
694 if !ok {
695 break;
696 }
697 valid += 1;
698 end += match item {
699 Item::Gdn { run, .. } => run.len(),
700 Item::Attn { .. } => 1,
701 };
702 }
703 plan.truncate(valid);
704 if plan.is_empty() {
705 return start;
706 }
707
708 let inv_freq = self.inv_freq.clone();
709 let pool = self.pool.clone();
710 let (nh, nkv, hd, hs, rd, eps) = (
711 self.num_heads,
712 self.num_kv_heads,
713 self.head_dim,
714 self.hidden_size,
715 self.rotary_dim,
716 self.rms_eps,
717 );
718 let norm_style = self.norm_style;
719 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
720 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
721 let kv_id = self.graph_kv_id;
722 let mut pending: Vec<(usize, usize)> = Vec::new();
725 let mut dev_attn: Vec<usize> = Vec::new();
728 for item in &plan {
729 match item {
730 Item::Gdn { run, first } => {
731 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
732 if l.linear_state.len() != want {
733 l.linear_state = vec![0f32; want];
734 }
735 }
736 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
737 .iter()
738 .map(|l| l.linear_state.as_slice())
739 .collect();
740 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
741 tracing::error!("q1 graph: GDN run refused after validation");
743 return start;
744 }
745 graph.commit();
748 pending.push((*first, run.len()));
749 }
750 Item::Attn {
751 l,
752 li,
753 q_norm,
754 k_norm,
755 output_gate,
756 bias,
757 full_gpu,
758 } => {
759 if *full_gpu {
761 let cache = &self.kv_cache.layers[*li];
762 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
763 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
764 let cpu_stored = cpu_k[0].len() / hd;
765 let p = crate::gpu::AttnDeviceParams {
766 kv_id,
767 layer: *li,
768 nh,
769 nkv,
770 hd,
771 rd,
772 position,
773 eps: eps as f32,
774 gemma,
775 output_gate: *output_gate,
776 q_norm: *q_norm,
777 k_norm: *k_norm,
778 inv_freq: &inv_freq,
779 cpu_k,
780 cpu_v,
781 cpu_stored,
782 };
783 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
784 graph.commit();
785 dev_attn.push(*li);
786 continue;
787 }
788 }
790 graph.encode_attn_prefix(l);
791 graph.sync();
792 if !pending.is_empty() {
793 let idxs: Vec<usize> =
794 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
795 let mut outs: Vec<&mut [f32]> = self
796 .kv_cache
797 .layers
798 .iter_mut()
799 .enumerate()
800 .filter(|(i, _)| idxs.binary_search(i).is_ok())
801 .map(|(_, s)| s.linear_state.as_mut_slice())
802 .collect();
803 graph.read_states(&mut outs);
804 }
805 let mut q_raw = attention::take_buf(l.wq.1);
806 let mut k = attention::take_buf(l.wk.1);
807 let mut v = attention::take_buf(l.wv.1);
808 graph.read_qkv(&mut q_raw, &mut k, &mut v);
809 let cfg = QwenAttnCfg {
810 num_heads: nh,
811 num_kv_heads: nkv,
812 head_dim: hd,
813 hidden_size: hs,
814 position,
815 inv_freq: &inv_freq,
816 rotary_dim: rd,
817 scale: self.attn_scale,
818 window: None,
819 v_norm: false,
820 q_norm: *q_norm,
821 k_norm: *k_norm,
822 output_gate: *output_gate,
823 softplus_gate: None,
824 rope_scale: 1.0,
825 bias: *bias,
826 rms_eps: eps,
827 norm_style,
828 pool: pool.as_deref(),
829 };
830 let mut ao = attention::qwen_attention_core(
831 q_raw,
832 k,
833 v,
834 &mut self.kv_cache.layers[*li],
835 &cfg,
836 );
837 graph.encode_attn_suffix(l, &ao);
838 graph.commit();
841 attention::recycle_buf(&mut ao);
842 }
843 }
844 }
845 let mut lm_rows = None;
850 if self.graph_want_logits
851 && upto.is_none()
852 && end == self.num_layers
853 && std::env::var("CMF_GPU_LMHEAD")
854 .map(|v| v != "0")
855 .unwrap_or(true)
856 {
857 if let Some(lm) = self.weights.lm_head.q1_parts() {
858 if graph.lm_head_ok(lm) {
859 graph.encode_lm_head(&self.weights.final_norm, lm);
860 lm_rows = Some(lm.1);
861 }
862 }
863 }
864 graph.sync();
865 if !pending.is_empty() {
866 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
867 let mut outs: Vec<&mut [f32]> = self
868 .kv_cache
869 .layers
870 .iter_mut()
871 .enumerate()
872 .filter(|(i, _)| idxs.binary_search(i).is_ok())
873 .map(|(_, s)| s.linear_state.as_mut_slice())
874 .collect();
875 graph.read_states(&mut outs);
876 }
877 if let Some(rows) = lm_rows {
878 let mut lg = attention::take_buf(rows.min(self.vocab_size));
879 graph.read_logits(&mut lg);
880 lg.resize(self.vocab_size, 0.0);
881 if let Some(c) = self.final_softcap {
882 for l in lg.iter_mut() {
883 *l = c * (*l / c).tanh();
884 }
885 }
886 self.graph_logits = Some(lg);
887 }
888 graph.finish(h);
889 for li in dev_attn {
893 let mut krow = attention::take_buf(nkv * hd);
894 let mut vrow = attention::take_buf(nkv * hd);
895 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
896 let cache = &mut self.kv_cache.layers[li];
897 cache.append(&krow, &vrow, &[]);
898 let n = cache.seq_len;
899 let mut imp = attention::take_buf(n);
900 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
901 cache.accumulate_imp(&imp);
902 attention::recycle_buf(&mut imp);
903 }
904 attention::recycle_buf(&mut krow);
905 attention::recycle_buf(&mut vrow);
906 }
907 end
908 }
909
910 pub fn new(
911 tokenizer: Tokenizer,
912 weights: PipelineWeights,
913 hidden_size: usize,
914 intermediate_size: usize,
915 num_heads: usize,
916 num_kv_heads: usize,
917 head_dim: usize,
918 num_layers: usize,
919 vocab_size: usize,
920 rms_eps: f64,
921 rope_base: f32,
922 norm_style: NormStyle,
923 max_seq_len: usize,
924 sampler_config: SamplerConfig,
925 ) -> Self {
926 let rng = match sampler_config.seed {
927 Some(s) => SplitMix64::new(s),
928 None => SplitMix64::from_entropy(),
929 };
930 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
931 let pool = Pool::from_env();
932 if let Some(p) = &pool {
933 tracing::info!("worker pool: {} threads", p.n_workers());
934 }
935 Self {
936 tokenizer: std::sync::Arc::new(tokenizer),
937 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
938 sampler_config,
939 weights,
940 hidden_size,
941 intermediate_size,
942 num_heads,
943 num_kv_heads,
944 head_dim,
945 num_layers,
946 vocab_size,
947 rms_eps,
948 rope_base,
949 norm_style,
950 rotary_dim: head_dim,
951 attention_heads_per_layer: None,
952 vmf_cfg: None,
953 gdn_cfg: None,
954 short_conv_cfg: None,
955 mtp: None,
956 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
957 rng,
958 sampler_scratch: SamplerScratch::default(),
959 inv_freq,
960 ws: ForwardScratch::new(hidden_size),
961 pool,
962 model: None,
963 dyn_force_f32: false,
964 dyn_skill_layers: Vec::new(),
965 dyn_active: None,
966 dyn_blend_loaded: false,
967 dyn_phi_layer: None,
968 dyn_phi_ema: Vec::new(),
969 dyn_phi_seen: 0,
970 dyn_router: None,
971 o1_cfg: None,
972 o1_flags: Vec::new(),
973 trace: false,
974 calib_temp: 1.0,
975 confidence_on: true,
976 embed_multiplier: 1.0,
977 attn_scale: 1.0 / (head_dim as f32).sqrt(),
978 swa: None,
979 sliding_layers: None,
980 inv_freq_local: None,
981 rotary_dim_local: None,
982 rope_scale: 1.0,
983 rope_scale_local: 1.0,
984 global_attn: None,
985 inv_freq_global: None,
986 attn_v_norm: false,
987 final_softcap: None,
988 graph_want_logits: false,
989 graph_logits: None,
990 graph_kv_id: {
991 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
992 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
993 },
994 }
995 }
996
997 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1004 self.o1_flags = match &cfg {
1005 Some(c) => {
1006 let mut flags = c.layer_flags(self.num_layers);
1007 for (li, f) in flags.iter_mut().enumerate() {
1008 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
1009 *f = false;
1010 }
1011 }
1012 flags
1013 }
1014 None => Vec::new(),
1015 };
1016 if let Some(c) = &cfg {
1017 let n = self.o1_flags.iter().filter(|&&f| f).count();
1018 tracing::info!(
1019 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1020 self.num_layers,
1021 c.m,
1022 c.w,
1023 c.sink,
1024 c.rect
1025 );
1026 }
1027 self.o1_cfg = cfg;
1028 }
1029
1030 pub fn o1_active(&self) -> bool {
1032 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1033 }
1034
1035 fn o1_begin(&mut self) {
1037 if let Some(c) = &self.o1_cfg {
1038 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1039 for (li, &f) in self.o1_flags.iter().enumerate() {
1040 if f {
1041 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1042 }
1043 }
1044 }
1045 }
1046
1047 fn o1_seal(&mut self) {
1050 if self.o1_cfg.is_none() {
1051 return;
1052 }
1053 for li in 0..self.num_layers {
1054 if self.o1_flags.get(li).copied().unwrap_or(false) {
1055 self.kv_cache.layers[li].o1_seal(self.num_heads);
1056 }
1057 }
1058 }
1059
1060 pub fn set_trace(&mut self, on: bool) {
1062 self.trace = on;
1063 }
1064
1065 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1068 self.rng = match config.seed {
1069 Some(seed) => SplitMix64::new(seed),
1070 None => SplitMix64::from_entropy(),
1071 };
1072 self.sampler_config = config;
1073 }
1074
1075 pub fn set_confidence(&mut self, on: bool) {
1080 self.confidence_on = on;
1081 }
1082
1083 pub fn set_calib_temp(&mut self, t: f32) {
1086 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1087 }
1088
1089 pub fn calib_temp(&self) -> f32 {
1091 self.calib_temp
1092 }
1093
1094 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1097 self.rotary_dim = rotary_dim.min(self.head_dim);
1098 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1099 }
1100
1101 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1102 QwenAttnCfg {
1103 num_heads: self.num_heads,
1104 num_kv_heads: self.num_kv_heads,
1105 head_dim: self.head_dim,
1106 hidden_size: self.hidden_size,
1107 position,
1108 inv_freq: &self.inv_freq,
1109 rotary_dim: self.rotary_dim,
1110 scale: self.attn_scale,
1111 window: None,
1112 v_norm: false,
1113 q_norm: None,
1114 k_norm: None,
1115 output_gate: false,
1116 softplus_gate: None,
1117 rope_scale: self.rope_scale,
1118 bias: None,
1119 rms_eps: self.rms_eps,
1120 norm_style: self.norm_style,
1121 pool: self.pool.as_deref(),
1122 }
1123 }
1124
1125 pub fn generate(
1127 &mut self,
1128 prompt: &str,
1129 max_tokens: usize,
1130 task_mask: Option<&TaskMask>,
1131 on_token: Option<TokenCallback>,
1132 ) -> Result<GenerateResult, String> {
1133 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1134 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1135 }
1136
1137 pub fn generate_from_ids(
1145 &mut self,
1146 input_ids: &[u32],
1147 max_tokens: usize,
1148 task_mask: Option<&TaskMask>,
1149 mut on_token: Option<TokenCallback>,
1150 ) -> Result<GenerateResult, String> {
1151 if std::env::var("CMF_TRACE_H").is_ok() {
1152 eprintln!("input_ids: {input_ids:?}");
1153 }
1154 if input_ids.is_empty() {
1155 return Err("empty prompt: nothing to generate from".to_string());
1156 }
1157
1158 self.kv_cache.clear();
1160 crate::gpu::graph_kv_reset(self.graph_kv_id);
1161 self.o1_begin();
1162
1163 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1169 .map(|v| v != "0")
1170 .unwrap_or_else(|_| {
1171 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
1172 });
1173 let spec_active = self.speculative
1174 && self.mtp.is_some()
1175 && task_mask.is_none()
1176 && !self.o1_active()
1177 && !graph_on
1178 && self.sampler_config.temperature < 1e-6;
1179 let mut mtp = if spec_active { self.mtp.take() } else { None };
1182 if let Some(m) = &mut mtp {
1183 m.kv.clear();
1184 }
1185 let mut router = if mtp.is_none() {
1189 self.dyn_router.take()
1190 } else {
1191 None
1192 };
1193 if let Some(r) = &mut router {
1194 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1197 }
1198
1199 let mut all_ids = input_ids.to_vec();
1200 let mut generated = 0usize;
1201 let mut finish_reason = "max_tokens".to_string();
1202 let mut drafted = 0usize;
1203 let mut accepted = 0usize;
1204 let mut confidence: Vec<f32> = Vec::new();
1205 let trace_on = self.trace;
1206 let calib_temp = self.calib_temp;
1207 let mut traces: Vec<TokenTrace> = Vec::new();
1208
1209 let mut hidden = vec![0.0f32; self.hidden_size];
1215 let mut pos = 0usize;
1216 let fuse_lm = mtp.is_none() && router.is_none();
1220 self.graph_logits = None;
1221 self.graph_want_logits = false;
1222 let dyn_prefill = router.is_some();
1227 let graph_prefill = self.graph_prefill_preferred();
1233 if task_mask.is_none()
1234 && !dyn_prefill
1235 && !graph_prefill
1236 && prefill_batched()
1237 && input_ids.len() > 2
1238 {
1239 let chunk = prefill_chunk();
1245 let hs = self.hidden_size;
1246 while pos < input_ids.len() {
1247 let end = (pos + chunk).min(input_ids.len());
1248 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1249 if let Some(m) = &mut mtp {
1250 for p in pos..end {
1251 if p + 1 < input_ids.len() {
1252 let _ = self.mtp_step(
1253 m,
1254 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1255 input_ids[p + 1],
1256 p,
1257 );
1258 }
1259 }
1260 }
1261 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1262 pos = end;
1263 }
1264 }
1265 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1266 while pos + 1 < input_ids.len() {
1267 let e1 = self.embed_single(input_ids[pos]);
1268 let e2 = self.embed_single(input_ids[pos + 1]);
1269 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1270 self.commit_linear_scratch();
1272 if let Some(m) = &mut mtp {
1273 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1274 if pos + 2 < input_ids.len() {
1275 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1276 }
1277 }
1278 hidden = h2;
1279 pos += 2;
1280 }
1281 }
1282 let _tpf = std::time::Instant::now();
1291 let batch_k = std::env::var("CMF_BATCH_K")
1292 .ok()
1293 .and_then(|v| v.parse::<usize>().ok())
1294 .unwrap_or(0);
1295 if batch_k > 0
1296 && graph_prefill
1297 && task_mask.is_none()
1298 && mtp.is_none()
1299 && !dyn_prefill
1300 && pos + 1 < input_ids.len()
1301 {
1302 let hs = self.hidden_size;
1303 let chunk = batch_k;
1304 while pos < input_ids.len() {
1305 let end = (pos + chunk).min(input_ids.len());
1306 let bk = end - pos;
1307 let mut hiddens = vec![0f32; bk * hs];
1308 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1309 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1310 }
1311 let positions: Vec<usize> = (pos..end).collect();
1312 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1313 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1314 pos = end;
1315 } else {
1316 break; }
1318 }
1319 }
1320 while pos < input_ids.len() {
1321 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1322 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1323 if let Some(m) = &mut mtp {
1324 if pos + 1 < input_ids.len() {
1325 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1326 }
1327 }
1328 pos += 1;
1329 }
1330 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1331 eprintln!(
1332 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1333 input_ids.len(),
1334 _tpf.elapsed().as_secs_f64() * 1000.0
1335 );
1336 }
1337 self.o1_seal();
1340
1341 macro_rules! commit {
1343 ($id:expr) => {{
1344 all_ids.push($id);
1345 generated += 1;
1346 if self.tokenizer.is_eos($id) {
1347 finish_reason = "stop".to_string();
1348 false
1349 } else {
1350 let token_text = self.tokenizer.decode_token($id);
1351 let mut go = true;
1352 if let Some(ref mut cb) = on_token {
1353 if !cb(&token_text) {
1354 finish_reason = "cancelled".to_string();
1355 go = false;
1356 }
1357 }
1358 go
1359 }
1360 }};
1361 }
1362
1363 let mut next_pos = input_ids.len();
1365 'decode: while generated < max_tokens {
1366 let mut logits = match self.graph_logits.take() {
1367 Some(lg) => lg,
1368 None => {
1369 inference::rms_norm_into(
1370 &hidden,
1371 &self.weights.final_norm,
1372 self.rms_eps,
1373 self.norm_style,
1374 &mut self.ws.n1,
1375 );
1376 self.lm_head_forward(&self.ws.n1)
1377 }
1378 };
1379 let t_next = sampler::sample_with_scratch(
1380 &logits,
1381 &self.sampler_config,
1382 &all_ids,
1383 &mut self.rng,
1384 &mut self.sampler_scratch,
1385 );
1386 if self.confidence_on {
1387 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1388 }
1389 attention::recycle_buf(&mut logits);
1390 if trace_on {
1391 let skill = router.as_ref().and_then(|r| r.active_id());
1395 traces.push(TokenTrace {
1396 t: generated,
1397 token_id: t_next,
1398 confidence: confidence.last().copied().unwrap_or(0.0),
1399 active_skill: skill,
1400 recon: None,
1401 switched: false,
1402 });
1403 }
1404 if !commit!(t_next) {
1405 break 'decode;
1406 }
1407 if generated >= max_tokens {
1408 break 'decode;
1409 }
1410
1411 if self.kv_cache.needs_eviction() {
1412 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1413 self.kv_cache.evict(keep);
1414 }
1415
1416 match &mut mtp {
1417 Some(m) if generated + 1 < max_tokens => {
1419 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1420 drafted += 1;
1421 let emb1 = self.embed_single(t_next);
1422 let emb2 = self.embed_single(draft);
1423 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1424
1425 inference::rms_norm_into(
1426 &h1,
1427 &self.weights.final_norm,
1428 self.rms_eps,
1429 self.norm_style,
1430 &mut self.ws.n1,
1431 );
1432 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1433 let t_after = sampler::sample_with_scratch(
1434 &logits1,
1435 &self.sampler_config,
1436 &all_ids,
1437 &mut self.rng,
1438 &mut self.sampler_scratch,
1439 );
1440 if self.confidence_on {
1441 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1442 }
1443 attention::recycle_buf(&mut logits1);
1444 if trace_on {
1445 traces.push(TokenTrace {
1448 t: generated,
1449 token_id: t_after,
1450 confidence: confidence.last().copied().unwrap_or(0.0),
1451 active_skill: None,
1452 recon: None,
1453 switched: false,
1454 });
1455 }
1456 let stop = !commit!(t_after);
1457
1458 if t_after == draft {
1459 accepted += 1;
1460 self.commit_linear_scratch();
1461 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1462 hidden = h2;
1463 next_pos += 2;
1464 } else {
1465 for layer in &mut self.kv_cache.layers {
1467 layer.truncate_last(1);
1468 }
1469 if !stop {
1470 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1471 hidden = self.forward_layers(
1472 &self.embed_single(t_after),
1473 next_pos + 1,
1474 None,
1475 );
1476 }
1477 next_pos += 2;
1478 }
1479 if stop {
1480 break 'decode;
1481 }
1482 }
1483 _ => {
1485 self.graph_want_logits = fuse_lm;
1486 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1487 next_pos += 1;
1488 if let Some(r) = &mut router {
1491 let phi = self.dyn_phi_ema.clone();
1492 let decision = r.step(&phi, generated);
1493 if let Some(new_active) = decision {
1494 let _ = self.set_active_skill(new_active);
1495 }
1496 if trace_on {
1499 if let Some(last) = traces.last_mut() {
1500 let e = r.last_best_e();
1501 last.recon = e.is_finite().then_some(e);
1502 last.switched = decision.is_some();
1503 }
1504 }
1505 }
1506 }
1507 }
1508 }
1509
1510 self.graph_want_logits = false;
1511 self.graph_logits = None;
1512 if router.is_some() {
1514 let _ = self.set_active_skill(None);
1515 }
1516 self.dyn_router = router.or(self.dyn_router.take());
1517 self.mtp = mtp.or(self.mtp.take());
1518
1519 let output_ids = &all_ids[input_ids.len()..];
1520 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1522 Ok(GenerateResult {
1523 text: self.tokenizer.decode(output_ids),
1524 token_ids: output_ids.to_vec(),
1525 prompt_tokens: input_ids.len(),
1526 tokens_generated: generated,
1527 finish_reason,
1528 mtp_drafted: drafted,
1529 mtp_accepted: accepted,
1530 token_confidence: confidence,
1531 traces,
1532 })
1533 }
1534
1535 fn mtp_step(
1539 &mut self,
1540 m: &mut MtpModule,
1541 hidden: &[f32],
1542 next_token: u32,
1543 position: usize,
1544 ) -> u32 {
1545 let e = self.embed_single(next_token);
1549 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1550 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1551 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1552 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1553 let mut x = vec![0.0f32; self.hidden_size];
1554 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1555
1556 let lw = &m.layer;
1558 inference::rms_norm_into(
1559 &x,
1560 &lw.input_norm,
1561 self.rms_eps,
1562 self.norm_style,
1563 &mut self.ws.n1,
1564 );
1565 let attn = match &lw.attn {
1566 AttnKind::Full {
1567 wq,
1568 wk,
1569 wv,
1570 wo,
1571 q_norm,
1572 k_norm,
1573 output_gate,
1574 softplus_gate,
1575 bias,
1576 } => {
1577 let mut cfg = self.attn_cfg(position);
1578 cfg.q_norm = q_norm.as_deref();
1579 cfg.k_norm = k_norm.as_deref();
1580 cfg.output_gate = *output_gate;
1581 cfg.softplus_gate = softplus_gate
1582 .as_ref()
1583 .map(|(gate, per_head)| (gate, *per_head));
1584 cfg.bias = bias
1585 .as_ref()
1586 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1587 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1588 }
1589 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1590 unreachable!("MTP block is full attention")
1591 }
1592 };
1593 for (i, &a) in attn.iter().enumerate() {
1594 x[i] += a;
1595 }
1596 inference::rms_norm_into(
1597 &x,
1598 &lw.post_norm,
1599 self.rms_eps,
1600 self.norm_style,
1601 &mut self.ws.p1,
1602 );
1603 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1604 for (i, &f) in ffn.iter().enumerate() {
1605 x[i] += f;
1606 }
1607
1608 inference::rms_norm_into(
1609 &x,
1610 &m.final_norm,
1611 self.rms_eps,
1612 self.norm_style,
1613 &mut self.ws.n1,
1614 );
1615 let mut lg = self.lm_head_forward(&self.ws.n1);
1616 let draft = sampler::argmax(&lg);
1617 attention::recycle_buf(&mut lg);
1618 draft
1619 }
1620
1621 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1625 let emb1 = self.embed_single(1);
1626 let emb2 = self.embed_single(2);
1627 let pos = self.kv_cache.seq_len();
1628
1629 let t0 = std::time::Instant::now();
1630 for _ in 0..iters {
1631 let _ = self.forward_layers(&emb1, pos, None);
1632 let _ = self.forward_layers(&emb2, pos + 1, None);
1633 for l in &mut self.kv_cache.layers {
1634 l.truncate_last(2);
1635 }
1636 }
1637 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1638
1639 let t1 = std::time::Instant::now();
1640 for _ in 0..iters {
1641 let _ = self.forward_pair(&emb1, &emb2, pos);
1642 for l in &mut self.kv_cache.layers {
1643 l.truncate_last(2);
1644 }
1645 }
1646 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1647 (singles_ms, pair_ms)
1648 }
1649
1650 fn forward_pair(
1655 &mut self,
1656 emb1: &[f32],
1657 emb2: &[f32],
1658 position: usize,
1659 ) -> (Vec<f32>, Vec<f32>) {
1660 let mut h1 = emb1.to_vec();
1661 let mut h2 = emb2.to_vec();
1662 let (_nkv, _hd, hs, _rd, eps) = (
1663 self.num_kv_heads,
1664 self.head_dim,
1665 self.hidden_size,
1666 self.rotary_dim,
1667 self.rms_eps,
1668 );
1669 let pool = self.pool.clone();
1670
1671 for li in 0..self.num_layers {
1672 let lw = &self.weights.layers[li];
1673 inference::rms_norm_into(
1676 &h1,
1677 &lw.input_norm,
1678 self.rms_eps,
1679 self.norm_style,
1680 &mut self.ws.n1,
1681 );
1682 inference::rms_norm_into(
1683 &h2,
1684 &lw.input_norm,
1685 self.rms_eps,
1686 self.norm_style,
1687 &mut self.ws.n2,
1688 );
1689
1690 let (a1, a2) = match &lw.attn {
1691 AttnKind::Linear(w) => {
1692 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1693 let layer = &mut self.kv_cache.layers[li];
1694 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1695 vmf_phase_pair(
1696 &self.ws.n1,
1697 &self.ws.n2,
1698 w,
1699 &cfg,
1700 state,
1701 scratch,
1702 self.pool.as_deref(),
1703 )
1704 }
1705 AttnKind::LinearGdn(w) => {
1706 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1707 let layer = &mut self.kv_cache.layers[li];
1708 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1709 gdn_pair(
1710 &self.ws.n1,
1711 &self.ws.n2,
1712 w,
1713 &cfg,
1714 state,
1715 scratch,
1716 self.pool.as_deref(),
1717 )
1718 }
1719 AttnKind::ShortConv(w) => {
1720 let cfg = self
1721 .short_conv_cfg
1722 .expect("short-conv layer without short_conv_cfg");
1723 let layer = &mut self.kv_cache.layers[li];
1724 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1725 short_conv_pair(
1726 &self.ws.n1,
1727 &self.ws.n2,
1728 w,
1729 &cfg,
1730 state,
1731 scratch,
1732 self.pool.as_deref(),
1733 )
1734 }
1735 AttnKind::Full {
1736 wq,
1737 wk,
1738 wv,
1739 wo,
1740 q_norm,
1741 k_norm,
1742 output_gate,
1743 softplus_gate,
1744 bias,
1745 } => {
1746 let inv_freq_l = self.layer_inv_freq(li);
1747 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1748 let cfg = QwenAttnCfg {
1749 num_heads: self.layer_num_heads(li),
1750 num_kv_heads: nkv_l,
1751 head_dim: hd_l,
1752 hidden_size: hs,
1753 position,
1754 inv_freq: &inv_freq_l,
1755 rotary_dim: rd_l,
1756 scale: self.attn_scale,
1757 window: self.layer_window(li),
1758 v_norm: self.attn_v_norm,
1759 q_norm: q_norm.as_deref(),
1760 k_norm: k_norm.as_deref(),
1761 output_gate: *output_gate,
1762 softplus_gate: softplus_gate
1763 .as_ref()
1764 .map(|(gate, per_head)| (gate, *per_head)),
1765 rope_scale: self.layer_rope_scale(li),
1766 bias: bias
1767 .as_ref()
1768 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1769 rms_eps: eps,
1770 norm_style: self.norm_style,
1771 pool: pool.as_deref(),
1772 };
1773 attention::qwen_attention_pair(
1774 &self.ws.n1,
1775 &self.ws.n2,
1776 wq,
1777 wk,
1778 wv,
1779 wo,
1780 &mut self.kv_cache.layers[li],
1781 &cfg,
1782 )
1783 }
1784 };
1785 let (a1, a2) = match &self.weights.layers[li].attn_out_norm {
1786 Some(w) => (
1787 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1788 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1789 ),
1790 None => (a1, a2),
1791 };
1792 for i in 0..self.hidden_size {
1793 h1[i] += a1[i];
1794 h2[i] += a2[i];
1795 }
1796 let (mut a1, mut a2) = (a1, a2);
1797 attention::recycle_buf(&mut a1);
1798 attention::recycle_buf(&mut a2);
1799
1800 let lw = &self.weights.layers[li];
1801 inference::rms_norm_into(
1802 &h1,
1803 &lw.post_norm,
1804 self.rms_eps,
1805 self.norm_style,
1806 &mut self.ws.p1,
1807 );
1808 inference::rms_norm_into(
1809 &h2,
1810 &lw.post_norm,
1811 self.rms_eps,
1812 self.norm_style,
1813 &mut self.ws.p2,
1814 );
1815 let (f1, f2) =
1816 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1817 let (f1, f2) = match &self.weights.layers[li].ffn_out_norm {
1818 Some(w) => (
1819 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1820 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1821 ),
1822 None => (f1, f2),
1823 };
1824 for i in 0..self.hidden_size {
1825 h1[i] += f1[i];
1826 h2[i] += f2[i];
1827 }
1828 let (mut f1, mut f2) = (f1, f2);
1829 attention::recycle_buf(&mut f1);
1830 attention::recycle_buf(&mut f2);
1831 if let Some(sc) = self.weights.layers[li].layer_scale {
1832 for i in 0..self.hidden_size {
1833 h1[i] *= sc;
1834 h2[i] *= sc;
1835 }
1836 }
1837 }
1838 (h1, h2)
1839 }
1840
1841 fn commit_linear_scratch(&mut self) {
1843 for layer in &mut self.kv_cache.layers {
1844 if !layer.linear_scratch.is_empty() {
1845 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1846 layer.linear_scratch.clear();
1847 }
1848 }
1849 }
1850
1851 pub fn forward_ids(
1854 &mut self,
1855 ids: &[u32],
1856 task_mask: Option<&TaskMask>,
1857 ) -> Result<Vec<f32>, String> {
1858 if ids.is_empty() {
1859 return Err("empty id sequence".to_string());
1860 }
1861 self.kv_cache.clear();
1862 self.o1_begin();
1863 let mut hidden = vec![0.0f32; self.hidden_size];
1864 let mut pos = 0usize;
1865 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1866 let chunk = prefill_chunk();
1870 let hs = self.hidden_size;
1871 while pos < ids.len() {
1872 let end = (pos + chunk).min(ids.len());
1873 let hb = self.prefill_batch(&ids[pos..end], pos);
1874 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1875 pos = end;
1876 }
1877 }
1878 if task_mask.is_none() {
1879 while pos + 1 < ids.len() {
1880 let e1 = self.embed_single(ids[pos]);
1881 let e2 = self.embed_single(ids[pos + 1]);
1882 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1883 self.commit_linear_scratch();
1884 hidden = h2;
1885 pos += 2;
1886 }
1887 }
1888 while pos < ids.len() {
1889 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1890 pos += 1;
1891 }
1892 self.o1_seal();
1896 let normed = inference::rms_norm(
1897 &hidden,
1898 &self.weights.final_norm,
1899 self.rms_eps,
1900 self.norm_style,
1901 );
1902 Ok(self.lm_head_forward(&normed))
1903 }
1904
1905 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1912 let (nll, cnt) = self.nll_ids_from(ids, 0);
1913 (nll / cnt.max(1) as f64).exp()
1914 }
1915
1916 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
1921 self.kv_cache.clear();
1922 FFN_PROBE.with(|p| {
1923 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
1924 });
1925 crate::gpu::cpu_scope(|| {
1926 for (pos, &id) in ids.iter().enumerate() {
1927 let emb = self.embed_single(id);
1928 let _ = self.forward_layers(&emb, pos, None);
1929 }
1930 });
1931 self.kv_cache.clear();
1932 FFN_PROBE
1933 .with(|p| p.borrow_mut().take())
1934 .unwrap_or_default()
1935 }
1936
1937 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
1941 self.kv_cache.clear();
1942 let mut nll = 0f64;
1943 let mut cnt = 0usize;
1944 let mut hidden = vec![0f32; self.hidden_size];
1945 for (pos, &id) in ids.iter().enumerate() {
1946 if pos > 0 {
1947 inference::rms_norm_into(
1948 &hidden,
1949 &self.weights.final_norm,
1950 self.rms_eps,
1951 self.norm_style,
1952 &mut self.ws.n1,
1953 );
1954 let mut logits = self.lm_head_forward(&self.ws.n1);
1955 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1956 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
1957 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
1958 nll -= p.max(1e-300).ln();
1959 cnt += 1;
1960 attention::recycle_buf(&mut logits);
1961 }
1962 let emb = self.embed_single(id);
1963 hidden = self.forward_layers(&emb, pos, Some(mask));
1964 }
1965 self.kv_cache.clear();
1966 (nll / cnt.max(1) as f64).exp()
1967 }
1968
1969 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1978 self.kv_cache.clear();
1979 let mut nll = 0f64;
1980 let mut cnt = 0usize;
1981 if prefill_batched() {
1982 const CHUNK: usize = 128;
1988 const LM_SUB: usize = 32;
1989 let n = ids.len().saturating_sub(1);
1990 let hs = self.hidden_size;
1991 let rows = self.weights.lm_head.rows();
1992 let mut pos = 0usize;
1993 while pos < n {
1994 let end = (pos + CHUNK).min(n);
1995 let bsz = end - pos;
1996 let hb = self.prefill_batch(&ids[pos..end], pos);
1997 let mut k0 = 0usize;
1998 while k0 < bsz {
1999 let k1 = (k0 + LM_SUB).min(bsz);
2000 let sb = k1 - k0;
2001 if pos + k1 <= start {
2004 k0 = k1;
2005 continue;
2006 }
2007 let mut normed = vec![0.0f32; sb * hs];
2008 for k in 0..sb {
2009 let r = inference::rms_norm(
2010 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2011 &self.weights.final_norm,
2012 self.rms_eps,
2013 self.norm_style,
2014 );
2015 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2016 }
2017 let mut logits = vec![0.0f32; sb * rows];
2018 self.weights
2019 .lm_head
2020 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2021 for k in 0..sb {
2022 if pos + k0 + k < start {
2023 continue;
2024 }
2025 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2026 let target = ids[pos + k0 + k + 1] as usize;
2027 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2028 let lse: f64 = lg
2029 .iter()
2030 .map(|&v| ((v - max) as f64).exp())
2031 .sum::<f64>()
2032 .ln()
2033 + max as f64;
2034 nll += lse - lg[target] as f64;
2035 cnt += 1;
2036 }
2037 k0 = k1;
2038 }
2039 pos = end;
2040 }
2041 self.kv_cache.clear();
2042 return (nll, cnt);
2043 }
2044 for pos in 0..ids.len().saturating_sub(1) {
2045 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2046 if pos < start {
2047 continue;
2048 }
2049 let normed = inference::rms_norm(
2050 &hidden,
2051 &self.weights.final_norm,
2052 self.rms_eps,
2053 self.norm_style,
2054 );
2055 let logits = self.lm_head_forward(&normed);
2056 let target = ids[pos + 1] as usize;
2057 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2058 let lse: f64 = logits
2059 .iter()
2060 .map(|&v| ((v - max) as f64).exp())
2061 .sum::<f64>()
2062 .ln()
2063 + max as f64;
2064 nll += lse - logits[target] as f64;
2065 cnt += 1;
2066 }
2067 self.kv_cache.clear();
2068 (nll, cnt)
2069 }
2070
2071 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
2087 self.kv_cache.clear();
2088 self.o1_begin();
2089 let n = ids.len().saturating_sub(1);
2090 let p = prefill.min(n);
2091 let mut pos = 0usize;
2093 if prefill_batched() {
2094 const CHUNK: usize = 128;
2095 while pos < p {
2096 let end = (pos + CHUNK).min(p);
2097 let _ = self.prefill_batch(&ids[pos..end], pos);
2098 pos = end;
2099 }
2100 } else {
2101 while pos < p {
2102 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2103 pos += 1;
2104 }
2105 }
2106 self.o1_seal();
2107
2108 let mut nll = 0f64;
2109 let mut cnt = 0usize;
2110 for pos in p..n {
2111 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2112 let normed = inference::rms_norm(
2113 &hidden,
2114 &self.weights.final_norm,
2115 self.rms_eps,
2116 self.norm_style,
2117 );
2118 let logits = self.lm_head_forward(&normed);
2119 let target = ids[pos + 1] as usize;
2120 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2121 let lse: f64 = logits
2122 .iter()
2123 .map(|&v| ((v - max) as f64).exp())
2124 .sum::<f64>()
2125 .ln()
2126 + max as f64;
2127 nll += lse - logits[target] as f64;
2128 cnt += 1;
2129 }
2130 self.kv_cache.clear();
2131 (nll, cnt)
2132 }
2133
2134 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
2142 self.kv_cache.clear();
2143 let n = ids.len().saturating_sub(1);
2144 let mut correct = Vec::with_capacity(n);
2145 let mut pmax = Vec::with_capacity(n);
2146 for pos in 0..n {
2147 let emb = self.embed_single(ids[pos]);
2148 let hidden = self.forward_layers(&emb, pos, None);
2149 let normed = inference::rms_norm(
2150 &hidden,
2151 &self.weights.final_norm,
2152 self.rms_eps,
2153 self.norm_style,
2154 );
2155 let logits = self.lm_head_forward(&normed);
2156 let target = ids[pos + 1] as usize;
2157 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
2158 for (i, &v) in logits.iter().enumerate() {
2159 if v > mval {
2160 mval = v;
2161 amax = i;
2162 }
2163 }
2164 correct.push(amax == target);
2165 let row: Vec<f32> = temps
2166 .iter()
2167 .map(|&t| {
2168 let tt = t.max(1e-3);
2169 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
2170 1.0 / s.max(1e-12) })
2172 .collect();
2173 pmax.push(row);
2174 }
2175 self.kv_cache.clear();
2176 (correct, pmax)
2177 }
2178
2179 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
2186 let mut router = match self.dyn_router.take() {
2187 Some(r) => r,
2188 None => return (self.ppl_ids(ids), 0),
2189 };
2190 router.reset();
2191 self.dyn_phi_seen = 0;
2192 let _ = self.set_active_skill(None);
2193
2194 self.kv_cache.clear();
2195 let mut nll = 0f64;
2196 let mut cnt = 0usize;
2197 for pos in 0..ids.len().saturating_sub(1) {
2198 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
2199 let normed = inference::rms_norm(
2200 &hidden,
2201 &self.weights.final_norm,
2202 self.rms_eps,
2203 self.norm_style,
2204 );
2205 let logits = self.lm_head_forward(&normed);
2206 let target = ids[pos + 1] as usize;
2207 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2208 let lse: f64 = logits
2209 .iter()
2210 .map(|&v| ((v - max) as f64).exp())
2211 .sum::<f64>()
2212 .ln()
2213 + max as f64;
2214 nll += lse - logits[target] as f64;
2215 cnt += 1;
2216 let phi = self.dyn_phi_ema.clone();
2218 if let Some(new_active) = router.step(&phi, pos) {
2219 let _ = self.set_active_skill(new_active);
2220 }
2221 }
2222 let switches = router.switches.len();
2223 let _ = self.set_active_skill(None);
2224 self.dyn_router = Some(router);
2225 self.kv_cache.clear();
2226 ((nll / cnt.max(1) as f64).exp(), switches)
2227 }
2228
2229 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2231 self.kv_cache.clear();
2232 let mut acc = vec![0f32; self.hidden_size];
2233 for (pos, &id) in ids.iter().enumerate() {
2234 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2235 for (a, v) in acc.iter_mut().zip(&h) {
2236 *a += v;
2237 }
2238 }
2239 let n = ids.len().max(1) as f32;
2240 for a in acc.iter_mut() {
2241 *a /= n;
2242 }
2243 self.kv_cache.clear();
2244 acc
2245 }
2246
2247 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2253 let b = ids.len();
2254 let hs = self.hidden_size;
2255 let mut h: Vec<f32> = vec![0.0; b * hs];
2258 let mut h_ready = false;
2259 let fill_h = |h: &mut Vec<f32>, me: &Self| {
2260 for (bi, &id) in ids.iter().enumerate() {
2261 let e = me.embed_single(id);
2262 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2263 }
2264 };
2265 let (_nkv, _hd, _rd, eps) = (
2266 self.num_kv_heads,
2267 self.head_dim,
2268 self.rotary_dim,
2269 self.rms_eps,
2270 );
2271 let pool = self.pool.clone();
2272 let norm_style = self.norm_style;
2273
2274 #[cfg(target_os = "macos")]
2275 let mut chunk_skip_until = 0usize;
2276 for li in 0..self.num_layers {
2277 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2284 {
2285 if li < chunk_skip_until {
2286 continue;
2287 }
2288 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2289 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2290 if end > li {
2291 h_ready = true;
2292 chunk_skip_until = end;
2293 continue;
2294 }
2295 }
2296 if !h_ready {
2297 fill_h(&mut h, self);
2298 h_ready = true;
2299 }
2300 let lw = &self.weights.layers[li];
2301 match &lw.attn {
2303 AttnKind::LinearGdn(w) => {
2304 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2306 let mut normed = vec![0.0f32; b * hs];
2307 for bi in 0..b {
2308 let r = inference::rms_norm(
2309 &h[bi * hs..(bi + 1) * hs],
2310 &lw.input_norm,
2311 eps,
2312 norm_style,
2313 );
2314 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2315 }
2316 let attn = crate::linear_core::gdn_forward_batch(
2317 &normed,
2318 b,
2319 w,
2320 &cfg,
2321 &mut self.kv_cache.layers[li].linear_state,
2322 pool.as_deref(),
2323 );
2324 for (dst, &a) in h.iter_mut().zip(&attn) {
2325 *dst += a;
2326 }
2327 }
2328 AttnKind::ShortConv(w) => {
2329 let cfg = self
2332 .short_conv_cfg
2333 .expect("short-conv layer without short_conv_cfg");
2334 let mut normed = vec![0.0f32; b * hs];
2335 for bi in 0..b {
2336 inference::rms_norm_into(
2337 &h[bi * hs..(bi + 1) * hs],
2338 &lw.input_norm,
2339 eps,
2340 norm_style,
2341 &mut normed[bi * hs..(bi + 1) * hs],
2342 );
2343 }
2344 let attn = short_conv_forward_batch(
2345 &normed,
2346 b,
2347 w,
2348 &cfg,
2349 &mut self.kv_cache.layers[li].linear_state,
2350 pool.as_deref(),
2351 );
2352 for (dst, &a) in h.iter_mut().zip(&attn) {
2353 *dst += a;
2354 }
2355 }
2356 AttnKind::Full {
2357 wq,
2358 wk,
2359 wv,
2360 wo,
2361 q_norm,
2362 k_norm,
2363 output_gate,
2364 softplus_gate,
2365 bias,
2366 } => {
2367 let mut normed = vec![0.0f32; b * hs];
2371 for bi in 0..b {
2372 inference::rms_norm_into(
2373 &h[bi * hs..(bi + 1) * hs],
2374 &lw.input_norm,
2375 eps,
2376 norm_style,
2377 &mut normed[bi * hs..(bi + 1) * hs],
2378 );
2379 }
2380 let inv_freq_l = self.layer_inv_freq(li);
2381 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2382 let cfg = QwenAttnCfg {
2383 num_heads: self.layer_num_heads(li),
2384 num_kv_heads: nkv_l,
2385 head_dim: hd_l,
2386 hidden_size: hs,
2387 position: start_pos,
2388 inv_freq: &inv_freq_l,
2389 rotary_dim: rd_l,
2390 scale: self.attn_scale,
2391 window: self.layer_window(li),
2392 v_norm: self.attn_v_norm,
2393 q_norm: q_norm.as_deref(),
2394 k_norm: k_norm.as_deref(),
2395 output_gate: *output_gate,
2396 softplus_gate: softplus_gate
2397 .as_ref()
2398 .map(|(gate, per_head)| (gate, *per_head)),
2399 rope_scale: self.layer_rope_scale(li),
2400 bias: bias
2401 .as_ref()
2402 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2403 rms_eps: eps,
2404 norm_style,
2405 pool: pool.as_deref(),
2406 };
2407 let mut attn = attention::qwen_attention_batch(
2408 &normed,
2409 b,
2410 wq,
2411 wk,
2412 wv,
2413 wo,
2414 &mut self.kv_cache.layers[li],
2415 &cfg,
2416 );
2417 if let Some(w) = &lw.attn_out_norm {
2418 for bi in 0..b {
2419 inference::rms_norm_into(
2420 &attn[bi * hs..(bi + 1) * hs],
2421 w,
2422 eps,
2423 norm_style,
2424 &mut normed[bi * hs..(bi + 1) * hs],
2425 );
2426 }
2427 attn.copy_from_slice(&normed);
2428 }
2429 for (dst, &a) in h.iter_mut().zip(&attn) {
2430 *dst += a;
2431 }
2432 }
2433 AttnKind::Linear(w) => {
2434 for bi in 0..b {
2435 let normed = inference::rms_norm(
2436 &h[bi * hs..(bi + 1) * hs],
2437 &lw.input_norm,
2438 eps,
2439 norm_style,
2440 );
2441 vmf_phase_forward(
2442 &normed,
2443 w,
2444 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2445 &mut self.kv_cache.layers[li].linear_state,
2446 pool.as_deref(),
2447 )
2448 .iter()
2449 .enumerate()
2450 .for_each(|(i, &a)| h[bi * hs + i] += a);
2451 }
2452 }
2453 }
2454
2455 let lw = &self.weights.layers[li];
2457 let mut post = vec![0.0f32; b * hs];
2458 for bi in 0..b {
2459 let r =
2460 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2461 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2462 }
2463 let mut ffn = match &lw.ffn {
2464 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2465 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
2466 };
2467 if let Some(w) = &lw.ffn_out_norm {
2468 for bi in 0..b {
2469 inference::rms_norm_into(
2470 &ffn[bi * hs..(bi + 1) * hs],
2471 w,
2472 eps,
2473 norm_style,
2474 &mut post[bi * hs..(bi + 1) * hs],
2475 );
2476 }
2477 ffn.copy_from_slice(&post);
2478 }
2479 for (dst, &f) in h.iter_mut().zip(&ffn) {
2480 *dst += f;
2481 }
2482 if let Some(sc) = lw.layer_scale {
2483 for v in h.iter_mut() {
2484 *v *= sc;
2485 }
2486 }
2487 if std::env::var("CMF_TRACE_H").is_ok() {
2488 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2489 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2490 eprintln!(
2491 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
2492 lw.layer_scale
2493 );
2494 }
2495 }
2496 crate::gpu::set_layer(-1); h
2498 }
2499
2500 fn embed_single(&self, id: u32) -> Vec<f32> {
2502 let mut out = vec![0.0f32; self.hidden_size];
2503 if (id as usize) < self.weights.embed_tokens.rows() {
2504 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2505 }
2506 if self.embed_multiplier != 1.0 {
2507 for v in out.iter_mut() {
2508 *v *= self.embed_multiplier;
2509 }
2510 }
2511 out
2512 }
2513
2514 #[cfg(target_os = "macos")]
2520 fn chunk_run_gpu(
2521 &mut self,
2522 li0: usize,
2523 h: &mut [f32],
2524 b: usize,
2525 pos0: usize,
2526 embed_ids: Option<&[u32]>,
2527 ) -> usize {
2528 if !crate::gpu::enabled_here()
2532 || std::env::var("CMF_GPU_CHUNK")
2533 .map(|v| v == "0")
2534 .unwrap_or(false)
2535 || b < 32
2536 || self.swa.is_some()
2537 || self.global_attn.is_some()
2538 || self.attn_v_norm
2539 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2540 {
2541 return li0;
2542 }
2543 let Some(model) = self.model.clone() else {
2544 return li0;
2545 };
2546 let inv_freq = self.inv_freq.clone();
2547 let (nh, nkv, hd, hs) = (
2548 self.num_heads,
2549 self.num_kv_heads,
2550 self.head_dim,
2551 self.hidden_size,
2552 );
2553 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2555 let mut stored_at: Vec<usize> = Vec::new();
2556 for li in li0..self.num_layers {
2557 let lw = &self.weights.layers[li];
2558 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
2559 break;
2560 }
2561 let AttnKind::Full {
2562 wq,
2563 wk,
2564 wv,
2565 wo,
2566 q_norm,
2567 k_norm,
2568 output_gate: false,
2569 softplus_gate: None,
2570 bias,
2571 } = &lw.attn
2572 else {
2573 break;
2574 };
2575 let FfnKind::Dense(d) = &lw.ffn else { break };
2576 if d.act != Act::Silu {
2577 break;
2578 }
2579 let parts = (
2580 wq.q8_row_parts(),
2581 wk.q8_row_parts(),
2582 wv.q8_row_parts(),
2583 wo.q8_row_parts(),
2584 d.gate_proj.q8_row_parts(),
2585 d.up_proj.q8_row_parts(),
2586 d.down_proj.q8_row_parts(),
2587 );
2588 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2589 else {
2590 break;
2591 };
2592 let layer = &self.kv_cache.layers[li];
2593 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2594 break;
2595 }
2596 stored_at.push(layer.head_len(0));
2597 layers.push(crate::gpu_metal::ChunkLayer {
2598 model: &model,
2599 kv_id: self.graph_kv_id,
2600 layer: li,
2601 wq: pq,
2602 wk: pk,
2603 wv: pv,
2604 wo: po,
2605 gate: pg,
2606 up: pu,
2607 down: pd,
2608 input_norm: &lw.input_norm,
2609 post_norm: &lw.post_norm,
2610 bias: bias
2611 .as_ref()
2612 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2613 q_norm: q_norm.as_deref(),
2614 k_norm: k_norm.as_deref(),
2615 inv_freq: &inv_freq,
2616 rd: self.rotary_dim,
2617 nh,
2618 nkv,
2619 hd,
2620 hs,
2621 inter: d.gate_proj.rows(),
2622 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2623 eps: self.rms_eps as f32,
2624 });
2625 }
2626 if layers.is_empty() {
2627 return li0;
2628 }
2629 let row = nkv * hd;
2630 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2631 .iter()
2632 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2633 .collect();
2634 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2635 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2636 let li = layers[i].layer;
2637 let layer = &self.kv_cache.layers[li];
2638 io.push(crate::gpu_metal::ChunkIo {
2639 cpu_stored: stored_at[i],
2640 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2641 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2642 out_k: ok,
2643 out_v: ov,
2644 imp: oi,
2645 });
2646 }
2647 let n_run = layers.len();
2648 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2649 let ep = embed_ids.and_then(|ids| {
2652 self.weights
2653 .embed_tokens
2654 .q8_row_parts()
2655 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
2656 idx,
2657 rows,
2658 row_scale: rs,
2659 ids,
2660 mult: self.embed_multiplier,
2661 })
2662 });
2663 if embed_ids.is_some() && ep.is_none() {
2664 return li0;
2665 }
2666 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2667 return li0;
2668 }
2669 drop(io);
2670 drop(layers);
2671 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2674 let li = li0 + i;
2675 let layer = &mut self.kv_cache.layers[li];
2676 for bi in 0..b {
2677 layer.append(
2678 &ok[bi * row..(bi + 1) * row],
2679 &ov[bi * row..(bi + 1) * row],
2680 &[],
2681 );
2682 }
2683 layer.accumulate_imp(oi);
2684 }
2685 last
2686 }
2687
2688 fn layer_is_local(&self, li: usize) -> bool {
2691 if let Some(layers) = &self.sliding_layers {
2692 return layers.get(li).copied().unwrap_or(false);
2693 }
2694 match self.swa {
2695 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2696 None => false,
2697 }
2698 }
2699
2700 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2703 if self.layer_is_local(li) {
2704 if let Some(f) = &self.inv_freq_local {
2705 return f.clone();
2706 }
2707 } else if let Some(f) = &self.inv_freq_global {
2708 return f.clone();
2709 }
2710 self.inv_freq.clone()
2711 }
2712
2713 fn layer_window(&self, li: usize) -> Option<usize> {
2715 self.swa
2716 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
2717 }
2718
2719 fn layer_num_heads(&self, li: usize) -> usize {
2720 self.attention_heads_per_layer
2721 .as_ref()
2722 .and_then(|v| v.get(li).copied())
2723 .unwrap_or(self.num_heads)
2724 }
2725
2726 fn layer_rope_scale(&self, li: usize) -> f32 {
2727 if self.layer_is_local(li) {
2728 self.rope_scale_local
2729 } else {
2730 self.rope_scale
2731 }
2732 }
2733
2734 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2737 if !self.layer_is_local(li) {
2738 if let Some((ghd, gkv)) = self.global_attn {
2739 return (gkv, ghd, ghd);
2740 }
2741 }
2742 (
2743 self.num_kv_heads,
2744 self.head_dim,
2745 if self.layer_is_local(li) {
2746 self.rotary_dim_local.unwrap_or(self.rotary_dim)
2747 } else {
2748 self.rotary_dim
2749 },
2750 )
2751 }
2752
2753 fn forward_layers(
2755 &mut self,
2756 hidden: &[f32],
2757 position: usize,
2758 task_mask: Option<&TaskMask>,
2759 ) -> Vec<f32> {
2760 self.forward_layers_upto(hidden, position, task_mask, None)
2761 }
2762
2763 fn try_token_graph_wgpu(
2767 &self,
2768 hidden: &[f32],
2769 position: usize,
2770 logits_out: &mut Vec<f32>,
2771 ) -> Option<Vec<f32>> {
2772 if self.o1_active() {
2775 return None;
2776 }
2777 let nh = self.num_heads;
2778 let (nkv, hd, rd) = self.layer_geom(0);
2779 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2780 let mut layers = Vec::with_capacity(self.num_layers);
2781 let mut model = None;
2782 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
2783 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2784 if let Some((_, i, kind, rs)) = t.graph_weight() {
2785 return Some(crate::gpu::GraphW {
2786 idx: i,
2787 kind,
2788 row_scale: rs,
2789 data: &[],
2790 });
2791 }
2792 t.as_f32().map(|d| crate::gpu::GraphW {
2794 idx: 0,
2795 kind: 4,
2796 row_scale: &[],
2797 data: d,
2798 })
2799 }
2800 for li in 0..self.num_layers {
2801 let lw = &self.weights.layers[li];
2802 if dbg {
2803 let ak = match &lw.attn {
2804 AttnKind::Full {
2805 output_gate, bias, ..
2806 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
2807 AttnKind::LinearGdn(_) => "LinearGdn".into(),
2808 AttnKind::Linear(_) => "Linear".into(),
2809 AttnKind::ShortConv(_) => "ShortConv".into(),
2810 };
2811 let fk = match &lw.ffn {
2812 FfnKind::Dense(_) => "Dense",
2813 FfnKind::Moe(_) => "Moe",
2814 };
2815 eprintln!("graph L{li}: attn={ak} ffn={fk}");
2816 }
2817 let (gate, up, down) = match &lw.ffn {
2818 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
2819 _ => return None,
2820 };
2821 let attn = match &lw.attn {
2822 AttnKind::Full {
2823 wq,
2824 wk,
2825 wv,
2826 wo,
2827 q_norm,
2828 k_norm,
2829 output_gate,
2830 softplus_gate,
2831 bias,
2832 } => {
2833 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
2834 return None;
2835 }
2836 let (m, _, _, _) = wq.graph_weight()?;
2837 model = Some(m.clone());
2838 crate::gpu::GraphAttn::Full {
2839 wq: gw(wq)?,
2840 wk: gw(wk)?,
2841 wv: gw(wv)?,
2842 wo: gw(wo)?,
2843 q_norm: q_norm.as_deref(),
2844 k_norm: k_norm.as_deref(),
2845 bias: bias
2846 .as_ref()
2847 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2848 output_gate: *output_gate,
2849 cpu_k: self.kv_cache.layers[li].k_heads(),
2850 cpu_v: self.kv_cache.layers[li].v_heads(),
2851 }
2852 }
2853 AttnKind::LinearGdn(w) => {
2854 let cfg = self.gdn_cfg?;
2855 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
2856 model = Some(m.clone());
2857 crate::gpu::GraphAttn::Gdn {
2858 qkv: gw(&w.in_proj_qkv)?,
2859 z: gw(&w.in_proj_z)?,
2860 a: gw(&w.in_proj_a)?,
2861 b: gw(&w.in_proj_b)?,
2862 out: gw(&w.out_proj)?,
2863 conv1d: &w.conv1d,
2864 a_log: &w.a_log,
2865 dt_bias: &w.dt_bias,
2866 norm: &w.norm,
2867 nv: cfg.num_v_heads,
2868 nk: cfg.num_k_heads,
2869 dk: cfg.key_head_dim,
2870 dv: cfg.value_head_dim,
2871 kk: cfg.conv_kernel,
2872 }
2873 }
2874 _ => return None,
2875 };
2876 layers.push(crate::gpu::GraphLayer {
2877 input_norm: &lw.input_norm,
2878 attn,
2879 post_norm: &lw.post_norm,
2880 gate: gw(gate)?,
2881 up: gw(up)?,
2882 down: gw(down)?,
2883 });
2884 }
2885 let model = model?;
2886 let lm_gw = if self.graph_want_logits
2892 && std::env::var("CMF_GPU_LMHEAD")
2893 .map(|v| v != "0")
2894 .unwrap_or(true)
2895 {
2896 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
2897 (
2898 crate::gpu::GraphW {
2899 idx: i,
2900 kind,
2901 row_scale: rs,
2902 data: &[],
2903 },
2904 self.weights.lm_head.rows(),
2905 )
2906 })
2907 } else {
2908 None
2909 };
2910 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
2911 let mut h = hidden.to_vec();
2912 crate::gpu::forward_token_graph(
2913 &model,
2914 self.graph_kv_id,
2915 &layers,
2916 &self.inv_freq,
2917 &mut h,
2918 nh,
2919 nkv,
2920 hd,
2921 rd,
2922 self.hidden_size,
2923 self.intermediate_size,
2924 position,
2925 self.kv_cache.max_seq_len,
2926 gemma,
2927 self.rms_eps as f32,
2928 lm,
2929 &self.weights.final_norm,
2930 logits_out,
2931 )
2932 .then_some(h)
2933 }
2934
2935 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
2940 if self.o1_active() {
2941 return false;
2942 }
2943 let nh = self.num_heads;
2944 let (nkv, hd, rd) = self.layer_geom(0);
2945 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2946 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2947 if let Some((_, i, kind, rs)) = t.graph_weight() {
2948 return Some(crate::gpu::GraphW {
2949 idx: i,
2950 kind,
2951 row_scale: rs,
2952 data: &[],
2953 });
2954 }
2955 t.as_f32().map(|d| crate::gpu::GraphW {
2956 idx: 0,
2957 kind: 4,
2958 row_scale: &[],
2959 data: d,
2960 })
2961 }
2962 let built: Option<(
2963 Vec<crate::gpu::GraphLayer<'_>>,
2964 std::sync::Arc<cortiq_core::CmfModel>,
2965 )> = (|| {
2966 let mut layers = Vec::with_capacity(self.num_layers);
2967 let mut model = None;
2968 for li in 0..self.num_layers {
2969 let lw = &self.weights.layers[li];
2970 let (gate, up, down) = match &lw.ffn {
2971 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
2972 _ => return None,
2973 };
2974 let attn = match &lw.attn {
2975 AttnKind::Full {
2976 wq,
2977 wk,
2978 wv,
2979 wo,
2980 q_norm,
2981 k_norm,
2982 output_gate,
2983 softplus_gate,
2984 bias,
2985 } => {
2986 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
2987 return None;
2988 }
2989 let (m, _, _, _) = wq.graph_weight()?;
2990 model = Some(m.clone());
2991 crate::gpu::GraphAttn::Full {
2992 wq: gw(wq)?,
2993 wk: gw(wk)?,
2994 wv: gw(wv)?,
2995 wo: gw(wo)?,
2996 q_norm: q_norm.as_deref(),
2997 k_norm: k_norm.as_deref(),
2998 bias: bias
2999 .as_ref()
3000 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3001 output_gate: *output_gate,
3002 cpu_k: self.kv_cache.layers[li].k_heads(),
3003 cpu_v: self.kv_cache.layers[li].v_heads(),
3004 }
3005 }
3006 AttnKind::LinearGdn(w) => {
3007 let cfg = self.gdn_cfg?;
3008 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
3009 model = Some(m.clone());
3010 crate::gpu::GraphAttn::Gdn {
3011 qkv: gw(&w.in_proj_qkv)?,
3012 z: gw(&w.in_proj_z)?,
3013 a: gw(&w.in_proj_a)?,
3014 b: gw(&w.in_proj_b)?,
3015 out: gw(&w.out_proj)?,
3016 conv1d: &w.conv1d,
3017 a_log: &w.a_log,
3018 dt_bias: &w.dt_bias,
3019 norm: &w.norm,
3020 nv: cfg.num_v_heads,
3021 nk: cfg.num_k_heads,
3022 dk: cfg.key_head_dim,
3023 dv: cfg.value_head_dim,
3024 kk: cfg.conv_kernel,
3025 }
3026 }
3027 _ => return None,
3028 };
3029 layers.push(crate::gpu::GraphLayer {
3030 input_norm: &lw.input_norm,
3031 attn,
3032 post_norm: &lw.post_norm,
3033 gate: gw(gate)?,
3034 up: gw(up)?,
3035 down: gw(down)?,
3036 });
3037 }
3038 Some((layers, model?))
3039 })();
3040 let Some((layers, model)) = built else {
3041 return false;
3042 };
3043 crate::gpu::forward_batch_graph(
3044 &model,
3045 self.graph_kv_id,
3046 &layers,
3047 &self.inv_freq,
3048 hiddens,
3049 nh,
3050 nkv,
3051 hd,
3052 rd,
3053 self.hidden_size,
3054 self.intermediate_size,
3055 positions,
3056 self.kv_cache.max_seq_len,
3057 gemma,
3058 self.rms_eps as f32,
3059 k,
3060 )
3061 }
3062
3063 fn forward_layers_upto(
3065 &mut self,
3066 hidden: &[f32],
3067 position: usize,
3068 task_mask: Option<&TaskMask>,
3069 upto: Option<usize>,
3070 ) -> Vec<f32> {
3071 let mut h = hidden.to_vec();
3072 let (nh, _nkv, _hd, hs, _rd, eps) = (
3075 self.num_heads,
3076 self.num_kv_heads,
3077 self.head_dim,
3078 self.hidden_size,
3079 self.rotary_dim,
3080 self.rms_eps,
3081 );
3082 let pool = self.pool.clone();
3083 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
3086 .map(|v| v != "0")
3087 .unwrap_or_else(|_| {
3088 crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed)
3089 });
3090 if graph_on && upto.is_none() && task_mask.is_none() {
3094 let mut lg = Vec::new();
3095 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
3096 if !lg.is_empty() {
3097 lg.resize(self.vocab_size, 0.0);
3100 if let Some(c) = self.final_softcap {
3101 for l in lg.iter_mut() {
3102 *l = c * (*l / c).tanh();
3103 }
3104 }
3105 self.graph_logits = Some(lg);
3106 }
3107 return hh;
3108 }
3109 }
3110
3111 #[cfg(target_os = "macos")]
3112 let mut gpu_skip_until = 0usize;
3113 for li in 0..self.num_layers {
3114 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
3116 if li > u {
3117 break;
3118 }
3119 }
3120 if let Some(mask) = task_mask {
3121 if !mask.layer_alive(li) {
3122 continue; }
3124 }
3125 #[cfg(target_os = "macos")]
3129 {
3130 if li < gpu_skip_until {
3131 continue;
3132 }
3133 if task_mask.is_none() {
3134 let end = self.q1_graph_gpu(li, upto, position, &mut h);
3135 if end > li {
3136 gpu_skip_until = end;
3137 continue;
3138 }
3139 }
3140 }
3141
3142 let lw = &self.weights.layers[li];
3143 inference::rms_norm_into(
3146 &h,
3147 &lw.input_norm,
3148 self.rms_eps,
3149 self.norm_style,
3150 &mut self.ws.n1,
3151 );
3152
3153 let attn_out = match &lw.attn {
3154 AttnKind::Linear(w) => {
3155 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
3156 vmf_phase_forward(
3157 &self.ws.n1,
3158 w,
3159 &cfg,
3160 &mut self.kv_cache.layers[li].linear_state,
3161 self.pool.as_deref(),
3162 )
3163 }
3164 AttnKind::LinearGdn(w) => {
3165 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3166 gdn_forward(
3167 &self.ws.n1,
3168 w,
3169 &cfg,
3170 &mut self.kv_cache.layers[li].linear_state,
3171 self.pool.as_deref(),
3172 )
3173 }
3174 AttnKind::ShortConv(w) => {
3175 let cfg = self
3176 .short_conv_cfg
3177 .expect("short-conv layer without short_conv_cfg");
3178 short_conv_forward(
3179 &self.ws.n1,
3180 w,
3181 &cfg,
3182 &mut self.kv_cache.layers[li].linear_state,
3183 self.pool.as_deref(),
3184 )
3185 }
3186 AttnKind::Full {
3187 wq,
3188 wk,
3189 wv,
3190 wo,
3191 q_norm,
3192 k_norm,
3193 output_gate,
3194 softplus_gate,
3195 bias,
3196 } if self.kv_cache.layers[li].o1_sealed() => {
3197 let inv_freq_l = self.layer_inv_freq(li);
3200 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3201 let cfg = QwenAttnCfg {
3202 num_heads: self.layer_num_heads(li),
3203 num_kv_heads: nkv_l,
3204 head_dim: hd_l,
3205 hidden_size: hs,
3206 position,
3207 inv_freq: &inv_freq_l,
3208 rotary_dim: rd_l,
3209 scale: self.attn_scale,
3210 window: None,
3211 v_norm: self.attn_v_norm,
3212 q_norm: q_norm.as_deref(),
3213 k_norm: k_norm.as_deref(),
3214 output_gate: *output_gate,
3215 softplus_gate: softplus_gate
3216 .as_ref()
3217 .map(|(gate, per_head)| (gate, *per_head)),
3218 rope_scale: self.layer_rope_scale(li),
3219 bias: bias
3220 .as_ref()
3221 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3222 rms_eps: eps,
3223 norm_style: self.norm_style,
3224 pool: pool.as_deref(),
3225 };
3226 attention::qwen_attention_nystrom(
3227 &self.ws.n1,
3228 wq,
3229 wk,
3230 wv,
3231 wo,
3232 &mut self.kv_cache.layers[li],
3233 &cfg,
3234 )
3235 }
3236 AttnKind::Full {
3237 wq,
3238 wk,
3239 wv,
3240 wo,
3241 q_norm,
3242 k_norm,
3243 output_gate,
3244 softplus_gate,
3245 bias,
3246 } => 'attn: {
3247 if graph_on
3250 && !*output_gate
3251 && softplus_gate.is_none()
3252 && self.attention_heads_per_layer.is_none()
3253 && bias.is_none()
3254 && task_mask.is_none()
3255 {
3256 let inv_freq_l = self.layer_inv_freq(li);
3257 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3258 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
3259 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
3260 wq.mapped_q1(),
3261 wk.mapped_q1(),
3262 wv.mapped_q1(),
3263 wo.mapped_q1(),
3264 ) {
3265 let gm = gm.clone();
3266 let mut out = vec![0f32; hs];
3267 let cache = &self.kv_cache.layers[li];
3268 if crate::gpu::attn_dropin(
3269 &gm,
3270 self.graph_kv_id,
3271 li,
3272 &self.ws.n1,
3273 qi,
3274 ki,
3275 vi,
3276 oi,
3277 q_norm.as_deref(),
3278 k_norm.as_deref(),
3279 &inv_freq_l,
3280 nh,
3281 nkv_l,
3282 hd_l,
3283 rd_l,
3284 hs,
3285 position,
3286 self.kv_cache.max_seq_len,
3287 gemma,
3288 eps as f32,
3289 cache.k_heads(),
3290 cache.v_heads(),
3291 &mut out,
3292 ) {
3293 break 'attn out;
3294 }
3295 }
3296 }
3297 let masked = task_mask
3298 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
3299 .unwrap_or(false);
3300 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
3301 match (masked, f32_view) {
3302 (true, (Some(q), Some(k), Some(v), Some(o))) => {
3305 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
3306 attention::multi_head_attention(
3307 &self.ws.n1,
3308 q,
3309 k,
3310 v,
3311 o,
3312 &mut self.kv_cache.layers[li],
3313 self.num_heads,
3314 self.num_kv_heads,
3315 self.head_dim,
3316 self.hidden_size,
3317 position,
3318 &active_heads,
3319 &self.inv_freq,
3320 )
3321 }
3322 (masked, _) => {
3323 if masked {
3324 tracing::warn!(
3325 "layer {li}: head mask on quantized weights not \
3326 supported yet — executing dense"
3327 );
3328 }
3329 let inv_freq_l = self.layer_inv_freq(li);
3330 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3331 let cfg = QwenAttnCfg {
3332 num_heads: self.layer_num_heads(li),
3333 num_kv_heads: nkv_l,
3334 head_dim: hd_l,
3335 hidden_size: hs,
3336 position,
3337 inv_freq: &inv_freq_l,
3338 rotary_dim: rd_l,
3339 scale: self.attn_scale,
3340 window: self.layer_window(li),
3341 v_norm: self.attn_v_norm,
3342 q_norm: q_norm.as_deref(),
3343 k_norm: k_norm.as_deref(),
3344 output_gate: *output_gate,
3345 softplus_gate: softplus_gate
3346 .as_ref()
3347 .map(|(gate, per_head)| (gate, *per_head)),
3348 rope_scale: self.layer_rope_scale(li),
3349 bias: bias
3350 .as_ref()
3351 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3352 rms_eps: eps,
3353 norm_style: self.norm_style,
3354 pool: pool.as_deref(),
3355 };
3356 attention::qwen_attention(
3357 &self.ws.n1,
3358 wq,
3359 wk,
3360 wv,
3361 wo,
3362 &mut self.kv_cache.layers[li],
3363 &cfg,
3364 )
3365 }
3366 }
3367 }
3368 };
3369 let attn_out = match &self.weights.layers[li].attn_out_norm {
3372 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
3373 None => attn_out,
3374 };
3375 let lw = &self.weights.layers[li];
3376 inference::add_rmsnorm_fused_into(
3377 &mut h,
3378 &attn_out,
3379 &lw.post_norm,
3380 self.rms_eps,
3381 self.norm_style,
3382 &mut self.ws.p1,
3383 );
3384 let mut attn_out = attn_out;
3385 attention::recycle_buf(&mut attn_out);
3386 let post_normed = &self.ws.p1;
3387
3388 let ffn_masked = task_mask
3389 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
3390 .unwrap_or(false);
3391 let f32_ffn = match &lw.ffn {
3394 FfnKind::Dense(d) => (
3395 d.gate_proj.as_f32(),
3396 d.up_proj.as_f32(),
3397 d.down_proj.as_f32(),
3398 ),
3399 FfnKind::Moe(_) => (None, None, None),
3400 };
3401 let ffn_out = match (ffn_masked, f32_ffn) {
3402 (true, (Some(g), Some(u), Some(d))) => {
3403 let active = task_mask.unwrap().ffn_active_indices(li);
3404 inference::sparse_ffn_forward(
3405 &post_normed,
3406 g,
3407 u,
3408 d,
3409 self.hidden_size,
3410 self.intermediate_size,
3411 &active,
3412 self.pool.as_deref(),
3413 )
3414 }
3415 (true, _) => match &lw.ffn {
3419 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
3420 let active = task_mask.unwrap().ffn_active_indices(li);
3421 sparse_ffn_quant(
3422 d,
3423 &post_normed,
3424 &active,
3425 self.hidden_size,
3426 self.pool.as_deref(),
3427 )
3428 }
3429 FfnKind::Dense(d) => {
3434 let active = task_mask.unwrap().ffn_active_indices(li);
3435 let (gf, uf, df) = dequant_dense_f32(d);
3436 inference::sparse_ffn_forward(
3437 &post_normed,
3438 &gf,
3439 &uf,
3440 &df,
3441 self.hidden_size,
3442 self.intermediate_size,
3443 &active,
3444 self.pool.as_deref(),
3445 )
3446 }
3447 FfnKind::Moe(_) => {
3448 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
3451 }
3452 },
3453 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
3454 };
3455 let ffn_out = match &self.weights.layers[li].ffn_out_norm {
3456 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
3457 None => ffn_out,
3458 };
3459 for (i, &f) in ffn_out.iter().enumerate() {
3460 h[i] += f;
3461 }
3462 let mut ffn_out = ffn_out;
3463 attention::recycle_buf(&mut ffn_out);
3464
3465 if let Some(sc) = self.weights.layers[li].layer_scale {
3467 for v in h.iter_mut() {
3468 *v *= sc;
3469 }
3470 }
3471
3472 if self.dyn_phi_layer == Some(li) {
3476 self.update_dyn_phi(&h);
3477 }
3478 }
3479 crate::gpu::set_layer(-1); h
3482 }
3483
3484 fn update_dyn_phi(&mut self, h: &[f32]) {
3487 const A: f32 = 0.2;
3488 if self.dyn_phi_ema.len() != h.len() {
3489 self.dyn_phi_ema = vec![0.0; h.len()];
3490 self.dyn_phi_seen = 0;
3491 }
3492 if self.dyn_phi_seen == 0 {
3493 self.dyn_phi_ema.copy_from_slice(h);
3494 } else {
3495 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
3496 *e = (1.0 - A) * *e + A * v;
3497 }
3498 }
3499 self.dyn_phi_seen += 1;
3500 }
3501
3502 pub fn dyn_phi(&self) -> &[f32] {
3504 &self.dyn_phi_ema
3505 }
3506
3507 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
3509 self.dyn_phi_layer = layer;
3510 self.dyn_phi_ema.clear();
3511 self.dyn_phi_seen = 0;
3512 }
3513
3514 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
3516 let Some(model) = &self.model else {
3517 return Vec::new();
3518 };
3519 model
3520 .header
3521 .skills
3522 .iter()
3523 .enumerate()
3524 .filter_map(|(i, sk)| {
3525 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
3526 let sel = sk.selection.as_ref()?;
3527 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
3528 })
3529 .collect()
3530 }
3531
3532 pub fn active_skill(&self) -> Option<usize> {
3534 self.dyn_active
3535 }
3536
3537 pub fn enable_dynamic_routing(&mut self) -> usize {
3542 use crate::swarm::{DynRouter, RoutableSkill};
3543 let Some(model) = self.model.clone() else {
3544 return 0;
3545 };
3546 if self.dyn_blend_loaded {
3549 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
3550 return 0;
3551 }
3552 if let Some(a) = self.dyn_active {
3556 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
3557 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
3558 return 0;
3559 }
3560 }
3561 let hidden = self.hidden_size;
3562 let mut skills = Vec::new();
3563 for (idx, id, _phi) in self.dynamic_skills() {
3564 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
3565 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
3566 skills.push(rs);
3567 }
3568 }
3569 }
3570 if skills.is_empty() {
3571 return 0;
3572 }
3573 let phi = skills[0].phi_layer;
3575 if skills.iter().any(|s| s.phi_layer != phi) {
3576 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
3577 }
3578 let n = skills.len();
3579 self.set_dyn_phi_layer(Some(phi));
3580 self.dyn_router = Some(DynRouter::new(skills));
3581 n
3582 }
3583
3584 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
3586 self.dyn_router
3587 .as_ref()
3588 .map(|r| r.switches.clone())
3589 .unwrap_or_default()
3590 }
3591
3592 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
3595 let rows = self.weights.lm_head.rows();
3596 let mut logits = attention::take_buf(rows.min(self.vocab_size));
3597 self.weights
3598 .lm_head
3599 .matvec(hidden, &mut logits, self.pool.as_deref());
3600 logits.resize(self.vocab_size, 0.0);
3601 if let Some(c) = self.final_softcap {
3602 for l in logits.iter_mut() {
3603 *l = c * (*l / c).tanh();
3604 }
3605 }
3606 logits
3607 }
3608
3609 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
3614 self.kv_cache.clear();
3615 let mut hidden = vec![0.0f32; self.hidden_size];
3616 for (pos, &id) in ids.iter().enumerate() {
3617 let emb = self.embed_single(id);
3618 hidden = self.forward_layers(&emb, pos, task_mask);
3619 }
3620 inference::rms_norm_into(
3621 &hidden,
3622 &self.weights.final_norm,
3623 self.rms_eps,
3624 self.norm_style,
3625 &mut self.ws.n1,
3626 );
3627 self.lm_head_forward(&self.ws.n1)
3628 }
3629}
3630
3631pub fn create_test_pipeline(
3633 hidden_size: usize,
3634 intermediate_size: usize,
3635 num_heads: usize,
3636 num_kv_heads: usize,
3637 head_dim: usize,
3638 num_layers: usize,
3639 vocab_size: usize,
3640) -> Pipeline {
3641 let synth = |n: usize, salt: usize| -> Vec<f32> {
3644 (0..n)
3645 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
3646 .collect()
3647 };
3648 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3649 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3650 };
3651 let layer_weights: Vec<LayerWeights> = (0..num_layers)
3652 .map(|li| LayerWeights {
3653 input_norm: vec![1.0; hidden_size],
3654 post_norm: vec![1.0; hidden_size],
3655 attn_out_norm: None,
3656 ffn_out_norm: None,
3657 layer_scale: None,
3658 ffn: FfnKind::Dense(DenseFfn {
3659 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
3660 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
3661 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
3662 act: Act::Silu,
3663 }),
3664 attn: AttnKind::Full {
3665 bias: None,
3666 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
3667 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
3668 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
3669 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
3670 q_norm: None,
3671 k_norm: None,
3672 output_gate: false,
3673 softplus_gate: None,
3674 },
3675 })
3676 .collect();
3677
3678 Pipeline::new(
3679 Tokenizer::byte_level(),
3680 PipelineWeights {
3681 embed_tokens: qt(vocab_size, hidden_size, 100),
3682 layers: layer_weights,
3683 lm_head: qt(vocab_size, hidden_size, 200),
3684 final_norm: vec![1.0; hidden_size],
3685 },
3686 hidden_size,
3687 intermediate_size,
3688 num_heads,
3689 num_kv_heads,
3690 head_dim,
3691 num_layers,
3692 vocab_size,
3693 1e-6,
3694 10_000.0,
3695 NormStyle::Qwen,
3696 4096,
3697 SamplerConfig {
3698 seed: Some(42),
3699 ..Default::default()
3700 },
3701 )
3702}
3703
3704fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
3707 let inter = d.gate_proj.rows();
3708 let hidden = d.down_proj.rows();
3709 let mut g = vec![0.0f32; b * inter];
3710 d.gate_proj.matmat(xs, b, &mut g, pool);
3711 let mut u = vec![0.0f32; b * inter];
3712 d.up_proj.matmat(xs, b, &mut u, pool);
3713 for i in 0..b * inter {
3714 g[i] = d.act.apply(g[i]) * u[i];
3715 }
3716 let mut out = vec![0.0f32; b * hidden];
3717 d.down_proj.matmat(&g, b, &mut out, pool);
3718 out
3719}
3720
3721fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
3725 let ne = m.experts.len();
3726 let mut logits = vec![0.0f32; b * ne];
3727 m.router.matmat(xs, b, &mut logits, pool);
3728
3729 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
3732 {
3733 let mut st = m.stats.borrow_mut();
3734 if st.len() < ne {
3735 st.resize(ne, 0);
3736 }
3737 for bi in 0..b {
3738 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m);
3739 for &e in &idx {
3740 st[e] += 1;
3741 assign[e].push((bi, p[e] / wsum));
3742 }
3743 }
3744 }
3745
3746 let mut out = vec![0.0f32; b * hidden];
3747 let cols = m.experts[0].gate_proj.cols();
3748 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
3749 let sb = list.len();
3750 let mut sub = vec![0.0f32; sb * cols];
3751 for (k, &(bi, _)) in list.iter().enumerate() {
3752 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
3753 }
3754 let eo = dense_ffn_batch(d, &sub, sb, pool);
3755 for (k, &(bi, w)) in list.iter().enumerate() {
3756 for i in 0..hidden {
3757 out[bi * hidden + i] += w * eo[k * hidden + i];
3758 }
3759 }
3760 };
3761 for e in 0..ne {
3762 if !assign[e].is_empty() {
3763 run_expert(&m.experts[e], &assign[e]);
3764 }
3765 }
3766 if let Some((se, gate)) = &m.shared {
3767 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
3768 let mut gl = vec![0.0f32; b];
3769 gate.matmat(xs, b, &mut gl, pool);
3770 (0..b)
3771 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
3772 .collect()
3773 } else {
3774 (0..b).map(|bi| (bi, 1.0)).collect()
3775 };
3776 run_expert(se, &all);
3777 }
3778 out
3779}
3780
3781thread_local! {
3782 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
3786 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
3787}
3788
3789fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3791 if crate::gpu::enabled_here()
3802 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
3803 {
3804 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
3805 crate::gpu::ProbeArm::Gpu
3806 } else {
3807 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
3808 };
3809 match arm {
3810 crate::gpu::ProbeArm::Gpu => {
3811 let t0 = std::time::Instant::now();
3812 if let Some(out) = dense_ffn_gpu(d, x, pool) {
3813 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3814 return out;
3815 }
3816 }
3817 crate::gpu::ProbeArm::CpuTimed => {
3818 let t0 = std::time::Instant::now();
3819 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3820 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3821 return out;
3822 }
3823 crate::gpu::ProbeArm::Cpu => {
3824 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3825 }
3826 }
3827 }
3828 dense_ffn_cpu(d, x, pool)
3829}
3830
3831fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3833 let inter = d.gate_proj.rows();
3834 FFN_SCRATCH.with(|s| {
3835 let mut s = s.borrow_mut();
3836 let [g, u, ..] = &mut *s;
3837 g.resize(inter, 0.0);
3838 u.resize(inter, 0.0);
3839 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
3841 for i in 0..inter {
3842 g[i] = d.act.apply(g[i]) * u[i];
3843 }
3844 FFN_PROBE.with(|pr| {
3847 if let Some(acc) = pr.borrow_mut().as_mut() {
3848 let li = crate::gpu::cur_layer();
3849 if li >= 0 {
3850 if let Some(row) = acc.get_mut(li as usize) {
3851 for (a, &v) in row.iter_mut().zip(g.iter()) {
3852 *a += (v as f64).abs();
3853 }
3854 }
3855 }
3856 }
3857 });
3858 let mut out = attention::take_buf(d.down_proj.rows());
3859 d.down_proj.matvec(g, &mut out, pool);
3860 out
3861 })
3862}
3863
3864thread_local! {
3865 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
3868 const { std::cell::RefCell::new(None) };
3869}
3870
3871fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
3877 if d.act != Act::Silu {
3879 return None;
3880 }
3881 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
3884 return None;
3885 }
3886 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
3887 let mut model_ref = None;
3888 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
3889 let model = model_ref?;
3890 let hidden = jobs[0].down.1;
3891 let mut out = attention::take_buf(hidden);
3892 if crate::gpu::moe_block(&model, &jobs, &mut out) {
3893 Some(out)
3894 } else {
3895 let mut out = out;
3896 attention::recycle_buf(&mut out);
3897 None
3898 }
3899}
3900
3901#[allow(clippy::type_complexity)]
3906#[allow(clippy::type_complexity)]
3907fn moe_parts(
3908 t: &QTensor,
3909) -> Option<(
3910 &std::sync::Arc<cortiq_core::CmfModel>,
3911 usize,
3912 usize,
3913 usize,
3914 &[f32],
3915 &[f32],
3916 bool,
3917)> {
3918 match t {
3919 QTensor::Mapped {
3920 model,
3921 idx,
3922 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
3923 rows,
3924 cols,
3925 row_scale,
3926 col_field,
3927 ..
3928 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
3929 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
3930 }
3931 QTensor::Mapped {
3933 model,
3934 idx,
3935 dtype: cortiq_core::TensorDtype::Q1,
3936 rows,
3937 cols,
3938 ..
3939 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
3940 _ => None,
3941 }
3942}
3943
3944fn moe_push_job<'a>(
3946 d: &'a DenseFfn,
3947 x: &[f32],
3948 w: f32,
3949 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
3950 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
3951) -> Option<()> {
3952 use crate::qtensor::prescale;
3953 if d.act != Act::Silu {
3954 return None; }
3956 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
3957 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
3958 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
3959 if gq1 != uq1 || uq1 != dq1 {
3960 return None; }
3962 model_ref.get_or_insert_with(|| gm.clone());
3963 let gdt = if gcf.is_empty() {
3964 cortiq_core::TensorDtype::Q8Row
3965 } else {
3966 cortiq_core::TensorDtype::Q8_2f
3967 };
3968 let udt = if ucf.is_empty() {
3969 cortiq_core::TensorDtype::Q8Row
3970 } else {
3971 cortiq_core::TensorDtype::Q8_2f
3972 };
3973 jobs.push(crate::gpu::MoeJob {
3974 gate: (gi, gr, gc, grs),
3975 up: (ui, ur, uc, urs),
3976 down: (di, dr, dc, drs),
3977 xs_gate: prescale(x, gcf, gdt).into_owned(),
3978 xs_up: prescale(x, ucf, udt).into_owned(),
3979 down_col: dcf,
3980 w,
3981 q1: gq1,
3982 });
3983 Some(())
3984}
3985
3986fn sparse_ffn_quant(
3993 d: &DenseFfn,
3994 x: &[f32],
3995 active: &[u16],
3996 hidden: usize,
3997 pool: Option<&Pool>,
3998) -> Vec<f32> {
3999 let n = active.len();
4000 let inter = d.gate_proj.rows();
4001 let mut act = vec![0.0f32; n];
4002 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
4005 let compute = |ai: usize| -> f32 {
4006 let idx = active[ai] as usize;
4007 if idx >= inter {
4008 return 0.0; }
4010 let mut s = if need_scratch {
4011 vec![0.0f32; hidden]
4012 } else {
4013 Vec::new()
4014 };
4015 let gate = d.gate_proj.row_dot(idx, x, &mut s);
4016 let up = d.up_proj.row_dot(idx, x, &mut s);
4017 d.act.apply(gate) * up
4018 };
4019 match pool {
4020 Some(p) if n >= 256 => {
4021 let ptr = SendMut(act.as_mut_ptr());
4022 p.run(&|widx, nw| {
4023 let chunk = n.div_ceil(nw);
4024 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
4025 for ai in s..e {
4026 unsafe { *ptr.at(ai) = compute(ai) };
4027 }
4028 });
4029 }
4030 _ => {
4031 for (ai, a) in act.iter_mut().enumerate() {
4032 *a = compute(ai);
4033 }
4034 }
4035 }
4036 let mut out = vec![0.0f32; hidden];
4038 for (ai, &idx) in active.iter().enumerate() {
4039 let w = act[ai];
4040 if w.abs() >= 1e-12 && (idx as usize) < inter {
4041 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
4042 }
4043 }
4044 out
4045}
4046
4047#[doc(hidden)]
4049pub fn sparse_ffn_quant_for_test(
4050 d: &DenseFfn,
4051 x: &[f32],
4052 active: &[u16],
4053 hidden: usize,
4054) -> Vec<f32> {
4055 sparse_ffn_quant(d, x, active, hidden, None)
4056}
4057
4058fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
4062 let deq = |t: &QTensor| -> Vec<f32> {
4063 let (rows, cols) = (t.rows(), t.cols());
4064 let mut out = vec![0.0f32; rows * cols];
4065 for r in 0..rows {
4066 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
4067 }
4068 out
4069 };
4070 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
4071}
4072
4073struct SendMut(*mut f32);
4075unsafe impl Send for SendMut {}
4076unsafe impl Sync for SendMut {}
4077impl SendMut {
4078 #[inline]
4079 #[allow(clippy::mut_from_ref)]
4082 unsafe fn at(&self, i: usize) -> &mut f32 {
4083 unsafe { &mut *self.0.add(i) }
4084 }
4085}
4086
4087fn moe_route(logits: &[f32], m: &MoeFfn) -> (Vec<usize>, Vec<f32>, f32) {
4097 let ne = logits.len();
4098 let p: Vec<f32> = if m.router_sigmoid {
4099 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
4100 } else {
4101 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4102 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
4103 let s: f32 = e.iter().sum();
4104 for v in &mut e {
4105 *v /= s;
4106 }
4107 e
4108 };
4109 let mut idx: Vec<usize> = (0..ne).collect();
4110 match &m.expert_bias {
4112 Some(b) => idx.sort_unstable_by(|&x, &y| {
4113 (p[y] + b[y])
4114 .partial_cmp(&(p[x] + b[x]))
4115 .unwrap()
4116 .then(x.cmp(&y))
4117 }),
4118 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
4119 }
4120 idx.truncate(m.top_k);
4121 let wsum: f32 = if m.norm_topk_prob {
4122 let s: f32 = idx.iter().map(|&e| p[e]).sum();
4123 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
4126 } else {
4127 1.0 / m.routed_scaling
4128 };
4129 (idx, p, wsum)
4130}
4131
4132fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4135 let ne = m.experts.len();
4136 let mut logits = vec![0.0f32; ne];
4137 m.router.matvec(x, &mut logits, pool);
4138 let (idx, p, wsum) = moe_route(&logits, m);
4139 {
4140 let mut st = m.stats.borrow_mut();
4141 if st.len() < ne {
4142 st.resize(ne, 0);
4143 }
4144 for &e in &idx {
4145 st[e] += 1;
4146 }
4147 }
4148 if crate::gpu::enabled_here() {
4153 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
4154 crate::gpu::ProbeArm::Gpu => {
4155 let t0 = std::time::Instant::now();
4156 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
4157 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
4158 return out;
4159 }
4160 }
4161 crate::gpu::ProbeArm::CpuTimed => {
4162 let t0 = std::time::Instant::now();
4163 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4164 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
4165 return out;
4166 }
4167 crate::gpu::ProbeArm::Cpu => {
4168 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
4169 }
4170 }
4171 }
4172 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
4173}
4174
4175fn moe_ffn_cpu(
4177 m: &MoeFfn,
4178 x: &[f32],
4179 idx: &[usize],
4180 p: &[f32],
4181 wsum: f32,
4182 pool: Option<&Pool>,
4183) -> Vec<f32> {
4184 let mut out = attention::take_buf(x.len());
4185 for &e in idx {
4186 let mut eo = dense_ffn(&m.experts[e], x, pool);
4187 let w = p[e] / wsum;
4188 for i in 0..out.len() {
4189 out[i] += w * eo[i];
4190 }
4191 attention::recycle_buf(&mut eo);
4192 }
4193 if let Some((se, gate)) = &m.shared {
4194 let mut so = dense_ffn(se, x, pool);
4195 let g = gate.as_ref().map_or(1.0, |gate| {
4196 let mut gl = [0.0f32; 1];
4197 gate.matvec(x, &mut gl, pool);
4198 1.0 / (1.0 + (-gl[0]).exp())
4199 });
4200 for i in 0..out.len() {
4201 out[i] += g * so[i];
4202 }
4203 attention::recycle_buf(&mut so);
4204 }
4205 out
4206}
4207
4208fn moe_ffn_gpu(
4211 m: &MoeFfn,
4212 x: &[f32],
4213 idx: &[usize],
4214 p: &[f32],
4215 wsum: f32,
4216 pool: Option<&Pool>,
4217) -> Option<Vec<f32>> {
4218 use crate::gpu::MoeJob;
4219
4220 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
4221 let mut model_ref = None;
4222 for &e in idx {
4223 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
4224 }
4225 if let Some((se, gate)) = &m.shared {
4226 let g = gate.as_ref().map_or(1.0, |gate| {
4227 let mut gl = [0.0f32; 1];
4228 gate.matvec(x, &mut gl, pool);
4229 1.0 / (1.0 + (-gl[0]).exp())
4230 });
4231 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
4232 }
4233 let model = model_ref?;
4234 let hidden = jobs[0].down.1;
4235 let mut out = vec![0.0f32; hidden];
4236 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
4237}
4238
4239fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
4241 match ffn {
4242 FfnKind::Dense(d) => dense_ffn(d, x, pool),
4243 FfnKind::Moe(m) => moe_ffn(m, x, pool),
4244 }
4245}
4246
4247fn ffn_forward_pair(
4251 ffn: &FfnKind,
4252 x1: &[f32],
4253 x2: &[f32],
4254 pool: Option<&Pool>,
4255) -> (Vec<f32>, Vec<f32>) {
4256 let d = match ffn {
4257 FfnKind::Dense(d) => d,
4258 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
4259 };
4260 let inter = d.gate_proj.rows();
4261 FFN_SCRATCH.with(|s| {
4262 let mut s = s.borrow_mut();
4263 let [g1, g2, u1, u2] = &mut *s;
4264 g1.resize(inter, 0.0);
4265 g2.resize(inter, 0.0);
4266 u1.resize(inter, 0.0);
4267 u2.resize(inter, 0.0);
4268 QTensor::matvec2_many(
4271 [&d.gate_proj, &d.up_proj],
4272 x1,
4273 x2,
4274 [g1.as_mut_slice(), u1.as_mut_slice()],
4275 [g2.as_mut_slice(), u2.as_mut_slice()],
4276 pool,
4277 );
4278 for i in 0..inter {
4279 g1[i] = d.act.apply(g1[i]) * u1[i];
4280 g2[i] = d.act.apply(g2[i]) * u2[i];
4281 }
4282 let mut o1 = attention::take_buf(d.down_proj.rows());
4283 let mut o2 = attention::take_buf(d.down_proj.rows());
4284 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
4285 (o1, o2)
4286 })
4287}
4288
4289#[cfg(test)]
4290mod tests {
4291 use super::*;
4292
4293 #[test]
4299 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
4300 let (hidden, inter) = (16usize, 40usize);
4301 let synth = |n: usize, salt: usize| -> Vec<f32> {
4302 (0..n)
4303 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
4304 .collect()
4305 };
4306 let d = DenseFfn {
4307 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
4308 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
4309 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
4310 act: Act::Silu,
4311 };
4312 let x = synth(hidden, 9);
4313 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
4315
4316 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
4317
4318 let mut g = vec![0.0f32; inter];
4320 d.gate_proj.matvec(&x, &mut g, None);
4321 let mut u = vec![0.0f32; inter];
4322 d.up_proj.matvec(&x, &mut u, None);
4323 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
4324 for i in 0..inter {
4325 g[i] = if act_set.contains(&(i as u16)) {
4326 inference::silu(g[i]) * u[i]
4327 } else {
4328 0.0
4329 };
4330 }
4331 let mut reference = vec![0.0f32; hidden];
4332 d.down_proj.matvec(&g, &mut reference, None);
4333
4334 let max_d = sparse
4335 .iter()
4336 .zip(&reference)
4337 .map(|(a, b)| (a - b).abs())
4338 .fold(0.0f32, f32::max);
4339 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
4340 }
4341
4342 fn attach_test_mtp(p: &mut Pipeline) {
4344 let (h, inter, heads, kv, hd) = (
4345 p.hidden_size,
4346 p.intermediate_size,
4347 p.num_heads,
4348 p.num_kv_heads,
4349 p.head_dim,
4350 );
4351 let synth = |n: usize, salt: usize| -> Vec<f32> {
4352 (0..n)
4353 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
4354 .collect()
4355 };
4356 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
4357 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
4358 };
4359 p.mtp = Some(MtpModule {
4360 enorm: vec![1.0; h],
4361 hnorm: vec![1.0; h],
4362 eh_proj: qt(h, 2 * h, 301),
4363 layer: LayerWeights {
4364 input_norm: vec![1.0; h],
4365 post_norm: vec![1.0; h],
4366 attn_out_norm: None,
4367 ffn_out_norm: None,
4368 layer_scale: None,
4369 ffn: FfnKind::Dense(DenseFfn {
4370 gate_proj: qt(inter, h, 315),
4371 up_proj: qt(inter, h, 316),
4372 down_proj: qt(h, inter, 317),
4373 act: Act::Silu,
4374 }),
4375 attn: AttnKind::Full {
4376 bias: None,
4377 wq: qt(heads * hd, h, 311),
4378 wk: qt(kv * hd, h, 312),
4379 wv: qt(kv * hd, h, 313),
4380 wo: qt(h, heads * hd, 314),
4381 q_norm: None,
4382 k_norm: None,
4383 output_gate: false,
4384 softplus_gate: None,
4385 },
4386 },
4387 final_norm: vec![1.0; h],
4388 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
4389 });
4390 }
4391
4392 #[test]
4393 fn speculative_equals_vanilla_greedy() {
4394 let run = |spec: bool| {
4395 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4396 p.sampler_config.temperature = 0.0;
4397 attach_test_mtp(&mut p);
4398 p.speculative = spec;
4399 let r = p.generate("abcdef", 12, None, None).unwrap();
4400 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
4401 };
4402 let (vanilla, d0, _) = run(false);
4403 let (spec, d1, a1) = run(true);
4404 assert_eq!(d0, 0, "vanilla path must not draft");
4405 assert!(d1 > 0, "speculative path must draft");
4406 assert_eq!(
4407 vanilla, spec,
4408 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
4409 );
4410 }
4411
4412 #[test]
4413 fn speculative_accepts_constant_oracle() {
4414 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4415 p.sampler_config.temperature = 0.0;
4416 p.sampler_config.repetition_penalty = 1.0;
4417 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
4420 attach_test_mtp(&mut p);
4421 p.speculative = true;
4422 let r = p.generate("abcd", 10, None, None).unwrap();
4423 assert!(r.mtp_drafted > 0);
4424 assert_eq!(
4425 r.mtp_accepted, r.mtp_drafted,
4426 "constant logits → every draft accepted"
4427 );
4428 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
4431 }
4432
4433 #[test]
4434 fn empty_prompt_is_an_error_not_a_panic() {
4435 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4436 let r = p.generate("", 4, None, None);
4437 assert!(r.is_err(), "empty prompt must be a clean error");
4438 }
4439
4440 #[test]
4441 fn every_token_enters_kv_exactly_once() {
4442 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4443 p.sampler_config.temperature = 0.0;
4445 let r = p.generate("abc", 2, None, None).unwrap();
4446 assert_eq!(r.prompt_tokens, 3);
4447 assert_eq!(
4451 p.kv_cache.seq_len(),
4452 3 + r.tokens_generated - 1,
4453 "each token must be cached exactly once (v1 cached the last prompt token twice)"
4454 );
4455 }
4456
4457 #[test]
4458 fn generation_is_reproducible_with_seed() {
4459 let run = || {
4460 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4461 p.generate("hello", 8, None, None).unwrap().token_ids
4462 };
4463 assert_eq!(run(), run());
4464 }
4465
4466 #[test]
4467 fn resetting_sampler_restarts_the_seeded_stream() {
4468 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
4469 let config = SamplerConfig {
4470 seed: Some(1234),
4471 ..SamplerConfig::default()
4472 };
4473 p.set_sampler_config(config.clone());
4474 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
4475 p.set_sampler_config(config);
4476 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
4477 assert_eq!(first, second);
4478 }
4479
4480 #[test]
4481 fn eviction_bounds_the_cache() {
4482 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
4483 p.kv_cache.max_seq_len = 6;
4484 p.sampler_config.temperature = 0.0;
4485 let _ = p.generate("abcd", 12, None, None).unwrap();
4486 assert!(
4487 p.kv_cache.seq_len() <= 6 + 1,
4488 "cache must stay bounded by max_seq_len (got {})",
4489 p.kv_cache.seq_len()
4490 );
4491 }
4492
4493 #[test]
4494 fn confidence_matches_tokens_and_is_a_probability() {
4495 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4496 p.sampler_config.temperature = 0.0;
4497 p.sampler_config.repetition_penalty = 1.0;
4498 let r = p.generate("abcd", 10, None, None).unwrap();
4499 assert_eq!(
4500 r.token_confidence.len(),
4501 r.token_ids.len(),
4502 "one confidence per emitted token"
4503 );
4504 for &c in &r.token_confidence {
4505 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
4506 }
4507 let logits = [1.0f32, 3.0, 0.5, 3.0];
4509 let p0 = top1_prob_t(&logits, 1, 1.0);
4510 let p1 = top1_prob_t(&logits, 3, 1.0);
4511 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
4512 assert!(p0 > 0.0 && p0 < 1.0);
4513 let sharp = top1_prob_t(&logits, 1, 1.0);
4515 let soft = top1_prob_t(&logits, 1, 2.0);
4516 assert!(soft < sharp, "higher temperature lowers peak confidence");
4517 }
4518
4519 #[test]
4520 fn trace_is_opt_in_and_parallels_the_output() {
4521 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4523 p.sampler_config.temperature = 0.0;
4524 p.sampler_config.repetition_penalty = 1.0;
4525 let r = p.generate("abcd", 10, None, None).unwrap();
4526 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
4527
4528 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4530 p.sampler_config.temperature = 0.0;
4531 p.sampler_config.repetition_penalty = 1.0;
4532 p.set_trace(true);
4533 let r = p.generate("abcd", 10, None, None).unwrap();
4534 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
4535 for (i, tr) in r.traces.iter().enumerate() {
4536 assert_eq!(tr.t, i, "trace index is sequential");
4537 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
4538 assert_eq!(
4539 tr.confidence, r.token_confidence[i],
4540 "trace confidence matches the confidence channel"
4541 );
4542 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
4544 }
4545 }
4546
4547 #[test]
4548 fn explain_prefill_logits_match_greedy_first_token() {
4549 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4553 p.sampler_config.temperature = 0.0;
4554 p.sampler_config.repetition_penalty = 1.0;
4555 let ids = p.tokenizer.encode("abcd");
4556 let logits = p.prefill_next_logits(&ids, None);
4557 let argmax = logits
4558 .iter()
4559 .enumerate()
4560 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4561 .unwrap()
4562 .0 as u32;
4563 let r = p.generate("abcd", 1, None, None).unwrap();
4564 assert_eq!(
4565 argmax, r.token_ids[0],
4566 "explain preview must match greedy emit"
4567 );
4568 }
4569
4570 #[test]
4571 fn laguna_shared_expert_is_unconditionally_added() {
4572 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
4573 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
4574 let zero_dense = || DenseFfn {
4575 gate_proj: matrix(vec![0.0; 4]),
4576 up_proj: matrix(vec![0.0; 4]),
4577 down_proj: matrix(vec![0.0; 4]),
4578 act: Act::Silu,
4579 };
4580 let shared = DenseFfn {
4581 gate_proj: identity(),
4582 up_proj: identity(),
4583 down_proj: identity(),
4584 act: Act::Silu,
4585 };
4586 let x = [1.0, 2.0];
4587 let expected = dense_ffn(&shared, &x, None);
4588 let moe = MoeFfn {
4589 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
4590 experts: vec![zero_dense()],
4591 top_k: 1,
4592 norm_topk_prob: true,
4593 router_sigmoid: true,
4594 expert_bias: None,
4595 routed_scaling: 1.0,
4596 shared: Some((shared, None)),
4597 stats: std::cell::RefCell::new(Vec::new()),
4598 };
4599 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
4600 for (actual, expected) in actual.iter().zip(expected) {
4601 assert!((actual - expected).abs() < 1e-6);
4602 }
4603 }
4604}