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 { return start };
880 if plan.is_empty() {
881 return start;
882 }
883 let dims = GraphDims {
884 hidden: self.hidden_size,
885 eps: self.rms_eps as f32,
886 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
887 };
888 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
889 return start;
890 };
891 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
892 nv: cfg.num_v_heads,
893 nk: cfg.num_k_heads,
894 dk: cfg.key_head_dim,
895 dv: cfg.value_head_dim,
896 kk: cfg.conv_kernel,
897 hidden: self.hidden_size,
898 inter: self.intermediate_size,
899 c_dim: cfg.conv_dim(),
900 eps: cfg.rms_eps as f32,
901 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
902 });
903 let mut valid = 0usize;
907 let mut end = start;
908 for item in &plan {
909 let ok = match item {
910 Item::Gdn { run, .. } => gcfg
911 .as_ref()
912 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
913 .unwrap_or(false),
914 Item::Attn { l, .. } => graph.attn_ok(l),
915 };
916 if !ok {
917 break;
918 }
919 valid += 1;
920 end += match item {
921 Item::Gdn { run, .. } => run.len(),
922 Item::Attn { .. } => 1,
923 };
924 }
925 plan.truncate(valid);
926 if plan.is_empty() {
927 return start;
928 }
929
930 let inv_freq = self.inv_freq.clone();
931 let pool = self.pool.clone();
932 let (nh, nkv, hd, hs, rd, eps) = (
933 self.num_heads,
934 self.num_kv_heads,
935 self.head_dim,
936 self.hidden_size,
937 self.rotary_dim,
938 self.rms_eps,
939 );
940 let norm_style = self.norm_style;
941 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
942 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
943 let kv_id = self.graph_kv_id;
944 let mut pending: Vec<(usize, usize)> = Vec::new();
947 let mut dev_attn: Vec<usize> = Vec::new();
950 for item in &plan {
951 if self.loop_final_norm {
953 let item_start = match item {
954 Item::Gdn { first, .. } => *first,
955 Item::Attn { li, .. } => *li,
956 };
957 if item_start > start && self.is_loop_end(item_start - 1) {
958 graph.encode_loop_norm(&self.weights.final_norm);
959 }
960 }
961 match item {
962 Item::Gdn { run, first } => {
963 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
964 if l.linear_state.len() != want {
965 l.linear_state = vec![0f32; want];
966 }
967 }
968 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
969 .iter()
970 .map(|l| l.linear_state.as_slice())
971 .collect();
972 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
973 tracing::error!("q1 graph: GDN run refused after validation");
975 return start;
976 }
977 graph.commit();
980 pending.push((*first, run.len()));
981 }
982 Item::Attn {
983 l,
984 li,
985 q_norm,
986 k_norm,
987 output_gate,
988 bias,
989 full_gpu,
990 } => {
991 if *full_gpu {
993 let cache = &self.kv_cache.layers[*li];
994 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
995 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
996 let cpu_stored = cpu_k[0].len() / hd;
997 let p = crate::gpu::AttnDeviceParams {
998 kv_id,
999 layer: *li,
1000 nh,
1001 nkv,
1002 hd,
1003 rd,
1004 position,
1005 eps: eps as f32,
1006 gemma,
1007 output_gate: *output_gate,
1008 q_norm: *q_norm,
1009 k_norm: *k_norm,
1010 inv_freq: &inv_freq,
1011 cpu_k,
1012 cpu_v,
1013 cpu_stored,
1014 };
1015 if graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p) {
1016 graph.commit();
1017 dev_attn.push(*li);
1018 continue;
1019 }
1020 }
1022 graph.encode_attn_prefix(l);
1023 graph.sync();
1024 if !pending.is_empty() {
1025 let idxs: Vec<usize> =
1026 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1027 let mut outs: Vec<&mut [f32]> = self
1028 .kv_cache
1029 .layers
1030 .iter_mut()
1031 .enumerate()
1032 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1033 .map(|(_, s)| s.linear_state.as_mut_slice())
1034 .collect();
1035 graph.read_states(&mut outs);
1036 }
1037 let mut q_raw = attention::take_buf(l.wq.1);
1038 let mut k = attention::take_buf(l.wk.1);
1039 let mut v = attention::take_buf(l.wv.1);
1040 graph.read_qkv(&mut q_raw, &mut k, &mut v);
1041 let cfg = QwenAttnCfg {
1042 num_heads: nh,
1043 num_kv_heads: nkv,
1044 head_dim: hd,
1045 hidden_size: hs,
1046 position,
1047 inv_freq: &inv_freq,
1048 rotary_dim: rd,
1049 scale: self.attn_scale,
1050 softcap: self.attn_softcap,
1051 window: None,
1052 v_norm: false,
1053 q_norm: *q_norm,
1054 k_norm: *k_norm,
1055 output_gate: *output_gate,
1056 softplus_gate: None,
1057 rope_scale: 1.0,
1058 bias: *bias,
1059 rms_eps: eps,
1060 norm_style,
1061 pool: pool.as_deref(),
1062 };
1063 let mut ao = attention::qwen_attention_core(
1064 q_raw,
1065 k,
1066 v,
1067 &mut self.kv_cache.layers[*li],
1068 &cfg,
1069 );
1070 graph.encode_attn_suffix(l, &ao);
1071 graph.commit();
1074 attention::recycle_buf(&mut ao);
1075 }
1076 }
1077 }
1078 let mut lm_rows = None;
1083 if self.graph_want_logits
1084 && upto.is_none()
1085 && end == self.num_layers
1086 && std::env::var("CMF_GPU_LMHEAD")
1087 .map(|v| v != "0")
1088 .unwrap_or(true)
1089 {
1090 if let Some(lm) = self.weights.lm_head.q1_parts() {
1091 if graph.lm_head_ok(lm) {
1092 graph.encode_lm_head(&self.weights.final_norm, lm);
1093 lm_rows = Some(lm.1);
1094 }
1095 }
1096 }
1097 graph.sync();
1098 if !pending.is_empty() {
1099 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
1100 let mut outs: Vec<&mut [f32]> = self
1101 .kv_cache
1102 .layers
1103 .iter_mut()
1104 .enumerate()
1105 .filter(|(i, _)| idxs.binary_search(i).is_ok())
1106 .map(|(_, s)| s.linear_state.as_mut_slice())
1107 .collect();
1108 graph.read_states(&mut outs);
1109 }
1110 if let Some(rows) = lm_rows {
1111 let mut lg = attention::take_buf(rows.min(self.vocab_size));
1112 graph.read_logits(&mut lg);
1113 lg.resize(self.vocab_size, 0.0);
1114 if let Some(c) = self.final_softcap {
1115 for l in lg.iter_mut() {
1116 *l = c * (*l / c).tanh();
1117 }
1118 }
1119 self.graph_logits = Some(lg);
1120 }
1121 graph.finish(h);
1122 for li in dev_attn {
1126 let mut krow = attention::take_buf(nkv * hd);
1127 let mut vrow = attention::take_buf(nkv * hd);
1128 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
1129 let cache = &mut self.kv_cache.layers[li];
1130 cache.append(&krow, &vrow, &[]);
1131 let n = cache.seq_len;
1132 let mut imp = attention::take_buf(n);
1133 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
1134 cache.accumulate_imp(&imp);
1135 attention::recycle_buf(&mut imp);
1136 }
1137 attention::recycle_buf(&mut krow);
1138 attention::recycle_buf(&mut vrow);
1139 }
1140 end
1141 }
1142
1143 pub fn new(
1144 tokenizer: Tokenizer,
1145 weights: PipelineWeights,
1146 hidden_size: usize,
1147 intermediate_size: usize,
1148 num_heads: usize,
1149 num_kv_heads: usize,
1150 head_dim: usize,
1151 num_layers: usize,
1152 physical_layers: usize,
1153 loop_final_norm: bool,
1154 vocab_size: usize,
1155 rms_eps: f64,
1156 rope_base: f32,
1157 norm_style: NormStyle,
1158 max_seq_len: usize,
1159 sampler_config: SamplerConfig,
1160 ) -> Self {
1161 let rng = match sampler_config.seed {
1162 Some(s) => SplitMix64::new(s),
1163 None => SplitMix64::from_entropy(),
1164 };
1165 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
1166 let pool = Pool::from_env();
1167 if let Some(p) = &pool {
1168 tracing::info!("worker pool: {} threads", p.n_workers());
1169 }
1170 Self {
1171 tokenizer: std::sync::Arc::new(tokenizer),
1172 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
1173 sampler_config,
1174 weights,
1175 hidden_size,
1176 intermediate_size,
1177 num_heads,
1178 num_kv_heads,
1179 head_dim,
1180 num_layers,
1181 physical_layers,
1182 loop_final_norm,
1183 vocab_size,
1184 rms_eps,
1185 rope_base,
1186 norm_style,
1187 rotary_dim: head_dim,
1188 attention_heads_per_layer: None,
1189 vmf_cfg: None,
1190 gdn_cfg: None,
1191 kda_cfg: None,
1192 g3n: None,
1193 dsv4: None,
1194 dsv4_mtp: Vec::new(),
1195 dspark: None,
1196 dspark_pending: Vec::new(),
1197 dspark_hist: Vec::new(),
1198 dspark_real: Vec::new(),
1199 dspark_trunk_picks: Vec::new(),
1200 dspark_exp: Vec::new(),
1201 dspark_draft_ns: 0,
1202 logit_multiplier: None,
1203 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1204 kv_history: Vec::new(),
1205 short_conv_cfg: None,
1206 mtp: None,
1207 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
1208 rng,
1209 sampler_scratch: SamplerScratch::default(),
1210 inv_freq,
1211 ws: ForwardScratch::new(hidden_size),
1212 pool,
1213 model: None,
1214 dyn_force_f32: false,
1215 dyn_skill_layers: Vec::new(),
1216 dyn_active: None,
1217 dyn_blend_loaded: false,
1218 dyn_phi_layer: None,
1219 dyn_phi_ema: Vec::new(),
1220 dyn_phi_seen: 0,
1221 dyn_router: None,
1222 o1_cfg: None,
1223 o1_epoch: 0,
1224 o1_flags: Vec::new(),
1225 trace: false,
1226 calib_temp: 1.0,
1227 confidence_on: true,
1228 embed_multiplier: 1.0,
1229 attn_scale: 1.0 / (head_dim as f32).sqrt(),
1230 swa: None,
1231 sliding_layers: None,
1232 inv_freq_local: None,
1233 rotary_dim_local: None,
1234 rope_scale: 1.0,
1235 rope_scale_local: 1.0,
1236 global_attn: None,
1237 inv_freq_global: None,
1238 attn_v_norm: false,
1239 final_softcap: None,
1240 attn_softcap: 0.0,
1241 graph_want_logits: false,
1242 graph_logits: None,
1243 graph_kv_id: {
1244 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1245 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1246 },
1247 }
1248 }
1249
1250 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
1257 self.o1_flags = match &cfg {
1258 Some(c) => {
1259 let mut flags = c.layer_flags(self.num_layers);
1260 for (li, f) in flags.iter_mut().enumerate() {
1261 if *f
1262 && !matches!(
1263 self.weights.layers[self.phys_layer(li)].attn,
1264 AttnKind::Full { .. }
1265 )
1266 {
1267 *f = false;
1268 }
1269 }
1270 flags
1271 }
1272 None => Vec::new(),
1273 };
1274 if let Some(c) = &cfg {
1275 let n = self.o1_flags.iter().filter(|&&f| f).count();
1276 tracing::info!(
1277 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
1278 self.num_layers,
1279 c.m,
1280 c.w,
1281 c.sink,
1282 c.rect
1283 );
1284 }
1285 self.o1_cfg = cfg;
1286 }
1287
1288 pub fn o1_active(&self) -> bool {
1290 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
1291 }
1292
1293 fn o1_begin(&mut self) {
1295 if let Some(c) = &self.o1_cfg {
1296 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
1297 for (li, &f) in self.o1_flags.iter().enumerate() {
1298 if f {
1299 self.kv_cache.layers[li].o1_begin(m, w, sink, rect);
1300 }
1301 }
1302 }
1303 }
1304
1305 fn o1_seal(&mut self) {
1308 self.o1_epoch = self.o1_epoch.wrapping_add(1);
1309 if self.o1_cfg.is_none() {
1310 return;
1311 }
1312 for li in 0..self.num_layers {
1313 if self.o1_flags.get(li).copied().unwrap_or(false) {
1314 self.kv_cache.layers[li].o1_seal(self.num_heads);
1315 }
1316 }
1317 }
1318
1319 pub fn set_trace(&mut self, on: bool) {
1321 self.trace = on;
1322 }
1323
1324 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
1327 self.rng = match config.seed {
1328 Some(seed) => SplitMix64::new(seed),
1329 None => SplitMix64::from_entropy(),
1330 };
1331 self.sampler_config = config;
1332 }
1333
1334 pub fn set_confidence(&mut self, on: bool) {
1339 self.confidence_on = on;
1340 }
1341
1342 pub fn set_calib_temp(&mut self, t: f32) {
1345 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
1346 }
1347
1348 pub fn calib_temp(&self) -> f32 {
1350 self.calib_temp
1351 }
1352
1353 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
1356 self.rotary_dim = rotary_dim.min(self.head_dim);
1357 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
1358 }
1359
1360 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
1361 QwenAttnCfg {
1362 num_heads: self.num_heads,
1363 num_kv_heads: self.num_kv_heads,
1364 head_dim: self.head_dim,
1365 hidden_size: self.hidden_size,
1366 position,
1367 inv_freq: &self.inv_freq,
1368 rotary_dim: self.rotary_dim,
1369 scale: self.attn_scale,
1370 softcap: self.attn_softcap,
1371 window: None,
1372 v_norm: false,
1373 q_norm: None,
1374 k_norm: None,
1375 output_gate: false,
1376 softplus_gate: None,
1377 rope_scale: self.rope_scale,
1378 bias: None,
1379 rms_eps: self.rms_eps,
1380 norm_style: self.norm_style,
1381 pool: self.pool.as_deref(),
1382 }
1383 }
1384
1385 pub fn generate(
1387 &mut self,
1388 prompt: &str,
1389 max_tokens: usize,
1390 task_mask: Option<&TaskMask>,
1391 on_token: Option<TokenCallback>,
1392 ) -> Result<GenerateResult, String> {
1393 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
1394 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
1395 }
1396
1397 pub fn generate_from_ids(
1405 &mut self,
1406 input_ids: &[u32],
1407 max_tokens: usize,
1408 task_mask: Option<&TaskMask>,
1409 mut on_token: Option<TokenCallback>,
1410 ) -> Result<GenerateResult, String> {
1411 if std::env::var("CMF_TRACE_H").is_ok() {
1412 eprintln!("input_ids: {input_ids:?}");
1413 }
1414 if input_ids.is_empty() {
1415 return Err("empty prompt: nothing to generate from".to_string());
1416 }
1417
1418 let reuse_from = {
1426 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
1427 let h = &self.kv_history;
1428 if on
1429 && task_mask.is_none()
1430 && self.mtp.is_none()
1431 && self.o1_cfg.is_none()
1432 && !h.is_empty()
1433 && h.len() < input_ids.len()
1434 && input_ids[..h.len()] == h[..]
1435 {
1436 h.len()
1437 } else {
1438 0
1439 }
1440 };
1441 if reuse_from == 0 {
1442 self.kv_cache.clear();
1444 self.kv_history.clear();
1445 crate::gpu::graph_kv_reset(self.graph_kv_id);
1446 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
1447 eprintln!(
1448 "kv-reuse: {} of {} prompt positions already cached",
1449 reuse_from,
1450 input_ids.len()
1451 );
1452 }
1453 crate::gpu::graph_race_begin_generation();
1454 self.o1_begin();
1455
1456 let graph_on = std::env::var("CMF_GPU_WGPU_GRAPH")
1462 .map(|v| v != "0")
1463 .unwrap_or_else(|_| {
1464 crate::gpu::wgpu_graph_default()
1468 });
1469 let graph_spec = self.speculative
1476 && graph_on
1477 && self.mtp.is_some()
1478 && task_mask.is_none()
1479 && !self.o1_active()
1480 && self.sampler_config.temperature < 1e-6
1481 && self.sampler_config.repetition_penalty == 1.0
1482 && std::env::var("CMF_GRAPH_SPEC").is_ok_and(|v| v != "0");
1483 let pair_pays = self.gdn_cfg.is_none()
1490 || std::env::var("CMF_MTP").as_deref() == Ok("1");
1491 let spec_active = self.speculative
1492 && self.mtp.is_some()
1493 && task_mask.is_none()
1494 && !self.o1_active()
1495 && ((!graph_on && pair_pays) || graph_spec)
1496 && self.sampler_config.temperature < 1e-6;
1497 let mut mtp = if spec_active { self.mtp.take() } else { None };
1500 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
1501 eprintln!(
1502 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
1503 mtp.is_some(),
1504 self.speculative,
1505 self.sampler_config.temperature < 1e-6,
1506 );
1507 }
1508 if let Some(m) = &mut mtp {
1509 m.kv.clear();
1510 }
1511 let mut router = if mtp.is_none() {
1515 self.dyn_router.take()
1516 } else {
1517 None
1518 };
1519 if let Some(r) = &mut router {
1520 r.reset(); self.dyn_phi_seen = 0; let _ = self.set_active_skill(None);
1523 }
1524
1525 let mut all_ids = input_ids.to_vec();
1526 let mut generated = 0usize;
1527 let mut finish_reason = "max_tokens".to_string();
1528 let mut drafted = 0usize;
1529 let mut accepted = 0usize;
1530 let mut confidence: Vec<f32> = Vec::new();
1531 let trace_on = self.trace;
1532 let calib_temp = self.calib_temp;
1533 let mut traces: Vec<TokenTrace> = Vec::new();
1534
1535 let mut hidden = vec![0.0f32; self.hidden_size];
1541 let mut pos = reuse_from;
1542 let fuse_lm = mtp.is_none()
1551 && router.is_none()
1552 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
1553 self.graph_logits = None;
1554 self.graph_want_logits = false;
1555 let _tpf = std::time::Instant::now();
1556 let batch_k = std::env::var("CMF_BATCH_K")
1557 .ok()
1558 .and_then(|v| v.parse::<usize>().ok())
1559 .unwrap_or(0);
1560 while self.dsv4.is_some()
1571 && mtp.is_none()
1572 && pos < input_ids.len()
1573 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1574 {
1575 let end = (pos + prefill_chunk()).min(input_ids.len());
1576 let ids: Vec<u32> = input_ids[pos..end].to_vec();
1577 let mut lg = Vec::new();
1578 if let Some(b) = &mut self.dsv4 {
1579 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
1580 crate::dsv4::forward_chunk(
1581 g,
1582 layers,
1583 &cfg,
1584 st,
1585 &ids,
1586 pos,
1587 &self.inv_freq,
1588 self.pool.as_deref(),
1589 &mut lg,
1590 end == input_ids.len(),
1591 );
1592 }
1593 if end == input_ids.len() {
1594 self.graph_logits = Some(lg);
1595 }
1596 pos = end;
1597 hidden = vec![0.0; self.hidden_size];
1598 }
1599 let dyn_prefill = router.is_some();
1604 let graph_prefill = self.graph_prefill_preferred();
1610 if task_mask.is_none()
1611 && !dyn_prefill
1612 && !graph_prefill
1613 && self.can_prefill_batched()
1614 && self.g3n.is_none()
1615 && input_ids.len() > 2
1616 {
1617 let chunk = prefill_chunk();
1623 let hs = self.hidden_size;
1624 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1625 let end = (pos + chunk).min(input_ids.len());
1626 let hb = self.prefill_batch(&input_ids[pos..end], pos);
1627 if let Some(m) = &mut mtp {
1628 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1629 .ok()
1630 .and_then(|v| v.parse().ok())
1631 .unwrap_or(0);
1632 for p in pos..end {
1633 if p + 1 < input_ids.len() {
1634 if probe >= 1 && p + 2 < input_ids.len() {
1635 let (d1, mut hx) = self.mtp_step_h(
1639 m,
1640 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1641 input_ids[p + 1],
1642 p,
1643 );
1644 let mut ok = d1 == input_ids[p + 2];
1645 Self::chain_probe_note(0, ok);
1646 let mut d_prev = d1;
1647 let mut extra = 0usize;
1648 for j in 1..probe {
1649 if p + 2 + j >= input_ids.len() {
1650 break;
1651 }
1652 let (dj, hj) =
1653 self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
1654 extra += 1;
1655 ok = ok && dj == input_ids[p + 2 + j];
1656 Self::chain_probe_note(j, ok);
1657 d_prev = dj;
1658 hx = hj;
1659 }
1660 m.kv.truncate_last(extra);
1661 } else {
1662 let _ = self.mtp_step(
1663 m,
1664 &hb[(p - pos) * hs..(p - pos + 1) * hs],
1665 input_ids[p + 1],
1666 p,
1667 );
1668 }
1669 }
1670 }
1671 }
1672 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
1673 pos = end;
1674 }
1675 }
1676 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
1677 if task_mask.is_none()
1678 && !dyn_prefill
1679 && !graph_prefill
1680 && !pair_off
1681 && self.pair_supported()
1682 {
1683 while pos + 1 < input_ids.len()
1684 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
1685 {
1686 let e1 = self.embed_single(input_ids[pos]);
1687 let e2 = self.embed_single(input_ids[pos + 1]);
1688 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
1689 self.commit_linear_scratch();
1691 if let Some(m) = &mut mtp {
1692 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
1693 if pos + 2 < input_ids.len() {
1694 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1695 .ok()
1696 .and_then(|v| v.parse().ok())
1697 .unwrap_or(0);
1698 if probe >= 1 && pos + 3 < input_ids.len() {
1699 let (d1, mut hx) =
1703 self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
1704 let mut ok = d1 == input_ids[pos + 3];
1705 Self::chain_probe_note(0, ok);
1706 let mut d_prev = d1;
1707 let mut extra = 0usize;
1708 for j in 1..probe {
1709 if pos + 3 + j >= input_ids.len() {
1710 break;
1711 }
1712 let (dj, hj) =
1713 self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
1714 extra += 1;
1715 ok = ok && dj == input_ids[pos + 3 + j];
1716 Self::chain_probe_note(j, ok);
1717 d_prev = dj;
1718 hx = hj;
1719 }
1720 m.kv.truncate_last(extra);
1721 } else {
1722 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
1723 }
1724 }
1725 }
1726 hidden = h2;
1727 pos += 2;
1728 }
1729 }
1730 if batch_k > 0
1739 && graph_prefill
1740 && task_mask.is_none()
1741 && !self.o1_active()
1742 && mtp.is_none()
1743 && !dyn_prefill
1744 && pos + 1 < input_ids.len()
1745 {
1746 let hs = self.hidden_size;
1747 let chunk = batch_k;
1748 while pos < input_ids.len() {
1749 let end = (pos + chunk).min(input_ids.len());
1750 let bk = end - pos;
1751 let mut hiddens = vec![0f32; bk * hs];
1752 for (j, &id) in input_ids[pos..end].iter().enumerate() {
1753 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1754 }
1755 let positions: Vec<usize> = (pos..end).collect();
1756 let t_chunk = std::time::Instant::now();
1757 let ok_b = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
1758 if std::env::var("CMF_GRAPH_PROF").is_ok() {
1759 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
1760 eprintln!(
1761 "batch-chunk: k={bk} ok={ok_b} {ms:.1} ms ({:.1} tok/s)",
1762 bk as f64 / (ms / 1000.0)
1763 );
1764 }
1765 {
1766 use std::sync::atomic::{AtomicBool, Ordering};
1767 static SAID: AtomicBool = AtomicBool::new(false);
1768 if !SAID.swap(true, Ordering::Relaxed) {
1769 if ok_b {
1770 tracing::info!("batched prefill: ACTIVE (k={bk})");
1771 } else {
1772 tracing::warn!("batched prefill declined — per-position graph");
1773 }
1774 }
1775 }
1776 if ok_b {
1777 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
1778 pos = end;
1779 } else {
1780 break; }
1782 }
1783 }
1784 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1785 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
1786 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
1787 if let Some(m) = &mut mtp {
1788 if pos + 1 < input_ids.len() {
1789 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
1795 .ok()
1796 .and_then(|v| v.parse().ok())
1797 .unwrap_or(0);
1798 if probe >= 1 && pos + 2 < input_ids.len() {
1799 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
1800 let mut ok = d1 == input_ids[pos + 2];
1801 Self::chain_probe_note(0, ok);
1802 let mut d_prev = d1;
1803 let mut extra = 0usize;
1804 for j in 1..probe {
1805 if pos + 2 + j >= input_ids.len() {
1806 break;
1807 }
1808 let (dj, hj) =
1809 self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
1810 extra += 1;
1811 ok = ok && dj == input_ids[pos + 2 + j];
1812 Self::chain_probe_note(j, ok);
1813 d_prev = dj;
1814 hx = hj;
1815 }
1816 m.kv.truncate_last(extra);
1819 } else {
1820 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
1821 }
1822 }
1823 }
1824 pos += 1;
1825 }
1826 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1827 eprintln!(
1828 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
1829 input_ids.len(),
1830 _tpf.elapsed().as_secs_f64() * 1000.0
1831 );
1832 }
1833 if self
1836 .cancel
1837 .swap(false, std::sync::atomic::Ordering::Relaxed)
1838 {
1839 self.kv_history.clear();
1840 if let Some(m) = mtp {
1841 self.mtp = Some(m);
1842 }
1843 return Ok(GenerateResult {
1844 text: String::new(),
1845 token_ids: Vec::new(),
1846 prompt_tokens: input_ids.len(),
1847 tokens_generated: 0,
1848 finish_reason: "cancelled".to_string(),
1849 mtp_drafted: 0,
1850 mtp_accepted: 0,
1851 token_confidence: Vec::new(),
1852 traces: Vec::new(),
1853 });
1854 }
1855
1856 self.o1_seal();
1859
1860 macro_rules! commit {
1862 ($id:expr) => {{
1863 all_ids.push($id);
1864 generated += 1;
1865 if self.tokenizer.is_eos($id) {
1866 finish_reason = "stop".to_string();
1867 false
1868 } else {
1869 let token_text = self.tokenizer.decode_token($id);
1870 let mut go = true;
1871 if let Some(ref mut cb) = on_token {
1872 if !cb(&token_text) {
1873 finish_reason = "cancelled".to_string();
1874 go = false;
1875 }
1876 }
1877 go
1878 }
1879 }};
1880 }
1881
1882 let mut next_pos = input_ids.len();
1884 'decode: while generated < max_tokens {
1885 if self
1886 .cancel
1887 .swap(false, std::sync::atomic::Ordering::Relaxed)
1888 {
1889 finish_reason = "cancelled".to_string();
1890 break 'decode;
1891 }
1892 let mut logits = match self.graph_logits.take() {
1893 Some(lg) => lg,
1894 None => {
1895 inference::rms_norm_into(
1896 &hidden,
1897 &self.weights.final_norm,
1898 self.rms_eps,
1899 self.norm_style,
1900 &mut self.ws.n1,
1901 );
1902 self.lm_head_forward(&self.ws.n1)
1903 }
1904 };
1905 let t_next = sampler::sample_with_scratch(
1906 &logits,
1907 &self.sampler_config,
1908 &all_ids,
1909 &mut self.rng,
1910 &mut self.sampler_scratch,
1911 );
1912 if self.confidence_on {
1913 confidence.push(top1_prob_t(&logits, t_next, calib_temp));
1914 }
1915 attention::recycle_buf(&mut logits);
1916 if trace_on {
1917 let skill = router.as_ref().and_then(|r| r.active_id());
1921 traces.push(TokenTrace {
1922 t: generated,
1923 token_id: t_next,
1924 confidence: confidence.last().copied().unwrap_or(0.0),
1925 active_skill: skill,
1926 recon: None,
1927 switched: false,
1928 });
1929 }
1930 if !commit!(t_next) {
1931 break 'decode;
1932 }
1933 if generated >= max_tokens {
1934 break 'decode;
1935 }
1936
1937 if self.kv_cache.needs_eviction() {
1938 let keep = (self.kv_cache.max_seq_len / 2).max(1);
1939 self.kv_cache.evict(keep);
1940 }
1941
1942 match &mut mtp {
1943 #[cfg(feature = "gpu")]
1945 Some(m) if graph_spec && generated + 1 < max_tokens && next_pos > 0 => {
1946 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
1947 m,
1948 &hidden,
1949 t_next,
1950 next_pos,
1951 &mut drafted,
1952 &mut accepted,
1953 ) {
1954 next_pos = n_pos;
1955 hidden = new_h;
1956 let mut stopped = false;
1957 for &id in &extra {
1958 if self.confidence_on {
1959 confidence.push(0.0);
1960 }
1961 if !commit!(id) {
1962 stopped = true;
1963 break;
1964 }
1965 }
1966 if stopped {
1967 break 'decode;
1968 }
1969 continue 'decode;
1970 }
1971 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
1973 next_pos += 1;
1974 continue 'decode;
1975 }
1976 Some(m) if !graph_spec && generated + 1 < max_tokens => {
1978 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
1979 drafted += 1;
1980 let emb1 = self.embed_single(t_next);
1981 let emb2 = self.embed_single(draft);
1982 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
1983
1984 inference::rms_norm_into(
1985 &h1,
1986 &self.weights.final_norm,
1987 self.rms_eps,
1988 self.norm_style,
1989 &mut self.ws.n1,
1990 );
1991 let mut logits1 = self.lm_head_forward(&self.ws.n1);
1992 let t_after = sampler::sample_with_scratch(
1993 &logits1,
1994 &self.sampler_config,
1995 &all_ids,
1996 &mut self.rng,
1997 &mut self.sampler_scratch,
1998 );
1999 if self.confidence_on {
2000 confidence.push(top1_prob_t(&logits1, t_after, calib_temp));
2001 }
2002 attention::recycle_buf(&mut logits1);
2003 if trace_on {
2004 traces.push(TokenTrace {
2007 t: generated,
2008 token_id: t_after,
2009 confidence: confidence.last().copied().unwrap_or(0.0),
2010 active_skill: None,
2011 recon: None,
2012 switched: false,
2013 });
2014 }
2015 let stop = !commit!(t_after);
2016
2017 if t_after == draft {
2018 accepted += 1;
2019 self.commit_linear_scratch();
2020 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2021 hidden = h2;
2022 next_pos += 2;
2023 } else {
2024 for layer in &mut self.kv_cache.layers {
2026 layer.truncate_last(1);
2027 }
2028 if !stop {
2029 let _ = self.mtp_step(m, &h1, t_after, next_pos);
2030 hidden = self.forward_layers(
2031 &self.embed_single(t_after),
2032 next_pos + 1,
2033 None,
2034 );
2035 }
2036 next_pos += 2;
2037 }
2038 if stop {
2039 break 'decode;
2040 }
2041 }
2042 _ => {
2044 #[cfg(feature = "gpu")]
2049 if Self::dsv4_spec_on() && self.dsv4.is_some() {
2050 static SAID: std::sync::Once = std::sync::Once::new();
2051 SAID.call_once(|| {
2052 eprintln!(
2053 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
2054 !self.dsv4_mtp.is_empty(),
2055 task_mask.is_none(),
2056 router.is_none(),
2057 !trace_on,
2058 self.sampler_config.temperature < 1e-6,
2059 self.sampler_config.repetition_penalty == 1.0,
2060 );
2061 });
2062 }
2063 #[cfg(feature = "gpu")]
2064 if Self::dsv4_spec_on()
2065 && self.dsv4.is_some()
2066 && !self.dsv4_mtp.is_empty()
2067 && task_mask.is_none()
2068 && router.is_none()
2069 && !trace_on
2070 && self.sampler_config.temperature < 1e-6
2071 && self.sampler_config.repetition_penalty == 1.0
2072 && generated + 1 < max_tokens
2073 && all_ids.len() >= 2
2074 {
2075 let tip_token = all_ids[all_ids.len() - 2];
2076 if let Some((extra, n_pos)) = self.dsv4_spec_step(
2077 tip_token,
2078 t_next,
2079 next_pos,
2080 &mut drafted,
2081 &mut accepted,
2082 ) {
2083 next_pos = n_pos;
2084 let mut stopped = false;
2085 for &id in &extra {
2086 if self.confidence_on {
2087 confidence.push(0.0);
2088 }
2089 if !commit!(id) {
2090 stopped = true;
2091 break;
2092 }
2093 }
2094 if stopped {
2095 break 'decode;
2096 }
2097 continue 'decode;
2098 }
2099 }
2100 self.graph_want_logits = fuse_lm;
2101 let mut t_fwd = t_next;
2107 let pure_greedy = self.sampler_config.temperature < 1e-6
2108 && self.sampler_config.repetition_penalty == 1.0
2109 && self.sampler_config.suppress_tokens.is_empty();
2110 let burst_k = std::env::var("CMF_MULTISTEP")
2115 .ok()
2116 .and_then(|v| v.parse::<usize>().ok())
2117 .unwrap_or(0);
2118 if pure_greedy
2119 && burst_k >= 1
2120 && fuse_lm
2121 && task_mask.is_none()
2122 && router.is_none()
2123 && !trace_on
2124 && !self.confidence_on
2125 {
2126 let mut stopped = false;
2127 loop {
2128 let room = max_tokens.saturating_sub(generated);
2129 if room <= 2 {
2130 break;
2131 }
2132 let k = burst_k.min(room - 1);
2133 if k < 1 {
2134 break;
2135 }
2136 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
2137 break;
2138 };
2139 next_pos += k;
2140 for &id in &ids {
2141 if !commit!(id) {
2142 stopped = true;
2143 break;
2144 }
2145 }
2146 if stopped {
2147 break;
2148 }
2149 t_fwd = *ids.last().unwrap();
2150 }
2151 if stopped {
2152 break 'decode;
2153 }
2154 }
2155 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
2156 next_pos += 1;
2157 if let Some(r) = &mut router {
2160 let phi = self.dyn_phi_ema.clone();
2161 let decision = r.step(&phi, generated);
2162 if let Some(new_active) = decision {
2163 let _ = self.set_active_skill(new_active);
2164 }
2165 if trace_on {
2168 if let Some(last) = traces.last_mut() {
2169 let e = r.last_best_e();
2170 last.recon = e.is_finite().then_some(e);
2171 last.switched = decision.is_some();
2172 }
2173 }
2174 }
2175 }
2176 }
2177 }
2178
2179 self.graph_want_logits = false;
2180 self.graph_logits = None;
2181 if router.is_some() {
2183 let _ = self.set_active_skill(None);
2184 }
2185 self.dyn_router = router.or(self.dyn_router.take());
2186 self.mtp = mtp.or(self.mtp.take());
2187
2188 let output_ids = &all_ids[input_ids.len()..];
2189 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
2193 self.kv_history = all_ids[..forwarded.min(all_ids.len())].to_vec();
2194 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
2196 Ok(GenerateResult {
2197 text: self.tokenizer.decode(output_ids),
2198 token_ids: output_ids.to_vec(),
2199 prompt_tokens: input_ids.len(),
2200 tokens_generated: generated,
2201 finish_reason,
2202 mtp_drafted: drafted,
2203 mtp_accepted: accepted,
2204 token_confidence: confidence,
2205 traces,
2206 })
2207 }
2208
2209 fn mtp_step(
2213 &mut self,
2214 m: &mut MtpModule,
2215 hidden: &[f32],
2216 next_token: u32,
2217 position: usize,
2218 ) -> u32 {
2219 self.mtp_step_h(m, hidden, next_token, position).0
2220 }
2221
2222 fn chain_probe_note(depth: usize, prefix_ok: bool) {
2226 use std::sync::Mutex;
2227 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
2228 let mut t = T.lock().unwrap();
2229 if t.len() <= depth {
2230 t.resize(depth + 1, (0, 0));
2231 }
2232 t[depth].0 += 1;
2233 t[depth].1 += prefix_ok as u64;
2234 if depth == 0 && t[0].0 % 128 == 0 {
2235 let line: Vec<String> = t
2236 .iter()
2237 .enumerate()
2238 .map(|(d, (n, k))| format!("d{}={:.0}%({n})", d + 1, 100.0 * *k as f64 / (*n).max(1) as f64))
2239 .collect();
2240 eprintln!("mtp-chain: {}", line.join(" "));
2241 }
2242 }
2243
2244 fn mtp_step_h(
2248 &mut self,
2249 m: &mut MtpModule,
2250 hidden: &[f32],
2251 next_token: u32,
2252 position: usize,
2253 ) -> (u32, Vec<f32>) {
2254 let e = self.embed_single(next_token);
2258 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2259 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2260 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2261 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2262 let mut x = vec![0.0f32; self.hidden_size];
2263 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2264
2265 let lw = &m.layer;
2267 inference::rms_norm_into(
2268 &x,
2269 &lw.input_norm,
2270 self.rms_eps,
2271 self.norm_style,
2272 &mut self.ws.n1,
2273 );
2274 let attn = match &lw.attn {
2275 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2277 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2278 AttnKind::Full {
2279 wq,
2280 wk,
2281 wv,
2282 wo,
2283 q_norm,
2284 k_norm,
2285 output_gate,
2286 softplus_gate,
2287 bias,
2288 } => {
2289 let mut cfg = self.attn_cfg(position);
2290 cfg.q_norm = q_norm.as_deref();
2291 cfg.k_norm = k_norm.as_deref();
2292 cfg.output_gate = *output_gate;
2293 cfg.softplus_gate = softplus_gate
2294 .as_ref()
2295 .map(|(gate, per_head)| (gate, *per_head));
2296 cfg.bias = bias
2297 .as_ref()
2298 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2299 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2300 }
2301 AttnKind::Linear(_) | AttnKind::LinearGdn(_) | AttnKind::ShortConv(_) => {
2302 unreachable!("MTP block is full attention")
2303 }
2304 };
2305 for (i, &a) in attn.iter().enumerate() {
2306 x[i] += a;
2307 }
2308 inference::rms_norm_into(
2309 &x,
2310 &lw.post_norm,
2311 self.rms_eps,
2312 self.norm_style,
2313 &mut self.ws.p1,
2314 );
2315 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
2316 for (i, &f) in ffn.iter().enumerate() {
2317 x[i] += f;
2318 }
2319
2320 inference::rms_norm_into(
2321 &x,
2322 &m.final_norm,
2323 self.rms_eps,
2324 self.norm_style,
2325 &mut self.ws.n1,
2326 );
2327 let mut lg = self.lm_head_forward(&self.ws.n1);
2328 let draft = sampler::argmax(&lg);
2329 attention::recycle_buf(&mut lg);
2330 (draft, x)
2331 }
2332
2333 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
2337 let e = self.embed_single(next_token);
2338 let mut cat = vec![0.0f32; 2 * self.hidden_size];
2339 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
2340 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
2341 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
2342 let mut x = vec![0.0f32; self.hidden_size];
2343 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
2344 inference::rms_norm_into(&x, &m.layer.input_norm, self.rms_eps, self.norm_style, &mut self.ws.n1);
2345 let attn = match &m.layer.attn {
2346 AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate, bias } => {
2347 let mut cfg = self.attn_cfg(position);
2348 cfg.q_norm = q_norm.as_deref();
2349 cfg.k_norm = k_norm.as_deref();
2350 cfg.output_gate = *output_gate;
2351 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
2352 cfg.bias = bias.as_ref().map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
2353 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
2354 }
2355 _ => return,
2356 };
2357 let _ = attn;
2358 }
2359
2360 #[cfg(feature = "gpu")]
2367 #[allow(clippy::too_many_arguments)]
2368 fn graph_spec_step(
2369 &mut self,
2370 m: &mut MtpModule,
2371 hidden: &[f32],
2372 t_next: u32,
2373 next_pos: usize,
2374 drafted: &mut usize,
2375 accepted: &mut usize,
2376 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
2377 let k_spec: usize = std::env::var("CMF_GRAPH_SPEC_K")
2378 .ok()
2379 .and_then(|v| v.parse().ok())
2380 .filter(|&v| (1..=8).contains(&v))
2381 .unwrap_or(2);
2382 if next_pos == 0 {
2383 return None;
2384 }
2385 let t_round = std::time::Instant::now();
2386 let mut drafts = Vec::with_capacity(k_spec);
2391 let (d1, mut hx) = self.mtp_step_h(m, hidden, t_next, next_pos - 1);
2392 drafts.push(d1);
2393 for j in 1..k_spec {
2394 let (dj, hj) = self.mtp_step_h(m, &hx, drafts[j - 1], next_pos - 1 + j);
2395 drafts.push(dj);
2396 hx = hj;
2397 }
2398 *drafted += k_spec;
2399 let t_draft = t_round.elapsed();
2400 let b = k_spec + 1;
2403 let mut hiddens = vec![0.0f32; b * self.hidden_size];
2404 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
2405 let e = self.embed_single(t);
2406 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
2407 }
2408 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
2409 let (lm_gw, lm_rows) = {
2410 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
2411 (
2412 crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[] },
2413 self.weights.lm_head.rows(),
2414 )
2415 };
2416 let mut logits = Vec::new();
2417 let final_norm = self.weights.final_norm.clone();
2418 let ok = self.try_batch_graph_wgpu(
2419 &mut hiddens,
2420 &positions,
2421 b,
2422 Some(crate::gpu::SpecTail {
2423 lm: lm_gw,
2424 lm_rows,
2425 final_norm: &final_norm,
2426 logits_out: &mut logits,
2427 }),
2428 );
2429 if !ok {
2430 m.kv.truncate_last(k_spec);
2433 return None;
2434 }
2435 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
2436 eprintln!(
2437 "spec-round: draft {:.1} ms | verify {:.1} ms",
2438 t_draft.as_secs_f64() * 1e3,
2439 (t_round.elapsed() - t_draft).as_secs_f64() * 1e3,
2440 );
2441 }
2442 let ids: Vec<u32> = (0..b)
2444 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
2445 .collect();
2446 let mut a = 0usize;
2447 while a < k_spec && ids[a] == drafts[a] {
2448 a += 1;
2449 }
2450 if a + 1 < b {
2452 crate::gpu::gdn_spec_restore(self.graph_kv_id, a);
2453 }
2454 *accepted += a;
2455 m.kv.truncate_last(k_spec.saturating_sub(1));
2458 for j in 0..a {
2459 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
2460 let row = row.to_vec();
2461 self.mtp_warm(m, &row, ids[j], next_pos + j);
2462 }
2463 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
2465 row.resize(self.vocab_size, 0.0);
2466 if let Some(c) = self.final_softcap {
2467 for l in row.iter_mut() {
2468 *l = c * (*l / c).tanh();
2469 }
2470 }
2471 self.graph_logits = Some(row);
2472 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
2473 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
2474 }
2475
2476 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
2485 if !self.pair_supported() {
2486 return (0.0, 0.0);
2487 }
2488 let emb1 = self.embed_single(1);
2489 let emb2 = self.embed_single(2);
2490 let pos = self.kv_cache.seq_len();
2491
2492 let t0 = std::time::Instant::now();
2493 for _ in 0..iters {
2494 let _ = self.forward_layers(&emb1, pos, None);
2495 let _ = self.forward_layers(&emb2, pos + 1, None);
2496 for l in &mut self.kv_cache.layers {
2497 l.truncate_last(2);
2498 }
2499 }
2500 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2501
2502 let t1 = std::time::Instant::now();
2503 for _ in 0..iters {
2504 let _ = self.forward_pair(&emb1, &emb2, pos);
2505 for l in &mut self.kv_cache.layers {
2506 l.truncate_last(2);
2507 }
2508 }
2509 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
2510 (singles_ms, pair_ms)
2511 }
2512
2513 fn pair_supported(&self) -> bool {
2521 !self.weights.layers.is_empty()
2528 && self.g3n.is_none()
2529 && !self
2530 .weights
2531 .layers
2532 .iter()
2533 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
2534 }
2535
2536 fn forward_pair(
2537 &mut self,
2538 emb1: &[f32],
2539 emb2: &[f32],
2540 position: usize,
2541 ) -> (Vec<f32>, Vec<f32>) {
2542 let mut h1 = emb1.to_vec();
2543 let mut h2 = emb2.to_vec();
2544 let (_nkv, _hd, hs, _rd, eps) = (
2545 self.num_kv_heads,
2546 self.head_dim,
2547 self.hidden_size,
2548 self.rotary_dim,
2549 self.rms_eps,
2550 );
2551 let pool = self.pool.clone();
2552
2553 for li in 0..self.num_layers {
2554 let lw = &self.weights.layers[self.phys_layer(li)];
2555 inference::rms_norm_into(
2558 &h1,
2559 &lw.input_norm,
2560 self.rms_eps,
2561 self.norm_style,
2562 &mut self.ws.n1,
2563 );
2564 inference::rms_norm_into(
2565 &h2,
2566 &lw.input_norm,
2567 self.rms_eps,
2568 self.norm_style,
2569 &mut self.ws.n2,
2570 );
2571
2572 let (a1, a2) = match &lw.attn {
2573 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
2574 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
2575 AttnKind::Linear(w) => {
2576 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
2577 let layer = &mut self.kv_cache.layers[li];
2578 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2579 vmf_phase_pair(
2580 &self.ws.n1,
2581 &self.ws.n2,
2582 w,
2583 &cfg,
2584 state,
2585 scratch,
2586 self.pool.as_deref(),
2587 )
2588 }
2589 AttnKind::LinearGdn(w) => {
2590 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
2591 let layer = &mut self.kv_cache.layers[li];
2592 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2593 gdn_pair(
2594 &self.ws.n1,
2595 &self.ws.n2,
2596 w,
2597 &cfg,
2598 state,
2599 scratch,
2600 self.pool.as_deref(),
2601 )
2602 }
2603 AttnKind::ShortConv(w) => {
2604 let cfg = self
2605 .short_conv_cfg
2606 .expect("short-conv layer without short_conv_cfg");
2607 let layer = &mut self.kv_cache.layers[li];
2608 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
2609 short_conv_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::Full {
2620 wq,
2621 wk,
2622 wv,
2623 wo,
2624 q_norm,
2625 k_norm,
2626 output_gate,
2627 softplus_gate,
2628 bias,
2629 } => {
2630 let inv_freq_l = self.layer_inv_freq(li);
2631 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
2632 let cfg = QwenAttnCfg {
2633 num_heads: self.layer_num_heads(li),
2634 num_kv_heads: nkv_l,
2635 head_dim: hd_l,
2636 hidden_size: hs,
2637 position,
2638 inv_freq: &inv_freq_l,
2639 rotary_dim: rd_l,
2640 scale: self.attn_scale,
2641 softcap: self.attn_softcap,
2642 window: self.layer_window(li),
2643 v_norm: self.attn_v_norm,
2644 q_norm: q_norm.as_deref(),
2645 k_norm: k_norm.as_deref(),
2646 output_gate: *output_gate,
2647 softplus_gate: softplus_gate
2648 .as_ref()
2649 .map(|(gate, per_head)| (gate, *per_head)),
2650 rope_scale: self.layer_rope_scale(li),
2651 bias: bias
2652 .as_ref()
2653 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2654 rms_eps: eps,
2655 norm_style: self.norm_style,
2656 pool: pool.as_deref(),
2657 };
2658 attention::qwen_attention_pair(
2659 &self.ws.n1,
2660 &self.ws.n2,
2661 wq,
2662 wk,
2663 wv,
2664 wo,
2665 &mut self.kv_cache.layers[li],
2666 &cfg,
2667 )
2668 }
2669 };
2670 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
2671 Some(w) => (
2672 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
2673 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
2674 ),
2675 None => (a1, a2),
2676 };
2677 for i in 0..self.hidden_size {
2678 h1[i] += a1[i];
2679 h2[i] += a2[i];
2680 }
2681 let (mut a1, mut a2) = (a1, a2);
2682 attention::recycle_buf(&mut a1);
2683 attention::recycle_buf(&mut a2);
2684
2685 let lw = &self.weights.layers[self.phys_layer(li)];
2686 inference::rms_norm_into(
2687 &h1,
2688 &lw.post_norm,
2689 self.rms_eps,
2690 self.norm_style,
2691 &mut self.ws.p1,
2692 );
2693 inference::rms_norm_into(
2694 &h2,
2695 &lw.post_norm,
2696 self.rms_eps,
2697 self.norm_style,
2698 &mut self.ws.p2,
2699 );
2700 let (f1, f2) = match &lw.ffn {
2701 FfnKind::DenseMoe(dm) => (
2704 dense_moe_ffn(
2705 dm,
2706 &self.ws.p1,
2707 &h1,
2708 self.rms_eps,
2709 self.norm_style,
2710 self.pool.as_deref(),
2711 ),
2712 dense_moe_ffn(
2713 dm,
2714 &self.ws.p2,
2715 &h2,
2716 self.rms_eps,
2717 self.norm_style,
2718 self.pool.as_deref(),
2719 ),
2720 ),
2721 _ => ffn_forward_pair(
2722 &lw.ffn,
2723 &self.ws.p1,
2724 &self.ws.p2,
2725 self.pool.as_deref(),
2726 None,
2727 ),
2728 };
2729 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
2730 Some(w) => (
2731 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
2732 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
2733 ),
2734 None => (f1, f2),
2735 };
2736 for i in 0..self.hidden_size {
2737 h1[i] += f1[i];
2738 h2[i] += f2[i];
2739 }
2740 let (mut f1, mut f2) = (f1, f2);
2741 attention::recycle_buf(&mut f1);
2742 attention::recycle_buf(&mut f2);
2743 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
2744 for i in 0..self.hidden_size {
2745 h1[i] *= sc;
2746 h2[i] *= sc;
2747 }
2748 }
2749 if self.is_loop_end(li) && li + 1 < self.num_layers {
2751 h1 = inference::rms_norm(
2752 &h1,
2753 &self.weights.final_norm,
2754 self.rms_eps,
2755 self.norm_style,
2756 );
2757 h2 = inference::rms_norm(
2758 &h2,
2759 &self.weights.final_norm,
2760 self.rms_eps,
2761 self.norm_style,
2762 );
2763 }
2764 }
2765 (h1, h2)
2766 }
2767
2768 fn commit_linear_scratch(&mut self) {
2770 for layer in &mut self.kv_cache.layers {
2771 if !layer.linear_scratch.is_empty() {
2772 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
2773 layer.linear_scratch.clear();
2774 }
2775 }
2776 }
2777
2778 pub fn forward_ids(
2781 &mut self,
2782 ids: &[u32],
2783 task_mask: Option<&TaskMask>,
2784 ) -> Result<Vec<f32>, String> {
2785 if ids.is_empty() {
2786 return Err("empty id sequence".to_string());
2787 }
2788 self.kv_cache.clear();
2789 self.kv_history.clear();
2790 self.o1_begin();
2791 let mut hidden = vec![0.0f32; self.hidden_size];
2792 let mut pos = 0usize;
2793 if task_mask.is_none() && self.can_prefill_batched() && ids.len() > 2 {
2794 let chunk = prefill_chunk();
2798 let hs = self.hidden_size;
2799 while pos < ids.len() {
2800 let end = (pos + chunk).min(ids.len());
2801 let hb = self.prefill_batch(&ids[pos..end], pos);
2802 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
2803 pos = end;
2804 }
2805 }
2806 if task_mask.is_none()
2810 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
2811 && self.pair_supported()
2812 {
2813 while pos + 1 < ids.len() {
2814 let e1 = self.embed_single(ids[pos]);
2815 let e2 = self.embed_single(ids[pos + 1]);
2816 let (_, h2) = self.forward_pair(&e1, &e2, pos);
2817 self.commit_linear_scratch();
2818 hidden = h2;
2819 pos += 2;
2820 }
2821 }
2822 while pos < ids.len() {
2823 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
2824 pos += 1;
2825 }
2826 self.o1_seal();
2830 let normed = inference::rms_norm(
2831 &hidden,
2832 &self.weights.final_norm,
2833 self.rms_eps,
2834 self.norm_style,
2835 );
2836 Ok(self.lm_head_forward(&normed))
2837 }
2838
2839 pub fn ppl_ids(&mut self, ids: &[u32]) -> f64 {
2846 let (nll, cnt) = self.nll_ids_from(ids, 0);
2847 (nll / cnt.max(1) as f64).exp()
2848 }
2849
2850 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
2855 self.kv_cache.clear();
2856 self.kv_history.clear();
2857 FFN_PROBE.with(|p| {
2858 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
2859 });
2860 crate::gpu::cpu_scope(|| {
2861 for (pos, &id) in ids.iter().enumerate() {
2862 let emb = self.embed_single(id);
2863 let _ = self.forward_layers(&emb, pos, None);
2864 }
2865 });
2866 self.kv_cache.clear();
2867 self.kv_history.clear();
2868 FFN_PROBE
2869 .with(|p| p.borrow_mut().take())
2870 .unwrap_or_default()
2871 }
2872
2873 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> f64 {
2877 self.kv_cache.clear();
2878 self.kv_history.clear();
2879 let mut nll = 0f64;
2880 let mut cnt = 0usize;
2881 let mut hidden = vec![0f32; self.hidden_size];
2882 for (pos, &id) in ids.iter().enumerate() {
2883 if pos > 0 {
2884 inference::rms_norm_into(
2885 &hidden,
2886 &self.weights.final_norm,
2887 self.rms_eps,
2888 self.norm_style,
2889 &mut self.ws.n1,
2890 );
2891 let mut logits = self.lm_head_forward(&self.ws.n1);
2892 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2893 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
2894 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
2895 nll -= p.max(1e-300).ln();
2896 cnt += 1;
2897 attention::recycle_buf(&mut logits);
2898 }
2899 let emb = self.embed_single(id);
2900 hidden = self.forward_layers(&emb, pos, Some(mask));
2901 }
2902 self.kv_cache.clear();
2903 self.kv_history.clear();
2904 (nll / cnt.max(1) as f64).exp()
2905 }
2906
2907 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> (f64, usize) {
2916 self.kv_cache.clear();
2917 self.kv_history.clear();
2918 let mut nll = 0f64;
2919 let mut cnt = 0usize;
2920 if self.can_prefill_batched() {
2921 const CHUNK: usize = 128;
2927 const LM_SUB: usize = 32;
2928 let n = ids.len().saturating_sub(1);
2929 let hs = self.hidden_size;
2930 let rows = self.weights.lm_head.rows();
2931 let mut pos = 0usize;
2932 while pos < n {
2933 let end = (pos + CHUNK).min(n);
2934 let bsz = end - pos;
2935 let hb = self.prefill_batch(&ids[pos..end], pos);
2936 let mut k0 = 0usize;
2937 while k0 < bsz {
2938 let k1 = (k0 + LM_SUB).min(bsz);
2939 let sb = k1 - k0;
2940 if pos + k1 <= start {
2943 k0 = k1;
2944 continue;
2945 }
2946 let mut normed = vec![0.0f32; sb * hs];
2947 for k in 0..sb {
2948 let r = inference::rms_norm(
2949 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
2950 &self.weights.final_norm,
2951 self.rms_eps,
2952 self.norm_style,
2953 );
2954 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
2955 }
2956 let mut logits = vec![0.0f32; sb * rows];
2957 self.weights
2958 .lm_head
2959 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
2960 for k in 0..sb {
2961 if pos + k0 + k < start {
2962 continue;
2963 }
2964 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
2965 if let Some(mu) = self.logit_multiplier {
2966 for v in lg.iter_mut() {
2967 *v *= mu;
2968 }
2969 }
2970 if let Some(c) = self.final_softcap {
2974 for v in lg.iter_mut() {
2975 *v = c * (*v / c).tanh();
2976 }
2977 }
2978 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
2979 let target = ids[pos + k0 + k + 1] as usize;
2980 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2981 let lse: f64 = lg
2982 .iter()
2983 .map(|&v| ((v - max) as f64).exp())
2984 .sum::<f64>()
2985 .ln()
2986 + max as f64;
2987 nll += lse - lg[target] as f64;
2988 cnt += 1;
2989 if std::env::var("CMF_PPL_TRACE").is_ok() {
2990 let top = lg
2991 .iter()
2992 .enumerate()
2993 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
2994 .map(|(i, _)| i)
2995 .unwrap_or(0);
2996 eprintln!(
2997 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
2998 pos + k0 + k,
2999 target,
3000 lse - lg[target] as f64,
3001 top,
3002 lg[target],
3003 lg[top]
3004 );
3005 }
3006 }
3007 k0 = k1;
3008 }
3009 pos = end;
3010 }
3011 self.kv_cache.clear();
3012 self.kv_history.clear();
3013 return (nll, cnt);
3014 }
3015 for pos in 0..ids.len().saturating_sub(1) {
3016 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3017 let out_of_band = self.graph_logits.take();
3025 if pos < start {
3026 continue;
3027 }
3028 let logits = match out_of_band {
3029 Some(lg) => lg,
3030 None => {
3031 let normed = inference::rms_norm(
3032 &hidden,
3033 &self.weights.final_norm,
3034 self.rms_eps,
3035 self.norm_style,
3036 );
3037 self.lm_head_forward(&normed)
3041 }
3042 };
3043 let target = ids[pos + 1] as usize;
3044 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3045 let lse: f64 = logits
3046 .iter()
3047 .map(|&v| ((v - max) as f64).exp())
3048 .sum::<f64>()
3049 .ln()
3050 + max as f64;
3051 let tok_nll = lse - logits[target] as f64;
3052 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3053 let top = logits
3054 .iter()
3055 .enumerate()
3056 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3057 .map(|(i, _)| i)
3058 .unwrap_or(0);
3059 eprintln!(
3060 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3061 logits[target], logits[top]
3062 );
3063 }
3064 nll += tok_nll;
3065 cnt += 1;
3066 }
3067 self.kv_cache.clear();
3068 self.kv_history.clear();
3069 (nll, cnt)
3070 }
3071
3072 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> (f64, usize) {
3088 self.kv_cache.clear();
3089 self.kv_history.clear();
3090 self.o1_begin();
3091 let n = ids.len().saturating_sub(1);
3092 let p = prefill.min(n);
3093 let mut pos = 0usize;
3095 if self.can_prefill_batched() {
3096 const CHUNK: usize = 128;
3097 while pos < p {
3098 let end = (pos + CHUNK).min(p);
3099 let _ = self.prefill_batch(&ids[pos..end], pos);
3100 pos = end;
3101 }
3102 } else {
3103 while pos < p {
3104 let _ = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3105 pos += 1;
3106 }
3107 }
3108 self.o1_seal();
3109
3110 let mut nll = 0f64;
3111 let mut cnt = 0usize;
3112 for pos in p..n {
3113 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3114 let normed = inference::rms_norm(
3115 &hidden,
3116 &self.weights.final_norm,
3117 self.rms_eps,
3118 self.norm_style,
3119 );
3120 let logits = self.lm_head_forward(&normed);
3124 let target = ids[pos + 1] as usize;
3125 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3126 let lse: f64 = logits
3127 .iter()
3128 .map(|&v| ((v - max) as f64).exp())
3129 .sum::<f64>()
3130 .ln()
3131 + max as f64;
3132 let tok_nll = lse - logits[target] as f64;
3133 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3134 let top = logits
3135 .iter()
3136 .enumerate()
3137 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3138 .map(|(i, _)| i)
3139 .unwrap_or(0);
3140 eprintln!(
3141 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3142 logits[target], logits[top]
3143 );
3144 }
3145 nll += tok_nll;
3146 cnt += 1;
3147 }
3148 self.kv_cache.clear();
3149 self.kv_history.clear();
3150 (nll, cnt)
3151 }
3152
3153 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
3161 self.kv_cache.clear();
3162 self.kv_history.clear();
3163 let n = ids.len().saturating_sub(1);
3164 let mut correct = Vec::with_capacity(n);
3165 let mut pmax = Vec::with_capacity(n);
3166 for pos in 0..n {
3167 let emb = self.embed_single(ids[pos]);
3168 let hidden = self.forward_layers(&emb, pos, None);
3169 let normed = inference::rms_norm(
3170 &hidden,
3171 &self.weights.final_norm,
3172 self.rms_eps,
3173 self.norm_style,
3174 );
3175 let logits = self.lm_head_forward(&normed);
3179 let target = ids[pos + 1] as usize;
3180 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
3181 for (i, &v) in logits.iter().enumerate() {
3182 if v > mval {
3183 mval = v;
3184 amax = i;
3185 }
3186 }
3187 correct.push(amax == target);
3188 let row: Vec<f32> = temps
3189 .iter()
3190 .map(|&t| {
3191 let tt = t.max(1e-3);
3192 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
3193 1.0 / s.max(1e-12) })
3195 .collect();
3196 pmax.push(row);
3197 }
3198 self.kv_cache.clear();
3199 self.kv_history.clear();
3200 (correct, pmax)
3201 }
3202
3203 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> (f64, usize) {
3210 let mut router = match self.dyn_router.take() {
3211 Some(r) => r,
3212 None => return (self.ppl_ids(ids), 0),
3213 };
3214 router.reset();
3215 self.dyn_phi_seen = 0;
3216 let _ = self.set_active_skill(None);
3217
3218 self.kv_cache.clear();
3219
3220 self.kv_history.clear();
3221 let mut nll = 0f64;
3222 let mut cnt = 0usize;
3223 for pos in 0..ids.len().saturating_sub(1) {
3224 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
3225 let normed = inference::rms_norm(
3226 &hidden,
3227 &self.weights.final_norm,
3228 self.rms_eps,
3229 self.norm_style,
3230 );
3231 let logits = self.lm_head_forward(&normed);
3235 let target = ids[pos + 1] as usize;
3236 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
3237 let lse: f64 = logits
3238 .iter()
3239 .map(|&v| ((v - max) as f64).exp())
3240 .sum::<f64>()
3241 .ln()
3242 + max as f64;
3243 let tok_nll = lse - logits[target] as f64;
3244 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
3245 let top = logits
3246 .iter()
3247 .enumerate()
3248 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3249 .map(|(i, _)| i)
3250 .unwrap_or(0);
3251 eprintln!(
3252 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
3253 logits[target], logits[top]
3254 );
3255 }
3256 nll += tok_nll;
3257 cnt += 1;
3258 let phi = self.dyn_phi_ema.clone();
3260 if let Some(new_active) = router.step(&phi, pos) {
3261 let _ = self.set_active_skill(new_active);
3262 }
3263 }
3264 let switches = router.switches.len();
3265 let _ = self.set_active_skill(None);
3266 self.dyn_router = Some(router);
3267 self.kv_cache.clear();
3268 self.kv_history.clear();
3269 ((nll / cnt.max(1) as f64).exp(), switches)
3270 }
3271
3272 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
3274 self.kv_cache.clear();
3275 self.kv_history.clear();
3276 let mut acc = vec![0f32; self.hidden_size];
3277 for (pos, &id) in ids.iter().enumerate() {
3278 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
3279 for (a, v) in acc.iter_mut().zip(&h) {
3280 *a += v;
3281 }
3282 }
3283 let n = ids.len().max(1) as f32;
3284 for a in acc.iter_mut() {
3285 *a /= n;
3286 }
3287 self.kv_cache.clear();
3288 self.kv_history.clear();
3289 acc
3290 }
3291
3292 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
3298 let b = ids.len();
3299 let hs = self.hidden_size;
3300 let mut h: Vec<f32> = vec![0.0; b * hs];
3303 let mut h_ready = false;
3304 let fill_h = |h: &mut Vec<f32>, me: &Self| {
3305 for (bi, &id) in ids.iter().enumerate() {
3306 let e = me.embed_single(id);
3307 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
3308 }
3309 };
3310 let (_nkv, _hd, _rd, eps) = (
3311 self.num_kv_heads,
3312 self.head_dim,
3313 self.rotary_dim,
3314 self.rms_eps,
3315 );
3316 let pool = self.pool.clone();
3317 let norm_style = self.norm_style;
3318
3319 #[cfg(target_os = "macos")]
3320 let mut chunk_skip_until = 0usize;
3321 for li in 0..self.num_layers {
3322 crate::gpu::set_layer(li as i64); #[cfg(target_os = "macos")]
3329 {
3330 if li < chunk_skip_until {
3331 continue;
3332 }
3333 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
3339 fill_h(&mut h, self);
3340 h_ready = true;
3341 }
3342 let ids_for_embed = (!h_ready && li == 0).then_some(ids);
3343 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed);
3344 if end > li {
3345 h_ready = true;
3346 chunk_skip_until = end;
3347 if self.is_loop_end(end - 1) && end < self.num_layers {
3350 for bi in 0..b {
3351 let normed = inference::rms_norm(
3352 &h[bi * hs..(bi + 1) * hs],
3353 &self.weights.final_norm,
3354 eps,
3355 norm_style,
3356 );
3357 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3358 }
3359 }
3360 continue;
3361 }
3362 }
3363 if !h_ready {
3364 fill_h(&mut h, self);
3365 h_ready = true;
3366 }
3367 let lw = &self.weights.layers[self.phys_layer(li)];
3368 match &lw.attn {
3370 AttnKind::Kda(w) => {
3371 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
3373 let mut normed = vec![0.0f32; b * hs];
3374 for bi in 0..b {
3375 inference::rms_norm_into(
3376 &h[bi * hs..(bi + 1) * hs],
3377 &lw.input_norm,
3378 eps,
3379 norm_style,
3380 &mut normed[bi * hs..(bi + 1) * hs],
3381 );
3382 }
3383 let attn = crate::linear_core::kda_forward_batch(
3384 &normed,
3385 b,
3386 w,
3387 &cfg,
3388 &mut self.kv_cache.layers[li].linear_state,
3389 pool.as_deref(),
3390 );
3391 for (dst, &a) in h.iter_mut().zip(&attn) {
3392 *dst += a;
3393 }
3394 }
3395 AttnKind::LinearGdn(w) => {
3396 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
3398 let mut normed = vec![0.0f32; b * hs];
3399 for bi in 0..b {
3400 let r = inference::rms_norm(
3401 &h[bi * hs..(bi + 1) * hs],
3402 &lw.input_norm,
3403 eps,
3404 norm_style,
3405 );
3406 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3407 }
3408 let attn = crate::linear_core::gdn_forward_batch(
3409 &normed,
3410 b,
3411 w,
3412 &cfg,
3413 &mut self.kv_cache.layers[li].linear_state,
3414 pool.as_deref(),
3415 );
3416 for (dst, &a) in h.iter_mut().zip(&attn) {
3417 *dst += a;
3418 }
3419 }
3420 AttnKind::ShortConv(w) => {
3421 let cfg = self
3424 .short_conv_cfg
3425 .expect("short-conv layer without short_conv_cfg");
3426 let mut normed = vec![0.0f32; b * hs];
3427 for bi in 0..b {
3428 inference::rms_norm_into(
3429 &h[bi * hs..(bi + 1) * hs],
3430 &lw.input_norm,
3431 eps,
3432 norm_style,
3433 &mut normed[bi * hs..(bi + 1) * hs],
3434 );
3435 }
3436 let attn = short_conv_forward_batch(
3437 &normed,
3438 b,
3439 w,
3440 &cfg,
3441 &mut self.kv_cache.layers[li].linear_state,
3442 pool.as_deref(),
3443 );
3444 for (dst, &a) in h.iter_mut().zip(&attn) {
3445 *dst += a;
3446 }
3447 }
3448 AttnKind::Mla(w) => {
3449 let inv_freq_l = self.layer_inv_freq(li);
3452 let rs = self.layer_rope_scale(li);
3453 let mut normed = vec![0.0f32; hs];
3454 for bi in 0..b {
3455 inference::rms_norm_into(
3456 &h[bi * hs..(bi + 1) * hs],
3457 &lw.input_norm,
3458 eps,
3459 norm_style,
3460 &mut normed,
3461 );
3462 let ao = mla_attention(
3463 w,
3464 &normed,
3465 &mut self.kv_cache.layers[li],
3466 start_pos + bi,
3467 &inv_freq_l,
3468 rs,
3469 eps,
3470 pool.as_deref(),
3471 );
3472 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
3473 *dst += a;
3474 }
3475 }
3476 }
3477 AttnKind::Full {
3478 wq,
3479 wk,
3480 wv,
3481 wo,
3482 q_norm,
3483 k_norm,
3484 output_gate,
3485 softplus_gate,
3486 bias,
3487 } => {
3488 let mut normed = vec![0.0f32; b * hs];
3492 for bi in 0..b {
3493 inference::rms_norm_into(
3494 &h[bi * hs..(bi + 1) * hs],
3495 &lw.input_norm,
3496 eps,
3497 norm_style,
3498 &mut normed[bi * hs..(bi + 1) * hs],
3499 );
3500 }
3501 let inv_freq_l = self.layer_inv_freq(li);
3502 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
3503 let cfg = QwenAttnCfg {
3504 num_heads: self.layer_num_heads(li),
3505 num_kv_heads: nkv_l,
3506 head_dim: hd_l,
3507 hidden_size: hs,
3508 position: start_pos,
3509 inv_freq: &inv_freq_l,
3510 rotary_dim: rd_l,
3511 scale: self.attn_scale,
3512 softcap: self.attn_softcap,
3513 window: self.layer_window(li),
3514 v_norm: self.attn_v_norm,
3515 q_norm: q_norm.as_deref(),
3516 k_norm: k_norm.as_deref(),
3517 output_gate: *output_gate,
3518 softplus_gate: softplus_gate
3519 .as_ref()
3520 .map(|(gate, per_head)| (gate, *per_head)),
3521 rope_scale: self.layer_rope_scale(li),
3522 bias: bias
3523 .as_ref()
3524 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3525 rms_eps: eps,
3526 norm_style,
3527 pool: pool.as_deref(),
3528 };
3529 let mut attn = attention::qwen_attention_batch(
3530 &normed,
3531 b,
3532 wq,
3533 wk,
3534 wv,
3535 wo,
3536 &mut self.kv_cache.layers[li],
3537 &cfg,
3538 );
3539 if let Some(w) = &lw.attn_out_norm {
3540 for bi in 0..b {
3541 inference::rms_norm_into(
3542 &attn[bi * hs..(bi + 1) * hs],
3543 w,
3544 eps,
3545 norm_style,
3546 &mut normed[bi * hs..(bi + 1) * hs],
3547 );
3548 }
3549 attn.copy_from_slice(&normed);
3550 }
3551 for (dst, &a) in h.iter_mut().zip(&attn) {
3552 *dst += a;
3553 }
3554 }
3555 AttnKind::Linear(w) => {
3556 for bi in 0..b {
3557 let normed = inference::rms_norm(
3558 &h[bi * hs..(bi + 1) * hs],
3559 &lw.input_norm,
3560 eps,
3561 norm_style,
3562 );
3563 vmf_phase_forward(
3564 &normed,
3565 w,
3566 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
3567 &mut self.kv_cache.layers[li].linear_state,
3568 pool.as_deref(),
3569 )
3570 .iter()
3571 .enumerate()
3572 .for_each(|(i, &a)| h[bi * hs + i] += a);
3573 }
3574 }
3575 }
3576
3577 let lw = &self.weights.layers[self.phys_layer(li)];
3579 let mut post = vec![0.0f32; b * hs];
3580 for bi in 0..b {
3581 let r =
3582 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
3583 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3584 }
3585 let mut ffn = match &lw.ffn {
3586 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref()),
3587 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
3588 FfnKind::DenseMoe(dm) => {
3591 let mut out = vec![0.0f32; b * hs];
3592 for bi in 0..b {
3593 let r = dense_moe_ffn(
3594 dm,
3595 &post[bi * hs..(bi + 1) * hs],
3596 &h[bi * hs..(bi + 1) * hs],
3597 eps,
3598 norm_style,
3599 pool.as_deref(),
3600 );
3601 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
3602 }
3603 out
3604 }
3605 };
3606 if let Some(w) = &lw.ffn_out_norm {
3607 for bi in 0..b {
3608 inference::rms_norm_into(
3609 &ffn[bi * hs..(bi + 1) * hs],
3610 w,
3611 eps,
3612 norm_style,
3613 &mut post[bi * hs..(bi + 1) * hs],
3614 );
3615 }
3616 ffn.copy_from_slice(&post);
3617 }
3618 for (dst, &f) in h.iter_mut().zip(&ffn) {
3619 *dst += f;
3620 }
3621 if let Some(sc) = lw.layer_scale {
3622 for v in h.iter_mut() {
3623 *v *= sc;
3624 }
3625 }
3626 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
3627 if let Some(t) = tp.parse::<usize>().ok() {
3628 if t >= start_pos && t < start_pos + b {
3629 let bi = t - start_pos;
3630 let row = &h[bi * hs..(bi + 1) * hs];
3631 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
3632 eprintln!(
3633 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
3634 row[0], row[1]
3635 );
3636 }
3637 }
3638 }
3639 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
3643 let row = &h[(b - 1) * hs..b * hs];
3644 let rms =
3645 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
3646 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
3647 eprintln!(
3648 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
3649 match &self.weights.layers[self.phys_layer(li)].attn {
3650 AttnKind::LinearGdn(_) => "gdn",
3651 AttnKind::Linear(_) => "vmf",
3652 AttnKind::ShortConv(_) => "conv",
3653 _ => "attn",
3654 },
3655 match &lw.ffn {
3656 FfnKind::Moe(_) => "moe",
3657 FfnKind::Dense(_) => "dense",
3658 FfnKind::DenseMoe(_) => "dense+moe",
3659 },
3660 );
3661 }
3662 if self.is_loop_end(li) && li + 1 < self.num_layers {
3664 for bi in 0..b {
3665 let normed = inference::rms_norm(
3666 &h[bi * hs..(bi + 1) * hs],
3667 &self.weights.final_norm,
3668 eps,
3669 norm_style,
3670 );
3671 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
3672 }
3673 }
3674 if std::env::var("CMF_TRACE_H").is_ok() {
3675 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
3676 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
3677 eprintln!(
3678 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
3679 lw.layer_scale
3680 );
3681 }
3682 }
3683 crate::gpu::set_layer(-1); h
3685 }
3686
3687 fn embed_single(&self, id: u32) -> Vec<f32> {
3689 let mut out = vec![0.0f32; self.hidden_size];
3690 if (id as usize) < self.weights.embed_tokens.rows() {
3691 self.weights.embed_tokens.row_f32(id as usize, &mut out);
3692 }
3693 if self.embed_multiplier != 1.0 {
3694 for v in out.iter_mut() {
3695 *v *= self.embed_multiplier;
3696 }
3697 }
3698 if self.dsv4.is_some() {
3702 let mut v = vec![0.0f32; self.hidden_size.max(1)];
3703 v[0] = id as f32;
3704 return v;
3705 }
3706 if let Some(b) = &self.g3n {
3709 return b.0.extend_embedding(id, &out, self.pool.as_deref());
3710 }
3711 out
3712 }
3713
3714 #[cfg(target_os = "macos")]
3720 fn chunk_run_gpu(
3721 &mut self,
3722 li0: usize,
3723 h: &mut [f32],
3724 b: usize,
3725 pos0: usize,
3726 embed_ids: Option<&[u32]>,
3727 ) -> usize {
3728 if !crate::gpu::enabled_here()
3732 || std::env::var("CMF_GPU_CHUNK")
3733 .map(|v| v == "0")
3734 .unwrap_or(false)
3735 || b < 32
3736 || self.swa.is_some()
3737 || self.global_attn.is_some()
3738 || self.attn_v_norm
3739 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
3740 {
3741 return li0;
3742 }
3743 let Some(model) = self.model.clone() else {
3744 return li0;
3745 };
3746 let inv_freq = self.inv_freq.clone();
3747 let (nh, nkv, hd, hs) = (
3748 self.num_heads,
3749 self.num_kv_heads,
3750 self.head_dim,
3751 self.hidden_size,
3752 );
3753 let loop_end = if self.loop_final_norm {
3757 ((li0 / self.physical_layers) + 1) * self.physical_layers
3758 } else {
3759 self.num_layers
3760 };
3761 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
3762 let mut stored_at: Vec<usize> = Vec::new();
3763 for li in li0..self.num_layers.min(loop_end) {
3764 let lw = &self.weights.layers[self.phys_layer(li)];
3765 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
3766 break;
3767 }
3768 let AttnKind::Full {
3769 wq,
3770 wk,
3771 wv,
3772 wo,
3773 q_norm,
3774 k_norm,
3775 output_gate: false,
3776 softplus_gate: None,
3777 bias,
3778 } = &lw.attn
3779 else {
3780 break;
3781 };
3782 let FfnKind::Dense(d) = &lw.ffn else { break };
3783 if d.act != Act::Silu {
3784 break;
3785 }
3786 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
3791 t.q8_row_parts()
3792 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3793 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
3794 }
3795 let parts = (
3796 cw(wq),
3797 cw(wk),
3798 cw(wv),
3799 cw(wo),
3800 cw(&d.gate_proj),
3801 cw(&d.up_proj),
3802 cw(&d.down_proj),
3803 );
3804 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
3805 else {
3806 break;
3807 };
3808 let layer = &self.kv_cache.layers[li];
3809 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
3810 break;
3811 }
3812 stored_at.push(layer.head_len(0));
3813 layers.push(crate::gpu_metal::ChunkLayer {
3814 model: &model,
3815 kv_id: self.graph_kv_id,
3816 layer: li,
3817 wq: pq,
3818 wk: pk,
3819 wv: pv,
3820 wo: po,
3821 gate: pg,
3822 up: pu,
3823 down: pd,
3824 input_norm: &lw.input_norm,
3825 post_norm: &lw.post_norm,
3826 bias: bias
3827 .as_ref()
3828 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
3829 q_norm: q_norm.as_deref(),
3830 k_norm: k_norm.as_deref(),
3831 inv_freq: &inv_freq,
3832 rd: self.rotary_dim,
3833 nh,
3834 nkv,
3835 hd,
3836 hs,
3837 inter: d.gate_proj.rows(),
3838 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
3839 eps: self.rms_eps as f32,
3840 });
3841 }
3842 if layers.is_empty() {
3843 return li0;
3844 }
3845 let row = nkv * hd;
3846 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
3847 .iter()
3848 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
3849 .collect();
3850 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
3851 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
3852 let li = layers[i].layer;
3853 let layer = &self.kv_cache.layers[li];
3854 io.push(crate::gpu_metal::ChunkIo {
3855 cpu_stored: stored_at[i],
3856 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
3857 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
3858 out_k: ok,
3859 out_v: ov,
3860 imp: oi,
3861 });
3862 }
3863 let n_run = layers.len();
3864 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
3865 let ep = embed_ids.and_then(|ids| {
3868 self.weights
3869 .embed_tokens
3870 .q8_row_parts()
3871 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
3872 idx,
3873 rows,
3874 row_scale: rs,
3875 ids,
3876 mult: self.embed_multiplier,
3877 })
3878 });
3879 if embed_ids.is_some() && ep.is_none() {
3880 return li0;
3881 }
3882 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
3883 return li0;
3884 }
3885 drop(io);
3886 drop(layers);
3887 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
3890 let li = li0 + i;
3891 let layer = &mut self.kv_cache.layers[li];
3892 for bi in 0..b {
3893 layer.append(
3894 &ok[bi * row..(bi + 1) * row],
3895 &ov[bi * row..(bi + 1) * row],
3896 &[],
3897 );
3898 }
3899 layer.accumulate_imp(oi);
3900 }
3901 last
3902 }
3903
3904 fn layer_is_local(&self, li: usize) -> bool {
3907 if let Some(layers) = &self.sliding_layers {
3908 return layers.get(li).copied().unwrap_or(false);
3909 }
3910 match self.swa {
3911 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
3912 None => false,
3913 }
3914 }
3915
3916 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
3919 if self.layer_is_local(li) {
3920 if let Some(f) = &self.inv_freq_local {
3921 return f.clone();
3922 }
3923 } else if let Some(f) = &self.inv_freq_global {
3924 return f.clone();
3925 }
3926 self.inv_freq.clone()
3927 }
3928
3929 fn layer_window(&self, li: usize) -> Option<usize> {
3931 self.swa
3932 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
3933 }
3934
3935 fn layer_num_heads(&self, li: usize) -> usize {
3936 self.attention_heads_per_layer
3937 .as_ref()
3938 .and_then(|v| v.get(li).copied())
3939 .unwrap_or(self.num_heads)
3940 }
3941
3942 fn layer_rope_scale(&self, li: usize) -> f32 {
3943 if self.layer_is_local(li) {
3944 self.rope_scale_local
3945 } else {
3946 self.rope_scale
3947 }
3948 }
3949
3950 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
3953 if !self.layer_is_local(li) {
3954 if let Some((ghd, gkv)) = self.global_attn {
3955 return (gkv, ghd, ghd);
3956 }
3957 }
3958 (
3959 self.num_kv_heads,
3960 self.head_dim,
3961 if self.layer_is_local(li) {
3962 self.rotary_dim_local.unwrap_or(self.rotary_dim)
3963 } else {
3964 self.rotary_dim
3965 },
3966 )
3967 }
3968
3969 fn forward_layers(
3971 &mut self,
3972 hidden: &[f32],
3973 position: usize,
3974 task_mask: Option<&TaskMask>,
3975 ) -> Vec<f32> {
3976 self.forward_layers_upto(hidden, position, task_mask, None)
3977 }
3978
3979 fn try_token_graph_wgpu(
3983 &self,
3984 hidden: &[f32],
3985 position: usize,
3986 logits_out: &mut Vec<f32>,
3987 layers_run: &mut usize,
3988 ) -> Option<Vec<f32>> {
3989 self.try_token_graph_wgpu_steps(hidden, position, logits_out, 1, None, Some(layers_run))
3990 }
3991
3992 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
3996 if self.o1_active() || self.attn_softcap > 0.0 {
3997 return None;
3998 }
3999 let graph_on = match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
4000 Some("0") => return None,
4001 Some(_) => true,
4002 None => crate::gpu::wgpu_graph_default(),
4003 };
4004 if !graph_on {
4005 return None;
4006 }
4007 let emb = self.embed_single(t_next);
4008 let mut lg = Vec::new();
4009 let mut ids = Vec::new();
4010 self.try_token_graph_wgpu_steps(&emb, position, &mut lg, k, Some(&mut ids), None)?;
4011 (ids.len() == k).then_some(ids)
4012 }
4013
4014 fn try_token_graph_wgpu_steps(
4018 &self,
4019 hidden: &[f32],
4020 position: usize,
4021 logits_out: &mut Vec<f32>,
4022 steps: usize,
4023 ids_out: Option<&mut Vec<u32>>,
4024 layers_run: Option<&mut usize>,
4025 ) -> Option<Vec<f32>> {
4026 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
4029 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
4030 return None;
4034 }
4035 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (0..self.num_layers)
4040 .map(|li| {
4041 if !o1_gpu {
4042 return None;
4043 }
4044 self.kv_cache.layers[self.phys_layer(li)].o1_views()
4045 })
4046 .collect();
4047 if self.o1_active() && o1_gpu {
4048 let want: usize = (0..self.num_layers)
4051 .filter(|li| !matches!(self.kv_cache.layers[self.phys_layer(*li)].o1, None))
4052 .count();
4053 let have = o1_views.iter().filter(|v| v.is_some()).count();
4054 if want == 0 || have != want {
4055 return None;
4056 }
4057 }
4058 let nh = self.num_heads;
4059 let (nkv, hd, rd) = self.layer_geom(0);
4060 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4061 let mut layers = Vec::with_capacity(self.num_layers);
4062 let mut model = None;
4063 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
4064 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4065 if let Some((_, i, kind, rs)) = t.graph_weight() {
4066 return Some(crate::gpu::GraphW {
4067 idx: i,
4068 kind,
4069 row_scale: rs,
4070 data: &[],
4071 });
4072 }
4073 t.as_f32().map(|d| crate::gpu::GraphW {
4075 idx: 0,
4076 kind: 4,
4077 row_scale: &[],
4078 data: d,
4079 })
4080 }
4081 for li in 0..self.num_layers {
4082 let lw = &self.weights.layers[self.phys_layer(li)];
4083 if dbg {
4084 let ak = match &lw.attn {
4085 AttnKind::Mla(_) => "Mla".into(),
4086 AttnKind::Full {
4087 output_gate, bias, ..
4088 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
4089 AttnKind::LinearGdn(_) => "LinearGdn".into(),
4090 AttnKind::Kda(_) => "Kda".into(),
4091 AttnKind::Linear(_) => "Linear".into(),
4092 AttnKind::ShortConv(_) => "ShortConv".into(),
4093 };
4094 let fk = match &lw.ffn {
4095 FfnKind::Dense(_) => "Dense",
4096 FfnKind::Moe(_) => "Moe",
4097 FfnKind::DenseMoe(_) => "DenseMoe",
4098 };
4099 eprintln!("graph L{li}: attn={ak} ffn={fk}");
4100 }
4101 let gffn = match &lw.ffn {
4102 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
4104 gate: gw(&d.gate_proj)?,
4105 up: gw(&d.up_proj)?,
4106 down: gw(&d.down_proj)?,
4107 },
4108 FfnKind::Moe(m) => {
4109 if m.router_sigmoid
4114 || m.expert_bias.is_some()
4115 || m.route_tau.is_some()
4116 || m.mask.is_some()
4117 {
4118 return None;
4119 }
4120 let (se, sg) = m.shared.as_ref()?;
4121 let sgate = gw(sg.as_ref()?)?;
4122 let router = gw(&m.router)?;
4123 let inter = m.experts.first()?.gate_proj.rows();
4124 let mut experts = Vec::with_capacity(m.experts.len() + 1);
4125 let mut q4tp: Option<bool> = None;
4128 let mut gu_q2: Option<bool> = None;
4131 for e in m.experts.iter().chain(std::iter::once(se)) {
4132 if !matches!(e.act, Act::Silu)
4133 || e.gate_proj.rows() != inter
4134 || e.up_proj.rows() != inter
4135 {
4136 return None;
4137 }
4138 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
4139 Some((mm, gi)) => (
4140 mm,
4141 gi,
4142 e.up_proj.mapped_q4t()?.1,
4143 e.down_proj.mapped_q4t()?.1,
4144 false,
4145 false,
4146 ),
4147 None => match e.gate_proj.mapped_q2tp() {
4148 Some((mm, gi)) => (
4149 mm,
4150 gi,
4151 e.up_proj.mapped_q2tp()?.1,
4152 e.down_proj.mapped_q4tp()?.1,
4153 true,
4154 true,
4155 ),
4156 None => {
4157 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
4158 (
4159 mm,
4160 gi,
4161 e.up_proj.mapped_q4tp()?.1,
4162 e.down_proj.mapped_q4tp()?.1,
4163 true,
4164 false,
4165 )
4166 }
4167 },
4168 };
4169 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
4170 {
4171 tracing::warn!(
4177 "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."
4178 );
4179 return None;
4180 }
4181 model.get_or_insert_with(|| mm.clone());
4182 experts.push((gi, ui, di));
4183 }
4184 crate::gpu::GraphFfn::Moe {
4185 router,
4186 shared_gate: sgate,
4187 experts,
4188 n_exp: m.experts.len(),
4189 top_k: std::env::var("CMF_TOPK_PROBE")
4195 .ok()
4196 .and_then(|v| v.parse::<usize>().ok())
4197 .filter(|k| *k > 0 && *k <= m.top_k)
4198 .unwrap_or(m.top_k),
4199 inter,
4200 norm_topk: m.norm_topk_prob,
4201 q4tp: q4tp?,
4202 gu_q2: gu_q2.unwrap_or(false),
4203 }
4204 }
4205 };
4206 let attn = match &lw.attn {
4207 AttnKind::Full {
4208 wq,
4209 wk,
4210 wv,
4211 wo,
4212 q_norm,
4213 k_norm,
4214 output_gate,
4215 softplus_gate,
4216 bias,
4217 } => {
4218 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
4219 return None;
4220 }
4221 let (m, _, _, _) = wq.graph_weight()?;
4222 model = Some(m.clone());
4223 crate::gpu::GraphAttn::Full {
4224 wq: gw(wq)?,
4225 wk: gw(wk)?,
4226 wv: gw(wv)?,
4227 wo: gw(wo)?,
4228 q_norm: q_norm.as_deref(),
4229 k_norm: k_norm.as_deref(),
4230 bias: bias
4231 .as_ref()
4232 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4233 output_gate: *output_gate,
4234 cpu_k: self.kv_cache.layers[li].k_heads(),
4235 cpu_v: self.kv_cache.layers[li].v_heads(),
4236 }
4237 }
4238 AttnKind::LinearGdn(w) => {
4239 let cfg = self.gdn_cfg?;
4240 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
4241 model = Some(m.clone());
4242 crate::gpu::GraphAttn::Gdn {
4243 qkv: gw(&w.in_proj_qkv)?,
4244 z: gw(&w.in_proj_z)?,
4245 a: gw(&w.in_proj_a)?,
4246 b: gw(&w.in_proj_b)?,
4247 out: gw(&w.out_proj)?,
4248 conv1d: &w.conv1d,
4249 a_log: &w.a_log,
4250 dt_bias: &w.dt_bias,
4251 norm: &w.norm,
4252 nv: cfg.num_v_heads,
4253 nk: cfg.num_k_heads,
4254 dk: cfg.key_head_dim,
4255 dv: cfg.value_head_dim,
4256 kk: cfg.conv_kernel,
4257 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
4258 }
4259 }
4260 _ => return None,
4261 };
4262 layers.push(crate::gpu::GraphLayer {
4263 input_norm: &lw.input_norm,
4264 attn,
4265 post_norm: &lw.post_norm,
4266 ffn: gffn,
4267 });
4268 }
4269 let model = model?;
4270 let lm_gw = if self.graph_want_logits
4276 && std::env::var("CMF_GPU_LMHEAD")
4277 .map(|v| v != "0")
4278 .unwrap_or(true)
4279 {
4280 self.weights.lm_head.graph_weight().map(|(_, i, kind, rs)| {
4281 (
4282 crate::gpu::GraphW {
4283 idx: i,
4284 kind,
4285 row_scale: rs,
4286 data: &[],
4287 },
4288 self.weights.lm_head.rows(),
4289 )
4290 })
4291 } else {
4292 None
4293 };
4294 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
4295 let emb_gw = if steps > 1 {
4297 self.weights
4298 .embed_tokens
4299 .graph_weight()
4300 .map(|(_, i, kind, rs)| {
4301 (
4302 crate::gpu::GraphW {
4303 idx: i,
4304 kind,
4305 row_scale: rs,
4306 data: &[],
4307 },
4308 self.weights.embed_tokens.rows(),
4309 self.embed_multiplier as f32,
4310 )
4311 })
4312 } else {
4313 None
4314 };
4315
4316 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
4319 (0..self.num_layers - 1)
4320 .filter(|&li| (li + 1) % self.physical_layers == 0)
4321 .collect()
4322 } else {
4323 Vec::new()
4324 };
4325 let mut h = hidden.to_vec();
4326 crate::gpu::forward_token_graph(
4327 &model,
4328 self.graph_kv_id,
4329 &layers,
4330 &o1_views,
4331 self.o1_epoch,
4332 &self.inv_freq,
4333 &mut h,
4334 nh,
4335 nkv,
4336 hd,
4337 rd,
4338 self.hidden_size,
4339 self.intermediate_size,
4340 position,
4341 self.kv_cache.max_seq_len,
4342 gemma,
4343 self.rms_eps as f32,
4344 lm,
4345 &self.weights.final_norm,
4346 logits_out,
4347 &loop_norm_at,
4348 steps,
4349 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
4350 ids_out,
4351 layers_run,
4352 )
4353 .then_some(h)
4354 }
4355
4356 fn try_batch_graph_wgpu(
4361 &self,
4362 hiddens: &mut [f32],
4363 positions: &[usize],
4364 k: usize,
4365 spec: Option<crate::gpu::SpecTail<'_>>,
4366 ) -> bool {
4367 let _tb = std::time::Instant::now();
4368 if self.attn_softcap > 0.0 {
4369 return false; }
4371 if self.o1_active() {
4372 return false;
4373 }
4374 let nh = self.num_heads;
4375 let (nkv, hd, rd) = self.layer_geom(0);
4376 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
4377 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
4378 if let Some((_, i, kind, rs)) = t.graph_weight() {
4379 return Some(crate::gpu::GraphW {
4380 idx: i,
4381 kind,
4382 row_scale: rs,
4383 data: &[],
4384 });
4385 }
4386 t.as_f32().map(|d| crate::gpu::GraphW {
4387 idx: 0,
4388 kind: 4,
4389 row_scale: &[],
4390 data: d,
4391 })
4392 }
4393 let built: Option<(
4394 Vec<crate::gpu::GraphLayer<'_>>,
4395 std::sync::Arc<cortiq_core::CmfModel>,
4396 )> = (|| {
4397 let mut layers = Vec::with_capacity(self.num_layers);
4398 let mut model = None;
4399 for li in 0..self.num_layers {
4400 let lw = &self.weights.layers[self.phys_layer(li)];
4401 let gffn = match &lw.ffn {
4408 FfnKind::Dense(d) => crate::gpu::GraphFfn::Dense {
4409 gate: gw(&d.gate_proj)?,
4410 up: gw(&d.up_proj)?,
4411 down: gw(&d.down_proj)?,
4412 },
4413 FfnKind::Moe(m) => {
4414 if m.router_sigmoid
4415 || m.expert_bias.is_some()
4416 || m.route_tau.is_some()
4417 || m.mask.is_some()
4418 {
4419 return None;
4420 }
4421 let (se, sg) = m.shared.as_ref()?;
4422 let sgate = gw(sg.as_ref()?)?;
4423 let router = gw(&m.router)?;
4424 let inter = m.experts.first()?.gate_proj.rows();
4425 let mut experts = Vec::with_capacity(m.experts.len() + 1);
4426 let mut q4tp: Option<bool> = None;
4427 for e in m.experts.iter().chain(std::iter::once(se)) {
4428 if !matches!(e.act, Act::Silu)
4429 || e.gate_proj.rows() != inter
4430 || e.up_proj.rows() != inter
4431 {
4432 return None;
4433 }
4434 let (mm, gi, ui, di, is_p) = match e.gate_proj.mapped_q4t() {
4435 Some((mm, gi)) => (
4436 mm,
4437 gi,
4438 e.up_proj.mapped_q4t()?.1,
4439 e.down_proj.mapped_q4t()?.1,
4440 false,
4441 ),
4442 None => {
4443 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
4444 (
4445 mm,
4446 gi,
4447 e.up_proj.mapped_q4tp()?.1,
4448 e.down_proj.mapped_q4tp()?.1,
4449 true,
4450 )
4451 }
4452 };
4453 if *q4tp.get_or_insert(is_p) != is_p {
4454 return None;
4455 }
4456 model.get_or_insert_with(|| mm.clone());
4457 experts.push((gi, ui, di));
4458 }
4459 crate::gpu::GraphFfn::Moe {
4460 router,
4461 shared_gate: sgate,
4462 experts,
4463 n_exp: m.experts.len(),
4464 top_k: m.top_k,
4465 inter,
4466 norm_topk: m.norm_topk_prob,
4467 q4tp: q4tp?,
4468 gu_q2: false,
4471 }
4472 }
4473 _ => return None,
4474 };
4475 let attn = match &lw.attn {
4476 AttnKind::Full {
4477 wq,
4478 wk,
4479 wv,
4480 wo,
4481 q_norm,
4482 k_norm,
4483 output_gate,
4484 softplus_gate,
4485 bias,
4486 } => {
4487 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
4488 return None;
4489 }
4490 let (m, _, _, _) = wq.graph_weight()?;
4491 model = Some(m.clone());
4492 crate::gpu::GraphAttn::Full {
4493 wq: gw(wq)?,
4494 wk: gw(wk)?,
4495 wv: gw(wv)?,
4496 wo: gw(wo)?,
4497 q_norm: q_norm.as_deref(),
4498 k_norm: k_norm.as_deref(),
4499 bias: bias
4500 .as_ref()
4501 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
4502 output_gate: *output_gate,
4503 cpu_k: self.kv_cache.layers[li].k_heads(),
4504 cpu_v: self.kv_cache.layers[li].v_heads(),
4505 }
4506 }
4507 AttnKind::LinearGdn(w) => {
4508 let cfg = self.gdn_cfg?;
4509 let (m, _, _, _) = w.in_proj_qkv.graph_weight()?;
4510 model = Some(m.clone());
4511 crate::gpu::GraphAttn::Gdn {
4512 qkv: gw(&w.in_proj_qkv)?,
4513 z: gw(&w.in_proj_z)?,
4514 a: gw(&w.in_proj_a)?,
4515 b: gw(&w.in_proj_b)?,
4516 out: gw(&w.out_proj)?,
4517 conv1d: &w.conv1d,
4518 a_log: &w.a_log,
4519 dt_bias: &w.dt_bias,
4520 norm: &w.norm,
4521 nv: cfg.num_v_heads,
4522 nk: cfg.num_k_heads,
4523 dk: cfg.key_head_dim,
4524 dv: cfg.value_head_dim,
4525 kk: cfg.conv_kernel,
4526 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
4527 }
4528 }
4529 _ => return None,
4530 };
4531 layers.push(crate::gpu::GraphLayer {
4532 input_norm: &lw.input_norm,
4533 attn,
4534 post_norm: &lw.post_norm,
4535 ffn: gffn,
4536 });
4537 }
4538 Some((layers, model?))
4539 })();
4540 let Some((layers, model)) = built else {
4541 {
4542 use std::sync::atomic::{AtomicBool, Ordering};
4543 static SAID: AtomicBool = AtomicBool::new(false);
4544 if !SAID.swap(true, Ordering::Relaxed) {
4545 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
4546 }
4547 }
4548 return false;
4549 };
4550 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
4551 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
4552 }
4553 crate::gpu::forward_batch_graph(
4554 &model,
4555 self.graph_kv_id,
4556 &layers,
4557 &self.inv_freq,
4558 hiddens,
4559 nh,
4560 nkv,
4561 hd,
4562 rd,
4563 self.hidden_size,
4564 self.intermediate_size,
4565 positions,
4566 self.kv_cache.max_seq_len,
4567 gemma,
4568 self.rms_eps as f32,
4569 k,
4570 spec,
4571 )
4572 }
4573
4574 fn draft_probe() -> bool {
4578 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4579 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
4580}
4581
4582 #[cfg(feature = "gpu")]
4594 fn dsv4_spec_on() -> bool {
4595 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4596 *ON.get_or_init(|| std::env::var("CMF_DSV4_SPEC").map(|v| v != "0").unwrap_or(true))
4597 }
4598
4599 #[cfg(feature = "gpu")]
4606 fn dsv4_spec_step(
4607 &mut self,
4608 tip_token: u32,
4609 t_next: u32,
4610 next_pos: usize,
4611 drafted: &mut usize,
4612 accepted_ctr: &mut usize,
4613 ) -> Option<(Vec<u32>, usize)> {
4614 let t_all = std::time::Instant::now();
4615 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4616 thread_local! {
4617 static LAST: std::cell::Cell<Option<std::time::Instant>> =
4618 const { std::cell::Cell::new(None) };
4619 }
4620 LAST.with(|l| {
4621 if let Some(prev) = l.get() {
4622 eprintln!("между раундами {:.1} мс", prev.elapsed().as_secs_f64() * 1e3);
4623 }
4624 l.set(Some(std::time::Instant::now()));
4625 });
4626 }
4627 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4628 eprintln!("spec_step: вход pos={next_pos}");
4629 }
4630 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
4631 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
4632 if self.dspark.is_none() {
4634 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4635 if t.is_empty() {
4636 return None;
4637 }
4638 crate::dsv4::dspark_arm(&t, cfg.dim);
4639 self.dspark = Some(crate::dsv4::DsparkState::new(
4640 self.dsv4_mtp.len(),
4641 &cfg,
4642 t.len(),
4643 ));
4644 }
4645 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4646 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
4647 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
4648 eprintln!("spec_step: пак не построился (targets {targets:?})");
4649 }
4650 let pack = pack?;
4651 let block = crate::dsv4::dspark_block();
4652 let b_box = self.dsv4.as_mut()?;
4653 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
4654 let ds = self.dspark.as_mut()?;
4655 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
4658 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
4659 if dbg {
4660 eprintln!("spec_step: нет захвата");
4661 }
4662 return None;
4663 }
4664 ds.have_hidden = true;
4665 let tip_pos = next_pos.checked_sub(1)?;
4666 let draft_started = std::time::Instant::now();
4667 let mut conf = Vec::new();
4668 let props = crate::dsv4::dspark_draft_gpu(
4669 g,
4670 &self.dsv4_mtp,
4671 &cfg,
4672 ds,
4673 pack,
4674 st.kv_id,
4675 tip_token,
4676 tip_pos,
4677 self.pool.as_deref(),
4678 &mut conf,
4679 );
4680 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
4681 *drafted += block;
4682 if props.is_empty() || props[0] != t_next {
4683 if dbg {
4684 eprintln!(
4685 "spec_step: черновик {} (props0={:?} t_next={t_next})",
4686 if props.is_empty() { "пуст" } else { "мимо" },
4687 props.first()
4688 );
4689 }
4690 return None;
4691 }
4692 let mut k_verify = crate::dsv4::dspark_verify_k().min(props.len());
4693 let conf_min = {
4699 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
4700 *M.get_or_init(|| {
4701 std::env::var("CMF_DSPARK_CONF_MIN")
4702 .ok()
4703 .and_then(|v| v.parse().ok())
4704 .unwrap_or(0.0)
4705 })
4706 };
4707 if conf_min > 0.0 && conf.len() >= props.len() {
4708 let mut keep = 1usize;
4709 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
4710 keep += 1;
4711 }
4712 k_verify = k_verify.min(keep.max(2));
4713 }
4714 if k_verify < 2 {
4715 return None;
4716 }
4717 let mut fed = Vec::with_capacity(k_verify);
4718 fed.push(t_next);
4719 fed.extend_from_slice(&props[1..k_verify]);
4720 let mut argmax = Vec::new();
4721 let mut logits_all = Vec::new();
4722 let mut walked = Vec::new();
4723 let txn = crate::dsv4::dsv4_verify_chunk(
4724 g,
4725 layers,
4726 &cfg,
4727 st,
4728 &fed,
4729 next_pos,
4730 &self.inv_freq,
4731 self.pool.as_deref(),
4732 &targets,
4733 &mut argmax,
4734 &mut logits_all,
4735 &mut walked,
4736 );
4737 if txn.is_none() && dbg {
4738 eprintln!("spec_step: verify отказал");
4739 }
4740 let txn = txn?;
4741 let b = fed.len();
4742 let mut accepted = 1usize;
4743 while accepted < b && fed[accepted] == argmax[accepted - 1] {
4744 accepted += 1;
4745 }
4746 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
4751 accepted = 1;
4752 }
4753 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
4754 eprintln!(
4755 "spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}"
4756 );
4757 }
4758 let t_fin = std::time::Instant::now();
4759 if !crate::dsv4::dsv4_spec_finish(
4760 g,
4761 layers,
4762 &cfg,
4763 st,
4764 txn,
4765 accepted,
4766 &fed,
4767 &self.inv_freq,
4768 self.pool.as_deref(),
4769 ) {
4770 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
4771 return None;
4772 }
4773 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4774 eprintln!("finish(k={accepted}): {:.1} мс", t_fin.elapsed().as_secs_f64() * 1e3);
4775 }
4776 *accepted_ctr += accepted - 1;
4777 let (hc, dim) = (cfg.hc_mult, cfg.dim);
4782 let dev_caps: Vec<usize> = targets
4789 .iter()
4790 .copied()
4791 .filter(|&t| {
4792 st.dev_set.get(t).copied().unwrap_or(false)
4793 && !st.partial_set.get(t).copied().unwrap_or(false)
4794 })
4795 .collect();
4796 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
4797 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
4798 return None;
4799 }
4800 for t in 0..accepted {
4801 let tip = t + 1 == accepted;
4802 for (slot, &tl) in targets.iter().enumerate() {
4803 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
4804 let lo = (di * b + t) * hc * dim;
4805 crate::dsv4::dspark_capture(
4806 &caps_all[lo..lo + hc * dim],
4807 &cfg,
4808 slot,
4809 &mut ds.main_hidden,
4810 );
4811 } else if tip
4812 && crate::dsv4::dspark_peek_slot(slot, dim, {
4813 let lo = slot * dim;
4814 &mut ds.main_hidden[lo..lo + dim]
4815 })
4816 {
4817 } else {
4822 crate::dsv4::dspark_capture(
4826 &walked[t * hc * dim..(t + 1) * hc * dim],
4827 &cfg,
4828 slot,
4829 &mut ds.main_hidden,
4830 );
4831 }
4832 }
4833 crate::dsv4::dspark_ring_append(g, &self.dsv4_mtp, &cfg, ds, next_pos + t, self.pool.as_deref());
4834 }
4835 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
4836 self.graph_logits = Some(row);
4837 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
4842 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
4843 crate::dsv4::pick_tally_arm();
4844 }
4845 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
4846 eprintln!("spec_step total {:.1} мс (k={accepted})", t_all.elapsed().as_secs_f64() * 1e3);
4847 }
4848 Some((fed[1..accepted].to_vec(), next_pos + accepted))
4849 }
4850
4851 fn dspark_probe(&mut self, position: usize, token_id: u32) {
4852 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
4853 return;
4854 }
4855 let trunk_now = crate::dsv4::pick_tally_take();
4857 crate::dsv4::trunk_freq_note(&trunk_now);
4858 if !trunk_now.is_empty() {
4859 self.dspark_trunk_picks.push(trunk_now);
4860 let keep = crate::dsv4::dspark_block();
4861 if self.dspark_trunk_picks.len() > keep {
4862 self.dspark_trunk_picks.remove(0);
4863 }
4864 }
4865 for p in std::mem::take(&mut self.dspark_pending) {
4868 let Some(i) = position.checked_sub(p.0 + 1) else {
4869 continue;
4870 };
4871 let mut p = p;
4872 if i < p.1.len() {
4873 if p.2 && p.1[i] == token_id {
4874 p.3 = i + 1;
4875 } else {
4876 p.2 = false;
4877 }
4878 if i + 1 < p.1.len() {
4879 self.dspark_pending.push(p);
4880 continue;
4881 }
4882 }
4883 self.dspark_hist.push(p.3);
4884 self.dspark_real.push(token_id);
4885 }
4886 let Some(b) = &mut self.dsv4 else { return };
4887 let (g, layers, cfg) = (&b.0, &b.1, b.2);
4888 let n_layers = layers.len();
4889 if self.dspark.is_none() {
4890 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
4891 if t.is_empty() {
4892 return;
4893 }
4894 eprintln!("DSpark: захват со слоёв {t:?}, блок {}", crate::dsv4::dspark_block());
4895 crate::dsv4::dspark_arm(&t, cfg.dim);
4896 self.dspark = Some(crate::dsv4::DsparkState::new(
4897 self.dsv4_mtp.len(),
4898 &cfg,
4899 t.len(),
4900 ));
4901 }
4902 let ds = self.dspark.as_mut().unwrap();
4903 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
4904 return; }
4906 let mut conf = Vec::new();
4907 crate::dsv4::pick_tally_arm();
4908 let draft_started = std::time::Instant::now();
4913 #[cfg(feature = "gpu")]
4914 let gpu_draft = crate::dsv4::dspark_gpu_on();
4915 #[cfg(not(feature = "gpu"))]
4916 let gpu_draft = false;
4917 let props = if gpu_draft {
4918 #[cfg(feature = "gpu")]
4919 {
4920 let kv_id = b.3.kv_id;
4921 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
4922 Some(pk) => crate::dsv4::dspark_draft_gpu(
4923 g,
4924 &self.dsv4_mtp,
4925 &cfg,
4926 ds,
4927 pk,
4928 kv_id,
4929 token_id,
4930 position,
4931 self.pool.as_deref(),
4932 &mut conf,
4933 ),
4934 None => Vec::new(),
4935 }
4936 }
4937 #[cfg(not(feature = "gpu"))]
4938 Vec::new()
4939 } else {
4940 crate::gpu::cpu_scope(|| {
4941 crate::dsv4::dspark_draft(
4942 g,
4943 &self.dsv4_mtp,
4944 &cfg,
4945 ds,
4946 token_id,
4947 position,
4948 self.pool.as_deref(),
4949 &mut conf,
4950 )
4951 })
4952 };
4953 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
4954 let draft_picks = crate::dsv4::pick_tally_take();
4955 crate::dsv4::dspark_freq_note(&draft_picks);
4956 crate::dsv4::pick_tally_arm();
4959 if !props.is_empty() {
4960 let (tu, tt) = {
4964 let flat: Vec<(usize, Vec<usize>)> = self
4965 .dspark_trunk_picks
4966 .iter()
4967 .flat_map(|v| v.iter().cloned())
4968 .collect();
4969 let mut per: std::collections::HashMap<usize, Vec<usize>> =
4971 std::collections::HashMap::new();
4972 for (li, picks) in flat {
4973 per.entry(li).or_default().extend(picks);
4974 }
4975 let n = per.len().max(1);
4976 let mut u = 0usize;
4977 let mut t = 0usize;
4978 for (_, v) in per {
4979 t += v.len();
4980 u += v.iter().collect::<std::collections::HashSet<_>>().len();
4981 }
4982 (u / n, t / n)
4983 };
4984 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
4985 self.dspark_exp.push((tu, tt, du, dt));
4986 self.dspark_pending.push((position, props, true, 0));
4987 }
4988 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
4989 let n = self.dspark_hist.len() as f32;
4990 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
4991 let block = crate::dsv4::dspark_block();
4992 let mut at = vec![0usize; block + 1];
4993 for &k in &self.dspark_hist {
4994 at[k] += 1;
4995 }
4996 let mut surv = Vec::with_capacity(block);
4998 for i in 1..=block {
4999 let k = at[i..].iter().sum::<usize>() as f32 / n;
5000 surv.push(format!("{k:.2}"));
5001 }
5002 let distinct = self
5003 .dspark_real
5004 .iter()
5005 .collect::<std::collections::HashSet<_>>()
5006 .len();
5007 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
5008 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
5009 });
5010 let m = self.dspark_exp.len().max(1);
5011 eprintln!(
5012 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
5013 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
5014 self.dspark_hist.len(),
5015 mean + 1.0,
5016 surv.join(" ")
5017 );
5018 eprintln!(
5019 "DSpark: разных токенов {distinct} из {} (вырожденность), \
5020 эксперты ствол {}/{} на слой за {block} токенов, \
5021 черновик {}/{} за блок, draft {:.2} мс/блок",
5022 self.dspark_real.len(),
5023 tu / m,
5024 tt / m,
5025 du / m,
5026 dt / m,
5027 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
5028 );
5029 }
5030 }
5031
5032 fn forward_layers_upto(
5033 &mut self,
5034 hidden: &[f32],
5035 position: usize,
5036 task_mask: Option<&TaskMask>,
5037 upto: Option<usize>,
5038 ) -> Vec<f32> {
5039 if let Some(b) = &mut self.dsv4 {
5045 let _ = (task_mask, upto);
5046 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
5047 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5048 st.pos = position;
5049 let mut logits = Vec::new();
5050 crate::dsv4::forward_token(
5051 g,
5052 layers,
5053 &cfg,
5054 st,
5055 token_id,
5056 &self.inv_freq,
5057 self.pool.as_deref(),
5058 &mut logits,
5059 );
5060 self.graph_logits = Some(logits);
5061 self.dspark_probe(position, token_id);
5062 return vec![0.0; self.hidden_size];
5065 }
5066 if let Some(b) = &self.g3n {
5069 let _ = (task_mask, upto);
5070 return crate::g3n::g3n_forward(
5071 &b.0,
5072 &b.1,
5073 hidden,
5074 position,
5075 &mut self.kv_cache.layers,
5076 self.num_heads,
5077 self.num_kv_heads,
5078 self.head_dim,
5079 self.pool.as_deref(),
5080 );
5081 }
5082 let mut h = hidden.to_vec();
5083 let (nh, _nkv, _hd, hs, _rd, eps) = (
5086 self.num_heads,
5087 self.num_kv_heads,
5088 self.head_dim,
5089 self.hidden_size,
5090 self.rotary_dim,
5091 self.rms_eps,
5092 );
5093 let pool = self.pool.clone();
5094 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
5106 let graph_on = match graph_env.as_deref() {
5107 Some("0") => false,
5108 Some(_) => true,
5109 None => crate::gpu::wgpu_graph_default(),
5115 };
5116 let graph_trusted =
5117 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
5118 let race_eligible = graph_on && upto.is_none() && task_mask.is_none();
5119 let mut tail_start = 0usize;
5120 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
5121 let t_graph = std::time::Instant::now();
5122 let mut lg = Vec::new();
5123 let mut gl = 0usize;
5124 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
5125 graph_note(built.is_some());
5126 if let Some(hh) = built {
5127 let dur = t_graph.elapsed();
5128 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5129 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
5130 }
5131 if gl > 0 && gl < self.num_layers {
5132 h = hh;
5138 tail_start = gl;
5139 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
5140 if !graph_trusted {
5141 crate::gpu::graph_race_record(true, dur);
5142 }
5143 if !lg.is_empty() {
5144 lg.resize(self.vocab_size, 0.0);
5147 if let Some(c) = self.final_softcap {
5148 for l in lg.iter_mut() {
5149 *l = c * (*l / c).tanh();
5150 }
5151 }
5152 self.graph_logits = Some(lg);
5153 }
5154 return hh;
5155 }
5156 }
5162 }
5163 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
5164
5165 #[cfg(target_os = "macos")]
5166 let mut gpu_skip_until = 0usize;
5167 for li in tail_start..self.num_layers {
5168 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
5170 if li > u {
5171 break;
5172 }
5173 }
5174 if let Some(mask) = task_mask {
5175 if !mask.layer_alive(li) {
5176 continue; }
5178 }
5179 #[cfg(target_os = "macos")]
5183 {
5184 if li < gpu_skip_until {
5185 continue;
5186 }
5187 if task_mask.is_none() {
5188 let end = self.q1_graph_gpu(li, upto, position, &mut h);
5189 if end > li {
5190 gpu_skip_until = end;
5191 if self.is_loop_end(end - 1) && end < self.num_layers {
5194 h = inference::rms_norm(
5195 &h,
5196 &self.weights.final_norm,
5197 self.rms_eps,
5198 self.norm_style,
5199 );
5200 }
5201 continue;
5202 }
5203 }
5204 }
5205
5206 let lw = &self.weights.layers[self.phys_layer(li)];
5207 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
5208 if tp.parse::<usize>().ok() == Some(position) {
5209 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
5210 eprintln!(
5211 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
5212 h[0], h[1]
5213 );
5214 }
5215 }
5216 inference::rms_norm_into(
5219 &h,
5220 &lw.input_norm,
5221 self.rms_eps,
5222 self.norm_style,
5223 &mut self.ws.n1,
5224 );
5225
5226 let attn_out = match &lw.attn {
5227 AttnKind::Mla(w) => {
5228 let inv_freq_l = self.layer_inv_freq(li);
5229 let rs = self.layer_rope_scale(li);
5230 let eps = self.rms_eps;
5231 let pool = self.pool.clone();
5232 mla_attention(
5233 w,
5234 &self.ws.n1,
5235 &mut self.kv_cache.layers[li],
5236 position,
5237 &inv_freq_l,
5238 rs,
5239 eps,
5240 pool.as_deref(),
5241 )
5242 }
5243 AttnKind::Linear(w) => {
5244 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
5245 vmf_phase_forward(
5246 &self.ws.n1,
5247 w,
5248 &cfg,
5249 &mut self.kv_cache.layers[li].linear_state,
5250 self.pool.as_deref(),
5251 )
5252 }
5253 AttnKind::Kda(w) => {
5254 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
5255 crate::linear_core::kda_forward(
5256 &self.ws.n1,
5257 w,
5258 &cfg,
5259 &mut self.kv_cache.layers[li].linear_state,
5260 self.pool.as_deref(),
5261 )
5262 }
5263 AttnKind::LinearGdn(w) => {
5264 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
5265 gdn_forward(
5266 &self.ws.n1,
5267 w,
5268 &cfg,
5269 &mut self.kv_cache.layers[li].linear_state,
5270 self.pool.as_deref(),
5271 )
5272 }
5273 AttnKind::ShortConv(w) => {
5274 let cfg = self
5275 .short_conv_cfg
5276 .expect("short-conv layer without short_conv_cfg");
5277 short_conv_forward(
5278 &self.ws.n1,
5279 w,
5280 &cfg,
5281 &mut self.kv_cache.layers[li].linear_state,
5282 self.pool.as_deref(),
5283 )
5284 }
5285 AttnKind::Full {
5286 wq,
5287 wk,
5288 wv,
5289 wo,
5290 q_norm,
5291 k_norm,
5292 output_gate,
5293 softplus_gate,
5294 bias,
5295 } if self.kv_cache.layers[li].o1_sealed() => {
5296 let inv_freq_l = self.layer_inv_freq(li);
5299 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5300 let cfg = QwenAttnCfg {
5301 num_heads: self.layer_num_heads(li),
5302 num_kv_heads: nkv_l,
5303 head_dim: hd_l,
5304 hidden_size: hs,
5305 position,
5306 inv_freq: &inv_freq_l,
5307 rotary_dim: rd_l,
5308 scale: self.attn_scale,
5309 softcap: self.attn_softcap,
5310 window: None,
5311 v_norm: self.attn_v_norm,
5312 q_norm: q_norm.as_deref(),
5313 k_norm: k_norm.as_deref(),
5314 output_gate: *output_gate,
5315 softplus_gate: softplus_gate
5316 .as_ref()
5317 .map(|(gate, per_head)| (gate, *per_head)),
5318 rope_scale: self.layer_rope_scale(li),
5319 bias: bias
5320 .as_ref()
5321 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5322 rms_eps: eps,
5323 norm_style: self.norm_style,
5324 pool: pool.as_deref(),
5325 };
5326 attention::qwen_attention_nystrom(
5327 &self.ws.n1,
5328 wq,
5329 wk,
5330 wv,
5331 wo,
5332 &mut self.kv_cache.layers[li],
5333 &cfg,
5334 )
5335 }
5336 AttnKind::Full {
5337 wq,
5338 wk,
5339 wv,
5340 wo,
5341 q_norm,
5342 k_norm,
5343 output_gate,
5344 softplus_gate,
5345 bias,
5346 } => 'attn: {
5347 if graph_on
5350 && !*output_gate
5351 && softplus_gate.is_none()
5352 && self.attention_heads_per_layer.is_none()
5353 && bias.is_none()
5354 && task_mask.is_none()
5355 {
5356 let inv_freq_l = self.layer_inv_freq(li);
5357 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5358 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
5359 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
5360 wq.mapped_q1(),
5361 wk.mapped_q1(),
5362 wv.mapped_q1(),
5363 wo.mapped_q1(),
5364 ) {
5365 let gm = gm.clone();
5366 let mut out = vec![0f32; hs];
5367 let cache = &self.kv_cache.layers[li];
5368 if crate::gpu::attn_dropin(
5369 &gm,
5370 self.graph_kv_id,
5371 li,
5372 &self.ws.n1,
5373 qi,
5374 ki,
5375 vi,
5376 oi,
5377 q_norm.as_deref(),
5378 k_norm.as_deref(),
5379 &inv_freq_l,
5380 nh,
5381 nkv_l,
5382 hd_l,
5383 rd_l,
5384 hs,
5385 position,
5386 self.kv_cache.max_seq_len,
5387 gemma,
5388 eps as f32,
5389 cache.k_heads(),
5390 cache.v_heads(),
5391 &mut out,
5392 ) {
5393 break 'attn out;
5394 }
5395 }
5396 }
5397 let masked = task_mask
5398 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
5399 .unwrap_or(false);
5400 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
5401 match (masked, f32_view) {
5402 (true, (Some(q), Some(k), Some(v), Some(o))) => {
5405 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
5406 attention::multi_head_attention(
5407 &self.ws.n1,
5408 q,
5409 k,
5410 v,
5411 o,
5412 &mut self.kv_cache.layers[li],
5413 self.num_heads,
5414 self.num_kv_heads,
5415 self.head_dim,
5416 self.hidden_size,
5417 position,
5418 &active_heads,
5419 &self.inv_freq,
5420 )
5421 }
5422 (masked, _) => {
5423 if masked {
5424 tracing::warn!(
5425 "layer {li}: head mask on quantized weights not \
5426 supported yet — executing dense"
5427 );
5428 }
5429 let inv_freq_l = self.layer_inv_freq(li);
5430 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
5431 let cfg = QwenAttnCfg {
5432 num_heads: self.layer_num_heads(li),
5433 num_kv_heads: nkv_l,
5434 head_dim: hd_l,
5435 hidden_size: hs,
5436 position,
5437 inv_freq: &inv_freq_l,
5438 rotary_dim: rd_l,
5439 scale: self.attn_scale,
5440 softcap: self.attn_softcap,
5441 window: self.layer_window(li),
5442 v_norm: self.attn_v_norm,
5443 q_norm: q_norm.as_deref(),
5444 k_norm: k_norm.as_deref(),
5445 output_gate: *output_gate,
5446 softplus_gate: softplus_gate
5447 .as_ref()
5448 .map(|(gate, per_head)| (gate, *per_head)),
5449 rope_scale: self.layer_rope_scale(li),
5450 bias: bias
5451 .as_ref()
5452 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
5453 rms_eps: eps,
5454 norm_style: self.norm_style,
5455 pool: pool.as_deref(),
5456 };
5457 attention::qwen_attention(
5458 &self.ws.n1,
5459 wq,
5460 wk,
5461 wv,
5462 wo,
5463 &mut self.kv_cache.layers[li],
5464 &cfg,
5465 )
5466 }
5467 }
5468 }
5469 };
5470 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
5473 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
5474 None => attn_out,
5475 };
5476 let lw = &self.weights.layers[self.phys_layer(li)];
5477 inference::add_rmsnorm_fused_into(
5478 &mut h,
5479 &attn_out,
5480 &lw.post_norm,
5481 self.rms_eps,
5482 self.norm_style,
5483 &mut self.ws.p1,
5484 );
5485 let mut attn_out = attn_out;
5486 attention::recycle_buf(&mut attn_out);
5487 let post_normed = &self.ws.p1;
5488
5489 let ffn_masked = task_mask
5490 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
5491 .unwrap_or(false);
5492 let f32_ffn = match &lw.ffn {
5495 FfnKind::Dense(d) => (
5496 d.gate_proj.as_f32(),
5497 d.up_proj.as_f32(),
5498 d.down_proj.as_f32(),
5499 ),
5500 FfnKind::Moe(_) | FfnKind::DenseMoe(_) => (None, None, None),
5501 };
5502 let ffn_out = match (ffn_masked, f32_ffn) {
5503 (true, (Some(g), Some(u), Some(d))) => {
5504 let active = task_mask.unwrap().ffn_active_indices(li);
5505 inference::sparse_ffn_forward(
5506 post_normed,
5507 g,
5508 u,
5509 d,
5510 self.hidden_size,
5511 self.intermediate_size,
5512 &active,
5513 self.pool.as_deref(),
5514 )
5515 }
5516 (true, _) => match &lw.ffn {
5520 FfnKind::Dense(d) if d.down_proj.sparse_col_ok() => {
5521 let active = task_mask.unwrap().ffn_active_indices(li);
5522 sparse_ffn_quant(
5523 d,
5524 post_normed,
5525 &active,
5526 self.hidden_size,
5527 self.pool.as_deref(),
5528 )
5529 }
5530 FfnKind::Dense(d) => {
5535 let active = task_mask.unwrap().ffn_active_indices(li);
5536 let (gf, uf, df) = dequant_dense_f32(d);
5537 inference::sparse_ffn_forward(
5538 post_normed,
5539 &gf,
5540 &uf,
5541 &df,
5542 self.hidden_size,
5543 self.intermediate_size,
5544 &active,
5545 self.pool.as_deref(),
5546 )
5547 }
5548 FfnKind::Moe(m) => {
5549 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
5553 ffn_forward(
5554 &lw.ffn,
5555 post_normed,
5556 self.pool.as_deref(),
5557 allowed.as_deref(),
5558 )
5559 }
5560 FfnKind::DenseMoe(dm) => dense_moe_ffn(
5561 dm,
5562 post_normed,
5563 &h,
5564 self.rms_eps,
5565 self.norm_style,
5566 self.pool.as_deref(),
5567 ),
5568 },
5569 (false, _) => match &lw.ffn {
5570 FfnKind::DenseMoe(dm) => dense_moe_ffn(
5571 dm,
5572 post_normed,
5573 &h,
5574 self.rms_eps,
5575 self.norm_style,
5576 self.pool.as_deref(),
5577 ),
5578 _ => {
5579 let allowed = match (&lw.ffn, task_mask) {
5580 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
5581 _ => None,
5582 };
5583 ffn_forward(
5584 &lw.ffn,
5585 post_normed,
5586 self.pool.as_deref(),
5587 allowed.as_deref(),
5588 )
5589 }
5590 },
5591 };
5592 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
5593 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
5594 None => ffn_out,
5595 };
5596 for (i, &f) in ffn_out.iter().enumerate() {
5597 h[i] += f;
5598 }
5599 let mut ffn_out = ffn_out;
5600 attention::recycle_buf(&mut ffn_out);
5601
5602 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
5604 for v in h.iter_mut() {
5605 *v *= sc;
5606 }
5607 }
5608
5609 if self.is_loop_end(li) && li + 1 < self.num_layers {
5612 h = inference::rms_norm(
5613 &h,
5614 &self.weights.final_norm,
5615 self.rms_eps,
5616 self.norm_style,
5617 );
5618 }
5619
5620 if self.dyn_phi_layer == Some(li) {
5624 self.update_dyn_phi(&h);
5625 }
5626 }
5627 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
5629 crate::gpu::graph_race_record(false, t.elapsed());
5630 }
5631
5632 h
5633 }
5634
5635 fn update_dyn_phi(&mut self, h: &[f32]) {
5638 const A: f32 = 0.2;
5639 if self.dyn_phi_ema.len() != h.len() {
5640 self.dyn_phi_ema = vec![0.0; h.len()];
5641 self.dyn_phi_seen = 0;
5642 }
5643 if self.dyn_phi_seen == 0 {
5644 self.dyn_phi_ema.copy_from_slice(h);
5645 } else {
5646 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
5647 *e = (1.0 - A) * *e + A * v;
5648 }
5649 }
5650 self.dyn_phi_seen += 1;
5651 }
5652
5653 pub fn dyn_phi(&self) -> &[f32] {
5655 &self.dyn_phi_ema
5656 }
5657
5658 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
5660 self.dyn_phi_layer = layer;
5661 self.dyn_phi_ema.clear();
5662 self.dyn_phi_seen = 0;
5663 }
5664
5665 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
5667 let Some(model) = &self.model else {
5668 return Vec::new();
5669 };
5670 model
5671 .header
5672 .skills
5673 .iter()
5674 .enumerate()
5675 .filter_map(|(i, sk)| {
5676 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
5677 let sel = sk.selection.as_ref()?;
5678 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
5679 })
5680 .collect()
5681 }
5682
5683 pub fn active_skill(&self) -> Option<usize> {
5685 self.dyn_active
5686 }
5687
5688 pub fn enable_dynamic_routing(&mut self) -> usize {
5693 use crate::swarm::{DynRouter, RoutableSkill};
5694 let Some(model) = self.model.clone() else {
5695 return 0;
5696 };
5697 if self.dyn_blend_loaded {
5700 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
5701 return 0;
5702 }
5703 if let Some(a) = self.dyn_active {
5707 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
5708 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
5709 return 0;
5710 }
5711 }
5712 let hidden = self.hidden_size;
5713 let mut skills = Vec::new();
5714 for (idx, id, _phi) in self.dynamic_skills() {
5715 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
5716 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
5717 skills.push(rs);
5718 }
5719 }
5720 }
5721 if skills.is_empty() {
5722 return 0;
5723 }
5724 let phi = skills[0].phi_layer;
5726 if skills.iter().any(|s| s.phi_layer != phi) {
5727 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
5728 }
5729 let n = skills.len();
5730 self.set_dyn_phi_layer(Some(phi));
5731 self.dyn_router = Some(DynRouter::new(skills));
5732 n
5733 }
5734
5735 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
5737 self.dyn_router
5738 .as_ref()
5739 .map(|r| r.switches.clone())
5740 .unwrap_or_default()
5741 }
5742
5743 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
5746 let rows = self.weights.lm_head.rows();
5747 let mut logits = attention::take_buf(rows.min(self.vocab_size));
5748 self.weights
5749 .lm_head
5750 .matvec(hidden, &mut logits, self.pool.as_deref());
5751 logits.resize(self.vocab_size, 0.0);
5752 if let Some(m) = self.logit_multiplier {
5753 for l in logits.iter_mut() {
5754 *l *= m;
5755 }
5756 }
5757 if let Some(c) = self.final_softcap {
5758 for l in logits.iter_mut() {
5759 *l = c * (*l / c).tanh();
5760 }
5761 }
5762 logits
5763 }
5764
5765 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
5770 self.kv_cache.clear();
5771 self.kv_history.clear();
5772 let mut hidden = vec![0.0f32; self.hidden_size];
5773 for (pos, &id) in ids.iter().enumerate() {
5774 let emb = self.embed_single(id);
5775 hidden = self.forward_layers(&emb, pos, task_mask);
5776 }
5777 inference::rms_norm_into(
5778 &hidden,
5779 &self.weights.final_norm,
5780 self.rms_eps,
5781 self.norm_style,
5782 &mut self.ws.n1,
5783 );
5784 self.lm_head_forward(&self.ws.n1)
5785 }
5786}
5787
5788pub fn create_test_pipeline(
5790 hidden_size: usize,
5791 intermediate_size: usize,
5792 num_heads: usize,
5793 num_kv_heads: usize,
5794 head_dim: usize,
5795 num_layers: usize,
5796 vocab_size: usize,
5797) -> Pipeline {
5798 let synth = |n: usize, salt: usize| -> Vec<f32> {
5801 (0..n)
5802 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
5803 .collect()
5804 };
5805 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
5806 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
5807 };
5808 let layer_weights: Vec<LayerWeights> = (0..num_layers)
5809 .map(|li| LayerWeights {
5810 input_norm: vec![1.0; hidden_size],
5811 post_norm: vec![1.0; hidden_size],
5812 attn_out_norm: None,
5813 ffn_out_norm: None,
5814 layer_scale: None,
5815 ffn: FfnKind::Dense(DenseFfn {
5816 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
5817 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
5818 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
5819 act: Act::Silu,
5820 }),
5821 attn: AttnKind::Full {
5822 bias: None,
5823 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
5824 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
5825 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
5826 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
5827 q_norm: None,
5828 k_norm: None,
5829 output_gate: false,
5830 softplus_gate: None,
5831 },
5832 })
5833 .collect();
5834
5835 Pipeline::new(
5836 Tokenizer::byte_level(),
5837 PipelineWeights {
5838 embed_tokens: qt(vocab_size, hidden_size, 100),
5839 layers: layer_weights,
5840 lm_head: qt(vocab_size, hidden_size, 200),
5841 final_norm: vec![1.0; hidden_size],
5842 },
5843 hidden_size,
5844 intermediate_size,
5845 num_heads,
5846 num_kv_heads,
5847 head_dim,
5848 num_layers,
5849 num_layers, false, vocab_size,
5852 1e-6,
5853 10_000.0,
5854 NormStyle::Qwen,
5855 4096,
5856 SamplerConfig {
5857 seed: Some(42),
5858 ..Default::default()
5859 },
5860 )
5861}
5862
5863fn dense_ffn_batch(d: &DenseFfn, xs: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
5866 let inter = d.gate_proj.rows();
5867 let hidden = d.down_proj.rows();
5868 if d.act == Act::Silu && b >= 32 && crate::gpu::enabled_here() && !crate::gpu::mm_killed() {
5874 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
5875 d.gate_proj.mapped_q4t(),
5876 d.up_proj.mapped_q4t(),
5877 d.down_proj.mapped_q4t(),
5878 ) {
5879 let mut out = vec![0.0f32; b * hidden];
5880 if crate::gpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, &mut out) {
5881 return out;
5882 }
5883 }
5884 }
5885 let mut g = vec![0.0f32; b * inter];
5886 d.gate_proj.matmat(xs, b, &mut g, pool);
5887 let mut u = vec![0.0f32; b * inter];
5888 d.up_proj.matmat(xs, b, &mut u, pool);
5889 for i in 0..b * inter {
5890 g[i] = d.act.combine(g[i], u[i]);
5891 }
5892 let mut out = vec![0.0f32; b * hidden];
5893 d.down_proj.matmat(&g, b, &mut out, pool);
5894 out
5895}
5896
5897fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
5902 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5903 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5904 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
5905 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
5906 if (!on && !dump) || b == 0 {
5907 return;
5908 }
5909 let hidden = xs.len() / b;
5910 if on {
5911 let mut acc = m.act_sq.borrow_mut();
5912 if acc.len() < hidden {
5913 acc.resize(hidden, 0.0);
5914 }
5915 for t in 0..b {
5916 let row = &xs[t * hidden..(t + 1) * hidden];
5917 for (a, &v) in acc.iter_mut().zip(row) {
5918 *a += (v as f64) * (v as f64);
5919 }
5920 }
5921 }
5922 if dump {
5923 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
5926 .ok()
5927 .and_then(|v| v.parse().ok())
5928 .unwrap_or(4096);
5929 let mut rows = m.act_rows.borrow_mut();
5930 if rows.len() < cap * hidden {
5931 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
5932 rows.extend_from_slice(&xs[..take * hidden]);
5933 }
5934 }
5935}
5936
5937fn moe_ffn_batch(
5938 m: &MoeFfn,
5939 xs: &[f32],
5940 b: usize,
5941 hidden: usize,
5942 pool: Option<&Pool>,
5943 allowed: Option<&[bool]>,
5944) -> Vec<f32> {
5945 accumulate_act(m, xs, b);
5946 let ne = m.experts.len();
5947 let mut logits = vec![0.0f32; b * ne];
5948 m.router.matmat(xs, b, &mut logits, pool);
5949
5950 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
5953 {
5954 let mut st = m.stats.borrow_mut();
5955 if st.len() < ne {
5956 st.resize(ne, 0);
5957 }
5958 for bi in 0..b {
5959 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
5960 for &e in &idx {
5961 st[e] += 1;
5962 assign[e].push((bi, p[e] / wsum));
5963 }
5964 }
5965 }
5966
5967 let mut out = vec![0.0f32; b * hidden];
5968 let cols = m.experts[0].gate_proj.cols();
5969 let mut run_expert = |d: &DenseFfn, list: &[(usize, f32)]| {
5970 let sb = list.len();
5971 let mut sub = vec![0.0f32; sb * cols];
5972 for (k, &(bi, _)) in list.iter().enumerate() {
5973 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
5974 }
5975 let eo = dense_ffn_batch(d, &sub, sb, pool);
5976 for (k, &(bi, w)) in list.iter().enumerate() {
5977 for i in 0..hidden {
5978 out[bi * hidden + i] += w * eo[k * hidden + i];
5979 }
5980 }
5981 };
5982 for (e, a) in assign.iter().enumerate().take(ne) {
5983 if !a.is_empty() {
5984 run_expert(&m.experts[e], a);
5985 }
5986 }
5987 if let Some((se, gate)) = &m.shared {
5988 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
5989 let mut gl = vec![0.0f32; b];
5990 gate.matmat(xs, b, &mut gl, pool);
5991 (0..b)
5992 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
5993 .collect()
5994 } else {
5995 (0..b).map(|bi| (bi, 1.0)).collect()
5996 };
5997 run_expert(se, &all);
5998 }
5999 out
6000}
6001
6002thread_local! {
6003 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
6007 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
6008}
6009
6010fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
6012 if crate::gpu::enabled_here()
6023 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
6024 {
6025 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
6026 crate::gpu::ProbeArm::Gpu
6027 } else {
6028 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
6029 };
6030 match arm {
6031 crate::gpu::ProbeArm::Gpu => {
6032 let t0 = std::time::Instant::now();
6033 if let Some(out) = dense_ffn_gpu(d, x, pool) {
6034 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
6035 return out;
6036 }
6037 }
6038 crate::gpu::ProbeArm::CpuTimed => {
6039 let t0 = std::time::Instant::now();
6040 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
6041 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
6042 return out;
6043 }
6044 crate::gpu::ProbeArm::Cpu => {
6045 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
6046 }
6047 }
6048 }
6049 dense_ffn_cpu(d, x, pool)
6050}
6051
6052fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
6054 let inter = d.gate_proj.rows();
6055 FFN_SCRATCH.with(|s| {
6056 let mut s = s.borrow_mut();
6057 let [g, u, ..] = &mut *s;
6058 g.resize(inter, 0.0);
6059 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
6062 } else {
6064 u.resize(inter, 0.0);
6065 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
6067 for i in 0..inter {
6068 g[i] = d.act.combine(g[i], u[i]);
6069 }
6070 }
6071 FFN_PROBE.with(|pr| {
6074 if let Some(acc) = pr.borrow_mut().as_mut() {
6075 let li = crate::gpu::cur_layer();
6076 if li >= 0 {
6077 if let Some(row) = acc.get_mut(li as usize) {
6078 for (a, &v) in row.iter_mut().zip(g.iter()) {
6079 *a += (v as f64).abs();
6080 }
6081 }
6082 }
6083 }
6084 });
6085 let mut out = attention::take_buf(d.down_proj.rows());
6086 d.down_proj.matvec(g, &mut out, pool);
6087 out
6088 })
6089}
6090
6091thread_local! {
6092 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
6095 const { std::cell::RefCell::new(None) };
6096}
6097
6098fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
6104 if d.act != Act::Silu {
6106 return None;
6107 }
6108 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
6111 return None;
6112 }
6113 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
6114 let mut model_ref = None;
6115 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
6116 let model = model_ref?;
6117 let hidden = jobs[0].down.1;
6118 let mut out = attention::take_buf(hidden);
6119 if crate::gpu::moe_block(&model, &jobs, &mut out) {
6120 Some(out)
6121 } else {
6122 let mut out = out;
6123 attention::recycle_buf(&mut out);
6124 None
6125 }
6126}
6127
6128#[allow(clippy::type_complexity)]
6133#[allow(clippy::type_complexity)]
6134pub(crate) fn moe_parts(
6135 t: &QTensor,
6136) -> Option<(
6137 &std::sync::Arc<cortiq_core::CmfModel>,
6138 usize,
6139 usize,
6140 usize,
6141 &[f32],
6142 &[f32],
6143 bool,
6144 bool,
6145)> {
6146 match t {
6147 QTensor::Mapped {
6148 model,
6149 idx,
6150 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
6151 rows,
6152 cols,
6153 row_scale,
6154 col_field,
6155 ..
6156 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
6157 model, *idx, *rows, *cols, row_scale, col_field, false, false,
6158 )),
6159 QTensor::Mapped {
6161 model,
6162 idx,
6163 dtype: cortiq_core::TensorDtype::Q1,
6164 rows,
6165 cols,
6166 ..
6167 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], true, false)),
6168 QTensor::Mapped {
6170 model,
6171 idx,
6172 dtype: cortiq_core::TensorDtype::Q4Tiled,
6173 rows,
6174 cols,
6175 ..
6176 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
6177 QTensor::Mapped {
6179 model,
6180 idx,
6181 dtype: cortiq_core::TensorDtype::Q4TiledP,
6182 rows,
6183 cols,
6184 ..
6185 } => Some((model, *idx, *rows, *cols, &[][..], &[][..], false, true)),
6186 _ => None,
6187 }
6188}
6189
6190pub(crate) fn moe_push_job_parts<'a>(
6194 gate: &'a QTensor,
6195 up: &'a QTensor,
6196 down: &'a QTensor,
6197 x: &[f32],
6198 w: f32,
6199 swiglu_limit: f32,
6200 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
6201 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
6202) -> Option<()> {
6203 use crate::qtensor::prescale;
6204 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(gate)?;
6205 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(up)?;
6206 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(down)?;
6207 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
6208 return None; }
6210 model_ref.get_or_insert_with(|| gm.clone());
6211 let dt = |cf: &[f32]| {
6212 if cf.is_empty() {
6213 cortiq_core::TensorDtype::Q8Row
6214 } else {
6215 cortiq_core::TensorDtype::Q8_2f
6216 }
6217 };
6218 jobs.push(crate::gpu::MoeJob {
6219 gate: (gi, gr, gc, grs),
6220 up: (ui, ur, uc, urs),
6221 down: (di, dr, dc, drs),
6222 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
6223 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
6224 down_col: dcf,
6225 w,
6226 q1: gq1,
6227 q4t: gq4 && gate.mapped_q4tp().is_none(),
6228 q4tp: gq4 && gate.mapped_q4tp().is_some(),
6229 swiglu_limit,
6230 });
6231 Some(())
6232}
6233
6234fn moe_push_job<'a>(
6236 d: &'a DenseFfn,
6237 x: &[f32],
6238 w: f32,
6239 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
6240 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
6241) -> Option<()> {
6242 use crate::qtensor::prescale;
6243 if d.act != Act::Silu {
6244 return None; }
6246 let (gm, gi, gr, gc, grs, gcf, gq1, gq4) = moe_parts(&d.gate_proj)?;
6247 let (_, ui, ur, uc, urs, ucf, uq1, uq4) = moe_parts(&d.up_proj)?;
6248 let (_, di, dr, dc, drs, dcf, dq1, dq4) = moe_parts(&d.down_proj)?;
6249 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 {
6250 return None; }
6252 model_ref.get_or_insert_with(|| gm.clone());
6253 let gdt = if gcf.is_empty() {
6254 cortiq_core::TensorDtype::Q8Row
6255 } else {
6256 cortiq_core::TensorDtype::Q8_2f
6257 };
6258 let udt = if ucf.is_empty() {
6259 cortiq_core::TensorDtype::Q8Row
6260 } else {
6261 cortiq_core::TensorDtype::Q8_2f
6262 };
6263 jobs.push(crate::gpu::MoeJob {
6264 gate: (gi, gr, gc, grs),
6265 up: (ui, ur, uc, urs),
6266 down: (di, dr, dc, drs),
6267 xs_gate: prescale(x, gcf, gdt).into_owned(),
6268 xs_up: prescale(x, ucf, udt).into_owned(),
6269 down_col: dcf,
6270 w,
6271 q1: gq1,
6272 q4t: gq4 && d.gate_proj.mapped_q4tp().is_none(),
6273 q4tp: gq4 && d.gate_proj.mapped_q4tp().is_some(),
6274 swiglu_limit: 0.0,
6275 });
6276 Some(())
6277}
6278
6279fn sparse_ffn_quant(
6286 d: &DenseFfn,
6287 x: &[f32],
6288 active: &[u16],
6289 hidden: usize,
6290 pool: Option<&Pool>,
6291) -> Vec<f32> {
6292 let n = active.len();
6293 let inter = d.gate_proj.rows();
6294 let mut act = vec![0.0f32; n];
6295 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
6298 let compute = |ai: usize| -> f32 {
6299 let idx = active[ai] as usize;
6300 if idx >= inter {
6301 return 0.0; }
6303 let mut s = if need_scratch {
6304 vec![0.0f32; hidden]
6305 } else {
6306 Vec::new()
6307 };
6308 let gate = d.gate_proj.row_dot(idx, x, &mut s);
6309 let up = d.up_proj.row_dot(idx, x, &mut s);
6310 d.act.combine(gate, up)
6311 };
6312 match pool {
6313 Some(p) if n >= 256 => {
6314 let ptr = SendMut(act.as_mut_ptr());
6315 p.run(&|widx, nw| {
6316 let chunk = n.div_ceil(nw);
6317 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
6318 for ai in s..e {
6319 unsafe { *ptr.at(ai) = compute(ai) };
6320 }
6321 });
6322 }
6323 _ => {
6324 for (ai, a) in act.iter_mut().enumerate() {
6325 *a = compute(ai);
6326 }
6327 }
6328 }
6329 let mut out = vec![0.0f32; hidden];
6331 for (ai, &idx) in active.iter().enumerate() {
6332 let w = act[ai];
6333 if w.abs() >= 1e-12 && (idx as usize) < inter {
6334 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
6335 }
6336 }
6337 out
6338}
6339
6340#[doc(hidden)]
6342pub fn sparse_ffn_quant_for_test(
6343 d: &DenseFfn,
6344 x: &[f32],
6345 active: &[u16],
6346 hidden: usize,
6347) -> Vec<f32> {
6348 sparse_ffn_quant(d, x, active, hidden, None)
6349}
6350
6351fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
6355 let deq = |t: &QTensor| -> Vec<f32> {
6356 let (rows, cols) = (t.rows(), t.cols());
6357 let mut out = vec![0.0f32; rows * cols];
6358 for r in 0..rows {
6359 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
6360 }
6361 out
6362 };
6363 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
6364}
6365
6366struct SendMut(*mut f32);
6368unsafe impl Send for SendMut {}
6369unsafe impl Sync for SendMut {}
6370impl SendMut {
6371 #[inline]
6372 #[allow(clippy::mut_from_ref)]
6375 unsafe fn at(&self, i: usize) -> &mut f32 {
6376 unsafe { &mut *self.0.add(i) }
6377 }
6378}
6379
6380fn moe_route(logits: &[f32], m: &MoeFfn, allowed: Option<&[bool]>) -> (Vec<usize>, Vec<f32>, f32) {
6390 let ne = logits.len();
6391 let p: Vec<f32> = if m.router_sigmoid {
6392 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
6393 } else {
6394 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
6395 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
6396 let s: f32 = e.iter().sum();
6397 for v in &mut e {
6398 *v /= s;
6399 }
6400 e
6401 };
6402 let admit = |e: usize| {
6408 m.mask.as_ref().is_none_or(|mk| mk[e])
6409 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
6410 };
6411 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
6412 match &m.expert_bias {
6414 Some(b) => idx.sort_unstable_by(|&x, &y| {
6415 (p[y] + b[y])
6416 .partial_cmp(&(p[x] + b[x]))
6417 .unwrap()
6418 .then(x.cmp(&y))
6419 }),
6420 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
6421 }
6422 idx.truncate(m.top_k);
6423 if let Some(tau) = m.route_tau {
6427 let total: f32 = idx.iter().map(|&e| p[e]).sum();
6428 if total > 0.0 {
6429 let mut acc = 0.0f32;
6430 let mut keep = idx.len();
6431 for (i, &e) in idx.iter().enumerate() {
6432 acc += p[e];
6433 if acc >= tau * total {
6434 keep = i + 1;
6435 break;
6436 }
6437 }
6438 idx.truncate(keep);
6439 }
6440 }
6441 let wsum: f32 = if m.norm_topk_prob {
6442 let s: f32 = idx.iter().map(|&e| p[e]).sum();
6443 (if m.router_sigmoid { s + 1e-6 } else { s }) / m.routed_scaling
6446 } else {
6447 1.0 / m.routed_scaling
6448 };
6449 (idx, p, wsum)
6450}
6451
6452fn moe_ffn(m: &MoeFfn, x: &[f32], pool: Option<&Pool>, allowed: Option<&[bool]>) -> Vec<f32> {
6455 accumulate_act(m, x, 1);
6456 let ne = m.experts.len();
6457 let mut logits = vec![0.0f32; ne];
6458 m.router.matvec(x, &mut logits, pool);
6459 let (idx, p, wsum) = moe_route(&logits, m, allowed);
6460 {
6461 let mut st = m.stats.borrow_mut();
6462 if st.len() < ne {
6463 st.resize(ne, 0);
6464 }
6465 for &e in &idx {
6466 st[e] += 1;
6467 }
6468 }
6469 if crate::gpu::enabled_here() {
6474 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
6475 crate::gpu::ProbeArm::Gpu => {
6476 let t0 = std::time::Instant::now();
6477 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
6478 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
6479 return out;
6480 }
6481 }
6482 crate::gpu::ProbeArm::CpuTimed => {
6483 let t0 = std::time::Instant::now();
6484 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
6485 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
6486 return out;
6487 }
6488 crate::gpu::ProbeArm::Cpu => {
6489 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
6490 }
6491 }
6492 }
6493 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
6494}
6495
6496fn graph_note(built: bool) {
6500 use std::sync::atomic::{AtomicBool, Ordering};
6501 static SAID: AtomicBool = AtomicBool::new(false);
6502 if !SAID.swap(true, Ordering::Relaxed) {
6503 if built {
6504 tracing::info!("wgpu whole-token graph: ACTIVE");
6505 } else {
6506 tracing::warn!("wgpu whole-token graph refused — per-op path");
6507 }
6508 }
6509}
6510
6511fn moe_batch_enabled() -> bool {
6514 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6515 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
6516}
6517
6518fn moe_ffn_cpu_batched(
6524 m: &MoeFfn,
6525 x: &[f32],
6526 idx: &[usize],
6527 p: &[f32],
6528 wsum: f32,
6529 pool: Option<&Pool>,
6530) -> Option<Vec<f32>> {
6531 if idx.is_empty() || !moe_batch_enabled() {
6532 return None;
6533 }
6534 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
6538 return None;
6539 }
6540 let n = idx.len() + usize::from(m.shared.is_some());
6541 let mut pairs = Vec::with_capacity(n);
6542 let mut downs = Vec::with_capacity(n);
6543 let mut ws = Vec::with_capacity(n);
6544 for &e in idx {
6545 let d = &m.experts[e];
6546 if d.act != Act::Silu {
6547 return None;
6548 }
6549 pairs.push((&d.gate_proj, &d.up_proj));
6550 downs.push(&d.down_proj);
6551 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
6552 }
6553 if let Some((se, gate)) = &m.shared {
6556 if se.act != Act::Silu {
6557 return None;
6558 }
6559 let g = gate.as_ref().map_or(1.0, |gate| {
6560 let mut gl = [0.0f32; 1];
6561 gate.matvec(x, &mut gl, pool);
6562 1.0 / (1.0 + (-gl[0]).exp())
6563 });
6564 pairs.push((&se.gate_proj, &se.up_proj));
6565 downs.push(&se.down_proj);
6566 ws.push(g);
6567 }
6568 let inter = pairs[0].0.rows();
6569 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
6570 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
6571 return None;
6572 }
6573 let mut out = attention::take_buf(x.len());
6574 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
6575 attention::recycle_buf(&mut out);
6576 return None;
6577 }
6578 Some(out)
6579}
6580
6581fn moe_ffn_cpu(
6583 m: &MoeFfn,
6584 x: &[f32],
6585 idx: &[usize],
6586 p: &[f32],
6587 wsum: f32,
6588 pool: Option<&Pool>,
6589) -> Vec<f32> {
6590 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
6591 return out;
6592 }
6593 let mut out = attention::take_buf(x.len());
6594 for &e in idx {
6595 let mut eo = dense_ffn(&m.experts[e], x, pool);
6596 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
6597 for i in 0..out.len() {
6598 out[i] += w * eo[i];
6599 }
6600 attention::recycle_buf(&mut eo);
6601 }
6602 if let Some((se, gate)) = &m.shared {
6603 let mut so = dense_ffn(se, x, pool);
6604 let g = gate.as_ref().map_or(1.0, |gate| {
6605 let mut gl = [0.0f32; 1];
6606 gate.matvec(x, &mut gl, pool);
6607 1.0 / (1.0 + (-gl[0]).exp())
6608 });
6609 for i in 0..out.len() {
6610 out[i] += g * so[i];
6611 }
6612 attention::recycle_buf(&mut so);
6613 }
6614 out
6615}
6616
6617#[allow(clippy::too_many_arguments)]
6625fn mla_attention(
6626 w: &MlaWeights,
6627 normed: &[f32],
6628 cache: &mut crate::kv_cache::LayerKvCache,
6629 position: usize,
6630 inv_freq: &[f32],
6631 rope_scale: f32,
6632 eps: f64,
6633 pool: Option<&Pool>,
6634) -> Vec<f32> {
6635 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
6636 let hd = dr + dn;
6637 let mut q = vec![0.0f32; nh * hd];
6638 match (&w.q_a, &w.q_a_norm) {
6639 (Some(qa), Some(qn)) => {
6640 let mut t = vec![0.0f32; qa.rows()];
6641 qa.matvec(normed, &mut t, pool);
6642 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
6643 w.q_proj.matvec(&tn, &mut q, pool);
6644 }
6645 _ => w.q_proj.matvec(normed, &mut q, pool),
6646 }
6647 let mut ca = vec![0.0f32; lora + dr];
6648 w.kv_a.matvec(normed, &mut ca, pool);
6649 let (c_lat, k_rope) = ca.split_at_mut(lora);
6650 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
6651 let mut kvb = vec![0.0f32; nh * (dn + dv)];
6652 w.kv_b.matvec(&latn, &mut kvb, pool);
6653 if !w.nope {
6654 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
6655 }
6656 for h in 0..nh {
6657 if !w.nope {
6658 attention::rope_rotate_scaled(
6659 &mut q[h * hd..h * hd + dr],
6660 position,
6661 inv_freq,
6662 rope_scale,
6663 );
6664 }
6665 }
6666 let mut k = vec![0.0f32; nh * hd];
6667 let mut v = vec![0.0f32; nh * hd];
6668 for h in 0..nh {
6669 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
6670 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
6671 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
6672 }
6673 cache.append(&k, &v, &vec![true; nh]);
6674 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
6675 attention::recycle_buf(&mut imp);
6676 let mut ov = vec![0.0f32; nh * dv];
6677 for h in 0..nh {
6678 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
6679 }
6680 let mut out = vec![0.0f32; w.o_proj.rows()];
6681 w.o_proj.matvec(&ov, &mut out, pool);
6682 out
6683}
6684
6685fn dense_moe_ffn(
6692 dm: &DenseMoeFfn,
6693 x_normed: &[f32],
6694 h_raw: &[f32],
6695 eps: f64,
6696 norm_style: NormStyle,
6697 pool: Option<&Pool>,
6698) -> Vec<f32> {
6699 let mut d = dense_ffn(&dm.dense, x_normed, pool);
6700 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
6701 let m = &dm.moe;
6702 let ne = m.experts.len();
6703 let mut logits = vec![0.0f32; ne];
6704 if m.router_input_norm {
6705 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
6706 let inv = 1.0 / (ss + eps as f32).sqrt();
6707 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
6708 m.router.matvec(&xr, &mut logits, pool);
6709 } else {
6710 m.router.matvec(h_raw, &mut logits, pool);
6711 }
6712 let (idx, p, wsum) = moe_route(&logits, m, None);
6713 {
6714 let mut st = m.stats.borrow_mut();
6715 if st.len() < ne {
6716 st.resize(ne, 0);
6717 }
6718 for &e in &idx {
6719 st[e] += 1;
6720 }
6721 }
6722 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
6723 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
6724 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
6725 for (di, mi) in d.iter_mut().zip(&mo) {
6726 *di += mi;
6727 }
6728 d
6729}
6730
6731fn moe_gpu_refused(why: &'static str) {
6738 use std::sync::atomic::{AtomicBool, Ordering};
6739 static SAID: AtomicBool = AtomicBool::new(false);
6740 if !SAID.swap(true, Ordering::Relaxed) {
6741 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
6742 }
6743}
6744
6745fn moe_ffn_gpu(
6746 m: &MoeFfn,
6747 x: &[f32],
6748 idx: &[usize],
6749 p: &[f32],
6750 wsum: f32,
6751 pool: Option<&Pool>,
6752) -> Option<Vec<f32>> {
6753 use crate::gpu::MoeJob;
6754
6755 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
6756 let mut model_ref = None;
6757 for &e in idx {
6758 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
6759 moe_gpu_refused("push_job(expert)");
6760 return None;
6761 }
6762 }
6763 if let Some((se, gate)) = &m.shared {
6764 let g = gate.as_ref().map_or(1.0, |gate| {
6765 let mut gl = [0.0f32; 1];
6766 gate.matvec(x, &mut gl, pool);
6767 1.0 / (1.0 + (-gl[0]).exp())
6768 });
6769 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
6770 moe_gpu_refused("push_job(shared)");
6771 return None;
6772 }
6773 }
6774 let Some(model) = model_ref else {
6775 moe_gpu_refused("no model_ref");
6776 return None;
6777 };
6778 let hidden = jobs[0].down.1;
6779 let mut out = vec![0.0f32; hidden];
6780 if crate::gpu::moe_block(&model, &jobs, &mut out) {
6781 Some(out)
6782 } else {
6783 moe_gpu_refused("gpu::moe_block");
6784 None
6785 }
6786}
6787
6788fn ffn_forward(
6790 ffn: &FfnKind,
6791 x: &[f32],
6792 pool: Option<&Pool>,
6793 experts_allowed: Option<&[bool]>,
6794) -> Vec<f32> {
6795 match ffn {
6796 FfnKind::Dense(d) => dense_ffn(d, x, pool),
6797 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
6798 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
6802 }
6803}
6804
6805fn ffn_forward_pair(
6809 ffn: &FfnKind,
6810 x1: &[f32],
6811 x2: &[f32],
6812 pool: Option<&Pool>,
6813 experts_allowed: Option<&[bool]>,
6814) -> (Vec<f32>, Vec<f32>) {
6815 let d = match ffn {
6816 FfnKind::Dense(d) => d,
6817 FfnKind::Moe(m) => {
6818 return (
6819 moe_ffn(m, x1, pool, experts_allowed),
6820 moe_ffn(m, x2, pool, experts_allowed),
6821 );
6822 }
6823 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
6824 };
6825 let inter = d.gate_proj.rows();
6826 FFN_SCRATCH.with(|s| {
6827 let mut s = s.borrow_mut();
6828 let [g1, g2, u1, u2] = &mut *s;
6829 g1.resize(inter, 0.0);
6830 g2.resize(inter, 0.0);
6831 u1.resize(inter, 0.0);
6832 u2.resize(inter, 0.0);
6833 QTensor::matvec2_many(
6836 [&d.gate_proj, &d.up_proj],
6837 x1,
6838 x2,
6839 [g1.as_mut_slice(), u1.as_mut_slice()],
6840 [g2.as_mut_slice(), u2.as_mut_slice()],
6841 pool,
6842 );
6843 for i in 0..inter {
6844 g1[i] = d.act.combine(g1[i], u1[i]);
6845 g2[i] = d.act.combine(g2[i], u2[i]);
6846 }
6847 let mut o1 = attention::take_buf(d.down_proj.rows());
6848 let mut o2 = attention::take_buf(d.down_proj.rows());
6849 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
6850 (o1, o2)
6851 })
6852}
6853
6854#[cfg(test)]
6855mod tests {
6856
6857 #[test]
6858 fn cancel_flag_stops_generation() {
6859 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
6860 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
6863 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
6864 assert_eq!(r.finish_reason, "cancelled");
6865 assert!(
6866 r.token_ids.is_empty(),
6867 "no tokens after cancel: {:?}",
6868 r.token_ids
6869 );
6870 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
6872 assert_ne!(r2.finish_reason, "cancelled");
6873 }
6874 use super::*;
6875
6876 #[test]
6882 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
6883 let (hidden, inter) = (16usize, 40usize);
6884 let synth = |n: usize, salt: usize| -> Vec<f32> {
6885 (0..n)
6886 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
6887 .collect()
6888 };
6889 let d = DenseFfn {
6890 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
6891 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
6892 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
6893 act: Act::Silu,
6894 };
6895 let x = synth(hidden, 9);
6896 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
6898
6899 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
6900
6901 let mut g = vec![0.0f32; inter];
6903 d.gate_proj.matvec(&x, &mut g, None);
6904 let mut u = vec![0.0f32; inter];
6905 d.up_proj.matvec(&x, &mut u, None);
6906 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
6907 for i in 0..inter {
6908 g[i] = if act_set.contains(&(i as u16)) {
6909 inference::silu(g[i]) * u[i]
6910 } else {
6911 0.0
6912 };
6913 }
6914 let mut reference = vec![0.0f32; hidden];
6915 d.down_proj.matvec(&g, &mut reference, None);
6916
6917 let max_d = sparse
6918 .iter()
6919 .zip(&reference)
6920 .map(|(a, b)| (a - b).abs())
6921 .fold(0.0f32, f32::max);
6922 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
6923 }
6924
6925 fn attach_test_mtp(p: &mut Pipeline) {
6927 let (h, inter, heads, kv, hd) = (
6928 p.hidden_size,
6929 p.intermediate_size,
6930 p.num_heads,
6931 p.num_kv_heads,
6932 p.head_dim,
6933 );
6934 let synth = |n: usize, salt: usize| -> Vec<f32> {
6935 (0..n)
6936 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
6937 .collect()
6938 };
6939 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
6940 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
6941 };
6942 p.mtp = Some(MtpModule {
6943 enorm: vec![1.0; h],
6944 hnorm: vec![1.0; h],
6945 eh_proj: qt(h, 2 * h, 301),
6946 layer: LayerWeights {
6947 input_norm: vec![1.0; h],
6948 post_norm: vec![1.0; h],
6949 attn_out_norm: None,
6950 ffn_out_norm: None,
6951 layer_scale: None,
6952 ffn: FfnKind::Dense(DenseFfn {
6953 gate_proj: qt(inter, h, 315),
6954 up_proj: qt(inter, h, 316),
6955 down_proj: qt(h, inter, 317),
6956 act: Act::Silu,
6957 }),
6958 attn: AttnKind::Full {
6959 bias: None,
6960 wq: qt(heads * hd, h, 311),
6961 wk: qt(kv * hd, h, 312),
6962 wv: qt(kv * hd, h, 313),
6963 wo: qt(h, heads * hd, 314),
6964 q_norm: None,
6965 k_norm: None,
6966 output_gate: false,
6967 softplus_gate: None,
6968 },
6969 },
6970 final_norm: vec![1.0; h],
6971 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
6972 });
6973 }
6974
6975 #[test]
6976 fn speculative_equals_vanilla_greedy() {
6977 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
6981 let run = |spec: bool| {
6982 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
6983 p.sampler_config.temperature = 0.0;
6984 attach_test_mtp(&mut p);
6985 p.speculative = spec;
6986 let r = p.generate("abcdef", 12, None, None).unwrap();
6987 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
6988 };
6989 let (vanilla, d0, _) = run(false);
6990 let (spec, d1, a1) = run(true);
6991 assert_eq!(d0, 0, "vanilla path must not draft");
6992 assert!(d1 > 0, "speculative path must draft");
6993 assert_eq!(
6994 vanilla, spec,
6995 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
6996 );
6997 }
6998
6999 #[test]
7000 fn speculative_accepts_constant_oracle() {
7001 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7003 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7004 p.sampler_config.temperature = 0.0;
7005 p.sampler_config.repetition_penalty = 1.0;
7006 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
7009 attach_test_mtp(&mut p);
7010 p.speculative = true;
7011 let r = p.generate("abcd", 10, None, None).unwrap();
7012 assert!(r.mtp_drafted > 0);
7013 assert_eq!(
7014 r.mtp_accepted, r.mtp_drafted,
7015 "constant logits → every draft accepted"
7016 );
7017 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
7020 }
7021
7022 #[test]
7023 fn empty_prompt_is_an_error_not_a_panic() {
7024 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
7025 let r = p.generate("", 4, None, None);
7026 assert!(r.is_err(), "empty prompt must be a clean error");
7027 }
7028
7029 #[test]
7030 fn every_token_enters_kv_exactly_once() {
7031 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7032 p.sampler_config.temperature = 0.0;
7034 let r = p.generate("abc", 2, None, None).unwrap();
7035 assert_eq!(r.prompt_tokens, 3);
7036 assert_eq!(
7040 p.kv_cache.seq_len(),
7041 3 + r.tokens_generated - 1,
7042 "each token must be cached exactly once (v1 cached the last prompt token twice)"
7043 );
7044 }
7045
7046 #[test]
7047 fn generation_is_reproducible_with_seed() {
7048 let run = || {
7049 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7050 p.generate("hello", 8, None, None).unwrap().token_ids
7051 };
7052 assert_eq!(run(), run());
7053 }
7054
7055 #[test]
7056 fn resetting_sampler_restarts_the_seeded_stream() {
7057 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
7058 let config = SamplerConfig {
7059 seed: Some(1234),
7060 ..SamplerConfig::default()
7061 };
7062 p.set_sampler_config(config.clone());
7063 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
7064 p.set_sampler_config(config);
7065 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
7066 assert_eq!(first, second);
7067 }
7068
7069 #[test]
7070 fn eviction_bounds_the_cache() {
7071 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
7072 p.kv_cache.max_seq_len = 6;
7073 p.sampler_config.temperature = 0.0;
7074 let _ = p.generate("abcd", 12, None, None).unwrap();
7075 assert!(
7076 p.kv_cache.seq_len() <= 6 + 1,
7077 "cache must stay bounded by max_seq_len (got {})",
7078 p.kv_cache.seq_len()
7079 );
7080 }
7081
7082 #[test]
7083 fn confidence_matches_tokens_and_is_a_probability() {
7084 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7085 p.sampler_config.temperature = 0.0;
7086 p.sampler_config.repetition_penalty = 1.0;
7087 let r = p.generate("abcd", 10, None, None).unwrap();
7088 assert_eq!(
7089 r.token_confidence.len(),
7090 r.token_ids.len(),
7091 "one confidence per emitted token"
7092 );
7093 for &c in &r.token_confidence {
7094 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
7095 }
7096 let logits = [1.0f32, 3.0, 0.5, 3.0];
7098 let p0 = top1_prob_t(&logits, 1, 1.0);
7099 let p1 = top1_prob_t(&logits, 3, 1.0);
7100 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
7101 assert!(p0 > 0.0 && p0 < 1.0);
7102 let sharp = top1_prob_t(&logits, 1, 1.0);
7104 let soft = top1_prob_t(&logits, 1, 2.0);
7105 assert!(soft < sharp, "higher temperature lowers peak confidence");
7106 }
7107
7108 #[test]
7109 fn trace_is_opt_in_and_parallels_the_output() {
7110 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7112 p.sampler_config.temperature = 0.0;
7113 p.sampler_config.repetition_penalty = 1.0;
7114 let r = p.generate("abcd", 10, None, None).unwrap();
7115 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
7116
7117 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7119 p.sampler_config.temperature = 0.0;
7120 p.sampler_config.repetition_penalty = 1.0;
7121 p.set_trace(true);
7122 let r = p.generate("abcd", 10, None, None).unwrap();
7123 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
7124 for (i, tr) in r.traces.iter().enumerate() {
7125 assert_eq!(tr.t, i, "trace index is sequential");
7126 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
7127 assert_eq!(
7128 tr.confidence, r.token_confidence[i],
7129 "trace confidence matches the confidence channel"
7130 );
7131 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
7133 }
7134 }
7135
7136 #[test]
7137 fn explain_prefill_logits_match_greedy_first_token() {
7138 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
7142 p.sampler_config.temperature = 0.0;
7143 p.sampler_config.repetition_penalty = 1.0;
7144 let ids = p.tokenizer.encode("abcd");
7145 let logits = p.prefill_next_logits(&ids, None);
7146 let argmax = logits
7147 .iter()
7148 .enumerate()
7149 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
7150 .unwrap()
7151 .0 as u32;
7152 let r = p.generate("abcd", 1, None, None).unwrap();
7153 assert_eq!(
7154 argmax, r.token_ids[0],
7155 "explain preview must match greedy emit"
7156 );
7157 }
7158
7159 #[test]
7160 fn laguna_shared_expert_is_unconditionally_added() {
7161 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
7162 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
7163 let zero_dense = || DenseFfn {
7164 gate_proj: matrix(vec![0.0; 4]),
7165 up_proj: matrix(vec![0.0; 4]),
7166 down_proj: matrix(vec![0.0; 4]),
7167 act: Act::Silu,
7168 };
7169 let shared = DenseFfn {
7170 gate_proj: identity(),
7171 up_proj: identity(),
7172 down_proj: identity(),
7173 act: Act::Silu,
7174 };
7175 let x = [1.0, 2.0];
7176 let expected = dense_ffn(&shared, &x, None);
7177 let moe = MoeFfn {
7178 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
7179 experts: vec![zero_dense()],
7180 top_k: 1,
7181 norm_topk_prob: true,
7182 router_sigmoid: true,
7183 expert_bias: None,
7184 routed_scaling: 1.0,
7185 route_tau: None,
7186 shared: Some((shared, None)),
7187 stats: std::cell::RefCell::new(Vec::new()),
7188 act_sq: std::cell::RefCell::new(Vec::new()),
7189 act_rows: std::cell::RefCell::new(Vec::new()),
7190 mask: None,
7191 per_expert_scale: None,
7192 router_input_norm: false,
7193 };
7194 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
7195 for (actual, expected) in actual.iter().zip(expected) {
7196 assert!((actual - expected).abs() < 1e-6);
7197 }
7198 }
7199}