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