1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13 gdn_forward, gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair,
14 vmf_phase_forward, vmf_phase_pair, GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights,
15 VmfPhaseCfg, VmfPhaseWeights,
16};
17use crate::pool::Pool;
18use crate::qtensor::QTensor;
19use crate::sampler::{self, SamplerConfig, 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 = std::sync::atomic::AtomicBool::new(false);
25
26struct ForwardScratch {
30 n1: Vec<f32>,
31 n2: Vec<f32>,
32 p1: Vec<f32>,
33 p2: Vec<f32>,
34}
35
36impl ForwardScratch {
37 fn new(hidden: usize) -> Self {
38 Self {
39 n1: vec![0.0; hidden],
40 n2: vec![0.0; hidden],
41 p1: vec![0.0; hidden],
42 p2: vec![0.0; hidden],
43 }
44 }
45}
46
47pub struct Pipeline {
49 pub tokenizer: std::sync::Arc<Tokenizer>,
52 pub kv_cache: KvCache,
53 pub sampler_config: SamplerConfig,
54 pub weights: PipelineWeights,
55 pub hidden_size: usize,
56 pub intermediate_size: usize,
57 pub num_heads: usize,
58 pub num_kv_heads: usize,
59 pub head_dim: usize,
60 pub num_layers: usize,
61 pub vocab_size: usize,
62 pub rms_eps: f64,
63 pub rope_base: f32,
64 pub norm_style: NormStyle,
65 pub rotary_dim: usize,
67 pub vmf_cfg: Option<VmfPhaseCfg>,
69 pub gdn_cfg: Option<GdnCfg>,
71 pub short_conv_cfg: Option<ShortConvCfg>,
74 pub mtp: Option<MtpModule>,
76 pub speculative: bool,
78 rng: SplitMix64,
79 inv_freq: std::sync::Arc<Vec<f32>>,
83 ws: ForwardScratch,
87 pool: Option<std::sync::Arc<Pool>>,
89 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
93 pub(crate) dyn_force_f32: bool,
95 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
100 pub(crate) dyn_active: Option<usize>,
106 pub(crate) dyn_blend_loaded: bool,
110 pub(crate) dyn_phi_layer: Option<usize>,
113 dyn_phi_ema: Vec<f32>,
115 dyn_phi_seen: usize,
116 pub dyn_router: Option<crate::swarm::DynRouter>,
119 o1_cfg: Option<crate::nystrom::O1Cfg>,
122 o1_flags: Vec<bool>,
124 trace: bool,
127 calib_temp: f32,
130 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
132 graph_kv_id: u64,
133 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
136 graph_want_logits: bool,
137 graph_logits: Option<Vec<f32>>,
140 pub embed_multiplier: f32,
142 pub attn_scale: f32,
145 pub swa: Option<(usize, usize)>,
148 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
151 pub global_attn: Option<(usize, usize)>,
154 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
157 pub attn_v_norm: bool,
159 pub final_softcap: Option<f32>,
161 confidence_on: bool,
165}
166
167#[cfg(target_os = "macos")]
168impl Drop for Pipeline {
169 fn drop(&mut self) {
170 crate::gpu::kv_mirror_drop(self.graph_kv_id);
171 }
172}
173
174pub struct PipelineWeights {
179 pub embed_tokens: QTensor,
181 pub layers: Vec<LayerWeights>,
183 pub lm_head: QTensor,
185 pub final_norm: Vec<f32>,
187}
188
189pub struct LayerWeights {
191 pub input_norm: Vec<f32>,
192 pub post_norm: Vec<f32>,
195 pub attn_out_norm: Option<Vec<f32>>,
198 pub layer_scale: Option<f32>,
200 pub ffn_out_norm: Option<Vec<f32>>,
203 pub ffn: FfnKind,
204 pub attn: AttnKind,
205}
206
207#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
210pub enum Act {
211 #[default]
212 Silu,
213 GeluTanh,
214}
215
216impl Act {
217 pub fn from_arch(name: &str) -> Self {
218 if name == "gelu_tanh" {
219 Self::GeluTanh
220 } else {
221 Self::Silu
222 }
223 }
224
225 #[inline]
226 pub fn apply(self, x: f32) -> f32 {
227 match self {
228 Self::Silu => inference::silu(x),
229 Self::GeluTanh => inference::gelu_tanh(x),
230 }
231 }
232}
233
234pub struct DenseFfn {
236 pub gate_proj: QTensor,
237 pub up_proj: QTensor,
238 pub down_proj: QTensor,
239 pub act: Act,
241}
242
243pub enum FfnKind {
246 Dense(DenseFfn),
247 Moe(MoeFfn),
251}
252
253pub struct MoeFfn {
254 pub router: QTensor,
256 pub experts: Vec<DenseFfn>,
257 pub top_k: usize,
258 pub norm_topk_prob: bool,
259 pub router_sigmoid: bool,
262 pub expert_bias: Option<Vec<f32>>,
266 pub routed_scaling: f32,
269 pub shared: Option<(DenseFfn, QTensor)>,
271 pub stats: std::cell::RefCell<Vec<u64>>,
275}
276
277pub enum AttnKind {
280 Full {
282 wq: QTensor,
283 wk: QTensor,
284 wv: QTensor,
285 wo: QTensor,
286 q_norm: Option<Vec<f32>>,
287 k_norm: Option<Vec<f32>>,
288 output_gate: bool,
289 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
291 },
292 Linear(VmfPhaseWeights),
294 LinearGdn(GdnWeights),
296 ShortConv(ShortConvWeights),
299}
300
301pub struct MtpModule {
306 pub enorm: Vec<f32>,
307 pub hnorm: Vec<f32>,
308 pub eh_proj: QTensor,
310 pub layer: LayerWeights,
311 pub final_norm: Vec<f32>,
312 pub kv: crate::kv_cache::LayerKvCache,
313}
314
315pub struct GenerateResult {
317 pub text: String,
318 pub token_ids: Vec<u32>,
319 pub prompt_tokens: usize,
320 pub tokens_generated: usize,
321 pub finish_reason: String,
322 pub mtp_drafted: usize,
324 pub mtp_accepted: usize,
325 pub token_confidence: Vec<f32>,
330 pub traces: Vec<TokenTrace>,
333}
334
335#[derive(Clone, Debug)]
340pub struct TokenTrace {
341 pub t: usize,
343 pub token_id: u32,
345 pub confidence: f32,
347 pub active_skill: Option<String>,
349 pub recon: Option<f32>,
353 pub switched: bool,
356}
357
358fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
363 let t = if temp > 1e-3 { temp } else { 1.0 };
364 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
365 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
366 if sum > 0.0 {
367 (((logits[id as usize] - max) / t).exp()) / sum
368 } else {
369 0.0
370 }
371}
372
373fn prefill_batched() -> bool {
376 std::env::var("CMF_PREFILL").map(|v| v != "seq").unwrap_or(true)
377}
378
379fn prefill_chunk() -> usize {
384 if let Some(n) =
385 std::env::var("CMF_PREFILL_CHUNK").ok().and_then(|v| v.parse::<usize>().ok())
386 {
387 return n.max(1);
388 }
389 if cfg!(target_os = "macos") {
390 512
391 } else if cfg!(target_arch = "aarch64") {
392 256
395 } else {
396 48
397 }
398}
399
400pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
402
403impl Pipeline {
404 #[allow(clippy::too_many_arguments)]
406
407 #[cfg(target_os = "macos")]
422 fn graph_prefill_preferred(&self) -> bool {
423 if !crate::gpu::enabled_here()
424 || !crate::gpu::q1_force()
425 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
426 {
427 return false;
428 }
429 self.weights.layers.iter().any(
430 |lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()),
431 )
432 }
433
434 #[cfg(not(target_os = "macos"))]
435 fn graph_prefill_preferred(&self) -> bool {
436 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH").map(|v| v != "0").unwrap_or_else(|_| crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed));
444 if !graph_on || !crate::gpu::enabled_here() {
445 return false;
446 }
447 self.weights
448 .layers
449 .iter()
450 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
451 }
452
453 #[cfg(target_os = "macos")]
454 fn q1_graph_gpu(
455 &mut self,
456 start: usize,
457 upto: Option<usize>,
458 position: usize,
459 h: &mut [f32],
460 ) -> usize {
461 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
462 if !crate::gpu::enabled_here()
463 || !crate::gpu::q1_force()
464 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
465 {
466 return start;
467 }
468 if self.swa.is_some()
473 || self.global_attn.is_some()
474 || self.attn_v_norm
475 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
476 || self.weights.layers.iter().any(|lw| {
477 lw.attn_out_norm.is_some()
478 || lw.ffn_out_norm.is_some()
479 || lw.layer_scale.is_some()
480 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
481 })
482 {
483 return start;
484 }
485 let limit = upto.map(|u| u + 1).unwrap_or(self.num_layers).min(self.num_layers);
486
487 enum Item<'a> {
488 Gdn {
489 run: Vec<GdnGpuLayer<'a>>,
490 first: usize,
491 },
492 Attn {
493 l: AttnGpuLayer<'a>,
494 li: usize,
495 q_norm: Option<&'a [f32]>,
496 k_norm: Option<&'a [f32]>,
497 output_gate: bool,
498 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
499 full_gpu: bool,
502 },
503 }
504
505 let dev_attend = std::env::var("CMF_GPU_ATTEND").map(|v| v != "0").unwrap_or(true)
507 && self.head_dim % 4 == 0
508 && self.head_dim <= 128
509 && self.rotary_dim >= 2
510 && self.rotary_dim <= self.head_dim
511 && (self.rotary_dim / 2) % 32 == 0
512 && self.num_kv_heads > 0
513 && self.num_heads % self.num_kv_heads == 0;
514
515 let mut plan: Vec<Item> = Vec::new();
516 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
517 let mut scan = start;
518 while scan < limit {
519 let lw = &self.weights.layers[scan];
520 let FfnKind::Dense(d) = &lw.ffn else { break };
521 let (Some(g), Some(u), Some(dn)) =
522 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
523 else {
524 break;
525 };
526 match &lw.attn {
527 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
528 let parts = (
529 w.in_proj_qkv.q1_parts(),
530 w.in_proj_z.q1_parts(),
531 w.in_proj_a.f32_parts(),
532 w.in_proj_b.f32_parts(),
533 w.out_proj.q1_parts(),
534 );
535 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else { break };
536 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
537 model_ref.get_or_insert_with(|| model.clone());
538 }
539 let gl = GdnGpuLayer {
540 attn_norm: &lw.input_norm,
541 post_norm: &lw.post_norm,
542 qkv,
543 z,
544 a,
545 b,
546 out,
547 gate: g,
548 up: u,
549 down: dn,
550 conv1d: &w.conv1d,
551 a_log: &w.a_log,
552 dt_bias: &w.dt_bias,
553 gnorm: &w.norm,
554 };
555 match plan.last_mut() {
556 Some(Item::Gdn { run, .. }) => run.push(gl),
557 _ => plan.push(Item::Gdn { run: vec![gl], first: scan }),
558 }
559 }
560 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, bias }
561 if !self.kv_cache.layers[scan].o1_sealed() =>
562 {
563 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
564 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else { break };
565 if let QTensor::Mapped { model, .. } = wq {
566 model_ref.get_or_insert_with(|| model.clone());
567 }
568 let cache = &self.kv_cache.layers[scan];
569 let full_gpu = dev_attend
570 && cache.mode == crate::kv_cache::KvMode::F32
571 && cache.o1.is_none()
572 && bias.is_none()
573 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
574 && pk.1 == self.num_kv_heads * self.head_dim
575 && pv.1 == self.num_kv_heads * self.head_dim
576 && po.2 == self.num_heads * self.head_dim;
577 plan.push(Item::Attn {
578 l: AttnGpuLayer {
579 attn_norm: &lw.input_norm,
580 post_norm: &lw.post_norm,
581 wq: pq,
582 wk: pk,
583 wv: pv,
584 wo: po,
585 gate: g,
586 up: u,
587 down: dn,
588 },
589 li: scan,
590 q_norm: q_norm.as_deref(),
591 k_norm: k_norm.as_deref(),
592 output_gate: *output_gate,
593 bias: bias
594 .as_ref()
595 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
596 full_gpu,
597 });
598 }
599 _ => break,
600 }
601 scan += 1;
602 }
603 let Some(model) = model_ref else { return start };
604 if plan.is_empty() {
605 return start;
606 }
607 let dims = GraphDims {
608 hidden: self.hidden_size,
609 eps: self.rms_eps as f32,
610 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
611 };
612 let Some(mut graph) = TokenGraph::new(&model, dims, h) else { return start };
613 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
614 nv: cfg.num_v_heads,
615 nk: cfg.num_k_heads,
616 dk: cfg.key_head_dim,
617 dv: cfg.value_head_dim,
618 kk: cfg.conv_kernel,
619 hidden: self.hidden_size,
620 inter: self.intermediate_size,
621 c_dim: cfg.conv_dim(),
622 eps: cfg.rms_eps as f32,
623 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
624 });
625 let mut valid = 0usize;
629 let mut end = start;
630 for item in &plan {
631 let ok = match item {
632 Item::Gdn { run, .. } => gcfg
633 .as_ref()
634 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
635 .unwrap_or(false),
636 Item::Attn { l, .. } => graph.attn_ok(l),
637 };
638 if !ok {
639 break;
640 }
641 valid += 1;
642 end += match item {
643 Item::Gdn { run, .. } => run.len(),
644 Item::Attn { .. } => 1,
645 };
646 }
647 plan.truncate(valid);
648 if plan.is_empty() {
649 return start;
650 }
651
652 let inv_freq = self.inv_freq.clone();
653 let pool = self.pool.clone();
654 let (nh, nkv, hd, hs, rd, eps) = (
655 self.num_heads,
656 self.num_kv_heads,
657 self.head_dim,
658 self.hidden_size,
659 self.rotary_dim,
660 self.rms_eps,
661 );
662 let norm_style = self.norm_style;
663 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
664 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
665 let kv_id = self.graph_kv_id;
666 let mut pending: Vec<(usize, usize)> = Vec::new();
669 let mut dev_attn: Vec<usize> = Vec::new();
672 for item in &plan {
673 match item {
674 Item::Gdn { run, first } => {
675 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
676 if l.linear_state.len() != want {
677 l.linear_state = vec![0f32; want];
678 }
679 }
680 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
681 .iter()
682 .map(|l| l.linear_state.as_slice())
683 .collect();
684 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
685 tracing::error!("q1 graph: GDN run refused after validation");
687 return start;
688 }
689 graph.commit();
692 pending.push((*first, run.len()));
693 }
694 Item::Attn { l, li, q_norm, k_norm, output_gate, bias, full_gpu } => {
695 if *full_gpu {
697 let cache = &self.kv_cache.layers[*li];
698 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
699 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
700 let cpu_stored = cpu_k[0].len() / hd;
701 let p = crate::gpu::AttnDeviceParams {
702 kv_id,
703 layer: *li,
704 nh,
705 nkv,
706 hd,
707 rd,
708 position,
709 eps: eps as f32,
710 gemma,
711 output_gate: *output_gate,
712 q_norm: *q_norm,
713 k_norm: *k_norm,
714 inv_freq: &inv_freq,
715 cpu_k,
716 cpu_v,
717 cpu_stored,
718 };
719 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
720 graph.commit();
721 dev_attn.push(*li);
722 continue;
723 }
724 }
726 graph.encode_attn_prefix(l);
727 graph.sync();
728 if !pending.is_empty() {
729 let idxs: Vec<usize> =
730 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
731 let mut outs: Vec<&mut [f32]> = self
732 .kv_cache
733 .layers
734 .iter_mut()
735 .enumerate()
736 .filter(|(i, _)| idxs.binary_search(i).is_ok())
737 .map(|(_, s)| s.linear_state.as_mut_slice())
738 .collect();
739 graph.read_states(&mut outs);
740 }
741 let mut q_raw = attention::take_buf(l.wq.1);
742 let mut k = attention::take_buf(l.wk.1);
743 let mut v = attention::take_buf(l.wv.1);
744 graph.read_qkv(&mut q_raw, &mut k, &mut v);
745 let cfg = QwenAttnCfg {
746 num_heads: nh,
747 num_kv_heads: nkv,
748 head_dim: hd,
749 hidden_size: hs,
750 position,
751 inv_freq: &inv_freq,
752 rotary_dim: rd,
753 scale: self.attn_scale,
754 window: None,
755 v_norm: false,
756 q_norm: *q_norm,
757 k_norm: *k_norm,
758 output_gate: *output_gate,
759 bias: *bias,
760 rms_eps: eps,
761 norm_style,
762 pool: pool.as_deref(),
763 };
764 let mut ao = attention::qwen_attention_core(
765 q_raw,
766 k,
767 v,
768 &mut self.kv_cache.layers[*li],
769 &cfg,
770 );
771 graph.encode_attn_suffix(l, &ao);
772 graph.commit();
775 attention::recycle_buf(&mut ao);
776 }
777 }
778 }
779 let mut lm_rows = None;
784 if self.graph_want_logits
785 && upto.is_none()
786 && end == self.num_layers
787 && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
788 {
789 if let Some(lm) = self.weights.lm_head.q1_parts() {
790 if graph.lm_head_ok(lm) {
791 graph.encode_lm_head(&self.weights.final_norm, lm);
792 lm_rows = Some(lm.1);
793 }
794 }
795 }
796 graph.sync();
797 if !pending.is_empty() {
798 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
799 let mut outs: Vec<&mut [f32]> = self
800 .kv_cache
801 .layers
802 .iter_mut()
803 .enumerate()
804 .filter(|(i, _)| idxs.binary_search(i).is_ok())
805 .map(|(_, s)| s.linear_state.as_mut_slice())
806 .collect();
807 graph.read_states(&mut outs);
808 }
809 if let Some(rows) = lm_rows {
810 let mut lg = attention::take_buf(rows.min(self.vocab_size));
811 graph.read_logits(&mut lg);
812 lg.resize(self.vocab_size, 0.0);
813 if let Some(c) = self.final_softcap {
814 for l in lg.iter_mut() {
815 *l = c * (*l / c).tanh();
816 }
817 }
818 self.graph_logits = Some(lg);
819 }
820 graph.finish(h);
821 for li in dev_attn {
825 let mut krow = attention::take_buf(nkv * hd);
826 let mut vrow = attention::take_buf(nkv * hd);
827 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
828 let cache = &mut self.kv_cache.layers[li];
829 cache.append(&krow, &vrow, &[]);
830 let n = cache.seq_len;
831 let mut imp = attention::take_buf(n);
832 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
833 cache.accumulate_imp(&imp);
834 attention::recycle_buf(&mut imp);
835 }
836 attention::recycle_buf(&mut krow);
837 attention::recycle_buf(&mut vrow);
838 }
839 end
840 }
841
842 pub fn new(
843 tokenizer: Tokenizer,
844 weights: PipelineWeights,
845 hidden_size: usize,
846 intermediate_size: usize,
847 num_heads: usize,
848 num_kv_heads: usize,
849 head_dim: usize,
850 num_layers: usize,
851 vocab_size: usize,
852 rms_eps: f64,
853 rope_base: f32,
854 norm_style: NormStyle,
855 max_seq_len: usize,
856 sampler_config: SamplerConfig,
857 ) -> Self {
858 let rng = match sampler_config.seed {
859 Some(s) => SplitMix64::new(s),
860 None => SplitMix64::from_entropy(),
861 };
862 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
863 let pool = Pool::from_env();
864 if let Some(p) = &pool {
865 tracing::info!("worker pool: {} threads", p.n_workers());
866 }
867 Self {
868 tokenizer: std::sync::Arc::new(tokenizer),
869 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
870 sampler_config,
871 weights,
872 hidden_size,
873 intermediate_size,
874 num_heads,
875 num_kv_heads,
876 head_dim,
877 num_layers,
878 vocab_size,
879 rms_eps,
880 rope_base,
881 norm_style,
882 rotary_dim: head_dim,
883 vmf_cfg: None,
884 gdn_cfg: None,
885 short_conv_cfg: None,
886 mtp: None,
887 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
888 rng,
889 inv_freq,
890 ws: ForwardScratch::new(hidden_size),
891 pool,
892 model: None,
893 dyn_force_f32: false,
894 dyn_skill_layers: Vec::new(),
895 dyn_active: None,
896 dyn_blend_loaded: false,
897 dyn_phi_layer: None,
898 dyn_phi_ema: Vec::new(),
899 dyn_phi_seen: 0,
900 dyn_router: None,
901 o1_cfg: None,
902 o1_flags: Vec::new(),
903 trace: false,
904 calib_temp: 1.0,
905 confidence_on: true,
906 embed_multiplier: 1.0,
907 attn_scale: 1.0 / (head_dim as f32).sqrt(),
908 swa: None,
909 inv_freq_local: None,
910 global_attn: None,
911 inv_freq_global: None,
912 attn_v_norm: false,
913 final_softcap: None,
914 graph_want_logits: false,
915 graph_logits: None,
916 graph_kv_id: {
917 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
918 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
919 },
920 }
921 }
922
923 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
930 self.o1_flags = match &cfg {
931 Some(c) => {
932 let mut flags = c.layer_flags(self.num_layers);
933 for (li, f) in flags.iter_mut().enumerate() {
934 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
935 *f = false;
936 }
937 }
938 flags
939 }
940 None => Vec::new(),
941 };
942 if let Some(c) = &cfg {
943 let n = self.o1_flags.iter().filter(|&&f| f).count();
944 tracing::info!(
945 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
946 self.num_layers, c.m, c.w, c.sink, c.rect
947 );
948 }
949 self.o1_cfg = cfg;
950 }
951
952 pub fn o1_active(&self) -> bool {
954 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
955 }
956
957 fn o1_begin(&mut self) {
959 if let Some(c) = &self.o1_cfg {
960 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
961 for (li, &f) in self.o1_flags.iter().enumerate() {
962 if f {
963 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
964 }
965 }
966 }
967 }
968
969 fn o1_seal(&mut self) {
972 if self.o1_cfg.is_none() {
973 return;
974 }
975 for li in 0..self.num_layers {
976 if self.o1_flags.get(li).copied().unwrap_or(false) {
977 self.kv_cache.layers[li].o1_seal(self.num_heads);
978 }
979 }
980 }
981
982 pub fn set_trace(&mut self, on: bool) {
984 self.trace = on;
985 }
986
987 pub fn set_confidence(&mut self, on: bool) {
992 self.confidence_on = on;
993 }
994
995 pub fn set_calib_temp(&mut self, t: f32) {
998 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
999 }
1000
1001 pub fn calib_temp(&self) -> f32 {
1003 self.calib_temp
1004 }
1005
1006 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1009 self.rotary_dim = rotary_dim.min(self.head_dim);
1010 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1011 }
1012
1013 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1014 QwenAttnCfg {
1015 num_heads: self.num_heads,
1016 num_kv_heads: self.num_kv_heads,
1017 head_dim: self.head_dim,
1018 hidden_size: self.hidden_size,
1019 position,
1020 inv_freq: &self.inv_freq,
1021 rotary_dim: self.rotary_dim,
1022 scale: self.attn_scale,
1023 window: None,
1024 v_norm: false,
1025 q_norm: None,
1026 k_norm: None,
1027 output_gate: false,
1028 bias: None,
1029 rms_eps: self.rms_eps,
1030 norm_style: self.norm_style,
1031 pool: self.pool.as_deref(),
1032 }
1033 }
1034
1035 pub fn generate(
1037 &mut self,
1038 prompt: &str,
1039 max_tokens: usize,
1040 task_mask: Option<&TaskMask>,
1041 on_token: Option<TokenCallback>,
1042 ) -> Result<GenerateResult, String> {
1043 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1044 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1045 }
1046
1047 pub fn generate_from_ids(
1055 &mut self,
1056 input_ids: &[u32],
1057 max_tokens: usize,
1058 task_mask: Option<&TaskMask>,
1059 mut on_token: Option<TokenCallback>,
1060 ) -> Result<GenerateResult, String> {
1061 if std::env::var("CMF_TRACE_H").is_ok() {
1062 eprintln!("input_ids: {input_ids:?}");
1063 }
1064 if input_ids.is_empty() {
1065 return Err("empty prompt: nothing to generate from".to_string());
1066 }
1067
1068 self.kv_cache.clear();
1070 crate::gpu::graph_kv_reset(self.graph_kv_id);
1071 self.o1_begin();
1072
1073 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH").map(|v| v != "0").unwrap_or_else(|_| crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed));
1079 let spec_active = self.speculative
1080 && self.mtp.is_some()
1081 && task_mask.is_none()
1082 && !self.o1_active()
1083 && !graph_on
1084 && self.sampler_config.temperature < 1e-6;
1085 let mut mtp = if spec_active { self.mtp.take() } else { None };
1088 if let Some(m) = &mut mtp {
1089 m.kv.clear();
1090 }
1091 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
1095 if let Some(r) = &mut router {
1096 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1099 }
1100
1101 let mut all_ids = input_ids.to_vec();
1102 let mut generated = 0usize;
1103 let mut finish_reason = "max_tokens".to_string();
1104 let mut drafted = 0usize;
1105 let mut accepted = 0usize;
1106 let mut confidence: Vec<f32> = Vec::new();
1107 let trace_on = self.trace;
1108 let calib_temp = self.calib_temp;
1109 let mut traces: Vec<TokenTrace> = Vec::new();
1110
1111 let mut hidden = vec![0.0f32; self.hidden_size];
1117 let mut pos = 0usize;
1118 let fuse_lm = mtp.is_none() && router.is_none();
1122 self.graph_logits = None;
1123 self.graph_want_logits = false;
1124 let dyn_prefill = router.is_some();
1129 let graph_prefill = self.graph_prefill_preferred();
1135 if task_mask.is_none()
1136 && !dyn_prefill
1137 && !graph_prefill
1138 && prefill_batched()
1139 && input_ids.len() > 2
1140 {
1141 let chunk = prefill_chunk();
1147 let hs = self.hidden_size;
1148 while pos < input_ids.len() {
1149 let end = (pos + chunk).min(input_ids.len());
1150 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1151 if let Some(m) = &mut mtp {
1152 for p in pos..end {
1153 if p + 1 < input_ids.len() {
1154 let _ = self.mtp_step(
1155 m,
1156 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1157 input_ids[p + 1],
1158 p,
1159 );
1160 }
1161 }
1162 }
1163 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1164 pos = end;
1165 }
1166 }
1167 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1168 while pos + 1 < input_ids.len() {
1169 let e1 = self.embed_single(input_ids[pos]);
1170 let e2 = self.embed_single(input_ids[pos + 1]);
1171 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1172 self.commit_linear_scratch();
1174 if let Some(m) = &mut mtp {
1175 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1176 if pos + 2 < input_ids.len() {
1177 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1178 }
1179 }
1180 hidden = h2;
1181 pos += 2;
1182 }
1183 }
1184 let _tpf = std::time::Instant::now();
1193 let batch_k = std::env::var("CMF_BATCH_K").ok().and_then(|v| v.parse::<usize>().ok()).unwrap_or(0);
1194 if batch_k > 0 && graph_prefill && task_mask.is_none() && mtp.is_none() && !dyn_prefill && pos + 1 < input_ids.len() {
1195 let hs = self.hidden_size;
1196 let chunk = batch_k;
1197 while pos < input_ids.len() {
1198 let end = (pos + chunk).min(input_ids.len());
1199 let bk = end - pos;
1200 let mut hiddens = vec![0f32; bk * hs];
1201 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1202 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1203 }
1204 let positions: Vec<usize> = (pos..end).collect();
1205 if self.try_batch_graph_wgpu(&mut hiddens, &positions, bk) {
1206 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1207 pos = end;
1208 } else {
1209 break; }
1211 }
1212 }
1213 while pos < input_ids.len() {
1214 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1215 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1216 if let Some(m) = &mut mtp {
1217 if pos + 1 < input_ids.len() {
1218 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1219 }
1220 }
1221 pos += 1;
1222 }
1223 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1224 eprintln!("prefill: {} tokens in {:.1} ms (batch_k={batch_k})", input_ids.len(), _tpf.elapsed().as_secs_f64() * 1000.0);
1225 }
1226 self.o1_seal();
1229
1230 macro_rules! commit {
1232 ($id:expr) => {{
1233 all_ids.push($id);
1234 generated += 1;
1235 if self.tokenizer.is_eos($id) {
1236 finish_reason = "stop".to_string();
1237 false
1238 } else {
1239 let token_text = self.tokenizer.decode_token($id);
1240 let mut go = true;
1241 if let Some(ref mut cb) = on_token {
1242 if !cb(&token_text) {
1243 finish_reason = "cancelled".to_string();
1244 go = false;
1245 }
1246 }
1247 go
1248 }
1249 }};
1250 }
1251
1252 let mut next_pos = input_ids.len();
1254 'decode: while generated < max_tokens {
1255 let mut logits = match self.graph_logits.take() {
1256 Some(lg) => lg,
1257 None => {
1258 inference::rms_norm_into(
1259 &hidden,
1260 &self.weights.final_norm,
1261 self.rms_eps,
1262 self.norm_style,
1263 &mut self.ws.n1,
1264 );
1265 self.lm_head_forward(&self.ws.n1)
1266 }
1267 };
1268 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
1269 if self.confidence_on {
1270 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1271 }
1272 attention::recycle_buf(&mut logits);
1273 if trace_on {
1274 let skill = router.as_ref().and_then(|r| r.active_id());
1278 traces.push(TokenTrace {
1279 t: generated,
1280 token_id: t_next,
1281 confidence: confidence.last().copied().unwrap_or(0.0),
1282 active_skill: skill,
1283 recon: None,
1284 switched: false,
1285 });
1286 }
1287 if !commit!(t_next) {
1288 break 'decode;
1289 }
1290 if generated >= max_tokens {
1291 break 'decode;
1292 }
1293
1294 if self.kv_cache.needs_eviction() {
1295 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1296 self.kv_cache.evict(keep);
1297 }
1298
1299 match &mut mtp {
1300 Some(m) if generated + 1 < max_tokens => {
1302 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1303 drafted += 1;
1304 let emb1 = self.embed_single(t_next);
1305 let emb2 = self.embed_single(draft);
1306 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1307
1308 inference::rms_norm_into(
1309 &h1,
1310 &self.weights.final_norm,
1311 self.rms_eps,
1312 self.norm_style,
1313 &mut self.ws.n1,
1314 );
1315 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1316 let t_after =
1317 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
1318 if self.confidence_on {
1319 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1320 }
1321 attention::recycle_buf(&mut logits1);
1322 if trace_on {
1323 traces.push(TokenTrace {
1326 t: generated,
1327 token_id: t_after,
1328 confidence: confidence.last().copied().unwrap_or(0.0),
1329 active_skill: None,
1330 recon: None,
1331 switched: false,
1332 });
1333 }
1334 let stop = !commit!(t_after);
1335
1336 if t_after == draft {
1337 accepted += 1;
1338 self.commit_linear_scratch();
1339 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1340 hidden = h2;
1341 next_pos += 2;
1342 } else {
1343 for layer in &mut self.kv_cache.layers {
1345 layer.truncate_last(1);
1346 }
1347 if !stop {
1348 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1349 hidden = self
1350 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
1351 }
1352 next_pos += 2;
1353 }
1354 if stop {
1355 break 'decode;
1356 }
1357 }
1358 _ => {
1360 self.graph_want_logits = fuse_lm;
1361 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1362 next_pos += 1;
1363 if let Some(r) = &mut router {
1366 let phi = self.dyn_phi_ema.clone();
1367 let decision = r.step(&phi, generated);
1368 if let Some(new_active) = decision {
1369 let _ = self.set_active_skill(new_active);
1370 }
1371 if trace_on {
1374 if let Some(last) = traces.last_mut() {
1375 let e = r.last_best_e();
1376 last.recon = e.is_finite().then_some(e);
1377 last.switched = decision.is_some();
1378 }
1379 }
1380 }
1381 }
1382 }
1383 }
1384
1385 self.graph_want_logits = false;
1386 self.graph_logits = None;
1387 if router.is_some() {
1389 let _ = self.set_active_skill(None);
1390 }
1391 self.dyn_router = router.or(self.dyn_router.take());
1392 self.mtp = mtp.or(self.mtp.take());
1393
1394 let output_ids = &all_ids[input_ids.len()..];
1395 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1397 Ok(GenerateResult {
1398 text: self.tokenizer.decode(output_ids),
1399 token_ids: output_ids.to_vec(),
1400 prompt_tokens: input_ids.len(),
1401 tokens_generated: generated,
1402 finish_reason,
1403 mtp_drafted: drafted,
1404 mtp_accepted: accepted,
1405 token_confidence: confidence,
1406 traces,
1407 })
1408 }
1409
1410 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
1414 let e = self.embed_single(next_token);
1418 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1419 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1420 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1421 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1422 let mut x = vec![0.0f32; self.hidden_size];
1423 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1424
1425 let lw = &m.layer;
1427 inference::rms_norm_into(&x, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1428 let attn = match &lw.attn {
1429 AttnKind::Full {
1430 wq,
1431 wk,
1432 wv,
1433 wo,
1434 q_norm,
1435 k_norm,
1436 output_gate,
1437 bias,
1438 } => {
1439 let mut cfg = self.attn_cfg(position);
1440 cfg.q_norm = q_norm.as_deref();
1441 cfg.k_norm = k_norm.as_deref();
1442 cfg.output_gate = *output_gate;
1443 cfg.bias = bias
1444 .as_ref()
1445 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1446 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1447 }
1448 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1449 unreachable!("MTP block is full attention")
1450 }
1451 };
1452 for (i, &a) in attn.iter().enumerate() {
1453 x[i] += a;
1454 }
1455 inference::rms_norm_into(&x, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1456 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1457 for (i, &f) in ffn.iter().enumerate() {
1458 x[i] += f;
1459 }
1460
1461 inference::rms_norm_into(&x, &m.final_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1462 let mut lg = self.lm_head_forward(&self.ws.n1);
1463 let draft = sampler::argmax(&lg);
1464 attention::recycle_buf(&mut lg);
1465 draft
1466 }
1467
1468 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1472 let emb1 = self.embed_single(1);
1473 let emb2 = self.embed_single(2);
1474 let pos = self.kv_cache.seq_len();
1475
1476 let t0 = std::time::Instant::now();
1477 for _ in 0..iters {
1478 let _ = self.forward_layers(&emb1, pos, None);
1479 let _ = self.forward_layers(&emb2, pos + 1, None);
1480 for l in &mut self.kv_cache.layers {
1481 l.truncate_last(2);
1482 }
1483 }
1484 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1485
1486 let t1 = std::time::Instant::now();
1487 for _ in 0..iters {
1488 let _ = self.forward_pair(&emb1, &emb2, pos);
1489 for l in &mut self.kv_cache.layers {
1490 l.truncate_last(2);
1491 }
1492 }
1493 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1494 (singles_ms, pair_ms)
1495 }
1496
1497 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
1502 let mut h1 = emb1.to_vec();
1503 let mut h2 = emb2.to_vec();
1504 let (nh, _nkv, _hd, hs, _rd, eps) = (
1505 self.num_heads,
1506 self.num_kv_heads,
1507 self.head_dim,
1508 self.hidden_size,
1509 self.rotary_dim,
1510 self.rms_eps,
1511 );
1512 let pool = self.pool.clone();
1513
1514 for li in 0..self.num_layers {
1515 let lw = &self.weights.layers[li];
1516 inference::rms_norm_into(&h1, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1519 inference::rms_norm_into(&h2, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n2);
1520
1521 let (a1, a2) = match &lw.attn {
1522 AttnKind::Linear(w) => {
1523 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1524 let layer = &mut self.kv_cache.layers[li];
1525 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1526 vmf_phase_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1527 }
1528 AttnKind::LinearGdn(w) => {
1529 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1530 let layer = &mut self.kv_cache.layers[li];
1531 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1532 gdn_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1533 }
1534 AttnKind::ShortConv(w) => {
1535 let cfg = self.short_conv_cfg.expect("short-conv layer without short_conv_cfg");
1536 let layer = &mut self.kv_cache.layers[li];
1537 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1538 short_conv_pair(
1539 &self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref(),
1540 )
1541 }
1542 AttnKind::Full {
1543 wq,
1544 wk,
1545 wv,
1546 wo,
1547 q_norm,
1548 k_norm,
1549 output_gate,
1550 bias,
1551 } => {
1552 let inv_freq_l = self.layer_inv_freq(li);
1553 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1554 let cfg = QwenAttnCfg {
1555 num_heads: nh,
1556 num_kv_heads: nkv_l,
1557 head_dim: hd_l,
1558 hidden_size: hs,
1559 position,
1560 inv_freq: &inv_freq_l,
1561 rotary_dim: rd_l,
1562 scale: self.attn_scale,
1563 window: self.layer_window(li),
1564 v_norm: self.attn_v_norm,
1565 q_norm: q_norm.as_deref(),
1566 k_norm: k_norm.as_deref(),
1567 output_gate: *output_gate,
1568 bias: bias
1569 .as_ref()
1570 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1571 rms_eps: eps,
1572 norm_style: self.norm_style,
1573 pool: pool.as_deref(),
1574 };
1575 attention::qwen_attention_pair(
1576 &self.ws.n1,
1577 &self.ws.n2,
1578 wq,
1579 wk,
1580 wv,
1581 wo,
1582 &mut self.kv_cache.layers[li],
1583 &cfg,
1584 )
1585 }
1586 };
1587 let (a1, a2) = match &self.weights.layers[li].attn_out_norm {
1588 Some(w) => (
1589 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1590 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1591 ),
1592 None => (a1, a2),
1593 };
1594 for i in 0..self.hidden_size {
1595 h1[i] += a1[i];
1596 h2[i] += a2[i];
1597 }
1598 let (mut a1, mut a2) = (a1, a2);
1599 attention::recycle_buf(&mut a1);
1600 attention::recycle_buf(&mut a2);
1601
1602 let lw = &self.weights.layers[li];
1603 inference::rms_norm_into(&h1, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1604 inference::rms_norm_into(&h2, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p2);
1605 let (f1, f2) =
1606 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1607 let (f1, f2) = match &self.weights.layers[li].ffn_out_norm {
1608 Some(w) => (
1609 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1610 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1611 ),
1612 None => (f1, f2),
1613 };
1614 for i in 0..self.hidden_size {
1615 h1[i] += f1[i];
1616 h2[i] += f2[i];
1617 }
1618 let (mut f1, mut f2) = (f1, f2);
1619 attention::recycle_buf(&mut f1);
1620 attention::recycle_buf(&mut f2);
1621 if let Some(sc) = self.weights.layers[li].layer_scale {
1622 for i in 0..self.hidden_size {
1623 h1[i] *= sc;
1624 h2[i] *= sc;
1625 }
1626 }
1627 }
1628 (h1, h2)
1629 }
1630
1631 fn commit_linear_scratch(&mut self) {
1633 for layer in &mut self.kv_cache.layers {
1634 if !layer.linear_scratch.is_empty() {
1635 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1636 layer.linear_scratch.clear();
1637 }
1638 }
1639 }
1640
1641 pub fn forward_ids(
1644 &mut self,
1645 ids: &[u32],
1646 task_mask: Option<&TaskMask>,
1647 ) -> Result<Vec<f32>, String> {
1648 if ids.is_empty() {
1649 return Err("empty id sequence".to_string());
1650 }
1651 self.kv_cache.clear();
1652 self.o1_begin();
1653 let mut hidden = vec![0.0f32; self.hidden_size];
1654 let mut pos = 0usize;
1655 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1656 let chunk = prefill_chunk();
1660 let hs = self.hidden_size;
1661 while pos < ids.len() {
1662 let end = (pos + chunk).min(ids.len());
1663 let hb = self.prefill_batch(&ids[pos..end], pos);
1664 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1665 pos = end;
1666 }
1667 }
1668 if task_mask.is_none() {
1669 while pos + 1 < ids.len() {
1670 let e1 = self.embed_single(ids[pos]);
1671 let e2 = self.embed_single(ids[pos + 1]);
1672 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1673 self.commit_linear_scratch();
1674 hidden = h2;
1675 pos += 2;
1676 }
1677 }
1678 while pos < ids.len() {
1679 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1680 pos += 1;
1681 }
1682 self.o1_seal();
1686 let normed = inference::rms_norm(
1687 &hidden,
1688 &self.weights.final_norm,
1689 self.rms_eps,
1690 self.norm_style,
1691 );
1692 Ok(self.lm_head_forward(&normed))
1693 }
1694
1695 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1702 let (nll, cnt) = self.nll_ids_from(ids, 0);
1703 (nll / cnt.max(1) as f64).exp()
1704 }
1705
1706 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
1711 self.kv_cache.clear();
1712 FFN_PROBE.with(|p| {
1713 *p.borrow_mut() =
1714 Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
1715 });
1716 crate::gpu::cpu_scope(|| {
1717 for (pos, &id) in ids.iter().enumerate() {
1718 let emb = self.embed_single(id);
1719 let _ = self.forward_layers(&emb, pos, None);
1720 }
1721 });
1722 self.kv_cache.clear();
1723 FFN_PROBE.with(|p| p.borrow_mut().take()).unwrap_or_default()
1724 }
1725
1726 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
1730 self.kv_cache.clear();
1731 let mut nll = 0f64;
1732 let mut cnt = 0usize;
1733 let mut hidden = vec![0f32; self.hidden_size];
1734 for (pos, &id) in ids.iter().enumerate() {
1735 if pos > 0 {
1736 inference::rms_norm_into(
1737 &hidden,
1738 &self.weights.final_norm,
1739 self.rms_eps,
1740 self.norm_style,
1741 &mut self.ws.n1,
1742 );
1743 let mut logits = self.lm_head_forward(&self.ws.n1);
1744 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1745 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
1746 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
1747 nll -= p.max(1e-300).ln();
1748 cnt += 1;
1749 attention::recycle_buf(&mut logits);
1750 }
1751 let emb = self.embed_single(id);
1752 hidden = self.forward_layers(&emb, pos, Some(mask));
1753 }
1754 self.kv_cache.clear();
1755 (nll / cnt.max(1) as f64).exp()
1756 }
1757
1758 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1767 self.kv_cache.clear();
1768 let mut nll = 0f64;
1769 let mut cnt = 0usize;
1770 if prefill_batched() {
1771 const CHUNK: usize = 128;
1777 const LM_SUB: usize = 32;
1778 let n = ids.len().saturating_sub(1);
1779 let hs = self.hidden_size;
1780 let rows = self.weights.lm_head.rows();
1781 let mut pos = 0usize;
1782 while pos < n {
1783 let end = (pos + CHUNK).min(n);
1784 let bsz = end - pos;
1785 let hb = self.prefill_batch(&ids[pos..end], pos);
1786 let mut k0 = 0usize;
1787 while k0 < bsz {
1788 let k1 = (k0 + LM_SUB).min(bsz);
1789 let sb = k1 - k0;
1790 if pos + k1 <= start {
1793 k0 = k1;
1794 continue;
1795 }
1796 let mut normed = vec![0.0f32; sb * hs];
1797 for k in 0..sb {
1798 let r = inference::rms_norm(
1799 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
1800 &self.weights.final_norm,
1801 self.rms_eps,
1802 self.norm_style,
1803 );
1804 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
1805 }
1806 let mut logits = vec![0.0f32; sb * rows];
1807 self.weights
1808 .lm_head
1809 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1810 for k in 0..sb {
1811 if pos + k0 + k < start {
1812 continue;
1813 }
1814 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1815 let target = ids[pos + k0 + k + 1] as usize;
1816 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1817 let lse: f64 = lg
1818 .iter()
1819 .map(|&v| ((v - max) as f64).exp())
1820 .sum::<f64>()
1821 .ln()
1822 + max as f64;
1823 nll += lse - lg[target] as f64;
1824 cnt += 1;
1825 }
1826 k0 = k1;
1827 }
1828 pos = end;
1829 }
1830 self.kv_cache.clear();
1831 return (nll, cnt);
1832 }
1833 for pos in 0..ids.len().saturating_sub(1) {
1834 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1835 if pos < start {
1836 continue;
1837 }
1838 let normed = inference::rms_norm(
1839 &hidden,
1840 &self.weights.final_norm,
1841 self.rms_eps,
1842 self.norm_style,
1843 );
1844 let logits = self.lm_head_forward(&normed);
1845 let target = ids[pos + 1] as usize;
1846 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1847 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1848 + max as f64;
1849 nll += lse - logits[target] as f64;
1850 cnt += 1;
1851 }
1852 self.kv_cache.clear();
1853 (nll, cnt)
1854 }
1855
1856 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1872 self.kv_cache.clear();
1873 self.o1_begin();
1874 let n = ids.len().saturating_sub(1);
1875 let p = prefill.min(n);
1876 let mut pos = 0usize;
1878 if prefill_batched() {
1879 const CHUNK: usize = 128;
1880 while pos < p {
1881 let end = (pos + CHUNK).min(p);
1882 let _ = self.prefill_batch(&ids[pos..end], pos);
1883 pos = end;
1884 }
1885 } else {
1886 while pos < p {
1887 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1888 pos += 1;
1889 }
1890 }
1891 self.o1_seal();
1892
1893 let mut nll = 0f64;
1894 let mut cnt = 0usize;
1895 for pos in p..n {
1896 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1897 let normed = inference::rms_norm(
1898 &hidden,
1899 &self.weights.final_norm,
1900 self.rms_eps,
1901 self.norm_style,
1902 );
1903 let logits = self.lm_head_forward(&normed);
1904 let target = ids[pos + 1] as usize;
1905 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1906 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1907 + max as f64;
1908 nll += lse - logits[target] as f64;
1909 cnt += 1;
1910 }
1911 self.kv_cache.clear();
1912 (nll, cnt)
1913 }
1914
1915 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1923 self.kv_cache.clear();
1924 let n = ids.len().saturating_sub(1);
1925 let mut correct = Vec::with_capacity(n);
1926 let mut pmax = Vec::with_capacity(n);
1927 for pos in 0..n {
1928 let emb = self.embed_single(ids[pos]);
1929 let hidden = self.forward_layers(&emb, pos, None);
1930 let normed = inference::rms_norm(
1931 &hidden,
1932 &self.weights.final_norm,
1933 self.rms_eps,
1934 self.norm_style,
1935 );
1936 let logits = self.lm_head_forward(&normed);
1937 let target = ids[pos + 1] as usize;
1938 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1939 for (i, &v) in logits.iter().enumerate() {
1940 if v > mval {
1941 mval = v;
1942 amax = i;
1943 }
1944 }
1945 correct.push(amax == target);
1946 let row: Vec<f32> = temps
1947 .iter()
1948 .map(|&t| {
1949 let tt = t.max(1e-3);
1950 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1951 1.0 / s.max(1e-12) })
1953 .collect();
1954 pmax.push(row);
1955 }
1956 self.kv_cache.clear();
1957 (correct, pmax)
1958 }
1959
1960 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1967 let mut router = match self.dyn_router.take() {
1968 Some(r) => r,
1969 None => return (self.ppl_ids(ids), 0),
1970 };
1971 router.reset();
1972 self.dyn_phi_seen = 0;
1973 let _ = self.set_active_skill(None);
1974
1975 self.kv_cache.clear();
1976 let mut nll = 0f64;
1977 let mut cnt = 0usize;
1978 for pos in 0..ids.len().saturating_sub(1) {
1979 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1980 let normed = inference::rms_norm(
1981 &hidden,
1982 &self.weights.final_norm,
1983 self.rms_eps,
1984 self.norm_style,
1985 );
1986 let logits = self.lm_head_forward(&normed);
1987 let target = ids[pos + 1] as usize;
1988 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1989 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1990 + max as f64;
1991 nll += lse - logits[target] as f64;
1992 cnt += 1;
1993 let phi = self.dyn_phi_ema.clone();
1995 if let Some(new_active) = router.step(&phi, pos) {
1996 let _ = self.set_active_skill(new_active);
1997 }
1998 }
1999 let switches = router.switches.len();
2000 let _ = self.set_active_skill(None);
2001 self.dyn_router = Some(router);
2002 self.kv_cache.clear();
2003 ((nll / cnt.max(1) as f64).exp(), switches)
2004 }
2005
2006 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
2008 self.kv_cache.clear();
2009 let mut acc = vec![0f32; self.hidden_size];
2010 for (pos, &id) in ids.iter().enumerate() {
2011 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
2012 for (a, v) in acc.iter_mut().zip(&h) {
2013 *a += v;
2014 }
2015 }
2016 let n = ids.len().max(1) as f32;
2017 for a in acc.iter_mut() {
2018 *a /= n;
2019 }
2020 self.kv_cache.clear();
2021 acc
2022 }
2023
2024 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
2030 let b = ids.len();
2031 let hs = self.hidden_size;
2032 let mut h: Vec<f32> = vec![0.0; b * hs];
2035 let mut h_ready = false;
2036 let mut fill_h = |h: &mut Vec<f32>, me: &Self| {
2037 for (bi, &id) in ids.iter().enumerate() {
2038 let e = me.embed_single(id);
2039 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
2040 }
2041 };
2042 let (nh, _nkv, _hd, _rd, eps) = (
2043 self.num_heads,
2044 self.num_kv_heads,
2045 self.head_dim,
2046 self.rotary_dim,
2047 self.rms_eps,
2048 );
2049 let pool = self.pool.clone();
2050 let norm_style = self.norm_style;
2051
2052 #[cfg(target_os = "macos")]
2053 let mut chunk_skip_until = 0usize;
2054 for li in 0..self.num_layers {
2055 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2062 {
2063 if li < chunk_skip_until {
2064 continue;
2065 }
2066 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2067 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2068 if end > li {
2069 h_ready = true;
2070 chunk_skip_until = end;
2071 continue;
2072 }
2073 }
2074 if !h_ready {
2075 fill_h(&mut h, self);
2076 h_ready = true;
2077 }
2078 let lw = &self.weights.layers[li];
2079 match &lw.attn {
2081 AttnKind::LinearGdn(w) => {
2082 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2084 let mut normed = vec![0.0f32; b * hs];
2085 for bi in 0..b {
2086 let r = inference::rms_norm(
2087 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
2088 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2089 }
2090 let attn = crate::linear_core::gdn_forward_batch(
2091 &normed, b, w, &cfg,
2092 &mut self.kv_cache.layers[li].linear_state,
2093 pool.as_deref(),
2094 );
2095 for (dst, &a) in h.iter_mut().zip(&attn) {
2096 *dst += a;
2097 }
2098 }
2099 AttnKind::ShortConv(w) => {
2100 let cfg =
2103 self.short_conv_cfg.expect("short-conv layer without short_conv_cfg");
2104 let mut normed = vec![0.0f32; b * hs];
2105 for bi in 0..b {
2106 inference::rms_norm_into(
2107 &h[bi * hs..(bi + 1) * hs],
2108 &lw.input_norm,
2109 eps,
2110 norm_style,
2111 &mut normed[bi * hs..(bi + 1) * hs],
2112 );
2113 }
2114 let attn = short_conv_forward_batch(
2115 &normed, b, w, &cfg,
2116 &mut self.kv_cache.layers[li].linear_state,
2117 pool.as_deref(),
2118 );
2119 for (dst, &a) in h.iter_mut().zip(&attn) {
2120 *dst += a;
2121 }
2122 }
2123 AttnKind::Full {
2124 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
2125 } => {
2126 let mut normed = vec![0.0f32; b * hs];
2130 for bi in 0..b {
2131 inference::rms_norm_into(
2132 &h[bi * hs..(bi + 1) * hs],
2133 &lw.input_norm,
2134 eps,
2135 norm_style,
2136 &mut normed[bi * hs..(bi + 1) * hs],
2137 );
2138 }
2139 let inv_freq_l = self.layer_inv_freq(li);
2140 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2141 let cfg = QwenAttnCfg {
2142 num_heads: nh,
2143 num_kv_heads: nkv_l,
2144 head_dim: hd_l,
2145 hidden_size: hs,
2146 position: start_pos,
2147 inv_freq: &inv_freq_l,
2148 rotary_dim: rd_l,
2149 scale: self.attn_scale,
2150 window: self.layer_window(li),
2151 v_norm: self.attn_v_norm,
2152 q_norm: q_norm.as_deref(),
2153 k_norm: k_norm.as_deref(),
2154 output_gate: *output_gate,
2155 bias: bias.as_ref().map(|(a, b, c)| {
2156 (a.as_slice(), b.as_slice(), c.as_slice())
2157 }),
2158 rms_eps: eps,
2159 norm_style,
2160 pool: pool.as_deref(),
2161 };
2162 let mut attn = attention::qwen_attention_batch(
2163 &normed, b, wq, wk, wv, wo,
2164 &mut self.kv_cache.layers[li], &cfg);
2165 if let Some(w) = &lw.attn_out_norm {
2166 for bi in 0..b {
2167 inference::rms_norm_into(
2168 &attn[bi * hs..(bi + 1) * hs], w, eps, norm_style,
2169 &mut normed[bi * hs..(bi + 1) * hs]);
2170 }
2171 attn.copy_from_slice(&normed);
2172 }
2173 for (dst, &a) in h.iter_mut().zip(&attn) {
2174 *dst += a;
2175 }
2176 }
2177 AttnKind::Linear(w) => {
2178 for bi in 0..b {
2179 let normed = inference::rms_norm(
2180 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
2181 vmf_phase_forward(
2182 &normed, w,
2183 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2184 &mut self.kv_cache.layers[li].linear_state,
2185 pool.as_deref(),
2186 )
2187 .iter()
2188 .enumerate()
2189 .for_each(|(i, &a)| h[bi * hs + i] += a);
2190 }
2191 }
2192 }
2193
2194 let lw = &self.weights.layers[li];
2196 let mut post = vec![0.0f32; b * hs];
2197 for bi in 0..b {
2198 let r = inference::rms_norm(
2199 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2200 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2201 }
2202 let mut ffn = match &lw.ffn {
2203 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2204 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
2205 };
2206 if let Some(w) = &lw.ffn_out_norm {
2207 for bi in 0..b {
2208 inference::rms_norm_into(
2209 &ffn[bi * hs..(bi + 1) * hs], w, eps, norm_style,
2210 &mut post[bi * hs..(bi + 1) * hs]);
2211 }
2212 ffn.copy_from_slice(&post);
2213 }
2214 for (dst, &f) in h.iter_mut().zip(&ffn) {
2215 *dst += f;
2216 }
2217 if let Some(sc) = lw.layer_scale {
2218 for v in h.iter_mut() {
2219 *v *= sc;
2220 }
2221 }
2222 if std::env::var("CMF_TRACE_H").is_ok() {
2223 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2224 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2225 eprintln!("layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}", lw.layer_scale);
2226 }
2227 }
2228 crate::gpu::set_layer(-1); h
2230 }
2231
2232 fn embed_single(&self, id: u32) -> Vec<f32> {
2234 let mut out = vec![0.0f32; self.hidden_size];
2235 if (id as usize) < self.weights.embed_tokens.rows() {
2236 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2237 }
2238 if self.embed_multiplier != 1.0 {
2239 for v in out.iter_mut() {
2240 *v *= self.embed_multiplier;
2241 }
2242 }
2243 out
2244 }
2245
2246 #[cfg(target_os = "macos")]
2252 fn chunk_run_gpu(
2253 &mut self,
2254 li0: usize,
2255 h: &mut [f32],
2256 b: usize,
2257 pos0: usize,
2258 embed_ids: Option<&[u32]>,
2259 ) -> usize {
2260 if !crate::gpu::enabled_here()
2264 || std::env::var("CMF_GPU_CHUNK").map(|v| v == "0").unwrap_or(false)
2265 || b < 32
2266 || self.swa.is_some()
2267 || self.global_attn.is_some()
2268 || self.attn_v_norm
2269 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2270 {
2271 return li0;
2272 }
2273 let Some(model) = self.model.clone() else { return li0 };
2274 let inv_freq = self.inv_freq.clone();
2275 let (nh, nkv, hd, hs) = (self.num_heads, self.num_kv_heads, self.head_dim, self.hidden_size);
2276 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2278 let mut stored_at: Vec<usize> = Vec::new();
2279 for li in li0..self.num_layers {
2280 let lw = &self.weights.layers[li];
2281 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
2282 {
2283 break;
2284 }
2285 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate: false, bias } =
2286 &lw.attn
2287 else {
2288 break;
2289 };
2290 let FfnKind::Dense(d) = &lw.ffn else { break };
2291 if d.act != Act::Silu {
2292 break;
2293 }
2294 let parts = (
2295 wq.q8_row_parts(),
2296 wk.q8_row_parts(),
2297 wv.q8_row_parts(),
2298 wo.q8_row_parts(),
2299 d.gate_proj.q8_row_parts(),
2300 d.up_proj.q8_row_parts(),
2301 d.down_proj.q8_row_parts(),
2302 );
2303 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2304 else {
2305 break;
2306 };
2307 let layer = &self.kv_cache.layers[li];
2308 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2309 break;
2310 }
2311 stored_at.push(layer.head_len(0));
2312 layers.push(crate::gpu_metal::ChunkLayer {
2313 model: &model,
2314 kv_id: self.graph_kv_id,
2315 layer: li,
2316 wq: pq,
2317 wk: pk,
2318 wv: pv,
2319 wo: po,
2320 gate: pg,
2321 up: pu,
2322 down: pd,
2323 input_norm: &lw.input_norm,
2324 post_norm: &lw.post_norm,
2325 bias: bias
2326 .as_ref()
2327 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2328 q_norm: q_norm.as_deref(),
2329 k_norm: k_norm.as_deref(),
2330 inv_freq: &inv_freq,
2331 rd: self.rotary_dim,
2332 nh,
2333 nkv,
2334 hd,
2335 hs,
2336 inter: d.gate_proj.rows(),
2337 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2338 eps: self.rms_eps as f32,
2339 });
2340 }
2341 if layers.is_empty() {
2342 return li0;
2343 }
2344 let row = nkv * hd;
2345 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2346 .iter()
2347 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2348 .collect();
2349 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2350 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2351 let li = layers[i].layer;
2352 let layer = &self.kv_cache.layers[li];
2353 io.push(crate::gpu_metal::ChunkIo {
2354 cpu_stored: stored_at[i],
2355 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2356 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2357 out_k: ok,
2358 out_v: ov,
2359 imp: oi,
2360 });
2361 }
2362 let n_run = layers.len();
2363 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2364 let ep = embed_ids.and_then(|ids| {
2367 self.weights.embed_tokens.q8_row_parts().map(|(idx, rows, _c, rs)| {
2368 crate::gpu_metal::ChunkEmbed {
2369 idx,
2370 rows,
2371 row_scale: rs,
2372 ids,
2373 mult: self.embed_multiplier,
2374 }
2375 })
2376 });
2377 if embed_ids.is_some() && ep.is_none() {
2378 return li0;
2379 }
2380 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2381 return li0;
2382 }
2383 drop(io);
2384 drop(layers);
2385 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2388 let li = li0 + i;
2389 let layer = &mut self.kv_cache.layers[li];
2390 for bi in 0..b {
2391 layer.append(&ok[bi * row..(bi + 1) * row], &ov[bi * row..(bi + 1) * row], &[]);
2392 }
2393 layer.accumulate_imp(oi);
2394 }
2395 last
2396 }
2397
2398 fn layer_is_local(&self, li: usize) -> bool {
2401 match self.swa {
2402 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2403 None => false,
2404 }
2405 }
2406
2407 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2410 if self.layer_is_local(li) {
2411 if let Some(f) = &self.inv_freq_local {
2412 return f.clone();
2413 }
2414 } else if let Some(f) = &self.inv_freq_global {
2415 return f.clone();
2416 }
2417 self.inv_freq.clone()
2418 }
2419
2420 fn layer_window(&self, li: usize) -> Option<usize> {
2422 match self.swa {
2423 Some((w, _)) if self.layer_is_local(li) => Some(w),
2424 _ => None,
2425 }
2426 }
2427
2428 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2431 if !self.layer_is_local(li) {
2432 if let Some((ghd, gkv)) = self.global_attn {
2433 return (gkv, ghd, ghd);
2434 }
2435 }
2436 (self.num_kv_heads, self.head_dim, self.rotary_dim)
2437 }
2438
2439 fn forward_layers(
2441 &mut self,
2442 hidden: &[f32],
2443 position: usize,
2444 task_mask: Option<&TaskMask>,
2445 ) -> Vec<f32> {
2446 self.forward_layers_upto(hidden, position, task_mask, None)
2447 }
2448
2449 fn try_token_graph_wgpu(&self, hidden: &[f32], position: usize, logits_out: &mut Vec<f32>) -> Option<Vec<f32>> {
2453 if self.o1_active() {
2456 return None;
2457 }
2458 let nh = self.num_heads;
2459 let (nkv, hd, rd) = self.layer_geom(0);
2460 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2461 let mut layers = Vec::with_capacity(self.num_layers);
2462 let mut model = None;
2463 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
2464 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2465 if let Some((_, i, kind, rs)) = t.graph_weight() {
2466 return Some(crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[] });
2467 }
2468 t.as_f32().map(|d| crate::gpu::GraphW { idx: 0, kind: 4, row_scale: &[], data: d })
2470 }
2471 for li in 0..self.num_layers {
2472 let lw = &self.weights.layers[li];
2473 if dbg {
2474 let ak = match &lw.attn {
2475 AttnKind::Full { output_gate, bias, .. } => format!("Full gate={output_gate} bias={}", bias.is_some()),
2476 AttnKind::LinearGdn(_) => "LinearGdn".into(),
2477 AttnKind::Linear(_) => "Linear".into(),
2478 AttnKind::ShortConv(_) => "ShortConv".into(),
2479 };
2480 let fk = match &lw.ffn { FfnKind::Dense(_) => "Dense", FfnKind::Moe(_) => "Moe" };
2481 eprintln!("graph L{li}: attn={ak} ffn={fk}");
2482 }
2483 let (gate, up, down) = match &lw.ffn {
2484 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
2485 _ => return None,
2486 };
2487 let attn = match &lw.attn {
2488 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, bias } => {
2489 let (m, _, _, _) = wq.graph_weight()?;
2490 model = Some(m.clone());
2491 crate::gpu::GraphAttn::Full {
2492 wq: gw(wq)?,
2493 wk: gw(wk)?,
2494 wv: gw(wv)?,
2495 wo: gw(wo)?,
2496 q_norm: q_norm.as_deref(),
2497 k_norm: k_norm.as_deref(),
2498 bias: bias.as_ref().map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2499 output_gate: *output_gate,
2500 cpu_k: self.kv_cache.layers[li].k_heads(),
2501 cpu_v: self.kv_cache.layers[li].v_heads(),
2502 }
2503 }
2504 AttnKind::LinearGdn(w) => {
2505 let cfg = self.gdn_cfg?;
2506 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
2507 model = Some(m.clone());
2508 crate::gpu::GraphAttn::Gdn {
2509 qkv: gw(&w.in_proj_qkv)?,
2510 z: gw(&w.in_proj_z)?,
2511 a: gw(&w.in_proj_a)?,
2512 b: gw(&w.in_proj_b)?,
2513 out: gw(&w.out_proj)?,
2514 conv1d: &w.conv1d,
2515 a_log: &w.a_log,
2516 dt_bias: &w.dt_bias,
2517 norm: &w.norm,
2518 nv: cfg.num_v_heads,
2519 nk: cfg.num_k_heads,
2520 dk: cfg.key_head_dim,
2521 dv: cfg.value_head_dim,
2522 kk: cfg.conv_kernel,
2523 }
2524 }
2525 _ => return None,
2526 };
2527 layers.push(crate::gpu::GraphLayer {
2528 input_norm: &lw.input_norm,
2529 attn,
2530 post_norm: &lw.post_norm,
2531 gate: gw(gate)?,
2532 up: gw(up)?,
2533 down: gw(down)?,
2534 });
2535 }
2536 let model = model?;
2537 let lm_gw = if self.graph_want_logits
2543 && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
2544 {
2545 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
2546 (crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[] }, self.weights.lm_head.rows())
2547 })
2548 } else {
2549 None
2550 };
2551 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
2552 let mut h = hidden.to_vec();
2553 crate::gpu::forward_token_graph(
2554 &model,
2555 self.graph_kv_id,
2556 &layers,
2557 &self.inv_freq,
2558 &mut h,
2559 nh,
2560 nkv,
2561 hd,
2562 rd,
2563 self.hidden_size,
2564 self.intermediate_size,
2565 position,
2566 self.kv_cache.max_seq_len,
2567 gemma,
2568 self.rms_eps as f32,
2569 lm,
2570 &self.weights.final_norm,
2571 logits_out,
2572 )
2573 .then_some(h)
2574 }
2575
2576 fn try_batch_graph_wgpu(&self, hiddens: &mut [f32], positions: &[usize], k: usize) -> bool {
2581 if self.o1_active() {
2582 return false;
2583 }
2584 let nh = self.num_heads;
2585 let (nkv, hd, rd) = self.layer_geom(0);
2586 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2587 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
2588 if let Some((_, i, kind, rs)) = t.graph_weight() {
2589 return Some(crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[] });
2590 }
2591 t.as_f32().map(|d| crate::gpu::GraphW { idx: 0, kind: 4, row_scale: &[], data: d })
2592 }
2593 let built: Option<(Vec<crate::gpu::GraphLayer<'_>>, std::sync::Arc<cortiq_core::CmfModel>)> = (|| {
2594 let mut layers = Vec::with_capacity(self.num_layers);
2595 let mut model = None;
2596 for li in 0..self.num_layers {
2597 let lw = &self.weights.layers[li];
2598 let (gate, up, down) = match &lw.ffn {
2599 FfnKind::Dense(d) => (&d.gate_proj, &d.up_proj, &d.down_proj),
2600 _ => return None,
2601 };
2602 let attn = match &lw.attn {
2603 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, bias } => {
2604 let (m, _, _, _) = wq.graph_weight()?;
2605 model = Some(m.clone());
2606 crate::gpu::GraphAttn::Full {
2607 wq: gw(wq)?, wk: gw(wk)?, wv: gw(wv)?, wo: gw(wo)?,
2608 q_norm: q_norm.as_deref(), k_norm: k_norm.as_deref(),
2609 bias: bias.as_ref().map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2610 output_gate: *output_gate,
2611 cpu_k: self.kv_cache.layers[li].k_heads(),
2612 cpu_v: self.kv_cache.layers[li].v_heads(),
2613 }
2614 }
2615 AttnKind::LinearGdn(w) => {
2616 let cfg = self.gdn_cfg?;
2617 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
2618 model = Some(m.clone());
2619 crate::gpu::GraphAttn::Gdn {
2620 qkv: gw(&w.in_proj_qkv)?, z: gw(&w.in_proj_z)?, a: gw(&w.in_proj_a)?, b: gw(&w.in_proj_b)?, out: gw(&w.out_proj)?,
2621 conv1d: &w.conv1d, a_log: &w.a_log, dt_bias: &w.dt_bias, norm: &w.norm,
2622 nv: cfg.num_v_heads, nk: cfg.num_k_heads, dk: cfg.key_head_dim, dv: cfg.value_head_dim, kk: cfg.conv_kernel,
2623 }
2624 }
2625 _ => return None,
2626 };
2627 layers.push(crate::gpu::GraphLayer { input_norm: &lw.input_norm, attn, post_norm: &lw.post_norm, gate: gw(gate)?, up: gw(up)?, down: gw(down)? });
2628 }
2629 Some((layers, model?))
2630 })();
2631 let Some((layers, model)) = built else { return false };
2632 crate::gpu::forward_batch_graph(
2633 &model, self.graph_kv_id, &layers, &self.inv_freq, hiddens,
2634 nh, nkv, hd, rd, self.hidden_size, self.intermediate_size,
2635 positions, self.kv_cache.max_seq_len, gemma, self.rms_eps as f32, k,
2636 )
2637 }
2638
2639 fn forward_layers_upto(
2641 &mut self,
2642 hidden: &[f32],
2643 position: usize,
2644 task_mask: Option<&TaskMask>,
2645 upto: Option<usize>,
2646 ) -> Vec<f32> {
2647 let mut h = hidden.to_vec();
2648 let (nh, _nkv, _hd, hs, _rd, eps) = (
2651 self.num_heads,
2652 self.num_kv_heads,
2653 self.head_dim,
2654 self.hidden_size,
2655 self.rotary_dim,
2656 self.rms_eps,
2657 );
2658 let pool = self.pool.clone();
2659 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH").map(|v| v != "0").unwrap_or_else(|_| crate::pipeline::GLOBAL_USE_GPU.load(std::sync::atomic::Ordering::Relaxed));
2662 if graph_on && upto.is_none() && task_mask.is_none() {
2666 let mut lg = Vec::new();
2667 if let Some(hh) = self.try_token_graph_wgpu(hidden, position, &mut lg) {
2668 if !lg.is_empty() {
2669 lg.resize(self.vocab_size, 0.0);
2672 if let Some(c) = self.final_softcap {
2673 for l in lg.iter_mut() {
2674 *l = c * (*l / c).tanh();
2675 }
2676 }
2677 self.graph_logits = Some(lg);
2678 }
2679 return hh;
2680 }
2681 }
2682
2683 #[cfg(target_os = "macos")]
2684 let mut gpu_skip_until = 0usize;
2685 for li in 0..self.num_layers {
2686 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
2688 if li > u {
2689 break;
2690 }
2691 }
2692 if let Some(mask) = task_mask {
2693 if !mask.layer_alive(li) {
2694 continue; }
2696 }
2697 #[cfg(target_os = "macos")]
2701 {
2702 if li < gpu_skip_until {
2703 continue;
2704 }
2705 if task_mask.is_none() {
2706 let end = self.q1_graph_gpu(li, upto, position, &mut h);
2707 if end > li {
2708 gpu_skip_until = end;
2709 continue;
2710 }
2711 }
2712 }
2713
2714 let lw = &self.weights.layers[li];
2715 inference::rms_norm_into(&h, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
2718
2719 let attn_out = match &lw.attn {
2720 AttnKind::Linear(w) => {
2721 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2722 vmf_phase_forward(
2723 &self.ws.n1,
2724 w,
2725 &cfg,
2726 &mut self.kv_cache.layers[li].linear_state,
2727 self.pool.as_deref(),
2728 )
2729 }
2730 AttnKind::LinearGdn(w) => {
2731 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2732 gdn_forward(
2733 &self.ws.n1,
2734 w,
2735 &cfg,
2736 &mut self.kv_cache.layers[li].linear_state,
2737 self.pool.as_deref(),
2738 )
2739 }
2740 AttnKind::ShortConv(w) => {
2741 let cfg =
2742 self.short_conv_cfg.expect("short-conv layer without short_conv_cfg");
2743 short_conv_forward(
2744 &self.ws.n1,
2745 w,
2746 &cfg,
2747 &mut self.kv_cache.layers[li].linear_state,
2748 self.pool.as_deref(),
2749 )
2750 }
2751 AttnKind::Full {
2752 wq,
2753 wk,
2754 wv,
2755 wo,
2756 q_norm,
2757 k_norm,
2758 output_gate,
2759 bias,
2760 } if self.kv_cache.layers[li].o1_sealed() => {
2761 let inv_freq_l = self.layer_inv_freq(li);
2764 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2765 let cfg = QwenAttnCfg {
2766 num_heads: nh,
2767 num_kv_heads: nkv_l,
2768 head_dim: hd_l,
2769 hidden_size: hs,
2770 position,
2771 inv_freq: &inv_freq_l,
2772 rotary_dim: rd_l,
2773 scale: self.attn_scale,
2774 window: None,
2775 v_norm: self.attn_v_norm,
2776 q_norm: q_norm.as_deref(),
2777 k_norm: k_norm.as_deref(),
2778 output_gate: *output_gate,
2779 bias: bias
2780 .as_ref()
2781 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2782 rms_eps: eps,
2783 norm_style: self.norm_style,
2784 pool: pool.as_deref(),
2785 };
2786 attention::qwen_attention_nystrom(
2787 &self.ws.n1,
2788 wq,
2789 wk,
2790 wv,
2791 wo,
2792 &mut self.kv_cache.layers[li],
2793 &cfg,
2794 )
2795 }
2796 AttnKind::Full {
2797 wq,
2798 wk,
2799 wv,
2800 wo,
2801 q_norm,
2802 k_norm,
2803 output_gate,
2804 bias,
2805 } => 'attn: {
2806 if graph_on && !*output_gate && bias.is_none() && task_mask.is_none() {
2809 let inv_freq_l = self.layer_inv_freq(li);
2810 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2811 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
2812 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) =
2813 (wq.mapped_q1(), wk.mapped_q1(), wv.mapped_q1(), wo.mapped_q1())
2814 {
2815 let gm = gm.clone();
2816 let mut out = vec![0f32; hs];
2817 let cache = &self.kv_cache.layers[li];
2818 if crate::gpu::attn_dropin(
2819 &gm, self.graph_kv_id, li, &self.ws.n1, qi, ki, vi, oi,
2820 q_norm.as_deref(), k_norm.as_deref(), &inv_freq_l, nh, nkv_l,
2821 hd_l, rd_l, hs, position, self.kv_cache.max_seq_len, gemma, eps as f32,
2822 cache.k_heads(), cache.v_heads(), &mut out,
2823 ) {
2824 break 'attn out;
2825 }
2826 }
2827 }
2828 let masked = task_mask
2829 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
2830 .unwrap_or(false);
2831 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
2832 match (masked, f32_view) {
2833 (true, (Some(q), Some(k), Some(v), Some(o))) => {
2836 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
2837 attention::multi_head_attention(
2838 &self.ws.n1,
2839 q,
2840 k,
2841 v,
2842 o,
2843 &mut self.kv_cache.layers[li],
2844 self.num_heads,
2845 self.num_kv_heads,
2846 self.head_dim,
2847 self.hidden_size,
2848 position,
2849 &active_heads,
2850 &self.inv_freq,
2851 )
2852 }
2853 (masked, _) => {
2854 if masked {
2855 tracing::warn!(
2856 "layer {li}: head mask on quantized weights not \
2857 supported yet — executing dense"
2858 );
2859 }
2860 let inv_freq_l = self.layer_inv_freq(li);
2861 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2862 let cfg = QwenAttnCfg {
2863 num_heads: nh,
2864 num_kv_heads: nkv_l,
2865 head_dim: hd_l,
2866 hidden_size: hs,
2867 position,
2868 inv_freq: &inv_freq_l,
2869 rotary_dim: rd_l,
2870 scale: self.attn_scale,
2871 window: self.layer_window(li),
2872 v_norm: self.attn_v_norm,
2873 q_norm: q_norm.as_deref(),
2874 k_norm: k_norm.as_deref(),
2875 output_gate: *output_gate,
2876 bias: bias
2877 .as_ref()
2878 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2879 rms_eps: eps,
2880 norm_style: self.norm_style,
2881 pool: pool.as_deref(),
2882 };
2883 attention::qwen_attention(
2884 &self.ws.n1,
2885 wq,
2886 wk,
2887 wv,
2888 wo,
2889 &mut self.kv_cache.layers[li],
2890 &cfg,
2891 )
2892 }
2893 }
2894 }
2895 };
2896 let attn_out = match &self.weights.layers[li].attn_out_norm {
2899 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
2900 None => attn_out,
2901 };
2902 let lw = &self.weights.layers[li];
2903 inference::add_rmsnorm_fused_into(
2904 &mut h,
2905 &attn_out,
2906 &lw.post_norm,
2907 self.rms_eps,
2908 self.norm_style,
2909 &mut self.ws.p1,
2910 );
2911 let mut attn_out = attn_out;
2912 attention::recycle_buf(&mut attn_out);
2913 let post_normed = &self.ws.p1;
2914
2915 let ffn_masked = task_mask
2916 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
2917 .unwrap_or(false);
2918 let f32_ffn = match &lw.ffn {
2921 FfnKind::Dense(d) => {
2922 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
2923 }
2924 FfnKind::Moe(_) => (None, None, None),
2925 };
2926 let ffn_out = match (ffn_masked, f32_ffn) {
2927 (true, (Some(g), Some(u), Some(d))) => {
2928 let active = task_mask.unwrap().ffn_active_indices(li);
2929 inference::sparse_ffn_forward(
2930 &post_normed,
2931 g,
2932 u,
2933 d,
2934 self.hidden_size,
2935 self.intermediate_size,
2936 &active,
2937 self.pool.as_deref(),
2938 )
2939 }
2940 (true, _) => match &lw.ffn {
2944 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
2945 let active = task_mask.unwrap().ffn_active_indices(li);
2946 sparse_ffn_quant(
2947 d,
2948 &post_normed,
2949 &active,
2950 self.hidden_size,
2951 self.pool.as_deref(),
2952 )
2953 }
2954 FfnKind::Dense(d) => {
2959 let active = task_mask.unwrap().ffn_active_indices(li);
2960 let (gf, uf, df) = dequant_dense_f32(d);
2961 inference::sparse_ffn_forward(
2962 &post_normed,
2963 &gf,
2964 &uf,
2965 &df,
2966 self.hidden_size,
2967 self.intermediate_size,
2968 &active,
2969 self.pool.as_deref(),
2970 )
2971 }
2972 FfnKind::Moe(_) => {
2973 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
2976 }
2977 },
2978 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
2979 };
2980 let ffn_out = match &self.weights.layers[li].ffn_out_norm {
2981 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
2982 None => ffn_out,
2983 };
2984 for (i, &f) in ffn_out.iter().enumerate() {
2985 h[i] += f;
2986 }
2987 let mut ffn_out = ffn_out;
2988 attention::recycle_buf(&mut ffn_out);
2989
2990 if let Some(sc) = self.weights.layers[li].layer_scale {
2992 for v in h.iter_mut() {
2993 *v *= sc;
2994 }
2995 }
2996
2997 if self.dyn_phi_layer == Some(li) {
3001 self.update_dyn_phi(&h);
3002 }
3003 }
3004 crate::gpu::set_layer(-1); h
3007 }
3008
3009 fn update_dyn_phi(&mut self, h: &[f32]) {
3012 const A: f32 = 0.2;
3013 if self.dyn_phi_ema.len() != h.len() {
3014 self.dyn_phi_ema = vec![0.0; h.len()];
3015 self.dyn_phi_seen = 0;
3016 }
3017 if self.dyn_phi_seen == 0 {
3018 self.dyn_phi_ema.copy_from_slice(h);
3019 } else {
3020 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
3021 *e = (1.0 - A) * *e + A * v;
3022 }
3023 }
3024 self.dyn_phi_seen += 1;
3025 }
3026
3027 pub fn dyn_phi(&self) -> &[f32] {
3029 &self.dyn_phi_ema
3030 }
3031
3032 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
3034 self.dyn_phi_layer = layer;
3035 self.dyn_phi_ema.clear();
3036 self.dyn_phi_seen = 0;
3037 }
3038
3039 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
3041 let Some(model) = &self.model else { return Vec::new() };
3042 model
3043 .header
3044 .skills
3045 .iter()
3046 .enumerate()
3047 .filter_map(|(i, sk)| {
3048 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
3049 let sel = sk.selection.as_ref()?;
3050 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
3051 })
3052 .collect()
3053 }
3054
3055 pub fn active_skill(&self) -> Option<usize> {
3057 self.dyn_active
3058 }
3059
3060 pub fn enable_dynamic_routing(&mut self) -> usize {
3065 use crate::swarm::{DynRouter, RoutableSkill};
3066 let Some(model) = self.model.clone() else { return 0 };
3067 if self.dyn_blend_loaded {
3070 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
3071 return 0;
3072 }
3073 if let Some(a) = self.dyn_active {
3077 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
3078 tracing::warn!(
3079 "loaded skill is not FFN-eligible — dynamic routing unavailable"
3080 );
3081 return 0;
3082 }
3083 }
3084 let hidden = self.hidden_size;
3085 let mut skills = Vec::new();
3086 for (idx, id, _phi) in self.dynamic_skills() {
3087 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
3088 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
3089 skills.push(rs);
3090 }
3091 }
3092 }
3093 if skills.is_empty() {
3094 return 0;
3095 }
3096 let phi = skills[0].phi_layer;
3098 if skills.iter().any(|s| s.phi_layer != phi) {
3099 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
3100 }
3101 let n = skills.len();
3102 self.set_dyn_phi_layer(Some(phi));
3103 self.dyn_router = Some(DynRouter::new(skills));
3104 n
3105 }
3106
3107 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
3109 self.dyn_router
3110 .as_ref()
3111 .map(|r| r.switches.clone())
3112 .unwrap_or_default()
3113 }
3114
3115 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
3118 let rows = self.weights.lm_head.rows();
3119 let mut logits = attention::take_buf(rows.min(self.vocab_size));
3120 self.weights
3121 .lm_head
3122 .matvec(hidden, &mut logits, self.pool.as_deref());
3123 logits.resize(self.vocab_size, 0.0);
3124 if let Some(c) = self.final_softcap {
3125 for l in logits.iter_mut() {
3126 *l = c * (*l / c).tanh();
3127 }
3128 }
3129 logits
3130 }
3131
3132 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
3137 self.kv_cache.clear();
3138 let mut hidden = vec![0.0f32; self.hidden_size];
3139 for (pos, &id) in ids.iter().enumerate() {
3140 let emb = self.embed_single(id);
3141 hidden = self.forward_layers(&emb, pos, task_mask);
3142 }
3143 inference::rms_norm_into(
3144 &hidden,
3145 &self.weights.final_norm,
3146 self.rms_eps,
3147 self.norm_style,
3148 &mut self.ws.n1,
3149 );
3150 self.lm_head_forward(&self.ws.n1)
3151 }
3152}
3153
3154pub fn create_test_pipeline(
3156 hidden_size: usize,
3157 intermediate_size: usize,
3158 num_heads: usize,
3159 num_kv_heads: usize,
3160 head_dim: usize,
3161 num_layers: usize,
3162 vocab_size: usize,
3163) -> Pipeline {
3164 let synth = |n: usize, salt: usize| -> Vec<f32> {
3167 (0..n)
3168 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
3169 .collect()
3170 };
3171 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3172 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3173 };
3174 let layer_weights: Vec<LayerWeights> = (0..num_layers)
3175 .map(|li| LayerWeights {
3176 input_norm: vec![1.0; hidden_size],
3177 post_norm: vec![1.0; hidden_size],
3178 attn_out_norm: None,
3179 ffn_out_norm: None,
3180 layer_scale: None,
3181 ffn: FfnKind::Dense(DenseFfn {
3182 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
3183 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
3184 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
3185 act: Act::Silu,
3186 }),
3187 attn: AttnKind::Full {
3188 bias: None,
3189 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
3190 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
3191 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
3192 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
3193 q_norm: None,
3194 k_norm: None,
3195 output_gate: false,
3196 },
3197 })
3198 .collect();
3199
3200 Pipeline::new(
3201 Tokenizer::byte_level(),
3202 PipelineWeights {
3203 embed_tokens: qt(vocab_size, hidden_size, 100),
3204 layers: layer_weights,
3205 lm_head: qt(vocab_size, hidden_size, 200),
3206 final_norm: vec![1.0; hidden_size],
3207 },
3208 hidden_size,
3209 intermediate_size,
3210 num_heads,
3211 num_kv_heads,
3212 head_dim,
3213 num_layers,
3214 vocab_size,
3215 1e-6,
3216 10_000.0,
3217 NormStyle::Qwen,
3218 4096,
3219 SamplerConfig {
3220 seed: Some(42),
3221 ..Default::default()
3222 },
3223 )
3224}
3225
3226fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
3229 let inter = d.gate_proj.rows();
3230 let hidden = d.down_proj.rows();
3231 let mut g = vec![0.0f32; b * inter];
3232 d.gate_proj.matmat(xs, b, &mut g, pool);
3233 let mut u = vec![0.0f32; b * inter];
3234 d.up_proj.matmat(xs, b, &mut u, pool);
3235 for i in 0..b * inter {
3236 g[i] = d.act.apply(g[i]) * u[i];
3237 }
3238 let mut out = vec![0.0f32; b * hidden];
3239 d.down_proj.matmat(&g, b, &mut out, pool);
3240 out
3241}
3242
3243fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
3247 let ne = m.experts.len();
3248 let mut logits = vec![0.0f32; b * ne];
3249 m.router.matmat(xs, b, &mut logits, pool);
3250
3251 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
3254 {
3255 let mut st = m.stats.borrow_mut();
3256 if st.len() < ne {
3257 st.resize(ne, 0);
3258 }
3259 for bi in 0..b {
3260 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m);
3261 for &e in &idx {
3262 st[e] += 1;
3263 assign[e].push((bi, p[e] / wsum));
3264 }
3265 }
3266 }
3267
3268 let mut out = vec![0.0f32; b * hidden];
3269 let cols = m.experts[0].gate_proj.cols();
3270 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
3271 let sb = list.len();
3272 let mut sub = vec![0.0f32; sb * cols];
3273 for (k, &(bi, _)) in list.iter().enumerate() {
3274 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
3275 }
3276 let eo = dense_ffn_batch(d, &sub, sb, pool);
3277 for (k, &(bi, w)) in list.iter().enumerate() {
3278 for i in 0..hidden {
3279 out[bi * hidden + i] += w * eo[k * hidden + i];
3280 }
3281 }
3282 };
3283 for e in 0..ne {
3284 if !assign[e].is_empty() {
3285 run_expert(&m.experts[e], &assign[e]);
3286 }
3287 }
3288 if let Some((se, gate)) = &m.shared {
3289 let mut gl = vec![0.0f32; b];
3290 gate.matmat(xs, b, &mut gl, pool);
3291 let all: Vec<(usize, f32)> = (0..b)
3292 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
3293 .collect();
3294 run_expert(se, &all);
3295 }
3296 out
3297}
3298
3299thread_local! {
3300 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
3304 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
3305}
3306
3307fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3309 if crate::gpu::enabled_here()
3320 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
3321 {
3322 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
3323 crate::gpu::ProbeArm::Gpu
3324 } else {
3325 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
3326 };
3327 match arm {
3328 crate::gpu::ProbeArm::Gpu => {
3329 let t0 = std::time::Instant::now();
3330 if let Some(out) = dense_ffn_gpu(d, x, pool) {
3331 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3332 return out;
3333 }
3334 }
3335 crate::gpu::ProbeArm::CpuTimed => {
3336 let t0 = std::time::Instant::now();
3337 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3338 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3339 return out;
3340 }
3341 crate::gpu::ProbeArm::Cpu => {
3342 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3343 }
3344 }
3345 }
3346 dense_ffn_cpu(d, x, pool)
3347}
3348
3349fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3351 let inter = d.gate_proj.rows();
3352 FFN_SCRATCH.with(|s| {
3353 let mut s = s.borrow_mut();
3354 let [g, u, ..] = &mut *s;
3355 g.resize(inter, 0.0);
3356 u.resize(inter, 0.0);
3357 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
3359 for i in 0..inter {
3360 g[i] = d.act.apply(g[i]) * u[i];
3361 }
3362 FFN_PROBE.with(|pr| {
3365 if let Some(acc) = pr.borrow_mut().as_mut() {
3366 let li = crate::gpu::cur_layer();
3367 if li >= 0 {
3368 if let Some(row) = acc.get_mut(li as usize) {
3369 for (a, &v) in row.iter_mut().zip(g.iter()) {
3370 *a += (v as f64).abs();
3371 }
3372 }
3373 }
3374 }
3375 });
3376 let mut out = attention::take_buf(d.down_proj.rows());
3377 d.down_proj.matvec(g, &mut out, pool);
3378 out
3379 })
3380}
3381
3382thread_local! {
3383 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
3386 const { std::cell::RefCell::new(None) };
3387}
3388
3389fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
3395 if d.act != Act::Silu {
3397 return None;
3398 }
3399 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
3402 return None;
3403 }
3404 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
3405 let mut model_ref = None;
3406 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
3407 let model = model_ref?;
3408 let hidden = jobs[0].down.1;
3409 let mut out = attention::take_buf(hidden);
3410 if crate::gpu::moe_block(&model, &jobs, &mut out) {
3411 Some(out)
3412 } else {
3413 let mut out = out;
3414 attention::recycle_buf(&mut out);
3415 None
3416 }
3417}
3418
3419#[allow(clippy::type_complexity)]
3424#[allow(clippy::type_complexity)]
3425fn moe_parts(
3426 t: &QTensor,
3427) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32], bool)> {
3428 match t {
3429 QTensor::Mapped {
3430 model,
3431 idx,
3432 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
3433 rows,
3434 cols,
3435 row_scale,
3436 col_field,
3437 ..
3438 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
3439 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
3440 }
3441 QTensor::Mapped {
3443 model,
3444 idx,
3445 dtype: cortiq_core::TensorDtype::Q1,
3446 rows,
3447 cols,
3448 ..
3449 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
3450 _ => None,
3451 }
3452}
3453
3454fn moe_push_job<'a>(
3456 d: &'a DenseFfn,
3457 x: &[f32],
3458 w: f32,
3459 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
3460 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
3461) -> Option<()> {
3462 use crate::qtensor::prescale;
3463 if d.act != Act::Silu {
3464 return None; }
3466 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
3467 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
3468 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
3469 if gq1 != uq1 || uq1 != dq1 {
3470 return None; }
3472 model_ref.get_or_insert_with(|| gm.clone());
3473 let gdt = if gcf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
3474 let udt = if ucf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
3475 jobs.push(crate::gpu::MoeJob {
3476 gate: (gi, gr, gc, grs),
3477 up: (ui, ur, uc, urs),
3478 down: (di, dr, dc, drs),
3479 xs_gate: prescale(x, gcf, gdt).into_owned(),
3480 xs_up: prescale(x, ucf, udt).into_owned(),
3481 down_col: dcf,
3482 w,
3483 q1: gq1,
3484 });
3485 Some(())
3486}
3487
3488fn sparse_ffn_quant(
3495 d: &DenseFfn,
3496 x: &[f32],
3497 active: &[u16],
3498 hidden: usize,
3499 pool: Option<&Pool>,
3500) -> Vec<f32> {
3501 let n = active.len();
3502 let inter = d.gate_proj.rows();
3503 let mut act = vec![0.0f32; n];
3504 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
3507 let compute = |ai: usize| -> f32 {
3508 let idx = active[ai] as usize;
3509 if idx >= inter {
3510 return 0.0; }
3512 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
3513 let gate = d.gate_proj.row_dot(idx, x, &mut s);
3514 let up = d.up_proj.row_dot(idx, x, &mut s);
3515 d.act.apply(gate) * up
3516 };
3517 match pool {
3518 Some(p) if n >= 256 => {
3519 let ptr = SendMut(act.as_mut_ptr());
3520 p.run(&|widx, nw| {
3521 let chunk = n.div_ceil(nw);
3522 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
3523 for ai in s..e {
3524 unsafe { *ptr.at(ai) = compute(ai) };
3525 }
3526 });
3527 }
3528 _ => {
3529 for (ai, a) in act.iter_mut().enumerate() {
3530 *a = compute(ai);
3531 }
3532 }
3533 }
3534 let mut out = vec![0.0f32; hidden];
3536 for (ai, &idx) in active.iter().enumerate() {
3537 let w = act[ai];
3538 if w.abs() >= 1e-12 && (idx as usize) < inter {
3539 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
3540 }
3541 }
3542 out
3543}
3544
3545#[doc(hidden)]
3547pub fn sparse_ffn_quant_for_test(
3548 d: &DenseFfn,
3549 x: &[f32],
3550 active: &[u16],
3551 hidden: usize,
3552) -> Vec<f32> {
3553 sparse_ffn_quant(d, x, active, hidden, None)
3554}
3555
3556fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
3560 let deq = |t: &QTensor| -> Vec<f32> {
3561 let (rows, cols) = (t.rows(), t.cols());
3562 let mut out = vec![0.0f32; rows * cols];
3563 for r in 0..rows {
3564 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
3565 }
3566 out
3567 };
3568 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
3569}
3570
3571struct SendMut(*mut f32);
3573unsafe impl Send for SendMut {}
3574unsafe impl Sync for SendMut {}
3575impl SendMut {
3576 #[inline]
3577 #[allow(clippy::mut_from_ref)]
3580 unsafe fn at(&self, i: usize) -> &mut f32 {
3581 unsafe { &mut *self.0.add(i) }
3582 }
3583}
3584
3585fn moe_route(logits: &[f32], m: &MoeFfn) -> (Vec<usize>, Vec<f32>, f32) {
3595 let ne = logits.len();
3596 let p: Vec<f32> = if m.router_sigmoid {
3597 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
3598 } else {
3599 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
3600 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
3601 let s: f32 = e.iter().sum();
3602 for v in &mut e {
3603 *v /= s;
3604 }
3605 e
3606 };
3607 let mut idx: Vec<usize> = (0..ne).collect();
3608 match &m.expert_bias {
3610 Some(b) => idx.sort_unstable_by(|&x, &y| {
3611 (p[y] + b[y]).partial_cmp(&(p[x] + b[x])).unwrap().then(x.cmp(&y))
3612 }),
3613 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
3614 }
3615 idx.truncate(m.top_k);
3616 let wsum: f32 = if m.norm_topk_prob {
3617 let s: f32 = idx.iter().map(|&e| p[e]).sum();
3618 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
3621 } else {
3622 1.0 / m.routed_scaling
3623 };
3624 (idx, p, wsum)
3625}
3626
3627fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3630 let ne = m.experts.len();
3631 let mut logits = vec![0.0f32; ne];
3632 m.router.matvec(x, &mut logits, pool);
3633 let (idx, p, wsum) = moe_route(&logits, m);
3634 {
3635 let mut st = m.stats.borrow_mut();
3636 if st.len() < ne {
3637 st.resize(ne, 0);
3638 }
3639 for &e in &idx {
3640 st[e] += 1;
3641 }
3642 }
3643 if crate::gpu::enabled_here() {
3648 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
3649 crate::gpu::ProbeArm::Gpu => {
3650 let t0 = std::time::Instant::now();
3651 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
3652 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3653 return out;
3654 }
3655 }
3656 crate::gpu::ProbeArm::CpuTimed => {
3657 let t0 = std::time::Instant::now();
3658 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
3659 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3660 return out;
3661 }
3662 crate::gpu::ProbeArm::Cpu => {
3663 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
3664 }
3665 }
3666 }
3667 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
3668}
3669
3670fn moe_ffn_cpu(
3672 m: &MoeFfn,
3673 x: &[f32],
3674 idx: &[usize],
3675 p: &[f32],
3676 wsum: f32,
3677 pool: Option<&Pool>,
3678) -> Vec<f32> {
3679 let mut out = attention::take_buf(x.len());
3680 for &e in idx {
3681 let mut eo = dense_ffn(&m.experts[e], x, pool);
3682 let w = p[e] / wsum;
3683 for i in 0..out.len() {
3684 out[i] += w * eo[i];
3685 }
3686 attention::recycle_buf(&mut eo);
3687 }
3688 if let Some((se, gate)) = &m.shared {
3689 let mut so = dense_ffn(se, x, pool);
3690 let mut gl = vec![0.0f32; 1];
3691 gate.matvec(x, &mut gl, pool);
3692 let g = 1.0 / (1.0 + (-gl[0]).exp());
3693 for i in 0..out.len() {
3694 out[i] += g * so[i];
3695 }
3696 attention::recycle_buf(&mut so);
3697 }
3698 out
3699}
3700
3701fn moe_ffn_gpu(
3704 m: &MoeFfn,
3705 x: &[f32],
3706 idx: &[usize],
3707 p: &[f32],
3708 wsum: f32,
3709 pool: Option<&Pool>,
3710) -> Option<Vec<f32>> {
3711 use crate::gpu::MoeJob;
3712
3713 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
3714 let mut model_ref = None;
3715 for &e in idx {
3716 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
3717 }
3718 if let Some((se, gate)) = &m.shared {
3719 let mut gl = vec![0.0f32; 1];
3720 gate.matvec(x, &mut gl, pool);
3721 let g = 1.0 / (1.0 + (-gl[0]).exp());
3722 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
3723 }
3724 let model = model_ref?;
3725 let hidden = jobs[0].down.1;
3726 let mut out = vec![0.0f32; hidden];
3727 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
3728}
3729
3730fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3732 match ffn {
3733 FfnKind::Dense(d) => dense_ffn(d, x, pool),
3734 FfnKind::Moe(m) => moe_ffn(m, x, pool),
3735 }
3736}
3737
3738fn ffn_forward_pair(
3742 ffn: &FfnKind,
3743 x1: &[f32],
3744 x2: &[f32],
3745 pool: Option<&Pool>,
3746) -> (Vec<f32>, Vec<f32>) {
3747 let d = match ffn {
3748 FfnKind::Dense(d) => d,
3749 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
3750 };
3751 let inter = d.gate_proj.rows();
3752 FFN_SCRATCH.with(|s| {
3753 let mut s = s.borrow_mut();
3754 let [g1, g2, u1, u2] = &mut *s;
3755 g1.resize(inter, 0.0);
3756 g2.resize(inter, 0.0);
3757 u1.resize(inter, 0.0);
3758 u2.resize(inter, 0.0);
3759 QTensor::matvec2_many(
3762 [&d.gate_proj, &d.up_proj],
3763 x1,
3764 x2,
3765 [g1.as_mut_slice(), u1.as_mut_slice()],
3766 [g2.as_mut_slice(), u2.as_mut_slice()],
3767 pool,
3768 );
3769 for i in 0..inter {
3770 g1[i] = d.act.apply(g1[i]) * u1[i];
3771 g2[i] = d.act.apply(g2[i]) * u2[i];
3772 }
3773 let mut o1 = attention::take_buf(d.down_proj.rows());
3774 let mut o2 = attention::take_buf(d.down_proj.rows());
3775 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
3776 (o1, o2)
3777 })
3778}
3779
3780#[cfg(test)]
3781mod tests {
3782 use super::*;
3783
3784 #[test]
3790 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
3791 let (hidden, inter) = (16usize, 40usize);
3792 let synth = |n: usize, salt: usize| -> Vec<f32> {
3793 (0..n)
3794 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
3795 .collect()
3796 };
3797 let d = DenseFfn {
3798 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
3799 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
3800 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
3801 act: Act::Silu,
3802 };
3803 let x = synth(hidden, 9);
3804 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
3806
3807 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
3808
3809 let mut g = vec![0.0f32; inter];
3811 d.gate_proj.matvec(&x, &mut g, None);
3812 let mut u = vec![0.0f32; inter];
3813 d.up_proj.matvec(&x, &mut u, None);
3814 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
3815 for i in 0..inter {
3816 g[i] = if act_set.contains(&(i as u16)) {
3817 inference::silu(g[i]) * u[i]
3818 } else {
3819 0.0
3820 };
3821 }
3822 let mut reference = vec![0.0f32; hidden];
3823 d.down_proj.matvec(&g, &mut reference, None);
3824
3825 let max_d = sparse
3826 .iter()
3827 .zip(&reference)
3828 .map(|(a, b)| (a - b).abs())
3829 .fold(0.0f32, f32::max);
3830 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
3831 }
3832
3833 fn attach_test_mtp(p: &mut Pipeline) {
3835 let (h, inter, heads, kv, hd) = (
3836 p.hidden_size,
3837 p.intermediate_size,
3838 p.num_heads,
3839 p.num_kv_heads,
3840 p.head_dim,
3841 );
3842 let synth = |n: usize, salt: usize| -> Vec<f32> {
3843 (0..n)
3844 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
3845 .collect()
3846 };
3847 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3848 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3849 };
3850 p.mtp = Some(MtpModule {
3851 enorm: vec![1.0; h],
3852 hnorm: vec![1.0; h],
3853 eh_proj: qt(h, 2 * h, 301),
3854 layer: LayerWeights {
3855 input_norm: vec![1.0; h],
3856 post_norm: vec![1.0; h],
3857 attn_out_norm: None,
3858 ffn_out_norm: None,
3859 layer_scale: None,
3860 ffn: FfnKind::Dense(DenseFfn {
3861 gate_proj: qt(inter, h, 315),
3862 up_proj: qt(inter, h, 316),
3863 down_proj: qt(h, inter, 317),
3864 act: Act::Silu,
3865 }),
3866 attn: AttnKind::Full {
3867 bias: None,
3868 wq: qt(heads * hd, h, 311),
3869 wk: qt(kv * hd, h, 312),
3870 wv: qt(kv * hd, h, 313),
3871 wo: qt(h, heads * hd, 314),
3872 q_norm: None,
3873 k_norm: None,
3874 output_gate: false,
3875 },
3876 },
3877 final_norm: vec![1.0; h],
3878 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
3879 });
3880 }
3881
3882 #[test]
3883 fn speculative_equals_vanilla_greedy() {
3884 let run = |spec: bool| {
3885 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3886 p.sampler_config.temperature = 0.0;
3887 attach_test_mtp(&mut p);
3888 p.speculative = spec;
3889 let r = p.generate("abcdef", 12, None, None).unwrap();
3890 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
3891 };
3892 let (vanilla, d0, _) = run(false);
3893 let (spec, d1, a1) = run(true);
3894 assert_eq!(d0, 0, "vanilla path must not draft");
3895 assert!(d1 > 0, "speculative path must draft");
3896 assert_eq!(
3897 vanilla, spec,
3898 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
3899 );
3900 }
3901
3902 #[test]
3903 fn speculative_accepts_constant_oracle() {
3904 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3905 p.sampler_config.temperature = 0.0;
3906 p.sampler_config.repetition_penalty = 1.0;
3907 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
3910 attach_test_mtp(&mut p);
3911 p.speculative = true;
3912 let r = p.generate("abcd", 10, None, None).unwrap();
3913 assert!(r.mtp_drafted > 0);
3914 assert_eq!(
3915 r.mtp_accepted, r.mtp_drafted,
3916 "constant logits → every draft accepted"
3917 );
3918 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
3921 }
3922
3923 #[test]
3924 fn empty_prompt_is_an_error_not_a_panic() {
3925 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3926 let r = p.generate("", 4, None, None);
3927 assert!(r.is_err(), "empty prompt must be a clean error");
3928 }
3929
3930 #[test]
3931 fn every_token_enters_kv_exactly_once() {
3932 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3933 p.sampler_config.temperature = 0.0;
3935 let r = p.generate("abc", 2, None, None).unwrap();
3936 assert_eq!(r.prompt_tokens, 3);
3937 assert_eq!(
3941 p.kv_cache.seq_len(),
3942 3 + r.tokens_generated - 1,
3943 "each token must be cached exactly once (v1 cached the last prompt token twice)"
3944 );
3945 }
3946
3947 #[test]
3948 fn generation_is_reproducible_with_seed() {
3949 let run = || {
3950 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3951 p.generate("hello", 8, None, None).unwrap().token_ids
3952 };
3953 assert_eq!(run(), run());
3954 }
3955
3956 #[test]
3957 fn eviction_bounds_the_cache() {
3958 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3959 p.kv_cache.max_seq_len = 6;
3960 p.sampler_config.temperature = 0.0;
3961 let _ = p.generate("abcd", 12, None, None).unwrap();
3962 assert!(
3963 p.kv_cache.seq_len() <= 6 + 1,
3964 "cache must stay bounded by max_seq_len (got {})",
3965 p.kv_cache.seq_len()
3966 );
3967 }
3968
3969 #[test]
3970 fn confidence_matches_tokens_and_is_a_probability() {
3971 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3972 p.sampler_config.temperature = 0.0;
3973 p.sampler_config.repetition_penalty = 1.0;
3974 let r = p.generate("abcd", 10, None, None).unwrap();
3975 assert_eq!(
3976 r.token_confidence.len(),
3977 r.token_ids.len(),
3978 "one confidence per emitted token"
3979 );
3980 for &c in &r.token_confidence {
3981 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
3982 }
3983 let logits = [1.0f32, 3.0, 0.5, 3.0];
3985 let p0 = top1_prob_t(&logits, 1, 1.0);
3986 let p1 = top1_prob_t(&logits, 3, 1.0);
3987 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
3988 assert!(p0 > 0.0 && p0 < 1.0);
3989 let sharp = top1_prob_t(&logits, 1, 1.0);
3991 let soft = top1_prob_t(&logits, 1, 2.0);
3992 assert!(soft < sharp, "higher temperature lowers peak confidence");
3993 }
3994
3995 #[test]
3996 fn trace_is_opt_in_and_parallels_the_output() {
3997 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3999 p.sampler_config.temperature = 0.0;
4000 p.sampler_config.repetition_penalty = 1.0;
4001 let r = p.generate("abcd", 10, None, None).unwrap();
4002 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
4003
4004 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4006 p.sampler_config.temperature = 0.0;
4007 p.sampler_config.repetition_penalty = 1.0;
4008 p.set_trace(true);
4009 let r = p.generate("abcd", 10, None, None).unwrap();
4010 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
4011 for (i, tr) in r.traces.iter().enumerate() {
4012 assert_eq!(tr.t, i, "trace index is sequential");
4013 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
4014 assert_eq!(
4015 tr.confidence, r.token_confidence[i],
4016 "trace confidence matches the confidence channel"
4017 );
4018 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
4020 }
4021 }
4022
4023 #[test]
4024 fn explain_prefill_logits_match_greedy_first_token() {
4025 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
4029 p.sampler_config.temperature = 0.0;
4030 p.sampler_config.repetition_penalty = 1.0;
4031 let ids = p.tokenizer.encode("abcd");
4032 let logits = p.prefill_next_logits(&ids, None);
4033 let argmax = logits
4034 .iter()
4035 .enumerate()
4036 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
4037 .unwrap()
4038 .0 as u32;
4039 let r = p.generate("abcd", 1, None, None).unwrap();
4040 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
4041 }
4042}