1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13 gdn_forward, gdn_pair, vmf_phase_forward, vmf_phase_pair, GdnCfg, GdnWeights, VmfPhaseCfg,
14 VmfPhaseWeights,
15};
16use crate::pool::Pool;
17use crate::qtensor::QTensor;
18use crate::sampler::{self, SamplerConfig, SplitMix64};
19use crate::tokenizer::Tokenizer;
20use cortiq_core::mask::TaskMask;
21use cortiq_core::types::NormStyle;
22
23struct ForwardScratch {
27 n1: Vec<f32>,
28 n2: Vec<f32>,
29 p1: Vec<f32>,
30 p2: Vec<f32>,
31}
32
33impl ForwardScratch {
34 fn new(hidden: usize) -> Self {
35 Self {
36 n1: vec![0.0; hidden],
37 n2: vec![0.0; hidden],
38 p1: vec![0.0; hidden],
39 p2: vec![0.0; hidden],
40 }
41 }
42}
43
44pub struct Pipeline {
46 pub tokenizer: std::sync::Arc<Tokenizer>,
49 pub kv_cache: KvCache,
50 pub sampler_config: SamplerConfig,
51 pub weights: PipelineWeights,
52 pub hidden_size: usize,
53 pub intermediate_size: usize,
54 pub num_heads: usize,
55 pub num_kv_heads: usize,
56 pub head_dim: usize,
57 pub num_layers: usize,
58 pub vocab_size: usize,
59 pub rms_eps: f64,
60 pub rope_base: f32,
61 pub norm_style: NormStyle,
62 pub rotary_dim: usize,
64 pub vmf_cfg: Option<VmfPhaseCfg>,
66 pub gdn_cfg: Option<GdnCfg>,
68 pub mtp: Option<MtpModule>,
70 pub speculative: bool,
72 rng: SplitMix64,
73 inv_freq: std::sync::Arc<Vec<f32>>,
77 ws: ForwardScratch,
81 pool: Option<std::sync::Arc<Pool>>,
83 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
87 pub(crate) dyn_force_f32: bool,
89 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
94 pub(crate) dyn_active: Option<usize>,
100 pub(crate) dyn_blend_loaded: bool,
104 pub(crate) dyn_phi_layer: Option<usize>,
107 dyn_phi_ema: Vec<f32>,
109 dyn_phi_seen: usize,
110 pub dyn_router: Option<crate::swarm::DynRouter>,
113 o1_cfg: Option<crate::nystrom::O1Cfg>,
116 o1_flags: Vec<bool>,
118 trace: bool,
121 calib_temp: f32,
124 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
126 graph_kv_id: u64,
127 confidence_on: bool,
131}
132
133#[cfg(target_os = "macos")]
134impl Drop for Pipeline {
135 fn drop(&mut self) {
136 crate::gpu::kv_mirror_drop(self.graph_kv_id);
137 }
138}
139
140pub struct PipelineWeights {
145 pub embed_tokens: QTensor,
147 pub layers: Vec<LayerWeights>,
149 pub lm_head: QTensor,
151 pub final_norm: Vec<f32>,
153}
154
155pub struct LayerWeights {
157 pub input_norm: Vec<f32>,
158 pub post_norm: Vec<f32>,
159 pub ffn: FfnKind,
160 pub attn: AttnKind,
161}
162
163pub struct DenseFfn {
165 pub gate_proj: QTensor,
166 pub up_proj: QTensor,
167 pub down_proj: QTensor,
168}
169
170pub enum FfnKind {
173 Dense(DenseFfn),
174 Moe(MoeFfn),
178}
179
180pub struct MoeFfn {
181 pub router: QTensor,
183 pub experts: Vec<DenseFfn>,
184 pub top_k: usize,
185 pub norm_topk_prob: bool,
186 pub shared: Option<(DenseFfn, QTensor)>,
188 pub stats: std::cell::RefCell<Vec<u64>>,
192}
193
194pub enum AttnKind {
197 Full {
199 wq: QTensor,
200 wk: QTensor,
201 wv: QTensor,
202 wo: QTensor,
203 q_norm: Option<Vec<f32>>,
204 k_norm: Option<Vec<f32>>,
205 output_gate: bool,
206 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
208 },
209 Linear(VmfPhaseWeights),
211 LinearGdn(GdnWeights),
213}
214
215pub struct MtpModule {
220 pub enorm: Vec<f32>,
221 pub hnorm: Vec<f32>,
222 pub eh_proj: QTensor,
224 pub layer: LayerWeights,
225 pub final_norm: Vec<f32>,
226 pub kv: crate::kv_cache::LayerKvCache,
227}
228
229pub struct GenerateResult {
231 pub text: String,
232 pub token_ids: Vec<u32>,
233 pub prompt_tokens: usize,
234 pub tokens_generated: usize,
235 pub finish_reason: String,
236 pub mtp_drafted: usize,
238 pub mtp_accepted: usize,
239 pub token_confidence: Vec<f32>,
244 pub traces: Vec<TokenTrace>,
247}
248
249#[derive(Clone, Debug)]
254pub struct TokenTrace {
255 pub t: usize,
257 pub token_id: u32,
259 pub confidence: f32,
261 pub active_skill: Option<String>,
263 pub recon: Option<f32>,
267 pub switched: bool,
270}
271
272fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
277 let t = if temp > 1e-3 { temp } else { 1.0 };
278 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
279 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
280 if sum > 0.0 {
281 (((logits[id as usize] - max) / t).exp()) / sum
282 } else {
283 0.0
284 }
285}
286
287fn prefill_batched() -> bool {
290 std::env::var("CMF_PREFILL").map(|v| v != "seq").unwrap_or(true)
291}
292
293fn prefill_chunk() -> usize {
298 if let Some(n) =
299 std::env::var("CMF_PREFILL_CHUNK").ok().and_then(|v| v.parse::<usize>().ok())
300 {
301 return n.max(1);
302 }
303 if cfg!(target_os = "macos") {
304 512
305 } else {
306 48
307 }
308}
309
310pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
312
313impl Pipeline {
314 #[allow(clippy::too_many_arguments)]
316
317 #[cfg(target_os = "macos")]
332 fn graph_prefill_preferred(&self) -> bool {
333 if !crate::gpu::enabled_here()
334 || !crate::gpu::q1_force()
335 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
336 {
337 return false;
338 }
339 self.weights.layers.iter().any(
340 |lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()),
341 )
342 }
343
344 #[cfg(not(target_os = "macos"))]
345 fn graph_prefill_preferred(&self) -> bool {
346 false
347 }
348
349 #[cfg(target_os = "macos")]
350 fn q1_graph_gpu(
351 &mut self,
352 start: usize,
353 upto: Option<usize>,
354 position: usize,
355 h: &mut [f32],
356 ) -> usize {
357 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
358 if !crate::gpu::enabled_here()
359 || !crate::gpu::q1_force()
360 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
361 {
362 return start;
363 }
364 let limit = upto.map(|u| u + 1).unwrap_or(self.num_layers).min(self.num_layers);
365
366 enum Item<'a> {
367 Gdn {
368 run: Vec<GdnGpuLayer<'a>>,
369 first: usize,
370 },
371 Attn {
372 l: AttnGpuLayer<'a>,
373 li: usize,
374 q_norm: Option<&'a [f32]>,
375 k_norm: Option<&'a [f32]>,
376 output_gate: bool,
377 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
378 full_gpu: bool,
381 },
382 }
383
384 let dev_attend = std::env::var("CMF_GPU_ATTEND").map(|v| v != "0").unwrap_or(true)
386 && self.head_dim % 4 == 0
387 && self.head_dim <= 128
388 && self.rotary_dim >= 2
389 && self.rotary_dim <= self.head_dim
390 && (self.rotary_dim / 2) % 32 == 0
391 && self.num_kv_heads > 0
392 && self.num_heads % self.num_kv_heads == 0;
393
394 let mut plan: Vec<Item> = Vec::new();
395 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
396 let mut scan = start;
397 while scan < limit {
398 let lw = &self.weights.layers[scan];
399 let FfnKind::Dense(d) = &lw.ffn else { break };
400 let (Some(g), Some(u), Some(dn)) =
401 (d.gate_proj.q1_parts(), d.up_proj.q1_parts(), d.down_proj.q1_parts())
402 else {
403 break;
404 };
405 match &lw.attn {
406 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
407 let parts = (
408 w.in_proj_qkv.q1_parts(),
409 w.in_proj_z.q1_parts(),
410 w.in_proj_a.f32_parts(),
411 w.in_proj_b.f32_parts(),
412 w.out_proj.q1_parts(),
413 );
414 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else { break };
415 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
416 model_ref.get_or_insert_with(|| model.clone());
417 }
418 let gl = GdnGpuLayer {
419 attn_norm: &lw.input_norm,
420 post_norm: &lw.post_norm,
421 qkv,
422 z,
423 a,
424 b,
425 out,
426 gate: g,
427 up: u,
428 down: dn,
429 conv1d: &w.conv1d,
430 a_log: &w.a_log,
431 dt_bias: &w.dt_bias,
432 gnorm: &w.norm,
433 };
434 match plan.last_mut() {
435 Some(Item::Gdn { run, .. }) => run.push(gl),
436 _ => plan.push(Item::Gdn { run: vec![gl], first: scan }),
437 }
438 }
439 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, bias }
440 if !self.kv_cache.layers[scan].o1_sealed() =>
441 {
442 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
443 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else { break };
444 if let QTensor::Mapped { model, .. } = wq {
445 model_ref.get_or_insert_with(|| model.clone());
446 }
447 let cache = &self.kv_cache.layers[scan];
448 let full_gpu = dev_attend
449 && cache.mode == crate::kv_cache::KvMode::F32
450 && cache.o1.is_none()
451 && bias.is_none()
452 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
453 && pk.1 == self.num_kv_heads * self.head_dim
454 && pv.1 == self.num_kv_heads * self.head_dim
455 && po.2 == self.num_heads * self.head_dim;
456 plan.push(Item::Attn {
457 l: AttnGpuLayer {
458 attn_norm: &lw.input_norm,
459 post_norm: &lw.post_norm,
460 wq: pq,
461 wk: pk,
462 wv: pv,
463 wo: po,
464 gate: g,
465 up: u,
466 down: dn,
467 },
468 li: scan,
469 q_norm: q_norm.as_deref(),
470 k_norm: k_norm.as_deref(),
471 output_gate: *output_gate,
472 bias: bias
473 .as_ref()
474 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
475 full_gpu,
476 });
477 }
478 _ => break,
479 }
480 scan += 1;
481 }
482 let Some(model) = model_ref else { return start };
483 if plan.is_empty() {
484 return start;
485 }
486 let dims = GraphDims {
487 hidden: self.hidden_size,
488 eps: self.rms_eps as f32,
489 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
490 };
491 let Some(mut graph) = TokenGraph::new(&model, dims, h) else { return start };
492 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
493 nv: cfg.num_v_heads,
494 nk: cfg.num_k_heads,
495 dk: cfg.key_head_dim,
496 dv: cfg.value_head_dim,
497 kk: cfg.conv_kernel,
498 hidden: self.hidden_size,
499 inter: self.intermediate_size,
500 c_dim: cfg.conv_dim(),
501 eps: cfg.rms_eps as f32,
502 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
503 });
504 let mut valid = 0usize;
508 let mut end = start;
509 for item in &plan {
510 let ok = match item {
511 Item::Gdn { run, .. } => gcfg
512 .as_ref()
513 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
514 .unwrap_or(false),
515 Item::Attn { l, .. } => graph.attn_ok(l),
516 };
517 if !ok {
518 break;
519 }
520 valid += 1;
521 end += match item {
522 Item::Gdn { run, .. } => run.len(),
523 Item::Attn { .. } => 1,
524 };
525 }
526 plan.truncate(valid);
527 if plan.is_empty() {
528 return start;
529 }
530
531 let inv_freq = self.inv_freq.clone();
532 let pool = self.pool.clone();
533 let (nh, nkv, hd, hs, rd, eps) = (
534 self.num_heads,
535 self.num_kv_heads,
536 self.head_dim,
537 self.hidden_size,
538 self.rotary_dim,
539 self.rms_eps,
540 );
541 let norm_style = self.norm_style;
542 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
543 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
544 let kv_id = self.graph_kv_id;
545 let mut pending: Vec<(usize, usize)> = Vec::new();
548 let mut dev_attn: Vec<usize> = Vec::new();
551 for item in &plan {
552 match item {
553 Item::Gdn { run, first } => {
554 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
555 if l.linear_state.len() != want {
556 l.linear_state = vec![0f32; want];
557 }
558 }
559 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
560 .iter()
561 .map(|l| l.linear_state.as_slice())
562 .collect();
563 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
564 tracing::error!("q1 graph: GDN run refused after validation");
566 return start;
567 }
568 graph.commit();
571 pending.push((*first, run.len()));
572 }
573 Item::Attn { l, li, q_norm, k_norm, output_gate, bias, full_gpu } => {
574 if *full_gpu {
576 let cache = &self.kv_cache.layers[*li];
577 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
578 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
579 let cpu_stored = cpu_k[0].len() / hd;
580 let p = crate::gpu::AttnDeviceParams {
581 kv_id,
582 layer: *li,
583 nh,
584 nkv,
585 hd,
586 rd,
587 position,
588 eps: eps as f32,
589 gemma,
590 output_gate: *output_gate,
591 q_norm: *q_norm,
592 k_norm: *k_norm,
593 inv_freq: &inv_freq,
594 cpu_k,
595 cpu_v,
596 cpu_stored,
597 };
598 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
599 graph.commit();
600 dev_attn.push(*li);
601 continue;
602 }
603 }
605 graph.encode_attn_prefix(l);
606 graph.sync();
607 if !pending.is_empty() {
608 let idxs: Vec<usize> =
609 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
610 let mut outs: Vec<&mut [f32]> = self
611 .kv_cache
612 .layers
613 .iter_mut()
614 .enumerate()
615 .filter(|(i, _)| idxs.binary_search(i).is_ok())
616 .map(|(_, s)| s.linear_state.as_mut_slice())
617 .collect();
618 graph.read_states(&mut outs);
619 }
620 let mut q_raw = attention::take_buf(l.wq.1);
621 let mut k = attention::take_buf(l.wk.1);
622 let mut v = attention::take_buf(l.wv.1);
623 graph.read_qkv(&mut q_raw, &mut k, &mut v);
624 let cfg = QwenAttnCfg {
625 num_heads: nh,
626 num_kv_heads: nkv,
627 head_dim: hd,
628 hidden_size: hs,
629 position,
630 inv_freq: &inv_freq,
631 rotary_dim: rd,
632 q_norm: *q_norm,
633 k_norm: *k_norm,
634 output_gate: *output_gate,
635 bias: *bias,
636 rms_eps: eps,
637 norm_style,
638 pool: pool.as_deref(),
639 };
640 let mut ao = attention::qwen_attention_core(
641 q_raw,
642 k,
643 v,
644 &mut self.kv_cache.layers[*li],
645 &cfg,
646 );
647 graph.encode_attn_suffix(l, &ao);
648 graph.commit();
651 attention::recycle_buf(&mut ao);
652 }
653 }
654 }
655 graph.sync();
656 if !pending.is_empty() {
657 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
658 let mut outs: Vec<&mut [f32]> = self
659 .kv_cache
660 .layers
661 .iter_mut()
662 .enumerate()
663 .filter(|(i, _)| idxs.binary_search(i).is_ok())
664 .map(|(_, s)| s.linear_state.as_mut_slice())
665 .collect();
666 graph.read_states(&mut outs);
667 }
668 graph.finish(h);
669 for li in dev_attn {
673 let mut krow = attention::take_buf(nkv * hd);
674 let mut vrow = attention::take_buf(nkv * hd);
675 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
676 let cache = &mut self.kv_cache.layers[li];
677 cache.append(&krow, &vrow, &[]);
678 let n = cache.seq_len;
679 let mut imp = attention::take_buf(n);
680 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
681 cache.accumulate_imp(&imp);
682 attention::recycle_buf(&mut imp);
683 }
684 attention::recycle_buf(&mut krow);
685 attention::recycle_buf(&mut vrow);
686 }
687 end
688 }
689
690 pub fn new(
691 tokenizer: Tokenizer,
692 weights: PipelineWeights,
693 hidden_size: usize,
694 intermediate_size: usize,
695 num_heads: usize,
696 num_kv_heads: usize,
697 head_dim: usize,
698 num_layers: usize,
699 vocab_size: usize,
700 rms_eps: f64,
701 rope_base: f32,
702 norm_style: NormStyle,
703 max_seq_len: usize,
704 sampler_config: SamplerConfig,
705 ) -> Self {
706 let rng = match sampler_config.seed {
707 Some(s) => SplitMix64::new(s),
708 None => SplitMix64::from_entropy(),
709 };
710 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
711 let pool = Pool::from_env();
712 if let Some(p) = &pool {
713 tracing::info!("worker pool: {} threads", p.n_workers());
714 }
715 Self {
716 tokenizer: std::sync::Arc::new(tokenizer),
717 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
718 sampler_config,
719 weights,
720 hidden_size,
721 intermediate_size,
722 num_heads,
723 num_kv_heads,
724 head_dim,
725 num_layers,
726 vocab_size,
727 rms_eps,
728 rope_base,
729 norm_style,
730 rotary_dim: head_dim,
731 vmf_cfg: None,
732 gdn_cfg: None,
733 mtp: None,
734 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
735 rng,
736 inv_freq,
737 ws: ForwardScratch::new(hidden_size),
738 pool,
739 model: None,
740 dyn_force_f32: false,
741 dyn_skill_layers: Vec::new(),
742 dyn_active: None,
743 dyn_blend_loaded: false,
744 dyn_phi_layer: None,
745 dyn_phi_ema: Vec::new(),
746 dyn_phi_seen: 0,
747 dyn_router: None,
748 o1_cfg: None,
749 o1_flags: Vec::new(),
750 trace: false,
751 calib_temp: 1.0,
752 confidence_on: true,
753 graph_kv_id: {
754 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
755 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
756 },
757 }
758 }
759
760 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
767 self.o1_flags = match &cfg {
768 Some(c) => {
769 let mut flags = c.layer_flags(self.num_layers);
770 for (li, f) in flags.iter_mut().enumerate() {
771 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
772 *f = false;
773 }
774 }
775 flags
776 }
777 None => Vec::new(),
778 };
779 if let Some(c) = &cfg {
780 let n = self.o1_flags.iter().filter(|&&f| f).count();
781 tracing::info!(
782 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
783 self.num_layers, c.m, c.w, c.sink, c.rect
784 );
785 }
786 self.o1_cfg = cfg;
787 }
788
789 pub fn o1_active(&self) -> bool {
791 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
792 }
793
794 fn o1_begin(&mut self) {
796 if let Some(c) = &self.o1_cfg {
797 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
798 for (li, &f) in self.o1_flags.iter().enumerate() {
799 if f {
800 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
801 }
802 }
803 }
804 }
805
806 fn o1_seal(&mut self) {
809 if self.o1_cfg.is_none() {
810 return;
811 }
812 for li in 0..self.num_layers {
813 if self.o1_flags.get(li).copied().unwrap_or(false) {
814 self.kv_cache.layers[li].o1_seal(self.num_heads);
815 }
816 }
817 }
818
819 pub fn set_trace(&mut self, on: bool) {
821 self.trace = on;
822 }
823
824 pub fn set_confidence(&mut self, on: bool) {
829 self.confidence_on = on;
830 }
831
832 pub fn set_calib_temp(&mut self, t: f32) {
835 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
836 }
837
838 pub fn calib_temp(&self) -> f32 {
840 self.calib_temp
841 }
842
843 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
846 self.rotary_dim = rotary_dim.min(self.head_dim);
847 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
848 }
849
850 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
851 QwenAttnCfg {
852 num_heads: self.num_heads,
853 num_kv_heads: self.num_kv_heads,
854 head_dim: self.head_dim,
855 hidden_size: self.hidden_size,
856 position,
857 inv_freq: &self.inv_freq,
858 rotary_dim: self.rotary_dim,
859 q_norm: None,
860 k_norm: None,
861 output_gate: false,
862 bias: None,
863 rms_eps: self.rms_eps,
864 norm_style: self.norm_style,
865 pool: self.pool.as_deref(),
866 }
867 }
868
869 pub fn generate(
871 &mut self,
872 prompt: &str,
873 max_tokens: usize,
874 task_mask: Option<&TaskMask>,
875 on_token: Option<TokenCallback>,
876 ) -> Result<GenerateResult, String> {
877 let input_ids = self.tokenizer.encode(prompt);
878 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
879 }
880
881 pub fn generate_from_ids(
889 &mut self,
890 input_ids: &[u32],
891 max_tokens: usize,
892 task_mask: Option<&TaskMask>,
893 mut on_token: Option<TokenCallback>,
894 ) -> Result<GenerateResult, String> {
895 if input_ids.is_empty() {
896 return Err("empty prompt: nothing to generate from".to_string());
897 }
898
899 self.kv_cache.clear();
901 self.o1_begin();
902
903 let spec_active = self.speculative
907 && self.mtp.is_some()
908 && task_mask.is_none()
909 && !self.o1_active()
910 && self.sampler_config.temperature < 1e-6;
911 let mut mtp = if spec_active { self.mtp.take() } else { None };
914 if let Some(m) = &mut mtp {
915 m.kv.clear();
916 }
917 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
921 if let Some(r) = &mut router {
922 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
925 }
926
927 let mut all_ids = input_ids.to_vec();
928 let mut generated = 0usize;
929 let mut finish_reason = "max_tokens".to_string();
930 let mut drafted = 0usize;
931 let mut accepted = 0usize;
932 let mut confidence: Vec<f32> = Vec::new();
933 let trace_on = self.trace;
934 let calib_temp = self.calib_temp;
935 let mut traces: Vec<TokenTrace> = Vec::new();
936
937 let mut hidden = vec![0.0f32; self.hidden_size];
943 let mut pos = 0usize;
944 let dyn_prefill = router.is_some();
949 let graph_prefill = self.graph_prefill_preferred();
955 if task_mask.is_none()
956 && !dyn_prefill
957 && !graph_prefill
958 && prefill_batched()
959 && input_ids.len() > 2
960 {
961 let chunk = prefill_chunk();
967 let hs = self.hidden_size;
968 while pos < input_ids.len() {
969 let end = (pos + chunk).min(input_ids.len());
970 let hb = self.prefill_batch(&input_ids[pos..end], pos);
971 if let Some(m) = &mut mtp {
972 for p in pos..end {
973 if p + 1 < input_ids.len() {
974 let _ = self.mtp_step(
975 m,
976 &hb[(p - pos) * hs..(p - pos + 1) * hs],
977 input_ids[p + 1],
978 p,
979 );
980 }
981 }
982 }
983 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
984 pos = end;
985 }
986 }
987 if task_mask.is_none() && !dyn_prefill && !graph_prefill {
988 while pos + 1 < input_ids.len() {
989 let e1 = self.embed_single(input_ids[pos]);
990 let e2 = self.embed_single(input_ids[pos + 1]);
991 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
992 self.commit_linear_scratch();
994 if let Some(m) = &mut mtp {
995 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
996 if pos + 2 < input_ids.len() {
997 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
998 }
999 }
1000 hidden = h2;
1001 pos += 2;
1002 }
1003 }
1004 while pos < input_ids.len() {
1005 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1006 if let Some(m) = &mut mtp {
1007 if pos + 1 < input_ids.len() {
1008 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1009 }
1010 }
1011 pos += 1;
1012 }
1013 self.o1_seal();
1016
1017 macro_rules! commit {
1019 ($id:expr) => {{
1020 all_ids.push($id);
1021 generated += 1;
1022 if self.tokenizer.is_eos($id) {
1023 finish_reason = "stop".to_string();
1024 false
1025 } else {
1026 let token_text = self.tokenizer.decode_token($id);
1027 let mut go = true;
1028 if let Some(ref mut cb) = on_token {
1029 if !cb(&token_text) {
1030 finish_reason = "cancelled".to_string();
1031 go = false;
1032 }
1033 }
1034 go
1035 }
1036 }};
1037 }
1038
1039 let mut next_pos = input_ids.len();
1041 'decode: while generated < max_tokens {
1042 inference::rms_norm_into(
1043 &hidden,
1044 &self.weights.final_norm,
1045 self.rms_eps,
1046 self.norm_style,
1047 &mut self.ws.n1,
1048 );
1049 let mut logits = self.lm_head_forward(&self.ws.n1);
1050 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
1051 if self.confidence_on {
1052 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1053 }
1054 attention::recycle_buf(&mut logits);
1055 if trace_on {
1056 let skill = router.as_ref().and_then(|r| r.active_id());
1060 traces.push(TokenTrace {
1061 t: generated,
1062 token_id: t_next,
1063 confidence: confidence.last().copied().unwrap_or(0.0),
1064 active_skill: skill,
1065 recon: None,
1066 switched: false,
1067 });
1068 }
1069 if !commit!(t_next) {
1070 break 'decode;
1071 }
1072 if generated >= max_tokens {
1073 break 'decode;
1074 }
1075
1076 if self.kv_cache.needs_eviction() {
1077 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1078 self.kv_cache.evict(keep);
1079 }
1080
1081 match &mut mtp {
1082 Some(m) if generated + 1 < max_tokens => {
1084 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1085 drafted += 1;
1086 let emb1 = self.embed_single(t_next);
1087 let emb2 = self.embed_single(draft);
1088 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1089
1090 inference::rms_norm_into(
1091 &h1,
1092 &self.weights.final_norm,
1093 self.rms_eps,
1094 self.norm_style,
1095 &mut self.ws.n1,
1096 );
1097 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1098 let t_after =
1099 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
1100 if self.confidence_on {
1101 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
1102 }
1103 attention::recycle_buf(&mut logits1);
1104 if trace_on {
1105 traces.push(TokenTrace {
1108 t: generated,
1109 token_id: t_after,
1110 confidence: confidence.last().copied().unwrap_or(0.0),
1111 active_skill: None,
1112 recon: None,
1113 switched: false,
1114 });
1115 }
1116 let stop = !commit!(t_after);
1117
1118 if t_after == draft {
1119 accepted += 1;
1120 self.commit_linear_scratch();
1121 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1122 hidden = h2;
1123 next_pos += 2;
1124 } else {
1125 for layer in &mut self.kv_cache.layers {
1127 layer.truncate_last(1);
1128 }
1129 if !stop {
1130 let _ = self.mtp_step(m, &h1, t_after, next_pos);
1131 hidden = self
1132 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
1133 }
1134 next_pos += 2;
1135 }
1136 if stop {
1137 break 'decode;
1138 }
1139 }
1140 _ => {
1142 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1143 next_pos += 1;
1144 if let Some(r) = &mut router {
1147 let phi = self.dyn_phi_ema.clone();
1148 let decision = r.step(&phi, generated);
1149 if let Some(new_active) = decision {
1150 let _ = self.set_active_skill(new_active);
1151 }
1152 if trace_on {
1155 if let Some(last) = traces.last_mut() {
1156 let e = r.last_best_e();
1157 last.recon = e.is_finite().then_some(e);
1158 last.switched = decision.is_some();
1159 }
1160 }
1161 }
1162 }
1163 }
1164 }
1165
1166 if router.is_some() {
1168 let _ = self.set_active_skill(None);
1169 }
1170 self.dyn_router = router.or(self.dyn_router.take());
1171 self.mtp = mtp.or(self.mtp.take());
1172
1173 let output_ids = &all_ids[input_ids.len()..];
1174 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
1176 Ok(GenerateResult {
1177 text: self.tokenizer.decode(output_ids),
1178 token_ids: output_ids.to_vec(),
1179 prompt_tokens: input_ids.len(),
1180 tokens_generated: generated,
1181 finish_reason,
1182 mtp_drafted: drafted,
1183 mtp_accepted: accepted,
1184 token_confidence: confidence,
1185 traces,
1186 })
1187 }
1188
1189 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
1193 let e = self.embed_single(next_token);
1197 let mut cat = vec![0.0f32; 2 * self.hidden_size];
1198 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
1199 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
1200 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
1201 let mut x = vec![0.0f32; self.hidden_size];
1202 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
1203
1204 let lw = &m.layer;
1206 inference::rms_norm_into(&x, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1207 let attn = match &lw.attn {
1208 AttnKind::Full {
1209 wq,
1210 wk,
1211 wv,
1212 wo,
1213 q_norm,
1214 k_norm,
1215 output_gate,
1216 bias,
1217 } => {
1218 let mut cfg = self.attn_cfg(position);
1219 cfg.q_norm = q_norm.as_deref();
1220 cfg.k_norm = k_norm.as_deref();
1221 cfg.output_gate = *output_gate;
1222 cfg.bias = bias
1223 .as_ref()
1224 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
1225 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
1226 }
1227 AttnKind::Linear(_) | AttnKind::LinearGdn(_) => {
1228 unreachable!("MTP block is full attention")
1229 }
1230 };
1231 for (i, &a) in attn.iter().enumerate() {
1232 x[i] += a;
1233 }
1234 inference::rms_norm_into(&x, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1235 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
1236 for (i, &f) in ffn.iter().enumerate() {
1237 x[i] += f;
1238 }
1239
1240 inference::rms_norm_into(&x, &m.final_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1241 let mut lg = self.lm_head_forward(&self.ws.n1);
1242 let draft = sampler::argmax(&lg);
1243 attention::recycle_buf(&mut lg);
1244 draft
1245 }
1246
1247 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
1251 let emb1 = self.embed_single(1);
1252 let emb2 = self.embed_single(2);
1253 let pos = self.kv_cache.seq_len();
1254
1255 let t0 = std::time::Instant::now();
1256 for _ in 0..iters {
1257 let _ = self.forward_layers(&emb1, pos, None);
1258 let _ = self.forward_layers(&emb2, pos + 1, None);
1259 for l in &mut self.kv_cache.layers {
1260 l.truncate_last(2);
1261 }
1262 }
1263 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1264
1265 let t1 = std::time::Instant::now();
1266 for _ in 0..iters {
1267 let _ = self.forward_pair(&emb1, &emb2, pos);
1268 for l in &mut self.kv_cache.layers {
1269 l.truncate_last(2);
1270 }
1271 }
1272 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
1273 (singles_ms, pair_ms)
1274 }
1275
1276 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
1281 let mut h1 = emb1.to_vec();
1282 let mut h2 = emb2.to_vec();
1283 let (nh, nkv, hd, hs, rd, eps) = (
1284 self.num_heads,
1285 self.num_kv_heads,
1286 self.head_dim,
1287 self.hidden_size,
1288 self.rotary_dim,
1289 self.rms_eps,
1290 );
1291 let inv_freq = self.inv_freq.clone();
1292 let pool = self.pool.clone();
1293
1294 for li in 0..self.num_layers {
1295 let lw = &self.weights.layers[li];
1296 inference::rms_norm_into(&h1, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1299 inference::rms_norm_into(&h2, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n2);
1300
1301 let (a1, a2) = match &lw.attn {
1302 AttnKind::Linear(w) => {
1303 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1304 let layer = &mut self.kv_cache.layers[li];
1305 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1306 vmf_phase_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1307 }
1308 AttnKind::LinearGdn(w) => {
1309 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1310 let layer = &mut self.kv_cache.layers[li];
1311 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
1312 gdn_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
1313 }
1314 AttnKind::Full {
1315 wq,
1316 wk,
1317 wv,
1318 wo,
1319 q_norm,
1320 k_norm,
1321 output_gate,
1322 bias,
1323 } => {
1324 let cfg = QwenAttnCfg {
1325 num_heads: nh,
1326 num_kv_heads: nkv,
1327 head_dim: hd,
1328 hidden_size: hs,
1329 position,
1330 inv_freq: &inv_freq,
1331 rotary_dim: rd,
1332 q_norm: q_norm.as_deref(),
1333 k_norm: k_norm.as_deref(),
1334 output_gate: *output_gate,
1335 bias: bias
1336 .as_ref()
1337 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1338 rms_eps: eps,
1339 norm_style: self.norm_style,
1340 pool: pool.as_deref(),
1341 };
1342 attention::qwen_attention_pair(
1343 &self.ws.n1,
1344 &self.ws.n2,
1345 wq,
1346 wk,
1347 wv,
1348 wo,
1349 &mut self.kv_cache.layers[li],
1350 &cfg,
1351 )
1352 }
1353 };
1354 for i in 0..self.hidden_size {
1355 h1[i] += a1[i];
1356 h2[i] += a2[i];
1357 }
1358 let (mut a1, mut a2) = (a1, a2);
1359 attention::recycle_buf(&mut a1);
1360 attention::recycle_buf(&mut a2);
1361
1362 let lw = &self.weights.layers[li];
1363 inference::rms_norm_into(&h1, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1364 inference::rms_norm_into(&h2, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p2);
1365 let (f1, f2) =
1366 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1367 for i in 0..self.hidden_size {
1368 h1[i] += f1[i];
1369 h2[i] += f2[i];
1370 }
1371 let (mut f1, mut f2) = (f1, f2);
1372 attention::recycle_buf(&mut f1);
1373 attention::recycle_buf(&mut f2);
1374 }
1375 (h1, h2)
1376 }
1377
1378 fn commit_linear_scratch(&mut self) {
1380 for layer in &mut self.kv_cache.layers {
1381 if !layer.linear_scratch.is_empty() {
1382 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1383 layer.linear_scratch.clear();
1384 }
1385 }
1386 }
1387
1388 pub fn forward_ids(
1391 &mut self,
1392 ids: &[u32],
1393 task_mask: Option<&TaskMask>,
1394 ) -> Result<Vec<f32>, String> {
1395 if ids.is_empty() {
1396 return Err("empty id sequence".to_string());
1397 }
1398 self.kv_cache.clear();
1399 self.o1_begin();
1400 let mut hidden = vec![0.0f32; self.hidden_size];
1401 let mut pos = 0usize;
1402 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1403 let chunk = prefill_chunk();
1407 let hs = self.hidden_size;
1408 while pos < ids.len() {
1409 let end = (pos + chunk).min(ids.len());
1410 let hb = self.prefill_batch(&ids[pos..end], pos);
1411 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1412 pos = end;
1413 }
1414 }
1415 if task_mask.is_none() {
1416 while pos + 1 < ids.len() {
1417 let e1 = self.embed_single(ids[pos]);
1418 let e2 = self.embed_single(ids[pos + 1]);
1419 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1420 self.commit_linear_scratch();
1421 hidden = h2;
1422 pos += 2;
1423 }
1424 }
1425 while pos < ids.len() {
1426 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1427 pos += 1;
1428 }
1429 self.o1_seal();
1433 let normed = inference::rms_norm(
1434 &hidden,
1435 &self.weights.final_norm,
1436 self.rms_eps,
1437 self.norm_style,
1438 );
1439 Ok(self.lm_head_forward(&normed))
1440 }
1441
1442 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1449 let (nll, cnt) = self.nll_ids_from(ids, 0);
1450 (nll / cnt.max(1) as f64).exp()
1451 }
1452
1453 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1462 self.kv_cache.clear();
1463 let mut nll = 0f64;
1464 let mut cnt = 0usize;
1465 if prefill_batched() {
1466 const CHUNK: usize = 128;
1472 const LM_SUB: usize = 32;
1473 let n = ids.len().saturating_sub(1);
1474 let hs = self.hidden_size;
1475 let rows = self.weights.lm_head.rows();
1476 let mut pos = 0usize;
1477 while pos < n {
1478 let end = (pos + CHUNK).min(n);
1479 let bsz = end - pos;
1480 let hb = self.prefill_batch(&ids[pos..end], pos);
1481 let mut k0 = 0usize;
1482 while k0 < bsz {
1483 let k1 = (k0 + LM_SUB).min(bsz);
1484 let sb = k1 - k0;
1485 if pos + k1 <= start {
1488 k0 = k1;
1489 continue;
1490 }
1491 let mut normed = vec![0.0f32; sb * hs];
1492 for k in 0..sb {
1493 let r = inference::rms_norm(
1494 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
1495 &self.weights.final_norm,
1496 self.rms_eps,
1497 self.norm_style,
1498 );
1499 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
1500 }
1501 let mut logits = vec![0.0f32; sb * rows];
1502 self.weights
1503 .lm_head
1504 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1505 for k in 0..sb {
1506 if pos + k0 + k < start {
1507 continue;
1508 }
1509 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1510 let target = ids[pos + k0 + k + 1] as usize;
1511 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1512 let lse: f64 = lg
1513 .iter()
1514 .map(|&v| ((v - max) as f64).exp())
1515 .sum::<f64>()
1516 .ln()
1517 + max as f64;
1518 nll += lse - lg[target] as f64;
1519 cnt += 1;
1520 }
1521 k0 = k1;
1522 }
1523 pos = end;
1524 }
1525 self.kv_cache.clear();
1526 return (nll, cnt);
1527 }
1528 for pos in 0..ids.len().saturating_sub(1) {
1529 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1530 if pos < start {
1531 continue;
1532 }
1533 let normed = inference::rms_norm(
1534 &hidden,
1535 &self.weights.final_norm,
1536 self.rms_eps,
1537 self.norm_style,
1538 );
1539 let logits = self.lm_head_forward(&normed);
1540 let target = ids[pos + 1] as usize;
1541 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1542 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1543 + max as f64;
1544 nll += lse - logits[target] as f64;
1545 cnt += 1;
1546 }
1547 self.kv_cache.clear();
1548 (nll, cnt)
1549 }
1550
1551 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1567 self.kv_cache.clear();
1568 self.o1_begin();
1569 let n = ids.len().saturating_sub(1);
1570 let p = prefill.min(n);
1571 let mut pos = 0usize;
1573 if prefill_batched() {
1574 const CHUNK: usize = 128;
1575 while pos < p {
1576 let end = (pos + CHUNK).min(p);
1577 let _ = self.prefill_batch(&ids[pos..end], pos);
1578 pos = end;
1579 }
1580 } else {
1581 while pos < p {
1582 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1583 pos += 1;
1584 }
1585 }
1586 self.o1_seal();
1587
1588 let mut nll = 0f64;
1589 let mut cnt = 0usize;
1590 for pos in p..n {
1591 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1592 let normed = inference::rms_norm(
1593 &hidden,
1594 &self.weights.final_norm,
1595 self.rms_eps,
1596 self.norm_style,
1597 );
1598 let logits = self.lm_head_forward(&normed);
1599 let target = ids[pos + 1] as usize;
1600 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1601 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1602 + max as f64;
1603 nll += lse - logits[target] as f64;
1604 cnt += 1;
1605 }
1606 self.kv_cache.clear();
1607 (nll, cnt)
1608 }
1609
1610 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1618 self.kv_cache.clear();
1619 let n = ids.len().saturating_sub(1);
1620 let mut correct = Vec::with_capacity(n);
1621 let mut pmax = Vec::with_capacity(n);
1622 for pos in 0..n {
1623 let emb = self.embed_single(ids[pos]);
1624 let hidden = self.forward_layers(&emb, pos, None);
1625 let normed = inference::rms_norm(
1626 &hidden,
1627 &self.weights.final_norm,
1628 self.rms_eps,
1629 self.norm_style,
1630 );
1631 let logits = self.lm_head_forward(&normed);
1632 let target = ids[pos + 1] as usize;
1633 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1634 for (i, &v) in logits.iter().enumerate() {
1635 if v > mval {
1636 mval = v;
1637 amax = i;
1638 }
1639 }
1640 correct.push(amax == target);
1641 let row: Vec<f32> = temps
1642 .iter()
1643 .map(|&t| {
1644 let tt = t.max(1e-3);
1645 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1646 1.0 / s.max(1e-12) })
1648 .collect();
1649 pmax.push(row);
1650 }
1651 self.kv_cache.clear();
1652 (correct, pmax)
1653 }
1654
1655 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1662 let mut router = match self.dyn_router.take() {
1663 Some(r) => r,
1664 None => return (self.ppl_ids(ids), 0),
1665 };
1666 router.reset();
1667 self.dyn_phi_seen = 0;
1668 let _ = self.set_active_skill(None);
1669
1670 self.kv_cache.clear();
1671 let mut nll = 0f64;
1672 let mut cnt = 0usize;
1673 for pos in 0..ids.len().saturating_sub(1) {
1674 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1675 let normed = inference::rms_norm(
1676 &hidden,
1677 &self.weights.final_norm,
1678 self.rms_eps,
1679 self.norm_style,
1680 );
1681 let logits = self.lm_head_forward(&normed);
1682 let target = ids[pos + 1] as usize;
1683 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1684 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1685 + max as f64;
1686 nll += lse - logits[target] as f64;
1687 cnt += 1;
1688 let phi = self.dyn_phi_ema.clone();
1690 if let Some(new_active) = router.step(&phi, pos) {
1691 let _ = self.set_active_skill(new_active);
1692 }
1693 }
1694 let switches = router.switches.len();
1695 let _ = self.set_active_skill(None);
1696 self.dyn_router = Some(router);
1697 self.kv_cache.clear();
1698 ((nll / cnt.max(1) as f64).exp(), switches)
1699 }
1700
1701 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1703 self.kv_cache.clear();
1704 let mut acc = vec![0f32; self.hidden_size];
1705 for (pos, &id) in ids.iter().enumerate() {
1706 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1707 for (a, v) in acc.iter_mut().zip(&h) {
1708 *a += v;
1709 }
1710 }
1711 let n = ids.len().max(1) as f32;
1712 for a in acc.iter_mut() {
1713 *a /= n;
1714 }
1715 self.kv_cache.clear();
1716 acc
1717 }
1718
1719 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1725 let b = ids.len();
1726 let hs = self.hidden_size;
1727 let mut h: Vec<f32> = Vec::with_capacity(b * hs);
1728 for &id in ids {
1729 h.extend_from_slice(&self.embed_single(id));
1730 }
1731 let (nh, nkv, hd, rd, eps) = (
1732 self.num_heads,
1733 self.num_kv_heads,
1734 self.head_dim,
1735 self.rotary_dim,
1736 self.rms_eps,
1737 );
1738 let inv_freq = self.inv_freq.clone();
1739 let pool = self.pool.clone();
1740 let norm_style = self.norm_style;
1741
1742 for li in 0..self.num_layers {
1743 crate::gpu::set_layer(li as i64); let lw = &self.weights.layers[li];
1745 match &lw.attn {
1747 AttnKind::LinearGdn(w) => {
1748 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1750 let mut normed = vec![0.0f32; b * hs];
1751 for bi in 0..b {
1752 let r = inference::rms_norm(
1753 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1754 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1755 }
1756 let attn = crate::linear_core::gdn_forward_batch(
1757 &normed, b, w, &cfg,
1758 &mut self.kv_cache.layers[li].linear_state,
1759 pool.as_deref(),
1760 );
1761 for (dst, &a) in h.iter_mut().zip(&attn) {
1762 *dst += a;
1763 }
1764 }
1765 AttnKind::Full {
1766 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
1767 } => {
1768 let mut normed = vec![0.0f32; b * hs];
1772 for bi in 0..b {
1773 inference::rms_norm_into(
1774 &h[bi * hs..(bi + 1) * hs],
1775 &lw.input_norm,
1776 eps,
1777 norm_style,
1778 &mut normed[bi * hs..(bi + 1) * hs],
1779 );
1780 }
1781 let cfg = QwenAttnCfg {
1782 num_heads: nh,
1783 num_kv_heads: nkv,
1784 head_dim: hd,
1785 hidden_size: hs,
1786 position: start_pos,
1787 inv_freq: &inv_freq,
1788 rotary_dim: rd,
1789 q_norm: q_norm.as_deref(),
1790 k_norm: k_norm.as_deref(),
1791 output_gate: *output_gate,
1792 bias: bias.as_ref().map(|(a, b, c)| {
1793 (a.as_slice(), b.as_slice(), c.as_slice())
1794 }),
1795 rms_eps: eps,
1796 norm_style,
1797 pool: pool.as_deref(),
1798 };
1799 let attn = attention::qwen_attention_batch(
1800 &normed, b, wq, wk, wv, wo,
1801 &mut self.kv_cache.layers[li], &cfg);
1802 for (dst, &a) in h.iter_mut().zip(&attn) {
1803 *dst += a;
1804 }
1805 }
1806 AttnKind::Linear(w) => {
1807 for bi in 0..b {
1808 let normed = inference::rms_norm(
1809 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1810 vmf_phase_forward(
1811 &normed, w,
1812 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
1813 &mut self.kv_cache.layers[li].linear_state,
1814 pool.as_deref(),
1815 )
1816 .iter()
1817 .enumerate()
1818 .for_each(|(i, &a)| h[bi * hs + i] += a);
1819 }
1820 }
1821 }
1822
1823 let lw = &self.weights.layers[li];
1825 let mut post = vec![0.0f32; b * hs];
1826 for bi in 0..b {
1827 let r = inference::rms_norm(
1828 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
1829 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1830 }
1831 let ffn = match &lw.ffn {
1832 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
1833 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
1834 };
1835 for (dst, &f) in h.iter_mut().zip(&ffn) {
1836 *dst += f;
1837 }
1838 }
1839 crate::gpu::set_layer(-1); h
1841 }
1842
1843 fn embed_single(&self, id: u32) -> Vec<f32> {
1845 let mut out = vec![0.0f32; self.hidden_size];
1846 if (id as usize) < self.weights.embed_tokens.rows() {
1847 self.weights.embed_tokens.row_f32(id as usize, &mut out);
1848 }
1849 out
1850 }
1851
1852 fn forward_layers(
1854 &mut self,
1855 hidden: &[f32],
1856 position: usize,
1857 task_mask: Option<&TaskMask>,
1858 ) -> Vec<f32> {
1859 self.forward_layers_upto(hidden, position, task_mask, None)
1860 }
1861
1862 fn forward_layers_upto(
1864 &mut self,
1865 hidden: &[f32],
1866 position: usize,
1867 task_mask: Option<&TaskMask>,
1868 upto: Option<usize>,
1869 ) -> Vec<f32> {
1870 let mut h = hidden.to_vec();
1871 let (nh, nkv, hd, hs, rd, eps) = (
1874 self.num_heads,
1875 self.num_kv_heads,
1876 self.head_dim,
1877 self.hidden_size,
1878 self.rotary_dim,
1879 self.rms_eps,
1880 );
1881 let inv_freq = self.inv_freq.clone();
1882 let pool = self.pool.clone();
1883
1884 #[cfg(target_os = "macos")]
1885 let mut gpu_skip_until = 0usize;
1886 for li in 0..self.num_layers {
1887 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
1889 if li > u {
1890 break;
1891 }
1892 }
1893 if let Some(mask) = task_mask {
1894 if !mask.layer_alive(li) {
1895 continue; }
1897 }
1898 #[cfg(target_os = "macos")]
1902 {
1903 if li < gpu_skip_until {
1904 continue;
1905 }
1906 if task_mask.is_none() {
1907 let end = self.q1_graph_gpu(li, upto, position, &mut h);
1908 if end > li {
1909 gpu_skip_until = end;
1910 continue;
1911 }
1912 }
1913 }
1914
1915 let lw = &self.weights.layers[li];
1916 inference::rms_norm_into(&h, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1919
1920 let attn_out = match &lw.attn {
1921 AttnKind::Linear(w) => {
1922 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1923 vmf_phase_forward(
1924 &self.ws.n1,
1925 w,
1926 &cfg,
1927 &mut self.kv_cache.layers[li].linear_state,
1928 self.pool.as_deref(),
1929 )
1930 }
1931 AttnKind::LinearGdn(w) => {
1932 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1933 gdn_forward(
1934 &self.ws.n1,
1935 w,
1936 &cfg,
1937 &mut self.kv_cache.layers[li].linear_state,
1938 self.pool.as_deref(),
1939 )
1940 }
1941 AttnKind::Full {
1942 wq,
1943 wk,
1944 wv,
1945 wo,
1946 q_norm,
1947 k_norm,
1948 output_gate,
1949 bias,
1950 } if self.kv_cache.layers[li].o1_sealed() => {
1951 let cfg = QwenAttnCfg {
1954 num_heads: nh,
1955 num_kv_heads: nkv,
1956 head_dim: hd,
1957 hidden_size: hs,
1958 position,
1959 inv_freq: &inv_freq,
1960 rotary_dim: rd,
1961 q_norm: q_norm.as_deref(),
1962 k_norm: k_norm.as_deref(),
1963 output_gate: *output_gate,
1964 bias: bias
1965 .as_ref()
1966 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1967 rms_eps: eps,
1968 norm_style: self.norm_style,
1969 pool: pool.as_deref(),
1970 };
1971 attention::qwen_attention_nystrom(
1972 &self.ws.n1,
1973 wq,
1974 wk,
1975 wv,
1976 wo,
1977 &mut self.kv_cache.layers[li],
1978 &cfg,
1979 )
1980 }
1981 AttnKind::Full {
1982 wq,
1983 wk,
1984 wv,
1985 wo,
1986 q_norm,
1987 k_norm,
1988 output_gate,
1989 bias,
1990 } => {
1991 let masked = task_mask
1992 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
1993 .unwrap_or(false);
1994 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
1995 match (masked, f32_view) {
1996 (true, (Some(q), Some(k), Some(v), Some(o))) => {
1999 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
2000 attention::multi_head_attention(
2001 &self.ws.n1,
2002 q,
2003 k,
2004 v,
2005 o,
2006 &mut self.kv_cache.layers[li],
2007 self.num_heads,
2008 self.num_kv_heads,
2009 self.head_dim,
2010 self.hidden_size,
2011 position,
2012 &active_heads,
2013 &self.inv_freq,
2014 )
2015 }
2016 (masked, _) => {
2017 if masked {
2018 tracing::warn!(
2019 "layer {li}: head mask on quantized weights not \
2020 supported yet — executing dense"
2021 );
2022 }
2023 let cfg = QwenAttnCfg {
2024 num_heads: nh,
2025 num_kv_heads: nkv,
2026 head_dim: hd,
2027 hidden_size: hs,
2028 position,
2029 inv_freq: &inv_freq,
2030 rotary_dim: rd,
2031 q_norm: q_norm.as_deref(),
2032 k_norm: k_norm.as_deref(),
2033 output_gate: *output_gate,
2034 bias: bias
2035 .as_ref()
2036 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2037 rms_eps: eps,
2038 norm_style: self.norm_style,
2039 pool: pool.as_deref(),
2040 };
2041 attention::qwen_attention(
2042 &self.ws.n1,
2043 wq,
2044 wk,
2045 wv,
2046 wo,
2047 &mut self.kv_cache.layers[li],
2048 &cfg,
2049 )
2050 }
2051 }
2052 }
2053 };
2054 for (i, &a) in attn_out.iter().enumerate() {
2055 h[i] += a;
2056 }
2057 let mut attn_out = attn_out;
2058 attention::recycle_buf(&mut attn_out);
2059
2060 let lw = &self.weights.layers[li];
2061 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
2062 let post_normed = &self.ws.p1;
2063
2064 let ffn_masked = task_mask
2065 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
2066 .unwrap_or(false);
2067 let f32_ffn = match &lw.ffn {
2070 FfnKind::Dense(d) => {
2071 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
2072 }
2073 FfnKind::Moe(_) => (None, None, None),
2074 };
2075 let ffn_out = match (ffn_masked, f32_ffn) {
2076 (true, (Some(g), Some(u), Some(d))) => {
2077 let active = task_mask.unwrap().ffn_active_indices(li);
2078 inference::sparse_ffn_forward(
2079 &post_normed,
2080 g,
2081 u,
2082 d,
2083 self.hidden_size,
2084 self.intermediate_size,
2085 &active,
2086 self.pool.as_deref(),
2087 )
2088 }
2089 (true, _) => match &lw.ffn {
2093 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
2094 let active = task_mask.unwrap().ffn_active_indices(li);
2095 sparse_ffn_quant(
2096 d,
2097 &post_normed,
2098 &active,
2099 self.hidden_size,
2100 self.pool.as_deref(),
2101 )
2102 }
2103 FfnKind::Dense(d) => {
2108 let active = task_mask.unwrap().ffn_active_indices(li);
2109 let (gf, uf, df) = dequant_dense_f32(d);
2110 inference::sparse_ffn_forward(
2111 &post_normed,
2112 &gf,
2113 &uf,
2114 &df,
2115 self.hidden_size,
2116 self.intermediate_size,
2117 &active,
2118 self.pool.as_deref(),
2119 )
2120 }
2121 FfnKind::Moe(_) => {
2122 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
2125 }
2126 },
2127 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
2128 };
2129 for (i, &f) in ffn_out.iter().enumerate() {
2130 h[i] += f;
2131 }
2132 let mut ffn_out = ffn_out;
2133 attention::recycle_buf(&mut ffn_out);
2134
2135 if self.dyn_phi_layer == Some(li) {
2139 self.update_dyn_phi(&h);
2140 }
2141 }
2142 crate::gpu::set_layer(-1); h
2145 }
2146
2147 fn update_dyn_phi(&mut self, h: &[f32]) {
2150 const A: f32 = 0.2;
2151 if self.dyn_phi_ema.len() != h.len() {
2152 self.dyn_phi_ema = vec![0.0; h.len()];
2153 self.dyn_phi_seen = 0;
2154 }
2155 if self.dyn_phi_seen == 0 {
2156 self.dyn_phi_ema.copy_from_slice(h);
2157 } else {
2158 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
2159 *e = (1.0 - A) * *e + A * v;
2160 }
2161 }
2162 self.dyn_phi_seen += 1;
2163 }
2164
2165 pub fn dyn_phi(&self) -> &[f32] {
2167 &self.dyn_phi_ema
2168 }
2169
2170 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
2172 self.dyn_phi_layer = layer;
2173 self.dyn_phi_ema.clear();
2174 self.dyn_phi_seen = 0;
2175 }
2176
2177 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
2179 let Some(model) = &self.model else { return Vec::new() };
2180 model
2181 .header
2182 .skills
2183 .iter()
2184 .enumerate()
2185 .filter_map(|(i, sk)| {
2186 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
2187 let sel = sk.selection.as_ref()?;
2188 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
2189 })
2190 .collect()
2191 }
2192
2193 pub fn active_skill(&self) -> Option<usize> {
2195 self.dyn_active
2196 }
2197
2198 pub fn enable_dynamic_routing(&mut self) -> usize {
2203 use crate::swarm::{DynRouter, RoutableSkill};
2204 let Some(model) = self.model.clone() else { return 0 };
2205 if self.dyn_blend_loaded {
2208 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
2209 return 0;
2210 }
2211 if let Some(a) = self.dyn_active {
2215 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
2216 tracing::warn!(
2217 "loaded skill is not FFN-eligible — dynamic routing unavailable"
2218 );
2219 return 0;
2220 }
2221 }
2222 let hidden = self.hidden_size;
2223 let mut skills = Vec::new();
2224 for (idx, id, _phi) in self.dynamic_skills() {
2225 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
2226 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
2227 skills.push(rs);
2228 }
2229 }
2230 }
2231 if skills.is_empty() {
2232 return 0;
2233 }
2234 let phi = skills[0].phi_layer;
2236 if skills.iter().any(|s| s.phi_layer != phi) {
2237 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
2238 }
2239 let n = skills.len();
2240 self.set_dyn_phi_layer(Some(phi));
2241 self.dyn_router = Some(DynRouter::new(skills));
2242 n
2243 }
2244
2245 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
2247 self.dyn_router
2248 .as_ref()
2249 .map(|r| r.switches.clone())
2250 .unwrap_or_default()
2251 }
2252
2253 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
2256 let rows = self.weights.lm_head.rows();
2257 let mut logits = attention::take_buf(rows.min(self.vocab_size));
2258 self.weights
2259 .lm_head
2260 .matvec(hidden, &mut logits, self.pool.as_deref());
2261 logits.resize(self.vocab_size, 0.0);
2262 logits
2263 }
2264
2265 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
2270 self.kv_cache.clear();
2271 let mut hidden = vec![0.0f32; self.hidden_size];
2272 for (pos, &id) in ids.iter().enumerate() {
2273 let emb = self.embed_single(id);
2274 hidden = self.forward_layers(&emb, pos, task_mask);
2275 }
2276 inference::rms_norm_into(
2277 &hidden,
2278 &self.weights.final_norm,
2279 self.rms_eps,
2280 self.norm_style,
2281 &mut self.ws.n1,
2282 );
2283 self.lm_head_forward(&self.ws.n1)
2284 }
2285}
2286
2287pub fn create_test_pipeline(
2289 hidden_size: usize,
2290 intermediate_size: usize,
2291 num_heads: usize,
2292 num_kv_heads: usize,
2293 head_dim: usize,
2294 num_layers: usize,
2295 vocab_size: usize,
2296) -> Pipeline {
2297 let synth = |n: usize, salt: usize| -> Vec<f32> {
2300 (0..n)
2301 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
2302 .collect()
2303 };
2304 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2305 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2306 };
2307 let layer_weights: Vec<LayerWeights> = (0..num_layers)
2308 .map(|li| LayerWeights {
2309 input_norm: vec![1.0; hidden_size],
2310 post_norm: vec![1.0; hidden_size],
2311 ffn: FfnKind::Dense(DenseFfn {
2312 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
2313 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
2314 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
2315 }),
2316 attn: AttnKind::Full {
2317 bias: None,
2318 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
2319 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
2320 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
2321 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
2322 q_norm: None,
2323 k_norm: None,
2324 output_gate: false,
2325 },
2326 })
2327 .collect();
2328
2329 Pipeline::new(
2330 Tokenizer::byte_level(),
2331 PipelineWeights {
2332 embed_tokens: qt(vocab_size, hidden_size, 100),
2333 layers: layer_weights,
2334 lm_head: qt(vocab_size, hidden_size, 200),
2335 final_norm: vec![1.0; hidden_size],
2336 },
2337 hidden_size,
2338 intermediate_size,
2339 num_heads,
2340 num_kv_heads,
2341 head_dim,
2342 num_layers,
2343 vocab_size,
2344 1e-6,
2345 10_000.0,
2346 NormStyle::Qwen,
2347 4096,
2348 SamplerConfig {
2349 seed: Some(42),
2350 ..Default::default()
2351 },
2352 )
2353}
2354
2355fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
2358 let inter = d.gate_proj.rows();
2359 let hidden = d.down_proj.rows();
2360 let mut g = vec![0.0f32; b * inter];
2361 d.gate_proj.matmat(xs, b, &mut g, pool);
2362 let mut u = vec![0.0f32; b * inter];
2363 d.up_proj.matmat(xs, b, &mut u, pool);
2364 for i in 0..b * inter {
2365 g[i] = inference::silu(g[i]) * u[i];
2366 }
2367 let mut out = vec![0.0f32; b * hidden];
2368 d.down_proj.matmat(&g, b, &mut out, pool);
2369 out
2370}
2371
2372fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
2376 let ne = m.experts.len();
2377 let mut logits = vec![0.0f32; b * ne];
2378 m.router.matmat(xs, b, &mut logits, pool);
2379
2380 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
2383 {
2384 let mut st = m.stats.borrow_mut();
2385 if st.len() < ne {
2386 st.resize(ne, 0);
2387 }
2388 for bi in 0..b {
2389 let lg = &logits[bi * ne..(bi + 1) * ne];
2390 let mx = lg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2391 let mut p: Vec<f32> = lg.iter().map(|&l| (l - mx).exp()).collect();
2392 let sum: f32 = p.iter().sum();
2393 for v in &mut p {
2394 *v /= sum;
2395 }
2396 let mut order: Vec<usize> = (0..ne).collect();
2397 order.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y)));
2398 order.truncate(m.top_k);
2399 let wsum: f32 = if m.norm_topk_prob {
2400 order.iter().map(|&e| p[e]).sum()
2401 } else {
2402 1.0
2403 };
2404 for &e in &order {
2405 st[e] += 1;
2406 assign[e].push((bi, p[e] / wsum));
2407 }
2408 }
2409 }
2410
2411 let mut out = vec![0.0f32; b * hidden];
2412 let cols = m.experts[0].gate_proj.cols();
2413 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
2414 let sb = list.len();
2415 let mut sub = vec![0.0f32; sb * cols];
2416 for (k, &(bi, _)) in list.iter().enumerate() {
2417 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
2418 }
2419 let eo = dense_ffn_batch(d, &sub, sb, pool);
2420 for (k, &(bi, w)) in list.iter().enumerate() {
2421 for i in 0..hidden {
2422 out[bi * hidden + i] += w * eo[k * hidden + i];
2423 }
2424 }
2425 };
2426 for e in 0..ne {
2427 if !assign[e].is_empty() {
2428 run_expert(&m.experts[e], &assign[e]);
2429 }
2430 }
2431 if let Some((se, gate)) = &m.shared {
2432 let mut gl = vec![0.0f32; b];
2433 gate.matmat(xs, b, &mut gl, pool);
2434 let all: Vec<(usize, f32)> = (0..b)
2435 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
2436 .collect();
2437 run_expert(se, &all);
2438 }
2439 out
2440}
2441
2442thread_local! {
2443 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
2447 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
2448}
2449
2450fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2452 if crate::gpu::enabled_here()
2463 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
2464 {
2465 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
2466 crate::gpu::ProbeArm::Gpu
2467 } else {
2468 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
2469 };
2470 match arm {
2471 crate::gpu::ProbeArm::Gpu => {
2472 let t0 = std::time::Instant::now();
2473 if let Some(out) = dense_ffn_gpu(d, x, pool) {
2474 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
2475 return out;
2476 }
2477 }
2478 crate::gpu::ProbeArm::CpuTimed => {
2479 let t0 = std::time::Instant::now();
2480 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
2481 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
2482 return out;
2483 }
2484 crate::gpu::ProbeArm::Cpu => {
2485 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
2486 }
2487 }
2488 }
2489 dense_ffn_cpu(d, x, pool)
2490}
2491
2492fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2494 let inter = d.gate_proj.rows();
2495 FFN_SCRATCH.with(|s| {
2496 let mut s = s.borrow_mut();
2497 let [g, u, ..] = &mut *s;
2498 g.resize(inter, 0.0);
2499 u.resize(inter, 0.0);
2500 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
2502 for i in 0..inter {
2503 g[i] = inference::silu(g[i]) * u[i];
2504 }
2505 let mut out = attention::take_buf(d.down_proj.rows());
2506 d.down_proj.matvec(g, &mut out, pool);
2507 out
2508 })
2509}
2510
2511fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
2517 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
2520 return None;
2521 }
2522 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
2523 let mut model_ref = None;
2524 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
2525 let model = model_ref?;
2526 let hidden = jobs[0].down.1;
2527 let mut out = attention::take_buf(hidden);
2528 if crate::gpu::moe_block(&model, &jobs, &mut out) {
2529 Some(out)
2530 } else {
2531 let mut out = out;
2532 attention::recycle_buf(&mut out);
2533 None
2534 }
2535}
2536
2537#[allow(clippy::type_complexity)]
2542#[allow(clippy::type_complexity)]
2543fn moe_parts(
2544 t: &QTensor,
2545) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32], bool)> {
2546 match t {
2547 QTensor::Mapped {
2548 model,
2549 idx,
2550 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
2551 rows,
2552 cols,
2553 row_scale,
2554 col_field,
2555 ..
2556 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
2557 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
2558 }
2559 QTensor::Mapped {
2561 model,
2562 idx,
2563 dtype: cortiq_core::TensorDtype::Q1,
2564 rows,
2565 cols,
2566 ..
2567 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
2568 _ => None,
2569 }
2570}
2571
2572fn moe_push_job<'a>(
2574 d: &'a DenseFfn,
2575 x: &[f32],
2576 w: f32,
2577 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
2578 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
2579) -> Option<()> {
2580 use crate::qtensor::prescale;
2581 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
2582 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
2583 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
2584 if gq1 != uq1 || uq1 != dq1 {
2585 return None; }
2587 model_ref.get_or_insert_with(|| gm.clone());
2588 let gdt = if gcf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
2589 let udt = if ucf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
2590 jobs.push(crate::gpu::MoeJob {
2591 gate: (gi, gr, gc, grs),
2592 up: (ui, ur, uc, urs),
2593 down: (di, dr, dc, drs),
2594 xs_gate: prescale(x, gcf, gdt).into_owned(),
2595 xs_up: prescale(x, ucf, udt).into_owned(),
2596 down_col: dcf,
2597 w,
2598 q1: gq1,
2599 });
2600 Some(())
2601}
2602
2603fn sparse_ffn_quant(
2610 d: &DenseFfn,
2611 x: &[f32],
2612 active: &[u16],
2613 hidden: usize,
2614 pool: Option<&Pool>,
2615) -> Vec<f32> {
2616 let n = active.len();
2617 let inter = d.gate_proj.rows();
2618 let mut act = vec![0.0f32; n];
2619 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
2622 let compute = |ai: usize| -> f32 {
2623 let idx = active[ai] as usize;
2624 if idx >= inter {
2625 return 0.0; }
2627 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
2628 let gate = d.gate_proj.row_dot(idx, x, &mut s);
2629 let up = d.up_proj.row_dot(idx, x, &mut s);
2630 inference::silu(gate) * up
2631 };
2632 match pool {
2633 Some(p) if n >= 256 => {
2634 let ptr = SendMut(act.as_mut_ptr());
2635 p.run(&|widx, nw| {
2636 let chunk = n.div_ceil(nw);
2637 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
2638 for ai in s..e {
2639 unsafe { *ptr.at(ai) = compute(ai) };
2640 }
2641 });
2642 }
2643 _ => {
2644 for (ai, a) in act.iter_mut().enumerate() {
2645 *a = compute(ai);
2646 }
2647 }
2648 }
2649 let mut out = vec![0.0f32; hidden];
2651 for (ai, &idx) in active.iter().enumerate() {
2652 let w = act[ai];
2653 if w.abs() >= 1e-12 && (idx as usize) < inter {
2654 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
2655 }
2656 }
2657 out
2658}
2659
2660#[doc(hidden)]
2662pub fn sparse_ffn_quant_for_test(
2663 d: &DenseFfn,
2664 x: &[f32],
2665 active: &[u16],
2666 hidden: usize,
2667) -> Vec<f32> {
2668 sparse_ffn_quant(d, x, active, hidden, None)
2669}
2670
2671fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
2675 let deq = |t: &QTensor| -> Vec<f32> {
2676 let (rows, cols) = (t.rows(), t.cols());
2677 let mut out = vec![0.0f32; rows * cols];
2678 for r in 0..rows {
2679 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
2680 }
2681 out
2682 };
2683 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
2684}
2685
2686struct SendMut(*mut f32);
2688unsafe impl Send for SendMut {}
2689unsafe impl Sync for SendMut {}
2690impl SendMut {
2691 #[inline]
2692 #[allow(clippy::mut_from_ref)]
2695 unsafe fn at(&self, i: usize) -> &mut f32 {
2696 unsafe { &mut *self.0.add(i) }
2697 }
2698}
2699
2700fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2704 let ne = m.experts.len();
2705 let mut logits = vec![0.0f32; ne];
2706 m.router.matvec(x, &mut logits, pool);
2707 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2708 let mut p: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
2709 let s: f32 = p.iter().sum();
2710 for v in &mut p {
2711 *v /= s;
2712 }
2713 let mut idx: Vec<usize> = (0..ne).collect();
2714 idx.sort_unstable_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap().then(a.cmp(&b)));
2716 idx.truncate(m.top_k);
2717 let wsum: f32 = if m.norm_topk_prob {
2718 idx.iter().map(|&e| p[e]).sum()
2719 } else {
2720 1.0
2721 };
2722 {
2723 let mut st = m.stats.borrow_mut();
2724 if st.len() < ne {
2725 st.resize(ne, 0);
2726 }
2727 for &e in &idx {
2728 st[e] += 1;
2729 }
2730 }
2731 if crate::gpu::enabled_here() {
2736 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
2737 crate::gpu::ProbeArm::Gpu => {
2738 let t0 = std::time::Instant::now();
2739 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
2740 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
2741 return out;
2742 }
2743 }
2744 crate::gpu::ProbeArm::CpuTimed => {
2745 let t0 = std::time::Instant::now();
2746 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
2747 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
2748 return out;
2749 }
2750 crate::gpu::ProbeArm::Cpu => {
2751 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
2752 }
2753 }
2754 }
2755 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
2756}
2757
2758fn moe_ffn_cpu(
2760 m: &MoeFfn,
2761 x: &[f32],
2762 idx: &[usize],
2763 p: &[f32],
2764 wsum: f32,
2765 pool: Option<&Pool>,
2766) -> Vec<f32> {
2767 let mut out = attention::take_buf(x.len());
2768 for &e in idx {
2769 let mut eo = dense_ffn(&m.experts[e], x, pool);
2770 let w = p[e] / wsum;
2771 for i in 0..out.len() {
2772 out[i] += w * eo[i];
2773 }
2774 attention::recycle_buf(&mut eo);
2775 }
2776 if let Some((se, gate)) = &m.shared {
2777 let mut so = dense_ffn(se, x, pool);
2778 let mut gl = vec![0.0f32; 1];
2779 gate.matvec(x, &mut gl, pool);
2780 let g = 1.0 / (1.0 + (-gl[0]).exp());
2781 for i in 0..out.len() {
2782 out[i] += g * so[i];
2783 }
2784 attention::recycle_buf(&mut so);
2785 }
2786 out
2787}
2788
2789fn moe_ffn_gpu(
2792 m: &MoeFfn,
2793 x: &[f32],
2794 idx: &[usize],
2795 p: &[f32],
2796 wsum: f32,
2797 pool: Option<&Pool>,
2798) -> Option<Vec<f32>> {
2799 use crate::gpu::MoeJob;
2800
2801 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
2802 let mut model_ref = None;
2803 for &e in idx {
2804 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
2805 }
2806 if let Some((se, gate)) = &m.shared {
2807 let mut gl = vec![0.0f32; 1];
2808 gate.matvec(x, &mut gl, pool);
2809 let g = 1.0 / (1.0 + (-gl[0]).exp());
2810 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
2811 }
2812 let model = model_ref?;
2813 let hidden = jobs[0].down.1;
2814 let mut out = vec![0.0f32; hidden];
2815 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
2816}
2817
2818fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2820 match ffn {
2821 FfnKind::Dense(d) => dense_ffn(d, x, pool),
2822 FfnKind::Moe(m) => moe_ffn(m, x, pool),
2823 }
2824}
2825
2826fn ffn_forward_pair(
2830 ffn: &FfnKind,
2831 x1: &[f32],
2832 x2: &[f32],
2833 pool: Option<&Pool>,
2834) -> (Vec<f32>, Vec<f32>) {
2835 let d = match ffn {
2836 FfnKind::Dense(d) => d,
2837 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
2838 };
2839 let inter = d.gate_proj.rows();
2840 FFN_SCRATCH.with(|s| {
2841 let mut s = s.borrow_mut();
2842 let [g1, g2, u1, u2] = &mut *s;
2843 g1.resize(inter, 0.0);
2844 g2.resize(inter, 0.0);
2845 u1.resize(inter, 0.0);
2846 u2.resize(inter, 0.0);
2847 QTensor::matvec2_many(
2850 [&d.gate_proj, &d.up_proj],
2851 x1,
2852 x2,
2853 [g1.as_mut_slice(), u1.as_mut_slice()],
2854 [g2.as_mut_slice(), u2.as_mut_slice()],
2855 pool,
2856 );
2857 for i in 0..inter {
2858 g1[i] = inference::silu(g1[i]) * u1[i];
2859 g2[i] = inference::silu(g2[i]) * u2[i];
2860 }
2861 let mut o1 = attention::take_buf(d.down_proj.rows());
2862 let mut o2 = attention::take_buf(d.down_proj.rows());
2863 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
2864 (o1, o2)
2865 })
2866}
2867
2868#[cfg(test)]
2869mod tests {
2870 use super::*;
2871
2872 #[test]
2878 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
2879 let (hidden, inter) = (16usize, 40usize);
2880 let synth = |n: usize, salt: usize| -> Vec<f32> {
2881 (0..n)
2882 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
2883 .collect()
2884 };
2885 let d = DenseFfn {
2886 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
2887 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
2888 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
2889 };
2890 let x = synth(hidden, 9);
2891 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
2893
2894 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
2895
2896 let mut g = vec![0.0f32; inter];
2898 d.gate_proj.matvec(&x, &mut g, None);
2899 let mut u = vec![0.0f32; inter];
2900 d.up_proj.matvec(&x, &mut u, None);
2901 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
2902 for i in 0..inter {
2903 g[i] = if act_set.contains(&(i as u16)) {
2904 inference::silu(g[i]) * u[i]
2905 } else {
2906 0.0
2907 };
2908 }
2909 let mut reference = vec![0.0f32; hidden];
2910 d.down_proj.matvec(&g, &mut reference, None);
2911
2912 let max_d = sparse
2913 .iter()
2914 .zip(&reference)
2915 .map(|(a, b)| (a - b).abs())
2916 .fold(0.0f32, f32::max);
2917 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
2918 }
2919
2920 fn attach_test_mtp(p: &mut Pipeline) {
2922 let (h, inter, heads, kv, hd) = (
2923 p.hidden_size,
2924 p.intermediate_size,
2925 p.num_heads,
2926 p.num_kv_heads,
2927 p.head_dim,
2928 );
2929 let synth = |n: usize, salt: usize| -> Vec<f32> {
2930 (0..n)
2931 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
2932 .collect()
2933 };
2934 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2935 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2936 };
2937 p.mtp = Some(MtpModule {
2938 enorm: vec![1.0; h],
2939 hnorm: vec![1.0; h],
2940 eh_proj: qt(h, 2 * h, 301),
2941 layer: LayerWeights {
2942 input_norm: vec![1.0; h],
2943 post_norm: vec![1.0; h],
2944 ffn: FfnKind::Dense(DenseFfn {
2945 gate_proj: qt(inter, h, 315),
2946 up_proj: qt(inter, h, 316),
2947 down_proj: qt(h, inter, 317),
2948 }),
2949 attn: AttnKind::Full {
2950 bias: None,
2951 wq: qt(heads * hd, h, 311),
2952 wk: qt(kv * hd, h, 312),
2953 wv: qt(kv * hd, h, 313),
2954 wo: qt(h, heads * hd, 314),
2955 q_norm: None,
2956 k_norm: None,
2957 output_gate: false,
2958 },
2959 },
2960 final_norm: vec![1.0; h],
2961 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
2962 });
2963 }
2964
2965 #[test]
2966 fn speculative_equals_vanilla_greedy() {
2967 let run = |spec: bool| {
2968 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2969 p.sampler_config.temperature = 0.0;
2970 attach_test_mtp(&mut p);
2971 p.speculative = spec;
2972 let r = p.generate("abcdef", 12, None, None).unwrap();
2973 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
2974 };
2975 let (vanilla, d0, _) = run(false);
2976 let (spec, d1, a1) = run(true);
2977 assert_eq!(d0, 0, "vanilla path must not draft");
2978 assert!(d1 > 0, "speculative path must draft");
2979 assert_eq!(
2980 vanilla, spec,
2981 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
2982 );
2983 }
2984
2985 #[test]
2986 fn speculative_accepts_constant_oracle() {
2987 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2988 p.sampler_config.temperature = 0.0;
2989 p.sampler_config.repetition_penalty = 1.0;
2990 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
2993 attach_test_mtp(&mut p);
2994 p.speculative = true;
2995 let r = p.generate("abcd", 10, None, None).unwrap();
2996 assert!(r.mtp_drafted > 0);
2997 assert_eq!(
2998 r.mtp_accepted, r.mtp_drafted,
2999 "constant logits → every draft accepted"
3000 );
3001 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
3004 }
3005
3006 #[test]
3007 fn empty_prompt_is_an_error_not_a_panic() {
3008 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3009 let r = p.generate("", 4, None, None);
3010 assert!(r.is_err(), "empty prompt must be a clean error");
3011 }
3012
3013 #[test]
3014 fn every_token_enters_kv_exactly_once() {
3015 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3016 p.sampler_config.temperature = 0.0;
3018 let r = p.generate("abc", 2, None, None).unwrap();
3019 assert_eq!(r.prompt_tokens, 3);
3020 assert_eq!(
3024 p.kv_cache.seq_len(),
3025 3 + r.tokens_generated - 1,
3026 "each token must be cached exactly once (v1 cached the last prompt token twice)"
3027 );
3028 }
3029
3030 #[test]
3031 fn generation_is_reproducible_with_seed() {
3032 let run = || {
3033 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
3034 p.generate("hello", 8, None, None).unwrap().token_ids
3035 };
3036 assert_eq!(run(), run());
3037 }
3038
3039 #[test]
3040 fn eviction_bounds_the_cache() {
3041 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
3042 p.kv_cache.max_seq_len = 6;
3043 p.sampler_config.temperature = 0.0;
3044 let _ = p.generate("abcd", 12, None, None).unwrap();
3045 assert!(
3046 p.kv_cache.seq_len() <= 6 + 1,
3047 "cache must stay bounded by max_seq_len (got {})",
3048 p.kv_cache.seq_len()
3049 );
3050 }
3051
3052 #[test]
3053 fn confidence_matches_tokens_and_is_a_probability() {
3054 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3055 p.sampler_config.temperature = 0.0;
3056 p.sampler_config.repetition_penalty = 1.0;
3057 let r = p.generate("abcd", 10, None, None).unwrap();
3058 assert_eq!(
3059 r.token_confidence.len(),
3060 r.token_ids.len(),
3061 "one confidence per emitted token"
3062 );
3063 for &c in &r.token_confidence {
3064 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
3065 }
3066 let logits = [1.0f32, 3.0, 0.5, 3.0];
3068 let p0 = top1_prob_t(&logits, 1, 1.0);
3069 let p1 = top1_prob_t(&logits, 3, 1.0);
3070 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
3071 assert!(p0 > 0.0 && p0 < 1.0);
3072 let sharp = top1_prob_t(&logits, 1, 1.0);
3074 let soft = top1_prob_t(&logits, 1, 2.0);
3075 assert!(soft < sharp, "higher temperature lowers peak confidence");
3076 }
3077
3078 #[test]
3079 fn trace_is_opt_in_and_parallels_the_output() {
3080 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3082 p.sampler_config.temperature = 0.0;
3083 p.sampler_config.repetition_penalty = 1.0;
3084 let r = p.generate("abcd", 10, None, None).unwrap();
3085 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
3086
3087 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3089 p.sampler_config.temperature = 0.0;
3090 p.sampler_config.repetition_penalty = 1.0;
3091 p.set_trace(true);
3092 let r = p.generate("abcd", 10, None, None).unwrap();
3093 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
3094 for (i, tr) in r.traces.iter().enumerate() {
3095 assert_eq!(tr.t, i, "trace index is sequential");
3096 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
3097 assert_eq!(
3098 tr.confidence, r.token_confidence[i],
3099 "trace confidence matches the confidence channel"
3100 );
3101 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
3103 }
3104 }
3105
3106 #[test]
3107 fn explain_prefill_logits_match_greedy_first_token() {
3108 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
3112 p.sampler_config.temperature = 0.0;
3113 p.sampler_config.repetition_penalty = 1.0;
3114 let ids = p.tokenizer.encode("abcd");
3115 let logits = p.prefill_next_logits(&ids, None);
3116 let argmax = logits
3117 .iter()
3118 .enumerate()
3119 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3120 .unwrap()
3121 .0 as u32;
3122 let r = p.generate("abcd", 1, None, None).unwrap();
3123 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
3124 }
3125}