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