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 trace: bool,
87 calib_temp: f32,
90}
91
92pub struct PipelineWeights {
97 pub embed_tokens: QTensor,
99 pub layers: Vec<LayerWeights>,
101 pub lm_head: QTensor,
103 pub final_norm: Vec<f32>,
105}
106
107pub struct LayerWeights {
109 pub input_norm: Vec<f32>,
110 pub post_norm: Vec<f32>,
111 pub ffn: FfnKind,
112 pub attn: AttnKind,
113}
114
115pub struct DenseFfn {
117 pub gate_proj: QTensor,
118 pub up_proj: QTensor,
119 pub down_proj: QTensor,
120}
121
122pub enum FfnKind {
125 Dense(DenseFfn),
126 Moe(MoeFfn),
130}
131
132pub struct MoeFfn {
133 pub router: QTensor,
135 pub experts: Vec<DenseFfn>,
136 pub top_k: usize,
137 pub norm_topk_prob: bool,
138 pub shared: Option<(DenseFfn, QTensor)>,
140 pub stats: std::cell::RefCell<Vec<u64>>,
144}
145
146pub enum AttnKind {
149 Full {
151 wq: QTensor,
152 wk: QTensor,
153 wv: QTensor,
154 wo: QTensor,
155 q_norm: Option<Vec<f32>>,
156 k_norm: Option<Vec<f32>>,
157 output_gate: bool,
158 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
160 },
161 Linear(VmfPhaseWeights),
163 LinearGdn(GdnWeights),
165}
166
167pub struct MtpModule {
172 pub enorm: Vec<f32>,
173 pub hnorm: Vec<f32>,
174 pub eh_proj: QTensor,
176 pub layer: LayerWeights,
177 pub final_norm: Vec<f32>,
178 pub kv: crate::kv_cache::LayerKvCache,
179}
180
181pub struct GenerateResult {
183 pub text: String,
184 pub token_ids: Vec<u32>,
185 pub prompt_tokens: usize,
186 pub tokens_generated: usize,
187 pub finish_reason: String,
188 pub mtp_drafted: usize,
190 pub mtp_accepted: usize,
191 pub token_confidence: Vec<f32>,
196 pub traces: Vec<TokenTrace>,
199}
200
201#[derive(Clone, Debug)]
206pub struct TokenTrace {
207 pub t: usize,
209 pub token_id: u32,
211 pub confidence: f32,
213 pub active_skill: Option<String>,
215 pub recon: Option<f32>,
219 pub switched: bool,
222}
223
224fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
229 let t = if temp > 1e-3 { temp } else { 1.0 };
230 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
231 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
232 if sum > 0.0 {
233 (((logits[id as usize] - max) / t).exp()) / sum
234 } else {
235 0.0
236 }
237}
238
239fn prefill_batched() -> bool {
242 std::env::var("CMF_PREFILL").map(|v| v != "seq").unwrap_or(true)
243}
244
245pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
247
248impl Pipeline {
249 #[allow(clippy::too_many_arguments)]
251 pub fn new(
252 tokenizer: Tokenizer,
253 weights: PipelineWeights,
254 hidden_size: usize,
255 intermediate_size: usize,
256 num_heads: usize,
257 num_kv_heads: usize,
258 head_dim: usize,
259 num_layers: usize,
260 vocab_size: usize,
261 rms_eps: f64,
262 rope_base: f32,
263 norm_style: NormStyle,
264 max_seq_len: usize,
265 sampler_config: SamplerConfig,
266 ) -> Self {
267 let rng = match sampler_config.seed {
268 Some(s) => SplitMix64::new(s),
269 None => SplitMix64::from_entropy(),
270 };
271 let inv_freq = attention::rope_inv_freq(head_dim, rope_base);
272 let pool = Pool::from_env();
273 if let Some(p) = &pool {
274 tracing::info!("worker pool: {} threads", p.n_workers());
275 }
276 Self {
277 tokenizer,
278 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
279 sampler_config,
280 weights,
281 hidden_size,
282 intermediate_size,
283 num_heads,
284 num_kv_heads,
285 head_dim,
286 num_layers,
287 vocab_size,
288 rms_eps,
289 rope_base,
290 norm_style,
291 rotary_dim: head_dim,
292 vmf_cfg: None,
293 gdn_cfg: None,
294 mtp: None,
295 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
296 rng,
297 inv_freq,
298 pool,
299 model: None,
300 dyn_force_f32: false,
301 dyn_skill_layers: Vec::new(),
302 dyn_active: None,
303 dyn_blend_loaded: false,
304 dyn_phi_layer: None,
305 dyn_phi_ema: Vec::new(),
306 dyn_phi_seen: 0,
307 dyn_router: None,
308 trace: false,
309 calib_temp: 1.0,
310 }
311 }
312
313 pub fn set_trace(&mut self, on: bool) {
315 self.trace = on;
316 }
317
318 pub fn set_calib_temp(&mut self, t: f32) {
321 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
322 }
323
324 pub fn calib_temp(&self) -> f32 {
326 self.calib_temp
327 }
328
329 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
332 self.rotary_dim = rotary_dim.min(self.head_dim);
333 self.inv_freq = attention::rope_inv_freq(self.rotary_dim, base);
334 }
335
336 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
337 QwenAttnCfg {
338 num_heads: self.num_heads,
339 num_kv_heads: self.num_kv_heads,
340 head_dim: self.head_dim,
341 hidden_size: self.hidden_size,
342 position,
343 inv_freq: &self.inv_freq,
344 rotary_dim: self.rotary_dim,
345 q_norm: None,
346 k_norm: None,
347 output_gate: false,
348 bias: None,
349 rms_eps: self.rms_eps,
350 norm_style: self.norm_style,
351 pool: self.pool.as_deref(),
352 }
353 }
354
355 pub fn generate(
357 &mut self,
358 prompt: &str,
359 max_tokens: usize,
360 task_mask: Option<&TaskMask>,
361 on_token: Option<TokenCallback>,
362 ) -> Result<GenerateResult, String> {
363 let input_ids = self.tokenizer.encode(prompt);
364 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
365 }
366
367 pub fn generate_from_ids(
375 &mut self,
376 input_ids: &[u32],
377 max_tokens: usize,
378 task_mask: Option<&TaskMask>,
379 mut on_token: Option<TokenCallback>,
380 ) -> Result<GenerateResult, String> {
381 if input_ids.is_empty() {
382 return Err("empty prompt: nothing to generate from".to_string());
383 }
384
385 self.kv_cache.clear();
387
388 let spec_active = self.speculative
389 && self.mtp.is_some()
390 && task_mask.is_none()
391 && self.sampler_config.temperature < 1e-6;
392 let mut mtp = if spec_active { self.mtp.take() } else { None };
395 if let Some(m) = &mut mtp {
396 m.kv.clear();
397 }
398 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
402 if let Some(r) = &mut router {
403 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
406 }
407
408 let mut all_ids = input_ids.to_vec();
409 let mut generated = 0usize;
410 let mut finish_reason = "max_tokens".to_string();
411 let mut drafted = 0usize;
412 let mut accepted = 0usize;
413 let mut confidence: Vec<f32> = Vec::new();
414 let trace_on = self.trace;
415 let calib_temp = self.calib_temp;
416 let mut traces: Vec<TokenTrace> = Vec::new();
417
418 let mut hidden = vec![0.0f32; self.hidden_size];
424 let mut pos = 0usize;
425 let dyn_prefill = router.is_some();
429 if task_mask.is_none() && !dyn_prefill {
430 while pos + 1 < input_ids.len() {
431 let e1 = self.embed_single(input_ids[pos]);
432 let e2 = self.embed_single(input_ids[pos + 1]);
433 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
434 self.commit_linear_scratch();
436 if let Some(m) = &mut mtp {
437 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
438 if pos + 2 < input_ids.len() {
439 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
440 }
441 }
442 hidden = h2;
443 pos += 2;
444 }
445 }
446 while pos < input_ids.len() {
447 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
448 if let Some(m) = &mut mtp {
449 if pos + 1 < input_ids.len() {
450 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
451 }
452 }
453 pos += 1;
454 }
455
456 macro_rules! commit {
458 ($id:expr) => {{
459 all_ids.push($id);
460 generated += 1;
461 if self.tokenizer.is_eos($id) {
462 finish_reason = "stop".to_string();
463 false
464 } else {
465 let token_text = self.tokenizer.decode_token($id);
466 let mut go = true;
467 if let Some(ref mut cb) = on_token {
468 if !cb(&token_text) {
469 finish_reason = "cancelled".to_string();
470 go = false;
471 }
472 }
473 go
474 }
475 }};
476 }
477
478 let mut next_pos = input_ids.len();
480 'decode: while generated < max_tokens {
481 let normed = inference::rms_norm(
482 &hidden,
483 &self.weights.final_norm,
484 self.rms_eps,
485 self.norm_style,
486 );
487 let logits = self.lm_head_forward(&normed);
488 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
489 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
490 if trace_on {
491 let skill = router.as_ref().and_then(|r| r.active_id());
495 traces.push(TokenTrace {
496 t: generated,
497 token_id: t_next,
498 confidence: *confidence.last().unwrap(),
499 active_skill: skill,
500 recon: None,
501 switched: false,
502 });
503 }
504 if !commit!(t_next) {
505 break 'decode;
506 }
507 if generated >= max_tokens {
508 break 'decode;
509 }
510
511 if self.kv_cache.needs_eviction() {
512 let keep = (self.kv_cache.max_seq_len / 2).max(1);
513 self.kv_cache.evict(keep);
514 }
515
516 match &mut mtp {
517 Some(m) if generated + 1 < max_tokens => {
519 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
520 drafted += 1;
521 let emb1 = self.embed_single(t_next);
522 let emb2 = self.embed_single(draft);
523 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
524
525 let n1 = inference::rms_norm(
526 &h1,
527 &self.weights.final_norm,
528 self.rms_eps,
529 self.norm_style,
530 );
531 let logits1 = self.lm_head_forward(&n1);
532 let t_after =
533 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
534 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
535 if trace_on {
536 traces.push(TokenTrace {
539 t: generated,
540 token_id: t_after,
541 confidence: *confidence.last().unwrap(),
542 active_skill: None,
543 recon: None,
544 switched: false,
545 });
546 }
547 let stop = !commit!(t_after);
548
549 if t_after == draft {
550 accepted += 1;
551 self.commit_linear_scratch();
552 let _ = self.mtp_step(m, &h1, t_after, next_pos);
553 hidden = h2;
554 next_pos += 2;
555 } else {
556 for layer in &mut self.kv_cache.layers {
558 layer.truncate_last(1);
559 }
560 if !stop {
561 let _ = self.mtp_step(m, &h1, t_after, next_pos);
562 hidden = self
563 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
564 }
565 next_pos += 2;
566 }
567 if stop {
568 break 'decode;
569 }
570 }
571 _ => {
573 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
574 next_pos += 1;
575 if let Some(r) = &mut router {
578 let phi = self.dyn_phi_ema.clone();
579 let decision = r.step(&phi, generated);
580 if let Some(new_active) = decision {
581 let _ = self.set_active_skill(new_active);
582 }
583 if trace_on {
586 if let Some(last) = traces.last_mut() {
587 let e = r.last_best_e();
588 last.recon = e.is_finite().then_some(e);
589 last.switched = decision.is_some();
590 }
591 }
592 }
593 }
594 }
595 }
596
597 if router.is_some() {
599 let _ = self.set_active_skill(None);
600 }
601 self.dyn_router = router.or(self.dyn_router.take());
602 self.mtp = mtp.or(self.mtp.take());
603
604 let output_ids = &all_ids[input_ids.len()..];
605 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
607 Ok(GenerateResult {
608 text: self.tokenizer.decode(output_ids),
609 token_ids: output_ids.to_vec(),
610 prompt_tokens: input_ids.len(),
611 tokens_generated: generated,
612 finish_reason,
613 mtp_drafted: drafted,
614 mtp_accepted: accepted,
615 token_confidence: confidence,
616 traces,
617 })
618 }
619
620 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
624 let h_n = inference::rms_norm(hidden, &m.hnorm, self.rms_eps, self.norm_style);
625 let e = self.embed_single(next_token);
626 let e_n = inference::rms_norm(&e, &m.enorm, self.rms_eps, self.norm_style);
627 let mut cat = Vec::with_capacity(2 * self.hidden_size);
631 cat.extend_from_slice(&e_n);
632 cat.extend_from_slice(&h_n);
633 let mut x = vec![0.0f32; self.hidden_size];
634 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
635
636 let lw = &m.layer;
638 let normed = inference::rms_norm(&x, &lw.input_norm, self.rms_eps, self.norm_style);
639 let attn = match &lw.attn {
640 AttnKind::Full {
641 wq,
642 wk,
643 wv,
644 wo,
645 q_norm,
646 k_norm,
647 output_gate,
648 bias,
649 } => {
650 let mut cfg = self.attn_cfg(position);
651 cfg.q_norm = q_norm.as_deref();
652 cfg.k_norm = k_norm.as_deref();
653 cfg.output_gate = *output_gate;
654 cfg.bias = bias
655 .as_ref()
656 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
657 attention::qwen_attention(&normed, wq, wk, wv, wo, &mut m.kv, &cfg)
658 }
659 AttnKind::Linear(_) | AttnKind::LinearGdn(_) => {
660 unreachable!("MTP block is full attention")
661 }
662 };
663 for (i, &a) in attn.iter().enumerate() {
664 x[i] += a;
665 }
666 let post = inference::rms_norm(&x, &lw.post_norm, self.rms_eps, self.norm_style);
667 let ffn = ffn_forward(&lw.ffn, &post, self.pool.as_deref());
668 for (i, &f) in ffn.iter().enumerate() {
669 x[i] += f;
670 }
671
672 let out = inference::rms_norm(&x, &m.final_norm, self.rms_eps, self.norm_style);
673 sampler::argmax(&self.lm_head_forward(&out))
674 }
675
676 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
680 let emb1 = self.embed_single(1);
681 let emb2 = self.embed_single(2);
682 let pos = self.kv_cache.seq_len();
683
684 let t0 = std::time::Instant::now();
685 for _ in 0..iters {
686 let _ = self.forward_layers(&emb1, pos, None);
687 let _ = self.forward_layers(&emb2, pos + 1, None);
688 for l in &mut self.kv_cache.layers {
689 l.truncate_last(2);
690 }
691 }
692 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
693
694 let t1 = std::time::Instant::now();
695 for _ in 0..iters {
696 let _ = self.forward_pair(&emb1, &emb2, pos);
697 for l in &mut self.kv_cache.layers {
698 l.truncate_last(2);
699 }
700 }
701 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
702 (singles_ms, pair_ms)
703 }
704
705 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
710 let mut h1 = emb1.to_vec();
711 let mut h2 = emb2.to_vec();
712 let (nh, nkv, hd, hs, rd, eps) = (
713 self.num_heads,
714 self.num_kv_heads,
715 self.head_dim,
716 self.hidden_size,
717 self.rotary_dim,
718 self.rms_eps,
719 );
720 let inv_freq = self.inv_freq.clone();
721 let pool = self.pool.clone();
722
723 for li in 0..self.num_layers {
724 let lw = &self.weights.layers[li];
725 let n1 = inference::rms_norm(&h1, &lw.input_norm, self.rms_eps, self.norm_style);
726 let n2 = inference::rms_norm(&h2, &lw.input_norm, self.rms_eps, self.norm_style);
727
728 let (a1, a2) = match &lw.attn {
729 AttnKind::Linear(w) => {
730 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
731 let layer = &mut self.kv_cache.layers[li];
732 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
733 vmf_phase_pair(&n1, &n2, w, &cfg, state, scratch, self.pool.as_deref())
734 }
735 AttnKind::LinearGdn(w) => {
736 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
737 let layer = &mut self.kv_cache.layers[li];
738 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
739 gdn_pair(&n1, &n2, w, &cfg, state, scratch, self.pool.as_deref())
740 }
741 AttnKind::Full {
742 wq,
743 wk,
744 wv,
745 wo,
746 q_norm,
747 k_norm,
748 output_gate,
749 bias,
750 } => {
751 let cfg = QwenAttnCfg {
752 num_heads: nh,
753 num_kv_heads: nkv,
754 head_dim: hd,
755 hidden_size: hs,
756 position,
757 inv_freq: &inv_freq,
758 rotary_dim: rd,
759 q_norm: q_norm.as_deref(),
760 k_norm: k_norm.as_deref(),
761 output_gate: *output_gate,
762 bias: bias
763 .as_ref()
764 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
765 rms_eps: eps,
766 norm_style: self.norm_style,
767 pool: pool.as_deref(),
768 };
769 attention::qwen_attention_pair(
770 &n1,
771 &n2,
772 wq,
773 wk,
774 wv,
775 wo,
776 &mut self.kv_cache.layers[li],
777 &cfg,
778 )
779 }
780 };
781 for i in 0..self.hidden_size {
782 h1[i] += a1[i];
783 h2[i] += a2[i];
784 }
785
786 let lw = &self.weights.layers[li];
787 let p1 = inference::rms_norm(&h1, &lw.post_norm, self.rms_eps, self.norm_style);
788 let p2 = inference::rms_norm(&h2, &lw.post_norm, self.rms_eps, self.norm_style);
789 let (f1, f2) = ffn_forward_pair(&lw.ffn, &p1, &p2, self.pool.as_deref());
790 for i in 0..self.hidden_size {
791 h1[i] += f1[i];
792 h2[i] += f2[i];
793 }
794 }
795 (h1, h2)
796 }
797
798 fn commit_linear_scratch(&mut self) {
800 for layer in &mut self.kv_cache.layers {
801 if !layer.linear_scratch.is_empty() {
802 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
803 layer.linear_scratch.clear();
804 }
805 }
806 }
807
808 pub fn forward_ids(
811 &mut self,
812 ids: &[u32],
813 task_mask: Option<&TaskMask>,
814 ) -> Result<Vec<f32>, String> {
815 if ids.is_empty() {
816 return Err("empty id sequence".to_string());
817 }
818 self.kv_cache.clear();
819 let mut hidden = vec![0.0f32; self.hidden_size];
820 let mut pos = 0usize;
821 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
822 const CHUNK: usize = 48;
824 let hs = self.hidden_size;
825 while pos < ids.len() {
826 let end = (pos + CHUNK).min(ids.len());
827 let hb = self.prefill_batch(&ids[pos..end], pos);
828 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
829 pos = end;
830 }
831 }
832 if task_mask.is_none() {
833 while pos + 1 < ids.len() {
834 let e1 = self.embed_single(ids[pos]);
835 let e2 = self.embed_single(ids[pos + 1]);
836 let (_, h2) = self.forward_pair(&e1, &e2, pos);
837 self.commit_linear_scratch();
838 hidden = h2;
839 pos += 2;
840 }
841 }
842 while pos < ids.len() {
843 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
844 pos += 1;
845 }
846 let normed = inference::rms_norm(
847 &hidden,
848 &self.weights.final_norm,
849 self.rms_eps,
850 self.norm_style,
851 );
852 Ok(self.lm_head_forward(&normed))
853 }
854
855 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
858 self.kv_cache.clear();
859 let mut nll = 0f64;
860 let mut cnt = 0usize;
861 if prefill_batched() {
862 const CHUNK: usize = 128;
868 const LM_SUB: usize = 32;
869 let n = ids.len().saturating_sub(1);
870 let hs = self.hidden_size;
871 let rows = self.weights.lm_head.rows();
872 let mut pos = 0usize;
873 while pos < n {
874 let end = (pos + CHUNK).min(n);
875 let bsz = end - pos;
876 let hb = self.prefill_batch(&ids[pos..end], pos);
877 let mut k0 = 0usize;
878 while k0 < bsz {
879 let k1 = (k0 + LM_SUB).min(bsz);
880 let sb = k1 - k0;
881 let mut normed = vec![0.0f32; sb * hs];
882 for k in 0..sb {
883 let r = inference::rms_norm(
884 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
885 &self.weights.final_norm,
886 self.rms_eps,
887 self.norm_style,
888 );
889 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
890 }
891 let mut logits = vec![0.0f32; sb * rows];
892 self.weights
893 .lm_head
894 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
895 for k in 0..sb {
896 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
897 let target = ids[pos + k0 + k + 1] as usize;
898 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
899 let lse: f64 = lg
900 .iter()
901 .map(|&v| ((v - max) as f64).exp())
902 .sum::<f64>()
903 .ln()
904 + max as f64;
905 nll += lse - lg[target] as f64;
906 cnt += 1;
907 }
908 k0 = k1;
909 }
910 pos = end;
911 }
912 self.kv_cache.clear();
913 return (nll / cnt.max(1) as f64).exp();
914 }
915 for pos in 0..ids.len().saturating_sub(1) {
916 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
917 let normed = inference::rms_norm(
918 &hidden,
919 &self.weights.final_norm,
920 self.rms_eps,
921 self.norm_style,
922 );
923 let logits = self.lm_head_forward(&normed);
924 let target = ids[pos + 1] as usize;
925 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
926 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
927 + max as f64;
928 nll += lse - logits[target] as f64;
929 cnt += 1;
930 }
931 self.kv_cache.clear();
932 (nll / cnt.max(1) as f64).exp()
933 }
934
935 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
943 self.kv_cache.clear();
944 let n = ids.len().saturating_sub(1);
945 let mut correct = Vec::with_capacity(n);
946 let mut pmax = Vec::with_capacity(n);
947 for pos in 0..n {
948 let emb = self.embed_single(ids[pos]);
949 let hidden = self.forward_layers(&emb, pos, None);
950 let normed = inference::rms_norm(
951 &hidden,
952 &self.weights.final_norm,
953 self.rms_eps,
954 self.norm_style,
955 );
956 let logits = self.lm_head_forward(&normed);
957 let target = ids[pos + 1] as usize;
958 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
959 for (i, &v) in logits.iter().enumerate() {
960 if v > mval {
961 mval = v;
962 amax = i;
963 }
964 }
965 correct.push(amax == target);
966 let row: Vec<f32> = temps
967 .iter()
968 .map(|&t| {
969 let tt = t.max(1e-3);
970 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
971 1.0 / s.max(1e-12) })
973 .collect();
974 pmax.push(row);
975 }
976 self.kv_cache.clear();
977 (correct, pmax)
978 }
979
980 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
987 let mut router = match self.dyn_router.take() {
988 Some(r) => r,
989 None => return (self.ppl_ids(ids), 0),
990 };
991 router.reset();
992 self.dyn_phi_seen = 0;
993 let _ = self.set_active_skill(None);
994
995 self.kv_cache.clear();
996 let mut nll = 0f64;
997 let mut cnt = 0usize;
998 for pos in 0..ids.len().saturating_sub(1) {
999 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1000 let normed = inference::rms_norm(
1001 &hidden,
1002 &self.weights.final_norm,
1003 self.rms_eps,
1004 self.norm_style,
1005 );
1006 let logits = self.lm_head_forward(&normed);
1007 let target = ids[pos + 1] as usize;
1008 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1009 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1010 + max as f64;
1011 nll += lse - logits[target] as f64;
1012 cnt += 1;
1013 let phi = self.dyn_phi_ema.clone();
1015 if let Some(new_active) = router.step(&phi, pos) {
1016 let _ = self.set_active_skill(new_active);
1017 }
1018 }
1019 let switches = router.switches.len();
1020 let _ = self.set_active_skill(None);
1021 self.dyn_router = Some(router);
1022 self.kv_cache.clear();
1023 ((nll / cnt.max(1) as f64).exp(), switches)
1024 }
1025
1026 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1028 self.kv_cache.clear();
1029 let mut acc = vec![0f32; self.hidden_size];
1030 for (pos, &id) in ids.iter().enumerate() {
1031 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1032 for (a, v) in acc.iter_mut().zip(&h) {
1033 *a += v;
1034 }
1035 }
1036 let n = ids.len().max(1) as f32;
1037 for a in acc.iter_mut() {
1038 *a /= n;
1039 }
1040 self.kv_cache.clear();
1041 acc
1042 }
1043
1044 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1050 let b = ids.len();
1051 let hs = self.hidden_size;
1052 let mut h: Vec<f32> = Vec::with_capacity(b * hs);
1053 for &id in ids {
1054 h.extend_from_slice(&self.embed_single(id));
1055 }
1056 let (nh, nkv, hd, rd, eps) = (
1057 self.num_heads,
1058 self.num_kv_heads,
1059 self.head_dim,
1060 self.rotary_dim,
1061 self.rms_eps,
1062 );
1063 let inv_freq = self.inv_freq.clone();
1064 let pool = self.pool.clone();
1065 let norm_style = self.norm_style;
1066
1067 for li in 0..self.num_layers {
1068 crate::gpu::set_layer(li as i64); let lw = &self.weights.layers[li];
1070 match &lw.attn {
1072 AttnKind::LinearGdn(w) => {
1073 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1075 let mut normed = vec![0.0f32; b * hs];
1076 for bi in 0..b {
1077 let r = inference::rms_norm(
1078 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1079 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1080 }
1081 let attn = crate::linear_core::gdn_forward_batch(
1082 &normed, b, w, &cfg,
1083 &mut self.kv_cache.layers[li].linear_state,
1084 pool.as_deref(),
1085 );
1086 for (dst, &a) in h.iter_mut().zip(&attn) {
1087 *dst += a;
1088 }
1089 }
1090 _ => {
1091 for bi in 0..b {
1092 let normed = inference::rms_norm(
1093 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1094 let position = start_pos + bi;
1095 let attn = match &lw.attn {
1096 AttnKind::Linear(w) => {
1097 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1098 vmf_phase_forward(
1099 &normed, w, &cfg,
1100 &mut self.kv_cache.layers[li].linear_state,
1101 pool.as_deref(),
1102 )
1103 }
1104 AttnKind::LinearGdn(_) => unreachable!(),
1105 AttnKind::Full {
1106 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
1107 } => {
1108 let cfg = QwenAttnCfg {
1109 num_heads: nh,
1110 num_kv_heads: nkv,
1111 head_dim: hd,
1112 hidden_size: hs,
1113 position,
1114 inv_freq: &inv_freq,
1115 rotary_dim: rd,
1116 q_norm: q_norm.as_deref(),
1117 k_norm: k_norm.as_deref(),
1118 output_gate: *output_gate,
1119 bias: bias.as_ref().map(|(a, b, c)| {
1120 (a.as_slice(), b.as_slice(), c.as_slice())
1121 }),
1122 rms_eps: eps,
1123 norm_style,
1124 pool: pool.as_deref(),
1125 };
1126 attention::qwen_attention(
1127 &normed, wq, wk, wv, wo,
1128 &mut self.kv_cache.layers[li], &cfg)
1129 }
1130 };
1131 for (i, &a) in attn.iter().enumerate() {
1132 h[bi * hs + i] += a;
1133 }
1134 }
1135 }
1136 }
1137
1138 let lw = &self.weights.layers[li];
1140 let mut post = vec![0.0f32; b * hs];
1141 for bi in 0..b {
1142 let r = inference::rms_norm(
1143 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
1144 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1145 }
1146 let ffn = match &lw.ffn {
1147 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
1148 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
1149 };
1150 for (dst, &f) in h.iter_mut().zip(&ffn) {
1151 *dst += f;
1152 }
1153 }
1154 crate::gpu::set_layer(-1); h
1156 }
1157
1158 fn embed_single(&self, id: u32) -> Vec<f32> {
1160 let mut out = vec![0.0f32; self.hidden_size];
1161 if (id as usize) < self.weights.embed_tokens.rows() {
1162 self.weights.embed_tokens.row_f32(id as usize, &mut out);
1163 }
1164 out
1165 }
1166
1167 fn forward_layers(
1169 &mut self,
1170 hidden: &[f32],
1171 position: usize,
1172 task_mask: Option<&TaskMask>,
1173 ) -> Vec<f32> {
1174 self.forward_layers_upto(hidden, position, task_mask, None)
1175 }
1176
1177 fn forward_layers_upto(
1179 &mut self,
1180 hidden: &[f32],
1181 position: usize,
1182 task_mask: Option<&TaskMask>,
1183 upto: Option<usize>,
1184 ) -> Vec<f32> {
1185 let mut h = hidden.to_vec();
1186 let (nh, nkv, hd, hs, rd, eps) = (
1189 self.num_heads,
1190 self.num_kv_heads,
1191 self.head_dim,
1192 self.hidden_size,
1193 self.rotary_dim,
1194 self.rms_eps,
1195 );
1196 let inv_freq = self.inv_freq.clone();
1197 let pool = self.pool.clone();
1198
1199 for li in 0..self.num_layers {
1200 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
1202 if li > u {
1203 break;
1204 }
1205 }
1206 if let Some(mask) = task_mask {
1207 if !mask.layer_alive(li) {
1208 continue; }
1210 }
1211
1212 let lw = &self.weights.layers[li];
1213 let normed = inference::rms_norm(&h, &lw.input_norm, self.rms_eps, self.norm_style);
1214
1215 let attn_out = match &lw.attn {
1216 AttnKind::Linear(w) => {
1217 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1218 vmf_phase_forward(
1219 &normed,
1220 w,
1221 &cfg,
1222 &mut self.kv_cache.layers[li].linear_state,
1223 self.pool.as_deref(),
1224 )
1225 }
1226 AttnKind::LinearGdn(w) => {
1227 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1228 gdn_forward(
1229 &normed,
1230 w,
1231 &cfg,
1232 &mut self.kv_cache.layers[li].linear_state,
1233 self.pool.as_deref(),
1234 )
1235 }
1236 AttnKind::Full {
1237 wq,
1238 wk,
1239 wv,
1240 wo,
1241 q_norm,
1242 k_norm,
1243 output_gate,
1244 bias,
1245 } => {
1246 let masked = task_mask
1247 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
1248 .unwrap_or(false);
1249 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
1250 match (masked, f32_view) {
1251 (true, (Some(q), Some(k), Some(v), Some(o))) => {
1254 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
1255 attention::multi_head_attention(
1256 &normed,
1257 q,
1258 k,
1259 v,
1260 o,
1261 &mut self.kv_cache.layers[li],
1262 self.num_heads,
1263 self.num_kv_heads,
1264 self.head_dim,
1265 self.hidden_size,
1266 position,
1267 &active_heads,
1268 &self.inv_freq,
1269 )
1270 }
1271 (masked, _) => {
1272 if masked {
1273 tracing::warn!(
1274 "layer {li}: head mask on quantized weights not \
1275 supported yet — executing dense"
1276 );
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
1290 .as_ref()
1291 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1292 rms_eps: eps,
1293 norm_style: self.norm_style,
1294 pool: pool.as_deref(),
1295 };
1296 attention::qwen_attention(
1297 &normed,
1298 wq,
1299 wk,
1300 wv,
1301 wo,
1302 &mut self.kv_cache.layers[li],
1303 &cfg,
1304 )
1305 }
1306 }
1307 }
1308 };
1309 for (i, &a) in attn_out.iter().enumerate() {
1310 h[i] += a;
1311 }
1312
1313 let lw = &self.weights.layers[li];
1314 let post_normed = inference::rms_norm(&h, &lw.post_norm, self.rms_eps, self.norm_style);
1315
1316 let ffn_masked = task_mask
1317 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
1318 .unwrap_or(false);
1319 let f32_ffn = match &lw.ffn {
1322 FfnKind::Dense(d) => {
1323 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
1324 }
1325 FfnKind::Moe(_) => (None, None, None),
1326 };
1327 let ffn_out = match (ffn_masked, f32_ffn) {
1328 (true, (Some(g), Some(u), Some(d))) => {
1329 let active = task_mask.unwrap().ffn_active_indices(li);
1330 inference::sparse_ffn_forward(
1331 &post_normed,
1332 g,
1333 u,
1334 d,
1335 self.hidden_size,
1336 self.intermediate_size,
1337 &active,
1338 self.pool.as_deref(),
1339 )
1340 }
1341 (true, _) => match &lw.ffn {
1345 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
1346 let active = task_mask.unwrap().ffn_active_indices(li);
1347 sparse_ffn_quant(
1348 d,
1349 &post_normed,
1350 &active,
1351 self.hidden_size,
1352 self.pool.as_deref(),
1353 )
1354 }
1355 FfnKind::Dense(d) => {
1360 let active = task_mask.unwrap().ffn_active_indices(li);
1361 let (gf, uf, df) = dequant_dense_f32(d);
1362 inference::sparse_ffn_forward(
1363 &post_normed,
1364 &gf,
1365 &uf,
1366 &df,
1367 self.hidden_size,
1368 self.intermediate_size,
1369 &active,
1370 self.pool.as_deref(),
1371 )
1372 }
1373 FfnKind::Moe(_) => {
1374 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
1377 }
1378 },
1379 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
1380 };
1381 for (i, &f) in ffn_out.iter().enumerate() {
1382 h[i] += f;
1383 }
1384
1385 if self.dyn_phi_layer == Some(li) {
1389 self.update_dyn_phi(&h);
1390 }
1391 }
1392 crate::gpu::set_layer(-1); h
1395 }
1396
1397 fn update_dyn_phi(&mut self, h: &[f32]) {
1400 const A: f32 = 0.2;
1401 if self.dyn_phi_ema.len() != h.len() {
1402 self.dyn_phi_ema = vec![0.0; h.len()];
1403 self.dyn_phi_seen = 0;
1404 }
1405 if self.dyn_phi_seen == 0 {
1406 self.dyn_phi_ema.copy_from_slice(h);
1407 } else {
1408 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
1409 *e = (1.0 - A) * *e + A * v;
1410 }
1411 }
1412 self.dyn_phi_seen += 1;
1413 }
1414
1415 pub fn dyn_phi(&self) -> &[f32] {
1417 &self.dyn_phi_ema
1418 }
1419
1420 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
1422 self.dyn_phi_layer = layer;
1423 self.dyn_phi_ema.clear();
1424 self.dyn_phi_seen = 0;
1425 }
1426
1427 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
1429 let Some(model) = &self.model else { return Vec::new() };
1430 model
1431 .header
1432 .skills
1433 .iter()
1434 .enumerate()
1435 .filter_map(|(i, sk)| {
1436 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
1437 let sel = sk.selection.as_ref()?;
1438 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
1439 })
1440 .collect()
1441 }
1442
1443 pub fn active_skill(&self) -> Option<usize> {
1445 self.dyn_active
1446 }
1447
1448 pub fn enable_dynamic_routing(&mut self) -> usize {
1453 use crate::swarm::{DynRouter, RoutableSkill};
1454 let Some(model) = self.model.clone() else { return 0 };
1455 if self.dyn_blend_loaded {
1458 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
1459 return 0;
1460 }
1461 if let Some(a) = self.dyn_active {
1465 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
1466 tracing::warn!(
1467 "loaded skill is not FFN-eligible — dynamic routing unavailable"
1468 );
1469 return 0;
1470 }
1471 }
1472 let hidden = self.hidden_size;
1473 let mut skills = Vec::new();
1474 for (idx, id, _phi) in self.dynamic_skills() {
1475 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
1476 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
1477 skills.push(rs);
1478 }
1479 }
1480 }
1481 if skills.is_empty() {
1482 return 0;
1483 }
1484 let phi = skills[0].phi_layer;
1486 if skills.iter().any(|s| s.phi_layer != phi) {
1487 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
1488 }
1489 let n = skills.len();
1490 self.set_dyn_phi_layer(Some(phi));
1491 self.dyn_router = Some(DynRouter::new(skills));
1492 n
1493 }
1494
1495 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
1497 self.dyn_router
1498 .as_ref()
1499 .map(|r| r.switches.clone())
1500 .unwrap_or_default()
1501 }
1502
1503 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
1506 let rows = self.weights.lm_head.rows();
1507 let mut logits = vec![0.0f32; rows.min(self.vocab_size)];
1508 self.weights
1509 .lm_head
1510 .matvec(hidden, &mut logits, self.pool.as_deref());
1511 logits.resize(self.vocab_size, 0.0);
1512 logits
1513 }
1514
1515 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
1520 self.kv_cache.clear();
1521 let mut hidden = vec![0.0f32; self.hidden_size];
1522 for (pos, &id) in ids.iter().enumerate() {
1523 let emb = self.embed_single(id);
1524 hidden = self.forward_layers(&emb, pos, task_mask);
1525 }
1526 let normed = inference::rms_norm(
1527 &hidden,
1528 &self.weights.final_norm,
1529 self.rms_eps,
1530 self.norm_style,
1531 );
1532 self.lm_head_forward(&normed)
1533 }
1534}
1535
1536pub fn create_test_pipeline(
1538 hidden_size: usize,
1539 intermediate_size: usize,
1540 num_heads: usize,
1541 num_kv_heads: usize,
1542 head_dim: usize,
1543 num_layers: usize,
1544 vocab_size: usize,
1545) -> Pipeline {
1546 let synth = |n: usize, salt: usize| -> Vec<f32> {
1549 (0..n)
1550 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
1551 .collect()
1552 };
1553 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
1554 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
1555 };
1556 let layer_weights: Vec<LayerWeights> = (0..num_layers)
1557 .map(|li| LayerWeights {
1558 input_norm: vec![1.0; hidden_size],
1559 post_norm: vec![1.0; hidden_size],
1560 ffn: FfnKind::Dense(DenseFfn {
1561 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
1562 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
1563 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
1564 }),
1565 attn: AttnKind::Full {
1566 bias: None,
1567 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
1568 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
1569 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
1570 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
1571 q_norm: None,
1572 k_norm: None,
1573 output_gate: false,
1574 },
1575 })
1576 .collect();
1577
1578 Pipeline::new(
1579 Tokenizer::byte_level(),
1580 PipelineWeights {
1581 embed_tokens: qt(vocab_size, hidden_size, 100),
1582 layers: layer_weights,
1583 lm_head: qt(vocab_size, hidden_size, 200),
1584 final_norm: vec![1.0; hidden_size],
1585 },
1586 hidden_size,
1587 intermediate_size,
1588 num_heads,
1589 num_kv_heads,
1590 head_dim,
1591 num_layers,
1592 vocab_size,
1593 1e-6,
1594 10_000.0,
1595 NormStyle::Qwen,
1596 4096,
1597 SamplerConfig {
1598 seed: Some(42),
1599 ..Default::default()
1600 },
1601 )
1602}
1603
1604fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
1607 let inter = d.gate_proj.rows();
1608 let hidden = d.down_proj.rows();
1609 let mut g = vec![0.0f32; b * inter];
1610 d.gate_proj.matmat(xs, b, &mut g, pool);
1611 let mut u = vec![0.0f32; b * inter];
1612 d.up_proj.matmat(xs, b, &mut u, pool);
1613 for i in 0..b * inter {
1614 g[i] = inference::silu(g[i]) * u[i];
1615 }
1616 let mut out = vec![0.0f32; b * hidden];
1617 d.down_proj.matmat(&g, b, &mut out, pool);
1618 out
1619}
1620
1621fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
1625 let ne = m.experts.len();
1626 let mut logits = vec![0.0f32; b * ne];
1627 m.router.matmat(xs, b, &mut logits, pool);
1628
1629 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
1632 {
1633 let mut st = m.stats.borrow_mut();
1634 if st.len() < ne {
1635 st.resize(ne, 0);
1636 }
1637 for bi in 0..b {
1638 let lg = &logits[bi * ne..(bi + 1) * ne];
1639 let mx = lg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1640 let mut p: Vec<f32> = lg.iter().map(|&l| (l - mx).exp()).collect();
1641 let sum: f32 = p.iter().sum();
1642 for v in &mut p {
1643 *v /= sum;
1644 }
1645 let mut order: Vec<usize> = (0..ne).collect();
1646 order.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y)));
1647 order.truncate(m.top_k);
1648 let wsum: f32 = if m.norm_topk_prob {
1649 order.iter().map(|&e| p[e]).sum()
1650 } else {
1651 1.0
1652 };
1653 for &e in &order {
1654 st[e] += 1;
1655 assign[e].push((bi, p[e] / wsum));
1656 }
1657 }
1658 }
1659
1660 let mut out = vec![0.0f32; b * hidden];
1661 let cols = m.experts[0].gate_proj.cols();
1662 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
1663 let sb = list.len();
1664 let mut sub = vec![0.0f32; sb * cols];
1665 for (k, &(bi, _)) in list.iter().enumerate() {
1666 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
1667 }
1668 let eo = dense_ffn_batch(d, &sub, sb, pool);
1669 for (k, &(bi, w)) in list.iter().enumerate() {
1670 for i in 0..hidden {
1671 out[bi * hidden + i] += w * eo[k * hidden + i];
1672 }
1673 }
1674 };
1675 for e in 0..ne {
1676 if !assign[e].is_empty() {
1677 run_expert(&m.experts[e], &assign[e]);
1678 }
1679 }
1680 if let Some((se, gate)) = &m.shared {
1681 let mut gl = vec![0.0f32; b];
1682 gate.matmat(xs, b, &mut gl, pool);
1683 let all: Vec<(usize, f32)> = (0..b)
1684 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
1685 .collect();
1686 run_expert(se, &all);
1687 }
1688 out
1689}
1690
1691fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
1693 let inter = d.gate_proj.rows();
1694 let mut g = vec![0.0f32; inter];
1695 d.gate_proj.matvec(x, &mut g, pool);
1696 let mut u = vec![0.0f32; inter];
1697 d.up_proj.matvec(x, &mut u, pool);
1698 for i in 0..inter {
1699 g[i] = inference::silu(g[i]) * u[i];
1700 }
1701 let mut out = vec![0.0f32; d.down_proj.rows()];
1702 d.down_proj.matvec(&g, &mut out, pool);
1703 out
1704}
1705
1706fn sparse_ffn_quant(
1713 d: &DenseFfn,
1714 x: &[f32],
1715 active: &[u16],
1716 hidden: usize,
1717 pool: Option<&Pool>,
1718) -> Vec<f32> {
1719 let n = active.len();
1720 let inter = d.gate_proj.rows();
1721 let mut act = vec![0.0f32; n];
1722 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
1725 let compute = |ai: usize| -> f32 {
1726 let idx = active[ai] as usize;
1727 if idx >= inter {
1728 return 0.0; }
1730 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
1731 let gate = d.gate_proj.row_dot(idx, x, &mut s);
1732 let up = d.up_proj.row_dot(idx, x, &mut s);
1733 inference::silu(gate) * up
1734 };
1735 match pool {
1736 Some(p) if n >= 256 => {
1737 let ptr = SendMut(act.as_mut_ptr());
1738 p.run(&|widx, nw| {
1739 let chunk = n.div_ceil(nw);
1740 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
1741 for ai in s..e {
1742 unsafe { *ptr.at(ai) = compute(ai) };
1743 }
1744 });
1745 }
1746 _ => {
1747 for (ai, a) in act.iter_mut().enumerate() {
1748 *a = compute(ai);
1749 }
1750 }
1751 }
1752 let mut out = vec![0.0f32; hidden];
1754 for (ai, &idx) in active.iter().enumerate() {
1755 let w = act[ai];
1756 if w.abs() >= 1e-12 && (idx as usize) < inter {
1757 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
1758 }
1759 }
1760 out
1761}
1762
1763#[doc(hidden)]
1765pub fn sparse_ffn_quant_for_test(
1766 d: &DenseFfn,
1767 x: &[f32],
1768 active: &[u16],
1769 hidden: usize,
1770) -> Vec<f32> {
1771 sparse_ffn_quant(d, x, active, hidden, None)
1772}
1773
1774fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
1778 let deq = |t: &QTensor| -> Vec<f32> {
1779 let (rows, cols) = (t.rows(), t.cols());
1780 let mut out = vec![0.0f32; rows * cols];
1781 for r in 0..rows {
1782 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
1783 }
1784 out
1785 };
1786 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
1787}
1788
1789struct SendMut(*mut f32);
1791unsafe impl Send for SendMut {}
1792unsafe impl Sync for SendMut {}
1793impl SendMut {
1794 #[inline]
1795 #[allow(clippy::mut_from_ref)]
1798 unsafe fn at(&self, i: usize) -> &mut f32 {
1799 unsafe { &mut *self.0.add(i) }
1800 }
1801}
1802
1803fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
1807 let ne = m.experts.len();
1808 let mut logits = vec![0.0f32; ne];
1809 m.router.matvec(x, &mut logits, pool);
1810 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1811 let mut p: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
1812 let s: f32 = p.iter().sum();
1813 for v in &mut p {
1814 *v /= s;
1815 }
1816 let mut idx: Vec<usize> = (0..ne).collect();
1817 idx.sort_unstable_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap().then(a.cmp(&b)));
1819 idx.truncate(m.top_k);
1820 let wsum: f32 = if m.norm_topk_prob {
1821 idx.iter().map(|&e| p[e]).sum()
1822 } else {
1823 1.0
1824 };
1825 {
1826 let mut st = m.stats.borrow_mut();
1827 if st.len() < ne {
1828 st.resize(ne, 0);
1829 }
1830 for &e in &idx {
1831 st[e] += 1;
1832 }
1833 }
1834 if crate::gpu::enabled_here() {
1837 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
1838 return out;
1839 }
1840 }
1841
1842 let mut out = vec![0.0f32; x.len()];
1843 for &e in &idx {
1844 let eo = dense_ffn(&m.experts[e], x, pool);
1845 let w = p[e] / wsum;
1846 for i in 0..out.len() {
1847 out[i] += w * eo[i];
1848 }
1849 }
1850 if let Some((se, gate)) = &m.shared {
1851 let so = dense_ffn(se, x, pool);
1852 let mut gl = vec![0.0f32; 1];
1853 gate.matvec(x, &mut gl, pool);
1854 let g = 1.0 / (1.0 + (-gl[0]).exp());
1855 for i in 0..out.len() {
1856 out[i] += g * so[i];
1857 }
1858 }
1859 out
1860}
1861
1862fn moe_ffn_gpu(
1865 m: &MoeFfn,
1866 x: &[f32],
1867 idx: &[usize],
1868 p: &[f32],
1869 wsum: f32,
1870 pool: Option<&Pool>,
1871) -> Option<Vec<f32>> {
1872 use crate::gpu::MoeJob;
1873 use crate::qtensor::prescale;
1874 use cortiq_core::TensorDtype;
1875
1876 fn parts(
1877 t: &QTensor,
1878 ) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32])>
1879 {
1880 match t {
1881 QTensor::Mapped {
1882 model,
1883 idx,
1884 dtype: TensorDtype::Q8_2f,
1885 rows,
1886 cols,
1887 row_scale,
1888 col_field,
1889 } if !col_field.is_empty() => {
1890 Some((model, *idx, *rows, *cols, row_scale, col_field))
1891 }
1892 _ => None,
1893 }
1894 }
1895
1896 fn push<'a>(
1897 d: &'a DenseFfn,
1898 x: &[f32],
1899 w: f32,
1900 jobs: &mut Vec<MoeJob<'a>>,
1901 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
1902 ) -> Option<()> {
1903 let (gm, gi, gr, gc, grs, gcf) = parts(&d.gate_proj)?;
1904 let (_, ui, ur, uc, urs, ucf) = parts(&d.up_proj)?;
1905 let (_, di, dr, dc, drs, dcf) = parts(&d.down_proj)?;
1906 model_ref.get_or_insert_with(|| gm.clone());
1907 jobs.push(MoeJob {
1908 gate: (gi, gr, gc, grs),
1909 up: (ui, ur, uc, urs),
1910 down: (di, dr, dc, drs),
1911 xs_gate: prescale(x, gcf, TensorDtype::Q8_2f),
1912 xs_up: prescale(x, ucf, TensorDtype::Q8_2f),
1913 down_col: dcf,
1914 w,
1915 });
1916 Some(())
1917 }
1918
1919 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
1920 let mut model_ref = None;
1921 for &e in idx {
1922 push(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
1923 }
1924 if let Some((se, gate)) = &m.shared {
1925 let mut gl = vec![0.0f32; 1];
1926 gate.matvec(x, &mut gl, pool);
1927 let g = 1.0 / (1.0 + (-gl[0]).exp());
1928 push(se, x, g, &mut jobs, &mut model_ref)?;
1929 }
1930 let model = model_ref?;
1931 let hidden = jobs[0].down.1;
1932 let mut out = vec![0.0f32; hidden];
1933 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
1934}
1935
1936fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
1938 match ffn {
1939 FfnKind::Dense(d) => dense_ffn(d, x, pool),
1940 FfnKind::Moe(m) => moe_ffn(m, x, pool),
1941 }
1942}
1943
1944fn ffn_forward_pair(
1948 ffn: &FfnKind,
1949 x1: &[f32],
1950 x2: &[f32],
1951 pool: Option<&Pool>,
1952) -> (Vec<f32>, Vec<f32>) {
1953 let d = match ffn {
1954 FfnKind::Dense(d) => d,
1955 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
1956 };
1957 let inter = d.gate_proj.rows();
1958 let mut g1 = vec![0.0f32; inter];
1959 let mut g2 = vec![0.0f32; inter];
1960 d.gate_proj.matvec2(x1, x2, &mut g1, &mut g2, pool);
1961 let mut u1 = vec![0.0f32; inter];
1962 let mut u2 = vec![0.0f32; inter];
1963 d.up_proj.matvec2(x1, x2, &mut u1, &mut u2, pool);
1964 for i in 0..inter {
1965 g1[i] = inference::silu(g1[i]) * u1[i];
1966 g2[i] = inference::silu(g2[i]) * u2[i];
1967 }
1968 let mut o1 = vec![0.0f32; d.down_proj.rows()];
1969 let mut o2 = vec![0.0f32; d.down_proj.rows()];
1970 d.down_proj.matvec2(&g1, &g2, &mut o1, &mut o2, pool);
1971 (o1, o2)
1972}
1973
1974#[cfg(test)]
1975mod tests {
1976 use super::*;
1977
1978 #[test]
1984 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
1985 let (hidden, inter) = (16usize, 40usize);
1986 let synth = |n: usize, salt: usize| -> Vec<f32> {
1987 (0..n)
1988 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
1989 .collect()
1990 };
1991 let d = DenseFfn {
1992 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
1993 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
1994 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
1995 };
1996 let x = synth(hidden, 9);
1997 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
1999
2000 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
2001
2002 let mut g = vec![0.0f32; inter];
2004 d.gate_proj.matvec(&x, &mut g, None);
2005 let mut u = vec![0.0f32; inter];
2006 d.up_proj.matvec(&x, &mut u, None);
2007 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
2008 for i in 0..inter {
2009 g[i] = if act_set.contains(&(i as u16)) {
2010 inference::silu(g[i]) * u[i]
2011 } else {
2012 0.0
2013 };
2014 }
2015 let mut reference = vec![0.0f32; hidden];
2016 d.down_proj.matvec(&g, &mut reference, None);
2017
2018 let max_d = sparse
2019 .iter()
2020 .zip(&reference)
2021 .map(|(a, b)| (a - b).abs())
2022 .fold(0.0f32, f32::max);
2023 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
2024 }
2025
2026 fn attach_test_mtp(p: &mut Pipeline) {
2028 let (h, inter, heads, kv, hd) = (
2029 p.hidden_size,
2030 p.intermediate_size,
2031 p.num_heads,
2032 p.num_kv_heads,
2033 p.head_dim,
2034 );
2035 let synth = |n: usize, salt: usize| -> Vec<f32> {
2036 (0..n)
2037 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
2038 .collect()
2039 };
2040 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2041 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2042 };
2043 p.mtp = Some(MtpModule {
2044 enorm: vec![1.0; h],
2045 hnorm: vec![1.0; h],
2046 eh_proj: qt(h, 2 * h, 301),
2047 layer: LayerWeights {
2048 input_norm: vec![1.0; h],
2049 post_norm: vec![1.0; h],
2050 ffn: FfnKind::Dense(DenseFfn {
2051 gate_proj: qt(inter, h, 315),
2052 up_proj: qt(inter, h, 316),
2053 down_proj: qt(h, inter, 317),
2054 }),
2055 attn: AttnKind::Full {
2056 bias: None,
2057 wq: qt(heads * hd, h, 311),
2058 wk: qt(kv * hd, h, 312),
2059 wv: qt(kv * hd, h, 313),
2060 wo: qt(h, heads * hd, 314),
2061 q_norm: None,
2062 k_norm: None,
2063 output_gate: false,
2064 },
2065 },
2066 final_norm: vec![1.0; h],
2067 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
2068 });
2069 }
2070
2071 #[test]
2072 fn speculative_equals_vanilla_greedy() {
2073 let run = |spec: bool| {
2074 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2075 p.sampler_config.temperature = 0.0;
2076 attach_test_mtp(&mut p);
2077 p.speculative = spec;
2078 let r = p.generate("abcdef", 12, None, None).unwrap();
2079 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
2080 };
2081 let (vanilla, d0, _) = run(false);
2082 let (spec, d1, a1) = run(true);
2083 assert_eq!(d0, 0, "vanilla path must not draft");
2084 assert!(d1 > 0, "speculative path must draft");
2085 assert_eq!(
2086 vanilla, spec,
2087 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
2088 );
2089 }
2090
2091 #[test]
2092 fn speculative_accepts_constant_oracle() {
2093 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2094 p.sampler_config.temperature = 0.0;
2095 p.sampler_config.repetition_penalty = 1.0;
2096 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
2099 attach_test_mtp(&mut p);
2100 p.speculative = true;
2101 let r = p.generate("abcd", 10, None, None).unwrap();
2102 assert!(r.mtp_drafted > 0);
2103 assert_eq!(
2104 r.mtp_accepted, r.mtp_drafted,
2105 "constant logits → every draft accepted"
2106 );
2107 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
2110 }
2111
2112 #[test]
2113 fn empty_prompt_is_an_error_not_a_panic() {
2114 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2115 let r = p.generate("", 4, None, None);
2116 assert!(r.is_err(), "empty prompt must be a clean error");
2117 }
2118
2119 #[test]
2120 fn every_token_enters_kv_exactly_once() {
2121 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2122 p.sampler_config.temperature = 0.0;
2124 let r = p.generate("abc", 2, None, None).unwrap();
2125 assert_eq!(r.prompt_tokens, 3);
2126 assert_eq!(
2130 p.kv_cache.seq_len(),
2131 3 + r.tokens_generated - 1,
2132 "each token must be cached exactly once (v1 cached the last prompt token twice)"
2133 );
2134 }
2135
2136 #[test]
2137 fn generation_is_reproducible_with_seed() {
2138 let run = || {
2139 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2140 p.generate("hello", 8, None, None).unwrap().token_ids
2141 };
2142 assert_eq!(run(), run());
2143 }
2144
2145 #[test]
2146 fn eviction_bounds_the_cache() {
2147 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2148 p.kv_cache.max_seq_len = 6;
2149 p.sampler_config.temperature = 0.0;
2150 let _ = p.generate("abcd", 12, None, None).unwrap();
2151 assert!(
2152 p.kv_cache.seq_len() <= 6 + 1,
2153 "cache must stay bounded by max_seq_len (got {})",
2154 p.kv_cache.seq_len()
2155 );
2156 }
2157
2158 #[test]
2159 fn confidence_matches_tokens_and_is_a_probability() {
2160 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2161 p.sampler_config.temperature = 0.0;
2162 p.sampler_config.repetition_penalty = 1.0;
2163 let r = p.generate("abcd", 10, None, None).unwrap();
2164 assert_eq!(
2165 r.token_confidence.len(),
2166 r.token_ids.len(),
2167 "one confidence per emitted token"
2168 );
2169 for &c in &r.token_confidence {
2170 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
2171 }
2172 let logits = [1.0f32, 3.0, 0.5, 3.0];
2174 let p0 = top1_prob_t(&logits, 1, 1.0);
2175 let p1 = top1_prob_t(&logits, 3, 1.0);
2176 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
2177 assert!(p0 > 0.0 && p0 < 1.0);
2178 let sharp = top1_prob_t(&logits, 1, 1.0);
2180 let soft = top1_prob_t(&logits, 1, 2.0);
2181 assert!(soft < sharp, "higher temperature lowers peak confidence");
2182 }
2183
2184 #[test]
2185 fn trace_is_opt_in_and_parallels_the_output() {
2186 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2188 p.sampler_config.temperature = 0.0;
2189 p.sampler_config.repetition_penalty = 1.0;
2190 let r = p.generate("abcd", 10, None, None).unwrap();
2191 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
2192
2193 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2195 p.sampler_config.temperature = 0.0;
2196 p.sampler_config.repetition_penalty = 1.0;
2197 p.set_trace(true);
2198 let r = p.generate("abcd", 10, None, None).unwrap();
2199 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
2200 for (i, tr) in r.traces.iter().enumerate() {
2201 assert_eq!(tr.t, i, "trace index is sequential");
2202 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
2203 assert_eq!(
2204 tr.confidence, r.token_confidence[i],
2205 "trace confidence matches the confidence channel"
2206 );
2207 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
2209 }
2210 }
2211
2212 #[test]
2213 fn explain_prefill_logits_match_greedy_first_token() {
2214 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2218 p.sampler_config.temperature = 0.0;
2219 p.sampler_config.repetition_penalty = 1.0;
2220 let ids = p.tokenizer.encode("abcd");
2221 let logits = p.prefill_next_logits(&ids, None);
2222 let argmax = logits
2223 .iter()
2224 .enumerate()
2225 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2226 .unwrap()
2227 .0 as u32;
2228 let r = p.generate("abcd", 1, None, None).unwrap();
2229 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
2230 }
2231}