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