1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13 gdn_forward, gdn_pair, vmf_phase_forward, vmf_phase_pair, GdnCfg, GdnWeights, VmfPhaseCfg,
14 VmfPhaseWeights,
15};
16use crate::pool::Pool;
17use crate::qtensor::QTensor;
18use crate::sampler::{self, SamplerConfig, SplitMix64};
19use crate::tokenizer::Tokenizer;
20use cortiq_core::mask::TaskMask;
21use cortiq_core::types::NormStyle;
22
23struct ForwardScratch {
27 n1: Vec<f32>,
28 n2: Vec<f32>,
29 p1: Vec<f32>,
30 p2: Vec<f32>,
31}
32
33impl ForwardScratch {
34 fn new(hidden: usize) -> Self {
35 Self {
36 n1: vec![0.0; hidden],
37 n2: vec![0.0; hidden],
38 p1: vec![0.0; hidden],
39 p2: vec![0.0; hidden],
40 }
41 }
42}
43
44pub struct Pipeline {
46 pub tokenizer: std::sync::Arc<Tokenizer>,
49 pub kv_cache: KvCache,
50 pub sampler_config: SamplerConfig,
51 pub weights: PipelineWeights,
52 pub hidden_size: usize,
53 pub intermediate_size: usize,
54 pub num_heads: usize,
55 pub num_kv_heads: usize,
56 pub head_dim: usize,
57 pub num_layers: usize,
58 pub vocab_size: usize,
59 pub rms_eps: f64,
60 pub rope_base: f32,
61 pub norm_style: NormStyle,
62 pub rotary_dim: usize,
64 pub vmf_cfg: Option<VmfPhaseCfg>,
66 pub gdn_cfg: Option<GdnCfg>,
68 pub mtp: Option<MtpModule>,
70 pub speculative: bool,
72 rng: SplitMix64,
73 inv_freq: std::sync::Arc<Vec<f32>>,
77 ws: ForwardScratch,
81 pool: Option<std::sync::Arc<Pool>>,
83 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
87 pub(crate) dyn_force_f32: bool,
89 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
94 pub(crate) dyn_active: Option<usize>,
100 pub(crate) dyn_blend_loaded: bool,
104 pub(crate) dyn_phi_layer: Option<usize>,
107 dyn_phi_ema: Vec<f32>,
109 dyn_phi_seen: usize,
110 pub dyn_router: Option<crate::swarm::DynRouter>,
113 o1_cfg: Option<crate::nystrom::O1Cfg>,
116 o1_flags: Vec<bool>,
118 trace: bool,
121 calib_temp: f32,
124 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
126 graph_kv_id: u64,
127 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
130 graph_want_logits: bool,
131 graph_logits: Option<Vec<f32>>,
134 pub embed_multiplier: f32,
136 pub attn_scale: f32,
139 pub swa: Option<(usize, usize)>,
142 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
145 pub global_attn: Option<(usize, usize)>,
148 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
151 pub attn_v_norm: bool,
153 pub final_softcap: Option<f32>,
155 confidence_on: bool,
159}
160
161#[cfg(target_os = "macos")]
162impl Drop for Pipeline {
163 fn drop(&mut self) {
164 crate::gpu::kv_mirror_drop(self.graph_kv_id);
165 }
166}
167
168pub struct PipelineWeights {
173 pub embed_tokens: QTensor,
175 pub layers: Vec<LayerWeights>,
177 pub lm_head: QTensor,
179 pub final_norm: Vec<f32>,
181}
182
183pub struct LayerWeights {
185 pub input_norm: Vec<f32>,
186 pub post_norm: Vec<f32>,
189 pub attn_out_norm: Option<Vec<f32>>,
192 pub layer_scale: Option<f32>,
194 pub ffn_out_norm: Option<Vec<f32>>,
197 pub ffn: FfnKind,
198 pub attn: AttnKind,
199}
200
201#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
204pub enum Act {
205 #[default]
206 Silu,
207 GeluTanh,
208}
209
210impl Act {
211 pub fn from_arch(name: &str) -> Self {
212 if name == "gelu_tanh" {
213 Self::GeluTanh
214 } else {
215 Self::Silu
216 }
217 }
218
219 #[inline]
220 pub fn apply(self, x: f32) -> f32 {
221 match self {
222 Self::Silu => inference::silu(x),
223 Self::GeluTanh => inference::gelu_tanh(x),
224 }
225 }
226}
227
228pub struct DenseFfn {
230 pub gate_proj: QTensor,
231 pub up_proj: QTensor,
232 pub down_proj: QTensor,
233 pub act: Act,
235}
236
237pub enum FfnKind {
240 Dense(DenseFfn),
241 Moe(MoeFfn),
245}
246
247pub struct MoeFfn {
248 pub router: QTensor,
250 pub experts: Vec<DenseFfn>,
251 pub top_k: usize,
252 pub norm_topk_prob: bool,
253 pub shared: Option<(DenseFfn, QTensor)>,
255 pub stats: std::cell::RefCell<Vec<u64>>,
259}
260
261pub enum AttnKind {
264 Full {
266 wq: QTensor,
267 wk: QTensor,
268 wv: QTensor,
269 wo: QTensor,
270 q_norm: Option<Vec<f32>>,
271 k_norm: Option<Vec<f32>>,
272 output_gate: bool,
273 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
275 },
276 Linear(VmfPhaseWeights),
278 LinearGdn(GdnWeights),
280}
281
282pub struct MtpModule {
287 pub enorm: Vec<f32>,
288 pub hnorm: Vec<f32>,
289 pub eh_proj: QTensor,
291 pub layer: LayerWeights,
292 pub final_norm: Vec<f32>,
293 pub kv: crate::kv_cache::LayerKvCache,
294}
295
296pub struct GenerateResult {
298 pub text: String,
299 pub token_ids: Vec<u32>,
300 pub prompt_tokens: usize,
301 pub tokens_generated: usize,
302 pub finish_reason: String,
303 pub mtp_drafted: usize,
305 pub mtp_accepted: usize,
306 pub token_confidence: Vec<f32>,
311 pub traces: Vec<TokenTrace>,
314}
315
316#[derive(Clone, Debug)]
321pub struct TokenTrace {
322 pub t: usize,
324 pub token_id: u32,
326 pub confidence: f32,
328 pub active_skill: Option<String>,
330 pub recon: Option<f32>,
334 pub switched: bool,
337}
338
339fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
344 let t = if temp > 1e-3 { temp } else { 1.0 };
345 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
346 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
347 if sum > 0.0 {
348 (((logits[id as usize] - max) / t).exp()) / sum
349 } else {
350 0.0
351 }
352}
353
354fn prefill_batched() -> bool {
357 std::env::var("CMF_PREFILL").map(|v| v != "seq").unwrap_or(true)
358}
359
360fn prefill_chunk() -> usize {
365 if let Some(n) =
366 std::env::var("CMF_PREFILL_CHUNK").ok().and_then(|v| v.parse::<usize>().ok())
367 {
368 return n.max(1);
369 }
370 if cfg!(target_os = "macos") {
371 512
372 } else if cfg!(target_arch = "aarch64") {
373 256
376 } else {
377 48
378 }
379}
380
381pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
383
384impl Pipeline {
385 #[allow(clippy::too_many_arguments)]
387
388 #[cfg(target_os = "macos")]
403 fn graph_prefill_preferred(&self) -> bool {
404 if !crate::gpu::enabled_here()
405 || !crate::gpu::q1_force()
406 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
407 {
408 return false;
409 }
410 self.weights.layers.iter().any(
411 |lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()),
412 )
413 }
414
415 #[cfg(not(target_os = "macos"))]
416 fn graph_prefill_preferred(&self) -> bool {
417 false
418 }
419
420 #[cfg(target_os = "macos")]
421 fn q1_graph_gpu(
422 &mut self,
423 start: usize,
424 upto: Option<usize>,
425 position: usize,
426 h: &mut [f32],
427 ) -> usize {
428 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
429 if !crate::gpu::enabled_here()
430 || !crate::gpu::q1_force()
431 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
432 {
433 return start;
434 }
435 if self.swa.is_some()
440 || self.global_attn.is_some()
441 || self.attn_v_norm
442 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
443 || self.weights.layers.iter().any(|lw| {
444 lw.attn_out_norm.is_some()
445 || lw.ffn_out_norm.is_some()
446 || lw.layer_scale.is_some()
447 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
448 })
449 {
450 return start;
451 }
452 let limit = upto.map(|u| u + 1).unwrap_or(self.num_layers).min(self.num_layers);
453
454 enum Item<'a> {
455 Gdn {
456 run: Vec<GdnGpuLayer<'a>>,
457 first: usize,
458 },
459 Attn {
460 l: AttnGpuLayer<'a>,
461 li: usize,
462 q_norm: Option<&'a [f32]>,
463 k_norm: Option<&'a [f32]>,
464 output_gate: bool,
465 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
466 full_gpu: bool,
469 },
470 }
471
472 let dev_attend = std::env::var("CMF_GPU_ATTEND").map(|v| v != "0").unwrap_or(true)
474 && self.head_dim % 4 == 0
475 && self.head_dim <= 128
476 && self.rotary_dim >= 2
477 && self.rotary_dim <= self.head_dim
478 && (self.rotary_dim / 2) % 32 == 0
479 && self.num_kv_heads > 0
480 && self.num_heads % self.num_kv_heads == 0;
481
482 let mut plan: Vec<Item> = Vec::new();
483 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
484 let mut scan = start;
485 while scan < limit {
486 let lw = &self.weights.layers[scan];
487 let FfnKind::Dense(d) = &lw.ffn else { break };
488 let (Some(g), Some(u), Some(dn)) =
489 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
490 else {
491 break;
492 };
493 match &lw.attn {
494 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
495 let parts = (
496 w.in_proj_qkv.q1_parts(),
497 w.in_proj_z.q1_parts(),
498 w.in_proj_a.f32_parts(),
499 w.in_proj_b.f32_parts(),
500 w.out_proj.q1_parts(),
501 );
502 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else { break };
503 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
504 model_ref.get_or_insert_with(|| model.clone());
505 }
506 let gl = GdnGpuLayer {
507 attn_norm: &lw.input_norm,
508 post_norm: &lw.post_norm,
509 qkv,
510 z,
511 a,
512 b,
513 out,
514 gate: g,
515 up: u,
516 down: dn,
517 conv1d: &w.conv1d,
518 a_log: &w.a_log,
519 dt_bias: &w.dt_bias,
520 gnorm: &w.norm,
521 };
522 match plan.last_mut() {
523 Some(Item::Gdn { run, .. }) => run.push(gl),
524 _ => plan.push(Item::Gdn { run: vec![gl], first: scan }),
525 }
526 }
527 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, bias }
528 if !self.kv_cache.layers[scan].o1_sealed() =>
529 {
530 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
531 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else { break };
532 if let QTensor::Mapped { model, .. } = wq {
533 model_ref.get_or_insert_with(|| model.clone());
534 }
535 let cache = &self.kv_cache.layers[scan];
536 let full_gpu = dev_attend
537 && cache.mode == crate::kv_cache::KvMode::F32
538 && cache.o1.is_none()
539 && bias.is_none()
540 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
541 && pk.1 == self.num_kv_heads * self.head_dim
542 && pv.1 == self.num_kv_heads * self.head_dim
543 && po.2 == self.num_heads * self.head_dim;
544 plan.push(Item::Attn {
545 l: AttnGpuLayer {
546 attn_norm: &lw.input_norm,
547 post_norm: &lw.post_norm,
548 wq: pq,
549 wk: pk,
550 wv: pv,
551 wo: po,
552 gate: g,
553 up: u,
554 down: dn,
555 },
556 li: scan,
557 q_norm: q_norm.as_deref(),
558 k_norm: k_norm.as_deref(),
559 output_gate: *output_gate,
560 bias: bias
561 .as_ref()
562 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
563 full_gpu,
564 });
565 }
566 _ => break,
567 }
568 scan += 1;
569 }
570 let Some(model) = model_ref else { return start };
571 if plan.is_empty() {
572 return start;
573 }
574 let dims = GraphDims {
575 hidden: self.hidden_size,
576 eps: self.rms_eps as f32,
577 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
578 };
579 let Some(mut graph) = TokenGraph::new(&model, dims, h) else { return start };
580 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
581 nv: cfg.num_v_heads,
582 nk: cfg.num_k_heads,
583 dk: cfg.key_head_dim,
584 dv: cfg.value_head_dim,
585 kk: cfg.conv_kernel,
586 hidden: self.hidden_size,
587 inter: self.intermediate_size,
588 c_dim: cfg.conv_dim(),
589 eps: cfg.rms_eps as f32,
590 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
591 });
592 let mut valid = 0usize;
596 let mut end = start;
597 for item in &plan {
598 let ok = match item {
599 Item::Gdn { run, .. } => gcfg
600 .as_ref()
601 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
602 .unwrap_or(false),
603 Item::Attn { l, .. } => graph.attn_ok(l),
604 };
605 if !ok {
606 break;
607 }
608 valid += 1;
609 end += match item {
610 Item::Gdn { run, .. } => run.len(),
611 Item::Attn { .. } => 1,
612 };
613 }
614 plan.truncate(valid);
615 if plan.is_empty() {
616 return start;
617 }
618
619 let inv_freq = self.inv_freq.clone();
620 let pool = self.pool.clone();
621 let (nh, nkv, hd, hs, rd, eps) = (
622 self.num_heads,
623 self.num_kv_heads,
624 self.head_dim,
625 self.hidden_size,
626 self.rotary_dim,
627 self.rms_eps,
628 );
629 let norm_style = self.norm_style;
630 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
631 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
632 let kv_id = self.graph_kv_id;
633 let mut pending: Vec<(usize, usize)> = Vec::new();
636 let mut dev_attn: Vec<usize> = Vec::new();
639 for item in &plan {
640 match item {
641 Item::Gdn { run, first } => {
642 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
643 if l.linear_state.len() != want {
644 l.linear_state = vec![0f32; want];
645 }
646 }
647 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
648 .iter()
649 .map(|l| l.linear_state.as_slice())
650 .collect();
651 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
652 tracing::error!("q1 graph: GDN run refused after validation");
654 return start;
655 }
656 graph.commit();
659 pending.push((*first, run.len()));
660 }
661 Item::Attn { l, li, q_norm, k_norm, output_gate, bias, full_gpu } => {
662 if *full_gpu {
664 let cache = &self.kv_cache.layers[*li];
665 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
666 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
667 let cpu_stored = cpu_k[0].len() / hd;
668 let p = crate::gpu::AttnDeviceParams {
669 kv_id,
670 layer: *li,
671 nh,
672 nkv,
673 hd,
674 rd,
675 position,
676 eps: eps as f32,
677 gemma,
678 output_gate: *output_gate,
679 q_norm: *q_norm,
680 k_norm: *k_norm,
681 inv_freq: &inv_freq,
682 cpu_k,
683 cpu_v,
684 cpu_stored,
685 };
686 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
687 graph.commit();
688 dev_attn.push(*li);
689 continue;
690 }
691 }
693 graph.encode_attn_prefix(l);
694 graph.sync();
695 if !pending.is_empty() {
696 let idxs: Vec<usize> =
697 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
698 let mut outs: Vec<&mut [f32]> = self
699 .kv_cache
700 .layers
701 .iter_mut()
702 .enumerate()
703 .filter(|(i, _)| idxs.binary_search(i).is_ok())
704 .map(|(_, s)| s.linear_state.as_mut_slice())
705 .collect();
706 graph.read_states(&mut outs);
707 }
708 let mut q_raw = attention::take_buf(l.wq.1);
709 let mut k = attention::take_buf(l.wk.1);
710 let mut v = attention::take_buf(l.wv.1);
711 graph.read_qkv(&mut q_raw, &mut k, &mut v);
712 let cfg = QwenAttnCfg {
713 num_heads: nh,
714 num_kv_heads: nkv,
715 head_dim: hd,
716 hidden_size: hs,
717 position,
718 inv_freq: &inv_freq,
719 rotary_dim: rd,
720 scale: self.attn_scale,
721 window: None,
722 v_norm: false,
723 q_norm: *q_norm,
724 k_norm: *k_norm,
725 output_gate: *output_gate,
726 bias: *bias,
727 rms_eps: eps,
728 norm_style,
729 pool: pool.as_deref(),
730 };
731 let mut ao = attention::qwen_attention_core(
732 q_raw,
733 k,
734 v,
735 &mut self.kv_cache.layers[*li],
736 &cfg,
737 );
738 graph.encode_attn_suffix(l, &ao);
739 graph.commit();
742 attention::recycle_buf(&mut ao);
743 }
744 }
745 }
746 let mut lm_rows = None;
751 if self.graph_want_logits
752 && upto.is_none()
753 && end == self.num_layers
754 && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
755 {
756 if let Some(lm) = self.weights.lm_head.q1_parts() {
757 if graph.lm_head_ok(lm) {
758 graph.encode_lm_head(&self.weights.final_norm, lm);
759 lm_rows = Some(lm.1);
760 }
761 }
762 }
763 graph.sync();
764 if !pending.is_empty() {
765 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
766 let mut outs: Vec<&mut [f32]> = self
767 .kv_cache
768 .layers
769 .iter_mut()
770 .enumerate()
771 .filter(|(i, _)| idxs.binary_search(i).is_ok())
772 .map(|(_, s)| s.linear_state.as_mut_slice())
773 .collect();
774 graph.read_states(&mut outs);
775 }
776 if let Some(rows) = lm_rows {
777 let mut lg = attention::take_buf(rows.min(self.vocab_size));
778 graph.read_logits(&mut lg);
779 lg.resize(self.vocab_size, 0.0);
780 if let Some(c) = self.final_softcap {
781 for l in lg.iter_mut() {
782 *l = c * (*l / c).tanh();
783 }
784 }
785 self.graph_logits = Some(lg);
786 }
787 graph.finish(h);
788 for li in dev_attn {
792 let mut krow = attention::take_buf(nkv * hd);
793 let mut vrow = attention::take_buf(nkv * hd);
794 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
795 let cache = &mut self.kv_cache.layers[li];
796 cache.append(&krow, &vrow, &[]);
797 let n = cache.seq_len;
798 let mut imp = attention::take_buf(n);
799 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
800 cache.accumulate_imp(&imp);
801 attention::recycle_buf(&mut imp);
802 }
803 attention::recycle_buf(&mut krow);
804 attention::recycle_buf(&mut vrow);
805 }
806 end
807 }
808
809 pub fn new(
810 tokenizer: Tokenizer,
811 weights: PipelineWeights,
812 hidden_size: usize,
813 intermediate_size: usize,
814 num_heads: usize,
815 num_kv_heads: usize,
816 head_dim: usize,
817 num_layers: usize,
818 vocab_size: usize,
819 rms_eps: f64,
820 rope_base: f32,
821 norm_style: NormStyle,
822 max_seq_len: usize,
823 sampler_config: SamplerConfig,
824 ) -> Self {
825 let rng = match sampler_config.seed {
826 Some(s) => SplitMix64::new(s),
827 None => SplitMix64::from_entropy(),
828 };
829 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
830 let pool = Pool::from_env();
831 if let Some(p) = &pool {
832 tracing::info!("worker pool: {} threads", p.n_workers());
833 }
834 Self {
835 tokenizer: std::sync::Arc::new(tokenizer),
836 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
837 sampler_config,
838 weights,
839 hidden_size,
840 intermediate_size,
841 num_heads,
842 num_kv_heads,
843 head_dim,
844 num_layers,
845 vocab_size,
846 rms_eps,
847 rope_base,
848 norm_style,
849 rotary_dim: head_dim,
850 vmf_cfg: None,
851 gdn_cfg: None,
852 mtp: None,
853 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
854 rng,
855 inv_freq,
856 ws: ForwardScratch::new(hidden_size),
857 pool,
858 model: None,
859 dyn_force_f32: false,
860 dyn_skill_layers: Vec::new(),
861 dyn_active: None,
862 dyn_blend_loaded: false,
863 dyn_phi_layer: None,
864 dyn_phi_ema: Vec::new(),
865 dyn_phi_seen: 0,
866 dyn_router: None,
867 o1_cfg: None,
868 o1_flags: Vec::new(),
869 trace: false,
870 calib_temp: 1.0,
871 confidence_on: true,
872 embed_multiplier: 1.0,
873 attn_scale: 1.0 / (head_dim as f32).sqrt(),
874 swa: None,
875 inv_freq_local: None,
876 global_attn: None,
877 inv_freq_global: None,
878 attn_v_norm: false,
879 final_softcap: None,
880 graph_want_logits: false,
881 graph_logits: None,
882 graph_kv_id: {
883 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
884 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
885 },
886 }
887 }
888
889 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
896 self.o1_flags = match &cfg {
897 Some(c) => {
898 let mut flags = c.layer_flags(self.num_layers);
899 for (li, f) in flags.iter_mut().enumerate() {
900 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
901 *f = false;
902 }
903 }
904 flags
905 }
906 None => Vec::new(),
907 };
908 if let Some(c) = &cfg {
909 let n = self.o1_flags.iter().filter(|&&f| f).count();
910 tracing::info!(
911 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
912 self.num_layers, c.m, c.w, c.sink, c.rect
913 );
914 }
915 self.o1_cfg = cfg;
916 }
917
918 pub fn o1_active(&self) -> bool {
920 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
921 }
922
923 fn o1_begin(&mut self) {
925 if let Some(c) = &self.o1_cfg {
926 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
927 for (li, &f) in self.o1_flags.iter().enumerate() {
928 if f {
929 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
930 }
931 }
932 }
933 }
934
935 fn o1_seal(&mut self) {
938 if self.o1_cfg.is_none() {
939 return;
940 }
941 for li in 0..self.num_layers {
942 if self.o1_flags.get(li).copied().unwrap_or(false) {
943 self.kv_cache.layers[li].o1_seal(self.num_heads);
944 }
945 }
946 }
947
948 pub fn set_trace(&mut self, on: bool) {
950 self.trace = on;
951 }
952
953 pub fn set_confidence(&mut self, on: bool) {
958 self.confidence_on = on;
959 }
960
961 pub fn set_calib_temp(&mut self, t: f32) {
964 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
965 }
966
967 pub fn calib_temp(&self) -> f32 {
969 self.calib_temp
970 }
971
972 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
975 self.rotary_dim = rotary_dim.min(self.head_dim);
976 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
977 }
978
979 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
980 QwenAttnCfg {
981 num_heads: self.num_heads,
982 num_kv_heads: self.num_kv_heads,
983 head_dim: self.head_dim,
984 hidden_size: self.hidden_size,
985 position,
986 inv_freq: &self.inv_freq,
987 rotary_dim: self.rotary_dim,
988 scale: self.attn_scale,
989 window: None,
990 v_norm: false,
991 q_norm: None,
992 k_norm: None,
993 output_gate: false,
994 bias: None,
995 rms_eps: self.rms_eps,
996 norm_style: self.norm_style,
997 pool: self.pool.as_deref(),
998 }
999 }
1000
1001 pub fn generate(
1003 &mut self,
1004 prompt: &str,
1005 max_tokens: usize,
1006 task_mask: Option<&TaskMask>,
1007 on_token: Option<TokenCallback>,
1008 ) -> Result<GenerateResult, String> {
1009 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1010 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1011 }
1012
1013 pub fn generate_from_ids(
1021 &mut self,
1022 input_ids: &[u32],
1023 max_tokens: usize,
1024 task_mask: Option<&TaskMask>,
1025 mut on_token: Option<TokenCallback>,
1026 ) -> Result<GenerateResult, String> {
1027 if std::env::var("CMF_TRACE_H").is_ok() {
1028 eprintln!("input_ids: {input_ids:?}");
1029 }
1030 if input_ids.is_empty() {
1031 return Err("empty prompt: nothing to generate from".to_string());
1032 }
1033
1034 self.kv_cache.clear();
1036 self.o1_begin();
1037
1038 let spec_active = self.speculative
1042 && self.mtp.is_some()
1043 && task_mask.is_none()
1044 && !self.o1_active()
1045 && self.sampler_config.temperature < 1e-6;
1046 let mut mtp = if spec_active { self.mtp.take() } else { None };
1049 if let Some(m) = &mut mtp {
1050 m.kv.clear();
1051 }
1052 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
1056 if let Some(r) = &mut router {
1057 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1060 }
1061
1062 let mut all_ids = input_ids.to_vec();
1063 let mut generated = 0usize;
1064 let mut finish_reason = "max_tokens".to_string();
1065 let mut drafted = 0usize;
1066 let mut accepted = 0usize;
1067 let mut confidence: Vec<f32> = Vec::new();
1068 let trace_on = self.trace;
1069 let calib_temp = self.calib_temp;
1070 let mut traces: Vec<TokenTrace> = Vec::new();
1071
1072 let mut hidden = vec![0.0f32; self.hidden_size];
1078 let mut pos = 0usize;
1079 let fuse_lm = mtp.is_none() && router.is_none();
1083 self.graph_logits = None;
1084 self.graph_want_logits = false;
1085 let dyn_prefill = router.is_some();
1090 let graph_prefill = self.graph_prefill_preferred();
1096 if task_mask.is_none()
1097 && !dyn_prefill
1098 && !graph_prefill
1099 && prefill_batched()
1100 && input_ids.len() > 2
1101 {
1102 let chunk = prefill_chunk();
1108 let hs = self.hidden_size;
1109 while pos < input_ids.len() {
1110 let end = (pos + chunk).min(input_ids.len());
1111 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1112 if let Some(m) = &mut mtp {
1113 for p in pos..end {
1114 if p + 1 < input_ids.len() {
1115 let _ = self.mtp_step(
1116 m,
1117 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1118 input_ids[p + 1],
1119 p,
1120 );
1121 }
1122 }
1123 }
1124 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1125 pos = end;
1126 }
1127 }
1128 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1129 while pos + 1 < input_ids.len() {
1130 let e1 = self.embed_single(input_ids[pos]);
1131 let e2 = self.embed_single(input_ids[pos + 1]);
1132 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1133 self.commit_linear_scratch();
1135 if let Some(m) = &mut mtp {
1136 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1137 if pos + 2 < input_ids.len() {
1138 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1139 }
1140 }
1141 hidden = h2;
1142 pos += 2;
1143 }
1144 }
1145 while pos < input_ids.len() {
1146 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1147 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1148 if let Some(m) = &mut mtp {
1149 if pos + 1 < input_ids.len() {
1150 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1151 }
1152 }
1153 pos += 1;
1154 }
1155 self.o1_seal();
1158
1159 macro_rules! commit {
1161 ($id:expr) => {{
1162 all_ids.push($id);
1163 generated += 1;
1164 if self.tokenizer.is_eos($id) {
1165 finish_reason = "stop".to_string();
1166 false
1167 } else {
1168 let token_text = self.tokenizer.decode_token($id);
1169 let mut go = true;
1170 if let Some(ref mut cb) = on_token {
1171 if !cb(&token_text) {
1172 finish_reason = "cancelled".to_string();
1173 go = false;
1174 }
1175 }
1176 go
1177 }
1178 }};
1179 }
1180
1181 let mut next_pos = input_ids.len();
1183 'decode: while generated < max_tokens {
1184 let mut logits = match self.graph_logits.take() {
1185 Some(lg) => lg,
1186 None => {
1187 inference::rms_norm_into(
1188 &hidden,
1189 &self.weights.final_norm,
1190 self.rms_eps,
1191 self.norm_style,
1192 &mut self.ws.n1,
1193 );
1194 self.lm_head_forward(&self.ws.n1)
1195 }
1196 };
1197 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
1198 if self.confidence_on {
1199 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1200 }
1201 attention::recycle_buf(&mut logits);
1202 if trace_on {
1203 let skill = router.as_ref().and_then(|r| r.active_id());
1207 traces.push(TokenTrace {
1208 t: generated,
1209 token_id: t_next,
1210 confidence: confidence.last().copied().unwrap_or(0.0),
1211 active_skill: skill,
1212 recon: None,
1213 switched: false,
1214 });
1215 }
1216 if !commit!(t_next) {
1217 break 'decode;
1218 }
1219 if generated >= max_tokens {
1220 break 'decode;
1221 }
1222
1223 if self.kv_cache.needs_eviction() {
1224 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1225 self.kv_cache.evict(keep);
1226 }
1227
1228 match &mut mtp {
1229 Some(m) if generated + 1 < max_tokens => {
1231 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1232 drafted += 1;
1233 let emb1 = self.embed_single(t_next);
1234 let emb2 = self.embed_single(draft);
1235 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1236
1237 inference::rms_norm_into(
1238 &h1,
1239 &self.weights.final_norm,
1240 self.rms_eps,
1241 self.norm_style,
1242 &mut self.ws.n1,
1243 );
1244 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1245 let t_after =
1246 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
1247 if self.confidence_on {
1248 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1249 }
1250 attention::recycle_buf(&mut logits1);
1251 if trace_on {
1252 traces.push(TokenTrace {
1255 t: generated,
1256 token_id: t_after,
1257 confidence: confidence.last().copied().unwrap_or(0.0),
1258 active_skill: None,
1259 recon: None,
1260 switched: false,
1261 });
1262 }
1263 let stop = !commit!(t_after);
1264
1265 if t_after == draft {
1266 accepted += 1;
1267 self.commit_linear_scratch();
1268 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1269 hidden = h2;
1270 next_pos += 2;
1271 } else {
1272 for layer in &mut self.kv_cache.layers {
1274 layer.truncate_last(1);
1275 }
1276 if !stop {
1277 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1278 hidden = self
1279 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
1280 }
1281 next_pos += 2;
1282 }
1283 if stop {
1284 break 'decode;
1285 }
1286 }
1287 _ => {
1289 self.graph_want_logits = fuse_lm;
1290 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1291 next_pos += 1;
1292 if let Some(r) = &mut router {
1295 let phi = self.dyn_phi_ema.clone();
1296 let decision = r.step(&phi, generated);
1297 if let Some(new_active) = decision {
1298 let _ = self.set_active_skill(new_active);
1299 }
1300 if trace_on {
1303 if let Some(last) = traces.last_mut() {
1304 let e = r.last_best_e();
1305 last.recon = e.is_finite().then_some(e);
1306 last.switched = decision.is_some();
1307 }
1308 }
1309 }
1310 }
1311 }
1312 }
1313
1314 self.graph_want_logits = false;
1315 self.graph_logits = None;
1316 if router.is_some() {
1318 let _ = self.set_active_skill(None);
1319 }
1320 self.dyn_router = router.or(self.dyn_router.take());
1321 self.mtp = mtp.or(self.mtp.take());
1322
1323 let output_ids = &all_ids[input_ids.len()..];
1324 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1326 Ok(GenerateResult {
1327 text: self.tokenizer.decode(output_ids),
1328 token_ids: output_ids.to_vec(),
1329 prompt_tokens: input_ids.len(),
1330 tokens_generated: generated,
1331 finish_reason,
1332 mtp_drafted: drafted,
1333 mtp_accepted: accepted,
1334 token_confidence: confidence,
1335 traces,
1336 })
1337 }
1338
1339 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
1343 let e = self.embed_single(next_token);
1347 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1348 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1349 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1350 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1351 let mut x = vec![0.0f32; self.hidden_size];
1352 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1353
1354 let lw = &m.layer;
1356 inference::rms_norm_into(&x, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1357 let attn = match &lw.attn {
1358 AttnKind::Full {
1359 wq,
1360 wk,
1361 wv,
1362 wo,
1363 q_norm,
1364 k_norm,
1365 output_gate,
1366 bias,
1367 } => {
1368 let mut cfg = self.attn_cfg(position);
1369 cfg.q_norm = q_norm.as_deref();
1370 cfg.k_norm = k_norm.as_deref();
1371 cfg.output_gate = *output_gate;
1372 cfg.bias = bias
1373 .as_ref()
1374 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1375 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1376 }
1377 AttnKind::Linear(_) | AttnKind::LinearGdn(_) => {
1378 unreachable!("MTP block is full attention")
1379 }
1380 };
1381 for (i, &a) in attn.iter().enumerate() {
1382 x[i] += a;
1383 }
1384 inference::rms_norm_into(&x, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1385 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1386 for (i, &f) in ffn.iter().enumerate() {
1387 x[i] += f;
1388 }
1389
1390 inference::rms_norm_into(&x, &m.final_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1391 let mut lg = self.lm_head_forward(&self.ws.n1);
1392 let draft = sampler::argmax(&lg);
1393 attention::recycle_buf(&mut lg);
1394 draft
1395 }
1396
1397 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1401 let emb1 = self.embed_single(1);
1402 let emb2 = self.embed_single(2);
1403 let pos = self.kv_cache.seq_len();
1404
1405 let t0 = std::time::Instant::now();
1406 for _ in 0..iters {
1407 let _ = self.forward_layers(&emb1, pos, None);
1408 let _ = self.forward_layers(&emb2, pos + 1, None);
1409 for l in &mut self.kv_cache.layers {
1410 l.truncate_last(2);
1411 }
1412 }
1413 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1414
1415 let t1 = std::time::Instant::now();
1416 for _ in 0..iters {
1417 let _ = self.forward_pair(&emb1, &emb2, pos);
1418 for l in &mut self.kv_cache.layers {
1419 l.truncate_last(2);
1420 }
1421 }
1422 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1423 (singles_ms, pair_ms)
1424 }
1425
1426 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
1431 let mut h1 = emb1.to_vec();
1432 let mut h2 = emb2.to_vec();
1433 let (nh, _nkv, _hd, hs, _rd, eps) = (
1434 self.num_heads,
1435 self.num_kv_heads,
1436 self.head_dim,
1437 self.hidden_size,
1438 self.rotary_dim,
1439 self.rms_eps,
1440 );
1441 let pool = self.pool.clone();
1442
1443 for li in 0..self.num_layers {
1444 let lw = &self.weights.layers[li];
1445 inference::rms_norm_into(&h1, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1448 inference::rms_norm_into(&h2, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n2);
1449
1450 let (a1, a2) = match &lw.attn {
1451 AttnKind::Linear(w) => {
1452 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1453 let layer = &mut self.kv_cache.layers[li];
1454 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1455 vmf_phase_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1456 }
1457 AttnKind::LinearGdn(w) => {
1458 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1459 let layer = &mut self.kv_cache.layers[li];
1460 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1461 gdn_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1462 }
1463 AttnKind::Full {
1464 wq,
1465 wk,
1466 wv,
1467 wo,
1468 q_norm,
1469 k_norm,
1470 output_gate,
1471 bias,
1472 } => {
1473 let inv_freq_l = self.layer_inv_freq(li);
1474 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1475 let cfg = QwenAttnCfg {
1476 num_heads: nh,
1477 num_kv_heads: nkv_l,
1478 head_dim: hd_l,
1479 hidden_size: hs,
1480 position,
1481 inv_freq: &inv_freq_l,
1482 rotary_dim: rd_l,
1483 scale: self.attn_scale,
1484 window: self.layer_window(li),
1485 v_norm: self.attn_v_norm,
1486 q_norm: q_norm.as_deref(),
1487 k_norm: k_norm.as_deref(),
1488 output_gate: *output_gate,
1489 bias: bias
1490 .as_ref()
1491 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1492 rms_eps: eps,
1493 norm_style: self.norm_style,
1494 pool: pool.as_deref(),
1495 };
1496 attention::qwen_attention_pair(
1497 &self.ws.n1,
1498 &self.ws.n2,
1499 wq,
1500 wk,
1501 wv,
1502 wo,
1503 &mut self.kv_cache.layers[li],
1504 &cfg,
1505 )
1506 }
1507 };
1508 let (a1, a2) = match &self.weights.layers[li].attn_out_norm {
1509 Some(w) => (
1510 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1511 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1512 ),
1513 None => (a1, a2),
1514 };
1515 for i in 0..self.hidden_size {
1516 h1[i] += a1[i];
1517 h2[i] += a2[i];
1518 }
1519 let (mut a1, mut a2) = (a1, a2);
1520 attention::recycle_buf(&mut a1);
1521 attention::recycle_buf(&mut a2);
1522
1523 let lw = &self.weights.layers[li];
1524 inference::rms_norm_into(&h1, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1525 inference::rms_norm_into(&h2, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p2);
1526 let (f1, f2) =
1527 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1528 let (f1, f2) = match &self.weights.layers[li].ffn_out_norm {
1529 Some(w) => (
1530 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1531 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1532 ),
1533 None => (f1, f2),
1534 };
1535 for i in 0..self.hidden_size {
1536 h1[i] += f1[i];
1537 h2[i] += f2[i];
1538 }
1539 let (mut f1, mut f2) = (f1, f2);
1540 attention::recycle_buf(&mut f1);
1541 attention::recycle_buf(&mut f2);
1542 if let Some(sc) = self.weights.layers[li].layer_scale {
1543 for i in 0..self.hidden_size {
1544 h1[i] *= sc;
1545 h2[i] *= sc;
1546 }
1547 }
1548 }
1549 (h1, h2)
1550 }
1551
1552 fn commit_linear_scratch(&mut self) {
1554 for layer in &mut self.kv_cache.layers {
1555 if !layer.linear_scratch.is_empty() {
1556 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1557 layer.linear_scratch.clear();
1558 }
1559 }
1560 }
1561
1562 pub fn forward_ids(
1565 &mut self,
1566 ids: &[u32],
1567 task_mask: Option<&TaskMask>,
1568 ) -> Result<Vec<f32>, String> {
1569 if ids.is_empty() {
1570 return Err("empty id sequence".to_string());
1571 }
1572 self.kv_cache.clear();
1573 self.o1_begin();
1574 let mut hidden = vec![0.0f32; self.hidden_size];
1575 let mut pos = 0usize;
1576 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1577 let chunk = prefill_chunk();
1581 let hs = self.hidden_size;
1582 while pos < ids.len() {
1583 let end = (pos + chunk).min(ids.len());
1584 let hb = self.prefill_batch(&ids[pos..end], pos);
1585 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1586 pos = end;
1587 }
1588 }
1589 if task_mask.is_none() {
1590 while pos + 1 < ids.len() {
1591 let e1 = self.embed_single(ids[pos]);
1592 let e2 = self.embed_single(ids[pos + 1]);
1593 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1594 self.commit_linear_scratch();
1595 hidden = h2;
1596 pos += 2;
1597 }
1598 }
1599 while pos < ids.len() {
1600 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1601 pos += 1;
1602 }
1603 self.o1_seal();
1607 let normed = inference::rms_norm(
1608 &hidden,
1609 &self.weights.final_norm,
1610 self.rms_eps,
1611 self.norm_style,
1612 );
1613 Ok(self.lm_head_forward(&normed))
1614 }
1615
1616 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1623 let (nll, cnt) = self.nll_ids_from(ids, 0);
1624 (nll / cnt.max(1) as f64).exp()
1625 }
1626
1627 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
1632 self.kv_cache.clear();
1633 FFN_PROBE.with(|p| {
1634 *p.borrow_mut() =
1635 Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
1636 });
1637 crate::gpu::cpu_scope(|| {
1638 for (pos, &id) in ids.iter().enumerate() {
1639 let emb = self.embed_single(id);
1640 let _ = self.forward_layers(&emb, pos, None);
1641 }
1642 });
1643 self.kv_cache.clear();
1644 FFN_PROBE.with(|p| p.borrow_mut().take()).unwrap_or_default()
1645 }
1646
1647 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
1651 self.kv_cache.clear();
1652 let mut nll = 0f64;
1653 let mut cnt = 0usize;
1654 let mut hidden = vec![0f32; self.hidden_size];
1655 for (pos, &id) in ids.iter().enumerate() {
1656 if pos > 0 {
1657 inference::rms_norm_into(
1658 &hidden,
1659 &self.weights.final_norm,
1660 self.rms_eps,
1661 self.norm_style,
1662 &mut self.ws.n1,
1663 );
1664 let mut logits = self.lm_head_forward(&self.ws.n1);
1665 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1666 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
1667 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
1668 nll -= p.max(1e-300).ln();
1669 cnt += 1;
1670 attention::recycle_buf(&mut logits);
1671 }
1672 let emb = self.embed_single(id);
1673 hidden = self.forward_layers(&emb, pos, Some(mask));
1674 }
1675 self.kv_cache.clear();
1676 (nll / cnt.max(1) as f64).exp()
1677 }
1678
1679 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1688 self.kv_cache.clear();
1689 let mut nll = 0f64;
1690 let mut cnt = 0usize;
1691 if prefill_batched() {
1692 const CHUNK: usize = 128;
1698 const LM_SUB: usize = 32;
1699 let n = ids.len().saturating_sub(1);
1700 let hs = self.hidden_size;
1701 let rows = self.weights.lm_head.rows();
1702 let mut pos = 0usize;
1703 while pos < n {
1704 let end = (pos + CHUNK).min(n);
1705 let bsz = end - pos;
1706 let hb = self.prefill_batch(&ids[pos..end], pos);
1707 let mut k0 = 0usize;
1708 while k0 < bsz {
1709 let k1 = (k0 + LM_SUB).min(bsz);
1710 let sb = k1 - k0;
1711 if pos + k1 <= start {
1714 k0 = k1;
1715 continue;
1716 }
1717 let mut normed = vec![0.0f32; sb * hs];
1718 for k in 0..sb {
1719 let r = inference::rms_norm(
1720 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
1721 &self.weights.final_norm,
1722 self.rms_eps,
1723 self.norm_style,
1724 );
1725 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
1726 }
1727 let mut logits = vec![0.0f32; sb * rows];
1728 self.weights
1729 .lm_head
1730 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1731 for k in 0..sb {
1732 if pos + k0 + k < start {
1733 continue;
1734 }
1735 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1736 let target = ids[pos + k0 + k + 1] as usize;
1737 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1738 let lse: f64 = lg
1739 .iter()
1740 .map(|&v| ((v - max) as f64).exp())
1741 .sum::<f64>()
1742 .ln()
1743 + max as f64;
1744 nll += lse - lg[target] as f64;
1745 cnt += 1;
1746 }
1747 k0 = k1;
1748 }
1749 pos = end;
1750 }
1751 self.kv_cache.clear();
1752 return (nll, cnt);
1753 }
1754 for pos in 0..ids.len().saturating_sub(1) {
1755 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1756 if pos < start {
1757 continue;
1758 }
1759 let normed = inference::rms_norm(
1760 &hidden,
1761 &self.weights.final_norm,
1762 self.rms_eps,
1763 self.norm_style,
1764 );
1765 let logits = self.lm_head_forward(&normed);
1766 let target = ids[pos + 1] as usize;
1767 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1768 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1769 + max as f64;
1770 nll += lse - logits[target] as f64;
1771 cnt += 1;
1772 }
1773 self.kv_cache.clear();
1774 (nll, cnt)
1775 }
1776
1777 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1793 self.kv_cache.clear();
1794 self.o1_begin();
1795 let n = ids.len().saturating_sub(1);
1796 let p = prefill.min(n);
1797 let mut pos = 0usize;
1799 if prefill_batched() {
1800 const CHUNK: usize = 128;
1801 while pos < p {
1802 let end = (pos + CHUNK).min(p);
1803 let _ = self.prefill_batch(&ids[pos..end], pos);
1804 pos = end;
1805 }
1806 } else {
1807 while pos < p {
1808 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1809 pos += 1;
1810 }
1811 }
1812 self.o1_seal();
1813
1814 let mut nll = 0f64;
1815 let mut cnt = 0usize;
1816 for pos in p..n {
1817 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1818 let normed = inference::rms_norm(
1819 &hidden,
1820 &self.weights.final_norm,
1821 self.rms_eps,
1822 self.norm_style,
1823 );
1824 let logits = self.lm_head_forward(&normed);
1825 let target = ids[pos + 1] as usize;
1826 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1827 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1828 + max as f64;
1829 nll += lse - logits[target] as f64;
1830 cnt += 1;
1831 }
1832 self.kv_cache.clear();
1833 (nll, cnt)
1834 }
1835
1836 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1844 self.kv_cache.clear();
1845 let n = ids.len().saturating_sub(1);
1846 let mut correct = Vec::with_capacity(n);
1847 let mut pmax = Vec::with_capacity(n);
1848 for pos in 0..n {
1849 let emb = self.embed_single(ids[pos]);
1850 let hidden = self.forward_layers(&emb, pos, None);
1851 let normed = inference::rms_norm(
1852 &hidden,
1853 &self.weights.final_norm,
1854 self.rms_eps,
1855 self.norm_style,
1856 );
1857 let logits = self.lm_head_forward(&normed);
1858 let target = ids[pos + 1] as usize;
1859 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1860 for (i, &v) in logits.iter().enumerate() {
1861 if v > mval {
1862 mval = v;
1863 amax = i;
1864 }
1865 }
1866 correct.push(amax == target);
1867 let row: Vec<f32> = temps
1868 .iter()
1869 .map(|&t| {
1870 let tt = t.max(1e-3);
1871 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1872 1.0 / s.max(1e-12) })
1874 .collect();
1875 pmax.push(row);
1876 }
1877 self.kv_cache.clear();
1878 (correct, pmax)
1879 }
1880
1881 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1888 let mut router = match self.dyn_router.take() {
1889 Some(r) => r,
1890 None => return (self.ppl_ids(ids), 0),
1891 };
1892 router.reset();
1893 self.dyn_phi_seen = 0;
1894 let _ = self.set_active_skill(None);
1895
1896 self.kv_cache.clear();
1897 let mut nll = 0f64;
1898 let mut cnt = 0usize;
1899 for pos in 0..ids.len().saturating_sub(1) {
1900 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1901 let normed = inference::rms_norm(
1902 &hidden,
1903 &self.weights.final_norm,
1904 self.rms_eps,
1905 self.norm_style,
1906 );
1907 let logits = self.lm_head_forward(&normed);
1908 let target = ids[pos + 1] as usize;
1909 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1910 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1911 + max as f64;
1912 nll += lse - logits[target] as f64;
1913 cnt += 1;
1914 let phi = self.dyn_phi_ema.clone();
1916 if let Some(new_active) = router.step(&phi, pos) {
1917 let _ = self.set_active_skill(new_active);
1918 }
1919 }
1920 let switches = router.switches.len();
1921 let _ = self.set_active_skill(None);
1922 self.dyn_router = Some(router);
1923 self.kv_cache.clear();
1924 ((nll / cnt.max(1) as f64).exp(), switches)
1925 }
1926
1927 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1929 self.kv_cache.clear();
1930 let mut acc = vec![0f32; self.hidden_size];
1931 for (pos, &id) in ids.iter().enumerate() {
1932 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1933 for (a, v) in acc.iter_mut().zip(&h) {
1934 *a += v;
1935 }
1936 }
1937 let n = ids.len().max(1) as f32;
1938 for a in acc.iter_mut() {
1939 *a /= n;
1940 }
1941 self.kv_cache.clear();
1942 acc
1943 }
1944
1945 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1951 let b = ids.len();
1952 let hs = self.hidden_size;
1953 let mut h: Vec<f32> = vec![0.0; b * hs];
1956 let mut h_ready = false;
1957 let mut fill_h = |h: &mut Vec<f32>, me: &Self| {
1958 for (bi, &id) in ids.iter().enumerate() {
1959 let e = me.embed_single(id);
1960 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
1961 }
1962 };
1963 let (nh, _nkv, _hd, _rd, eps) = (
1964 self.num_heads,
1965 self.num_kv_heads,
1966 self.head_dim,
1967 self.rotary_dim,
1968 self.rms_eps,
1969 );
1970 let pool = self.pool.clone();
1971 let norm_style = self.norm_style;
1972
1973 #[cfg(target_os = "macos")]
1974 let mut chunk_skip_until = 0usize;
1975 for li in 0..self.num_layers {
1976 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
1983 {
1984 if li < chunk_skip_until {
1985 continue;
1986 }
1987 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
1988 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
1989 if end > li {
1990 h_ready = true;
1991 chunk_skip_until = end;
1992 continue;
1993 }
1994 }
1995 if !h_ready {
1996 fill_h(&mut h, self);
1997 h_ready = true;
1998 }
1999 let lw = &self.weights.layers[li];
2000 match &lw.attn {
2002 AttnKind::LinearGdn(w) => {
2003 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2005 let mut normed = vec![0.0f32; b * hs];
2006 for bi in 0..b {
2007 let r = inference::rms_norm(
2008 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
2009 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2010 }
2011 let attn = crate::linear_core::gdn_forward_batch(
2012 &normed, b, w, &cfg,
2013 &mut self.kv_cache.layers[li].linear_state,
2014 pool.as_deref(),
2015 );
2016 for (dst, &a) in h.iter_mut().zip(&attn) {
2017 *dst += a;
2018 }
2019 }
2020 AttnKind::Full {
2021 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
2022 } => {
2023 let mut normed = vec![0.0f32; b * hs];
2027 for bi in 0..b {
2028 inference::rms_norm_into(
2029 &h[bi * hs..(bi + 1) * hs],
2030 &lw.input_norm,
2031 eps,
2032 norm_style,
2033 &mut normed[bi * hs..(bi + 1) * hs],
2034 );
2035 }
2036 let inv_freq_l = self.layer_inv_freq(li);
2037 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2038 let cfg = QwenAttnCfg {
2039 num_heads: nh,
2040 num_kv_heads: nkv_l,
2041 head_dim: hd_l,
2042 hidden_size: hs,
2043 position: start_pos,
2044 inv_freq: &inv_freq_l,
2045 rotary_dim: rd_l,
2046 scale: self.attn_scale,
2047 window: self.layer_window(li),
2048 v_norm: self.attn_v_norm,
2049 q_norm: q_norm.as_deref(),
2050 k_norm: k_norm.as_deref(),
2051 output_gate: *output_gate,
2052 bias: bias.as_ref().map(|(a, b, c)| {
2053 (a.as_slice(), b.as_slice(), c.as_slice())
2054 }),
2055 rms_eps: eps,
2056 norm_style,
2057 pool: pool.as_deref(),
2058 };
2059 let mut attn = attention::qwen_attention_batch(
2060 &normed, b, wq, wk, wv, wo,
2061 &mut self.kv_cache.layers[li], &cfg);
2062 if let Some(w) = &lw.attn_out_norm {
2063 for bi in 0..b {
2064 inference::rms_norm_into(
2065 &attn[bi * hs..(bi + 1) * hs], w, eps, norm_style,
2066 &mut normed[bi * hs..(bi + 1) * hs]);
2067 }
2068 attn.copy_from_slice(&normed);
2069 }
2070 for (dst, &a) in h.iter_mut().zip(&attn) {
2071 *dst += a;
2072 }
2073 }
2074 AttnKind::Linear(w) => {
2075 for bi in 0..b {
2076 let normed = inference::rms_norm(
2077 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
2078 vmf_phase_forward(
2079 &normed, w,
2080 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2081 &mut self.kv_cache.layers[li].linear_state,
2082 pool.as_deref(),
2083 )
2084 .iter()
2085 .enumerate()
2086 .for_each(|(i, &a)| h[bi * hs + i] += a);
2087 }
2088 }
2089 }
2090
2091 let lw = &self.weights.layers[li];
2093 let mut post = vec![0.0f32; b * hs];
2094 for bi in 0..b {
2095 let r = inference::rms_norm(
2096 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2097 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2098 }
2099 let mut ffn = match &lw.ffn {
2100 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2101 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
2102 };
2103 if let Some(w) = &lw.ffn_out_norm {
2104 for bi in 0..b {
2105 inference::rms_norm_into(
2106 &ffn[bi * hs..(bi + 1) * hs], w, eps, norm_style,
2107 &mut post[bi * hs..(bi + 1) * hs]);
2108 }
2109 ffn.copy_from_slice(&post);
2110 }
2111 for (dst, &f) in h.iter_mut().zip(&ffn) {
2112 *dst += f;
2113 }
2114 if let Some(sc) = lw.layer_scale {
2115 for v in h.iter_mut() {
2116 *v *= sc;
2117 }
2118 }
2119 if std::env::var("CMF_TRACE_H").is_ok() {
2120 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2121 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2122 eprintln!("layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}", lw.layer_scale);
2123 }
2124 }
2125 crate::gpu::set_layer(-1); h
2127 }
2128
2129 fn embed_single(&self, id: u32) -> Vec<f32> {
2131 let mut out = vec![0.0f32; self.hidden_size];
2132 if (id as usize) < self.weights.embed_tokens.rows() {
2133 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2134 }
2135 if self.embed_multiplier != 1.0 {
2136 for v in out.iter_mut() {
2137 *v *= self.embed_multiplier;
2138 }
2139 }
2140 out
2141 }
2142
2143 #[cfg(target_os = "macos")]
2149 fn chunk_run_gpu(
2150 &mut self,
2151 li0: usize,
2152 h: &mut [f32],
2153 b: usize,
2154 pos0: usize,
2155 embed_ids: Option<&[u32]>,
2156 ) -> usize {
2157 if !crate::gpu::enabled_here()
2161 || std::env::var("CMF_GPU_CHUNK").map(|v| v == "0").unwrap_or(false)
2162 || b < 32
2163 || self.swa.is_some()
2164 || self.global_attn.is_some()
2165 || self.attn_v_norm
2166 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2167 {
2168 return li0;
2169 }
2170 let Some(model) = self.model.clone() else { return li0 };
2171 let inv_freq = self.inv_freq.clone();
2172 let (nh, nkv, hd, hs) = (self.num_heads, self.num_kv_heads, self.head_dim, self.hidden_size);
2173 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2175 let mut stored_at: Vec<usize> = Vec::new();
2176 for li in li0..self.num_layers {
2177 let lw = &self.weights.layers[li];
2178 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
2179 {
2180 break;
2181 }
2182 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate: false, bias } =
2183 &lw.attn
2184 else {
2185 break;
2186 };
2187 let FfnKind::Dense(d) = &lw.ffn else { break };
2188 if d.act != Act::Silu {
2189 break;
2190 }
2191 let parts = (
2192 wq.q8_row_parts(),
2193 wk.q8_row_parts(),
2194 wv.q8_row_parts(),
2195 wo.q8_row_parts(),
2196 d.gate_proj.q8_row_parts(),
2197 d.up_proj.q8_row_parts(),
2198 d.down_proj.q8_row_parts(),
2199 );
2200 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2201 else {
2202 break;
2203 };
2204 let layer = &self.kv_cache.layers[li];
2205 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2206 break;
2207 }
2208 stored_at.push(layer.head_len(0));
2209 layers.push(crate::gpu_metal::ChunkLayer {
2210 model: &model,
2211 kv_id: self.graph_kv_id,
2212 layer: li,
2213 wq: pq,
2214 wk: pk,
2215 wv: pv,
2216 wo: po,
2217 gate: pg,
2218 up: pu,
2219 down: pd,
2220 input_norm: &lw.input_norm,
2221 post_norm: &lw.post_norm,
2222 bias: bias
2223 .as_ref()
2224 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2225 q_norm: q_norm.as_deref(),
2226 k_norm: k_norm.as_deref(),
2227 inv_freq: &inv_freq,
2228 rd: self.rotary_dim,
2229 nh,
2230 nkv,
2231 hd,
2232 hs,
2233 inter: d.gate_proj.rows(),
2234 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2235 eps: self.rms_eps as f32,
2236 });
2237 }
2238 if layers.is_empty() {
2239 return li0;
2240 }
2241 let row = nkv * hd;
2242 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2243 .iter()
2244 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2245 .collect();
2246 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2247 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2248 let li = layers[i].layer;
2249 let layer = &self.kv_cache.layers[li];
2250 io.push(crate::gpu_metal::ChunkIo {
2251 cpu_stored: stored_at[i],
2252 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2253 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2254 out_k: ok,
2255 out_v: ov,
2256 imp: oi,
2257 });
2258 }
2259 let n_run = layers.len();
2260 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2261 let ep = embed_ids.and_then(|ids| {
2264 self.weights.embed_tokens.q8_row_parts().map(|(idx, rows, _c, rs)| {
2265 crate::gpu_metal::ChunkEmbed {
2266 idx,
2267 rows,
2268 row_scale: rs,
2269 ids,
2270 mult: self.embed_multiplier,
2271 }
2272 })
2273 });
2274 if embed_ids.is_some() && ep.is_none() {
2275 return li0;
2276 }
2277 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2278 return li0;
2279 }
2280 drop(io);
2281 drop(layers);
2282 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2285 let li = li0 + i;
2286 let layer = &mut self.kv_cache.layers[li];
2287 for bi in 0..b {
2288 layer.append(&ok[bi * row..(bi + 1) * row], &ov[bi * row..(bi + 1) * row], &[]);
2289 }
2290 layer.accumulate_imp(oi);
2291 }
2292 last
2293 }
2294
2295 fn layer_is_local(&self, li: usize) -> bool {
2298 match self.swa {
2299 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2300 None => false,
2301 }
2302 }
2303
2304 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2307 if self.layer_is_local(li) {
2308 if let Some(f) = &self.inv_freq_local {
2309 return f.clone();
2310 }
2311 } else if let Some(f) = &self.inv_freq_global {
2312 return f.clone();
2313 }
2314 self.inv_freq.clone()
2315 }
2316
2317 fn layer_window(&self, li: usize) -> Option<usize> {
2319 match self.swa {
2320 Some((w, _)) if self.layer_is_local(li) => Some(w),
2321 _ => None,
2322 }
2323 }
2324
2325 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2328 if !self.layer_is_local(li) {
2329 if let Some((ghd, gkv)) = self.global_attn {
2330 return (gkv, ghd, ghd);
2331 }
2332 }
2333 (self.num_kv_heads, self.head_dim, self.rotary_dim)
2334 }
2335
2336 fn forward_layers(
2338 &mut self,
2339 hidden: &[f32],
2340 position: usize,
2341 task_mask: Option<&TaskMask>,
2342 ) -> Vec<f32> {
2343 self.forward_layers_upto(hidden, position, task_mask, None)
2344 }
2345
2346 fn forward_layers_upto(
2348 &mut self,
2349 hidden: &[f32],
2350 position: usize,
2351 task_mask: Option<&TaskMask>,
2352 upto: Option<usize>,
2353 ) -> Vec<f32> {
2354 let mut h = hidden.to_vec();
2355 let (nh, _nkv, _hd, hs, _rd, eps) = (
2358 self.num_heads,
2359 self.num_kv_heads,
2360 self.head_dim,
2361 self.hidden_size,
2362 self.rotary_dim,
2363 self.rms_eps,
2364 );
2365 let pool = self.pool.clone();
2366
2367 #[cfg(target_os = "macos")]
2368 let mut gpu_skip_until = 0usize;
2369 for li in 0..self.num_layers {
2370 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
2372 if li > u {
2373 break;
2374 }
2375 }
2376 if let Some(mask) = task_mask {
2377 if !mask.layer_alive(li) {
2378 continue; }
2380 }
2381 #[cfg(target_os = "macos")]
2385 {
2386 if li < gpu_skip_until {
2387 continue;
2388 }
2389 if task_mask.is_none() {
2390 let end = self.q1_graph_gpu(li, upto, position, &mut h);
2391 if end > li {
2392 gpu_skip_until = end;
2393 continue;
2394 }
2395 }
2396 }
2397
2398 let lw = &self.weights.layers[li];
2399 inference::rms_norm_into(&h, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
2402
2403 let attn_out = match &lw.attn {
2404 AttnKind::Linear(w) => {
2405 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2406 vmf_phase_forward(
2407 &self.ws.n1,
2408 w,
2409 &cfg,
2410 &mut self.kv_cache.layers[li].linear_state,
2411 self.pool.as_deref(),
2412 )
2413 }
2414 AttnKind::LinearGdn(w) => {
2415 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2416 gdn_forward(
2417 &self.ws.n1,
2418 w,
2419 &cfg,
2420 &mut self.kv_cache.layers[li].linear_state,
2421 self.pool.as_deref(),
2422 )
2423 }
2424 AttnKind::Full {
2425 wq,
2426 wk,
2427 wv,
2428 wo,
2429 q_norm,
2430 k_norm,
2431 output_gate,
2432 bias,
2433 } if self.kv_cache.layers[li].o1_sealed() => {
2434 let inv_freq_l = self.layer_inv_freq(li);
2437 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2438 let cfg = QwenAttnCfg {
2439 num_heads: nh,
2440 num_kv_heads: nkv_l,
2441 head_dim: hd_l,
2442 hidden_size: hs,
2443 position,
2444 inv_freq: &inv_freq_l,
2445 rotary_dim: rd_l,
2446 scale: self.attn_scale,
2447 window: None,
2448 v_norm: self.attn_v_norm,
2449 q_norm: q_norm.as_deref(),
2450 k_norm: k_norm.as_deref(),
2451 output_gate: *output_gate,
2452 bias: bias
2453 .as_ref()
2454 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2455 rms_eps: eps,
2456 norm_style: self.norm_style,
2457 pool: pool.as_deref(),
2458 };
2459 attention::qwen_attention_nystrom(
2460 &self.ws.n1,
2461 wq,
2462 wk,
2463 wv,
2464 wo,
2465 &mut self.kv_cache.layers[li],
2466 &cfg,
2467 )
2468 }
2469 AttnKind::Full {
2470 wq,
2471 wk,
2472 wv,
2473 wo,
2474 q_norm,
2475 k_norm,
2476 output_gate,
2477 bias,
2478 } => {
2479 let masked = task_mask
2480 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
2481 .unwrap_or(false);
2482 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
2483 match (masked, f32_view) {
2484 (true, (Some(q), Some(k), Some(v), Some(o))) => {
2487 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
2488 attention::multi_head_attention(
2489 &self.ws.n1,
2490 q,
2491 k,
2492 v,
2493 o,
2494 &mut self.kv_cache.layers[li],
2495 self.num_heads,
2496 self.num_kv_heads,
2497 self.head_dim,
2498 self.hidden_size,
2499 position,
2500 &active_heads,
2501 &self.inv_freq,
2502 )
2503 }
2504 (masked, _) => {
2505 if masked {
2506 tracing::warn!(
2507 "layer {li}: head mask on quantized weights not \
2508 supported yet — executing dense"
2509 );
2510 }
2511 let inv_freq_l = self.layer_inv_freq(li);
2512 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2513 let cfg = QwenAttnCfg {
2514 num_heads: nh,
2515 num_kv_heads: nkv_l,
2516 head_dim: hd_l,
2517 hidden_size: hs,
2518 position,
2519 inv_freq: &inv_freq_l,
2520 rotary_dim: rd_l,
2521 scale: self.attn_scale,
2522 window: self.layer_window(li),
2523 v_norm: self.attn_v_norm,
2524 q_norm: q_norm.as_deref(),
2525 k_norm: k_norm.as_deref(),
2526 output_gate: *output_gate,
2527 bias: bias
2528 .as_ref()
2529 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2530 rms_eps: eps,
2531 norm_style: self.norm_style,
2532 pool: pool.as_deref(),
2533 };
2534 attention::qwen_attention(
2535 &self.ws.n1,
2536 wq,
2537 wk,
2538 wv,
2539 wo,
2540 &mut self.kv_cache.layers[li],
2541 &cfg,
2542 )
2543 }
2544 }
2545 }
2546 };
2547 let attn_out = match &self.weights.layers[li].attn_out_norm {
2550 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
2551 None => attn_out,
2552 };
2553 for (i, &a) in attn_out.iter().enumerate() {
2554 h[i] += a;
2555 }
2556 let mut attn_out = attn_out;
2557 attention::recycle_buf(&mut attn_out);
2558
2559 let lw = &self.weights.layers[li];
2560 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
2561 let post_normed = &self.ws.p1;
2562
2563 let ffn_masked = task_mask
2564 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
2565 .unwrap_or(false);
2566 let f32_ffn = match &lw.ffn {
2569 FfnKind::Dense(d) => {
2570 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
2571 }
2572 FfnKind::Moe(_) => (None, None, None),
2573 };
2574 let ffn_out = match (ffn_masked, f32_ffn) {
2575 (true, (Some(g), Some(u), Some(d))) => {
2576 let active = task_mask.unwrap().ffn_active_indices(li);
2577 inference::sparse_ffn_forward(
2578 &post_normed,
2579 g,
2580 u,
2581 d,
2582 self.hidden_size,
2583 self.intermediate_size,
2584 &active,
2585 self.pool.as_deref(),
2586 )
2587 }
2588 (true, _) => match &lw.ffn {
2592 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
2593 let active = task_mask.unwrap().ffn_active_indices(li);
2594 sparse_ffn_quant(
2595 d,
2596 &post_normed,
2597 &active,
2598 self.hidden_size,
2599 self.pool.as_deref(),
2600 )
2601 }
2602 FfnKind::Dense(d) => {
2607 let active = task_mask.unwrap().ffn_active_indices(li);
2608 let (gf, uf, df) = dequant_dense_f32(d);
2609 inference::sparse_ffn_forward(
2610 &post_normed,
2611 &gf,
2612 &uf,
2613 &df,
2614 self.hidden_size,
2615 self.intermediate_size,
2616 &active,
2617 self.pool.as_deref(),
2618 )
2619 }
2620 FfnKind::Moe(_) => {
2621 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
2624 }
2625 },
2626 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
2627 };
2628 let ffn_out = match &self.weights.layers[li].ffn_out_norm {
2629 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
2630 None => ffn_out,
2631 };
2632 for (i, &f) in ffn_out.iter().enumerate() {
2633 h[i] += f;
2634 }
2635 let mut ffn_out = ffn_out;
2636 attention::recycle_buf(&mut ffn_out);
2637
2638 if let Some(sc) = self.weights.layers[li].layer_scale {
2640 for v in h.iter_mut() {
2641 *v *= sc;
2642 }
2643 }
2644
2645 if self.dyn_phi_layer == Some(li) {
2649 self.update_dyn_phi(&h);
2650 }
2651 }
2652 crate::gpu::set_layer(-1); h
2655 }
2656
2657 fn update_dyn_phi(&mut self, h: &[f32]) {
2660 const A: f32 = 0.2;
2661 if self.dyn_phi_ema.len() != h.len() {
2662 self.dyn_phi_ema = vec![0.0; h.len()];
2663 self.dyn_phi_seen = 0;
2664 }
2665 if self.dyn_phi_seen == 0 {
2666 self.dyn_phi_ema.copy_from_slice(h);
2667 } else {
2668 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
2669 *e = (1.0 - A) * *e + A * v;
2670 }
2671 }
2672 self.dyn_phi_seen += 1;
2673 }
2674
2675 pub fn dyn_phi(&self) -> &[f32] {
2677 &self.dyn_phi_ema
2678 }
2679
2680 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
2682 self.dyn_phi_layer = layer;
2683 self.dyn_phi_ema.clear();
2684 self.dyn_phi_seen = 0;
2685 }
2686
2687 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
2689 let Some(model) = &self.model else { return Vec::new() };
2690 model
2691 .header
2692 .skills
2693 .iter()
2694 .enumerate()
2695 .filter_map(|(i, sk)| {
2696 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
2697 let sel = sk.selection.as_ref()?;
2698 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
2699 })
2700 .collect()
2701 }
2702
2703 pub fn active_skill(&self) -> Option<usize> {
2705 self.dyn_active
2706 }
2707
2708 pub fn enable_dynamic_routing(&mut self) -> usize {
2713 use crate::swarm::{DynRouter, RoutableSkill};
2714 let Some(model) = self.model.clone() else { return 0 };
2715 if self.dyn_blend_loaded {
2718 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
2719 return 0;
2720 }
2721 if let Some(a) = self.dyn_active {
2725 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
2726 tracing::warn!(
2727 "loaded skill is not FFN-eligible — dynamic routing unavailable"
2728 );
2729 return 0;
2730 }
2731 }
2732 let hidden = self.hidden_size;
2733 let mut skills = Vec::new();
2734 for (idx, id, _phi) in self.dynamic_skills() {
2735 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
2736 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
2737 skills.push(rs);
2738 }
2739 }
2740 }
2741 if skills.is_empty() {
2742 return 0;
2743 }
2744 let phi = skills[0].phi_layer;
2746 if skills.iter().any(|s| s.phi_layer != phi) {
2747 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
2748 }
2749 let n = skills.len();
2750 self.set_dyn_phi_layer(Some(phi));
2751 self.dyn_router = Some(DynRouter::new(skills));
2752 n
2753 }
2754
2755 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
2757 self.dyn_router
2758 .as_ref()
2759 .map(|r| r.switches.clone())
2760 .unwrap_or_default()
2761 }
2762
2763 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
2766 let rows = self.weights.lm_head.rows();
2767 let mut logits = attention::take_buf(rows.min(self.vocab_size));
2768 self.weights
2769 .lm_head
2770 .matvec(hidden, &mut logits, self.pool.as_deref());
2771 logits.resize(self.vocab_size, 0.0);
2772 if let Some(c) = self.final_softcap {
2773 for l in logits.iter_mut() {
2774 *l = c * (*l / c).tanh();
2775 }
2776 }
2777 logits
2778 }
2779
2780 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
2785 self.kv_cache.clear();
2786 let mut hidden = vec![0.0f32; self.hidden_size];
2787 for (pos, &id) in ids.iter().enumerate() {
2788 let emb = self.embed_single(id);
2789 hidden = self.forward_layers(&emb, pos, task_mask);
2790 }
2791 inference::rms_norm_into(
2792 &hidden,
2793 &self.weights.final_norm,
2794 self.rms_eps,
2795 self.norm_style,
2796 &mut self.ws.n1,
2797 );
2798 self.lm_head_forward(&self.ws.n1)
2799 }
2800}
2801
2802pub fn create_test_pipeline(
2804 hidden_size: usize,
2805 intermediate_size: usize,
2806 num_heads: usize,
2807 num_kv_heads: usize,
2808 head_dim: usize,
2809 num_layers: usize,
2810 vocab_size: usize,
2811) -> Pipeline {
2812 let synth = |n: usize, salt: usize| -> Vec<f32> {
2815 (0..n)
2816 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
2817 .collect()
2818 };
2819 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2820 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2821 };
2822 let layer_weights: Vec<LayerWeights> = (0..num_layers)
2823 .map(|li| LayerWeights {
2824 input_norm: vec![1.0; hidden_size],
2825 post_norm: vec![1.0; hidden_size],
2826 attn_out_norm: None,
2827 ffn_out_norm: None,
2828 layer_scale: None,
2829 ffn: FfnKind::Dense(DenseFfn {
2830 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
2831 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
2832 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
2833 act: Act::Silu,
2834 }),
2835 attn: AttnKind::Full {
2836 bias: None,
2837 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
2838 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
2839 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
2840 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
2841 q_norm: None,
2842 k_norm: None,
2843 output_gate: false,
2844 },
2845 })
2846 .collect();
2847
2848 Pipeline::new(
2849 Tokenizer::byte_level(),
2850 PipelineWeights {
2851 embed_tokens: qt(vocab_size, hidden_size, 100),
2852 layers: layer_weights,
2853 lm_head: qt(vocab_size, hidden_size, 200),
2854 final_norm: vec![1.0; hidden_size],
2855 },
2856 hidden_size,
2857 intermediate_size,
2858 num_heads,
2859 num_kv_heads,
2860 head_dim,
2861 num_layers,
2862 vocab_size,
2863 1e-6,
2864 10_000.0,
2865 NormStyle::Qwen,
2866 4096,
2867 SamplerConfig {
2868 seed: Some(42),
2869 ..Default::default()
2870 },
2871 )
2872}
2873
2874fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
2877 let inter = d.gate_proj.rows();
2878 let hidden = d.down_proj.rows();
2879 let mut g = vec![0.0f32; b * inter];
2880 d.gate_proj.matmat(xs, b, &mut g, pool);
2881 let mut u = vec![0.0f32; b * inter];
2882 d.up_proj.matmat(xs, b, &mut u, pool);
2883 for i in 0..b * inter {
2884 g[i] = d.act.apply(g[i]) * u[i];
2885 }
2886 let mut out = vec![0.0f32; b * hidden];
2887 d.down_proj.matmat(&g, b, &mut out, pool);
2888 out
2889}
2890
2891fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
2895 let ne = m.experts.len();
2896 let mut logits = vec![0.0f32; b * ne];
2897 m.router.matmat(xs, b, &mut logits, pool);
2898
2899 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
2902 {
2903 let mut st = m.stats.borrow_mut();
2904 if st.len() < ne {
2905 st.resize(ne, 0);
2906 }
2907 for bi in 0..b {
2908 let lg = &logits[bi * ne..(bi + 1) * ne];
2909 let mx = lg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2910 let mut p: Vec<f32> = lg.iter().map(|&l| (l - mx).exp()).collect();
2911 let sum: f32 = p.iter().sum();
2912 for v in &mut p {
2913 *v /= sum;
2914 }
2915 let mut order: Vec<usize> = (0..ne).collect();
2916 order.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y)));
2917 order.truncate(m.top_k);
2918 let wsum: f32 = if m.norm_topk_prob {
2919 order.iter().map(|&e| p[e]).sum()
2920 } else {
2921 1.0
2922 };
2923 for &e in &order {
2924 st[e] += 1;
2925 assign[e].push((bi, p[e] / wsum));
2926 }
2927 }
2928 }
2929
2930 let mut out = vec![0.0f32; b * hidden];
2931 let cols = m.experts[0].gate_proj.cols();
2932 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
2933 let sb = list.len();
2934 let mut sub = vec![0.0f32; sb * cols];
2935 for (k, &(bi, _)) in list.iter().enumerate() {
2936 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
2937 }
2938 let eo = dense_ffn_batch(d, &sub, sb, pool);
2939 for (k, &(bi, w)) in list.iter().enumerate() {
2940 for i in 0..hidden {
2941 out[bi * hidden + i] += w * eo[k * hidden + i];
2942 }
2943 }
2944 };
2945 for e in 0..ne {
2946 if !assign[e].is_empty() {
2947 run_expert(&m.experts[e], &assign[e]);
2948 }
2949 }
2950 if let Some((se, gate)) = &m.shared {
2951 let mut gl = vec![0.0f32; b];
2952 gate.matmat(xs, b, &mut gl, pool);
2953 let all: Vec<(usize, f32)> = (0..b)
2954 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
2955 .collect();
2956 run_expert(se, &all);
2957 }
2958 out
2959}
2960
2961thread_local! {
2962 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
2966 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
2967}
2968
2969fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2971 if crate::gpu::enabled_here()
2982 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
2983 {
2984 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
2985 crate::gpu::ProbeArm::Gpu
2986 } else {
2987 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
2988 };
2989 match arm {
2990 crate::gpu::ProbeArm::Gpu => {
2991 let t0 = std::time::Instant::now();
2992 if let Some(out) = dense_ffn_gpu(d, x, pool) {
2993 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
2994 return out;
2995 }
2996 }
2997 crate::gpu::ProbeArm::CpuTimed => {
2998 let t0 = std::time::Instant::now();
2999 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3000 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3001 return out;
3002 }
3003 crate::gpu::ProbeArm::Cpu => {
3004 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3005 }
3006 }
3007 }
3008 dense_ffn_cpu(d, x, pool)
3009}
3010
3011fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3013 let inter = d.gate_proj.rows();
3014 FFN_SCRATCH.with(|s| {
3015 let mut s = s.borrow_mut();
3016 let [g, u, ..] = &mut *s;
3017 g.resize(inter, 0.0);
3018 u.resize(inter, 0.0);
3019 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
3021 for i in 0..inter {
3022 g[i] = d.act.apply(g[i]) * u[i];
3023 }
3024 FFN_PROBE.with(|pr| {
3027 if let Some(acc) = pr.borrow_mut().as_mut() {
3028 let li = crate::gpu::cur_layer();
3029 if li >= 0 {
3030 if let Some(row) = acc.get_mut(li as usize) {
3031 for (a, &v) in row.iter_mut().zip(g.iter()) {
3032 *a += (v as f64).abs();
3033 }
3034 }
3035 }
3036 }
3037 });
3038 let mut out = attention::take_buf(d.down_proj.rows());
3039 d.down_proj.matvec(g, &mut out, pool);
3040 out
3041 })
3042}
3043
3044thread_local! {
3045 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
3048 const { std::cell::RefCell::new(None) };
3049}
3050
3051fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
3057 if d.act != Act::Silu {
3059 return None;
3060 }
3061 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
3064 return None;
3065 }
3066 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
3067 let mut model_ref = None;
3068 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
3069 let model = model_ref?;
3070 let hidden = jobs[0].down.1;
3071 let mut out = attention::take_buf(hidden);
3072 if crate::gpu::moe_block(&model, &jobs, &mut out) {
3073 Some(out)
3074 } else {
3075 let mut out = out;
3076 attention::recycle_buf(&mut out);
3077 None
3078 }
3079}
3080
3081#[allow(clippy::type_complexity)]
3086#[allow(clippy::type_complexity)]
3087fn moe_parts(
3088 t: &QTensor,
3089) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32], bool)> {
3090 match t {
3091 QTensor::Mapped {
3092 model,
3093 idx,
3094 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
3095 rows,
3096 cols,
3097 row_scale,
3098 col_field,
3099 ..
3100 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
3101 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
3102 }
3103 QTensor::Mapped {
3105 model,
3106 idx,
3107 dtype: cortiq_core::TensorDtype::Q1,
3108 rows,
3109 cols,
3110 ..
3111 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
3112 _ => None,
3113 }
3114}
3115
3116fn moe_push_job<'a>(
3118 d: &'a DenseFfn,
3119 x: &[f32],
3120 w: f32,
3121 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
3122 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
3123) -> Option<()> {
3124 use crate::qtensor::prescale;
3125 if d.act != Act::Silu {
3126 return None; }
3128 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
3129 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
3130 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
3131 if gq1 != uq1 || uq1 != dq1 {
3132 return None; }
3134 model_ref.get_or_insert_with(|| gm.clone());
3135 let gdt = if gcf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
3136 let udt = if ucf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
3137 jobs.push(crate::gpu::MoeJob {
3138 gate: (gi, gr, gc, grs),
3139 up: (ui, ur, uc, urs),
3140 down: (di, dr, dc, drs),
3141 xs_gate: prescale(x, gcf, gdt).into_owned(),
3142 xs_up: prescale(x, ucf, udt).into_owned(),
3143 down_col: dcf,
3144 w,
3145 q1: gq1,
3146 });
3147 Some(())
3148}
3149
3150fn sparse_ffn_quant(
3157 d: &DenseFfn,
3158 x: &[f32],
3159 active: &[u16],
3160 hidden: usize,
3161 pool: Option<&Pool>,
3162) -> Vec<f32> {
3163 let n = active.len();
3164 let inter = d.gate_proj.rows();
3165 let mut act = vec![0.0f32; n];
3166 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
3169 let compute = |ai: usize| -> f32 {
3170 let idx = active[ai] as usize;
3171 if idx >= inter {
3172 return 0.0; }
3174 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
3175 let gate = d.gate_proj.row_dot(idx, x, &mut s);
3176 let up = d.up_proj.row_dot(idx, x, &mut s);
3177 d.act.apply(gate) * up
3178 };
3179 match pool {
3180 Some(p) if n >= 256 => {
3181 let ptr = SendMut(act.as_mut_ptr());
3182 p.run(&|widx, nw| {
3183 let chunk = n.div_ceil(nw);
3184 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
3185 for ai in s..e {
3186 unsafe { *ptr.at(ai) = compute(ai) };
3187 }
3188 });
3189 }
3190 _ => {
3191 for (ai, a) in act.iter_mut().enumerate() {
3192 *a = compute(ai);
3193 }
3194 }
3195 }
3196 let mut out = vec![0.0f32; hidden];
3198 for (ai, &idx) in active.iter().enumerate() {
3199 let w = act[ai];
3200 if w.abs() >= 1e-12 && (idx as usize) < inter {
3201 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
3202 }
3203 }
3204 out
3205}
3206
3207#[doc(hidden)]
3209pub fn sparse_ffn_quant_for_test(
3210 d: &DenseFfn,
3211 x: &[f32],
3212 active: &[u16],
3213 hidden: usize,
3214) -> Vec<f32> {
3215 sparse_ffn_quant(d, x, active, hidden, None)
3216}
3217
3218fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
3222 let deq = |t: &QTensor| -> Vec<f32> {
3223 let (rows, cols) = (t.rows(), t.cols());
3224 let mut out = vec![0.0f32; rows * cols];
3225 for r in 0..rows {
3226 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
3227 }
3228 out
3229 };
3230 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
3231}
3232
3233struct SendMut(*mut f32);
3235unsafe impl Send for SendMut {}
3236unsafe impl Sync for SendMut {}
3237impl SendMut {
3238 #[inline]
3239 #[allow(clippy::mut_from_ref)]
3242 unsafe fn at(&self, i: usize) -> &mut f32 {
3243 unsafe { &mut *self.0.add(i) }
3244 }
3245}
3246
3247fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3251 let ne = m.experts.len();
3252 let mut logits = vec![0.0f32; ne];
3253 m.router.matvec(x, &mut logits, pool);
3254 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
3255 let mut p: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
3256 let s: f32 = p.iter().sum();
3257 for v in &mut p {
3258 *v /= s;
3259 }
3260 let mut idx: Vec<usize> = (0..ne).collect();
3261 idx.sort_unstable_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap().then(a.cmp(&b)));
3263 idx.truncate(m.top_k);
3264 let wsum: f32 = if m.norm_topk_prob {
3265 idx.iter().map(|&e| p[e]).sum()
3266 } else {
3267 1.0
3268 };
3269 {
3270 let mut st = m.stats.borrow_mut();
3271 if st.len() < ne {
3272 st.resize(ne, 0);
3273 }
3274 for &e in &idx {
3275 st[e] += 1;
3276 }
3277 }
3278 if crate::gpu::enabled_here() {
3283 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
3284 crate::gpu::ProbeArm::Gpu => {
3285 let t0 = std::time::Instant::now();
3286 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
3287 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3288 return out;
3289 }
3290 }
3291 crate::gpu::ProbeArm::CpuTimed => {
3292 let t0 = std::time::Instant::now();
3293 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
3294 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3295 return out;
3296 }
3297 crate::gpu::ProbeArm::Cpu => {
3298 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
3299 }
3300 }
3301 }
3302 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
3303}
3304
3305fn moe_ffn_cpu(
3307 m: &MoeFfn,
3308 x: &[f32],
3309 idx: &[usize],
3310 p: &[f32],
3311 wsum: f32,
3312 pool: Option<&Pool>,
3313) -> Vec<f32> {
3314 let mut out = attention::take_buf(x.len());
3315 for &e in idx {
3316 let mut eo = dense_ffn(&m.experts[e], x, pool);
3317 let w = p[e] / wsum;
3318 for i in 0..out.len() {
3319 out[i] += w * eo[i];
3320 }
3321 attention::recycle_buf(&mut eo);
3322 }
3323 if let Some((se, gate)) = &m.shared {
3324 let mut so = dense_ffn(se, x, pool);
3325 let mut gl = vec![0.0f32; 1];
3326 gate.matvec(x, &mut gl, pool);
3327 let g = 1.0 / (1.0 + (-gl[0]).exp());
3328 for i in 0..out.len() {
3329 out[i] += g * so[i];
3330 }
3331 attention::recycle_buf(&mut so);
3332 }
3333 out
3334}
3335
3336fn moe_ffn_gpu(
3339 m: &MoeFfn,
3340 x: &[f32],
3341 idx: &[usize],
3342 p: &[f32],
3343 wsum: f32,
3344 pool: Option<&Pool>,
3345) -> Option<Vec<f32>> {
3346 use crate::gpu::MoeJob;
3347
3348 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
3349 let mut model_ref = None;
3350 for &e in idx {
3351 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
3352 }
3353 if let Some((se, gate)) = &m.shared {
3354 let mut gl = vec![0.0f32; 1];
3355 gate.matvec(x, &mut gl, pool);
3356 let g = 1.0 / (1.0 + (-gl[0]).exp());
3357 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
3358 }
3359 let model = model_ref?;
3360 let hidden = jobs[0].down.1;
3361 let mut out = vec![0.0f32; hidden];
3362 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
3363}
3364
3365fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3367 match ffn {
3368 FfnKind::Dense(d) => dense_ffn(d, x, pool),
3369 FfnKind::Moe(m) => moe_ffn(m, x, pool),
3370 }
3371}
3372
3373fn ffn_forward_pair(
3377 ffn: &FfnKind,
3378 x1: &[f32],
3379 x2: &[f32],
3380 pool: Option<&Pool>,
3381) -> (Vec<f32>, Vec<f32>) {
3382 let d = match ffn {
3383 FfnKind::Dense(d) => d,
3384 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
3385 };
3386 let inter = d.gate_proj.rows();
3387 FFN_SCRATCH.with(|s| {
3388 let mut s = s.borrow_mut();
3389 let [g1, g2, u1, u2] = &mut *s;
3390 g1.resize(inter, 0.0);
3391 g2.resize(inter, 0.0);
3392 u1.resize(inter, 0.0);
3393 u2.resize(inter, 0.0);
3394 QTensor::matvec2_many(
3397 [&d.gate_proj, &d.up_proj],
3398 x1,
3399 x2,
3400 [g1.as_mut_slice(), u1.as_mut_slice()],
3401 [g2.as_mut_slice(), u2.as_mut_slice()],
3402 pool,
3403 );
3404 for i in 0..inter {
3405 g1[i] = d.act.apply(g1[i]) * u1[i];
3406 g2[i] = d.act.apply(g2[i]) * u2[i];
3407 }
3408 let mut o1 = attention::take_buf(d.down_proj.rows());
3409 let mut o2 = attention::take_buf(d.down_proj.rows());
3410 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
3411 (o1, o2)
3412 })
3413}
3414
3415#[cfg(test)]
3416mod tests {
3417 use super::*;
3418
3419 #[test]
3425 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
3426 let (hidden, inter) = (16usize, 40usize);
3427 let synth = |n: usize, salt: usize| -> Vec<f32> {
3428 (0..n)
3429 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
3430 .collect()
3431 };
3432 let d = DenseFfn {
3433 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
3434 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
3435 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
3436 act: Act::Silu,
3437 };
3438 let x = synth(hidden, 9);
3439 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
3441
3442 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
3443
3444 let mut g = vec![0.0f32; inter];
3446 d.gate_proj.matvec(&x, &mut g, None);
3447 let mut u = vec![0.0f32; inter];
3448 d.up_proj.matvec(&x, &mut u, None);
3449 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
3450 for i in 0..inter {
3451 g[i] = if act_set.contains(&(i as u16)) {
3452 inference::silu(g[i]) * u[i]
3453 } else {
3454 0.0
3455 };
3456 }
3457 let mut reference = vec![0.0f32; hidden];
3458 d.down_proj.matvec(&g, &mut reference, None);
3459
3460 let max_d = sparse
3461 .iter()
3462 .zip(&reference)
3463 .map(|(a, b)| (a - b).abs())
3464 .fold(0.0f32, f32::max);
3465 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
3466 }
3467
3468 fn attach_test_mtp(p: &mut Pipeline) {
3470 let (h, inter, heads, kv, hd) = (
3471 p.hidden_size,
3472 p.intermediate_size,
3473 p.num_heads,
3474 p.num_kv_heads,
3475 p.head_dim,
3476 );
3477 let synth = |n: usize, salt: usize| -> Vec<f32> {
3478 (0..n)
3479 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
3480 .collect()
3481 };
3482 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3483 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3484 };
3485 p.mtp = Some(MtpModule {
3486 enorm: vec![1.0; h],
3487 hnorm: vec![1.0; h],
3488 eh_proj: qt(h, 2 * h, 301),
3489 layer: LayerWeights {
3490 input_norm: vec![1.0; h],
3491 post_norm: vec![1.0; h],
3492 attn_out_norm: None,
3493 ffn_out_norm: None,
3494 layer_scale: None,
3495 ffn: FfnKind::Dense(DenseFfn {
3496 gate_proj: qt(inter, h, 315),
3497 up_proj: qt(inter, h, 316),
3498 down_proj: qt(h, inter, 317),
3499 act: Act::Silu,
3500 }),
3501 attn: AttnKind::Full {
3502 bias: None,
3503 wq: qt(heads * hd, h, 311),
3504 wk: qt(kv * hd, h, 312),
3505 wv: qt(kv * hd, h, 313),
3506 wo: qt(h, heads * hd, 314),
3507 q_norm: None,
3508 k_norm: None,
3509 output_gate: false,
3510 },
3511 },
3512 final_norm: vec![1.0; h],
3513 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
3514 });
3515 }
3516
3517 #[test]
3518 fn speculative_equals_vanilla_greedy() {
3519 let run = |spec: bool| {
3520 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3521 p.sampler_config.temperature = 0.0;
3522 attach_test_mtp(&mut p);
3523 p.speculative = spec;
3524 let r = p.generate("abcdef", 12, None, None).unwrap();
3525 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
3526 };
3527 let (vanilla, d0, _) = run(false);
3528 let (spec, d1, a1) = run(true);
3529 assert_eq!(d0, 0, "vanilla path must not draft");
3530 assert!(d1 > 0, "speculative path must draft");
3531 assert_eq!(
3532 vanilla, spec,
3533 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
3534 );
3535 }
3536
3537 #[test]
3538 fn speculative_accepts_constant_oracle() {
3539 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3540 p.sampler_config.temperature = 0.0;
3541 p.sampler_config.repetition_penalty = 1.0;
3542 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
3545 attach_test_mtp(&mut p);
3546 p.speculative = true;
3547 let r = p.generate("abcd", 10, None, None).unwrap();
3548 assert!(r.mtp_drafted > 0);
3549 assert_eq!(
3550 r.mtp_accepted, r.mtp_drafted,
3551 "constant logits → every draft accepted"
3552 );
3553 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
3556 }
3557
3558 #[test]
3559 fn empty_prompt_is_an_error_not_a_panic() {
3560 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3561 let r = p.generate("", 4, None, None);
3562 assert!(r.is_err(), "empty prompt must be a clean error");
3563 }
3564
3565 #[test]
3566 fn every_token_enters_kv_exactly_once() {
3567 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3568 p.sampler_config.temperature = 0.0;
3570 let r = p.generate("abc", 2, None, None).unwrap();
3571 assert_eq!(r.prompt_tokens, 3);
3572 assert_eq!(
3576 p.kv_cache.seq_len(),
3577 3 + r.tokens_generated - 1,
3578 "each token must be cached exactly once (v1 cached the last prompt token twice)"
3579 );
3580 }
3581
3582 #[test]
3583 fn generation_is_reproducible_with_seed() {
3584 let run = || {
3585 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3586 p.generate("hello", 8, None, None).unwrap().token_ids
3587 };
3588 assert_eq!(run(), run());
3589 }
3590
3591 #[test]
3592 fn eviction_bounds_the_cache() {
3593 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3594 p.kv_cache.max_seq_len = 6;
3595 p.sampler_config.temperature = 0.0;
3596 let _ = p.generate("abcd", 12, None, None).unwrap();
3597 assert!(
3598 p.kv_cache.seq_len() <= 6 + 1,
3599 "cache must stay bounded by max_seq_len (got {})",
3600 p.kv_cache.seq_len()
3601 );
3602 }
3603
3604 #[test]
3605 fn confidence_matches_tokens_and_is_a_probability() {
3606 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3607 p.sampler_config.temperature = 0.0;
3608 p.sampler_config.repetition_penalty = 1.0;
3609 let r = p.generate("abcd", 10, None, None).unwrap();
3610 assert_eq!(
3611 r.token_confidence.len(),
3612 r.token_ids.len(),
3613 "one confidence per emitted token"
3614 );
3615 for &c in &r.token_confidence {
3616 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
3617 }
3618 let logits = [1.0f32, 3.0, 0.5, 3.0];
3620 let p0 = top1_prob_t(&logits, 1, 1.0);
3621 let p1 = top1_prob_t(&logits, 3, 1.0);
3622 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
3623 assert!(p0 > 0.0 && p0 < 1.0);
3624 let sharp = top1_prob_t(&logits, 1, 1.0);
3626 let soft = top1_prob_t(&logits, 1, 2.0);
3627 assert!(soft < sharp, "higher temperature lowers peak confidence");
3628 }
3629
3630 #[test]
3631 fn trace_is_opt_in_and_parallels_the_output() {
3632 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3634 p.sampler_config.temperature = 0.0;
3635 p.sampler_config.repetition_penalty = 1.0;
3636 let r = p.generate("abcd", 10, None, None).unwrap();
3637 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
3638
3639 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3641 p.sampler_config.temperature = 0.0;
3642 p.sampler_config.repetition_penalty = 1.0;
3643 p.set_trace(true);
3644 let r = p.generate("abcd", 10, None, None).unwrap();
3645 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
3646 for (i, tr) in r.traces.iter().enumerate() {
3647 assert_eq!(tr.t, i, "trace index is sequential");
3648 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
3649 assert_eq!(
3650 tr.confidence, r.token_confidence[i],
3651 "trace confidence matches the confidence channel"
3652 );
3653 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
3655 }
3656 }
3657
3658 #[test]
3659 fn explain_prefill_logits_match_greedy_first_token() {
3660 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3664 p.sampler_config.temperature = 0.0;
3665 p.sampler_config.repetition_penalty = 1.0;
3666 let ids = p.tokenizer.encode("abcd");
3667 let logits = p.prefill_next_logits(&ids, None);
3668 let argmax = logits
3669 .iter()
3670 .enumerate()
3671 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3672 .unwrap()
3673 .0 as u32;
3674 let r = p.generate("abcd", 1, None, None).unwrap();
3675 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
3676 }
3677}