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
286 #[cfg(target_os = "macos")]
294 fn gdn_block_gpu(&mut self, start: usize, upto: Option<usize>, h: &mut [f32]) -> usize {
295 use crate::gpu::{GdnGpuCfg, GdnGpuLayer};
296 if !crate::gpu::enabled_here()
297 || std::env::var("CMF_GPU_BLOCK").map(|v| v == "0").unwrap_or(false)
298 {
299 return start;
300 }
301 let Some(cfg) = self.gdn_cfg else { return start };
302 let limit = upto.map(|u| u + 1).unwrap_or(self.num_layers).min(self.num_layers);
303 let mut layers: Vec<GdnGpuLayer> = Vec::new();
304 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
305 let mut inter = 0usize;
306 let mut end = start;
307 while end < limit {
308 let lw = &self.weights.layers[end];
309 let AttnKind::LinearGdn(w) = &lw.attn else { break };
310 let FfnKind::Dense(d) = &lw.ffn else { break };
311 let parts = (
312 w.in_proj_qkv.q1_parts(),
313 w.in_proj_z.q1_parts(),
314 w.in_proj_a.f32_parts(),
315 w.in_proj_b.f32_parts(),
316 w.out_proj.q1_parts(),
317 d.gate_proj.q1_parts(),
318 d.up_proj.q1_parts(),
319 d.down_proj.q1_parts(),
320 );
321 let (Some(qkv), Some(z), Some(a), Some(b), Some(out), Some(g), Some(u), Some(dn)) =
322 parts
323 else {
324 break;
325 };
326 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
327 model_ref.get_or_insert_with(|| model.clone());
328 }
329 inter = g.1;
330 layers.push(GdnGpuLayer {
331 attn_norm: &lw.input_norm,
332 post_norm: &lw.post_norm,
333 qkv,
334 z,
335 a,
336 b,
337 out,
338 gate: g,
339 up: u,
340 down: dn,
341 conv1d: &w.conv1d,
342 a_log: &w.a_log,
343 dt_bias: &w.dt_bias,
344 gnorm: &w.norm,
345 });
346 end += 1;
347 }
348 if layers.is_empty() {
349 return start;
350 }
351 let Some(model) = model_ref else { return start };
352 let gcfg = GdnGpuCfg {
353 nv: cfg.num_v_heads,
354 nk: cfg.num_k_heads,
355 dk: cfg.key_head_dim,
356 dv: cfg.value_head_dim,
357 kk: cfg.conv_kernel,
358 hidden: self.hidden_size,
359 inter,
360 c_dim: cfg.conv_dim(),
361 eps: cfg.rms_eps as f32,
362 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
363 };
364 let want = cfg.state_len();
366 for l in &mut self.kv_cache.layers[start..end] {
367 if l.linear_state.len() != want {
368 l.linear_state = vec![0f32; want];
369 }
370 }
371 let mut states: Vec<&mut [f32]> = self.kv_cache.layers[start..end]
372 .iter_mut()
373 .map(|l| l.linear_state.as_mut_slice())
374 .collect();
375 if crate::gpu::gdn_block(&model, &layers, &mut states, &gcfg, h) {
376 end
377 } else {
378 start
379 }
380 }
381
382 pub fn new(
383 tokenizer: Tokenizer,
384 weights: PipelineWeights,
385 hidden_size: usize,
386 intermediate_size: usize,
387 num_heads: usize,
388 num_kv_heads: usize,
389 head_dim: usize,
390 num_layers: usize,
391 vocab_size: usize,
392 rms_eps: f64,
393 rope_base: f32,
394 norm_style: NormStyle,
395 max_seq_len: usize,
396 sampler_config: SamplerConfig,
397 ) -> Self {
398 let rng = match sampler_config.seed {
399 Some(s) => SplitMix64::new(s),
400 None => SplitMix64::from_entropy(),
401 };
402 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
403 let pool = Pool::from_env();
404 if let Some(p) = &pool {
405 tracing::info!("worker pool: {} threads", p.n_workers());
406 }
407 Self {
408 tokenizer: std::sync::Arc::new(tokenizer),
409 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
410 sampler_config,
411 weights,
412 hidden_size,
413 intermediate_size,
414 num_heads,
415 num_kv_heads,
416 head_dim,
417 num_layers,
418 vocab_size,
419 rms_eps,
420 rope_base,
421 norm_style,
422 rotary_dim: head_dim,
423 vmf_cfg: None,
424 gdn_cfg: None,
425 mtp: None,
426 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
427 rng,
428 inv_freq,
429 ws: ForwardScratch::new(hidden_size),
430 pool,
431 model: None,
432 dyn_force_f32: false,
433 dyn_skill_layers: Vec::new(),
434 dyn_active: None,
435 dyn_blend_loaded: false,
436 dyn_phi_layer: None,
437 dyn_phi_ema: Vec::new(),
438 dyn_phi_seen: 0,
439 dyn_router: None,
440 o1_cfg: None,
441 o1_flags: Vec::new(),
442 trace: false,
443 calib_temp: 1.0,
444 }
445 }
446
447 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
454 self.o1_flags = match &cfg {
455 Some(c) => {
456 let mut flags = c.layer_flags(self.num_layers);
457 for (li, f) in flags.iter_mut().enumerate() {
458 if *f && !matches!(self.weights.layers[li].attn, AttnKind::Full { .. }) {
459 *f = false;
460 }
461 }
462 flags
463 }
464 None => Vec::new(),
465 };
466 if let Some(c) = &cfg {
467 let n = self.o1_flags.iter().filter(|&&f| f).count();
468 tracing::info!(
469 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
470 self.num_layers, c.m, c.w, c.sink, c.rect
471 );
472 }
473 self.o1_cfg = cfg;
474 }
475
476 pub fn o1_active(&self) -> bool {
478 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
479 }
480
481 fn o1_begin(&mut self) {
483 if let Some(c) = &self.o1_cfg {
484 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
485 for (li, &f) in self.o1_flags.iter().enumerate() {
486 if f {
487 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
488 }
489 }
490 }
491 }
492
493 fn o1_seal(&mut self) {
496 if self.o1_cfg.is_none() {
497 return;
498 }
499 for li in 0..self.num_layers {
500 if self.o1_flags.get(li).copied().unwrap_or(false) {
501 self.kv_cache.layers[li].o1_seal(self.num_heads);
502 }
503 }
504 }
505
506 pub fn set_trace(&mut self, on: bool) {
508 self.trace = on;
509 }
510
511 pub fn set_calib_temp(&mut self, t: f32) {
514 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
515 }
516
517 pub fn calib_temp(&self) -> f32 {
519 self.calib_temp
520 }
521
522 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
525 self.rotary_dim = rotary_dim.min(self.head_dim);
526 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
527 }
528
529 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
530 QwenAttnCfg {
531 num_heads: self.num_heads,
532 num_kv_heads: self.num_kv_heads,
533 head_dim: self.head_dim,
534 hidden_size: self.hidden_size,
535 position,
536 inv_freq: &self.inv_freq,
537 rotary_dim: self.rotary_dim,
538 q_norm: None,
539 k_norm: None,
540 output_gate: false,
541 bias: None,
542 rms_eps: self.rms_eps,
543 norm_style: self.norm_style,
544 pool: self.pool.as_deref(),
545 }
546 }
547
548 pub fn generate(
550 &mut self,
551 prompt: &str,
552 max_tokens: usize,
553 task_mask: Option<&TaskMask>,
554 on_token: Option<TokenCallback>,
555 ) -> Result<GenerateResult, String> {
556 let input_ids = self.tokenizer.encode(prompt);
557 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
558 }
559
560 pub fn generate_from_ids(
568 &mut self,
569 input_ids: &[u32],
570 max_tokens: usize,
571 task_mask: Option<&TaskMask>,
572 mut on_token: Option<TokenCallback>,
573 ) -> Result<GenerateResult, String> {
574 if input_ids.is_empty() {
575 return Err("empty prompt: nothing to generate from".to_string());
576 }
577
578 self.kv_cache.clear();
580 self.o1_begin();
581
582 let spec_active = self.speculative
586 && self.mtp.is_some()
587 && task_mask.is_none()
588 && !self.o1_active()
589 && self.sampler_config.temperature < 1e-6;
590 let mut mtp = if spec_active { self.mtp.take() } else { None };
593 if let Some(m) = &mut mtp {
594 m.kv.clear();
595 }
596 let mut router = if mtp.is_none() { self.dyn_router.take() } else { None };
600 if let Some(r) = &mut router {
601 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
604 }
605
606 let mut all_ids = input_ids.to_vec();
607 let mut generated = 0usize;
608 let mut finish_reason = "max_tokens".to_string();
609 let mut drafted = 0usize;
610 let mut accepted = 0usize;
611 let mut confidence: Vec<f32> = Vec::new();
612 let trace_on = self.trace;
613 let calib_temp = self.calib_temp;
614 let mut traces: Vec<TokenTrace> = Vec::new();
615
616 let mut hidden = vec![0.0f32; self.hidden_size];
622 let mut pos = 0usize;
623 let dyn_prefill = router.is_some();
628 if task_mask.is_none() && !dyn_prefill && prefill_batched() && input_ids.len() > 2 {
629 const CHUNK: usize = 48;
635 let hs = self.hidden_size;
636 while pos < input_ids.len() {
637 let end = (pos + CHUNK).min(input_ids.len());
638 let hb = self.prefill_batch(&input_ids[pos..end], pos);
639 if let Some(m) = &mut mtp {
640 for p in pos..end {
641 if p + 1 < input_ids.len() {
642 let _ = self.mtp_step(
643 m,
644 &hb[(p - pos) * hs..(p - pos + 1) * hs],
645 input_ids[p + 1],
646 p,
647 );
648 }
649 }
650 }
651 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
652 pos = end;
653 }
654 }
655 if task_mask.is_none() && !dyn_prefill {
656 while pos + 1 < input_ids.len() {
657 let e1 = self.embed_single(input_ids[pos]);
658 let e2 = self.embed_single(input_ids[pos + 1]);
659 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
660 self.commit_linear_scratch();
662 if let Some(m) = &mut mtp {
663 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
664 if pos + 2 < input_ids.len() {
665 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
666 }
667 }
668 hidden = h2;
669 pos += 2;
670 }
671 }
672 while pos < input_ids.len() {
673 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
674 if let Some(m) = &mut mtp {
675 if pos + 1 < input_ids.len() {
676 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
677 }
678 }
679 pos += 1;
680 }
681 self.o1_seal();
684
685 macro_rules! commit {
687 ($id:expr) => {{
688 all_ids.push($id);
689 generated += 1;
690 if self.tokenizer.is_eos($id) {
691 finish_reason = "stop".to_string();
692 false
693 } else {
694 let token_text = self.tokenizer.decode_token($id);
695 let mut go = true;
696 if let Some(ref mut cb) = on_token {
697 if !cb(&token_text) {
698 finish_reason = "cancelled".to_string();
699 go = false;
700 }
701 }
702 go
703 }
704 }};
705 }
706
707 let mut next_pos = input_ids.len();
709 'decode: while generated < max_tokens {
710 inference::rms_norm_into(
711 &hidden,
712 &self.weights.final_norm,
713 self.rms_eps,
714 self.norm_style,
715 &mut self.ws.n1,
716 );
717 let mut logits = self.lm_head_forward(&self.ws.n1);
718 let t_next = sampler::sample(&logits, &self.sampler_config, &all_ids, &mut self.rng);
719 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
720 attention::recycle_buf(&mut logits);
721 if trace_on {
722 let skill = router.as_ref().and_then(|r| r.active_id());
726 traces.push(TokenTrace {
727 t: generated,
728 token_id: t_next,
729 confidence: *confidence.last().unwrap(),
730 active_skill: skill,
731 recon: None,
732 switched: false,
733 });
734 }
735 if !commit!(t_next) {
736 break 'decode;
737 }
738 if generated >= max_tokens {
739 break 'decode;
740 }
741
742 if self.kv_cache.needs_eviction() {
743 let keep = (self.kv_cache.max_seq_len / 2).max(1);
744 self.kv_cache.evict(keep);
745 }
746
747 match &mut mtp {
748 Some(m) if generated + 1 < max_tokens => {
750 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
751 drafted += 1;
752 let emb1 = self.embed_single(t_next);
753 let emb2 = self.embed_single(draft);
754 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
755
756 inference::rms_norm_into(
757 &h1,
758 &self.weights.final_norm,
759 self.rms_eps,
760 self.norm_style,
761 &mut self.ws.n1,
762 );
763 let mut logits1 = self.lm_head_forward(&self.ws.n1);
764 let t_after =
765 sampler::sample(&logits1, &self.sampler_config, &all_ids, &mut self.rng);
766 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
767 attention::recycle_buf(&mut logits1);
768 if trace_on {
769 traces.push(TokenTrace {
772 t: generated,
773 token_id: t_after,
774 confidence: *confidence.last().unwrap(),
775 active_skill: None,
776 recon: None,
777 switched: false,
778 });
779 }
780 let stop = !commit!(t_after);
781
782 if t_after == draft {
783 accepted += 1;
784 self.commit_linear_scratch();
785 let _ = self.mtp_step(m, &h1, t_after, next_pos);
786 hidden = h2;
787 next_pos += 2;
788 } else {
789 for layer in &mut self.kv_cache.layers {
791 layer.truncate_last(1);
792 }
793 if !stop {
794 let _ = self.mtp_step(m, &h1, t_after, next_pos);
795 hidden = self
796 .forward_layers(&self.embed_single(t_after), next_pos + 1, None);
797 }
798 next_pos += 2;
799 }
800 if stop {
801 break 'decode;
802 }
803 }
804 _ => {
806 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
807 next_pos += 1;
808 if let Some(r) = &mut router {
811 let phi = self.dyn_phi_ema.clone();
812 let decision = r.step(&phi, generated);
813 if let Some(new_active) = decision {
814 let _ = self.set_active_skill(new_active);
815 }
816 if trace_on {
819 if let Some(last) = traces.last_mut() {
820 let e = r.last_best_e();
821 last.recon = e.is_finite().then_some(e);
822 last.switched = decision.is_some();
823 }
824 }
825 }
826 }
827 }
828 }
829
830 if router.is_some() {
832 let _ = self.set_active_skill(None);
833 }
834 self.dyn_router = router.or(self.dyn_router.take());
835 self.mtp = mtp.or(self.mtp.take());
836
837 let output_ids = &all_ids[input_ids.len()..];
838 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
840 Ok(GenerateResult {
841 text: self.tokenizer.decode(output_ids),
842 token_ids: output_ids.to_vec(),
843 prompt_tokens: input_ids.len(),
844 tokens_generated: generated,
845 finish_reason,
846 mtp_drafted: drafted,
847 mtp_accepted: accepted,
848 token_confidence: confidence,
849 traces,
850 })
851 }
852
853 fn mtp_step(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) -> u32 {
857 let e = self.embed_single(next_token);
861 let mut cat = vec![0.0f32; 2 * self.hidden_size];
862 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
863 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
864 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
865 let mut x = vec![0.0f32; self.hidden_size];
866 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
867
868 let lw = &m.layer;
870 inference::rms_norm_into(&x, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
871 let attn = match &lw.attn {
872 AttnKind::Full {
873 wq,
874 wk,
875 wv,
876 wo,
877 q_norm,
878 k_norm,
879 output_gate,
880 bias,
881 } => {
882 let mut cfg = self.attn_cfg(position);
883 cfg.q_norm = q_norm.as_deref();
884 cfg.k_norm = k_norm.as_deref();
885 cfg.output_gate = *output_gate;
886 cfg.bias = bias
887 .as_ref()
888 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
889 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
890 }
891 AttnKind::Linear(_) | AttnKind::LinearGdn(_) => {
892 unreachable!("MTP block is full attention")
893 }
894 };
895 for (i, &a) in attn.iter().enumerate() {
896 x[i] += a;
897 }
898 inference::rms_norm_into(&x, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
899 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref());
900 for (i, &f) in ffn.iter().enumerate() {
901 x[i] += f;
902 }
903
904 inference::rms_norm_into(&x, &m.final_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
905 let mut lg = self.lm_head_forward(&self.ws.n1);
906 let draft = sampler::argmax(&lg);
907 attention::recycle_buf(&mut lg);
908 draft
909 }
910
911 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
915 let emb1 = self.embed_single(1);
916 let emb2 = self.embed_single(2);
917 let pos = self.kv_cache.seq_len();
918
919 let t0 = std::time::Instant::now();
920 for _ in 0..iters {
921 let _ = self.forward_layers(&emb1, pos, None);
922 let _ = self.forward_layers(&emb2, pos + 1, None);
923 for l in &mut self.kv_cache.layers {
924 l.truncate_last(2);
925 }
926 }
927 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
928
929 let t1 = std::time::Instant::now();
930 for _ in 0..iters {
931 let _ = self.forward_pair(&emb1, &emb2, pos);
932 for l in &mut self.kv_cache.layers {
933 l.truncate_last(2);
934 }
935 }
936 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
937 (singles_ms, pair_ms)
938 }
939
940 fn forward_pair(&mut self, emb1: &[f32], emb2: &[f32], position: usize) -> (Vec<f32>, Vec<f32>) {
945 let mut h1 = emb1.to_vec();
946 let mut h2 = emb2.to_vec();
947 let (nh, nkv, hd, hs, rd, eps) = (
948 self.num_heads,
949 self.num_kv_heads,
950 self.head_dim,
951 self.hidden_size,
952 self.rotary_dim,
953 self.rms_eps,
954 );
955 let inv_freq = self.inv_freq.clone();
956 let pool = self.pool.clone();
957
958 for li in 0..self.num_layers {
959 let lw = &self.weights.layers[li];
960 inference::rms_norm_into(&h1, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
963 inference::rms_norm_into(&h2, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n2);
964
965 let (a1, a2) = match &lw.attn {
966 AttnKind::Linear(w) => {
967 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
968 let layer = &mut self.kv_cache.layers[li];
969 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
970 vmf_phase_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
971 }
972 AttnKind::LinearGdn(w) => {
973 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
974 let layer = &mut self.kv_cache.layers[li];
975 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
976 gdn_pair(&self.ws.n1, &self.ws.n2, w, &cfg, state, scratch, self.pool.as_deref())
977 }
978 AttnKind::Full {
979 wq,
980 wk,
981 wv,
982 wo,
983 q_norm,
984 k_norm,
985 output_gate,
986 bias,
987 } => {
988 let cfg = QwenAttnCfg {
989 num_heads: nh,
990 num_kv_heads: nkv,
991 head_dim: hd,
992 hidden_size: hs,
993 position,
994 inv_freq: &inv_freq,
995 rotary_dim: rd,
996 q_norm: q_norm.as_deref(),
997 k_norm: k_norm.as_deref(),
998 output_gate: *output_gate,
999 bias: bias
1000 .as_ref()
1001 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1002 rms_eps: eps,
1003 norm_style: self.norm_style,
1004 pool: pool.as_deref(),
1005 };
1006 attention::qwen_attention_pair(
1007 &self.ws.n1,
1008 &self.ws.n2,
1009 wq,
1010 wk,
1011 wv,
1012 wo,
1013 &mut self.kv_cache.layers[li],
1014 &cfg,
1015 )
1016 }
1017 };
1018 for i in 0..self.hidden_size {
1019 h1[i] += a1[i];
1020 h2[i] += a2[i];
1021 }
1022 let (mut a1, mut a2) = (a1, a2);
1023 attention::recycle_buf(&mut a1);
1024 attention::recycle_buf(&mut a2);
1025
1026 let lw = &self.weights.layers[li];
1027 inference::rms_norm_into(&h1, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1028 inference::rms_norm_into(&h2, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p2);
1029 let (f1, f2) =
1030 ffn_forward_pair(&lw.ffn, &self.ws.p1, &self.ws.p2, self.pool.as_deref());
1031 for i in 0..self.hidden_size {
1032 h1[i] += f1[i];
1033 h2[i] += f2[i];
1034 }
1035 let (mut f1, mut f2) = (f1, f2);
1036 attention::recycle_buf(&mut f1);
1037 attention::recycle_buf(&mut f2);
1038 }
1039 (h1, h2)
1040 }
1041
1042 fn commit_linear_scratch(&mut self) {
1044 for layer in &mut self.kv_cache.layers {
1045 if !layer.linear_scratch.is_empty() {
1046 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
1047 layer.linear_scratch.clear();
1048 }
1049 }
1050 }
1051
1052 pub fn forward_ids(
1055 &mut self,
1056 ids: &[u32],
1057 task_mask: Option<&TaskMask>,
1058 ) -> Result<Vec<f32>, String> {
1059 if ids.is_empty() {
1060 return Err("empty id sequence".to_string());
1061 }
1062 self.kv_cache.clear();
1063 self.o1_begin();
1064 let mut hidden = vec![0.0f32; self.hidden_size];
1065 let mut pos = 0usize;
1066 if task_mask.is_none() && prefill_batched() && ids.len() > 2 {
1067 const CHUNK: usize = 48;
1071 let hs = self.hidden_size;
1072 while pos < ids.len() {
1073 let end = (pos + CHUNK).min(ids.len());
1074 let hb = self.prefill_batch(&ids[pos..end], pos);
1075 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1076 pos = end;
1077 }
1078 }
1079 if task_mask.is_none() {
1080 while pos + 1 < ids.len() {
1081 let e1 = self.embed_single(ids[pos]);
1082 let e2 = self.embed_single(ids[pos + 1]);
1083 let (_, h2) = self.forward_pair(&e1, &e2, pos);
1084 self.commit_linear_scratch();
1085 hidden = h2;
1086 pos += 2;
1087 }
1088 }
1089 while pos < ids.len() {
1090 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
1091 pos += 1;
1092 }
1093 self.o1_seal();
1097 let normed = inference::rms_norm(
1098 &hidden,
1099 &self.weights.final_norm,
1100 self.rms_eps,
1101 self.norm_style,
1102 );
1103 Ok(self.lm_head_forward(&normed))
1104 }
1105
1106 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
1113 let (nll, cnt) = self.nll_ids_from(ids, 0);
1114 (nll / cnt.max(1) as f64).exp()
1115 }
1116
1117 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
1126 self.kv_cache.clear();
1127 let mut nll = 0f64;
1128 let mut cnt = 0usize;
1129 if prefill_batched() {
1130 const CHUNK: usize = 128;
1136 const LM_SUB: usize = 32;
1137 let n = ids.len().saturating_sub(1);
1138 let hs = self.hidden_size;
1139 let rows = self.weights.lm_head.rows();
1140 let mut pos = 0usize;
1141 while pos < n {
1142 let end = (pos + CHUNK).min(n);
1143 let bsz = end - pos;
1144 let hb = self.prefill_batch(&ids[pos..end], pos);
1145 let mut k0 = 0usize;
1146 while k0 < bsz {
1147 let k1 = (k0 + LM_SUB).min(bsz);
1148 let sb = k1 - k0;
1149 if pos + k1 <= start {
1152 k0 = k1;
1153 continue;
1154 }
1155 let mut normed = vec![0.0f32; sb * hs];
1156 for k in 0..sb {
1157 let r = inference::rms_norm(
1158 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
1159 &self.weights.final_norm,
1160 self.rms_eps,
1161 self.norm_style,
1162 );
1163 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
1164 }
1165 let mut logits = vec![0.0f32; sb * rows];
1166 self.weights
1167 .lm_head
1168 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
1169 for k in 0..sb {
1170 if pos + k0 + k < start {
1171 continue;
1172 }
1173 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
1174 let target = ids[pos + k0 + k + 1] as usize;
1175 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1176 let lse: f64 = lg
1177 .iter()
1178 .map(|&v| ((v - max) as f64).exp())
1179 .sum::<f64>()
1180 .ln()
1181 + max as f64;
1182 nll += lse - lg[target] as f64;
1183 cnt += 1;
1184 }
1185 k0 = k1;
1186 }
1187 pos = end;
1188 }
1189 self.kv_cache.clear();
1190 return (nll, cnt);
1191 }
1192 for pos in 0..ids.len().saturating_sub(1) {
1193 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1194 if pos < start {
1195 continue;
1196 }
1197 let normed = inference::rms_norm(
1198 &hidden,
1199 &self.weights.final_norm,
1200 self.rms_eps,
1201 self.norm_style,
1202 );
1203 let logits = self.lm_head_forward(&normed);
1204 let target = ids[pos + 1] as usize;
1205 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1206 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1207 + max as f64;
1208 nll += lse - logits[target] as f64;
1209 cnt += 1;
1210 }
1211 self.kv_cache.clear();
1212 (nll, cnt)
1213 }
1214
1215 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
1231 self.kv_cache.clear();
1232 self.o1_begin();
1233 let n = ids.len().saturating_sub(1);
1234 let p = prefill.min(n);
1235 let mut pos = 0usize;
1237 if prefill_batched() {
1238 const CHUNK: usize = 128;
1239 while pos < p {
1240 let end = (pos + CHUNK).min(p);
1241 let _ = self.prefill_batch(&ids[pos..end], pos);
1242 pos = end;
1243 }
1244 } else {
1245 while pos < p {
1246 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1247 pos += 1;
1248 }
1249 }
1250 self.o1_seal();
1251
1252 let mut nll = 0f64;
1253 let mut cnt = 0usize;
1254 for pos in p..n {
1255 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1256 let normed = inference::rms_norm(
1257 &hidden,
1258 &self.weights.final_norm,
1259 self.rms_eps,
1260 self.norm_style,
1261 );
1262 let logits = self.lm_head_forward(&normed);
1263 let target = ids[pos + 1] as usize;
1264 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1265 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1266 + max as f64;
1267 nll += lse - logits[target] as f64;
1268 cnt += 1;
1269 }
1270 self.kv_cache.clear();
1271 (nll, cnt)
1272 }
1273
1274 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
1282 self.kv_cache.clear();
1283 let n = ids.len().saturating_sub(1);
1284 let mut correct = Vec::with_capacity(n);
1285 let mut pmax = Vec::with_capacity(n);
1286 for pos in 0..n {
1287 let emb = self.embed_single(ids[pos]);
1288 let hidden = self.forward_layers(&emb, pos, None);
1289 let normed = inference::rms_norm(
1290 &hidden,
1291 &self.weights.final_norm,
1292 self.rms_eps,
1293 self.norm_style,
1294 );
1295 let logits = self.lm_head_forward(&normed);
1296 let target = ids[pos + 1] as usize;
1297 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
1298 for (i, &v) in logits.iter().enumerate() {
1299 if v > mval {
1300 mval = v;
1301 amax = i;
1302 }
1303 }
1304 correct.push(amax == target);
1305 let row: Vec<f32> = temps
1306 .iter()
1307 .map(|&t| {
1308 let tt = t.max(1e-3);
1309 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
1310 1.0 / s.max(1e-12) })
1312 .collect();
1313 pmax.push(row);
1314 }
1315 self.kv_cache.clear();
1316 (correct, pmax)
1317 }
1318
1319 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
1326 let mut router = match self.dyn_router.take() {
1327 Some(r) => r,
1328 None => return (self.ppl_ids(ids), 0),
1329 };
1330 router.reset();
1331 self.dyn_phi_seen = 0;
1332 let _ = self.set_active_skill(None);
1333
1334 self.kv_cache.clear();
1335 let mut nll = 0f64;
1336 let mut cnt = 0usize;
1337 for pos in 0..ids.len().saturating_sub(1) {
1338 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
1339 let normed = inference::rms_norm(
1340 &hidden,
1341 &self.weights.final_norm,
1342 self.rms_eps,
1343 self.norm_style,
1344 );
1345 let logits = self.lm_head_forward(&normed);
1346 let target = ids[pos + 1] as usize;
1347 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1348 let lse: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum::<f64>().ln()
1349 + max as f64;
1350 nll += lse - logits[target] as f64;
1351 cnt += 1;
1352 let phi = self.dyn_phi_ema.clone();
1354 if let Some(new_active) = router.step(&phi, pos) {
1355 let _ = self.set_active_skill(new_active);
1356 }
1357 }
1358 let switches = router.switches.len();
1359 let _ = self.set_active_skill(None);
1360 self.dyn_router = Some(router);
1361 self.kv_cache.clear();
1362 ((nll / cnt.max(1) as f64).exp(), switches)
1363 }
1364
1365 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
1367 self.kv_cache.clear();
1368 let mut acc = vec![0f32; self.hidden_size];
1369 for (pos, &id) in ids.iter().enumerate() {
1370 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
1371 for (a, v) in acc.iter_mut().zip(&h) {
1372 *a += v;
1373 }
1374 }
1375 let n = ids.len().max(1) as f32;
1376 for a in acc.iter_mut() {
1377 *a /= n;
1378 }
1379 self.kv_cache.clear();
1380 acc
1381 }
1382
1383 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
1389 let b = ids.len();
1390 let hs = self.hidden_size;
1391 let mut h: Vec<f32> = Vec::with_capacity(b * hs);
1392 for &id in ids {
1393 h.extend_from_slice(&self.embed_single(id));
1394 }
1395 let (nh, nkv, hd, rd, eps) = (
1396 self.num_heads,
1397 self.num_kv_heads,
1398 self.head_dim,
1399 self.rotary_dim,
1400 self.rms_eps,
1401 );
1402 let inv_freq = self.inv_freq.clone();
1403 let pool = self.pool.clone();
1404 let norm_style = self.norm_style;
1405
1406 for li in 0..self.num_layers {
1407 crate::gpu::set_layer(li as i64); let lw = &self.weights.layers[li];
1409 match &lw.attn {
1411 AttnKind::LinearGdn(w) => {
1412 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1414 let mut normed = vec![0.0f32; b * hs];
1415 for bi in 0..b {
1416 let r = inference::rms_norm(
1417 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1418 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1419 }
1420 let attn = crate::linear_core::gdn_forward_batch(
1421 &normed, b, w, &cfg,
1422 &mut self.kv_cache.layers[li].linear_state,
1423 pool.as_deref(),
1424 );
1425 for (dst, &a) in h.iter_mut().zip(&attn) {
1426 *dst += a;
1427 }
1428 }
1429 AttnKind::Full {
1430 wq, wk, wv, wo, q_norm, k_norm, output_gate, bias,
1431 } => {
1432 let mut normed = vec![0.0f32; b * hs];
1436 for bi in 0..b {
1437 inference::rms_norm_into(
1438 &h[bi * hs..(bi + 1) * hs],
1439 &lw.input_norm,
1440 eps,
1441 norm_style,
1442 &mut normed[bi * hs..(bi + 1) * hs],
1443 );
1444 }
1445 let cfg = QwenAttnCfg {
1446 num_heads: nh,
1447 num_kv_heads: nkv,
1448 head_dim: hd,
1449 hidden_size: hs,
1450 position: start_pos,
1451 inv_freq: &inv_freq,
1452 rotary_dim: rd,
1453 q_norm: q_norm.as_deref(),
1454 k_norm: k_norm.as_deref(),
1455 output_gate: *output_gate,
1456 bias: bias.as_ref().map(|(a, b, c)| {
1457 (a.as_slice(), b.as_slice(), c.as_slice())
1458 }),
1459 rms_eps: eps,
1460 norm_style,
1461 pool: pool.as_deref(),
1462 };
1463 let attn = attention::qwen_attention_batch(
1464 &normed, b, wq, wk, wv, wo,
1465 &mut self.kv_cache.layers[li], &cfg);
1466 for (dst, &a) in h.iter_mut().zip(&attn) {
1467 *dst += a;
1468 }
1469 }
1470 AttnKind::Linear(w) => {
1471 for bi in 0..b {
1472 let normed = inference::rms_norm(
1473 &h[bi * hs..(bi + 1) * hs], &lw.input_norm, eps, norm_style);
1474 vmf_phase_forward(
1475 &normed, w,
1476 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
1477 &mut self.kv_cache.layers[li].linear_state,
1478 pool.as_deref(),
1479 )
1480 .iter()
1481 .enumerate()
1482 .for_each(|(i, &a)| h[bi * hs + i] += a);
1483 }
1484 }
1485 }
1486
1487 let lw = &self.weights.layers[li];
1489 let mut post = vec![0.0f32; b * hs];
1490 for bi in 0..b {
1491 let r = inference::rms_norm(
1492 &h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
1493 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
1494 }
1495 let ffn = match &lw.ffn {
1496 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
1497 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref()),
1498 };
1499 for (dst, &f) in h.iter_mut().zip(&ffn) {
1500 *dst += f;
1501 }
1502 }
1503 crate::gpu::set_layer(-1); h
1505 }
1506
1507 fn embed_single(&self, id: u32) -> Vec<f32> {
1509 let mut out = vec![0.0f32; self.hidden_size];
1510 if (id as usize) < self.weights.embed_tokens.rows() {
1511 self.weights.embed_tokens.row_f32(id as usize, &mut out);
1512 }
1513 out
1514 }
1515
1516 fn forward_layers(
1518 &mut self,
1519 hidden: &[f32],
1520 position: usize,
1521 task_mask: Option<&TaskMask>,
1522 ) -> Vec<f32> {
1523 self.forward_layers_upto(hidden, position, task_mask, None)
1524 }
1525
1526 fn forward_layers_upto(
1528 &mut self,
1529 hidden: &[f32],
1530 position: usize,
1531 task_mask: Option<&TaskMask>,
1532 upto: Option<usize>,
1533 ) -> Vec<f32> {
1534 let mut h = hidden.to_vec();
1535 let (nh, nkv, hd, hs, rd, eps) = (
1538 self.num_heads,
1539 self.num_kv_heads,
1540 self.head_dim,
1541 self.hidden_size,
1542 self.rotary_dim,
1543 self.rms_eps,
1544 );
1545 let inv_freq = self.inv_freq.clone();
1546 let pool = self.pool.clone();
1547
1548 #[cfg(target_os = "macos")]
1549 let mut gdn_skip_until = 0usize;
1550 for li in 0..self.num_layers {
1551 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
1553 if li > u {
1554 break;
1555 }
1556 }
1557 if let Some(mask) = task_mask {
1558 if !mask.layer_alive(li) {
1559 continue; }
1561 }
1562 #[cfg(target_os = "macos")]
1565 {
1566 if li < gdn_skip_until {
1567 continue;
1568 }
1569 if task_mask.is_none() {
1570 let end = self.gdn_block_gpu(li, upto, &mut h);
1571 if end > li {
1572 gdn_skip_until = end;
1573 continue;
1574 }
1575 }
1576 }
1577
1578 let lw = &self.weights.layers[li];
1579 inference::rms_norm_into(&h, &lw.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
1582
1583 let attn_out = match &lw.attn {
1584 AttnKind::Linear(w) => {
1585 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
1586 vmf_phase_forward(
1587 &self.ws.n1,
1588 w,
1589 &cfg,
1590 &mut self.kv_cache.layers[li].linear_state,
1591 self.pool.as_deref(),
1592 )
1593 }
1594 AttnKind::LinearGdn(w) => {
1595 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
1596 gdn_forward(
1597 &self.ws.n1,
1598 w,
1599 &cfg,
1600 &mut self.kv_cache.layers[li].linear_state,
1601 self.pool.as_deref(),
1602 )
1603 }
1604 AttnKind::Full {
1605 wq,
1606 wk,
1607 wv,
1608 wo,
1609 q_norm,
1610 k_norm,
1611 output_gate,
1612 bias,
1613 } if self.kv_cache.layers[li].o1_sealed() => {
1614 let cfg = QwenAttnCfg {
1617 num_heads: nh,
1618 num_kv_heads: nkv,
1619 head_dim: hd,
1620 hidden_size: hs,
1621 position,
1622 inv_freq: &inv_freq,
1623 rotary_dim: rd,
1624 q_norm: q_norm.as_deref(),
1625 k_norm: k_norm.as_deref(),
1626 output_gate: *output_gate,
1627 bias: bias
1628 .as_ref()
1629 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1630 rms_eps: eps,
1631 norm_style: self.norm_style,
1632 pool: pool.as_deref(),
1633 };
1634 attention::qwen_attention_nystrom(
1635 &self.ws.n1,
1636 wq,
1637 wk,
1638 wv,
1639 wo,
1640 &mut self.kv_cache.layers[li],
1641 &cfg,
1642 )
1643 }
1644 AttnKind::Full {
1645 wq,
1646 wk,
1647 wv,
1648 wo,
1649 q_norm,
1650 k_norm,
1651 output_gate,
1652 bias,
1653 } => {
1654 let masked = task_mask
1655 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
1656 .unwrap_or(false);
1657 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
1658 match (masked, f32_view) {
1659 (true, (Some(q), Some(k), Some(v), Some(o))) => {
1662 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
1663 attention::multi_head_attention(
1664 &self.ws.n1,
1665 q,
1666 k,
1667 v,
1668 o,
1669 &mut self.kv_cache.layers[li],
1670 self.num_heads,
1671 self.num_kv_heads,
1672 self.head_dim,
1673 self.hidden_size,
1674 position,
1675 &active_heads,
1676 &self.inv_freq,
1677 )
1678 }
1679 (masked, _) => {
1680 if masked {
1681 tracing::warn!(
1682 "layer {li}: head mask on quantized weights not \
1683 supported yet — executing dense"
1684 );
1685 }
1686 let cfg = QwenAttnCfg {
1687 num_heads: nh,
1688 num_kv_heads: nkv,
1689 head_dim: hd,
1690 hidden_size: hs,
1691 position,
1692 inv_freq: &inv_freq,
1693 rotary_dim: rd,
1694 q_norm: q_norm.as_deref(),
1695 k_norm: k_norm.as_deref(),
1696 output_gate: *output_gate,
1697 bias: bias
1698 .as_ref()
1699 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
1700 rms_eps: eps,
1701 norm_style: self.norm_style,
1702 pool: pool.as_deref(),
1703 };
1704 attention::qwen_attention(
1705 &self.ws.n1,
1706 wq,
1707 wk,
1708 wv,
1709 wo,
1710 &mut self.kv_cache.layers[li],
1711 &cfg,
1712 )
1713 }
1714 }
1715 }
1716 };
1717 for (i, &a) in attn_out.iter().enumerate() {
1718 h[i] += a;
1719 }
1720 let mut attn_out = attn_out;
1721 attention::recycle_buf(&mut attn_out);
1722
1723 let lw = &self.weights.layers[li];
1724 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
1725 let post_normed = &self.ws.p1;
1726
1727 let ffn_masked = task_mask
1728 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
1729 .unwrap_or(false);
1730 let f32_ffn = match &lw.ffn {
1733 FfnKind::Dense(d) => {
1734 (d.gate_proj.as_f32(), d.up_proj.as_f32(), d.down_proj.as_f32())
1735 }
1736 FfnKind::Moe(_) => (None, None, None),
1737 };
1738 let ffn_out = match (ffn_masked, f32_ffn) {
1739 (true, (Some(g), Some(u), Some(d))) => {
1740 let active = task_mask.unwrap().ffn_active_indices(li);
1741 inference::sparse_ffn_forward(
1742 &post_normed,
1743 g,
1744 u,
1745 d,
1746 self.hidden_size,
1747 self.intermediate_size,
1748 &active,
1749 self.pool.as_deref(),
1750 )
1751 }
1752 (true, _) => match &lw.ffn {
1756 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
1757 let active = task_mask.unwrap().ffn_active_indices(li);
1758 sparse_ffn_quant(
1759 d,
1760 &post_normed,
1761 &active,
1762 self.hidden_size,
1763 self.pool.as_deref(),
1764 )
1765 }
1766 FfnKind::Dense(d) => {
1771 let active = task_mask.unwrap().ffn_active_indices(li);
1772 let (gf, uf, df) = dequant_dense_f32(d);
1773 inference::sparse_ffn_forward(
1774 &post_normed,
1775 &gf,
1776 &uf,
1777 &df,
1778 self.hidden_size,
1779 self.intermediate_size,
1780 &active,
1781 self.pool.as_deref(),
1782 )
1783 }
1784 FfnKind::Moe(_) => {
1785 ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref())
1788 }
1789 },
1790 (false, _) => ffn_forward(&lw.ffn, &post_normed, self.pool.as_deref()),
1791 };
1792 for (i, &f) in ffn_out.iter().enumerate() {
1793 h[i] += f;
1794 }
1795 let mut ffn_out = ffn_out;
1796 attention::recycle_buf(&mut ffn_out);
1797
1798 if self.dyn_phi_layer == Some(li) {
1802 self.update_dyn_phi(&h);
1803 }
1804 }
1805 crate::gpu::set_layer(-1); h
1808 }
1809
1810 fn update_dyn_phi(&mut self, h: &[f32]) {
1813 const A: f32 = 0.2;
1814 if self.dyn_phi_ema.len() != h.len() {
1815 self.dyn_phi_ema = vec![0.0; h.len()];
1816 self.dyn_phi_seen = 0;
1817 }
1818 if self.dyn_phi_seen == 0 {
1819 self.dyn_phi_ema.copy_from_slice(h);
1820 } else {
1821 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
1822 *e = (1.0 - A) * *e + A * v;
1823 }
1824 }
1825 self.dyn_phi_seen += 1;
1826 }
1827
1828 pub fn dyn_phi(&self) -> &[f32] {
1830 &self.dyn_phi_ema
1831 }
1832
1833 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
1835 self.dyn_phi_layer = layer;
1836 self.dyn_phi_ema.clear();
1837 self.dyn_phi_seen = 0;
1838 }
1839
1840 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
1842 let Some(model) = &self.model else { return Vec::new() };
1843 model
1844 .header
1845 .skills
1846 .iter()
1847 .enumerate()
1848 .filter_map(|(i, sk)| {
1849 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
1850 let sel = sk.selection.as_ref()?;
1851 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
1852 })
1853 .collect()
1854 }
1855
1856 pub fn active_skill(&self) -> Option<usize> {
1858 self.dyn_active
1859 }
1860
1861 pub fn enable_dynamic_routing(&mut self) -> usize {
1866 use crate::swarm::{DynRouter, RoutableSkill};
1867 let Some(model) = self.model.clone() else { return 0 };
1868 if self.dyn_blend_loaded {
1871 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
1872 return 0;
1873 }
1874 if let Some(a) = self.dyn_active {
1878 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
1879 tracing::warn!(
1880 "loaded skill is not FFN-eligible — dynamic routing unavailable"
1881 );
1882 return 0;
1883 }
1884 }
1885 let hidden = self.hidden_size;
1886 let mut skills = Vec::new();
1887 for (idx, id, _phi) in self.dynamic_skills() {
1888 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
1889 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
1890 skills.push(rs);
1891 }
1892 }
1893 }
1894 if skills.is_empty() {
1895 return 0;
1896 }
1897 let phi = skills[0].phi_layer;
1899 if skills.iter().any(|s| s.phi_layer != phi) {
1900 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
1901 }
1902 let n = skills.len();
1903 self.set_dyn_phi_layer(Some(phi));
1904 self.dyn_router = Some(DynRouter::new(skills));
1905 n
1906 }
1907
1908 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
1910 self.dyn_router
1911 .as_ref()
1912 .map(|r| r.switches.clone())
1913 .unwrap_or_default()
1914 }
1915
1916 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
1919 let rows = self.weights.lm_head.rows();
1920 let mut logits = attention::take_buf(rows.min(self.vocab_size));
1921 self.weights
1922 .lm_head
1923 .matvec(hidden, &mut logits, self.pool.as_deref());
1924 logits.resize(self.vocab_size, 0.0);
1925 logits
1926 }
1927
1928 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
1933 self.kv_cache.clear();
1934 let mut hidden = vec![0.0f32; self.hidden_size];
1935 for (pos, &id) in ids.iter().enumerate() {
1936 let emb = self.embed_single(id);
1937 hidden = self.forward_layers(&emb, pos, task_mask);
1938 }
1939 inference::rms_norm_into(
1940 &hidden,
1941 &self.weights.final_norm,
1942 self.rms_eps,
1943 self.norm_style,
1944 &mut self.ws.n1,
1945 );
1946 self.lm_head_forward(&self.ws.n1)
1947 }
1948}
1949
1950pub fn create_test_pipeline(
1952 hidden_size: usize,
1953 intermediate_size: usize,
1954 num_heads: usize,
1955 num_kv_heads: usize,
1956 head_dim: usize,
1957 num_layers: usize,
1958 vocab_size: usize,
1959) -> Pipeline {
1960 let synth = |n: usize, salt: usize| -> Vec<f32> {
1963 (0..n)
1964 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
1965 .collect()
1966 };
1967 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
1968 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
1969 };
1970 let layer_weights: Vec<LayerWeights> = (0..num_layers)
1971 .map(|li| LayerWeights {
1972 input_norm: vec![1.0; hidden_size],
1973 post_norm: vec![1.0; hidden_size],
1974 ffn: FfnKind::Dense(DenseFfn {
1975 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
1976 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
1977 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
1978 }),
1979 attn: AttnKind::Full {
1980 bias: None,
1981 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
1982 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
1983 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
1984 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
1985 q_norm: None,
1986 k_norm: None,
1987 output_gate: false,
1988 },
1989 })
1990 .collect();
1991
1992 Pipeline::new(
1993 Tokenizer::byte_level(),
1994 PipelineWeights {
1995 embed_tokens: qt(vocab_size, hidden_size, 100),
1996 layers: layer_weights,
1997 lm_head: qt(vocab_size, hidden_size, 200),
1998 final_norm: vec![1.0; hidden_size],
1999 },
2000 hidden_size,
2001 intermediate_size,
2002 num_heads,
2003 num_kv_heads,
2004 head_dim,
2005 num_layers,
2006 vocab_size,
2007 1e-6,
2008 10_000.0,
2009 NormStyle::Qwen,
2010 4096,
2011 SamplerConfig {
2012 seed: Some(42),
2013 ..Default::default()
2014 },
2015 )
2016}
2017
2018fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
2021 let inter = d.gate_proj.rows();
2022 let hidden = d.down_proj.rows();
2023 let mut g = vec![0.0f32; b * inter];
2024 d.gate_proj.matmat(xs, b, &mut g, pool);
2025 let mut u = vec![0.0f32; b * inter];
2026 d.up_proj.matmat(xs, b, &mut u, pool);
2027 for i in 0..b * inter {
2028 g[i] = inference::silu(g[i]) * u[i];
2029 }
2030 let mut out = vec![0.0f32; b * hidden];
2031 d.down_proj.matmat(&g, b, &mut out, pool);
2032 out
2033}
2034
2035fn moe_ffn_batch(m: &MoeFfn, xs: &[f32], b: usize, hidden: usize, pool: Option<&Pool>) -> Vec<f32> {
2039 let ne = m.experts.len();
2040 let mut logits = vec![0.0f32; b * ne];
2041 m.router.matmat(xs, b, &mut logits, pool);
2042
2043 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
2046 {
2047 let mut st = m.stats.borrow_mut();
2048 if st.len() < ne {
2049 st.resize(ne, 0);
2050 }
2051 for bi in 0..b {
2052 let lg = &logits[bi * ne..(bi + 1) * ne];
2053 let mx = lg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2054 let mut p: Vec<f32> = lg.iter().map(|&l| (l - mx).exp()).collect();
2055 let sum: f32 = p.iter().sum();
2056 for v in &mut p {
2057 *v /= sum;
2058 }
2059 let mut order: Vec<usize> = (0..ne).collect();
2060 order.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y)));
2061 order.truncate(m.top_k);
2062 let wsum: f32 = if m.norm_topk_prob {
2063 order.iter().map(|&e| p[e]).sum()
2064 } else {
2065 1.0
2066 };
2067 for &e in &order {
2068 st[e] += 1;
2069 assign[e].push((bi, p[e] / wsum));
2070 }
2071 }
2072 }
2073
2074 let mut out = vec![0.0f32; b * hidden];
2075 let cols = m.experts[0].gate_proj.cols();
2076 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
2077 let sb = list.len();
2078 let mut sub = vec![0.0f32; sb * cols];
2079 for (k, &(bi, _)) in list.iter().enumerate() {
2080 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
2081 }
2082 let eo = dense_ffn_batch(d, &sub, sb, pool);
2083 for (k, &(bi, w)) in list.iter().enumerate() {
2084 for i in 0..hidden {
2085 out[bi * hidden + i] += w * eo[k * hidden + i];
2086 }
2087 }
2088 };
2089 for e in 0..ne {
2090 if !assign[e].is_empty() {
2091 run_expert(&m.experts[e], &assign[e]);
2092 }
2093 }
2094 if let Some((se, gate)) = &m.shared {
2095 let mut gl = vec![0.0f32; b];
2096 gate.matmat(xs, b, &mut gl, pool);
2097 let all: Vec<(usize, f32)> = (0..b)
2098 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
2099 .collect();
2100 run_expert(se, &all);
2101 }
2102 out
2103}
2104
2105thread_local! {
2106 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
2110 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
2111}
2112
2113fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2115 if crate::gpu::enabled_here()
2126 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
2127 {
2128 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
2129 crate::gpu::ProbeArm::Gpu
2130 } else {
2131 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
2132 };
2133 match arm {
2134 crate::gpu::ProbeArm::Gpu => {
2135 let t0 = std::time::Instant::now();
2136 if let Some(out) = dense_ffn_gpu(d, x, pool) {
2137 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
2138 return out;
2139 }
2140 }
2141 crate::gpu::ProbeArm::CpuTimed => {
2142 let t0 = std::time::Instant::now();
2143 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
2144 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
2145 return out;
2146 }
2147 crate::gpu::ProbeArm::Cpu => {
2148 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
2149 }
2150 }
2151 }
2152 dense_ffn_cpu(d, x, pool)
2153}
2154
2155fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2157 let inter = d.gate_proj.rows();
2158 FFN_SCRATCH.with(|s| {
2159 let mut s = s.borrow_mut();
2160 let [g, u, ..] = &mut *s;
2161 g.resize(inter, 0.0);
2162 u.resize(inter, 0.0);
2163 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
2165 for i in 0..inter {
2166 g[i] = inference::silu(g[i]) * u[i];
2167 }
2168 let mut out = attention::take_buf(d.down_proj.rows());
2169 d.down_proj.matvec(g, &mut out, pool);
2170 out
2171 })
2172}
2173
2174fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
2180 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
2183 return None;
2184 }
2185 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
2186 let mut model_ref = None;
2187 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
2188 let model = model_ref?;
2189 let hidden = jobs[0].down.1;
2190 let mut out = attention::take_buf(hidden);
2191 if crate::gpu::moe_block(&model, &jobs, &mut out) {
2192 Some(out)
2193 } else {
2194 let mut out = out;
2195 attention::recycle_buf(&mut out);
2196 None
2197 }
2198}
2199
2200#[allow(clippy::type_complexity)]
2205#[allow(clippy::type_complexity)]
2206fn moe_parts(
2207 t: &QTensor,
2208) -> Option<(&std::sync::Arc<cortiq_core::CmfModel>, usize, usize, usize, &[f32], &[f32], bool)> {
2209 match t {
2210 QTensor::Mapped {
2211 model,
2212 idx,
2213 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
2214 rows,
2215 cols,
2216 row_scale,
2217 col_field,
2218 ..
2219 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => {
2220 Some((model, *idx, *rows, *cols, row_scale, col_field, false))
2221 }
2222 QTensor::Mapped {
2224 model,
2225 idx,
2226 dtype: cortiq_core::TensorDtype::Q1,
2227 rows,
2228 cols,
2229 ..
2230 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true)),
2231 _ => None,
2232 }
2233}
2234
2235fn moe_push_job<'a>(
2237 d: &'a DenseFfn,
2238 x: &[f32],
2239 w: f32,
2240 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
2241 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
2242) -> Option<()> {
2243 use crate::qtensor::prescale;
2244 let (gm, gi, gr, gc, grs, gcf, gq1) = moe_parts(&d.gate_proj)?;
2245 let (_, ui, ur, uc, urs, ucf, uq1) = moe_parts(&d.up_proj)?;
2246 let (_, di, dr, dc, drs, dcf, dq1) = moe_parts(&d.down_proj)?;
2247 if gq1 != uq1 || uq1 != dq1 {
2248 return None; }
2250 model_ref.get_or_insert_with(|| gm.clone());
2251 let gdt = if gcf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
2252 let udt = if ucf.is_empty() { cortiq_core::TensorDtype::Q8Row } else { cortiq_core::TensorDtype::Q8_2f };
2253 jobs.push(crate::gpu::MoeJob {
2254 gate: (gi, gr, gc, grs),
2255 up: (ui, ur, uc, urs),
2256 down: (di, dr, dc, drs),
2257 xs_gate: prescale(x, gcf, gdt).into_owned(),
2258 xs_up: prescale(x, ucf, udt).into_owned(),
2259 down_col: dcf,
2260 w,
2261 q1: gq1,
2262 });
2263 Some(())
2264}
2265
2266fn sparse_ffn_quant(
2273 d: &DenseFfn,
2274 x: &[f32],
2275 active: &[u16],
2276 hidden: usize,
2277 pool: Option<&Pool>,
2278) -> Vec<f32> {
2279 let n = active.len();
2280 let inter = d.gate_proj.rows();
2281 let mut act = vec![0.0f32; n];
2282 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
2285 let compute = |ai: usize| -> f32 {
2286 let idx = active[ai] as usize;
2287 if idx >= inter {
2288 return 0.0; }
2290 let mut s = if need_scratch { vec![0.0f32; hidden] } else { Vec::new() };
2291 let gate = d.gate_proj.row_dot(idx, x, &mut s);
2292 let up = d.up_proj.row_dot(idx, x, &mut s);
2293 inference::silu(gate) * up
2294 };
2295 match pool {
2296 Some(p) if n >= 256 => {
2297 let ptr = SendMut(act.as_mut_ptr());
2298 p.run(&|widx, nw| {
2299 let chunk = n.div_ceil(nw);
2300 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
2301 for ai in s..e {
2302 unsafe { *ptr.at(ai) = compute(ai) };
2303 }
2304 });
2305 }
2306 _ => {
2307 for (ai, a) in act.iter_mut().enumerate() {
2308 *a = compute(ai);
2309 }
2310 }
2311 }
2312 let mut out = vec![0.0f32; hidden];
2314 for (ai, &idx) in active.iter().enumerate() {
2315 let w = act[ai];
2316 if w.abs() >= 1e-12 && (idx as usize) < inter {
2317 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
2318 }
2319 }
2320 out
2321}
2322
2323#[doc(hidden)]
2325pub fn sparse_ffn_quant_for_test(
2326 d: &DenseFfn,
2327 x: &[f32],
2328 active: &[u16],
2329 hidden: usize,
2330) -> Vec<f32> {
2331 sparse_ffn_quant(d, x, active, hidden, None)
2332}
2333
2334fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
2338 let deq = |t: &QTensor| -> Vec<f32> {
2339 let (rows, cols) = (t.rows(), t.cols());
2340 let mut out = vec![0.0f32; rows * cols];
2341 for r in 0..rows {
2342 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
2343 }
2344 out
2345 };
2346 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
2347}
2348
2349struct SendMut(*mut f32);
2351unsafe impl Send for SendMut {}
2352unsafe impl Sync for SendMut {}
2353impl SendMut {
2354 #[inline]
2355 #[allow(clippy::mut_from_ref)]
2358 unsafe fn at(&self, i: usize) -> &mut f32 {
2359 unsafe { &mut *self.0.add(i) }
2360 }
2361}
2362
2363fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2367 let ne = m.experts.len();
2368 let mut logits = vec![0.0f32; ne];
2369 m.router.matvec(x, &mut logits, pool);
2370 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2371 let mut p: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
2372 let s: f32 = p.iter().sum();
2373 for v in &mut p {
2374 *v /= s;
2375 }
2376 let mut idx: Vec<usize> = (0..ne).collect();
2377 idx.sort_unstable_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap().then(a.cmp(&b)));
2379 idx.truncate(m.top_k);
2380 let wsum: f32 = if m.norm_topk_prob {
2381 idx.iter().map(|&e| p[e]).sum()
2382 } else {
2383 1.0
2384 };
2385 {
2386 let mut st = m.stats.borrow_mut();
2387 if st.len() < ne {
2388 st.resize(ne, 0);
2389 }
2390 for &e in &idx {
2391 st[e] += 1;
2392 }
2393 }
2394 if crate::gpu::enabled_here() {
2399 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
2400 crate::gpu::ProbeArm::Gpu => {
2401 let t0 = std::time::Instant::now();
2402 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
2403 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
2404 return out;
2405 }
2406 }
2407 crate::gpu::ProbeArm::CpuTimed => {
2408 let t0 = std::time::Instant::now();
2409 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
2410 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
2411 return out;
2412 }
2413 crate::gpu::ProbeArm::Cpu => {
2414 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
2415 }
2416 }
2417 }
2418 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
2419}
2420
2421fn moe_ffn_cpu(
2423 m: &MoeFfn,
2424 x: &[f32],
2425 idx: &[usize],
2426 p: &[f32],
2427 wsum: f32,
2428 pool: Option<&Pool>,
2429) -> Vec<f32> {
2430 let mut out = attention::take_buf(x.len());
2431 for &e in idx {
2432 let mut eo = dense_ffn(&m.experts[e], x, pool);
2433 let w = p[e] / wsum;
2434 for i in 0..out.len() {
2435 out[i] += w * eo[i];
2436 }
2437 attention::recycle_buf(&mut eo);
2438 }
2439 if let Some((se, gate)) = &m.shared {
2440 let mut so = dense_ffn(se, x, pool);
2441 let mut gl = vec![0.0f32; 1];
2442 gate.matvec(x, &mut gl, pool);
2443 let g = 1.0 / (1.0 + (-gl[0]).exp());
2444 for i in 0..out.len() {
2445 out[i] += g * so[i];
2446 }
2447 attention::recycle_buf(&mut so);
2448 }
2449 out
2450}
2451
2452fn moe_ffn_gpu(
2455 m: &MoeFfn,
2456 x: &[f32],
2457 idx: &[usize],
2458 p: &[f32],
2459 wsum: f32,
2460 pool: Option<&Pool>,
2461) -> Option<Vec<f32>> {
2462 use crate::gpu::MoeJob;
2463
2464 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
2465 let mut model_ref = None;
2466 for &e in idx {
2467 moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref)?;
2468 }
2469 if let Some((se, gate)) = &m.shared {
2470 let mut gl = vec![0.0f32; 1];
2471 gate.matvec(x, &mut gl, pool);
2472 let g = 1.0 / (1.0 + (-gl[0]).exp());
2473 moe_push_job(se, x, g, &mut jobs, &mut model_ref)?;
2474 }
2475 let model = model_ref?;
2476 let hidden = jobs[0].down.1;
2477 let mut out = vec![0.0f32; hidden];
2478 crate::gpu::moe_block(&model, &jobs, &mut out).then_some(out)
2479}
2480
2481fn ffn_forward(ffn: &FfnKind, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
2483 match ffn {
2484 FfnKind::Dense(d) => dense_ffn(d, x, pool),
2485 FfnKind::Moe(m) => moe_ffn(m, x, pool),
2486 }
2487}
2488
2489fn ffn_forward_pair(
2493 ffn: &FfnKind,
2494 x1: &[f32],
2495 x2: &[f32],
2496 pool: Option<&Pool>,
2497) -> (Vec<f32>, Vec<f32>) {
2498 let d = match ffn {
2499 FfnKind::Dense(d) => d,
2500 FfnKind::Moe(m) => return (moe_ffn(m, x1, pool), moe_ffn(m, x2, pool)),
2501 };
2502 let inter = d.gate_proj.rows();
2503 FFN_SCRATCH.with(|s| {
2504 let mut s = s.borrow_mut();
2505 let [g1, g2, u1, u2] = &mut *s;
2506 g1.resize(inter, 0.0);
2507 g2.resize(inter, 0.0);
2508 u1.resize(inter, 0.0);
2509 u2.resize(inter, 0.0);
2510 QTensor::matvec2_many(
2513 [&d.gate_proj, &d.up_proj],
2514 x1,
2515 x2,
2516 [g1.as_mut_slice(), u1.as_mut_slice()],
2517 [g2.as_mut_slice(), u2.as_mut_slice()],
2518 pool,
2519 );
2520 for i in 0..inter {
2521 g1[i] = inference::silu(g1[i]) * u1[i];
2522 g2[i] = inference::silu(g2[i]) * u2[i];
2523 }
2524 let mut o1 = attention::take_buf(d.down_proj.rows());
2525 let mut o2 = attention::take_buf(d.down_proj.rows());
2526 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
2527 (o1, o2)
2528 })
2529}
2530
2531#[cfg(test)]
2532mod tests {
2533 use super::*;
2534
2535 #[test]
2541 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
2542 let (hidden, inter) = (16usize, 40usize);
2543 let synth = |n: usize, salt: usize| -> Vec<f32> {
2544 (0..n)
2545 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
2546 .collect()
2547 };
2548 let d = DenseFfn {
2549 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
2550 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
2551 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
2552 };
2553 let x = synth(hidden, 9);
2554 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
2556
2557 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
2558
2559 let mut g = vec![0.0f32; inter];
2561 d.gate_proj.matvec(&x, &mut g, None);
2562 let mut u = vec![0.0f32; inter];
2563 d.up_proj.matvec(&x, &mut u, None);
2564 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
2565 for i in 0..inter {
2566 g[i] = if act_set.contains(&(i as u16)) {
2567 inference::silu(g[i]) * u[i]
2568 } else {
2569 0.0
2570 };
2571 }
2572 let mut reference = vec![0.0f32; hidden];
2573 d.down_proj.matvec(&g, &mut reference, None);
2574
2575 let max_d = sparse
2576 .iter()
2577 .zip(&reference)
2578 .map(|(a, b)| (a - b).abs())
2579 .fold(0.0f32, f32::max);
2580 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
2581 }
2582
2583 fn attach_test_mtp(p: &mut Pipeline) {
2585 let (h, inter, heads, kv, hd) = (
2586 p.hidden_size,
2587 p.intermediate_size,
2588 p.num_heads,
2589 p.num_kv_heads,
2590 p.head_dim,
2591 );
2592 let synth = |n: usize, salt: usize| -> Vec<f32> {
2593 (0..n)
2594 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
2595 .collect()
2596 };
2597 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
2598 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
2599 };
2600 p.mtp = Some(MtpModule {
2601 enorm: vec![1.0; h],
2602 hnorm: vec![1.0; h],
2603 eh_proj: qt(h, 2 * h, 301),
2604 layer: LayerWeights {
2605 input_norm: vec![1.0; h],
2606 post_norm: vec![1.0; h],
2607 ffn: FfnKind::Dense(DenseFfn {
2608 gate_proj: qt(inter, h, 315),
2609 up_proj: qt(inter, h, 316),
2610 down_proj: qt(h, inter, 317),
2611 }),
2612 attn: AttnKind::Full {
2613 bias: None,
2614 wq: qt(heads * hd, h, 311),
2615 wk: qt(kv * hd, h, 312),
2616 wv: qt(kv * hd, h, 313),
2617 wo: qt(h, heads * hd, 314),
2618 q_norm: None,
2619 k_norm: None,
2620 output_gate: false,
2621 },
2622 },
2623 final_norm: vec![1.0; h],
2624 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
2625 });
2626 }
2627
2628 #[test]
2629 fn speculative_equals_vanilla_greedy() {
2630 let run = |spec: bool| {
2631 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2632 p.sampler_config.temperature = 0.0;
2633 attach_test_mtp(&mut p);
2634 p.speculative = spec;
2635 let r = p.generate("abcdef", 12, None, None).unwrap();
2636 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
2637 };
2638 let (vanilla, d0, _) = run(false);
2639 let (spec, d1, a1) = run(true);
2640 assert_eq!(d0, 0, "vanilla path must not draft");
2641 assert!(d1 > 0, "speculative path must draft");
2642 assert_eq!(
2643 vanilla, spec,
2644 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
2645 );
2646 }
2647
2648 #[test]
2649 fn speculative_accepts_constant_oracle() {
2650 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2651 p.sampler_config.temperature = 0.0;
2652 p.sampler_config.repetition_penalty = 1.0;
2653 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
2656 attach_test_mtp(&mut p);
2657 p.speculative = true;
2658 let r = p.generate("abcd", 10, None, None).unwrap();
2659 assert!(r.mtp_drafted > 0);
2660 assert_eq!(
2661 r.mtp_accepted, r.mtp_drafted,
2662 "constant logits → every draft accepted"
2663 );
2664 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
2667 }
2668
2669 #[test]
2670 fn empty_prompt_is_an_error_not_a_panic() {
2671 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2672 let r = p.generate("", 4, None, None);
2673 assert!(r.is_err(), "empty prompt must be a clean error");
2674 }
2675
2676 #[test]
2677 fn every_token_enters_kv_exactly_once() {
2678 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2679 p.sampler_config.temperature = 0.0;
2681 let r = p.generate("abc", 2, None, None).unwrap();
2682 assert_eq!(r.prompt_tokens, 3);
2683 assert_eq!(
2687 p.kv_cache.seq_len(),
2688 3 + r.tokens_generated - 1,
2689 "each token must be cached exactly once (v1 cached the last prompt token twice)"
2690 );
2691 }
2692
2693 #[test]
2694 fn generation_is_reproducible_with_seed() {
2695 let run = || {
2696 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
2697 p.generate("hello", 8, None, None).unwrap().token_ids
2698 };
2699 assert_eq!(run(), run());
2700 }
2701
2702 #[test]
2703 fn eviction_bounds_the_cache() {
2704 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
2705 p.kv_cache.max_seq_len = 6;
2706 p.sampler_config.temperature = 0.0;
2707 let _ = p.generate("abcd", 12, None, None).unwrap();
2708 assert!(
2709 p.kv_cache.seq_len() <= 6 + 1,
2710 "cache must stay bounded by max_seq_len (got {})",
2711 p.kv_cache.seq_len()
2712 );
2713 }
2714
2715 #[test]
2716 fn confidence_matches_tokens_and_is_a_probability() {
2717 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2718 p.sampler_config.temperature = 0.0;
2719 p.sampler_config.repetition_penalty = 1.0;
2720 let r = p.generate("abcd", 10, None, None).unwrap();
2721 assert_eq!(
2722 r.token_confidence.len(),
2723 r.token_ids.len(),
2724 "one confidence per emitted token"
2725 );
2726 for &c in &r.token_confidence {
2727 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
2728 }
2729 let logits = [1.0f32, 3.0, 0.5, 3.0];
2731 let p0 = top1_prob_t(&logits, 1, 1.0);
2732 let p1 = top1_prob_t(&logits, 3, 1.0);
2733 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
2734 assert!(p0 > 0.0 && p0 < 1.0);
2735 let sharp = top1_prob_t(&logits, 1, 1.0);
2737 let soft = top1_prob_t(&logits, 1, 2.0);
2738 assert!(soft < sharp, "higher temperature lowers peak confidence");
2739 }
2740
2741 #[test]
2742 fn trace_is_opt_in_and_parallels_the_output() {
2743 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2745 p.sampler_config.temperature = 0.0;
2746 p.sampler_config.repetition_penalty = 1.0;
2747 let r = p.generate("abcd", 10, None, None).unwrap();
2748 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
2749
2750 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2752 p.sampler_config.temperature = 0.0;
2753 p.sampler_config.repetition_penalty = 1.0;
2754 p.set_trace(true);
2755 let r = p.generate("abcd", 10, None, None).unwrap();
2756 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
2757 for (i, tr) in r.traces.iter().enumerate() {
2758 assert_eq!(tr.t, i, "trace index is sequential");
2759 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
2760 assert_eq!(
2761 tr.confidence, r.token_confidence[i],
2762 "trace confidence matches the confidence channel"
2763 );
2764 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
2766 }
2767 }
2768
2769 #[test]
2770 fn explain_prefill_logits_match_greedy_first_token() {
2771 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
2775 p.sampler_config.temperature = 0.0;
2776 p.sampler_config.repetition_penalty = 1.0;
2777 let ids = p.tokenizer.encode("abcd");
2778 let logits = p.prefill_next_logits(&ids, None);
2779 let argmax = logits
2780 .iter()
2781 .enumerate()
2782 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2783 .unwrap()
2784 .0 as u32;
2785 let r = p.generate("abcd", 1, None, None).unwrap();
2786 assert_eq!(argmax, r.token_ids[0], "explain preview must match greedy emit");
2787 }
2788}