1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11use crate::kv_cache::KvCache;
12use crate::linear_core::{
13 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
14 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
15 vmf_phase_pair,
16};
17use crate::pool::Pool;
18use crate::qtensor::QTensor;
19use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
20use crate::tokenizer::Tokenizer;
21use cortiq_core::mask::TaskMask;
22use cortiq_core::types::NormStyle;
23
24pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
25 std::sync::atomic::AtomicBool::new(false);
26
27struct ForwardScratch {
31 n1: Vec<f32>,
32 n2: Vec<f32>,
33 p1: Vec<f32>,
34 p2: Vec<f32>,
35}
36
37impl ForwardScratch {
38 fn new(hidden: usize) -> Self {
39 Self {
40 n1: vec![0.0; hidden],
41 n2: vec![0.0; hidden],
42 p1: vec![0.0; hidden],
43 p2: vec![0.0; hidden],
44 }
45 }
46}
47
48pub struct Pipeline {
50 pub tokenizer: std::sync::Arc<Tokenizer>,
53 pub kv_cache: KvCache,
54 pub sampler_config: SamplerConfig,
55 pub weights: PipelineWeights,
56 pub hidden_size: usize,
57 pub intermediate_size: usize,
58 pub num_heads: usize,
59 pub num_kv_heads: usize,
60 pub head_dim: usize,
61 pub num_layers: usize,
63 pub physical_layers: usize,
65 pub loop_final_norm: bool,
67 pub vocab_size: usize,
68 pub rms_eps: f64,
69 pub rope_base: f32,
70 pub norm_style: NormStyle,
71 pub rotary_dim: usize,
73 pub attention_heads_per_layer: Option<Vec<usize>>,
75 pub vmf_cfg: Option<VmfPhaseCfg>,
77 pub gdn_cfg: Option<GdnCfg>,
79 pub logit_multiplier: Option<f32>,
81 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
86 pub kv_history: Vec<u32>,
91 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
93 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
96 pub dsv4: Option<
100 Box<(
101 crate::dsv4::Dsv4Globals,
102 Vec<crate::dsv4::Dsv4Layer>,
103 crate::dsv4::Dsv4Cfg,
104 crate::dsv4::Dsv4State,
105 )>,
106 >,
107 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
111 pub dspark: Option<crate::dsv4::DsparkState>,
113 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
116 pub dspark_hist: Vec<usize>,
118 pub dspark_real: Vec<u32>,
122 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
126 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
128 pub dspark_draft_ns: u128,
132 pub short_conv_cfg: Option<ShortConvCfg>,
135 pub mtp: Option<MtpModule>,
137 pub speculative: bool,
139 rng: SplitMix64,
140 sampler_scratch: SamplerScratch,
141 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
145 ws: ForwardScratch,
149 pool: Option<std::sync::Arc<Pool>>,
151 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
155 pub(crate) dyn_force_f32: bool,
157 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
162 pub(crate) dyn_active: Option<usize>,
168 pub(crate) dyn_blend_loaded: bool,
172 pub(crate) dyn_phi_layer: Option<usize>,
175 dyn_phi_ema: Vec<f32>,
177 dyn_phi_seen: usize,
178 pub dyn_router: Option<crate::swarm::DynRouter>,
181 o1_cfg: Option<crate::nystrom::O1Cfg>,
184 o1_epoch: u64,
187 o1_flags: Vec<bool>,
189 trace: bool,
192 calib_temp: f32,
195 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
197 graph_kv_id: u64,
198 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
201 graph_want_logits: bool,
202 graph_logits: Option<Vec<f32>>,
205 pub embed_multiplier: f32,
207 pub attn_scale: f32,
210 pub swa: Option<(usize, usize)>,
213 pub sliding_layers: Option<Vec<bool>>,
216 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
219 pub rotary_dim_local: Option<usize>,
220 pub rope_scale: f32,
221 pub rope_scale_local: f32,
222 pub global_attn: Option<(usize, usize)>,
225 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
228 pub attn_v_norm: bool,
230 pub final_softcap: Option<f32>,
232 pub attn_softcap: f32,
234 confidence_on: bool,
238}
239
240#[cfg(target_os = "macos")]
241impl Drop for Pipeline {
242 fn drop(&mut self) {
243 crate::gpu::kv_mirror_drop(self.graph_kv_id);
244 }
245}
246
247pub struct PipelineWeights {
252 pub embed_tokens: QTensor,
254 pub layers: Vec<LayerWeights>,
256 pub lm_head: QTensor,
258 pub final_norm: Vec<f32>,
260}
261
262pub struct LayerWeights {
264 pub input_norm: Vec<f32>,
265 pub post_norm: Vec<f32>,
268 pub attn_out_norm: Option<Vec<f32>>,
271 pub layer_scale: Option<f32>,
273 pub ffn_out_norm: Option<Vec<f32>>,
276 pub ffn: FfnKind,
277 pub attn: AttnKind,
278}
279
280#[derive(Clone, Copy, PartialEq, Debug, Default)]
283pub enum Act {
284 #[default]
285 Silu,
286 GeluTanh,
287 Situ {
290 beta: f32,
291 linear_beta: f32,
292 },
293}
294
295impl Act {
296 pub fn from_arch(name: &str) -> Self {
297 if name == "gelu_tanh" {
298 Self::GeluTanh
299 } else {
300 Self::Silu
301 }
302 }
303
304 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
306 match arch.hidden_act.as_str() {
307 "situ" => Self::Situ {
308 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
309 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
310 },
311 other => Self::from_arch(other),
312 }
313 }
314
315 #[inline]
316 pub fn apply(self, x: f32) -> f32 {
317 match self {
318 Self::Silu => inference::silu(x),
319 Self::GeluTanh => inference::gelu_tanh(x),
320 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
321 }
322 }
323
324 #[inline]
327 pub fn combine(self, g: f32, u: f32) -> f32 {
328 match self {
329 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
330 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
331 }
332 _ => self.apply(g) * u,
333 }
334 }
335}
336
337pub struct DenseFfn {
339 pub gate_proj: QTensor,
340 pub up_proj: QTensor,
341 pub down_proj: QTensor,
342 pub act: Act,
344}
345
346pub enum FfnKind {
349 Dense(DenseFfn),
350 Moe(MoeFfn),
354 DenseMoe(Box<DenseMoeFfn>),
361}
362
363pub struct DenseMoeFfn {
365 pub dense: DenseFfn,
366 pub moe: MoeFfn,
367 pub post_norm_1: Vec<f32>,
369 pub pre_norm_2: Vec<f32>,
372 pub post_norm_2: Vec<f32>,
374}
375
376pub struct MoeFfn {
377 pub router: QTensor,
379 pub experts: Vec<DenseFfn>,
380 pub top_k: usize,
381 pub norm_topk_prob: bool,
382 pub router_sigmoid: bool,
385 pub expert_bias: Option<Vec<f32>>,
389 pub routed_scaling: f32,
392 pub route_tau: Option<f32>,
398 pub shared: Option<(DenseFfn, Option<QTensor>)>,
401 pub stats: std::cell::RefCell<Vec<u64>>,
405 pub act_sq: std::cell::RefCell<Vec<f64>>,
412 pub act_rows: std::cell::RefCell<Vec<f32>>,
418 pub mask: Option<Vec<bool>>,
423 pub per_expert_scale: Option<Vec<f32>>,
426 pub router_input_norm: bool,
430}
431
432pub enum AttnKind {
435 Full {
437 wq: QTensor,
438 wk: QTensor,
439 wv: QTensor,
440 wo: QTensor,
441 q_norm: Option<Vec<f32>>,
442 k_norm: Option<Vec<f32>>,
443 output_gate: bool,
444 softplus_gate: Option<(QTensor, bool)>,
448 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
450 },
451 Linear(VmfPhaseWeights),
453 LinearGdn(GdnWeights),
455 ShortConv(ShortConvWeights),
458 Mla(Box<MlaWeights>),
466 Kda(Box<crate::linear_core::KdaWeights>),
470}
471
472pub struct MlaWeights {
474 pub q_proj: QTensor,
478 pub q_a: Option<QTensor>,
481 pub q_a_norm: Option<Vec<f32>>,
482 pub kv_a: QTensor,
484 pub kv_a_norm: Vec<f32>,
486 pub kv_b: QTensor,
488 pub o_proj: QTensor,
490 pub nh: usize,
491 pub qk_rope: usize,
492 pub qk_nope: usize,
493 pub v_dim: usize,
494 pub lora: usize,
495 pub scale: f32,
497 pub nope: bool,
499}
500
501pub struct MtpModule {
506 pub enorm: Vec<f32>,
507 pub hnorm: Vec<f32>,
508 pub eh_proj: QTensor,
510 pub layer: LayerWeights,
511 pub final_norm: Vec<f32>,
512 pub kv: crate::kv_cache::LayerKvCache,
513}
514
515pub struct GenerateResult {
517 pub text: String,
518 pub token_ids: Vec<u32>,
519 pub prompt_tokens: usize,
520 pub tokens_generated: usize,
521 pub finish_reason: String,
522 pub mtp_drafted: usize,
524 pub mtp_accepted: usize,
525 pub token_confidence: Vec<f32>,
530 pub traces: Vec<TokenTrace>,
533}
534
535#[derive(Clone, Debug)]
540pub struct TokenTrace {
541 pub t: usize,
543 pub token_id: u32,
545 pub confidence: f32,
547 pub active_skill: Option<String>,
549 pub recon: Option<f32>,
553 pub switched: bool,
556}
557
558fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
563 let t = if temp > 1e-3 { temp } else { 1.0 };
564 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
565 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
566 if sum > 0.0 {
567 (((logits[id as usize] - max) / t).exp()) / sum
568 } else {
569 0.0
570 }
571}
572
573fn prefill_batched() -> bool {
576 std::env::var("CMF_PREFILL")
577 .map(|v| v != "seq")
578 .unwrap_or(true)
579}
580
581impl Pipeline {
588 fn can_prefill_batched(&self) -> bool {
589 prefill_batched() && !self.weights.layers.is_empty()
590 }
591}
592
593fn prefill_chunk() -> usize {
598 if let Some(n) = std::env::var("CMF_PREFILL_CHUNK")
599 .ok()
600 .and_then(|v| v.parse::<usize>().ok())
601 {
602 return n.max(1);
603 }
604 if cfg!(target_os = "macos") {
605 512
606 } else if cfg!(target_arch = "aarch64") {
607 256
610 } else {
611 48
612 }
613}
614
615pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
617
618impl Pipeline {
619 #[inline]
623 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
624 virtual_idx % self.physical_layers
625 }
626
627 #[inline]
630 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
631 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
632 }
633
634 #[allow(clippy::too_many_arguments)]
636
637 #[cfg(target_os = "macos")]
656 fn graph_prefill_preferred(&self) -> bool {
657 if !crate::gpu::enabled_here()
658 || !crate::gpu::q1_force()
659 || std::env::var("CMF_GPU_BLOCK")
660 .map(|v| v == "0")
661 .unwrap_or(false)
662 {
663 return false;
664 }
665 self.weights
666 .layers
667 .iter()
668 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.is_q1()))
669 }
670
671 #[cfg(not(target_os = "macos"))]
672 fn graph_prefill_preferred(&self) -> bool {
673 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
681 .map(|v| v != "0")
682 .unwrap_or_else(|_| {
683 crate::gpu::wgpu_graph_default()
687 });
688 if !graph_on || !crate::gpu::enabled_here() {
689 return false;
690 }
691 self.weights
692 .layers
693 .iter()
694 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
695 }
696
697 #[cfg(target_os = "macos")]
698 fn q1_graph_gpu(
699 &mut self,
700 start: usize,
701 upto: Option<usize>,
702 position: usize,
703 h: &mut [f32],
704 ) -> usize {
705 use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph};
706 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
708 || !crate::gpu::q1_force()
709 || std::env::var("CMF_GPU_BLOCK")
710 .map(|v| v == "0")
711 .unwrap_or(false)
712 {
713 return start;
714 }
715 if self.swa.is_some()
720 || self.global_attn.is_some()
721 || self.attention_heads_per_layer.is_some()
722 || self.attn_v_norm
723 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
724 || self.weights.layers.iter().any(|lw| {
725 lw.attn_out_norm.is_some()
726 || lw.ffn_out_norm.is_some()
727 || lw.layer_scale.is_some()
728 || matches!(&lw.ffn, FfnKind::Dense(d) if d.act != Act::Silu)
729 })
730 {
731 return start;
732 }
733 let limit = upto
736 .map(|u| u + 1)
737 .unwrap_or(self.num_layers)
738 .min(self.num_layers);
739
740 enum Item<'a> {
741 Gdn {
742 run: Vec<GdnGpuLayer<'a>>,
743 first: usize,
744 },
745 Attn {
746 l: AttnGpuLayer<'a>,
747 li: usize,
748 q_norm: Option<&'a [f32]>,
749 k_norm: Option<&'a [f32]>,
750 output_gate: bool,
751 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
752 full_gpu: bool,
755 },
756 }
757
758 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
760 let dev_attend = attend_mode != "0"
761 && attend_mode != "off"
762 && (self.head_dim <= 128 || attend_mode == "force" || attend_mode == "256")
766 && self.head_dim % 4 == 0
767 && self.head_dim <= 256
768 && self.rotary_dim >= 2
769 && self.rotary_dim <= self.head_dim
770 && (self.rotary_dim / 2) % 32 == 0
771 && self.num_kv_heads > 0
772 && self.num_heads % self.num_kv_heads == 0;
773
774 let mut plan: Vec<Item> = Vec::new();
775 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
776 let mut scan = start;
777 while scan < limit {
778 let lw = &self.weights.layers[self.phys_layer(scan)];
779 let FfnKind::Dense(d) = &lw.ffn else { break };
780 let (Some(g), Some(u), Some(dn)) = (
781 d.gate_proj.q1_parts(),
782 d.up_proj.q1_parts(),
783 d.down_proj.q1_parts(),
784 ) else {
785 break;
786 };
787 match &lw.attn {
788 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
789 let parts = (
790 w.in_proj_qkv.q1_parts(),
791 w.in_proj_z.q1_parts(),
792 w.in_proj_a.f32_parts(),
793 w.in_proj_b.f32_parts(),
794 w.out_proj.q1_parts(),
795 );
796 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
797 break;
798 };
799 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
800 model_ref.get_or_insert_with(|| model.clone());
801 }
802 let gl = GdnGpuLayer {
803 attn_norm: &lw.input_norm,
804 post_norm: &lw.post_norm,
805 qkv,
806 z,
807 a,
808 b,
809 out,
810 gate: g,
811 up: u,
812 down: dn,
813 conv1d: &w.conv1d,
814 a_log: &w.a_log,
815 dt_bias: &w.dt_bias,
816 gnorm: &w.norm,
817 };
818 match plan.last_mut() {
819 Some(Item::Gdn { run, .. }) => run.push(gl),
820 _ => plan.push(Item::Gdn {
821 run: vec![gl],
822 first: scan,
823 }),
824 }
825 }
826 AttnKind::Full {
827 wq,
828 wk,
829 wv,
830 wo,
831 q_norm,
832 k_norm,
833 output_gate,
834 softplus_gate: None,
835 bias,
836 } if !self.kv_cache.layers[scan].o1_sealed() => {
837 let parts = (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts());
838 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
839 break;
840 };
841 if let QTensor::Mapped { model, .. } = wq {
842 model_ref.get_or_insert_with(|| model.clone());
843 }
844 let cache = &self.kv_cache.layers[scan];
845 let full_gpu = dev_attend
846 && cache.mode == crate::kv_cache::KvMode::F32
847 && cache.o1.is_none()
848 && bias.is_none()
849 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
850 && pk.1 == self.num_kv_heads * self.head_dim
851 && pv.1 == self.num_kv_heads * self.head_dim
852 && po.2 == self.num_heads * self.head_dim;
853 plan.push(Item::Attn {
854 l: AttnGpuLayer {
855 attn_norm: &lw.input_norm,
856 post_norm: &lw.post_norm,
857 wq: pq,
858 wk: pk,
859 wv: pv,
860 wo: po,
861 gate: g,
862 up: u,
863 down: dn,
864 },
865 li: scan,
866 q_norm: q_norm.as_deref(),
867 k_norm: k_norm.as_deref(),
868 output_gate: *output_gate,
869 bias: bias
870 .as_ref()
871 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
872 full_gpu,
873 });
874 }
875 _ => break,
876 }
877 scan += 1;
878 }
879 let Some(model) = model_ref else {
880 if std::env::var("CMF_GRAPH_DBG").is_ok() {
881 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
882 }
883 return start;
884 };
885 if plan.is_empty() {
886 if std::env::var("CMF_GRAPH_DBG").is_ok() {
887 eprintln!("q1-graph: empty plan at layer {start}");
888 }
889 return start;
890 }
891 if std::env::var("CMF_GRAPH_DBG").is_ok() {
892 use std::sync::atomic::{AtomicBool, Ordering};
893 static SAID: AtomicBool = AtomicBool::new(false);
894 if !SAID.swap(true, Ordering::Relaxed) {
895 let fg = plan
896 .iter()
897 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
898 .count();
899 let att = plan
900 .iter()
901 .filter(|it| matches!(it, Item::Attn { .. }))
902 .count();
903 eprintln!(
904 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
905 plan.len(),
906 self.head_dim,
907 self.rotary_dim,
908 self.num_kv_heads,
909 self.num_heads,
910 );
911 }
912 }
913 let dims = GraphDims {
914 hidden: self.hidden_size,
915 eps: self.rms_eps as f32,
916 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
917 };
918 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
919 return start;
920 };
921 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
922 nv: cfg.num_v_heads,
923 nk: cfg.num_k_heads,
924 dk: cfg.key_head_dim,
925 dv: cfg.value_head_dim,
926 kk: cfg.conv_kernel,
927 hidden: self.hidden_size,
928 inter: self.intermediate_size,
929 c_dim: cfg.conv_dim(),
930 eps: cfg.rms_eps as f32,
931 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
932 });
933 let mut valid = 0usize;
937 let mut end = start;
938 for item in &plan {
939 let ok = match item {
940 Item::Gdn { run, .. } => gcfg
941 .as_ref()
942 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
943 .unwrap_or(false),
944 Item::Attn { l, .. } => graph.attn_ok(l),
945 };
946 if !ok {
947 break;
948 }
949 valid += 1;
950 end += match item {
951 Item::Gdn { run, .. } => run.len(),
952 Item::Attn { .. } => 1,
953 };
954 }
955 plan.truncate(valid);
956 if plan.is_empty() {
957 return start;
958 }
959
960 let inv_freq = self.inv_freq.clone();
961 let pool = self.pool.clone();
962 let (nh, nkv, hd, hs, rd, eps) = (
963 self.num_heads,
964 self.num_kv_heads,
965 self.head_dim,
966 self.hidden_size,
967 self.rotary_dim,
968 self.rms_eps,
969 );
970 let norm_style = self.norm_style;
971 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
972 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
973 let kv_id = self.graph_kv_id;
974 let mut pending: Vec<(usize, usize)> = Vec::new();
977 let mut dev_attn: Vec<usize> = Vec::new();
980 for item in &plan {
981 if self.loop_final_norm {
983 let item_start = match item {
984 Item::Gdn { first, .. } => *first,
985 Item::Attn { li, .. } => *li,
986 };
987 if item_start > start && self.is_loop_end(item_start - 1) {
988 graph.encode_loop_norm(&self.weights.final_norm);
989 }
990 }
991 match item {
992 Item::Gdn { run, first } => {
993 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
994 if l.linear_state.len() != want {
995 l.linear_state = vec![0f32; want];
996 }
997 }
998 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
999 .iter()
1000 .map(|l| l.linear_state.as_slice())
1001 .collect();
1002 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
1003 tracing::error!("q1 graph: GDN run refused after validation");
1005 return start;
1006 }
1007 graph.commit();
1010 pending.push((*first, run.len()));
1011 }
1012 Item::Attn {
1013 l,
1014 li,
1015 q_norm,
1016 k_norm,
1017 output_gate,
1018 bias,
1019 full_gpu,
1020 } => {
1021 if *full_gpu {
1023 let cache = &self.kv_cache.layers[*li];
1024 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
1025 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
1026 let cpu_stored = cpu_k[0].len() / hd;
1027 let p = crate::gpu::AttnDeviceParams {
1028 kv_id,
1029 layer: *li,
1030 nh,
1031 nkv,
1032 hd,
1033 rd,
1034 position,
1035 eps: eps as f32,
1036 gemma,
1037 output_gate: *output_gate,
1038 q_norm: *q_norm,
1039 k_norm: *k_norm,
1040 inv_freq: &inv_freq,
1041 cpu_k,
1042 cpu_v,
1043 cpu_stored,
1044 };
1045 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
1046 graph.commit();
1047 dev_attn.push(*li);
1048 continue;
1049 }
1050 }
1052 graph.encode_attn_prefix(l);
1053 graph.sync();
1054 if !pending.is_empty() {
1055 let idxs: Vec<usize> =
1056 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1057 let mut outs: Vec<&mut [f32]> = self
1058 .kv_cache
1059 .layers
1060 .iter_mut()
1061 .enumerate()
1062 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1063 .map(|(_, s)| s.linear_state.as_mut_slice())
1064 .collect();
1065 graph.read_states(&mut outs);
1066 }
1067 let mut q_raw = attention::take_buf(l.wq.1);
1068 let mut k = attention::take_buf(l.wk.1);
1069 let mut v = attention::take_buf(l.wv.1);
1070 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1071 let cfg = QwenAttnCfg {
1072 num_heads: nh,
1073 num_kv_heads: nkv,
1074 head_dim: hd,
1075 hidden_size: hs,
1076 position,
1077 inv_freq: &inv_freq,
1078 rotary_dim: rd,
1079 scale: self.attn_scale,
1080 softcap: self.attn_softcap,
1081 window: None,
1082 v_norm: false,
1083 q_norm: *q_norm,
1084 k_norm: *k_norm,
1085 output_gate: *output_gate,
1086 softplus_gate: None,
1087 rope_scale: 1.0,
1088 bias: *bias,
1089 rms_eps: eps,
1090 norm_style,
1091 pool: pool.as_deref(),
1092 };
1093 let mut ao = attention::qwen_attention_core(
1094 q_raw,
1095 k,
1096 v,
1097 &mut self.kv_cache.layers[*li],
1098 &cfg,
1099 );
1100 graph.encode_attn_suffix(l, &ao);
1101 graph.commit();
1104 attention::recycle_buf(&mut ao);
1105 }
1106 }
1107 }
1108 let mut lm_rows = None;
1113 if self.graph_want_logits
1114 && upto.is_none()
1115 && end == self.num_layers
1116 && std::env::var("CMF_GPU_LMHEAD")
1117 .map(|v| v != "0")
1118 .unwrap_or(true)
1119 {
1120 if let Some(lm) = self.weights.lm_head.q1_parts() {
1121 if graph.lm_head_ok(lm) {
1122 graph.encode_lm_head(&self.weights.final_norm, lm);
1123 lm_rows = Some(lm.1);
1124 }
1125 }
1126 }
1127 graph.sync();
1128 if !pending.is_empty() {
1129 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1130 let mut outs: Vec<&mut [f32]> = self
1131 .kv_cache
1132 .layers
1133 .iter_mut()
1134 .enumerate()
1135 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1136 .map(|(_, s)| s.linear_state.as_mut_slice())
1137 .collect();
1138 graph.read_states(&mut outs);
1139 }
1140 if let Some(rows) = lm_rows {
1141 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1142 graph.read_logits(&mut lg);
1143 lg.resize(self.vocab_size, 0.0);
1144 if let Some(c) = self.final_softcap {
1145 for l in lg.iter_mut() {
1146 *l = c * (*l / c).tanh();
1147 }
1148 }
1149 self.graph_logits = Some(lg);
1150 }
1151 graph.finish(h);
1152 for li in dev_attn {
1156 let mut krow = attention::take_buf(nkv * hd);
1157 let mut vrow = attention::take_buf(nkv * hd);
1158 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1159 let cache = &mut self.kv_cache.layers[li];
1160 cache.append(&krow, &vrow, &[]);
1161 let n = cache.seq_len;
1162 let mut imp = attention::take_buf(n);
1163 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1164 cache.accumulate_imp(&imp);
1165 attention::recycle_buf(&mut imp);
1166 }
1167 attention::recycle_buf(&mut krow);
1168 attention::recycle_buf(&mut vrow);
1169 }
1170 end
1171 }
1172
1173 pub fn new(
1174 tokenizer: Tokenizer,
1175 weights: PipelineWeights,
1176 hidden_size: usize,
1177 intermediate_size: usize,
1178 num_heads: usize,
1179 num_kv_heads: usize,
1180 head_dim: usize,
1181 num_layers: usize,
1182 physical_layers: usize,
1183 loop_final_norm: bool,
1184 vocab_size: usize,
1185 rms_eps: f64,
1186 rope_base: f32,
1187 norm_style: NormStyle,
1188 max_seq_len: usize,
1189 sampler_config: SamplerConfig,
1190 ) -> Self {
1191 let rng = match sampler_config.seed {
1192 Some(s) => SplitMix64::new(s),
1193 None => SplitMix64::from_entropy(),
1194 };
1195 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1196 let pool = Pool::from_env();
1197 if let Some(p) = &pool {
1198 tracing::info!("worker pool: {} threads", p.n_workers());
1199 }
1200 Self {
1201 tokenizer: std::sync::Arc::new(tokenizer),
1202 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1203 sampler_config,
1204 weights,
1205 hidden_size,
1206 intermediate_size,
1207 num_heads,
1208 num_kv_heads,
1209 head_dim,
1210 num_layers,
1211 physical_layers,
1212 loop_final_norm,
1213 vocab_size,
1214 rms_eps,
1215 rope_base,
1216 norm_style,
1217 rotary_dim: head_dim,
1218 attention_heads_per_layer: None,
1219 vmf_cfg: None,
1220 gdn_cfg: None,
1221 kda_cfg: None,
1222 g3n: None,
1223 dsv4: None,
1224 dsv4_mtp: Vec::new(),
1225 dspark: None,
1226 dspark_pending: Vec::new(),
1227 dspark_hist: Vec::new(),
1228 dspark_real: Vec::new(),
1229 dspark_trunk_picks: Vec::new(),
1230 dspark_exp: Vec::new(),
1231 dspark_draft_ns: 0,
1232 logit_multiplier: None,
1233 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1234 kv_history: Vec::new(),
1235 short_conv_cfg: None,
1236 mtp: None,
1237 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1238 rng,
1239 sampler_scratch: SamplerScratch::default(),
1240 inv_freq,
1241 ws: ForwardScratch::new(hidden_size),
1242 pool,
1243 model: None,
1244 dyn_force_f32: false,
1245 dyn_skill_layers: Vec::new(),
1246 dyn_active: None,
1247 dyn_blend_loaded: false,
1248 dyn_phi_layer: None,
1249 dyn_phi_ema: Vec::new(),
1250 dyn_phi_seen: 0,
1251 dyn_router: None,
1252 o1_cfg: None,
1253 o1_epoch: 0,
1254 o1_flags: Vec::new(),
1255 trace: false,
1256 calib_temp: 1.0,
1257 confidence_on: true,
1258 embed_multiplier: 1.0,
1259 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1260 swa: None,
1261 sliding_layers: None,
1262 inv_freq_local: None,
1263 rotary_dim_local: None,
1264 rope_scale: 1.0,
1265 rope_scale_local: 1.0,
1266 global_attn: None,
1267 inv_freq_global: None,
1268 attn_v_norm: false,
1269 final_softcap: None,
1270 attn_softcap: 0.0,
1271 graph_want_logits: false,
1272 graph_logits: None,
1273 graph_kv_id: {
1274 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1275 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1276 },
1277 }
1278 }
1279
1280 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1287 self.o1_flags = match &cfg {
1288 Some(c) => {
1289 let mut flags = c.layer_flags(self.num_layers);
1290 for (li, f) in flags.iter_mut().enumerate() {
1291 if *f
1292 && !matches!(
1293 self.weights.layers[self.phys_layer(li)].attn,
1294 AttnKind::Full { .. }
1295 )
1296 {
1297 *f = false;
1298 }
1299 }
1300 flags
1301 }
1302 None => Vec::new(),
1303 };
1304 if let Some(c) = &cfg {
1305 let n = self.o1_flags.iter().filter(|&&f| f).count();
1306 tracing::info!(
1307 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1308 self.num_layers,
1309 c.m,
1310 c.w,
1311 c.sink,
1312 c.rect
1313 );
1314 }
1315 self.o1_cfg = cfg;
1316 }
1317
1318 pub fn o1_active(&self) -> bool {
1320 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1321 }
1322
1323 fn o1_begin(&mut self) {
1325 if let Some(c) = &self.o1_cfg {
1326 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1327 for (li, &f) in self.o1_flags.iter().enumerate() {
1328 if f {
1329 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1330 }
1331 }
1332 }
1333 }
1334
1335 fn o1_seal(&mut self) {
1338 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1339 if self.o1_cfg.is_none() {
1340 return;
1341 }
1342 for li in 0..self.num_layers {
1343 if self.o1_flags.get(li).copied().unwrap_or(false) {
1344 self.kv_cache.layers[li].o1_seal(self.num_heads);
1345 }
1346 }
1347 }
1348
1349 pub fn set_trace(&mut self, on: bool) {
1351 self.trace = on;
1352 }
1353
1354 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1357 self.rng = match config.seed {
1358 Some(seed) => SplitMix64::new(seed),
1359 None => SplitMix64::from_entropy(),
1360 };
1361 self.sampler_config = config;
1362 }
1363
1364 pub fn set_confidence(&mut self, on: bool) {
1369 self.confidence_on = on;
1370 }
1371
1372 pub fn set_calib_temp(&mut self, t: f32) {
1375 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1376 }
1377
1378 pub fn calib_temp(&self) -> f32 {
1380 self.calib_temp
1381 }
1382
1383 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1386 self.rotary_dim = rotary_dim.min(self.head_dim);
1387 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1388 }
1389
1390 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1391 QwenAttnCfg {
1392 num_heads: self.num_heads,
1393 num_kv_heads: self.num_kv_heads,
1394 head_dim: self.head_dim,
1395 hidden_size: self.hidden_size,
1396 position,
1397 inv_freq: &self.inv_freq,
1398 rotary_dim: self.rotary_dim,
1399 scale: self.attn_scale,
1400 softcap: self.attn_softcap,
1401 window: None,
1402 v_norm: false,
1403 q_norm: None,
1404 k_norm: None,
1405 output_gate: false,
1406 softplus_gate: None,
1407 rope_scale: self.rope_scale,
1408 bias: None,
1409 rms_eps: self.rms_eps,
1410 norm_style: self.norm_style,
1411 pool: self.pool.as_deref(),
1412 }
1413 }
1414
1415 pub fn generate(
1417 &mut self,
1418 prompt: &str,
1419 max_tokens: usize,
1420 task_mask: Option<&TaskMask>,
1421 on_token: Option<TokenCallback>,
1422 ) -> Result<GenerateResult, String> {
1423 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1424 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1425 }
1426
1427 pub fn generate_from_ids(
1435 &mut self,
1436 input_ids: &[u32],
1437 max_tokens: usize,
1438 task_mask: Option<&TaskMask>,
1439 mut on_token: Option<TokenCallback>,
1440 ) -> Result<GenerateResult, String> {
1441 if std::env::var("CMF_TRACE_H").is_ok() {
1442 eprintln!("input_ids: {input_ids:?}");
1443 }
1444 if input_ids.is_empty() {
1445 return Err("empty prompt: nothing to generate from".to_string());
1446 }
1447
1448 let reuse_from = {
1456 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1457 let h = &self.kv_history;
1458 if on
1459 && task_mask.is_none()
1460 && self.mtp.is_none()
1461 && self.o1_cfg.is_none()
1462 && !h.is_empty()
1463 && h.len() < input_ids.len()
1464 && input_ids[..h.len()] == h[..]
1465 {
1466 h.len()
1467 } else {
1468 0
1469 }
1470 };
1471 if reuse_from == 0 {
1472 self.kv_cache.clear();
1474 self.kv_history.clear();
1475 crate::gpu::graph_kv_reset(self.graph_kv_id);
1476 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1477 eprintln!(
1478 "kv-reuse: {} of {} prompt positions already cached",
1479 reuse_from,
1480 input_ids.len()
1481 );
1482 }
1483 crate::gpu::graph_race_begin_generation();
1484 self.o1_begin();
1485
1486 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1492 .map(|v| v != "0")
1493 .unwrap_or_else(|_| {
1494 crate::gpu::wgpu_graph_default()
1498 });
1499 let graph_spec = self.speculative
1506 && graph_on
1507 && self.mtp.is_some()
1508 && task_mask.is_none()
1509 && !self.o1_active()
1510 && self.sampler_config.temperature < 1e-6
1511 && self.sampler_config.repetition_penalty == 1.0
1512 && std::env::var("CMF_GRAPH_SPEC").is_ok_and(|v| v != "0");
1513 let pair_pays = self.gdn_cfg.is_none()
1520 || std::env::var("CMF_MTP").as_deref() == Ok("1");
1521 let spec_active = self.speculative
1522 && self.mtp.is_some()
1523 && task_mask.is_none()
1524 && !self.o1_active()
1525 && ((!graph_on && pair_pays) || graph_spec)
1526 && self.sampler_config.temperature < 1e-6;
1527 let mut mtp = if spec_active { self.mtp.take() } else { None };
1530 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
1531 eprintln!(
1532 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
1533 mtp.is_some(),
1534 self.speculative,
1535 self.sampler_config.temperature < 1e-6,
1536 );
1537 }
1538 if let Some(m) = &mut mtp {
1539 m.kv.clear();
1540 }
1541 let mut router = if mtp.is_none() {
1545 self.dyn_router.take()
1546 } else {
1547 None
1548 };
1549 if let Some(r) = &mut router {
1550 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1553 }
1554
1555 let mut all_ids = input_ids.to_vec();
1556 let mut generated = 0usize;
1557 let mut finish_reason = "max_tokens".to_string();
1558 let mut drafted = 0usize;
1559 let mut accepted = 0usize;
1560 let mut confidence: Vec<f32> = Vec::new();
1561 let trace_on = self.trace;
1562 let calib_temp = self.calib_temp;
1563 let mut traces: Vec<TokenTrace> = Vec::new();
1564
1565 let mut hidden = vec![0.0f32; self.hidden_size];
1571 let mut pos = reuse_from;
1572 let fuse_lm = mtp.is_none()
1581 && router.is_none()
1582 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
1583 self.graph_logits = None;
1584 self.graph_want_logits = false;
1585 let _tpf = std::time::Instant::now();
1586 let batch_k = std::env::var("CMF_BATCH_K")
1587 .ok()
1588 .and_then(|v| v.parse::<usize>().ok())
1589 .unwrap_or(0);
1590 while self.dsv4.is_some()
1601 && mtp.is_none()
1602 && pos < input_ids.len()
1603 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1604 {
1605 let end = (pos + prefill_chunk()).min(input_ids.len());
1606 let ids: Vec<u32> = input_ids[pos..end].to_vec();
1607 let mut lg = Vec::new();
1608 if let Some(b) = &mut self.dsv4 {
1609 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
1610 crate::dsv4::forward_chunk(
1611 g,
1612 layers,
1613 &cfg,
1614 st,
1615 &ids,
1616 pos,
1617 &self.inv_freq,
1618 self.pool.as_deref(),
1619 &mut lg,
1620 end == input_ids.len(),
1621 );
1622 }
1623 if end == input_ids.len() {
1624 self.graph_logits = Some(lg);
1625 }
1626 pos = end;
1627 hidden = vec![0.0; self.hidden_size];
1628 }
1629 let dyn_prefill = router.is_some();
1634 let graph_prefill = self.graph_prefill_preferred();
1640 if task_mask.is_none()
1641 && !dyn_prefill
1642 && !graph_prefill
1643 && self.can_prefill_batched()
1644 && self.g3n.is_none()
1645 && input_ids.len() > 2
1646 {
1647 let chunk = prefill_chunk();
1653 let hs = self.hidden_size;
1654 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1655 let end = (pos + chunk).min(input_ids.len());
1656 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1657 if let Some(m) = &mut mtp {
1658 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1659 .ok()
1660 .and_then(|v| v.parse().ok())
1661 .unwrap_or(0);
1662 for p in pos..end {
1663 if p + 1 < input_ids.len() {
1664 if probe >= 1 && p + 2 < input_ids.len() {
1665 let (d1, mut hx) = self.mtp_step_h(
1669 m,
1670 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1671 input_ids[p + 1],
1672 p,
1673 );
1674 let mut ok = d1 == input_ids[p + 2];
1675 Self::chain_probe_note(0, ok);
1676 let mut d_prev = d1;
1677 let mut extra = 0usize;
1678 for j in 1..probe {
1679 if p + 2 + j >= input_ids.len() {
1680 break;
1681 }
1682 let (dj, hj) =
1683 self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
1684 extra += 1;
1685 ok = ok && dj == input_ids[p + 2 + j];
1686 Self::chain_probe_note(j, ok);
1687 d_prev = dj;
1688 hx = hj;
1689 }
1690 m.kv.truncate_last(extra);
1691 } else {
1692 let _ = self.mtp_step(
1693 m,
1694 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1695 input_ids[p + 1],
1696 p,
1697 );
1698 }
1699 }
1700 }
1701 }
1702 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1703 pos = end;
1704 }
1705 }
1706 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
1707 if task_mask.is_none()
1708 && !dyn_prefill
1709 && !graph_prefill
1710 && !pair_off
1711 && self.pair_supported()
1712 {
1713 while pos + 1 < input_ids.len()
1714 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1715 {
1716 let e1 = self.embed_single(input_ids[pos]);
1717 let e2 = self.embed_single(input_ids[pos + 1]);
1718 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1719 self.commit_linear_scratch();
1721 if let Some(m) = &mut mtp {
1722 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1723 if pos + 2 < input_ids.len() {
1724 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1725 .ok()
1726 .and_then(|v| v.parse().ok())
1727 .unwrap_or(0);
1728 if probe >= 1 && pos + 3 < input_ids.len() {
1729 let (d1, mut hx) =
1733 self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
1734 let mut ok = d1 == input_ids[pos + 3];
1735 Self::chain_probe_note(0, ok);
1736 let mut d_prev = d1;
1737 let mut extra = 0usize;
1738 for j in 1..probe {
1739 if pos + 3 + j >= input_ids.len() {
1740 break;
1741 }
1742 let (dj, hj) =
1743 self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
1744 extra += 1;
1745 ok = ok && dj == input_ids[pos + 3 + j];
1746 Self::chain_probe_note(j, ok);
1747 d_prev = dj;
1748 hx = hj;
1749 }
1750 m.kv.truncate_last(extra);
1751 } else {
1752 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1753 }
1754 }
1755 }
1756 hidden = h2;
1757 pos += 2;
1758 }
1759 }
1760 if batch_k > 0
1769 && graph_prefill
1770 && task_mask.is_none()
1771 && !self.o1_active()
1772 && mtp.is_none()
1773 && !dyn_prefill
1774 && pos + 1 < input_ids.len()
1775 {
1776 let hs = self.hidden_size;
1777 let chunk = batch_k;
1778 while pos < input_ids.len() {
1779 let end = (pos + chunk).min(input_ids.len());
1780 let bk = end - pos;
1781 let mut hiddens = vec![0f32; bk * hs];
1782 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1783 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1784 }
1785 let positions: Vec<usize> = (pos..end).collect();
1786 let t_chunk = std::time::Instant::now();
1787 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
1788 if std::env::var("CMF_GRAPH_PROF").is_ok() {
1789 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
1790 eprintln!(
1791 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
1792 bk as f64 / (ms / 1000.0)
1793 );
1794 }
1795 {
1796 use std::sync::atomic::{AtomicBool, Ordering};
1797 static SAID: AtomicBool = AtomicBool::new(false);
1798 if !SAID.swap(true, Ordering::Relaxed) {
1799 if ok_b {
1800 tracing::info!("batched prefill: ACTIVE (k={bk})");
1801 } else {
1802 tracing::warn!("batched prefill declined — per-position graph");
1803 }
1804 }
1805 }
1806 if ok_b {
1807 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1808 pos = end;
1809 } else {
1810 break; }
1812 }
1813 }
1814 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1815 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1816 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1817 if let Some(m) = &mut mtp {
1818 if pos + 1 < input_ids.len() {
1819 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1825 .ok()
1826 .and_then(|v| v.parse().ok())
1827 .unwrap_or(0);
1828 if probe >= 1 && pos + 2 < input_ids.len() {
1829 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
1830 let mut ok = d1 == input_ids[pos + 2];
1831 Self::chain_probe_note(0, ok);
1832 let mut d_prev = d1;
1833 let mut extra = 0usize;
1834 for j in 1..probe {
1835 if pos + 2 + j >= input_ids.len() {
1836 break;
1837 }
1838 let (dj, hj) =
1839 self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
1840 extra += 1;
1841 ok = ok && dj == input_ids[pos + 2 + j];
1842 Self::chain_probe_note(j, ok);
1843 d_prev = dj;
1844 hx = hj;
1845 }
1846 m.kv.truncate_last(extra);
1849 } else {
1850 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1851 }
1852 }
1853 }
1854 pos += 1;
1855 }
1856 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1857 eprintln!(
1858 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1859 input_ids.len(),
1860 _tpf.elapsed().as_secs_f64() * 1000.0
1861 );
1862 }
1863 if self
1866 .cancel
1867 .swap(false, std::sync::atomic::Ordering::Relaxed)
1868 {
1869 self.kv_history.clear();
1870 if let Some(m) = mtp {
1871 self.mtp = Some(m);
1872 }
1873 return Ok(GenerateResult {
1874 text: String::new(),
1875 token_ids: Vec::new(),
1876 prompt_tokens: input_ids.len(),
1877 tokens_generated: 0,
1878 finish_reason: "cancelled".to_string(),
1879 mtp_drafted: 0,
1880 mtp_accepted: 0,
1881 token_confidence: Vec::new(),
1882 traces: Vec::new(),
1883 });
1884 }
1885
1886 self.o1_seal();
1889
1890 macro_rules! commit {
1892 ($id:expr) => {{
1893 all_ids.push($id);
1894 generated += 1;
1895 if self.tokenizer.is_eos($id) {
1896 finish_reason = "stop".to_string();
1897 false
1898 } else {
1899 let token_text = self.tokenizer.decode_token($id);
1900 let mut go = true;
1901 if let Some(ref mut cb) = on_token {
1902 if !cb(&token_text) {
1903 finish_reason = "cancelled".to_string();
1904 go = false;
1905 }
1906 }
1907 go
1908 }
1909 }};
1910 }
1911
1912 let mut next_pos = input_ids.len();
1914 'decode: while generated < max_tokens {
1915 if self
1916 .cancel
1917 .swap(false, std::sync::atomic::Ordering::Relaxed)
1918 {
1919 finish_reason = "cancelled".to_string();
1920 break 'decode;
1921 }
1922 let mut logits = match self.graph_logits.take() {
1923 Some(lg) => lg,
1924 None => {
1925 inference::rms_norm_into(
1926 &hidden,
1927 &self.weights.final_norm,
1928 self.rms_eps,
1929 self.norm_style,
1930 &mut self.ws.n1,
1931 );
1932 self.lm_head_forward(&self.ws.n1)
1933 }
1934 };
1935 let t_next = sampler::sample_with_scratch(
1936 &logits,
1937 &self.sampler_config,
1938 &all_ids,
1939 &mut self.rng,
1940 &mut self.sampler_scratch,
1941 );
1942 if self.confidence_on {
1943 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1944 }
1945 attention::recycle_buf(&mut logits);
1946 if trace_on {
1947 let skill = router.as_ref().and_then(|r| r.active_id());
1951 traces.push(TokenTrace {
1952 t: generated,
1953 token_id: t_next,
1954 confidence: confidence.last().copied().unwrap_or(0.0),
1955 active_skill: skill,
1956 recon: None,
1957 switched: false,
1958 });
1959 }
1960 if !commit!(t_next) {
1961 break 'decode;
1962 }
1963 if generated >= max_tokens {
1964 break 'decode;
1965 }
1966
1967 if self.kv_cache.needs_eviction() {
1968 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1969 self.kv_cache.evict(keep);
1970 }
1971
1972 match &mut mtp {
1973 #[cfg(feature = "gpu")]
1975 Some(m) if graph_spec && generated + 1 < max_tokens && next_pos > 0 => {
1976 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
1977 m,
1978 &hidden,
1979 t_next,
1980 next_pos,
1981 &mut drafted,
1982 &mut accepted,
1983 ) {
1984 next_pos = n_pos;
1985 hidden = new_h;
1986 let mut stopped = false;
1987 for &id in &extra {
1988 if self.confidence_on {
1989 confidence.push(0.0);
1990 }
1991 if !commit!(id) {
1992 stopped = true;
1993 break;
1994 }
1995 }
1996 if stopped {
1997 break 'decode;
1998 }
1999 continue 'decode;
2000 }
2001 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
2003 next_pos += 1;
2004 continue 'decode;
2005 }
2006 Some(m) if !graph_spec && generated + 1 < max_tokens => {
2008 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
2009 drafted += 1;
2010 let emb1 = self.embed_single(t_next);
2011 let emb2 = self.embed_single(draft);
2012 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
2013
2014 inference::rms_norm_into(
2015 &h1,
2016 &self.weights.final_norm,
2017 self.rms_eps,
2018 self.norm_style,
2019 &mut self.ws.n1,
2020 );
2021 let mut logits1 = self.lm_head_forward(&self.ws.n1);
2022 let t_after = sampler::sample_with_scratch(
2023 &logits1,
2024 &self.sampler_config,
2025 &all_ids,
2026 &mut self.rng,
2027 &mut self.sampler_scratch,
2028 );
2029 if self.confidence_on {
2030 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
2031 }
2032 attention::recycle_buf(&mut logits1);
2033 if trace_on {
2034 traces.push(TokenTrace {
2037 t: generated,
2038 token_id: t_after,
2039 confidence: confidence.last().copied().unwrap_or(0.0),
2040 active_skill: None,
2041 recon: None,
2042 switched: false,
2043 });
2044 }
2045 let stop = !commit!(t_after);
2046
2047 if t_after == draft {
2048 accepted += 1;
2049 self.commit_linear_scratch();
2050 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2051 hidden = h2;
2052 next_pos += 2;
2053 } else {
2054 for layer in &mut self.kv_cache.layers {
2056 layer.truncate_last(1);
2057 }
2058 if !stop {
2059 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2060 hidden = self.forward_layers(
2061 &self.embed_single(t_after),
2062 next_pos + 1,
2063 None,
2064 );
2065 }
2066 next_pos += 2;
2067 }
2068 if stop {
2069 break 'decode;
2070 }
2071 }
2072 _ => {
2074 #[cfg(feature = "gpu")]
2079 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2080 static SAID: std::sync::Once = std::sync::Once::new();
2081 SAID.call_once(|| {
2082 eprintln!(
2083 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2084 !self.dsv4_mtp.is_empty(),
2085 task_mask.is_none(),
2086 router.is_none(),
2087 !trace_on,
2088 self.sampler_config.temperature < 1e-6,
2089 self.sampler_config.repetition_penalty == 1.0,
2090 );
2091 });
2092 }
2093 #[cfg(feature = "gpu")]
2094 if Self::dsv4_spec_on()
2095 && self.dsv4.is_some()
2096 && !self.dsv4_mtp.is_empty()
2097 && task_mask.is_none()
2098 && router.is_none()
2099 && !trace_on
2100 && self.sampler_config.temperature < 1e-6
2101 && self.sampler_config.repetition_penalty == 1.0
2102 && generated + 1 < max_tokens
2103 && all_ids.len() >= 2
2104 {
2105 let tip_token = all_ids[all_ids.len() - 2];
2106 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2107 tip_token,
2108 t_next,
2109 next_pos,
2110 &mut drafted,
2111 &mut accepted,
2112 ) {
2113 next_pos = n_pos;
2114 let mut stopped = false;
2115 for &id in &extra {
2116 if self.confidence_on {
2117 confidence.push(0.0);
2118 }
2119 if !commit!(id) {
2120 stopped = true;
2121 break;
2122 }
2123 }
2124 if stopped {
2125 break 'decode;
2126 }
2127 continue 'decode;
2128 }
2129 }
2130 self.graph_want_logits = fuse_lm;
2131 let mut t_fwd = t_next;
2137 let pure_greedy = self.sampler_config.temperature < 1e-6
2138 && self.sampler_config.repetition_penalty == 1.0
2139 && self.sampler_config.suppress_tokens.is_empty();
2140 let burst_k = std::env::var("CMF_MULTISTEP")
2145 .ok()
2146 .and_then(|v| v.parse::<usize>().ok())
2147 .unwrap_or(0);
2148 if pure_greedy
2149 && burst_k >= 1
2150 && fuse_lm
2151 && task_mask.is_none()
2152 && router.is_none()
2153 && !trace_on
2154 && !self.confidence_on
2155 {
2156 let mut stopped = false;
2157 loop {
2158 let room = max_tokens.saturating_sub(generated);
2159 if room <= 2 {
2160 break;
2161 }
2162 let k = burst_k.min(room - 1);
2163 if k < 1 {
2164 break;
2165 }
2166 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2167 break;
2168 };
2169 next_pos += k;
2170 for &id in &ids {
2171 if !commit!(id) {
2172 stopped = true;
2173 break;
2174 }
2175 }
2176 if stopped {
2177 break;
2178 }
2179 t_fwd = *ids.last().unwrap();
2180 }
2181 if stopped {
2182 break 'decode;
2183 }
2184 }
2185 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2186 next_pos += 1;
2187 if let Some(r) = &mut router {
2190 let phi = self.dyn_phi_ema.clone();
2191 let decision = r.step(&phi, generated);
2192 if let Some(new_active) = decision {
2193 let _ = self.set_active_skill(new_active);
2194 }
2195 if trace_on {
2198 if let Some(last) = traces.last_mut() {
2199 let e = r.last_best_e();
2200 last.recon = e.is_finite().then_some(e);
2201 last.switched = decision.is_some();
2202 }
2203 }
2204 }
2205 }
2206 }
2207 }
2208
2209 self.graph_want_logits = false;
2210 self.graph_logits = None;
2211 if router.is_some() {
2213 let _ = self.set_active_skill(None);
2214 }
2215 self.dyn_router = router.or(self.dyn_router.take());
2216 self.mtp = mtp.or(self.mtp.take());
2217
2218 let output_ids = &all_ids[input_ids.len()..];
2219 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2223 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2224 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2226 Ok(GenerateResult {
2227 text: self.tokenizer.decode(output_ids),
2228 token_ids: output_ids.to_vec(),
2229 prompt_tokens: input_ids.len(),
2230 tokens_generated: generated,
2231 finish_reason,
2232 mtp_drafted: drafted,
2233 mtp_accepted: accepted,
2234 token_confidence: confidence,
2235 traces,
2236 })
2237 }
2238
2239 fn mtp_step(
2243 &mut self,
2244 m: &mut MtpModule,
2245 hidden: &[f32],
2246 next_token: u32,
2247 position: usize,
2248 ) -> u32 {
2249 self.mtp_step_h(m, hidden, next_token, position).0
2250 }
2251
2252 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2256 use std::sync::Mutex;
2257 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2258 let mut t = T.lock().unwrap();
2259 if t.len() <= depth {
2260 t.resize(depth + 1, (0, 0));
2261 }
2262 t[depth].0 += 1;
2263 t[depth].1 += prefix_ok as u64;
2264 if depth == 0 && t[0].0 % 128 == 0 {
2265 let line: Vec<String> = t
2266 .iter()
2267 .enumerate()
2268 .map(|(d, (n, k))| format!("d{}={:.0}%({n})", d + 1, 100.0 * *k as f64 / (*n).max(1) as f64))
2269 .collect();
2270 eprintln!("mtp-chain: {}", line.join(" "));
2271 }
2272 }
2273
2274 fn mtp_step_h(
2278 &mut self,
2279 m: &mut MtpModule,
2280 hidden: &[f32],
2281 next_token: u32,
2282 position: usize,
2283 ) -> (u32, Vec<f32>) {
2284 let e = self.embed_single(next_token);
2288 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2289 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2290 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2291 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2292 let mut x = vec![0.0f32; self.hidden_size];
2293 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2294
2295 let lw = &m.layer;
2297 inference::rms_norm_into(
2298 &x,
2299 &lw.input_norm,
2300 self.rms_eps,
2301 self.norm_style,
2302 &mut self.ws.n1,
2303 );
2304 let attn = match &lw.attn {
2305 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2307 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2308 AttnKind::Full {
2309 wq,
2310 wk,
2311 wv,
2312 wo,
2313 q_norm,
2314 k_norm,
2315 output_gate,
2316 softplus_gate,
2317 bias,
2318 } => {
2319 let mut cfg = self.attn_cfg(position);
2320 cfg.q_norm = q_norm.as_deref();
2321 cfg.k_norm = k_norm.as_deref();
2322 cfg.output_gate = *output_gate;
2323 cfg.softplus_gate = softplus_gate
2324 .as_ref()
2325 .map(|(gate, per_head)| (gate, *per_head));
2326 cfg.bias = bias
2327 .as_ref()
2328 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2329 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2330 }
2331 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
2332 unreachable!("MTP block is full attention")
2333 }
2334 };
2335 for (i, &a) in attn.iter().enumerate() {
2336 x[i] += a;
2337 }
2338 inference::rms_norm_into(
2339 &x,
2340 &lw.post_norm,
2341 self.rms_eps,
2342 self.norm_style,
2343 &mut self.ws.p1,
2344 );
2345 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
2346 for (i, &f) in ffn.iter().enumerate() {
2347 x[i] += f;
2348 }
2349
2350 inference::rms_norm_into(
2351 &x,
2352 &m.final_norm,
2353 self.rms_eps,
2354 self.norm_style,
2355 &mut self.ws.n1,
2356 );
2357 let mut lg = self.lm_head_forward(&self.ws.n1);
2358 let draft = sampler::argmax(&lg);
2359 attention::recycle_buf(&mut lg);
2360 (draft, x)
2361 }
2362
2363 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
2367 let e = self.embed_single(next_token);
2368 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2369 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2370 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2371 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2372 let mut x = vec![0.0f32; self.hidden_size];
2373 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2374 inference::rms_norm_into(&x, &m.layer.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
2375 let attn = match &m.layer.attn {
2376 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate, bias } => {
2377 let mut cfg = self.attn_cfg(position);
2378 cfg.q_norm = q_norm.as_deref();
2379 cfg.k_norm = k_norm.as_deref();
2380 cfg.output_gate = *output_gate;
2381 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
2382 cfg.bias = bias.as_ref().map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2383 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2384 }
2385 _ => return,
2386 };
2387 let _ = attn;
2388 }
2389
2390 #[cfg(feature = "gpu")]
2397 #[allow(clippy::too_many_arguments)]
2398 fn graph_spec_step(
2399 &mut self,
2400 m: &mut MtpModule,
2401 hidden: &[f32],
2402 t_next: u32,
2403 next_pos: usize,
2404 drafted: &mut usize,
2405 accepted: &mut usize,
2406 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
2407 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
2408 .ok()
2409 .and_then(|v| v.parse().ok())
2410 .filter(|&v| (1..=8).contains(&v))
2411 .unwrap_or(2);
2412 if next_pos == 0 {
2413 return None;
2414 }
2415 let t_round = std::time::Instant::now();
2416 let mut drafts = Vec::with_capacity(k_spec);
2421 let (d1, mut hx) = self.mtp_step_h(m, hidden, t_next, next_pos - 1);
2422 drafts.push(d1);
2423 for j in 1..k_spec {
2424 let (dj, hj) = self.mtp_step_h(m, &hx, drafts[j - 1], next_pos - 1 + j);
2425 drafts.push(dj);
2426 hx = hj;
2427 }
2428 *drafted += k_spec;
2429 let t_draft = t_round.elapsed();
2430 let b = k_spec + 1;
2433 let mut hiddens = vec![0.0f32; b * self.hidden_size];
2434 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
2435 let e = self.embed_single(t);
2436 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
2437 }
2438 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
2439 let (lm_gw, lm_rows) = {
2440 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
2441 (
2442 crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[] },
2443 self.weights.lm_head.rows(),
2444 )
2445 };
2446 let mut logits = Vec::new();
2447 let final_norm = self.weights.final_norm.clone();
2448 let ok = self.try_batch_graph_wgpu(
2449 &mut hiddens,
2450 &positions,
2451 b,
2452 Some(crate::gpu::SpecTail {
2453 lm: lm_gw,
2454 lm_rows,
2455 final_norm: &final_norm,
2456 logits_out: &mut logits,
2457 }),
2458 );
2459 if !ok {
2460 m.kv.truncate_last(k_spec);
2463 return None;
2464 }
2465 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2466 eprintln!(
2467 "spec-round: draft {:.1} ms | verify {:.1} ms",
2468 t_draft.as_secs_f64() * 1e3,
2469 (t_round.elapsed() - t_draft).as_secs_f64() * 1e3,
2470 );
2471 }
2472 let ids: Vec<u32> = (0..b)
2474 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
2475 .collect();
2476 let mut a = 0usize;
2477 while a < k_spec && ids[a] == drafts[a] {
2478 a += 1;
2479 }
2480 if a + 1 < b {
2482 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
2483 }
2484 *accepted += a;
2485 m.kv.truncate_last(k_spec.saturating_sub(1));
2488 for j in 0..a {
2489 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
2490 let row = row.to_vec();
2491 self.mtp_warm(m, &row, ids[j], next_pos + j);
2492 }
2493 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
2495 row.resize(self.vocab_size, 0.0);
2496 if let Some(c) = self.final_softcap {
2497 for l in row.iter_mut() {
2498 *l = c * (*l / c).tanh();
2499 }
2500 }
2501 self.graph_logits = Some(row);
2502 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
2503 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
2504 }
2505
2506 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
2515 if !self.pair_supported() {
2516 return (0.0, 0.0);
2517 }
2518 let emb1 = self.embed_single(1);
2519 let emb2 = self.embed_single(2);
2520 let pos = self.kv_cache.seq_len();
2521
2522 let t0 = std::time::Instant::now();
2523 for _ in 0..iters {
2524 let _ = self.forward_layers(&emb1, pos, None);
2525 let _ = self.forward_layers(&emb2, pos + 1, None);
2526 for l in &mut self.kv_cache.layers {
2527 l.truncate_last(2);
2528 }
2529 }
2530 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2531
2532 let t1 = std::time::Instant::now();
2533 for _ in 0..iters {
2534 let _ = self.forward_pair(&emb1, &emb2, pos);
2535 for l in &mut self.kv_cache.layers {
2536 l.truncate_last(2);
2537 }
2538 }
2539 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2540 (singles_ms, pair_ms)
2541 }
2542
2543 fn pair_supported(&self) -> bool {
2551 !self.weights.layers.is_empty()
2558 && self.g3n.is_none()
2559 && !self
2560 .weights
2561 .layers
2562 .iter()
2563 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
2564 }
2565
2566 fn forward_pair(
2567 &mut self,
2568 emb1: &[f32],
2569 emb2: &[f32],
2570 position: usize,
2571 ) -> (Vec<f32>, Vec<f32>) {
2572 let mut h1 = emb1.to_vec();
2573 let mut h2 = emb2.to_vec();
2574 let (_nkv, _hd, hs, _rd, eps) = (
2575 self.num_kv_heads,
2576 self.head_dim,
2577 self.hidden_size,
2578 self.rotary_dim,
2579 self.rms_eps,
2580 );
2581 let pool = self.pool.clone();
2582
2583 for li in 0..self.num_layers {
2584 let lw = &self.weights.layers[self.phys_layer(li)];
2585 inference::rms_norm_into(
2588 &h1,
2589 &lw.input_norm,
2590 self.rms_eps,
2591 self.norm_style,
2592 &mut self.ws.n1,
2593 );
2594 inference::rms_norm_into(
2595 &h2,
2596 &lw.input_norm,
2597 self.rms_eps,
2598 self.norm_style,
2599 &mut self.ws.n2,
2600 );
2601
2602 let (a1, a2) = match &lw.attn {
2603 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2604 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2605 AttnKind::Linear(w) => {
2606 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2607 let layer = &mut self.kv_cache.layers[li];
2608 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2609 vmf_phase_pair(
2610 &self.ws.n1,
2611 &self.ws.n2,
2612 w,
2613 &cfg,
2614 state,
2615 scratch,
2616 self.pool.as_deref(),
2617 )
2618 }
2619 AttnKind::LinearGdn(w) => {
2620 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2621 let layer = &mut self.kv_cache.layers[li];
2622 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2623 gdn_pair(
2624 &self.ws.n1,
2625 &self.ws.n2,
2626 w,
2627 &cfg,
2628 state,
2629 scratch,
2630 self.pool.as_deref(),
2631 )
2632 }
2633 AttnKind::ShortConv(w) => {
2634 let cfg = self
2635 .short_conv_cfg
2636 .expect("short-conv layer without short_conv_cfg");
2637 let layer = &mut self.kv_cache.layers[li];
2638 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2639 short_conv_pair(
2640 &self.ws.n1,
2641 &self.ws.n2,
2642 w,
2643 &cfg,
2644 state,
2645 scratch,
2646 self.pool.as_deref(),
2647 )
2648 }
2649 AttnKind::Full {
2650 wq,
2651 wk,
2652 wv,
2653 wo,
2654 q_norm,
2655 k_norm,
2656 output_gate,
2657 softplus_gate,
2658 bias,
2659 } => {
2660 let inv_freq_l = self.layer_inv_freq(li);
2661 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2662 let cfg = QwenAttnCfg {
2663 num_heads: self.layer_num_heads(li),
2664 num_kv_heads: nkv_l,
2665 head_dim: hd_l,
2666 hidden_size: hs,
2667 position,
2668 inv_freq: &inv_freq_l,
2669 rotary_dim: rd_l,
2670 scale: self.attn_scale,
2671 softcap: self.attn_softcap,
2672 window: self.layer_window(li),
2673 v_norm: self.attn_v_norm,
2674 q_norm: q_norm.as_deref(),
2675 k_norm: k_norm.as_deref(),
2676 output_gate: *output_gate,
2677 softplus_gate: softplus_gate
2678 .as_ref()
2679 .map(|(gate, per_head)| (gate, *per_head)),
2680 rope_scale: self.layer_rope_scale(li),
2681 bias: bias
2682 .as_ref()
2683 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2684 rms_eps: eps,
2685 norm_style: self.norm_style,
2686 pool: pool.as_deref(),
2687 };
2688 attention::qwen_attention_pair(
2689 &self.ws.n1,
2690 &self.ws.n2,
2691 wq,
2692 wk,
2693 wv,
2694 wo,
2695 &mut self.kv_cache.layers[li],
2696 &cfg,
2697 )
2698 }
2699 };
2700 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
2701 Some(w) => (
2702 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
2703 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
2704 ),
2705 None => (a1, a2),
2706 };
2707 for i in 0..self.hidden_size {
2708 h1[i] += a1[i];
2709 h2[i] += a2[i];
2710 }
2711 let (mut a1, mut a2) = (a1, a2);
2712 attention::recycle_buf(&mut a1);
2713 attention::recycle_buf(&mut a2);
2714
2715 let lw = &self.weights.layers[self.phys_layer(li)];
2716 inference::rms_norm_into(
2717 &h1,
2718 &lw.post_norm,
2719 self.rms_eps,
2720 self.norm_style,
2721 &mut self.ws.p1,
2722 );
2723 inference::rms_norm_into(
2724 &h2,
2725 &lw.post_norm,
2726 self.rms_eps,
2727 self.norm_style,
2728 &mut self.ws.p2,
2729 );
2730 let (f1, f2) = match &lw.ffn {
2731 FfnKind::DenseMoe(dm) => (
2734 dense_moe_ffn(
2735 dm,
2736 &self.ws.p1,
2737 &h1,
2738 self.rms_eps,
2739 self.norm_style,
2740 self.pool.as_deref(),
2741 ),
2742 dense_moe_ffn(
2743 dm,
2744 &self.ws.p2,
2745 &h2,
2746 self.rms_eps,
2747 self.norm_style,
2748 self.pool.as_deref(),
2749 ),
2750 ),
2751 _ => ffn_forward_pair(
2752 &lw.ffn,
2753 &self.ws.p1,
2754 &self.ws.p2,
2755 self.pool.as_deref(),
2756 None,
2757 ),
2758 };
2759 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
2760 Some(w) => (
2761 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
2762 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
2763 ),
2764 None => (f1, f2),
2765 };
2766 for i in 0..self.hidden_size {
2767 h1[i] += f1[i];
2768 h2[i] += f2[i];
2769 }
2770 let (mut f1, mut f2) = (f1, f2);
2771 attention::recycle_buf(&mut f1);
2772 attention::recycle_buf(&mut f2);
2773 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
2774 for i in 0..self.hidden_size {
2775 h1[i] *= sc;
2776 h2[i] *= sc;
2777 }
2778 }
2779 if self.is_loop_end(li) && li + 1 < self.num_layers {
2781 h1 = inference::rms_norm(
2782 &h1,
2783 &self.weights.final_norm,
2784 self.rms_eps,
2785 self.norm_style,
2786 );
2787 h2 = inference::rms_norm(
2788 &h2,
2789 &self.weights.final_norm,
2790 self.rms_eps,
2791 self.norm_style,
2792 );
2793 }
2794 }
2795 (h1, h2)
2796 }
2797
2798 fn commit_linear_scratch(&mut self) {
2800 for layer in &mut self.kv_cache.layers {
2801 if !layer.linear_scratch.is_empty() {
2802 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
2803 layer.linear_scratch.clear();
2804 }
2805 }
2806 }
2807
2808 pub fn forward_ids(
2811 &mut self,
2812 ids: &[u32],
2813 task_mask: Option<&TaskMask>,
2814 ) -> Result<Vec<f32>, String> {
2815 if ids.is_empty() {
2816 return Err("empty id sequence".to_string());
2817 }
2818 self.kv_cache.clear();
2819 self.kv_history.clear();
2820 self.o1_begin();
2821 let mut hidden = vec![0.0f32; self.hidden_size];
2822 let mut pos = 0usize;
2823 if self.can_prefill_batched() && ids.len() > 2 {
2824 let chunk = prefill_chunk();
2828 let hs = self.hidden_size;
2829 while pos < ids.len() {
2830 let end = (pos + chunk).min(ids.len());
2831 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
2832 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2833 pos = end;
2834 }
2835 }
2836 if task_mask.is_none()
2840 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
2841 && self.pair_supported()
2842 {
2843 while pos + 1 < ids.len() {
2844 let e1 = self.embed_single(ids[pos]);
2845 let e2 = self.embed_single(ids[pos + 1]);
2846 let (_, h2) = self.forward_pair(&e1, &e2, pos);
2847 self.commit_linear_scratch();
2848 hidden = h2;
2849 pos += 2;
2850 }
2851 }
2852 while pos < ids.len() {
2853 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2854 pos += 1;
2855 }
2856 self.o1_seal();
2860 let normed = inference::rms_norm(
2861 &hidden,
2862 &self.weights.final_norm,
2863 self.rms_eps,
2864 self.norm_style,
2865 );
2866 Ok(self.lm_head_forward(&normed))
2867 }
2868
2869 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
2876 let (nll, cnt) = self.nll_ids_from(ids, 0);
2877 (nll / cnt.max(1) as f64).exp()
2878 }
2879
2880 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2885 self.kv_cache.clear();
2886 self.kv_history.clear();
2887 FFN_PROBE.with(|p| {
2888 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2889 });
2890 crate::gpu::cpu_scope(|| {
2891 for (pos, &id) in ids.iter().enumerate() {
2892 let emb = self.embed_single(id);
2893 let _ = self.forward_layers(&emb, pos, None);
2894 }
2895 });
2896 self.kv_cache.clear();
2897 self.kv_history.clear();
2898 FFN_PROBE
2899 .with(|p| p.borrow_mut().take())
2900 .unwrap_or_default()
2901 }
2902
2903 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2907 self.kv_cache.clear();
2908 self.kv_history.clear();
2909 let mut nll = 0f64;
2910 let mut cnt = 0usize;
2911 let mut hidden = vec![0f32; self.hidden_size];
2912 for (pos, &id) in ids.iter().enumerate() {
2913 if pos > 0 {
2914 inference::rms_norm_into(
2915 &hidden,
2916 &self.weights.final_norm,
2917 self.rms_eps,
2918 self.norm_style,
2919 &mut self.ws.n1,
2920 );
2921 let mut logits = self.lm_head_forward(&self.ws.n1);
2922 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2923 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2924 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2925 nll -= p.max(1e-300).ln();
2926 cnt += 1;
2927 attention::recycle_buf(&mut logits);
2928 }
2929 let emb = self.embed_single(id);
2930 hidden = self.forward_layers(&emb, pos, Some(mask));
2931 }
2932 self.kv_cache.clear();
2933 self.kv_history.clear();
2934 (nll / cnt.max(1) as f64).exp()
2935 }
2936
2937 pub fn nll_ids_masked(
2956 &mut self,
2957 ids: &[u32],
2958 start: usize,
2959 task_mask: Option<&TaskMask>,
2960 ) -> (f64, usize) {
2961 self.nll_ids_inner(ids, start, task_mask)
2962 }
2963
2964 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2965 self.nll_ids_inner(ids, start, None)
2966 }
2967
2968 fn nll_ids_inner(
2969 &mut self,
2970 ids: &[u32],
2971 start: usize,
2972 task_mask: Option<&TaskMask>,
2973 ) -> (f64, usize) {
2974 self.kv_cache.clear();
2975 self.kv_history.clear();
2976 let mut nll = 0f64;
2977 let mut cnt = 0usize;
2978 if self.can_prefill_batched() {
2979 const CHUNK: usize = 128;
2985 const LM_SUB: usize = 32;
2986 let n = ids.len().saturating_sub(1);
2987 let hs = self.hidden_size;
2988 let rows = self.weights.lm_head.rows();
2989 let mut pos = 0usize;
2990 while pos < n {
2991 let end = (pos + CHUNK).min(n);
2992 let bsz = end - pos;
2993 let hb = self.prefill_batch_masked(&ids[pos..end], pos, task_mask);
2994 let mut k0 = 0usize;
2995 while k0 < bsz {
2996 let k1 = (k0 + LM_SUB).min(bsz);
2997 let sb = k1 - k0;
2998 if pos + k1 <= start {
3001 k0 = k1;
3002 continue;
3003 }
3004 let mut normed = vec![0.0f32; sb * hs];
3005 for k in 0..sb {
3006 let r = inference::rms_norm(
3007 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
3008 &self.weights.final_norm,
3009 self.rms_eps,
3010 self.norm_style,
3011 );
3012 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
3013 }
3014 let mut logits = vec![0.0f32; sb * rows];
3015 self.weights
3016 .lm_head
3017 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
3018 for k in 0..sb {
3019 if pos + k0 + k < start {
3020 continue;
3021 }
3022 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
3023 if let Some(mu) = self.logit_multiplier {
3024 for v in lg.iter_mut() {
3025 *v *= mu;
3026 }
3027 }
3028 if let Some(c) = self.final_softcap {
3032 for v in lg.iter_mut() {
3033 *v = c * (*v / c).tanh();
3034 }
3035 }
3036 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
3037 let target = ids[pos + k0 + k + 1] as usize;
3038 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3039 let lse: f64 = lg
3040 .iter()
3041 .map(|&v| ((v - max) as f64).exp())
3042 .sum::<f64>()
3043 .ln()
3044 + max as f64;
3045 nll += lse - lg[target] as f64;
3046 cnt += 1;
3047 if std::env::var("CMF_PPL_TRACE").is_ok() {
3048 let top = lg
3049 .iter()
3050 .enumerate()
3051 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3052 .map(|(i, _)| i)
3053 .unwrap_or(0);
3054 eprintln!(
3055 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
3056 pos + k0 + k,
3057 target,
3058 lse - lg[target] as f64,
3059 top,
3060 lg[target],
3061 lg[top]
3062 );
3063 }
3064 }
3065 k0 = k1;
3066 }
3067 pos = end;
3068 }
3069 self.kv_cache.clear();
3070 self.kv_history.clear();
3071 return (nll, cnt);
3072 }
3073 for pos in 0..ids.len().saturating_sub(1) {
3074 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
3075 let out_of_band = self.graph_logits.take();
3083 if pos < start {
3084 continue;
3085 }
3086 let logits = match out_of_band {
3087 Some(lg) => lg,
3088 None => {
3089 let normed = inference::rms_norm(
3090 &hidden,
3091 &self.weights.final_norm,
3092 self.rms_eps,
3093 self.norm_style,
3094 );
3095 self.lm_head_forward(&normed)
3099 }
3100 };
3101 let target = ids[pos + 1] as usize;
3102 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3103 let lse: f64 = logits
3104 .iter()
3105 .map(|&v| ((v - max) as f64).exp())
3106 .sum::<f64>()
3107 .ln()
3108 + max as f64;
3109 let tok_nll = lse - logits[target] as f64;
3110 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3111 let top = logits
3112 .iter()
3113 .enumerate()
3114 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3115 .map(|(i, _)| i)
3116 .unwrap_or(0);
3117 eprintln!(
3118 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3119 logits[target], logits[top]
3120 );
3121 }
3122 nll += tok_nll;
3123 cnt += 1;
3124 }
3125 self.kv_cache.clear();
3126 self.kv_history.clear();
3127 (nll, cnt)
3128 }
3129
3130 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
3146 self.kv_cache.clear();
3147 self.kv_history.clear();
3148 self.o1_begin();
3149 let n = ids.len().saturating_sub(1);
3150 let p = prefill.min(n);
3151 let mut pos = 0usize;
3153 if self.can_prefill_batched() {
3154 const CHUNK: usize = 128;
3155 while pos < p {
3156 let end = (pos + CHUNK).min(p);
3157 let _ = self.prefill_batch(&ids[pos..end], pos);
3158 pos = end;
3159 }
3160 } else {
3161 while pos < p {
3162 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3163 pos += 1;
3164 }
3165 }
3166 self.o1_seal();
3167
3168 let mut nll = 0f64;
3169 let mut cnt = 0usize;
3170 for pos in p..n {
3171 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3172 let normed = inference::rms_norm(
3173 &hidden,
3174 &self.weights.final_norm,
3175 self.rms_eps,
3176 self.norm_style,
3177 );
3178 let logits = self.lm_head_forward(&normed);
3182 let target = ids[pos + 1] as usize;
3183 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3184 let lse: f64 = logits
3185 .iter()
3186 .map(|&v| ((v - max) as f64).exp())
3187 .sum::<f64>()
3188 .ln()
3189 + max as f64;
3190 let tok_nll = lse - logits[target] as f64;
3191 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3192 let top = logits
3193 .iter()
3194 .enumerate()
3195 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3196 .map(|(i, _)| i)
3197 .unwrap_or(0);
3198 eprintln!(
3199 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3200 logits[target], logits[top]
3201 );
3202 }
3203 nll += tok_nll;
3204 cnt += 1;
3205 }
3206 self.kv_cache.clear();
3207 self.kv_history.clear();
3208 (nll, cnt)
3209 }
3210
3211 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
3219 self.kv_cache.clear();
3220 self.kv_history.clear();
3221 let n = ids.len().saturating_sub(1);
3222 let mut correct = Vec::with_capacity(n);
3223 let mut pmax = Vec::with_capacity(n);
3224 for pos in 0..n {
3225 let emb = self.embed_single(ids[pos]);
3226 let hidden = self.forward_layers(&emb, pos, None);
3227 let normed = inference::rms_norm(
3228 &hidden,
3229 &self.weights.final_norm,
3230 self.rms_eps,
3231 self.norm_style,
3232 );
3233 let logits = self.lm_head_forward(&normed);
3237 let target = ids[pos + 1] as usize;
3238 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
3239 for (i, &v) in logits.iter().enumerate() {
3240 if v > mval {
3241 mval = v;
3242 amax = i;
3243 }
3244 }
3245 correct.push(amax == target);
3246 let row: Vec<f32> = temps
3247 .iter()
3248 .map(|&t| {
3249 let tt = t.max(1e-3);
3250 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
3251 1.0 / s.max(1e-12) })
3253 .collect();
3254 pmax.push(row);
3255 }
3256 self.kv_cache.clear();
3257 self.kv_history.clear();
3258 (correct, pmax)
3259 }
3260
3261 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
3268 let mut router = match self.dyn_router.take() {
3269 Some(r) => r,
3270 None => return (self.ppl_ids(ids), 0),
3271 };
3272 router.reset();
3273 self.dyn_phi_seen = 0;
3274 let _ = self.set_active_skill(None);
3275
3276 self.kv_cache.clear();
3277
3278 self.kv_history.clear();
3279 let mut nll = 0f64;
3280 let mut cnt = 0usize;
3281 for pos in 0..ids.len().saturating_sub(1) {
3282 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3283 let normed = inference::rms_norm(
3284 &hidden,
3285 &self.weights.final_norm,
3286 self.rms_eps,
3287 self.norm_style,
3288 );
3289 let logits = self.lm_head_forward(&normed);
3293 let target = ids[pos + 1] as usize;
3294 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3295 let lse: f64 = logits
3296 .iter()
3297 .map(|&v| ((v - max) as f64).exp())
3298 .sum::<f64>()
3299 .ln()
3300 + max as f64;
3301 let tok_nll = lse - logits[target] as f64;
3302 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3303 let top = logits
3304 .iter()
3305 .enumerate()
3306 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3307 .map(|(i, _)| i)
3308 .unwrap_or(0);
3309 eprintln!(
3310 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3311 logits[target], logits[top]
3312 );
3313 }
3314 nll += tok_nll;
3315 cnt += 1;
3316 let phi = self.dyn_phi_ema.clone();
3318 if let Some(new_active) = router.step(&phi, pos) {
3319 let _ = self.set_active_skill(new_active);
3320 }
3321 }
3322 let switches = router.switches.len();
3323 let _ = self.set_active_skill(None);
3324 self.dyn_router = Some(router);
3325 self.kv_cache.clear();
3326 self.kv_history.clear();
3327 ((nll / cnt.max(1) as f64).exp(), switches)
3328 }
3329
3330 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
3332 self.kv_cache.clear();
3333 self.kv_history.clear();
3334 let mut acc = vec![0f32; self.hidden_size];
3335 for (pos, &id) in ids.iter().enumerate() {
3336 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
3337 for (a, v) in acc.iter_mut().zip(&h) {
3338 *a += v;
3339 }
3340 }
3341 let n = ids.len().max(1) as f32;
3342 for a in acc.iter_mut() {
3343 *a /= n;
3344 }
3345 self.kv_cache.clear();
3346 self.kv_history.clear();
3347 acc
3348 }
3349
3350 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
3356 self.prefill_batch_masked(ids, start_pos, None)
3357 }
3358
3359 fn prefill_batch_masked(
3365 &mut self,
3366 ids: &[u32],
3367 start_pos: usize,
3368 task_mask: Option<&TaskMask>,
3369 ) -> Vec<f32> {
3370 let b = ids.len();
3371 let hs = self.hidden_size;
3372 let mut h: Vec<f32> = vec![0.0; b * hs];
3375 let mut h_ready = false;
3376 let fill_h = |h: &mut Vec<f32>, me: &Self| {
3377 for (bi, &id) in ids.iter().enumerate() {
3378 let e = me.embed_single(id);
3379 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
3380 }
3381 };
3382 let (_nkv, _hd, _rd, eps) = (
3383 self.num_kv_heads,
3384 self.head_dim,
3385 self.rotary_dim,
3386 self.rms_eps,
3387 );
3388 let pool = self.pool.clone();
3389 let norm_style = self.norm_style;
3390
3391 #[cfg(target_os = "macos")]
3392 let mut chunk_skip_until = 0usize;
3393 for li in 0..self.num_layers {
3394 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
3401 if task_mask.is_none() {
3402 if li < chunk_skip_until {
3403 continue;
3404 }
3405 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
3411 fill_h(&mut h, self);
3412 h_ready = true;
3413 }
3414 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
3415 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
3416 if end > li {
3417 h_ready = true;
3418 chunk_skip_until = end;
3419 if self.is_loop_end(end - 1) && end < self.num_layers {
3422 for bi in 0..b {
3423 let normed = inference::rms_norm(
3424 &h[bi * hs..(bi + 1) * hs],
3425 &self.weights.final_norm,
3426 eps,
3427 norm_style,
3428 );
3429 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3430 }
3431 }
3432 continue;
3433 }
3434 }
3435 if !h_ready {
3436 fill_h(&mut h, self);
3437 h_ready = true;
3438 }
3439 let lw = &self.weights.layers[self.phys_layer(li)];
3440 match &lw.attn {
3442 AttnKind::Kda(w) => {
3443 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
3445 let mut normed = vec![0.0f32; b * hs];
3446 for bi in 0..b {
3447 inference::rms_norm_into(
3448 &h[bi * hs..(bi + 1) * hs],
3449 &lw.input_norm,
3450 eps,
3451 norm_style,
3452 &mut normed[bi * hs..(bi + 1) * hs],
3453 );
3454 }
3455 let attn = crate::linear_core::kda_forward_batch(
3456 &normed,
3457 b,
3458 w,
3459 &cfg,
3460 &mut self.kv_cache.layers[li].linear_state,
3461 pool.as_deref(),
3462 );
3463 for (dst, &a) in h.iter_mut().zip(&attn) {
3464 *dst += a;
3465 }
3466 }
3467 AttnKind::LinearGdn(w) => {
3468 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3470 let mut normed = vec![0.0f32; b * hs];
3471 for bi in 0..b {
3472 let r = inference::rms_norm(
3473 &h[bi * hs..(bi + 1) * hs],
3474 &lw.input_norm,
3475 eps,
3476 norm_style,
3477 );
3478 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3479 }
3480 let attn = crate::linear_core::gdn_forward_batch(
3481 &normed,
3482 b,
3483 w,
3484 &cfg,
3485 &mut self.kv_cache.layers[li].linear_state,
3486 pool.as_deref(),
3487 );
3488 for (dst, &a) in h.iter_mut().zip(&attn) {
3489 *dst += a;
3490 }
3491 }
3492 AttnKind::ShortConv(w) => {
3493 let cfg = self
3496 .short_conv_cfg
3497 .expect("short-conv layer without short_conv_cfg");
3498 let mut normed = vec![0.0f32; b * hs];
3499 for bi in 0..b {
3500 inference::rms_norm_into(
3501 &h[bi * hs..(bi + 1) * hs],
3502 &lw.input_norm,
3503 eps,
3504 norm_style,
3505 &mut normed[bi * hs..(bi + 1) * hs],
3506 );
3507 }
3508 let attn = short_conv_forward_batch(
3509 &normed,
3510 b,
3511 w,
3512 &cfg,
3513 &mut self.kv_cache.layers[li].linear_state,
3514 pool.as_deref(),
3515 );
3516 for (dst, &a) in h.iter_mut().zip(&attn) {
3517 *dst += a;
3518 }
3519 }
3520 AttnKind::Mla(w) => {
3521 let inv_freq_l = self.layer_inv_freq(li);
3524 let rs = self.layer_rope_scale(li);
3525 let mut normed = vec![0.0f32; hs];
3526 for bi in 0..b {
3527 inference::rms_norm_into(
3528 &h[bi * hs..(bi + 1) * hs],
3529 &lw.input_norm,
3530 eps,
3531 norm_style,
3532 &mut normed,
3533 );
3534 let ao = mla_attention(
3535 w,
3536 &normed,
3537 &mut self.kv_cache.layers[li],
3538 start_pos + bi,
3539 &inv_freq_l,
3540 rs,
3541 eps,
3542 pool.as_deref(),
3543 );
3544 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
3545 *dst += a;
3546 }
3547 }
3548 }
3549 AttnKind::Full {
3550 wq,
3551 wk,
3552 wv,
3553 wo,
3554 q_norm,
3555 k_norm,
3556 output_gate,
3557 softplus_gate,
3558 bias,
3559 } => {
3560 let mut normed = vec![0.0f32; b * hs];
3564 for bi in 0..b {
3565 inference::rms_norm_into(
3566 &h[bi * hs..(bi + 1) * hs],
3567 &lw.input_norm,
3568 eps,
3569 norm_style,
3570 &mut normed[bi * hs..(bi + 1) * hs],
3571 );
3572 }
3573 let inv_freq_l = self.layer_inv_freq(li);
3574 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3575 let cfg = QwenAttnCfg {
3576 num_heads: self.layer_num_heads(li),
3577 num_kv_heads: nkv_l,
3578 head_dim: hd_l,
3579 hidden_size: hs,
3580 position: start_pos,
3581 inv_freq: &inv_freq_l,
3582 rotary_dim: rd_l,
3583 scale: self.attn_scale,
3584 softcap: self.attn_softcap,
3585 window: self.layer_window(li),
3586 v_norm: self.attn_v_norm,
3587 q_norm: q_norm.as_deref(),
3588 k_norm: k_norm.as_deref(),
3589 output_gate: *output_gate,
3590 softplus_gate: softplus_gate
3591 .as_ref()
3592 .map(|(gate, per_head)| (gate, *per_head)),
3593 rope_scale: self.layer_rope_scale(li),
3594 bias: bias
3595 .as_ref()
3596 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3597 rms_eps: eps,
3598 norm_style,
3599 pool: pool.as_deref(),
3600 };
3601 let mut attn = attention::qwen_attention_batch(
3602 &normed,
3603 b,
3604 wq,
3605 wk,
3606 wv,
3607 wo,
3608 &mut self.kv_cache.layers[li],
3609 &cfg,
3610 );
3611 if let Some(w) = &lw.attn_out_norm {
3612 for bi in 0..b {
3613 inference::rms_norm_into(
3614 &attn[bi * hs..(bi + 1) * hs],
3615 w,
3616 eps,
3617 norm_style,
3618 &mut normed[bi * hs..(bi + 1) * hs],
3619 );
3620 }
3621 attn.copy_from_slice(&normed);
3622 }
3623 for (dst, &a) in h.iter_mut().zip(&attn) {
3624 *dst += a;
3625 }
3626 }
3627 AttnKind::Linear(w) => {
3628 for bi in 0..b {
3629 let normed = inference::rms_norm(
3630 &h[bi * hs..(bi + 1) * hs],
3631 &lw.input_norm,
3632 eps,
3633 norm_style,
3634 );
3635 vmf_phase_forward(
3636 &normed,
3637 w,
3638 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
3639 &mut self.kv_cache.layers[li].linear_state,
3640 pool.as_deref(),
3641 )
3642 .iter()
3643 .enumerate()
3644 .for_each(|(i, &a)| h[bi * hs + i] += a);
3645 }
3646 }
3647 }
3648
3649 let lw = &self.weights.layers[self.phys_layer(li)];
3651 let mut post = vec![0.0f32; b * hs];
3652 for bi in 0..b {
3653 let r =
3654 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
3655 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3656 }
3657 let mask_row = task_mask
3660 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
3661 .and_then(|m| m.ffn_masks.get(li))
3662 .map(|v| v.as_slice());
3663 let mut ffn = match &lw.ffn {
3664 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
3665 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
3666 FfnKind::DenseMoe(dm) => {
3669 let mut out = vec![0.0f32; b * hs];
3670 for bi in 0..b {
3671 let r = dense_moe_ffn(
3672 dm,
3673 &post[bi * hs..(bi + 1) * hs],
3674 &h[bi * hs..(bi + 1) * hs],
3675 eps,
3676 norm_style,
3677 pool.as_deref(),
3678 );
3679 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3680 }
3681 out
3682 }
3683 };
3684 if let Some(w) = &lw.ffn_out_norm {
3685 for bi in 0..b {
3686 inference::rms_norm_into(
3687 &ffn[bi * hs..(bi + 1) * hs],
3688 w,
3689 eps,
3690 norm_style,
3691 &mut post[bi * hs..(bi + 1) * hs],
3692 );
3693 }
3694 ffn.copy_from_slice(&post);
3695 }
3696 for (dst, &f) in h.iter_mut().zip(&ffn) {
3697 *dst += f;
3698 }
3699 if let Some(sc) = lw.layer_scale {
3700 for v in h.iter_mut() {
3701 *v *= sc;
3702 }
3703 }
3704 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
3705 if let Some(t) = tp.parse::<usize>().ok() {
3706 if t >= start_pos && t < start_pos + b {
3707 let bi = t - start_pos;
3708 let row = &h[bi * hs..(bi + 1) * hs];
3709 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
3710 eprintln!(
3711 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
3712 row[0], row[1]
3713 );
3714 }
3715 }
3716 }
3717 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
3721 let row = &h[(b - 1) * hs..b * hs];
3722 let rms =
3723 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
3724 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
3725 eprintln!(
3726 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
3727 match &self.weights.layers[self.phys_layer(li)].attn {
3728 AttnKind::LinearGdn(_) => "gdn",
3729 AttnKind::Linear(_) => "vmf",
3730 AttnKind::ShortConv(_) => "conv",
3731 _ => "attn",
3732 },
3733 match &lw.ffn {
3734 FfnKind::Moe(_) => "moe",
3735 FfnKind::Dense(_) => "dense",
3736 FfnKind::DenseMoe(_) => "dense+moe",
3737 },
3738 );
3739 }
3740 if self.is_loop_end(li) && li + 1 < self.num_layers {
3742 for bi in 0..b {
3743 let normed = inference::rms_norm(
3744 &h[bi * hs..(bi + 1) * hs],
3745 &self.weights.final_norm,
3746 eps,
3747 norm_style,
3748 );
3749 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3750 }
3751 }
3752 if std::env::var("CMF_TRACE_H").is_ok() {
3753 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
3754 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
3755 eprintln!(
3756 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
3757 lw.layer_scale
3758 );
3759 }
3760 }
3761 crate::gpu::set_layer(-1); h
3763 }
3764
3765 fn embed_single(&self, id: u32) -> Vec<f32> {
3767 let mut out = vec![0.0f32; self.hidden_size];
3768 if (id as usize) < self.weights.embed_tokens.rows() {
3769 self.weights.embed_tokens.row_f32(id as usize, &mut out);
3770 }
3771 if self.embed_multiplier != 1.0 {
3772 for v in out.iter_mut() {
3773 *v *= self.embed_multiplier;
3774 }
3775 }
3776 if self.dsv4.is_some() {
3780 let mut v = vec![0.0f32; self.hidden_size.max(1)];
3781 v[0] = id as f32;
3782 return v;
3783 }
3784 if let Some(b) = &self.g3n {
3787 return b.0.extend_embedding(id, &out, self.pool.as_deref());
3788 }
3789 out
3790 }
3791
3792 #[cfg(target_os = "macos")]
3798 fn chunk_run_gpu(
3799 &mut self,
3800 li0: usize,
3801 h: &mut [f32],
3802 b: usize,
3803 pos0: usize,
3804 embed_ids: Option<&[u32]>,
3805 ) -> usize {
3806 if !crate::gpu::enabled_here()
3810 || std::env::var("CMF_GPU_CHUNK")
3811 .map(|v| v == "0")
3812 .unwrap_or(false)
3813 || b < 32
3814 || self.swa.is_some()
3815 || self.global_attn.is_some()
3816 || self.attn_v_norm
3817 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
3818 {
3819 return li0;
3820 }
3821 let Some(model) = self.model.clone() else {
3822 return li0;
3823 };
3824 let inv_freq = self.inv_freq.clone();
3825 let (nh, nkv, hd, hs) = (
3826 self.num_heads,
3827 self.num_kv_heads,
3828 self.head_dim,
3829 self.hidden_size,
3830 );
3831 let loop_end = if self.loop_final_norm {
3835 ((li0 / self.physical_layers) + 1) * self.physical_layers
3836 } else {
3837 self.num_layers
3838 };
3839 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
3840 let mut stored_at: Vec<usize> = Vec::new();
3841 for li in li0..self.num_layers.min(loop_end) {
3842 let lw = &self.weights.layers[self.phys_layer(li)];
3843 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
3844 break;
3845 }
3846 let AttnKind::Full {
3847 wq,
3848 wk,
3849 wv,
3850 wo,
3851 q_norm,
3852 k_norm,
3853 output_gate: false,
3854 softplus_gate: None,
3855 bias,
3856 } = &lw.attn
3857 else {
3858 break;
3859 };
3860 let FfnKind::Dense(d) = &lw.ffn else { break };
3861 if d.act != Act::Silu {
3862 break;
3863 }
3864 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
3869 t.q8_row_parts()
3870 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3871 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3872 }
3873 let parts = (
3874 cw(wq),
3875 cw(wk),
3876 cw(wv),
3877 cw(wo),
3878 cw(&d.gate_proj),
3879 cw(&d.up_proj),
3880 cw(&d.down_proj),
3881 );
3882 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
3883 else {
3884 break;
3885 };
3886 let layer = &self.kv_cache.layers[li];
3887 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
3888 break;
3889 }
3890 stored_at.push(layer.head_len(0));
3891 layers.push(crate::gpu_metal::ChunkLayer {
3892 model: &model,
3893 kv_id: self.graph_kv_id,
3894 layer: li,
3895 wq: pq,
3896 wk: pk,
3897 wv: pv,
3898 wo: po,
3899 gate: pg,
3900 up: pu,
3901 down: pd,
3902 input_norm: &lw.input_norm,
3903 post_norm: &lw.post_norm,
3904 bias: bias
3905 .as_ref()
3906 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
3907 q_norm: q_norm.as_deref(),
3908 k_norm: k_norm.as_deref(),
3909 inv_freq: &inv_freq,
3910 rd: self.rotary_dim,
3911 nh,
3912 nkv,
3913 hd,
3914 hs,
3915 inter: d.gate_proj.rows(),
3916 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
3917 eps: self.rms_eps as f32,
3918 });
3919 }
3920 if layers.is_empty() {
3921 return li0;
3922 }
3923 let row = nkv * hd;
3924 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
3925 .iter()
3926 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
3927 .collect();
3928 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
3929 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
3930 let li = layers[i].layer;
3931 let layer = &self.kv_cache.layers[li];
3932 io.push(crate::gpu_metal::ChunkIo {
3933 cpu_stored: stored_at[i],
3934 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
3935 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
3936 out_k: ok,
3937 out_v: ov,
3938 imp: oi,
3939 });
3940 }
3941 let n_run = layers.len();
3942 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
3943 let ep = embed_ids.and_then(|ids| {
3946 self.weights
3947 .embed_tokens
3948 .q8_row_parts()
3949 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
3950 idx,
3951 rows,
3952 row_scale: rs,
3953 ids,
3954 mult: self.embed_multiplier,
3955 })
3956 });
3957 if embed_ids.is_some() && ep.is_none() {
3958 return li0;
3959 }
3960 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
3961 return li0;
3962 }
3963 drop(io);
3964 drop(layers);
3965 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
3968 let li = li0 + i;
3969 let layer = &mut self.kv_cache.layers[li];
3970 for bi in 0..b {
3971 layer.append(
3972 &ok[bi * row..(bi + 1) * row],
3973 &ov[bi * row..(bi + 1) * row],
3974 &[],
3975 );
3976 }
3977 layer.accumulate_imp(oi);
3978 }
3979 last
3980 }
3981
3982 fn layer_is_local(&self, li: usize) -> bool {
3985 if let Some(layers) = &self.sliding_layers {
3986 return layers.get(li).copied().unwrap_or(false);
3987 }
3988 match self.swa {
3989 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
3990 None => false,
3991 }
3992 }
3993
3994 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
3997 if self.layer_is_local(li) {
3998 if let Some(f) = &self.inv_freq_local {
3999 return f.clone();
4000 }
4001 } else if let Some(f) = &self.inv_freq_global {
4002 return f.clone();
4003 }
4004 self.inv_freq.clone()
4005 }
4006
4007 fn layer_window(&self, li: usize) -> Option<usize> {
4009 self.swa
4010 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
4011 }
4012
4013 fn layer_num_heads(&self, li: usize) -> usize {
4014 self.attention_heads_per_layer
4015 .as_ref()
4016 .and_then(|v| v.get(li).copied())
4017 .unwrap_or(self.num_heads)
4018 }
4019
4020 fn layer_rope_scale(&self, li: usize) -> f32 {
4021 if self.layer_is_local(li) {
4022 self.rope_scale_local
4023 } else {
4024 self.rope_scale
4025 }
4026 }
4027
4028 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
4031 if !self.layer_is_local(li) {
4032 if let Some((ghd, gkv)) = self.global_attn {
4033 return (gkv, ghd, ghd);
4034 }
4035 }
4036 (
4037 self.num_kv_heads,
4038 self.head_dim,
4039 if self.layer_is_local(li) {
4040 self.rotary_dim_local.unwrap_or(self.rotary_dim)
4041 } else {
4042 self.rotary_dim
4043 },
4044 )
4045 }
4046
4047 fn forward_layers(
4049 &mut self,
4050 hidden: &[f32],
4051 position: usize,
4052 task_mask: Option<&TaskMask>,
4053 ) -> Vec<f32> {
4054 self.forward_layers_upto(hidden, position, task_mask, None)
4055 }
4056
4057 fn try_token_graph_wgpu(
4061 &self,
4062 hidden: &[f32],
4063 position: usize,
4064 logits_out: &mut Vec<f32>,
4065 layers_run: &mut usize,
4066 ) -> Option<Vec<f32>> {
4067 self.try_token_graph_wgpu_steps(hidden, position, logits_out, 1, None, Some(layers_run))
4068 }
4069
4070 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
4074 if self.o1_active() || self.attn_softcap > 0.0 {
4075 return None;
4076 }
4077 let graph_on = match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
4078 Some("0") => return None,
4079 Some(_) => true,
4080 None => crate::gpu::wgpu_graph_default(),
4081 };
4082 if !graph_on {
4083 return None;
4084 }
4085 let emb = self.embed_single(t_next);
4086 let mut lg = Vec::new();
4087 let mut ids = Vec::new();
4088 self.try_token_graph_wgpu_steps(&emb, position, &mut lg, k, Some(&mut ids), None)?;
4089 (ids.len() == k).then_some(ids)
4090 }
4091
4092 fn try_token_graph_wgpu_steps(
4096 &self,
4097 hidden: &[f32],
4098 position: usize,
4099 logits_out: &mut Vec<f32>,
4100 steps: usize,
4101 ids_out: Option<&mut Vec<u32>>,
4102 layers_run: Option<&mut usize>,
4103 ) -> Option<Vec<f32>> {
4104 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
4107 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
4108 return None;
4112 }
4113 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (0..self.num_layers)
4118 .map(|li| {
4119 if !o1_gpu {
4120 return None;
4121 }
4122 self.kv_cache.layers[self.phys_layer(li)].o1_views()
4123 })
4124 .collect();
4125 if self.o1_active() && o1_gpu {
4126 let want: usize = (0..self.num_layers)
4129 .filter(|li| !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None))
4130 .count();
4131 let have = o1_views.iter().filter(|v| v.is_some()).count();
4132 if want == 0 || have != want {
4133 return None;
4134 }
4135 }
4136 let nh = self.num_heads;
4137 let (nkv, hd, rd) = self.layer_geom(0);
4138 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4139 let mut layers = Vec::with_capacity(self.num_layers);
4140 let mut model = None;
4141 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
4142 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4143 if let Some((_, i, kind, rs)) = t.graph_weight() {
4144 return Some(crate::gpu::GraphW {
4145 idx: i,
4146 kind,
4147 row_scale: rs,
4148 data: &[],
4149 });
4150 }
4151 t.as_f32().map(|d| crate::gpu::GraphW {
4153 idx: 0,
4154 kind: 4,
4155 row_scale: &[],
4156 data: d,
4157 })
4158 }
4159 for li in 0..self.num_layers {
4160 let lw = &self.weights.layers[self.phys_layer(li)];
4161 if dbg {
4162 let ak = match &lw.attn {
4163 AttnKind::Mla(_) => "Mla".into(),
4164 AttnKind::Full {
4165 output_gate, bias, ..
4166 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
4167 AttnKind::LinearGdn(_) => "LinearGdn".into(),
4168 AttnKind::Kda(_) => "Kda".into(),
4169 AttnKind::Linear(_) => "Linear".into(),
4170 AttnKind::ShortConv(_) => "ShortConv".into(),
4171 };
4172 let fk = match &lw.ffn {
4173 FfnKind::Dense(_) => "Dense",
4174 FfnKind::Moe(_) => "Moe",
4175 FfnKind::DenseMoe(_) => "DenseMoe",
4176 };
4177 eprintln!("graph L{li}: attn={ak} ffn={fk}");
4178 }
4179 let gffn = match &lw.ffn {
4180 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
4182 gate: gw(&d.gate_proj)?,
4183 up: gw(&d.up_proj)?,
4184 down: gw(&d.down_proj)?,
4185 },
4186 FfnKind::Moe(m) => {
4187 if m.router_sigmoid
4192 || m.expert_bias.is_some()
4193 || m.route_tau.is_some()
4194 || m.mask.is_some()
4195 {
4196 return None;
4197 }
4198 let (se, sg) = m.shared.as_ref()?;
4199 let sgate = gw(sg.as_ref()?)?;
4200 let router = gw(&m.router)?;
4201 let inter = m.experts.first()?.gate_proj.rows();
4202 let mut experts = Vec::with_capacity(m.experts.len() + 1);
4203 let mut q4tp: Option<bool> = None;
4206 let mut gu_q2: Option<bool> = None;
4209 for e in m.experts.iter().chain(std::iter::once(se)) {
4210 if !matches!(e.act, Act::Silu)
4211 || e.gate_proj.rows() != inter
4212 || e.up_proj.rows() != inter
4213 {
4214 return None;
4215 }
4216 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
4217 Some((mm, gi)) => (
4218 mm,
4219 gi,
4220 e.up_proj.mapped_q4t()?.1,
4221 e.down_proj.mapped_q4t()?.1,
4222 false,
4223 false,
4224 ),
4225 None => match e.gate_proj.mapped_q2tp() {
4226 Some((mm, gi)) => (
4227 mm,
4228 gi,
4229 e.up_proj.mapped_q2tp()?.1,
4230 e.down_proj.mapped_q4tp()?.1,
4231 true,
4232 true,
4233 ),
4234 None => {
4235 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
4236 (
4237 mm,
4238 gi,
4239 e.up_proj.mapped_q4tp()?.1,
4240 e.down_proj.mapped_q4tp()?.1,
4241 true,
4242 false,
4243 )
4244 }
4245 },
4246 };
4247 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
4248 {
4249 tracing::warn!(
4255 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
4256 );
4257 return None;
4258 }
4259 model.get_or_insert_with(|| mm.clone());
4260 experts.push((gi, ui, di));
4261 }
4262 crate::gpu::GraphFfn::Moe {
4263 router,
4264 shared_gate: sgate,
4265 experts,
4266 n_exp: m.experts.len(),
4267 top_k: std::env::var("CMF_TOPK_PROBE")
4273 .ok()
4274 .and_then(|v| v.parse::<usize>().ok())
4275 .filter(|k| *k > 0 && *k <= m.top_k)
4276 .unwrap_or(m.top_k),
4277 inter,
4278 norm_topk: m.norm_topk_prob,
4279 q4tp: q4tp?,
4280 gu_q2: gu_q2.unwrap_or(false),
4281 }
4282 }
4283 };
4284 let attn = match &lw.attn {
4285 AttnKind::Full {
4286 wq,
4287 wk,
4288 wv,
4289 wo,
4290 q_norm,
4291 k_norm,
4292 output_gate,
4293 softplus_gate,
4294 bias,
4295 } => {
4296 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
4297 return None;
4298 }
4299 let (m, _, _, _) = wq.graph_weight()?;
4300 model = Some(m.clone());
4301 crate::gpu::GraphAttn::Full {
4302 wq: gw(wq)?,
4303 wk: gw(wk)?,
4304 wv: gw(wv)?,
4305 wo: gw(wo)?,
4306 q_norm: q_norm.as_deref(),
4307 k_norm: k_norm.as_deref(),
4308 bias: bias
4309 .as_ref()
4310 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4311 output_gate: *output_gate,
4312 cpu_k: self.kv_cache.layers[li].k_heads(),
4313 cpu_v: self.kv_cache.layers[li].v_heads(),
4314 }
4315 }
4316 AttnKind::LinearGdn(w) => {
4317 let cfg = self.gdn_cfg?;
4318 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
4319 model = Some(m.clone());
4320 crate::gpu::GraphAttn::Gdn {
4321 qkv: gw(&w.in_proj_qkv)?,
4322 z: gw(&w.in_proj_z)?,
4323 a: gw(&w.in_proj_a)?,
4324 b: gw(&w.in_proj_b)?,
4325 out: gw(&w.out_proj)?,
4326 conv1d: &w.conv1d,
4327 a_log: &w.a_log,
4328 dt_bias: &w.dt_bias,
4329 norm: &w.norm,
4330 nv: cfg.num_v_heads,
4331 nk: cfg.num_k_heads,
4332 dk: cfg.key_head_dim,
4333 dv: cfg.value_head_dim,
4334 kk: cfg.conv_kernel,
4335 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
4336 }
4337 }
4338 _ => return None,
4339 };
4340 layers.push(crate::gpu::GraphLayer {
4341 input_norm: &lw.input_norm,
4342 attn,
4343 post_norm: &lw.post_norm,
4344 ffn: gffn,
4345 });
4346 }
4347 let model = model?;
4348 let lm_gw = if self.graph_want_logits
4354 && std::env::var("CMF_GPU_LMHEAD")
4355 .map(|v| v != "0")
4356 .unwrap_or(true)
4357 {
4358 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
4359 (
4360 crate::gpu::GraphW {
4361 idx: i,
4362 kind,
4363 row_scale: rs,
4364 data: &[],
4365 },
4366 self.weights.lm_head.rows(),
4367 )
4368 })
4369 } else {
4370 None
4371 };
4372 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
4373 let emb_gw = if steps > 1 {
4375 self.weights
4376 .embed_tokens
4377 .graph_weight()
4378 .map(|(_, i, kind, rs)| {
4379 (
4380 crate::gpu::GraphW {
4381 idx: i,
4382 kind,
4383 row_scale: rs,
4384 data: &[],
4385 },
4386 self.weights.embed_tokens.rows(),
4387 self.embed_multiplier as f32,
4388 )
4389 })
4390 } else {
4391 None
4392 };
4393
4394 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
4397 (0..self.num_layers - 1)
4398 .filter(|&li| (li + 1) % self.physical_layers == 0)
4399 .collect()
4400 } else {
4401 Vec::new()
4402 };
4403 let mut h = hidden.to_vec();
4404 crate::gpu::forward_token_graph(
4405 &model,
4406 self.graph_kv_id,
4407 &layers,
4408 &o1_views,
4409 self.o1_epoch,
4410 &self.inv_freq,
4411 &mut h,
4412 nh,
4413 nkv,
4414 hd,
4415 rd,
4416 self.hidden_size,
4417 self.intermediate_size,
4418 position,
4419 self.kv_cache.max_seq_len,
4420 gemma,
4421 self.rms_eps as f32,
4422 lm,
4423 &self.weights.final_norm,
4424 logits_out,
4425 &loop_norm_at,
4426 steps,
4427 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
4428 ids_out,
4429 layers_run,
4430 )
4431 .then_some(h)
4432 }
4433
4434 fn try_batch_graph_wgpu(
4439 &self,
4440 hiddens: &mut [f32],
4441 positions: &[usize],
4442 k: usize,
4443 spec: Option<crate::gpu::SpecTail<'_>>,
4444 ) -> bool {
4445 let _tb = std::time::Instant::now();
4446 if self.attn_softcap > 0.0 {
4447 return false; }
4449 if self.o1_active() {
4450 return false;
4451 }
4452 let nh = self.num_heads;
4453 let (nkv, hd, rd) = self.layer_geom(0);
4454 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4455 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4456 if let Some((_, i, kind, rs)) = t.graph_weight() {
4457 return Some(crate::gpu::GraphW {
4458 idx: i,
4459 kind,
4460 row_scale: rs,
4461 data: &[],
4462 });
4463 }
4464 t.as_f32().map(|d| crate::gpu::GraphW {
4465 idx: 0,
4466 kind: 4,
4467 row_scale: &[],
4468 data: d,
4469 })
4470 }
4471 let built: Option<(
4472 Vec<crate::gpu::GraphLayer<'_>>,
4473 std::sync::Arc<cortiq_core::CmfModel>,
4474 )> = (|| {
4475 let mut layers = Vec::with_capacity(self.num_layers);
4476 let mut model = None;
4477 for li in 0..self.num_layers {
4478 let lw = &self.weights.layers[self.phys_layer(li)];
4479 let gffn = match &lw.ffn {
4486 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
4487 gate: gw(&d.gate_proj)?,
4488 up: gw(&d.up_proj)?,
4489 down: gw(&d.down_proj)?,
4490 },
4491 FfnKind::Moe(m) => {
4492 if m.router_sigmoid
4493 || m.expert_bias.is_some()
4494 || m.route_tau.is_some()
4495 || m.mask.is_some()
4496 {
4497 return None;
4498 }
4499 let (se, sg) = m.shared.as_ref()?;
4500 let sgate = gw(sg.as_ref()?)?;
4501 let router = gw(&m.router)?;
4502 let inter = m.experts.first()?.gate_proj.rows();
4503 let mut experts = Vec::with_capacity(m.experts.len() + 1);
4504 let mut q4tp: Option<bool> = None;
4505 for e in m.experts.iter().chain(std::iter::once(se)) {
4506 if !matches!(e.act, Act::Silu)
4507 || e.gate_proj.rows() != inter
4508 || e.up_proj.rows() != inter
4509 {
4510 return None;
4511 }
4512 let (mm, gi, ui, di, is_p) = match e.gate_proj.mapped_q4t() {
4513 Some((mm, gi)) => (
4514 mm,
4515 gi,
4516 e.up_proj.mapped_q4t()?.1,
4517 e.down_proj.mapped_q4t()?.1,
4518 false,
4519 ),
4520 None => {
4521 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
4522 (
4523 mm,
4524 gi,
4525 e.up_proj.mapped_q4tp()?.1,
4526 e.down_proj.mapped_q4tp()?.1,
4527 true,
4528 )
4529 }
4530 };
4531 if *q4tp.get_or_insert(is_p) != is_p {
4532 return None;
4533 }
4534 model.get_or_insert_with(|| mm.clone());
4535 experts.push((gi, ui, di));
4536 }
4537 crate::gpu::GraphFfn::Moe {
4538 router,
4539 shared_gate: sgate,
4540 experts,
4541 n_exp: m.experts.len(),
4542 top_k: m.top_k,
4543 inter,
4544 norm_topk: m.norm_topk_prob,
4545 q4tp: q4tp?,
4546 gu_q2: false,
4549 }
4550 }
4551 _ => return None,
4552 };
4553 let attn = match &lw.attn {
4554 AttnKind::Full {
4555 wq,
4556 wk,
4557 wv,
4558 wo,
4559 q_norm,
4560 k_norm,
4561 output_gate,
4562 softplus_gate,
4563 bias,
4564 } => {
4565 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
4566 return None;
4567 }
4568 let (m, _, _, _) = wq.graph_weight()?;
4569 model = Some(m.clone());
4570 crate::gpu::GraphAttn::Full {
4571 wq: gw(wq)?,
4572 wk: gw(wk)?,
4573 wv: gw(wv)?,
4574 wo: gw(wo)?,
4575 q_norm: q_norm.as_deref(),
4576 k_norm: k_norm.as_deref(),
4577 bias: bias
4578 .as_ref()
4579 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4580 output_gate: *output_gate,
4581 cpu_k: self.kv_cache.layers[li].k_heads(),
4582 cpu_v: self.kv_cache.layers[li].v_heads(),
4583 }
4584 }
4585 AttnKind::LinearGdn(w) => {
4586 let cfg = self.gdn_cfg?;
4587 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
4588 model = Some(m.clone());
4589 crate::gpu::GraphAttn::Gdn {
4590 qkv: gw(&w.in_proj_qkv)?,
4591 z: gw(&w.in_proj_z)?,
4592 a: gw(&w.in_proj_a)?,
4593 b: gw(&w.in_proj_b)?,
4594 out: gw(&w.out_proj)?,
4595 conv1d: &w.conv1d,
4596 a_log: &w.a_log,
4597 dt_bias: &w.dt_bias,
4598 norm: &w.norm,
4599 nv: cfg.num_v_heads,
4600 nk: cfg.num_k_heads,
4601 dk: cfg.key_head_dim,
4602 dv: cfg.value_head_dim,
4603 kk: cfg.conv_kernel,
4604 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
4605 }
4606 }
4607 _ => return None,
4608 };
4609 layers.push(crate::gpu::GraphLayer {
4610 input_norm: &lw.input_norm,
4611 attn,
4612 post_norm: &lw.post_norm,
4613 ffn: gffn,
4614 });
4615 }
4616 Some((layers, model?))
4617 })();
4618 let Some((layers, model)) = built else {
4619 {
4620 use std::sync::atomic::{AtomicBool, Ordering};
4621 static SAID: AtomicBool = AtomicBool::new(false);
4622 if !SAID.swap(true, Ordering::Relaxed) {
4623 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
4624 }
4625 }
4626 return false;
4627 };
4628 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4629 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
4630 }
4631 crate::gpu::forward_batch_graph(
4632 &model,
4633 self.graph_kv_id,
4634 &layers,
4635 &self.inv_freq,
4636 hiddens,
4637 nh,
4638 nkv,
4639 hd,
4640 rd,
4641 self.hidden_size,
4642 self.intermediate_size,
4643 positions,
4644 self.kv_cache.max_seq_len,
4645 gemma,
4646 self.rms_eps as f32,
4647 k,
4648 spec,
4649 )
4650 }
4651
4652 fn draft_probe() -> bool {
4656 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4657 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
4658}
4659
4660 #[cfg(feature = "gpu")]
4672 fn dsv4_spec_on() -> bool {
4673 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4674 *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC").map(|v| v != "0").unwrap_or(true))
4675 }
4676
4677 #[cfg(feature = "gpu")]
4684 fn dsv4_spec_step(
4685 &mut self,
4686 tip_token: u32,
4687 t_next: u32,
4688 next_pos: usize,
4689 drafted: &mut usize,
4690 accepted_ctr: &mut usize,
4691 ) -> Option<(Vec<u32>, usize)> {
4692 let t_all = std::time::Instant::now();
4693 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4694 thread_local! {
4695 static LAST: std::cell::Cell<Option<std::time::Instant>> =
4696 const { std::cell::Cell::new(None) };
4697 }
4698 LAST.with(|l| {
4699 if let Some(prev) = l.get() {
4700 eprintln!("между раундами {:.1} мс", prev.elapsed().as_secs_f64() * 1e3);
4701 }
4702 l.set(Some(std::time::Instant::now()));
4703 });
4704 }
4705 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4706 eprintln!("spec_step: вход pos={next_pos}");
4707 }
4708 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
4709 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
4710 if self.dspark.is_none() {
4712 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4713 if t.is_empty() {
4714 return None;
4715 }
4716 crate::dsv4::dspark_arm(&t, cfg.dim);
4717 self.dspark = Some(crate::dsv4::DsparkState::new(
4718 self.dsv4_mtp.len(),
4719 &cfg,
4720 t.len(),
4721 ));
4722 }
4723 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4724 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
4725 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4726 eprintln!("spec_step: пак не построился (targets {targets:?})");
4727 }
4728 let pack = pack?;
4729 let block = crate::dsv4::dspark_block();
4730 let b_box = self.dsv4.as_mut()?;
4731 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
4732 let ds = self.dspark.as_mut()?;
4733 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
4736 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
4737 if dbg {
4738 eprintln!("spec_step: нет захвата");
4739 }
4740 return None;
4741 }
4742 ds.have_hidden = true;
4743 let tip_pos = next_pos.checked_sub(1)?;
4744 let draft_started = std::time::Instant::now();
4745 let mut conf = Vec::new();
4746 let props = crate::dsv4::dspark_draft_gpu(
4747 g,
4748 &self.dsv4_mtp,
4749 &cfg,
4750 ds,
4751 pack,
4752 st.kv_id,
4753 tip_token,
4754 tip_pos,
4755 self.pool.as_deref(),
4756 &mut conf,
4757 );
4758 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
4759 *drafted += block;
4760 if props.is_empty() || props[0] != t_next {
4761 if dbg {
4762 eprintln!(
4763 "spec_step: черновик {} (props0={:?} t_next={t_next})",
4764 if props.is_empty() { "пуст" } else { "мимо" },
4765 props.first()
4766 );
4767 }
4768 return None;
4769 }
4770 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
4771 let conf_min = {
4777 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
4778 *M.get_or_init(|| {
4779 std::env::var("CMF_DSPARK_CONF_MIN")
4780 .ok()
4781 .and_then(|v| v.parse().ok())
4782 .unwrap_or(0.0)
4783 })
4784 };
4785 if conf_min > 0.0 && conf.len() >= props.len() {
4786 let mut keep = 1usize;
4787 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
4788 keep += 1;
4789 }
4790 k_verify = k_verify.min(keep.max(2));
4791 }
4792 if k_verify < 2 {
4793 return None;
4794 }
4795 let mut fed = Vec::with_capacity(k_verify);
4796 fed.push(t_next);
4797 fed.extend_from_slice(&props[1..k_verify]);
4798 let mut argmax = Vec::new();
4799 let mut logits_all = Vec::new();
4800 let mut walked = Vec::new();
4801 let txn = crate::dsv4::dsv4_verify_chunk(
4802 g,
4803 layers,
4804 &cfg,
4805 st,
4806 &fed,
4807 next_pos,
4808 &self.inv_freq,
4809 self.pool.as_deref(),
4810 &targets,
4811 &mut argmax,
4812 &mut logits_all,
4813 &mut walked,
4814 );
4815 if txn.is_none() && dbg {
4816 eprintln!("spec_step: verify отказал");
4817 }
4818 let txn = txn?;
4819 let b = fed.len();
4820 let mut accepted = 1usize;
4821 while accepted < b && fed[accepted] == argmax[accepted - 1] {
4822 accepted += 1;
4823 }
4824 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
4829 accepted = 1;
4830 }
4831 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
4832 eprintln!(
4833 "spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}"
4834 );
4835 }
4836 let t_fin = std::time::Instant::now();
4837 if !crate::dsv4::dsv4_spec_finish(
4838 g,
4839 layers,
4840 &cfg,
4841 st,
4842 txn,
4843 accepted,
4844 &fed,
4845 &self.inv_freq,
4846 self.pool.as_deref(),
4847 ) {
4848 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
4849 return None;
4850 }
4851 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4852 eprintln!("finish(k={accepted}): {:.1} мс", t_fin.elapsed().as_secs_f64() * 1e3);
4853 }
4854 *accepted_ctr += accepted - 1;
4855 let (hc, dim) = (cfg.hc_mult, cfg.dim);
4860 let dev_caps: Vec<usize> = targets
4867 .iter()
4868 .copied()
4869 .filter(|&t| {
4870 st.dev_set.get(t).copied().unwrap_or(false)
4871 && !st.partial_set.get(t).copied().unwrap_or(false)
4872 })
4873 .collect();
4874 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
4875 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
4876 return None;
4877 }
4878 for t in 0..accepted {
4879 let tip = t + 1 == accepted;
4880 for (slot, &tl) in targets.iter().enumerate() {
4881 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
4882 let lo = (di * b + t) * hc * dim;
4883 crate::dsv4::dspark_capture(
4884 &caps_all[lo..lo + hc * dim],
4885 &cfg,
4886 slot,
4887 &mut ds.main_hidden,
4888 );
4889 } else if tip
4890 && crate::dsv4::dspark_peek_slot(slot, dim, {
4891 let lo = slot * dim;
4892 &mut ds.main_hidden[lo..lo + dim]
4893 })
4894 {
4895 } else {
4900 crate::dsv4::dspark_capture(
4904 &walked[t * hc * dim..(t + 1) * hc * dim],
4905 &cfg,
4906 slot,
4907 &mut ds.main_hidden,
4908 );
4909 }
4910 }
4911 crate::dsv4::dspark_ring_append(g, &self.dsv4_mtp, &cfg, ds, next_pos + t, self.pool.as_deref());
4912 }
4913 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
4914 self.graph_logits = Some(row);
4915 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
4920 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
4921 crate::dsv4::pick_tally_arm();
4922 }
4923 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4924 eprintln!("spec_step total {:.1} мс (k={accepted})", t_all.elapsed().as_secs_f64() * 1e3);
4925 }
4926 Some((fed[1..accepted].to_vec(), next_pos + accepted))
4927 }
4928
4929 fn dspark_probe(&mut self, position: usize, token_id: u32) {
4930 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
4931 return;
4932 }
4933 let trunk_now = crate::dsv4::pick_tally_take();
4935 crate::dsv4::trunk_freq_note(&trunk_now);
4936 if !trunk_now.is_empty() {
4937 self.dspark_trunk_picks.push(trunk_now);
4938 let keep = crate::dsv4::dspark_block();
4939 if self.dspark_trunk_picks.len() > keep {
4940 self.dspark_trunk_picks.remove(0);
4941 }
4942 }
4943 for p in std::mem::take(&mut self.dspark_pending) {
4946 let Some(i) = position.checked_sub(p.0 + 1) else {
4947 continue;
4948 };
4949 let mut p = p;
4950 if i < p.1.len() {
4951 if p.2 && p.1[i] == token_id {
4952 p.3 = i + 1;
4953 } else {
4954 p.2 = false;
4955 }
4956 if i + 1 < p.1.len() {
4957 self.dspark_pending.push(p);
4958 continue;
4959 }
4960 }
4961 self.dspark_hist.push(p.3);
4962 self.dspark_real.push(token_id);
4963 }
4964 let Some(b) = &mut self.dsv4 else { return };
4965 let (g, layers, cfg) = (&b.0, &b.1, b.2);
4966 let n_layers = layers.len();
4967 if self.dspark.is_none() {
4968 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4969 if t.is_empty() {
4970 return;
4971 }
4972 eprintln!("DSpark: захват со слоёв {t:?}, блок {}", crate::dsv4::dspark_block());
4973 crate::dsv4::dspark_arm(&t, cfg.dim);
4974 self.dspark = Some(crate::dsv4::DsparkState::new(
4975 self.dsv4_mtp.len(),
4976 &cfg,
4977 t.len(),
4978 ));
4979 }
4980 let ds = self.dspark.as_mut().unwrap();
4981 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
4982 return; }
4984 let mut conf = Vec::new();
4985 crate::dsv4::pick_tally_arm();
4986 let draft_started = std::time::Instant::now();
4991 #[cfg(feature = "gpu")]
4992 let gpu_draft = crate::dsv4::dspark_gpu_on();
4993 #[cfg(not(feature = "gpu"))]
4994 let gpu_draft = false;
4995 let props = if gpu_draft {
4996 #[cfg(feature = "gpu")]
4997 {
4998 let kv_id = b.3.kv_id;
4999 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
5000 Some(pk) => crate::dsv4::dspark_draft_gpu(
5001 g,
5002 &self.dsv4_mtp,
5003 &cfg,
5004 ds,
5005 pk,
5006 kv_id,
5007 token_id,
5008 position,
5009 self.pool.as_deref(),
5010 &mut conf,
5011 ),
5012 None => Vec::new(),
5013 }
5014 }
5015 #[cfg(not(feature = "gpu"))]
5016 Vec::new()
5017 } else {
5018 crate::gpu::cpu_scope(|| {
5019 crate::dsv4::dspark_draft(
5020 g,
5021 &self.dsv4_mtp,
5022 &cfg,
5023 ds,
5024 token_id,
5025 position,
5026 self.pool.as_deref(),
5027 &mut conf,
5028 )
5029 })
5030 };
5031 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
5032 let draft_picks = crate::dsv4::pick_tally_take();
5033 crate::dsv4::dspark_freq_note(&draft_picks);
5034 crate::dsv4::pick_tally_arm();
5037 if !props.is_empty() {
5038 let (tu, tt) = {
5042 let flat: Vec<(usize, Vec<usize>)> = self
5043 .dspark_trunk_picks
5044 .iter()
5045 .flat_map(|v| v.iter().cloned())
5046 .collect();
5047 let mut per: std::collections::HashMap<usize, Vec<usize>> =
5049 std::collections::HashMap::new();
5050 for (li, picks) in flat {
5051 per.entry(li).or_default().extend(picks);
5052 }
5053 let n = per.len().max(1);
5054 let mut u = 0usize;
5055 let mut t = 0usize;
5056 for (_, v) in per {
5057 t += v.len();
5058 u += v.iter().collect::<std::collections::HashSet<_>>().len();
5059 }
5060 (u / n, t / n)
5061 };
5062 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
5063 self.dspark_exp.push((tu, tt, du, dt));
5064 self.dspark_pending.push((position, props, true, 0));
5065 }
5066 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
5067 let n = self.dspark_hist.len() as f32;
5068 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
5069 let block = crate::dsv4::dspark_block();
5070 let mut at = vec![0usize; block + 1];
5071 for &k in &self.dspark_hist {
5072 at[k] += 1;
5073 }
5074 let mut surv = Vec::with_capacity(block);
5076 for i in 1..=block {
5077 let k = at[i..].iter().sum::<usize>() as f32 / n;
5078 surv.push(format!("{k:.2}"));
5079 }
5080 let distinct = self
5081 .dspark_real
5082 .iter()
5083 .collect::<std::collections::HashSet<_>>()
5084 .len();
5085 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
5086 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
5087 });
5088 let m = self.dspark_exp.len().max(1);
5089 eprintln!(
5090 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
5091 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
5092 self.dspark_hist.len(),
5093 mean + 1.0,
5094 surv.join(" ")
5095 );
5096 eprintln!(
5097 "DSpark: разных токенов {distinct} из {} (вырожденность), \
5098 эксперты ствол {}/{} на слой за {block} токенов, \
5099 черновик {}/{} за блок, draft {:.2} мс/блок",
5100 self.dspark_real.len(),
5101 tu / m,
5102 tt / m,
5103 du / m,
5104 dt / m,
5105 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
5106 );
5107 }
5108 }
5109
5110 fn forward_layers_upto(
5111 &mut self,
5112 hidden: &[f32],
5113 position: usize,
5114 task_mask: Option<&TaskMask>,
5115 upto: Option<usize>,
5116 ) -> Vec<f32> {
5117 if let Some(b) = &mut self.dsv4 {
5123 let _ = (task_mask, upto);
5124 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
5125 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5126 st.pos = position;
5127 let mut logits = Vec::new();
5128 crate::dsv4::forward_token(
5129 g,
5130 layers,
5131 &cfg,
5132 st,
5133 token_id,
5134 &self.inv_freq,
5135 self.pool.as_deref(),
5136 &mut logits,
5137 );
5138 self.graph_logits = Some(logits);
5139 self.dspark_probe(position, token_id);
5140 return vec![0.0; self.hidden_size];
5143 }
5144 if let Some(b) = &self.g3n {
5147 let _ = (task_mask, upto);
5148 return crate::g3n::g3n_forward(
5149 &b.0,
5150 &b.1,
5151 hidden,
5152 position,
5153 &mut self.kv_cache.layers,
5154 self.num_heads,
5155 self.num_kv_heads,
5156 self.head_dim,
5157 self.pool.as_deref(),
5158 );
5159 }
5160 let mut h = hidden.to_vec();
5161 let (nh, _nkv, _hd, hs, _rd, eps) = (
5164 self.num_heads,
5165 self.num_kv_heads,
5166 self.head_dim,
5167 self.hidden_size,
5168 self.rotary_dim,
5169 self.rms_eps,
5170 );
5171 let pool = self.pool.clone();
5172 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
5184 let graph_on = match graph_env.as_deref() {
5185 Some("0") => false,
5186 Some(_) => true,
5187 None => crate::gpu::wgpu_graph_default(),
5193 };
5194 let graph_trusted =
5195 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
5196 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
5197 let mut tail_start = 0usize;
5198 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
5199 let t_graph = std::time::Instant::now();
5200 let mut lg = Vec::new();
5201 let mut gl = 0usize;
5202 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
5203 graph_note(built.is_some());
5204 if let Some(hh) = built {
5205 let dur = t_graph.elapsed();
5206 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5207 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
5208 }
5209 if gl > 0 && gl < self.num_layers {
5210 h = hh;
5216 tail_start = gl;
5217 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
5218 if !graph_trusted {
5219 crate::gpu::graph_race_record(true, dur);
5220 }
5221 if !lg.is_empty() {
5222 lg.resize(self.vocab_size, 0.0);
5225 if let Some(c) = self.final_softcap {
5226 for l in lg.iter_mut() {
5227 *l = c * (*l / c).tanh();
5228 }
5229 }
5230 self.graph_logits = Some(lg);
5231 }
5232 return hh;
5233 }
5234 }
5240 }
5241 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
5242
5243 #[cfg(target_os = "macos")]
5244 let mut gpu_skip_until = 0usize;
5245 for li in tail_start..self.num_layers {
5246 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
5248 if li > u {
5249 break;
5250 }
5251 }
5252 if let Some(mask) = task_mask {
5253 if !mask.layer_alive(li) {
5254 continue; }
5256 }
5257 #[cfg(target_os = "macos")]
5261 {
5262 if li < gpu_skip_until {
5263 continue;
5264 }
5265 if task_mask.is_none() {
5266 let end = self.q1_graph_gpu(li, upto, position, &mut h);
5267 if end > li {
5268 gpu_skip_until = end;
5269 if self.is_loop_end(end - 1) && end < self.num_layers {
5272 h = inference::rms_norm(
5273 &h,
5274 &self.weights.final_norm,
5275 self.rms_eps,
5276 self.norm_style,
5277 );
5278 }
5279 continue;
5280 }
5281 }
5282 }
5283
5284 let lw = &self.weights.layers[self.phys_layer(li)];
5285 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5286 if tp.parse::<usize>().ok() == Some(position) {
5287 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
5288 eprintln!(
5289 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5290 h[0], h[1]
5291 );
5292 }
5293 }
5294 inference::rms_norm_into(
5297 &h,
5298 &lw.input_norm,
5299 self.rms_eps,
5300 self.norm_style,
5301 &mut self.ws.n1,
5302 );
5303
5304 let attn_out = match &lw.attn {
5305 AttnKind::Mla(w) => {
5306 let inv_freq_l = self.layer_inv_freq(li);
5307 let rs = self.layer_rope_scale(li);
5308 let eps = self.rms_eps;
5309 let pool = self.pool.clone();
5310 mla_attention(
5311 w,
5312 &self.ws.n1,
5313 &mut self.kv_cache.layers[li],
5314 position,
5315 &inv_freq_l,
5316 rs,
5317 eps,
5318 pool.as_deref(),
5319 )
5320 }
5321 AttnKind::Linear(w) => {
5322 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5323 vmf_phase_forward(
5324 &self.ws.n1,
5325 w,
5326 &cfg,
5327 &mut self.kv_cache.layers[li].linear_state,
5328 self.pool.as_deref(),
5329 )
5330 }
5331 AttnKind::Kda(w) => {
5332 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5333 crate::linear_core::kda_forward(
5334 &self.ws.n1,
5335 w,
5336 &cfg,
5337 &mut self.kv_cache.layers[li].linear_state,
5338 self.pool.as_deref(),
5339 )
5340 }
5341 AttnKind::LinearGdn(w) => {
5342 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5343 gdn_forward(
5344 &self.ws.n1,
5345 w,
5346 &cfg,
5347 &mut self.kv_cache.layers[li].linear_state,
5348 self.pool.as_deref(),
5349 )
5350 }
5351 AttnKind::ShortConv(w) => {
5352 let cfg = self
5353 .short_conv_cfg
5354 .expect("short-conv layer without short_conv_cfg");
5355 short_conv_forward(
5356 &self.ws.n1,
5357 w,
5358 &cfg,
5359 &mut self.kv_cache.layers[li].linear_state,
5360 self.pool.as_deref(),
5361 )
5362 }
5363 AttnKind::Full {
5364 wq,
5365 wk,
5366 wv,
5367 wo,
5368 q_norm,
5369 k_norm,
5370 output_gate,
5371 softplus_gate,
5372 bias,
5373 } if self.kv_cache.layers[li].o1_sealed() => {
5374 let inv_freq_l = self.layer_inv_freq(li);
5377 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5378 let cfg = QwenAttnCfg {
5379 num_heads: self.layer_num_heads(li),
5380 num_kv_heads: nkv_l,
5381 head_dim: hd_l,
5382 hidden_size: hs,
5383 position,
5384 inv_freq: &inv_freq_l,
5385 rotary_dim: rd_l,
5386 scale: self.attn_scale,
5387 softcap: self.attn_softcap,
5388 window: None,
5389 v_norm: self.attn_v_norm,
5390 q_norm: q_norm.as_deref(),
5391 k_norm: k_norm.as_deref(),
5392 output_gate: *output_gate,
5393 softplus_gate: softplus_gate
5394 .as_ref()
5395 .map(|(gate, per_head)| (gate, *per_head)),
5396 rope_scale: self.layer_rope_scale(li),
5397 bias: bias
5398 .as_ref()
5399 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5400 rms_eps: eps,
5401 norm_style: self.norm_style,
5402 pool: pool.as_deref(),
5403 };
5404 attention::qwen_attention_nystrom(
5405 &self.ws.n1,
5406 wq,
5407 wk,
5408 wv,
5409 wo,
5410 &mut self.kv_cache.layers[li],
5411 &cfg,
5412 )
5413 }
5414 AttnKind::Full {
5415 wq,
5416 wk,
5417 wv,
5418 wo,
5419 q_norm,
5420 k_norm,
5421 output_gate,
5422 softplus_gate,
5423 bias,
5424 } => 'attn: {
5425 if graph_on
5428 && !*output_gate
5429 && softplus_gate.is_none()
5430 && self.attention_heads_per_layer.is_none()
5431 && bias.is_none()
5432 && task_mask.is_none()
5433 {
5434 let inv_freq_l = self.layer_inv_freq(li);
5435 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5436 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5437 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
5438 wq.mapped_q1(),
5439 wk.mapped_q1(),
5440 wv.mapped_q1(),
5441 wo.mapped_q1(),
5442 ) {
5443 let gm = gm.clone();
5444 let mut out = vec![0f32; hs];
5445 let cache = &self.kv_cache.layers[li];
5446 if crate::gpu::attn_dropin(
5447 &gm,
5448 self.graph_kv_id,
5449 li,
5450 &self.ws.n1,
5451 qi,
5452 ki,
5453 vi,
5454 oi,
5455 q_norm.as_deref(),
5456 k_norm.as_deref(),
5457 &inv_freq_l,
5458 nh,
5459 nkv_l,
5460 hd_l,
5461 rd_l,
5462 hs,
5463 position,
5464 self.kv_cache.max_seq_len,
5465 gemma,
5466 eps as f32,
5467 cache.k_heads(),
5468 cache.v_heads(),
5469 &mut out,
5470 ) {
5471 break 'attn out;
5472 }
5473 }
5474 }
5475 let masked = task_mask
5476 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
5477 .unwrap_or(false);
5478 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
5479 match (masked, f32_view) {
5480 (true, (Some(q), Some(k), Some(v), Some(o))) => {
5483 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
5484 attention::multi_head_attention(
5485 &self.ws.n1,
5486 q,
5487 k,
5488 v,
5489 o,
5490 &mut self.kv_cache.layers[li],
5491 self.num_heads,
5492 self.num_kv_heads,
5493 self.head_dim,
5494 self.hidden_size,
5495 position,
5496 &active_heads,
5497 &self.inv_freq,
5498 )
5499 }
5500 (masked, _) => {
5501 if masked {
5502 tracing::warn!(
5503 "layer {li}: head mask on quantized weights not \
5504 supported yet — executing dense"
5505 );
5506 }
5507 let inv_freq_l = self.layer_inv_freq(li);
5508 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5509 let cfg = QwenAttnCfg {
5510 num_heads: self.layer_num_heads(li),
5511 num_kv_heads: nkv_l,
5512 head_dim: hd_l,
5513 hidden_size: hs,
5514 position,
5515 inv_freq: &inv_freq_l,
5516 rotary_dim: rd_l,
5517 scale: self.attn_scale,
5518 softcap: self.attn_softcap,
5519 window: self.layer_window(li),
5520 v_norm: self.attn_v_norm,
5521 q_norm: q_norm.as_deref(),
5522 k_norm: k_norm.as_deref(),
5523 output_gate: *output_gate,
5524 softplus_gate: softplus_gate
5525 .as_ref()
5526 .map(|(gate, per_head)| (gate, *per_head)),
5527 rope_scale: self.layer_rope_scale(li),
5528 bias: bias
5529 .as_ref()
5530 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5531 rms_eps: eps,
5532 norm_style: self.norm_style,
5533 pool: pool.as_deref(),
5534 };
5535 attention::qwen_attention(
5536 &self.ws.n1,
5537 wq,
5538 wk,
5539 wv,
5540 wo,
5541 &mut self.kv_cache.layers[li],
5542 &cfg,
5543 )
5544 }
5545 }
5546 }
5547 };
5548 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5551 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
5552 None => attn_out,
5553 };
5554 let lw = &self.weights.layers[self.phys_layer(li)];
5555 inference::add_rmsnorm_fused_into(
5556 &mut h,
5557 &attn_out,
5558 &lw.post_norm,
5559 self.rms_eps,
5560 self.norm_style,
5561 &mut self.ws.p1,
5562 );
5563 let mut attn_out = attn_out;
5564 attention::recycle_buf(&mut attn_out);
5565 let post_normed = &self.ws.p1;
5566
5567 let ffn_masked = task_mask
5568 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
5569 .unwrap_or(false);
5570 let f32_ffn = match &lw.ffn {
5573 FfnKind::Dense(d) => (
5574 d.gate_proj.as_f32(),
5575 d.up_proj.as_f32(),
5576 d.down_proj.as_f32(),
5577 ),
5578 FfnKind::Moe(_) | FfnKind::DenseMoe(_) => (None, None, None),
5579 };
5580 let ffn_out = match (ffn_masked, f32_ffn) {
5581 (true, (Some(g), Some(u), Some(d))) => {
5582 let active = task_mask.unwrap().ffn_active_indices(li);
5583 inference::sparse_ffn_forward(
5584 post_normed,
5585 g,
5586 u,
5587 d,
5588 self.hidden_size,
5589 self.intermediate_size,
5590 &active,
5591 self.pool.as_deref(),
5592 )
5593 }
5594 (true, _) => match &lw.ffn {
5598 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
5599 let active = task_mask.unwrap().ffn_active_indices(li);
5600 sparse_ffn_quant(
5601 d,
5602 post_normed,
5603 &active,
5604 self.hidden_size,
5605 self.pool.as_deref(),
5606 )
5607 }
5608 FfnKind::Dense(d) => {
5613 let active = task_mask.unwrap().ffn_active_indices(li);
5614 let (gf, uf, df) = dequant_dense_f32(d);
5615 inference::sparse_ffn_forward(
5616 post_normed,
5617 &gf,
5618 &uf,
5619 &df,
5620 self.hidden_size,
5621 self.intermediate_size,
5622 &active,
5623 self.pool.as_deref(),
5624 )
5625 }
5626 FfnKind::Moe(m) => {
5627 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
5631 ffn_forward(
5632 &lw.ffn,
5633 post_normed,
5634 self.pool.as_deref(),
5635 allowed.as_deref(),
5636 )
5637 }
5638 FfnKind::DenseMoe(dm) => dense_moe_ffn(
5639 dm,
5640 post_normed,
5641 &h,
5642 self.rms_eps,
5643 self.norm_style,
5644 self.pool.as_deref(),
5645 ),
5646 },
5647 (false, _) => match &lw.ffn {
5648 FfnKind::DenseMoe(dm) => dense_moe_ffn(
5649 dm,
5650 post_normed,
5651 &h,
5652 self.rms_eps,
5653 self.norm_style,
5654 self.pool.as_deref(),
5655 ),
5656 _ => {
5657 let allowed = match (&lw.ffn, task_mask) {
5658 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
5659 _ => None,
5660 };
5661 ffn_forward(
5662 &lw.ffn,
5663 post_normed,
5664 self.pool.as_deref(),
5665 allowed.as_deref(),
5666 )
5667 }
5668 },
5669 };
5670 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5671 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
5672 None => ffn_out,
5673 };
5674 for (i, &f) in ffn_out.iter().enumerate() {
5675 h[i] += f;
5676 }
5677 let mut ffn_out = ffn_out;
5678 attention::recycle_buf(&mut ffn_out);
5679
5680 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5682 for v in h.iter_mut() {
5683 *v *= sc;
5684 }
5685 }
5686
5687 if self.is_loop_end(li) && li + 1 < self.num_layers {
5690 h = inference::rms_norm(
5691 &h,
5692 &self.weights.final_norm,
5693 self.rms_eps,
5694 self.norm_style,
5695 );
5696 }
5697
5698 if self.dyn_phi_layer == Some(li) {
5702 self.update_dyn_phi(&h);
5703 }
5704 }
5705 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
5707 crate::gpu::graph_race_record(false, t.elapsed());
5708 }
5709
5710 h
5711 }
5712
5713 fn update_dyn_phi(&mut self, h: &[f32]) {
5716 const A: f32 = 0.2;
5717 if self.dyn_phi_ema.len() != h.len() {
5718 self.dyn_phi_ema = vec![0.0; h.len()];
5719 self.dyn_phi_seen = 0;
5720 }
5721 if self.dyn_phi_seen == 0 {
5722 self.dyn_phi_ema.copy_from_slice(h);
5723 } else {
5724 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
5725 *e = (1.0 - A) * *e + A * v;
5726 }
5727 }
5728 self.dyn_phi_seen += 1;
5729 }
5730
5731 pub fn dyn_phi(&self) -> &[f32] {
5733 &self.dyn_phi_ema
5734 }
5735
5736 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
5738 self.dyn_phi_layer = layer;
5739 self.dyn_phi_ema.clear();
5740 self.dyn_phi_seen = 0;
5741 }
5742
5743 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
5745 let Some(model) = &self.model else {
5746 return Vec::new();
5747 };
5748 model
5749 .header
5750 .skills
5751 .iter()
5752 .enumerate()
5753 .filter_map(|(i, sk)| {
5754 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
5755 let sel = sk.selection.as_ref()?;
5756 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
5757 })
5758 .collect()
5759 }
5760
5761 pub fn active_skill(&self) -> Option<usize> {
5763 self.dyn_active
5764 }
5765
5766 pub fn enable_dynamic_routing(&mut self) -> usize {
5771 use crate::swarm::{DynRouter, RoutableSkill};
5772 let Some(model) = self.model.clone() else {
5773 return 0;
5774 };
5775 if self.dyn_blend_loaded {
5778 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
5779 return 0;
5780 }
5781 if let Some(a) = self.dyn_active {
5785 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
5786 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
5787 return 0;
5788 }
5789 }
5790 let hidden = self.hidden_size;
5791 let mut skills = Vec::new();
5792 for (idx, id, _phi) in self.dynamic_skills() {
5793 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
5794 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
5795 skills.push(rs);
5796 }
5797 }
5798 }
5799 if skills.is_empty() {
5800 return 0;
5801 }
5802 let phi = skills[0].phi_layer;
5804 if skills.iter().any(|s| s.phi_layer != phi) {
5805 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
5806 }
5807 let n = skills.len();
5808 self.set_dyn_phi_layer(Some(phi));
5809 self.dyn_router = Some(DynRouter::new(skills));
5810 n
5811 }
5812
5813 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
5815 self.dyn_router
5816 .as_ref()
5817 .map(|r| r.switches.clone())
5818 .unwrap_or_default()
5819 }
5820
5821 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
5824 let rows = self.weights.lm_head.rows();
5825 let mut logits = attention::take_buf(rows.min(self.vocab_size));
5826 self.weights
5827 .lm_head
5828 .matvec(hidden, &mut logits, self.pool.as_deref());
5829 logits.resize(self.vocab_size, 0.0);
5830 if let Some(m) = self.logit_multiplier {
5831 for l in logits.iter_mut() {
5832 *l *= m;
5833 }
5834 }
5835 if let Some(c) = self.final_softcap {
5836 for l in logits.iter_mut() {
5837 *l = c * (*l / c).tanh();
5838 }
5839 }
5840 logits
5841 }
5842
5843 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
5848 self.kv_cache.clear();
5849 self.kv_history.clear();
5850 let mut hidden = vec![0.0f32; self.hidden_size];
5851 for (pos, &id) in ids.iter().enumerate() {
5852 let emb = self.embed_single(id);
5853 hidden = self.forward_layers(&emb, pos, task_mask);
5854 }
5855 inference::rms_norm_into(
5856 &hidden,
5857 &self.weights.final_norm,
5858 self.rms_eps,
5859 self.norm_style,
5860 &mut self.ws.n1,
5861 );
5862 self.lm_head_forward(&self.ws.n1)
5863 }
5864}
5865
5866pub fn create_test_pipeline(
5868 hidden_size: usize,
5869 intermediate_size: usize,
5870 num_heads: usize,
5871 num_kv_heads: usize,
5872 head_dim: usize,
5873 num_layers: usize,
5874 vocab_size: usize,
5875) -> Pipeline {
5876 let synth = |n: usize, salt: usize| -> Vec<f32> {
5879 (0..n)
5880 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
5881 .collect()
5882 };
5883 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
5884 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
5885 };
5886 let layer_weights: Vec<LayerWeights> = (0..num_layers)
5887 .map(|li| LayerWeights {
5888 input_norm: vec![1.0; hidden_size],
5889 post_norm: vec![1.0; hidden_size],
5890 attn_out_norm: None,
5891 ffn_out_norm: None,
5892 layer_scale: None,
5893 ffn: FfnKind::Dense(DenseFfn {
5894 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
5895 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
5896 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
5897 act: Act::Silu,
5898 }),
5899 attn: AttnKind::Full {
5900 bias: None,
5901 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
5902 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
5903 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
5904 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
5905 q_norm: None,
5906 k_norm: None,
5907 output_gate: false,
5908 softplus_gate: None,
5909 },
5910 })
5911 .collect();
5912
5913 Pipeline::new(
5914 Tokenizer::byte_level(),
5915 PipelineWeights {
5916 embed_tokens: qt(vocab_size, hidden_size, 100),
5917 layers: layer_weights,
5918 lm_head: qt(vocab_size, hidden_size, 200),
5919 final_norm: vec![1.0; hidden_size],
5920 },
5921 hidden_size,
5922 intermediate_size,
5923 num_heads,
5924 num_kv_heads,
5925 head_dim,
5926 num_layers,
5927 num_layers, false, vocab_size,
5930 1e-6,
5931 10_000.0,
5932 NormStyle::Qwen,
5933 4096,
5934 SamplerConfig {
5935 seed: Some(42),
5936 ..Default::default()
5937 },
5938 )
5939}
5940
5941#[inline]
5946fn mask_bit(row: &[u8], j: usize) -> bool {
5947 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
5948}
5949
5950fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
5956 for r in 0..rows {
5957 let base = r * inter;
5958 for (bi, &byte) in row.iter().enumerate() {
5959 if byte == 0xFF {
5960 continue;
5961 }
5962 let j0 = bi * 8;
5963 for bit in 0..8 {
5964 let j = j0 + bit;
5965 if j < inter && byte & (1 << bit) == 0 {
5966 g[base + j] = 0.0;
5967 }
5968 }
5969 }
5970 }
5971}
5972
5973fn dense_ffn_batch(
5974 d: &DenseFfn,
5975 xs: &[f32],
5976 b: usize,
5977 pool: Option<&Pool>,
5978 mask_row: Option<&[u8]>,
5979) -> Vec<f32> {
5980 let inter = d.gate_proj.rows();
5981 let hidden = d.down_proj.rows();
5982 if mask_row.is_none()
5990 && d.act == Act::Silu
5991 && b >= 32
5992 && crate::gpu::enabled_here()
5993 && !crate::gpu::mm_killed()
5994 {
5995 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
5996 d.gate_proj.mapped_q4t(),
5997 d.up_proj.mapped_q4t(),
5998 d.down_proj.mapped_q4t(),
5999 ) {
6000 let mut out = vec![0.0f32; b * hidden];
6001 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
6002 return out;
6003 }
6004 }
6005 }
6006 let mut g = vec![0.0f32; b * inter];
6007 d.gate_proj.matmat(xs, b, &mut g, pool);
6008 let mut u = vec![0.0f32; b * inter];
6009 d.up_proj.matmat(xs, b, &mut u, pool);
6010 for i in 0..b * inter {
6011 g[i] = d.act.combine(g[i], u[i]);
6012 }
6013 if let Some(row) = mask_row {
6014 zero_masked_cols(&mut g, b, inter, row);
6015 }
6016 let mut out = vec![0.0f32; b * hidden];
6017 d.down_proj.matmat(&g, b, &mut out, pool);
6018 out
6019}
6020
6021fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
6026 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6027 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6028 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
6029 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
6030 if (!on && !dump) || b == 0 {
6031 return;
6032 }
6033 let hidden = xs.len() / b;
6034 if on {
6035 let mut acc = m.act_sq.borrow_mut();
6036 if acc.len() < hidden {
6037 acc.resize(hidden, 0.0);
6038 }
6039 for t in 0..b {
6040 let row = &xs[t * hidden..(t + 1) * hidden];
6041 for (a, &v) in acc.iter_mut().zip(row) {
6042 *a += (v as f64) * (v as f64);
6043 }
6044 }
6045 }
6046 if dump {
6047 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
6050 .ok()
6051 .and_then(|v| v.parse().ok())
6052 .unwrap_or(4096);
6053 let mut rows = m.act_rows.borrow_mut();
6054 if rows.len() < cap * hidden {
6055 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
6056 rows.extend_from_slice(&xs[..take * hidden]);
6057 }
6058 }
6059}
6060
6061fn moe_ffn_batch(
6062 m: &MoeFfn,
6063 xs: &[f32],
6064 b: usize,
6065 hidden: usize,
6066 pool: Option<&Pool>,
6067 allowed: Option<&[bool]>,
6068) -> Vec<f32> {
6069 accumulate_act(m, xs, b);
6070 let ne = m.experts.len();
6071 let mut logits = vec![0.0f32; b * ne];
6072 m.router.matmat(xs, b, &mut logits, pool);
6073
6074 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
6077 {
6078 let mut st = m.stats.borrow_mut();
6079 if st.len() < ne {
6080 st.resize(ne, 0);
6081 }
6082 for bi in 0..b {
6083 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
6084 for &e in &idx {
6085 st[e] += 1;
6086 assign[e].push((bi, p[e] / wsum));
6087 }
6088 }
6089 }
6090
6091 let mut out = vec![0.0f32; b * hidden];
6092 let cols = m.experts[0].gate_proj.cols();
6093 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
6094 let sb = list.len();
6095 let mut sub = vec![0.0f32; sb * cols];
6096 for (k, &(bi, _)) in list.iter().enumerate() {
6097 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
6098 }
6099 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
6100 for (k, &(bi, w)) in list.iter().enumerate() {
6101 for i in 0..hidden {
6102 out[bi * hidden + i] += w * eo[k * hidden + i];
6103 }
6104 }
6105 };
6106 for (e, a) in assign.iter().enumerate().take(ne) {
6107 if !a.is_empty() {
6108 run_expert(&m.experts[e], a);
6109 }
6110 }
6111 if let Some((se, gate)) = &m.shared {
6112 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
6113 let mut gl = vec![0.0f32; b];
6114 gate.matmat(xs, b, &mut gl, pool);
6115 (0..b)
6116 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
6117 .collect()
6118 } else {
6119 (0..b).map(|bi| (bi, 1.0)).collect()
6120 };
6121 run_expert(se, &all);
6122 }
6123 out
6124}
6125
6126thread_local! {
6127 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
6131 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
6132}
6133
6134fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
6136 if crate::gpu::enabled_here()
6147 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
6148 {
6149 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
6150 crate::gpu::ProbeArm::Gpu
6151 } else {
6152 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
6153 };
6154 match arm {
6155 crate::gpu::ProbeArm::Gpu => {
6156 let t0 = std::time::Instant::now();
6157 if let Some(out) = dense_ffn_gpu(d, x, pool) {
6158 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
6159 return out;
6160 }
6161 }
6162 crate::gpu::ProbeArm::CpuTimed => {
6163 let t0 = std::time::Instant::now();
6164 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
6165 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
6166 return out;
6167 }
6168 crate::gpu::ProbeArm::Cpu => {
6169 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
6170 }
6171 }
6172 }
6173 dense_ffn_cpu(d, x, pool)
6174}
6175
6176fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
6178 let inter = d.gate_proj.rows();
6179 FFN_SCRATCH.with(|s| {
6180 let mut s = s.borrow_mut();
6181 let [g, u, ..] = &mut *s;
6182 g.resize(inter, 0.0);
6183 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
6186 } else {
6188 u.resize(inter, 0.0);
6189 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
6191 for i in 0..inter {
6192 g[i] = d.act.combine(g[i], u[i]);
6193 }
6194 }
6195 FFN_PROBE.with(|pr| {
6198 if let Some(acc) = pr.borrow_mut().as_mut() {
6199 let li = crate::gpu::cur_layer();
6200 if li >= 0 {
6201 if let Some(row) = acc.get_mut(li as usize) {
6202 for (a, &v) in row.iter_mut().zip(g.iter()) {
6203 *a += (v as f64).abs();
6204 }
6205 }
6206 }
6207 }
6208 });
6209 let mut out = attention::take_buf(d.down_proj.rows());
6210 d.down_proj.matvec(g, &mut out, pool);
6211 out
6212 })
6213}
6214
6215thread_local! {
6216 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
6219 const { std::cell::RefCell::new(None) };
6220}
6221
6222fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
6228 if d.act != Act::Silu {
6230 return None;
6231 }
6232 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
6235 return None;
6236 }
6237 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
6238 let mut model_ref = None;
6239 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
6240 let model = model_ref?;
6241 let hidden = jobs[0].down.1;
6242 let mut out = attention::take_buf(hidden);
6243 if crate::gpu::moe_block(&model, &jobs, &mut out) {
6244 Some(out)
6245 } else {
6246 let mut out = out;
6247 attention::recycle_buf(&mut out);
6248 None
6249 }
6250}
6251
6252#[allow(clippy::type_complexity)]
6257#[allow(clippy::type_complexity)]
6258pub(crate) fn moe_parts(
6259 t: &QTensor,
6260) -> Option<(
6261 &std::sync::Arc<cortiq_core::CmfModel>,
6262 usize,
6263 usize,
6264 usize,
6265 &[f32],
6266 &[f32],
6267 bool,
6268 bool,
6269)> {
6270 match t {
6271 QTensor::Mapped {
6272 model,
6273 idx,
6274 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
6275 rows,
6276 cols,
6277 row_scale,
6278 col_field,
6279 ..
6280 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
6281 model, *idx, *rows, *cols, row_scale, col_field, false, false,
6282 )),
6283 QTensor::Mapped {
6285 model,
6286 idx,
6287 dtype: cortiq_core::TensorDtype::Q1,
6288 rows,
6289 cols,
6290 ..
6291 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
6292 QTensor::Mapped {
6294 model,
6295 idx,
6296 dtype: cortiq_core::TensorDtype::Q4Tiled,
6297 rows,
6298 cols,
6299 ..
6300 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
6301 QTensor::Mapped {
6303 model,
6304 idx,
6305 dtype: cortiq_core::TensorDtype::Q4TiledP,
6306 rows,
6307 cols,
6308 ..
6309 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
6310 _ => None,
6311 }
6312}
6313
6314pub(crate) fn moe_push_job_parts<'a>(
6318 gate: &'a QTensor,
6319 up: &'a QTensor,
6320 down: &'a QTensor,
6321 x: &[f32],
6322 w: f32,
6323 swiglu_limit: f32,
6324 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
6325 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
6326) -> Option<()> {
6327 use crate::qtensor::prescale;
6328 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(gate)?;
6329 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(up)?;
6330 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(down)?;
6331 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
6332 return None; }
6334 model_ref.get_or_insert_with(|| gm.clone());
6335 let dt = |cf: &[f32]| {
6336 if cf.is_empty() {
6337 cortiq_core::TensorDtype::Q8Row
6338 } else {
6339 cortiq_core::TensorDtype::Q8_2f
6340 }
6341 };
6342 jobs.push(crate::gpu::MoeJob {
6343 gate: (gi, gr, gc, grs),
6344 up: (ui, ur, uc, urs),
6345 down: (di, dr, dc, drs),
6346 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
6347 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
6348 down_col: dcf,
6349 w,
6350 q1: gq1,
6351 q4t: gq4 && gate.mapped_q4tp().is_none(),
6352 q4tp: gq4 && gate.mapped_q4tp().is_some(),
6353 swiglu_limit,
6354 });
6355 Some(())
6356}
6357
6358fn moe_push_job<'a>(
6360 d: &'a DenseFfn,
6361 x: &[f32],
6362 w: f32,
6363 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
6364 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
6365) -> Option<()> {
6366 use crate::qtensor::prescale;
6367 if d.act != Act::Silu {
6368 return None; }
6370 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
6371 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
6372 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
6373 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
6374 return None; }
6376 model_ref.get_or_insert_with(|| gm.clone());
6377 let gdt = if gcf.is_empty() {
6378 cortiq_core::TensorDtype::Q8Row
6379 } else {
6380 cortiq_core::TensorDtype::Q8_2f
6381 };
6382 let udt = if ucf.is_empty() {
6383 cortiq_core::TensorDtype::Q8Row
6384 } else {
6385 cortiq_core::TensorDtype::Q8_2f
6386 };
6387 jobs.push(crate::gpu::MoeJob {
6388 gate: (gi, gr, gc, grs),
6389 up: (ui, ur, uc, urs),
6390 down: (di, dr, dc, drs),
6391 xs_gate: prescale(x, gcf, gdt).into_owned(),
6392 xs_up: prescale(x, ucf, udt).into_owned(),
6393 down_col: dcf,
6394 w,
6395 q1: gq1,
6396 q4t: gq4 && d.gate_proj.mapped_q4tp().is_none(),
6397 q4tp: gq4 && d.gate_proj.mapped_q4tp().is_some(),
6398 swiglu_limit: 0.0,
6399 });
6400 Some(())
6401}
6402
6403fn sparse_ffn_quant(
6410 d: &DenseFfn,
6411 x: &[f32],
6412 active: &[u16],
6413 hidden: usize,
6414 pool: Option<&Pool>,
6415) -> Vec<f32> {
6416 let n = active.len();
6417 let inter = d.gate_proj.rows();
6418 let mut act = vec![0.0f32; n];
6419 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
6422 let compute = |ai: usize| -> f32 {
6423 let idx = active[ai] as usize;
6424 if idx >= inter {
6425 return 0.0; }
6427 let mut s = if need_scratch {
6428 vec![0.0f32; hidden]
6429 } else {
6430 Vec::new()
6431 };
6432 let gate = d.gate_proj.row_dot(idx, x, &mut s);
6433 let up = d.up_proj.row_dot(idx, x, &mut s);
6434 d.act.combine(gate, up)
6435 };
6436 match pool {
6437 Some(p) if n >= 256 => {
6438 let ptr = SendMut(act.as_mut_ptr());
6439 p.run(&|widx, nw| {
6440 let chunk = n.div_ceil(nw);
6441 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
6442 for ai in s..e {
6443 unsafe { *ptr.at(ai) = compute(ai) };
6444 }
6445 });
6446 }
6447 _ => {
6448 for (ai, a) in act.iter_mut().enumerate() {
6449 *a = compute(ai);
6450 }
6451 }
6452 }
6453 let mut out = vec![0.0f32; hidden];
6455 for (ai, &idx) in active.iter().enumerate() {
6456 let w = act[ai];
6457 if w.abs() >= 1e-12 && (idx as usize) < inter {
6458 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
6459 }
6460 }
6461 out
6462}
6463
6464#[doc(hidden)]
6466pub fn sparse_ffn_quant_for_test(
6467 d: &DenseFfn,
6468 x: &[f32],
6469 active: &[u16],
6470 hidden: usize,
6471) -> Vec<f32> {
6472 sparse_ffn_quant(d, x, active, hidden, None)
6473}
6474
6475fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
6479 let deq = |t: &QTensor| -> Vec<f32> {
6480 let (rows, cols) = (t.rows(), t.cols());
6481 let mut out = vec![0.0f32; rows * cols];
6482 for r in 0..rows {
6483 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
6484 }
6485 out
6486 };
6487 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
6488}
6489
6490struct SendMut(*mut f32);
6492unsafe impl Send for SendMut {}
6493unsafe impl Sync for SendMut {}
6494impl SendMut {
6495 #[inline]
6496 #[allow(clippy::mut_from_ref)]
6499 unsafe fn at(&self, i: usize) -> &mut f32 {
6500 unsafe { &mut *self.0.add(i) }
6501 }
6502}
6503
6504fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
6514 let ne = logits.len();
6515 let p: Vec<f32> = if m.router_sigmoid {
6516 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
6517 } else {
6518 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6519 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
6520 let s: f32 = e.iter().sum();
6521 for v in &mut e {
6522 *v /= s;
6523 }
6524 e
6525 };
6526 let admit = |e: usize| {
6532 m.mask.as_ref().is_none_or(|mk| mk[e])
6533 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
6534 };
6535 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
6536 match &m.expert_bias {
6538 Some(b) => idx.sort_unstable_by(|&x, &y| {
6539 (p[y] + b[y])
6540 .partial_cmp(&(p[x] + b[x]))
6541 .unwrap()
6542 .then(x.cmp(&y))
6543 }),
6544 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
6545 }
6546 idx.truncate(m.top_k);
6547 if let Some(tau) = m.route_tau {
6551 let total: f32 = idx.iter().map(|&e| p[e]).sum();
6552 if total > 0.0 {
6553 let mut acc = 0.0f32;
6554 let mut keep = idx.len();
6555 for (i, &e) in idx.iter().enumerate() {
6556 acc += p[e];
6557 if acc >= tau * total {
6558 keep = i + 1;
6559 break;
6560 }
6561 }
6562 idx.truncate(keep);
6563 }
6564 }
6565 let wsum: f32 = if m.norm_topk_prob {
6566 let s: f32 = idx.iter().map(|&e| p[e]).sum();
6567 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
6570 } else {
6571 1.0 / m.routed_scaling
6572 };
6573 (idx, p, wsum)
6574}
6575
6576fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
6579 accumulate_act(m, x, 1);
6580 let ne = m.experts.len();
6581 let mut logits = vec![0.0f32; ne];
6582 m.router.matvec(x, &mut logits, pool);
6583 let (idx, p, wsum) = moe_route(&logits, m, allowed);
6584 {
6585 let mut st = m.stats.borrow_mut();
6586 if st.len() < ne {
6587 st.resize(ne, 0);
6588 }
6589 for &e in &idx {
6590 st[e] += 1;
6591 }
6592 }
6593 if crate::gpu::enabled_here() {
6598 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
6599 crate::gpu::ProbeArm::Gpu => {
6600 let t0 = std::time::Instant::now();
6601 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
6602 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
6603 return out;
6604 }
6605 }
6606 crate::gpu::ProbeArm::CpuTimed => {
6607 let t0 = std::time::Instant::now();
6608 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
6609 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
6610 return out;
6611 }
6612 crate::gpu::ProbeArm::Cpu => {
6613 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
6614 }
6615 }
6616 }
6617 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
6618}
6619
6620fn graph_note(built: bool) {
6624 use std::sync::atomic::{AtomicBool, Ordering};
6625 static SAID: AtomicBool = AtomicBool::new(false);
6626 if !SAID.swap(true, Ordering::Relaxed) {
6627 if built {
6628 tracing::info!("wgpu whole-token graph: ACTIVE");
6629 } else {
6630 tracing::warn!("wgpu whole-token graph refused — per-op path");
6631 }
6632 }
6633}
6634
6635fn moe_batch_enabled() -> bool {
6638 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6639 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
6640}
6641
6642fn moe_ffn_cpu_batched(
6648 m: &MoeFfn,
6649 x: &[f32],
6650 idx: &[usize],
6651 p: &[f32],
6652 wsum: f32,
6653 pool: Option<&Pool>,
6654) -> Option<Vec<f32>> {
6655 if idx.is_empty() || !moe_batch_enabled() {
6656 return None;
6657 }
6658 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
6662 return None;
6663 }
6664 let n = idx.len() + usize::from(m.shared.is_some());
6665 let mut pairs = Vec::with_capacity(n);
6666 let mut downs = Vec::with_capacity(n);
6667 let mut ws = Vec::with_capacity(n);
6668 for &e in idx {
6669 let d = &m.experts[e];
6670 if d.act != Act::Silu {
6671 return None;
6672 }
6673 pairs.push((&d.gate_proj, &d.up_proj));
6674 downs.push(&d.down_proj);
6675 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
6676 }
6677 if let Some((se, gate)) = &m.shared {
6680 if se.act != Act::Silu {
6681 return None;
6682 }
6683 let g = gate.as_ref().map_or(1.0, |gate| {
6684 let mut gl = [0.0f32; 1];
6685 gate.matvec(x, &mut gl, pool);
6686 1.0 / (1.0 + (-gl[0]).exp())
6687 });
6688 pairs.push((&se.gate_proj, &se.up_proj));
6689 downs.push(&se.down_proj);
6690 ws.push(g);
6691 }
6692 let inter = pairs[0].0.rows();
6693 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
6694 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
6695 return None;
6696 }
6697 let mut out = attention::take_buf(x.len());
6698 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
6699 attention::recycle_buf(&mut out);
6700 return None;
6701 }
6702 Some(out)
6703}
6704
6705fn moe_ffn_cpu(
6707 m: &MoeFfn,
6708 x: &[f32],
6709 idx: &[usize],
6710 p: &[f32],
6711 wsum: f32,
6712 pool: Option<&Pool>,
6713) -> Vec<f32> {
6714 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
6715 return out;
6716 }
6717 let mut out = attention::take_buf(x.len());
6718 for &e in idx {
6719 let mut eo = dense_ffn(&m.experts[e], x, pool);
6720 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
6721 for i in 0..out.len() {
6722 out[i] += w * eo[i];
6723 }
6724 attention::recycle_buf(&mut eo);
6725 }
6726 if let Some((se, gate)) = &m.shared {
6727 let mut so = dense_ffn(se, x, pool);
6728 let g = gate.as_ref().map_or(1.0, |gate| {
6729 let mut gl = [0.0f32; 1];
6730 gate.matvec(x, &mut gl, pool);
6731 1.0 / (1.0 + (-gl[0]).exp())
6732 });
6733 for i in 0..out.len() {
6734 out[i] += g * so[i];
6735 }
6736 attention::recycle_buf(&mut so);
6737 }
6738 out
6739}
6740
6741#[allow(clippy::too_many_arguments)]
6749fn mla_attention(
6750 w: &MlaWeights,
6751 normed: &[f32],
6752 cache: &mut crate::kv_cache::LayerKvCache,
6753 position: usize,
6754 inv_freq: &[f32],
6755 rope_scale: f32,
6756 eps: f64,
6757 pool: Option<&Pool>,
6758) -> Vec<f32> {
6759 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
6760 let hd = dr + dn;
6761 let mut q = vec![0.0f32; nh * hd];
6762 match (&w.q_a, &w.q_a_norm) {
6763 (Some(qa), Some(qn)) => {
6764 let mut t = vec![0.0f32; qa.rows()];
6765 qa.matvec(normed, &mut t, pool);
6766 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
6767 w.q_proj.matvec(&tn, &mut q, pool);
6768 }
6769 _ => w.q_proj.matvec(normed, &mut q, pool),
6770 }
6771 let mut ca = vec![0.0f32; lora + dr];
6772 w.kv_a.matvec(normed, &mut ca, pool);
6773 let (c_lat, k_rope) = ca.split_at_mut(lora);
6774 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
6775 let mut kvb = vec![0.0f32; nh * (dn + dv)];
6776 w.kv_b.matvec(&latn, &mut kvb, pool);
6777 if !w.nope {
6778 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
6779 }
6780 for h in 0..nh {
6781 if !w.nope {
6782 attention::rope_rotate_scaled(
6783 &mut q[h * hd..h * hd + dr],
6784 position,
6785 inv_freq,
6786 rope_scale,
6787 );
6788 }
6789 }
6790 let mut k = vec![0.0f32; nh * hd];
6791 let mut v = vec![0.0f32; nh * hd];
6792 for h in 0..nh {
6793 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
6794 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
6795 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
6796 }
6797 cache.append(&k, &v, &vec![true; nh]);
6798 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
6799 attention::recycle_buf(&mut imp);
6800 let mut ov = vec![0.0f32; nh * dv];
6801 for h in 0..nh {
6802 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
6803 }
6804 let mut out = vec![0.0f32; w.o_proj.rows()];
6805 w.o_proj.matvec(&ov, &mut out, pool);
6806 out
6807}
6808
6809fn dense_moe_ffn(
6816 dm: &DenseMoeFfn,
6817 x_normed: &[f32],
6818 h_raw: &[f32],
6819 eps: f64,
6820 norm_style: NormStyle,
6821 pool: Option<&Pool>,
6822) -> Vec<f32> {
6823 let mut d = dense_ffn(&dm.dense, x_normed, pool);
6824 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
6825 let m = &dm.moe;
6826 let ne = m.experts.len();
6827 let mut logits = vec![0.0f32; ne];
6828 if m.router_input_norm {
6829 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
6830 let inv = 1.0 / (ss + eps as f32).sqrt();
6831 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
6832 m.router.matvec(&xr, &mut logits, pool);
6833 } else {
6834 m.router.matvec(h_raw, &mut logits, pool);
6835 }
6836 let (idx, p, wsum) = moe_route(&logits, m, None);
6837 {
6838 let mut st = m.stats.borrow_mut();
6839 if st.len() < ne {
6840 st.resize(ne, 0);
6841 }
6842 for &e in &idx {
6843 st[e] += 1;
6844 }
6845 }
6846 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
6847 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
6848 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
6849 for (di, mi) in d.iter_mut().zip(&mo) {
6850 *di += mi;
6851 }
6852 d
6853}
6854
6855fn moe_gpu_refused(why: &'static str) {
6862 use std::sync::atomic::{AtomicBool, Ordering};
6863 static SAID: AtomicBool = AtomicBool::new(false);
6864 if !SAID.swap(true, Ordering::Relaxed) {
6865 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
6866 }
6867}
6868
6869fn moe_ffn_gpu(
6870 m: &MoeFfn,
6871 x: &[f32],
6872 idx: &[usize],
6873 p: &[f32],
6874 wsum: f32,
6875 pool: Option<&Pool>,
6876) -> Option<Vec<f32>> {
6877 use crate::gpu::MoeJob;
6878
6879 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
6880 let mut model_ref = None;
6881 for &e in idx {
6882 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
6883 moe_gpu_refused("push_job(expert)");
6884 return None;
6885 }
6886 }
6887 if let Some((se, gate)) = &m.shared {
6888 let g = gate.as_ref().map_or(1.0, |gate| {
6889 let mut gl = [0.0f32; 1];
6890 gate.matvec(x, &mut gl, pool);
6891 1.0 / (1.0 + (-gl[0]).exp())
6892 });
6893 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
6894 moe_gpu_refused("push_job(shared)");
6895 return None;
6896 }
6897 }
6898 let Some(model) = model_ref else {
6899 moe_gpu_refused("no model_ref");
6900 return None;
6901 };
6902 let hidden = jobs[0].down.1;
6903 let mut out = vec![0.0f32; hidden];
6904 if crate::gpu::moe_block(&model, &jobs, &mut out) {
6905 Some(out)
6906 } else {
6907 moe_gpu_refused("gpu::moe_block");
6908 None
6909 }
6910}
6911
6912fn ffn_forward(
6914 ffn: &FfnKind,
6915 x: &[f32],
6916 pool: Option<&Pool>,
6917 experts_allowed: Option<&[bool]>,
6918) -> Vec<f32> {
6919 match ffn {
6920 FfnKind::Dense(d) => dense_ffn(d, x, pool),
6921 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
6922 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
6926 }
6927}
6928
6929fn ffn_forward_pair(
6933 ffn: &FfnKind,
6934 x1: &[f32],
6935 x2: &[f32],
6936 pool: Option<&Pool>,
6937 experts_allowed: Option<&[bool]>,
6938) -> (Vec<f32>, Vec<f32>) {
6939 let d = match ffn {
6940 FfnKind::Dense(d) => d,
6941 FfnKind::Moe(m) => {
6942 return (
6943 moe_ffn(m, x1, pool, experts_allowed),
6944 moe_ffn(m, x2, pool, experts_allowed),
6945 );
6946 }
6947 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
6948 };
6949 let inter = d.gate_proj.rows();
6950 FFN_SCRATCH.with(|s| {
6951 let mut s = s.borrow_mut();
6952 let [g1, g2, u1, u2] = &mut *s;
6953 g1.resize(inter, 0.0);
6954 g2.resize(inter, 0.0);
6955 u1.resize(inter, 0.0);
6956 u2.resize(inter, 0.0);
6957 QTensor::matvec2_many(
6960 [&d.gate_proj, &d.up_proj],
6961 x1,
6962 x2,
6963 [g1.as_mut_slice(), u1.as_mut_slice()],
6964 [g2.as_mut_slice(), u2.as_mut_slice()],
6965 pool,
6966 );
6967 for i in 0..inter {
6968 g1[i] = d.act.combine(g1[i], u1[i]);
6969 g2[i] = d.act.combine(g2[i], u2[i]);
6970 }
6971 let mut o1 = attention::take_buf(d.down_proj.rows());
6972 let mut o2 = attention::take_buf(d.down_proj.rows());
6973 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
6974 (o1, o2)
6975 })
6976}
6977
6978#[cfg(test)]
6979mod tests {
6980
6981 #[test]
6982 fn cancel_flag_stops_generation() {
6983 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
6984 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
6987 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
6988 assert_eq!(r.finish_reason, "cancelled");
6989 assert!(
6990 r.token_ids.is_empty(),
6991 "no tokens after cancel: {:?}",
6992 r.token_ids
6993 );
6994 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
6996 assert_ne!(r2.finish_reason, "cancelled");
6997 }
6998 use super::*;
6999
7000 #[test]
7006 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
7007 let (hidden, inter) = (16usize, 40usize);
7008 let synth = |n: usize, salt: usize| -> Vec<f32> {
7009 (0..n)
7010 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
7011 .collect()
7012 };
7013 let d = DenseFfn {
7014 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
7015 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
7016 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
7017 act: Act::Silu,
7018 };
7019 let x = synth(hidden, 9);
7020 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
7022
7023 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
7024
7025 let mut g = vec![0.0f32; inter];
7027 d.gate_proj.matvec(&x, &mut g, None);
7028 let mut u = vec![0.0f32; inter];
7029 d.up_proj.matvec(&x, &mut u, None);
7030 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
7031 for i in 0..inter {
7032 g[i] = if act_set.contains(&(i as u16)) {
7033 inference::silu(g[i]) * u[i]
7034 } else {
7035 0.0
7036 };
7037 }
7038 let mut reference = vec![0.0f32; hidden];
7039 d.down_proj.matvec(&g, &mut reference, None);
7040
7041 let max_d = sparse
7042 .iter()
7043 .zip(&reference)
7044 .map(|(a, b)| (a - b).abs())
7045 .fold(0.0f32, f32::max);
7046 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
7047 }
7048
7049 fn attach_test_mtp(p: &mut Pipeline) {
7051 let (h, inter, heads, kv, hd) = (
7052 p.hidden_size,
7053 p.intermediate_size,
7054 p.num_heads,
7055 p.num_kv_heads,
7056 p.head_dim,
7057 );
7058 let synth = |n: usize, salt: usize| -> Vec<f32> {
7059 (0..n)
7060 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
7061 .collect()
7062 };
7063 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
7064 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
7065 };
7066 p.mtp = Some(MtpModule {
7067 enorm: vec![1.0; h],
7068 hnorm: vec![1.0; h],
7069 eh_proj: qt(h, 2 * h, 301),
7070 layer: LayerWeights {
7071 input_norm: vec![1.0; h],
7072 post_norm: vec![1.0; h],
7073 attn_out_norm: None,
7074 ffn_out_norm: None,
7075 layer_scale: None,
7076 ffn: FfnKind::Dense(DenseFfn {
7077 gate_proj: qt(inter, h, 315),
7078 up_proj: qt(inter, h, 316),
7079 down_proj: qt(h, inter, 317),
7080 act: Act::Silu,
7081 }),
7082 attn: AttnKind::Full {
7083 bias: None,
7084 wq: qt(heads * hd, h, 311),
7085 wk: qt(kv * hd, h, 312),
7086 wv: qt(kv * hd, h, 313),
7087 wo: qt(h, heads * hd, 314),
7088 q_norm: None,
7089 k_norm: None,
7090 output_gate: false,
7091 softplus_gate: None,
7092 },
7093 },
7094 final_norm: vec![1.0; h],
7095 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
7096 });
7097 }
7098
7099 #[test]
7100 fn speculative_equals_vanilla_greedy() {
7101 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7105 let run = |spec: bool| {
7106 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7107 p.sampler_config.temperature = 0.0;
7108 attach_test_mtp(&mut p);
7109 p.speculative = spec;
7110 let r = p.generate("abcdef", 12, None, None).unwrap();
7111 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
7112 };
7113 let (vanilla, d0, _) = run(false);
7114 let (spec, d1, a1) = run(true);
7115 assert_eq!(d0, 0, "vanilla path must not draft");
7116 assert!(d1 > 0, "speculative path must draft");
7117 assert_eq!(
7118 vanilla, spec,
7119 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
7120 );
7121 }
7122
7123 #[test]
7124 fn speculative_accepts_constant_oracle() {
7125 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7127 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7128 p.sampler_config.temperature = 0.0;
7129 p.sampler_config.repetition_penalty = 1.0;
7130 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
7133 attach_test_mtp(&mut p);
7134 p.speculative = true;
7135 let r = p.generate("abcd", 10, None, None).unwrap();
7136 assert!(r.mtp_drafted > 0);
7137 assert_eq!(
7138 r.mtp_accepted, r.mtp_drafted,
7139 "constant logits → every draft accepted"
7140 );
7141 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
7144 }
7145
7146 #[test]
7147 fn empty_prompt_is_an_error_not_a_panic() {
7148 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
7149 let r = p.generate("", 4, None, None);
7150 assert!(r.is_err(), "empty prompt must be a clean error");
7151 }
7152
7153 #[test]
7154 fn every_token_enters_kv_exactly_once() {
7155 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7156 p.sampler_config.temperature = 0.0;
7158 let r = p.generate("abc", 2, None, None).unwrap();
7159 assert_eq!(r.prompt_tokens, 3);
7160 assert_eq!(
7164 p.kv_cache.seq_len(),
7165 3 + r.tokens_generated - 1,
7166 "each token must be cached exactly once (v1 cached the last prompt token twice)"
7167 );
7168 }
7169
7170 #[test]
7171 fn generation_is_reproducible_with_seed() {
7172 let run = || {
7173 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7174 p.generate("hello", 8, None, None).unwrap().token_ids
7175 };
7176 assert_eq!(run(), run());
7177 }
7178
7179 #[test]
7180 fn resetting_sampler_restarts_the_seeded_stream() {
7181 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7182 let config = SamplerConfig {
7183 seed: Some(1234),
7184 ..SamplerConfig::default()
7185 };
7186 p.set_sampler_config(config.clone());
7187 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
7188 p.set_sampler_config(config);
7189 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
7190 assert_eq!(first, second);
7191 }
7192
7193 #[test]
7194 fn eviction_bounds_the_cache() {
7195 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
7196 p.kv_cache.max_seq_len = 6;
7197 p.sampler_config.temperature = 0.0;
7198 let _ = p.generate("abcd", 12, None, None).unwrap();
7199 assert!(
7200 p.kv_cache.seq_len() <= 6 + 1,
7201 "cache must stay bounded by max_seq_len (got {})",
7202 p.kv_cache.seq_len()
7203 );
7204 }
7205
7206 #[test]
7207 fn confidence_matches_tokens_and_is_a_probability() {
7208 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7209 p.sampler_config.temperature = 0.0;
7210 p.sampler_config.repetition_penalty = 1.0;
7211 let r = p.generate("abcd", 10, None, None).unwrap();
7212 assert_eq!(
7213 r.token_confidence.len(),
7214 r.token_ids.len(),
7215 "one confidence per emitted token"
7216 );
7217 for &c in &r.token_confidence {
7218 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
7219 }
7220 let logits = [1.0f32, 3.0, 0.5, 3.0];
7222 let p0 = top1_prob_t(&logits, 1, 1.0);
7223 let p1 = top1_prob_t(&logits, 3, 1.0);
7224 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
7225 assert!(p0 > 0.0 && p0 < 1.0);
7226 let sharp = top1_prob_t(&logits, 1, 1.0);
7228 let soft = top1_prob_t(&logits, 1, 2.0);
7229 assert!(soft < sharp, "higher temperature lowers peak confidence");
7230 }
7231
7232 #[test]
7233 fn trace_is_opt_in_and_parallels_the_output() {
7234 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7236 p.sampler_config.temperature = 0.0;
7237 p.sampler_config.repetition_penalty = 1.0;
7238 let r = p.generate("abcd", 10, None, None).unwrap();
7239 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
7240
7241 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7243 p.sampler_config.temperature = 0.0;
7244 p.sampler_config.repetition_penalty = 1.0;
7245 p.set_trace(true);
7246 let r = p.generate("abcd", 10, None, None).unwrap();
7247 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
7248 for (i, tr) in r.traces.iter().enumerate() {
7249 assert_eq!(tr.t, i, "trace index is sequential");
7250 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
7251 assert_eq!(
7252 tr.confidence, r.token_confidence[i],
7253 "trace confidence matches the confidence channel"
7254 );
7255 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
7257 }
7258 }
7259
7260 #[test]
7261 fn explain_prefill_logits_match_greedy_first_token() {
7262 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7266 p.sampler_config.temperature = 0.0;
7267 p.sampler_config.repetition_penalty = 1.0;
7268 let ids = p.tokenizer.encode("abcd");
7269 let logits = p.prefill_next_logits(&ids, None);
7270 let argmax = logits
7271 .iter()
7272 .enumerate()
7273 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7274 .unwrap()
7275 .0 as u32;
7276 let r = p.generate("abcd", 1, None, None).unwrap();
7277 assert_eq!(
7278 argmax, r.token_ids[0],
7279 "explain preview must match greedy emit"
7280 );
7281 }
7282
7283 #[test]
7284 fn laguna_shared_expert_is_unconditionally_added() {
7285 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
7286 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
7287 let zero_dense = || DenseFfn {
7288 gate_proj: matrix(vec![0.0; 4]),
7289 up_proj: matrix(vec![0.0; 4]),
7290 down_proj: matrix(vec![0.0; 4]),
7291 act: Act::Silu,
7292 };
7293 let shared = DenseFfn {
7294 gate_proj: identity(),
7295 up_proj: identity(),
7296 down_proj: identity(),
7297 act: Act::Silu,
7298 };
7299 let x = [1.0, 2.0];
7300 let expected = dense_ffn(&shared, &x, None);
7301 let moe = MoeFfn {
7302 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
7303 experts: vec![zero_dense()],
7304 top_k: 1,
7305 norm_topk_prob: true,
7306 router_sigmoid: true,
7307 expert_bias: None,
7308 routed_scaling: 1.0,
7309 route_tau: None,
7310 shared: Some((shared, None)),
7311 stats: std::cell::RefCell::new(Vec::new()),
7312 act_sq: std::cell::RefCell::new(Vec::new()),
7313 act_rows: std::cell::RefCell::new(Vec::new()),
7314 mask: None,
7315 per_expert_scale: None,
7316 router_input_norm: false,
7317 };
7318 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
7319 for (actual, expected) in actual.iter().zip(expected) {
7320 assert!((actual - expected).abs() < 1e-6);
7321 }
7322 }
7323}