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
23pub struct Pipeline {
25 pub tokenizer: Tokenizer,
26 pub kv_cache: KvCache,
27 pub sampler_config: SamplerConfig,
28 pub weights: PipelineWeights,
29 pub hidden_size: usize,
30 pub intermediate_size: usize,
31 pub num_heads: usize,
32 pub num_kv_heads: usize,
33 pub head_dim: usize,
34 pub num_layers: usize,
35 pub vocab_size: usize,
36 pub rms_eps: f64,
37 pub rope_base: f32,
38 pub norm_style: NormStyle,
39 pub rotary_dim: usize,
41 pub vmf_cfg: Option<VmfPhaseCfg>,
43 pub gdn_cfg: Option<GdnCfg>,
45 pub mtp: Option<MtpModule>,
47 pub speculative: bool,
49 rng: SplitMix64,
50 inv_freq: Vec<f32>,
52 pool: Option<std::sync::Arc<Pool>>,
54 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
58 pub(crate) dyn_force_f32: bool,
60 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
65 pub(crate) dyn_active: Option<usize>,
71 pub(crate) dyn_blend_loaded: bool,
75 pub(crate) dyn_phi_layer: Option<usize>,
78 dyn_phi_ema: Vec<f32>,
80 dyn_phi_seen: usize,
81 pub dyn_router: Option<crate::swarm::DynRouter>,
84 o1_cfg: Option<crate::nystrom::O1Cfg>,
87 o1_flags: Vec<bool>,
89 trace: bool,
92 calib_temp: f32,
95}
96
97pub struct PipelineWeights {
102 pub embed_tokens: QTensor,
104 pub layers: Vec<LayerWeights>,
106 pub lm_head: QTensor,
108 pub final_norm: Vec<f32>,
110}
111
112pub struct LayerWeights {
114 pub input_norm: Vec<f32>,
115 pub post_norm: Vec<f32>,
116 pub ffn: FfnKind,
117 pub attn: AttnKind,
118}
119
120pub struct DenseFfn {
122 pub gate_proj: QTensor,
123 pub up_proj: QTensor,
124 pub down_proj: QTensor,
125}
126
127pub enum FfnKind {
130 Dense(DenseFfn),
131 Moe(MoeFfn),
135}
136
137pub struct MoeFfn {
138 pub router: QTensor,
140 pub experts: Vec<DenseFfn>,
141 pub top_k: usize,
142 pub norm_topk_prob: bool,
143 pub shared: Option<(DenseFfn, QTensor)>,
145 pub stats: std::cell::RefCell<Vec<u64>>,
149}
150
151pub enum AttnKind {
154 Full {
156 wq: QTensor,
157 wk: QTensor,
158 wv: QTensor,
159 wo: QTensor,
160 q_norm: Option<Vec<f32>>,
161 k_norm: Option<Vec<f32>>,
162 output_gate: bool,
163 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
165 },
166 Linear(VmfPhaseWeights),
168 LinearGdn(GdnWeights),
170}
171
172pub struct MtpModule {
177 pub enorm: Vec<f32>,
178 pub hnorm: Vec<f32>,
179 pub eh_proj: QTensor,
181 pub layer: LayerWeights,
182 pub final_norm: Vec<f32>,
183 pub kv: crate::kv_cache::LayerKvCache,
184}
185
186pub struct GenerateResult {
188 pub text: String,
189 pub token_ids: Vec<u32>,
190 pub prompt_tokens: usize,
191 pub tokens_generated: usize,
192 pub finish_reason: String,
193 pub mtp_drafted: usize,
195 pub mtp_accepted: usize,
196 pub token_confidence: Vec<f32>,
201 pub traces: Vec<TokenTrace>,
204}
205
206#[derive(Clone, Debug)]
211pub struct TokenTrace {
212 pub t: usize,
214 pub token_id: u32,
216 pub confidence: f32,
218 pub active_skill: Option<String>,
220 pub recon: Option<f32>,
224 pub switched: bool,
227}
228
229fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
234 let t = if temp > 1e-3 { temp } else { 1.0 };
235 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
236 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
237 if sum > 0.0 {
238 (((logits[id as usize] - max) / t).exp()) / sum
239 } else {
240 0.0
241 }
242}
243
244fn prefill_batched() -> bool {
247 std::env::var("CMF_PREFILL").map(|v| v != "seq").unwrap_or(true)
248}
249
250pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
252
253impl Pipeline {
254 #[allow(clippy::too_many_arguments)]
256 pub fn new(
257 tokenizer: Tokenizer,
258 weights: PipelineWeights,
259 hidden_size: usize,
260 intermediate_size: usize,
261 num_heads: usize,
262 num_kv_heads: usize,
263 head_dim: usize,
264 num_layers: usize,
265 vocab_size: usize,
266 rms_eps: f64,
267 rope_base: f32,
268 norm_style: NormStyle,
269 max_seq_len: usize,
270 sampler_config: SamplerConfig,
271 ) -> Self {
272 let rng = match sampler_config.seed {
273 Some(s) => SplitMix64::new(s),
274 None => SplitMix64::from_entropy(),
275 };
276 let inv_freq = attention::rope_inv_freq(head_dim, rope_base);
277 let pool = Pool::from_env();
278 if let Some(p) = &pool {
279 tracing::info!("worker pool: {} threads", p.n_workers());
280 }
281 Self {
282 tokenizer,
283 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
284 sampler_config,
285 weights,
286 hidden_size,
287 intermediate_size,
288 num_heads,
289 num_kv_heads,
290 head_dim,
291 num_layers,
292 vocab_size,
293 rms_eps,
294 rope_base,
295 norm_style,
296 rotary_dim: head_dim,
297 vmf_cfg: None,
298 gdn_cfg: None,
299 mtp: None,
300 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
301 rng,
302 inv_freq,
303 pool,
304 model: None,
305 dyn_force_f32: false,
306 dyn_skill_layers: Vec::new(),
307 dyn_active: None,
308 dyn_blend_loaded: false,
309 dyn_phi_layer: None,
310 dyn_phi_ema: Vec::new(),
311 dyn_phi_seen: 0,
312 dyn_router: None,
313 o1_cfg: None,
314 o1_flags: Vec::new(),
315 trace: false,
316 calib_temp: 1.0,
317 }
318 }
319
320 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
327 self.o1_flags = match &cfg {
328 Some(c) => {
329 let mut flags = c.layer_flags(self.num_layers);
330 for (li, f) in flags.iter_mut().enumerate() {
331 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
332 *f = false;
333 }
334 }
335 flags
336 }
337 None => Vec::new(),
338 };
339 if let Some(c) = &cfg {
340 let n = self.o1_flags.iter().filter(|&&f| f).count();
341 tracing::info!(
342 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
343 self.num_layers, c.m, c.w, c.sink, c.rect
344 );
345 }
346 self.o1_cfg = cfg;
347 }
348
349 pub fn o1_active(&self) -> bool {
351 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
352 }
353
354 fn o1_begin(&mut self) {
356 if let Some(c) = &self.o1_cfg {
357 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
358 for (li, &f) in self.o1_flags.iter().enumerate() {
359 if f {
360 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
361 }
362 }
363 }
364 }
365
366 fn o1_seal(&mut self) {
369 if self.o1_cfg.is_none() {
370 return;
371 }
372 for li in 0..self.num_layers {
373 if self.o1_flags.get(li).copied().unwrap_or(false) {
374 self.kv_cache.layers[li].o1_seal(self.num_heads);
375 }
376 }
377 }
378
379 pub fn set_trace(&mut self, on: bool) {
381 self.trace = on;
382 }
383
384 pub fn set_calib_temp(&mut self, t: f32) {
387 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
388 }
389
390 pub fn calib_temp(&self) -> f32 {
392 self.calib_temp
393 }
394
395 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
398 self.rotary_dim = rotary_dim.min(self.head_dim);
399 self.inv_freq = attention::rope_inv_freq(self.rotary_dim, base);
400 }
401
402 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
403 QwenAttnCfg {
404 num_heads: self.num_heads,
405 num_kv_heads: self.num_kv_heads,
406 head_dim: self.head_dim,
407 hidden_size: self.hidden_size,
408 position,
409 inv_freq: &self.inv_freq,
410 rotary_dim: self.rotary_dim,
411 q_norm: None,
412 k_norm: None,
413 output_gate: false,
414 bias: None,
415 rms_eps: self.rms_eps,
416 norm_style: self.norm_style,
417 pool: self.pool.as_deref(),
418 }
419 }
420
421 pub fn generate(
423 &mut self,
424 prompt: &str,
425 max_tokens: usize,
426 task_mask: Option<&TaskMask>,
427 on_token: Option<TokenCallback>,
428 ) -> Result<GenerateResult, String> {
429 let input_ids = self.tokenizer.encode(prompt);
430 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
431 }
432
433 pub fn generate_from_ids(
441 &mut self,
442 input_ids: &[u32],
443 max_tokens: usize,
444 task_mask: Option<&TaskMask>,
445 mut on_token: Option<TokenCallback>,
446 ) -> Result<GenerateResult, String> {
447 if input_ids.is_empty() {
448 return Err("empty prompt: nothing to generate from".to_string());
449 }
450
451 self.kv_cache.clear();
453 self.o1_begin();
454
455 let spec_active = self.speculative
459 && self.mtp.is_some()
460 && task_mask.is_none()
461 && !self.o1_active()
462 && self.sampler_config.temperature < 1e-6;
463 let mut mtp = if spec_active { self.mtp.take() } else { None };
466 if let Some(m) = &mut mtp {
467 m.kv.clear();
468 }
469 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
473 if let Some(r) = &mut router {
474 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
477 }
478
479 let mut all_ids = input_ids.to_vec();
480 let mut generated = 0usize;
481 let mut finish_reason = "max_tokens".to_string();
482 let mut drafted = 0usize;
483 let mut accepted = 0usize;
484 let mut confidence: Vec<f32> = Vec::new();
485 let trace_on = self.trace;
486 let calib_temp = self.calib_temp;
487 let mut traces: Vec<TokenTrace> = Vec::new();
488
489 let mut hidden = vec![0.0f32; self.hidden_size];
495 let mut pos = 0usize;
496 let dyn_prefill = router.is_some();
501 if task_mask.is_none() && !dyn_prefill {
502 while pos + 1 < input_ids.len() {
503 let e1 = self.embed_single(input_ids[pos]);
504 let e2 = self.embed_single(input_ids[pos + 1]);
505 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
506 self.commit_linear_scratch();
508 if let Some(m) = &mut mtp {
509 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
510 if pos + 2 < input_ids.len() {
511 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
512 }
513 }
514 hidden = h2;
515 pos += 2;
516 }
517 }
518 while pos < input_ids.len() {
519 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
520 if let Some(m) = &mut mtp {
521 if pos + 1 < input_ids.len() {
522 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
523 }
524 }
525 pos += 1;
526 }
527 self.o1_seal();
530
531 macro_rules! commit {
533 ($id:expr) => {{
534 all_ids.push($id);
535 generated += 1;
536 if self.tokenizer.is_eos($id) {
537 finish_reason = "stop".to_string();
538 false
539 } else {
540 let token_text = self.tokenizer.decode_token($id);
541 let mut go = true;
542 if let Some(ref mut cb) = on_token {
543 if !cb(&token_text) {
544 finish_reason = "cancelled".to_string();
545 go = false;
546 }
547 }
548 go
549 }
550 }};
551 }
552
553 let mut next_pos = input_ids.len();
555 'decode: while generated < max_tokens {
556 let normed = inference::rms_norm(
557 &hidden,
558 &self.weights.final_norm,
559 self.rms_eps,
560 self.norm_style,
561 );
562 let logits = self.lm_head_forward(&normed);
563 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
564 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
565 if trace_on {
566 let skill = router.as_ref().and_then(|r| r.active_id());
570 traces.push(TokenTrace {
571 t: generated,
572 token_id: t_next,
573 confidence: *confidence.last().unwrap(),
574 active_skill: skill,
575 recon: None,
576 switched: false,
577 });
578 }
579 if !commit!(t_next) {
580 break 'decode;
581 }
582 if generated >= max_tokens {
583 break 'decode;
584 }
585
586 if self.kv_cache.needs_eviction() {
587 let keep = (self.kv_cache.max_seq_len / 2).max(1);
588 self.kv_cache.evict(keep);
589 }
590
591 match &mut mtp {
592 Some(m) if generated + 1 < max_tokens => {
594 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
595 drafted += 1;
596 let emb1 = self.embed_single(t_next);
597 let emb2 = self.embed_single(draft);
598 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
599
600 let n1 = inference::rms_norm(
601 &h1,
602 &self.weights.final_norm,
603 self.rms_eps,
604 self.norm_style,
605 );
606 let logits1 = self.lm_head_forward(&n1);
607 let t_after =
608 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
609 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
610 if trace_on {
611 traces.push(TokenTrace {
614 t: generated,
615 token_id: t_after,
616 confidence: *confidence.last().unwrap(),
617 active_skill: None,
618 recon: None,
619 switched: false,
620 });
621 }
622 let stop = !commit!(t_after);
623
624 if t_after == draft {
625 accepted += 1;
626 self.commit_linear_scratch();
627 let _ = self.mtp_step(m, &h1, t_after, next_pos);
628 hidden = h2;
629 next_pos += 2;
630 } else {
631 for layer in &mut self.kv_cache.layers {
633 layer.truncate_last(1);
634 }
635 if !stop {
636 let _ = self.mtp_step(m, &h1, t_after, next_pos);
637 hidden = self
638 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
639 }
640 next_pos += 2;
641 }
642 if stop {
643 break 'decode;
644 }
645 }
646 _ => {
648 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
649 next_pos += 1;
650 if let Some(r) = &mut router {
653 let phi = self.dyn_phi_ema.clone();
654 let decision = r.step(&phi, generated);
655 if let Some(new_active) = decision {
656 let _ = self.set_active_skill(new_active);
657 }
658 if trace_on {
661 if let Some(last) = traces.last_mut() {
662 let e = r.last_best_e();
663 last.recon = e.is_finite().then_some(e);
664 last.switched = decision.is_some();
665 }
666 }
667 }
668 }
669 }
670 }
671
672 if router.is_some() {
674 let _ = self.set_active_skill(None);
675 }
676 self.dyn_router = router.or(self.dyn_router.take());
677 self.mtp = mtp.or(self.mtp.take());
678
679 let output_ids = &all_ids[input_ids.len()..];
680 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
682 Ok(GenerateResult {
683 text: self.tokenizer.decode(output_ids),
684 token_ids: output_ids.to_vec(),
685 prompt_tokens: input_ids.len(),
686 tokens_generated: generated,
687 finish_reason,
688 mtp_drafted: drafted,
689 mtp_accepted: accepted,
690 token_confidence: confidence,
691 traces,
692 })
693 }
694
695 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
699 let h_n = inference::rms_norm(hidden, &m.hnorm, self.rms_eps, self.norm_style);
700 let e = self.embed_single(next_token);
701 let e_n = inference::rms_norm(&e, &m.enorm, self.rms_eps, self.norm_style);
702 let mut cat = Vec::with_capacity(2 * self.hidden_size);
706 cat.extend_from_slice(&e_n);
707 cat.extend_from_slice(&h_n);
708 let mut x = vec![0.0f32; self.hidden_size];
709 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
710
711 let lw = &m.layer;
713 let normed = inference::rms_norm(&x, &lw.input_norm, self.rms_eps, self.norm_style);
714 let attn = match &lw.attn {
715 AttnKind::Full {
716 wq,
717 wk,
718 wv,
719 wo,
720 q_norm,
721 k_norm,
722 output_gate,
723 bias,
724 } => {
725 let mut cfg = self.attn_cfg(position);
726 cfg.q_norm = q_norm.as_deref();
727 cfg.k_norm = k_norm.as_deref();
728 cfg.output_gate = *output_gate;
729 cfg.bias = bias
730 .as_ref()
731 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
732 attention::qwen_attention(&normed, wq, wk, wv, wo, &mut m.kv, &cfg)
733 }
734 AttnKind::Linear(_) | AttnKind::LinearGdn(_) => {
735 unreachable!("MTP block is full attention")
736 }
737 };
738 for (i, &a) in attn.iter().enumerate() {
739 x[i] += a;
740 }
741 let post = inference::rms_norm(&x, &lw.post_norm, self.rms_eps, self.norm_style);
742 let ffn = ffn_forward(&lw.ffn, &post, self.pool.as_deref());
743 for (i, &f) in ffn.iter().enumerate() {
744 x[i] += f;
745 }
746
747 let out = inference::rms_norm(&x, &m.final_norm, self.rms_eps, self.norm_style);
748 sampler::argmax(&self.lm_head_forward(&out))
749 }
750
751 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
755 let emb1 = self.embed_single(1);
756 let emb2 = self.embed_single(2);
757 let pos = self.kv_cache.seq_len();
758
759 let t0 = std::time::Instant::now();
760 for _ in 0..iters {
761 let _ = self.forward_layers(&emb1, pos, None);
762 let _ = self.forward_layers(&emb2, pos + 1, None);
763 for l in &mut self.kv_cache.layers {
764 l.truncate_last(2);
765 }
766 }
767 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
768
769 let t1 = std::time::Instant::now();
770 for _ in 0..iters {
771 let _ = self.forward_pair(&emb1, &emb2, pos);
772 for l in &mut self.kv_cache.layers {
773 l.truncate_last(2);
774 }
775 }
776 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
777 (singles_ms, pair_ms)
778 }
779
780 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
785 let mut h1 = emb1.to_vec();
786 let mut h2 = emb2.to_vec();
787 let (nh, nkv, hd, hs, rd, eps) = (
788 self.num_heads,
789 self.num_kv_heads,
790 self.head_dim,
791 self.hidden_size,
792 self.rotary_dim,
793 self.rms_eps,
794 );
795 let inv_freq = self.inv_freq.clone();
796 let pool = self.pool.clone();
797
798 for li in 0..self.num_layers {
799 let lw = &self.weights.layers[li];
800 let n1 = inference::rms_norm(&h1, &lw.input_norm, self.rms_eps, self.norm_style);
801 let n2 = inference::rms_norm(&h2, &lw.input_norm, self.rms_eps, self.norm_style);
802
803 let (a1, a2) = match &lw.attn {
804 AttnKind::Linear(w) => {
805 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
806 let layer = &mut self.kv_cache.layers[li];
807 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
808 vmf_phase_pair(&n1, &n2, w, &cfg, state, scratch, self.pool.as_deref())
809 }
810 AttnKind::LinearGdn(w) => {
811 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
812 let layer = &mut self.kv_cache.layers[li];
813 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
814 gdn_pair(&n1, &n2, w, &cfg, state, scratch, self.pool.as_deref())
815 }
816 AttnKind::Full {
817 wq,
818 wk,
819 wv,
820 wo,
821 q_norm,
822 k_norm,
823 output_gate,
824 bias,
825 } => {
826 let cfg = QwenAttnCfg {
827 num_heads: nh,
828 num_kv_heads: nkv,
829 head_dim: hd,
830 hidden_size: hs,
831 position,
832 inv_freq: &inv_freq,
833 rotary_dim: rd,
834 q_norm: q_norm.as_deref(),
835 k_norm: k_norm.as_deref(),
836 output_gate: *output_gate,
837 bias: bias
838 .as_ref()
839 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
840 rms_eps: eps,
841 norm_style: self.norm_style,
842 pool: pool.as_deref(),
843 };
844 attention::qwen_attention_pair(
845 &n1,
846 &n2,
847 wq,
848 wk,
849 wv,
850 wo,
851 &mut self.kv_cache.layers[li],
852 &cfg,
853 )
854 }
855 };
856 for i in 0..self.hidden_size {
857 h1[i] += a1[i];
858 h2[i] += a2[i];
859 }
860
861 let lw = &self.weights.layers[li];
862 let p1 = inference::rms_norm(&h1, &lw.post_norm, self.rms_eps, self.norm_style);
863 let p2 = inference::rms_norm(&h2, &lw.post_norm, self.rms_eps, self.norm_style);
864 let (f1, f2) = ffn_forward_pair(&lw.ffn, &p1, &p2, self.pool.as_deref());
865 for i in 0..self.hidden_size {
866 h1[i] += f1[i];
867 h2[i] += f2[i];
868 }
869 }
870 (h1, h2)
871 }
872
873 fn commit_linear_scratch(&mut self) {
875 for layer in &mut self.kv_cache.layers {
876 if !layer.linear_scratch.is_empty() {
877 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
878 layer.linear_scratch.clear();
879 }
880 }
881 }
882
883 pub fn forward_ids(
886 &mut self,
887 ids: &[u32],
888 task_mask: Option<&TaskMask>,
889 ) -> Result<Vec<f32>, String> {
890 if ids.is_empty() {
891 return Err("empty id sequence".to_string());
892 }
893 self.kv_cache.clear();
894 self.o1_begin();
895 let mut hidden = vec![0.0f32; self.hidden_size];
896 let mut pos = 0usize;
897 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
898 const CHUNK: usize = 48;
902 let hs = self.hidden_size;
903 while pos < ids.len() {
904 let end = (pos + CHUNK).min(ids.len());
905 let hb = self.prefill_batch(&ids[pos..end], pos);
906 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
907 pos = end;
908 }
909 }
910 if task_mask.is_none() {
911 while pos + 1 < ids.len() {
912 let e1 = self.embed_single(ids[pos]);
913 let e2 = self.embed_single(ids[pos + 1]);
914 let (_, h2) = self.forward_pair(&e1, &e2, pos);
915 self.commit_linear_scratch();
916 hidden = h2;
917 pos += 2;
918 }
919 }
920 while pos < ids.len() {
921 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
922 pos += 1;
923 }
924 self.o1_seal();
928 let normed = inference::rms_norm(
929 &hidden,
930 &self.weights.final_norm,
931 self.rms_eps,
932 self.norm_style,
933 );
934 Ok(self.lm_head_forward(&normed))
935 }
936
937 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
944 let (nll, cnt) = self.nll_ids_from(ids, 0);
945 (nll / cnt.max(1) as f64).exp()
946 }
947
948 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
957 self.kv_cache.clear();
958 let mut nll = 0f64;
959 let mut cnt = 0usize;
960 if prefill_batched() {
961 const CHUNK: usize = 128;
967 const LM_SUB: usize = 32;
968 let n = ids.len().saturating_sub(1);
969 let hs = self.hidden_size;
970 let rows = self.weights.lm_head.rows();
971 let mut pos = 0usize;
972 while pos < n {
973 let end = (pos + CHUNK).min(n);
974 let bsz = end - pos;
975 let hb = self.prefill_batch(&ids[pos..end], pos);
976 let mut k0 = 0usize;
977 while k0 < bsz {
978 let k1 = (k0 + LM_SUB).min(bsz);
979 let sb = k1 - k0;
980 if pos + k1 <= start {
983 k0 = k1;
984 continue;
985 }
986 let mut normed = vec![0.0f32; sb * hs];
987 for k in 0..sb {
988 let r = inference::rms_norm(
989 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
990 &self.weights.final_norm,
991 self.rms_eps,
992 self.norm_style,
993 );
994 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
995 }
996 let mut logits = vec![0.0f32; sb * rows];
997 self.weights
998 .lm_head
999 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1000 for k in 0..sb {
1001 if pos + k0 + k < start {
1002 continue;
1003 }
1004 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1005 let target = ids[pos + k0 + k + 1] as usize;
1006 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1007 let lse: f64 = lg
1008 .iter()
1009 .map(|&v| ((v - max) as f64).exp())
1010 .sum::<f64>()
1011 .ln()
1012 + max as f64;
1013 nll += lse - lg[target] as f64;
1014 cnt += 1;
1015 }
1016 k0 = k1;
1017 }
1018 pos = end;
1019 }
1020 self.kv_cache.clear();
1021 return (nll, cnt);
1022 }
1023 for pos in 0..ids.len().saturating_sub(1) {
1024 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1025 if pos < start {
1026 continue;
1027 }
1028 let normed = inference::rms_norm(
1029 &hidden,
1030 &self.weights.final_norm,
1031 self.rms_eps,
1032 self.norm_style,
1033 );
1034 let logits = self.lm_head_forward(&normed);
1035 let target = ids[pos + 1] as usize;
1036 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1037 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1038 + max as f64;
1039 nll += lse - logits[target] as f64;
1040 cnt += 1;
1041 }
1042 self.kv_cache.clear();
1043 (nll, cnt)
1044 }
1045
1046 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1062 self.kv_cache.clear();
1063 self.o1_begin();
1064 let n = ids.len().saturating_sub(1);
1065 let p = prefill.min(n);
1066 let mut pos = 0usize;
1068 if prefill_batched() {
1069 const CHUNK: usize = 128;
1070 while pos < p {
1071 let end = (pos + CHUNK).min(p);
1072 let _ = self.prefill_batch(&ids[pos..end], pos);
1073 pos = end;
1074 }
1075 } else {
1076 while pos < p {
1077 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1078 pos += 1;
1079 }
1080 }
1081 self.o1_seal();
1082
1083 let mut nll = 0f64;
1084 let mut cnt = 0usize;
1085 for pos in p..n {
1086 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1087 let normed = inference::rms_norm(
1088 &hidden,
1089 &self.weights.final_norm,
1090 self.rms_eps,
1091 self.norm_style,
1092 );
1093 let logits = self.lm_head_forward(&normed);
1094 let target = ids[pos + 1] as usize;
1095 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1096 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1097 + max as f64;
1098 nll += lse - logits[target] as f64;
1099 cnt += 1;
1100 }
1101 self.kv_cache.clear();
1102 (nll, cnt)
1103 }
1104
1105 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1113 self.kv_cache.clear();
1114 let n = ids.len().saturating_sub(1);
1115 let mut correct = Vec::with_capacity(n);
1116 let mut pmax = Vec::with_capacity(n);
1117 for pos in 0..n {
1118 let emb = self.embed_single(ids[pos]);
1119 let hidden = self.forward_layers(&emb, pos, None);
1120 let normed = inference::rms_norm(
1121 &hidden,
1122 &self.weights.final_norm,
1123 self.rms_eps,
1124 self.norm_style,
1125 );
1126 let logits = self.lm_head_forward(&normed);
1127 let target = ids[pos + 1] as usize;
1128 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1129 for (i, &v) in logits.iter().enumerate() {
1130 if v > mval {
1131 mval = v;
1132 amax = i;
1133 }
1134 }
1135 correct.push(amax == target);
1136 let row: Vec<f32> = temps
1137 .iter()
1138 .map(|&t| {
1139 let tt = t.max(1e-3);
1140 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1141 1.0 / s.max(1e-12) })
1143 .collect();
1144 pmax.push(row);
1145 }
1146 self.kv_cache.clear();
1147 (correct, pmax)
1148 }
1149
1150 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1157 let mut router = match self.dyn_router.take() {
1158 Some(r) => r,
1159 None => return (self.ppl_ids(ids), 0),
1160 };
1161 router.reset();
1162 self.dyn_phi_seen = 0;
1163 let _ = self.set_active_skill(None);
1164
1165 self.kv_cache.clear();
1166 let mut nll = 0f64;
1167 let mut cnt = 0usize;
1168 for pos in 0..ids.len().saturating_sub(1) {
1169 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1170 let normed = inference::rms_norm(
1171 &hidden,
1172 &self.weights.final_norm,
1173 self.rms_eps,
1174 self.norm_style,
1175 );
1176 let logits = self.lm_head_forward(&normed);
1177 let target = ids[pos + 1] as usize;
1178 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1179 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1180 + max as f64;
1181 nll += lse - logits[target] as f64;
1182 cnt += 1;
1183 let phi = self.dyn_phi_ema.clone();
1185 if let Some(new_active) = router.step(&phi, pos) {
1186 let _ = self.set_active_skill(new_active);
1187 }
1188 }
1189 let switches = router.switches.len();
1190 let _ = self.set_active_skill(None);
1191 self.dyn_router = Some(router);
1192 self.kv_cache.clear();
1193 ((nll / cnt.max(1) as f64).exp(), switches)
1194 }
1195
1196 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1198 self.kv_cache.clear();
1199 let mut acc = vec![0f32; self.hidden_size];
1200 for (pos, &id) in ids.iter().enumerate() {
1201 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1202 for (a, v) in acc.iter_mut().zip(&h) {
1203 *a += v;
1204 }
1205 }
1206 let n = ids.len().max(1) as f32;
1207 for a in acc.iter_mut() {
1208 *a /= n;
1209 }
1210 self.kv_cache.clear();
1211 acc
1212 }
1213
1214 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1220 let b = ids.len();
1221 let hs = self.hidden_size;
1222 let mut h: Vec<f32> = Vec::with_capacity(b * hs);
1223 for &id in ids {
1224 h.extend_from_slice(&self.embed_single(id));
1225 }
1226 let (nh, nkv, hd, rd, eps) = (
1227 self.num_heads,
1228 self.num_kv_heads,
1229 self.head_dim,
1230 self.rotary_dim,
1231 self.rms_eps,
1232 );
1233 let inv_freq = self.inv_freq.clone();
1234 let pool = self.pool.clone();
1235 let norm_style = self.norm_style;
1236
1237 for li in 0..self.num_layers {
1238 crate::gpu::set_layer(li as i64); let lw = &self.weights.layers[li];
1240 match &lw.attn {
1242 AttnKind::LinearGdn(w) => {
1243 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1245 let mut normed = vec![0.0f32; b * hs];
1246 for bi in 0..b {
1247 let r = inference::rms_norm(
1248 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1249 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1250 }
1251 let attn = crate::linear_core::gdn_forward_batch(
1252 &normed, b, w, &cfg,
1253 &mut self.kv_cache.layers[li].linear_state,
1254 pool.as_deref(),
1255 );
1256 for (dst, &a) in h.iter_mut().zip(&attn) {
1257 *dst += a;
1258 }
1259 }
1260 _ => {
1261 for bi in 0..b {
1262 let normed = inference::rms_norm(
1263 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1264 let position = start_pos + bi;
1265 let attn = match &lw.attn {
1266 AttnKind::Linear(w) => {
1267 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1268 vmf_phase_forward(
1269 &normed, w, &cfg,
1270 &mut self.kv_cache.layers[li].linear_state,
1271 pool.as_deref(),
1272 )
1273 }
1274 AttnKind::LinearGdn(_) => unreachable!(),
1275 AttnKind::Full {
1276 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
1277 } => {
1278 let cfg = QwenAttnCfg {
1279 num_heads: nh,
1280 num_kv_heads: nkv,
1281 head_dim: hd,
1282 hidden_size: hs,
1283 position,
1284 inv_freq: &inv_freq,
1285 rotary_dim: rd,
1286 q_norm: q_norm.as_deref(),
1287 k_norm: k_norm.as_deref(),
1288 output_gate: *output_gate,
1289 bias: bias.as_ref().map(|(a, b, c)| {
1290 (a.as_slice(), b.as_slice(), c.as_slice())
1291 }),
1292 rms_eps: eps,
1293 norm_style,
1294 pool: pool.as_deref(),
1295 };
1296 attention::qwen_attention(
1297 &normed, wq, wk, wv, wo,
1298 &mut self.kv_cache.layers[li], &cfg)
1299 }
1300 };
1301 for (i, &a) in attn.iter().enumerate() {
1302 h[bi * hs + i] += a;
1303 }
1304 }
1305 }
1306 }
1307
1308 let lw = &self.weights.layers[li];
1310 let mut post = vec![0.0f32; b * hs];
1311 for bi in 0..b {
1312 let r = inference::rms_norm(
1313 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
1314 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1315 }
1316 let ffn = match &lw.ffn {
1317 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
1318 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
1319 };
1320 for (dst, &f) in h.iter_mut().zip(&ffn) {
1321 *dst += f;
1322 }
1323 }
1324 crate::gpu::set_layer(-1); h
1326 }
1327
1328 fn embed_single(&self, id: u32) -> Vec<f32> {
1330 let mut out = vec![0.0f32; self.hidden_size];
1331 if (id as usize) < self.weights.embed_tokens.rows() {
1332 self.weights.embed_tokens.row_f32(id as usize, &mut out);
1333 }
1334 out
1335 }
1336
1337 fn forward_layers(
1339 &mut self,
1340 hidden: &[f32],
1341 position: usize,
1342 task_mask: Option<&TaskMask>,
1343 ) -> Vec<f32> {
1344 self.forward_layers_upto(hidden, position, task_mask, None)
1345 }
1346
1347 fn forward_layers_upto(
1349 &mut self,
1350 hidden: &[f32],
1351 position: usize,
1352 task_mask: Option<&TaskMask>,
1353 upto: Option<usize>,
1354 ) -> Vec<f32> {
1355 let mut h = hidden.to_vec();
1356 let (nh, nkv, hd, hs, rd, eps) = (
1359 self.num_heads,
1360 self.num_kv_heads,
1361 self.head_dim,
1362 self.hidden_size,
1363 self.rotary_dim,
1364 self.rms_eps,
1365 );
1366 let inv_freq = self.inv_freq.clone();
1367 let pool = self.pool.clone();
1368
1369 for li in 0..self.num_layers {
1370 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
1372 if li > u {
1373 break;
1374 }
1375 }
1376 if let Some(mask) = task_mask {
1377 if !mask.layer_alive(li) {
1378 continue; }
1380 }
1381
1382 let lw = &self.weights.layers[li];
1383 let normed = inference::rms_norm(&h, &lw.input_norm, self.rms_eps, self.norm_style);
1384
1385 let attn_out = match &lw.attn {
1386 AttnKind::Linear(w) => {
1387 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1388 vmf_phase_forward(
1389 &normed,
1390 w,
1391 &cfg,
1392 &mut self.kv_cache.layers[li].linear_state,
1393 self.pool.as_deref(),
1394 )
1395 }
1396 AttnKind::LinearGdn(w) => {
1397 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1398 gdn_forward(
1399 &normed,
1400 w,
1401 &cfg,
1402 &mut self.kv_cache.layers[li].linear_state,
1403 self.pool.as_deref(),
1404 )
1405 }
1406 AttnKind::Full {
1407 wq,
1408 wk,
1409 wv,
1410 wo,
1411 q_norm,
1412 k_norm,
1413 output_gate,
1414 bias,
1415 } if self.kv_cache.layers[li].o1_sealed() => {
1416 let cfg = QwenAttnCfg {
1419 num_heads: nh,
1420 num_kv_heads: nkv,
1421 head_dim: hd,
1422 hidden_size: hs,
1423 position,
1424 inv_freq: &inv_freq,
1425 rotary_dim: rd,
1426 q_norm: q_norm.as_deref(),
1427 k_norm: k_norm.as_deref(),
1428 output_gate: *output_gate,
1429 bias: bias
1430 .as_ref()
1431 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1432 rms_eps: eps,
1433 norm_style: self.norm_style,
1434 pool: pool.as_deref(),
1435 };
1436 attention::qwen_attention_nystrom(
1437 &normed,
1438 wq,
1439 wk,
1440 wv,
1441 wo,
1442 &mut self.kv_cache.layers[li],
1443 &cfg,
1444 )
1445 }
1446 AttnKind::Full {
1447 wq,
1448 wk,
1449 wv,
1450 wo,
1451 q_norm,
1452 k_norm,
1453 output_gate,
1454 bias,
1455 } => {
1456 let masked = task_mask
1457 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
1458 .unwrap_or(false);
1459 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
1460 match (masked, f32_view) {
1461 (true, (Some(q), Some(k), Some(v), Some(o))) => {
1464 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
1465 attention::multi_head_attention(
1466 &normed,
1467 q,
1468 k,
1469 v,
1470 o,
1471 &mut self.kv_cache.layers[li],
1472 self.num_heads,
1473 self.num_kv_heads,
1474 self.head_dim,
1475 self.hidden_size,
1476 position,
1477 &active_heads,
1478 &self.inv_freq,
1479 )
1480 }
1481 (masked, _) => {
1482 if masked {
1483 tracing::warn!(
1484 "layer {li}: head mask on quantized weights not \
1485 supported yet — executing dense"
1486 );
1487 }
1488 let cfg = QwenAttnCfg {
1489 num_heads: nh,
1490 num_kv_heads: nkv,
1491 head_dim: hd,
1492 hidden_size: hs,
1493 position,
1494 inv_freq: &inv_freq,
1495 rotary_dim: rd,
1496 q_norm: q_norm.as_deref(),
1497 k_norm: k_norm.as_deref(),
1498 output_gate: *output_gate,
1499 bias: bias
1500 .as_ref()
1501 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1502 rms_eps: eps,
1503 norm_style: self.norm_style,
1504 pool: pool.as_deref(),
1505 };
1506 attention::qwen_attention(
1507 &normed,
1508 wq,
1509 wk,
1510 wv,
1511 wo,
1512 &mut self.kv_cache.layers[li],
1513 &cfg,
1514 )
1515 }
1516 }
1517 }
1518 };
1519 for (i, &a) in attn_out.iter().enumerate() {
1520 h[i] += a;
1521 }
1522
1523 let lw = &self.weights.layers[li];
1524 let post_normed = inference::rms_norm(&h, &lw.post_norm, self.rms_eps, self.norm_style);
1525
1526 let ffn_masked = task_mask
1527 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
1528 .unwrap_or(false);
1529 let f32_ffn = match &lw.ffn {
1532 FfnKind::Dense(d) => {
1533 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
1534 }
1535 FfnKind::Moe(_) => (None, None, None),
1536 };
1537 let ffn_out = match (ffn_masked, f32_ffn) {
1538 (true, (Some(g), Some(u), Some(d))) => {
1539 let active = task_mask.unwrap().ffn_active_indices(li);
1540 inference::sparse_ffn_forward(
1541 &post_normed,
1542 g,
1543 u,
1544 d,
1545 self.hidden_size,
1546 self.intermediate_size,
1547 &active,
1548 self.pool.as_deref(),
1549 )
1550 }
1551 (true, _) => match &lw.ffn {
1555 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
1556 let active = task_mask.unwrap().ffn_active_indices(li);
1557 sparse_ffn_quant(
1558 d,
1559 &post_normed,
1560 &active,
1561 self.hidden_size,
1562 self.pool.as_deref(),
1563 )
1564 }
1565 FfnKind::Dense(d) => {
1570 let active = task_mask.unwrap().ffn_active_indices(li);
1571 let (gf, uf, df) = dequant_dense_f32(d);
1572 inference::sparse_ffn_forward(
1573 &post_normed,
1574 &gf,
1575 &uf,
1576 &df,
1577 self.hidden_size,
1578 self.intermediate_size,
1579 &active,
1580 self.pool.as_deref(),
1581 )
1582 }
1583 FfnKind::Moe(_) => {
1584 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
1587 }
1588 },
1589 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
1590 };
1591 for (i, &f) in ffn_out.iter().enumerate() {
1592 h[i] += f;
1593 }
1594
1595 if self.dyn_phi_layer == Some(li) {
1599 self.update_dyn_phi(&h);
1600 }
1601 }
1602 crate::gpu::set_layer(-1); h
1605 }
1606
1607 fn update_dyn_phi(&mut self, h: &[f32]) {
1610 const A: f32 = 0.2;
1611 if self.dyn_phi_ema.len() != h.len() {
1612 self.dyn_phi_ema = vec![0.0; h.len()];
1613 self.dyn_phi_seen = 0;
1614 }
1615 if self.dyn_phi_seen == 0 {
1616 self.dyn_phi_ema.copy_from_slice(h);
1617 } else {
1618 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
1619 *e = (1.0 - A) * *e + A * v;
1620 }
1621 }
1622 self.dyn_phi_seen += 1;
1623 }
1624
1625 pub fn dyn_phi(&self) -> &[f32] {
1627 &self.dyn_phi_ema
1628 }
1629
1630 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
1632 self.dyn_phi_layer = layer;
1633 self.dyn_phi_ema.clear();
1634 self.dyn_phi_seen = 0;
1635 }
1636
1637 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
1639 let Some(model) = &self.model else { return Vec::new() };
1640 model
1641 .header
1642 .skills
1643 .iter()
1644 .enumerate()
1645 .filter_map(|(i, sk)| {
1646 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
1647 let sel = sk.selection.as_ref()?;
1648 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
1649 })
1650 .collect()
1651 }
1652
1653 pub fn active_skill(&self) -> Option<usize> {
1655 self.dyn_active
1656 }
1657
1658 pub fn enable_dynamic_routing(&mut self) -> usize {
1663 use crate::swarm::{DynRouter, RoutableSkill};
1664 let Some(model) = self.model.clone() else { return 0 };
1665 if self.dyn_blend_loaded {
1668 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
1669 return 0;
1670 }
1671 if let Some(a) = self.dyn_active {
1675 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
1676 tracing::warn!(
1677 "loaded skill is not FFN-eligible — dynamic routing unavailable"
1678 );
1679 return 0;
1680 }
1681 }
1682 let hidden = self.hidden_size;
1683 let mut skills = Vec::new();
1684 for (idx, id, _phi) in self.dynamic_skills() {
1685 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
1686 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
1687 skills.push(rs);
1688 }
1689 }
1690 }
1691 if skills.is_empty() {
1692 return 0;
1693 }
1694 let phi = skills[0].phi_layer;
1696 if skills.iter().any(|s| s.phi_layer != phi) {
1697 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
1698 }
1699 let n = skills.len();
1700 self.set_dyn_phi_layer(Some(phi));
1701 self.dyn_router = Some(DynRouter::new(skills));
1702 n
1703 }
1704
1705 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
1707 self.dyn_router
1708 .as_ref()
1709 .map(|r| r.switches.clone())
1710 .unwrap_or_default()
1711 }
1712
1713 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
1716 let rows = self.weights.lm_head.rows();
1717 let mut logits = vec![0.0f32; rows.min(self.vocab_size)];
1718 self.weights
1719 .lm_head
1720 .matvec(hidden, &mut logits, self.pool.as_deref());
1721 logits.resize(self.vocab_size, 0.0);
1722 logits
1723 }
1724
1725 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
1730 self.kv_cache.clear();
1731 let mut hidden = vec![0.0f32; self.hidden_size];
1732 for (pos, &id) in ids.iter().enumerate() {
1733 let emb = self.embed_single(id);
1734 hidden = self.forward_layers(&emb, pos, task_mask);
1735 }
1736 let normed = inference::rms_norm(
1737 &hidden,
1738 &self.weights.final_norm,
1739 self.rms_eps,
1740 self.norm_style,
1741 );
1742 self.lm_head_forward(&normed)
1743 }
1744}
1745
1746pub fn create_test_pipeline(
1748 hidden_size: usize,
1749 intermediate_size: usize,
1750 num_heads: usize,
1751 num_kv_heads: usize,
1752 head_dim: usize,
1753 num_layers: usize,
1754 vocab_size: usize,
1755) -> Pipeline {
1756 let synth = |n: usize, salt: usize| -> Vec<f32> {
1759 (0..n)
1760 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
1761 .collect()
1762 };
1763 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
1764 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
1765 };
1766 let layer_weights: Vec<LayerWeights> = (0..num_layers)
1767 .map(|li| LayerWeights {
1768 input_norm: vec![1.0; hidden_size],
1769 post_norm: vec![1.0; hidden_size],
1770 ffn: FfnKind::Dense(DenseFfn {
1771 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
1772 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
1773 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
1774 }),
1775 attn: AttnKind::Full {
1776 bias: None,
1777 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
1778 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
1779 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
1780 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
1781 q_norm: None,
1782 k_norm: None,
1783 output_gate: false,
1784 },
1785 })
1786 .collect();
1787
1788 Pipeline::new(
1789 Tokenizer::byte_level(),
1790 PipelineWeights {
1791 embed_tokens: qt(vocab_size, hidden_size, 100),
1792 layers: layer_weights,
1793 lm_head: qt(vocab_size, hidden_size, 200),
1794 final_norm: vec![1.0; hidden_size],
1795 },
1796 hidden_size,
1797 intermediate_size,
1798 num_heads,
1799 num_kv_heads,
1800 head_dim,
1801 num_layers,
1802 vocab_size,
1803 1e-6,
1804 10_000.0,
1805 NormStyle::Qwen,
1806 4096,
1807 SamplerConfig {
1808 seed: Some(42),
1809 ..Default::default()
1810 },
1811 )
1812}
1813
1814fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
1817 let inter = d.gate_proj.rows();
1818 let hidden = d.down_proj.rows();
1819 let mut g = vec![0.0f32; b * inter];
1820 d.gate_proj.matmat(xs, b, &mut g, pool);
1821 let mut u = vec![0.0f32; b * inter];
1822 d.up_proj.matmat(xs, b, &mut u, pool);
1823 for i in 0..b * inter {
1824 g[i] = inference::silu(g[i]) * u[i];
1825 }
1826 let mut out = vec![0.0f32; b * hidden];
1827 d.down_proj.matmat(&g, b, &mut out, pool);
1828 out
1829}
1830
1831fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
1835 let ne = m.experts.len();
1836 let mut logits = vec![0.0f32; b * ne];
1837 m.router.matmat(xs, b, &mut logits, pool);
1838
1839 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
1842 {
1843 let mut st = m.stats.borrow_mut();
1844 if st.len() < ne {
1845 st.resize(ne, 0);
1846 }
1847 for bi in 0..b {
1848 let lg = &logits[bi * ne..(bi + 1) * ne];
1849 let mx = lg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1850 let mut p: Vec<f32> = lg.iter().map(|&l| (l - mx).exp()).collect();
1851 let sum: f32 = p.iter().sum();
1852 for v in &mut p {
1853 *v /= sum;
1854 }
1855 let mut order: Vec<usize> = (0..ne).collect();
1856 order.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y)));
1857 order.truncate(m.top_k);
1858 let wsum: f32 = if m.norm_topk_prob {
1859 order.iter().map(|&e| p[e]).sum()
1860 } else {
1861 1.0
1862 };
1863 for &e in &order {
1864 st[e] += 1;
1865 assign[e].push((bi, p[e] / wsum));
1866 }
1867 }
1868 }
1869
1870 let mut out = vec![0.0f32; b * hidden];
1871 let cols = m.experts[0].gate_proj.cols();
1872 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
1873 let sb = list.len();
1874 let mut sub = vec![0.0f32; sb * cols];
1875 for (k, &(bi, _)) in list.iter().enumerate() {
1876 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
1877 }
1878 let eo = dense_ffn_batch(d, &sub, sb, pool);
1879 for (k, &(bi, w)) in list.iter().enumerate() {
1880 for i in 0..hidden {
1881 out[bi * hidden + i] += w * eo[k * hidden + i];
1882 }
1883 }
1884 };
1885 for e in 0..ne {
1886 if !assign[e].is_empty() {
1887 run_expert(&m.experts[e], &assign[e]);
1888 }
1889 }
1890 if let Some((se, gate)) = &m.shared {
1891 let mut gl = vec![0.0f32; b];
1892 gate.matmat(xs, b, &mut gl, pool);
1893 let all: Vec<(usize, f32)> = (0..b)
1894 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
1895 .collect();
1896 run_expert(se, &all);
1897 }
1898 out
1899}
1900
1901fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
1903 let inter = d.gate_proj.rows();
1904 let mut g = vec![0.0f32; inter];
1905 d.gate_proj.matvec(x, &mut g, pool);
1906 let mut u = vec![0.0f32; inter];
1907 d.up_proj.matvec(x, &mut u, pool);
1908 for i in 0..inter {
1909 g[i] = inference::silu(g[i]) * u[i];
1910 }
1911 let mut out = vec![0.0f32; d.down_proj.rows()];
1912 d.down_proj.matvec(&g, &mut out, pool);
1913 out
1914}
1915
1916fn sparse_ffn_quant(
1923 d: &DenseFfn,
1924 x: &[f32],
1925 active: &[u16],
1926 hidden: usize,
1927 pool: Option<&Pool>,
1928) -> Vec<f32> {
1929 let n = active.len();
1930 let inter = d.gate_proj.rows();
1931 let mut act = vec![0.0f32; n];
1932 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
1935 let compute = |ai: usize| -> f32 {
1936 let idx = active[ai] as usize;
1937 if idx >= inter {
1938 return 0.0; }
1940 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
1941 let gate = d.gate_proj.row_dot(idx, x, &mut s);
1942 let up = d.up_proj.row_dot(idx, x, &mut s);
1943 inference::silu(gate) * up
1944 };
1945 match pool {
1946 Some(p) if n >= 256 => {
1947 let ptr = SendMut(act.as_mut_ptr());
1948 p.run(&|widx, nw| {
1949 let chunk = n.div_ceil(nw);
1950 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
1951 for ai in s..e {
1952 unsafe { *ptr.at(ai) = compute(ai) };
1953 }
1954 });
1955 }
1956 _ => {
1957 for (ai, a) in act.iter_mut().enumerate() {
1958 *a = compute(ai);
1959 }
1960 }
1961 }
1962 let mut out = vec![0.0f32; hidden];
1964 for (ai, &idx) in active.iter().enumerate() {
1965 let w = act[ai];
1966 if w.abs() >= 1e-12 && (idx as usize) < inter {
1967 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
1968 }
1969 }
1970 out
1971}
1972
1973#[doc(hidden)]
1975pub fn sparse_ffn_quant_for_test(
1976 d: &DenseFfn,
1977 x: &[f32],
1978 active: &[u16],
1979 hidden: usize,
1980) -> Vec<f32> {
1981 sparse_ffn_quant(d, x, active, hidden, None)
1982}
1983
1984fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
1988 let deq = |t: &QTensor| -> Vec<f32> {
1989 let (rows, cols) = (t.rows(), t.cols());
1990 let mut out = vec![0.0f32; rows * cols];
1991 for r in 0..rows {
1992 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
1993 }
1994 out
1995 };
1996 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
1997}
1998
1999struct SendMut(*mut f32);
2001unsafe impl Send for SendMut {}
2002unsafe impl Sync for SendMut {}
2003impl SendMut {
2004 #[inline]
2005 #[allow(clippy::mut_from_ref)]
2008 unsafe fn at(&self, i: usize) -> &mut f32 {
2009 unsafe { &mut *self.0.add(i) }
2010 }
2011}
2012
2013fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2017 let ne = m.experts.len();
2018 let mut logits = vec![0.0f32; ne];
2019 m.router.matvec(x, &mut logits, pool);
2020 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2021 let mut p: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
2022 let s: f32 = p.iter().sum();
2023 for v in &mut p {
2024 *v /= s;
2025 }
2026 let mut idx: Vec<usize> = (0..ne).collect();
2027 idx.sort_unstable_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap().then(a.cmp(&b)));
2029 idx.truncate(m.top_k);
2030 let wsum: f32 = if m.norm_topk_prob {
2031 idx.iter().map(|&e| p[e]).sum()
2032 } else {
2033 1.0
2034 };
2035 {
2036 let mut st = m.stats.borrow_mut();
2037 if st.len() < ne {
2038 st.resize(ne, 0);
2039 }
2040 for &e in &idx {
2041 st[e] += 1;
2042 }
2043 }
2044 if crate::gpu::enabled_here() {
2047 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
2048 return out;
2049 }
2050 }
2051
2052 let mut out = vec![0.0f32; x.len()];
2053 for &e in &idx {
2054 let eo = dense_ffn(&m.experts[e], x, pool);
2055 let w = p[e] / wsum;
2056 for i in 0..out.len() {
2057 out[i] += w * eo[i];
2058 }
2059 }
2060 if let Some((se, gate)) = &m.shared {
2061 let so = dense_ffn(se, x, pool);
2062 let mut gl = vec![0.0f32; 1];
2063 gate.matvec(x, &mut gl, pool);
2064 let g = 1.0 / (1.0 + (-gl[0]).exp());
2065 for i in 0..out.len() {
2066 out[i] += g * so[i];
2067 }
2068 }
2069 out
2070}
2071
2072fn moe_ffn_gpu(
2075 m: &MoeFfn,
2076 x: &[f32],
2077 idx: &[usize],
2078 p: &[f32],
2079 wsum: f32,
2080 pool: Option<&Pool>,
2081) -> Option<Vec<f32>> {
2082 use crate::gpu::MoeJob;
2083 use crate::qtensor::prescale;
2084 use cortiq_core::TensorDtype;
2085
2086 fn parts(
2087 t: &QTensor,
2088 ) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32])>
2089 {
2090 match t {
2091 QTensor::Mapped {
2092 model,
2093 idx,
2094 dtype: TensorDtype::Q8_2f,
2095 rows,
2096 cols,
2097 row_scale,
2098 col_field,
2099 } if !col_field.is_empty() => {
2100 Some((model, *idx, *rows, *cols, row_scale, col_field))
2101 }
2102 _ => None,
2103 }
2104 }
2105
2106 fn push<'a>(
2107 d: &'a DenseFfn,
2108 x: &[f32],
2109 w: f32,
2110 jobs: &mut Vec<MoeJob<'a>>,
2111 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
2112 ) -> Option<()> {
2113 let (gm, gi, gr, gc, grs, gcf) = parts(&d.gate_proj)?;
2114 let (_, ui, ur, uc, urs, ucf) = parts(&d.up_proj)?;
2115 let (_, di, dr, dc, drs, dcf) = parts(&d.down_proj)?;
2116 model_ref.get_or_insert_with(|| gm.clone());
2117 jobs.push(MoeJob {
2118 gate: (gi, gr, gc, grs),
2119 up: (ui, ur, uc, urs),
2120 down: (di, dr, dc, drs),
2121 xs_gate: prescale(x, gcf, TensorDtype::Q8_2f).into_owned(),
2122 xs_up: prescale(x, ucf, TensorDtype::Q8_2f).into_owned(),
2123 down_col: dcf,
2124 w,
2125 });
2126 Some(())
2127 }
2128
2129 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
2130 let mut model_ref = None;
2131 for &e in idx {
2132 push(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
2133 }
2134 if let Some((se, gate)) = &m.shared {
2135 let mut gl = vec![0.0f32; 1];
2136 gate.matvec(x, &mut gl, pool);
2137 let g = 1.0 / (1.0 + (-gl[0]).exp());
2138 push(se, x, g, &mut jobs, &mut model_ref)?;
2139 }
2140 let model = model_ref?;
2141 let hidden = jobs[0].down.1;
2142 let mut out = vec![0.0f32; hidden];
2143 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
2144}
2145
2146fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2148 match ffn {
2149 FfnKind::Dense(d) => dense_ffn(d, x, pool),
2150 FfnKind::Moe(m) => moe_ffn(m, x, pool),
2151 }
2152}
2153
2154fn ffn_forward_pair(
2158 ffn: &FfnKind,
2159 x1: &[f32],
2160 x2: &[f32],
2161 pool: Option<&Pool>,
2162) -> (Vec<f32>, Vec<f32>) {
2163 let d = match ffn {
2164 FfnKind::Dense(d) => d,
2165 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
2166 };
2167 let inter = d.gate_proj.rows();
2168 let mut g1 = vec![0.0f32; inter];
2169 let mut g2 = vec![0.0f32; inter];
2170 d.gate_proj.matvec2(x1, x2, &mut g1, &mut g2, pool);
2171 let mut u1 = vec![0.0f32; inter];
2172 let mut u2 = vec![0.0f32; inter];
2173 d.up_proj.matvec2(x1, x2, &mut u1, &mut u2, pool);
2174 for i in 0..inter {
2175 g1[i] = inference::silu(g1[i]) * u1[i];
2176 g2[i] = inference::silu(g2[i]) * u2[i];
2177 }
2178 let mut o1 = vec![0.0f32; d.down_proj.rows()];
2179 let mut o2 = vec![0.0f32; d.down_proj.rows()];
2180 d.down_proj.matvec2(&g1, &g2, &mut o1, &mut o2, pool);
2181 (o1, o2)
2182}
2183
2184#[cfg(test)]
2185mod tests {
2186 use super::*;
2187
2188 #[test]
2194 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
2195 let (hidden, inter) = (16usize, 40usize);
2196 let synth = |n: usize, salt: usize| -> Vec<f32> {
2197 (0..n)
2198 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
2199 .collect()
2200 };
2201 let d = DenseFfn {
2202 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
2203 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
2204 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
2205 };
2206 let x = synth(hidden, 9);
2207 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
2209
2210 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
2211
2212 let mut g = vec![0.0f32; inter];
2214 d.gate_proj.matvec(&x, &mut g, None);
2215 let mut u = vec![0.0f32; inter];
2216 d.up_proj.matvec(&x, &mut u, None);
2217 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
2218 for i in 0..inter {
2219 g[i] = if act_set.contains(&(i as u16)) {
2220 inference::silu(g[i]) * u[i]
2221 } else {
2222 0.0
2223 };
2224 }
2225 let mut reference = vec![0.0f32; hidden];
2226 d.down_proj.matvec(&g, &mut reference, None);
2227
2228 let max_d = sparse
2229 .iter()
2230 .zip(&reference)
2231 .map(|(a, b)| (a - b).abs())
2232 .fold(0.0f32, f32::max);
2233 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
2234 }
2235
2236 fn attach_test_mtp(p: &mut Pipeline) {
2238 let (h, inter, heads, kv, hd) = (
2239 p.hidden_size,
2240 p.intermediate_size,
2241 p.num_heads,
2242 p.num_kv_heads,
2243 p.head_dim,
2244 );
2245 let synth = |n: usize, salt: usize| -> Vec<f32> {
2246 (0..n)
2247 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
2248 .collect()
2249 };
2250 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2251 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2252 };
2253 p.mtp = Some(MtpModule {
2254 enorm: vec![1.0; h],
2255 hnorm: vec![1.0; h],
2256 eh_proj: qt(h, 2 * h, 301),
2257 layer: LayerWeights {
2258 input_norm: vec![1.0; h],
2259 post_norm: vec![1.0; h],
2260 ffn: FfnKind::Dense(DenseFfn {
2261 gate_proj: qt(inter, h, 315),
2262 up_proj: qt(inter, h, 316),
2263 down_proj: qt(h, inter, 317),
2264 }),
2265 attn: AttnKind::Full {
2266 bias: None,
2267 wq: qt(heads * hd, h, 311),
2268 wk: qt(kv * hd, h, 312),
2269 wv: qt(kv * hd, h, 313),
2270 wo: qt(h, heads * hd, 314),
2271 q_norm: None,
2272 k_norm: None,
2273 output_gate: false,
2274 },
2275 },
2276 final_norm: vec![1.0; h],
2277 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
2278 });
2279 }
2280
2281 #[test]
2282 fn speculative_equals_vanilla_greedy() {
2283 let run = |spec: bool| {
2284 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2285 p.sampler_config.temperature = 0.0;
2286 attach_test_mtp(&mut p);
2287 p.speculative = spec;
2288 let r = p.generate("abcdef", 12, None, None).unwrap();
2289 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
2290 };
2291 let (vanilla, d0, _) = run(false);
2292 let (spec, d1, a1) = run(true);
2293 assert_eq!(d0, 0, "vanilla path must not draft");
2294 assert!(d1 > 0, "speculative path must draft");
2295 assert_eq!(
2296 vanilla, spec,
2297 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
2298 );
2299 }
2300
2301 #[test]
2302 fn speculative_accepts_constant_oracle() {
2303 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2304 p.sampler_config.temperature = 0.0;
2305 p.sampler_config.repetition_penalty = 1.0;
2306 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
2309 attach_test_mtp(&mut p);
2310 p.speculative = true;
2311 let r = p.generate("abcd", 10, None, None).unwrap();
2312 assert!(r.mtp_drafted > 0);
2313 assert_eq!(
2314 r.mtp_accepted, r.mtp_drafted,
2315 "constant logits → every draft accepted"
2316 );
2317 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
2320 }
2321
2322 #[test]
2323 fn empty_prompt_is_an_error_not_a_panic() {
2324 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2325 let r = p.generate("", 4, None, None);
2326 assert!(r.is_err(), "empty prompt must be a clean error");
2327 }
2328
2329 #[test]
2330 fn every_token_enters_kv_exactly_once() {
2331 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2332 p.sampler_config.temperature = 0.0;
2334 let r = p.generate("abc", 2, None, None).unwrap();
2335 assert_eq!(r.prompt_tokens, 3);
2336 assert_eq!(
2340 p.kv_cache.seq_len(),
2341 3 + r.tokens_generated - 1,
2342 "each token must be cached exactly once (v1 cached the last prompt token twice)"
2343 );
2344 }
2345
2346 #[test]
2347 fn generation_is_reproducible_with_seed() {
2348 let run = || {
2349 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2350 p.generate("hello", 8, None, None).unwrap().token_ids
2351 };
2352 assert_eq!(run(), run());
2353 }
2354
2355 #[test]
2356 fn eviction_bounds_the_cache() {
2357 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2358 p.kv_cache.max_seq_len = 6;
2359 p.sampler_config.temperature = 0.0;
2360 let _ = p.generate("abcd", 12, None, None).unwrap();
2361 assert!(
2362 p.kv_cache.seq_len() <= 6 + 1,
2363 "cache must stay bounded by max_seq_len (got {})",
2364 p.kv_cache.seq_len()
2365 );
2366 }
2367
2368 #[test]
2369 fn confidence_matches_tokens_and_is_a_probability() {
2370 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2371 p.sampler_config.temperature = 0.0;
2372 p.sampler_config.repetition_penalty = 1.0;
2373 let r = p.generate("abcd", 10, None, None).unwrap();
2374 assert_eq!(
2375 r.token_confidence.len(),
2376 r.token_ids.len(),
2377 "one confidence per emitted token"
2378 );
2379 for &c in &r.token_confidence {
2380 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
2381 }
2382 let logits = [1.0f32, 3.0, 0.5, 3.0];
2384 let p0 = top1_prob_t(&logits, 1, 1.0);
2385 let p1 = top1_prob_t(&logits, 3, 1.0);
2386 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
2387 assert!(p0 > 0.0 && p0 < 1.0);
2388 let sharp = top1_prob_t(&logits, 1, 1.0);
2390 let soft = top1_prob_t(&logits, 1, 2.0);
2391 assert!(soft < sharp, "higher temperature lowers peak confidence");
2392 }
2393
2394 #[test]
2395 fn trace_is_opt_in_and_parallels_the_output() {
2396 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2398 p.sampler_config.temperature = 0.0;
2399 p.sampler_config.repetition_penalty = 1.0;
2400 let r = p.generate("abcd", 10, None, None).unwrap();
2401 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
2402
2403 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2405 p.sampler_config.temperature = 0.0;
2406 p.sampler_config.repetition_penalty = 1.0;
2407 p.set_trace(true);
2408 let r = p.generate("abcd", 10, None, None).unwrap();
2409 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
2410 for (i, tr) in r.traces.iter().enumerate() {
2411 assert_eq!(tr.t, i, "trace index is sequential");
2412 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
2413 assert_eq!(
2414 tr.confidence, r.token_confidence[i],
2415 "trace confidence matches the confidence channel"
2416 );
2417 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
2419 }
2420 }
2421
2422 #[test]
2423 fn explain_prefill_logits_match_greedy_first_token() {
2424 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2428 p.sampler_config.temperature = 0.0;
2429 p.sampler_config.repetition_penalty = 1.0;
2430 let ids = p.tokenizer.encode("abcd");
2431 let logits = p.prefill_next_logits(&ids, None);
2432 let argmax = logits
2433 .iter()
2434 .enumerate()
2435 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2436 .unwrap()
2437 .0 as u32;
2438 let r = p.generate("abcd", 1, None, None).unwrap();
2439 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
2440 }
2441}