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: Tokenizer,
47 pub kv_cache: KvCache,
48 pub sampler_config: SamplerConfig,
49 pub weights: PipelineWeights,
50 pub hidden_size: usize,
51 pub intermediate_size: usize,
52 pub num_heads: usize,
53 pub num_kv_heads: usize,
54 pub head_dim: usize,
55 pub num_layers: usize,
56 pub vocab_size: usize,
57 pub rms_eps: f64,
58 pub rope_base: f32,
59 pub norm_style: NormStyle,
60 pub rotary_dim: usize,
62 pub vmf_cfg: Option<VmfPhaseCfg>,
64 pub gdn_cfg: Option<GdnCfg>,
66 pub mtp: Option<MtpModule>,
68 pub speculative: bool,
70 rng: SplitMix64,
71 inv_freq: std::sync::Arc<Vec<f32>>,
75 ws: ForwardScratch,
79 pool: Option<std::sync::Arc<Pool>>,
81 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
85 pub(crate) dyn_force_f32: bool,
87 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
92 pub(crate) dyn_active: Option<usize>,
98 pub(crate) dyn_blend_loaded: bool,
102 pub(crate) dyn_phi_layer: Option<usize>,
105 dyn_phi_ema: Vec<f32>,
107 dyn_phi_seen: usize,
108 pub dyn_router: Option<crate::swarm::DynRouter>,
111 o1_cfg: Option<crate::nystrom::O1Cfg>,
114 o1_flags: Vec<bool>,
116 trace: bool,
119 calib_temp: f32,
122}
123
124pub struct PipelineWeights {
129 pub embed_tokens: QTensor,
131 pub layers: Vec<LayerWeights>,
133 pub lm_head: QTensor,
135 pub final_norm: Vec<f32>,
137}
138
139pub struct LayerWeights {
141 pub input_norm: Vec<f32>,
142 pub post_norm: Vec<f32>,
143 pub ffn: FfnKind,
144 pub attn: AttnKind,
145}
146
147pub struct DenseFfn {
149 pub gate_proj: QTensor,
150 pub up_proj: QTensor,
151 pub down_proj: QTensor,
152}
153
154pub enum FfnKind {
157 Dense(DenseFfn),
158 Moe(MoeFfn),
162}
163
164pub struct MoeFfn {
165 pub router: QTensor,
167 pub experts: Vec<DenseFfn>,
168 pub top_k: usize,
169 pub norm_topk_prob: bool,
170 pub shared: Option<(DenseFfn, QTensor)>,
172 pub stats: std::cell::RefCell<Vec<u64>>,
176}
177
178pub enum AttnKind {
181 Full {
183 wq: QTensor,
184 wk: QTensor,
185 wv: QTensor,
186 wo: QTensor,
187 q_norm: Option<Vec<f32>>,
188 k_norm: Option<Vec<f32>>,
189 output_gate: bool,
190 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
192 },
193 Linear(VmfPhaseWeights),
195 LinearGdn(GdnWeights),
197}
198
199pub struct MtpModule {
204 pub enorm: Vec<f32>,
205 pub hnorm: Vec<f32>,
206 pub eh_proj: QTensor,
208 pub layer: LayerWeights,
209 pub final_norm: Vec<f32>,
210 pub kv: crate::kv_cache::LayerKvCache,
211}
212
213pub struct GenerateResult {
215 pub text: String,
216 pub token_ids: Vec<u32>,
217 pub prompt_tokens: usize,
218 pub tokens_generated: usize,
219 pub finish_reason: String,
220 pub mtp_drafted: usize,
222 pub mtp_accepted: usize,
223 pub token_confidence: Vec<f32>,
228 pub traces: Vec<TokenTrace>,
231}
232
233#[derive(Clone, Debug)]
238pub struct TokenTrace {
239 pub t: usize,
241 pub token_id: u32,
243 pub confidence: f32,
245 pub active_skill: Option<String>,
247 pub recon: Option<f32>,
251 pub switched: bool,
254}
255
256fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
261 let t = if temp > 1e-3 { temp } else { 1.0 };
262 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
263 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
264 if sum > 0.0 {
265 (((logits[id as usize] - max) / t).exp()) / sum
266 } else {
267 0.0
268 }
269}
270
271fn prefill_batched() -> bool {
274 std::env::var("CMF_PREFILL").map(|v| v != "seq").unwrap_or(true)
275}
276
277pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
279
280impl Pipeline {
281 #[allow(clippy::too_many_arguments)]
283 pub fn new(
284 tokenizer: Tokenizer,
285 weights: PipelineWeights,
286 hidden_size: usize,
287 intermediate_size: usize,
288 num_heads: usize,
289 num_kv_heads: usize,
290 head_dim: usize,
291 num_layers: usize,
292 vocab_size: usize,
293 rms_eps: f64,
294 rope_base: f32,
295 norm_style: NormStyle,
296 max_seq_len: usize,
297 sampler_config: SamplerConfig,
298 ) -> Self {
299 let rng = match sampler_config.seed {
300 Some(s) => SplitMix64::new(s),
301 None => SplitMix64::from_entropy(),
302 };
303 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
304 let pool = Pool::from_env();
305 if let Some(p) = &pool {
306 tracing::info!("worker pool: {} threads", p.n_workers());
307 }
308 Self {
309 tokenizer,
310 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
311 sampler_config,
312 weights,
313 hidden_size,
314 intermediate_size,
315 num_heads,
316 num_kv_heads,
317 head_dim,
318 num_layers,
319 vocab_size,
320 rms_eps,
321 rope_base,
322 norm_style,
323 rotary_dim: head_dim,
324 vmf_cfg: None,
325 gdn_cfg: None,
326 mtp: None,
327 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
328 rng,
329 inv_freq,
330 ws: ForwardScratch::new(hidden_size),
331 pool,
332 model: None,
333 dyn_force_f32: false,
334 dyn_skill_layers: Vec::new(),
335 dyn_active: None,
336 dyn_blend_loaded: false,
337 dyn_phi_layer: None,
338 dyn_phi_ema: Vec::new(),
339 dyn_phi_seen: 0,
340 dyn_router: None,
341 o1_cfg: None,
342 o1_flags: Vec::new(),
343 trace: false,
344 calib_temp: 1.0,
345 }
346 }
347
348 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
355 self.o1_flags = match &cfg {
356 Some(c) => {
357 let mut flags = c.layer_flags(self.num_layers);
358 for (li, f) in flags.iter_mut().enumerate() {
359 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
360 *f = false;
361 }
362 }
363 flags
364 }
365 None => Vec::new(),
366 };
367 if let Some(c) = &cfg {
368 let n = self.o1_flags.iter().filter(|&&f| f).count();
369 tracing::info!(
370 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
371 self.num_layers, c.m, c.w, c.sink, c.rect
372 );
373 }
374 self.o1_cfg = cfg;
375 }
376
377 pub fn o1_active(&self) -> bool {
379 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
380 }
381
382 fn o1_begin(&mut self) {
384 if let Some(c) = &self.o1_cfg {
385 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
386 for (li, &f) in self.o1_flags.iter().enumerate() {
387 if f {
388 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
389 }
390 }
391 }
392 }
393
394 fn o1_seal(&mut self) {
397 if self.o1_cfg.is_none() {
398 return;
399 }
400 for li in 0..self.num_layers {
401 if self.o1_flags.get(li).copied().unwrap_or(false) {
402 self.kv_cache.layers[li].o1_seal(self.num_heads);
403 }
404 }
405 }
406
407 pub fn set_trace(&mut self, on: bool) {
409 self.trace = on;
410 }
411
412 pub fn set_calib_temp(&mut self, t: f32) {
415 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
416 }
417
418 pub fn calib_temp(&self) -> f32 {
420 self.calib_temp
421 }
422
423 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
426 self.rotary_dim = rotary_dim.min(self.head_dim);
427 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
428 }
429
430 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
431 QwenAttnCfg {
432 num_heads: self.num_heads,
433 num_kv_heads: self.num_kv_heads,
434 head_dim: self.head_dim,
435 hidden_size: self.hidden_size,
436 position,
437 inv_freq: &self.inv_freq,
438 rotary_dim: self.rotary_dim,
439 q_norm: None,
440 k_norm: None,
441 output_gate: false,
442 bias: None,
443 rms_eps: self.rms_eps,
444 norm_style: self.norm_style,
445 pool: self.pool.as_deref(),
446 }
447 }
448
449 pub fn generate(
451 &mut self,
452 prompt: &str,
453 max_tokens: usize,
454 task_mask: Option<&TaskMask>,
455 on_token: Option<TokenCallback>,
456 ) -> Result<GenerateResult, String> {
457 let input_ids = self.tokenizer.encode(prompt);
458 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
459 }
460
461 pub fn generate_from_ids(
469 &mut self,
470 input_ids: &[u32],
471 max_tokens: usize,
472 task_mask: Option<&TaskMask>,
473 mut on_token: Option<TokenCallback>,
474 ) -> Result<GenerateResult, String> {
475 if input_ids.is_empty() {
476 return Err("empty prompt: nothing to generate from".to_string());
477 }
478
479 self.kv_cache.clear();
481 self.o1_begin();
482
483 let spec_active = self.speculative
487 && self.mtp.is_some()
488 && task_mask.is_none()
489 && !self.o1_active()
490 && self.sampler_config.temperature < 1e-6;
491 let mut mtp = if spec_active { self.mtp.take() } else { None };
494 if let Some(m) = &mut mtp {
495 m.kv.clear();
496 }
497 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
501 if let Some(r) = &mut router {
502 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
505 }
506
507 let mut all_ids = input_ids.to_vec();
508 let mut generated = 0usize;
509 let mut finish_reason = "max_tokens".to_string();
510 let mut drafted = 0usize;
511 let mut accepted = 0usize;
512 let mut confidence: Vec<f32> = Vec::new();
513 let trace_on = self.trace;
514 let calib_temp = self.calib_temp;
515 let mut traces: Vec<TokenTrace> = Vec::new();
516
517 let mut hidden = vec![0.0f32; self.hidden_size];
523 let mut pos = 0usize;
524 let dyn_prefill = router.is_some();
529 if task_mask.is_none() && !dyn_prefill && prefill_batched() && input_ids.len() > 2 {
530 const CHUNK: usize = 48;
536 let hs = self.hidden_size;
537 while pos < input_ids.len() {
538 let end = (pos + CHUNK).min(input_ids.len());
539 let hb = self.prefill_batch(&input_ids[pos..end], pos);
540 if let Some(m) = &mut mtp {
541 for p in pos..end {
542 if p + 1 < input_ids.len() {
543 let _ = self.mtp_step(
544 m,
545 &hb[(p - pos) * hs..(p - pos + 1) * hs],
546 input_ids[p + 1],
547 p,
548 );
549 }
550 }
551 }
552 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
553 pos = end;
554 }
555 }
556 if task_mask.is_none() && !dyn_prefill {
557 while pos + 1 < input_ids.len() {
558 let e1 = self.embed_single(input_ids[pos]);
559 let e2 = self.embed_single(input_ids[pos + 1]);
560 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
561 self.commit_linear_scratch();
563 if let Some(m) = &mut mtp {
564 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
565 if pos + 2 < input_ids.len() {
566 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
567 }
568 }
569 hidden = h2;
570 pos += 2;
571 }
572 }
573 while pos < input_ids.len() {
574 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
575 if let Some(m) = &mut mtp {
576 if pos + 1 < input_ids.len() {
577 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
578 }
579 }
580 pos += 1;
581 }
582 self.o1_seal();
585
586 macro_rules! commit {
588 ($id:expr) => {{
589 all_ids.push($id);
590 generated += 1;
591 if self.tokenizer.is_eos($id) {
592 finish_reason = "stop".to_string();
593 false
594 } else {
595 let token_text = self.tokenizer.decode_token($id);
596 let mut go = true;
597 if let Some(ref mut cb) = on_token {
598 if !cb(&token_text) {
599 finish_reason = "cancelled".to_string();
600 go = false;
601 }
602 }
603 go
604 }
605 }};
606 }
607
608 let mut next_pos = input_ids.len();
610 'decode: while generated < max_tokens {
611 inference::rms_norm_into(
612 &hidden,
613 &self.weights.final_norm,
614 self.rms_eps,
615 self.norm_style,
616 &mut self.ws.n1,
617 );
618 let mut logits = self.lm_head_forward(&self.ws.n1);
619 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
620 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
621 attention::recycle_buf(&mut logits);
622 if trace_on {
623 let skill = router.as_ref().and_then(|r| r.active_id());
627 traces.push(TokenTrace {
628 t: generated,
629 token_id: t_next,
630 confidence: *confidence.last().unwrap(),
631 active_skill: skill,
632 recon: None,
633 switched: false,
634 });
635 }
636 if !commit!(t_next) {
637 break 'decode;
638 }
639 if generated >= max_tokens {
640 break 'decode;
641 }
642
643 if self.kv_cache.needs_eviction() {
644 let keep = (self.kv_cache.max_seq_len / 2).max(1);
645 self.kv_cache.evict(keep);
646 }
647
648 match &mut mtp {
649 Some(m) if generated + 1 < max_tokens => {
651 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
652 drafted += 1;
653 let emb1 = self.embed_single(t_next);
654 let emb2 = self.embed_single(draft);
655 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
656
657 inference::rms_norm_into(
658 &h1,
659 &self.weights.final_norm,
660 self.rms_eps,
661 self.norm_style,
662 &mut self.ws.n1,
663 );
664 let mut logits1 = self.lm_head_forward(&self.ws.n1);
665 let t_after =
666 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
667 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
668 attention::recycle_buf(&mut logits1);
669 if trace_on {
670 traces.push(TokenTrace {
673 t: generated,
674 token_id: t_after,
675 confidence: *confidence.last().unwrap(),
676 active_skill: None,
677 recon: None,
678 switched: false,
679 });
680 }
681 let stop = !commit!(t_after);
682
683 if t_after == draft {
684 accepted += 1;
685 self.commit_linear_scratch();
686 let _ = self.mtp_step(m, &h1, t_after, next_pos);
687 hidden = h2;
688 next_pos += 2;
689 } else {
690 for layer in &mut self.kv_cache.layers {
692 layer.truncate_last(1);
693 }
694 if !stop {
695 let _ = self.mtp_step(m, &h1, t_after, next_pos);
696 hidden = self
697 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
698 }
699 next_pos += 2;
700 }
701 if stop {
702 break 'decode;
703 }
704 }
705 _ => {
707 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
708 next_pos += 1;
709 if let Some(r) = &mut router {
712 let phi = self.dyn_phi_ema.clone();
713 let decision = r.step(&phi, generated);
714 if let Some(new_active) = decision {
715 let _ = self.set_active_skill(new_active);
716 }
717 if trace_on {
720 if let Some(last) = traces.last_mut() {
721 let e = r.last_best_e();
722 last.recon = e.is_finite().then_some(e);
723 last.switched = decision.is_some();
724 }
725 }
726 }
727 }
728 }
729 }
730
731 if router.is_some() {
733 let _ = self.set_active_skill(None);
734 }
735 self.dyn_router = router.or(self.dyn_router.take());
736 self.mtp = mtp.or(self.mtp.take());
737
738 let output_ids = &all_ids[input_ids.len()..];
739 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
741 Ok(GenerateResult {
742 text: self.tokenizer.decode(output_ids),
743 token_ids: output_ids.to_vec(),
744 prompt_tokens: input_ids.len(),
745 tokens_generated: generated,
746 finish_reason,
747 mtp_drafted: drafted,
748 mtp_accepted: accepted,
749 token_confidence: confidence,
750 traces,
751 })
752 }
753
754 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
758 let e = self.embed_single(next_token);
762 let mut cat = vec![0.0f32; 2 * self.hidden_size];
763 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
764 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
765 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
766 let mut x = vec![0.0f32; self.hidden_size];
767 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
768
769 let lw = &m.layer;
771 inference::rms_norm_into(&x, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
772 let attn = match &lw.attn {
773 AttnKind::Full {
774 wq,
775 wk,
776 wv,
777 wo,
778 q_norm,
779 k_norm,
780 output_gate,
781 bias,
782 } => {
783 let mut cfg = self.attn_cfg(position);
784 cfg.q_norm = q_norm.as_deref();
785 cfg.k_norm = k_norm.as_deref();
786 cfg.output_gate = *output_gate;
787 cfg.bias = bias
788 .as_ref()
789 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
790 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
791 }
792 AttnKind::Linear(_) | AttnKind::LinearGdn(_) => {
793 unreachable!("MTP block is full attention")
794 }
795 };
796 for (i, &a) in attn.iter().enumerate() {
797 x[i] += a;
798 }
799 inference::rms_norm_into(&x, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
800 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
801 for (i, &f) in ffn.iter().enumerate() {
802 x[i] += f;
803 }
804
805 inference::rms_norm_into(&x, &m.final_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
806 let mut lg = self.lm_head_forward(&self.ws.n1);
807 let draft = sampler::argmax(&lg);
808 attention::recycle_buf(&mut lg);
809 draft
810 }
811
812 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
816 let emb1 = self.embed_single(1);
817 let emb2 = self.embed_single(2);
818 let pos = self.kv_cache.seq_len();
819
820 let t0 = std::time::Instant::now();
821 for _ in 0..iters {
822 let _ = self.forward_layers(&emb1, pos, None);
823 let _ = self.forward_layers(&emb2, pos + 1, None);
824 for l in &mut self.kv_cache.layers {
825 l.truncate_last(2);
826 }
827 }
828 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
829
830 let t1 = std::time::Instant::now();
831 for _ in 0..iters {
832 let _ = self.forward_pair(&emb1, &emb2, pos);
833 for l in &mut self.kv_cache.layers {
834 l.truncate_last(2);
835 }
836 }
837 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
838 (singles_ms, pair_ms)
839 }
840
841 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
846 let mut h1 = emb1.to_vec();
847 let mut h2 = emb2.to_vec();
848 let (nh, nkv, hd, hs, rd, eps) = (
849 self.num_heads,
850 self.num_kv_heads,
851 self.head_dim,
852 self.hidden_size,
853 self.rotary_dim,
854 self.rms_eps,
855 );
856 let inv_freq = self.inv_freq.clone();
857 let pool = self.pool.clone();
858
859 for li in 0..self.num_layers {
860 let lw = &self.weights.layers[li];
861 inference::rms_norm_into(&h1, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
864 inference::rms_norm_into(&h2, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n2);
865
866 let (a1, a2) = match &lw.attn {
867 AttnKind::Linear(w) => {
868 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
869 let layer = &mut self.kv_cache.layers[li];
870 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
871 vmf_phase_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
872 }
873 AttnKind::LinearGdn(w) => {
874 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
875 let layer = &mut self.kv_cache.layers[li];
876 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
877 gdn_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
878 }
879 AttnKind::Full {
880 wq,
881 wk,
882 wv,
883 wo,
884 q_norm,
885 k_norm,
886 output_gate,
887 bias,
888 } => {
889 let cfg = QwenAttnCfg {
890 num_heads: nh,
891 num_kv_heads: nkv,
892 head_dim: hd,
893 hidden_size: hs,
894 position,
895 inv_freq: &inv_freq,
896 rotary_dim: rd,
897 q_norm: q_norm.as_deref(),
898 k_norm: k_norm.as_deref(),
899 output_gate: *output_gate,
900 bias: bias
901 .as_ref()
902 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
903 rms_eps: eps,
904 norm_style: self.norm_style,
905 pool: pool.as_deref(),
906 };
907 attention::qwen_attention_pair(
908 &self.ws.n1,
909 &self.ws.n2,
910 wq,
911 wk,
912 wv,
913 wo,
914 &mut self.kv_cache.layers[li],
915 &cfg,
916 )
917 }
918 };
919 for i in 0..self.hidden_size {
920 h1[i] += a1[i];
921 h2[i] += a2[i];
922 }
923 let (mut a1, mut a2) = (a1, a2);
924 attention::recycle_buf(&mut a1);
925 attention::recycle_buf(&mut a2);
926
927 let lw = &self.weights.layers[li];
928 inference::rms_norm_into(&h1, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
929 inference::rms_norm_into(&h2, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p2);
930 let (f1, f2) =
931 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
932 for i in 0..self.hidden_size {
933 h1[i] += f1[i];
934 h2[i] += f2[i];
935 }
936 let (mut f1, mut f2) = (f1, f2);
937 attention::recycle_buf(&mut f1);
938 attention::recycle_buf(&mut f2);
939 }
940 (h1, h2)
941 }
942
943 fn commit_linear_scratch(&mut self) {
945 for layer in &mut self.kv_cache.layers {
946 if !layer.linear_scratch.is_empty() {
947 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
948 layer.linear_scratch.clear();
949 }
950 }
951 }
952
953 pub fn forward_ids(
956 &mut self,
957 ids: &[u32],
958 task_mask: Option<&TaskMask>,
959 ) -> Result<Vec<f32>, String> {
960 if ids.is_empty() {
961 return Err("empty id sequence".to_string());
962 }
963 self.kv_cache.clear();
964 self.o1_begin();
965 let mut hidden = vec![0.0f32; self.hidden_size];
966 let mut pos = 0usize;
967 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
968 const CHUNK: usize = 48;
972 let hs = self.hidden_size;
973 while pos < ids.len() {
974 let end = (pos + CHUNK).min(ids.len());
975 let hb = self.prefill_batch(&ids[pos..end], pos);
976 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
977 pos = end;
978 }
979 }
980 if task_mask.is_none() {
981 while pos + 1 < ids.len() {
982 let e1 = self.embed_single(ids[pos]);
983 let e2 = self.embed_single(ids[pos + 1]);
984 let (_, h2) = self.forward_pair(&e1, &e2, pos);
985 self.commit_linear_scratch();
986 hidden = h2;
987 pos += 2;
988 }
989 }
990 while pos < ids.len() {
991 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
992 pos += 1;
993 }
994 self.o1_seal();
998 let normed = inference::rms_norm(
999 &hidden,
1000 &self.weights.final_norm,
1001 self.rms_eps,
1002 self.norm_style,
1003 );
1004 Ok(self.lm_head_forward(&normed))
1005 }
1006
1007 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1014 let (nll, cnt) = self.nll_ids_from(ids, 0);
1015 (nll / cnt.max(1) as f64).exp()
1016 }
1017
1018 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1027 self.kv_cache.clear();
1028 let mut nll = 0f64;
1029 let mut cnt = 0usize;
1030 if prefill_batched() {
1031 const CHUNK: usize = 128;
1037 const LM_SUB: usize = 32;
1038 let n = ids.len().saturating_sub(1);
1039 let hs = self.hidden_size;
1040 let rows = self.weights.lm_head.rows();
1041 let mut pos = 0usize;
1042 while pos < n {
1043 let end = (pos + CHUNK).min(n);
1044 let bsz = end - pos;
1045 let hb = self.prefill_batch(&ids[pos..end], pos);
1046 let mut k0 = 0usize;
1047 while k0 < bsz {
1048 let k1 = (k0 + LM_SUB).min(bsz);
1049 let sb = k1 - k0;
1050 if pos + k1 <= start {
1053 k0 = k1;
1054 continue;
1055 }
1056 let mut normed = vec![0.0f32; sb * hs];
1057 for k in 0..sb {
1058 let r = inference::rms_norm(
1059 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
1060 &self.weights.final_norm,
1061 self.rms_eps,
1062 self.norm_style,
1063 );
1064 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
1065 }
1066 let mut logits = vec![0.0f32; sb * rows];
1067 self.weights
1068 .lm_head
1069 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1070 for k in 0..sb {
1071 if pos + k0 + k < start {
1072 continue;
1073 }
1074 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1075 let target = ids[pos + k0 + k + 1] as usize;
1076 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1077 let lse: f64 = lg
1078 .iter()
1079 .map(|&v| ((v - max) as f64).exp())
1080 .sum::<f64>()
1081 .ln()
1082 + max as f64;
1083 nll += lse - lg[target] as f64;
1084 cnt += 1;
1085 }
1086 k0 = k1;
1087 }
1088 pos = end;
1089 }
1090 self.kv_cache.clear();
1091 return (nll, cnt);
1092 }
1093 for pos in 0..ids.len().saturating_sub(1) {
1094 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1095 if pos < start {
1096 continue;
1097 }
1098 let normed = inference::rms_norm(
1099 &hidden,
1100 &self.weights.final_norm,
1101 self.rms_eps,
1102 self.norm_style,
1103 );
1104 let logits = self.lm_head_forward(&normed);
1105 let target = ids[pos + 1] as usize;
1106 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1107 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1108 + max as f64;
1109 nll += lse - logits[target] as f64;
1110 cnt += 1;
1111 }
1112 self.kv_cache.clear();
1113 (nll, cnt)
1114 }
1115
1116 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1132 self.kv_cache.clear();
1133 self.o1_begin();
1134 let n = ids.len().saturating_sub(1);
1135 let p = prefill.min(n);
1136 let mut pos = 0usize;
1138 if prefill_batched() {
1139 const CHUNK: usize = 128;
1140 while pos < p {
1141 let end = (pos + CHUNK).min(p);
1142 let _ = self.prefill_batch(&ids[pos..end], pos);
1143 pos = end;
1144 }
1145 } else {
1146 while pos < p {
1147 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1148 pos += 1;
1149 }
1150 }
1151 self.o1_seal();
1152
1153 let mut nll = 0f64;
1154 let mut cnt = 0usize;
1155 for pos in p..n {
1156 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1157 let normed = inference::rms_norm(
1158 &hidden,
1159 &self.weights.final_norm,
1160 self.rms_eps,
1161 self.norm_style,
1162 );
1163 let logits = self.lm_head_forward(&normed);
1164 let target = ids[pos + 1] as usize;
1165 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1166 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1167 + max as f64;
1168 nll += lse - logits[target] as f64;
1169 cnt += 1;
1170 }
1171 self.kv_cache.clear();
1172 (nll, cnt)
1173 }
1174
1175 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1183 self.kv_cache.clear();
1184 let n = ids.len().saturating_sub(1);
1185 let mut correct = Vec::with_capacity(n);
1186 let mut pmax = Vec::with_capacity(n);
1187 for pos in 0..n {
1188 let emb = self.embed_single(ids[pos]);
1189 let hidden = self.forward_layers(&emb, pos, None);
1190 let normed = inference::rms_norm(
1191 &hidden,
1192 &self.weights.final_norm,
1193 self.rms_eps,
1194 self.norm_style,
1195 );
1196 let logits = self.lm_head_forward(&normed);
1197 let target = ids[pos + 1] as usize;
1198 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1199 for (i, &v) in logits.iter().enumerate() {
1200 if v > mval {
1201 mval = v;
1202 amax = i;
1203 }
1204 }
1205 correct.push(amax == target);
1206 let row: Vec<f32> = temps
1207 .iter()
1208 .map(|&t| {
1209 let tt = t.max(1e-3);
1210 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1211 1.0 / s.max(1e-12) })
1213 .collect();
1214 pmax.push(row);
1215 }
1216 self.kv_cache.clear();
1217 (correct, pmax)
1218 }
1219
1220 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1227 let mut router = match self.dyn_router.take() {
1228 Some(r) => r,
1229 None => return (self.ppl_ids(ids), 0),
1230 };
1231 router.reset();
1232 self.dyn_phi_seen = 0;
1233 let _ = self.set_active_skill(None);
1234
1235 self.kv_cache.clear();
1236 let mut nll = 0f64;
1237 let mut cnt = 0usize;
1238 for pos in 0..ids.len().saturating_sub(1) {
1239 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1240 let normed = inference::rms_norm(
1241 &hidden,
1242 &self.weights.final_norm,
1243 self.rms_eps,
1244 self.norm_style,
1245 );
1246 let logits = self.lm_head_forward(&normed);
1247 let target = ids[pos + 1] as usize;
1248 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1249 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1250 + max as f64;
1251 nll += lse - logits[target] as f64;
1252 cnt += 1;
1253 let phi = self.dyn_phi_ema.clone();
1255 if let Some(new_active) = router.step(&phi, pos) {
1256 let _ = self.set_active_skill(new_active);
1257 }
1258 }
1259 let switches = router.switches.len();
1260 let _ = self.set_active_skill(None);
1261 self.dyn_router = Some(router);
1262 self.kv_cache.clear();
1263 ((nll / cnt.max(1) as f64).exp(), switches)
1264 }
1265
1266 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1268 self.kv_cache.clear();
1269 let mut acc = vec![0f32; self.hidden_size];
1270 for (pos, &id) in ids.iter().enumerate() {
1271 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1272 for (a, v) in acc.iter_mut().zip(&h) {
1273 *a += v;
1274 }
1275 }
1276 let n = ids.len().max(1) as f32;
1277 for a in acc.iter_mut() {
1278 *a /= n;
1279 }
1280 self.kv_cache.clear();
1281 acc
1282 }
1283
1284 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1290 let b = ids.len();
1291 let hs = self.hidden_size;
1292 let mut h: Vec<f32> = Vec::with_capacity(b * hs);
1293 for &id in ids {
1294 h.extend_from_slice(&self.embed_single(id));
1295 }
1296 let (nh, nkv, hd, rd, eps) = (
1297 self.num_heads,
1298 self.num_kv_heads,
1299 self.head_dim,
1300 self.rotary_dim,
1301 self.rms_eps,
1302 );
1303 let inv_freq = self.inv_freq.clone();
1304 let pool = self.pool.clone();
1305 let norm_style = self.norm_style;
1306
1307 for li in 0..self.num_layers {
1308 crate::gpu::set_layer(li as i64); let lw = &self.weights.layers[li];
1310 match &lw.attn {
1312 AttnKind::LinearGdn(w) => {
1313 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1315 let mut normed = vec![0.0f32; b * hs];
1316 for bi in 0..b {
1317 let r = inference::rms_norm(
1318 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1319 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1320 }
1321 let attn = crate::linear_core::gdn_forward_batch(
1322 &normed, b, w, &cfg,
1323 &mut self.kv_cache.layers[li].linear_state,
1324 pool.as_deref(),
1325 );
1326 for (dst, &a) in h.iter_mut().zip(&attn) {
1327 *dst += a;
1328 }
1329 }
1330 AttnKind::Full {
1331 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
1332 } => {
1333 let mut normed = vec![0.0f32; b * hs];
1337 for bi in 0..b {
1338 inference::rms_norm_into(
1339 &h[bi * hs..(bi + 1) * hs],
1340 &lw.input_norm,
1341 eps,
1342 norm_style,
1343 &mut normed[bi * hs..(bi + 1) * hs],
1344 );
1345 }
1346 let cfg = QwenAttnCfg {
1347 num_heads: nh,
1348 num_kv_heads: nkv,
1349 head_dim: hd,
1350 hidden_size: hs,
1351 position: start_pos,
1352 inv_freq: &inv_freq,
1353 rotary_dim: rd,
1354 q_norm: q_norm.as_deref(),
1355 k_norm: k_norm.as_deref(),
1356 output_gate: *output_gate,
1357 bias: bias.as_ref().map(|(a, b, c)| {
1358 (a.as_slice(), b.as_slice(), c.as_slice())
1359 }),
1360 rms_eps: eps,
1361 norm_style,
1362 pool: pool.as_deref(),
1363 };
1364 let attn = attention::qwen_attention_batch(
1365 &normed, b, wq, wk, wv, wo,
1366 &mut self.kv_cache.layers[li], &cfg);
1367 for (dst, &a) in h.iter_mut().zip(&attn) {
1368 *dst += a;
1369 }
1370 }
1371 AttnKind::Linear(w) => {
1372 for bi in 0..b {
1373 let normed = inference::rms_norm(
1374 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1375 vmf_phase_forward(
1376 &normed, w,
1377 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
1378 &mut self.kv_cache.layers[li].linear_state,
1379 pool.as_deref(),
1380 )
1381 .iter()
1382 .enumerate()
1383 .for_each(|(i, &a)| h[bi * hs + i] += a);
1384 }
1385 }
1386 }
1387
1388 let lw = &self.weights.layers[li];
1390 let mut post = vec![0.0f32; b * hs];
1391 for bi in 0..b {
1392 let r = inference::rms_norm(
1393 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
1394 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1395 }
1396 let ffn = match &lw.ffn {
1397 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
1398 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
1399 };
1400 for (dst, &f) in h.iter_mut().zip(&ffn) {
1401 *dst += f;
1402 }
1403 }
1404 crate::gpu::set_layer(-1); h
1406 }
1407
1408 fn embed_single(&self, id: u32) -> Vec<f32> {
1410 let mut out = vec![0.0f32; self.hidden_size];
1411 if (id as usize) < self.weights.embed_tokens.rows() {
1412 self.weights.embed_tokens.row_f32(id as usize, &mut out);
1413 }
1414 out
1415 }
1416
1417 fn forward_layers(
1419 &mut self,
1420 hidden: &[f32],
1421 position: usize,
1422 task_mask: Option<&TaskMask>,
1423 ) -> Vec<f32> {
1424 self.forward_layers_upto(hidden, position, task_mask, None)
1425 }
1426
1427 fn forward_layers_upto(
1429 &mut self,
1430 hidden: &[f32],
1431 position: usize,
1432 task_mask: Option<&TaskMask>,
1433 upto: Option<usize>,
1434 ) -> Vec<f32> {
1435 let mut h = hidden.to_vec();
1436 let (nh, nkv, hd, hs, rd, eps) = (
1439 self.num_heads,
1440 self.num_kv_heads,
1441 self.head_dim,
1442 self.hidden_size,
1443 self.rotary_dim,
1444 self.rms_eps,
1445 );
1446 let inv_freq = self.inv_freq.clone();
1447 let pool = self.pool.clone();
1448
1449 for li in 0..self.num_layers {
1450 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
1452 if li > u {
1453 break;
1454 }
1455 }
1456 if let Some(mask) = task_mask {
1457 if !mask.layer_alive(li) {
1458 continue; }
1460 }
1461
1462 let lw = &self.weights.layers[li];
1463 inference::rms_norm_into(&h, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1466
1467 let attn_out = match &lw.attn {
1468 AttnKind::Linear(w) => {
1469 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1470 vmf_phase_forward(
1471 &self.ws.n1,
1472 w,
1473 &cfg,
1474 &mut self.kv_cache.layers[li].linear_state,
1475 self.pool.as_deref(),
1476 )
1477 }
1478 AttnKind::LinearGdn(w) => {
1479 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1480 gdn_forward(
1481 &self.ws.n1,
1482 w,
1483 &cfg,
1484 &mut self.kv_cache.layers[li].linear_state,
1485 self.pool.as_deref(),
1486 )
1487 }
1488 AttnKind::Full {
1489 wq,
1490 wk,
1491 wv,
1492 wo,
1493 q_norm,
1494 k_norm,
1495 output_gate,
1496 bias,
1497 } if self.kv_cache.layers[li].o1_sealed() => {
1498 let cfg = QwenAttnCfg {
1501 num_heads: nh,
1502 num_kv_heads: nkv,
1503 head_dim: hd,
1504 hidden_size: hs,
1505 position,
1506 inv_freq: &inv_freq,
1507 rotary_dim: rd,
1508 q_norm: q_norm.as_deref(),
1509 k_norm: k_norm.as_deref(),
1510 output_gate: *output_gate,
1511 bias: bias
1512 .as_ref()
1513 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1514 rms_eps: eps,
1515 norm_style: self.norm_style,
1516 pool: pool.as_deref(),
1517 };
1518 attention::qwen_attention_nystrom(
1519 &self.ws.n1,
1520 wq,
1521 wk,
1522 wv,
1523 wo,
1524 &mut self.kv_cache.layers[li],
1525 &cfg,
1526 )
1527 }
1528 AttnKind::Full {
1529 wq,
1530 wk,
1531 wv,
1532 wo,
1533 q_norm,
1534 k_norm,
1535 output_gate,
1536 bias,
1537 } => {
1538 let masked = task_mask
1539 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
1540 .unwrap_or(false);
1541 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
1542 match (masked, f32_view) {
1543 (true, (Some(q), Some(k), Some(v), Some(o))) => {
1546 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
1547 attention::multi_head_attention(
1548 &self.ws.n1,
1549 q,
1550 k,
1551 v,
1552 o,
1553 &mut self.kv_cache.layers[li],
1554 self.num_heads,
1555 self.num_kv_heads,
1556 self.head_dim,
1557 self.hidden_size,
1558 position,
1559 &active_heads,
1560 &self.inv_freq,
1561 )
1562 }
1563 (masked, _) => {
1564 if masked {
1565 tracing::warn!(
1566 "layer {li}: head mask on quantized weights not \
1567 supported yet — executing dense"
1568 );
1569 }
1570 let cfg = QwenAttnCfg {
1571 num_heads: nh,
1572 num_kv_heads: nkv,
1573 head_dim: hd,
1574 hidden_size: hs,
1575 position,
1576 inv_freq: &inv_freq,
1577 rotary_dim: rd,
1578 q_norm: q_norm.as_deref(),
1579 k_norm: k_norm.as_deref(),
1580 output_gate: *output_gate,
1581 bias: bias
1582 .as_ref()
1583 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1584 rms_eps: eps,
1585 norm_style: self.norm_style,
1586 pool: pool.as_deref(),
1587 };
1588 attention::qwen_attention(
1589 &self.ws.n1,
1590 wq,
1591 wk,
1592 wv,
1593 wo,
1594 &mut self.kv_cache.layers[li],
1595 &cfg,
1596 )
1597 }
1598 }
1599 }
1600 };
1601 for (i, &a) in attn_out.iter().enumerate() {
1602 h[i] += a;
1603 }
1604 let mut attn_out = attn_out;
1605 attention::recycle_buf(&mut attn_out);
1606
1607 let lw = &self.weights.layers[li];
1608 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1609 let post_normed = &self.ws.p1;
1610
1611 let ffn_masked = task_mask
1612 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
1613 .unwrap_or(false);
1614 let f32_ffn = match &lw.ffn {
1617 FfnKind::Dense(d) => {
1618 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
1619 }
1620 FfnKind::Moe(_) => (None, None, None),
1621 };
1622 let ffn_out = match (ffn_masked, f32_ffn) {
1623 (true, (Some(g), Some(u), Some(d))) => {
1624 let active = task_mask.unwrap().ffn_active_indices(li);
1625 inference::sparse_ffn_forward(
1626 &post_normed,
1627 g,
1628 u,
1629 d,
1630 self.hidden_size,
1631 self.intermediate_size,
1632 &active,
1633 self.pool.as_deref(),
1634 )
1635 }
1636 (true, _) => match &lw.ffn {
1640 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
1641 let active = task_mask.unwrap().ffn_active_indices(li);
1642 sparse_ffn_quant(
1643 d,
1644 &post_normed,
1645 &active,
1646 self.hidden_size,
1647 self.pool.as_deref(),
1648 )
1649 }
1650 FfnKind::Dense(d) => {
1655 let active = task_mask.unwrap().ffn_active_indices(li);
1656 let (gf, uf, df) = dequant_dense_f32(d);
1657 inference::sparse_ffn_forward(
1658 &post_normed,
1659 &gf,
1660 &uf,
1661 &df,
1662 self.hidden_size,
1663 self.intermediate_size,
1664 &active,
1665 self.pool.as_deref(),
1666 )
1667 }
1668 FfnKind::Moe(_) => {
1669 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
1672 }
1673 },
1674 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
1675 };
1676 for (i, &f) in ffn_out.iter().enumerate() {
1677 h[i] += f;
1678 }
1679 let mut ffn_out = ffn_out;
1680 attention::recycle_buf(&mut ffn_out);
1681
1682 if self.dyn_phi_layer == Some(li) {
1686 self.update_dyn_phi(&h);
1687 }
1688 }
1689 crate::gpu::set_layer(-1); h
1692 }
1693
1694 fn update_dyn_phi(&mut self, h: &[f32]) {
1697 const A: f32 = 0.2;
1698 if self.dyn_phi_ema.len() != h.len() {
1699 self.dyn_phi_ema = vec![0.0; h.len()];
1700 self.dyn_phi_seen = 0;
1701 }
1702 if self.dyn_phi_seen == 0 {
1703 self.dyn_phi_ema.copy_from_slice(h);
1704 } else {
1705 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
1706 *e = (1.0 - A) * *e + A * v;
1707 }
1708 }
1709 self.dyn_phi_seen += 1;
1710 }
1711
1712 pub fn dyn_phi(&self) -> &[f32] {
1714 &self.dyn_phi_ema
1715 }
1716
1717 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
1719 self.dyn_phi_layer = layer;
1720 self.dyn_phi_ema.clear();
1721 self.dyn_phi_seen = 0;
1722 }
1723
1724 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
1726 let Some(model) = &self.model else { return Vec::new() };
1727 model
1728 .header
1729 .skills
1730 .iter()
1731 .enumerate()
1732 .filter_map(|(i, sk)| {
1733 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
1734 let sel = sk.selection.as_ref()?;
1735 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
1736 })
1737 .collect()
1738 }
1739
1740 pub fn active_skill(&self) -> Option<usize> {
1742 self.dyn_active
1743 }
1744
1745 pub fn enable_dynamic_routing(&mut self) -> usize {
1750 use crate::swarm::{DynRouter, RoutableSkill};
1751 let Some(model) = self.model.clone() else { return 0 };
1752 if self.dyn_blend_loaded {
1755 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
1756 return 0;
1757 }
1758 if let Some(a) = self.dyn_active {
1762 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
1763 tracing::warn!(
1764 "loaded skill is not FFN-eligible — dynamic routing unavailable"
1765 );
1766 return 0;
1767 }
1768 }
1769 let hidden = self.hidden_size;
1770 let mut skills = Vec::new();
1771 for (idx, id, _phi) in self.dynamic_skills() {
1772 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
1773 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
1774 skills.push(rs);
1775 }
1776 }
1777 }
1778 if skills.is_empty() {
1779 return 0;
1780 }
1781 let phi = skills[0].phi_layer;
1783 if skills.iter().any(|s| s.phi_layer != phi) {
1784 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
1785 }
1786 let n = skills.len();
1787 self.set_dyn_phi_layer(Some(phi));
1788 self.dyn_router = Some(DynRouter::new(skills));
1789 n
1790 }
1791
1792 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
1794 self.dyn_router
1795 .as_ref()
1796 .map(|r| r.switches.clone())
1797 .unwrap_or_default()
1798 }
1799
1800 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
1803 let rows = self.weights.lm_head.rows();
1804 let mut logits = attention::take_buf(rows.min(self.vocab_size));
1805 self.weights
1806 .lm_head
1807 .matvec(hidden, &mut logits, self.pool.as_deref());
1808 logits.resize(self.vocab_size, 0.0);
1809 logits
1810 }
1811
1812 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
1817 self.kv_cache.clear();
1818 let mut hidden = vec![0.0f32; self.hidden_size];
1819 for (pos, &id) in ids.iter().enumerate() {
1820 let emb = self.embed_single(id);
1821 hidden = self.forward_layers(&emb, pos, task_mask);
1822 }
1823 inference::rms_norm_into(
1824 &hidden,
1825 &self.weights.final_norm,
1826 self.rms_eps,
1827 self.norm_style,
1828 &mut self.ws.n1,
1829 );
1830 self.lm_head_forward(&self.ws.n1)
1831 }
1832}
1833
1834pub fn create_test_pipeline(
1836 hidden_size: usize,
1837 intermediate_size: usize,
1838 num_heads: usize,
1839 num_kv_heads: usize,
1840 head_dim: usize,
1841 num_layers: usize,
1842 vocab_size: usize,
1843) -> Pipeline {
1844 let synth = |n: usize, salt: usize| -> Vec<f32> {
1847 (0..n)
1848 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
1849 .collect()
1850 };
1851 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
1852 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
1853 };
1854 let layer_weights: Vec<LayerWeights> = (0..num_layers)
1855 .map(|li| LayerWeights {
1856 input_norm: vec![1.0; hidden_size],
1857 post_norm: vec![1.0; hidden_size],
1858 ffn: FfnKind::Dense(DenseFfn {
1859 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
1860 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
1861 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
1862 }),
1863 attn: AttnKind::Full {
1864 bias: None,
1865 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
1866 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
1867 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
1868 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
1869 q_norm: None,
1870 k_norm: None,
1871 output_gate: false,
1872 },
1873 })
1874 .collect();
1875
1876 Pipeline::new(
1877 Tokenizer::byte_level(),
1878 PipelineWeights {
1879 embed_tokens: qt(vocab_size, hidden_size, 100),
1880 layers: layer_weights,
1881 lm_head: qt(vocab_size, hidden_size, 200),
1882 final_norm: vec![1.0; hidden_size],
1883 },
1884 hidden_size,
1885 intermediate_size,
1886 num_heads,
1887 num_kv_heads,
1888 head_dim,
1889 num_layers,
1890 vocab_size,
1891 1e-6,
1892 10_000.0,
1893 NormStyle::Qwen,
1894 4096,
1895 SamplerConfig {
1896 seed: Some(42),
1897 ..Default::default()
1898 },
1899 )
1900}
1901
1902fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
1905 let inter = d.gate_proj.rows();
1906 let hidden = d.down_proj.rows();
1907 let mut g = vec![0.0f32; b * inter];
1908 d.gate_proj.matmat(xs, b, &mut g, pool);
1909 let mut u = vec![0.0f32; b * inter];
1910 d.up_proj.matmat(xs, b, &mut u, pool);
1911 for i in 0..b * inter {
1912 g[i] = inference::silu(g[i]) * u[i];
1913 }
1914 let mut out = vec![0.0f32; b * hidden];
1915 d.down_proj.matmat(&g, b, &mut out, pool);
1916 out
1917}
1918
1919fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
1923 let ne = m.experts.len();
1924 let mut logits = vec![0.0f32; b * ne];
1925 m.router.matmat(xs, b, &mut logits, pool);
1926
1927 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
1930 {
1931 let mut st = m.stats.borrow_mut();
1932 if st.len() < ne {
1933 st.resize(ne, 0);
1934 }
1935 for bi in 0..b {
1936 let lg = &logits[bi * ne..(bi + 1) * ne];
1937 let mx = lg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1938 let mut p: Vec<f32> = lg.iter().map(|&l| (l - mx).exp()).collect();
1939 let sum: f32 = p.iter().sum();
1940 for v in &mut p {
1941 *v /= sum;
1942 }
1943 let mut order: Vec<usize> = (0..ne).collect();
1944 order.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y)));
1945 order.truncate(m.top_k);
1946 let wsum: f32 = if m.norm_topk_prob {
1947 order.iter().map(|&e| p[e]).sum()
1948 } else {
1949 1.0
1950 };
1951 for &e in &order {
1952 st[e] += 1;
1953 assign[e].push((bi, p[e] / wsum));
1954 }
1955 }
1956 }
1957
1958 let mut out = vec![0.0f32; b * hidden];
1959 let cols = m.experts[0].gate_proj.cols();
1960 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
1961 let sb = list.len();
1962 let mut sub = vec![0.0f32; sb * cols];
1963 for (k, &(bi, _)) in list.iter().enumerate() {
1964 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
1965 }
1966 let eo = dense_ffn_batch(d, &sub, sb, pool);
1967 for (k, &(bi, w)) in list.iter().enumerate() {
1968 for i in 0..hidden {
1969 out[bi * hidden + i] += w * eo[k * hidden + i];
1970 }
1971 }
1972 };
1973 for e in 0..ne {
1974 if !assign[e].is_empty() {
1975 run_expert(&m.experts[e], &assign[e]);
1976 }
1977 }
1978 if let Some((se, gate)) = &m.shared {
1979 let mut gl = vec![0.0f32; b];
1980 gate.matmat(xs, b, &mut gl, pool);
1981 let all: Vec<(usize, f32)> = (0..b)
1982 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
1983 .collect();
1984 run_expert(se, &all);
1985 }
1986 out
1987}
1988
1989thread_local! {
1990 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
1994 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
1995}
1996
1997fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
1999 let inter = d.gate_proj.rows();
2000 FFN_SCRATCH.with(|s| {
2001 let mut s = s.borrow_mut();
2002 let [g, u, ..] = &mut *s;
2003 g.resize(inter, 0.0);
2004 u.resize(inter, 0.0);
2005 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
2007 for i in 0..inter {
2008 g[i] = inference::silu(g[i]) * u[i];
2009 }
2010 let mut out = attention::take_buf(d.down_proj.rows());
2011 d.down_proj.matvec(g, &mut out, pool);
2012 out
2013 })
2014}
2015
2016fn sparse_ffn_quant(
2023 d: &DenseFfn,
2024 x: &[f32],
2025 active: &[u16],
2026 hidden: usize,
2027 pool: Option<&Pool>,
2028) -> Vec<f32> {
2029 let n = active.len();
2030 let inter = d.gate_proj.rows();
2031 let mut act = vec![0.0f32; n];
2032 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
2035 let compute = |ai: usize| -> f32 {
2036 let idx = active[ai] as usize;
2037 if idx >= inter {
2038 return 0.0; }
2040 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
2041 let gate = d.gate_proj.row_dot(idx, x, &mut s);
2042 let up = d.up_proj.row_dot(idx, x, &mut s);
2043 inference::silu(gate) * up
2044 };
2045 match pool {
2046 Some(p) if n >= 256 => {
2047 let ptr = SendMut(act.as_mut_ptr());
2048 p.run(&|widx, nw| {
2049 let chunk = n.div_ceil(nw);
2050 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
2051 for ai in s..e {
2052 unsafe { *ptr.at(ai) = compute(ai) };
2053 }
2054 });
2055 }
2056 _ => {
2057 for (ai, a) in act.iter_mut().enumerate() {
2058 *a = compute(ai);
2059 }
2060 }
2061 }
2062 let mut out = vec![0.0f32; hidden];
2064 for (ai, &idx) in active.iter().enumerate() {
2065 let w = act[ai];
2066 if w.abs() >= 1e-12 && (idx as usize) < inter {
2067 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
2068 }
2069 }
2070 out
2071}
2072
2073#[doc(hidden)]
2075pub fn sparse_ffn_quant_for_test(
2076 d: &DenseFfn,
2077 x: &[f32],
2078 active: &[u16],
2079 hidden: usize,
2080) -> Vec<f32> {
2081 sparse_ffn_quant(d, x, active, hidden, None)
2082}
2083
2084fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
2088 let deq = |t: &QTensor| -> Vec<f32> {
2089 let (rows, cols) = (t.rows(), t.cols());
2090 let mut out = vec![0.0f32; rows * cols];
2091 for r in 0..rows {
2092 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
2093 }
2094 out
2095 };
2096 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
2097}
2098
2099struct SendMut(*mut f32);
2101unsafe impl Send for SendMut {}
2102unsafe impl Sync for SendMut {}
2103impl SendMut {
2104 #[inline]
2105 #[allow(clippy::mut_from_ref)]
2108 unsafe fn at(&self, i: usize) -> &mut f32 {
2109 unsafe { &mut *self.0.add(i) }
2110 }
2111}
2112
2113fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2117 let ne = m.experts.len();
2118 let mut logits = vec![0.0f32; ne];
2119 m.router.matvec(x, &mut logits, pool);
2120 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2121 let mut p: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
2122 let s: f32 = p.iter().sum();
2123 for v in &mut p {
2124 *v /= s;
2125 }
2126 let mut idx: Vec<usize> = (0..ne).collect();
2127 idx.sort_unstable_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap().then(a.cmp(&b)));
2129 idx.truncate(m.top_k);
2130 let wsum: f32 = if m.norm_topk_prob {
2131 idx.iter().map(|&e| p[e]).sum()
2132 } else {
2133 1.0
2134 };
2135 {
2136 let mut st = m.stats.borrow_mut();
2137 if st.len() < ne {
2138 st.resize(ne, 0);
2139 }
2140 for &e in &idx {
2141 st[e] += 1;
2142 }
2143 }
2144 if crate::gpu::enabled_here() {
2147 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
2148 return out;
2149 }
2150 }
2151
2152 let mut out = attention::take_buf(x.len());
2153 for &e in &idx {
2154 let mut eo = dense_ffn(&m.experts[e], x, pool);
2155 let w = p[e] / wsum;
2156 for i in 0..out.len() {
2157 out[i] += w * eo[i];
2158 }
2159 attention::recycle_buf(&mut eo);
2160 }
2161 if let Some((se, gate)) = &m.shared {
2162 let mut so = dense_ffn(se, x, pool);
2163 let mut gl = vec![0.0f32; 1];
2164 gate.matvec(x, &mut gl, pool);
2165 let g = 1.0 / (1.0 + (-gl[0]).exp());
2166 for i in 0..out.len() {
2167 out[i] += g * so[i];
2168 }
2169 attention::recycle_buf(&mut so);
2170 }
2171 out
2172}
2173
2174fn moe_ffn_gpu(
2177 m: &MoeFfn,
2178 x: &[f32],
2179 idx: &[usize],
2180 p: &[f32],
2181 wsum: f32,
2182 pool: Option<&Pool>,
2183) -> Option<Vec<f32>> {
2184 use crate::gpu::MoeJob;
2185 use crate::qtensor::prescale;
2186 use cortiq_core::TensorDtype;
2187
2188 fn parts(
2189 t: &QTensor,
2190 ) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32])>
2191 {
2192 match t {
2193 QTensor::Mapped {
2194 model,
2195 idx,
2196 dtype: TensorDtype::Q8_2f,
2197 rows,
2198 cols,
2199 row_scale,
2200 col_field,
2201 ..
2202 } if !col_field.is_empty() => {
2203 Some((model, *idx, *rows, *cols, row_scale, col_field))
2204 }
2205 _ => None,
2206 }
2207 }
2208
2209 fn push<'a>(
2210 d: &'a DenseFfn,
2211 x: &[f32],
2212 w: f32,
2213 jobs: &mut Vec<MoeJob<'a>>,
2214 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
2215 ) -> Option<()> {
2216 let (gm, gi, gr, gc, grs, gcf) = parts(&d.gate_proj)?;
2217 let (_, ui, ur, uc, urs, ucf) = parts(&d.up_proj)?;
2218 let (_, di, dr, dc, drs, dcf) = parts(&d.down_proj)?;
2219 model_ref.get_or_insert_with(|| gm.clone());
2220 jobs.push(MoeJob {
2221 gate: (gi, gr, gc, grs),
2222 up: (ui, ur, uc, urs),
2223 down: (di, dr, dc, drs),
2224 xs_gate: prescale(x, gcf, TensorDtype::Q8_2f).into_owned(),
2225 xs_up: prescale(x, ucf, TensorDtype::Q8_2f).into_owned(),
2226 down_col: dcf,
2227 w,
2228 });
2229 Some(())
2230 }
2231
2232 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
2233 let mut model_ref = None;
2234 for &e in idx {
2235 push(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
2236 }
2237 if let Some((se, gate)) = &m.shared {
2238 let mut gl = vec![0.0f32; 1];
2239 gate.matvec(x, &mut gl, pool);
2240 let g = 1.0 / (1.0 + (-gl[0]).exp());
2241 push(se, x, g, &mut jobs, &mut model_ref)?;
2242 }
2243 let model = model_ref?;
2244 let hidden = jobs[0].down.1;
2245 let mut out = vec![0.0f32; hidden];
2246 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
2247}
2248
2249fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2251 match ffn {
2252 FfnKind::Dense(d) => dense_ffn(d, x, pool),
2253 FfnKind::Moe(m) => moe_ffn(m, x, pool),
2254 }
2255}
2256
2257fn ffn_forward_pair(
2261 ffn: &FfnKind,
2262 x1: &[f32],
2263 x2: &[f32],
2264 pool: Option<&Pool>,
2265) -> (Vec<f32>, Vec<f32>) {
2266 let d = match ffn {
2267 FfnKind::Dense(d) => d,
2268 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
2269 };
2270 let inter = d.gate_proj.rows();
2271 FFN_SCRATCH.with(|s| {
2272 let mut s = s.borrow_mut();
2273 let [g1, g2, u1, u2] = &mut *s;
2274 g1.resize(inter, 0.0);
2275 g2.resize(inter, 0.0);
2276 u1.resize(inter, 0.0);
2277 u2.resize(inter, 0.0);
2278 QTensor::matvec2_many(
2281 [&d.gate_proj, &d.up_proj],
2282 x1,
2283 x2,
2284 [g1.as_mut_slice(), u1.as_mut_slice()],
2285 [g2.as_mut_slice(), u2.as_mut_slice()],
2286 pool,
2287 );
2288 for i in 0..inter {
2289 g1[i] = inference::silu(g1[i]) * u1[i];
2290 g2[i] = inference::silu(g2[i]) * u2[i];
2291 }
2292 let mut o1 = attention::take_buf(d.down_proj.rows());
2293 let mut o2 = attention::take_buf(d.down_proj.rows());
2294 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
2295 (o1, o2)
2296 })
2297}
2298
2299#[cfg(test)]
2300mod tests {
2301 use super::*;
2302
2303 #[test]
2309 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
2310 let (hidden, inter) = (16usize, 40usize);
2311 let synth = |n: usize, salt: usize| -> Vec<f32> {
2312 (0..n)
2313 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
2314 .collect()
2315 };
2316 let d = DenseFfn {
2317 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
2318 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
2319 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
2320 };
2321 let x = synth(hidden, 9);
2322 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
2324
2325 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
2326
2327 let mut g = vec![0.0f32; inter];
2329 d.gate_proj.matvec(&x, &mut g, None);
2330 let mut u = vec![0.0f32; inter];
2331 d.up_proj.matvec(&x, &mut u, None);
2332 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
2333 for i in 0..inter {
2334 g[i] = if act_set.contains(&(i as u16)) {
2335 inference::silu(g[i]) * u[i]
2336 } else {
2337 0.0
2338 };
2339 }
2340 let mut reference = vec![0.0f32; hidden];
2341 d.down_proj.matvec(&g, &mut reference, None);
2342
2343 let max_d = sparse
2344 .iter()
2345 .zip(&reference)
2346 .map(|(a, b)| (a - b).abs())
2347 .fold(0.0f32, f32::max);
2348 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
2349 }
2350
2351 fn attach_test_mtp(p: &mut Pipeline) {
2353 let (h, inter, heads, kv, hd) = (
2354 p.hidden_size,
2355 p.intermediate_size,
2356 p.num_heads,
2357 p.num_kv_heads,
2358 p.head_dim,
2359 );
2360 let synth = |n: usize, salt: usize| -> Vec<f32> {
2361 (0..n)
2362 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
2363 .collect()
2364 };
2365 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2366 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2367 };
2368 p.mtp = Some(MtpModule {
2369 enorm: vec![1.0; h],
2370 hnorm: vec![1.0; h],
2371 eh_proj: qt(h, 2 * h, 301),
2372 layer: LayerWeights {
2373 input_norm: vec![1.0; h],
2374 post_norm: vec![1.0; h],
2375 ffn: FfnKind::Dense(DenseFfn {
2376 gate_proj: qt(inter, h, 315),
2377 up_proj: qt(inter, h, 316),
2378 down_proj: qt(h, inter, 317),
2379 }),
2380 attn: AttnKind::Full {
2381 bias: None,
2382 wq: qt(heads * hd, h, 311),
2383 wk: qt(kv * hd, h, 312),
2384 wv: qt(kv * hd, h, 313),
2385 wo: qt(h, heads * hd, 314),
2386 q_norm: None,
2387 k_norm: None,
2388 output_gate: false,
2389 },
2390 },
2391 final_norm: vec![1.0; h],
2392 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
2393 });
2394 }
2395
2396 #[test]
2397 fn speculative_equals_vanilla_greedy() {
2398 let run = |spec: bool| {
2399 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2400 p.sampler_config.temperature = 0.0;
2401 attach_test_mtp(&mut p);
2402 p.speculative = spec;
2403 let r = p.generate("abcdef", 12, None, None).unwrap();
2404 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
2405 };
2406 let (vanilla, d0, _) = run(false);
2407 let (spec, d1, a1) = run(true);
2408 assert_eq!(d0, 0, "vanilla path must not draft");
2409 assert!(d1 > 0, "speculative path must draft");
2410 assert_eq!(
2411 vanilla, spec,
2412 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
2413 );
2414 }
2415
2416 #[test]
2417 fn speculative_accepts_constant_oracle() {
2418 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2419 p.sampler_config.temperature = 0.0;
2420 p.sampler_config.repetition_penalty = 1.0;
2421 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
2424 attach_test_mtp(&mut p);
2425 p.speculative = true;
2426 let r = p.generate("abcd", 10, None, None).unwrap();
2427 assert!(r.mtp_drafted > 0);
2428 assert_eq!(
2429 r.mtp_accepted, r.mtp_drafted,
2430 "constant logits → every draft accepted"
2431 );
2432 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
2435 }
2436
2437 #[test]
2438 fn empty_prompt_is_an_error_not_a_panic() {
2439 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2440 let r = p.generate("", 4, None, None);
2441 assert!(r.is_err(), "empty prompt must be a clean error");
2442 }
2443
2444 #[test]
2445 fn every_token_enters_kv_exactly_once() {
2446 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2447 p.sampler_config.temperature = 0.0;
2449 let r = p.generate("abc", 2, None, None).unwrap();
2450 assert_eq!(r.prompt_tokens, 3);
2451 assert_eq!(
2455 p.kv_cache.seq_len(),
2456 3 + r.tokens_generated - 1,
2457 "each token must be cached exactly once (v1 cached the last prompt token twice)"
2458 );
2459 }
2460
2461 #[test]
2462 fn generation_is_reproducible_with_seed() {
2463 let run = || {
2464 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2465 p.generate("hello", 8, None, None).unwrap().token_ids
2466 };
2467 assert_eq!(run(), run());
2468 }
2469
2470 #[test]
2471 fn eviction_bounds_the_cache() {
2472 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2473 p.kv_cache.max_seq_len = 6;
2474 p.sampler_config.temperature = 0.0;
2475 let _ = p.generate("abcd", 12, None, None).unwrap();
2476 assert!(
2477 p.kv_cache.seq_len() <= 6 + 1,
2478 "cache must stay bounded by max_seq_len (got {})",
2479 p.kv_cache.seq_len()
2480 );
2481 }
2482
2483 #[test]
2484 fn confidence_matches_tokens_and_is_a_probability() {
2485 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2486 p.sampler_config.temperature = 0.0;
2487 p.sampler_config.repetition_penalty = 1.0;
2488 let r = p.generate("abcd", 10, None, None).unwrap();
2489 assert_eq!(
2490 r.token_confidence.len(),
2491 r.token_ids.len(),
2492 "one confidence per emitted token"
2493 );
2494 for &c in &r.token_confidence {
2495 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
2496 }
2497 let logits = [1.0f32, 3.0, 0.5, 3.0];
2499 let p0 = top1_prob_t(&logits, 1, 1.0);
2500 let p1 = top1_prob_t(&logits, 3, 1.0);
2501 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
2502 assert!(p0 > 0.0 && p0 < 1.0);
2503 let sharp = top1_prob_t(&logits, 1, 1.0);
2505 let soft = top1_prob_t(&logits, 1, 2.0);
2506 assert!(soft < sharp, "higher temperature lowers peak confidence");
2507 }
2508
2509 #[test]
2510 fn trace_is_opt_in_and_parallels_the_output() {
2511 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2513 p.sampler_config.temperature = 0.0;
2514 p.sampler_config.repetition_penalty = 1.0;
2515 let r = p.generate("abcd", 10, None, None).unwrap();
2516 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
2517
2518 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2520 p.sampler_config.temperature = 0.0;
2521 p.sampler_config.repetition_penalty = 1.0;
2522 p.set_trace(true);
2523 let r = p.generate("abcd", 10, None, None).unwrap();
2524 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
2525 for (i, tr) in r.traces.iter().enumerate() {
2526 assert_eq!(tr.t, i, "trace index is sequential");
2527 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
2528 assert_eq!(
2529 tr.confidence, r.token_confidence[i],
2530 "trace confidence matches the confidence channel"
2531 );
2532 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
2534 }
2535 }
2536
2537 #[test]
2538 fn explain_prefill_logits_match_greedy_first_token() {
2539 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2543 p.sampler_config.temperature = 0.0;
2544 p.sampler_config.repetition_penalty = 1.0;
2545 let ids = p.tokenizer.encode("abcd");
2546 let logits = p.prefill_next_logits(&ids, None);
2547 let argmax = logits
2548 .iter()
2549 .enumerate()
2550 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2551 .unwrap()
2552 .0 as u32;
2553 let r = p.generate("abcd", 1, None, None).unwrap();
2554 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
2555 }
2556}