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 false
435 }
436
437 #[cfg(target_os = "macos")]
438 fn q1_graph_gpu(
439 &mut self,
440 start: usize,
441 upto: Option<usize>,
442 position: usize,
443 h: &mut [f32],
444 ) -> usize {
445 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
446 if !crate::gpu::enabled_here()
447 || !crate::gpu::q1_force()
448 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
449 {
450 return start;
451 }
452 if self.swa.is_some()
457 || self.global_attn.is_some()
458 || self.attn_v_norm
459 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
460 || self.weights.layers.iter().any(|lw| {
461 lw.attn_out_norm.is_some()
462 || lw.ffn_out_norm.is_some()
463 || lw.layer_scale.is_some()
464 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
465 })
466 {
467 return start;
468 }
469 let limit = upto.map(|u| u + 1).unwrap_or(self.num_layers).min(self.num_layers);
470
471 enum Item<'a> {
472 Gdn {
473 run: Vec<GdnGpuLayer<'a>>,
474 first: usize,
475 },
476 Attn {
477 l: AttnGpuLayer<'a>,
478 li: usize,
479 q_norm: Option<&'a [f32]>,
480 k_norm: Option<&'a [f32]>,
481 output_gate: bool,
482 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
483 full_gpu: bool,
486 },
487 }
488
489 let dev_attend = std::env::var("CMF_GPU_ATTEND").map(|v| v != "0").unwrap_or(true)
491 && self.head_dim % 4 == 0
492 && self.head_dim <= 128
493 && self.rotary_dim >= 2
494 && self.rotary_dim <= self.head_dim
495 && (self.rotary_dim / 2) % 32 == 0
496 && self.num_kv_heads > 0
497 && self.num_heads % self.num_kv_heads == 0;
498
499 let mut plan: Vec<Item> = Vec::new();
500 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
501 let mut scan = start;
502 while scan < limit {
503 let lw = &self.weights.layers[scan];
504 let FfnKind::Dense(d) = &lw.ffn else { break };
505 let (Some(g), Some(u), Some(dn)) =
506 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
507 else {
508 break;
509 };
510 match &lw.attn {
511 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
512 let parts = (
513 w.in_proj_qkv.q1_parts(),
514 w.in_proj_z.q1_parts(),
515 w.in_proj_a.f32_parts(),
516 w.in_proj_b.f32_parts(),
517 w.out_proj.q1_parts(),
518 );
519 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else { break };
520 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
521 model_ref.get_or_insert_with(|| model.clone());
522 }
523 let gl = GdnGpuLayer {
524 attn_norm: &lw.input_norm,
525 post_norm: &lw.post_norm,
526 qkv,
527 z,
528 a,
529 b,
530 out,
531 gate: g,
532 up: u,
533 down: dn,
534 conv1d: &w.conv1d,
535 a_log: &w.a_log,
536 dt_bias: &w.dt_bias,
537 gnorm: &w.norm,
538 };
539 match plan.last_mut() {
540 Some(Item::Gdn { run, .. }) => run.push(gl),
541 _ => plan.push(Item::Gdn { run: vec![gl], first: scan }),
542 }
543 }
544 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, bias }
545 if !self.kv_cache.layers[scan].o1_sealed() =>
546 {
547 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
548 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else { break };
549 if let QTensor::Mapped { model, .. } = wq {
550 model_ref.get_or_insert_with(|| model.clone());
551 }
552 let cache = &self.kv_cache.layers[scan];
553 let full_gpu = dev_attend
554 && cache.mode == crate::kv_cache::KvMode::F32
555 && cache.o1.is_none()
556 && bias.is_none()
557 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
558 && pk.1 == self.num_kv_heads * self.head_dim
559 && pv.1 == self.num_kv_heads * self.head_dim
560 && po.2 == self.num_heads * self.head_dim;
561 plan.push(Item::Attn {
562 l: AttnGpuLayer {
563 attn_norm: &lw.input_norm,
564 post_norm: &lw.post_norm,
565 wq: pq,
566 wk: pk,
567 wv: pv,
568 wo: po,
569 gate: g,
570 up: u,
571 down: dn,
572 },
573 li: scan,
574 q_norm: q_norm.as_deref(),
575 k_norm: k_norm.as_deref(),
576 output_gate: *output_gate,
577 bias: bias
578 .as_ref()
579 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
580 full_gpu,
581 });
582 }
583 _ => break,
584 }
585 scan += 1;
586 }
587 let Some(model) = model_ref else { return start };
588 if plan.is_empty() {
589 return start;
590 }
591 let dims = GraphDims {
592 hidden: self.hidden_size,
593 eps: self.rms_eps as f32,
594 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
595 };
596 let Some(mut graph) = TokenGraph::new(&model, dims, h) else { return start };
597 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
598 nv: cfg.num_v_heads,
599 nk: cfg.num_k_heads,
600 dk: cfg.key_head_dim,
601 dv: cfg.value_head_dim,
602 kk: cfg.conv_kernel,
603 hidden: self.hidden_size,
604 inter: self.intermediate_size,
605 c_dim: cfg.conv_dim(),
606 eps: cfg.rms_eps as f32,
607 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
608 });
609 let mut valid = 0usize;
613 let mut end = start;
614 for item in &plan {
615 let ok = match item {
616 Item::Gdn { run, .. } => gcfg
617 .as_ref()
618 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
619 .unwrap_or(false),
620 Item::Attn { l, .. } => graph.attn_ok(l),
621 };
622 if !ok {
623 break;
624 }
625 valid += 1;
626 end += match item {
627 Item::Gdn { run, .. } => run.len(),
628 Item::Attn { .. } => 1,
629 };
630 }
631 plan.truncate(valid);
632 if plan.is_empty() {
633 return start;
634 }
635
636 let inv_freq = self.inv_freq.clone();
637 let pool = self.pool.clone();
638 let (nh, nkv, hd, hs, rd, eps) = (
639 self.num_heads,
640 self.num_kv_heads,
641 self.head_dim,
642 self.hidden_size,
643 self.rotary_dim,
644 self.rms_eps,
645 );
646 let norm_style = self.norm_style;
647 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
648 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
649 let kv_id = self.graph_kv_id;
650 let mut pending: Vec<(usize, usize)> = Vec::new();
653 let mut dev_attn: Vec<usize> = Vec::new();
656 for item in &plan {
657 match item {
658 Item::Gdn { run, first } => {
659 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
660 if l.linear_state.len() != want {
661 l.linear_state = vec![0f32; want];
662 }
663 }
664 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
665 .iter()
666 .map(|l| l.linear_state.as_slice())
667 .collect();
668 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
669 tracing::error!("q1 graph: GDN run refused after validation");
671 return start;
672 }
673 graph.commit();
676 pending.push((*first, run.len()));
677 }
678 Item::Attn { l, li, q_norm, k_norm, output_gate, bias, full_gpu } => {
679 if *full_gpu {
681 let cache = &self.kv_cache.layers[*li];
682 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
683 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
684 let cpu_stored = cpu_k[0].len() / hd;
685 let p = crate::gpu::AttnDeviceParams {
686 kv_id,
687 layer: *li,
688 nh,
689 nkv,
690 hd,
691 rd,
692 position,
693 eps: eps as f32,
694 gemma,
695 output_gate: *output_gate,
696 q_norm: *q_norm,
697 k_norm: *k_norm,
698 inv_freq: &inv_freq,
699 cpu_k,
700 cpu_v,
701 cpu_stored,
702 };
703 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
704 graph.commit();
705 dev_attn.push(*li);
706 continue;
707 }
708 }
710 graph.encode_attn_prefix(l);
711 graph.sync();
712 if !pending.is_empty() {
713 let idxs: Vec<usize> =
714 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
715 let mut outs: Vec<&mut [f32]> = self
716 .kv_cache
717 .layers
718 .iter_mut()
719 .enumerate()
720 .filter(|(i, _)| idxs.binary_search(i).is_ok())
721 .map(|(_, s)| s.linear_state.as_mut_slice())
722 .collect();
723 graph.read_states(&mut outs);
724 }
725 let mut q_raw = attention::take_buf(l.wq.1);
726 let mut k = attention::take_buf(l.wk.1);
727 let mut v = attention::take_buf(l.wv.1);
728 graph.read_qkv(&mut q_raw, &mut k, &mut v);
729 let cfg = QwenAttnCfg {
730 num_heads: nh,
731 num_kv_heads: nkv,
732 head_dim: hd,
733 hidden_size: hs,
734 position,
735 inv_freq: &inv_freq,
736 rotary_dim: rd,
737 scale: self.attn_scale,
738 window: None,
739 v_norm: false,
740 q_norm: *q_norm,
741 k_norm: *k_norm,
742 output_gate: *output_gate,
743 bias: *bias,
744 rms_eps: eps,
745 norm_style,
746 pool: pool.as_deref(),
747 };
748 let mut ao = attention::qwen_attention_core(
749 q_raw,
750 k,
751 v,
752 &mut self.kv_cache.layers[*li],
753 &cfg,
754 );
755 graph.encode_attn_suffix(l, &ao);
756 graph.commit();
759 attention::recycle_buf(&mut ao);
760 }
761 }
762 }
763 let mut lm_rows = None;
768 if self.graph_want_logits
769 && upto.is_none()
770 && end == self.num_layers
771 && std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0").unwrap_or(true)
772 {
773 if let Some(lm) = self.weights.lm_head.q1_parts() {
774 if graph.lm_head_ok(lm) {
775 graph.encode_lm_head(&self.weights.final_norm, lm);
776 lm_rows = Some(lm.1);
777 }
778 }
779 }
780 graph.sync();
781 if !pending.is_empty() {
782 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
783 let mut outs: Vec<&mut [f32]> = self
784 .kv_cache
785 .layers
786 .iter_mut()
787 .enumerate()
788 .filter(|(i, _)| idxs.binary_search(i).is_ok())
789 .map(|(_, s)| s.linear_state.as_mut_slice())
790 .collect();
791 graph.read_states(&mut outs);
792 }
793 if let Some(rows) = lm_rows {
794 let mut lg = attention::take_buf(rows.min(self.vocab_size));
795 graph.read_logits(&mut lg);
796 lg.resize(self.vocab_size, 0.0);
797 if let Some(c) = self.final_softcap {
798 for l in lg.iter_mut() {
799 *l = c * (*l / c).tanh();
800 }
801 }
802 self.graph_logits = Some(lg);
803 }
804 graph.finish(h);
805 for li in dev_attn {
809 let mut krow = attention::take_buf(nkv * hd);
810 let mut vrow = attention::take_buf(nkv * hd);
811 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
812 let cache = &mut self.kv_cache.layers[li];
813 cache.append(&krow, &vrow, &[]);
814 let n = cache.seq_len;
815 let mut imp = attention::take_buf(n);
816 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
817 cache.accumulate_imp(&imp);
818 attention::recycle_buf(&mut imp);
819 }
820 attention::recycle_buf(&mut krow);
821 attention::recycle_buf(&mut vrow);
822 }
823 end
824 }
825
826 pub fn new(
827 tokenizer: Tokenizer,
828 weights: PipelineWeights,
829 hidden_size: usize,
830 intermediate_size: usize,
831 num_heads: usize,
832 num_kv_heads: usize,
833 head_dim: usize,
834 num_layers: usize,
835 vocab_size: usize,
836 rms_eps: f64,
837 rope_base: f32,
838 norm_style: NormStyle,
839 max_seq_len: usize,
840 sampler_config: SamplerConfig,
841 ) -> Self {
842 let rng = match sampler_config.seed {
843 Some(s) => SplitMix64::new(s),
844 None => SplitMix64::from_entropy(),
845 };
846 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
847 let pool = Pool::from_env();
848 if let Some(p) = &pool {
849 tracing::info!("worker pool: {} threads", p.n_workers());
850 }
851 Self {
852 tokenizer: std::sync::Arc::new(tokenizer),
853 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
854 sampler_config,
855 weights,
856 hidden_size,
857 intermediate_size,
858 num_heads,
859 num_kv_heads,
860 head_dim,
861 num_layers,
862 vocab_size,
863 rms_eps,
864 rope_base,
865 norm_style,
866 rotary_dim: head_dim,
867 vmf_cfg: None,
868 gdn_cfg: None,
869 short_conv_cfg: None,
870 mtp: None,
871 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
872 rng,
873 inv_freq,
874 ws: ForwardScratch::new(hidden_size),
875 pool,
876 model: None,
877 dyn_force_f32: false,
878 dyn_skill_layers: Vec::new(),
879 dyn_active: None,
880 dyn_blend_loaded: false,
881 dyn_phi_layer: None,
882 dyn_phi_ema: Vec::new(),
883 dyn_phi_seen: 0,
884 dyn_router: None,
885 o1_cfg: None,
886 o1_flags: Vec::new(),
887 trace: false,
888 calib_temp: 1.0,
889 confidence_on: true,
890 embed_multiplier: 1.0,
891 attn_scale: 1.0 / (head_dim as f32).sqrt(),
892 swa: None,
893 inv_freq_local: None,
894 global_attn: None,
895 inv_freq_global: None,
896 attn_v_norm: false,
897 final_softcap: None,
898 graph_want_logits: false,
899 graph_logits: None,
900 graph_kv_id: {
901 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
902 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
903 },
904 }
905 }
906
907 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
914 self.o1_flags = match &cfg {
915 Some(c) => {
916 let mut flags = c.layer_flags(self.num_layers);
917 for (li, f) in flags.iter_mut().enumerate() {
918 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
919 *f = false;
920 }
921 }
922 flags
923 }
924 None => Vec::new(),
925 };
926 if let Some(c) = &cfg {
927 let n = self.o1_flags.iter().filter(|&&f| f).count();
928 tracing::info!(
929 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
930 self.num_layers, c.m, c.w, c.sink, c.rect
931 );
932 }
933 self.o1_cfg = cfg;
934 }
935
936 pub fn o1_active(&self) -> bool {
938 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
939 }
940
941 fn o1_begin(&mut self) {
943 if let Some(c) = &self.o1_cfg {
944 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
945 for (li, &f) in self.o1_flags.iter().enumerate() {
946 if f {
947 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
948 }
949 }
950 }
951 }
952
953 fn o1_seal(&mut self) {
956 if self.o1_cfg.is_none() {
957 return;
958 }
959 for li in 0..self.num_layers {
960 if self.o1_flags.get(li).copied().unwrap_or(false) {
961 self.kv_cache.layers[li].o1_seal(self.num_heads);
962 }
963 }
964 }
965
966 pub fn set_trace(&mut self, on: bool) {
968 self.trace = on;
969 }
970
971 pub fn set_confidence(&mut self, on: bool) {
976 self.confidence_on = on;
977 }
978
979 pub fn set_calib_temp(&mut self, t: f32) {
982 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
983 }
984
985 pub fn calib_temp(&self) -> f32 {
987 self.calib_temp
988 }
989
990 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
993 self.rotary_dim = rotary_dim.min(self.head_dim);
994 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
995 }
996
997 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
998 QwenAttnCfg {
999 num_heads: self.num_heads,
1000 num_kv_heads: self.num_kv_heads,
1001 head_dim: self.head_dim,
1002 hidden_size: self.hidden_size,
1003 position,
1004 inv_freq: &self.inv_freq,
1005 rotary_dim: self.rotary_dim,
1006 scale: self.attn_scale,
1007 window: None,
1008 v_norm: false,
1009 q_norm: None,
1010 k_norm: None,
1011 output_gate: false,
1012 bias: None,
1013 rms_eps: self.rms_eps,
1014 norm_style: self.norm_style,
1015 pool: self.pool.as_deref(),
1016 }
1017 }
1018
1019 pub fn generate(
1021 &mut self,
1022 prompt: &str,
1023 max_tokens: usize,
1024 task_mask: Option<&TaskMask>,
1025 on_token: Option<TokenCallback>,
1026 ) -> Result<GenerateResult, String> {
1027 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1028 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1029 }
1030
1031 pub fn generate_from_ids(
1039 &mut self,
1040 input_ids: &[u32],
1041 max_tokens: usize,
1042 task_mask: Option<&TaskMask>,
1043 mut on_token: Option<TokenCallback>,
1044 ) -> Result<GenerateResult, String> {
1045 if std::env::var("CMF_TRACE_H").is_ok() {
1046 eprintln!("input_ids: {input_ids:?}");
1047 }
1048 if input_ids.is_empty() {
1049 return Err("empty prompt: nothing to generate from".to_string());
1050 }
1051
1052 self.kv_cache.clear();
1054 self.o1_begin();
1055
1056 let spec_active = self.speculative
1060 && self.mtp.is_some()
1061 && task_mask.is_none()
1062 && !self.o1_active()
1063 && self.sampler_config.temperature < 1e-6;
1064 let mut mtp = if spec_active { self.mtp.take() } else { None };
1067 if let Some(m) = &mut mtp {
1068 m.kv.clear();
1069 }
1070 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
1074 if let Some(r) = &mut router {
1075 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1078 }
1079
1080 let mut all_ids = input_ids.to_vec();
1081 let mut generated = 0usize;
1082 let mut finish_reason = "max_tokens".to_string();
1083 let mut drafted = 0usize;
1084 let mut accepted = 0usize;
1085 let mut confidence: Vec<f32> = Vec::new();
1086 let trace_on = self.trace;
1087 let calib_temp = self.calib_temp;
1088 let mut traces: Vec<TokenTrace> = Vec::new();
1089
1090 let mut hidden = vec![0.0f32; self.hidden_size];
1096 let mut pos = 0usize;
1097 let fuse_lm = mtp.is_none() && router.is_none();
1101 self.graph_logits = None;
1102 self.graph_want_logits = false;
1103 let dyn_prefill = router.is_some();
1108 let graph_prefill = self.graph_prefill_preferred();
1114 if task_mask.is_none()
1115 && !dyn_prefill
1116 && !graph_prefill
1117 && prefill_batched()
1118 && input_ids.len() > 2
1119 {
1120 let chunk = prefill_chunk();
1126 let hs = self.hidden_size;
1127 while pos < input_ids.len() {
1128 let end = (pos + chunk).min(input_ids.len());
1129 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1130 if let Some(m) = &mut mtp {
1131 for p in pos..end {
1132 if p + 1 < input_ids.len() {
1133 let _ = self.mtp_step(
1134 m,
1135 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1136 input_ids[p + 1],
1137 p,
1138 );
1139 }
1140 }
1141 }
1142 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1143 pos = end;
1144 }
1145 }
1146 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
1147 while pos + 1 < input_ids.len() {
1148 let e1 = self.embed_single(input_ids[pos]);
1149 let e2 = self.embed_single(input_ids[pos + 1]);
1150 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1151 self.commit_linear_scratch();
1153 if let Some(m) = &mut mtp {
1154 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1155 if pos + 2 < input_ids.len() {
1156 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1157 }
1158 }
1159 hidden = h2;
1160 pos += 2;
1161 }
1162 }
1163 while pos < input_ids.len() {
1164 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1165 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1166 if let Some(m) = &mut mtp {
1167 if pos + 1 < input_ids.len() {
1168 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1169 }
1170 }
1171 pos += 1;
1172 }
1173 self.o1_seal();
1176
1177 macro_rules! commit {
1179 ($id:expr) => {{
1180 all_ids.push($id);
1181 generated += 1;
1182 if self.tokenizer.is_eos($id) {
1183 finish_reason = "stop".to_string();
1184 false
1185 } else {
1186 let token_text = self.tokenizer.decode_token($id);
1187 let mut go = true;
1188 if let Some(ref mut cb) = on_token {
1189 if !cb(&token_text) {
1190 finish_reason = "cancelled".to_string();
1191 go = false;
1192 }
1193 }
1194 go
1195 }
1196 }};
1197 }
1198
1199 let mut next_pos = input_ids.len();
1201 'decode: while generated < max_tokens {
1202 let mut logits = match self.graph_logits.take() {
1203 Some(lg) => lg,
1204 None => {
1205 inference::rms_norm_into(
1206 &hidden,
1207 &self.weights.final_norm,
1208 self.rms_eps,
1209 self.norm_style,
1210 &mut self.ws.n1,
1211 );
1212 self.lm_head_forward(&self.ws.n1)
1213 }
1214 };
1215 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
1216 if self.confidence_on {
1217 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1218 }
1219 attention::recycle_buf(&mut logits);
1220 if trace_on {
1221 let skill = router.as_ref().and_then(|r| r.active_id());
1225 traces.push(TokenTrace {
1226 t: generated,
1227 token_id: t_next,
1228 confidence: confidence.last().copied().unwrap_or(0.0),
1229 active_skill: skill,
1230 recon: None,
1231 switched: false,
1232 });
1233 }
1234 if !commit!(t_next) {
1235 break 'decode;
1236 }
1237 if generated >= max_tokens {
1238 break 'decode;
1239 }
1240
1241 if self.kv_cache.needs_eviction() {
1242 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1243 self.kv_cache.evict(keep);
1244 }
1245
1246 match &mut mtp {
1247 Some(m) if generated + 1 < max_tokens => {
1249 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1250 drafted += 1;
1251 let emb1 = self.embed_single(t_next);
1252 let emb2 = self.embed_single(draft);
1253 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1254
1255 inference::rms_norm_into(
1256 &h1,
1257 &self.weights.final_norm,
1258 self.rms_eps,
1259 self.norm_style,
1260 &mut self.ws.n1,
1261 );
1262 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1263 let t_after =
1264 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
1265 if self.confidence_on {
1266 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1267 }
1268 attention::recycle_buf(&mut logits1);
1269 if trace_on {
1270 traces.push(TokenTrace {
1273 t: generated,
1274 token_id: t_after,
1275 confidence: confidence.last().copied().unwrap_or(0.0),
1276 active_skill: None,
1277 recon: None,
1278 switched: false,
1279 });
1280 }
1281 let stop = !commit!(t_after);
1282
1283 if t_after == draft {
1284 accepted += 1;
1285 self.commit_linear_scratch();
1286 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1287 hidden = h2;
1288 next_pos += 2;
1289 } else {
1290 for layer in &mut self.kv_cache.layers {
1292 layer.truncate_last(1);
1293 }
1294 if !stop {
1295 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1296 hidden = self
1297 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
1298 }
1299 next_pos += 2;
1300 }
1301 if stop {
1302 break 'decode;
1303 }
1304 }
1305 _ => {
1307 self.graph_want_logits = fuse_lm;
1308 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1309 next_pos += 1;
1310 if let Some(r) = &mut router {
1313 let phi = self.dyn_phi_ema.clone();
1314 let decision = r.step(&phi, generated);
1315 if let Some(new_active) = decision {
1316 let _ = self.set_active_skill(new_active);
1317 }
1318 if trace_on {
1321 if let Some(last) = traces.last_mut() {
1322 let e = r.last_best_e();
1323 last.recon = e.is_finite().then_some(e);
1324 last.switched = decision.is_some();
1325 }
1326 }
1327 }
1328 }
1329 }
1330 }
1331
1332 self.graph_want_logits = false;
1333 self.graph_logits = None;
1334 if router.is_some() {
1336 let _ = self.set_active_skill(None);
1337 }
1338 self.dyn_router = router.or(self.dyn_router.take());
1339 self.mtp = mtp.or(self.mtp.take());
1340
1341 let output_ids = &all_ids[input_ids.len()..];
1342 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1344 Ok(GenerateResult {
1345 text: self.tokenizer.decode(output_ids),
1346 token_ids: output_ids.to_vec(),
1347 prompt_tokens: input_ids.len(),
1348 tokens_generated: generated,
1349 finish_reason,
1350 mtp_drafted: drafted,
1351 mtp_accepted: accepted,
1352 token_confidence: confidence,
1353 traces,
1354 })
1355 }
1356
1357 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
1361 let e = self.embed_single(next_token);
1365 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1366 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1367 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1368 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1369 let mut x = vec![0.0f32; self.hidden_size];
1370 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1371
1372 let lw = &m.layer;
1374 inference::rms_norm_into(&x, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1375 let attn = match &lw.attn {
1376 AttnKind::Full {
1377 wq,
1378 wk,
1379 wv,
1380 wo,
1381 q_norm,
1382 k_norm,
1383 output_gate,
1384 bias,
1385 } => {
1386 let mut cfg = self.attn_cfg(position);
1387 cfg.q_norm = q_norm.as_deref();
1388 cfg.k_norm = k_norm.as_deref();
1389 cfg.output_gate = *output_gate;
1390 cfg.bias = bias
1391 .as_ref()
1392 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1393 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1394 }
1395 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
1396 unreachable!("MTP block is full attention")
1397 }
1398 };
1399 for (i, &a) in attn.iter().enumerate() {
1400 x[i] += a;
1401 }
1402 inference::rms_norm_into(&x, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1403 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1404 for (i, &f) in ffn.iter().enumerate() {
1405 x[i] += f;
1406 }
1407
1408 inference::rms_norm_into(&x, &m.final_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1409 let mut lg = self.lm_head_forward(&self.ws.n1);
1410 let draft = sampler::argmax(&lg);
1411 attention::recycle_buf(&mut lg);
1412 draft
1413 }
1414
1415 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1419 let emb1 = self.embed_single(1);
1420 let emb2 = self.embed_single(2);
1421 let pos = self.kv_cache.seq_len();
1422
1423 let t0 = std::time::Instant::now();
1424 for _ in 0..iters {
1425 let _ = self.forward_layers(&emb1, pos, None);
1426 let _ = self.forward_layers(&emb2, pos + 1, None);
1427 for l in &mut self.kv_cache.layers {
1428 l.truncate_last(2);
1429 }
1430 }
1431 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1432
1433 let t1 = std::time::Instant::now();
1434 for _ in 0..iters {
1435 let _ = self.forward_pair(&emb1, &emb2, pos);
1436 for l in &mut self.kv_cache.layers {
1437 l.truncate_last(2);
1438 }
1439 }
1440 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1441 (singles_ms, pair_ms)
1442 }
1443
1444 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
1449 let mut h1 = emb1.to_vec();
1450 let mut h2 = emb2.to_vec();
1451 let (nh, _nkv, _hd, hs, _rd, eps) = (
1452 self.num_heads,
1453 self.num_kv_heads,
1454 self.head_dim,
1455 self.hidden_size,
1456 self.rotary_dim,
1457 self.rms_eps,
1458 );
1459 let pool = self.pool.clone();
1460
1461 for li in 0..self.num_layers {
1462 let lw = &self.weights.layers[li];
1463 inference::rms_norm_into(&h1, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1466 inference::rms_norm_into(&h2, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n2);
1467
1468 let (a1, a2) = match &lw.attn {
1469 AttnKind::Linear(w) => {
1470 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1471 let layer = &mut self.kv_cache.layers[li];
1472 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1473 vmf_phase_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1474 }
1475 AttnKind::LinearGdn(w) => {
1476 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1477 let layer = &mut self.kv_cache.layers[li];
1478 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1479 gdn_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1480 }
1481 AttnKind::ShortConv(w) => {
1482 let cfg = self.short_conv_cfg.expect("short-conv layer without short_conv_cfg");
1483 let layer = &mut self.kv_cache.layers[li];
1484 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1485 short_conv_pair(
1486 &self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref(),
1487 )
1488 }
1489 AttnKind::Full {
1490 wq,
1491 wk,
1492 wv,
1493 wo,
1494 q_norm,
1495 k_norm,
1496 output_gate,
1497 bias,
1498 } => {
1499 let inv_freq_l = self.layer_inv_freq(li);
1500 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
1501 let cfg = QwenAttnCfg {
1502 num_heads: nh,
1503 num_kv_heads: nkv_l,
1504 head_dim: hd_l,
1505 hidden_size: hs,
1506 position,
1507 inv_freq: &inv_freq_l,
1508 rotary_dim: rd_l,
1509 scale: self.attn_scale,
1510 window: self.layer_window(li),
1511 v_norm: self.attn_v_norm,
1512 q_norm: q_norm.as_deref(),
1513 k_norm: k_norm.as_deref(),
1514 output_gate: *output_gate,
1515 bias: bias
1516 .as_ref()
1517 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1518 rms_eps: eps,
1519 norm_style: self.norm_style,
1520 pool: pool.as_deref(),
1521 };
1522 attention::qwen_attention_pair(
1523 &self.ws.n1,
1524 &self.ws.n2,
1525 wq,
1526 wk,
1527 wv,
1528 wo,
1529 &mut self.kv_cache.layers[li],
1530 &cfg,
1531 )
1532 }
1533 };
1534 let (a1, a2) = match &self.weights.layers[li].attn_out_norm {
1535 Some(w) => (
1536 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
1537 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
1538 ),
1539 None => (a1, a2),
1540 };
1541 for i in 0..self.hidden_size {
1542 h1[i] += a1[i];
1543 h2[i] += a2[i];
1544 }
1545 let (mut a1, mut a2) = (a1, a2);
1546 attention::recycle_buf(&mut a1);
1547 attention::recycle_buf(&mut a2);
1548
1549 let lw = &self.weights.layers[li];
1550 inference::rms_norm_into(&h1, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1551 inference::rms_norm_into(&h2, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p2);
1552 let (f1, f2) =
1553 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1554 let (f1, f2) = match &self.weights.layers[li].ffn_out_norm {
1555 Some(w) => (
1556 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
1557 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
1558 ),
1559 None => (f1, f2),
1560 };
1561 for i in 0..self.hidden_size {
1562 h1[i] += f1[i];
1563 h2[i] += f2[i];
1564 }
1565 let (mut f1, mut f2) = (f1, f2);
1566 attention::recycle_buf(&mut f1);
1567 attention::recycle_buf(&mut f2);
1568 if let Some(sc) = self.weights.layers[li].layer_scale {
1569 for i in 0..self.hidden_size {
1570 h1[i] *= sc;
1571 h2[i] *= sc;
1572 }
1573 }
1574 }
1575 (h1, h2)
1576 }
1577
1578 fn commit_linear_scratch(&mut self) {
1580 for layer in &mut self.kv_cache.layers {
1581 if !layer.linear_scratch.is_empty() {
1582 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1583 layer.linear_scratch.clear();
1584 }
1585 }
1586 }
1587
1588 pub fn forward_ids(
1591 &mut self,
1592 ids: &[u32],
1593 task_mask: Option<&TaskMask>,
1594 ) -> Result<Vec<f32>, String> {
1595 if ids.is_empty() {
1596 return Err("empty id sequence".to_string());
1597 }
1598 self.kv_cache.clear();
1599 self.o1_begin();
1600 let mut hidden = vec![0.0f32; self.hidden_size];
1601 let mut pos = 0usize;
1602 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1603 let chunk = prefill_chunk();
1607 let hs = self.hidden_size;
1608 while pos < ids.len() {
1609 let end = (pos + chunk).min(ids.len());
1610 let hb = self.prefill_batch(&ids[pos..end], pos);
1611 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1612 pos = end;
1613 }
1614 }
1615 if task_mask.is_none() {
1616 while pos + 1 < ids.len() {
1617 let e1 = self.embed_single(ids[pos]);
1618 let e2 = self.embed_single(ids[pos + 1]);
1619 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1620 self.commit_linear_scratch();
1621 hidden = h2;
1622 pos += 2;
1623 }
1624 }
1625 while pos < ids.len() {
1626 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1627 pos += 1;
1628 }
1629 self.o1_seal();
1633 let normed = inference::rms_norm(
1634 &hidden,
1635 &self.weights.final_norm,
1636 self.rms_eps,
1637 self.norm_style,
1638 );
1639 Ok(self.lm_head_forward(&normed))
1640 }
1641
1642 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1649 let (nll, cnt) = self.nll_ids_from(ids, 0);
1650 (nll / cnt.max(1) as f64).exp()
1651 }
1652
1653 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
1658 self.kv_cache.clear();
1659 FFN_PROBE.with(|p| {
1660 *p.borrow_mut() =
1661 Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
1662 });
1663 crate::gpu::cpu_scope(|| {
1664 for (pos, &id) in ids.iter().enumerate() {
1665 let emb = self.embed_single(id);
1666 let _ = self.forward_layers(&emb, pos, None);
1667 }
1668 });
1669 self.kv_cache.clear();
1670 FFN_PROBE.with(|p| p.borrow_mut().take()).unwrap_or_default()
1671 }
1672
1673 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
1677 self.kv_cache.clear();
1678 let mut nll = 0f64;
1679 let mut cnt = 0usize;
1680 let mut hidden = vec![0f32; self.hidden_size];
1681 for (pos, &id) in ids.iter().enumerate() {
1682 if pos > 0 {
1683 inference::rms_norm_into(
1684 &hidden,
1685 &self.weights.final_norm,
1686 self.rms_eps,
1687 self.norm_style,
1688 &mut self.ws.n1,
1689 );
1690 let mut logits = self.lm_head_forward(&self.ws.n1);
1691 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1692 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
1693 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
1694 nll -= p.max(1e-300).ln();
1695 cnt += 1;
1696 attention::recycle_buf(&mut logits);
1697 }
1698 let emb = self.embed_single(id);
1699 hidden = self.forward_layers(&emb, pos, Some(mask));
1700 }
1701 self.kv_cache.clear();
1702 (nll / cnt.max(1) as f64).exp()
1703 }
1704
1705 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1714 self.kv_cache.clear();
1715 let mut nll = 0f64;
1716 let mut cnt = 0usize;
1717 if prefill_batched() {
1718 const CHUNK: usize = 128;
1724 const LM_SUB: usize = 32;
1725 let n = ids.len().saturating_sub(1);
1726 let hs = self.hidden_size;
1727 let rows = self.weights.lm_head.rows();
1728 let mut pos = 0usize;
1729 while pos < n {
1730 let end = (pos + CHUNK).min(n);
1731 let bsz = end - pos;
1732 let hb = self.prefill_batch(&ids[pos..end], pos);
1733 let mut k0 = 0usize;
1734 while k0 < bsz {
1735 let k1 = (k0 + LM_SUB).min(bsz);
1736 let sb = k1 - k0;
1737 if pos + k1 <= start {
1740 k0 = k1;
1741 continue;
1742 }
1743 let mut normed = vec![0.0f32; sb * hs];
1744 for k in 0..sb {
1745 let r = inference::rms_norm(
1746 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
1747 &self.weights.final_norm,
1748 self.rms_eps,
1749 self.norm_style,
1750 );
1751 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
1752 }
1753 let mut logits = vec![0.0f32; sb * rows];
1754 self.weights
1755 .lm_head
1756 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1757 for k in 0..sb {
1758 if pos + k0 + k < start {
1759 continue;
1760 }
1761 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1762 let target = ids[pos + k0 + k + 1] as usize;
1763 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1764 let lse: f64 = lg
1765 .iter()
1766 .map(|&v| ((v - max) as f64).exp())
1767 .sum::<f64>()
1768 .ln()
1769 + max as f64;
1770 nll += lse - lg[target] as f64;
1771 cnt += 1;
1772 }
1773 k0 = k1;
1774 }
1775 pos = end;
1776 }
1777 self.kv_cache.clear();
1778 return (nll, cnt);
1779 }
1780 for pos in 0..ids.len().saturating_sub(1) {
1781 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1782 if pos < start {
1783 continue;
1784 }
1785 let normed = inference::rms_norm(
1786 &hidden,
1787 &self.weights.final_norm,
1788 self.rms_eps,
1789 self.norm_style,
1790 );
1791 let logits = self.lm_head_forward(&normed);
1792 let target = ids[pos + 1] as usize;
1793 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1794 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1795 + max as f64;
1796 nll += lse - logits[target] as f64;
1797 cnt += 1;
1798 }
1799 self.kv_cache.clear();
1800 (nll, cnt)
1801 }
1802
1803 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1819 self.kv_cache.clear();
1820 self.o1_begin();
1821 let n = ids.len().saturating_sub(1);
1822 let p = prefill.min(n);
1823 let mut pos = 0usize;
1825 if prefill_batched() {
1826 const CHUNK: usize = 128;
1827 while pos < p {
1828 let end = (pos + CHUNK).min(p);
1829 let _ = self.prefill_batch(&ids[pos..end], pos);
1830 pos = end;
1831 }
1832 } else {
1833 while pos < p {
1834 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1835 pos += 1;
1836 }
1837 }
1838 self.o1_seal();
1839
1840 let mut nll = 0f64;
1841 let mut cnt = 0usize;
1842 for pos in p..n {
1843 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1844 let normed = inference::rms_norm(
1845 &hidden,
1846 &self.weights.final_norm,
1847 self.rms_eps,
1848 self.norm_style,
1849 );
1850 let logits = self.lm_head_forward(&normed);
1851 let target = ids[pos + 1] as usize;
1852 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1853 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1854 + max as f64;
1855 nll += lse - logits[target] as f64;
1856 cnt += 1;
1857 }
1858 self.kv_cache.clear();
1859 (nll, cnt)
1860 }
1861
1862 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1870 self.kv_cache.clear();
1871 let n = ids.len().saturating_sub(1);
1872 let mut correct = Vec::with_capacity(n);
1873 let mut pmax = Vec::with_capacity(n);
1874 for pos in 0..n {
1875 let emb = self.embed_single(ids[pos]);
1876 let hidden = self.forward_layers(&emb, pos, None);
1877 let normed = inference::rms_norm(
1878 &hidden,
1879 &self.weights.final_norm,
1880 self.rms_eps,
1881 self.norm_style,
1882 );
1883 let logits = self.lm_head_forward(&normed);
1884 let target = ids[pos + 1] as usize;
1885 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1886 for (i, &v) in logits.iter().enumerate() {
1887 if v > mval {
1888 mval = v;
1889 amax = i;
1890 }
1891 }
1892 correct.push(amax == target);
1893 let row: Vec<f32> = temps
1894 .iter()
1895 .map(|&t| {
1896 let tt = t.max(1e-3);
1897 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1898 1.0 / s.max(1e-12) })
1900 .collect();
1901 pmax.push(row);
1902 }
1903 self.kv_cache.clear();
1904 (correct, pmax)
1905 }
1906
1907 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1914 let mut router = match self.dyn_router.take() {
1915 Some(r) => r,
1916 None => return (self.ppl_ids(ids), 0),
1917 };
1918 router.reset();
1919 self.dyn_phi_seen = 0;
1920 let _ = self.set_active_skill(None);
1921
1922 self.kv_cache.clear();
1923 let mut nll = 0f64;
1924 let mut cnt = 0usize;
1925 for pos in 0..ids.len().saturating_sub(1) {
1926 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1927 let normed = inference::rms_norm(
1928 &hidden,
1929 &self.weights.final_norm,
1930 self.rms_eps,
1931 self.norm_style,
1932 );
1933 let logits = self.lm_head_forward(&normed);
1934 let target = ids[pos + 1] as usize;
1935 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1936 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1937 + max as f64;
1938 nll += lse - logits[target] as f64;
1939 cnt += 1;
1940 let phi = self.dyn_phi_ema.clone();
1942 if let Some(new_active) = router.step(&phi, pos) {
1943 let _ = self.set_active_skill(new_active);
1944 }
1945 }
1946 let switches = router.switches.len();
1947 let _ = self.set_active_skill(None);
1948 self.dyn_router = Some(router);
1949 self.kv_cache.clear();
1950 ((nll / cnt.max(1) as f64).exp(), switches)
1951 }
1952
1953 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1955 self.kv_cache.clear();
1956 let mut acc = vec![0f32; self.hidden_size];
1957 for (pos, &id) in ids.iter().enumerate() {
1958 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1959 for (a, v) in acc.iter_mut().zip(&h) {
1960 *a += v;
1961 }
1962 }
1963 let n = ids.len().max(1) as f32;
1964 for a in acc.iter_mut() {
1965 *a /= n;
1966 }
1967 self.kv_cache.clear();
1968 acc
1969 }
1970
1971 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1977 let b = ids.len();
1978 let hs = self.hidden_size;
1979 let mut h: Vec<f32> = vec![0.0; b * hs];
1982 let mut h_ready = false;
1983 let mut fill_h = |h: &mut Vec<f32>, me: &Self| {
1984 for (bi, &id) in ids.iter().enumerate() {
1985 let e = me.embed_single(id);
1986 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
1987 }
1988 };
1989 let (nh, _nkv, _hd, _rd, eps) = (
1990 self.num_heads,
1991 self.num_kv_heads,
1992 self.head_dim,
1993 self.rotary_dim,
1994 self.rms_eps,
1995 );
1996 let pool = self.pool.clone();
1997 let norm_style = self.norm_style;
1998
1999 #[cfg(target_os = "macos")]
2000 let mut chunk_skip_until = 0usize;
2001 for li in 0..self.num_layers {
2002 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
2009 {
2010 if li < chunk_skip_until {
2011 continue;
2012 }
2013 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
2014 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
2015 if end > li {
2016 h_ready = true;
2017 chunk_skip_until = end;
2018 continue;
2019 }
2020 }
2021 if !h_ready {
2022 fill_h(&mut h, self);
2023 h_ready = true;
2024 }
2025 let lw = &self.weights.layers[li];
2026 match &lw.attn {
2028 AttnKind::LinearGdn(w) => {
2029 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2031 let mut normed = vec![0.0f32; b * hs];
2032 for bi in 0..b {
2033 let r = inference::rms_norm(
2034 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
2035 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2036 }
2037 let attn = crate::linear_core::gdn_forward_batch(
2038 &normed, b, w, &cfg,
2039 &mut self.kv_cache.layers[li].linear_state,
2040 pool.as_deref(),
2041 );
2042 for (dst, &a) in h.iter_mut().zip(&attn) {
2043 *dst += a;
2044 }
2045 }
2046 AttnKind::ShortConv(w) => {
2047 let cfg =
2050 self.short_conv_cfg.expect("short-conv layer without short_conv_cfg");
2051 let mut normed = vec![0.0f32; b * hs];
2052 for bi in 0..b {
2053 inference::rms_norm_into(
2054 &h[bi * hs..(bi + 1) * hs],
2055 &lw.input_norm,
2056 eps,
2057 norm_style,
2058 &mut normed[bi * hs..(bi + 1) * hs],
2059 );
2060 }
2061 let attn = short_conv_forward_batch(
2062 &normed, b, w, &cfg,
2063 &mut self.kv_cache.layers[li].linear_state,
2064 pool.as_deref(),
2065 );
2066 for (dst, &a) in h.iter_mut().zip(&attn) {
2067 *dst += a;
2068 }
2069 }
2070 AttnKind::Full {
2071 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
2072 } => {
2073 let mut normed = vec![0.0f32; b * hs];
2077 for bi in 0..b {
2078 inference::rms_norm_into(
2079 &h[bi * hs..(bi + 1) * hs],
2080 &lw.input_norm,
2081 eps,
2082 norm_style,
2083 &mut normed[bi * hs..(bi + 1) * hs],
2084 );
2085 }
2086 let inv_freq_l = self.layer_inv_freq(li);
2087 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2088 let cfg = QwenAttnCfg {
2089 num_heads: nh,
2090 num_kv_heads: nkv_l,
2091 head_dim: hd_l,
2092 hidden_size: hs,
2093 position: start_pos,
2094 inv_freq: &inv_freq_l,
2095 rotary_dim: rd_l,
2096 scale: self.attn_scale,
2097 window: self.layer_window(li),
2098 v_norm: self.attn_v_norm,
2099 q_norm: q_norm.as_deref(),
2100 k_norm: k_norm.as_deref(),
2101 output_gate: *output_gate,
2102 bias: bias.as_ref().map(|(a, b, c)| {
2103 (a.as_slice(), b.as_slice(), c.as_slice())
2104 }),
2105 rms_eps: eps,
2106 norm_style,
2107 pool: pool.as_deref(),
2108 };
2109 let mut attn = attention::qwen_attention_batch(
2110 &normed, b, wq, wk, wv, wo,
2111 &mut self.kv_cache.layers[li], &cfg);
2112 if let Some(w) = &lw.attn_out_norm {
2113 for bi in 0..b {
2114 inference::rms_norm_into(
2115 &attn[bi * hs..(bi + 1) * hs], w, eps, norm_style,
2116 &mut normed[bi * hs..(bi + 1) * hs]);
2117 }
2118 attn.copy_from_slice(&normed);
2119 }
2120 for (dst, &a) in h.iter_mut().zip(&attn) {
2121 *dst += a;
2122 }
2123 }
2124 AttnKind::Linear(w) => {
2125 for bi in 0..b {
2126 let normed = inference::rms_norm(
2127 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
2128 vmf_phase_forward(
2129 &normed, w,
2130 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
2131 &mut self.kv_cache.layers[li].linear_state,
2132 pool.as_deref(),
2133 )
2134 .iter()
2135 .enumerate()
2136 .for_each(|(i, &a)| h[bi * hs + i] += a);
2137 }
2138 }
2139 }
2140
2141 let lw = &self.weights.layers[li];
2143 let mut post = vec![0.0f32; b * hs];
2144 for bi in 0..b {
2145 let r = inference::rms_norm(
2146 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
2147 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
2148 }
2149 let mut ffn = match &lw.ffn {
2150 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
2151 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
2152 };
2153 if let Some(w) = &lw.ffn_out_norm {
2154 for bi in 0..b {
2155 inference::rms_norm_into(
2156 &ffn[bi * hs..(bi + 1) * hs], w, eps, norm_style,
2157 &mut post[bi * hs..(bi + 1) * hs]);
2158 }
2159 ffn.copy_from_slice(&post);
2160 }
2161 for (dst, &f) in h.iter_mut().zip(&ffn) {
2162 *dst += f;
2163 }
2164 if let Some(sc) = lw.layer_scale {
2165 for v in h.iter_mut() {
2166 *v *= sc;
2167 }
2168 }
2169 if std::env::var("CMF_TRACE_H").is_ok() {
2170 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
2171 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
2172 eprintln!("layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}", lw.layer_scale);
2173 }
2174 }
2175 crate::gpu::set_layer(-1); h
2177 }
2178
2179 fn embed_single(&self, id: u32) -> Vec<f32> {
2181 let mut out = vec![0.0f32; self.hidden_size];
2182 if (id as usize) < self.weights.embed_tokens.rows() {
2183 self.weights.embed_tokens.row_f32(id as usize, &mut out);
2184 }
2185 if self.embed_multiplier != 1.0 {
2186 for v in out.iter_mut() {
2187 *v *= self.embed_multiplier;
2188 }
2189 }
2190 out
2191 }
2192
2193 #[cfg(target_os = "macos")]
2199 fn chunk_run_gpu(
2200 &mut self,
2201 li0: usize,
2202 h: &mut [f32],
2203 b: usize,
2204 pos0: usize,
2205 embed_ids: Option<&[u32]>,
2206 ) -> usize {
2207 if !crate::gpu::enabled_here()
2211 || std::env::var("CMF_GPU_CHUNK").map(|v| v == "0").unwrap_or(false)
2212 || b < 32
2213 || self.swa.is_some()
2214 || self.global_attn.is_some()
2215 || self.attn_v_norm
2216 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
2217 {
2218 return li0;
2219 }
2220 let Some(model) = self.model.clone() else { return li0 };
2221 let inv_freq = self.inv_freq.clone();
2222 let (nh, nkv, hd, hs) = (self.num_heads, self.num_kv_heads, self.head_dim, self.hidden_size);
2223 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
2225 let mut stored_at: Vec<usize> = Vec::new();
2226 for li in li0..self.num_layers {
2227 let lw = &self.weights.layers[li];
2228 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
2229 {
2230 break;
2231 }
2232 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate: false, bias } =
2233 &lw.attn
2234 else {
2235 break;
2236 };
2237 let FfnKind::Dense(d) = &lw.ffn else { break };
2238 if d.act != Act::Silu {
2239 break;
2240 }
2241 let parts = (
2242 wq.q8_row_parts(),
2243 wk.q8_row_parts(),
2244 wv.q8_row_parts(),
2245 wo.q8_row_parts(),
2246 d.gate_proj.q8_row_parts(),
2247 d.up_proj.q8_row_parts(),
2248 d.down_proj.q8_row_parts(),
2249 );
2250 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
2251 else {
2252 break;
2253 };
2254 let layer = &self.kv_cache.layers[li];
2255 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
2256 break;
2257 }
2258 stored_at.push(layer.head_len(0));
2259 layers.push(crate::gpu_metal::ChunkLayer {
2260 model: &model,
2261 kv_id: self.graph_kv_id,
2262 layer: li,
2263 wq: pq,
2264 wk: pk,
2265 wv: pv,
2266 wo: po,
2267 gate: pg,
2268 up: pu,
2269 down: pd,
2270 input_norm: &lw.input_norm,
2271 post_norm: &lw.post_norm,
2272 bias: bias
2273 .as_ref()
2274 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
2275 q_norm: q_norm.as_deref(),
2276 k_norm: k_norm.as_deref(),
2277 inv_freq: &inv_freq,
2278 rd: self.rotary_dim,
2279 nh,
2280 nkv,
2281 hd,
2282 hs,
2283 inter: d.gate_proj.rows(),
2284 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
2285 eps: self.rms_eps as f32,
2286 });
2287 }
2288 if layers.is_empty() {
2289 return li0;
2290 }
2291 let row = nkv * hd;
2292 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
2293 .iter()
2294 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
2295 .collect();
2296 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
2297 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
2298 let li = layers[i].layer;
2299 let layer = &self.kv_cache.layers[li];
2300 io.push(crate::gpu_metal::ChunkIo {
2301 cpu_stored: stored_at[i],
2302 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
2303 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
2304 out_k: ok,
2305 out_v: ov,
2306 imp: oi,
2307 });
2308 }
2309 let n_run = layers.len();
2310 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
2311 let ep = embed_ids.and_then(|ids| {
2314 self.weights.embed_tokens.q8_row_parts().map(|(idx, rows, _c, rs)| {
2315 crate::gpu_metal::ChunkEmbed {
2316 idx,
2317 rows,
2318 row_scale: rs,
2319 ids,
2320 mult: self.embed_multiplier,
2321 }
2322 })
2323 });
2324 if embed_ids.is_some() && ep.is_none() {
2325 return li0;
2326 }
2327 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
2328 return li0;
2329 }
2330 drop(io);
2331 drop(layers);
2332 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
2335 let li = li0 + i;
2336 let layer = &mut self.kv_cache.layers[li];
2337 for bi in 0..b {
2338 layer.append(&ok[bi * row..(bi + 1) * row], &ov[bi * row..(bi + 1) * row], &[]);
2339 }
2340 layer.accumulate_imp(oi);
2341 }
2342 last
2343 }
2344
2345 fn layer_is_local(&self, li: usize) -> bool {
2348 match self.swa {
2349 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
2350 None => false,
2351 }
2352 }
2353
2354 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
2357 if self.layer_is_local(li) {
2358 if let Some(f) = &self.inv_freq_local {
2359 return f.clone();
2360 }
2361 } else if let Some(f) = &self.inv_freq_global {
2362 return f.clone();
2363 }
2364 self.inv_freq.clone()
2365 }
2366
2367 fn layer_window(&self, li: usize) -> Option<usize> {
2369 match self.swa {
2370 Some((w, _)) if self.layer_is_local(li) => Some(w),
2371 _ => None,
2372 }
2373 }
2374
2375 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
2378 if !self.layer_is_local(li) {
2379 if let Some((ghd, gkv)) = self.global_attn {
2380 return (gkv, ghd, ghd);
2381 }
2382 }
2383 (self.num_kv_heads, self.head_dim, self.rotary_dim)
2384 }
2385
2386 fn forward_layers(
2388 &mut self,
2389 hidden: &[f32],
2390 position: usize,
2391 task_mask: Option<&TaskMask>,
2392 ) -> Vec<f32> {
2393 self.forward_layers_upto(hidden, position, task_mask, None)
2394 }
2395
2396 fn forward_layers_upto(
2398 &mut self,
2399 hidden: &[f32],
2400 position: usize,
2401 task_mask: Option<&TaskMask>,
2402 upto: Option<usize>,
2403 ) -> Vec<f32> {
2404 let mut h = hidden.to_vec();
2405 let (nh, _nkv, _hd, hs, _rd, eps) = (
2408 self.num_heads,
2409 self.num_kv_heads,
2410 self.head_dim,
2411 self.hidden_size,
2412 self.rotary_dim,
2413 self.rms_eps,
2414 );
2415 let pool = self.pool.clone();
2416
2417 #[cfg(target_os = "macos")]
2418 let mut gpu_skip_until = 0usize;
2419 for li in 0..self.num_layers {
2420 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
2422 if li > u {
2423 break;
2424 }
2425 }
2426 if let Some(mask) = task_mask {
2427 if !mask.layer_alive(li) {
2428 continue; }
2430 }
2431 #[cfg(target_os = "macos")]
2435 {
2436 if li < gpu_skip_until {
2437 continue;
2438 }
2439 if task_mask.is_none() {
2440 let end = self.q1_graph_gpu(li, upto, position, &mut h);
2441 if end > li {
2442 gpu_skip_until = end;
2443 continue;
2444 }
2445 }
2446 }
2447
2448 let lw = &self.weights.layers[li];
2449 inference::rms_norm_into(&h, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
2452
2453 let attn_out = match &lw.attn {
2454 AttnKind::Linear(w) => {
2455 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2456 vmf_phase_forward(
2457 &self.ws.n1,
2458 w,
2459 &cfg,
2460 &mut self.kv_cache.layers[li].linear_state,
2461 self.pool.as_deref(),
2462 )
2463 }
2464 AttnKind::LinearGdn(w) => {
2465 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2466 gdn_forward(
2467 &self.ws.n1,
2468 w,
2469 &cfg,
2470 &mut self.kv_cache.layers[li].linear_state,
2471 self.pool.as_deref(),
2472 )
2473 }
2474 AttnKind::ShortConv(w) => {
2475 let cfg =
2476 self.short_conv_cfg.expect("short-conv layer without short_conv_cfg");
2477 short_conv_forward(
2478 &self.ws.n1,
2479 w,
2480 &cfg,
2481 &mut self.kv_cache.layers[li].linear_state,
2482 self.pool.as_deref(),
2483 )
2484 }
2485 AttnKind::Full {
2486 wq,
2487 wk,
2488 wv,
2489 wo,
2490 q_norm,
2491 k_norm,
2492 output_gate,
2493 bias,
2494 } if self.kv_cache.layers[li].o1_sealed() => {
2495 let inv_freq_l = self.layer_inv_freq(li);
2498 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2499 let cfg = QwenAttnCfg {
2500 num_heads: nh,
2501 num_kv_heads: nkv_l,
2502 head_dim: hd_l,
2503 hidden_size: hs,
2504 position,
2505 inv_freq: &inv_freq_l,
2506 rotary_dim: rd_l,
2507 scale: self.attn_scale,
2508 window: None,
2509 v_norm: self.attn_v_norm,
2510 q_norm: q_norm.as_deref(),
2511 k_norm: k_norm.as_deref(),
2512 output_gate: *output_gate,
2513 bias: bias
2514 .as_ref()
2515 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2516 rms_eps: eps,
2517 norm_style: self.norm_style,
2518 pool: pool.as_deref(),
2519 };
2520 attention::qwen_attention_nystrom(
2521 &self.ws.n1,
2522 wq,
2523 wk,
2524 wv,
2525 wo,
2526 &mut self.kv_cache.layers[li],
2527 &cfg,
2528 )
2529 }
2530 AttnKind::Full {
2531 wq,
2532 wk,
2533 wv,
2534 wo,
2535 q_norm,
2536 k_norm,
2537 output_gate,
2538 bias,
2539 } => {
2540 let masked = task_mask
2541 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
2542 .unwrap_or(false);
2543 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
2544 match (masked, f32_view) {
2545 (true, (Some(q), Some(k), Some(v), Some(o))) => {
2548 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
2549 attention::multi_head_attention(
2550 &self.ws.n1,
2551 q,
2552 k,
2553 v,
2554 o,
2555 &mut self.kv_cache.layers[li],
2556 self.num_heads,
2557 self.num_kv_heads,
2558 self.head_dim,
2559 self.hidden_size,
2560 position,
2561 &active_heads,
2562 &self.inv_freq,
2563 )
2564 }
2565 (masked, _) => {
2566 if masked {
2567 tracing::warn!(
2568 "layer {li}: head mask on quantized weights not \
2569 supported yet — executing dense"
2570 );
2571 }
2572 let inv_freq_l = self.layer_inv_freq(li);
2573 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2574 let cfg = QwenAttnCfg {
2575 num_heads: nh,
2576 num_kv_heads: nkv_l,
2577 head_dim: hd_l,
2578 hidden_size: hs,
2579 position,
2580 inv_freq: &inv_freq_l,
2581 rotary_dim: rd_l,
2582 scale: self.attn_scale,
2583 window: self.layer_window(li),
2584 v_norm: self.attn_v_norm,
2585 q_norm: q_norm.as_deref(),
2586 k_norm: k_norm.as_deref(),
2587 output_gate: *output_gate,
2588 bias: bias
2589 .as_ref()
2590 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2591 rms_eps: eps,
2592 norm_style: self.norm_style,
2593 pool: pool.as_deref(),
2594 };
2595 attention::qwen_attention(
2596 &self.ws.n1,
2597 wq,
2598 wk,
2599 wv,
2600 wo,
2601 &mut self.kv_cache.layers[li],
2602 &cfg,
2603 )
2604 }
2605 }
2606 }
2607 };
2608 let attn_out = match &self.weights.layers[li].attn_out_norm {
2611 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
2612 None => attn_out,
2613 };
2614 for (i, &a) in attn_out.iter().enumerate() {
2615 h[i] += a;
2616 }
2617 let mut attn_out = attn_out;
2618 attention::recycle_buf(&mut attn_out);
2619
2620 let lw = &self.weights.layers[li];
2621 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
2622 let post_normed = &self.ws.p1;
2623
2624 let ffn_masked = task_mask
2625 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
2626 .unwrap_or(false);
2627 let f32_ffn = match &lw.ffn {
2630 FfnKind::Dense(d) => {
2631 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
2632 }
2633 FfnKind::Moe(_) => (None, None, None),
2634 };
2635 let ffn_out = match (ffn_masked, f32_ffn) {
2636 (true, (Some(g), Some(u), Some(d))) => {
2637 let active = task_mask.unwrap().ffn_active_indices(li);
2638 inference::sparse_ffn_forward(
2639 &post_normed,
2640 g,
2641 u,
2642 d,
2643 self.hidden_size,
2644 self.intermediate_size,
2645 &active,
2646 self.pool.as_deref(),
2647 )
2648 }
2649 (true, _) => match &lw.ffn {
2653 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
2654 let active = task_mask.unwrap().ffn_active_indices(li);
2655 sparse_ffn_quant(
2656 d,
2657 &post_normed,
2658 &active,
2659 self.hidden_size,
2660 self.pool.as_deref(),
2661 )
2662 }
2663 FfnKind::Dense(d) => {
2668 let active = task_mask.unwrap().ffn_active_indices(li);
2669 let (gf, uf, df) = dequant_dense_f32(d);
2670 inference::sparse_ffn_forward(
2671 &post_normed,
2672 &gf,
2673 &uf,
2674 &df,
2675 self.hidden_size,
2676 self.intermediate_size,
2677 &active,
2678 self.pool.as_deref(),
2679 )
2680 }
2681 FfnKind::Moe(_) => {
2682 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
2685 }
2686 },
2687 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
2688 };
2689 let ffn_out = match &self.weights.layers[li].ffn_out_norm {
2690 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
2691 None => ffn_out,
2692 };
2693 for (i, &f) in ffn_out.iter().enumerate() {
2694 h[i] += f;
2695 }
2696 let mut ffn_out = ffn_out;
2697 attention::recycle_buf(&mut ffn_out);
2698
2699 if let Some(sc) = self.weights.layers[li].layer_scale {
2701 for v in h.iter_mut() {
2702 *v *= sc;
2703 }
2704 }
2705
2706 if self.dyn_phi_layer == Some(li) {
2710 self.update_dyn_phi(&h);
2711 }
2712 }
2713 crate::gpu::set_layer(-1); h
2716 }
2717
2718 fn update_dyn_phi(&mut self, h: &[f32]) {
2721 const A: f32 = 0.2;
2722 if self.dyn_phi_ema.len() != h.len() {
2723 self.dyn_phi_ema = vec![0.0; h.len()];
2724 self.dyn_phi_seen = 0;
2725 }
2726 if self.dyn_phi_seen == 0 {
2727 self.dyn_phi_ema.copy_from_slice(h);
2728 } else {
2729 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
2730 *e = (1.0 - A) * *e + A * v;
2731 }
2732 }
2733 self.dyn_phi_seen += 1;
2734 }
2735
2736 pub fn dyn_phi(&self) -> &[f32] {
2738 &self.dyn_phi_ema
2739 }
2740
2741 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
2743 self.dyn_phi_layer = layer;
2744 self.dyn_phi_ema.clear();
2745 self.dyn_phi_seen = 0;
2746 }
2747
2748 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
2750 let Some(model) = &self.model else { return Vec::new() };
2751 model
2752 .header
2753 .skills
2754 .iter()
2755 .enumerate()
2756 .filter_map(|(i, sk)| {
2757 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
2758 let sel = sk.selection.as_ref()?;
2759 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
2760 })
2761 .collect()
2762 }
2763
2764 pub fn active_skill(&self) -> Option<usize> {
2766 self.dyn_active
2767 }
2768
2769 pub fn enable_dynamic_routing(&mut self) -> usize {
2774 use crate::swarm::{DynRouter, RoutableSkill};
2775 let Some(model) = self.model.clone() else { return 0 };
2776 if self.dyn_blend_loaded {
2779 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
2780 return 0;
2781 }
2782 if let Some(a) = self.dyn_active {
2786 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
2787 tracing::warn!(
2788 "loaded skill is not FFN-eligible — dynamic routing unavailable"
2789 );
2790 return 0;
2791 }
2792 }
2793 let hidden = self.hidden_size;
2794 let mut skills = Vec::new();
2795 for (idx, id, _phi) in self.dynamic_skills() {
2796 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
2797 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
2798 skills.push(rs);
2799 }
2800 }
2801 }
2802 if skills.is_empty() {
2803 return 0;
2804 }
2805 let phi = skills[0].phi_layer;
2807 if skills.iter().any(|s| s.phi_layer != phi) {
2808 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
2809 }
2810 let n = skills.len();
2811 self.set_dyn_phi_layer(Some(phi));
2812 self.dyn_router = Some(DynRouter::new(skills));
2813 n
2814 }
2815
2816 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
2818 self.dyn_router
2819 .as_ref()
2820 .map(|r| r.switches.clone())
2821 .unwrap_or_default()
2822 }
2823
2824 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
2827 let rows = self.weights.lm_head.rows();
2828 let mut logits = attention::take_buf(rows.min(self.vocab_size));
2829 self.weights
2830 .lm_head
2831 .matvec(hidden, &mut logits, self.pool.as_deref());
2832 logits.resize(self.vocab_size, 0.0);
2833 if let Some(c) = self.final_softcap {
2834 for l in logits.iter_mut() {
2835 *l = c * (*l / c).tanh();
2836 }
2837 }
2838 logits
2839 }
2840
2841 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
2846 self.kv_cache.clear();
2847 let mut hidden = vec![0.0f32; self.hidden_size];
2848 for (pos, &id) in ids.iter().enumerate() {
2849 let emb = self.embed_single(id);
2850 hidden = self.forward_layers(&emb, pos, task_mask);
2851 }
2852 inference::rms_norm_into(
2853 &hidden,
2854 &self.weights.final_norm,
2855 self.rms_eps,
2856 self.norm_style,
2857 &mut self.ws.n1,
2858 );
2859 self.lm_head_forward(&self.ws.n1)
2860 }
2861}
2862
2863pub fn create_test_pipeline(
2865 hidden_size: usize,
2866 intermediate_size: usize,
2867 num_heads: usize,
2868 num_kv_heads: usize,
2869 head_dim: usize,
2870 num_layers: usize,
2871 vocab_size: usize,
2872) -> Pipeline {
2873 let synth = |n: usize, salt: usize| -> Vec<f32> {
2876 (0..n)
2877 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
2878 .collect()
2879 };
2880 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2881 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2882 };
2883 let layer_weights: Vec<LayerWeights> = (0..num_layers)
2884 .map(|li| LayerWeights {
2885 input_norm: vec![1.0; hidden_size],
2886 post_norm: vec![1.0; hidden_size],
2887 attn_out_norm: None,
2888 ffn_out_norm: None,
2889 layer_scale: None,
2890 ffn: FfnKind::Dense(DenseFfn {
2891 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
2892 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
2893 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
2894 act: Act::Silu,
2895 }),
2896 attn: AttnKind::Full {
2897 bias: None,
2898 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
2899 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
2900 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
2901 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
2902 q_norm: None,
2903 k_norm: None,
2904 output_gate: false,
2905 },
2906 })
2907 .collect();
2908
2909 Pipeline::new(
2910 Tokenizer::byte_level(),
2911 PipelineWeights {
2912 embed_tokens: qt(vocab_size, hidden_size, 100),
2913 layers: layer_weights,
2914 lm_head: qt(vocab_size, hidden_size, 200),
2915 final_norm: vec![1.0; hidden_size],
2916 },
2917 hidden_size,
2918 intermediate_size,
2919 num_heads,
2920 num_kv_heads,
2921 head_dim,
2922 num_layers,
2923 vocab_size,
2924 1e-6,
2925 10_000.0,
2926 NormStyle::Qwen,
2927 4096,
2928 SamplerConfig {
2929 seed: Some(42),
2930 ..Default::default()
2931 },
2932 )
2933}
2934
2935fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
2938 let inter = d.gate_proj.rows();
2939 let hidden = d.down_proj.rows();
2940 let mut g = vec![0.0f32; b * inter];
2941 d.gate_proj.matmat(xs, b, &mut g, pool);
2942 let mut u = vec![0.0f32; b * inter];
2943 d.up_proj.matmat(xs, b, &mut u, pool);
2944 for i in 0..b * inter {
2945 g[i] = d.act.apply(g[i]) * u[i];
2946 }
2947 let mut out = vec![0.0f32; b * hidden];
2948 d.down_proj.matmat(&g, b, &mut out, pool);
2949 out
2950}
2951
2952fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
2956 let ne = m.experts.len();
2957 let mut logits = vec![0.0f32; b * ne];
2958 m.router.matmat(xs, b, &mut logits, pool);
2959
2960 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
2963 {
2964 let mut st = m.stats.borrow_mut();
2965 if st.len() < ne {
2966 st.resize(ne, 0);
2967 }
2968 for bi in 0..b {
2969 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m);
2970 for &e in &idx {
2971 st[e] += 1;
2972 assign[e].push((bi, p[e] / wsum));
2973 }
2974 }
2975 }
2976
2977 let mut out = vec![0.0f32; b * hidden];
2978 let cols = m.experts[0].gate_proj.cols();
2979 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
2980 let sb = list.len();
2981 let mut sub = vec![0.0f32; sb * cols];
2982 for (k, &(bi, _)) in list.iter().enumerate() {
2983 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
2984 }
2985 let eo = dense_ffn_batch(d, &sub, sb, pool);
2986 for (k, &(bi, w)) in list.iter().enumerate() {
2987 for i in 0..hidden {
2988 out[bi * hidden + i] += w * eo[k * hidden + i];
2989 }
2990 }
2991 };
2992 for e in 0..ne {
2993 if !assign[e].is_empty() {
2994 run_expert(&m.experts[e], &assign[e]);
2995 }
2996 }
2997 if let Some((se, gate)) = &m.shared {
2998 let mut gl = vec![0.0f32; b];
2999 gate.matmat(xs, b, &mut gl, pool);
3000 let all: Vec<(usize, f32)> = (0..b)
3001 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
3002 .collect();
3003 run_expert(se, &all);
3004 }
3005 out
3006}
3007
3008thread_local! {
3009 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
3013 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
3014}
3015
3016fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3018 if crate::gpu::enabled_here()
3029 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
3030 {
3031 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
3032 crate::gpu::ProbeArm::Gpu
3033 } else {
3034 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
3035 };
3036 match arm {
3037 crate::gpu::ProbeArm::Gpu => {
3038 let t0 = std::time::Instant::now();
3039 if let Some(out) = dense_ffn_gpu(d, x, pool) {
3040 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3041 return out;
3042 }
3043 }
3044 crate::gpu::ProbeArm::CpuTimed => {
3045 let t0 = std::time::Instant::now();
3046 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3047 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3048 return out;
3049 }
3050 crate::gpu::ProbeArm::Cpu => {
3051 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
3052 }
3053 }
3054 }
3055 dense_ffn_cpu(d, x, pool)
3056}
3057
3058fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3060 let inter = d.gate_proj.rows();
3061 FFN_SCRATCH.with(|s| {
3062 let mut s = s.borrow_mut();
3063 let [g, u, ..] = &mut *s;
3064 g.resize(inter, 0.0);
3065 u.resize(inter, 0.0);
3066 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
3068 for i in 0..inter {
3069 g[i] = d.act.apply(g[i]) * u[i];
3070 }
3071 FFN_PROBE.with(|pr| {
3074 if let Some(acc) = pr.borrow_mut().as_mut() {
3075 let li = crate::gpu::cur_layer();
3076 if li >= 0 {
3077 if let Some(row) = acc.get_mut(li as usize) {
3078 for (a, &v) in row.iter_mut().zip(g.iter()) {
3079 *a += (v as f64).abs();
3080 }
3081 }
3082 }
3083 }
3084 });
3085 let mut out = attention::take_buf(d.down_proj.rows());
3086 d.down_proj.matvec(g, &mut out, pool);
3087 out
3088 })
3089}
3090
3091thread_local! {
3092 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
3095 const { std::cell::RefCell::new(None) };
3096}
3097
3098fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
3104 if d.act != Act::Silu {
3106 return None;
3107 }
3108 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
3111 return None;
3112 }
3113 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
3114 let mut model_ref = None;
3115 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
3116 let model = model_ref?;
3117 let hidden = jobs[0].down.1;
3118 let mut out = attention::take_buf(hidden);
3119 if crate::gpu::moe_block(&model, &jobs, &mut out) {
3120 Some(out)
3121 } else {
3122 let mut out = out;
3123 attention::recycle_buf(&mut out);
3124 None
3125 }
3126}
3127
3128#[allow(clippy::type_complexity)]
3133#[allow(clippy::type_complexity)]
3134fn moe_parts(
3135 t: &QTensor,
3136) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32], bool)> {
3137 match t {
3138 QTensor::Mapped {
3139 model,
3140 idx,
3141 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
3142 rows,
3143 cols,
3144 row_scale,
3145 col_field,
3146 ..
3147 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
3148 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
3149 }
3150 QTensor::Mapped {
3152 model,
3153 idx,
3154 dtype: cortiq_core::TensorDtype::Q1,
3155 rows,
3156 cols,
3157 ..
3158 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
3159 _ => None,
3160 }
3161}
3162
3163fn moe_push_job<'a>(
3165 d: &'a DenseFfn,
3166 x: &[f32],
3167 w: f32,
3168 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
3169 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
3170) -> Option<()> {
3171 use crate::qtensor::prescale;
3172 if d.act != Act::Silu {
3173 return None; }
3175 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
3176 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
3177 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
3178 if gq1 != uq1 || uq1 != dq1 {
3179 return None; }
3181 model_ref.get_or_insert_with(|| gm.clone());
3182 let gdt = if gcf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
3183 let udt = if ucf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
3184 jobs.push(crate::gpu::MoeJob {
3185 gate: (gi, gr, gc, grs),
3186 up: (ui, ur, uc, urs),
3187 down: (di, dr, dc, drs),
3188 xs_gate: prescale(x, gcf, gdt).into_owned(),
3189 xs_up: prescale(x, ucf, udt).into_owned(),
3190 down_col: dcf,
3191 w,
3192 q1: gq1,
3193 });
3194 Some(())
3195}
3196
3197fn sparse_ffn_quant(
3204 d: &DenseFfn,
3205 x: &[f32],
3206 active: &[u16],
3207 hidden: usize,
3208 pool: Option<&Pool>,
3209) -> Vec<f32> {
3210 let n = active.len();
3211 let inter = d.gate_proj.rows();
3212 let mut act = vec![0.0f32; n];
3213 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
3216 let compute = |ai: usize| -> f32 {
3217 let idx = active[ai] as usize;
3218 if idx >= inter {
3219 return 0.0; }
3221 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
3222 let gate = d.gate_proj.row_dot(idx, x, &mut s);
3223 let up = d.up_proj.row_dot(idx, x, &mut s);
3224 d.act.apply(gate) * up
3225 };
3226 match pool {
3227 Some(p) if n >= 256 => {
3228 let ptr = SendMut(act.as_mut_ptr());
3229 p.run(&|widx, nw| {
3230 let chunk = n.div_ceil(nw);
3231 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
3232 for ai in s..e {
3233 unsafe { *ptr.at(ai) = compute(ai) };
3234 }
3235 });
3236 }
3237 _ => {
3238 for (ai, a) in act.iter_mut().enumerate() {
3239 *a = compute(ai);
3240 }
3241 }
3242 }
3243 let mut out = vec![0.0f32; hidden];
3245 for (ai, &idx) in active.iter().enumerate() {
3246 let w = act[ai];
3247 if w.abs() >= 1e-12 && (idx as usize) < inter {
3248 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
3249 }
3250 }
3251 out
3252}
3253
3254#[doc(hidden)]
3256pub fn sparse_ffn_quant_for_test(
3257 d: &DenseFfn,
3258 x: &[f32],
3259 active: &[u16],
3260 hidden: usize,
3261) -> Vec<f32> {
3262 sparse_ffn_quant(d, x, active, hidden, None)
3263}
3264
3265fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
3269 let deq = |t: &QTensor| -> Vec<f32> {
3270 let (rows, cols) = (t.rows(), t.cols());
3271 let mut out = vec![0.0f32; rows * cols];
3272 for r in 0..rows {
3273 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
3274 }
3275 out
3276 };
3277 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
3278}
3279
3280struct SendMut(*mut f32);
3282unsafe impl Send for SendMut {}
3283unsafe impl Sync for SendMut {}
3284impl SendMut {
3285 #[inline]
3286 #[allow(clippy::mut_from_ref)]
3289 unsafe fn at(&self, i: usize) -> &mut f32 {
3290 unsafe { &mut *self.0.add(i) }
3291 }
3292}
3293
3294fn moe_route(logits: &[f32], m: &MoeFfn) -> (Vec<usize>, Vec<f32>, f32) {
3304 let ne = logits.len();
3305 let p: Vec<f32> = if m.router_sigmoid {
3306 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
3307 } else {
3308 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
3309 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
3310 let s: f32 = e.iter().sum();
3311 for v in &mut e {
3312 *v /= s;
3313 }
3314 e
3315 };
3316 let mut idx: Vec<usize> = (0..ne).collect();
3317 match &m.expert_bias {
3319 Some(b) => idx.sort_unstable_by(|&x, &y| {
3320 (p[y] + b[y]).partial_cmp(&(p[x] + b[x])).unwrap().then(x.cmp(&y))
3321 }),
3322 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
3323 }
3324 idx.truncate(m.top_k);
3325 let wsum: f32 = if m.norm_topk_prob {
3326 let s: f32 = idx.iter().map(|&e| p[e]).sum();
3327 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
3330 } else {
3331 1.0 / m.routed_scaling
3332 };
3333 (idx, p, wsum)
3334}
3335
3336fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3339 let ne = m.experts.len();
3340 let mut logits = vec![0.0f32; ne];
3341 m.router.matvec(x, &mut logits, pool);
3342 let (idx, p, wsum) = moe_route(&logits, m);
3343 {
3344 let mut st = m.stats.borrow_mut();
3345 if st.len() < ne {
3346 st.resize(ne, 0);
3347 }
3348 for &e in &idx {
3349 st[e] += 1;
3350 }
3351 }
3352 if crate::gpu::enabled_here() {
3357 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
3358 crate::gpu::ProbeArm::Gpu => {
3359 let t0 = std::time::Instant::now();
3360 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
3361 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
3362 return out;
3363 }
3364 }
3365 crate::gpu::ProbeArm::CpuTimed => {
3366 let t0 = std::time::Instant::now();
3367 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
3368 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
3369 return out;
3370 }
3371 crate::gpu::ProbeArm::Cpu => {
3372 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
3373 }
3374 }
3375 }
3376 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
3377}
3378
3379fn moe_ffn_cpu(
3381 m: &MoeFfn,
3382 x: &[f32],
3383 idx: &[usize],
3384 p: &[f32],
3385 wsum: f32,
3386 pool: Option<&Pool>,
3387) -> Vec<f32> {
3388 let mut out = attention::take_buf(x.len());
3389 for &e in idx {
3390 let mut eo = dense_ffn(&m.experts[e], x, pool);
3391 let w = p[e] / wsum;
3392 for i in 0..out.len() {
3393 out[i] += w * eo[i];
3394 }
3395 attention::recycle_buf(&mut eo);
3396 }
3397 if let Some((se, gate)) = &m.shared {
3398 let mut so = dense_ffn(se, x, pool);
3399 let mut gl = vec![0.0f32; 1];
3400 gate.matvec(x, &mut gl, pool);
3401 let g = 1.0 / (1.0 + (-gl[0]).exp());
3402 for i in 0..out.len() {
3403 out[i] += g * so[i];
3404 }
3405 attention::recycle_buf(&mut so);
3406 }
3407 out
3408}
3409
3410fn moe_ffn_gpu(
3413 m: &MoeFfn,
3414 x: &[f32],
3415 idx: &[usize],
3416 p: &[f32],
3417 wsum: f32,
3418 pool: Option<&Pool>,
3419) -> Option<Vec<f32>> {
3420 use crate::gpu::MoeJob;
3421
3422 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
3423 let mut model_ref = None;
3424 for &e in idx {
3425 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
3426 }
3427 if let Some((se, gate)) = &m.shared {
3428 let mut gl = vec![0.0f32; 1];
3429 gate.matvec(x, &mut gl, pool);
3430 let g = 1.0 / (1.0 + (-gl[0]).exp());
3431 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
3432 }
3433 let model = model_ref?;
3434 let hidden = jobs[0].down.1;
3435 let mut out = vec![0.0f32; hidden];
3436 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
3437}
3438
3439fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
3441 match ffn {
3442 FfnKind::Dense(d) => dense_ffn(d, x, pool),
3443 FfnKind::Moe(m) => moe_ffn(m, x, pool),
3444 }
3445}
3446
3447fn ffn_forward_pair(
3451 ffn: &FfnKind,
3452 x1: &[f32],
3453 x2: &[f32],
3454 pool: Option<&Pool>,
3455) -> (Vec<f32>, Vec<f32>) {
3456 let d = match ffn {
3457 FfnKind::Dense(d) => d,
3458 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
3459 };
3460 let inter = d.gate_proj.rows();
3461 FFN_SCRATCH.with(|s| {
3462 let mut s = s.borrow_mut();
3463 let [g1, g2, u1, u2] = &mut *s;
3464 g1.resize(inter, 0.0);
3465 g2.resize(inter, 0.0);
3466 u1.resize(inter, 0.0);
3467 u2.resize(inter, 0.0);
3468 QTensor::matvec2_many(
3471 [&d.gate_proj, &d.up_proj],
3472 x1,
3473 x2,
3474 [g1.as_mut_slice(), u1.as_mut_slice()],
3475 [g2.as_mut_slice(), u2.as_mut_slice()],
3476 pool,
3477 );
3478 for i in 0..inter {
3479 g1[i] = d.act.apply(g1[i]) * u1[i];
3480 g2[i] = d.act.apply(g2[i]) * u2[i];
3481 }
3482 let mut o1 = attention::take_buf(d.down_proj.rows());
3483 let mut o2 = attention::take_buf(d.down_proj.rows());
3484 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
3485 (o1, o2)
3486 })
3487}
3488
3489#[cfg(test)]
3490mod tests {
3491 use super::*;
3492
3493 #[test]
3499 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
3500 let (hidden, inter) = (16usize, 40usize);
3501 let synth = |n: usize, salt: usize| -> Vec<f32> {
3502 (0..n)
3503 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
3504 .collect()
3505 };
3506 let d = DenseFfn {
3507 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
3508 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
3509 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
3510 act: Act::Silu,
3511 };
3512 let x = synth(hidden, 9);
3513 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
3515
3516 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
3517
3518 let mut g = vec![0.0f32; inter];
3520 d.gate_proj.matvec(&x, &mut g, None);
3521 let mut u = vec![0.0f32; inter];
3522 d.up_proj.matvec(&x, &mut u, None);
3523 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
3524 for i in 0..inter {
3525 g[i] = if act_set.contains(&(i as u16)) {
3526 inference::silu(g[i]) * u[i]
3527 } else {
3528 0.0
3529 };
3530 }
3531 let mut reference = vec![0.0f32; hidden];
3532 d.down_proj.matvec(&g, &mut reference, None);
3533
3534 let max_d = sparse
3535 .iter()
3536 .zip(&reference)
3537 .map(|(a, b)| (a - b).abs())
3538 .fold(0.0f32, f32::max);
3539 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
3540 }
3541
3542 fn attach_test_mtp(p: &mut Pipeline) {
3544 let (h, inter, heads, kv, hd) = (
3545 p.hidden_size,
3546 p.intermediate_size,
3547 p.num_heads,
3548 p.num_kv_heads,
3549 p.head_dim,
3550 );
3551 let synth = |n: usize, salt: usize| -> Vec<f32> {
3552 (0..n)
3553 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
3554 .collect()
3555 };
3556 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
3557 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
3558 };
3559 p.mtp = Some(MtpModule {
3560 enorm: vec![1.0; h],
3561 hnorm: vec![1.0; h],
3562 eh_proj: qt(h, 2 * h, 301),
3563 layer: LayerWeights {
3564 input_norm: vec![1.0; h],
3565 post_norm: vec![1.0; h],
3566 attn_out_norm: None,
3567 ffn_out_norm: None,
3568 layer_scale: None,
3569 ffn: FfnKind::Dense(DenseFfn {
3570 gate_proj: qt(inter, h, 315),
3571 up_proj: qt(inter, h, 316),
3572 down_proj: qt(h, inter, 317),
3573 act: Act::Silu,
3574 }),
3575 attn: AttnKind::Full {
3576 bias: None,
3577 wq: qt(heads * hd, h, 311),
3578 wk: qt(kv * hd, h, 312),
3579 wv: qt(kv * hd, h, 313),
3580 wo: qt(h, heads * hd, 314),
3581 q_norm: None,
3582 k_norm: None,
3583 output_gate: false,
3584 },
3585 },
3586 final_norm: vec![1.0; h],
3587 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
3588 });
3589 }
3590
3591 #[test]
3592 fn speculative_equals_vanilla_greedy() {
3593 let run = |spec: bool| {
3594 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3595 p.sampler_config.temperature = 0.0;
3596 attach_test_mtp(&mut p);
3597 p.speculative = spec;
3598 let r = p.generate("abcdef", 12, None, None).unwrap();
3599 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
3600 };
3601 let (vanilla, d0, _) = run(false);
3602 let (spec, d1, a1) = run(true);
3603 assert_eq!(d0, 0, "vanilla path must not draft");
3604 assert!(d1 > 0, "speculative path must draft");
3605 assert_eq!(
3606 vanilla, spec,
3607 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
3608 );
3609 }
3610
3611 #[test]
3612 fn speculative_accepts_constant_oracle() {
3613 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3614 p.sampler_config.temperature = 0.0;
3615 p.sampler_config.repetition_penalty = 1.0;
3616 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
3619 attach_test_mtp(&mut p);
3620 p.speculative = true;
3621 let r = p.generate("abcd", 10, None, None).unwrap();
3622 assert!(r.mtp_drafted > 0);
3623 assert_eq!(
3624 r.mtp_accepted, r.mtp_drafted,
3625 "constant logits → every draft accepted"
3626 );
3627 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
3630 }
3631
3632 #[test]
3633 fn empty_prompt_is_an_error_not_a_panic() {
3634 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3635 let r = p.generate("", 4, None, None);
3636 assert!(r.is_err(), "empty prompt must be a clean error");
3637 }
3638
3639 #[test]
3640 fn every_token_enters_kv_exactly_once() {
3641 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3642 p.sampler_config.temperature = 0.0;
3644 let r = p.generate("abc", 2, None, None).unwrap();
3645 assert_eq!(r.prompt_tokens, 3);
3646 assert_eq!(
3650 p.kv_cache.seq_len(),
3651 3 + r.tokens_generated - 1,
3652 "each token must be cached exactly once (v1 cached the last prompt token twice)"
3653 );
3654 }
3655
3656 #[test]
3657 fn generation_is_reproducible_with_seed() {
3658 let run = || {
3659 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3660 p.generate("hello", 8, None, None).unwrap().token_ids
3661 };
3662 assert_eq!(run(), run());
3663 }
3664
3665 #[test]
3666 fn eviction_bounds_the_cache() {
3667 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3668 p.kv_cache.max_seq_len = 6;
3669 p.sampler_config.temperature = 0.0;
3670 let _ = p.generate("abcd", 12, None, None).unwrap();
3671 assert!(
3672 p.kv_cache.seq_len() <= 6 + 1,
3673 "cache must stay bounded by max_seq_len (got {})",
3674 p.kv_cache.seq_len()
3675 );
3676 }
3677
3678 #[test]
3679 fn confidence_matches_tokens_and_is_a_probability() {
3680 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3681 p.sampler_config.temperature = 0.0;
3682 p.sampler_config.repetition_penalty = 1.0;
3683 let r = p.generate("abcd", 10, None, None).unwrap();
3684 assert_eq!(
3685 r.token_confidence.len(),
3686 r.token_ids.len(),
3687 "one confidence per emitted token"
3688 );
3689 for &c in &r.token_confidence {
3690 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
3691 }
3692 let logits = [1.0f32, 3.0, 0.5, 3.0];
3694 let p0 = top1_prob_t(&logits, 1, 1.0);
3695 let p1 = top1_prob_t(&logits, 3, 1.0);
3696 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
3697 assert!(p0 > 0.0 && p0 < 1.0);
3698 let sharp = top1_prob_t(&logits, 1, 1.0);
3700 let soft = top1_prob_t(&logits, 1, 2.0);
3701 assert!(soft < sharp, "higher temperature lowers peak confidence");
3702 }
3703
3704 #[test]
3705 fn trace_is_opt_in_and_parallels_the_output() {
3706 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3708 p.sampler_config.temperature = 0.0;
3709 p.sampler_config.repetition_penalty = 1.0;
3710 let r = p.generate("abcd", 10, None, None).unwrap();
3711 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
3712
3713 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3715 p.sampler_config.temperature = 0.0;
3716 p.sampler_config.repetition_penalty = 1.0;
3717 p.set_trace(true);
3718 let r = p.generate("abcd", 10, None, None).unwrap();
3719 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
3720 for (i, tr) in r.traces.iter().enumerate() {
3721 assert_eq!(tr.t, i, "trace index is sequential");
3722 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
3723 assert_eq!(
3724 tr.confidence, r.token_confidence[i],
3725 "trace confidence matches the confidence channel"
3726 );
3727 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
3729 }
3730 }
3731
3732 #[test]
3733 fn explain_prefill_logits_match_greedy_first_token() {
3734 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3738 p.sampler_config.temperature = 0.0;
3739 p.sampler_config.repetition_penalty = 1.0;
3740 let ids = p.tokenizer.encode("abcd");
3741 let logits = p.prefill_next_logits(&ids, None);
3742 let argmax = logits
3743 .iter()
3744 .enumerate()
3745 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3746 .unwrap()
3747 .0 as u32;
3748 let r = p.generate("abcd", 1, None, None).unwrap();
3749 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
3750 }
3751}